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

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

pos 算法復雜度 信息 環比 數組長度 暫時 並且 直接排序 win

作者:waterxi

原文鏈接

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

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

  1. 雖然專業是數學,但是自己還是比較討厭繁瑣的公式,所以基本上文章所有的邏輯,我都盡可能的用大白話說,希望能說明白;
  2. 語言使用的是Python,原因是寫的快一些,當然會盡可能的拋開一些Python的特點,比如數組處理的時候盡可能的不使用一些tuple交換等方式;
  3. 測試算法的時候會用到一些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=<score<=900。那麽我們就可以考慮桶排序這樣一個“投機取巧”的辦法、讓其在毫秒級別就完成500萬排序。

方法:創建801(900-100)個桶。將每個考生的分數丟進f(score)=score-100的桶中。這個過程從頭到尾遍歷一遍數據只需要500W次。然後根據桶號大小依次將桶中數值輸出,即可以得到一個有序的序列。而且可以很容易的得到100分有***人,501分有***人。

實際上,桶排序對數據的條件有特殊要求,如果上面的分數不是從100-900,而是從0-2億,那麽分配2億個桶顯然是不可能的。所以桶排序有其局限性,適合元素值集合並不大的情況。

-------------------------------------------------

當給定的待排序數組沒有重復數據,而且數據量非常大,即屬於桶排序情況的一種極端特殊的情況,使用桶排序的話,我們需要很大的空間消耗,並且桶中的計數,對於結果意義不大。

我們可以將其改造和優化,優化的部分就是如果減少空間的消耗,而不關心桶的計數——數量只有0或者1,而不是>1。0或者1的特點,給了我們啟發——我們可以使用計算機的位進行存儲。這種方法就是位圖排序。

位圖排序

如果使用其他語言,可能很難想到這種方法。它使用位圖的方法記錄待排序數組中元素的數據,在一些高級的編程語言中,開辟二維數據空間很容易做到——但是在C中,位圖是使用位操作實現的,這大大的提高了空間的使用率,而且,使用位運算,效率遠大於除和余的操作。

白話的解釋這種算法,就是在桶排序的大思路下,不在使用桶記錄數據,而是使用bit位。如果申請一個int的二維數組,每個位置上只放1和0,那麽太浪費空間了——在32位的操作系統上,一個Int完全可以存儲32位的標記,算法的核心就是如何找到這個位置。為了更好的使用位圖的方式,這裏我采用位運算進行編寫,對應的除和余可以達到同樣的操作,但是性能很低,python的代碼如下:

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語法說明:

  1. 對於兩個值交換的方法,python風格的方式為a,b=b,c;例子中兩種方式都有;
  2. 如果需要大容量的數組,使用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: <function bubbleSort at 0x01DDF2F0> -----

used: 1.84792233602

-----end-----

.-----start: <function selectSort at 0x01DDF430> -----

used: 1.00796886225

-----end-----

.-----start: <function insertSort at 0x01DDF470> -----

used: 1.07336808288

-----end-----

.-----start: <function mergeSort at 0x01DDF4B0> -----

used: 0.0483045602555

-----end-----

.-----start: <function heapSort at 0x01DDF4F0> -----

used: 0.0332463558075

-----end-----

.-----start: <function binaryTreeSort at 0x01DDF530> -----

used: 0.114560597626

-----end-----

-----start: <function quickSort at 0x01DDF570> -----

.used: 0.0272368204168

-----end-----

.-----start: <function quickSortPython at 0x01DDF5B0> -----

used: 0.0161404992862

-----end-----

.-----start: <function countSort at 0x01DDF5F0> -----

used: 0.00377434774631

-----end-----

.-----start: <function setSort at 0x01DDF630> -----

used: 0.294515750811

-----end-----

.-----start: <built-in function sorted> -----

used: 0.00143839472039

-----end-----

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

  1. 冒泡,插入和選擇排序的時間消耗最大;
  2. 其他純比較模型的排序中,快速排序最快;
  3. 使用空間換取時間的話,改造過的快速排序時間上優於初始快速排序,因為免去了數據交換的消耗;
  4. 本示例中,桶排序(計數排序)速度優於其他幾種示例排序;
  5. 內置的排序方法速度最優

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

原文鏈接

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