編程馬拉松 Day03 冒泡排序、選擇排序、插入排序

排序是科學計算和數據處理必不可少的一個環節,今天起我們就來聊聊排序。

本文將介紹三個初級排序算法

  1. 冒泡排序
  2. 選擇排序
  3. 插入排序

先來看下圖這樣的一組初始數據,每一個矩形的高度都與其下方的數字成比例,數值越大則矩形的高度就越高。

初始數據

假設有如下兩個問題,我們該如何求解。

  • 找出最(小/大)值
  • 找出第k(小/大)的值

顯然,在亂序的數組中這兩個問題都不太容易求解,但如果數據是有序的就會容易很多。

冒泡排序

冒泡排序是最容易想到的排序算法。以對N個元素的數組進行升序排序為例,其基本思路如下:

  1. 從數組內的前兩個元素開始,將這兩個元素進行比較,如果前一個元素大于后一個元素,則交換兩者的位置
  2. 接著取數組中的第2-3個元素進行比較,若第2個元素大于第3個元素,則交換兩者的位置
  3. 循環往復,直到數組中的最后兩個元素,此時,若第N-1個元素大于第N個元素,則交換它們的位置。經過一輪的比較與交換,我們已經得到了數組中最大的元素,并將其安置在了數組的第N位。
  4. 經過前三個步驟,我們將數組中最大的元素放到了第N位,下邊只用排序數組中的前N-1個元素即可。此時我們將N的值減1,并判斷新N的值,若新N>0,則循環1-3步驟;若新N=0,則代表我們已經完成了排序。

冒泡排序的過程(剪輯版)如下圖所示,你也可以點擊這里查看完整的冒泡排序過程

冒泡排序剪輯版

我們知道,水杯中出現氣泡時,越大的氣泡浮力越大,上升速度也就越快,最先到達水面,冒牌排序中每輪遴選較大元素放置末尾的行為與水中氣泡上升的現象十分相似,因此得名冒泡排序。

冒泡排序代碼

public static void bubbleSort(Integer arr[]) {
        int compareCount = 0;//比較次數
        int swapCount = 0;//交換次數
        long before = System.currentTimeMillis();
        for (int i = 0; i < arr.length; i++) { //外層循環,與數組元素個數相關
            for (int j = 1; j < arr.length - i; j++) { //內層循環,只需在前 n-1 個元素內進行相鄰比較
                compareCount++;
                if (arr[j - 1] > arr[j]) {
                    swapCount++;
                    swap(arr, j - 1, j);
                }
            }
        }
        long after = System.currentTimeMillis();
        System.out.println("冒泡排序耗時:"+(after-before)+"ms,"+"比較次數:"+compareCount+",交換次數:"+swapCount);
}

使用上述代碼對以下兩組數據排序時,其比較次數一致,僅交換次數不同。但顯而易見的是,第二組本身就是有序的,也就是說上邊的代碼中,存在冗余比較。
3 5 1 6 10 9 11
1 3 5 7 9 10 11

原有數組 3 5 1 6 10 9 11 
冒泡排序耗時:0ms 比較次數:21,交換次數:3
排序后 1 3 5 6 9 10 11 
---
原有數組 1 3 5 6 9 10 11 
冒泡排序耗時:0ms 比較次數:21,交換次數:0
排序后 1 3 5 6 9 10 11 

針對這樣的問題,我們可以采用如下思路對冒泡排序的代碼進行優化。

  • 當某輪內循環沒有發生元素交換時,表明數組已然有序,無需再進行后續的比較,此時可直接中止循環

冒泡排序優化代碼

public static void bubbleSortOpt(Integer arr[]) {
        int compareCount = 0;//比較次數
        int swapCount = 0;//交換次數
        long before = System.currentTimeMillis();
        for (int i = 0; i < arr.length; i++) { //外層循環,與數組元素個數相關
            boolean isSwap = false; //交換標記,每輪外循環開始時,將其置位false
            for (int j = 1; j < arr.length - i; j++) { //內層循環,只需在前 n-1 個元素內進行相鄰比較
                compareCount++;
                if (arr[j - 1] > arr[j]) {
                    isSwap = true;//若內循環內發生交換,則將交換標志置位true
                    swapCount++;
                    swap(arr, j - 1, j);
                }
            }
            if (!isSwap) {//判斷循環標記,若未發生交換,則跳出循環
                break;
            }
        }
        long after = System.currentTimeMillis();
        System.out.println("冒泡排序耗時:"+(after-before)+"ms,"+"比較次數:"+compareCount+",交換次數:"+swapCount);
}
原有數組 3 5 1 6 10 9 11 
冒泡排序耗時:0ms 比較次數:15,交換次數:3
排序后 1 3 5 6 9 10 11 
---
原有數組 1 3 5 6 9 10 11 
冒泡排序耗時:0ms 比較次數:5,交換次數:0
排序后 

