유니버설 정렬. Hoare 방식 - 퀵 정렬

프로그래밍하고 코드가 계산기를 작성하는 것 이상이라면 이 또는 저 데이터 배열을 정렬해야 하는 필요성을 여러 번 접했거나 경험했을 것입니다. 정렬하는 방법에는 여러 가지가 있습니다. 이 기사에서는 주요 항목을 분석하고 빠른 정렬에 중점을 둘 것입니다.

퀵 정렬의 개념

빠른 정렬 - 빠른 정렬 또는 qsort. 이름을 보면 그것이 무엇인지, 왜 그런지 명확해집니다. 하지만 명확하지 않다면 이것은 배열을 빠르게 정렬하는 알고리즘입니다. 이 알고리즘의 평균 효율성은 O(n log n)입니다. 무슨 뜻이에요? 이는 알고리즘의 평균 실행 시간이 이 함수의 그래프와 동일한 궤적을 따라 증가한다는 것을 의미합니다. 일부에서는 인기있는 언어이 알고리즘이 내장된 라이브러리가 있으며 이는 이미 이 알고리즘이 매우 효과적이라는 것을 시사합니다. Java, C++, C#과 같은 언어가 있습니다.

연산

퀵 정렬 방법은 재귀와 분할 정복 전략을 사용합니다.

1. 특정 참조 요소가 배열에서 검색됩니다. 단순화를 위해 중앙 요소를 선택하는 것이 좋지만 최적화 작업을 수행하려면 다른 옵션을 시도해야 합니다.

2. 지지대의 왼쪽에서 지지대보다 큰 요소를 찾고 오른쪽에서 지지대보다 작은 요소를 찾은 다음 교체합니다. 오른쪽의 최대값이 왼쪽의 최소값보다 작아질 때까지 이 작업을 수행합니다. 따라서 우리는 작은 요소를 모두 처음에 던지고 큰 요소는 끝에 던집니다.

3. 우리는 이 알고리즘을 알고리즘의 왼쪽과 오른쪽 부분에 개별적으로 반복적으로 적용한 다음 하나의 요소 또는 특정 수의 요소에 도달할 때까지 반복해서 적용합니다. 이 요소 수는 얼마입니까? 이 알고리즘을 최적화하는 또 다른 방법이 있습니다. 정렬된 부분이 대략 8 또는 16이 되면 버블 정렬과 같은 기존 정렬을 통해 처리할 수 있습니다. 이렇게 하면 알고리즘의 효율성이 높아집니다. 우리가 원하는 만큼 빠르게 작은 배열을 처리하지 않습니다.

이렇게 하면 전체 배열이 처리되고 정렬됩니다. 이제 이 알고리즘을 명확하게 연구해 봅시다.

퀵소트의 효율성

퀵소트(quicksort)가 가장 많나요? 빠른 알고리즘정렬? 기필코 아니다. 이제 점점 더 많은 정렬이 나타나고 있으며 현재 가장 빠른 정렬은 Timsort이며 처음에 다르게 정렬된 배열에 대해 매우 빠르게 작동합니다. 그러나 빠른 정렬 방법은 작성하기 가장 쉬운 방법 중 하나라는 점을 잊지 마십시오. 일반적으로 일반 프로젝트의 경우 작성할 수 없는 거대한 알고리즘이 아니라 간단한 작성만 필요하기 때문입니다. 당신 자신. Timsort는 또한 가장 복잡한 알고리즘은 아니지만 가장 단순한 알고리즘이라는 타이틀을 얻지는 못할 것입니다.

알고리즘 구현

글쎄, 우리는 "맛있는"부분에 도달했습니다. 이제 이 알고리즘이 어떻게 구현되는지 살펴보겠습니다. 앞서 언급했듯이 구현하기가 그리 어렵지 않고 오히려 간단합니다. 그러나 빠른 정렬이 어떻게 작동하는지 이해할 수 있도록 코드의 각 동작을 완전히 분석할 것입니다.

우리의 방법은 QuickSort라고 불립니다. 이는 배열, 첫 번째 및 마지막 요소를 전달하는 기본 알고리즘을 실행합니다. 정렬된 세그먼트의 첫 번째 요소와 마지막 요소를 i 및 k 변수에 저장하여 변수가 필요하므로 변경하지 않도록 합니다. 그런 다음 첫 번째 검사와 마지막 검사 사이의 거리를 확인합니다. 이 거리가 1보다 크거나 같은가요? 그렇지 않다면 중앙에 도달한 것이므로 이 세그먼트의 정렬을 종료해야 하며, 그렇다면 정렬을 계속합니다.

