Java實現八大排序算法

本文對常見的排序算法進行了總結。php

常見排序算法以下:前端

  1. 直接插入排序
  2. 希爾排序
  3. 簡單選擇排序
  4. 堆排序
  5. 冒泡排序
  6. 快速排序
  7. 歸併排序
  8. 基數排序

它們都屬於內部排序,也就是隻考慮數據量較小僅須要使用內存的排序算法,他們之間關係以下:
\[ \begin{cases}內部排序 \begin{cases}插入排序\begin{cases}直接插入排序\\希爾排序\end{cases}\\選擇排序\begin{cases}簡單選擇排序\\堆排序\end{cases}\\交換排序\begin{cases}冒泡排序\\快速排序 \end{cases}\\歸併排序\\ 基數排序\end{cases}\\外部排序 \end{cases} \]java

\[ \left\{\begin{matrix} 內部排序\\ 外部排序 \end{matrix}\right. \]git

穩定與非穩定:github

若是一個排序算法可以保留數組中重複元素的相對位置則能夠被稱爲是 穩定 的。反之,則是 非穩定 的。面試

直接插入排序

基本思想

一般人們整理橋牌的方法是一張一張的來,將每一張牌插入到其餘已經有序的牌中的適當位置。在計算機的實現中,爲了要給插入的元素騰出空間,咱們須要將其他全部元素在插入以前都向右移動一位。算法

算法描述

通常來講,插入排序都採用in-place在數組上實現。具體算法描述以下:segmentfault

  1. 從第一個元素開始,該元素能夠認爲已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 若是該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟2~5

動態效果以下:api

注意
若是 比較操做 的代價比 交換操做 大的話,能夠採用二分查找法來減小 比較操做 的數目。該算法能夠認爲是 插入排序 的一個變種,稱爲二分查找插入排序數組

代碼實現

/**
 * 經過交換進行插入排序,借鑑冒泡排序
 *
 * @param a
 */
public static void sort(int[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        for (int j = i + 1; j > 0; j--) {
            if (a[j] < a[j - 1]) {
                int temp = a[j];
                a[j] = a[j - 1];
                a[j - 1] = temp;
            }
        }
    }
}

/**
 * 經過將較大的元素都向右移動而不老是交換兩個元素
 *
 * @param a
 */
public static void sort2(int[] a) {
    for (int i = 1; i < a.length; i++) {
        int num = a[i];
        int j;
        for (j = i; j > 0 && num < a[j - 1]; j--) {
            a[j] = a[j - 1];
        }
        a[j] = num;
    }
}

複雜度分析

直接插入排序複雜度以下:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(n²) O(n²) O(n²) O(1)

比較與總結

插入排序所需的時間取決於輸入元素的初始順序。例如,對一個很大且其中的元素已經有序(或接近有序)的數組進行排序將會比隨機順序的數組或是逆序數組進行排序要快得多。

希爾排序

希爾排序,也稱 遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是 非穩定排序算法

希爾排序是基於插入排序的如下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操做時,效率高,便可以達到線性排序的效率
  • 但插入排序通常來講是低效的,由於插入排序每次只能將數據移動一

希爾排序是先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。

基本思想

將待排序數組按照步長gap進行分組,而後將每組的元素利用直接插入排序的方法進行排序;每次再將gap折半減少,循環上述操做;當gap=1時,利用直接插入,完成排序。

能夠看到步長的選擇是希爾排序的重要部分。只要最終步長爲1任何步長序列均可以工做。通常來講最簡單的步長取值是初次取數組長度的一半爲增量,以後每次再減半,直到增量爲1。更好的步長序列取值能夠參考維基百科

算法描述

  1. 選擇一個增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列個數 k,對序列進行 k 趟排序;
  3. 每趟排序,根據對應的增量 ti,將待排序列分割成若干長度爲 m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲 1 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。

效果以下:

代碼實現

下面參考《算法》中給出的步長選擇策略,《算法》中給出的解釋是

下面代碼中遞增序列的計算和使用都很簡單,和複雜遞增序列的性能接近。當能夠證實複雜的序列在最壞狀況下的性能要好於咱們所使用的遞增序列。更加優秀的遞增序列有待咱們去發現。

public static void sort(int[] a) {
    int length = a.length;
    int h = 1;
    while (h < length / 3) h = 3 * h + 1;
    for (; h >= 1; h /= 3) {
        for (int i = 0; i < a.length - h; i += h) {
            for (int j = i + h; j > 0; j -= h) {
                if (a[j] < a[j - h]) {
                    int temp = a[j];
                    a[j] = a[j - h];
                    a[j - h] = temp;
                }
            }
        }
    }
}

複雜度分析

如下是希爾排序複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(nlog2 n) O(nlog2 n) O(nlog2 n) O(1)

總結與思考

希爾排序更高效的緣由是它權衡了子數組的規模和有序性。排序之初,各個子數組都很短,排序以後子數組都是部分有序的,這兩種狀況都很適合插入排序。

簡單選擇排序

基本思想

選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工做原理以下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。

選擇排序的主要優勢與數據移動有關。若是某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,所以對 n個元素的表進行排序總共進行至多 n-1 次交換。在全部的徹底依靠交換去移動元素的排序方法中,選擇排序屬於很是好的一種。

算法描述

  1. 從未排序序列中,找到關鍵字最小的元素
  2. 若是最小元素不是未排序序列的第一個元素,將其和未排序序列第一個元素互換
  3. 重複一、2步,直到排序結束。

動圖效果以下所示:

代碼實現

public static void sort(int[] a) {
    for (int i = 0; i < a.length; i++) {
        int min = i;
        //選出以後待排序中值最小的位置
        for (int j = i + 1; j < a.length; j++) {
            if (a[j] < a[min]) {
                min = j;
            }
        }
        //最小值不等於當前值時進行交換
        if (min != i) {
            int temp = a[i];
            a[i] = a[min];
            a[min] = temp;
        }
    }
}

複雜度分析

如下是選擇排序複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(n²) O(n²) O(n²) O(1)

總結與思考

選擇排序的簡單和直觀名副其實,這也造就了它」出了名的慢性子」,不管是哪一種狀況,哪怕原數組已排序完成,它也將花費將近n²/2次遍從來確認一遍。即使是這樣,它的排序結果也仍是不穩定的。 惟一值得高興的是,它並不耗費額外的內存空間。

堆排序

1991年的計算機先驅獎得到者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同發明了著名的堆排序算法(Heap Sort).

堆的定義以下:\(n\)個元素的序列{k1,k2,..,kn}
當且僅當知足下關係時,稱之爲堆。

把此序列對應的二維數組當作一個徹底二叉樹。那麼堆的含義就是:徹底二叉樹中任何一個非葉子節點的值均不大於(或不小於)其左,右孩子節點的值。 由上述性質可知大頂堆的堆頂的關鍵字確定是全部關鍵字中最大的,小頂堆的堆頂的關鍵字是全部關鍵字中最小的。所以咱們可以使用大頂堆進行升序排序, 使用小頂堆進行降序排序。

基本思想

此處以大頂堆爲例,堆排序的過程就是將待排序的序列構形成一個堆,選出堆中最大的移走,再把剩餘的元素調整成堆,找出最大的再移走,重複直至有序。

算法描述

  1. 先將初始序列\(K[1..n]\)建成一個大頂堆, 那麼此時第一個元素\(K_1\)最大, 此堆爲初始的無序區.
  2. 再將關鍵字最大的記錄\(K_1\) (即堆頂, 第一個元素)和無序區的最後一個記錄 \(K_n\) 交換, 由此獲得新的無序區\(K[1..n-1]\)和有序區\(K[n]\), 且知足\(K[1..n-1].keys \leqslant K[n].key\)
  3. 交換\(K_1\)\(K_n\) 後, 堆頂可能違反堆性質, 所以需將\(K[1..n-1]\)調整爲堆. 而後重複步驟2, 直到無序區只有一個元素時中止。

動圖效果以下所示:

堆排序過程

代碼實現

從算法描述來看,堆排序須要兩個過程,一是創建堆,二是堆頂與堆的最後一個元素交換位置。因此堆排序有兩個函數組成。一是建堆函數,二是反覆調用建堆函數以選擇出剩餘未排元素中最大的數來實現排序的函數。

總結起來就是定義瞭如下幾種操做:

  • 最大堆調整(Max_Heapify):將堆的末端子節點做調整,使得子節點永遠小於父節點
  • 建立最大堆(Build_Max_Heap):將堆全部數據從新排序
  • 堆排序(HeapSort):移除位在第一個數據的根節點,並作最大堆調整的遞歸運算

對於堆節點的訪問:

  • 父節點i的左子節點在位置:(2*i+1);
  • 父節點i的右子節點在位置:(2*i+2);
  • 子節點i的父節點在位置:floor((i-1)/2);
/**
 * @param a
 */
public static void sort(int[] a) {

    for (int i = a.length - 1; i > 0; i--) {
        max_heapify(a, i);

        //堆頂元素(第一個元素)與Kn交換
        int temp = a[0];
        a[0] = a[i];
        a[i] = temp;
    }
}

/***
 *
 *  將數組堆化
 *  i = 第一個非葉子節點。
 *  從第一個非葉子節點開始便可。無需從最後一個葉子節點開始。
 *  葉子節點能夠看做已符合堆要求的節點,根節點就是它本身且本身如下值爲最大。
 *
 * @param a
 * @param n
 */
public static void max_heapify(int[] a, int n) {
    int child;
    for (int i = (n - 1) / 2; i >= 0; i--) {
        //左子節點位置
        child = 2 * i + 1;
        //右子節點存在且大於左子節點,child變成右子節點
        if (child != n && a[child] < a[child + 1]) {
            child++;
        }
        //交換父節點與左右子節點中的最大值
        if (a[i] < a[child]) {
            int temp = a[i];
            a[i] = a[child];
            a[child] = temp;
        }
    }
}

複雜度分析

  1. 創建堆的過程, 從length/2 一直處理到0, 時間複雜度爲O(n);
  2. 調整堆的過程是沿着堆的父子節點進行調整, 執行次數爲堆的深度, 時間複雜度爲O(lgn);
  3. 堆排序的過程由n次第2步完成, 時間複雜度爲O(nlgn).
平均時間複雜度 最好狀況 最壞狀況 空間複雜度
\(O(n \log_{2}n)\) \(O(n \log_{2}n)\) \(O(n \log_{2}n)\) \(O(1)\)

總結與思考

因爲堆排序中初始化堆的過程比較次數較多, 所以它不太適用於小序列。 同時因爲屢次任意下標相互交換位置, 相同元素之間本來相對的順序被破壞了, 所以, 它是不穩定的排序。

冒泡排序

我想對於它每一個學過C語言的都會了解,這多是不少人接觸的第一個排序算法。

基本思想

冒泡排序(Bubble Sort)是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。

算法描述

冒泡排序算法的運做以下:

  1. 比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。這步作完後,最後的元素會是最大的數。
  3. 針對全部的元素重複以上的步驟,除了最後一個。
  4. 持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。

代碼實現

public static void sort(int[] a) {
    //外層循環控制比較的次數
    for (int i = 0; i < a.length - 1; i++) {
      //內層循環控制到達位置
        for (int j = 0; j < a.length - i - 1; j++) {
            //前面的元素比後面大就交換
            if (a[j] > a[j + 1]) {
                int temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}

複雜度分析

如下是冒泡排序算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(n²) O(n) O(n²) O(1)

冒泡排序是最容易實現的排序, 最壞的狀況是每次都須要交換, 共需遍歷並交換將近n²/2次, 時間複雜度爲O(n²). 最佳的狀況是內循環遍歷一次後發現排序是對的, 所以退出循環, 時間複雜度爲O(n). 平均來說, 時間複雜度爲O(n²). 因爲冒泡排序中只有緩存的temp變量須要內存空間, 所以空間複雜度爲常量O(1).

總結與思考

因爲冒泡排序只在相鄰元素大小不符合要求時才調換他們的位置, 它並不改變相同元素之間的相對順序, 所以它是穩定的排序算法。

快速排序

快速排序是由東尼·霍爾所發展的一種排序算法。在平均情況下,排序 n 個項目要 Ο(nlogn) 次比較。在最壞情況下則須要 Ο(n2) 次比較,但這種情況並不常見。事實上,快速排序一般明顯比其餘 Ο(nlogn) 算法更快,由於它的內部循環(inner loop)能夠在大部分的架構上頗有效率地被實現出來。

基本思想

快速排序的基本思想:挖坑填數+分治法

快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。

快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。

快速排序的名字起的是簡單粗暴,由於一聽到這個名字你就知道它存在的意義,就是快,並且效率高!它是處理大數據最快的排序算法之一了。雖然 Worst Case 的時間複雜度達到了 O(n²),可是人家就是優秀,在大多數狀況下都比平均時間複雜度爲 O(n logn) 的排序算法表現要更好。

算法描述

快速排序使用分治策略來把一個序列(list)分爲兩個子序列(sub-lists)。步驟爲:

  1. 從數列中挑出一個元素,稱爲"基準"(pivot)。
  2. 從新排序數列,全部比基準值小的元素擺放在基準前面,全部比基準值大的元素擺在基準後面(相同的數能夠到任一邊)。在這個分區結束以後,該基準就處於數列的中間位置。這個稱爲分區(partition)操做。
  3. 遞歸地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。

遞歸到最底部時,數列的大小是零或一,也就是已經排序好了。這個算法必定會結束,由於在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

代碼實現

用僞代碼描述以下:

  1. i = L; j = R; 將基準數挖出造成第一個坑a[i]
  2. j--,由後向前找比它小的數,找到後挖出此數填前一個坑a[i]中。
  3. i++,由前向後找比它大的數,找到後也挖出此數填到前一個坑a[j]中。
  4. 再重複執行2,3二步,直到i==j,將基準數填入a[i]
public static void sort(int[] a, int low, int high) {
    //已經排完
    if (low >= high) {
        return;
    }
    int left = low;
    int right = high;

    //保存基準值
    int pivot = a[left];
    while (left < right) {
        //從後向前找到比基準小的元素
        while (left < right && a[right] >= pivot)
            right--;
        a[left] = a[right];
        //從前日後找到比基準大的元素
        while (left < right && a[left] <= pivot)
            left++;
        a[right] = a[left];
    }
    // 放置基準值,準備分治遞歸快排
    a[left] = pivot;
    sort(a, low, left - 1);
    sort(a, left + 1, high);
}

上面是遞歸版的快速排序:經過把基準插入到合適的位置來實現分治,並遞歸地對分治後的兩個劃分繼續快排。那麼非遞歸版的快排如何實現呢?

由於 遞歸的本質是棧 ,因此咱們非遞歸實現的過程當中,能夠藉助棧來保存中間變量就能夠實現非遞歸了。在這裏中間變量也就是經過Pritation函數劃分區間以後分紅左右兩部分的首尾指針,只須要保存這兩部分的首尾指針便可。

public static void sortByStack(int[] a) {
    Stack<Integer> stack = new Stack<Integer>();

    //初始狀態的左右指針入棧
    stack.push(0);
    stack.push(a.length - 1);
    while (!stack.isEmpty()) {
        //出棧進行劃分
        int high = stack.pop();
        int low = stack.pop();

        int pivotIndex = partition(a, low, high);

        //保存中間變量
        if (pivotIndex > low) {
            stack.push(low);
            stack.push(pivotIndex - 1);
        }
        if (pivotIndex < high && pivotIndex >= 0) {
            stack.push(pivotIndex + 1);
            stack.push(high);
        }
    }
}

private static int partition(int[] a, int low, int high) {
    if (low >= high) return -1;
    int left = low;
    int right = high;
    //保存基準的值
    int pivot = a[left];
    while (left < right) {
        //從後向前找到比基準小的元素,插入到基準位置中
        while (left < right && a[right] >= pivot) {
            right--;
        }
        a[left] = a[right];
        //從前日後找到比基準大的元素
        while (left < right && a[left] <= pivot) {
            left++;
        }
        a[right] = a[left];
    }
    //放置基準值,準備分治遞歸快排
    a[left] = pivot;
    return left;
}

算法改進

切換到插入排序

和大多數遞歸排序算法同樣,改進快速排序性能的一個簡單方法基於如下兩點:

  • 對於小數組,快速排序比插入排序慢
  • 由於遞歸,快速排序的sort()方法在小數組中葉會調用本身

所以,在排序小數組時應該切換到插入排序。

三者取中法

快速排序是一般被認爲在同數量級(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按關鍵碼有序或基本有序時,快排序反而蛻化爲冒泡排序。爲改進之,一般以「三者取中法」來選取基準記錄,即將排序區間的兩個端點與中點三個記錄關鍵碼居中的調整爲支點記錄。

三向快速排序

實際應用中常常會出現含有大量重複元素的數組。例如,一個元素所有重複的子數組就不須要繼續排序了,但咱們的算法還會繼續將它切分爲更小的數組。在有大量重複元素的狀況下,快速排序的遞歸性會使元素所有重複的子數組常常出現,這就有很大的改進潛力,經當前實現的線性對數級的性能提升到線性級別。

算法描述

  • 在lt以前的(lo~lt-1)都小於中間值
  • 在gt以前的(gt+1~hi)都大於中間值
  • 在lt~i-1的都等於中間值
  • 在i~gt的都還不肯定(最終i會大於gt,即不肯定的將不復存在)

代碼實現

public static void sortThreeWay(int[] a, int lo, int hi) {
    if (lo >= hi) {
        return;
    }
    int v = a[lo], lt = lo, i = lo + 1, gt = hi;
    while (i <= gt) {
        if (a[i] < v) {
            swap(a, i++, lt++);
        } else if (a[i] > v) {
            swap(a, i, gt--);
        } else {
            i++;
        }
    }
    sortThreeWay(a, lo, lt - 1);
    sortThreeWay(a, gt + 1, hi);
}

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

複雜度分析

如下是快速排序算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(nlog₂n) O(nlog₂n) O(n²) O(1)(原地分區遞歸版)

歸併排序

歸併排序是創建在歸併操做上的一種有效的排序算法,1945年由約翰·馮·諾伊曼首次提出。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用,且各層分治遞歸能夠同時進行。

基本思想

歸併排序算法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列。

這個圖頗有歸納性,來自維基

算法描述

歸併排序可經過兩種方式實現:

  • 自上而下的遞歸
  • 自下而上的迭代

遞歸法(假設序列共有n個元素):

  1. 將序列每相鄰兩個數字進行歸併操做,造成 floor(n/2)個序列,排序後每一個序列包含兩個元素;
  2. 將上述序列再次歸併,造成 floor(n/4)個序列,每一個序列包含四個元素;
  3. 重複步驟2,直到全部元素排序完畢。

迭代法

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
  4. 重複步驟3直到某一指針到達序列尾
  5. 將另外一序列剩下的全部元素直接複製到合併序列尾

代碼實現

歸併排序其實要作兩件事:

  • 分解:將序列每次折半拆分
  • 合併:將劃分後的序列段兩兩排序合併

所以,歸併排序實際上就是兩個操做,拆分+合併

下面是遞歸的方法:

public class Merge {

    //歸併所需的輔助數組
    private static int[] aux;

    public static void sort(int[] a) {
        //一次性分配空間
        aux = new int[a.length];
        sort(a, 0, a.length - 1);
    }

    public static void sort(int[] a, int low, int high) {
        if (low >= high) {
            return;
        }
        int mid = (low + high) / 2;
        //將左半邊排序
        sort(a, low, mid);
        //將右半邊排序
        sort(a, mid + 1, high);
        merge(a, low, mid, high);
    }

    /**
     * 該方法先將全部元素複製到aux[]中,而後在歸併會a[]中。方法咋歸併時(第二個for循環)
     * 進行了4個條件判斷:
     * - 左半邊用盡(取右半邊的元素)
     * - 右半邊用盡(取左半邊的元素)
     * - 右半邊的當前元素小於左半邊的當前元素(取右半邊的元素)
     * - 右半邊的當前元素大於等於左半邊的當前元素(取左半邊的元素)
     * @param a
     * @param low
     * @param mid
     * @param high
     */
    public static void merge(int[] a, int low, int mid, int high) {
        //將a[low..mid]和a[mid+1..high]歸併
        int i = low, j = mid + 1;
        for (int k = low; k <= high; k++) {
            aux[k] = a[k];
        }

        for (int k = low; k <= high; k++) {
            if (i > mid) {
                a[k] = aux[j++];
            } else if (j > high) {
                a[k] = aux[i++];
            } else if (aux[j] < aux[i]) {
                a[k] = aux[j++];
            } else {
                a[k] = aux[i++];
            }
        }
    }

}

複雜度分析

如下是歸併排序算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n)

從效率上看,歸併排序可算是排序算法中的」佼佼者」. 假設數組長度爲n,那麼拆分數組共需logn,, 又每步都是一個普通的合併子數組的過程, 時間複雜度爲O(n), 故其綜合時間複雜度爲O(nlogn)。另外一方面, 歸併排序屢次遞歸過程當中拆分的子數組須要保存在內存空間, 其空間複雜度爲O(n)。

總結與思考

歸併排序最吸引人的性質是它可以保證將任意長度爲N的數組排序所需時間和NlogN成正比,它的主要缺點則是他所需的額外空間和N成正比。

基數排序

基數排序的發明能夠追溯到1887年赫爾曼·何樂禮在打孔卡片製表機(Tabulation Machine), 排序器每次只能看到一個列。它是基於元素值的每一個位上的字符來排序的。 對於數字而言就是分別基於個位,十位, 百位或千位等等數字來排序。

基數排序(Radix sort)是一種非比較型整數排序算法,其原理是將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序也不是隻能使用於整數。

基本思想

它是這樣實現的:將全部待比較數值(正整數)統一爲一樣的數位長度,數位較短的數前面補零。而後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成之後,數列就變成一個有序序列。

基數排序按照優先從高位或低位來排序有兩種實現方案:

  • MSD(Most significant digital) 從最左側高位開始進行排序。先按k1排序分組, 同一組中記錄, 關鍵碼k1相等, 再對各組按k2排序分紅子組, 以後, 對後面的關鍵碼繼續這樣的排序分組, 直到按最次位關鍵碼kd對各子組排序後. 再將各組鏈接起來, 便獲得一個有序序列。MSD方式適用於位數多的序列

  • LSD (Least significant digital)從最右側低位開始進行排序。先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便獲得一個有序序列。LSD方式適用於位數少的序列

基數排序LSD動圖演示

算法描述

咱們以LSD爲例,從最低位開始,具體算法描述以下:

  1. 取得數組中的最大數,並取得位數;
  2. arr爲原始數組,從最低位開始取每一個位組成radix數組;
  3. 對radix進行計數排序(利用計數排序適用於小範圍數的特色);

代碼實現

基數排序:經過序列中各個元素的值,對排序的N個元素進行若干趟的「分配」與「收集」來實現排序。

  • 分配:咱們將L[i]中的元素取出,首先肯定其個位上的數字,根據該數字分配到與之序號相同的桶中

  • 收集:當序列中全部的元素都分配到對應的桶中,再按照順序依次將桶中的元素收集造成新的一個待排序列L[]。對新造成的序列L[]重複執行分配和收集元素中的十位、百位...直到分配完該序列中的最高位,則排序結束

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

    //取得數組中的最大數,並取得位數
    int max = 0;
    for (int i = 0; i < arr.length; i++) {
        if (max < arr[i]) {
            max = arr[i];
        }
    }
    int maxDigit = 1;
    while (max / 10 > 0) {
        maxDigit++;
        max = max / 10;
    }
    //申請一個桶空間
    int[][] buckets = new int[10][arr.length];
    int base = 10;

    //從低位到高位,對每一位遍歷,將全部元素分配到桶中
    for (int i = 0; i < maxDigit; i++) {
        int[] bktLen = new int[10];        //存儲各個桶中存儲元素的數量

        //分配:將全部元素分配到桶中
        for (int j = 0; j < arr.length; j++) {
            int whichBucket = (arr[j] % base) / (base / 10);
            buckets[whichBucket][bktLen[whichBucket]] = arr[j];
            bktLen[whichBucket]++;
        }

        //收集:將不一樣桶裏數據挨個撈出來,爲下一輪高位排序作準備,因爲靠近桶底的元素排名靠前,所以從桶底先撈
        int k = 0;
        for (int b = 0; b < buckets.length; b++) {
            for (int p = 0; p < bktLen[b]; p++) {
                arr[k++] = buckets[b][p];
            }
        }
        System.out.println("Sorting: " + Arrays.toString(arr));
        base *= 10;
    }
}

複雜度分析

如下是基數排序算法複雜度,其中k爲最大數的位數:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(d*(n+r)) O(d*(n+r)) O(d*(n+r)) O(n+r)

其中,d 爲位數,r 爲基數,n 爲原數組個數。在基數排序中,由於沒有比較操做,因此在複雜上,最好的狀況與最壞的狀況在時間上是一致的,均爲 O(d*(n + r))

總結和思考

基數排序更適合用於對時間, 字符串等這些 總體權值未知的數據 進行排序。

基數排序不改變相同元素之間的相對順序,所以它是穩定的排序算法。

基數排序 vs 計數排序 vs 桶排序

這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差別:

  1. 基數排序:根據鍵值的每位數字來分配桶
  2. 計數排序:每一個桶只存儲單一鍵值
  3. 桶排序:每一個桶存儲必定範圍的數值

八大排序算法總結

各類排序性能對好比下:

排序類型 平均狀況 最好狀況 最壞狀況 輔助空間 穩定性
冒泡排序 O(n²) O(n) O(n²) O(1) 穩定
選擇排序 O(n²) O(n²) O(n²) O(1) 不穩定
直接插入排序 O(n²) O(n) O(n²) O(1) 穩定
折半插入排序 O(n²) O(n) O(n²) O(1) 穩定
希爾排序 O(n^1.3) O(nlogn) O(n²) O(1) 不穩定
歸併排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n) 穩定
快速排序 O(nlog₂n) O(nlog₂n) O(n²) O(nlog₂n) 不穩定
堆排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(1) 不穩定
計數排序 O(n+k) O(n+k) O(n+k) O(k) 穩定
桶排序 O(n+k) O(n+k) O(n²) O(n+k) (不)穩定
基數排序 O(d(n+k)) O(d(n+k)) O(d(n+kd)) O(n+kd) 穩定

從時間複雜度來講:

  1. 平方階O(n²)排序:各種簡單排序:直接插入、直接選擇和冒泡排序
  2. 線性對數階O(nlog₂n)排序:快速排序、堆排序和歸併排序
  3. O(n1+§))排序,§是介於0和1之間的常數:希爾排序
  4. 線性階O(n)排序:基數排序,此外還有桶、箱排序

論是否有序的影響:

  • 當原表有序或基本有序時,直接插入排序和冒泡排序將大大減小比較次數和移動記錄的次數,時間複雜度可降至O(n);
  • 而快速排序則相反,當原表基本有序時,將蛻化爲冒泡排序,時間複雜度提升爲O(n2);
  • 原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。

代碼地址

參考資料

  1. 《算法》第四版
  2. 維基百科
  3. 八大排序算法總結與java實現
  4. 前端面試必備——十大經典排序算法
  5. 必須知道的八大種排序算法【java實現】