比較型排序與非比較型排序算法的總結

排序方法

冒泡排序

In [1]:html

def insert_sort(array):
    # 這個i表示該找第i+1大的數
    for i in xrange(len(array)-1):
        for j in xrange(0,len(array)-i-1):
            if array[j]>array[j+1]:
                array[j+1],array[j] = array[j],array[j+1]

    return array

array = [12,18,29,9,4,1,6]
bubleSort(array)

Out[1]:python

[1, 4, 6, 9, 12, 18, 29]

In [21]:算法

def bubleSort2(array):
    # 這個i該找存在這個下標上的數了
    for i in xrange(len(array)-1,0,-1):
        for j in xrange(0,i):
            if array[j]>array[j+1]:
                array[j+1],array[j] = array[j],array[j+1]

    return array

array = [12,18,29,9,4,1,6]
bubleSort(array)

Out[21]:shell

[1, 4, 6, 9, 12, 18, 29]

複雜度分析

平均狀況與最壞狀況均爲 $O(n^2)$, 使用了 temp 做爲臨時交換變量,空間複雜度爲 $O(1)$.api

選擇排序

核心:不斷地選擇剩餘元素中的最小者。數組

  1. 找到數組中最小元素並將其和數組第一個元素交換位置。緩存

  2. 在剩下的元素中找到最小元素並將其與數組第二個元素交換,直至整個數組排序。數據結構

性質:app

  1. 比較次數=(N-1)+(N-2)+(N-3)+...+2+1~N^2/2dom

  2. 交換次數=N

  3. 運行時間與輸入無關

  4. 數據移動最少

In [9]:

def selectSort(array):
    # 這裏i記得是此次該決定誰存到這個下標上了
    for i in xrange(len(array)-1):
        min_ind = i
        for j in xrange(i+1,len(array)):
            if array[min_ind]>array[j]:
                min_ind = j
        if min_ind != i:
            array[min_ind],array[i] = array[i],array[min_ind]

    return array

array = [12,18,29,9,4,1,6]
selectSort(array)

Out[9]:

[1, 4, 6, 9, 12, 18, 29]

In [1]:

def selectSort2(array):
    # 這裏i記得是此次該決定誰存到這個下標上了
    for i in xrange(len(array)-1,0,-1):
        max_ind = i
        #print i
        for j in xrange(0,i):
            #print j,array[max_ind],array[j]
            if array[max_ind]<array[j]:
                max_ind = j
        if max_ind != i:
            array[max_ind],array[i] = array[i],array[max_ind]
        #print array
    return array

array = [12,18,29,9,4,1,6]
selectSort2(array)

Out[1]:

[1, 4, 6, 9, 12, 18, 29]

插入排序

In [1]:

def insertSort(alist):
    for i,item_i in enumerate(alist):
        # 開始肯定第i個元素應該在的位子;
        index = i-1
        # 從前一個元素開始,遍歷到0,
        while index>=0 and alist[index]>item_i:
            alist[index+1]=alist[index]
            index-=1
        alist[index+1] = item_i
    return alist

alist = [12,18,29,9,4,1,6]
insertSort(alist)

Out[1]:

[1, 4, 6, 9, 12, 18, 29]

希爾排序

其實就是分組插入排序

In [7]:

def shellSort(alist):
    n = len(alist)
    gap = int(round(n/2)) # 去長度的通常做爲gap
    while gap>0:
        # print gap
        for i in xrange(gap,n):
            # 把i插入該列前面已經排好的地方 
            temp = alist[i]
            while i-gap >=0 and alist[i-gap]>temp:
                alist[i]=alist[i-gap]
                i-=gap
            alist[i] = temp
        gap = int(round(gap/2)) # 獲得新的步長
    return alist

array = [12,18,29,9,4,1,6]
shellSort(array)

Out[7]:

[1, 4, 6, 9, 12, 18, 29]

歸併排序

