九大排序算法
一、直接插入排序
每次選擇一個(gè)元素,并且將這個(gè)元素和已經(jīng)排好序的數(shù)組的所有元素進(jìn)行比較,然后插入到合適的位置
舉例: 38,65,97,76,13,27,49
[38],65,97,76,13,27,49
[38,65],97,76,13,27,49
[38,65,97],76,13,27,49
[38,65,76,97],13,27,49
[13,38,65,76,97],27,49
...
[13,27,38,49,65,76,97]
最好O(N),最壞/平均時(shí)間復(fù)雜度O(N^2)
空間復(fù)雜度O(1)
代碼如下:
def insertion_sort(arr):
len_ = len(arr)
for i in range(1,len_):
tmp = arr[i]
j = i - 1
while j >= 0:
if arr[j] > tmp:#逆序遍歷已排好序的數(shù),如果當(dāng)前的數(shù)比要插入的數(shù)大,就將要插入的數(shù)前移
arr[j+1] = arr[j]
arr[j] = tmp
j -= 1
print(arr)
二、希爾排序
這個(gè)是插入排序的修改版,根據(jù)步長(zhǎng)由長(zhǎng)到短分組,進(jìn)行排序,直到步長(zhǎng)為1為止,屬于插入排序的一種。
基本原理:首先將待排序的元素分成多個(gè)子序列,使得每個(gè)子序列的元素個(gè)數(shù)相對(duì)較少,對(duì)各個(gè)子序列分別進(jìn)行直接插入排序,待整個(gè)待排序序列“基本有序后”再進(jìn)行一次直接插入排序
平均時(shí)間復(fù)雜度O(NlogN),最差O(N^s)
空間復(fù)雜度O(1)
代碼如下:
# 希爾排序的實(shí)質(zhì)就是分組插入排序,該方法又稱縮小增量排序,因DL.Shell于1959年提出而得名。
# 希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進(jìn)版本。希爾排序是非穩(wěn)定排序算法。
# 希爾排序是基于插入排序的以下兩點(diǎn)性質(zhì)而提出改進(jìn)方法的:
# 插入排序在對(duì)幾乎已經(jīng)排好序的數(shù)據(jù)操作時(shí),效率高,即可以達(dá)到線性排序的效率
# 但插入排序一般來(lái)說(shuō)是低效的,因?yàn)椴迦肱判蛎看沃荒軐?shù)據(jù)移動(dòng)一位
def shell_sort(arr):
step = len(arr) // 2 #設(shè)定步長(zhǎng)
while step > 0:
for i in range(step,len(arr)):
while i >= step and arr[i-step] > arr[i]: #類似插入排序,當(dāng)前值與指定步長(zhǎng)之前的值比較,符合條件則交換位置
arr[i],arr[i-step] = arr[i-step],arr[i]
i -= step
step = step // 2
return arr
三、冒泡排序
最好O(N),最壞/平均O(N^2)
空間復(fù)雜度O(1)
代碼如下:
def bubble_sort(arr):
len_ = len(arr)
for i in range(len_):
j = i
while j < len_-1:
if arr[j] > arr[j+1]: #先冒大的
arr[j],arr[j+1] = arr[j+1],arr[j]
j += 1
print(arr)
def bubble_sort1(arr):
len_ = len(arr)
for i in range(0,len_):
for j in range(i+1,len_):
if arr[i] > arr[j]: #先冒小的
arr[i],arr[j] = arr[j],arr[i]
print(arr)
return arr
res= bubble_sort1(arr=[36,25,48,12,25,65,43,57])
# print(res)
四、快速排序
原理:對(duì)于一組給定的記錄,通過(guò)一趟排序后,將原序列分為兩部分,其中前部分的所有記錄均比后部分的所有記錄小,然后再一次對(duì)前后兩部分的記錄進(jìn)行快速排序,遞歸該過(guò)程,直到序列中的所有記錄均有序?yàn)橹埂?/p>
最好O(NlogN):每次都恰好五五分,一次遞歸共需要比較n次,遞歸深度為lgn;
最壞O(N^2):已排序數(shù)組,比較次數(shù)為
平均O(NlogN):
快排在所有平均時(shí)間復(fù)雜度為O(NlogN)的算法中,平均性能最好
空間復(fù)雜度O(logN)
當(dāng)初始序列整體或局部有序時(shí),快排的會(huì)退化為冒泡排序。
代碼如下:
#分治法
def quick_sort(arr,left,right):
if left >= right:
return arr
pivot = arr[left] #取第一個(gè)元素為哨兵
low = left #保留初始的left和right的值,后面要用
high = right
while left < right:
while left < right and arr[right] >= pivot: #從右向左,找到第一個(gè)比pivot小的元素
right -= 1
arr[left] = arr[right] #把該右邊值放到左邊位置上
while left < right and arr[left] < pivot: #從左向右,找到第一個(gè)比pivot大的元素
left += 1
arr[right] = arr[left] #把該左邊值放到右邊位置上
arr[right] = pivot #此時(shí),left和right指向同一個(gè)位置
quick_sort(arr,low,left-1)
quick_sort(arr,right+1,high)
return arr
arr = [3,4,2,8,9,5,1]
left,right = 0,len(arr)-1
res = quick_sort(arr,left,right)
print(res)
【快排的優(yōu)化】三數(shù)取中(median-of-three)
引入的原因:雖然隨機(jī)選取樞軸時(shí),減少出現(xiàn)不好分割的幾率,但是還是最壞情況下還是O(n2),要緩解這種情況,就引入了三數(shù)取中選取樞軸
分析:最佳的劃分是將待排序的序列分成等長(zhǎng)的子序列,最佳的狀態(tài)我們可以使用序列的中間的值,也就是第N/2個(gè)數(shù)。可是,這很難算出來(lái),并且會(huì)明顯減慢快速排序的速度。這樣的中值的估計(jì)可以通過(guò)隨機(jī)選取三個(gè)元素并用它們的中值作為樞紐元而得到。事實(shí)上,隨機(jī)性并沒(méi)有多大的幫助,因此一般的做法是使用左端、右端和中心位置上的三個(gè)元素的中值作為樞紐元。顯然使用三數(shù)中值分割法消除了預(yù)排序輸入的不好情形,并且減少快排大約14%的比較次數(shù)
舉例:待排序序列為:8 1 4 9 6 3 5 2 7 0
左邊為:8,右邊為0,中間為6.
我們這里取三個(gè)數(shù)排序后,中間那個(gè)數(shù)作為樞軸,則樞軸為6
注意:在選取中軸值時(shí),可以從由左中右三個(gè)中選取擴(kuò)大到五個(gè)元素中或者更多元素中選取,一般的,會(huì)有(2t+1)平均分區(qū)法(median-of-(2t+1),三平均分區(qū)法英文為median-of-three)。
具體思想:對(duì)待排序序列中l(wèi)ow、mid、high三個(gè)位置上數(shù)據(jù)進(jìn)行排序,取他們中間的那個(gè)數(shù)據(jù)作為樞軸,并用0下標(biāo)元素存儲(chǔ)樞軸。
即:采用三數(shù)取中,并用0下標(biāo)元素存儲(chǔ)樞軸。
取中樞的代碼如下:
#三數(shù)取中
#取待排序序列中l(wèi)ow、mid、high三個(gè)位置上數(shù)據(jù),選取他們中間的那個(gè)數(shù)據(jù)作為樞軸
def findMedian(arr,low,high):
mid = low + (high-low) >> 1
if arr[mid] > arr[high]: #目標(biāo): arr[mid] <= arr[high]
arr[mid],arr[high] = arr[high],arr[mid]
if arr[low] > arr[high]: #目標(biāo): arr[low] <= arr[high]
arr[low],arr[high] = arr[high],arr[low]
if arr[mid] > arr[low]: #目標(biāo): arr[low] >= arr[mid]
arr[low], arr[mid] = arr[mid], arr[low]
print(arr)
return arr[low] #low的位置上保存這三個(gè)位置中間的值
arr = [1,3,2]
res = findMedian(arr,0,2)
print(res)
測(cè)試數(shù)據(jù)分析:使用三數(shù)取中選擇樞軸優(yōu)勢(shì)還是很明顯的,但是還是處理不了重復(fù)數(shù)組
五、簡(jiǎn)單選擇排序
原理:對(duì)于給定的一組記錄,經(jīng)過(guò)第一輪比較后得到最小的記錄,然后將該記錄與第一個(gè)記錄進(jìn)行交換;接著對(duì)不包括第一個(gè)記錄以外的其他記錄進(jìn)行第二輪比較,得到最小的記錄并與第二個(gè)記錄進(jìn)行位置交換;重復(fù)該過(guò)程,直到進(jìn)行比較的記錄只有1個(gè)時(shí)為止。
舉例: 38,65,97,76,13,27,49
13,[65,97,76,38,27,49]
13,27,[97,76,38,65,49]
...
13,27,38,49,65,76,97
最好最壞時(shí)間復(fù)雜度O(N^2)
空間復(fù)雜度O(1)
代碼如下:
def select_sort(arr):
len_ = len(arr)
for i in range(len_):
min = i
for j in range(i+1,len_):
if arr[j] < arr[min]:
min = j
arr[min],arr[i] = arr[i],arr[min] #一輪結(jié)束再交換
# print (arr)
return arr
res = select_sort(arr=[38,65,97,76,13,27,49])
print(res)
六、堆排序
1.堆排序
大頂堆:父節(jié)點(diǎn)比子節(jié)點(diǎn)大,堆頂元素必為最大值
小頂堆:子節(jié)點(diǎn)比父節(jié)點(diǎn)大,堆頂元素必為最小值
原理:
對(duì)于給定的n個(gè)記錄,初始時(shí)把這些記錄看作為一棵順序存儲(chǔ)的二叉樹(shù),然后將其調(diào)整為一個(gè)大頂堆,然后將堆的最后一個(gè)元素與堆頂元素(二叉樹(shù)根節(jié)點(diǎn))
進(jìn)行交換后,堆的最后一個(gè)元素即為最大記錄;接著將前(n-1)個(gè)元素(不包括最大記錄)重新調(diào)整為一個(gè)大頂堆,再將堆頂元素與當(dāng)前堆的最后一個(gè)元素進(jìn)行交
換后得到次大的記錄,重復(fù)該過(guò)程直到調(diào)整的堆中只剩下一個(gè)元素時(shí)為止,該元素即為最小記錄,此時(shí)可得到一個(gè)有序序列。
兩過(guò)程:(1)建堆,自下(最后一個(gè)非葉子節(jié)點(diǎn))而上(第一個(gè)非葉子節(jié)點(diǎn)),自右向左
(2)交換堆頂元素與最后一個(gè)元素的位置
---------->>
堆排序過(guò)程:
建堆時(shí)間復(fù)雜度O(n)
初始化建堆只需要對(duì)二叉樹(shù)的非葉子節(jié)點(diǎn)調(diào)用adjusthead()函數(shù),由下至上,由右至左選取非葉子節(jié)點(diǎn)來(lái)調(diào)用adjusthead()函數(shù)。那么倒數(shù)第二層的最右邊的非葉子節(jié)點(diǎn)就是最后一個(gè)非葉子結(jié)點(diǎn)。
假設(shè)高度為k,則從倒數(shù)第二層右邊的節(jié)點(diǎn)開(kāi)始,這一層的節(jié)點(diǎn)都要執(zhí)行子節(jié)點(diǎn)比較然后交換(如果順序是對(duì)的就不用交換);倒數(shù)第三層呢,則會(huì)選擇其子節(jié)點(diǎn)進(jìn)行比較和交換,如果沒(méi)交換就可以不用再執(zhí)行下去了。如果交換了,那么又要選擇一支子樹(shù)進(jìn)行比較和交換;高層也是這樣逐漸遞歸。
那么總的時(shí)間計(jì)算為:s=2^(i-1)*(k-i);其中i表示第幾層,2^(i-1)表示該層上有多少個(gè)元素,(k-i)表示子樹(shù)上要下調(diào)比較的次數(shù)。
S=2^(k-2)*1+2^(k-3)2…..+2(k-2)+2^(0)*(k-1)===>因?yàn)槿~子層不用交換,所以i從k-1開(kāi)始到1;
S=2^k-k-1;又因?yàn)閗為完全二叉樹(shù)的深度,而log(n)=k,把此式帶入;
得到:S=n-log(n)-1,所以時(shí)間復(fù)雜度為:O(n)
-------------------------------------------------------------------------------------------------------
排序重建堆時(shí)間復(fù)雜度O(NlogN)
在取出堆頂點(diǎn)放到對(duì)應(yīng)位置并把原堆的最后一個(gè)節(jié)點(diǎn)填充到堆頂點(diǎn)之后,需要對(duì)堆進(jìn)行重建,只需要對(duì)堆的頂點(diǎn)調(diào)用adjustheap()函數(shù)。
每次重建意味著有一個(gè)節(jié)點(diǎn)出堆,所以需要將堆的容量減一。adjustheap()函數(shù)的時(shí)間復(fù)雜度k=log(n),k為堆的層數(shù)。所以在每次重建時(shí),隨著堆的容量的減小,層數(shù)會(huì)下降,函數(shù)時(shí)間復(fù)雜度會(huì)變化。重建堆一共需要n-1次循環(huán),每次循環(huán)的比較次數(shù)為log(i),則相加為:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以證明log(n!)和nlog(n)是同階函數(shù):
初始化建堆的時(shí)間復(fù)雜度為O(n),排序重建堆的時(shí)間復(fù)雜度為nlog(n),所以總的時(shí)間復(fù)雜度為O(n+nlogn)=O(nlogn)。另外堆排序的比較次數(shù)和序列的初始狀態(tài)有關(guān),但只是在序列初始狀態(tài)為堆的情況下比較次數(shù)顯著減少,在序列有序或逆序的情況下比較次數(shù)不會(huì)發(fā)生明顯變化。
代碼如下:
#!user/bin/env python3
# -*- coding: gbk -*-
import time
import heapq
def adjust_heap(arr,i,len_):
# 在堆中做結(jié)構(gòu)調(diào)整使得父節(jié)點(diǎn)的值大于子節(jié)點(diǎn)
lchild = 2 * i + 1 #左節(jié)點(diǎn)
rchild = 2 * i + 2 #右節(jié)點(diǎn)
maxs = i
if i < len_ / 2:
if lchild < len_ and arr[lchild] > arr[maxs]:
maxs = lchild
if rchild < len_ and arr[rchild] > arr[maxs]:
maxs = rchild
if maxs != i:
arr[maxs],arr[i] = arr[i],arr[maxs]
adjust_heap(arr,maxs,len_) #調(diào)整maxs下面的子堆
def build_heap(arr,len_):
for i in range(len_//2,-1,-1): #從最后一個(gè)非葉子節(jié)點(diǎn)開(kāi)始到第一個(gè)非葉子節(jié)點(diǎn)結(jié)束
adjust_heap(arr,i,len_)
def heap_sort(arr):
len_ = len(arr)
#step1 建堆
build_heap(arr,len_) #將原始數(shù)組構(gòu)建成一個(gè)堆
#step2 對(duì)堆進(jìn)行排序
for i in range(len_-1,-1,-1):
arr[i],arr[0] = arr[0],arr[i]#將大頂堆的頂點(diǎn)值與最后一個(gè)葉子節(jié)點(diǎn)交換,這樣,就能得到一個(gè)從小到大的排序
adjust_heap(arr,0,i) #繼續(xù)對(duì)剩下的元素進(jìn)行堆排序,此時(shí),最大值在數(shù)組的末尾
return arr
import random
arr = [random.randint(1, 2000) for i in range(1000)]
start1_time = time.time()
res1 = heap_sort(arr)
# print(res1)
end1_time = time.time()
print('my method',end1_time-start1_time)
start2_time = time.time()
heapq.heapify(arr)
end2_time = time.time()
print('inner method',end2_time-start2_time)
#可以看到,我們實(shí)現(xiàn)的排序算法在時(shí)間上不如內(nèi)置的heapq.heapify()
2.n個(gè)數(shù)組找topk
有n個(gè)數(shù)組,每個(gè)數(shù)組中有m個(gè)元素,并且是有序排列好的,現(xiàn)在如何在這n*m個(gè)數(shù)中找出排名前500的數(shù)?
思路1:快排partition,缺點(diǎn)會(huì)改變數(shù)組順序
思路2:堆排序,速度快,不改變數(shù)組順序
(1)首先建立大頂堆,堆的大小為數(shù)組的個(gè)數(shù),即n,把每個(gè)數(shù)組最大的值(數(shù)組第一個(gè)值)存放到堆中。Python中heapq是小頂堆,通過(guò)對(duì)輸入和輸出的元素分別取相反數(shù)來(lái)實(shí)現(xiàn)大頂堆的功能
(2)接著刪除堆頂元素,保存到另外一個(gè)大小為500的數(shù)組中,然后向大頂堆插入刪除的元素所在數(shù)組的下一個(gè)元素。
(3)重復(fù)第(1)、(2)個(gè)步驟,直到刪除個(gè)數(shù)為最大的k個(gè)數(shù),這里為500。
import heapq
def getTop(data):
rowSize = len(data)
columnSize = len(data[0])
result = [None] * columnSize #保持一個(gè)最小堆,這個(gè)堆存放來(lái)自20個(gè)數(shù)組的最大數(shù)
heap = []
i = 0
while i < rowSize:
#數(shù)值,數(shù)值來(lái)源的數(shù)組,數(shù)值在數(shù)組中的次序index
arr = (-data[i][0],i,0)
heapq.heappush(heap,arr)
i += 1
num = 0
while num < columnSize:
d = heapq.heappop(heap)
result[num] = -d[0]
num += 1
if num >= columnSize:
break
arr = (-data[d[1]][d[2]+1],d[1],d[2]+1)
heapq.heappush(heap,arr)
return result
#3個(gè)數(shù)組,每個(gè)數(shù)組有5個(gè)元素且有序,找出排名前5的值
data = [[29,17,14,2,1],[19,17,16,15,6],[30,25,20,14,5]]
print(getTop(data))
七、歸并排序
關(guān)鍵兩步驟:(1)劃分子表(2)合并半子表
原理(歸并排序相比較之前的排序算法而言加入了分治法的思想):
1.如果給的數(shù)組只有一個(gè)元素的話,直接返回(也就是遞歸到最底層的一個(gè)情況)
2.把整個(gè)數(shù)組分為盡可能相等的兩個(gè)部分(分)
3.對(duì)于兩個(gè)被分開(kāi)的兩個(gè)部分進(jìn)行整個(gè)歸并排序(治)
4.把兩個(gè)被分開(kāi)且排好序的數(shù)組拼接在一起
最好/最壞/平均O(NlogN)
空間復(fù)雜度O(1)
代碼如下:
def merge(left,right):
'''將兩個(gè)長(zhǎng)度之和為n的有序子序列合并為一個(gè)有序序列,最多執(zhí)行n-1次關(guān)鍵字值間的比較,時(shí)間復(fù)雜度為O(n)'''
i,j = 0,0
result = []
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
#循環(huán)結(jié)束之后可能有一個(gè)列表有剩余元素,直接加到result后面
result += left[i:]
result += right[j:]
return result
def merge_sort(arr):#歸并排序
if len(arr) <= 1:
return arr
m = len(arr) // 2
#分
left = merge_sort(arr[:m])
right = merge_sort(arr[m:])
#合并
return merge(left,right)
arr = [3,4,2,8,9,5,1]
res = merge_sort(arr)
print(res)
八、基數(shù)排序
原理:將最低位優(yōu)先法用于單關(guān)鍵字的情況,個(gè)位-十位-百位,依此類推
時(shí)間復(fù)雜度O(Nlog(r)m)
代碼如下:
def radix_sort(arr,radix=10):
#求最大數(shù)的位數(shù)
k = len(str(max(arr)))
bucket = [[] for i in range(radix)]
tmp = 0
for i in range(1,k+1):#遍歷k位,從低位到高位
for j in arr:#遍歷每個(gè)數(shù)
tmp = j%(radix**i)//radix**(i-1)
bucket[tmp].append(j)#計(jì)算j在第k位上的數(shù)
del arr[:]#清空原arr
for z in bucket:
arr += z#按照桶內(nèi)元素的順序依次加入arr
del z[:]#清空該桶內(nèi)的元素
return arr
arr = [33,24,2,8,19,5,1]
res = radix_sort(arr)
print(res)
九、Timsort
Timsort是結(jié)合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法,它在現(xiàn)實(shí)中有很好的效率。
參考文獻(xiàn):
【1】排序算法時(shí)間復(fù)雜度、空間復(fù)雜度、穩(wěn)定性比較
【2】基數(shù)排序與桶排序,計(jì)數(shù)排序【詳解】
【3】手撕九大經(jīng)典排序算法
【4】三種快速排序以及快速排序的優(yōu)化
【5】Timsort原理介紹 - 微信公眾號(hào):猴子聊人物 - CSDN博客
總結(jié)
- 上一篇: ModelSim 修改测量时间显示的单位
- 下一篇: MyISAM引擎的特点及优化方法