Quicksort : 피벗 선택


109

Quicksort를 구현할 때해야 할 일 중 하나는 피벗을 선택하는 것입니다. 그러나 아래와 같은 의사 코드를 보면 피벗을 어떻게 선택해야하는지 명확하지 않습니다. 목록의 첫 번째 요소? 다른 것?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

누군가가 피벗 선택의 개념과 다른 시나리오가 다른 전략을 요구하는지 여부를 이해하도록 도와 줄 수 있습니까?


답변:


87

무작위 피벗을 선택하면 최악의 O (n 2 ) 성능 이 발생할 가능성이 최소화 됩니다 (항상 첫 번째 또는 마지막을 선택하면 거의 정렬되거나 거의 역 정렬 된 데이터에 대해 최악의 성능이 발생 함). 대부분의 경우 중간 요소를 선택하는 것도 허용됩니다.

또한이를 직접 구현하는 경우 제자리에서 작동하는 알고리즘 버전이 있습니다 (즉, 두 개의 새 목록을 만든 다음 연결하지 않고).


10
나는 검색을 직접 구현하는 것이 노력의 가치가 없을 수도 있다는 개념을 두 번째로 생각합니다. 또한 난수 생성기는 가끔 느리기 때문에 난수를 선택하는 방법에주의하십시오.
PeterAllenWebb

@Jonathan Leffler의 대답이 더 낫습니다
Nathan

60

요구 사항에 따라 다릅니다. 피벗을 무작위로 선택하면 O (N ^ 2) 성능을 생성하는 데이터 세트를 생성하기가 더 어려워집니다. '중앙값'(첫 번째, 마지막, 중간)도 문제를 피하는 방법입니다. 하지만 상대적인 비교 성능에주의하십시오. 비교 비용이 많이 드는 경우 Mo3는 무작위로 선택하는 것보다 더 많은 비교를 수행합니다 (단일 피벗 값). 데이터베이스 레코드는 비교하는 데 많은 비용이들 수 있습니다.


업데이트 : 댓글을 답변으로 가져옵니다.

mdkess 주장 :

'중앙값 3'은 첫 번째 마지막 중간이 아닙니다. 세 개의 임의 인덱스를 선택하고이 값의 중간 값을 가져옵니다. 요점은 피벗 선택이 결정적이지 않은지 확인하는 것입니다. 그렇다면 최악의 경우 데이터가 매우 쉽게 생성 될 수 있습니다.

내가 응답 한 내용 :

  • P Kirschenhofer, H Prodinger, C Martínez 의 Hoare의 Find Algorithm With Median-Of-Three Partition (1997)은 귀하의 경합을 지원합니다 ( 'median-of-three'는 무작위 항목 3 개임).

  • The Computer Journal, Vol 27, No 3, 1984에 게재 된 Hannu Erkiö의 'The Worst Case Permutation for Median-of-Three Quicksort'에 대한 기사가 portal.acm.org에 설명되어 있습니다 . [Update 2012-02- 26 : 기사 의 텍스트를 얻었습니다 . 섹션 2 '알고리즘'은 다음과 같이 시작합니다. ' A [L : R]의 첫 번째, 중간 및 마지막 요소의 중앙값을 사용하면 대부분의 실제 상황에서 상당히 동일한 크기의 부분으로 효율적으로 분할 할 수 있습니다. '따라서 첫 번째 중간 마지막 Mo3 접근 방식을 논의하고 있습니다.]

  • 흥미로운 또 다른 짧은 기사는 MD McIlroy의 "A Killer Adversary for Quicksort" 이며 Software-Practice and Experience, Vol. 29 (0), 1–4 (0 1999). 거의 모든 Quicksort가 2 차적으로 작동하도록하는 방법을 설명합니다.

  • AT & T Bell Labs Tech Journal, 1984 년 10 월 "작업 정렬 루틴 구축의 이론 및 실습"은 "Hoare는 무작위로 선택된 여러 라인의 중앙값을 기준으로 분할을 제안했습니다. Sedgewick [...]은 첫 번째 [. ..] 마지막 [...] 및 중간 ". 이것은 '중위수'에 대한 두 가지 기술이 문헌에 알려져 있음을 나타냅니다. (2014-11-23 업데이트 :이 기사는 IEEE Xplore 또는 Wiley 에서 볼 수있는 것으로 보입니다. 멤버십이 있거나 요금을 지불 할 준비가되어있는 경우)

  • 1993 년 11 월 Software Practice and Experience Vol 23 (11)에 게시 된 JL Bentley와 MD McIlroy의 'Engineering a Sort Function' 은이 문제에 대한 광범위한 논의를 다루며, 부분적으로는 적응 형 파티셔닝 알고리즘을 선택했습니다. 데이터 세트의 크기. 다양한 접근 방식에 대한 장단점에 대해 많은 논의가 있습니다.

  • '중간 값 3'에 대한 Google 검색은 추가 추적에 매우 적합합니다.

