Quicksort 대 힙 정렬


답변:


60

이 논문 에는 몇 가지 분석이 있습니다.

또한 Wikipedia에서 :

quicksort의 가장 직접적인 경쟁자는 heapsort입니다. 힙 정렬은 일반적으로 빠른 정렬보다 다소 느리지 만 최악의 경우 실행 시간은 항상 Θ (nlogn)입니다. Quicksort는 일반적으로 더 빠르지 만 잘못된 경우가 감지되면 heapsort로 전환되는 introsort 변형을 제외하고는 최악의 성능 가능성이 남아 있습니다. 힙 정렬이 필요하다는 것을 미리 알고있는 경우 직접 사용하는 것이 introsort가 전환 될 때까지 기다리는 것보다 빠릅니다.


12
일반적인 구현에서는 quicksort도 heapsort도 안정적인 정렬이 아니라는 점에 유의하는 것이 중요 할 수 있습니다.
MjrKusanagi

@DVK, 당신의 링크에 따르면 cs.auckland.ac.nz/~jmor159/PLDS210/qsort3.html , 힙 정렬 N = 100 2842 개 비교 걸리지 만 = 500 N에 대한 53113 개 비교를합니다. 그리고 그것은 n = 500과 n = 100 사이의 비율이 18 배라는 것을 의미하며, O (N logN) 복잡성을 가진 힙 정렬 알고리즘과 일치하지 않습니다. 힙 정렬 구현에 어떤 종류의 버그가있을 가능성이 큽니다.
DU Jiaen

@DUJiaen-O ()는 큰 N에서 점근 적 동작에 관한 것이며 가능한 승수를 가지고 있음을 기억하십시오
DVK

이것은 승수와 관련이 없습니다. 알고리즘의 복잡성이 O (N log N)이면 Time (N) = C1 * N * log (N) 추세를 따라야합니다. 그리고 Time (500) / Time (100)을 취하면 C1이 사라지고 결과는 (500 log500) / (100 log100) = 6.7로 닫혀 야합니다.하지만 링크에서 보면 18입니다. 규모를 너무 많이 벗어났습니다.
DU Jiaen

2
링크가 죽었습니다
PlsWork

123

힙 정렬은 O (N log N) 보장되며, Quicksort의 최악의 경우보다 훨씬 낫습니다. Heapsort는 Mergesort에 필요한대로 정렬 된 데이터를 넣는 데 다른 배열에 대해 더 많은 메모리가 필요하지 않습니다. 그렇다면 상용 애플리케이션이 Quicksort를 고수하는 이유는 무엇입니까? 다른 구현에 비해 특별한 Quicksort는 무엇입니까?

나는 알고리즘을 직접 테스트했으며 Quicksort에 실제로 특별한 것이 있음을 보았습니다. 힙 및 병합 알고리즘보다 훨씬 빠르게 실행됩니다.

Quicksort의 비결은 : 불필요한 요소 교체를 거의 수행하지 않습니다. 스왑은 시간이 많이 걸립니다.

Heapsort를 사용하면 모든 데이터가 이미 정렬 된 경우에도 배열을 정렬하기 위해 100 % 요소를 교체 할 것입니다.

Mergesort를 사용하면 더 나빠집니다. 데이터가 이미 정렬 된 경우에도 다른 배열에 100 % 요소를 쓰고 원래 배열에 다시 쓸 것입니다.

Quicksort를 사용하면 이미 주문한 것을 교체 할 수 없습니다. 데이터가 완전히 주문되면 거의 스왑이 없습니다! 최악의 경우에 대해 많은 소란이 있지만, 배열의 첫 번째 또는 마지막 요소를 얻는 것 외에 피벗 선택을 약간 개선하면 피벗을 피할 수 있습니다. 첫 번째, 마지막 및 중간 요소 사이의 중간 요소에서 피벗을 얻으면 최악의 경우를 피하는 것으로 충분합니다.

Quicksort에서 우월한 것은 최악의 경우가 아니라 최상의 경우입니다! 가장 좋은 경우에는 동일한 수의 비교를 수행하지만 거의 아무것도 바꾸지 않습니다. 평균적으로 Heapsort 및 Mergesort에서와 같이 모든 요소가 아닌 요소의 일부를 교체합니다. 이것이 Quicksort에 최고의 시간을 제공하는 것입니다. 더 적은 스왑, 더 빠른 속도.

