排序算法 python實現

1、排序的基本概念和分類算法

所謂排序,就是使一串記錄,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操做。排序算法,就是如何使得記錄按照要求排列的方法。sql

排序的穩定性:
通過某種排序後,若是兩個記錄序號同等,且二者在原無序記錄中的前後秩序依然保持不變,則稱所使用的排序方法是穩定的,反之是不穩定的。shell

內排序和外排序
內排序:排序過程當中,待排序的全部記錄所有放在內存中
外排序:排序過程當中,使用到了外部存儲。
一般討論的都是內排序。數組

影響內排序算法性能的三個因素:緩存

時間複雜度:即時間性能,高效率的排序算法應該是具備儘量少的關鍵字比較次數和記錄的移動次數
空間複雜度:主要是執行算法所須要的輔助空間,越少越好。
算法複雜性。主要是指代碼的複雜性。

根據排序過程當中藉助的主要操做,可把內排序分爲:ide

插入排序
交換排序
選擇排序
歸併排序

按照算法複雜度可分爲兩類:函數

簡單算法:包括冒泡排序、簡單選擇排序和直接插入排序
改進算法:包括希爾排序、堆排序、歸併排序和快速排序

如下的七種排序算法只是全部排序算法中最經典的幾種,不表明所有。
2、 冒泡排序性能

冒泡排序(Bubble sort):時間複雜度O(n^2)
交換排序的一種。其核心思想是:兩兩比較相鄰記錄的關鍵字,若是反序則交換,直到沒有反序記錄爲止。測試

其實現細節能夠不一樣,好比下面3種:優化

最簡單排序實現:bubble_sort_simple
冒泡排序:bubble_sort
改進的冒泡排序:bubble_sort_advance

冒泡排序算法

class SQList:
def init(self, lis=None):
self.r = lis

def swap(self, i, j):
    """定義一個交換元素的方法,方便後面調用。"""
    temp = self.r[i]
    self.r[i] = self.r[j]
    self.r[j] = temp

def bubble_sort_simple(self):
    """
    最簡單的交換排序,時間複雜度O(n^2)
    """
    lis = self.r
    length = len(self.r)
    for i in range(length):
        for j in range(i+1, length):
            if lis[i] > lis[j]:
                self.swap(i, j)

def bubble_sort(self):
    """
    冒泡排序,時間複雜度O(n^2)
    """
    lis = self.r
    length = len(self.r)
    for i in range(length):
        j = length-2
        while j >= i:
            if lis[j] > lis[j+1]:
                self.swap(j, j+1)
            j -= 1

def bubble_sort_advance(self):
    """
    冒泡排序改進算法,時間複雜度O(n^2)
    設置flag,當一輪比較中未發生交換動做,則說明後面的元素其實已經有序排列了。
    對於比較規整的元素集合,可提升必定的排序效率。
    """
    lis = self.r
    length = len(self.r)
    flag = True
    i = 0
    while i < length and flag:
        flag = False
        j = length - 2
        while j >= i:
            if lis[j] > lis[j + 1]:
                self.swap(j, j + 1)
                flag = True
            j -= 1
        i += 1

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4,1,7,3,8,5,9,2,6])
# sqlist.bubble_sort_simple()
# sqlist.bubble_sort()
sqlist.bubble_sort_advance()
print(sqlist)

3、簡單選擇排序

簡單選擇排序(simple selection sort):時間複雜度O(n^2)
經過n-i次關鍵字之間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第i(1<=i<=n)個記錄進行交換。

通俗的說就是,對還沒有完成排序的全部元素,從頭至尾比一遍,記錄下最小的那個元素的下標,也就是該元素的位置。再把該元素交換到當前遍歷的最前面。其效率之處在於,每一輪中比較了不少次,但只交換一次。所以雖然它的時間複雜度也是O(n^2),但比冒泡算法仍是要好一點。

簡單選擇排序

class SQList:
def init(self, lis=None):
self.r = lis

def swap(self, i, j):
    """定義一個交換元素的方法,方便後面調用。"""
    temp = self.r[i]
    self.r[i] = self.r[j]
    self.r[j] = temp

def select_sort(self):
    """
    簡單選擇排序,時間複雜度O(n^2)
    """
    lis = self.r
    length = len(self.r)
    for i in range(length):
        minimum = i
        for j in range(i+1, length):
            if lis[minimum] > lis[j]:
                minimum = j
        if i != minimum:
            self.swap(i, minimum)

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0])
sqlist.select_sort()
print(sqlist)

