咱們一塊兒來排序——使用Java語言優雅地實現經常使用排序算法

破陣子·春景java

燕子來時新社,梨花落後清明。面試

池上碧苔三四點,葉底黃鸝一兩聲。日長飛絮輕。算法

巧笑同桌夥伴,上學徑裏逢迎。shell

疑怪昨宵春夢好,元是今朝Offer拿。笑從雙臉生。api

排序算法——最基礎的算法,互聯網面試必備技能。春來來了,排序的季節來了!數組

本文使用Java語言優雅地實現經常使用排序算法,但願對你們有幫助,早日拿到Offer!ide

冒泡排序

最暴力、最無腦、最簡單的排序算法。名字的由來是由於越大的元素會經由交換慢慢「浮」到數組的頂端,就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端同樣,故名「冒泡排序」。ui

冒泡排序的基本思想是:每次比較相鄰的元素,若是它們的順序和理想順序不一致,就把它們進行交換。很少叨叨了,直接看代碼。指針

public static void bubbleSort(int[] arr) {
    int n = arr.length;
    if (n <= 1) {
        return;
    }
    //冒泡排序,遇到亂序無論三七二十一直接交換完事
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            if (arr[i] > arr[j]) {
                swap(arr, i, j);
            }
        }
    }
}

private static void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

選擇排序

選擇排序,這樣記憶,選擇最小的元素與未進行排序的首元素進行交換。code

選擇排序具體過程:

  1. 找到數組中最小的元素,將它與數組的第一個元素交換位置;
  2. 在剩下的元素中尋找最小的元素,將它和數組第二個元素交換位置;
  3. 往復執行,直到將整個數組排序完成。

選擇排序特色:

  1. 運行時間和輸入無關;選擇排序爲了找到最小的元素須要每次都掃描一遍整個輸入數組,這也是它的平均時間複雜度、最好狀況、最壞狀況都是O(n^2)。
  2. 數據移動最少;每次交換都會改變兩個數組元素的值,交換次數和要排序的數組大小呈線性關係。
public static void selectSort(int[] arr) {
    int n = arr.length;
    if (n <= 1) {
        return;
    }

    //選擇排序,每次選擇最小的元素與未進行排序的首元素進行交換
    for (int i = 0; i < n; i++) {
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[minIndex] > arr[j]) {
                minIndex = j;
            }
        }
        swap(arr, i, minIndex);
    }
}

private static void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

插入排序

插入排序,這樣記憶,將一個元素插入到已經排好序的有序數組中。

插入排序的基本思想是:每步將一個待排序的元素,插入前面已經排序的數組中適當位置上,直到所有插入完爲止。

在程序的實現中,爲了給要插入的元素騰出空間,須要將其他全部元素在插入以前都向右移動一位。

插入排序所需的時間取決於輸入元素的初始順序,對數據量比較大且基本有序的數組進行排序要比對隨機順序或者逆序數組排序要快的多。

public static void insertSort(int[] arr) {
    int n = arr.length;
    if (n <= 1) {
        return;
    }

    //插入排序:找到位置,將其他全部元素在插入以前都向右移動一位
    for (int i = 1; i < n; i++) {
        for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
            swap(arr, j, j - 1);
        }
    }
}

private static void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

希爾排序

希爾排序是1959年Shell發明,是第一個突破O(n^2)的排序算法,是簡單插入排序的改進版。與插入排序的不一樣之處在於,它會優先比較距離較遠的元素。

希爾排序是把記錄按下標的必定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量減至1時,整個文件恰被分紅一組,算法便終止。

希爾排序的核心在於間隔序列的設定。既能夠提早設定好間隔序列,也能夠動態的定義間隔序列。動態定義間隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。

關於希爾排序的時間複雜度,有人在大量的實驗以後得出結論:當n在某個特定的範圍後希爾排序的比較和移動次數減小至n^1.3 ,關於數學論證,這就很困難了。這種科學難題咱們就不用太糾結了。

public static void shellSort(int[] arr) {
    int n = arr.length;
    int h = 1;
    while (h < n / 3) {
        h = 3 * h + 1;//1,4,13,40,121,364,1093, ...
    }
    while (h >= 1) {
        //將數組變爲h有序
        for (int i = h; i < n; i++) {
            //將arr[i]插入到arr[i-h],arrr[i-2*h],arr[i-3*h]...中
            for (int j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
                swap(arr, j, j - h);
            }
        }
        h = h / 3;
    }
}

