排序算法總結及Python實現

目錄

一、冒泡排序、選擇排序和插入排序:O(n*n)

1、冒泡排序

2、選擇排序

3、插入排序

二、歸併排序和快速排序:O(nlogn)

1、歸併排序

2、快速排序

三、桶排序和基數排序:O(n)

1、桶排序

2、基數排序


一、冒泡排序、選擇排序和插入排序:O(n*n)

1、冒泡排序

1、冒泡排序原理

冒泡排序對相鄰的兩個元素進行比較,看是否滿足大小關係要求,如果不滿足就讓他倆交換,如下圖所示。一次冒泡會讓至少一個元素移動到它應該在的位置,重複n次,就完成n個數據的排序。

2、Python實現冒泡排序

from typing import List


def bubble_sort(a: List[int]):
    length = len(a)
    if length <= 1:
        return

    for i in range(length):
        made_swap = False
        for j in range(length - i - 1):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                made_swap = True
        if not made_swap:
            break

3、冒泡排序性能分析

冒泡排序是穩定的排序算法。當相鄰的兩個元素大小相等的時候,我們不做交換,相同大小的數據在排序前後不會改變順序。

時間複雜度。最好的情況,數據有序,時間複雜度爲O(n)。最壞的情況,時間複雜度爲O(n*n)。平均情況,時間複雜度爲O(n*n)。

空間複雜度。冒泡排序只涉及相鄰數據的交換操作,時間複雜度爲O(1)。

2、選擇排序

1、選擇排序原理

選擇排序分爲已排序區間和未排序區間,每次從未排序區間中找到最小的元素,將其放到已排序區間的末尾。如下圖所示:

2、Python實現選擇排序

from typing import List


def selection_sort(a: List[int]):
    length = len(a)
    if length <= 1:
        return

    for i in range(length):
        min_index = i
        min_val = a[i]
        for j in range(i, length):
            if a[j] < min_val:
                min_val = a[j]
                min_index = j
        a[i], a[min_index] = a[min_index], a[i]

3、選擇排序性能分析

選擇排序不是穩定的排序算法。選擇排序每次都要找未排序空間中最小值,並和前面的元素交換位置,破壞了穩定性。例如3,4,3,1

時間複雜度。最好情況、最壞情況和平均情況的時間複雜度都爲O(n*n)。

空間複雜度。選擇排序屬於原地排序,空間複雜度爲O(1)。

3、插入排序

1、插入排序原理

插入排序,將數組中的數據分爲兩個區間,已排序區間和未排序區間,初始已排序區間只有一個元素,就是數組的第一個元素。插入排序取未排序區間中的元素,在已排序區間中找到合適的位置將其插入,保證已排序區間數據一直有序。重複這個過程,直到未排序區間中元素爲空,算法結束。如下圖所示:

2、Python實現插入排序

from typing import List


def insertion_sort(a: List[int]):
    length = len(a)
    if length <= 1:
        return

    for i in range(1, length):
        value = a[i]
        j = i - 1
        while j >= 0 and a[j] > value:
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = value

3、插入排序性能分析

插入排序是穩定的排序算法。對於值相同的元素,我們可以選擇將後面出現的元素,插入到前面出現的元素後面,保持原有的前後順序不變。

空間複雜度。插入排序是原地排序算法,時間複雜度爲O(1)。

時間複雜度。最好情況,數組有序,不需要移動元素,時間複雜度爲O(n)。最壞情況爲O(n*n)。平均情況爲O(n*n)。與冒泡排序相比,插入排序運行時間更少,因爲每次移動元素,插入排序只需要1次賦值,而冒泡排序需要3次賦值才能交換元素。

二、歸併排序和快速排序:O(nlogn)

1、歸併排序

1、歸併排序原理

歸併排序的核心思想:如果排序一個數組,先把數組從中間分成前後兩部分,然後對前後兩部分分別排序,再將排序好的兩部分合併在一起,這樣整個數組就都有序了。如下圖所示:

