《數據結構與算法之美》——冒泡排序、插入排序、選擇排序


排序,是每一本數據結構的書都繞不開的重要部分。

排序的算法也是琳琅滿目、五花八門。

每一個算法的背后都是智慧的結晶,思想精華的沉淀。

個人覺得排序算法沒有絕對的孰優孰劣,用對了場景,就是最有的排序算法。

當然,撇開這些業務場景,排序算法本身有一些自己的衡量指標,比如我們經常提到的復雜度分析。

我們如何分析一個算法?

排序算法的執行效率

1、最好、最壞和平均情況的時間復雜度

2、時間復雜度的系數、常數和低階

一般來說,在數據規模n很大的時候,可以忽略這些,但是如果我們需要排序的數據規模在幾百、幾千,那么這些指標就變的更加重要。

3、比較的次數和移動的次數

排序的過程涉及數據的比較和交換(移動)

排序算法的內存消耗

除了時間復雜度,我們還有空間復雜度,用來衡量內存消耗。這里我們引入原地排序的概念。原地排序即特指空間復雜度為O(1)的排序算法。

排序算法的穩定性

什么是穩定性,這比較抽象。

舉個例子,現在有一組集合1,3,5,3,7

按照從小打到的順序進行排序,結果應該是1,3,3,5,7

穩定指的是原集合的第二個3仍然在第四個3前面。不穩定則情況相反。

冒泡排序

原理

相鄰元素兩兩比較,如果滿足大小關系就保持不動,如果不滿足,則兩兩交換位置,以此類推,直到集合有序為止。

之所以叫冒泡排序,因為其過程就猶如水中的氣泡,泡泡越大的就在上面,越小的就在下面。

舉例

現在給定一個集合4,5,6,3,2,1

第一次冒泡過程如下所示

可以看出在這趟冒泡中,最大的泡泡6已經到達最高的位置,要讓集合中所有元素都有序,還要繼續冒泡,如下圖:

代碼


package com.jackie.algo.geek.time.chapter11_sort;

/**
 * @Author: Jackie
 * @date 2019/1/12
 */