private static void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

快速排序

重要!重要!重要!>在現場筆試和麪試中遇到好屢次了(阿里巴巴、字節跳動、騰訊、百度等)。

與冒泡排序相比,快速排序每次交換是跳躍式的,這也是快速排序速度較快的緣由。每次排序的時候選擇一個基準點,將小於基準點的所有放到基準點左邊,將大於基準點的都放到基準點右邊。這樣每次交換的時候就不會想冒泡排序同樣只交換相鄰位置的元素,交換距離變大,交換次數變小,從而提升速度。固然在最壞狀況下,仍多是相鄰兩個數進行了交換。所以快速排序的最差時間複雜度和冒泡排序是同樣的,都是O(n^2)。快速排序的平均時間複雜度爲O(nlogn)。並且,快速排序是原地排序(只須要一個很小的輔助棧),時間和空間複雜度都很優秀。用《算法(第四版)》的話來講就是:

快速排序是最快的通用排序算法。

程序怎麼寫:

  1. 定義一個基準數(初始化值設置爲左邊第一個元素)和兩個左右指針(分別爲i和j);
  2. 當i和j沒有相遇的時候,在循環中進行尋找i和j,讓j從右往左尋找比基準數小的,i從左往右尋找比基準數大的,固然須要知足條件i<j;找到了的時候,進行交換。爲何要右邊的指針先走呢?當從左邊開始時,那麼 i 所停留的那個位置確定是大於基數base的,爲了知足i<j的條件,j也會停下。那麼若是在此時進行交換,會發現交換之後並不知足基準數左邊都比基準數小,右邊都比基準數大。
  3. 當i和j相遇的時候,說明i右邊已經沒有比基準數base小的元素了,左邊沒有比基準數大的元素了,此時交換i位置上的元素arr[i]和基準數,基準數的位置就定好了。
  4. 基準數歸位
  5. 繼續快速排序處理i的左半部分和右半部分。

若是理解了,本身能寫出來最好。若是尚未徹底理解,須要進行面試,那我以爲仍是背下來吧。對,沒有看錯,就是背下來,現場筆試的時候直接默寫!!!

