和麪試官聊聊「插入排序」的正確姿勢

點擊上方“五分鐘學算法”,選擇“星標”公衆號

重磅乾貨,第一時間送達

轉自景禹

大家好呀,我是景禹。

今日分享一下插入排序,希望你從中有所收穫!

面試官最愛考察的是一個被試者對知識掌握的靈活程度和熟練程度,當一道題目可以同時考察到被試者多個知識點的掌握程度和思考能力時,面試官最愛這樣的題目,而且對於插入排序這樣被大家耳熟能詳的知識點,常常成爲考點。

插入排序

插入排序簡單的就像你玩撲克牌(雙Q,鬥地主)。基本操作就是將一個記錄插入到已排好序的有序表中,直到將所有的未排序記錄插入到適當的位置。

插入排序好簡單

將其插入正確洞

直到插完所有洞

爲了深入理解插入排序,來看一個簡單的例子。

剛開始,我們將數組的第一個元素 5 當做有序元素,假設他在正確的 “洞”:

然後將 1 插入到正確的洞,將 15 比,1<5 ,5 前面再沒有任何元素,所以 1 正確的洞就在 5 的前面:

4 插入到正確的洞,將 4  和 5 比較, 4 < 5;將 41 比較,4 > 1 ,所以 4 正確的洞就在 15 之間,將 4 插入 15 之間:

2 插入到正確的洞,將 25 比較,2 < 5;將 24 比較,2 < 4;將 21 比較,2 > 1,所以 2 正確的洞應該在 14 之間:

將 8 插入到正確的洞,將 85 比較, 8 > 5  ,所以 8 的正確的洞就在當前位置:

然後將最後一個記錄 4 插入到正確的洞,將 48 比較,4 < 8;將 4  和 5 比較,4 < 5;將 4(當前記錄)4(已排序) 比較,兩者相等,所以當前記錄 4 正確的洞在已排序的 45 之間:

再來看實現代碼就再簡單不過了

void insertionSort(int arr[], int n) 
{ 
    int i, key, j; 
    for (i = 1; i < n; i++) { 
        key = arr[i]; //當前要插入正確洞的記錄
        j = i - 1;  // 將 key 插入正確的洞
        // 在 0 到 i-1 中找到 key 的正確洞
        while (j >= 0 &&  key < arr[j]) { 
            arr[j + 1] = arr[j]; 
            j--; 
        } 
        arr[j + 1] = key; 
    } 
} 

for 循環從 i = 1 開始,是因爲 i = 0 的元素我們當做有序元素處理,while 循環做的事情就是將當前記錄 key  插入正確的洞。

複雜度分析

時間複雜度分析

內層 while 循環的次數取決於待插入記錄的關鍵字 key 與前 i -1 個記錄的關鍵字之間的關係。

當 i = 1 時,while 循環中最差情況與 arr[0] 比較一次,並將記錄 arr[0] 後移。

當 i = 2 時,while 循環中最差情況與 arr[0]arr[1] 各比較一次,並將記錄 arr[0]arr[1] 後移。

......

當 i = n - 1 時,while 循環中最差情況與 arr[0]arr[n-2] 的元素都要比較,並將記錄後移。

∴ 最差情況下比較和移動次數爲 1 + 2 + ... + (n - 2) + (n - 1) = n(n - 1) / 2 .

∴ 最壞的時間複雜度爲 量級。

最好情況下,while 循環每一次都僅執行一次,總的執行次數就是外層 for 循環的次數,最好的時間複雜度爲 量級。

空間複雜度分析

插入排序的沒有使用額外的空間,爲原地排序算法,所以空間複雜度爲 .

穩定性分析

在之前講的示例中,我們可以看到排序前後的兩個 4 的相對位置沒有發生變化:

排序前:

排序後:

插入排序穩定的根本原因是,待插入的元素不會插入到與自身值相同的關鍵字之前,所以排序前後值相同的關鍵字的相對順序被保留了下來。

實戰演練

二分插入排序

