教小學妹學算法:十大經典排序算法深度解析

Hello,我是 Alex 007,一個熱愛計算機編程和硬件設計的小白,爲啥是007呢?因爲叫 Alex 的人太多了,再加上每天007的生活,Alex 007就誕生了。

最近有一位小學妹 Coco 入坑了算法,結果上來就被幾個排序算法給整懵逼了,各種排序眼花繚亂,也分不清什麼時候該用什麼排序了,於是哭着來找我來了。
在這裏插入圖片描述
咳咳,我是一個沒有感情的coder,只是單純的給她講了算法。
今天呢,就在這分享一下我給小學妹講十大經典排序算法的過程。

在這裏插入圖片描述
好吧,那我們就先來看一下十大經典排序算法是哪些:
在這裏插入圖片描述
排序算法大致可以分爲兩大類,一種是比較類排序,即通過元素之間的比較來決定相對次序;另一種是非比較類排序,運行時間比較快,但是也有諸多限制。

在開始正式講解之前呢,先來介紹一個工具,對數器:
在這裏插入圖片描述
比如說我們寫了一個比較NB的Algorithm,但是又不確定right or wrong的時候,就可以通過對數器來驗證。

拿第一個要講的冒泡排序爲例:

import copy
import random


def bubbleSort(arr: list):
	length = len(arr)
	for trip in range(length):
		for index in range(length - trip - 1):
			if arr[index] > arr[index + 1]:
				arr[index], arr[index + 1] = arr[index + 1], arr[index]


if __name__ == '__main__':
	flag = True
	for i in range(100):
		list1 = [random.randint(0, 100) for _ in range(random.randint(0, 100))]
		list2 = copy.deepcopy(list1)
		list3 = copy.deepcopy(list1)
		bubbleSort(list2)
		list3.sort()
		if list2 != list3:
			flag = False
			print(list1)
			print(list2)
			print(list3)
			break
	print("Nice" if flag else "Fuck")

假如說bubbleSort是我們自己編寫的一個算法,但是我不確定結果是不是正確,這時候,我們可以隨機造一堆數據,然後拷貝一份,第一份用Python內置的排序算法進行排序,第二份用我們自己編寫的algorithm進行排序,如果兩個算法排序的結果一樣的話,就可以大致證明我們的算法正確。

當然,一次驗證的結果可能存在偶然性,所以我們可以多驗證幾次,如果對於大量隨機的結果來說,我們的algorithm輸出結果都正確,那麼就有很大把握確定這個algorithm是right的。

一、比較類排序

比較類排序還是比較好理解的,就是兩個元素之間比大小然後排隊嘛,比較常規。

在算法層面,比較類排序由於其時間複雜度不能突破O(nlogn),所以也被稱爲非線性時間複雜度排序。

1.冒泡排序Bubble Sort

冒泡排序是一種非常簡單易懂的排序算法,它在遍歷整個數列的過程中,一次次的比較相鄰的兩個元素的大小,如果順序錯誤就將其交換過來。

冒泡排序每次都可以將一個當前最大的數移動到數列的最後,就好像冒泡泡一樣,算法的名字也是由此而來。

先來看一張動圖演示:
在這裏插入圖片描述

實現思路

  1. 比較相鄰的兩個元素,如果順序錯誤,就交換兩個的位置;
  2. 對每兩個相鄰的元素都做相同的工作,這樣一趟下來會將最大的元素排在最後;
  3. 對除最後一個元素之外剩下的數列重複上述操作;
  4. 重複步驟1~3,直至排序完成

Code

def bubbleSort(arr: list):
	length = len(arr)
	for trip in range(length):
		for index in range(length - trip - 1):
			if arr[index] > arr[index + 1]:
				arr[index], arr[index + 1] = arr[index + 1], arr[index]

可以看到,冒泡排序必須通過兩層循環,並且循環的次數與待排序數組的長度有關,因此其時間複雜度爲O(n2)

算法分析

冒泡排序每次都要比較完所有的相鄰的兩個數,但實際上,如果在某一次比較過程沒有交換髮生的話,即可證明數列已經有序的,因此我們可以在這點下文章,稍微優化一下。