4、直接插入排序

直接插入排序(Straight Insertion Sort):時間複雜度O(n^2)
基本操做是將一個記錄插入到已經排好序的有序表中,從而獲得一個新的、記錄數增1的有序表。

直接插入排序

class SQList:
def init(self, lis=None):
self.r = lis

def insert_sort(self):
    lis = self.r
    length = len(self.r)
    # 下標從1開始
    for i in range(1, length):
        if lis[i] < lis[i-1]:
            temp = lis[i]
            j = i-1
            while lis[j] > temp and j >= 0:
                lis[j+1] = lis[j]
                j -= 1
            lis[j+1] = temp

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0])
sqlist.insert_sort()
print(sqlist)

該算法須要一個記錄的輔助空間。最好狀況下,當原始數據就是有序的時候,只須要一輪對比,不須要移動記錄,此時時間複雜度爲O(n)。然而,這基本是幻想。
image_1b2pgl1061h8s6mdg4416vm6f19.png-119.9kB
5、希爾排序

希爾排序(Shell Sort)是插入排序的改進版本,其核心思想是將原數據集合分割成若干個子序列,而後再對子序列分別進行直接插入排序,使子序列基本有序,最後再對全體記錄進行一次直接插入排序。

這裏最關鍵的是跳躍和分割的策略,也就是咱們要怎麼分割數據,間隔多大的問題。一般將相距某個「增量」的記錄組成一個子序列,這樣才能保證在子序列內分別進行直接插入排序後獲得的結果是基本有序而不是局部有序。下面的例子中經過:increment = int(increment/3)+1來肯定「增量」的值。

希爾排序的時間複雜度爲:O(n^(3/2))

希爾排序

class SQList:
def init(self, lis=None):
self.r = lis

def shell_sort(self):
    """希爾排序"""
    lis = self.r
    length = len(lis)
    increment = len(lis)
    while increment > 1:
        increment = int(increment/3)+1
        for i in range(increment+1, length):
            if lis[i] < lis[i - increment]:
                temp = lis[i]
                j = i - increment
                while j >= 0 and temp < lis[j]:
                    lis[j+increment] = lis[j]
                    j -= increment
                lis[j+increment] = temp

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0,123,22])
sqlist.shell_sort()
print(sqlist)

6、堆排序

堆是具備下列性質的徹底二叉樹:
每一個分支節點的值都大於或等於其左右孩子的值,稱爲大頂堆;
每一個分支節點的值都小於或等於其作右孩子的值,稱爲小頂堆;
所以,其根節點必定是全部節點中最大(最小)的值。
image_1b3bbf6pq1f847cq6uf70jpte13.png-142.4kB
若是按照層序遍歷的方式(廣度優先)給節點從1開始編號,則節點之間知足以下關係:
image_1b3bbe6ml1s5r6cq1dd41hctuaim.png-36.9kB

堆排序(Heap Sort)就是利用大頂堆或小頂堆的性質進行排序的方法。堆排序的整體時間複雜度爲O(nlogn)。(下面採用大頂堆的方式)

其核心思想是:將待排序的序列構形成一個大頂堆。此時,整個序列的最大值就是堆的根節點。將它與堆數組的末尾元素交換,而後將剩餘的n-1個序列從新構形成一個大頂堆。反覆執行前面的操做,最後得到一個有序序列。

堆排序

class SQList:
def init(self, lis=None):
self.r = lis

def swap(self, i, j):
    """定義一個交換元素的方法,方便後面調用。"""
    temp = self.r[i]
    self.r[i] = self.r[j]
    self.r[j] = temp

def heap_sort(self):
    length = len(self.r)
    i = int(length/2)
    # 將原始序列構形成一個大頂堆
    # 遍歷從中間開始,到0結束,其實這些是堆的分支節點。
    while i >= 0:
        self.heap_adjust(i, length-1)
        i -= 1
    # 逆序遍歷整個序列,不斷取出根節點的值,完成實際的排序。
    j = length-1
    while j > 0:
        # 將當前根節點,也就是列表最開頭,下標爲0的值,交換到最後面j處
        self.swap(0, j)
        # 將發生變化的序列從新構形成大頂堆
        self.heap_adjust(0, j-1)
        j -= 1

