教小学妹学算法:十大经典排序算法深度解析
最近有一位小學妹 Coco 入坑了算法,結果上來就被幾個排序算法給整懵逼了,各種排序眼花繚亂,也分不清什么時候該用什么排序了。
今天呢,就在這分享一下我給小學妹講十大經典排序算法的過程。
好吧,那我們就先來看一下十大經典排序算法是哪些:
排序算法大致可以分為兩大類,一種是比較類排序,即通過元素之間的比較來決定相對次序;另一種是非比較類排序,運行時間比較快,但是也有諸多限制。
在開始正式講解之前呢,先來介紹一個工具,對數器:
比如說我們寫了一個比較NB的Algorithm,但是又不確定right or wrong的時候,就可以通過對數器來驗證。
拿第一個要講的冒泡排序為例:
import copy import randomdef 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 = Truefor 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 = Falseprint(list1)print(list2)print(list3)breakprint("Nice" if flag else "Fuck")假如說bubbleSort是我們自己編寫的一個算法,但是我不確定結果是不是正確,這時候,我們可以隨機造一堆數據,然后拷貝一份,第一份用Python內置的排序算法進行排序,第二份用我們自己編寫的algorithm進行排序,如果兩個算法排序的結果一樣的話,就可以大致證明我們的算法正確。
當然,一次驗證的結果可能存在偶然性,所以我們可以多驗證幾次,如果對于大量隨機的結果來說,我們的algorithm輸出結果都正確,那么就有很大把握確定這個algorithm是right的。
一、比較類排序
比較類排序還是比較好理解的,就是兩個元素之間比大小然后排隊嘛,比較常規。
在算法層面,比較類排序由于其時間復雜度不能突破O(nlogn),所以也被稱為非線性時間復雜度排序。
1.冒泡排序Bubble Sort
冒泡排序是一種非常簡單易懂的排序算法,它在遍歷整個數列的過程中,一次次的比較相鄰的兩個元素的大小,如果順序錯誤就將其交換過來。
冒泡排序每次都可以將一個當前最大的數移動到數列的最后,就好像冒泡泡一樣,算法的名字也是由此而來。
先來看一張動圖演示:
實現思路
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 = Falsefor index in range(length - trip - 1):# 相鄰的兩個元素,如果順序錯誤,就交換兩個的位置if arr[index] > arr[index + 1]:# 如果有交換發生, 標記為 TrueexChange = Truearr[index], arr[index + 1] = arr[index + 1], arr[index]# 如果沒有交換發生,說明數列已經有序了if not exChange:break如果待排序的數列本身就是有序的,那么bubbleSortV1走一遍就可以了,即最好時間復雜度為O(n),如果待排序的數列本身是逆序的,那么時間復雜度還是O(n2)。
2.選擇排序Select Sort
選擇排序的思路比較類似于我們人類的想法,它的工作原理:首先在未排序數列中找到最小或最大的元素,交換到已排序數列的末尾,然后再從剩余未排序數列中繼續尋找最小的元素或最大的元素繼續做交換,以此類推,直到所有元素都排序完。
還是先來看一個動圖演示:
實現思路
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
插入排序的思想類似于我們打撲克的時候抓牌,保證手里的牌有序,當抓到一張新的牌時,按照大小排序將牌插入到適當的位置。
來看動圖演示:
實現思路
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是把數列按照一定的間隔分組,在每組內使用直接插入排序,隨著間隔的減小,整個數列將會變得有序。
Code
def shellSort(array: list):length, gap = len(array), len(array) // 2while 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.快速排序
快速排序的基本思想比較有意思,它通過一趟排序將待排記錄分割成兩部分,其中一部分數列均比關鍵字小,另一部分均比關鍵字大,然后繼續對這個兩部分進行快速排序,最終達到整個數列有序。
動圖演示:
實現思路
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 分別表示與基準值相等的數列的左右邊界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 + 1while left < more:if array[left] < pivot:less += 1array[left], array[less] = array[less], array[left]left += 1elif array[left] > pivot:more -= 1array[left], array[more] = array[more], array[left]else:left += 1return less, more算法分析
隨機快速排序的一次劃分從數列的兩頭開始交替搜索,知道left和right重合,因此其時間復雜度為O(n)。
快速排序算法的時間復雜度與劃分的趟數有關系,理想的情況是每次劃分所選擇的基準值恰好是當前數列的中位數,經過log2n趟劃分,便可得到長度為1的數列,因此快速排序的時間復雜度為O(nlog2n)。
最壞的情況是,每次所選的基準值是當前數列的最大或最小值,這使得每次劃分的數列中有一個為空,另一個數列的長度為原數列的長度減去基準值數列的長度,這樣,長度為n的數列的快速排序需要經過n趟劃分,這時整個隨機快速排序的時間復雜度為O(n2)。
從空間性能上看,隨機快速排序可以在數列內部進行交換,因此隨機快速排序的空間復雜度為O(1)。
6.歸并排序
歸并排序采用分治法(Divide and Conquer),將已有序的子數列合并,得到完全有序我的數列,即先使每個子數列有序,再使子數列間有序,將兩個有序數列合并成一個有序數列成為2-路歸并。
動圖演示:
實現思路
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 += 1else:helpList.append(arr[p2])p2 += 1# 將剩下的數列全部添加到合并數列的末尾while p1 < mid + 1:helpList.append(arr[p1])p1 += 1while 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.堆排序
堆排序這個算法就比較有意思了,利用堆這種數據結構,其實就是將數列想象成一個完全二叉樹,然后根據最大堆或者最小堆的性質做調整,即可將數列排序。
動圖演示:
實現思路
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) // 2def heapify(arr: list, index: int, length: int):left = 2 * index + 1while 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 indexif largest == index:# 如果節點即為最大值則無需繼續調整breakelse:# 否則交換節點與最大值節點arr[index], arr[largest] = arr[largest], arr[index]index = largestleft = 2 * index + 1def heapSort(array: list):length = len(array)if length < 2:returnfor 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.計數排序
計數排序是一種統計排序,而不是比較排序了,計數排序需要知道待排序數列的范圍,然后統計在范圍內每個元素的出現次數,最后按照次數輸出即是排序結果。
動圖演示:
實現思路
Code
def countSort(array: list):count = [0 for _ in range(max(array) + 1)]for value in array:count[value] += 1array.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.桶排序
桶排序在計數排序的方法上利用了函數的映射關系進行改進,不需要知道數列元素的范圍,但也需要額外創建一個序列空間,空間中的每個區間存放屬于該范圍的有序元素,最后遍歷整個空間,從小到大輸出即是有序數列。
動圖演示:
實現思路
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 + 1while left < more:if array[left] < pivot:less += 1array[left], array[less] = array[less], array[left]left += 1elif array[left] > pivot:more -= 1array[left], array[more] = array[more], array[left]else:left += 1return less, moredef bucketSort(array: list):length = len(array)if length < 2:returnbucketNumber = 10maxNumber, 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.基數排序
基數排序的實現原理比較特別,對于數列中的每個元素,先按照它的個位進行排序,然后按照十位進行排序,以此類推。
動圖演示:
實現思路
Code
def radixSort(array: list):length, maxNumber, base = len(array), max(array), 0while 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,因此基本上是線性級別的。
三、總結
總結
以上是生活随笔為你收集整理的教小学妹学算法:十大经典排序算法深度解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 视觉盛宴 HTML5 3D动画应用赏析
- 下一篇: 教小学妹学算法:搜索算法解决迷宫问题