실제로 다른 정렬 알고리즘보다 퀵 정렬이 더 나은 이유는 무엇입니까?


308

표준 알고리즘 과정에서 quicksort 는 평균 이고 최악의 경우 입니다. 동시에, 최악의 경우에는 ( sortsortheapsort 와 같은 , 심지어 가장 좋은 경우에는 선형 시간 ( bubblesort 와 같은 )이지만 메모리가 추가로 필요한 다른 정렬 알고리즘이 연구 됩니다.O를 ( N 2 ) O ( N 로그 없음을 )O(nlogn)O(n2)O(nlogn)

에 잠깐 한눈 후 좀 더 실행 시간 이 퀵는 말을 당연 등의 효율적합니다.

또한 학생들은 기본 프로그래밍 과정에서 재귀가 너무 많은 메모리를 사용할 수 있기 때문에 일반적으로 좋지 않다는 것을 배우는 것을 고려하십시오. 따라서 (그리고 이것이 실제 논쟁이 아니더라도), 이것은 퀵 정렬이 그렇지 않을 수도 있다는 생각을줍니다. 재귀 알고리즘이기 때문에 정말 좋습니다.

그렇다면 퀵 정렬이 실제로 다른 정렬 알고리즘보다 우수한 이유는 무엇입니까? 실제 데이터 의 구조와 관련이 있습니까? 컴퓨터에서 메모리가 작동하는 방식과 관련이 있습니까? 나는 어떤 기억들이 다른 기억들보다 더 빠르다는 것을 알고 있지만, 이것이 이론상 추정치와 비교할 때이 반 직관적 인 성능의 실제 이유인지는 모른다.


업데이트 1 : 정식 답변은 평균 사례 의 에 관련된 상수가 다른 알고리즘에 관련된 상수보다 작다 는 것을 말합니다 . 그러나 나는 아직 직관적 인 아이디어 대신 정확한 계산으로 이것에 대한 적절한 정당화를 보지 못했습니다.O를 ( N 로그 없음을 )O(nlogn)O(nlogn)

어쨌든 일부 대답에서 알 수 있듯이 메모리 수준에서 구현이 컴퓨터의 내부 구조를 활용하는 경우와 같이 실제 차이가 발생하는 것처럼 보입니다. 예를 들어 캐시 메모리가 RAM보다 빠릅니다. 토론은 이미 흥미롭지 만, 여전히 메모리 관리와 관련하여 더 자세한 내용을보고 싶습니다. 왜냐하면 답변이 그것과 관련이 있는 것처럼 보이기 때문입니다.


업데이트 2 : 정렬 알고리즘 비교를 제공하는 여러 웹 페이지가 있으며, 일부는 다른 알고리즘보다 우수합니다 (주로 sorting-algorithms.com ). 훌륭한 시각 자료를 제시하는 것 외에,이 접근법은 내 질문에 대답하지 않습니다.


2
병합 정렬은 최악의 경우 이며, 정수 크기에 알려진 경계가있는 정수 배열을 정렬하면 계수 정렬을 사용하여 시간 내에 수행 할 수 있습니다 . O ( n )O(nlogn)O(n)
Carl Mummert

13
sorting-algorithms.com 은 정렬 알고리즘을 상당히 철저히 비교합니다.
Joe

2
광고 업데이트 1 : 나는 당신이 엄격한 분석 이나 현실적인 가정을 가질 수 있다고 추측합니다 . 나는 둘 다 보지 못했다. 예를 들어, 대부분의 공식 분석은 비교 만 계산합니다.
Raphael

9
이 질문은 프로그래머들에 대한 최근 콘테스트에서 우승했습니다 .
Raphael

3
흥미로운 질문입니다. 얼마 전에 무작위 데이터와 빠른 정렬 및 병합 정렬의 순진한 구현으로 몇 가지 테스트를 실행했습니다. 두 알고리즘 모두 작은 데이터 세트 (100000 개 항목까지)에서 잘 수행되었지만 병합 정렬 후에는 훨씬 더 나은 것으로 나타났습니다. 이것은 빠른 정렬이 너무 좋고 여전히 그것에 대한 설명을 찾지 못했다는 일반적인 가정과 모순되는 것 같습니다. 내가 생각해 낼 수있는 유일한 아이디어는 일반적으로 빠른 정렬이라는 용어가 인트로 정렬과 같은보다 복잡한 알고리즘에 사용되며 임의의 피벗으로 빠른 정렬을 순진하게 구현하는 것이 좋지 않다는 것입니다.
조르지오

답변:


215

짧은 답변

캐시 효율 인수는 이미 자세히 설명되어 있습니다. 또한 Quicksort가 빠른 이유는 본질적인 주장이 있습니다. 예를 들어 here 와 같이 두 개의 "교차 포인터"로 구현 된 경우 내부 루프의 바디는 매우 작습니다. 이것이 가장 자주 실행되는 코드이기 때문에 이것이 보상입니다.

긴 답변

가장 먼저,

평균 케이스가 존재하지 않습니다!

최상의 경우와 최악의 경우는 실제로 거의 발생하지 않는 경우가 많으므로 평균 사례 분석이 수행됩니다. 그러나 모든 평균 사례 분석 은 입력의 일부 분포를 가정합니다 ! 정렬의 경우 일반적인 선택은 무작위 순열 모델입니다 (위키 백과에서 암묵적으로 가정).

왜 표기법인가?O

알고리즘 분석에서 상수를 폐기하는 것은 한 가지 주된 이유 때문에 수행됩니다. 정확한 실행 시간에 관심이있는 경우 모든 기본 작업의 비용 (상대적) 비용이 필요합니다 ( 아직 캐싱 문제를 무시하고 최신 프로세서의 파이프 라이닝을 무시 함). 수학적 분석은 각 명령어가 얼마나 자주 실행되는지 계산할 수 있지만 단일 명령어의 실행 시간은 프로세서 세부 정보 (예 : 32 비트 정수 곱하기에 더하는 데 시간이 더 걸리는지 여부)에 따라 다릅니다.

두 가지 방법이 있습니다.

  1. 일부 기계 모델을 수정하십시오.

    이것은 저자가 발명 한 인공적인 "일반적인"컴퓨터를위한 Don Knuth 의 책 "컴퓨터 프로그래밍의 예술" 에서 이루어집니다 . 3 권 에서 많은 정렬 알고리즘에 대한 정확한 평균 사례 결과 를 찾을 수 있습니다.

    • 퀵 :11.667(n+1)ln(n)1.74n18.74
    • Mergesort :12.5nln(n)
    • 힙 정렬 : 16nln(n)+0.01n
    • Insertionsort : [ 소스 ]2.25n2+7.75n3ln(n) 여러 정렬 알고리즘의 런타임

    이 결과는 Quicksort가 가장 빠름을 나타냅니다. 그러나 그것은 Knuth의 인공 기계에서만 입증되었으며 x86 PC라고 말할 수있는 것은 아닙니다. 또한 알고리즘은 작은 입력에 대해 다르게 관련됩니다.
    작은 입력을위한 여러 정렬 알고리즘의 런타임
    [ source ]

  2. 추상적 인 기본 작업을 분석 합니다 .

    비교 기반 정렬의 경우 일반적으로 스왑키 비교 입니다. Robert Sedgewick의 저서 (예 : "알고리즘" )에서이 접근 방식이 추구됩니다. 거기에서 찾을 수 있습니다

    • 퀵 : 비교와 평균 스왑12nln(n)13nln(n)
    • Mergesort : 비교, 최대 배열 액세스 (mergesort는 스왑 기반이 아니므로 계산할 수 없음)1.44nln(n)8.66nln(n)
    • Insertionsort : 평균 비교 및 스왑.14n214n2

    보시다시피 정확한 런타임 분석으로 알고리즘을 쉽게 비교할 수는 없지만 결과는 기계 세부 사항과 독립적입니다.

다른 입력 분포

위에서 언급했듯이 평균 사례는 항상 일부 입력 분포와 관련이 있으므로 임의 순열 이외의 경우를 고려할 수 있습니다. 예를 들어 동일한 요소를 가진 Quicksort에 대한 연구가 수행 되었으며 Java표준 정렬 기능 에 대한 좋은 기사가 있습니다.


8
유형 2의 결과는 기계 종속 상수를 삽입하여 유형 1의 결과로 변환 될 수 있습니다. 그러므로 나는 2가 우월한 접근법이라고 주장 할 것이다.
라파엘

2
@ 라파엘 +1. 나는 당신이 machine-dependant 가 또한 구현에 의존적 이라고 가정한다고 가정 합니까? 빠른 기계 + 나쁜 구현은 그리 효율적이지 않을 것입니다.
Janoma

2
@Janoma 나는 분석 된 알고리즘이 매우 상세한 형태 (분석이 상세 함)로 제공되고 가능한 한 문자로 구현되는 것으로 가정했습니다. 그러나 그렇습니다. 구현도 고려해야합니다.
Raphael

3
실제로, 유형 2 분석은 실제로 열등합니다. 실제 기계는 너무 복잡하여 유형 2의 결과를 유형 1로 변환 할 수 없습니다. 유형 1과 비교하면 실험 실행 시간을 플롯하는 데 5 분의 작업이 소요됩니다.
Jules

4
@ 줄 : "플로팅 실험 실행 시간"은 유형 1 이 아닙니다 . 공식적인 분석이 아니며 다른 기계로 전송할 수 없습니다. 그래서 우리는 결국 공식적인 분석을 수행합니다.
Raphael

78

이 질문과 관련하여 여러 가지 사항이 있습니다.

퀵 정렬은 일반적으로 빠릅니다

Quicksort는 최악의 동작을 가지고 있지만 일반적으로 빠릅니다. 랜덤 피벗 선택을 가정 할 때 입력을 비슷한 크기의 두 하위 집합으로 구분하는 숫자를 선택할 가능성이 매우 높습니다. 있다.O(n2)

특히, 10 스플릿마다 10 % -90 % 스 플리트 (meh 스 플리트)를 생성하는 피벗을 선택하더라도 1 요소 요소 스 플리트 (그렇지 않으면 최악의 스 플리트) , 우리의 실행 시간은 여전히 (이것은 병합 정렬이 아마도 더 빠를 정도로 상수를 날려 버릴 것입니다).n1O(nlogn)

퀵 정렬은 일반적으로 대부분의 종류보다 빠릅니다.

Quicksort는 일반적으로 보다 느린 정렬 (즉, 실행 시간에 따른 삽입 정렬 보다 빠릅니다 . 단순히 큰 의 실행 시간이 폭발 하기 때문입니다 .O(nlogn)O(n2)n

Quicksort가 Heapsort와 같은 대부분의 다른 알고리즘과 비교하여 실제로 매우 빠른 이유 는 비교적 캐시 효율이 높기 때문입니다. 실행 시간은 실제로 . 여기서 는 블록 크기입니다. 반면에 힙 정렬은 그러한 속도 향상이 없습니다. 메모리 캐시에 전혀 효율적으로 액세스하는 것은 아닙니다.O(nlogn)O(nBlog(nB))B

이 캐시 효율성의 이유는 입력을 선형으로 스캔하고 입력을 선형으로 분할하기 때문입니다. 즉, 캐시에 다른 번호를 교체하기 전에 캐시에로드하는 모든 수를 읽을 때 수행하는 모든 캐시로드를 최대한 활용할 수 있습니다. 특히, 알고리즘은 캐시를 인식하지 못하므로 모든 캐시 레벨에 대해 우수한 캐시 성능을 제공하며 이는 또 다른 승리입니다.

캐시 효율성은 로 더욱 향상 될 수 있습니다 . 여기서 은 기본 메모리의 크기입니다. way Quicksort를 사용하는 경우 Mergesort는 Quicksort와 동일한 캐시 효율성을 가지고 있으며 메모리가 심각한 제약 조건 인 경우 k-way 버전은 실제로 상수가 낮을수록 성능이 향상됩니다. 다음 단계로 넘어갑니다. 다른 요인들에 대해서는 Quicksort와 Mergesort를 비교해야합니다.O(nBlogMB(nB))Mk

Quicksort는 일반적으로 Mergesort보다 빠릅니다.

이 비교는 일정한 요소에 대한 것입니다 (일반적인 경우를 고려한다면). 특히, 선택은 Quicksort에 대한 피벗의 차선 선택과 Mergesort에 대한 전체 입력의 복사 사이 (또는이 복사를 피하는 데 필요한 알고리즘의 복잡성)입니다. 전자가 더 효율적이라는 것이 밝혀졌습니다. 이것 뒤에는 이론이 없으며 단지 더 빠릅니다.

Quicksort는보다 재귀적인 호출을 수행하지만 스택 공간 할당은 저렴하며 (실제로 스택을 날리지 않는 한 거의 무료) 스택을 재사용합니다. 힙 (또는 이 실제로 큰 경우 하드 드라이브)에 거대한 블록을 할당하는 것은 약간 비싸지 만 위에서 언급 한 작업 과 비교할 때 창백한 오버 헤드입니다 .nO(logn)O(n)

마지막으로 Quicksort는 올바른 순서로 발생하는 입력에 약간 민감하므로 일부 스왑을 건너 뛸 수 있습니다. Mergesort에는 그러한 최적화 기능이 없으므로 Mergesort에 비해 Quicksort가 약간 더 빠릅니다.

필요에 맞는 종류를 사용하십시오

결론적으로 정렬 알고리즘이 항상 최적의 것은 아닙니다. 필요에 맞는 것을 선택하십시오. 대부분의 경우 가장 빠른 알고리즘이 필요하고 드문 경우에는 약간 느려질 수 있으며 안정적인 정렬이 필요하지 않은 경우 Quicksort를 사용하십시오. 그렇지 않으면 필요에 맞는 알고리즘을 사용하십시오.


3
마지막 말은 특히 가치가 있습니다. 내 동료는 현재 다양한 입력 배포에서 Quicksort 구현을 분석합니다. 예를 들어, 일부는 여러 복제본으로 분류됩니다.
Raphael

4
@Raphael, McIllroy의 "Quicksort를위한 킬러 대적", 소프트웨어-실습 및 경험 29 (4), 341-344 (1999)를 살펴보십시오. Quicksort가 항상 시간 을 갖도록하는 끔찍한 기술에 대해 설명합니다 . Bentley와 McIllroy의 "정렬 기능 엔지니어링", 소프트웨어-실습 및 경험 23 (11), 1249-1265 (1993)도 관련이있을 수 있습니다. O(n2)
vonbrand

8
"[T]이 뒤에는 이론이 없으며 단지 더 빠릅니다." 그 진술은 과학적 관점에서 보면 매우 불만족 스럽다. 뉴턴이 "나비가 날아가고 사과가 쓰러진다"는 이론은없고 사과도 쓰러진다고한다.
David Richerby

2
@Alex ten Brink,“특히 알고리즘이 캐시를 모르는 것 ”이란 무엇입니까?
Hibou57

4
@David Richerby는“이러한 진술은 과학적 관점에서 보면 매우 불만족 스럽다”고 생각합니다. 일부 알고리즘 제품군은 완전한 공식화가 부족합니다. 해싱 함수가 예제입니다.
Hibou57

45

우리 대학의 프로그래밍 튜토리얼 중 하나에서 학생들에게 quicksort, mergesort, insertion sort와 Python의 내장 list.sort ( Timsort 라고 ) 의 성능을 비교하도록 요청했습니다 . 내장 된 list.sort가 다른 정렬 알고리즘보다 훨씬 더 나은 성능을 보여 주었기 때문에 실험 결과는 매우 놀라웠습니다. 따라서 일반적인 퀵소트 구현이 실제로 가장 우수하다고 결론을 내릴 수는 없습니다. 그러나 나는 더 나은 퀵 정렬의 구현 또는 거기에 하이브리드 버전이 있다고 확신합니다.

이 기사는 David R. MacIver 의 멋진 블로그 기사 로서 Timsort를 적응 형 병합 정렬 형식으로 설명합니다.


17
@Raphael 간결하게 표현하기 위해 Timsort는 점근선에 대한 병합 정렬과 짧은 입력에 대한 삽입 정렬 및 때때로 정렬 된 버스트 (실제로 자주 발생하는)가있는 데이터에 효율적으로 대처하기위한 일부 휴리스틱 스에 대한 병합 정렬입니다. Dai : 알고리즘 외에도 list.sort전문가가 최적화 한 내장 기능을 활용할 수 있습니다. 공정한 비교에는 모든 기능이 동일한 수준의 노력으로 동일한 언어로 작성됩니다.
Gilles

1
@Dai : 적어도 어떤 상황에서 어떤 입력 (예 : 분포)을 설명 할 수 있었는데 (RAM이 적고, 하나의 구현이 병렬화 되었는가 ...) 결과를 얻었습니다.
Raphael

7
우리는 난수 목록을 테스트하고 부분적으로 정렬하고 완전히 정렬하고 반대로 정렬했습니다. 1 학년 입문 과정 이었으므로 경험적 연구가 아니 었습니다. 그러나 이제 공식적으로 Java SE 7 및 Android 플랫폼에서 배열을 정렬하는 데 사용된다는 사실은 의미가 있습니다.
Dai

3
이것은 또한 여기에서 논의되었다 cstheory.stackexchange.com/a/927/74을
유카 Suomela을

34

QuickSort가 다른 정렬 알고리즘과 비교할 때 빠른 이유는 캐시 친화적이기 때문입니다. QS는 어레이의 세그먼트를 처리 할 때 세그먼트의 시작과 끝에서 요소에 액세스하고 세그먼트의 중심을 향해 이동합니다.

따라서 시작하면 배열의 첫 번째 요소에 액세스하고 메모리 ( "위치")가 캐시에로드됩니다. 그리고 두 번째 요소에 액세스하려고하면 이미 캐시에 들어 있으므로 매우 빠릅니다.

heapsort와 같은 다른 알고리즘은 이와 같이 작동하지 않으며 배열에서 많이 점프하여 속도가 느려집니다.


5
논쟁의 여지가있는 설명입니다 : mergesort는 캐시 친화적이기도합니다.
Dmytro Korduban

2
이 답변은 기본적으로 옳다고 생각하지만 여기에 youtube.com/watch?v=aMnn0Jq0J-E
rgrig

3
아마도 빠른 정렬의 평균 사례 시간 복잡성에 대한 곱셈 상수가 더 좋을 것입니다 (앞서 언급 한 캐시 요소와 무관).
Kaveh

1
당신이 언급 한 요점은 빠른 정렬의 다른 좋은 속성과 비교할 때 그렇게 중요하지 않습니다.
MMS

1
@Kaveh : "빠른 정렬의 평균 사례 시간 복잡도에 대한 곱셈 상수도 더 낫습니다"이것에 대한 데이터가 있습니까?
Giorgio

29

다른 사람들은 Quicksort 의 점근 평균 런타임이 다른 정렬 알고리즘 (일정한 설정)의 것보다 일정하게 더 우수 하다고 이미 말했습니다 .

그게 무슨 뜻이야? 순열이 임의로 선택되었다고 가정합니다 (고른 분포를 가정). 이 경우에 전형적인 피벗 선택 방법을 제공하는 피벗 기대에 반 목록 / 배열 대략 분할하는 단계; 그것이 우리를 만듭니다. 그러나 또한 재귀로 얻은 부분 솔루션을 병합 하는 데는 일정한 시간 만 걸립니다 (Mergesort의 경우 선형 시간과 반대). 물론 피벗에 따라 두 목록에서 입력을 분리하는 것은 선형 시간이지만 실제 스왑이 거의 필요하지 않습니다.O(nlogn)

Quicksort에는 여러 가지 변형이 있습니다 (예 : Sedgewick의 논문 참조). 서로 다른 입력 분포 (균일, 거의 정렬, 거의 역 정렬, 많은 중복 등)에서 다르게 수행되며 다른 알고리즘이 더 좋을 수 있습니다.

주목해야 할 또 다른 사실은 Quicksort가 오버 헤드가 적은 단순한 알고리즘에 비해 짧은 입력에서 느리다 는 것입니다 . 따라서 좋은 라이브러리는 길이가 1 인 목록으로 재귀하지 않지만 입력 길이가 보다 작은 경우 삽입 정렬을 사용합니다 (예 .k10


20

시간 복잡성을 가진 다른 비교 기반 정렬 알고리즘과 비교할 때 빠른 정렬은 종종 내부 정렬 알고리즘이기 때문에 merge-sort와 같은 다른 알고리즘보다 나은 것으로 간주됩니다. 다시 말해, 배열 멤버를 저장하기 위해 메모리가 훨씬 더 필요하지 않습니다.O(nlgn)

추신 : 정확하고 다른 알고리즘보다 낫다는 것은 작업에 달려 있습니다. 일부 작업의 경우 다른 정렬 알고리즘을 사용하는 것이 좋습니다.

또한보십시오:


3
@Janoma 이것은 사용하는 언어와 컴파일러의 문제입니다. 거의 모든 기능적 언어 (ML, Lisp, Haskell)는 스택이 커지는 것을 방지하는 최적화를 수행 할 수 있으며 명령형 언어를위한 더 똑똑한 컴파일러도 동일한 작업을 수행 할 수 있습니다 (GCC, G ++ 및 MSVC 모두이 작업을 수행한다고 생각합니다). 주목할만한 예외는 Java입니다.이 최적화는 결코 수행하지 않으므로 Java에서는 재귀를 반복으로 다시 작성하는 것이 합리적입니다.
Rafe Kettler

4
@ JD, quicksort (최소하지는 않지만)로 꼬리 호출 최적화를 사용할 수는 없습니다. 두 번째 통화는 최적화 할 수 있지만 첫 번째 통화는 최적화 할 수 없습니다.
svick

1
@ Janoma, 재귀 구현이 실제로 필요하지 않습니다. 예를 들어 C에서 qsort 함수의 구현을 보면 재귀 호출을 사용하지 않으므로 구현이 훨씬 빨라집니다.
Kaveh

1
Heapsort도 제자리에 있습니다. QS가 더 빠른 이유는 무엇입니까?
Kevin

6
스택 최적화가 없어도 재귀가 매번 입력 크기를 절반으로 줄이면 메모리가 그렇게 크지 않기 때문에 재귀가 매우 깊어지지 않습니다. 스택 깊이를 이상으로 늘리지 않고 전체 바이트 메모리 의 모든 바이트를 제자리에서 재귀 정렬 할 수 있습니다. 23240
Carl Mummert

16

quick-sort의 최악의 실행 시간은 이지만, quicksort는 평균적으로 매우 효율적이기 때문에 최상의 정렬로 간주됩니다. 예상 실행 시간은 입니다. 상수는 다른 정렬 알고리즘에 비해 매우 작습니다. 이것이 다른 정렬 알고리즘보다 빠른 정렬을 사용하는 주된 이유입니다.Θ(n2)Θ(nlogn)

두 번째 이유는 in-place정렬 을 수행 하고 가상 메모리 환경에서 잘 작동 하기 때문입니다 .

업데이트 : : (자 노마와 스빅의 의견 후)

이것을 더 잘 설명하기 위해 Merge Sort (Merge Sort는 빠른 정렬 후 널리 사용되는 다음 정렬 알고리즘이기 때문에)를 사용하여 예제를 제공하고 추가 상수가 어디에서 왔는지 (내가 아는 한 가장 큰 이유와 내가 생각하는 이유) 빠른 정렬이 더 좋습니다) :

다음과 같은 순서를 고려하십시오.

12,30,21,8,6,9,1,7. The merge sort algorithm works as follows:

(a) 12,30,21,8    6,9,1,7  //divide stage
(b) 12,30   21,8   6,9   1,7   //divide stage
(c) 12   30   21   8   6   9   1   7   //Final divide stage
(d) 12,30   8,21   6,9   1,7   //Merge Stage
(e) 8,12,21,30   .....     // Analyze this stage

마지막 단계가 어떻게 진행되는지 완전히 신경 쓰면 처음 12는 8과 비교되고 8은 작으므로 먼저 진행됩니다. 이제 12는 21에 비해 AGAIN이고 12는 다음에 계속됩니다. 최종 병합, 즉 4 개의 다른 요소가있는 4 개의 요소를 사용하는 경우 빠른 정렬에서 발생하지 않는 상수로 많은 EXTRA 비교가 발생합니다. 이것이 빠른 정렬이 선호되는 이유입니다.


1
그러나 상수가 왜 그렇게 작습니까?
svick

1
@svick 정렬되어 있기 때문에 in-place추가 메모리가 필요하지 않습니다.
0x0

heapsort가 모든 경우에 이고 quicksort와 같은 위치에 있기 때문에 quicksort와 heapsort를 더 잘 비교하지 않겠습니까 ? Θ(nlgn)
Robert S. Barnes

15

실제 데이터 작업에 대한 나의 경험은 퀵 정렬이 좋지 않은 선택이라는 것 입니다. Quicksort는 무작위 데이터와 잘 작동하지만 실제 데이터는 대부분 무작위가 아닙니다.

2008 년에 나는 퀵 정렬을 사용하기 위해 매달린 소프트웨어 버그를 추적했습니다. 잠시 후 나는 삽입 정렬, 퀵 정렬, 힙 정렬 및 병합 정렬의 간단한 함축을 작성하고 이것을 테스트했습니다. 병합 데이터 정렬은 대규모 데이터 세트 작업을 수행하는 동안 다른 모든 데이터보다 성능이 뛰어났습니다.

그 이후로 병합 정렬은 내가 선택한 정렬 알고리즘입니다. 우아합니다. 구현이 간단합니다. 안정적인 정렬입니다. 그것은 퀵 정렬과 같이 이차 행동으로 퇴보하지 않습니다. 작은 배열을 정렬하기 위해 삽입 정렬로 전환합니다.

많은 경우에 나는 주어진 구현이 실제로는 퀵 정렬이 아니라는 것을 알기 위해 퀵 정렬에 대해 놀랍게 잘 작동한다는 내 자신의 생각을 발견했습니다. 때로는 구현이 quicksort와 다른 알고리즘 사이를 전환하고 때로는 quicksort를 전혀 사용하지 않습니다. 예를 들어, GLibc의 qsort () 함수는 실제로 병합 정렬을 사용합니다. 작업 공간 할당에 실패한 경우에만 코드 주석이 "느린 알고리즘"이라고 부르는 퀵 정렬로 되돌아갑니다 .

편집 : Java, Python 및 Perl과 같은 프로그래밍 언어는 병합 정렬을 사용하거나 더 큰 세트의 경우 Timsort 또는 병합 정렬과 같은 작은 파생어 및 작은 세트의 삽입 정렬과 같은 파생물을 사용합니다. Java는 일반 퀵 정렬보다 빠른 듀얼 피벗 퀵 정렬을 사용합니다.


우리는 이미 정렬 된 데이터 묶음에 삽입하기 위해 지속적으로 추가 / 복원하기 때문에 이와 비슷한 것을 보았습니다. 무작위 퀵 정렬을 사용하여이 문제를 평균적으로 해결하고 (그리고 희귀하고 무작위로 느린 느린 정렬에 놀라게 할 수 있음), 또는 완료하는 데 시간이 전혀 걸리지 않는 항상 느린 정렬을 허용 할 수 있습니다. 때로는 정렬 안정성도 필요합니다. Java는 병합 정렬 사용에서 빠른 정렬 변형으로 바뀌 었습니다.
Rob

@Rob 이것은 정확하지 않습니다. Java는 오늘날까지 mergesort (Timsort)의 변형을 사용합니다. 퀵 정렬의 변형도 사용합니다 (이중 피봇 퀵 정렬).
Erwan Legrand 2016 년

14

1- 빠른 정렬이 완료되었습니다 (정액 이외의 추가 메모리가 필요하지 않음).

2- 빠른 정렬은 다른 효율적인 정렬 알고리즘보다 구현하기가 더 쉽습니다.

3- 빠른 정렬은 다른 효율적인 정렬 알고리즘보다 실행 시간이 일정하지 않습니다.

업데이트 : 병합 정렬의 경우 병합하기 전에 데이터를 저장하기 위해 추가 배열이 필요한 "병합"을 수행해야합니다. 그러나 빠른 정렬에서는 그렇지 않습니다. 이것이 바로 빠른 정렬이 제자리에있는 이유입니다. 병합 정렬에서 상수 요소를 증가시키는 병합에 대한 추가 비교도 있습니다.


3
당신은 에 적절한 고급 반복 퀵 구현을? 그것들은 많은 것들이지만 "쉬운"것은 아닙니다.
Raphael

2
2 번은 내 질문에 전혀 답 하지 않고 1 번과 3 번은 내 의견으로는 적절한 정당화가 필요합니다.
Janoma

@Raphael : 쉬워요. 포인터 대신 배열을 사용하여 제자리에서 빠른 정렬을 구현하는 것이 훨씬 쉽습니다. 그리고 제자리에있는 것이 반복적 일 필요는 없습니다.
MMS

병합을위한 배열은 그렇게 나쁘지 않습니다. 소스 파일에서 대상 파일로 한 항목을 이동하면 더 이상 해당 항목이 없어도됩니다. 동적 배열을 사용하는 경우 병합 할 때 일정한 메모리 오버 헤드가 있습니다.
Oskar Skog 2016 년

@ 1 Mergesort도 제자리에있을 수 있습니다. @ 2 무엇이 효율적인가? 병합 정렬은 매우 간단하지만 내 의견으로는 효율적이기 때문에 병합 정렬을 좋아합니다. @ 3 대량의 데이터를 정렬 할 때는 관련이 없으며 알고리즘을 효율적으로 구현해야합니다.
Oskar Skog 2016 년

11

특정 정렬 알고리즘은 어떤 조건에서 실제로 가장 빠른 알고리즘입니까?

Θ(log(n)2)Θ(nlog(n)2)

Θ(nk)Θ(nm)k=2#number_of_Possible_valuesm=#maximum_length_of_keys

3) 기본 데이터 구조가 연결된 요소로 구성되어 있습니까? 예-> 항상 병합 정렬을 사용합니다. 링크 된 데이터 구조에 대해 서로 다른 종류의 병합 된 종류의 병합 된 고정 크기 또는 적응 형 (일명 자연) 상향식을 쉽게 구현할 수 있으며 각 단계에서 전체 데이터를 복사 할 필요가 없으며 재귀가 필요하지 않기 때문에 다른 일반적인 비교 기반 정렬보다 빠르며 빠른 정렬보다 빠릅니다.

4) 분류가 안정적이어야합니까? 예-> 임의의 정렬을 안정화하는 것처럼 빠른 정렬이 선호되는 경우에도 기본 데이터 구조 및 예상되는 데이터의 종류에 따라 적절한 크기 또는 적응 형 병합 정렬을 사용합니다. 알고리즘은 원래 인덱스로 구성된 최악의 경우 추가 메모리를 필요로하며, 입력 데이터에 대해 수행 될 각 스왑과도 동기화 상태를 유지해야합니다. 병합 정렬이 차단되었을 수 있습니다.Θ(n)