그런 다음 정렬되는 세그먼트의 첫 번째 요소를 지원 요소로 사용합니다. 우리는 중심에 도달할 때까지 다음 사이클을 수행합니다. 그 안에서 우리는 두 개의 사이클을 더 만듭니다. 첫 번째는 왼쪽 부분이고 두 번째는 오른쪽입니다. 조건을 충족하는 요소가 있는 한, 또는 지원 요소에 도달할 때까지 이를 수행합니다. 그런 다음 최소 요소가 여전히 오른쪽에 있고 최대 요소가 왼쪽에 있으면 이를 교환합니다. 주기가 끝나면 첫 번째 요소와 참조 요소가 더 작은 경우 참조 요소를 변경합니다. 그런 다음 배열의 오른쪽과 왼쪽 섹션에 대해 알고리즘을 재귀적으로 수행하고 섹션 1 요소 길이에 도달할 때까지 계속합니다. 그런 다음 모든 재귀 알고리즘이 반환되고 정렬이 완전히 종료됩니다. 또한 하단에는 교체 방법으로 배열을 정렬하는 완전히 표준적인 방법인 스왑 방법이 있습니다. 요소 교체를 여러 번 작성하지 않기 위해 한 번 작성하고 이 배열의 요소를 변경합니다.

결론적으로, 품질-복잡도 비율 측면에서 보면 모든 알고리즘 중에서 퀵 정렬이 가장 앞서 있다고 할 수 있으므로 반드시 이 방법을 참고하고 프로젝트에 필요하다면 활용해야 합니다.

지금까지 내 블로그에서 정렬에 관한 유일한 출판물은 다음과 같습니다. 이제 문제를 해결할 시간입니다! 모든 유형의 정렬을 정복하기 위해 거창한 계획을 세우지는 않겠지만 가장 인기 있는 것 중 하나인 빠른 정렬부터 시작하겠습니다.

나는 "빠르다"는 것은 단지 이름뿐이며 이제는 많은 유사점이 있으며 일반적으로 각 데이터 유형에는 자체 정렬이 필요하다고 말하는 첫 번째도 마지막도 아닐 것입니다. 그래, 다 사실이야. 하지만 이 진실은 변하지 않아 단순한 사실, 무엇 퀵 정렬을 직접 구현하면 전반적인 프로그래밍 기술이 향상됩니다. , 그리고 그것은 항상 대학 과정에 있을 것이라고 확신합니다. 같은 이유로 구현을 위한 프로그래밍 언어를 선택했습니다. 왜냐하면 C/C++에서는 포인터를 사용하여 즉시 연습할 수 있기 때문입니다.

나는 사업을 시작하고 먼저 알고리즘의 본질을 간략하게 고려할 것을 제안합니다.

빠른 정렬 작동 방식

알고리즘 다이어그램은 다음과 같이 설명할 수 있습니다.

  1. 선택하다 지원 배열의 요소 - 중앙 요소가 있는 변형이 종종 발견됩니다.
  2. 배열을 다음과 같이 나눕니다. 두 부분 다음과 같습니다: 왼쪽 부품 보다 크거나 같음 지지하다, 던지다 오른쪽 , 마찬가지로, 오른쪽 , 어느 작거나 같음 우리는 그것을 지지대에 던졌습니다 왼쪽 부분.
  3. 이전 단계의 결과로 중앙 요소보다 작거나 같은 요소는 배열의 왼쪽에 남고 오른쪽보다 크거나 같은 요소는 남습니다.
    이는 다음과 같이 명확하게 표시될 수 있습니다.
    |———————|—————————|———————|
    | 마스[나]<= mid | mid = mas | mas[i] >= 중간 |
    |———————-|—————————|———————|
  4. 배열의 왼쪽과 오른쪽 부분에 대해 작업을 반복적으로 반복합니다.

재귀 항목은 두 부분의 크기가 1보다 작거나 같은 순간에 중지됩니다.

알고리즘의 한 단계에 대한 그림을 빌렸는데, 매우 명확합니다.

퀵 정렬의 재귀적 구현

