教小学妹学算法:十大经典排序算法深度解析
最近有一位小學(xué)妹 Coco 入坑了算法,結(jié)果上來就被幾個(gè)排序算法給整懵逼了,各種排序眼花繚亂,也分不清什么時(shí)候該用什么排序了。
今天呢,就在這分享一下我給小學(xué)妹講十大經(jīng)典排序算法的過程。
好吧,那我們就先來看一下十大經(jīng)典排序算法是哪些:
排序算法大致可以分為兩大類,一種是比較類排序,即通過元素之間的比較來決定相對次序;另一種是非比較類排序,運(yùn)行時(shí)間比較快,但是也有諸多限制。
在開始正式講解之前呢,先來介紹一個(gè)工具,對數(shù)器:
比如說我們寫了一個(gè)比較NB的Algorithm,但是又不確定right or wrong的時(shí)候,就可以通過對數(shù)器來驗(yàn)證。
拿第一個(gè)要講的冒泡排序?yàn)槔?#xff1a;
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是我們自己編寫的一個(gè)算法,但是我不確定結(jié)果是不是正確,這時(shí)候,我們可以隨機(jī)造一堆數(shù)據(jù),然后拷貝一份,第一份用Python內(nèi)置的排序算法進(jìn)行排序,第二份用我們自己編寫的algorithm進(jìn)行排序,如果兩個(gè)算法排序的結(jié)果一樣的話,就可以大致證明我們的算法正確。
當(dāng)然,一次驗(yàn)證的結(jié)果可能存在偶然性,所以我們可以多驗(yàn)證幾次,如果對于大量隨機(jī)的結(jié)果來說,我們的algorithm輸出結(jié)果都正確,那么就有很大把握確定這個(gè)algorithm是right的。
一、比較類排序
比較類排序還是比較好理解的,就是兩個(gè)元素之間比大小然后排隊(duì)嘛,比較常規(guī)。
在算法層面,比較類排序由于其時(shí)間復(fù)雜度不能突破O(nlogn),所以也被稱為非線性時(shí)間復(fù)雜度排序。
1.冒泡排序Bubble Sort
冒泡排序是一種非常簡單易懂的排序算法,它在遍歷整個(gè)數(shù)列的過程中,一次次的比較相鄰的兩個(gè)元素的大小,如果順序錯(cuò)誤就將其交換過來。
冒泡排序每次都可以將一個(gè)當(dāng)前最大的數(shù)移動(dòng)到數(shù)列的最后,就好像冒泡泡一樣,算法的名字也是由此而來。
先來看一張動(dòng)圖演示:
實(shí)現(xiàn)思路
Code
def bubbleSort(arr: list):length = len(arr)for trip in range(length):for index in range(length - trip - 1):# 相鄰的兩個(gè)元素,如果順序錯(cuò)誤,就交換兩個(gè)的位置if arr[index] > arr[index + 1]:arr[index], arr[index + 1] = arr[index + 1], arr[index]可以看到,冒泡排序必須通過兩層循環(huán),并且循環(huán)的次數(shù)與待排序數(shù)組的長度有關(guān),因此其時(shí)間復(fù)雜度為O(n2)。
算法分析
冒泡排序每次都要比較完所有的相鄰的兩個(gè)數(shù),但實(shí)際上,如果在某一次比較過程沒有交換發(fā)生的話,即可證明數(shù)列已經(jīng)有序的,因此我們可以在這點(diǎn)下文章,稍微優(yōu)化一下。
def bubbleSortV1(arr: list):length = len(arr)for trip in range(length):# 交換標(biāo)志exChange = Falsefor index in range(length - trip - 1):# 相鄰的兩個(gè)元素,如果順序錯(cuò)誤,就交換兩個(gè)的位置if arr[index] > arr[index + 1]:# 如果有交換發(fā)生, 標(biāo)記為 TrueexChange = Truearr[index], arr[index + 1] = arr[index + 1], arr[index]# 如果沒有交換發(fā)生,說明數(shù)列已經(jīng)有序了if not exChange:break如果待排序的數(shù)列本身就是有序的,那么bubbleSortV1走一遍就可以了,即最好時(shí)間復(fù)雜度為O(n),如果待排序的數(shù)列本身是逆序的,那么時(shí)間復(fù)雜度還是O(n2)。
2.選擇排序Select Sort
選擇排序的思路比較類似于我們?nèi)祟惖南敕?#xff0c;它的工作原理:首先在未排序數(shù)列中找到最小或最大的元素,交換到已排序數(shù)列的末尾,然后再從剩余未排序數(shù)列中繼續(xù)尋找最小的元素或最大的元素繼續(xù)做交換,以此類推,直到所有元素都排序完。
還是先來看一個(gè)動(dòng)圖演示:
實(shí)現(xiàn)思路
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]算法分析
選擇排序是最穩(wěn)定的排序算法之一,任何數(shù)列放進(jìn)去都是O(n2)的時(shí)間復(fù)雜度,所以適用于數(shù)據(jù)規(guī)模比較小的數(shù)列,不過選擇排序不占用額外的內(nèi)存空間。
3.插入排序Insert Sort
插入排序的思想類似于我們打撲克的時(shí)候抓牌,保證手里的牌有序,當(dāng)抓到一張新的牌時(shí),按照大小排序?qū)⑴撇迦氲竭m當(dāng)?shù)奈恢谩?/p>
來看動(dòng)圖演示:
實(shí)現(xiàn)思路
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]算法分析
插入排序在實(shí)現(xiàn)中采用in-place排序,從后往前掃描的過程中需要反復(fù)將已排序元素向后移動(dòng)為新元素提供插入空間,因此時(shí)間復(fù)雜度也為O(n2)。
4.希爾排序Shell Sort
希爾排序(Shell Sort),這是一個(gè)以人命名的排序算法,1959年由Shell發(fā)明,這是第一個(gè)時(shí)間復(fù)雜度突破O(2)的排序算法,它是簡單插入排序的改進(jìn)版,與其不同之處在于Shell Sort會(huì)優(yōu)先比較距離較遠(yuǎn)的元素,所以也叫縮小增量排序。
動(dòng)圖演示:
實(shí)現(xiàn)思路
Shell Sort是把數(shù)列按照一定的間隔分組,在每組內(nèi)使用直接插入排序,隨著間隔的減小,整個(gè)數(shù)列將會(huì)變得有序。
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 的核心在于增量序列的設(shè)定,既可以提前設(shè)定好增量序列,也可以在排序的過程中動(dòng)態(tài)生成。
5.快速排序
快速排序的基本思想比較有意思,它通過一趟排序?qū)⒋庞涗浄指畛蓛刹糠?#xff0c;其中一部分?jǐn)?shù)列均比關(guān)鍵字小,另一部分均比關(guān)鍵字大,然后繼續(xù)對這個(gè)兩部分進(jìn)行快速排序,最終達(dá)到整個(gè)數(shù)列有序。
動(dòng)圖演示:
實(shí)現(xiàn)思路
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 分別表示與基準(zhǔn)值相等的數(shù)列的左右邊界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):"""將比基準(zhǔn)值小的數(shù)放在左邊,相等的放中間,大的放右邊"""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算法分析
隨機(jī)快速排序的一次劃分從數(shù)列的兩頭開始交替搜索,知道left和right重合,因此其時(shí)間復(fù)雜度為O(n)。
快速排序算法的時(shí)間復(fù)雜度與劃分的趟數(shù)有關(guān)系,理想的情況是每次劃分所選擇的基準(zhǔn)值恰好是當(dāng)前數(shù)列的中位數(shù),經(jīng)過log2n趟劃分,便可得到長度為1的數(shù)列,因此快速排序的時(shí)間復(fù)雜度為O(nlog2n)。
最壞的情況是,每次所選的基準(zhǔn)值是當(dāng)前數(shù)列的最大或最小值,這使得每次劃分的數(shù)列中有一個(gè)為空,另一個(gè)數(shù)列的長度為原數(shù)列的長度減去基準(zhǔn)值數(shù)列的長度,這樣,長度為n的數(shù)列的快速排序需要經(jīng)過n趟劃分,這時(shí)整個(gè)隨機(jī)快速排序的時(shí)間復(fù)雜度為O(n2)。
從空間性能上看,隨機(jī)快速排序可以在數(shù)列內(nèi)部進(jìn)行交換,因此隨機(jī)快速排序的空間復(fù)雜度為O(1)。
6.歸并排序
歸并排序采用分治法(Divide and Conquer),將已有序的子數(shù)列合并,得到完全有序我的數(shù)列,即先使每個(gè)子數(shù)列有序,再使子數(shù)列間有序,將兩個(gè)有序數(shù)列合并成一個(gè)有序數(shù)列成為2-路歸并。
動(dòng)圖演示:
實(shí)現(xiàn)思路
Code
def mergeSort(arr: list, left: int, right: int):if left == right:return# 通過位運(yùn)算計(jì)算可以加快計(jì)算效率,下式可以避免溢出mid = left + ((right - left) >> 1)# 遞歸排序子數(shù)列mergeSort(arr, left, mid)mergeSort(arr, mid + 1, right)# 將排序好的子數(shù)列合并merge(arr, left, mid, right)def merge(arr: list, left: int, mid: int, right: int):helpList, p1, p2 = [], left, mid + 1# 合并兩個(gè)子數(shù)列直至一個(gè)數(shù)列為空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# 將剩下的數(shù)列全部添加到合并數(shù)列的末尾while p1 < mid + 1:helpList.append(arr[p1])p1 += 1while p2 < right + 1:helpList.append(arr[p2])p2 += 1# 將合并數(shù)列拷貝到原數(shù)列for index in range(len(helpList)):arr[left + index] = helpList[index]算法分析
歸并排序的性能不受輸入數(shù)據(jù)的影響,時(shí)間復(fù)雜度始終是O(nlogn),然而代價(jià)是需要額外的內(nèi)存空間。
其實(shí)歸并排序的額外空間復(fù)雜度可以變成O(1),采用歸并排序內(nèi)部緩存法,但是非常難。
7.堆排序
堆排序這個(gè)算法就比較有意思了,利用堆這種數(shù)據(jù)結(jié)構(gòu),其實(shí)就是將數(shù)列想象成一個(gè)完全二叉樹,然后根據(jù)最大堆或者最小堆的性質(zhì)做調(diào)整,即可將數(shù)列排序。
動(dòng)圖演示:
實(shí)現(xiàn)思路
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:# 左右子節(jié)點(diǎn)中的最大值索引largest = left + 1 if (left + 1 < length) and (arr[left + 1] > arr[left]) else left# 節(jié)點(diǎn)與子節(jié)點(diǎn)中的最大值索引largest = largest if arr[largest] > arr[index] else indexif largest == index:# 如果節(jié)點(diǎn)即為最大值則無需繼續(xù)調(diào)整breakelse:# 否則交換節(jié)點(diǎn)與最大值節(jié)點(diǎn)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.計(jì)數(shù)排序
計(jì)數(shù)排序是一種統(tǒng)計(jì)排序,而不是比較排序了,計(jì)數(shù)排序需要知道待排序數(shù)列的范圍,然后統(tǒng)計(jì)在范圍內(nèi)每個(gè)元素的出現(xiàn)次數(shù),最后按照次數(shù)輸出即是排序結(jié)果。
動(dòng)圖演示:
實(shí)現(xiàn)思路
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)算法分析
計(jì)數(shù)排序的速度非常快,但是它需要知道數(shù)列的元素范圍,如果數(shù)列元素的范圍非常大,則需要?jiǎng)?chuàng)建非常大的額外空間。
作為一種線性時(shí)間復(fù)雜度的排序,計(jì)數(shù)排序要求輸入的數(shù)據(jù)必須是有確定范圍的整數(shù)。
計(jì)數(shù)排序是一個(gè)穩(wěn)定的排序算法。當(dāng)輸入的元素是 n 個(gè) 0到 k 之間的整數(shù)時(shí),時(shí)間復(fù)雜度是O(n+k),空間復(fù)雜度也是O(n+k),其排序速度快于任何比較排序算法。
當(dāng)k不是很大并且序列比較集中時(shí),計(jì)數(shù)排序是一個(gè)很有效的排序算法。
2.桶排序
桶排序在計(jì)數(shù)排序的方法上利用了函數(shù)的映射關(guān)系進(jìn)行改進(jìn),不需要知道數(shù)列元素的范圍,但也需要額外創(chuàng)建一個(gè)序列空間,空間中的每個(gè)區(qū)間存放屬于該范圍的有序元素,最后遍歷整個(gè)空間,從小到大輸出即是有序數(shù)列。
動(dòng)圖演示:
實(shí)現(xiàn)思路
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)算法分析
桶排序的最佳時(shí)間復(fù)雜度為線性時(shí)間O(n),平均時(shí)間復(fù)雜度取決于桶內(nèi)數(shù)據(jù)排序的時(shí)間復(fù)雜度,因?yàn)槠渌糠值臅r(shí)間復(fù)雜度都是O(n),所以桶劃分的越小,各個(gè)桶之間的數(shù)據(jù)越少,排序所用的時(shí)間也會(huì)越少,但相應(yīng)消耗的空間就會(huì)增大。
3.基數(shù)排序
基數(shù)排序的實(shí)現(xiàn)原理比較特別,對于數(shù)列中的每個(gè)元素,先按照它的個(gè)位進(jìn)行排序,然后按照十位進(jìn)行排序,以此類推。
動(dòng)圖演示:
實(shí)現(xiàn)思路
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算法分析
基數(shù)排序是穩(wěn)定的,但是性能要比桶排序略差,每一次元素的桶分配都需要O(n)的時(shí)間復(fù)雜度,而且分配之后得到新的數(shù)列又需要O(n)的時(shí)間復(fù)雜度,假如待排數(shù)列可以分為K個(gè)關(guān)鍵字,則基數(shù)排序的時(shí)間復(fù)雜度將是O(d*2n),當(dāng)然d要遠(yuǎn)小于n,因此基本上是線性級別的。
三、總結(jié)
總結(jié)
以上是生活随笔為你收集整理的教小学妹学算法:十大经典排序算法深度解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 视觉盛宴 HTML5 3D动画应用赏析
- 下一篇: 教小学妹学算法:搜索算法解决迷宫问题