1. 程式人生 > >python快速排序遞迴與非遞迴

python快速排序遞迴與非遞迴

寫在前面

眾所周知,快速排序相對於選擇排序,插入排序,氣泡排序等初級排序有著天然的優勢。這是因為快排在交換元素的過程中,兩個發生交換的元素,距離較遠。比如插入排序,新的元素要在已經有序的序列中,一次又一次地找到它應該處於的位置,交換的次數遠遠高於快排。但是,使用快排時,要特別的小心,尤其是它的邊界條件設定,還有就是重複元素比較多的情況。

快速排序的遞迴函式

這裡我們使用快排的最常見的遞迴函式,這裡要注意一下,要先寫退出遞迴的條件,一些小細節還是要注意一下的。

def QuickSort(lo,hi):
    if lo >= hi:
        return
    j = partition(
lo,hi) QuickSort(lo,j-1) QuickSort(j+1,hi)

快排的切分函式

切分函式很重要,是快速排序的精髓。以下是python實現。
由於python裡沒有java,C++中a[++i]這種騷操作,因此不能照著Algorithm那本書那麼寫,要稍微做一些改動,其實主要就是邊界條件。特殊情況下,如果切分元素是陣列中最大或者最小的那個元素,就要小心別讓掃描指標跑出陣列的邊界。實際上,第二個內迴圈邊界條件(j>lo)是多餘的,因為j遞減到lo+1,若繼續進入迴圈,再減到lo時,已經不滿足進入迴圈的條件了,此時a[j]=v,迴圈自動退出,因此這個邊界條件冗餘。這都是在後期發現的,剛開始學習的話以防萬一可以加上,熟練之後就可以去掉了。Algorithm中的java實現請點選

連結

def partition(lo,hi):
    i = lo+1
    j = hi
    v = a[lo]
    while 1:
        while (i<hi)and(a[i]<=v):#使用i++必須要及時使用哨兵,使用<=跳過重複元素,防止重複元素之間交換形成死迴圈                              
            i += 1
        while (j>lo)and(a[j]>v):
            j -= 1   
        if i >= j:
            break
a[i],a[j] = a[j],a[i] a[lo],a[j]=a[j],a[lo] return j

快排的非遞迴函式

這裡手動維護一個下壓棧來儲存待排序陣列的切分指標以及頭尾指標

def QuickSort(lo,hi):
    stack = []
    stack.insert(0,lo)#頭指標入棧
    stack.insert(0,hi)#尾指標入棧
    while(len(stack)!= 0):#若棧不為空
        hi= stack.pop(0)#頭指標出棧
        lo = stack.pop(0)#尾指標出棧
        j = partition(lo,hi)#當返回切分指標,發現子陣列長度為1,則無法入棧
        if lo<j-1:
            stack.insert(0,lo)
            stack.insert(0,j-1)
        elif j+1<hi:
            stack.insert(0,j+1)
            stack.insert(0,hi)

以下是非遞迴的入棧出棧圖
在這裡插入圖片描述

  • 圖1中lo和hi指標入棧,然後出棧
    在這裡插入圖片描述
  • 圖2中切分指標兩邊的左右子陣列分別入棧,然後釋放右子陣列的頭尾指標

在這裡插入圖片描述

  • 圖3中接著入棧4個指標,j’是下一次切分的指標。持續的出棧,入棧過程,意味著右子陣列不斷細化,不斷被切小,而切分指標不斷入棧。當子陣列長度為1時,棧容納的指標數量達到了最大,partition函式返回的切分指標j已經不能入棧了,所以下壓棧開始了釋放過程,一旦子陣列長度不是1,那麼意味著還有切分指標入棧的餘地,於是切分指標繼續入棧。不斷重複這個過程。
  • 總的來說,棧從空到滿溢的過程中,拿別人的多,給別人的少。在滿溢到空的過程中,下壓棧給別人的多,拿別人的少。我們可以把這種過程看做是股市的跌漲。在牛市的時候,雖然個別股是下跌的,但是整個股市形勢一片大好啊,人們不停的注資,撤資的少。但是到達了股市的頂點,整個形勢就不好了。雖然還是有人往裡注資,但撤資的人數高於注資的人。

完整的原始碼

import random
import datetime
#切分函式
def partition(lo,hi):
    i = lo+1
    j = hi
    v = a[lo]
    while 1:
        while (i<hi)and(a[i]<=v):#使用i++必須要及時使用哨兵,使用<=跳過重複元素,防止重複元素之間交換形成死迴圈                              
            i += 1
        while (j>lo)and(a[j]>=v):
            j -= 1   
        if i >= j:
            break
        
        a[i],a[j] = a[j],a[i]
    a[lo],a[j]=a[j],a[lo]
    return j
    
#遞迴
def QuickSort(lo,hi):
    if lo >= hi:
        return
    j = partition(lo,hi)
    QuickSort(lo,j-1)
    QuickSort(j+1,hi)
#非遞迴   
def QuickSort(lo,hi):
    stack = []
    stack.insert(0,lo)#頭指標入棧
    stack.insert(0,hi)#尾指標入棧
    while(len(stack)!= 0):#若棧不為空
        hi= stack.pop(0)#頭指標出棧
        lo = stack.pop(0)#尾指標出棧
        j = partition(lo,hi)#當返回切分指標,發現子陣列長度為1,則無法入棧
        if lo<j-1:
            stack.insert(0,lo)
            stack.insert(0,j-1)
        elif j+1<hi:
            stack.insert(0,j+1)
            stack.insert(0,hi)
            
if __name__ =='__main__':
    a = []
#    random.seed(12345)
    for i in range(10):
        a.append(random.randint(0,10))
    random.shuffle(a)
    lo = 0
    hi = len(a)-1
    print(a)
    time1 = datetime.datetime.now()
    QuickSort(lo,hi)
    time2 = datetime.datetime.now()
    duration = time2-time1
    print(duration)
    print(a)            

快排很容易出錯,在單元測試的時候,大家不妨用random函式多試幾次,來驗證自己寫的快排是否正確,尤其是切分元素在陣列中還有重複的元素,以及一些邊界條件,有沒有等於之類的情況