이 함수는 배열 자체(시작 부분에 대한 포인터)와 크기를 입력으로 사용합니다.

Void qsortRecursive(int *mas, int size) ( //배열의 시작과 끝을 가리키는 포인터 int i = 0; int j = size - 1; //배열의 중앙 요소 int mid = mas; //나누기 array do ( // 요소를 살펴보며 다른 부분으로 전송해야 하는 요소를 찾습니다. // 배열의 왼쪽에서 중앙 요소보다 작은 요소를 건너뜁니다(제자리에 둡니다) while(mas[ 나]< mid) { i++; } //В правой части пропускаем элементы, которые больше центрального while(mas[j] >mid) ( j--; ) //요소 교체 if (i<= j) { int tmp = mas[i]; mas[i] = mas[j]; mas[j] = tmp; i++; j--; } } while (i <= j); //Рекурсивные вызовы, если осталось, что сортировать if(j >0) ( //"왼쪽 조각" qsortRecursive(mas, j + 1); ) if (i< size) { //"Првый кусок" qsortRecursive(&mas[i], size - i); } }

결론

이 작업은 동시에 재귀의 작동 방식을 이해하는 데 도움이 되며 알고리즘 실행 중 데이터 변경 사항을 추적하는 방법을 알려줍니다. "계속해서" 직접 작성하는 것이 좋습니다. 하지만 여전히 여기에 제가 구현한 내용이 있으므로 제 자신도 유용할 것 같습니다. 그게 전부입니다. 관심을 가져주셔서 감사합니다!

영형( N) 보조
오(로그 N) 보조 (Sedgwick 1978)

빠른 정렬, 호어 정렬(영어 퀵소트), 종종 호출됨 qsort(C 표준 라이브러리에 이름 있음)은 1960년 영국의 컴퓨터 과학자 Charles Hoare가 모스크바 주립 대학에서 연구하는 동안 개발한 잘 알려진 정렬 알고리즘입니다.

연산퀵소트(A, lo, hi) ~이다 만약에봐라< hi 그 다음에 p:= 파티션(A, lo, hi) 퀵정렬(A, lo, p – 1) 퀵정렬(A, p + 1, hi) 연산 partition(A, lo, hi) ~이다피벗:= A i:= lo - 1 ~을 위한 j:=lo 에게안녕 - 1 하다 만약에 A[j] ≤ 피벗 그 다음에 i:= i + 1 A[i]를 A[j]로 교환 A를 A로 교환 반품나+1

전체 배열 정렬은 Quicksort(A, 1, length(A)) 를 수행하여 수행할 수 있습니다.

호어 파티션

이 구성표는 두 개의 인덱스(하나는 배열의 시작 부분에, 다른 하나는 끝 부분에 있음)를 사용합니다. 이 인덱스는 하나가 참조보다 크고 그 앞에 위치하며 두 번째는 더 작은 요소 쌍이 있을 때까지 서로 접근합니다. 그리고 그 뒤에 위치합니다. 이러한 요소는 교체됩니다. 교환은 인덱스가 교차할 때까지 발생합니다. 알고리즘은 마지막 인덱스를 반환합니다. . Hoare 체계는 Lomuto 체계보다 더 효율적입니다. 왜냐하면 평균적으로 요소 교환 횟수가 3배 적고 모든 요소가 동일한 경우에도 분할이 더 효율적이기 때문입니다. Lomuto 계획과 유사하게 이 계획은 다음과 같은 측면에서도 효율성을 보여줍니다. 영형(N 2) 입력 배열이 이미 정렬되어 있는 경우. 이 체계를 사용한 정렬은 불안정합니다. 참조 요소의 끝 위치가 반환된 인덱스와 반드시 동일할 필요는 없습니다. 유사 코드:

연산퀵소트(A, lo, hi) ~이다 만약에봐라< hi 그 다음에 p:= 파티션(A, lo, hi) 퀵정렬(A, lo, p) 퀵정렬(A, p + 1, hi) 연산 partition(A, lo, hi) ~이다피벗:= A i:= lo - 1 j:= hi + 1 영원히 반복하다 하다나는:= 나는 + 1 ~하는 동안일체 포함]< pivot 하다 j:= j - 1 ~하는 동안 A[j] > 피벗 만약에나는 >= j 그 다음에 반품 j A[i]를 A[j]로 교환