5) 기본 데이터의 크기를 중소 규모로 묶을 수 있습니까? 예 : n <10,000 ... 100,000,000 (기본 아키텍처 및 데이터 구조에 따라 다름)입니까? 예-> 비 토닉 정렬 또는 배처 홀수-짝 병합을 사용하십시오. 고토 1)

6) 다른 메모리 를 절약 할 수 있습니까 ? 예-> a) 입력 데이터가 이미 정렬 된 대량의 순차적 데이터로 구성되어 있습니까? -> 적응 형 (일명 자연) 병합 정렬 또는 팀 정렬 사용 예-> b) 입력 데이터가 대부분 올바른 위치에있는 요소로 구성되어 있습니까? -> 버블 정렬 또는 삽입 정렬을 사용하십시오. 당신이 그들의 두려워하는 경우 (거의 정렬 된 데이터에 대한 병적 인) 시간의 복잡성을, 어쩌면 간격의 (거의) 점근 적으로 최적의 순서와 종류의 쉘로 전환을 고려, 일부 시퀀스 산출하는 최악의 경우 런타임이 알려져 있거나 콤 정렬을 시도해보십시오. 셸 정렬 또는 빗 정렬이 실제로 제대로 수행되는지 확실하지 않습니다.Θ(n)Θ(n2)Θ(nlog(n)2)