核心:將兩個有序對數組歸併成一個更大的有序數組。一般作法爲遞歸排序,並將兩個不一樣的有序數組歸併到第三個數組中。歸併排序是一種典型的分治應用。

分兩步:分段 / 合併

時間複雜度爲$ O(NlogN)$, 使用了等長的輔助數組,空間複雜度爲 $O(N)$。

In [5]:

def mergeSort(alist):
    if(len(alist) <=1 ):
        return alist
    mid   = len(alist)/2
    left  = mergeSort(alist[:mid])
    right = mergeSort(alist[mid:])
    return combine(left,right)

def combine(left,right):
    alist = []
    l = 0
    r = 0

    while l<len(left) and r<len(right):
        if left[l]<=right[r]:
            alist.append(left[l])
            l+=1
        else:
            alist.append(right[r])
            r+=1

    while l<len(left):
        alist.append(left[l])
        l+=1

    while r<len(right):
        alist.append(right[r])
        r+=1

    return alist

unsortedArray = [6, 5,27, 3, 41,1, 8, 12,7, 2, 4,18]
mergeSort(unsortedArray)

Out[5]:

[1, 2, 3, 4, 5, 6, 7, 8, 12, 18, 27, 41]

quick-sort 快速排序

核心:快排是一種採用分治思想的排序算法,大體分爲三個步驟。

  1. 定基準——首先隨機選擇一個元素最爲基準

  2. 劃分區——全部比基準小的元素置於基準左側,比基準大的元素置於右側

  3. 遞歸調用——遞歸地調用此切分過程

out-in-place 非原地排序

『遞歸 + 非原地排序』的實現雖然簡單易懂,可是如此一來『快速排序』便再也不是最快的通用排序算法了,由於遞歸調用過程當中非原地排序須要生成新數組,空間複雜度頗高。list comprehension 大法雖然好寫,可是用在『快速排序』算法上就不是那麼可取了。

In [1]:

def qsort1(alist):
    if len(alist)<=1:
        return alist
    privot = alist[0]

    # 下面的作法用到了list的合併,這些語言底層其實沒什麼計數含量
    # 徹底能夠用高級語言的庫取代,我沒有必要去實現這些底層
    return qsort1([item for item in alist[1:] if item<privot]) + \
            [privot] + \
            qsort1([item for item in alist[1:] if item>privot])

unsortedArray = [6, 5, 3, 1, 8, 7, 2, 4]
print(qsort1(unsortedArray))
[1, 2, 3, 4, 5, 6, 7, 8]

非原地排序複雜度分析

在最好的狀況下,快速排序的基準元素正好是正好是整個數組的中位數,能夠近似爲二分,最好狀況下遞歸的層數爲$logn$.

那麼對於遞歸非原地快排來講,最好的狀況下,每往下遞歸一層,所須要的存儲空間是上一層的一半,完成最底層調用後就向上返回執行出棧操做,故並不須要保存本層全部元素的值(空間重用),因此須要的存儲空間就是 $ sum^{log_2n}_{i=0} frac{n}{2^i} = n2(1-frac{1}{n}) = 2n$

hehe

那麼最壞的狀況就是,每次選的基準元素就是整個數組的最值,這樣遞歸的層數就是n-1次

那麼對於遞歸非原地快排來講,最壞的狀況是第i層的空間位n-i+1,總的空間位$ sum^{n-1}_{i=0} n-i+1= frac{n(n+1)}{2}=O(n^2) $

    • *

in-palce 原地排序

過程當中控制4個下標:

  • l 待排序部分的下界

  • u 待排序部分的上界

  • m 不大於基準元素的最後一個位置,在遍歷結束後與基準元素位置交換

  • i 遍歷的元素下標,從l到u

In [2]:

def qsort2(alist,l,u):
    # 遞歸結束條件
    if l>=u:
        return 

    # 初始化幾個下標
    m=l

    privot = alist[l]
    for i in xrange(l+1,u+1):
        if alist[i]<privot:
            m+=1
            # 把比基準元素小的挪到前半部分
            alist[m],alist[i] = alist[i],alist[m]

    alist[m],alist[l] = alist[l],alist[m]

    qsort2(alist,l,m-1) #
    qsort2(alist,m+1,u) #必須這樣不然若是序列已是有序的就會進入死循環

    return alist