def heap_adjust(self, s, m):
    """核心的大頂堆構造方法,維持序列的堆結構。"""
    lis = self.r
    temp = lis[s]
    i = 2*s
    while i <= m:
        if i < m and lis[i] < lis[i+1]:
            i += 1
        if temp >= lis[i]:
            break
        lis[s] = lis[i]
        s = i
        i *= 2
    lis[s] = temp

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 123, 22])
sqlist.heap_sort()
print(sqlist)

堆排序的運行時間主要消耗在初始構建堆和重建堆的反覆篩選上。
其初始構建堆時間複雜度爲O(n)。
正式排序時,重建堆的時間複雜度爲O(nlogn)。
因此堆排序的整體時間複雜度爲O(nlogn)。

堆排序對原始記錄的排序狀態不敏感,所以它不管最好、最壞和平均時間複雜度都是O(nlogn)。在性能上要好於冒泡、簡單選擇和直接插入算法。

空間複雜度上,只須要一個用於交換的暫存單元。可是因爲記錄的比較和交換是跳躍式的,所以,堆排序也是一種不穩定的排序方法。

此外,因爲初始構建堆的比較次數較多,堆排序不適合序列個數較少的排序工做。
7、歸併排序

歸併排序(Merging Sort):創建在歸併操做上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。

歸併排序

class SQList:
def init(self, lis=None):
self.r = lis

def swap(self, i, j):
    """定義一個交換元素的方法,方便後面調用。"""
    temp = self.r[i]
    self.r[i] = self.r[j]
    self.r[j] = temp

def merge_sort(self):
    self.msort(self.r, self.r, 0, len(self.r)-1)

def msort(self, list_sr, list_tr, s, t):
    temp = [None for i in range(0, len(list_sr))]
    if s == t:
        list_tr[s] = list_sr[s]
    else:
        m = int((s+t)/2)
        self.msort(list_sr, temp, s,  m)
        self.msort(list_sr, temp, m+1, t)
        self.merge(temp, list_tr, s, m, t)

def merge(self, list_sr, list_tr, i, m,  n):
    j = m+1
    k = i
    while i <= m and j <= n:
        if list_sr[i] < list_sr[j]:
            list_tr[k] = list_sr[i]
            i += 1
        else:
            list_tr[k] = list_sr[j]
            j += 1

        k += 1
    if i <= m:
        for l in range(0, m-i+1):
            list_tr[k+l] = list_sr[i+l]
    if j <= n:
        for l in range(0, n-j+1):
            list_tr[k+l] = list_sr[j+l]

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 12, 77, 34, 23])
sqlist.merge_sort()
print(sqlist)

另一個版本:

def merge(lfrom, lto, low, mid, high):
"""
兩段須要歸併的序列從左往右遍歷,逐一比較,小的就放到
lto裏去,lfrom下標+1,lto下標+1,而後再取,再比,再放,
最後lfrom裏的兩段比完了,lto裏留下的就是從小到大排好的一段。
:param lfrom: 原來的列表
:param lto: 緩存的列表
:param low: 左邊一段的開頭下標
:param mid: 左右兩段的中間相隔的下標
:param high: 右邊一段的最右下標
:return:
"""
i, j, k = low, mid, low
while i < mid and j < high:
if lfrom[i] <= lfrom[j]:
lto[k] = lfrom[i]
i += 1
else:
lto[k] = lfrom[j]
j += 1
k += 1
while i < mid:
lto[k] = lfrom[i]
i += 1
k += 1
while j < high:
lto[k] = lfrom[j]
j += 1
k += 1

def merge_pass(lfrom, lto, llen, slen):
"""
用來處理全部須要合併的段,這須要每段的長度,以及列表的總長。
最後的if語句處理表最後部分不規則的狀況。
:param lfrom: 原來的列表
:param lto: 緩存的列表
:param llen: 列表總長
:param slen: 每段的長度
:return:
"""
i = 0
while i+2slen < llen:
merge(lfrom, lto, i, i+slen, i+2
slen)
i += 2*slen
if i+slen < llen:
merge(lfrom, lto, i, i+slen, llen)
else:
for j in range(i, llen):
lto[j] = lfrom[j]

def merge_sort(lst):
"""
主函數。
先安排一個一樣大小的列表,做爲輔助空間。
而後在兩個列表直接作往復的歸併,每歸併一次slen的長度增長一倍,
逐漸向llen靠攏,當slen==llen時說明歸併結束了。
歸併完成後最終結果可能剛好保存在templist裏,所以代碼裏作兩次歸併,
保證最後的結果體如今原始的lst列表裏。
:param lst: 要排序的原始列表
:return:
"""
slen, llen = 1, len(lst)
templist = [None]llen
while slen < llen:
merge_pass(lst, templist, llen, slen)
slen
= 2
merge_pass(templist, lst, llen, slen)
slen *= 2

