1. 程式人生 > >常用排序演算法的python實現和效能分析

常用排序演算法的python實現和效能分析

一年一度的換工作高峰又到了,HR大概每天都塞幾份簡歷過來,基本上一天安排兩個面試的話,當天就只能加班幹活了。趁著面試別人的機會,自己也把一些基礎演算法和一些面試題整了一下,可以階段性的留下些腳印——沒辦法,平時太忙,基本上沒有時間寫部落格。面試測試開發的話,這些也許能幫得上一些。

這篇是關於排序的,把常見的排序演算法和麵試中經常提到的一些問題整理了一下。這裡面大概有3個需要提到的問題:

  • 雖然專業是數學,但是自己還是比較討厭繁瑣的公式,所以基本上文章所有的邏輯,我都儘可能的用大白話說,希望能說明白;
  • 語言使用的是Python,原因是寫的快一些,當然會儘可能的拋開一些Python的特點,比如陣列處理的時候儘可能的不使用一些tuple交換等方式;
  • 測試演算法的時候會用到一些Python程式設計的技巧,這裡只是簡單的提一下,不做深入介紹;

常用的排序演算法(主要指面試中)包含兩大類,一類是基礎比較模型的,也就是排序的過程,是建立在兩個數進行對比得出大小的基礎上,這樣的排序演算法又可以分為兩類:一類是基於陣列的,一類是基於樹的;基礎陣列的比較排序演算法主要有:冒泡法,插入法,選擇法,歸併法,快速排序法;基礎樹的比較排序演算法主要有:堆排序和二叉樹排序;基於非比較模型的排序,主要有桶排序和點陣圖排序(個人認為這兩個屬於同一思路的兩個極端)。

對於上面提到的這些排序演算法,個人認為並沒有優劣之分,主要看關注點,也就是需求。綜合去看待這些演算法,我們可以通過以下幾個方面(不完全)判斷:時間複雜度,空間複雜度,待排序陣列長度,待排序陣列特點,程式編寫複雜度,實際程式執行環境,實際程式可接受水平等等。說白了就是考慮各種需求和限制條件,程式快不快,佔得空間,排序的數多不多,規律不規律,資料重合的多不多,程式設計師水平,執行的機器高配還是低配,客戶或者使用者對執行時間的底線等等。

拋開主觀的這些因為,從技術上講,時間複雜度和空間複雜度,是最為關心的,下面是這些排序演算法的一個總結和特點——分類和總結完全是個人體會,請不要拿教科書上的東西較真。
總結

冒泡法:對比模型,原陣列上排序,穩定,慢

插入法:對比模型,原陣列上排序,穩定,慢

選擇法:對比模型,原陣列上排序,穩定,慢

歸併法:對比模型,非原陣列上排序,穩定,快

快速法:對比模型,原陣列上排序,不穩定,快

堆排序:對比模型,原陣列上排序,不穩定,快

二叉樹排序:對比模型,非陣列上排序,不穩定,快

桶排序:非對比模型,非原陣列上排序,不穩定,快

點陣圖排序:非對比模型,非原陣列上排序,不穩定,快

現在開始正經的東西,逐一討論一下這些排序演算法;事實上,理解了演算法本身的意義,虛擬碼很容易寫出來,但是寫程式碼是另外一回事——演算法忽略常量,忽略對於複雜度影響不大的東西,但是寫程式碼的時候,卻必須關心這些:

冒泡法

入門級演算法,但是它的思路很具有特點:迴圈,兩兩向後比較。具體方法是針對迴圈中的每一元素,都對它後面的元素迴圈比較,交換大小值,每次迴圈“冒”一個最大值(或最小值)放在裡層迴圈初始的地方;python中的程式碼如下:

def bubbleSort(L):
assert(type(L)==type([”]))
length = len(L)
if length==0 or length==1:
return L
for i in xrange(length):
for j in xrange(length-1-i):
if L[j] < L[j+1]:
temp = L[j]
L[j] = L[j+1]
L[j+1] = temp
return L
pass

