排序對於大家來說肯定都不陌生,我們在平常的項目里都會或多或少的用到排序功能。排序算法作為一個最基礎最常用的算法,你真的學會了嗎?下面我來帶領大家由淺入深的學習一下經典的排序算法。
如何分析一個排序算法
學習排序算法,我們不僅要學習它的算法原理、代碼實現,更要學會如何評價、分析一個排序算法。分析一個排序算法,主要是從以下3個方面入手。
-
排序算法的執行效率
我們在分析排序算法的時間復雜度時候,要分別給出最好情況、最壞情況和平均情況下的時間復雜度。除此之外,我們還要了解最好、最壞時間復雜度對應的要排序的原始數據是什么樣子。
-
排序算法的內存消耗
算法的內存消耗可以通過空間復雜度來衡量,不過對於排序算法來說,引入了“原地排序”這個概念,原地排序算法,就是指空間復雜度是O(1)的排序算法。
-
排序算法的穩定性
排序算法的穩定性是指,如果待排序的序列中存在值相等的元素,經過排序
之后,相等元素之間原有的先后順序不變。
經典的排序算法
一、冒泡排序
我們先從最簡單的冒泡排序開始,學習我們的經典排序算法。
冒泡排序的算法原理是:依次比較相鄰的倆個元素,看是否滿足大小關系要求,如果不滿足就讓他倆互換。一次冒泡操作會讓至少一個元素移動到它應該在的位置,就這樣重復n次,這完成了n個數據的排序工作。
下面我們來看一個例子,假如我們要對5,7,8,3,1進行從小到大排序。第一次冒泡操作的過程如下:
可以看出,經過一次冒泡排序之后,8已經存儲在正確的位置上了。所以,只要經過5次冒泡操作,就可以完成數據的排序了。
冒泡排序算法原理比較容易理解,我們來看一下它的代碼實現。
def bubble_sort(a,n): if n<=1: return ##提前退出標志位 flag=False for i in range(n): for j in range(n - i - 1): if (a[j] > a[j + 1]): temp = a[j] a[j] = a[j + 1] a[j + 1] = temp flag=True if(not flag): break a=[5,7,8,3,1] bubble_sort(a,len(a)) print(a)
接下來我們來分析一下冒泡排序。
-
冒泡排序是原地排序算法嗎?
冒泡的過程只是涉及相鄰數據的交換操作,只需要常量級的臨時空間,所以他的空間復雜度是O(1),是一個原地排序算法。
-
冒泡排序是穩定的排序算法嗎?
當有相鄰的兩個元素大小相同時,我們不做數據交換,所以相同大小的數據在排序前后不會改變順序,所以冒泡排序是穩定的排序算法。
-
冒泡排序的時間復雜度是多少?
最好的情況下,要排序的數據已經是有序的,我們只需要進行一次冒泡操作,所以最好的時間復雜度是O(1)。最壞的情況下,要排序的數據是逆序的,我們需要進行n次冒泡操作,所以最壞情況時間復雜度是O(n2)
二、插入排序
我們將數組中的數據分為兩個區間,已排序區間和未排序區間。插入算法的核心思想就是取未排序區間中的元素,在已排序的的區間中找到合適的位置將其插入進去,並保證已排序區間數據一直是有序的。重復此過程,直到未排序區間為空。 如圖,要排序的數據序列為5,7,8,3,1,其中左側為已排序區間,右側是未排序區間。
我們來看一下他的代碼實現。
def insert_sort(a,n): if n<=1: return for i in range(1,n): value=a[i] j=i-1 while(j>=0): if(a[j]>value): a[j+1]=a[j] j=j-1 else: break a[j+1]=value a=[5,7,8,3,1] insert_sort(a,len(a)) print(a)
接下來我們來分析一下插入排序算法。
-
插入排序算法是原地排序嗎?
從上面的代碼來看,插入排序算法不需要額外的存儲空間,所以空間復雜度為O(1),也就是說這是一個原地排序算法。
-
插入排序是穩定的排序算法嗎?
在插入排序中,我們可以將后面出現的元素,插入到前面出現元素的后面,這樣就可以保持原有的前后順序不變,所以插入排序是穩定的排序算法。
-
插入排序的時間復雜度是多少?
如果待排序的數據是有序的。我們每次只比較一個數據就能確定插入位置,所以最好的時間復雜度是O(n)。如果待排序的數據是逆序的,每次插入都相當於在數組的首部插入數據,所以需要移動大量的數據,所以最壞的情況,時間復雜度是O(n*n)。
三、選擇排序
選擇排序類似於插入排序,也分為已排序區和未排序區。但是選擇排序每次都會從未排序區間找到最小的元素,將其放入已排序區間的末尾。如下圖所示:
def select_sort(a,n): if n<=1: return for i in range(n): min_index=i for j in range(i,n): if a[j]<a[min_index]: min_index=j temp = a[i] a[i] = a[min_index] a[min_index] = temp a=[5,7,8,3,1] insert_sort(a,len(a)) print(a)
下面我們來分析一下選擇排序。
-
選擇排序算法是原地排序嗎?
通過代碼可以看到,選擇排序只用到了常數級的臨時空間,所以選擇排序的空間復雜度為O(1),是一種原地排序算法。
-
選擇排序的時間復雜度是多少?
通過代碼可以看到,選擇排序的最好情況的時間復雜度、最壞情況的時間復雜度和平均情況的時間復雜度都是O(n*n)。
-
選擇排序是穩定的排序算法嗎?
選擇排序是不穩定的排序算法。從代碼可以看到,選擇排序每次都找剩余未排序元素中的最小值來和前面的元素交換位置,這樣就破壞了穩定性。
四、歸並排序
歸並排序的思想是:如果要排序一個數組,我們先把數組從中間分成前后兩部分,然后對前后兩部分分別排序,再將排好的兩部分合並在一起,這樣整個數組就有序了。
歸並排序使用的是分治思想。分治就是分而治之,將一個大問題分解成小的子問題來解決。小的子問題解決了,大的問題也就解決了。
def merget_sort(A,n): merget_sort_m(A,0,n-1) def merget_sort_m(A,p,r): #遞歸終止條件 if p>=r: return q=p+(r-p)//2 merget_sort_m(A,p,q) merget_sort_m(A,q+1,r) #將A[p:q],A[q+1:r]合並到A[p:r] merge(A,p,q,r) def merge(A,p,q,r): i=p j=q+1 tmp=[] while i<=q and j<=r: if A[i]<=A[j]: tmp.append(A[i]) i=i+1 else: tmp.append(A[j]) j = j + 1 start=i if i<=q else j end=q if i<=q else r tmp.extend(a[start:end+1]) a[p:r+1]=tmp a=[4, 3, 2, 1] merget_sort(a,len(a)) print(a)
下面我們來分析一下歸並排序。
-
歸並排序是穩定的排序算法嗎?
結合代碼可以看出,歸並排序穩定不穩定的關鍵在於merge函數,在合並的過程中,如果A[p:q]和A[q+1:r]之間值有相同的元素時,把A[p:q]中的元素放入tmp數組中,這樣就保證了值相同的元素在合並前后順序不變。所以歸並排序是一個穩定的排序算法。
-
歸並排序的時間復雜度是多少?
歸並排序的時間復雜度是O(nlogn)
-
歸並排序的空間復雜度是多少?
通過代碼可以看到,歸並排序不是原地排序算法,需要借助額外的存儲空間。通過merge函數可以看到,歸並排序的空間復雜度是O(n)。
五、快速排序
最后,我們來看一下快速排序。快速排序也是利用分治的思想。快速排序的核心思想是:如果要排序數組中下標從p到r之間的數,我們選擇p到r之間的任何一個數作為pivot(分區點)。我們遍歷p到r之間的數,將小於pivot的放到左邊,將大於pivot的放到右邊,將pivot放到中間。經過這一步后,數組p到r之間的數據就被分成了三部分,前面p到q-1都是小於pivot的,中間是pivot,后面q+1到r都是大於pivot的。
假如我們選擇數組p到r的最后一個元素作為pivot,對數組5,7,8,3,6來進行快速排序。我們來看一下代碼實現。
def quick_sort(A,n): quick_sort_m(A,0,n-1) def quick_sort_m(A,p,r): #遞歸終止條件 if p>=r: return q=partition(A,p,r) quick_sort_m(A,p,q-1) #將A[p:q],A[q+1:r]合並到A[p:r] quick_sort_m(A,q+1,r) def partition(A,p,r): povit=A[r] i=p for j in range(p,r): print(j) if(A[j]<povit): temp=A[i] A[i]=A[j] A[j]=temp i=i+1 temp=A[i] A[i]=A[r] A[r]=temp return i a=[5, 7, 8, 3, 6] quick_sort(a,len(a)) print(a)
其中partition函數是選擇數組的最后一個元素作為pivot,然后對A[p:r]分區,函數返回pivot的下標。
我們來看一下一次快速排序的過程。如下圖所示:
下面我們來分析一下快速排序。
-
快速排序是穩定的排序算法嗎?
因為分區會涉及到交換操作,如果數組中有兩個相同的元素,經過分區操作以后,相同元素的先后順序會發生改變,所以快速排序不是穩定的排序算法。
-
快速排序算法的時間復雜度是多少?
快速排序也是用遞歸來實現的,如果每次分區操作,正好把數組分成大小相
近的兩個小區間,那快排的時間復雜度就是O(nlogn)。最壞的情況是如果數組是有序的,每次取最后一個元素作為pivot,那每次分區得到的區間是不均等的。我們需要n次分區操作,才能完成快排的整個過程。每次分區大概都需要掃描n/2個元素。這種情況下,快排的時間復雜度就從O(nlogn)退化成O(n*n)。
-
快速排序算法的空間復雜度是多少?
通過代碼可以看到,快速排序只用到了常數級的臨時空間,所以選擇排序的
空間復雜度為O(1),是一種原地排序算法。
到此為止,我們已經分享了五種經典的排序算法,你學會了嗎?
歡迎大家留言和我交流。
了解更多有趣內容,獲取更多資料,請關注公眾號“程序員學長” 。