아니오-> 7) 다른 메모리 를 절약 할 수 있습니까 ? 예-> a) 신뢰할 수없는 데이터 구조가 순차적 순차 액세스를 허용합니까? 예-> 데이터 끝에 도달 할 때까지 한 번에 하나의 읽기 / 쓰기 액세스 시퀀스 만 허용합니까 (예 : 테이프 액세스)? 예-> i) 병합 정렬을 사용하지만이 경우를 정할 수있는 확실한 방법은 없으므로 추가 메모리 가 필요할 수 있습니다 . 그러나 시간과 공이 있다면 만 사용하여 시간 에 2 개의 배열을 병합하는 방법이 있습니다Θ(log(n))Θ(n)Θ(n)Θ(log(n))Donald E. Knuth "컴퓨터 프로그래밍의 기술, 3 권 : 분류 및 검색"에 따르면, 안정적인 방식으로 공간 5.5.3. L. Trabb-Pardo의 알고리즘이 있다고 말합니다. 그러나 이것이 순진한 mergesort 버전이나 위의 경우보다 quicksort보다 빠를 것이라고 의심합니다. 아니요, 그것은 일련의 데이터에 여러 번 동시에 액세스 할 수 있습니다 (예 : 테이프 드라이브가 아닙니다)-> ii) 퀵 정렬을 사용하십시오. 실제적인 목적으로 무작위 또는 대략적인 중앙값을 권장합니다. 병리학 적 사례에 주의를 기울이면 소개 정렬 사용을 고려하십시오. 결정 론적 행동에 지친 사람이라면 중간 값 알고리즘을 사용하여 피벗 요소를 선택하는 것이 좋습니다.Θ(n2)Θ(n)시간과 순진한 구현에는 공간 (병렬화 가능)이 필요한 반면 공간 (병렬화 불가능 만 필요로 구현 될 수 있습니다 . 그러나 중앙값 중간 알고리즘은 최악의 경우 런타임 을 갖는 결정적인 빠른 정렬을 제공합니다 . 아니요-> 문제가 발생했습니다 (죄송합니다. 각 데이터 요소에 한 번 이상 액세스 할 수있는 방법은 최소한 한 번은 필요합니다)Θ(n)Θ(log(n))Θ(nlog(n))

아니오-> 8) 적은 양의 메모리를 절약 할 수 있습니까? 예-> 기본 데이터 구조가 임의 액세스를 허용합니까? 예-> 힙 정렬을 사용하면 의 점근 최적의 런타임이 있지만 캐시 일관성이 떨어지고 병렬화되지 않습니다. 아니오-> 당신은 망했다 아니오-> 당신은 망했다Θ(nlog(n))