從名字就能看出來,運用了二分查找的插入排序。在上面標準的插入排序算法中,我們會將待插入關鍵字 key = arr[i] ,然後在數組 [0,i - 1]  的範圍內查找待插入關鍵字 key 的正確位置,這裏的查找操作的時間複雜度爲 量級。但是如果使用二分查找在數組 arr[0,i - 1] 的範圍內查找關鍵字 key ,那麼就可以將查找操作的時間複雜度降到 量級。關於二分查找不熟悉的可以看一下這篇文章 二分查找就該這樣學

這樣,就可以將標準的插入排序優化如下,一般最好自己能寫出二分查找:

void insertionSort(int arr[], int n) 
{ 
    int i, key, j; 
    int loc;
    for (i = 1; i < n; i++) { 
        key = arr[i]; //當前要插入正確洞的記錄
        j = i - 1;  // 將 key 插入正確的洞
        
        // 使用二分查找找到 key 正確的洞
        loc = binarySearch(a, key, 0, j);
        
        while (j >= loc) { 
            arr[j + 1] = arr[j]; 
            j--; 
        } 
        arr[j + 1] = key; 
    } 
} 

二分查找就不寫了奧!不會的看文章。但是這裏僅僅只是將查找待插入關鍵字 key = arr[i] 的正確洞的時間降到了 ,但是需要將 [loc,i-1] 的關鍵字向後移動,所以二分插入排序的時間複雜度依舊是

遞歸實現插入排序

這道題目能考察到你對於遞歸和插入排序兩個知識點的理解程度。對於遞歸的三要素,不再強調,我們一起看一下實現代碼:

//1.進行插入排序
void insertionSortRecursive(int arr[], int n) 
{ 
    //2遞歸的出口,僅有一個元素時有序 
    if (n <= 1) 
        return; 
  
    //3.對前 n-1 個元素遞歸進行插入排序
    insertionSortRecursive( arr, n-1 ); 
  
    // 歸:將關鍵字key插入到正確的位置
    int key = arr[n-1]; 
    int j = n-2; 
  
    /* 將key之前arr[0,i-1]中比 key 大的元素向後移動*/
    while (j >= 0 && arr[j] > key) 
    { 
        arr[j+1] = arr[j]; 
        j--; 
    } 
    arr[j+1] = key; //將key放入正確的洞
} 

我們將數組 arr = [5,1,4,2,8,4]n = 6 帶入上面的代碼:

我想看完這個圖,結合插入排序,定對你理解遞歸有幫助。

交換實現插入排序

標準的插入排序中,需要將 arr[0,i-1] 中大於 arr[i] 的關鍵字向後移動,這裏我們不再採用這種方式,而是在比較之後,如果滿足大於的關係,直接交換兩個元素。如下面動畫中將關鍵字 2 插入到正確的洞,採用交換實現:

void insertionSort(int arr[],int n) 
{ 
    int i, j; 
  
    for (i = 1; i < n; i++) { 
        j = i; 
        // 將 arr[j] 插入到 arr[0,i-1] 正確的位置
        while (j > 0 and arr[j] < arr[j - 1]) { 
     //交換
            swap(arr[j], arr[j - 1]); 
     j--;
        } 
    } 
} 

代碼是不是簡潔多了,但是同時也要意識到這裏最壞情況下的時間複雜度依舊是 .

有興趣的可以嘗試一下遞歸的寫法,這裏給出參考代碼,一定要自己動手寫了再回來看(至少腦子裏過一遍)。

void insertionSortRecursive(int arr[],int n) 
{ 
    if (n <= 1)
        return;
    
    insertionSortRecursive(arr, n-1);
 int j = n-1;
    while (j > 0 and arr[j] < arr[j - 1]) { 
        //交換
        swap(arr[j], arr[j - 1]); 
        j--;
    } 
} 

  


推薦閱讀

•   吳師兄實名吐槽 LeetCode 上的一道題目。。。•   面試字節跳動時,我竟然遇到了原題……•   Leetcode 驚現馬化騰每天刷題 ?爲啥大佬都這麼努力!•   爲什麼 MySQL 使用 B+ 樹•   一道簡簡單單的字節跳動算法面試題•   新手使用 GitHub 必備的兩個神器•   臥槽!紅警代碼竟然開源了!!!


歡迎關注我的公衆號“五分鐘學算法”,如果喜歡,麻煩點一下“在看”~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章