演算法日積月累 7 兩路快排

2021-08-08 03:36:14 字數 3332 閱讀 2107

二、第 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、在**中輸出一些列印語句,或者使用**編輯器的 debug 功能對**進行除錯;

3、使用小規模的測試用例在紙上走一下**邏輯,把設定的指標的含義,迴圈不變數是如何維持的寫出來,很多問題就看得比較清晰了。

對於這種比較抽象的邏輯,如果在腦子裡不能想得特別清楚,在紙上寫寫畫畫是乙個很不錯的選擇,我在寫這個邏輯的時候,把「指標」含義和迴圈不變數是怎麼維持的寫出來以後,一些邊界條件,例如,1、什麼時候退出迴圈;2、退出迴圈以後,標定點(pivot)和哪個指標交換;3、指標 i 和指標 j 的初始值是多少;這 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...