為什麼要排序?排序算法的性能提升之道

2020-12-11 讀芯術

全文共4076字,預計學習時長11分鐘

圖源:unsplash

排序有什麼用?想像一下,如果字典不是按照字母順序排列,查找一個單詞,你得查到什麼時候?這就是為什麼人們引入了分類的概念,因為其極大地幫助我們快速搜索物品。

那麼,如何實現排序呢?這些排序算法,你應該了解。

冒泡排序

這是最簡單的排序算法。只需比較每對相鄰的元素,並檢查元素是否有序,否則交換兩個元素,直到所有元素都被排序為止。

for(int i =0;i < n; i++){

for(int j=0;j < n -1; j++){

if(arr[j] > arr[j+1]){

int temp = arr[j];

arr[j] = arr[j+1];

arr[j+1] = temp;

}

}

}

圖源:谷歌

性能分析:

· 時間複雜度:

1.最壞情況:O(n)——由於循環n次元素n次,n為數組的長度,因此冒泡排序排序的時間複雜度變為O(n)。

2.最佳情況:O(n)——即使數組已經排序了,算法也會檢查每個相鄰對,因此最佳情況下的時間複雜度將與最壞情況相同。

· 空間複雜度:O(1)。

由於只輸入了數組並未使用任何額外的數據結構,因此空間複雜度將為O(1)。

改進版冒泡排序:

如果看一下代碼,就會發現在上述排序算法中即使數組已經排序,時間複雜度也將相同,即O(n)。

為了克服這個問題,提出了一種改進算法。創建一個標誌來確定數組是否已排序,該標誌會檢查所有相鄰對之間是否發生了交換。如果遍歷整個數組時沒有交換,則該數組已完全排序,因此可以跳出循環。這大大降低了算法的時間複雜度。

for(int i =0;i < n; i++){

boolean isSwapped =false;

for(int j=0;j < n -1; j++){

if(arr[j] > arr[j+1]){

int temp = arr[j];

arr[j] = arr[j+1];

arr[j+1] = temp;

isSwapped =true;

}

if(!isSwapped){

break;

}

}

}

性能分析:

· 時間複雜度:

1.最壞情況:O(n)——與上述算法相同。

2.最佳情況:O(n)——由於在此算法中,如果數組已經排序,就會中斷循環,因此最佳情況下的時間複雜度將變為O(n)。

· 空間複雜度:O(1)。

選擇排序

假設排序算法中第一個元素是最小元素,然後檢查數組的其餘部分中是否存在小於假定最小值的元素。若存在,就交換假定的最小值和實際的最小值,否則轉移到下一個元素。

for(int i=0;i<arr.length; i++) {

int minIndex = i;

for(int j=i+1;j<arr.length; j++) {

if(arr[j]<arr[minIndex]) {

minIndex = j;

}

}

int temp = arr[i];

arr[i] = arr[minIndex];

arr[minIndex] = temp;

}

性能分析:

· 時間複雜度:

1.最壞情況:O(n)——由於對於數組中的每個元素,遍歷其餘數組以找到最小值,因此時間複雜度將變為O(n)。

2.最佳情況:O(n)——即使已對數組進行排序,我們的算法也會在其餘數組中尋找最小值,因此最佳情況下的時間複雜度與最壞情況相同。

· 空間複雜度:O(1)。

就像之前的算法一樣,除了輸入數組之外沒有利用任何額外的數據結構,因此空間複雜度將為O(1)。

插入排序:

在這種排序算法中,對於每個元素,都要檢查其順序是否正確,直到當前元素為止。由於第一個元素是有序的,所以我們從第二個元素開始檢查順序是否正確否則交換元素。因此,在任何給定元素上,檢查當前元素是否大於上一個元素。如果不是,繼續交換元素,直到當前元素大於上一個元素為止。

for(int i =1;i < n; i++) {

int j = i;

while(j >0&& arr[j] < arr[j-1]) {

int temp = arr[j];

arr[j] = arr[j-1];

arr[j-1] = temp;

j--;

}

}

性能分析:

· 時間複雜度:

1.最壞情況:O(n)——在最壞情況下,數組按降序排序。因此,必須遍歷每個元素並向左交換。