unsortedArray = [6, 5, 3, 1, 8, 6, 2, 4,6]
qsort2(unsortedArray, 0, len(unsortedArray) - 1)
unsortedArray

Out[2]:

[1, 2, 3, 4, 5, 6, 6, 6, 8]

Two-way partitioning 兩路分塊

選擇一個基準元素放在開頭,在兩端開始遍歷,當l端找到大於基於元素的位置停下來,當r端找到不小於基準元素的位置停下來,二者進行交換,直到二者交叉或者相等,那麼這個它r的位置就是基準元素的位置

In [31]:

import random
def qsort3(alist,lower,upper):
    if(lower >= upper):
        return 
    idx = random.randint(lower,upper)
    alist[lower],alist[idx]=alist[idx],alist[lower]
    privot = alist[lower]
    left,right = lower+1,upper
    while left<=right:  # 等於的時候依然要繼續由於還沒劃定邊界,最後退出循環的時候必定是交叉的,r的右邊都是不小於基準的,l的左邊都是小於基準的
        while left <= right and alist[left]<privot:
            left+=1
        while right >=left and alist[right]>=privot:
            right-=1

        # 循環到這裏要麼是交叉了,要麼是找到合適的位置了
        if left<right:
            alist[left],alist[right] = alist[right],alist[left]
            left+=1
            right-=1
        #print alist
    alist[lower],alist[right] = alist[right],alist[lower]
    qsort3(alist,lower,right-1)
    qsort3(alist,right+1,upper)

unsortedArray = [6, 5, 3,4, 1, 7,8, 7, 3,2, 4]
qsort3(unsortedArray, 0, len(unsortedArray) - 1)
unsortedArray

Out[31]:

[1, 2, 3, 3, 4, 4, 5, 6, 7, 7, 8]

隨機分區

若是待排序列正好是順序的時候,整個的遞歸將會達到最大遞歸深度(序列的長度)。而實際上在操做的時候,當列表長度大於1000(理論值)的時候,程序會中斷,報超出最大遞歸深度的錯誤(maximum recursion depth exceeded)。在查過資料後咱們知道,Python在默認狀況下,最大遞歸深度爲1000(理論值,其實真實狀況下,只有995左右,各個系統這個值的大小也不一樣)。 這個問題有兩種解決方案, 1)從新設置最大遞歸深度,採用如下方法設置:

import sys

sys.setrecursionlimit(99999)

2)第二種方法就是採用另一個版本的分區函數,稱爲隨機化分區函數。因爲以前咱們的選擇都是子序列的第一個數,所以對於特殊狀況的健壯性就差了許多。如今咱們隨機從子序列選擇基準元素,這樣能夠減小對特殊狀況的差錯率。

i = random.randint(p, r)

Heap Sort - 堆排序

堆排序的實現過程分爲兩個子過程。第一步爲取出大根堆的根節點(當前堆的最大值), 因爲取走了一個節點,故第二步須要對餘下的元素從新建堆。從新建堆後繼續取根節點,循環直至取完全部節點,此時數組已經有序。

堆的實現經過構造二叉堆(binary heap),實爲二叉樹的一種;因爲其應用的廣泛性,當不加限定時,均指該數據結構的這種實現。這種數據結構具備如下性質。

  • 任意節點小於(或大於)它的全部後裔,最小元(或最大元)在堆的根上(堆序性)。

  • 堆老是一棵徹底樹。即除了最底層,其餘層的節點都被元素填滿,且最底層儘量地從左到右填入。

須要注意的是,堆是用數組保存的,因此根結點在下標爲0的位子,又由於二叉堆是徹底二叉樹,因此下標位n/2的地方是二叉樹裏面最後一個非葉子結點的地方(有子結點),因此在創建大頂堆的時候,自底向上用下濾的方法創建各個小樹,建每一個小樹用時$log(i)$,共建了$frac{n}{2} $個小樹