def bubbleSortV1(arr: list):
	length = len(arr)
	for trip in range(length):
		exChange = False
		for index in range(length - trip - 1):
			if arr[index] > arr[index + 1]:
				exChange = True
				arr[index], arr[index + 1] = arr[index + 1], arr[index]
		if not exChange:
			break

如果待排序的數列本身就是有序的,那麼bubbleSortV1走一遍就可以了,即最好時間複雜度爲O(n),如果待排序的數列本身是逆序的,那麼時間複雜度還是O(n2)

2.選擇排序Select Sort

選擇排序的思路比較類似於我們人類的想法,它的工作原理:首先在未排序數列中找到最小或最大的元素,交換到已排序數列的末尾,然後再從剩餘未排序數列中繼續尋找最小的元素或最大的元素繼續做交換,以此類推,直到所有元素都排序完。

還是先來看一個動圖演示:
在這裏插入圖片描述

實現思路

  1. 初始狀態:有序區間爲空,無序區間爲[1:n];
  2. 第i(i=1,2,3,…)趟排序時,有序區間爲[1:i],無序區間爲(i:n],該趟排序在無序區間中找到最小或最大的元素,插入到有序區間的最後;
  3. 重複循環n-1趟。

Code

def selectSort(array: list):
    length = len(array)
    for trip in range(length - 1):
        for index in range(trip + 1, length):
            if array[index] < array[trip]:
                array[trip], array[index] = array[index], array[trip]

算法分析

選擇排序是最穩定的排序算法之一,任何數列放進去都是O(n2)的時間複雜度,所以適用於數據規模比較小的數列,不過選擇排序不佔用額外的內存空間。

3.插入排序Insert Sort

插入排序的思想類似於我們打撲克的時候抓牌,保證手裏的牌有序,當抓到一張新的牌時,按照大小排序將牌插入到適當的位置。

來看動圖演示:
在這裏插入圖片描述

實現思路

  1. 從第一個元素開始,該元素可以被認爲已經被排序;
  2. 取出下一個元素,從後向前掃描已排序的數列,如果被掃描的元素大於新元素則繼續向前掃描,否則將新元素插入到該元素後邊;
  3. 重複步驟2,直到數列有序。

Code

def insertSort(arr: list):
    for trip in range(1, len(arr)):
        for index in range(trip - 1, -1, -1):
            if arr[index] > arr[index + 1]:
                arr[index], arr[index + 1] = arr[index + 1], arr[index]

算法分析

插入排序在實現中採用in-place排序,從後往前掃描的過程中需要反覆將已排序元素向後移動爲新元素提供插入空間,因此時間複雜度也爲O(n2)。

4.希爾排序Shell Sort

希爾排序(Shell Sort),這是一個以人命名的排序算法,1959年由Shell發明,這是第一個時間複雜度突破O(2)的排序算法,它是簡單插入排序的改進版,與其不同之處在於Shell Sort會優先比較距離較遠的元素,所以也叫縮小增量排序

動圖演示:
在這裏插入圖片描述

實現思路

Shell Sort是把數列按照一定的間隔分組,在每組內使用直接插入排序,隨着間隔的減小,整個數列將會變得有序。

  1. 確定一個增量序列,即間隔長度t1,t2,…,tk,最終tk=1;
  2. 按照增量序列的個數x,對數列進行x趟排序;
  3. 在每趟排序過程中,根據對應的增量ti將待排數列分割成若干長度爲m的子數列,分別在各個子數列中進行直接插入排序。

Code

def shellSort(array: list):
    length, gap = len(array), len(array) // 2
    while gap > 0:
        for trip in range(gap, length):
            for index in range(trip - gap, -1, -gap):
                if array[index] > array[index + gap]:
                    array[index], array[index + gap] = array[index + gap], array[index]
        gap //= 2

算法分析

Shell Sort 的核心在於增量序列的設定,既可以提前設定好增量序列,也可以在排序的過程中動態生成。

5.快速排序

快速排序的基本思想比較有意思,它通過一趟排序將待排記錄分割成兩部分,其中一部分數列均比關鍵字小,另一部分均比關鍵字大,然後繼續對這個兩部分進行快速排序,最終達到整個數列有序。

動圖演示:
在這裏插入圖片描述