2.最佳情況:O(n)——在最佳情況下,數組已經排序。因此,對於每個元素,僅將當前元素與左側的元素進行比較。由於順序正確,不會交換並繼續進行下一個元素。因此,時間複雜度將為O(n)。

· 空間複雜度:O(1)。

由於除了輸入數組之外,沒有使用任何額外的數據結構,因此空間複雜度將為O(1)。

快速排序

快速排序也被稱為分區排序。該排序算法因其分而治之的概念相較於之前的算法效率更高

首先確定一個主元,然後找到該主元位置的正確索引,將該數組分為兩個子數組。一個子數組包含小於主元的元素,另一個子數組包含大於主元的元素。然後,遞歸調用這兩個子數組,直到無法進一步劃分數組為止。

publicstaticvoid quicksort(int[] arr, int low, int high) {

if(low >= high) return;

int pivotPosition = partition(arr, low, high);

quicksort(arr,low, pivotPosition-1);

quicksort(arr, pivotPosition+1, high);

}

但是如何劃分子數組呢?

假設數組的最後一個元素是主元,則用兩個指針遍歷整個數組。左指針指向的元素應小於主元,右指針指向的元素應大於主元。如果不是,則在左右指針處交換元素以對應數組中的特定位置,左邊的元素較小,而右邊的元素較大。然後,將主元插入此位置。

publicstaticint partition(int[] arr, int low, int high) {

int pivot = arr[high];

int left = low, right = high-1;

while(left < right) {

while(arr[left]<pivot) {

left++;

}

while(arr[right]>pivot) {

right--;

}

if(left >= right) {

break;

}

int temp = arr[left];

arr[left] = arr[right];

arr[right] = temp;

}

int temp = arr[left];

arr[left] = arr[high];

arr[high] = temp;

returnleft;

}

性能分析:

· 時間複雜度:

1.最佳情況:O(nlogn)——首先將數組遞歸分為兩個子數組,時間複雜度為O(logn)。每次函數調用都將調用時間複雜度為O(n)的分區函數,因此,總時間複雜度為O(nlogn)。

2.最壞情況:O(n)——當數組以降序排序或數組中的所有元素都相同時,由於子數組高度不平衡,因此時間複雜度躍升至O(n)。

· 空間複雜度:O(n)。

由於遞歸調用quicksort函數,因此使用內部堆棧來存儲這些函數調用。堆棧中最多有n個調用,因此空間複雜度為O(n)。

合併排序

合併排序和快速排序一樣,都使用分而治之概念。在合併排序主要工作是合併子數組,而在快速排序中,主要工作是對數組進行分區/劃分,因此快速排序也稱為分區排序。

下面的函數會一直將數組遞歸地分成兩個子數組直到每個子數組只有一個元素。

publicvoid mergeSort(int arr[], int l, int r) {

if (l < r) {

intm = l + (r-l)/2;

sort(arr, l, m);

sort(arr , m+1, r);

merge(arr, l, m, r);

}

}

將這些子數組存儲在兩個新數組中後,就根據它們的順序進行合併,並將它們存儲到輸入數組中。所有這些子數組合併後,輸入數組就排序完成了。

publicvoid merge(int arr[], int l, int m, int r) {

int n1 = m-l+1;

int n2 = r-m;

int[] L =newint[n1];

int[] R =newint[n2];

for(int i =0;i < n1; i++) {

L[i] = arr[l+i];

}

for(int i =0;i < n2; i++) {

R[i] = arr[m+1+i];

}

int i =0, j =0, k =l;

while(i < n1 && j < n2) {

if(L[i] <=R[j]) {

arr[k++] =L[i++];

}

else {

arr[k++] =R[j++];

}

}

while(i < n1) {

arr[k++] =L[i++];

}

while(j < n2) {

arr[k++] =R[j++];

}

}

性能分析:

· 時間複雜度

1.最佳情況:O(nlogn)——首先將數組遞歸分為兩個子數組,時間複雜度為O(logn)。每次函數調用都將調用時間複雜度為O(n)的分區函數,因此,總時間複雜度為O(nlogn)。

2.最壞情況:O(nlogn)——最壞情況下的時間複雜度與最佳情況相同。

· 空間複雜度:O(n)