릴리스 모드에서 실행되는 내 컴퓨터의 C #에서 아래 구현은 Array.Sort를 중간 피벗으로 3 초, 개선 된 피벗으로 2 초 더 낫습니다 (예, 좋은 피벗을 얻기위한 오버 헤드가 있음).

static void Main(string[] args)
{
    int[] arrToSort = new int[100000000];
    var r = new Random();
    for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);

    Console.WriteLine("Press q to quick sort, s to Array.Sort");
    while (true)
    {
        var k = Console.ReadKey(true);
        if (k.KeyChar == 'q')
        {
            // quick sort
            Console.WriteLine("Beg quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            QuickSort(arrToSort, 0, arrToSort.Length - 1);
            Console.WriteLine("End quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
        }
        else if (k.KeyChar == 's')
        {
            Console.WriteLine("Beg Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            Array.Sort(arrToSort);
            Console.WriteLine("End Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
        }
    }
}

static public void QuickSort(int[] arr, int left, int right)
{
    int begin = left
        , end = right
        , pivot
        // get middle element pivot
        //= arr[(left + right) / 2]
        ;

    //improved pivot
    int middle = (left + right) / 2;
    int
        LM = arr[left].CompareTo(arr[middle])
        , MR = arr[middle].CompareTo(arr[right])
        , LR = arr[left].CompareTo(arr[right])
        ;
    if (-1 * LM == LR)
        pivot = arr[left];
    else
        if (MR == -1 * LR)
            pivot = arr[right];
        else
            pivot = arr[middle];
    do
    {
        while (arr[left] < pivot) left++;
        while (arr[right] > pivot) right--;

        if(left <= right)
        {
            int temp = arr[right];
            arr[right] = arr[left];
            arr[left] = temp;

            left++;
            right--;
        }
    } while (left <= right);

    if (left < end) QuickSort(arr, left, end);
    if (begin < right) QuickSort(arr, begin, right);
}

10
아니오에 대한 고려 사항에 +1. 다른 정렬 알고리즘에 필요한 스왑, 읽기 / 쓰기 작업
ycy

2
결정론적이고 일정한 시간 피벗 선택 전략의 경우 O (n ^ 2) 최악의 경우를 생성하는 배열을 찾을 수 있습니다. 최소값 만 제거하는 것만으로는 충분하지 않습니다. 특정 발 육성 밴드 내에있는 피벗을 안정적으로 선택해야합니다.
Antimony

1
이것이 직접 코딩 한 빠른 정렬과 C # 기본 제공 Array.sort 간의 시뮬레이션을 위해 실행 한 정확한 코드인지 궁금합니다. 이 코드를 테스트했고 모든 테스트에서 손으로 코딩 한 빠른 정렬은 Array.sort와 동일했습니다. 이 테스트에서 내가 제어 한 한 가지는 무작위 배열의 동일한 복사본을 두 개 만드는 것이 었습니다. 결국, 주어진 무작위 화는 잠재적으로 다른 무작위 화보다 더 유리할 수 있습니다 (최선의 경우에 기울임). 그래서 나는 각 세트를 통해 동일한 세트를 실행했습니다. Array.sort 매번 동점 또는 이길 (릴리스 빌드 btw).
Chris

1
병합 정렬은 교과서에서 매우 순진한 구현이 아니라면 요소의 100 %를 복사 할 필요가 없습니다. 50 % 만 복사하면됩니다 (두 병합 된 배열의 왼쪽). 실제로 두 요소를 "교환"해야 할 때까지 복사를 미루는 것도 사소한 일이므로 이미 정렬 된 데이터를 사용하면 메모리 오버 헤드가 발생하지 않습니다. 그래서 50 %조차도 실제로 최악의 경우이고, 당신은 그것과 0 % 사이의 어떤 것도 가질 수 있습니다.
ddekany dec.

1
@MarquinhoPeli 저는 정렬 된 목록의 크기에 비해 사용 가능한 메모리가 100 %가 아닌 50 % 만 더 필요하다고 말하려고했는데, 이는 일반적인 오해 인 것 같습니다. 그래서 저는 최대 메모리 사용량에 대해 이야기했습니다. 링크를 줄 수는 없지만 이미 정렬 된 배열의 절반을 제자리에 병합하려고하면 쉽게 알 수 있습니다 (아직 사용하지 않은 요소를 덮어 쓰는 문제는 왼쪽 절반에만 있습니다). 전체 정렬 과정에서 얼마나 많은 메모리 복사를해야 하는가는 또 다른 질문이지만, 분명히 최악의 경우는 정렬 알고리즘에 대해 100 % 미만이 될 수 없습니다.
ddekany

15

대부분의 상황에서 빠른 속도와 약간 빠른 속도는 관련이 없습니다. 때때로 속도가 느려지는 것을 결코 원하지 않습니다. 느린 상황을 피하기 위해 QuickSort를 조정할 수 있지만 기본 QuickSort의 우아함을 잃게됩니다. 따라서 대부분의 경우 실제로 HeapSort를 선호합니다. 완전히 단순하면서도 우아하게 구현할 수 있으며 결코 느린 정렬을 얻을 수 없습니다.

대부분의 경우 최대 속도를 원할 경우 QuickSort가 HeapSort보다 선호 될 수 있지만 둘 다 정답이 아닐 수 있습니다. 속도가 중요한 상황의 경우 상황의 세부 사항을 면밀히 검토하는 것이 좋습니다. 예를 들어, 속도가 중요한 일부 코드에서 데이터가 이미 정렬되었거나 거의 정렬되어있는 것이 매우 일반적입니다 (종종 함께 위아래로 이동하거나 서로 반대 방향으로 위아래로 이동하는 여러 관련 필드를 인덱싱하고 있습니다. 따라서 하나를 기준으로 정렬하면 다른 항목이 정렬되거나 반대로 정렬되거나 닫힙니다. 둘 중 하나가 QuickSort를 종료 할 수 있습니다. 이 경우 둘 다 구현하지 않았습니다 ... 대신 Dijkstra의 SmoothSort를 구현했습니다 ... 이미 정렬되었거나 거의 정렬되었을 때 O (N) 인 HeapSort 변형 ... 그다지 우아하지 않고 이해하기 쉽지 않습니다. 하지만 빨리 ... 읽기http://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF 코드에 좀 더 어려운 것을 원한다면.


6

Quicksort-Heapsort 인플레 이스 하이브리드도 정말 흥미 롭습니다. 대부분의 경우 최악의 경우 n * log n 비교 만 필요하기 때문입니다 (무증상의 첫 번째 항과 관련하여 최적이므로 최악의 시나리오를 피할 수 있습니다). of Quicksort), O (log n) 추가 공간이 있으며 이미 주문 된 데이터 세트와 관련하여 Quicksort의 양호한 동작의 "반"이상을 보존합니다. 매우 흥미로운 알고리즘은 http://arxiv.org/pdf/1209.4214v1.pdf 에 Dikert와 Weiss가 제공합니다 .

  • sqrt (n) 요소의 무작위 샘플 중앙값으로 피벗 p를 선택합니다 (이는 Tarjan & co 알고리즘을 통해 최대 24 sqrt (n) 비교 또는 훨씬 더 복잡한 스파이더를 통한 5 sqrt (n) 비교에서 수행 할 수 있습니다. -Schonhage의 공장 알고리즘);
  • Quicksort의 첫 번째 단계 에서처럼 어레이를 두 부분으로 분할하십시오.
  • 가장 작은 부분을 힙화하고 O (log n) 추가 비트를 사용하여 모든 왼쪽 자식이 형제보다 큰 값을 갖는 힙을 인코딩합니다.
  • 힙의 루트를 재귀 적으로 추출하고, 힙의 잎에 도달 할 때까지 루트가 남긴 lacune을 걸러 내고, 배열의 다른 부분에서 가져온 적절한 요소로 lacune을 채 웁니다.
  • 배열의 순서가 지정되지 않은 나머지 부분에 대해 반복합니다 (정확한 중앙값으로 p를 선택한 경우에는 전혀 재귀가 없습니다).

2

Comp. 사이 quick sortmerge sort두 이후는 빠른 정렬을위한 시간을 실행하는 wrost 케이스의 실행 시간을 wrost 경우 사이에 차이가 정렬 장소에서의 유형 O(n^2)분류는 여전히 힙에 대한 O(n*log(n))데이터의 평균 금액에 대한 빠른 정렬 더 유용 할 것이다. 무작위 알고리즘이므로 정확한 ans를 얻을 확률이 있습니다. 선택한 피벗 요소의 위치에 따라 더 짧은 시간에 달라집니다.

그래서

좋은 전화 : L과 G의 크기는 각각 3s / 4 미만입니다.

잘못된 호출 : L과 G 중 하나의 크기가 3s / 4보다 큽니다.

소량의 경우 삽입 정렬로 이동하고 매우 많은 양의 데이터의 경우 힙 정렬로 이동할 수 있습니다.


병합 정렬은 내부 정렬로 구현할 수 있지만 구현은 복잡합니다. AFAIK, 대부분의 병합 정렬 구현은 제자리에 있지 않지만 안정적입니다.
MjrKusanagi

2

힙 정렬은 O (n * log (n)) 의 최악의 실행 사례를 갖는 이점이 있으므로 퀵 정렬이 제대로 수행되지 않을 가능성이있는 경우 (일반적으로 정렬 된 데이터 세트) 힙 정렬이 훨씬 선호됩니다.


4
Quicksort는 잘못된 피벗 선택 방법을 선택한 경우 대부분 정렬 된 데이터 세트에서만 제대로 수행되지 않습니다. 즉, 잘못된 피벗 선택 방법은 항상 첫 번째 또는 마지막 요소를 피벗으로 선택하는 것입니다. 매번 임의의 피벗을 선택하고 반복되는 요소를 처리하는 좋은 방법을 사용하는 경우 최악의 퀵 정렬 가능성은 매우 적습니다.
Justin Peel

1
@Justin-그것은 매우 사실입니다, 나는 순진한 구현에 대해 말하고있었습니다.
zellio

1
@Justin : 사실입니다.하지만 큰 속도 저하의 가능성은 항상 존재합니다. 일부 응용 프로그램의 경우 속도가 느리더라도 O (n log n) 동작을 보장하고 싶을 수 있습니다.
David Thornley

2

아키텍처 수준으로 이동하면 캐시 메모리에서 큐 데이터 구조를 사용하므로 큐에서 사용할 수있는 모든 항목이 정렬됩니다. 빠른 정렬에서는 배열을 길이로 나누는 데 문제가 없습니다. sort (배열을 사용하여) 부모가 캐시에서 사용 가능한 하위 배열에 없을 수 있으며 캐시 메모리로 가져와야 할 수 있습니다. 시간이 많이 걸립니다. 퀵소트가 최고입니다 !! 😀


1

Heapsort 는 힙을 빌드 한 다음 최대 항목을 반복적으로 추출합니다. 최악의 경우는 O (n log n)입니다.

그러나 O (n2) 인 빠른 정렬 의 최악의 경우를 보게된다면 빠른 정렬이 대용량 데이터에 적합하지 않다는 것을 깨달았을 것입니다.

그래서 이것은 정렬을 흥미롭게 만듭니다. 오늘날 많은 정렬 알고리즘이 사용되는 이유는 모두 최고의 장소에서 '최고'이기 때문이라고 생각합니다. 예를 들어, 거품 정렬은 데이터가 정렬되면 빠른 정렬을 수행 할 수 있습니다. 또는 정렬 할 항목에 대해 알고 있으면 더 잘할 수 있습니다.

이것은 귀하의 질문에 직접 답변하지 않을 수 있습니다.


1
버블 정렬을 사용하지 마십시오. 데이터가 정렬 될 것이라고 합리적으로 생각하는 경우 삽입 정렬을 사용하거나 데이터가 정렬되었는지 확인하기 위해 데이터를 테스트 할 수도 있습니다. Bubblesort를 사용하지 마십시오.
vy32 2014

매우 큰 RANDOM 데이터 세트가있는 경우 가장 좋은 방법은 빠른 정렬입니다. 부분적으로 주문한 경우에는 그렇지 않지만 방대한 데이터 세트로 작업을 시작하면 최소한이 정도만 알고 있어야합니다.
Kobor42

1

힙 정렬은 매우 큰 입력을 처리 할 때 안전한 방법입니다. 점근 분석에 따르면 최악의 경우 Heapsort의 성장 순서는이며 최악의 경우 Big-O(n logn)Quicksort보다 낫습니다 Big-O(n^2). 그러나 Heapsort 는 잘 구현 된 빠른 정렬보다 실제로 대부분의 컴퓨터에서 다소 느립니다. Heapsort는 안정적인 정렬 알고리즘도 아닙니다.

heapsort가 실제로 quicksort보다 느린 이유는 데이터 요소가 상대적으로 가까운 저장 위치에있는 quicksort 의 더 나은 참조 지역성 ( " https://en.wikipedia.org/wiki/Locality_of_reference ") 때문입니다. 강력한 참조 지역성을 나타내는 시스템은 성능 최적화를위한 훌륭한 후보입니다. 그러나 힙 정렬은 더 큰 도약을 처리합니다. 이것은 더 작은 입력에 대해 퀵 정렬을 더 유리하게 만듭니다.


2
빠른 정렬도 안정적이지 않습니다.
Antimony

1

나에게는 heapsort와 quicksort 사이에 매우 근본적인 차이점이 있습니다. 후자는 재귀를 사용합니다. 재귀 알고리즘에서 힙은 재귀 횟수에 따라 증가합니다. n 이 작은 경우에는 중요하지 않지만 지금은 n = 10 ^ 9 !!로 두 개의 행렬을 정렬하고 있습니다. 이 프로그램은 거의 10GB의 램을 사용하며 추가 메모리가 있으면 내 컴퓨터가 가상 디스크 메모리로 스와핑을 시작합니다. 내 디스크는 RAM 디스크이지만 여전히 교체하면 속도에 큰 차이가 있습니다. 따라서 프로그래머에게 미리 알려지지 않은 크기와 비모수 적 통계 정렬 유형을 포함하는 조정 가능한 차원 매트릭스를 포함하는 C ++로 코딩 된 statpack에서 매우 큰 데이터 매트릭스와 함께 사용하는 지연을 피하기 위해 힙 정렬을 선호합니다.


1
평균적으로 O (logn) 메모리 만 필요합니다. 재귀 오버 헤드는 사소한 것입니다. 피벗에 운이 좋지 않다고 가정하면 걱정해야 할 더 큰 문제가 있습니다.
안티몬

-1

원래 질문에 답하고 다른 의견을 여기에서 해결하려면 :

방금 선택, 퀵, 병합 및 힙 정렬의 구현을 비교하여 서로 겹쳐지는 방식을 확인했습니다. 대답은 모두 단점이 있다는 것입니다.

요약 : Quick은 최고의 범용 정렬 (합리적으로 빠르고 안정적이며 대부분 제자리에 있음)입니다. 개인적으로 안정적인 정렬이 필요하지 않는 한 힙 정렬을 선호합니다.

선택-N ^ 2-20 개 미만의 요소에만 적합하며 성능이 뛰어납니다. 데이터가 이미 정렬되어 있지 않거나 거의 거의 정렬되지 않은 경우. N ^ 2는 정말 빠르게 느려집니다.

내 경험상 빠르다 는 것은 실제로 항상 그렇게 빠르지 는 않습니다 . 빠른 정렬을 일반 정렬로 사용하는 경우 보너스는 합리적으로 빠르고 안정적이라는 것입니다. 또한 내부 알고리즘이지만 일반적으로 재귀 적으로 구현되므로 추가 스택 공간을 차지합니다. 또한 O (n log n)과 O (n ^ 2) 사이에 있습니다. 어떤 종류의 타이밍은 특히 값이 좁은 범위 내에있을 때이를 확인하는 것 같습니다. 10,000,000 개 항목에 대한 선택 정렬보다 빠르지 만 병합 또는 힙보다 느립니다.

병합 정렬은 데이터에 종속되지 않으므로 O (n log n)가 보장됩니다. 당신이 어떤 가치를 부여했는지에 상관없이 그것은 단지 그것이하는 일을합니다. 또한 안정적이지만 구현에주의하지 않으면 매우 큰 정렬이 스택을 날려 버릴 수 있습니다. 복잡한 내부 병합 정렬 구현이 있지만 일반적으로 값을 병합하려면 각 수준에 다른 배열이 필요합니다. 이러한 어레이가 스택에 있으면 문제가 발생할 수 있습니다.

힙 정렬은 최대 O (n log n)이지만 대부분의 경우 값을 log n 딥 힙 위로 얼마나 멀리 이동해야하는지에 따라 더 빠릅니다. 힙은 원래 배열에서 쉽게 제자리에 구현할 수 있으므로 추가 메모리가 필요하지 않고 반복적이므로 반복되는 동안 스택 오버플로에 대해 걱정할 필요가 없습니다. 힙 정렬 의 단점은 안정적인 정렬이 아니라는 것입니다. 즉, 필요한 경우 바로 사용할 수 있습니다.


빠른 정렬은 안정적인 정렬이 아닙니다. 그 외에도 이러한 성격의 질문은 의견 기반 응답을 장려하고 전쟁과 논쟁을 편집 할 수 있습니다. 의견 기반 답변을 요구하는 질문은 SO 지침에 의해 명시 적으로 권장되지 않습니다. 답변자는 상당한 경험과 지혜가 있어도 답변하려는 유혹을 피해야합니다. 닫을 수 있도록 플래그를 지정하거나 평판이 충분한 사람이 플래그를 지정하고 닫을 때까지 기다리십시오. 이 의견은 귀하의 지식이나 답변의 타당성에 대한 반영이 아닙니다.
MikeC 2016
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.