快速排序的实现对比
原地 v.s. 非原地 (in-place v.s. out-place)
原地:快慢指针、左右指针、挖坑法
单独处理相等元素:三路快排
非递归实现
当序列有序时,时间复杂度将是N^2,所以此时我们选取 前中后 中值为中间的值和关键字的位置进行交换,那么我们再进行排序的序列的时间复杂度基本上被优化为N*logN。人为打乱排序
快排的核心是序列分割:以参考元素(pivot)为界将序列分为大小两部分,并递归该操作。下面的示例代码未采用in-place操作,实现上简单粗暴,也更突出该核心思想。
1 | def partition(arr): |
通常的快速排序会采用in-place操作,这里仅用于展示快排的核心思想。需要注意,虽然与 pivot 相等的元素最终会与 pivot元素 并列排布,但在实现时我们仅将 pivot元素 置于两个分组之间,与之相等的元素通常不做特殊处理,可归于任一分组,留待递归处理。当然也可以将与pivot相等的元素都找出来,即所谓的三路快排,后面也会讲到,适用于元素存在大量重复情况,如荷兰国旗问题。
快慢指针
快慢指针或前后指针(Lomuto partition scheme):less | great | unprocessed
遍历指标(快指针)在前,遍历序列(标记已处理|未处理序列界线);分界指标(慢指针)在后,标记已处理部分中大小元素界线。遍历遇到小于pivot元素,就将其与分界指标处元素(第一个大元素)交换位置,并相应移动分界指针位置,之后继续遍历,直至全部比较一遍,最后再将 pivot元素调换到中间。
细节:分界指标可指向小元素末尾,也可指向大元素起始,前者指标递增要先于元素调换,后者指标递增要后于元素调换。此外pivot可取序列首位或末位,尤其是取首位元素时,需格外注意不能移动首位的pivot,具体可有多种处理方式,pivot取末位元素时,则相对简单。
1 | #-------pivot取末位元素-------# |
As this scheme is more compact and easy to understand, it is frequently used in introductory material, although it is less efficient than Hoare’s original scheme e.g., when all elements are equal.[17] This scheme degrades to O(n2) when the array is already in order.
左右指针
左右指针(Hoare partition scheme):less | unprocessed | great
左指针向右找小元素,遇到大元素暂停;右指针向左找大元素,遇到小元素暂停;两指针都暂停时交换元素位置,之后继续左右夹逼,直至两指针相遇,最后再将 pivot元素调换到中间。实现上的关键的点为:
- 必须确保pivot元素在最终调换前不能被移动;
- pivot取首位元素时与小元素末位调换,pivot取末位元素时与大元素首位调换。
而为了确保上述要点,最佳实践为:
- 与pivot同侧的指针在比较元素时要包含等号;
- pivot取首位元素则先移动右指针,pivot取末位元素则先移动左指针;
- 左右指针移动时应确保不越过对方(
left < right
),并在相遇时结束;
这些操作可确保相遇位置刚好指向pivot最终位置:先动左指针(pivot取末位),相遇于大元素首位;先动右指针(pivot取首位),相遇于小元素末位。不遵守这些实践要求也可以,且实现方式有很多种,只是需要小心应对各种corner case,代码会比较丑。
1 | def partition(arr, left, right): |
1 | def partition(arr, left, right): |
挖坑法
挖坑法:less | unprocessed | great
挖坑法与左右指针类似,从两侧交替向中间移动,但不同于左右指针交换元素的做法,挖坑法将pivot元素的值保存后,将其所在位置作为“空位”使用(挖坑)。pivot取序列首位,则空位需存小元素,因此先走右指针,遇到小元素就将元素填入空位,并将元素所处位置作为新的空位,用于存大元素,供左指针使用,如此交替循环,直至两指针相遇。类似的,如果pivot取序列末位,则需先走左指针。最后将pivot元素值填入位于中间的空位。实现上的关键的点为:
- pivot取序列首位,需先走右指针;pivot取序列末位,需先走左指针
同时,类似上述左右指针的要点: - 元素与pivot比较,左右两边至少一边要取等号,可以两边都取等号
- 移动左右指针时应确保不越过对方(
left < right
)
1 | def partition(arr, left, right): |
1 | def quick_sort_test(arr, start=None, end=None): |
三路快排
https://algs4.cs.princeton.edu/23quicksort/
Entropy-optimal Sorting
三路快排可以基于前后指针实现,也可基于左右指针实现
- 基于前后指针扩展:
less | equal | unprocessed | great
将等于pivot的元素放在原本的大元素区间,引入右指针,在序列右侧存放大元素 - 基于左右指针扩展:
equal | less | unprocessed | great | equal
将等于pivot的元素存放在左右两边的外侧,最后再将两侧这些元素调换到中间
1 | def partition(arr, start, end): |
尾递归调用优化 空间复杂度
Most practical implementations of Quick Sort use randomized version. The randomized version has expected time complexity of O(nLogn). The worst case is possible in randomized version also, but worst case doesn’t occur for a particular pattern (like sorted array) and randomized Quick Sort works well in practice.
Merge sort accesses data sequentially and the need of random access is low, preferred over QuickSort for Linked Lists.
The depth of quicksort’s divide-and-conquer tree directly impacts the algorithm’s scalability, and this depth is highly dependent on the algorithm’s choice of pivot.
Python没有尾递归优化,默认递归栈深度为1000,数据量多时很容易就达到最大深度了
非递归操作
需要借助栈或队列
1 | def quick_sort(array, l, r): |