冒泡法的優點是穩定,不需要大量額外的空間開銷,而且容易想到。很多面試人員知道快速排序,但是不知道冒泡法——大部分是培訓學校出來的,快速排序稍微改動一些就不知道怎麼辦了,但是冒泡法,雖然不知道,但是解釋和優化起來,確很容易。畢竟對於程式設計來說,巢狀一個迴圈和判斷,是最基本的。

選擇排序

     選擇法也算是入門的一種排序演算法,比起冒泡法,它的方法巧妙了一些,它的出發點在於“挑”,每次挑選陣列的最值,與前置元素換位,然後繼續挑選剩餘元素的最值並重復操作。個人認為選擇排序的意義不在於排序本身,而在於挑選和置換的方法,對於一些問題很有幫助。先看一下選擇排序的python實現:

def selectSort(L):
assert(type(L)==type([”]))
length = len(L)
if length==0 or length==1:
return L

def _max(s):
    largest = s
    for i in xrange(s,length):
        if L[i] > L[largest]:
            largest = i
    return largest

for i in xrange(length):
    largest = _max(i)
    if i!=largest:
        temp = L[largest]
        L[largest] = L[i]
        L[i] = temp
return L

pass

和氣泡排序一樣,穩定,原位排序,同樣比較慢。但是它的挑選和置換的方法,確實巧妙的,比如另一個面試提:0~100的已經排序的序列,如何隨機打亂它的順序,當然也可以變成如何最快的生成0~100的隨機數。一種比較好的方法,就是隨機在數組裡挑選元素,然後置換這個資料和最後一個元素的位置,接下來在不包含最後一個元素的數組裡繼續找隨機數,然後繼續後置。

這個shuffle的方法,事實上難倒了很多面試的同學,甚至有些連結串列和樹什麼的都已經用到了。選擇排序的思維可以輕鬆搞定這個問題。

插入排序

     冒泡,選擇和插入,在排序演算法中算最為入門的,雖然簡單,但是也都各自代表著常用的程式設計方法。插入法和之前兩個排序對比,並不在於如何按順序的“取”,而在於如何按數序的“插”。具體方法是,順序地從數組裡獲取資料,並在一個已經排序好的序列裡,插入到對應的位置,當然,最好的放置已經排序的資料的容器,也是這個陣列本身——它的長度是固定的,取了多少資料,就有多少空位。具體Python實現如下:
def insertSort(L):
    assert(type(L)==type(['']))
    length = len(L)
    if length==0 or length==1:
        return L
    for i in xrange(1,length):
        value = L[i]
        j = i-1
        while j>=0 and L[j]<value:
            L[j+1] = L[j]
            j-=1
        L[j+1] = value
return L

前面這三個排序方法,冒泡,選擇和插入,在比較模型中速度很慢,它的原因是這樣的,這三種方法,都不可避免的兩兩排序,也就是任意兩個元素都相互的做過對比,所以它們不管給定的陣列的資料特點,都很穩定的進行對比,複雜度也就是NXN。

但是有沒有方法,不進行兩兩的對比呢?我們知道有些遞迴演算法中,重複的操作可以通過迭代進行傳遞,或者使用容器將之前重複計算的部分儲存起來,對於對比模型中的比較,也是有一些辦法去除一些對比操作。比如去傳遞比較的結果,或者隔離的進行比較等等。一種經典的方法,就是分治法。

分治法並不是一種特定的演算法,就像動態演算法一樣,只是一個解決問題的思路,並不是解決具體問題的方法。它使用在那種不斷的重複去處理同一個小問題的情況下,也就是“分而治之”,大事化小,小事化無。經典的分治法包括歸併排序和快速排序,它們的方法,都是先分,再合。

