分治法:快速排序,3種劃分方式,隨機化快排,快排快,還是歸併排序快?
阿新 • • 發佈:2018-12-17
快速排序不同於之前瞭解的分治,他是通過一系列操作劃分得到子問題,不同之前的劃分子問題很簡單,劃分子問題的過程也是解決問題的過程
我們通常劃分子問題儘量保持均衡,而快排缺無法保持均衡
快排第一種劃分子問題實現方式,左右填空的雙指標方式
def partition_1(arr,low,high):
# 把基準元素取出來,留出一個空位,這裡是在首位,這種留出空位的方式,比較容易理解
pivot = arr[low]
# 迴圈體終止條件,因為是先走右邊再走左邊,終止的時候一定是兩個指標重合在一起
# 也可以交叉,但是可以控制迴圈他們重合在一起跳出迴圈
# 這裡解釋以下low,high這兩個指標代表什麼,low,high代表從其實到low都是小於基準的元素
# 從high到end都是大於基準的元素,當low和high重合時,那左邊都是小的,右邊都是大的
# 重合的位置是空的(實際上有值),因為每個時刻都有一個位置都是空的,重合剩下最後一個位置,
# 這個位置也必然是空的,也可以用一個小的例項分析一下
while low < high:
# 首先右邊一直往左走,直到遇到小於基準的元素,這裡控制一下,不讓他們交叉
# 不新增的low <high,往左走不會越界,但是可能小於low
while arr[high] > pivot and low <high:
high -=1
# 避免他們兩交叉,只要相等就退出,右邊遇到小於基準的元素,把左邊的那個空位填上,左邊的指標更新一下
if low <high:
arr[low] = arr[high]
low +=1
# 左指標往左就是小於基準的元素,這時右邊空出來一個位置,左指標往右掃描
while arr[low] < pivot and low <high:
low +=1
# 找到大於基準的元素,放到右邊空出來的位置,那右指標往右全部都是大於基準元素的
if low <high:
arr[high] = arr[low]
high -=1
# 當只剩下唯一的空位置時,把基準元素放待空的位置上
arr[low] = pivot
return low
快排第二種劃分子問題方式,單指標方式
def partition_2(arr,low,high):
# 這時另外一種考慮方式,而且他是不需要額外空間的,他只使用一個指標來區分小於基準和大於基準的
# pointer_less_than代表這個指標的左邊全部都是小於基準的(包括自己,不包括首元素)
# 然後從左往右掃描,遇到小於基準的元素,就把小於基準元素區域的後面緊接著的一個元素和他交換
# 那麼小於基準元素區域就多了一個元素,。。。就這樣小於基準的元素就連在了一起
# 首元素是基準元素,小於基準元素區域塊,大於基準元素區域塊,現在分成了三個部分
# 把首元素和小於基準元素區域塊最後一個元素交換,那三部分就變成,小於的,基準,大於的
# 剛開始小於基準的元素為0,暫且指向首位值
pointer_less_than = low
# 然後一次掃描後面所有元素
for i in range(pointer_less_than +1,high+1):
# 遇到小於基準的,就把小於基準元素區域的後面緊接著的一個元素和他交換,小於的塊相當於也更新了
if arr[i] < arr[low] :
pointer_less_than +=1
arr[pointer_less_than],arr[i]=arr[i],arr[pointer_less_than]
# 把首元素和小於基準元素區域塊最後一個元素交換,那三部分就變成,小於的,基準,大於的
arr[low],arr[pointer_less_than] = arr[pointer_less_than],arr[low]
return pointer_less_than
第三種劃分子問題實現方式,左右同時交換,這種方式注意結束的情況
def partition_3(arr,start,end):
# 這個方式也是不需要額外的輔助空間的
# 他的思想是:從左(或者右也可以)掃描到第一個大於基準的元素,然後從右往左掃描到第一個小於基準的
# 元素,將他們兩交換,然後再重複上述操作,直到兩個指標重合位置
# 這兩個指標分別代表:前面(除了首元素)到low為小於基準,high到end為大於基準元素
# 他們是可能會交叉的,也有可能重合,這時陣列分成三個部分:首元素基準,小於的,大於的
# 這個地方可能會交叉的,也有可能重合,分3種情況:第一種情況[大於,小於],然後他們兩個交換
# [小於,大於],low-->大於,high-->小於,這時首元素需要和high互換
# [小於],high-->小於,沒有大於的元素和他互換,low一直加直到等於high,這時這時首元素需要和high互換
# [大於],low-->大於,沒有小於的元素和他互換,high會一直減,直到比low小1,這時這時這時首元素需要和high互換
# 不管是那種情況下,high指向肯定是最後一個小於基準的元素
# 這裡不能利用兩者指標重合,因為兩個指標重合指向的元素,可能大於基準也可能小於基準,
# 要使用high指向的元素
# 初始化左指標和右指標
low = start
high = end +1
# 迴圈體退出條件為兩指標重合或者交叉
while True:
# 需要先-1,因為交換之後,指標需要更新一下,不更新的話,迴圈體會多運算一步
high -=1
# 這裡就是需要兩指標交叉,這樣high才能指向小於區域裡面的最後一個元素
while arr[high] > arr[start] :
high -=1
low +=1
while arr[low] < arr[start] and low < end:
low +=1
# 在這個時候,陣列分成三個部分:首元素是基準元素,小於基準元素區域塊,大於基準元素區域塊
if low >= high:
break
# 把這兩個元素交換,小的跑到左邊,大的跑到右邊
arr[low],arr[high] = arr[high],arr[low]
# 把首元素和小於基準元素區域塊最後一個元素交換,那三部分就變成,小於的,基準,大於的
arr[start],arr[high] = arr[high],arr[start]
return high
執行結果
#%%
def quickSort(arr,low,high):
if low < high:
index = partition_1(arr,low,high)
quickSort(arr,low,index-1)
quickSort(arr,index+1,high)
def quickSort1(arr,low,high):
if low < high:
index = partition_2(arr,low,high)
quickSort1(arr,low,index-1)
quickSort1(arr,index+1,high)
def quickSort2(arr,low,high):
if low < high:
index = partition_3(arr,low,high)
quickSort2(arr,low,index-1)
quickSort2(arr,index+1,high)
arr = [7,3,66,33,22,66,99,0,1]
print(arr)
quickSort(arr,0,len(arr)-1)
print(arr)
arr1 = [7,3,66,33,22,66,99,0,1]
#print(arr1)
quickSort1(arr1,0,len(arr1)-1)
print(arr1)
arr2 = [7,3,66,33,22,66,99,0,1]
#print(arr2)
quickSort2(arr2,0,len(arr2)-1)
print(arr2)
[7, 3, 66, 33, 22, 66, 99, 0, 1]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
前面提到快排的劃分不一定是均衡劃分,而快排的效率取決於劃分的對稱性,稍微改進一下為隨機化演算法,這類把某一步修改成隨機步驟的演算法,叫做拉斯維加斯演算法
import random
def randomizedPartition(arr,low,high):
def partition(arr,low,high):
# 這時另外一種考慮方式,而且他是不需要額外空間的,他只使用一個指標來區分小於基準和大於基準的
# pointer_less_than代表這個指標的左邊全部都是小於基準的(包括自己,不包括首元素)
# 然後從左往右掃描,遇到小於基準的元素,就把小於基準元素區域的後面緊接著的一個元素和他交換
# 那麼小於基準元素區域就多了一個元素,。。。就這樣小於基準的元素就連在了一起
# 首元素是基準元素,小於基準元素區域塊,大於基準元素區域塊,現在分成了三個部分
# 把首元素和小於基準元素區域塊最後一個元素交換,那三部分就變成,小於的,基準,大於的
# 剛開始小於基準的元素為0,暫且指向首位值
pointer_less_than = low
# 然後一次掃描後面所有元素
for i in range(pointer_less_than +1,high+1):
# 遇到小於基準的,就把小於基準元素區域的後面緊接著的一個元素和他交換,小於的塊相當於也更新了
if arr[i] < arr[low] :
pointer_less_than +=1
arr[pointer_less_than],arr[i]=arr[i],arr[pointer_less_than]
# 把首元素和小於基準元素區域塊最後一個元素交換,那三部分就變成,小於的,基準,大於的
arr[low],arr[pointer_less_than] = arr[pointer_less_than],arr[low]
return pointer_less_than
index = random.randint(low,high)
arr[low],arr[index]=arr[index],arr[low]
return partition(arr,low,high)
def randomizedQuicksort(arr,low,high):
if low < high:
index = randomizedPartition(arr,low,high)
randomizedQuicksort(arr,low,index-1)
randomizedQuicksort(arr,index+1,high)
arr3 = [7,3,66,33,22,66,99,0,1]
print(arr3)
randomizedQuicksort(arr3,0,len(arr3)-1)
print(arr3)
[7, 3, 66, 33, 22, 66, 99, 0, 1]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
快排快,還是歸併排序快?
按照理論上分析,快排平均時間複雜度O(nlogn),最壞為n^2,歸併平均和最壞均為nlogn。
理論上應該是歸併快才對,但是有好多人測試很大規模資料的情況下快排比歸併快。
可能的原因,歸併有空間開銷,有陣列複製合併的操作,快排屬於原地排序,在資料規模大的情況下,差距就出來了。