정보 주셔서 감사합니다; 나는 결정 론적 '3 중위수'를 전에 만났었다.


4
3의 중앙값은 첫 번째 마지막 중간이 아닙니다. 세 개의 임의 인덱스를 선택하고이 값의 중간 값을 가져옵니다. 요점은 피벗 선택이 결정적이지 않은지 확인하는 것입니다. 그렇다면 최악의 경우 데이터가 매우 쉽게 생성 될 수 있습니다.
mindvirus

나는 quicksort와 heapsort의 좋은 기능을 결합한 abt introsort를 읽고있었습니다. 중앙값 3을 사용하여 피벗을 선택하는 방법이 항상 좋은 것은 아닙니다.
Sumit Kumar Saha 2013

4
랜덤 인덱스 선택의 문제점은 난수 생성기가 상당히 비싸다는 것입니다. 정렬의 큰 비용을 증가 시키지는 않지만 첫 번째, 마지막 및 중간 요소를 방금 선택한 경우보다 작업 속도가 느려질 것입니다. (현실 세계에서, 나는 아무도 당신의 빠른 정렬을 천천히 인위적인 상황을하지 않습니다 내기.)
케빈 첸

20

헤, 방금이 수업을 가르쳤어요.

몇 가지 옵션이 있습니다.
단순 : 범위의 첫 번째 또는 마지막 요소를 선택합니다. (부분적으로 정렬 된 입력에 좋지 않음) 더 좋음 : 범위 중간에있는 항목을 선택합니다. (부분적으로 정렬 된 입력에 더 적합)

그러나 임의의 요소를 선택하면 크기 n의 배열을 크기 1과 n-1의 두 배열로 잘못 분할 할 위험이 있습니다. 그렇게 자주하면 퀵소트가 O (n ^ 2)가 될 위험이 있습니다.

내가 본 개선점 중 하나는 중앙값 선택 (첫 번째, 마지막, 중간); 최악의 경우에는 여전히 O (n ^ 2)로 갈 수 있지만 확률 적으로 이것은 드문 경우입니다.

대부분의 데이터에서 첫 번째 또는 마지막을 선택하는 것으로 충분합니다. 그러나 최악의 시나리오 (부분적으로 정렬 된 입력)가 자주 발생하는 경우 첫 번째 옵션은 중앙 값을 선택하는 것입니다 (부분적으로 정렬 된 데이터에 대해 통계적으로 좋은 피벗).

여전히 문제가 발생하면 중앙 경로로 이동하십시오.


1
우리는 수업에서 정렬 된 순서로 배열에서 가장 작은 k 개의 요소를 가져 오는 실험을했습니다. 무작위 배열을 생성 한 다음 최소 힙 또는 무작위 선택 및 고정 피벗 퀵소트를 사용하고 비교 횟수를 계산했습니다. 이 "무작위"데이터에서 두 번째 솔루션은 첫 번째 솔루션보다 평균적으로 더 나빴습니다. 무작위 피벗으로 전환하면 성능 문제가 해결됩니다. 따라서 임의의 데이터에 대해서도 고정 피벗은 무작위 피벗보다 성능이 훨씬 떨어집니다.
Robert S. Barnes

크기 n의 배열을 크기 1과 n-1의 두 배열로 분할하면 왜 O (n ^ 2)가 될 위험이 있습니까?
Aaron Franke 19 년