반복되는 요소

다음과 같은 경우 성능을 향상하려면 대량배열의 동일한 요소가 있는 경우 배열을 세 그룹으로 나누는 절차를 적용할 수 있습니다. 즉, 참조 항목보다 작은 요소, 동일하거나 큰 요소입니다. (Bentley와 McIlroy는 이것을 "팻 파티션"이라고 부릅니다. 이 파티션은 함수에서 사용됩니다. qsort일곱 번째 버전의 유닉스에서. ). 유사 코드:

연산퀵소트(A, lo, hi) ~이다 만약에봐라< hi 그 다음에 p:= 피벗(A, lo, hi) 왼쪽, 오른쪽:= partition(A, p, lo, hi) // 두 개의 값을 반환합니다.퀵소트(A, lo, 왼쪽) 퀵소트(A, 오른쪽, 안녕)

알고리즘 복잡성 평가

참조 요소를 기준으로 배열을 두 부분으로 나누는 작업에는 시간이 걸리는 것이 분명합니다. 동일한 재귀 깊이에서 수행되는 모든 분할 작업은 크기가 일정한 원래 배열의 다른 부분을 처리하므로 각 재귀 수준에서 전체적으로 필요합니다. O(n) (\displaystyle O(n))운영. 결과적으로 알고리즘의 전체 복잡도는 분할 수, 즉 재귀 깊이에 의해서만 결정됩니다. 반복의 깊이는 입력 데이터의 조합과 참조 요소가 정의되는 방식에 따라 달라집니다.

가장 좋은 경우입니다. 가장 균형 잡힌 버전에서는 각 분할 작업을 통해 배열이 두 개의 동일한(+ 또는 - 1 요소) 부분으로 분할되므로 처리된 하위 배열의 크기가 1에 도달하는 최대 재귀 깊이는 다음과 같습니다. 로그 2 ⁡ n (\displaystyle \log _(2)n). 결과적으로 퀵 정렬의 비교 횟수는 재귀 표현식의 값과 같습니다. C n = 2 ⋅ C n / 2 + n (\displaystyle C_(n)=2\cdot C_(n/2)+n)이는 알고리즘의 전반적인 복잡성을 제공합니다. O (n ⋅ log 2 ⁡ n) (\displaystyle O(n\cdot \log _(2)n)). 평균. 입력 데이터의 무작위 분포에 따른 평균 복잡도는 확률적으로만 추정할 수 있습니다. 우선, 실제로는 피벗 요소가 배열을 매번 두 개로 나눌 필요가 없다는 점에 유의해야 합니다. 동일한부속. 예를 들어, 각 단계에서 원본의 75% 길이와 25% 길이의 배열로 분할된 경우 재귀 깊이는 와 같게 되어 여전히 복잡해집니다. 일반적으로 어떤 경우에는 결정된좌파와 좌파의 관계 오른쪽분리하면 알고리즘의 복잡성은 동일하지만 상수만 다릅니다. 우리는 지지 요소가 배열의 분할된 부분 요소의 중앙 50%에 속하는 분할을 "성공적인" 분할로 간주합니다. 분명히 요소가 무작위로 분포될 때 행운이 발생할 확률은 0.5입니다. 분할이 성공하면 할당된 하위 배열의 크기는 원본의 25% 이상 75% 이하가 됩니다. 할당된 각 하위 배열에는 무작위 분포, 이러한 모든 인수는 정렬 단계와 배열의 초기 조각에 적용됩니다. 성공적인 분할은 다음 이하의 재귀 깊이를 제공합니다. 로그 4 / 3 ⁡ n (\displaystyle \log _(4/3)n). 행운의 확률은 0.5이므로, k (\표시스타일 k)평균적으로 성공적인 분할이 필요합니다. 2 ⋅ k (\displaystyle 2\cdot k)참조 요소에 대한 재귀 호출 케이한때 자신이 배열의 중앙 50%에 속했음을 발견했습니다. 이러한 고려 사항을 적용하면 평균적으로 재귀 깊이가 다음을 초과하지 않는다는 결론을 내릴 수 있습니다. 2 ⋅ 로그 4 / 3 ⁡ n (\displaystyle 2\cdot \log _(4/3)n), 이는 동일하다 O (log ⁡ n) (\displaystyle O(\log n))그리고 각 재귀 수준에는 여전히 O(n) (\displaystyle O(n))작업의 평균 복잡성은 다음과 같습니다. O (n log ⁡ n) (\displaystyle O(n\log n)). 최악의 경우. 가장 불균형한 버전에서 각 분할은 크기가 1과 2인 하위 배열 2개를 생성합니다. 즉, 각 재귀 호출에서 더 큰 배열은 이전 시간보다 1이 더 짧아집니다. 이는 처리된 모든 요소 중 가장 작거나 가장 큰 요소가 각 단계에서 참조 요소로 선택된 경우 발생할 수 있습니다. 참조 요소(배열의 첫 번째 또는 마지막 요소)를 가장 간단하게 선택하면 이러한 효과는 중간 또는 기타 고정 요소에 대해 이미 정렬된(정방향 또는 역순) 배열인 "최악의 경우" 배열에 의해 제공됩니다. ”을 특별히 선택할 수도 있습니다. 이 경우에는 다음이 필요합니다. n − 1 (\displaystyle n-1) 분리 작업 및 총 작업 시간은∑ i = 0 n (n − i) = O (n 2) (\displaystyle \textstyle \sum _(i=0)^(n)(n-i)=O(n^(2)))