實現思路

  1. 從數列中隨機挑選一個元素作爲基準(pivot);
  2. 遍歷整個序列,所有比基準值小的放在基準前,比基準值大的放在基準後,這樣就將基準值放在了數列的中間位置;
  3. 遞歸重複排序小於基準值的數列和大於基準值的數列。

Code

def randomQuickSort(array: list):
    if len(array) < 2:
        return
    _randomQuickSort(array, 0, len(array) - 1)


def _randomQuickSort(array: list, left: int, right: int):
    if left < right:
        less, more = partition(array, left, right, array[random.randint(left, right)])
        _randomQuickSort(array, left, less)
        _randomQuickSort(array, more, right)


def partition(array: list, left: int, right: int, p: int):
    less, more = left - 1, right + 1
    while left < more:
        if array[left] < p:
            less += 1
            array[left], array[less] = array[less], array[left]
            left += 1
        elif array[left] > p:
            more -= 1
            array[left], array[more] = array[more], array[left]
        else:
            left += 1
    return less, more

6.歸併排序

歸併排序採用分治法(Divide and Conquer),將已有序的子數列合併,得到完全有序我的數列,即先使每個子數列有序,再使子數列間有序,將兩個有序數列合併成一個有序數列成爲2-路歸併。

動圖演示:

在這裏插入圖片描述

實現思路

  1. 把長度爲n的數列分成兩個長度爲n/2的子數列;
  2. 對兩個子數列分別採用歸併排序;
  3. 將兩個排好序的子數列合併成一個有序數列。

Code

def mergeSort(arr: list, left: int, right: int):
	if left == right:
		return
	mid = left + ((right - left) >> 1)
	mergeSort(arr, left, mid)
	mergeSort(arr, mid + 1, right)
	merge(arr, left, mid, right)


def merge(arr: list, left: int, mid: int, right: int):
	helpList, p1, p2 = [], left, mid + 1
	while p1 < mid + 1 and p2 < right + 1:
		if arr[p1] < arr[p2]:
			helpList.append(arr[p1])
			p1 += 1
		else:
			helpList.append(arr[p2])
			p2 += 1
	while p1 < mid + 1:
		helpList.append(arr[p1])
		p1 += 1
	while p2 < right + 1:
		helpList.append(arr[p2])
		p2 += 1
	for index in range(len(helpList)):
		arr[left + index] = helpList[index]

算法分析

歸併排序的性能不受輸入數據的影響,時間複雜度始終是O(nlogn),然而代價是需要額外的內存空間。

其實歸併排序的額外空間複雜度可以變成O(1),採用歸併排序內部緩存法,但是非常難。

7.堆排序

堆排序這個算法就比較有意思了,利用堆這種數據結構,其實就是將數列想象成一個完全二叉樹,然後根據最大堆或者最小堆的性質做調整,即可將數列排序。

動圖演示:
在這裏插入圖片描述

實現思路

  1. 將初始數列構成成大根堆,此爲初始無序區間;
  2. 堆頂元素與數列末尾元素交換,即將無序區間最大值插入到有序區間;
  3. 交換後的新堆違反大根堆的性質,必須重新調整爲大根堆;
  4. 重複2、3步驟,直至數列有序。

Code