빠른 정렬을위한 구현 힌트 :

1) 순진 바이너리 퀵 정렬에는 추가 메모리가 필요하지만 마지막 재귀 호출을 루프에 다시 작성하여 로 줄이는 것이 상대적으로 쉽습니다 . k> 2의 k-ary 퀵 정렬에 대해 동일한 작업을 수행하려면 공간 (마스터 정리에 따라)이 필요하므로 이진 퀵 정렬에는 최소의 메모리가 필요하지만 k> 2의 k-ary 퀵 정렬이 실제 설정에서 바이너리 퀵 정렬보다 빠를 지 아는 사람이 있다면 기뻐하십시오.Θ는 ( 로그 ( N ) ) Θ를 ( N 로그 K ( 케이 - 1 ) )Θ(n)Θ(log(n))Θ(nlogk(k1))

2) 빠른 정렬의 상향식 반복 변형이 있지만 AFAIK에는 하향식과 동일한 점근 적 공간 및 시간 경계가 있으며 구현하기 어려운 추가 측면이 있습니다 (예 : 대기열을 명시 적으로 관리). 내 경험은 실제적인 목적을 위해 고려할 가치가 없다는 것입니다.

mergesort에 대한 구현 힌트 :

1) bottum-up mergesort는 재귀 호출이 필요하지 않으므로 항상 top-down mergesort보다 빠릅니다.

