1. 程式人生 > 其它 >老生常談的直接插入排序、冒泡和選擇排序

老生常談的直接插入排序、冒泡和選擇排序

排序原理

有些排序演算法,可以歸納為“有序區擴張,無序區收縮”。所謂有序區,就是在此區間的元素都已經是有順序的了,無序區則是在此區間的元素都是雜亂無章的。接下來我們先看幾個典型的“有序區擴張,無序區收縮”演算法。

直接插入排序

首先,選定一端為有序區,然後將其餘所有元素歸入無序區,比如說有個待排序列是這樣的:

[0, 1, 2, 3,  4,  5,  6, 7]  // 下標
[1, 5, 8, 10, 36, 24, 5, 9]  // 元素列表

首先,我們可以選定以下標 0 為有序區,其餘所有元素 1 - 7 則都屬於無序區,按照有序區擴張,無序區收縮的原則,每次從無序區中選定一個元素加入有序區中,對於直接插入排序來說,則順序地從無序區中選擇一個元素,然後放入有序區中,我們用 | 來分隔有序區和無序區,則剛開始時:

[1, | 5, 8, 10, 36, 24, 5, 9]

然後這樣:

[1, 5, | 8, 10, 36, 24, 5, 9]

就這樣直到無序區不再有元素:

[1, 5, 8, | 10, 36, 24, 5, 9]
[1, 5, 8, 10, | 36, 24, 5, 9]
[1, 5, 8, 10, 36, | 24, 5, 9]
[1, 5, 8, 10, 24, 36, | 5, 9]
[1, 5, 5, 8, 10, 24, 36, | 9]
[1, 5, 5, 8, 9, 10, 24, 36 |]    // 無序區不再有任何元素,結束

然後轉化為核心程式碼就像這樣:

// 引數表示從下標為 begin - end 的元素集合將被排序,下標從 0 開始,包括 begin 和 end
public static void insertSort(int[] data, int begin, int end) {

    // i 表示無序區第一個元素的下標,即等於在無序區中選中了一個元素,相當於無序區在收縮
    for (int i = begin + 1; i <= end; i++) {

        // 那麼接下來就是要把這個從無序區中選中的元素放入有序區中,等同於有序區在擴張
        // 如果從無序區中選中的元素已經比有序區最後一個元素大了,也就是不用再找位置了,直接坐下就行!
        if(data[i] < data[i-1]) {

            // 用一個臨時值代表這個選中的元素,從而避免需要頻繁交換元素
            int tmp = data[i];

            // j 初始化為有序區最後一個元素下標
            int j = i - 1;

            // 怎麼放呢?這裡提供一種方式:
            // 從有序區逆序遍歷,只要有序區中選中的元素不如無序區中選中的元素小,那麼自動向前一步
            for (; j >= begin && data[j] > tmp; j--) {
                data[j + 1] = data[j];
            }

            // 經過有序區的大元素讓步後,無序區中選中的元素就找到了自己的位置
            // 為什麼是 j + 1 呢?因為下標為 j 的元素不滿足向前一步的條件,所以是在它前面空了一個位置
            data[j + 1] = tmp;
        }
    }
}

氣泡排序

所謂氣泡排序,就是說像魚吐泡泡一樣,從無序區的第一個元素開始遍歷直到無序區再無元素,每個元素都與前一個元素對比,這叫一趟冒泡,每趟都會產生一個元素進入有序區,當無序區再無元素時,待排數列已是有序。可能我說地太抽象了,來看下這張圖:

初始時,大家都在無序區,一趟冒泡後,變成了下面這樣,有一個元素脫離了無序區,進入了有序區:

就是像這樣,一個兩個喵,你和我,心連心,手牽手... 咳咳,打個比方,有某桃園三結義某天排隊去買奶茶,他們剛開始是這樣排的(他們三個都屬於無序區):

| 小劉、小張、小關 |

突然大哥覺得自己應該在前面,也就是看上了二弟的位置,於是對三弟說,三弟,站大哥後面來!小張一聽,大哥這是想插隊啊,但秉著尊老愛幼的美德還是站到了大哥後面,然後就變成了這樣:

| 小張、小劉、小關 |

大哥再看,二弟還在前面,不行,再來,於是就變成了這樣,大哥成功佔領二弟的位置:

| 小張、小關、| 小劉

這裡,小劉的這種做法就是典型的一趟冒泡,最終小劉進入了有序區,只留下二弟和三弟在無序區中凌亂

轉化為核心程式碼就像這樣:

// 引數表示從下標為 begin - end 的元素集合將被排序,下標從 0 開始,包括 begin 和 end
public static void bubbleSort(int[] data, int begin, int end) {

    // i 表示從無序區中的選出來的元素將要放到有序區中的位置
    for (int i = end; i > begin; i--) {

        // 如果一輪冒泡中元素之間未發生過任何位置交換,那麼這序列已是有序的了
        boolean changed = false;

        // 有序區的位置已經確定了,那麼接下來就是從無序區中確定誰將坐上那個位置!
        // 怎麼確定呢?打一架就行,誰贏誰上,贏者繼續打,直到產生一個最終贏家,無疑它就是全場最厲害的
        for (int j = begin; j < i; j++) {

            // 如果打贏了就能夠往前一步,繼續下一場比賽
            if (data[j] > data[j + 1]) {
                swap(data, j, j + 1);

                // 元素位置發生了交換
                changed = true;
            }
        }

        // 如果沒有元素交換過位置,已是有序,結束
        if (!changed) return;
    }
}

選擇排序

有了氣泡排序的基礎,難道還要慌這個嗎?前面說過,氣泡排序是先確定了從無序區中選中的某個元素在有序區的位置,然後開始從無序區中選擇一個元素放入這個位置。在氣泡排序中使用的是一個兩個喵,啊呸,是擂臺賽打架的方式確定最強者,打贏的就往前走,贏者的贏者的贏者...就是走在時代前列的人!但這樣打一下走一下實在太費時間了,大家原地打就好了,如果我是最強者,我直接坐上有序區的寶座,就不用走那麼多彎路了。

那麼,既然這樣,程式碼就變成這個風格了:

public static void selectSort(int[] data, int begin, int end){

    // i 表示本輪有序區的唯一位置,依次擴張
    for (int i = end; i > begin; i--) {
        
        // 假設現在坐在有序區唯一寶座上的是最強者
        int max = i;

        // 底層人物個個不服這最強者,紛紛欲與之一較高下
        for (int j = begin; j < i; j++) {
            
            // 打贏的
            if (data[j] > data[max]){
                // 最強者稱號就是你的,但還不能坐上寶座,還得接受別人的挑戰,直到大家都打不過你
                max = j;
            }
        }

        // 都打服之後,如果發現最強者不是原來坐在寶座上的人,那麼不好意思,你來我的位置,我帶著我的榮耀登上這寶座
        if (max != i){
            swap(data, i, max);
        }
    }
}