排序演算法你學會了嗎?

公眾號老韓隨筆發表於2021-07-17

排序對於大家來說肯定都不陌生,我們在平常的專案裡都會或多或少的用到排序功能。排序演算法作為一個最基礎最常用的演算法,你真的學會了嗎?下面我來帶領大家由淺入深的學習一下經典的排序演算法。

如何分析一個排序演算法

      學習排序演算法,我們不僅要學習它的演算法原理、程式碼實現,更要學會如何評價、分析一個排序演算法。分析一個排序演算法,主要是從以下3個方面入手。

  1. 排序演算法的執行效率

    我們在分析排序演算法的時間複雜度時候,要分別給出最好情況、最壞情況和平均情況下的時間複雜度。除此之外,我們還要了解最好、最壞時間複雜度對應的要排序的原始資料是什麼樣子。

  2. 排序演算法的記憶體消耗

    演算法的記憶體消耗可以通過空間複雜度來衡量,不過對於排序演算法來說,引入了“原地排序”這個概念,原地排序演算法,就是指空間複雜度是O(1)的排序演算法。

  3. 排序演算法的穩定性

    排序演算法的穩定性是指,如果待排序的序列中存在值相等的元素,經過排序

         之後,相等元素之間原有的先後順序不變。

經典的排序演算法
一、氣泡排序

      我們先從最簡單的氣泡排序開始,學習我們的經典排序演算法。

     氣泡排序的演算法原理是:依次比較相鄰的倆個元素,看是否滿足大小關係要求,如果不滿足就讓他倆互換。一次冒泡操作會讓至少一個元素移動到它應該在的位置,就這樣重複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)

  

接下來我們來分析一下氣泡排序。

  1. 氣泡排序是原地排序演算法嗎?

    冒泡的過程只是涉及相鄰資料的交換操作,只需要常量級的臨時空間,所以他的空間複雜度是O(1),是一個原地排序演算法。

  2. 氣泡排序是穩定的排序演算法嗎?

    當有相鄰的兩個元素大小相同時,我們不做資料交換,所以相同大小的資料在排序前後不會改變順序,所以氣泡排序是穩定的排序演算法。

  3. 氣泡排序的時間複雜度是多少?

    最好的情況下,要排序的資料已經是有序的,我們只需要進行一次冒泡操作,所以最好的時間複雜度是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)

  

接下來我們來分析一下插入排序演算法。

  1. 插入排序演算法是原地排序嗎?

    從上面的程式碼來看,插入排序演算法不需要額外的儲存空間,所以空間複雜度為O(1),也就是說這是一個原地排序演算法。

  2. 插入排序是穩定的排序演算法嗎?

    在插入排序中,我們可以將後面出現的元素,插入到前面出現元素的後面,這樣就可以保持原有的前後順序不變,所以插入排序是穩定的排序演算法。

  3. 插入排序的時間複雜度是多少?

    如果待排序的資料是有序的。我們每次只比較一個資料就能確定插入位置,所以最好的時間複雜度是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)

  

下面我們來分析一下選擇排序。

  1.  選擇排序演算法是原地排序嗎?

    通過程式碼可以看到,選擇排序只用到了常數級的臨時空間,所以選擇排序的空間複雜度為O(1),是一種原地排序演算法。

  2. 選擇排序的時間複雜度是多少?

    通過程式碼可以看到,選擇排序的最好情況的時間複雜度、最壞情況的時間複雜度和平均情況的時間複雜度都是O(n*n)。

  3. 選擇排序是穩定的排序演算法嗎?

    選擇排序是不穩定的排序演算法。從程式碼可以看到,選擇排序每次都找剩餘未排序元素中的最小值來和前面的元素交換位置,這樣就破壞了穩定性。

     

四、歸併排序

         歸併排序的思想是:如果要排序一個陣列,我們先把陣列從中間分成前後兩部分,然後對前後兩部分分別排序,再將排好的兩部分合並在一起,這樣整個陣列就有序了。

 

  歸併排序使用的是分治思想。分治就是分而治之,將一個大問題分解成小的子問題來解決。小的子問題解決了,大的問題也就解決了。

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)

  

下面我們來分析一下歸併排序。

  1. 歸併排序是穩定的排序演算法嗎?

    結合程式碼可以看出,歸併排序穩定不穩定的關鍵在於merge函式,在合併的過程中,如果A[p:q]和A[q+1:r]之間值有相同的元素時,把A[p:q]中的元素放入tmp陣列中,這樣就保證了值相同的元素在合併前後順序不變。所以歸併排序是一個穩定的排序演算法。

  2. 歸併排序的時間複雜度是多少?

    歸併排序的時間複雜度是O(nlogn)

  3. 歸併排序的空間複雜度是多少?

    通過程式碼可以看到,歸併排序不是原地排序演算法,需要藉助額外的儲存空間。通過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的下標。

       我們來看一下一次快速排序的過程。如下圖所示:

 

下面我們來分析一下快速排序。

  1. 快速排序是穩定的排序演算法嗎?

    因為分割槽會涉及到交換操作,如果陣列中有兩個相同的元素,經過分割槽操作以後,相同元素的先後順序會發生改變,所以快速排序不是穩定的排序演算法。

  2. 快速排序演算法的時間複雜度是多少?

     

    快速排序也是用遞迴來實現的,如果每次分割槽操作,正好把陣列分成大小相         

    近的兩個小區間,那快排的時間複雜度就是O(nlogn)。最壞的情況是如果陣列是有序的,每次取最後一個元素作為pivot,那每次分割槽得到的區間是不均等的。我們需要n次分割槽操作,才能完成快排的整個過程。每次分割槽大概都需要掃描n/2個元素。這種情況下,快排的時間複雜度就從O(nlogn)退化成O(n*n)。

  3. 快速排序演算法的空間複雜度是多少?

     

         通過程式碼可以看到,快速排序只用到了常數級的臨時空間,所以選擇排序的

         空間複雜度為O(1),是一種原地排序演算法。

到此為止,我們已經分享了五種經典的排序演算法,你學會了嗎? 

歡迎大家留言和我交流。

瞭解更多有趣內容,獲取更多資料,請關注公眾號“老韓隨筆” 。

 

相關文章