public static void quickSort(int[] arr, int left, int right){
    if(left > right){
        return;
    }
    int base = arr[0];//基準數
    int i = left;
    int j = right;
    //i和j沒有相遇,在循環中進行檢索
    while(i != j){
        //先由j從右往左檢索比基準數小的,找到就停下
        while(arr[j] >= base && i < j){
            j--;
        }
        //i從左往右檢索比基準數大的,找到就停下
        while(arr[i] <= base && i < j){
            i++;
        }
        //此時,找到了i和j,進行交換
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    //基準數歸位
    arr[left] = arr[i];//相遇位置的元素賦值給基準位置的元素
    arr[i] = base;//基準數賦值給相遇位置的元素
    //此時,i左邊的都比i小,右邊的都比i大;再進行快速排序
    quickSort(arr, left, i-1);
    quickSort(arr, i+1, right);
}

歸併排序

上文提到,快速排序是最快的通用排序算法。的確,在大多數狀況下,快速排序是最佳選擇。可是,有一個明顯的例外:若是穩定性很重要且空間又不是問題,歸併排序多是最好的。

歸併排序是分治思想(divide-and-conquer)的典型應用。將待排序的數組,能夠先(遞歸地)將它分紅兩半分別排序,而後將結果歸併起來。

歸併排序的優勢是可以保證將任意長度爲n的數組排序所需的時間與nlogn成正比,時間複雜度爲O(nlogn);缺點也很明顯,所需的額外空間與n成正比,空間複雜度O(n)。

/**
     * 
     * @param arr
     *            待歸併的數組
     * @param l
     *            左邊界
     * @param mid
     *            中
     * @param r
     *            右邊界
     */
public static void merge(int[] arr, int l, int mid, int r) {
    int[] aux = Arrays.copyOfRange(arr, 0, arr.length);//複製數組
    //將[l,mid]和[mid+1,r]歸併
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {
        if (i > mid)
            arr[k] = aux[j++];
        else if (j > r)
            arr[k] = aux[i++];
        else if (aux[j] < aux[i]) {
            arr[k] = aux[j++];
        } else {
            arr[k] = aux[i++];
        }

    }
}

public static void sort(int[] arr, int l, int r) {
    if (l >= r)
        return;
    int mid = (l + r) / 2;
    sort(arr, l, mid);//左邊歸併排序
    sort(arr, mid + 1, r);//右邊歸併排序
    merge(arr, l, mid, r);//將兩個有序子數組合並
}

public static void sort(int[] arr) {
    sort(arr, 0, arr.length - 1);
}

堆排序

堆排序,首要問題是要知道什麼是堆?

通俗來講,堆是一種特殊的徹底二叉樹。若是這課二叉樹全部父節點都要比子節點大,就叫大頂堆;若是全部父節點都比子節點小,就叫小頂堆。

《算法(第四版)》是這麼說的:

當一棵二叉樹的每一個節點都大於等於它的兩個節點時,它被稱爲堆有序。

二叉堆是一組可以用堆有序的徹底二叉樹排序的元素,並在數組中按照層序存儲。

也就是說:對於n個元素的待排序數組arr[0,...,n-1],當且僅當知足下列要求(0 <= i <= (n-1)/2):

array[i] >= array[2*i + 1]array[i] >= array[2*i + 2]; 稱爲大根堆;

array[i] <= array[2*i + 1]array[i] <= array[2*i + 2]; 稱爲小根堆;

堆排序的基本思想(大頂堆爲例):將待排序數組構形成一個大頂堆,此時,整個數組的最大值就是堆頂元素。將其與末尾元素進行交換,此時末尾就爲最大值。而後將剩餘n-1個元素從新構形成一個堆,這樣會獲得n個元素的次小值。如此反覆執行,就能夠獲得一個有序數組。

具體過程:

  1. 建堆;
  2. 將堆頂元素與堆底元素進行交換;
  3. 堆頂元素向下調整使其繼續保持大根堆的性質;
  4. 重複過程2,3,直到堆中只剩下堆頂元素未交換,此時也沒法交換了,排序完成。

其中建堆的時間複雜度爲O(n);

因爲堆的高度爲logn,因此將堆頂元素與堆底元素進行交換並進行排序的時間複雜度爲O(logn);

因此總體的時間複雜度爲O(nlogn)。

堆排序過程當中只有交換的時候藉助了輔助空間,空間複雜度爲O(1)。

/**
     * 
     * @param arr
     *            要進行堆排序的數組
     * @param n
     *            數組元素個數
     * @param i
     *            對節點i進行heapify操做
     */
public static void heapify(int[] arr, int n, int i) {
    if (i >= n) {
        return;
    }
    int c1 = 2 * i + 1;
    int c2 = 2 * i + 2;
    int max = i;//假設最大的爲arr[i]
    //取左右孩子中較大者的進行交換
    if (c1 < n && arr[c1] > arr[max]) {
        max = c1;
    }
    if (c2 < n && arr[c2] > arr[max]) {
        max = c2;
    }
    if (max != i) {
        swap(arr, max, i);
        heapify(arr, n, max);
    }

}

public static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

public static void buildHeap(int arr[], int n) {
    int lastNode = n - 1; 
    int parent = (lastNode - 1) / 2;
    //從最後一個節點的父節點開始,直到根節點0,反覆調整堆heapify
    for (int i = parent; i >= 0; i--) {
        heapify(arr, n, i);
    }
}

public static void heapSort(int[] arr, int n) {
    buildHeap(arr, n);
    for (int i = n - 1; i >= 0; i--) {
        swap(arr, i, 0);
        heapify(arr, i, 0);
    }
}

總結

以上的排序算法都是基於比較的排序算法。經過比較來決定元素之間的相對次序,其時間複雜度不能突破O(nlogn)的界限。

關於穩定性,若是一個排序算法可以保留數組中重複元素的相對位置,就是穩定的。怎麼記憶呢?不穩定的排序算法能夠用」快些選對「諧音來記:快速排序、希爾排序、選擇排序、堆排序。

用一張表格來做爲小結:

排序方法 平均狀況 最好狀況 最壞狀況 空間複雜度 穩定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定
插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(nlogn) ~ O(n^2) O(n1.3) O(n^2) O(1) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
快速排序 O(nlogn) O(nlogn) O(n^2) O(logn)~O(n) 不穩定

高曉鬆老師曾說:生活不僅是眼前的苟且,還有詩和遠方。而我但願遠方不遠,有處可尋,祝你們早日拿到Offer。

相關文章
相關標籤/搜索