public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = new int[]{100,82,74,62,54,147};
        bubbleSort(arr);
        bubbleSort2(arr);
    }
    /**
     * 外層i的循環代表比較的趟數,內層j的循環代表的元素位置
     *  a[0],a[1],a[2],a[3],a[4],a[5]
     *  第一趟走完,最大的元素冒泡到最后a[5]的位置,需要比較的位置即為:
     *  a[0],a[1],a[2],a[3],a[4]
     *  所以可以看到j的終止條件是動態變化的,與i的位置相關,趟數每增加一次,終止的位置就往前挪一個,因為每次都能固定一個元素
     *
     *  注意這里的邊界條件,是<還是<=
     *  第一層是小於,因為是從0開始,對於上面的例子來說,是比較length-1=6-1=5趟,因為總共6個元素,只要5趟就能比較完成
     *  好比有兩個元素,只要一趟就能比較完成
     *  第二層是同樣的道理,假設在i=0時,length-i-1=6-0-1=5,
     *  但是這里<,所以只會到j=4,乍一看你會覺得之比較到了a[j]=a[4],最后a[5]是不是就丟了
     *  其實不是,仔細看下面的比較條件就會發現有a[j+1]即a[5]
     *  所以,綜上內層和外層都是從0開始,且都是<而不是<=
     */
    public static void bubbleSort(int[] arr) {
        int length = arr.length;
        if (length <= 0) {
            return;
        }
        int temp;
        for (int i = 0; i < length - 1; i++) {
            boolean flag = false;
            for (int j = 0; j < length - i - 1; j++) {
                if (arr[j] > arr[j+1]) {
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;

                    flag = true;
                }
            }
            if (!flag) {
                System.out.println("total loop: " + (i+1) + " times, stop at index:" + i);
                break;
            }
        }
        for (int i = 0; i < length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }
    /**
     * 和上面的不同之處在於,上面的是保證數組從后往前有序,這里的是保證從前往后的有序
     * 上面的做法如下所示,每次要遍歷的元素如下
     * a[0],a[1],a[2],a[3],a[4],a[5]
     * a[0],a[1],a[2],a[3],a[4]     (這里不再遍歷a[5]的位置,因為a[5]在第一輪遍歷已是最大,不需要參與遍歷,下面遍歷同理)
     * a[0],a[1],a[2],a[3]
     * a[0],a[1],a[2]
     * a[0],a[1]
     * a[0]
     *
     * 下面的做法如下所示,每次要遍歷的元素如下
     * a[0],a[1],a[2],a[3],a[4],a[5]
     *      a[1],a[2],a[3],a[4],a[5]   (這里不再遍歷a[0]的位置,因為a[0]在第一輪遍歷已是最小,不需要參與遍歷,下面遍歷同理)
     *           a[2],a[3],a[4],a[5]
     *                a[3],a[4],a[5]
     *                     a[4],a[5]
     *                          a[5]
     */
    public static void bubbleSort2(int[] arr) {
        int length = arr.length;
        if (length <= 0) {
            return;
        }
        int temp;
        for (int i = 0; i < length - 1; i++) {
            boolean flag = false;
            for (int j = length - 1; j > i; j--) {
                if (arr[j] < arr[j-1]) {
                    temp = arr[j];
                    arr[j] = arr[j-1];
                    arr[j-1] = temp;

                    flag = true;
                }
            }
            if (!flag) {
                System.out.println("total loop: " + (length - i - 1) + " times, stop at index:" + i);
                break;
            }
        }
        for (int i = 0; i < length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }
}

寫這類算法對於邊界判定、起始條件和結束條件要非常謹慎,比如是用<還是用<=;是從0開始還是從1開始;是到length結束還是到length-1結束。

看似惺忪平常,有時候弄錯一個符號就無法得到正確的排序結果。

冒泡排序的這些注意事項已經寫在代碼的注釋中,參見如上代碼。

同時,代碼已經上傳至Github

各項指標

1、是否是原地排序

是,因為冒泡排序只涉及兩兩元素交換,空間復雜度為O(1)

2、是否是穩定排序

是,對於元素相等的情況,不會交換順序

3、時間復雜度

平均時間復雜度是O(n2), 這里是n的平方

插入排序

原理

對於給定集合,從左至右,依次保證當前元素的左邊集合有序。然后依次順延當前位置,直至遍歷完所有集合元素,保證整個集合有序。

有點抽象,沒有關系,看舉例。

舉例

借用文章https://www.cnblogs.com/bjh1117/p/8335628.html中的例子說明插入排序的過程。


待比較數據:7, 6, 9, 8, 5,1



  第一輪:指針指向第二個元素6,假設6左面的元素為有序的,將6抽離出來,形成7,_,9,8,5,1,從7開始,6和7比較,發現7>6。將7右移,形成_,7,9,8,5,1,6插入到7前面的空位,結果:6,7,9,8,5,1



  第二輪:指針指向第三個元素9,此時其左面的元素6,7為有序的,將9抽離出來,形成6,7,_,8,5,1,從7開始,依次與9比較,發現9左側的元素都比9小,於是無需移動,把9放到空位中,結果仍為:6,7,9,8,5,1



  第三輪:指針指向第四個元素8,此時其左面的元素6,7,9為有序的,將8抽離出來,形成6,7,9,_,5,1,從9開始,依次與8比較,發現8<9,將9向后移,形成6,7,_,9,5,1,8插入到空位中,結果為:6,7,8,9,5,1



  第四輪:指針指向第五個元素5,此時其左面的元素6,7,8,9為有序的,將5抽離出來,形成6,7,8,9,_,1,從9開始依次與5比較,發現5比其左側所有元素都小,5左側元素全部向右移動,形成_,6,7,8,9,1,將5放入空位,結果5,6,7,8,9,1。



  第五輪:同上,1被移到最左面,最后結果:1,5,6,7,8,9。

代碼


package com.jackie.algo.geek.time.chapter11_sort;

/**
 * @Author: Jackie
 * @date 2019/1/13
 */
public class InsertSort {
    public static void main(String[] args) {
        int[] arr = new int[]{100,82,74,62,54,147};
        insertSort(arr);
    }
    /**
     * 借用https://www.cnblogs.com/bjh1117/p/8335628.html文中的舉例,我們可以看到一個完整的插入排序的過程
     * 通過這個過程,我們可以更好的理解插入排序的思想
     * 待比較數據:7, 6, 9, 8, 5,1
     *
     *   第一輪:指針指向第二個元素6,假設6左面的元素為有序的,將6抽離出來,形成7,_,9,8,5,1,從7開始,6和7比較,發現7>6。將7右移,形成_,7,9,8,5,1,6插入到7前面的空位,結果:6,7,9,8,5,1
     *
     *   第二輪:指針指向第三個元素9,此時其左面的元素6,7為有序的,將9抽離出來,形成6,7,_,8,5,1,從7開始,依次與9比較,發現9左側的元素都比9小,於是無需移動,把9放到空位中,結果仍為:6,7,9,8,5,1
     *
     *   第三輪:指針指向第四個元素8,此時其左面的元素6,7,9為有序的,將8抽離出來,形成6,7,9,_,5,1,從9開始,依次與8比較,發現8<9,將9向后移,形成6,7,_,9,5,1,8插入到空位中,結果為:6,7,8,9,5,1
     *
     *   第四輪:指針指向第五個元素5,此時其左面的元素6,7,8,9為有序的,將5抽離出來,形成6,7,8,9,_,1,從9開始依次與5比較,發現5比其左側所有元素都小,5左側元素全部向右移動,形成_,6,7,8,9,1,將5放入空位,結果5,6,7,8,9,1。
     *
     *   第五輪:同上,1被移到最左面,最后結果:1,5,6,7,8,9。
     *
     * 所以插入排序是保證一個元素的左邊所有元素都是有序的,然后逐漸右移,直到遍歷完所有的元素來保證整個數據是有序的
     * 下面i從1開始,是表示以a[1]作為哨兵,第一次比較是a[0]和其比較,這里的j的其實位置都是小於i一個位移,即j=i-1
     * 然后依次從右向左挨個比較,如果發現哨兵值小於左側有序集合,則一直位移,以此保證始終留有一個位置用於插入待排序的值
     * 一旦發現哨兵值如果大於等於(保證穩定性,即不會跑到等於某個值的左側)左側集合中的某個值,
     * 則跳出內層循環,仔細想想左側集合是有序的就明白了
     * 至於最后為什么是a[j+1]=value,直覺上更應該是a[j]=value,但是記得,在跳出內層循環的時候進行了一次j--操作,
     * 所以需要把這個操作補償進來,變成了j+1
     */
    public static void insertSort(int arr[]) {
        int length = arr.length;
        if (length <= 0) {
            return;
        }
        for (int i = 1; i < length; i++) {
            int value = arr[i];
            int j = i - 1;

            for (; j >= 0; j--) {
                if (arr[j] > value) {
                    arr[j+1] = arr[j];  // 位移
                } else {
                    break;
                }
            }
            arr[j+1] = value;
        }
        for (int i = 0; i < length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }
}

同冒泡排序,有關邊界判定、起始條件和結束條件也都寫在注釋中,不再贅述。

各項指標

1、是否是原地排序

是,同冒泡排序,空間復雜度為O(1)

2、是否是穩定排序

是,對於元素相等的情況,不會交換順序

3、時間復雜度

平均時間復雜度是O(n2), 這里是n的平方

選擇排序

原理

選擇排序思想和插入排序思想比較接近。每次排序從未排序的集合中找到最小的元素放進有序集合,通過這樣的遍歷排序保證整個集合有序。

舉例

代碼


package com.jackie.algo.geek.time.chapter11_sort;

/**
 * @Author: Jackie
 * @date 2019/1/13
 */
public class SelectionSort {
    public static void main(String[] args) {
        int[] arr = new int[]{100,82,74,62,54,147};
        selectionSort(arr);
    }
    public static void selectionSort(int[] arr) {
        int length = arr.length;
        if (length <= 1) return;

        for (int i = 0; i < length - 1; ++i) {
            // 查找最小值
            int minIndex = i;
            for (int j = i + 1; j < length; ++j) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 交換
            int tmp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = tmp;
        }
        for (int i = 0; i < length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

各項指標

1、是否是原地排序

是,同冒泡排序,空間復雜度為O(1)

2、是否是穩定排序

否,通過元素的交換可能改變原來的穩定結構,比如5,8,5,2,9,第一次排序后,5和2交換,則第一個5就跑到第二個5后面了,破壞了穩定結構。

3、時間復雜度

平均時間復雜度是O(n2), 這里是n的平方,且最好最壞都是O(n2)。

聲明:

文中圖片來自極客時間王爭老師專題《數據結構與算法之美》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM