排序算法彙總
在筆試面試的過程當中,經常會考察一下常見的幾種排序算法,包括冒泡排序,選擇排序,插入排序,希爾排序,快速排序,堆排序歸併排序等7種排序算法,下面將分別進行講解:css
1.冒泡排序
所謂冒泡排序法,就是對一組數字進行從大到小或者從小到大排序的一種算法。具體方法是,相鄰數值兩兩交換。從第一個數值開始,若是相鄰兩個數的排列順序與咱們的指望不一樣,則將兩個數的位置進行交換(對調);若是其與咱們的指望一致,則不用交換。重複這樣的過程,一直到最後沒有數值須要交換,則排序完成。通常地,若是有N個數須要排序,則須要進行(N-1)趟起泡。html
冒泡排序是最簡單的排序之一了,其大致思想就是經過與相鄰元素的比較和交換來把小的數交換到最前面。這個過程相似於水泡向上升同樣,所以而得名。舉個栗子,對5,3,8,6,4這個無序序列進行冒泡排序。首先從後向前冒泡,4和6比較,把4交換到前面,序列變成5,3,8,4,6。同理4和8交換,變成5,3,4,8,6,3和4無需交換。5和3交換,變成3,5,4,8,6,3.這樣一次冒泡就完了,把最小的數3排到最前面了。對剩下的序列依次冒泡就會獲得一個有序序列。冒泡排序的時間複雜度爲O(n^2)。python
-
def BubbleSort(A):
#冒泡,一趟最小排最前,N-1趟
-
if A ==
None
or len(A) ==
0:
-
return
-
for i
in range(len(A)):
-
for j
in range(len(A)
-1, i,
-1):
-
if A[j] < A[j
-1]:
-
tmp = A[j
-1]
#Swap()
-
A[j
-1] = A[j]
-
A[j] = tmp
-
#print(A)
-
return A
冒泡排序的改進:面試
在冒泡排序中,經過先後2個數據的兩兩交換,來完成排序過程,而若是某一趟並無發生交換,說明此時序列已經有序,就能夠終止排序過程。算法
-
def BubbleSort2(A):
-
flag =
True
-
for i
in range(len(A)):
-
if flag:
#爲真時才執行一趟
-
for j
in range(len(A)
-1, i,
-1):
-
flag =
False
-
if A[j] < A[j
-1]:
-
tmp = A[j
-1]
#Swap()
-
A[j
-1] = A[j]
-
A[j] = tmp
-
flag =
True
#交換
-
#print(A)#看比較的次數
-
return A
2.選擇排序
選擇排序簡單的說就是每次找到序列中的最小值,而後將該值放在有序序列的最後一個位置,以造成一個更大的有序序列。選擇排序進行n趟,每趟從i+1開始,每趟找到最小值下標min,再將a[min]與a[i]交換。swift
選擇排序的思想其實和冒泡排序有點相似,都是在一次排序後把最小的元素放到最前面。可是過程不一樣,冒泡排序是經過相鄰的比較和交換。而選擇排序是經過對總體的選擇。舉個栗子,對5,3,8,6,4這個無序序列進行簡單選擇排序,首先要選擇5之外的最小數來和5交換,也就是選擇3和5交換,一次排序後就變成了3,5,8,6,4.對剩下的序列一次進行選擇和交換,最終就會獲得一個有序序列。其實選擇排序能夠當作冒泡排序的優化,由於其目的相同,只是選擇排序只有在肯定了最小數的前提下才進行交換,大大減小了交換的次數。選擇排序的時間複雜度爲O(n^2)。數組
-
def
SelectSort(
A):
-
for i
in range(len(
A)):
-
min = i#最小值所在的位置,最小放最前
-
for j
in range(i+
1, len(
A)):
-
if
A[
min] >
A[j]:
-
min = j
-
if
min != i:
-
tmp =
A[
min]
-
A[
min] =
A[i]
-
A[i] = tmp
-
return
A
3.插入排序
插入排序能夠簡單歸納爲:假定序列下標i以前數據是有序的,則從i-1位置數據開始,依次將其與i進行比較並交換(當該值不知足插入條件,即該位置值大於i位置值時),最終找到一個合適的位置插入下標i數據,以造成一個更大的有序序列。 ruby
插入排序不是經過交換位置而是經過比較找到合適的位置插入元素來達到排序的目的的。相信你們都有過打撲克牌的經歷,特別是牌數較大的。在分牌時可能要整理本身的牌,牌多的時候怎麼整理呢?就是拿到一張牌,找到一個合適的位置插入。這個原理其實和插入排序是同樣的。舉個栗子,對5,3,8,6,4這個無序序列進行簡單插入排序,首先假設第一個數的位置時正確的,想一下在拿到第一張牌的時候,不必整理。而後3要插到5前面,把5後移一位,變成3,5,8,6,4.想一下整理牌的時候應該也是這樣吧。而後8不用動,6插在8前面,8後移一位,4插在5前面,從5開始都向後移一位。注意在插入一個數的時候要保證這個數前面的數已經有序。簡單插入排序的時間複雜度也是O(n^2)。函數
-
def InsertSort(A):
-
for i
in range(
1, len(A)):
#第一個默認有序
-
tmp = A[i]
-
for j
in range(i
-1,
-1,
-1):
#j=i-1,j>=0,j--
-
if tmp < A[j]:
-
A[j+
1] = A[j]
-
A[j] = tmp
-
return A
4.希爾排序
希爾排序算法能夠歸納爲:先將整個待排序序列分割成若干個子序列(通常分紅2個),分別進行直接插入排序,而後依次縮減增量再進行排序,待整個序列中整個元素增量爲1時,再對全體元素進行一次直接插入排序。post
希爾排序是插入排序的一種高效率的實現,也叫縮小增量排序。簡單的插入排序中,若是待排序列是正序時,時間複雜度是O(n),若是序列是基本有序的,使用直接插入排序效率就很是高。希爾排序就利用了這個特色。基本思想是:先將整個待排記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄基本有序時再對全體記錄進行一次直接插入排序。
希爾排序的分析是複雜的,時間複雜度是所取增量的函數,這涉及一些數學上的難題。可是在大量實驗的基礎上推出當n在某個範圍內時,時間複雜度能夠達到O(n^1.3)。
-
def ShellSort(A):
#希爾排序/至關加了個間隔,將數據分組處理
-
gap = len(A) /
2
-
while gap >=
1:
#下面就是一個插入排序過程,只是每一個過程都是有間隔,j+gap
-
for i
in range(gap, len(A)):
-
tmp = A[i]
-
j = i - gap
-
while j >=
0
and tmp < A[j]:
-
A[j+gap] = A[j]
-
j -= gap
-
A[j+gap] = tmp
-
gap /=
2
-
return A
5.快速排序
快速排序通常是選定第一個數爲基準數,而後分別從後向前找比基準數小的數,從前向後找比基準數大的數,而後交換先後找到的數的位置,並在最後爲基準數找到一個合適的位置,使得基準數左側的數據都比基準數小,基準數右側的數據都比基準數大,而後以基準數爲界將序列分爲左右2個子序列,最後利用遞歸分解的方法完成排序過程。
提示:在遇到選擇或者填空題時,在作某一趟的快速排序推算時,用「挖坑填數法」+「分治法」,而在寫程序時,用「交換法」+「分治法」。
快速排序在實際應用當中快速排序確實也是表現最好的排序算法。快速排序雖然高端,但其實其思想是來自冒泡排序,冒泡排序是經過相鄰元素的比較和交換把最小的冒泡到最頂端,而快速排序是比較和交換小數和大數,這樣一來不只把小數冒泡到上面同時也把大數沉到下面。
舉個例子:對5,3,8,6,4這個無序序列進行快速排序,思路是右指針找比基準數小的,左指針找比基準數大的,交換之。
5,3,8,6,4 用5做爲比較的基準,最終會把5小的移動到5的左邊,比5大的移動到5的右邊。
5,3,8,6,4 首先設置i,j兩個指針分別指向兩端,j指針先掃描(首先這也不是絕對的,這取決於基準數的位置,由於在最後兩個指針相遇的時候,要交換基準數到相遇的位置。通常選取第一個數做爲基準數,那麼就是在左邊,因此最後相遇的數要和基準數交換,那麼相遇的數必定要比基準數小。因此j指針先移動才能先找到比基準數小的數。)4比5小中止。而後i掃描,8比5大中止。交換i,j位置。
5,3,4,6,8 而後j指針再掃描,這時j掃描4時兩指針相遇。中止。而後交換4和基準數。
4,3,5,6,8 一次劃分後達到了左邊比5小,右邊比5大的目的。以後對左右子序列遞歸排序,最終獲得有序序列。
快速排序是不穩定的,其時間平均時間複雜度是O(nlgn)。
-
def QuickSort(A, left, right):
-
#left = 0
-
#right = len(A)-1
-
i = left
-
j = right
-
if i > j:
-
return
-
mid = A[i]
#初始值爲第一個
-
while i < j:
-
#先從right高位開始
-
while i < j
and A[j] >=
mid:
-
j -=
1
-
A[i] = A[j]
#小的移到左邊
-
while i < j
and A[i] <=
mid:
-
i +=
1
-
A[j] = A[i]
#大的移到右邊
-
#print(i,j)
-
A[i] =
mid
#中間,也能夠A[j]=mid,此時i=j
-
QuickSort(A, left, j
-1)
#左遞歸
-
QuickSort(A, i+
1, right)
#右遞歸
-
return A
6.堆排序
堆排序其實是利用堆的性質來進行排序的。
堆的定義:
堆其實是一棵徹底二叉樹。
堆知足兩個性質:
一、堆的每個父節點都大於(或小於)其子節點;
二、堆的每一個左子樹和右子樹也是一個堆。
堆的分類:
堆分爲兩類:
一、最大堆(大頂堆):堆的每一個父節點都大於其孩子節點;
二、最小堆(小頂堆):堆的每一個父節點都小於其孩子節點;
堆的存儲:
通常都用數組來表示堆,i結點的父結點下標就爲(i – 1) / 2。它的左右子結點下標分別爲2 * i + 1和2 * i + 2。以下圖所示:
堆排序:
由上面的介紹咱們能夠看出堆的第一個元素要麼是最大值(大頂堆),要麼是最小值(小頂堆),這樣在排序的時候(假設共n個節點),直接將第一個元素和最後一個元素進行交換,而後從第一個元素開始進行向下調整至第n-1個元素。因此,若是須要升序,就建一個大堆,須要降序,就建一個小堆。
堆排序的步驟分爲三步:
一、建堆(升序建大堆,降序建小堆);
二、交換數據;
三、向下調整。
假設咱們如今要對數組arr[]={8,5,0,3,7,1,2}進行排序(降序):
首先要先建小堆:
堆建好了下來就要開始排序了:
-
class Solution(object):#小頂堆,降序
-
def HeapAdjust(self, A, i, n)
:
#刪除
-
tmp = A[i]
#i表示當前節點開始調整,主要是通用性
-
index =
2*i+
1
-
while index <
n:
-
if index+
1 < n
and A[index+
1] < A[index]
:
#找到左右兒子最小值的索引
-
index +=
1
-
if tmp < A[index]
:
#知足該條件時說明原始堆有序
-
break
-
#將最小兒子上移動
-
A[i] = A[index]
-
i = index
-
index =
2*i+
1
-
A[i] = tmp
#temp一直沒有變,並且用來做爲比較的參考值
-
print(A)
-
def ConstructMinHeap(self, A, n)
:
#構建小根堆
-
#葉子節點不用參與重組,至關因而已經建好的堆
-
for i
in range(n/
2-
1, -
1, -
1):
-
self.HeapAdjust(A, i, n)
-
def HeapSort(self, A):
-
#第一次將A[0]與A[n - 1]交換,再對A[0…n-2]從新恢復堆,
-
#第二次將A[0]與A[n – 2]交換,再對A[0…n - 3]從新恢復堆,
-
#重複這樣的操做直到A[0]與A[1]交換。
-
self.ConstructMinHeap(A, len(A))
#構建
-
for i
in range(len(A)-
1,
0, -
1):
-
tmp = A[i]
-
A[i] = A[
0]
-
A[
0] = tmp
-
self.HeapAdjust(A,
0, i)
-
return A
從一個無序序列建堆的過程就是一個反覆篩選的過程。若將此序列當作是一個徹底二叉樹,則最後一個非終端節點是n/2取底個元素,由此篩選便可。舉個栗子:
對一個無序的序列A={5,4,17,13,15,12,10 }按從小到大進行排序,序列的下標分別爲{1,2,3,4,5,6,7},A[i]表示下標爲i的元素。
第一步:對無序的數組構造大根堆
大根堆的根節點是整個序列的最大值。
第二步:
將A[1]與A[7]互換,此時A[7]爲序列的最大值,A[7]已經排序完畢,剩餘的元素A[1]~A[6]造成新的未排序序列,因爲此時序列不是大根堆,須要重構大根堆。
第三步:
將A[1]與A[6]互換,此時A[6]爲序列的最大值,A[6]已經排序完畢,剩餘的元素A[1]~A[5]造成新的未排序序列,因爲此時序列不是大根堆,須要重構大根堆。
第四步:
將A[1]與A[5]互換,此時A[5]爲序列的最大值,A[5]已經排序完畢,剩餘的元素A[1]~A[4]造成新的未排序序列,因爲此時序列不是大根堆,須要重構大根堆。
第五步:
將A[1]與A[4]互換,此時A[4]爲序列的最大值,A[4]已經排序完畢,剩餘的元素A[1]~A[3]造成新的未排序序列,因爲此時序列不是大根堆,須要重構大根堆。
第六步:
將A[1]與A[3]互換,此時A[3]爲序列的最大值,A[3]已經排序完畢,因爲此時未排序的序列只剩下兩個元素,並且A[0]>A[1],將A[0]與A[1]互換便可獲得最終的已排序序列。
-
class Solution2(object):#大頂堆,升序
-
def HeapAdjust(self, A, i, n):
-
tmp = A[i]
-
index =
2*i+
1
#左右孩子的節點分別爲2*i+1,2*i+2
-
while index <=
n:
-
#選擇出左右孩子較小的下標
-
if index < n
and A[index] < A[index+
1]:
-
index +=
1
-
if tmp >= A[index]
:
#已經爲大頂堆,=保持穩定性
-
break
-
A[i] = A[index]
#將子節點上移
-
i = index
#下一輪篩選
-
index *=
2
#右孩子的節點
-
A[i] = tmp
#temp一直沒有變,插入正確的位置
-
print(A)
-
def ConstructMaxHeap(self, A, n)
:
#構建大根堆
-
#葉子節點不用參與重組,至關因而已經建好的堆
-
for i
in range(n/
2-
1, -
1, -
1):
-
self.HeapAdjust(A, i, n-
1)
-
def HeapSort2(self, A):
-
#第一次將A[0]與A[n - 1]交換,再對A[0…n-2]從新恢復堆,
-
#第二次將A[0]與A[n – 2]交換,再對A[0…n - 3]從新恢復堆,
-
#重複這樣的操做直到A[0]與A[1]交換。
-
self.ConstructMaxHeap(A, len(A))
#構建
-
for i
in range(len(A)-
1, -
1, -
1):
-
tmp = A[i]
-
A[i] = A[
0]
-
A[
0] = tmp
-
self.HeapAdjust(A,
0, i-
1)
-
return A
7.歸併排序
對於歸併排序,記好一句話便可:遞歸的分解+合併。另外歸併排序須要O(n)的輔助空間
歸併排序是另外一種不一樣的排序方法,由於歸併排序使用了遞歸分治的思想,因此理解起來比較容易。其基本思想是,先遞歸劃分子問題,而後合併結果。把待排序列當作由兩個有序的子序列,而後合併兩個子序列,而後把子序列當作由兩個有序序列。。。。。倒着來看,其實就是先兩兩合併,而後四四合並。。。最終造成有序序列。空間複雜度爲O(n),時間複雜度爲O(nlogn)。
-
#/usr/bin/python
-
#coding:-*-utf
-8-*-
-
class Solution(object):
-
def MergeSort(self, A):
-
left =
0
-
right =
len(A)
-1
-
self.MergeArray(A,
left,
right)
-
return A
-
def Merge(self, A,
left,
mid,
right):
-
tmp = [
0]*(
right-
left+
1)#
len(A),初始化tmp中間數組
-
i =
left
-
j =
mid+
1
-
k =
0
-
while i <=
mid
and j <=
right:
-
if A[i] <= A[j]:
-
tmp[k] = A[i]
-
k +=
1
-
i +=
1
-
else:
-
tmp[k] = A[j]
-
k +=
1
-
j +=
1
-
print(A)
-
while i <=
mid:
-
tmp[k] = A[i]
-
k +=
1
-
i +=
1 #tmp[k++] = A[i++],不會發生越界,由於i也是先賦值,再++;
-
while j <=
right:
-
tmp[k] = A[j]
-
k +=
1
-
j +=
1
-
#將輔助空間內的數據轉移到原始數組A
-
for p
in range(
len(tmp)):
-
A[
left+p] = tmp[p]
-
def MergeArray(self, A,
left,
right):
-
if
left >=
right:
-
return
-
mid = (
left+
right)/
2
-
self.MergeArray(A,
left,
mid)#左邊
-
self.MergeArray(A,
mid+
1,
right)#右邊
-
self.Merge(A,
left,
mid,
right)#合併
幾種排序算法的性能比較
1:複雜度
平均複雜度:
O(N^2)的有冒泡排序、插入排序、選擇排序
O(N*logN)的有希爾排序、歸併排序、快速排序、堆排序
複雜度最壞狀況:冒泡排序、插入排序、選擇排序、快速排序均爲O(N^2)(對於快速排序:最壞的狀況,待排序的序列爲正序或者逆序,每次劃分只獲得一個比上一次劃分少一個的子序列,另一個爲空。若是遞歸樹畫出來,就是一顆斜樹。此時須要執行n-1次遞歸調用,且第i次劃分須要經(n-i)次關鍵字比較才能找到才能找到第i個記錄,所以比較的次數爲(n-1)+(n-2)+…+1 = n*(n-1)/2,最終時間複雜度爲O(n^2)),歸併排序,堆排序均爲O(N*logN)。
複雜度最好狀況:冒泡排序、插入排序均爲O(N),選擇排序仍爲O(N^2),歸併排序,快速排序,堆排序仍爲O(N*logN)。
最好、最壞、平均三項複雜度全是同樣的、就是與初始排序無關的排序方法爲:選擇排序、堆排序、歸併排序。
2:空間複雜度
除歸併排序空間複雜度爲O(N),快速排序空間複雜度爲O(logN)外,其餘幾種排序方法空間複雜度均爲O(1)
3:穩定性
所謂排序過程當中的穩定性是指:假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,則稱這種排序算法是穩定的;不然稱爲不穩定的。
爲穩定排序的有:冒泡排序,插入排序,歸併排序;其他幾種均爲非穩定排序。
補充:找出若干個數中最大/最小的前K個數(K遠小於n),用什麼排序方法最好? 答:用堆排序是最好的。建堆O(n),k個數據排序klogn,總的複雜度爲n+klogn。不考慮桶排序,n+klogn小於n*logn只有在k趨近n時纔不成立,因此堆排序在絕大多數狀況下是最好的。N較大時使用堆排序。