創建大頂堆有兩種方法 :

  1. 自底向上,先隨便把這些數據放在一個數組裏面,而後從最深一層的節點開始自底向上下濾操做

  2. 自頂向下,先建一個空的堆,而後慢慢在最後位置插入新元素,並讓它向上濾

步驟:

  • 構造最大堆(Build_Max_Heap):若數組下標範圍爲0~n,考慮到單獨一個元素是大根堆,則從下標n/2開始的元素均爲大根堆。因而只要從n/2-1開始,向前依次構造大根堆,這樣就能保證,構造到某個節點時,它的左右子樹都已是大根堆。

  • 堆排序(HeapSort):因爲堆是用數組模擬的。獲得一個大根堆後,數組內部並非有序的。所以須要將堆化數組有序化。思想是移除根節點,並作最大堆調整的遞歸運算。第一次將heap[0]與heap[n-1]交換,再對heap[0...n-2]作最大堆調整。第二次將heap[0]與heap[n-2]交換,再對heap[0...n-3]作最大堆調整。重複該操做直至heap[0]和heap[1]交換。因爲每次都是將最大的數併入到後面的有序區間,故操做完後整個數組就是有序的了。

  • 最大堆調整(Max_Heapify):該方法是提供給上述兩個過程調用的。目的是將堆的末端子節點做調整,使得子節點永遠小於父節點 。

