1. 程式人生 > >分治法:快速排序,3種劃分方式,隨機化快排,快排快,還是歸併排序快?

分治法:快速排序,3種劃分方式,隨機化快排,快排快,還是歸併排序快?

快速排序不同於之前瞭解的分治,他是通過一系列操作劃分得到子問題,不同之前的劃分子問題很簡單,劃分子問題的過程也是解決問題的過程

我們通常劃分子問題儘量保持均衡,而快排缺無法保持均衡

快排第一種劃分子問題實現方式,左右填空的雙指標方式

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。
理論上應該是歸併快才對,但是有好多人測試很大規模資料的情況下快排比歸併快。
可能的原因,歸併有空間開銷,有陣列複製合併的操作,快排屬於原地排序,在資料規模大的情況下,差距就出來了。