크기가 N 인 배열을 가정합니다. 크기를 [1, N-1]로 분할합니다. 다음 단계는 오른쪽 절반을 [1, N-2]로 분할하는 것입니다. 그래서 우리는 크기가 1 인 N 개의 파티션을 가질 때까지 계속합니다. 그러나 만약 우리가 반으로 분할한다면, 우리는 각 단계마다 N / 2의 2 개의 파티션을 수행 할 것이고, 이는 복잡성의 Log (n) 항으로 이어집니다.
Chris Cudmore

11

고정 피벗을 선택하지 마십시오. 알고리즘의 최악의 경우 O (n ^ 2) 런타임을 악용하기 위해 공격을받을 수 있습니다. Quicksort의 최악의 경우 런타임은 파티셔닝 결과 1 요소의 배열 하나와 n-1 요소의 배열 하나가 될 때 발생합니다. 파티션으로 첫 번째 요소를 선택한다고 가정합니다. 누군가가 내림차순으로 알고리즘에 배열을 공급하면 첫 번째 피벗이 가장 크므로 배열의 다른 모든 항목이 왼쪽으로 이동합니다. 그런 다음 재귀하면 첫 번째 요소가 다시 가장 커지므로 다시 한 번 모든 요소를 ​​왼쪽에 배치하는 식입니다.

더 나은 기법은 3 개의 요소 중위수를 무작위로 선택하고 중간을 선택하는 방법입니다. 선택한 요소가 첫 번째 또는 마지막 요소가 아니라는 것을 알고 있습니다. 중앙 극한 정리에 따르면 중간 요소의 분포는 정상이 될 것입니다. 즉, 중간으로 향하는 경향이 있음을 의미합니다. , n lg n 시간).

알고리즘에 대해 O (nlgn) 런타임을 절대적으로 보장하려면 배열의 중앙값을 찾는 5 열 방법이 O (n) 시간에 실행됩니다. 즉, 최악의 경우 빠른 정렬을위한 반복 방정식이 be T (n) = O (n) (중앙값 찾기) + O (n) (분할) + 2T (n / 2) (왼쪽과 오른쪽으로 반복) 마스터 정리에 따르면 이것은 O (n lg n)입니다. . 그러나 상수 요소는 엄청날 것이며 최악의 경우 성능이 주요 관심사라면 병합 정렬을 대신 사용하십시오. 이는 평균적으로 퀵 정렬보다 약간 느리고 O (nlgn) 시간을 보장합니다 (훨씬 빠를 것입니다). 이 절름발이 중앙값 빠른 정렬보다).

중앙값 알고리즘의 중앙값 설명


6

너무 영리 해 지거나 피벗 전략을 결합하려고하지 마십시오. 첫 번째, 마지막 및 중간의 임의 인덱스의 중앙값을 선택하여 3의 중앙값을 임의의 피벗과 결합한 경우 3 차 중앙값을 보내는 많은 분포에 여전히 취약합니다 (실제로는 일반 무작위 피벗)

예를 들어 파이프 오르간 분포 (1,2,3 ... N / 2..3,2,1) 첫 번째와 마지막은 모두 1이되고 랜덤 인덱스는 1보다 큰 숫자가 될 것입니다. 중앙값을 취하면 1 ( 첫 번째 또는 마지막) 그리고 완전히 불균형 한 파티셔닝이 발생합니다.


2

이렇게하면 퀵소트를 세 부분으로 나누는 것이 더 쉽습니다.

  1. 데이터 요소 교환 또는 스왑 기능
  2. 파티션 기능
  3. 파티션 처리

하나의 긴 함수보다 약간 더 비효율적이지만 이해하기가 훨씬 쉽습니다.

코드는 다음과 같습니다.

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y){  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
};


/* This is the partition code */

int partition (DATATYPE list[], int l, int h){

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h]) {                 // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    }
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
};



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h){

  int p;                                      // index of partition 
  if ((h - l) > 0) {
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  };
};

1

시작하기 위해 데이터가 정렬되는 방식에 전적으로 의존합니다. 의사 랜덤이라고 생각되면 가장 좋은 방법은 무작위 선택을 선택하거나 중간을 선택하는 것입니다.


1

