演算法學習 - 基礎排序演算法

吳與倫發表於2018-08-22

最近在學習演算法與資料結構,演算法是一個程式設計師的基本功,但是我不是科班出身,所以這方面的知識有所欠缺。在慕課網上找到一套對應的課程,主講老師是liuyubobobo,從我學習的感受和體驗來看,bobo老師對一個問題講解的相當清晰和透徹,普通話說的也好,適合初學者理解和學習。大家如果想學習演算法與資料結構的知識,我推薦這一套教程。地址連結:coding.imooc.com/class/71.ht… 這一系列文章主要是對這套課程的內容以文字的形式展示。做這個事情一是想對視訊上的知識點在通過形成文字的過程中,加深自己的理解。二是想加強自己的表達能力。

這篇文章主要講解3個基礎的排序演算法,選擇排序,插入排序,以及氣泡排序,其時間複雜度都是0(n^2)級別的,實現程式碼使用c++語言。

選擇排序

首先給定一個元素是無序的整數陣列:

演算法學習 - 基礎排序演算法
需要對這個陣列中的8個整數進行從小到大的排序。

選擇排序的基本思路

首先從起始位置index = 0開始,遍歷一遍陣列,獲取到最小的元素值index = 4 的元素,元素值為1,然後將index = 0與index = 4 上的兩個元素交換位置,此時index = 0 上的元素值1。index = 4上的元素值為5。紅色為已排序完成的有序區域。

演算法學習 - 基礎排序演算法

找到了最小的元素放在陣列的第一位後,接著從index = 1開始往後遍歷陣列。找到第二小的元素後再與index = 1的位置交換元素,此時,index = 1上的元素值為2,index = 2 上的元素為4.

演算法學習 - 基礎排序演算法

接著再從index = 2的位置往後遍歷陣列,找到第三小的元素後再與index = 2上的元素交換位置,交換後,index = 2上的元素為3,index = 5 上的元素為4。

演算法學習 - 基礎排序演算法

然後依此遍歷的方法,直到陣列的最後一個位置存放的是最大的元素。選擇排序的整體思想不難理解,就是迴圈一遍陣列找到迴圈元素中最小的元素,再與已排好序的下一個索引上的元素交換位置。

程式碼實現

我寫成一個函式,傳入一個陣列以及元素的個數:

template <typename T>
void selectionSort(T arr[],int n){
    
    for (int i = 0; i < n; i++) {
        int minIndex = i; // minIndex儲存內層迴圈中最小值的索引
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        swap(arr[i], arr[minIndex]);
    }
}
複製程式碼

插入排序

插入排序的基本思路

bobo老師打個這樣一個比方,我覺得用來理解插入排序非常合適:就好像有一副牌,三個人用這副牌來鬥地主,分牌階段,你需要將你摸到的牌放在手中的牌中的合適位置,手中的牌是整理好的,是有序的,放入後,使其整體依舊有序。我們來模擬一下這個過程。首先固定手中有一張牌,用紅色標記為手中已整理好的牌。

演算法學習 - 基礎排序演算法

接著摸第二張牌,為4,比5小,所以放在5的前面,4與5交換位置。

演算法學習 - 基礎排序演算法

接著摸第三張牌,為2,2比5小,所以先放在5的前面,4的後面。

演算法學習 - 基礎排序演算法

此時這裡並不是2合適的位置,繼續和前面的元素比較,2比4小,所以要放到4的前面。此時變成有序了。

演算法學習 - 基礎排序演算法

接著再摸第四張牌,為7,7比5小,發現不用交換位置,放在原地就好了。

演算法學習 - 基礎排序演算法

就這樣,按照這個方法,將摸到的牌與整理好的牌依次做比較,使其放在合適的位置,直到摸完最後一張牌。

程式碼實現

template <typename T>
void insertionSort(T arr[],int n){
    
    for (int i = 1; i < n; i++) {
        
        for (int j = i; j > 0; j--) {
            if (arr[j-1] > arr[j]) {
                swap(arr[j - 1],arr[j]);
            } else {
                break;
            }
        }
    }
}
複製程式碼

注意:第一層for迴圈時 i = 1,i不是從零開始。因為第一張牌不需要排序,從第二張牌開始。所以設定i = 1。

在[0 i) 前閉後開這個區間中為已排好序的元素,i為待排序的元素的索引,第二層for迴圈是從待整理的元素索引i開始。在已排好序的區間[0,i)中,從後往前遍歷。 如果發現前面的元素比它大,則兩個元素交換位置。當發現前面的元素比它小的時候,此時它就找到了合適的位置。就不需要再遍歷了,直接結束這一層迴圈。

插入排序優化

在交換兩個元素位置的時候,呼叫了swap(arr[j - 1],arr[j]),而一次交換其實是三次賦值,這句程式碼其實也可以改寫為:

int temp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = temp;
複製程式碼

如果一個較小的元素要插入到合適的位置,肯定要一路交換很多次。這無形中就增加了排序時間。優化思路就是隻進行賦值操作而不進行交換操作,先儲存一份待排序的元素,然後遍歷已排好序的元素,從後往前進行逐一比較,如果當遍歷到index = j的元素時,j - 1索引上的元素要比j位置上的元素大,則將j - 1索引位置的元素往後挪,往後挪也就是將j索引位置的元素賦值為j-1索引位置的元素,當遍歷到j-1索引上的元素比元素j位置上的元素小或相等時,就說明上一次迴圈往後挪而騰出來的位置為待排序的合適位置,在將該位置的值賦值為待排序的值就好了。

假定現在按照優化的思路對1進行排序,紅色為已排好序的部分。

演算法學習 - 基礎排序演算法

首先先複製一份待排序的元素1