2) 매우 순진한 병합 정렬은 각 단계 후에 시간 배열에서 데이터를 다시 복사하는 대신 이중 버퍼를 사용하여 속도를 높이고 버퍼를 전환 할 수 있습니다.

3) 많은 실제 데이터의 경우 적응 병합 병합은 고정 크기 병합 정렬보다 훨씬 빠릅니다.

4) 입력 데이터를 거의 동일한 크기의 k 개로 분할하여 병합 알고리즘을 쉽게 병렬화 할 수 있습니다. 이것은 데이터에 대한 k 참조를 필요로하며, 모든 k (또는 작은 상수 c> = 1의 경우 c * k)가 가장 가까운 메모리 계층 (보통 L1 데이터 캐시)에 맞도록 k를 선택하는 것이 좋습니다. k 요소 중에서 가장 작은 요소를 선택하면 순진한 방법 (선형 검색)은 시간이 걸리지 만 k 요소 내에 최소 힙을 작성하고 가장 작은 요소를 선택하면 상각 된 만 필요합니다 시간 (최소한의 선택은 입니다. 그러나 각 요소에서 한 요소가 제거되고 다른 요소로 대체되므로 약간의 유지 관리가 필요합니다). 병렬 병합에는 항상 필요합니다Θ ( 로그 ( k ) ) Θ ( 1 ) Θ ( n )Θ(k)Θ(log(k))Θ(1)Θ(n) k에 관계없이 메모리.