歸併排序對原始序列元素分佈狀況不敏感,其時間複雜度爲O(nlogn)。

歸併排序在計算過程當中須要使用必定的輔助空間,用於遞歸和存放結果,所以其空間複雜度爲O(n+logn)。

歸併排序中不存在跳躍,只有兩兩比較,所以是一種穩定排序。

總之,歸併排序是一種比較佔用內存,但效率高,而且穩定的算法。
8、快速排序

快速排序(Quick Sort)由圖靈獎得到者Tony Hoare發明,被列爲20世紀十大算法之一。冒泡排序的升級版,交換排序的一種。快速排序的時間複雜度爲O(nlog(n))。

快速排序算法的核心思想:經過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另外一部分記錄的關鍵字小,而後分別對這兩部分繼續進行排序,以達到整個記錄集合的排序目的。

快速排序

class SQList:
def init(self, lis=None):
self.r = lis

def swap(self, i, j):
    """定義一個交換元素的方法,方便後面調用。"""
    temp = self.r[i]
    self.r[i] = self.r[j]
    self.r[j] = temp

def quick_sort(self):
    """調用入口"""
    self.qsort(0, len(self.r)-1)

def qsort(self, low, high):
    """遞歸調用"""
    if low < high:
        pivot = self.partition(low, high)
        self.qsort(low, pivot-1)
        self.qsort(pivot+1, high)

def partition(self, low, high):
    """
    快速排序的核心代碼。
    其實就是將選取的pivot_key不斷交換,將比它小的換到左邊,將比它大的換到右邊。
    它本身也在交換中不斷變換本身的位置,直到完成全部的交換爲止。
    但在函數調用的過程當中,pivot_key的值始終不變。
    :param low:左邊界下標
    :param high:右邊界下標
    :return:分完左右區後pivot_key所在位置的下標
    """
    lis = self.r
    pivot_key = lis[low]
    while low < high:
        while low < high and lis[high] >= pivot_key:
            high -= 1
        self.swap(low, high)
        while low < high and lis[low] <= pivot_key:
            low += 1
        self.swap(low, high)
    return low

def __str__(self):
    ret = ""
    for i in self.r:
        ret += " %s" % i
    return ret

if name == 'main':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 123, 22])
sqlist.quick_sort()
print(sqlist)

另一個版本:

def quick_sort(nums):
# 封裝一層的目的是方便用戶調用
def qsort(lst, begin, end):
if begin >= end:
return
i = begin
key = lst[begin]
for j in range(begin+1, end+1):
if lst[j] < key:
i += 1
lst[i], lst[j] = lst[j], lst[i]
lst[begin], lst[i] = lst[i], lst[begin]
qsort(lst, begin, i-1)
qsort(lst,i+1,end)
qsort(nums, 0, len(nums)-1)

快速排序的時間性能取決於遞歸的深度。
當pivot_key剛好處於記錄關鍵碼的中間值時,大小兩區的劃分比較均衡,接近一個平衡二叉樹,此時的時間複雜度爲O(nlog(n))。
當原記錄集合是一個正序或逆序的狀況下,分區的結果就是一棵斜樹,其深度爲n-1,每一次執行大小分區,都要使用n-i次比較,其最終時間複雜度爲O(n^2)。
在通常狀況下,經過數學概括法可證實,快速排序的時間複雜度爲O(nlog(n))。
可是因爲關鍵字的比較和交換是跳躍式的,所以,快速排序是一種不穩定排序。
同時因爲採用的遞歸技術,該算法須要必定的輔助空間,其空間複雜度爲O(logn)。

下面是一個實例測試數據:
測試數據量(個) 100 1000 10000 100000 1000000
冒泡 0.001 0.11 11s - -
反序冒泡 0.001 0.14 14s - -
快速排序 0.001 0.003~0.004 0.040~0.046 0.51~0.53 6.36~6.56s

從數據中可見:

數據過萬,冒泡算法基本不可用。測試時間忠實的反映了n平方的時間複雜度,數據擴大10倍,耗時增長100倍
對於Python的列表,反序遍歷比正序遍歷仍是要消耗必定的時間的
快速排序在數據較大時,其威力顯現,但不夠穩定,整體仍是維護了nlog(n)的複雜度。

