聊聊幾個簡單的排序演算法
前言
排序是演算法的入門知識,其經典思想可以用在許多演算法中,在實際應用中是相當常見的一類。記得在本科的資料結構課上就有講過幾個經典的排序演算法,現在來好好地回顧下。
在回顧之前,瞭解一個概念,這個概念也是我剛剛瞭解的。(手動扶額-。-)
排序演算法穩定性:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。
這個穩定的概念有什麼用呢?不管,先記著再說啦。
一、氣泡排序(BubbleSort)
兩個相鄰的數比較大小,較大的數下沉,較小的冒起來。
過程為:
- 比較相鄰的兩個資料,如果第二個數小,就交換位置;
- 從前往後兩兩比較,一直到比較最後面兩個資料。最終最大的數被交換到末尾位置,這樣第一個最大數的位置就排好了;
- 繼續重複上述過程,依次將第2、3…n-1個最大的數排好位置。
當然也可以反著來,從後面往前比較,先排好最小的數到數列到開頭的位置。
python程式碼實現:
def bubble_sort(list):
l = len(list)
for j in range(len(list)-1): #單純地設定迴圈次數,標識趟次
for i in range(l-1 ):
if list[i] > list[i+1]:
list[i],list[i+1] = list[i+1],list[i]
else:
pass
l = l - 1 #減少比較次數,最後n位的最大數已經排好,不需要再進行比較了
return list
搬運來的動圖,特別直觀:
1.1雞尾酒排序
這是一種氣泡排序的改進演算法,可以稱之為雙向氣泡排序:
- 先對陣列從左往右進行升序的氣泡排序;
- 再對陣列進行從右往左的降序氣泡排序;
- 迴圈往復,不斷縮小沒有排序的陣列範圍。
def cocktail_sort(list):
l = len(list)
start = 0
end = l - 1
flag = True #標誌上一輪迴圈是否有交換,若無,則表示排序已經完成,無需繼續迴圈(氣泡排序優化點)
while flag:
flag = False
for i in range(start,end,1):
if list[i] > list[i+1]:
list[i],list[i+1] = list[i+1],list[i]
flag = True
else:
pass
end = end - 1
for i in range(end,start,-1):
if list[i] < list[i-1]:
list[i],list[i-1] = list[i-1],list[i]
flag = True
else:
pass
start = start + 1
return list
再搬運一張雞尾酒排序動圖:
二、 選擇排序(SelctionSort)
選擇排序十分簡單直觀,步驟如下:
- 在序列中找到最小(大)元素,存放在序列的起始位置;
- 再從剩餘的未排序序列種繼續尋找最小(大)的元素,存放在已排序序列的後一位;
- 重複到第n-1次,完成排序。
def selection_sort(list):
l = len(list)
for i in range(l-1):
_index = list.index(min(list[i:l])) #也可以再套一層迴圈,逐一比較出最小值
list[i],list[_index] = list[_index],list[i]
感謝網上的動圖:
三、插入排序(InsertionSort)
對於未排序資料,在已排序序列中從後向前掃描,找到相應位置插入。十分類似我們打撲克時抓牌的過程。
- 從第一個元素開始,單個元素當然是可以認為已排序;
- 取出下一個元素,與已排序序列從後向前掃描比較,直到找到已排序元素小於或等於新元素的位置;
- 將新元素插入到找到的位置後一個的位置;
- 繼續取下一個元素重複步驟。
def insertion_sort(lists):
l = len(lists)
for i in range(1,l): #大迴圈,開始依次抽取元素進行插入
_tmp = lists[i]
for j in list(range(i))[::-1]:
if _tmp < lists[j]: #比前一個小就繼續前進,被比較元素向後移一位
lists[j+1] = lists[j]
if j == 0: #比較到隊首了,說明臨時值是最小的
lists[j] = _tmp
else:
lists[j+1] = _tmp
break
return lists
繼續搬運:
3.1希爾排序
插入排序對那些基本有序的序列排序效率高,但對於亂序的序列,移動次數非常多導致效率較低。所以就有了插入排序的改進版——希爾排序。希爾排序會優先比較距離較遠的元素,又稱之為縮小增量排序。
- 設定步長,按步長將原序列分為若干子序列,分別對子序列進行插入排序;
- 逐漸減小步長,重複步驟1。直至步長為1,此時序列基本有序,最後進行一次插入排序。
可以看出,希爾排序可以在一開始就對距離較遠的元素進行換位、排序。避免了插入排序中,一個元素在往前比較過程中大量元素被逐一移動的過程。希爾排序的關鍵就是步長的選擇,看到一種說法說步長用質數是個不錯的選擇;也有一說用序列[1,4,13,40,121,364…]後一元素是前一元素的3倍+1;當然還有更加簡單粗暴的len/2,然後依次除以2直至1.
希爾排序動圖展示(這裡展示的步長分別為5、2、1):
程式碼略,本質上就是按照步長進行多次插入排序。
四、快速排序(Quicksort)
快速排序簡稱快排,這個排序演算法就厲害了,聽說面試官特別喜歡考,所以重點來了,同志們。
- 從序列中取出一個值作為基準;
- 把所有比基準值小的擺放在基準的左邊,比基準大的擺在基準的右邊(相等的數任意一邊)。這就完成了一次分割槽操作;
- 對左右兩個子序列繼續做這樣的分割槽操作,直至每個區間只有一個數。這是一個遞迴過程。
失敗的一次嘗試:
def quick_sort(arr):
lefti = 0
righti = len(arr) - 1
if righti == -1:
return 0
else:
x = arr[0] #x即為基準
count = 0
k = 0
while lefti < righti and count < 2:
for i in range(righti,lefti,-1):
if righti - lefti == 1:
count += 1
if arr[i] < x and count < 2:
arr[lefti] = arr[i]
righti = i
k = i
lefti += 1
break
for j in range(lefti,righti,1):
if righti - lefti == 1:
count += 1
if arr[j] > x and count < 2:
arr[righti] = arr[j]
lefti = j
k = j
righti -= 1
break
arr[k] = x
quick_sort(arr[:k])
quick_sort(arr[k+1:])
排序過程沒問題,只是迭代時修改的不是原陣列,而是新的被拆分的陣列,導致只有第一層迴圈的元素交換被保留。無奈還是參照下別的程式碼學習一哈把,coding能力還是有待加強。
參照資料後的改進版:
def quick_sort(arr,left,right):
if left >= right:
return
low = left
high = right
key = arr[low] #取序列的第一個值為基準
while left < right:
while left < right and arr[right] >= key: #外層迴圈裡已經有left<right但內層迴圈裡仍然需要,因為要保證left和rigth最終會合相等,而不能讓left在自增過程中超過right
right -= 1
arr[left] = arr[right]
while left < right and arr[left] <= key:
left += 1
arr[right] = arr[left]
arr[left] = key #此時left和right已經相等
quick_sort(arr,low,left-1)
quick_sort(arr,left+1,high)
大神秀技巧版(一行程式碼實現):
quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]])
這個lambda真的很精妙,用兩個列表生成式拼接出完整列表,所有小於等於arr[0]的放左邊,所有大於arr[0]的放右邊。劣勢是佔用了新的記憶體空間,常規版的快排是in-palce的,都是原地操作。
動圖演示:
五、歸併排序(MergeSort)
將兩個有序序列合併的方法很簡單,比較2個序列的第一個數,誰小就取誰,取出後刪除對應序列中的這個數。繼續比較直至所有元素都被取出。將兩個有序序列合併的過程稱為2-路歸併。歸併排序就基於此:
- 把待排序的序列一分為二,分出兩個子序列;
- 繼續將子序列分裂直至子序列中只有一個元素,一個元素自然就算排序完成;
- 一路分裂一路歸併,最終獲得完整序列。
def merge(arrX,arrY): #合併排序演算法
i,j = 0,0
arrN = []
while i < len(arrX) and j < len(arrY):
if arrX[i] < arrY[j]:
arrN.append(arrX[i])
i += 1
else:
arrN.append(arrY[j])
j += 1
if i == len(arrX):
arrN.extend(arrY[j:])
if j == len(arrY):
arrN.extend(arrX[i:])
return arrN
def merge_sort(arr): #迭代過程
l = len(arr)
if l <= 1:
return arr
else:
X = merge_sort(arr[:round(l/2)])
Y = merge_sort(arr[round(l/2):])
return merge(X,Y)
動圖演示:
六、計數排序(CountingSort)
計數排序不是基於比較的排序演算法,它依靠一個輔助陣列來實現,將輸入的資料轉化為鍵儲存在專門準備的陣列空間中,計數排序要求輸入的資料必須是正整數,且最好不要過大。計數排序是用來排序0到100之間的數字且重複項比較多的最好的演算法。
- 找到待排序序列種的最大值(也可以找出最小值,建立中間陣列時節省一定的空間);
- 統計序列中每個值為i的元素出現的次數,存入陣列C的第i項;
- 對所有計數從低到高向上累加,陣列C[i]中的值會變成所有小於等於i的元素個數;
- 從原序列反向填充目標陣列:將每個元素i填入新陣列的第C[i]項,每放一個元素C[i]減1.
def counting_sort(arr):
m = max(arr)
c = [0 for i in range(m+1)]
for i in arr:
c[i] += 1 #c[i]表示在原序列arr中值為i的元素有幾個
for j in range(1,m+1):
c[j] = c[j] + c[j-1] #c[j]表示在原arr序列中最後一個值為j的元素排第幾位,或者表示小於等於j的元素個數
res = [None for i in range(len(arr))]
for r in range(len(arr)-1,-1,-1):
res[c[arr[r]]-1] = arr[r] #因為索引是從0開始的,需要-1
c[arr[r]] -= 1 #放置好一個元素,就需要在c陣列中去掉一個元素,最後c陣列會變成全0的陣列
return res
依然是動圖伺候:
後話
其實還是有一些排序演算法沒有涉及,比如桶排序、基數排序、堆排序。畢竟不是專業的程式設計師,就不繼續深究了。文中展示的所有程式碼都是本人手寫並執行驗證通過的,可放心複製貼上食用。
關於複雜度也可以一張圖說明:
還記得開頭講過的穩定性麼,開寫這篇博文時並不明白其作用。實現了這幾個排序演算法後也自己琢磨明白了一點。穩定的好處是:從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。
也就是說,實際應用中我們遇到的排序不是簡單地針對一個數組或者一個序列,而是有很多維度的。我們針對其中一個維度進行穩定排序,原先其他維度的先後順序不會被改變。這才是穩定性的意義所在。
最後感謝來自他人部落格的動圖,轉載宣告:
來源於一畫素的部落格:https://www.cnblogs.com/onepixel/articles/7674659.html