무작위로 액세스 할 수있는 컬렉션 (예 : 배열)을 정렬하는 경우 일반적으로 물리적 중간 항목을 선택하는 것이 가장 좋습니다. 이를 통해 배열이 모두 준비 (또는 거의 정렬) 된 경우 두 파티션이 짝수에 가까워지고 최상의 속도를 얻을 수 있습니다.

선형 액세스 만있는 항목 (예 : 연결 목록)을 정렬하는 경우 액세스하기 가장 빠른 항목이기 때문에 첫 번째 항목을 선택하는 것이 가장 좋습니다. 그러나 여기에서 목록이 이미 정렬 된 경우에는 문제가 발생합니다. 한 파티션은 항상 null이고 다른 파티션에는 모든 것이있어 최악의 시간을 생성합니다.

그러나 연결 목록의 경우 첫 번째 항목 이외의 항목을 선택하면 문제가 더 악화됩니다. 나열된 목록에서 중간 항목을 선택하면 각 파티션 단계에서 단계를 거쳐야합니다 .-- 총 시간 O (1.5 N * log N)를 만드는 logN 번 수행되는 O (N / 2) 작업 추가 그리고 그것은 우리가 시작하기 전에 목록이 얼마나 오래 있는지 안다면-일반적으로 우리는 그들을 계산하기 위해 끝까지 한 단계 씩 밟아야 할 것입니다. 그리고 중간을 찾기 위해 중간을 밟은 다음, 세 번째로 실제 파티션 수행 : O (2.5N * log N)


0

이상적으로 피벗은 전체 배열의 중간 값이어야합니다. 이렇게하면 최악의 성능을 얻을 가능성이 줄어 듭니다.


1
여기 말 앞의 카트.
ncmathsadist

0

빠른 정렬의 복잡성은 피벗 값 선택에 따라 크게 달라집니다. 예를 들어 항상 첫 번째 요소를 피벗으로 선택하면 알고리즘의 복잡성이 O (n ^ 2)만큼 최악이됩니다. 여기 피벗 요소를 선택하는 현명한 방법이 있습니다. 1. 배열의 첫 번째, 중간, 마지막 요소를 선택합니다. 2.이 세 숫자를 비교하여 1보다 크고 다른 숫자, 즉 중앙값보다 작은 숫자를 찾으십시오. 3.이 요소를 피벗 요소로 만듭니다.

이 방법으로 피벗을 선택하면 배열이 거의 절반으로 분할되므로 복잡성이 O (nlog (n))로 줄어 듭니다.


0

평균적으로 중앙값 3은 작은 n에 적합합니다. 중앙값 5는 n이 클수록 약간 좋습니다. 아홉 번째, 즉 "3 중 3의 중앙값"은 매우 큰 n의 경우 더 좋습니다.

샘플링을 더 많이할수록 n이 증가함에 따라 더 나은 결과를 얻을 수 있지만 샘플을 늘리면 개선 속도가 크게 느려집니다. 또한 샘플 샘플링 및 정렬에 따른 오버 헤드가 발생합니다.


0

쉽게 계산할 수있는 중간 인덱스를 사용하는 것이 좋습니다.

반올림하여 계산할 수 있습니다 (array.length / 2).


-1

진정으로 최적화 된 구현에서 피벗을 선택하는 방법은 어레이 크기에 따라 달라져야합니다. 대형 어레이의 경우 좋은 피벗을 선택하는 데 더 많은 시간을 할애하는 것이 좋습니다. 전체 분석을 수행하지 않고 "O (log (n)) 요소의 중간"이 좋은 시작이라고 생각합니다. 이것은 추가 메모리가 필요하지 않다는 추가 보너스가 있습니다. 더 큰 파티션에서 꼬리 호출을 사용하여 파티셔닝을 배치하면 알고리즘의 거의 모든 단계에서 동일한 O (log (n)) 추가 메모리를 사용합니다.


1
3 가지 요소의 중간을 찾는 것은 일정한 시간에 이루어질 수 있습니다. 더 이상 기본적으로 하위 배열을 정렬해야합니다. n이 커지면 다시 정렬 문제로 돌아갑니다.
Chris Cudmore
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.