歸併排序使用的分治思想,就是分而治之,將一個大問題分解成小的子問題來解決,一般用遞歸實現:

遞推公式:
merge_sort(p..r) = merge(merge_sort(p..q), merge_sort(q+1..r))

終止條件:
p >= r 不用再繼續分解

2、python實現歸併排序

from typing import List


def merge_sort(a: List[int]):
    _merge_sort_between(a, 0, len(a)-1)


def _merge_sort_between(a: List[int], low: int, high: int):
    if low < high:
        mid = low + (high - low) // 2
        _merge_sort_between(a, low, mid)
        _merge_sort_between(a, mid+1, high)
        _merge(a, low, mid, high)


def _merge(a: List[int], low: int, mid: int, high: int):
    #a[low:mid],a[mid+1, high] are sorted
    i, j = low, mid+1
    tmp = []
    while i <= mid and j <= high:
        if a[i] <= a[j]:
            tmp.append(a[i])
            i += 1
        else:
            tmp.append(a[j])
            j += 1
    start = i if i <= mid else j
    end = mid if i <= mid else high
    tmp.extend(a[start: end + 1])
    a[low:high+1] = tmp


if __name__ == "__main__":
    a1 = [3, 6, 5, 8, 7]
    merge_sort(a1)
    print(a1)

3、歸併排序性能分析

歸併排序是穩定的排序算法。歸併排序穩定性關鍵要看merge函數,也就是兩個有序子數組合併成一個有序數組,值相同的元素在合併時保持順序不變。

時間複雜度。對n個元素進行歸併排序需要的時間是T(n),那分解成兩個子數組排序的時間是T(n/2),merge函數合併兩個有序子數組的時間是n,T(n)的計算過程如下:

T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2n
     = 4*(2*T(n/8) + n/4) + 2n = 8*T(n/8) + 3n
     = .....
     = 2^k*T(n/2^k) + k*n

當T(n/2^k)=T(1)時,k=logn, T(n)=n + n*logn。所以歸併排序的時間複雜度爲O(nlogn).

空間複雜度。歸併排序在合併兩個有序數組爲一個有序數組時,需要藉助額外的存儲空間,空間複雜度爲O(n).(在任意時刻,cpu只會有一個函數在執行,也就只會有一個臨時的內存空間在使用)

2、快速排序

1、快排原理

快排核心思想:如果要排數組中下標從p到r之間的一組數據,我們選擇p到r之間的任意一個數據爲pivot(分區點)。遍歷p到r之間的數據,將小於pivot的放到左邊,大於pivot的放到右邊,pivot放在中間。數組p到r之間的數據分成三個部分,前面p到q-1之間是小於pivot的,中間是pivot, 後面的q+1到r之間是大於pivot的。如下圖所示:

快排利用的也是分治思想,可以用遞歸實現:

遞推公式:
quick_sort(p..r) = quick_sort(p..q-1) + quick_sort(q+1..r)

終止條件:
p >= r

2、python實現快速排序

from typing import List
import random


def quick_sort(a: List[int]):
    _quick_sort_between(a, 0, len(a)-1)


def _quick_sort_between(a: List[int], low: int, high: int):
    if low < high:
        #get a random position as the pivot
        k = random.randint(low, high)  #防止數組有序,最壞情況發生
        a[low], a[k] = a[k], a[low]

        m = _partition(a, low, high)
        _quick_sort_between(a, low, m-1)
        _quick_sort_between(a, m+1, high)


def _partition(a: List[int], low: int, high: int):
    pivot, j = a[low], low
    for i in range(low+1, high+1):
        if a[i] < pivot:
            j += 1
            a[j], a[i] = a[i], a[j]
    a[low], a[j] = a[j], a[low]
    return j


if __name__ == "__main__":
    a1 = [4, 3, 2, 1]
    quick_sort(a1)
    print(a1)
    a2 = [3, 4, 5, 6, 7]
    quick_sort(a2)
    print(a2)
    a4 = [5, -1, 9, 3, 7, 8, 3, -2, 9]
    quick_sort(a4)
    print(a4)