def heapInsert(array: list, index: int):
    while array[(index - 1) // 2] < array[index] and index > 0:
        array[(index - 1) // 2], array[index] = array[index], array[(index - 1) // 2]
        index = (index - 1) // 2


def heapify(arr: list, index: int, length: int):
    left = 2 * index + 1
    while left < length:
        # 左右子節點中的最大值索引
        largest = left + 1 if (left + 1 < length) and (arr[left + 1] > arr[left]) else left
        # 節點與子節點中的最大值索引
        largest = largest if arr[largest] > arr[index] else index
        if largest == index:
            # 如果節點即爲最大值則無需繼續調整
            break
        else:
            # 否則交換節點與最大值節點
            arr[index], arr[largest] = arr[largest], arr[index]
            index = largest
            left = 2 * index + 1


def heapSort(array: list):
    length = len(array)
    if length < 2:
        return
    for index in range(1, length):
        heapInsert(array, index)
    for index in range(length - 1, -1, -1):
        array[0], array[index] = array[index], array[0]
        heapify(array, 0, index)

二、非比較類排序

1.計數排序

計數排序是一種統計排序,而不是比較排序了,計數排序需要知道待排序數列的範圍,然後統計在範圍內每個元素的出現次數,最後按照次數輸出即是排序結果。

動圖演示:

在這裏插入圖片描述

實現思路

  1. 根據數列的最大元素計數創建空間;
  2. 遍歷整個數列,統計每個元素出現的次數;
  3. 遍歷統計區間,按照統計次數輸出結果。

Code

def countSort(array: list):
	count = [0 for _ in range(max(array) + 1)]
	for value in array:
		count[value] += 1
	array.clear()
	for index, values in enumerate(count):
		for _ in range(values):
			array.append(index)

算法分析

計數排序的速度非常快,但是它需要知道數列的元素範圍,如果數列元素的範圍非常大,則需要創建非常大的額外空間。

作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

計數排序是一個穩定的排序算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k),空間複雜度也是O(n+k),其排序速度快於任何比較排序算法。

當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法。

2.桶排序

桶排序在計數排序的方法上利用了函數的映射關係進行改進,不需要知道數列元素的範圍,但也需要額外創建一個序列空間,空間中的每個區間存放屬於該範圍的有序元素,最後遍歷整個空間,從小到大輸出即是有序數列。

動圖演示:

在這裏插入圖片描述

實現思路

  1. 設置一個定量的數組作爲空桶;
  2. 遍歷待排序數列,並把元素一個一個放到對應的桶裏;
  3. 對每個桶內的元素進行排序;
  4. 從非空桶中將排好序的元素拼接起來。

Code

def randomQuickSort(array: list):
	if len(array) < 2:
		return
	_randomQuickSort(array, 0, len(array) - 1)


def _randomQuickSort(array: list, left: int, right: int):
	if left < right:
		less, more = partition(array, left, right, array[random.randint(left, right)])
		_randomQuickSort(array, left, less)
		_randomQuickSort(array, more, right)


def partition(array: list, left: int, right: int, pivot: int):
	less, more = left - 1, right + 1
	while left < more:
		if array[left] < pivot:
			less += 1
			array[left], array[less] = array[less], array[left]
			left += 1
		elif array[left] > pivot:
			more -= 1
			array[left], array[more] = array[more], array[left]
		else:
			left += 1
	return less, more


def bucketSort(array: list):
	length = len(array)
	if length < 2:
		return
	bucketNumber = 10
	maxNumber, bucket = max(array), [[] for _ in range(bucketNumber)]
	for item in array:
		index = min(item // (maxNumber // bucketNumber), bucketNumber - 1)
		bucket[index].append(item)
		randomQuickSort(bucket[index])
	array.clear()
	for value in bucket:
		array.extend(value)

算法分析

桶排序的最佳時間複雜度爲線性時間O(n),平均時間複雜度取決於桶內數據排序的時間複雜度,因爲其它部分的時間複雜度都是O(n),所以桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少,但相應消耗的空間就會增大。

3.基數排序

基數排序的實現原理比較特別,對於數列中的每個元素,先按照它的個位進行排序,然後按照十位進行排序,以此類推。

動圖演示:
在這裏插入圖片描述

實現思路

  1. 取得數列中最大數,計算其位數;
  2. 從最低位開始對數列的每一個元素分類;
  3. 將每個分類中的元素按照順序重新組合在一次;

Code

def radixSort(array: list):
	length, maxNumber, base = len(array), max(array), 0
	while 10 ** base <= maxNumber:
		buckets = [[] for _ in range(10)]
		for value in array:
			buckets[(value // 10 ** base) % 10].append(value)
		array.clear()
		for bucket in buckets:
			array.extend(bucket)
		base += 1

算法分析

基數排序是穩定的,但是性能要比桶排序略差,每一次元素的桶分配都需要O(n)的時間複雜度,而且分配之後得到新的數列又需要O(n)的時間複雜度,假如待排數列可以分爲K個關鍵字,則基數排序的時間複雜度將是O(d*2n),當然d要遠小於n,因此基本上是線性級別的。

三、總結

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

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