즉, 정렬은 2차 시간으로 수행됩니다. 그러나 교환 횟수와 그에 따른 운영 시간이 가장 큰 단점은 아닙니다. 더 나쁜 것은 이 경우 알고리즘을 실행할 때 재귀 깊이가 n에 도달한다는 것입니다. 이는 배열 분할 절차의 반환 주소와 지역 변수를 n번 저장한다는 의미입니다. n 값이 큰 경우 최악의 경우 프로그램이 실행되는 동안 메모리가 고갈(스택 오버플로)될 수 있습니다.

장점과 단점

장점:

결점:

개량 알고리즘 개선은 주로 위에서 언급한 단점을 제거하거나 완화하는 것을 목표로 하며, 그 결과 모든 단점은 알고리즘을 견고하게 만들고, 참조 요소의 특별한 선택으로 인한 성능 저하를 제거하고, 보호하는 세 가지 그룹으로 나눌 수 있습니다. 호출 스택 오버플로로 인한 방지대단한 깊이

  • 입력 데이터가 실패하면 재귀합니다.
  • 중간 요소를 선택합니다. 사전 정렬된 데이터의 성능 저하를 제거하지만 "잘못된" 배열이 실수로 나타나거나 의도적으로 선택될 가능성은 그대로 둡니다.
  • 첫 번째, 중간, 마지막 세 요소의 중앙값을 선택합니다. 중간 요소를 선택하는 것에 비해 최악의 경우가 발생할 가능성이 줄어듭니다.
  • 무작위 선택. 우연히 최악의 경우가 발생할 확률은 점점 작아지고, 의도적인 선택은 사실상 불가능해진다. 정렬 알고리즘의 예상 실행 시간은 O( N LG N).
지원 요소를 선택하는 모든 복잡한 방법의 단점은 추가 오버헤드입니다. 그러나 그다지 훌륭하지는 않습니다.
  • 큰 재귀 깊이로 인한 프로그램 실패를 방지하려면 다음 방법을 사용할 수 있습니다.