由於遞歸調用MergeSort函數,因此使用內部堆棧來存儲這些函數調用。堆棧中最多有n個調用,因此空間複雜度為O(n)。

圖源:unsplash

上面提到的算法是基於比較的排序算法,因為在對元素進行相互比較之後再對其進行排序。但是,還有其他基於非比較的排序算法,例如計數排序、基數排序、桶排序等,由於時間複雜度為O(n),因此也稱為線性排序算法。

每種算法各自都有優缺點,採用哪種算法取決於優先級。如果效率上沒有問題,可以使用易實現的冒泡排序。或者在數組幾乎排好序時使用插入排序,因為此時插入排序的時間複雜度是線性的。

留言點讚關注

我們一起分享AI學習與發展的乾貨

如轉載,請後臺留言,遵守轉載規範

相關焦點

  • 排序算法之插入排序
    排序算法在編程領域中起著舉足輕重的作用,在目標檢索、機器學習、數值計算、圖像處理等領域有著廣泛地應用。為了追本溯源,公眾號特推出常用經典排序算法系列推文,讓小夥伴們深入了解排序算法的實現原理,同時也提升matlab編程能力。
  • 重溫7種排序算法 第一篇,交換排序算法:冒泡排序、快速排序
    排序算法可分為以下幾類:    交換排序算法:冒泡排序、快速排序;    選擇排序算法:簡單選擇排序、堆排序;    插入排序算法:直接插入排序、希爾排序;    歸併排序算法。    主要做了以上7種排序算法的筆記,字數可能較多,所以分為幾篇。
  • 十大經典排序算法之希爾排序算法
    今天我們要講的希爾排序雖然也是插入排序的一種,但是它是插入排序的一個高效變形,脫離了 先用第一個增量把數組分為若干個子數組,每個子數組中的元素下標距離等於增量;再使用第二個增量,然後繼續同樣的操作,直到增量序列裡的增量都使用過一次(增量為 1 時,其實就是對整個數組進行簡單插入排序)。可能你有點疑惑為啥剛開始要進行大跨度的插入排序呢?說實話我剛開始的時候也覺得怪怪的,舉個極端的例子幫助你理解下。
  • JS家的排序算法 十大經典算法排序總結
    冒泡排序還有一種優化算法,就是立一個flag,當在一趟序列遍歷中元素沒有發生交換,則證明該序列已經有序。但這種改進對於提升性能來說並沒有什麼太大作用。。。什麼時候最快(Best Cases):當輸入的數據已經是正序時(都已經是正序了,我還要你冒泡排序有何用啊。。。。)
  • 排序算法之冒泡排序及其優化
    冒泡排序思想比較相鄰兩個元素,如果前面的元素比後面的元素大,則交換位置。最後一個元素必定會是最大值。排除掉最後一位元素,繼續循環,直至沒有元素需要比較可以看出,冒牌排序其實和選擇排序時很像的,只不過選擇排序是比較數據,先記錄下最小/大值的下標,等一趟循環結束的時候再交換位置;而冒泡排序在比較數據的同時也做了交換。性能時間複雜度與選擇排序一樣,時間複雜度為O(n^2)。
  • 算法圖解 | 分而治之與快速排序算法
    分而治之D&C分而治之不是一種解決問題的算法,而是一種希望問題分解,將複雜的問題劃分為多個簡單問題來解決的思想。(O表示法表示)快速排序的性能高度依賴於你選擇的基準值。最糟情況:算法複雜度O(n^2)假設總是將第一個元素用作基準值,且要處理的數組是有序的。由於快速排序算法不檢查輸入數組是否有序,因此它依然嘗試對其進行排序。注意,數組並沒有被分成兩半,相反,其中一個子數組始終為空,這導致調用棧非常長。
  • 經典排序算法和Python詳解之(一)選擇排序和二元選擇排序
    本文源自微信公眾號【Python編程和深度學習】原文連結:經典排序算法和Python詳解之(一)選擇排序和二元選擇排序,歡迎掃碼關注鴨!掃它!掃它!掃它排序算法是《數據結構與算法》中最基本的算法之一。排序就是對於一個列表,按照某關鍵字遞增或遞減的順序進行操作。
  • 排序算法專題之插入排序
    經典插入排序例題1.使用插入排序算法對列表a升序排序。函數名:insert_sort(a)參數表:a -- 待排序列表。返回值:該方法沒有返回值,但是會對列表的對象進行升序排序。算法思想:外層循環控制待排序數組的左邊界,即a[i:]為待排序部分;內層循環掃描a[:i],將元素a[i]插入到適當位置def insert_sort (a):    for i in range(1, len(a)):        t, j =  ①                           while j >
  • 排序算法之高效排序法
    高效排序算法桶排序桶排序 (Bucket sort)或所謂的箱排序,是一個排序算法,工作的原理是將數組分到有限數量的桶子裡。每個桶子再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。桶排序原理介紹桶排序是計數排序的升級版。
  • 算法連載之快速排序
    切分完成後,劃分為兩個待排序數組:左側為空數組,右側是9。<6>索引位置為2-2,開始索引不小於結束索引,返回。<7>切分:數組9隻有一項元素,因此i索引為3。左側索引為3-3,右側索引為4-4。
  • JavaScript算法之快速排序
    如果你在面試軟體工程師的職位,經常被問到的一個問題是快速排序是如何工作的。快速排序最簡單的邏輯步驟如下:選擇一個基準元素,把數組分為兩個子數組。有些算法選擇第一個元素為基準,這樣不是最佳選擇,因為對於已排序的數組來說這樣做的性能是最差的。
  • 排序算法問題:穩定排序與不穩定排序
    正文這幾天筆試了好幾次了,連續碰到一個關於常見排序算法穩定性判別的問題,往往還是多選,對於我以及和我一樣拿不準的同學可不是一個能輕易下結論的題目,當然如果你筆試之前已經記住了數據結構書上哪些是穩定的,哪些不是穩定的,做起來應該可以輕鬆搞定。本文是針對老是記不住這個或者想真正明白到底為什麼是穩定或者不穩定的人準備的。
  • 排序算法:歸併排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助今天介紹一下歸併排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,歸併排序的循環次數為由算法導論的主定理可以推導出,可見之前的文章(文末有連結)歸併排序的時間複雜度為03空間複雜度由上圖邏輯可以得出,歸併排序每次分解都是要復一份新的數據片段
  • 排序算法總結(1):冒泡排序
    冒泡排序是一種交換排序。什麼是交換排序呢?交換排序:兩兩比較待排序的關鍵字,並交換不滿足次序要求的那對數,直到整個表都滿足次序要求為止。算法思想它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。
  • 排序算法:快速排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助今天介紹一下快速排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,參照遞歸表達式漸進複雜度分析快速排序的時間複雜度遞歸表達式為,其中a=2,b=2,d=1由算法導論的主定理可以推導出,快速排序的時間複雜度為03空間複雜度由上圖邏輯可以得出
  • 排序算法:選擇排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助的今天介紹一下選擇排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,選擇排序的循環次數為由循環次數可以得出,選擇排序的時間複雜度為03空間複雜度由上圖邏輯可以得出
  • JavaScript算法之冒泡排序
    冒泡排序是討論最多的算法之一,它比較容易理解,但是效率較低。如果數組是已排序的,冒泡排序只需要遍歷一次數組,不過最壞的情況下運行時間為 O(n2),非常低效。儘管如此,理解這個算法依然很重要,需要明白它為什麼效率低下。本文將介紹實現冒泡排序的兩個思路。
  • Go語言實現常用排序算法
    我們把冒泡排序,直接選擇排序,直接插入排序認為是初級的排序算法,其中直接插入排序的性能是綜合最好的,一般來說,當排序數組規模 n 較小時,直接插入排序可能比任何排序算法都要快,建議只在小規模排序中使用。
  • Java實現冒泡排序算法
    1.引子1.1.為什麼要學習數據結構與算法?有人說,數據結構與算法,計算機網絡,與作業系統都一樣,脫離日常開發,除了面試這輩子可能都用不到呀!有人說,我是做業務開發的,只要熟練API,熟練框架,熟練各種中間件,寫的代碼不也能「飛」起來嗎?於是問題來了:為什麼還要學習數據結構與算法呢?
  • 第二篇,選擇排序算法:簡單選擇排序、堆排序
    此為第二篇,選擇排序算法:簡單選擇排序、堆排序。