基本的快速排序還有能夠優化的地方:

  1. 優化選取的pivot_key

前面咱們每次選取pivot_key的都是子序列的第一個元素,也就是lis[low],這就比較看運氣。運氣好時,該值處於整個序列的靠近中間值,則構造的樹比較平衡,運氣比較差,處於最大或最小位置附近則構造的樹接近斜樹。
爲了保證pivot_key選取的儘量適中,採起選取序列左中右三個特殊位置的值中,處於中間值的那個數爲pivot_key,一般會比直接用lis[low]要好一點。在代碼中,在原來的pivot_key = lis[low]這一行前面增長下面的代碼:

m = low + int((high-low)/2)
if lis[low] > lis[high]:
self.swap(low, high)
if lis[m] > lis[high]:
self.swap(high, m)
if lis[m] > lis[low]:
self.swap(m, low)

若是以爲這樣還不夠好,還能夠將整個序列先劃分爲3部分,每一部分求出個pivot_key,再對3個pivot_key再作一次上面的比較得出最終的pivot_key。這時的pivot_key應該很大機率是一個比較靠譜的值。

  1. 減小沒必要要的交換

原來的代碼中pivot_key這個記錄老是再不斷的交換中,其實這是不必的,徹底能夠將它暫存在某個臨時變量中,以下所示:

def partition(self, low, high):

lis = self.r

    m = low + int((high-low)/2)
    if lis[low] > lis[high]:
        self.swap(low, high)
    if lis[m] > lis[high]:
        self.swap(high, m)
    if lis[m] > lis[low]:
        self.swap(m, low)

    pivot_key = lis[low]
    # temp暫存pivot_key的值
    temp = pivot_key
    while low < high:
        while low < high and lis[high] >= pivot_key:
            high -= 1
        # 直接替換,而不交換了
        lis[low] = lis[high]
        while low < high and lis[low] <= pivot_key:
            low += 1
        lis[high] = lis[low]
        lis[low] = temp
    return low
  1. 優化小數組時的排序

快速排序算法的遞歸操做在進行大量數據排序時,其開銷能被接受,速度較快。但進行小數組排序時則不如直接插入排序來得快,也就是殺雞用牛刀,未必就比菜刀來得快。
所以,一種很樸素的作法就是根據數據的多少,作個使用哪一種算法的選擇而已,以下改寫qsort方法:

def qsort(self, low, high):
"""根據序列長短,選擇使用快速排序仍是簡單插入排序"""
# 7是一個經驗值,可根據實際狀況自行決定該數值。
MAX_LENGTH = 7
if high-low < MAX_LENGTH:
if low < high:
pivot = self.partition(low, high)
self.qsort(low, pivot - 1)
self.qsort(pivot + 1, high)
else:
# insert_sort方法是咱們前面寫過的簡單插入排序算法
self.insert_sort()

  1. 優化遞歸操做

能夠採用尾遞歸的方式對整個算法的遞歸操做進行優化,改寫qsort方法以下:

def qsort(self, low, high):
"""根據序列長短,選擇使用快速排序仍是簡單插入排序"""
# 7是一個經驗值,可根據實際狀況自行決定該數值。
MAX_LENGTH = 7
if high-low < MAX_LENGTH:
# 改用while循環
while low < high:
pivot = self.partition(low, high)
self.qsort(low, pivot - 1)
# 採用了尾遞歸的方式
low = pivot + 1
else:
# insert_sort方法是咱們前面寫過的簡單插入排序算法
self.insert_sort()

9、排序算法總結

排序算法的分類:
image_1b3cphm9hdq25sl2a1ccn1mn01t.png-373.9kB

沒有十全十美的算法,有有點就會有缺點,即便是快速排序算法,也只是總體性能上的優越,也存在排序不穩定,須要大量輔助空間,不適於少許數據排序等缺點。

七種排序算法性能對比

image_1b3cpmfb7dnhciglnfirbi1s9.png-277.3kB

若是待排序列基本有序,請直接使用簡單的算法,不要使用複雜的改進算法。
歸併排序和快速排序雖然性能高,可是須要更多的輔助空間。其實就是用空間換時間。
待排序列的元素個數越少,就越適合用簡單的排序方法;元素個數越多就越適合用改進的排序算法。
簡單選擇排序雖然在時間性能上很差,但它在空間利用上性能很高。特別適合,那些數據量不大,每條數據的信息量又比較多的一類元素的排序。
相關文章
相關標籤/搜索