選擇排序

選擇排序的思路同樣很簡單,以對含有N個元素的數組進行升序排序為例,其步驟如下所示:

  1. 假設首元素是最小的,并記錄其索引值為minIndex,遍歷數組,分別與其比較,若數組中第i個元素的數值小于第minIndex個元素的數組,則將i賦值與minIndex。
  2. 遍歷結束后,我們得到了數值最小的元素的索引值,將其與首元素進行交換,交換后的首元素即為數組中數值最小的元素。
  3. 經過前邊兩個步驟,此時數組中可分為首元素和與第2個元素開始到末尾的N-1個元素。判斷N-1,若N-1>0,則將第二個元素視作首元素,重復步驟1-2;若N-1=0,則表明數組已然有序,中止循環。

選擇排序的過程(剪輯版)如下圖所示,你也可以點擊這里查看完整的選擇排序過程

選擇排序剪輯版

根據這個思路,不難寫出其代碼

public static void selectSort(Integer arr[]) {
    int compareCount = 0;
    int swapCount = 0;
    long before = System.currentTimeMillis();
    for (int i = 0; i < arr.length; i++) {
        int minIndex = i;
        for (int j = i+1; j < arr.length - i; j++) {
            compareCount++;
            if (arr[j]<arr[minIndex]){
                minIndex = j;
            }
        }
        if (minIndex!=i){
            swapCount++;
            swap(arr,i,minIndex);
        }
    }
    long after = System.currentTimeMillis();
    System.out.println("選擇排序耗時:" + (after - before) + "ms," + "比較次數:" + compareCount + ",交換次數:" + swapCount);
}

插入排序

插入排序是我們需要了解是最后一個簡單排序算法,其思路與我們打撲克牌時的起牌手法相似。

  1. 假設我們用左手持牌,右手起牌,每次起牌完成后,左手中的手牌均為有序的。
  2. 開始起牌時,左手手牌為空,此時從牌堆頂取一張牌,直接放入左手
  3. 在起后邊的牌時,我們拿右手中剛起到的那張新牌,與左手中的所有手牌進行比較,并放入到合適的位置。
    • 按從大到小的順序分別拿左手中的手牌與新牌進行比較
    • 在從大到小的比較過程中,若左手當前手牌比新牌大,則取交換著兩張牌的位置,并以左手當前手牌作為新牌,與剩余的左手手牌進行比較
    • 若左手的當前手牌小于等于新牌,則將新牌插入到當前手牌之后,并將此后的手牌依次向后挪動
使用插入排序來排序手中撲克牌

需要注意的是,在插入排序時,我們將數組分為了兩部分,一部分是"左手"中的有序子數組,另一部分是"牌堆"中無序的子數組。初始時,我們將數組中的第一個元素視作已排序子數組,并將第二個元素至最后一個元素視作無序子數組。我們每次從無序子數組中取出首元素p,從后往前分別與有序子數組中的元素q進行比較,若p小于q的數值,則將p與q交換,并繼續用p與子數組中剩下的元素進行比較和交換,直到p不小q時,完成此輪插入,此時有序子數組的長度+1,無序子數組的長度-1。

插入排序的過程(剪輯版)如下圖所示,你也可以點擊這里查看完整的插入排序過程

插入排序剪輯版

插入排序代碼

public static void insertSort(Integer arr[]) {
    int compareCount = 0;
    int swapCount = 0;
    long before = System.currentTimeMillis();
    //外層循環,i表示有序子數組與無序子數組間的界限,i之前的元素為有序的,i及i之后的元素為無序的
    for (int i = 1; i < arr.length; i++) {
        //內層循環,將i到0之間的元素兩兩比較,若i<i-1,則交換兩者的位置
        for (int j = i; j > 0; j--) {
            compareCount++;
            if (arr[j] < arr[j - 1]) {
                swapCount++;
                swap(arr, j, j - 1);//兩兩交換
            }
        }
    }
    long after = System.currentTimeMillis();
    System.out.println("選擇排序耗時:" + (after - before) + "ms," + "比較次數:" + compareCount + ",交換次數:" + swapCount);
}