最差時間複雜度 $O ( n log ⁡ n )¥ 最優時間複雜度$O ( n log ⁡ n ) 平均時間複雜[$Θ ( n log ⁡ n ) $

堆排在空間比較小(嵌入式設備和手機)時特別有用,可是由於現代系統每每有較多的緩存,堆排序沒法有效利用緩存,數組元素不多和相鄰的其餘元素比較,故緩存未命中的機率遠大於其餘在相鄰元素間比較的算法。可是在海量數據的排序下又從新發揮了重要做用,由於它在插入操做和刪除最大元素的混合動態場景中能保證對數級別的運行時間

In [17]:

def heap_sort(alist):
    # 下面是下濾的操做,下濾的前提是保證下面的子樹是已經建好的子樹 
    def sink(alist,i,length):
        largest = i

        if (2*i+1)<length and alist[2*i+1]>alist[largest]:
            largest = 2*i+1
        if (2*i+2)<length and alist[2*i+2]>alist[largest]:
            largest = 2*i+2

        if largest != i:
            alist[largest],alist[i] = alist[i],alist[largest]
            sink(alist,largest,length)

    # 下面開始堆排序的操做,從最深一層的父節點開始循環,這樣能夠保證下濾的時候,下面都是排好序的字節點

    # 先創建大頂堆 
    # n/2表示最後一個非葉結點 
    for i in xrange(len(alist)/2,-1,-1): #倒序到0,因此第二位設置爲-1
        sink(alist,i,len(alist))

    # 而後不斷底摘掉頂
    length = len(alist)
    for i in xrange(0,len(alist)):
        alist[0],alist[length-1] = alist[length-1],alist[0]
        length-=1
        sink(alist,0,length)

    return alist

unsortedArray = [6, 5, 3,13, 1, 8,28, 7, 2, 4]
sortedArray = heap_sort(unsortedArray)
sortedArray

Out[17]:

[1, 2, 3, 4, 5, 6, 7, 8, 13, 28]

堆排序在 top K 問題中使用比較頻繁。堆排序是採用二叉堆的數據結構來實現的,雖然實質上仍是一維數組。二叉堆是一個近似徹底二叉樹 。

堆排序解決Top K問題

In [38]:

import heapq

heapq.nlargest(2,[9,5,12,38,32,21,1,-23])
heapq.nsmallest(2,[9,5,12,38,32,21,1,-23])

Out[38]:

[-23, 1]

[經典排序算法](http://www.cnblogs.com/kkun/a...

對於比較排序算法,咱們知道,能夠把全部可能出現的狀況畫成二叉樹(決策樹模型),對於n個長度的列表,其決策樹的高度爲h,葉子節點就是這個列表亂序的所有可能性爲n!,而咱們知道,這個二叉樹的葉子節點不會超過2^h,因此有2^h>=n!,取對數,能夠知道,h>=logn!,這個是近似於O(nlogn)。也就是說比較排序算法的最好性能就是O(nlgn)。

那有沒有線性時間,也就是時間複雜度爲O(n)的算法呢?答案是確定的。不過因爲排序在實際應用中算法實際上是很是複雜的。這裏只是討論在一些特殊情形下的線性排序算法。特殊情形下的線性排序算法主要有計數排序,桶排序和基數排序。這裏只簡單說一下計數排序。

用Python實現常見排序算法

    • *

桶排序Bucket sort

經典排序算法 - 桶排序Bucket sort

補充說明三點

  1. 桶排序是穩定的

  2. 桶排序是常見排序裏最快的一種,比快排還要快…大多數狀況下

  3. 桶排序很是快,可是同時也很是耗空間,基本上是最耗空間的一種排序算法

但桶排序是有要求的,就是數組元素隸屬於固定(有限的)的區間,如範圍爲[0-9] (考試分數爲1-100等),有時候想着感受桶排序的第一步入桶操做和哈希表的構造有點像呢

假設數據分佈在[0,100)之間,每一個桶內部用鏈表表示,在數據入桶的同時插入排序。而後把各個桶中的數據合併。

In [20]:

import numpy as np

def bucket_sort(alist):
    # 下面是入桶的過程
    dic = dict()
    for item in alist:
        if item in dic:
            dic[item].append(item)
        else:
            dic[item]=[]
            dic[item].append(item)
    # 下面是歸併的過程

    result = []    
    for key,items in dic.items():
        for i in items:
            result.append(i)
    return result

unsortedArray = [6, 5, 3,5, 6, 5,3, 7, 3, 6]
sortedArray = bucket_sort(unsortedArray)
print sortedArray
[3, 3, 3, 5, 5, 5, 6, 6, 6, 7]

In [59]:

import heapq
def bucket_sort2(alist):
    # 下面是入桶的過程

    k = heapq.nlargest(1,alist)[0]+1
    dic = [None for i in range(k)]
    for item in alist:
        if dic[item]!=None:
            dic[item].append(item)
        else:
            dic[item]=[]
            dic[item].append(item)
    # 下面是歸併的過程

    result = []    
    for i in xrange(0,k):
        while dic[i]:
            result.append(dic[i].pop())
    return result

unsortedArray = [6, 5, 3,5, 6, 5,3, 7, 3, 6]
sortedArray = bucket_sort2(unsortedArray)
print sortedArray
[3, 3, 3, 5, 5, 5, 6, 6, 6, 7]

計數排序

計數排序是創建在對待排序列這樣的假設下:假設待排序列都是正整數(作下標來用)。首先,聲明一個新序列list2,序列的長度爲待排序列中的最大數(能夠經過創建大頂堆得到)。遍歷待排序列,對每一個數,設其大小爲i,list2[i]++,這至關於計數大小爲i的數出現的次數。而後在依次加上前面的數,就知道最終這個數應該排在哪裏了,而後,申請一個list,長度等於待排序列的長度(這個是輸出序列,由此能夠看出計數排序不是就地排序算法),倒序遍歷待排序列(倒排的緣由是爲了保持排序的穩定性,即大小相同的兩個數在排完序後位置不會調換),假設當前數大小爲i,list[list2[i]-1] = i,同時list2[i]自減1(這是由於這個大小的數已經輸出一個,因此大小要自減)。因而,計數排序的源代碼以下。

In [48]:

import heapq
def count_sort(alist):

    k = heapq.nlargest(1,alist)[0]+1 ;# 算出alist中的最大數,解決Top k 問題

    alist2 = [0 for i in range(k)] # 創建長度爲k初始化位0的數組
    alist3 = [0 for i in range(len(alist))] # 排序後的結果存在這裏

    # 下面開始計數 
    for item in alist:
        alist2[item]+=1

    # 下面開始統計前面的,有點像積分操做 哈哈哈 算出來的就是最終的位置
    for i in range(1,k):
        alist2[i] += alist2[i-1]

    # 下面開始倒序把數填在它該在的地方去
    for item in alist[::-1]: #倒序遍歷
        #print item
        alist3[alist2[item]-1] = item
        alist2[item]-=1
    return alist3

unsortedArray = [6, 5, 3,5, 6, 5,3, 7, 3, 6]
sortedArray = count_sort(unsortedArray)
print sortedArray
[3, 3, 3, 5, 5, 5, 6, 6, 6, 7]

基數排序

流程

維基百科-基數排序

是一種非比較型整數排序算法,其原理是將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序也不是隻能使用於整數。

它是這樣實現的:將全部待比較數值(正整數)統一爲一樣的數位長度,數位較短的數前面補零。而後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成之後,數列就變成一個有序序列。

複雜度分析

基數排序的時間複雜度是O(k·n),其中n是排序元素個數,k是數字位數。注意這不是說這個時間複雜度必定優於O(n·log(n)),k的大小取決於數字位的選擇(好比比特位數),和待排序數據所屬數據類型的全集的大小;k決定了進行多少輪處理,而n是每輪處理的操做數目。

k約等於logB(N)

因此,基數排序的平均時間T就是:

T~= logB(N)·n

其中前一項是一個與輸入數據無關的常數,固然該項不必定小於logn

若是考慮和比較排序進行對照,基數排序的形式複雜度雖然不必定更小,但因爲不進行比較,所以其基本操做的代價較小,並且在適當選擇的B之下,k通常不大於logn,因此基數排序通常要快過基於比較的排序,好比快速排序。

例如

經典排序算法 - 基數排序Radix sort

待排序數組[62,14,59,88,16]簡單點五個數字

分配10個桶,桶編號爲0-9,以個位數數字爲桶編號依次入桶,變成下邊這樣

| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶編號

將桶裏的數字順序取出來,

輸出結果:[62,14,16,88,59]

再次入桶,不過此次以十位數的數字爲準,進入相應的桶,變成下邊這樣:

因爲前邊作了個位數的排序,因此當十位數相等時,個位數字是由小到大的順序入桶的,就是說,入完桶仍是有序

| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶編號

由於沒有大過100的數字,沒有百位數,因此到這排序完畢,順序取出便可

最後輸出結果:[14,16,59,62,88]

In [17]:

import math  # 求log和上約界

def radix_sort(alist,radix=10):
    # 首先計算入桶的次數 
    k = int( math.ceil(math.log(max(alist),radix)) )  # math.ceil返回的是整數

    # 而後每一個數k次進桶
    ## 首先定義一個能夠接受list的桶的序列
    result = [ i for i in alist]
    bucket = [[] for i in range(radix) ]  # 按照基數大小肯定桶的個數多少 
    for i in range(k):
        for item in result:  # 每次使用上次排完序的內容
            # 求當前位上應該對應的桶號;
            ## 這裏沒必要擔憂位數多少的問題,由於位數不夠下面求完就是0
            bucket[ item%(radix**(i+1))/(radix**i) ].append(item)

        # 而後合併各個桶
        del result[:]
        for small_bucket in bucket:
            result.extend(small_bucket)
        bucket = [[] for i in range(radix)]
    return result

unsortedArray = [26, 25, 23,5, 36, 5,3, 17, 3, 6,12,23,34,35,]
sortedArray = radix_sort(unsortedArray,10)
print sortedArray
[3, 3, 5, 5, 6, 12, 17, 23, 23, 25, 26, 34, 35, 36]
相關文章
相關標籤/搜索