필자가 작성한 내용에서 다음 조건이 모두 적용되는 경우를 제외하고 quicksort가 종종 가장 빠른 알고리즘이 아니라는 것이 분명합니다.

1) "몇 가지"가능한 값 이상이 있습니다

2) 기본 데이터 구조가 연결되어 있지 않습니다

3) 우리는 안정적인 주문이 필요하지 않습니다

4) 데이터는 bitonic sorter 또는 Batcher 홀수-짝 mergesort 킥의 약간 차선의 비 점근 적 런타임에 적합합니다.

5) 데이터가 거의 정렬되지 않았으며 이미 정렬 된 더 큰 부분으로 구성되지 않음

6) 여러 장소에서 동시에 데이터 시퀀스에 액세스 할 수 있습니다

7) {메모리 정렬은 퀵 정렬의 가능한 하위 최적 분할을 넘어 알고리즘 속도를 늦추는 한 특히 비용이 많이 듭니다 (병합 정렬의 주요 단점이기 때문에). } 또는 { 추가 메모리 만 가질 수 있습니다 . 이 너무 많습니다 (예 : 외부 저장소)}Θ ( n )Θ(log(n))Θ(n)

추신 : 누군가 텍스트의 서식을 지정하는 데 도움이 필요합니다.


(5) : Apple의 정렬 구현은 배열의 시작과 끝에서 모두 오름차순 또는 내림차순으로 하나의 실행을 먼저 확인합니다. 이러한 요소가 많지 않으면 매우 빠르며 n / ln n 이상인 경우 이러한 요소를 매우 효과적으로 처리 할 수 ​​있습니다. 두 개의 정렬 된 배열을 연결하고 결과를 정렬하면 병합이 발생합니다
gnasher729

8

대부분의 정렬 방법은 짧은 단계로 데이터를 이동해야합니다 (예를 들어, 병합 정렬은 로컬로 변경 한 다음이 작은 데이터를 병합 한 다음 더 큰 데이터를 병합합니다). 결과적으로 데이터가 대상에서 멀리 떨어져있는 경우 많은 데이터 이동이 필요합니다.

반면에 Quicksort는 메모리의 첫 번째 부분에있는 숫자와 배열의 두 번째 부분에있는 숫자와 작은 숫자를 교환하려고 시도합니다 ( 정렬 경우 인수는 다른 의미에서 동일하므로) 최종 목적지 근처에 빠르게 할당됩니다.ab


5
quicksort vs merge sort에 대한 귀하의 주장은 물을 보유하지 않습니다. Quicksort는 큰 움직임으로 시작한 다음 더 작고 작은 움직임을 만듭니다 (각 단계에서 약 절반 정도). 병합 정렬은 작은 이동으로 시작한 다음 더 크고 더 크게 이동합니다 (각 단계에서 약 2 배). 이것은 하나가 다른 것보다 더 효율적이라고 지적하지는 않습니다.
Gilles
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.