演算法學習 - 基礎排序演算法

然後將待排序的元素與已排好序的index = 3位置,值為7的元素比較,如果這個元素比待排序的元素要大,則直接將待排序的值複製為這個元素。7 比 1 大,7往後挪,所以待排序位置上的元素值賦值為7,而index = 3位置就騰出來了。

演算法學習 - 基礎排序演算法

然後依次往前與帶排序的元素比較。這次是index = 2 時的 5 與 1比較, 5比1大,5往後挪,將index = 3 位置上的元素賦值為5,index = 2這個位置就騰出來了。

演算法學習 - 基礎排序演算法

再將index = 1 時的4 與 待排序的元素1比較,4 還是比1 大,4往後挪,將 index = 2位置賦值為4,index = 1這個位置就騰出來了。

演算法學習 - 基礎排序演算法

繼續。再將index = 0時的2, 與待排序的元素1比較,2還是比1大,2往後挪,將index = 1位置賦值為2,index = 0這個位置騰出來了。

演算法學習 - 基礎排序演算法

那麼騰出來的位置上為元素的前面已沒有元素可以比較了。所以此時在index = 0的位置上放置上待排序的元素。

演算法學習 - 基礎排序演算法

元素1經過一番波折,塵埃落定,終於找到了他合適的位置,元素1排序完畢

演算法學習 - 基礎排序演算法

然後index = 5上的元素3按照上述方法找到合適的位置,index = 6 上的元素按照上述方法找到合適的位置,接著是index = 7上的元素。將所有的元素都按照上述方法找到自己合適的位置。

程式碼實現

template <typename T>
void insertionSort(T arr[],int n){
    
    for (int i = 1; i < n; i++) {
        T e = arr[i]; // 儲存待插入索引為i時的元素e
        int j = i; // j儲存元素e應該插入的位置
        for (j = i; j > 0; j--) {
            if (arr[j-1] > e) {
                arr[j] = arr[j-1];
            }
        }
        arr[j] = e;
    }
}
複製程式碼

氣泡排序

氣泡排序實現思路

首先還是這一個由8個陣列成的一個無序的陣列

演算法學習 - 基礎排序演算法

氣泡排序的核心思想就是將相鄰的兩個元素兩兩比較,根據大小來交換元素的位置

首先,5與4比較,5比4大,我們的需求是從小到達排列,4需要在5的前面,所以4與5交換位置,紅色表示兩個要交換位置的元素。

演算法學習 - 基礎排序演算法
交換後

演算法學習 - 基礎排序演算法

然後, 5與2開始比較,5比2大,交換位置

演算法學習 - 基礎排序演算法
交換後
演算法學習 - 基礎排序演算法

繼續, 5和7比較,5比7小,位置保持不變

演算法學習 - 基礎排序演算法

go on, 7與1比較,7比1大,交換位置

演算法學習 - 基礎排序演算法
交換後
演算法學習 - 基礎排序演算法

不要停,7與3比較,7比3大,交換位置

演算法學習 - 基礎排序演算法
交換後

演算法學習 - 基礎排序演算法

繼續深入, 7與8比較,7比8小,保持不變

演算法學習 - 基礎排序演算法

還有最後一個,8與6比,8比6大,交換位置

演算法學習 - 基礎排序演算法
交換後

演算法學習 - 基礎排序演算法

這樣一輪下來,元素8排到了最右側,此時紅色代表️已排好序的區域

演算法學習 - 基礎排序演算法

接著按照上述方法,重新重頭開始相鄰元素兩兩遍歷,前一個元素比後一個元素大則交換位置,小則保持位置不變,一路比較下來。找到第二大的元素放到元素8的左側。

演算法學習 - 基礎排序演算法

然後是

演算法學習 - 基礎排序演算法

然後是

演算法學習 - 基礎排序演算法

給定的一組無序的整數陣列,按照排序的思路來講,藍色區域屬於無序的區域,還需要比較,但是上面圖片中藍色部分已經是有序的了,這可以作為一個優化的方面,就是當發現代排區域中的元素已經是有序了的時候,排序完成,結束排序,下面的程式碼中不涉及這部分的優化。

實現程式碼

template <typename T>
void BubbleSort(T arr[],int n){
    
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr[j],arr[j+1]);
            }
        }
    }
}
複製程式碼

程式碼不難理解,雙層迴圈,外層每一次迴圈確定一個最大值放在陣列的右側,內層尋找這個最大值。先進行元素比較,滿足則交換。

氣泡排序的優化

剛剛在逐步比較的時候,出現了這樣一種情況:藍色的無序區域中的元素已經是有序的了。

演算法學習 - 基礎排序演算法

但是上面的氣泡排序的程式碼還會繼續的比較下去。每一個元素都會參與外層迴圈,直到迴圈結束。所以優化方向是在無序區域中的元素已經有序了的情況下結束迴圈。 優化後的程式碼,部分解釋見註釋。

void BubbleSort(T arr[],int n){
    
    for (int i = 0; i < n; i++) {
        bool isSorted = true; // 用一個bool值來標記每一層是否是有序的,預設為true.
        
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr[j],arr[j+1]);
                isSorted = false; // 如果有交換元素,那麼不是有序,該bool值改為false.
            }
        }
        if (isSorted) { // 在內層中,如果該bool值沒有被改為false,那麼就說明內層沒有元素交換,那麼該數列排序完成了,直接結束迴圈。
            break;
        }
    }
}
複製程式碼

這個優化主要是用一個bool值做標記,來確定是否有序。在內層迴圈中如果沒有元素交換,那麼這個數列肯定就是有序的了,那麼外層就無需在迴圈了。

下次學習內容:歸併排序

相關文章