歸併排序

     偉大的計算機先驅馮諾依曼提出來的一種辦法,說到這裡不得不感嘆一下早起這些科學家的智慧了。歸併排序的“分”和“合”的核心,就是將兩個已經排序好的陣列,合成一個排序的陣列;如何構造兩個已經排序好的陣列呢?既然同樣是排序,依然使用歸併去遞迴處理。

     具體的方法是,每次都將待排序的陣列從中間分成兩個陣列,分別排序這兩個陣列,然後將它們再合併。所以歸併排序的核心在於如何合併兩個已經排序的陣列——這貌似是一個面試題的原題,當然如果瞭解了歸併演算法,這道題也就無所謂了。解決合併的關鍵,一般的方法是準備一個新的空陣列,然後需要三個指標,分別指向兩個待合併陣列和這個新陣列,之後的操作,就是每次比較指向兩個陣列指標位置的指,選擇大的那個放入新陣列指標位置,然後被選擇的陣列指標後移,同時指向新陣列的指標也後移。用指標來解釋並不是什麼好辦法,更確切的描述應該是一個索引位置。當然Python的語法中,是很好解釋的:
def mergeSort(L,start,end):
    assert(type(L)==type(['']))
    length = len(L)
    if length==0 or length==1:
        return L
    def merge(L,s,m,e):
        left = L[s:m+1]
        right = L[m+1:e+1]
        while s<e:
            while(len(left)>0 and len(right)>0):
                if left[0]>right[0]:
                    L[s] = left.pop(0)
                else:
                    L[s] = right.pop(0)
                s+=1
            while(len(left)>0):
                L[s] = left.pop(0)
                s+=1
            while(len(right)>0):
                L[s] = right.pop(0)
                s+=1
            pass

    if start<end:
        mid = int((start+end)/2)
        mergeSort(L,start,mid)
        mergeSort(L,mid+1,end)
        merge(L,start,mid,end)

歸併排序在比較模型中,是速度較快的一種,由於每次都選擇中間位置,所以它是穩定的,而且屬於同一陣列中的資料本身並不需要相互比較,它減少了比較的次數,只需要大約N次這樣的比較,但是由於它需要不停的將陣列等分,所以複雜度是Nlog2(N)。如果真的理解了歸併排序,我想之前提到的那個面試題,肯定不是問題,另外,如果每次並不是兩等分,而是在1/10的位置進行劃分呢,它的複雜度又是多少呢?有時候我面試的時候會這麼去問。下面繼續另一個典型的分治演算法。

快速排序

     作為排序演算法中老大級的快速排序,絕對是很多人的老大難。難就難在虛擬碼到程式碼的轉換上——對與它的“分”和“合”,大部分人都能搞明白:選取待排序陣列中的一個元素,將陣列中比這個元素大的元素作為一部分,而比這個元素小的元素作為另一部分,再將這兩個部分和並。

     如果不考慮空間的申請,也就是不在元素組就行排序的話,這個演算法寫起來就是基本的遞迴呼叫,在python中尤為突出,如下:
def quickSortPython(l):
    assert(type(l)==type(['']))
    length = len(l)
    if length==0 or length==1:
        return l
    if len(l)<=1:
        return l
    left = [i for i in l[1:] if i>l[0]]
    right = [i for i in l[1:] if i<=l[0]]
return quickSortPython(left) +[l[0],]+ quickSortPython(right)
python的這種列表推導的寫法,簡化了程式碼書寫,卻犧牲了資源——這也就是快速排序難的部分,需要在原陣列進行排序,也就是不使用額外的空間。

     解決這個問題的關鍵,是在進行“分”的時候,不只從陣列的一邊進行比較,而是從陣列的兩邊同時進行比較,然後相互補位。程式碼如下:
def quickSort(l,s,e):
    assert(type(l)==type(['']))
    length = len(l)
    if length==0 or length==1:
        return l
    def partition(l,start,end):
        pivot = l[start]
        while start<end-1:
            while end>start and l[end]<pivot:
                end-=1
            l[start] = l[end]
            while end>start and l[start]>pivot:
                start+=1
            l[end] = l[start]
        l[start] = pivot
        return start
        pass
    #random pivot
    def random_partition(l,start,end):
        i = random.randint(start,end)
        temp = l[i]
        l[i] = l[start]
        l[start] = temp
        return partition(l,start,end)

    if s<e:
        m = partition (l,s,e)
        quickSort(l,s,m-1)
        quickSort(l,m+1,e)
    return l
pass

上面的程式碼,有一部分並沒有使用,也就是random_partition這個函式。解釋這個需要先討論一下快速排序的特點。快速排序在原陣列排序,所以空間複雜度很好,但是它的時間消耗呢?它在“分”的時候,和歸併演算法不同的,是歸併演算法選取的是“位置”,而快速排序選取的是“值”。我們能保證每次的位置都是中間位置,但是我們不能保證每次遞迴的時候,每次的Pivot都是最中間的值。

這就導致了快速排序演算法的不穩定性,我們無法確定給定的待排序陣列,如果給定的是一個已經排序的陣列,而每次“分”的時候又選取了它的最值,那麼結果是極端的——不僅每次“分”的時候需要對比N次,而且最終會被劃分為N份,也就是最糟糕的情況——NxN的複雜度。還記得我最後在歸併演算法裡提到的問題嗎,如果按照1/10去分組的情況,其實這裡同樣適用,通過歸併演算法可以知道,最好的情況,是每次都正好分到了中間位置上,這時候的複雜度和歸併演算法一樣,是Nlog2N。

由於我們不可能去改變使用者的輸入,只能從程式角度進行優化,所以在每次選取pivot的時候,隨機的進行選取,從整體的概率角度來將,它的複雜度趨於最優。

上面這幾種,是比較模型中陣列形式進行比較的,如果熟悉資料結構的話,當然會想到陣列的另一個表示方式——樹。使用樹的方法進行對比的排序,這裡討論兩個方法,堆排序和二叉樹排序。

堆排序

     對於沒有學過資料結構的我來說,第一次看到堆排序的時,各種定義和公式,讓我感覺腦袋疼。在這裡討論這種排序的時候,我也不想用那種讓我腦袋疼的辦法。

     首先要知道的是,陣列可以又一個二叉樹來表示,既然是二叉樹,它的表示也就是第一層一個節點,第二層兩個節點,第三層四個節點,第四層八個節點。。。陣列元素的放置位置就是挨著放,第一個元素放在第一層的唯一一個點,第二層的兩個點放接下來的兩個元素,即元素2和3,第三層的四個點,繼續接下來的4個元素,即元素5、6、7、8。。。一直這麼放下去,由於是二叉樹,每次兩分,所以樹的深度是log2N。對於每一個節點,它的根節點在它的下一層,陣列上的位置,就是2倍。         

這就是一個數組的二叉樹形式的理解,這是堆排序的基礎(事實上這並不需要程式碼完成)。接下來的任務,是要把這個二叉樹改造成所謂的堆。堆可以這樣去理解,也就是對於二叉樹來說,父節點的值大於子節點。在上面陣列對應的二叉樹中,我們需要將它改造成一個父節點值大於子節點值的二叉樹。辦法是從後向前的遍歷每個父節點,每個父節點和兩個子節點進行對比,並進行調整,直到形成一個堆——這個時候,根節點的值是最大的。

將這個跟節點的值和陣列最後一個值進行換位,後然繼續上面的調整,形成堆,找到根節點,與倒數第二個值換位。。。以此類推,直到陣列排序完畢。這就是所謂的堆排序,它的python程式碼如下:

def heapSort(L):
    assert(type(L)==type(['']))
    length = len(L)
    if length==0 or length==1:
        return L
    def sift_down(L,start,end):
        root = start
        while True:
            child = 2*root + 1
            if child > end:break
            if child+1 <= end and L[child] > L[child+1]:
                child += 1
            if L[root] > L[child]:
                L[root],L[child] = L[child],L[root]
                root = child
            else:
                break
    for start in range((len(L)-2)/2,-1,-1):
        sift_down(L,start,len(L)-1)

    for end in range(len(L)-1,0,-1):
        L[0],L[end] = L[end],L[0]
        sift_down(L,0,end-1)
    return L
     由於堆排序的堆的高度為log2N,而它每次調整的時候需要對比的次數趨向於N,所以整體的時間複雜度是N*log2N,但是它並不穩定的一種演算法,依賴於給定的待排序陣列。另外,堆排序是在原來的陣列(二叉樹)上進行調整和換位,並沒有申請多餘的空間。和冒泡一類兩兩相比的排序演算法比較,堆排序主要是使用二叉樹構建堆的方式,傳遞的排序結果。

     但是事實上,每次根節點和後面元素置換的同時,二叉樹其他節點並沒有改變,所以我們可以使用額外的空間來記錄這些節點的排列情況,提高排序速度。

二叉樹排序

     這是另一個使用樹進行排序的方法,和堆排序不同的是,這種方法需要這正的構建二叉樹,而不是使用陣列的二叉樹形式。它的核心在與構建二叉樹時的順序以及輸入二叉樹時的順序。

     具體方法是,依次讀取待排序陣列的元素,並將其新增為一個二叉樹的節點;新增的時候,按值的大小放在節點的左右,如果左右節點已經被佔用,則遞迴到子節點進行新增。二叉樹輸出的時候,採取前序遍歷或者後序遍歷的方式輸出。具體的Python程式碼如下:
def binaryTreeSort(l):
    assert(type(l)==type(['']))
    length = len(l)
    if length==0 or length==1:
        return l
    class Node:
        def __init__(self,value=None,left=None,right=None):
            self.__value = value
            self.__left = left
            self.__right = right
        @property
        def value(self):
            return self.__value
        @property
        def left(self):
            return self.__left
        @property
        def right(self):
            return self.__right

    class BinaryTree:
        def __init__(self,root=None):
            self.__root = root
            self.__ret=[]

        @property
        def result(self):
            return self.__ret
        def add(self,parent,node):
            if parent.value>node.value:
                if not parent.left:
                    parent.left = node
                else:
                    self.add(parent.left,node)
                pass
            else:
                if not parent.right:
                    parent.right = node
                else:
                    self.add(parent.right,node)

        def Add(self,node):
            if not self.__root:
                self.__root = node
            else:
                self.add(self.__root, node)

        def show(self,node):
            if not node:
                return
            if node.right:
                self.show(node.right)
            self.__ret.append(node.value)
            if node.left:
                self.show(node.left)

        def Show(self):
            self.show(self.__root)

    b = BinaryTree()
    for i in l:
        b.Add(Node(i))
    b.Show()
return b.result

按之前提到的,我們需要構建節點和二叉樹的物件或者結構,然後進行遍歷排序。本身需要構建二叉樹和遍歷輸入,所以複雜度不如好的直接排序演算法;如果不考慮空間開銷和輸出遍歷,它整體的複雜度還是N*log2N的。所以整體的複雜度介於冒泡演算法等普通排序演算法和快速排序等高階排序演算法之間。

     文中要討論的基於比較模型的排序演算法暫時只討論這麼多,最後討論二叉樹排序,是為了引深一個問題——比較模型的排序演算法複雜度還能在優化嗎?答案是不行的,純比較模型的排序演算法,最好的時間複雜度就是N*log2N了。我們可以改造二叉樹排序來證明這一點,當然還是以大白話為主,我不喜歡繁瑣的公式。

     這個問題的證明,是需要一套模型理論的,即決策樹。我們拋開各種理論,可以簡單的認為,這就是一個二叉樹。這個二叉樹的最終展開,就是所有的決策,在這裡就是一個待排序陣列的所有數序集合,一個N個元素的所有排序個數為N!個。也就是說,從這個二叉樹的根節點開始,最終會有N!個子節點。那麼這個二叉樹的深度,也就是最終執行的次數。實際上,也就是2^h=N!,通過數學推導,可以得到h<N*log2N。推理過程就是兩邊同時取Log,但這不是這裡的重點,重點是基於比較模型的排序演算法,時間複雜度不會小於N*log2N。

