二、第 2 版快速排序:雙路快排
在有很多重複元素的情況下,放在中間的那個 j 的位置也會使得遞迴的過程變得很不平衡,這個時候我們也可以採取一定的優化措施。
我們可以編寫乙個測試用例,構造出乙個有很多個重複鍵值的陣列,分別使用「歸併排序」和「快速排序」,看看它們的耗時。
from sort.sort_helper import generate_random_array
from sort.c_merge_sort_1 import merge_sort
from sort.d_quick_sort import quick_sort
from sort.sort_helper import check_sorted
import time
# 最小值是 10,最大值是 20,都可以取到
# 取了 10000 個元素,用快排1和歸併排序測試一下
nums = generate_random_array(10,
20,10000
)print
(nums)
nums_for_merge_sort = nums[:]
nums_for_quick_sort_1 = nums[:]
begin = time.time(
)merge_sort(nums_for_merge_sort)
print
('歸併排序耗時:'
, time.time(
)- begin)
begin = time.time(
)quick_sort(nums_for_quick_sort_1)
print
('快速排序耗時:'
, time.time(
)- begin)
check_sorted(nums, nums_for_merge_sort)
check_sorted(nums, nums_for_quick_sort_1)
執行結果:
可以看到,「快速排序」比我們第 1 版沒有優化過的「歸併排序」都慢很多。
我們不妨將待測試陣列的重複元素搞得多一些。
可以看到,此時「歸併排序」可以完成排序任務,而我們第 1 版的「快速排序」已經丟擲異常了,這個異常不是因為我們編寫的邏輯有嚴重錯誤,而是因為我們這個測試用例太極端了,這個異常就是「遞迴深度太深」,因為重複元素太多,都被分到了陣列的同一側,而導致遞迴深度太深,導致系統棧都不夠用了。
發現問題:在有很多重複元素的情況下,放在中間的那個j
的位置也會使得遞迴的過程變得很不平衡。
基本思想:指標對撞的雙路快速排序,在有很多鍵重複的情況下,重複的鍵能夠比較「均勻地」分布在陣列的前後,即將與標定點相等的元素等概率分散到遞迴函式的兩邊。
實現方式:把等於標定點的元素「等概率地」分散到了標定元素左右兩邊。
小技巧:在編寫與「指標」(不是 c++ 中的指標)相關的邏輯的時候,我們一定要把握住我們設定的指標的含義,在遍歷的過程中,位置這個指標的含義不變,這樣才能編寫出正確的**。對於這種比較抽象的邏輯,如果在腦子裡不能想得特別清楚,在紙上寫寫畫畫是乙個很不錯的選擇,我在寫這個邏輯的時候,把「指標」含義和迴圈不變數是怎麼維持的寫出來以後,一些邊界條件,例如,1、什麼時候退出迴圈;2、退出迴圈以後,標定點(pivot)和哪個指標交換;3、指標 i 和指標 j 的初始值是多少;這 3 個問題就看得非常清楚了。聰明的你或許不用像我一樣寫這麼多,不過我想寫寫畫畫會加速你的思考過程,也能加深你對問題的理解,這其實也是我們常常寫**時「用空間換時間」的一種體現吧。對於一些邊界條件,一定要思考清楚,如果剛開始寫有困難的,可以考慮以下幾種方式把**寫對:
1、參考他人優秀的**,即使是抄**也要抄明白,抄完以後自己復現一下;
2、在**中輸出一些列印語句,或者使用**編輯器的 debug 功能對**進行除錯;
3、使用小規模的測試用例在紙上走一下**邏輯,把設定的指標的含義,迴圈不變數是如何維持的寫出來,很多問題就看得比較清晰了。
第 2 版基於「指標對撞」的 partition 的快速排序:
def
__partition_2
(nums, left, right)
: p = nums[left]
i = left +
1 j = right
while
true
:while i <= right and nums[i]
< p:
i +=
1while j >= left +
1and nums[j]
> p:
j -=
1if i > j:
break
nums[i]
, nums[j]
= nums[j]
, nums[i]
i +=
1 j -=
1 nums[left]
, nums[j]
= nums[j]
, nums[left]
return j
def__quick_sort
(nums, left, right)
:if left >= right:
return
p_index = __partition_2(nums, left, right)
__quick_sort(nums, left, p_index -1)
__quick_sort(nums, p_index +
1, right)
defquick_sort
(nums)
: __quick_sort(nums,0,
len(nums)-1
)
此時,我們可以把測試用例弄得再極端一些,發現「快速排序」不僅可以完成排序任務,而且比「歸併排序」還要快一些。
這一版「快速排序」最重要的優化就是針對陣列中有大量和標定元素重複的元素,我們通過「指標對撞」的方式把它們分散到陣列的兩端,以減少遞迴的深度。
關於「指標對撞」其實是乙個常用的演算法技巧,leetcode 上有很多關於「雙指標」的問題,當然有些是鍊錶中的,有些不是「對撞」,而是一前一後,感興趣的朋友們不妨練習一下。
其實,我們還可以做得更好一些,我們可以把與標定點相等的元素都趕到陣列的中間去,這樣在有很多重複元素的陣列中,一下子就可以把中間的很多元素排定,同時遞迴呼叫的深度也大大減少了,這就是我們第 3 版的快速排序,它用到的技巧我們剛剛提到過,也是「雙指標」,只不過不是「對撞」,而是「一前一後」。
演算法複習之兩路歸併排序
兩路歸併排序 最差時間複雜度 o nlogn 平均時間複雜度 o nlogn 最差空間複雜度 o n 穩定性 穩定 兩路歸併排序 merge sort 也就是我們常說的歸併排序,也叫合併排序。它是建立在歸併操作上的一種有效的排序演算法,歸併操作即將兩個已經排序的序列合併成乙個序列的操作。該演算法是採...
資料結構 排序 兩路歸併排序演算法
歸併排序 merge sort 是利用 歸併 技術來進行排序。歸併是指將若干個已排序的子檔案合併成乙個有序的檔案。1 演算法基本思路 設兩個有序的子檔案 相當於輸入堆 放在同一向量中相鄰的位置上 r low.m r m 1.high 先將它們合併到乙個區域性的暫存向量r1 相當於輸出堆 中,待合併完...
快速排序 快排 演算法的C 兩種實現
快排演算法在分治的時候有兩種實現,一種實現是從兩邊到中間 partition 另一種實現是從一邊到另一邊 partition2 我用乙個100000陣列測試發現前一種實現執行速度快一些。這兩種的c 實現如下 注 我用的 風格是gnu的 風格 bool sort qsort int ini,int s...