文章目錄
十大排序算法
排序算法介紹
非線性時間比較類排序:
通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱爲非線性時間比較類排序。
線性時間非比較類排序:
不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此稱爲線性時間非比較類排序。
相關概念
- 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面
- 不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
- 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
- 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數
交換排序
1冒泡排序(Bubble Sort)
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 重複步驟1~3,直到排序完成。
冒泡排序對n個數據操作n-1輪,每輪找出一個最大(小)值。
操作只對相鄰兩個數比較與交換,每輪會將一個最值交換到數據列首(尾),像冒泡一樣。
每輪操作O(n)次,共O(n)輪,時間複雜度O(n^2)。
額外空間開銷出在交換數據時那一個過渡空間,空間複雜度O(1)
def buddle_sort(arr):
n=len(arr)
for i in range(n-1):
count=0
for j in range(0,n-i-1):
if arr[j]>arr[j+1]:
arr[j],arr[j+1]=arr[j+1],arr[j]
count+=1
if count==0:
break
if __name__=="__main__":
li=[5,9,6,3,4,2,7]
print(li)
buddle_sort(li)
print(li)
2 快速排序(Quick Sort)
- 從數列中挑出一個元素,稱爲 “基準”(pivot);
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作;
- 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
快速排序基於選擇劃分,是簡單選擇排序的優化。
每次劃分將數據選到基準值兩邊,循環對兩邊的數據進行劃分,類似於二分法。
算法的整體性能取決於劃分的平均程度,即基準值的選擇,此處衍生出快速排序的許多優化方案,甚至可以劃分爲多塊。
基準值若能把數據分爲平均的兩塊,劃分次數O(logn),每次劃分遍歷比較一遍O(n),時間複雜度O(nlogn)。
額外空間開銷出在暫存基準值,O(logn)次劃分需要O(logn)個,空間複雜度O(logn)
def quick_sort(arr,first,last):
if first>=last:
return
n=len(arr)
left=first
right=last
mid_value=arr[first]
while left<right:
while left<right and arr[right]>=mid_value:
right-=1
arr[left]=arr[right]
while left<right and arr[left]<mid_value:
left+=1
arr[right]=arr[left]
arr[left]=mid_value
quick_sort(arr,first,left-1)
quick_sort(arr,left+1,last)
if __name__=="__main__":
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
quick_sort(li,0,len(li)-1)
print(li)
插入排序
3 簡單插入排序(Insert Sort)
- 從第一個元素開始,該元素可以認爲已經被排序;
- 取出下一個元素,在已經排序的元素序列中從後向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置後;
- 重複步驟2~5。
- 簡單插入排序同樣操作n-1輪,每輪將一個未排序樹插入排好序列。
開始時默認第一個數有序,將剩餘n-1個數逐個插入。插入操作具體包括:比較確定插入位置,數據移位騰出合適空位
每輪操作O(n)次,共O(n)輪,時間複雜度O(n^2)。
額外空間開銷出在數據移位時那一個過渡空間,空間複雜度O(1)。
def insert_sort(arr):
n=len(arr)
for i in range(1,n):
j=i
while j>0:
if arr[j]<arr[j-1]:
arr[j-1],arr[j]=arr[j],arr[j-1]
j-=1
else:
break
if __name__=="__main__":
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
insert_sort(li)
print(li)
4希爾排序(Shell Sort)
先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,具體算法描述:
- 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
希爾排序是插入排序的高效實現(大家可以比對一下插入排序和希爾排序的代碼),對簡單插入排序減少移動次數優化而來。
簡單插入排序每次插入都要移動大量數據,前後插入時的許多移動都是重複操作,若一步到位移動效率會高很多。
若序列基本有序,簡單插入排序不必做很多移動操作,效率很高。
希爾排序將序列按固定間隔劃分爲多個子序列,在子序列中簡單插入排序,先做遠距離移動使序列基本有序;逐漸縮小間隔重複操作,最後間隔爲1時即簡單插入排序。
希爾排序對序列劃分O(n)次,每次簡單插入排序O(logn),時間複雜度O(nlogn)
額外空間開銷出在插入過程數據移動需要的一個暫存,空間複雜度O(1)
(1)希爾排序(shell sort)這個排序方法又稱爲縮小增量排序,是1959年D·L·Shell提出來的。該方法的基本思想是:設待排序元素序列有n個元素,首先取一個整數increment(小於n)作爲間隔將全部元素分爲increment個子序列,所有距離爲increment的元素放在同一個子序列中,在每一個子序列中分別實行直接插入排序。然後縮小間隔increment,重複上述子序列劃分和排序工作。直到最後取increment=1,將所有元素放在同一個子序列中排序爲止。
(2)由於開始時,increment的取值較大,每個子序列中的元素較少,排序速度較快,到排序後期increment取值逐漸變小,子序列中元素個數逐漸增多,但由於前面工作的基礎,大多數元素已經基本有序,所以排序速度仍然很快。
(3)希爾排序舉例:
第一趟取increment的方法是:n/3向下取整+1=3(關於increment的取法之後會有介紹)。將整個數據列劃分爲間隔爲3的3個子序列,然後對每一個子序列執行直接插入排序,相當於對整個序列執行了部分排序調整。圖解如下:
第二趟將間隔increment= increment/3向下取整+1=2,將整個元素序列劃分爲2個間隔爲2的子序列,分別進行排序。圖解如下:
第3趟把間隔縮小爲increment= increment/3向下取整+1=1,當增量爲1的時候,實際上就是把整個數列作爲一個子序列進行插入排序,圖解如下:
到increment=1時,就是對整個數列做最後一次調整,因爲前面的序列調整已經使得整個序列部分有序,所以最後一次調整也變得十分輕鬆,這也是希爾排序性能優越的體現。
關於希爾排序increment(增量)的取法。
增量increment的取法有各種方案。最初shell提出取increment=n/2向下取整,increment=increment/2向下取整,直到increment=1。但由於直到最後一步,在奇數位置的元素纔會與偶數位置的元素進行比較,這樣使用這個序列的效率會很低。後來Knuth提出取increment=n/3向下取整+1.還有人提出都取奇數爲好,也有人提出increment互質爲好。應用不同的序列會使希爾排序算法的性能有很大的差異。
def shell_sort(arr):
n=len(arr)
gap=n//2
while gap>0:
for i in range(1,n):
j=i
while j>0:
if arr[j]<arr[j-gap]:
arr[j-gap],arr[j]=arr[j],arr[j-gap]
j-=gap
else:
break
gap//=2
if __name__=="__main__":
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
shell_sort(li)
print(li)
def ShellSort(lst):
def shellinsert(arr,d):
n=len(arr)
for i in range(d,n):
j=i-d
temp=arr[i] #記錄要出入的數
while(j>=0 and arr[j]>temp): #從後向前,找打比其小的數的位置
arr[j+d]=arr[j] #向後挪動
j-=d
if j!=i-d:
arr[j+d]=temp
n=len(lst)
if n<=1:
return lst
d=n//2
while d>=1:
shellinsert(lst,d)
d=d//2
return lst
選擇排序
5.簡單選擇排序(Select Sort)
- 初始狀態:無序區爲R[1…n],有序區爲空;
- 第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別爲R[1…i-1]和R(i…n)。該趟排序從當前無序區中-選出關鍵字最小的記錄R[k],將它與無序區的第1個記錄R交換,使R[1…i]和R[i+1…n)分別變爲記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
- n-1趟結束,數組有序化了。
簡單選擇排序同樣對數據操作n-1輪,每輪找出一個最大(小)值。
操作指選擇,即未排序數逐個比較交換,爭奪最值位置,每輪將一個未排序位置上的數交換成已排序數,即每輪選一個最值。
每輪操作O(n)次,共O(n)輪,時間複雜度O(n^2)。
額外空間開銷出在交換數據時那一個過渡空間,空間複雜度O(1)。
def select_sort(arr):
n=len(arr)
for i in range(n-1):
min_index=i
for j in range(i+1,n):
if arr[min_index]>arr[j]:
min_index=j
arr[i],arr[min_index]=arr[min_index],arr[i]
if __name__=="__main__":
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
select_sort(li)
print(li)
堆排序
選擇排序最大的問題,就是不能知道待排序數據是否已經有序,比較了所有數據也沒有在比較中確定數據的順序。
堆排序對簡單選擇排序進行了改進。
準備知識
- 堆排序是利用 堆進行排序的
- 堆是一種完全二叉樹
- 堆有兩種類型: 大根堆 小根堆
兩種類型的概念如下:
- 大根堆:每個結點的值都大於或等於左右孩子結點
- 小根堆:每個結點的值都小於或等於左右孩子結點
因爲比較抽象,所以專門花了兩個圖表示
完全二叉樹 是 一種除了最後一層之外的其他每一層都被完全填充,並且所有結點都保持向左對齊的樹,向左對齊指的是:
像這樣的樹就不是完全二叉樹:
如果給上面的大小根堆的根節點從1開始編號,則滿足下面關係(下圖就滿足這個關係):
如果把這些數字放入數組中,則如下圖所示:其中,上面的數字是數組下標值,第一個元素佔位用。
堆排序算法詳解+Python實現
瞭解了堆。下面我們來看下堆排序的思想是怎樣的(以大根堆爲例):
- 首先將待排序的數組構造出一個大根堆
- 取出這個大根堆的堆頂節點(最大值),與堆的最下最右的元素進行交換,然後把剩下的元素再構造出一個大根堆
- 重複第二步,直到這個大根堆的長度爲1,此時完成排序。
下面通過圖片來看下,第二個步驟是如何進行的:
參考
from collections import deque
def swap(arr,i,j):
arr[i],arr[j]=arr[j],arr[i]
return arr
def heap_adjust(arr,start,end):
temp=arr[start]
i=start
j=2*i
while j <=end:
if (j<end) and (arr[j]<arr[j+1]):
j+=1
if temp<arr[j]:
arr[i]=arr[j]
i=j
j=2*i
else:
break
arr[i]=temp
def heap_sort(arr):
length=len(arr)-1
start=length//2
for i in range(start):
heap_adjust(arr,start-i,length)
for i in range(length-1):
swap(arr,1,length-i)
heap_adjust(arr,1,length-i-1)
return [arr[i] for i in range(1,len(arr))]
def main():
arr=deque([50, 16, 30, 10, 60, 90, 2, 80, 70])
arr.appendleft(0)
print(heap_sort(arr))
if __name__=="__main__":
main()
歸併排序
二路歸併排序(Two-way Merge Sort)
歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
- 把長度爲n的輸入序列分成兩個長度爲n/2的子序列;
- 對這兩個子序列分別採用歸併排序;
- 將兩個排序好的子序列合併成一個最終的排序序列。
def merge_sort(arr):
n=len(arr)
if n<=1:
return arr
mid=n//2
left=merge_sort(arr[:mid])
right=merge_sort(arr[mid:])
return merge(left,right)
def merge(left,right):
l,r=0,0
res=[]
while l<len(left) and r<len(right):
if left[l]<right[r]:
res.append(left[l])
l+=1
else:
res.append(right[r])
r+=1
res+=right[r:]
res+=left[l:]
return res
if __name__=="__main__":
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
sorted_merge=merge_sort(li)
print(sorted_merge)
線性時間非比較類排序
8 計數排序
- 找出待排序的數組中最大和最小的元素;
- 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項;
- 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
- 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。
計數排序用待排序的數值作爲計數數組(列表)的下標,統計每個數值的個數,然後依次輸出即可。
計數數組的大小取決於待排數據取值範圍,所以對數據有一定要求,否則空間開銷無法承受。
計數排序只需遍歷一次數據,在計數數組中記錄,輸出計數數組中有記錄的下標,時間複雜度爲O(n+k)。
額外空間開銷即指計數數組,實際上按數據值分爲k類(大小取決於數據取值),空間複雜度O(k)。
def count_sort(arr):
n=len(arr)
num=max(arr)
count=[0]*(num+1)
for i in range(n):
count[arr[i]]+=1
arr=[]
for i in range(num+1):
for j in range(count[i]):
arr.append(i)
return arr
計數排序是一個穩定的排序算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k),空間複雜度也是O(n+k),其排序速度快於任何比較排序算法。當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法。
9 桶排序(Bucket Sort)
- 設置一個定量的數組當作空桶;
- 遍歷輸入數據,並且把數據一個一個放到對應的桶裏去;
- 對每個不是空的桶進行排序;
- 從不是空的桶裏把排好序的數據拼接起來。
桶排序實際上是計數排序的推廣,但實現上要複雜許多。
桶排序先用一定的函數關係將數據劃分到不同有序的區域(桶)內,然後子數據分別在桶內排序,之後順次輸出。
當每一個不同數據分配一個桶時,也就相當於計數排序。
def bucket_sort(s):
"""桶排序"""
min_num = min(s)
max_num = max(s)
# 桶的大小
bucket_range = (max_num-min_num) / len(s)
# 桶數組
count_list = [ [] for i in range(len(s) + 1)]
# 向桶數組填數
for i in s:
count_list[int((i-min_num)//bucket_range)].append(i)
s.clear()
# 回填,這裏桶內部排序直接調用了sorted
for i in count_list:
for j in sorted(i):
s.append(j)
if __name__ == '__main__':
a = [3.2,6,8,4,7,6,7,9]
bucket_sort(a)
print(a)
桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,因爲其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
基數排序(Radix Sort)
- 取得數組中的最大數,並取得位數;
- arr爲原始數組,從最低位開始取每個位組成radix數組;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
def radix_sort(s):
"""基數排序"""
i = 0 # 記錄當前正在排拿一位,最低位爲1
max_num = max(s) # 最大值
j = len(str(max_num)) # 記錄最大值的位數
while i < j:
bucket_list =[[] for _ in range(10)] #初始化桶數組
for x in s:
bucket_list[int(x / (10**i)) % 10].append(x) # 找到位置放入桶數組
print(bucket_list)
s.clear()
for x in bucket_list: # 放回原序列
for y in x:
s.append(y)
i += 1
if __name__ == '__main__':
a = [334,5,67,345,7,345345,99,4,23,78,45,1,3453,23424]
radix_sort(a)
print(a)