의사코드.
QuickSort(array a, upperbound N) ( 피벗 요소 p 선택 - 배열의 중간 해당 요소에서 배열 분할 p의 왼쪽 하위 배열에 요소가 두 개 이상 포함되어 있으면 그에 대해 QuickSort를 호출합니다. p 오른쪽의 하위 배열이 있으면 요소가 두 개 이상 포함되어 있으면 QuickSort를 호출하세요.) C로 구현합니다.
주형 무효 QuickSortR(T* a, long N) ( // 입력은 배열 a이고, a[N]은 마지막 요소입니다. long i = 0, j = N-1; // 원래 위치에 포인터를 넣습니다. T 온도, p; p = a[ N>>1 ]; // 중심 요소 // 나누기 절차 do ( 동안 (a[i]< p) i++; while (a[j] >p)j--; 만약 내가<= j) { temp = a[i]; a[i] = a[j]; a[j] = temp; i++; j--; } } while (i<=j); // 재귀 호출, 정렬할 항목이 있는 경우 if (j > 0) QuickSortR(a, j); if (N > i) QuickSortR(a+i, N-i); )

각 분할에는 분명히 Theta(n) 연산이 필요합니다. 분할 단계 수(재귀 깊이)는 배열이 어느 정도 동일한 부분으로 분할되는 경우 대략 log n입니다. 따라서 전체 성능은 O(n log n)이며, 이는 실제로 발생합니다.

그러나 알고리즘이 O(n 2) 연산에서 작동하는 입력 데이터의 경우가 있을 수 있습니다. 이는 입력 시퀀스의 최대 또는 최소가 중심 요소로 선택될 때마다 발생합니다. 데이터를 무작위로 추출한 경우 확률은 2/n입니다. 그리고 이 확률은 모든 단계에서 실현되어야 합니다... 일반적으로 이것은 비현실적인 상황입니다.

방법이 불안정합니다. 부분 순서 지정이 배열을 더 동일한 부분으로 나눌 가능성을 높인다는 점을 고려하면 동작은 매우 자연스럽습니다.

대략적인 재귀 깊이가 O(log n)이고 재귀 하위 호출이 매번 스택에 푸시되기 때문에 정렬에는 추가 메모리가 사용됩니다.

코드 및 메소드 수정

    재귀 및 기타 오버헤드로 인해 Quicksort는 짧은 배열의 경우 그다지 빠르지 않을 수 있습니다. 따라서 배열에 CUTOFF 요소 수가 더 적으면(구현 종속 상수, 일반적으로 3~40개) 삽입 정렬이 호출됩니다. 속도 증가는 최대 15%까지 가능합니다.

    메서드를 구현하려면 마지막 2줄을 다음과 같이 바꿔서 QuickSortR 함수를 수정할 수 있습니다.

    If (j > CUTOFF) QuickSortR(a, j); if (N > i + CUTOFF) QuickSortR(a+i, N-i);

    따라서 CUTOFF 요소 이하의 배열은 정렬되지 않으며, QuickSortR()이 끝나면 배열은 다음과 같은 연속적인 부분으로 나뉩니다.<=CUTOFF элементов, отсортированные друг относительно друга. Близкие элементы имеют близкие позиции, поэтому, аналогично сортировке Шелла, вызывается insertSort(), которая доводит процесс до конца.

    주형 void qsortR(T *a, long size) ( QuickSortR(a, size-1); insertSort(a, size); // insertSortGuarded가 더 빠르지만 setmax() 함수가 필요합니다.}

  1. 명시적 재귀의 경우 위 프로그램처럼 하위 배열의 경계뿐 아니라 지역 변수 등 완전히 불필요한 매개변수도 다수 저장된다. 프로그래밍 방식으로 스택을 에뮬레이션하면 크기가 여러 번 줄어들 수 있습니다.
  2. 배열을 동일한 부분으로 더 많이 나눌수록 좋습니다. 따라서 3개의 평균을 참조로 사용하고 배열이 충분히 크면 9개의 임의 요소를 사용하는 것이 좋습니다.
  3. 입력 시퀀스가 ​​알고리즘에 매우 나쁘다고 가정합니다. 예를 들어, 중간 요소가 매번 최소가 되도록 특별히 선택됩니다. QuickSort가 그러한 "사보타주"에 저항하도록 만드는 방법은 무엇입니까? 매우 간단합니다. 입력 배열의 임의 요소를 참조로 선택합니다. 그러면 입력 스트림의 불쾌한 패턴이 모두 무력화됩니다. 또 다른 옵션은 정렬하기 전에 배열 요소를 무작위로 재배열하는 것입니다.
  4. 빠른 정렬은 이중 연결 목록에도 사용할 수 있습니다. 이것의 유일한 문제는 무작위 요소에 직접 접근할 수 없다는 것입니다. 따라서 첫 번째 요소를 참조로 선택하고 좋은 초기 데이터를 원하거나 정렬하기 전에 요소를 무작위로 재배열해야 합니다.

무작위로 선택된 지원 요소가 매우 나쁜 것으로 판명된 최악의 경우(극단에 가까움)를 고려해 보겠습니다. 이에 대한 확률은 매우 낮습니다. 이미 n = 1024에서는 2 -50 미만이므로 관심은 실제보다 이론적인 것입니다. 그러나 "quicksort" 동작은 유사하게 구현된 분할 정복 알고리즘에 대한 "벤치마크"입니다. 최악의 경우의 확률을 거의 0으로 줄이는 것이 모든 곳에서 가능한 것은 아니므로 이 상황은 연구할 가치가 있습니다.

명확성을 위해 매번 가장 작은 요소를 선택하도록 하겠습니다. 그런 다음 분할 절차는 이 요소를 배열의 시작 부분으로 이동하고 두 부분이 다음 재귀 수준으로 전송됩니다. 하나는 유일한 요소 a min 이고 다른 하나는 배열의 나머지 n-1 요소를 포함합니다. 그런 다음 (n-1) 요소의 일부에 대해 프로세스가 반복됩니다. 등등..
위와 같은 재귀 코드를 사용하면 이는 QuickSort에 대한 n개의 중첩 재귀 호출을 의미합니다.
각 재귀 호출은 현재 상황에 대한 정보를 저장하는 것을 의미합니다. 따라서 정렬에는 O(n) 추가 메모리가 필요합니다... 그리고 어디에서나 스택이 아니라 스택에 있습니다. n이 충분히 큰 경우 이러한 요구 사항은 예측할 수 없는 결과를 초래할 수 있습니다.

이러한 상황을 제거하려면 배열 기반 스택을 구현하여 재귀를 반복으로 대체할 수 있습니다. 분할 절차는 루프로 수행됩니다.
배열이 두 부분으로 나누어질 때마다 더 큰 부분을 정렬하라는 요청이 스택으로 전송되고, 다음 반복에서는 더 작은 부분이 처리됩니다. 분할 절차에서 현재 작업이 없어지면 쿼리가 스택에서 팝됩니다. 더 이상 요청이 없으면 정렬이 종료됩니다.

의사코드.
반복적 QuickSort(배열 a, 크기 크기) (스택의 배열을 0에서 크기-1로 정렬하도록 요청을 푸시합니다. do (스택에서 현재 배열의 경계 lb 및 ub를 팝합니다. do ( 1. 분할 수행 2. 결과 부분 중 더 큰 부분의 경계를 스택으로 보냅니다. 3. 더 작은 부분이 두 개 이상으로 구성되는 동안 ub, lb의 경계를 이동하여 더 작은 부분을 가리킵니다. 요소) 스택에 요청이 있는 동안) C로 구현.
#MAXSTACK 2048 정의 // 최대 스택 크기주형 void qSortI(T a, long size) ( long i, j; // 나눗셈에 관련된 포인터긴 lb, ub; // 루프에서 정렬되는 조각의 경계긴 lbstack, ubstack; // 요청 스택 // 각 요청은 한 쌍의 값으로 지정됩니다. // 즉, 왼쪽(lbstack) 및 오른쪽(ubstack) // 간격의 경계긴 스택포스 = 1; // 현재 스택 위치긴 ppos; // 배열의 중간 T 피벗; // 지원 요소 T 온도; lb스택 = 0; ubstack = 크기-1; 하다 ( // 스택에서 현재 배열의 경계 lb와 ub를 가져옵니다. lb = lbstack[스택포스]; ub = ubstack[스택포스]; 스택포스--; 하다 ( // 1단계. 피벗 요소로 분할 ppos = (lb + ub) >> 1; 나는 = lb; j = ub; 피벗 = a; do ( 동안 (a[i]< pivot) i++; while (pivot < a[j]) j--; if (i <= j) { temp = a[i]; a[i] = a[j]; a[j] = temp; i++; j--; } } while (i <= j); // 이제 포인터 i는 오른쪽 하위 배열의 시작 부분을 가리킵니다. // j - 왼쪽 끝까지(위 그림 참조), lb ? 제이? 나? ub. // 포인터 i 또는 j가 배열 경계를 넘어갈 가능성이 있습니다. // 2, 3단계. 대부분을 스택에 밀어넣고 lb,ub를 이동합니다.만약 내가< ppos) { // 오른쪽이 더 크다만약 내가< ub) { // 2개 이상의 요소가 포함된 경우 필수입니다.스택포스++; // 정렬, 스택 요청 lbstack[ stackpos ] = i; ubstack[ stackpos ] = ub; ) ub = j; // 다음 나누기 반복 // 왼쪽에서 작동합니다.) 또 다른 ( // 왼쪽이 더 크다 if (j > lb) ( stackpos++; lbstack[ stackpos ] = lb; ubstack[ stackpos ] = j; ) lb = i; ) ) 동안 (lb< ub); // 더 작은 부분에 1개 이상의 요소가 있는 동안) while (stackpos != 0); //스택에 요청이 있는 동안}

이 구현의 스택 크기는 항상 O(log n) 정도이므로 MAXSTACK에 지정된 값이 충분합니다.

안녕하세요 여러분! 퀵소트 알고리즘에 대해 이야기하고 이를 프로그래밍 방식으로 구현하는 방법을 보여드리겠습니다.

따라서 Quick Sort, 또는 C의 함수 이름에 따르면 Qsort는 복잡한 정렬 알고리즘입니다. 평균 O(n log(n))입니다. 그 본질은 매우 간단합니다. 소위 참조 요소가 선택되고 배열이 참조보다 작고, 참조와 같고, 참조보다 큰 3개의 하위 배열로 나뉩니다. 그런 다음 이 알고리즘은 하위 배열에 재귀적으로 적용됩니다.

연산

  1. 지원 요소 선택
  2. 배열을 3개 부분으로 나눕니다.
    • 고려 중인 하위 배열의 시작과 끝의 인덱스인 변수 l과 r을 각각 생성합니다.
    • l번째 요소가 참조 요소보다 작아질 때까지 l을 늘립니다.
    • r번째 요소가 참조 요소보다 커질 때까지 r을 줄입니다.
    • l이 여전히 r보다 작으면 l번째 요소와 r번째 요소를 교환하고 l을 증가시키고 r을 감소시킵니다.
    • l이 갑자기 r보다 커지면 사이클이 중단됩니다.
  3. 1개의 요소로 구성된 배열에 도달할 때까지 재귀적으로 반복합니다.
뭐, 그렇게 어려워 보이지는 않네요. C로 구현해볼까요? 괜찮아요!
무효 qsort(int b, int e)
{
int l = b, r = e;
int piv = arr[(l + r) / 2]; // 예를 들어 중간 요소를 참조 요소로 사용하겠습니다.
동안 (나는<= r)
{
동안 (arr[l]< piv)
++;
동안 (arr[r] > piv)
아르 자형--;
만약(만약<= r)
교환(arr, arr);
}
만약(비< r)
qsort(b,r);
만약 (e > l)
qsort(l,e);
} /* ----- 함수 끝 qsort ----- */

// qsort(0, n-1);


* 이 소스 코드는 소스 코드 하이라이터로 강조 표시되었습니다. .

이 구현에는 많은 양의 중첩 재귀로 인해 스택 오버플로가 발생할 수 있고 참조 요소가 항상 중간 요소로 간주된다는 사실과 같은 여러 가지 단점이 있습니다. 예를 들어, 이는 정상일 수 있지만 예를 들어 올림피아드 문제를 해결할 때 교활한 배심원은 이 솔루션이 너무 오랫동안 작동하고 한계를 초과하지 않도록 이러한 테스트를 특별히 선택할 수 있습니다. 원칙적으로는 어떤 요소든 참조 요소로 사용할 수 있지만 가능한 한 중앙값에 가까운 것이 좋으므로 무작위로 선택하거나 첫 번째, 평균, 마지막에서 평균값을 가져올 수 있습니다. 참조 요소에 대한 성능의 의존성은 알고리즘의 단점 중 하나입니다. 이에 대해 아무것도 할 수 없지만 일반적으로 특별히 선택된 숫자 집합이 정렬되는 경우 심각한 성능 저하가 거의 발생하지 않습니다. 빠른 작업이 보장되는 정렬이 여전히 필요한 경우 예를 들어 항상 O(n log n)로 엄격하게 작동하는 힙 정렬을 사용할 수 있습니다. 일반적으로 Qsort는 여전히 다른 종류보다 성능이 뛰어나고 추가 메모리가 많이 필요하지 않으며 구현이 매우 간단하므로 당연한 인기를 누리고 있습니다.

제가 직접 썼고 가끔 Wikipedia를 훑어봤습니다. 이 기회를 빌어 이 알고리즘을 포함하여 많은 유용한 것들을 가르쳐 주신 PetrSU의 훌륭한 선생님과 학생들에게 감사의 말씀을 전하고 싶습니다!

태그: Qsort, 빠른 정렬, 정렬 알고리즘, 알고리즘, C

주제에 관한 출판물