정렬은 선형 목록의 요소를 특정 순서(숫자 또는 알파벳순)로 정렬하는 것을 의미합니다. 정렬은 검색과 함께 사용되는 경우가 많습니다.
정렬 알고리즘에는 여러 가지가 있으며, 그 중 가장 빠른 것 중 하나는Quicksort입니다.
Quicksort는 분할 정복 전략을 사용하여 주어진 목록 요소를 정렬합니다. 이는 하위 문제가 직접 해결될 수 있을 만큼 단순해질 때까지 알고리즘이 문제를 하위 문제로 나누는 것을 의미합니다.
알고리즘적으로 이는 재귀 또는 반복을 사용하여 달성할 수 있습니다. 하지만 이 문제에서는 재귀를 사용하는 것이 더 자연스럽습니다.
먼저 빠른 정렬이 어떻게 작동하는지 살펴보세요.
다음으로 예제를 통해 이러한 단계를 이해하세요. 정렬되지 않은[7, -2, 4, 1, 6, 5, 0, -4, 2]
요소가 있는 배열이 있다고 가정합니다. 마지막 요소를 기본으로 선택합니다. 배열의 분해 단계는 아래 그림에 나와 있습니다.[7, -2, 4, 1, 6, 5, 0, -4, 2]
的数组。选择最后一个元素作为基准。数组的分解步骤如下图所示:
在算法的步骤1中被选为基准的元素带颜色。分区后,基准元素始终处于数组中的正确位置。
黑色粗体边框的数组表示该特定递归分支结束时的样子,最后得到的数组只包含一个元素。
最后可以看到该算法的结果排序。
这一算法的主干是“分区”步骤。无论用递归还是循环的方法,这个步骤都是一样的。
正是因为这个特点,首先编写为数组分区的代码partition()
:
function partition(arr, start, end){ // 以最后一个元素为基准 const pivotValue = arr[end]; let pivotIndex = start; for (let i = start; i < end; i++) { if (arr[i] < pivotValue) { // 交换元素 [arr[i], arr[pivotIndex]] = [arr[pivotIndex], arr[i]]; // 移动到下一个元素 pivotIndex++; } } // 把基准值放在中间 [arr[pivotIndex], arr[end]] = [arr[end], arr[pivotIndex]] return pivotIndex; };
代码以最后一个元素为基准,用变量pivotIndex
来跟踪“中间”位置,这个位置左侧的所有元素都比pivotValue
小,而右侧的元素都比pivotValue
大。
最后一步把基准(最后一个元素)与pivotIndex
交换。
在实现了partition()
函数之后,我们必须递归地解决这个问题,并应用分区逻辑以完成其余步骤:
function quickSortRecursive(arr, start, end) { // 终止条件 if (start >= end) { return; } // 返回 pivotIndex let index = partition(arr, start, end); // 将相同的逻辑递归地用于左右子数组 quickSort(arr, start, index - 1); quickSort(arr, index + 1, end); }
在这个函数中首先对数组进行分区,之后对左右两个子数组进行分区。只要这个函数收到一个不为空或有多个元素的数组,则将重复该过程。
空数组和仅包含一个元素的数组被视为已排序。
最后用下面的例子进行测试:
array = [7, -2, 4, 1, 6, 5, 0, -4, 2] quickSortRecursive(array, 0, array.length - 1) console.log(array)
输出:
-4,-2,0,1,2,4,5,6,7
快速排序的递归方法更加直观。但是用循环实现快速排序是一个相对常见的面试题。
与大多数的递归到循环的转换方案一样,最先想到的是用栈来模拟递归调用。这样做可以重用一些我们熟悉的递归逻辑,并在循环中使用。
我们需要一种跟踪剩下的未排序子数组的方法。一种方法是简单地把“成对”的元素保留在堆栈中,用来表示给定未排序子数组的start
和end
。
JavaScript 没有显式的栈数据结构,但是数组支持push()
和pop()
函数。但是不支持peek()
函数,所以必须用stack [stack.length-1]
이 기능 때문에 배열 분할을 위한 코드가 먼저
partition()
으로 작성되었습니다.
function quickSortIterative(arr) { // 用push()和pop()函数创建一个将作为栈使用的数组 stack = []; // 将整个初始数组做为“未排序的子数组” stack.push(0); stack.push(arr.length - 1); // 没有显式的peek()函数 // 只要存在未排序的子数组,就重复循环 while(stack[stack.length - 1] >= 0){ // 提取顶部未排序的子数组 end = stack.pop(); start = stack.pop(); pivotIndex = partition(arr, start, end); // 如果基准的左侧有未排序的元素, // 则将该子数组添加到栈中,以便稍后对其进行排序 if (pivotIndex - 1 > start){ stack.push(start); stack.push(pivotIndex - 1); } // 如果基准的右侧有未排序的元素, // 则将该子数组添加到栈中,以便稍后对其进行排序 if (pivotIndex + 1 < end){ stack.push(pivotIndex + 1); stack.push(end); } } }
코드는 마지막 요소를 기반으로 하며 변수pivotIndex를 사용합니다. code>를 사용하여 ""중간" 위치를 추적하는 경우 이 위치의 왼쪽에 있는 모든 요소는
pivotValue
보다 작고 오른쪽에 있는 모든 요소는pivotValue
보다 큽니다.
pivotIndex
로 바꾸는 것입니다.partition()
함수를 구현한 후에는 문제를 재귀적으로 해결하고 파티셔닝 논리를 적용하여 나머지 단계를 완료해야 합니다.
ourArray = [7, -2, 4, 1, 6, 5, 0, -4, 2] quickSortIterative(ourArray) console.log(ourArray)
이 함수에서 먼저 배열을 분할한 다음 왼쪽 및 오른쪽 하위 배열을 분할합니다. 이 함수가 비어 있지 않거나 요소가 두 개 이상인 배열을 수신하는 한 프로세스는 반복됩니다.
빈 배열과 요소가 하나만 포함된 배열은 정렬된 것으로 간주됩니다. 마지막으로 다음 예제를 사용하여 테스트합니다.-4,-2,0,1,2,4,5,6,7
시작
및
끝
을 나타내는 요소의 "쌍"을 스택에 유지하는 것입니다. JavaScript에는 명시적인 스택 데이터 구조가 없지만 배열은
push()
및
pop()
함수를 지원합니다. 단,
peek()
함수는 지원하지 않으므로
stack [stack.length-1]
를 이용하여 스택의 상단을 수동으로 확인해야 합니다. 재귀적 방법과 동일한 "파티션" 기능을 사용하겠습니다. Quicksort 부분을 작성하는 방법을 살펴보십시오. rrreee 테스트 코드는 다음과 같습니다. rrreee출력: rrreee시각적 데모정렬 알고리즘에 관해서 시각화하면 작동 방식을 직관적으로 이해하는 데 도움이 될 수 있습니다. 아래 이 예는 Wikipedia에서 가져온 것입니다. 그림의 마지막 요소도 기본으로 사용됩니다. 분할된 배열이 주어지면 완전히 정렬될 때까지 왼쪽을 재귀적으로 탐색합니다. 그런 다음 오른쪽을 정렬하십시오. 빠른 정렬의 효율성이제 시간과 공간의 복잡성에 대해 논의해 보세요. 퀵 정렬의 최악의 시간 복잡도는 $O(n^2)$입니다. 평균 시간 복잡도는 $O(nlog n)$입니다. 일반적으로 퀵 정렬의 무작위 버전을 사용하면 최악의 시나리오를 피할 수 있습니다. 퀵 정렬 알고리즘의 약점은 벤치마크 선택입니다. 잘못된 피벗(대부분의 요소보다 크거나 작은 피벗)을 선택할 때마다 최악의 시간 복잡성을 얻게 됩니다. 기저를 반복적으로 선택할 때 요소의 기저 값이 요소의 기저보다 작거나 클 경우 시간 복잡도는 $O(nlog n)$입니다.
어떤 데이터 벤치마크 선택 전략을 채택하든 퀵 정렬의 시간 복잡도는 $O(nlog n)$를 갖는 경향이 있다는 것을 경험적으로 볼 수 있습니다.
빠른 정렬은 추가 공간을 차지하지 않습니다(재귀 호출을 위해 예약된 공간 제외). 이 알고리즘을in-place알고리즘이라고 하며 추가 공간이 필요하지 않습니다.
더 많은 프로그래밍 관련 지식을 보려면프로그래밍 소개를 방문하세요! !
위 내용은 JavaScript의 빠른 정렬에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!