我們知道頻繁的兩兩交換也是有性能損耗的,對于插入排序,我們通過如下的思路進一步優化:

  • 在新牌插入過程中,先在左手手牌的后方將新牌的空間給預留出來,從大到小,依次比較當前手牌與新牌。
  • 若當前手牌大于新牌,則將當前手牌向后挪動一下(注意,此時并不拿新牌與當前手牌交換),將左手手牌后方的空間擠壓到當前手牌的前方
  • 若當前手牌小于新牌,則將新牌放到這里

有了這個思路,我們便可以將此前頻繁的兩兩交換,換為單個元素后移,從而減少了一定的性能開銷。

優化插入排序

public static void insertSortOpt(Integer arr[]) {
    long before = System.currentTimeMillis();
    for (int i = 1; i < arr.length; i++) {
        //使用臨時變量保存新牌
        int temp  =arr[i];
        int j = i;
        //從大到小,依次取左手中的牌與temp進行比較,若左手當前手牌大于temp,則將當前手牌后移一位
        while(j>0 && temp<arr[j-1]){
            arr[j] = arr[j -1];
            j--;//繼續下一張較大的牌
        }
        arr[j] = temp;//最終將temp插入左手手牌中合適的位置
    }
    long after = System.currentTimeMillis();
    System.out.println("插入排序耗時:" + (after - before) + "ms");
}

在規模較大的問題中,這種方式帶來的好處非常明顯。

10W條數據
   插入排序耗時:12296ms
優化插入排序耗時:2742ms

測試

排序算法 問題規模(待排序元素個數) 解題時間1 解題時間2 解題時間3 平均解題時間
優化冒泡排序 1W 293ms 293ms 278ms 288ms
選擇排序 1W 28ms 43ms 52ms 41ms
優化插入排序 1W 22ms 36ms 28ms 28.7ms
優化冒泡排序 5W 7806ms 7428ms 8011ms 7748.3ms
選擇排序 5W 603ms 617ms 606ms 608.7ms
優化插入排序 5W 598ms 606ms 600ms 601.3ms
優化冒泡排序 10W 28801ms 30978ms 29308ms 29725.7ms
選擇排序 10W 2609ms 2649ms 2658ms 2638.7ms
優化插入排序 10W 2693ms 2712ms 2685ms 2696.7ms

經過測試,可以看到冒泡排序的耗時最多。插入排序在規模較小的數組中明顯快于選擇排序,在規模較大的數組中與選擇排序相當,從此也證明了我們此前算法分析環節中得到的結論。

小結

本文介紹了三個基礎的排序算法,在這里先對它們做一個總結,希望能讓大家對排序及算法效率有一個直觀的感受。

排序算法 核心思路 最好情況(有序) 最壞情況(逆序) 時間復雜度O 特點
優化冒泡排序 相鄰元素兩兩比較并交換 比較n-1次,交換0次 比較n(n-1)/2次,交換n(n-1)/2次 O(n2) 簡單易懂,效率較低
直接選擇排序 已知位次找第k(大/小)元素 比較n(n-1)/2次,交換0次 比較n(n-1)/2次,交換n次 O(n2) 運行時間與原始數據無關;交換次數最少
優化插入排序 撲克牌起牌法 比較n-1次,交換0次 比較n(n-1)/2次,后移(n-1)(n-2)/2次 O(n2) 運行時間與原始數據強相關;對部分有序數據小規模數據極為友好

選擇排序和插入排序的異同點:

  1. 插入排序與選擇排序一樣,當前索引左邊的所有元素都是有序的。但在選擇排序中,當前索引左邊的元素位置是固定的(與最終位置一致);而插入排序當前索引左邊的元素位置未必固定,為了給后邊更小的元素騰出空間,它們可能會被移動,當索引到達數組的右端時,排序完成。
  2. 選擇排序的運行時間與原始數據無關(比較次數恒定);插入排序的運行時間與原始數據強相關,當對一個有序或接近有序的數組排序時,會比隨機順序或逆序的數組快很多。

參考書目

《算法導論》 - CLRS
《算法》第四版 - Sedgewick
《數據結構與算法分析》 - Weiss

參考博客

關于插入排序和選擇排序的比較
Java排序算法分析與實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容