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

http://www.cnblogs.com/wiki-royzhang/p/3614694.htmlhtml

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

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

  1. 雖然專業是數學,可是本身仍是比較討厭繁瑣的公式,因此基本上文章全部的邏輯,我都儘量的用大白話說,但願能說明白;
  2. 語言使用的是Python,緣由是寫的快一些,固然會盡量的拋開一些Python的特色,好比數組處理的時候儘量的不使用一些tuple交換等方式;
  3. 測試算法的時候會用到一些Python編程的技巧,這裏只是簡單的提一下,不作深刻介紹;

 

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

對於上面提到的這些排序算法,我的認爲並無優劣之分,主要看關注點,也就是需求。綜合去看待這些算法,咱們能夠經過如下幾個方面(不徹底)判斷:時間複雜度,空間複雜度,待排序數組長度,待排序數組特色,程序編寫複雜度,實際程序運行環境,實際程序可接受水平等等。說白了就是考慮各類需求和限制條件,程序快不快,佔得空間,排序的數多很少,規律不規律,數據重合的多很少,程序員水平,運行的機器高配仍是低配,客戶或者用戶對運行時間的底線等等。面試

拋開主觀的這些由於,從技術上講,時間複雜度和空間複雜度,是最爲關心的,下面是這些排序算法的一個總結和特色——分類和總結徹底是我的體會,請不要拿教科書上的東西較真。算法

冒泡法:對比模型,原數組上排序,穩定,慢編程

插入法:對比模型,原數組上排序,穩定,慢windows

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

歸併法:對比模型,非原數組上排序,穩定,快數據結構

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

堆排序:對比模型,原數組上排序,不穩定,快

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

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

位圖排序:非對比模型,非原數組上排序,不穩定,快

 

如今開始正經的東西,逐一討論一下這些排序算法;事實上,理解了算法自己的意義,僞代碼很容易寫出來,可是寫代碼是另一回事——算法忽略常量,忽略對於複雜度影響不大的東西,可是寫代碼的時候,卻必須關心這些:

 

冒泡法

入門級算法,可是它的思路很具備特色:循環,兩兩向後比較。具體方法是針對循環中的每一元素,都對它後面的元素循環比較,交換大小值,每次循環「冒」一個最大值(或最小值)放在裏層循環初始的地方;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個元素,即元素五、六、七、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. 內置的排序方法速度最優

固然,每種方法都還能夠優化,甚至優化到和內置排序算法同樣的速度。

相關文章
相關標籤/搜索