3、快速排序性能分析

快排不是穩定的排序算法。快排中遍歷數組元素,將小於分區點的數放在左邊,大於分區點的數放在右邊。值相同元素的順序會發生改變。

時間複雜度:最好的情況如同歸併排序,時間複雜度爲O(nlogn).最壞的情況,數組是有序的,時間複雜度爲O(n^2).

空間複雜度:快排是原地排序算法,空間複雜度爲O(1)。因而對大規模數據排序時,快排比歸併排序更常用。

三、桶排序和基數排序:O(n)

1、桶排序

1、桶排序原理

桶排序核心思想,將要排序的數據分到幾個有序的桶裏,每個桶裏的數據再單獨進行快速排序,桶內排完序後,再把每個桶裏的數據按照順序依次取出,組成的序列就是有序的了。

2、桶排序性能分析

時間複雜度分析。如果要排序的數據有n個,我們把他們均勻地分到m個桶內,每個桶內有k=n/m個元素。每個桶內部使用快速排序,時間複雜度爲O(k*logk),m個桶排序的時間複雜度爲O(m*k*logk),因爲k=n/m,所以整個桶的時間複雜度爲O(n*log(n/m)).當桶的個數m接近數據個數n時,log(n/m)就是一個非常小的常量,桶排序的時間複雜度接近O(n).

3、適用場景

桶排序對要排序的數據非常苛刻。首先,要排序的數據需要容易地劃分成m個桶,並且桶與桶之間有天然的大小順序;其次,數據在各個桶之間的分佈式比較均勻的。

桶排序比較適合用在外部排序中,數據存儲在外部磁盤中,數據量比較大,內存有限,無法將數據全部加載在內存中。例如,我們有10GB的訂單數據,需要按照訂單金額進行排序,但內存只有4G,無法一次性將10G數據都加載到內存中。可以藉助桶排序解決這個問題:先掃描一遍文件,看訂單金額的數據範圍,假設最小是1元,最大是10萬元;將所有訂單根據金額劃分到100個桶裏,第一個桶我們存儲金額在1元到1000元之內的訂單,第二個桶存儲金額在1001到2000元之內的訂單,以此類推,每一個桶對應一個文件;理想情況,每個小文件大約100M的訂單數據,我們可以將這100個小文件依次放到內存中,用快排來排序;所有小文件都排好序,我們只需要按照文件編號,從小到大依次讀取每個小文件中的訂單數據,並將其寫入一個文件中,這個文件中存儲的就是按照金額從小到大排序的訂單數據。

2、基數排序

1、基數排序原理

基數排序按照每位來排序,排序算法需要是穩定的。如下圖所示:

2、基數排序性能分析

時間複雜度。根據每一位來排序,可以用桶排序,時間複雜度爲O(n),如果要排序的數據有k位,時間複雜度爲O(k*n),k爲常數時,基數排序的時間複雜度近似於O(n).

3、適用場景

要排序的數據可以分割出獨立的位來比較,而且位之間有遞進的關係,如果a數據的高位比b數據大,那剩下的地位就不用比較了。

 

總結:

 

穩定性

空間複雜度

最好

最壞

平均

冒泡排序

穩定

O(1)

O(n)

O(n*n)

O(n*n)

插入排序

穩定

O(1)

O(n)

O(n*n)

O(n*n)

選擇排序

不穩定

O(1)

O(n*n)

O(n*n)

O(n*n)

歸併排序

穩定

O(n)

O(nlogn)

O(nlogn)

O(nlogn)

快速排序

不穩定

O(1)

O(nlogn)

O(n*n)

O(nlogn)

桶排序

穩定

O(n)

                          O(n)

基數排序

穩定

O(n)

                          O(n)

 

 

參考資料:

極客時間:《數據結構與算法之美》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章