如果想要在比較模型上繼續提高排序速度,在模型本身上沒有可以改進的空間,只能使用其他辦法——比如剛才提到的空間換時間的方法,使用其他空間儲存一些重複的對比,或者使用混合的比較模型。

事實上,大多數內建的排序演算法都是混合型的,我們的目的是加快排序的速度,而不是模型本身。一種廣泛採取的排序演算法,是在資料量很大的時候,採取快速排序的方式,而在當分組很小的時候,使用其他穩定的排序方法。這樣的混合型演算法,綜合效果是最好的,也就是一般內建排序使用的方法。

     除了建立在比較模型上的排序演算法,還有一些其他的排序演算法,它們並非比較的去排序,而是其他的方法,基本上很難想到。其中一個比較簡單的,是桶排序。

桶排序

     桶排序是一種計數排序方法,用標記過號碼的桶,去裝待排序陣列中的資料,陣列元素的值對應著桶的編號,最後按桶的標號取出。具體的方式是,獲取待排序陣列的最大值,以這個最大值建立陣列,並將所有元素置為0,遍歷待排序陣列,如果元素的值和桶的編號相等,則桶的值自動加一。遍歷完畢後,按照桶的編號倒序輸入。具體pythono實現如下:
def countSort(l):
    assert(type(l)==type(['']))
    length = len(l)
    if length==0 or length==1:
        return l
    m = max(l)
    ret = []
    storage = [0]*(m+1)
    def count(x):
        storage[x]+=1
    def pop(x):
        tem = storage[x]
        while tem>0:
            ret.append(x)
            tem-=1
    map(lambda x:count(x),l)
    map(lambda x:pop(x),xrange(m,0,-1))
return ret

這種計數排序的方法並不是用於數序很大的情況,而且資料越緊湊排序效果越好。當然這樣的演算法還有可以提高的地方,那就是除了找到待排序陣列的最大值以外,還可以找到它的最小值,以縮短申請的空間。但是提高的效果有限。這樣的演算法對環境要求很高,但是如果滿足這樣的環境,它的排序效果,非常高效。比如百度百科中的一個例子:

海量資料

一年的全國高考考生人數為500 萬,分數使用標準分,最低100 ,最高900 ,沒有小數,你把這500 萬元素的陣列排個序。

分析:對500W資料排序,如果基於比較的先進排序,平均比較次數為O(5000000*log5000000)≈1.112億。但是我們發現,這些資料都有特殊的條件: 100=

def setSort(L):
    assert(type(L)==type(['']))
    length = len(L)
    if length==0 or length==1:
        return L
    BIT = 32
    SHIFT = 5
    MASK = 0x1f
    N = 1+len(L)/BIT
    a = [0]*N
    ret = []

    def clearmap(i):
        a[i>>SHIFT] &= ~(1<<(i & MASK))

    def setmap(i):
        a[i>>SHIFT] |=(1<<(i & MASK))

    def showmap(i):
        for i in xrange(N):
            for j in xrange(32):
                if a[i]&(1<<j): 
                    ret.append(32*i+j)

    map(lambda x: clearmap(x),L)
    map(lambda x: setmap(x),L)
    map(lambda x: showmap(x),xrange(N))
    if ret:
        return ret

這種方法很巧妙,但是使用範圍比較窄。《程式設計珠璣》有過類似的問題,書中就是用這種方法實現的:

假設整數佔32位,1M記憶體可以儲存大概250000個整數,第一個方法就是採用基於磁碟的合併排序演算法,第二個辦法就是將0-9999999切割成40個區間,分40次掃描(10000000/250000),每次讀入250000個在一個區間的整數,並在記憶體中使用快速排序。

儘管這種排序方法使用範圍比較小,但是在演算法設計上,給了我們很大的思考空間——比如雜湊結構的設計,一些面試題可能用涉及到使用陣列去構建雜湊表或者字典,本質上都是用空間定位換取時間。當然這裡不深入討論。

討論完這些常用的排序演算法後,需要對它們進行一下測試,python中的測試和分析還是比較容易的,可以借用unittest直接編寫。當然還需要一些準備:

亂序陣列:

L = range(5000)
random.shuffle(L)

使用裝飾器用來計算時間(語法糖),時間計算上儘可能使用time.clock(),windows系統上它和時鐘時間是一致的,更加精確:

def timeCount(func):
    def wrapper(*arg,**kwarg):
        start = time.clock()
        func(*arg,**kwarg)
        end =time.clock()
        print 'used:', end - start
return wrapper

一個執行的類,用來invode方法,並列印資訊:

class Executor:
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.do()

    @timeCount
    def do(self):
        print '-----start:',self.func,'-----'
        self.ret = self.func(*self.args, **self.kwargs)

    def __del__(self):
        print '-----end-----'

其他一些Python語法說明:

對於兩個值交換的方法,python風格的方式為a,b=b,c;例子中兩種方式都有;
如果需要大容量的陣列,使用range(N)生成;如果只是進行遍歷迭代,請使用xrange(N),它不會佔據很大空間,只是一個迭代工具;

接下來的是對方法的呼叫:

class TestSort(unittest.TestCase):

    def test_01_bubbleSort(self):
        Executor(bubbleSort,L[:])
        pass
    def test_02_selectSort(self):
        Executor(selectSort,L[:])
        pass
    def test_03_insertSort(self):
        Executor(insertSort,L[:])
        pass
    def test_04_mergeSort(self):
        Executor(mergeSort,L[:],0,len(L)-1)
        pass
    def test_05_heapSort(self):
        Executor(heapSort,L[:])
        pass
    def test_06_binaryTreeSort(self):
        Executor(binaryTreeSort,L[:])
        pass
    def test_07_quickSort(self):
        Executor(quickSort,L[:],0,len(L)-1)
        pass
    def test_08_quickSortPython(self):
        Executor(quickSortPython,L[:])
        pass
    def test_09_countSort(self):
        Executor(countSort,L[:])
        pass
    def test_10_setSort(self):
        Executor(setSort,L[:])
        pass
    def test_11_builtinSort(self):
        Executor(sorted,L[:])
        pass

if __name__=="__main__":
    unittest.main()

對於5000的無序資料,我們最終的結果如下:

—–start: —–

used: 1.84792233602

—–end—–

.—–start: —–

used: 1.00796886225

—–end—–

.—–start: —–

used: 1.07336808288

—–end—–

.—–start: —–

used: 0.0483045602555

—–end—–

.—–start: —–

used: 0.0332463558075

—–end—–

.—–start: —–

used: 0.114560597626

—–end—–

—–start: —–

.used: 0.0272368204168

—–end—–

.—–start: —–

used: 0.0161404992862

—–end—–

.—–start: —–

used: 0.00377434774631

—–end—–

.—–start: —–

used: 0.294515750811

—–end—–

.—–start: —–

used: 0.00143839472039

—–end—–

不考慮一些python呼叫上效能的問題,這樣的結果我們可以分析得到:

  • 冒泡,插入和選擇排序的時間消耗最大;
  • 其他純比較模型的排序中,快速排序最快;
  • 使用空間換取時間的話,改造過的快速排序時間上優於初始快速排序,因為免去了資料交換的消耗;
  • 內建的排序方法速度最優

當然,每種方法都還可以優化,甚至優化到和內建排序演算法一樣的速度。