Java實現十大排序演算法,配合動態圖片

我愛2b哥發表於2020-04-16

Java實現排序演算法

其中不理解時間複雜度得可以看一下我的時間複雜度的文章,有助於理解

十大排序演算法

  • 1、氣泡排序
  • 2、選擇排序
  • 3、插入排序
  • 4、希爾排序
  • 5、歸併排序
  • 6、快速排序
  • 7、堆排序
  • 8、計數排序
  • 9、桶排序
  • 10、基數排序

程式碼git地址:https://github.com/gaoyeming/sort-algorithm.git

排序演算法說明

  • 1,排序的定義
    對一序列物件或者陣列根據某個關鍵字進行排序

  • 2、術語說明
    穩定: 如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
    不穩定: 如果a原本在b的前面,而a=b,排序之後a可能會出現在b的前面;
    內排序: 所有排序操作都在記憶體中完成;
    外排序: 由於資料太大,因此把資料放在磁碟中,而排序通過磁碟和記憶體的資料傳輸才能進行;
    時間複雜度: 一個演算法執行所耗費的時間。
    空間複雜度: 執行完一個程式所需記憶體的大小。

  • 3、演算法總結
    在這裡插入圖片描述
    圖片名詞解釋:

    • n:資料規模
    • k:”桶“的個數
    • In-place:佔用常數記憶體,不佔用額外記憶體
    • Out-place:佔用額外記憶體
  • 4、演算法分類
    在這裡插入圖片描述

    • 非線性時間比較類排序 :通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱為非線性時間比較類排序。
    • 線性時間非比較類排序 :不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間執行,因此稱為線性時間非比較類排序。
  • 5、比較和非比較的區別

    • 常見的快速排序、歸併排序、堆排序、氣泡排序等屬於比較排序。在排序的最終結果裡,元素之間的次序依賴於它們之間的比較。每個數都必須和其他數進行比較,才能確定自己的位置。
    • 在氣泡排序之類的排序中,問題規模為n,又因為需要比較n次,所以平均的時間複雜度為O(n^2)。在歸併排序、快速排序之類的排序中,問題規模通過分治法消減為logN次,所以時間複雜度平均為O(nlogn)。比較排序的優勢是,適用於各種規模的資料,也不在乎資料的分佈,都能進行排序。可以說,比較排序適用於一切需要排序的情況。
    • 計數排序,基數排序,桶排序則屬於非比較排序。非比較排序是通過確定每個元素之前,應該有多少個元素來排序。針對陣列arr,計算arr[i]之前有多少個元素,則唯一確定了arr[i]在排序後陣列中的位置。非比較排序只要確定每個元素之前的已有的元素個數即可,所有一次遍歷即可解決。演算法時間複雜度O(n).
      非比較排序時間複雜度低,但由於非比較排序需要佔用空間來確定唯一位置。所以對資料規模和資料分佈有一定的要求。

下面開始真正實現排序及理解對應思想

一、氣泡排序

百度百科解釋: 氣泡排序是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢”浮“到數列的頂端。

  • 1.1、演算法描述
    • 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個;
    • 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
    • 針對所有的元素重複以上的步驟,除了最後一個;
    • 重複以上步驟,直到排序完成
  • 1.2、動圖演示
    在這裡插入圖片描述
  • 1.3、程式碼實現
/**
*氣泡排序
*@param arry 需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] bubbleSort(int[] arry) {
    for (int i = 0; i < arry.length; i++) {
        for (int j = i + 1; j < arry.length; j++) {
            if (arry[j] < arry[i]) {
                int tmp = arry[j];
                arry[j] = arry[i];
                arry[i] = tmp;
            }
        }
    }
    return arry;
}
  • 1.4、時間複雜度
    • 最佳情況:T(n)=O(n)
    • 最差情況:T(n)=O(n^2)
    • 平均情況:T(n)=O(n^2)

二、選擇排序

百度百科解釋: 選擇排序(Selection-sort)是一種簡單直觀的排序演算法。它的工作原理:首先在未排序列中找到最小或者最大的元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小或者最大元素,然後存放到已排序的序列的末尾。以此類推,直到所有元素均排序完畢。

  • 2.1、演算法描述
    n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序的結果。具體演算法描述如下:
    • 初始狀態,無序區為R[1…n],有序區為空
    • 第一趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別為R[1…i-1]和R[i…n]。該趟排序從當前無序區中選出關鍵字最小的記錄R[k],將它與無序區的第一個記錄R交換,使得R[1…i]和R[i+1…n]分別變為記錄個數增加1個的新有序區和記錄個數減少一個的新無序區
    • n-1趟結束,陣列有序化了。
  • 2.2、動態演示
    在這裡插入圖片描述
  • 2.3、程式碼實現
/**
*選擇排序
*@param array 需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] selectionSort(int[] array) {
    for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                int tmp = array[minIndex];
                array[minIndex] = array[i];
                array[i] = tmp;
            }
        }
    return array;
}
  • 2.4、時間複雜度
    • 最佳情況:T(n)=O(n^2)
    • 最差情況T(n)=O(n^2)
    • 平均情況T(n)=O(n^2)

三、插入排序

百度百科解釋: 插入排序,一般也被稱為直接插入排序。對於少量元素的排序,它是一個有效的演算法。插入排序是一種最簡單的排序方法,它的基本思想是將一個記錄插入到已經排好序的有序表中,從而一個新的、記錄數增1的有序表。在其實現過程使用雙層迴圈,外層迴圈對除了第一個元素之外的所有元素,內層迴圈對當前元素前面有序表進行待插入位置查詢,並進行移動。

  • 3.1、演算法描述
    一般來說,插入排序都採用in-place在陣列上實現。具體演算法描述如下:
    • 從第一個元素開始,該元素可以認為已經被排序;
    • 取出下一個元素,在已經排序的元素序列中從後向前掃描;
    • 如果該元素(已排序)大於新元素,將該元素移到下一位置;
    • 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
    • 將新元素插入到該位置後;
    • 重複步驟2~5
  • 3.2、動圖演示
    在這裡插入圖片描述
  • 3.3、程式碼實現
/**
*插入排序
*@param array 需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] insertionSort(int[] array) {
    for (int i = 1; i < array.length; i++) {
        int current = array[i];
        int preIndex = i - 1;
        while (preIndex >= 0 && current < array[preIndex] ) {
            array[preIndex + 1] = array[preIndex];
            preIndex--;
        }
        array[preIndex + 1] = current;
    }

    return array;
}
  • 3.4、時間複雜度
    • 最佳情況:T(n)=O(n)
    • 最壞情況T(n)=O(n^2)
    • 平均情況T(n)=O(n^2)

四、希爾排序

百度百科解釋: 希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序演算法的一種更高效的改進版本。希爾排序是非穩定排序演算法。該方法因D.L.Shell於1959年提出而得名。
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止。

  • 4.1、演算法描述
    我們來看下希爾排序的基本步驟,在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2,(n/2)/2…1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但起始這個增量序列不是最優的。此處我們做示例使用希爾增量。
    先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,具體演算法描述:
    • 選擇一個增量序列t1, t2, …, tk,其中ti>tj,tk=1;
    • 按增量序列個數k,對序列進行k趟排序
    • 每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m的子序列,分別對各字表進行直接插入排序。僅增量因子為1時,整個序列作為一個表來處理,表長度即為整個序列的長度
  • 4.2、過程演示
    在這裡插入圖片描述
  • 4.3、程式碼實現
/**
*希爾排序
*@param array 需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] shellSort(int[] array) {
    int length = array.length;
    int gap = length / 2;
    while (gap > 0) {
        for (int i = gap; i < length; i++) {
            int current = array[i];
            int preIndex = i - gap;
            while ( preIndex >= 0 && current < array[preIndex]) {
                array[preIndex + gap] = array[preIndex];
                preIndex -= gap;
            }
            array[preIndex + gap] = current;
        }
        gap /= 2;
    }
    return array;
}
  • 4.4、時間複雜度
    • 最佳情況:T(n) = O(n)
    • 最壞情況:T(n) = O(n^2)

五、歸併排序

百度百科解釋: 歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。歸併排序是一種穩定的排序方法。

  • 5.1、演算法描述
    • 把長度為n的輸入序列分成兩個長度為n/2的子序列;
    • 對這兩個子序列分別採用歸併排序;
    • 將兩個排序好的子序列合併成一個最終的排序序列
  • 5.2、動圖演示
    在這裡插入圖片描述
  • 5.3、程式碼實現
/**
*歸併排序
*@param array 需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] mergeSort(int[] array) {
    if (array.length < 2) {
        return array;
    }
    int mid = array.length / 2;
    int[] left = Arrays.copyOfRange(array, 0, mid);
    int[] right = Arrays.copyOfRange(array, mid, array.length);
    return merge(mergeSort(left), mergeSort(right));
}
/**
*歸併排序——將兩段排序好的陣列結合成一個排序陣列
*@param left
*@param right
*@return
*/
private static int[] merge(int[] left, int[] right) {
    int[] result = new int[left.length + right.length];
    for (int index = 0, i = 0, j = 0; index < result.length; index++) {
        if (i >= left.length) {
            result[index] = right[j++];
        } else if (j >= right.length) {
            result[index] = left[i++];
        } else if (left[i] > right[j]) {
            result[index] = right[j++];
        } else {
            result[index] = left[i++];
        }
    }

    return result;
}
  • 5.4、時間複雜度
    • 最佳情況:T(n) = O(nlogn)
    • 最差情況:T(n) = O(nlogn)
    • 平均情況:T(n) = O(nlogn)

六、快速排序

百度百科解釋: 快速排序(Quicksort)是對氣泡排序的一種改進。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

  • 6.1、演算法描述
    快速排序使用分治法來把一個串(list)分為兩個子串(sub-list)。具體演算法描述如下:
    • 從數列中挑出一個元素,稱為“基準”(pivot);
    • 重新排序數列,所有元素比基準值小的擺放在基準的前面,所有元素比基準值大的擺放在基準的後面(相同的數可以放到任意一邊)。在這個區分退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
    • 遞迴(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序
  • 6.2、動圖演示
    在這裡插入圖片描述
  • 6.3、程式碼實現
/**
*快速排序
*@param array      需要排序的int陣列
*@param startIndex 開始的索引位置
*@param endIndex   結束的索引位置
*@return 排序後的陣列
*/
public static int[] quickSort(int[] array, int startIndex, int endIndex) {
    if (array.length < 1 || startIndex < 0 || endIndex >= array.length || startIndex > endIndex) {
        return null;
    }
    int pivotIndex = partition(array, startIndex, endIndex);
    if (pivotIndex > startIndex) {
        quickSort(array, startIndex, pivotIndex - 1);
    }
    if (pivotIndex < endIndex) {
        quickSort(array, pivotIndex + 1, endIndex);
    }
    return array;
}
/**
*快速排序演算法——partition
*@param array      分割槽的陣列
*@param startIndex 開始的索引位置
*@param endIndex   結束的索引位置
*@return 返回分割槽過後的陣列
*/
private static int partition(int[] array, int startIndex, int endIndex) {
    //隨機獲取一個基準(需要保證在陣列內)
    int pivotIndex = (int) (startIndex + Math.random() * (endIndex - startIndex + 1));
    int i = startIndex;
    while (i <= endIndex) {
        if (i <= pivotIndex) {
            if (array[i] > array[pivotIndex]) {
                swap(array, i, pivotIndex);
                pivotIndex = i;
            }
        } else {
            if (array[i] < array[pivotIndex]) {
                swap(array, i, pivotIndex);
                int tmp = pivotIndex;
                pivotIndex = i;
                i = tmp;
            }
        }
        i++;
    }

    return pivotIndex;
}
/**
*交換陣列內兩個元素
*@param array 源元素陣列
*@param i     交換位置i
*@param j     交換位置j
*/
private static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}
  • 6.4、演算法分析
    • 最佳情況:T(n) = O(nlogn)
    • 最差情況:T(n) = O(n2)
    • 平均情況:T(n) = O(nlogn)

七、堆排序

首先了解二叉樹:
在這裡插入圖片描述
上圖就是一個完全二叉樹,其特點在於:
1,從作為第一層根開始,除了最後一層之外,第N層的元素個數都必須是2的N-1次方;第一層一個,第二層兩個,第三層四個,以此類推
2,而最後一行的元素,都是緊貼在左邊,換句話說,每一行的元素都是從最左邊開始安放,兩個元素之間不能有空閒;具備了這兩個特點的樹,就是一顆完全二叉樹。
那麼,完全二叉樹與堆有什麼關係呢?
我們假設有一顆完全二叉樹,在滿足作為二叉樹的基礎上,對於任意一個擁有父節點的子節點,其數值均不小於父節點的值;這樣層層遞推,就是根節點的值最小,這樣的數稱為小根堆
同理,又有一顆完全二叉樹,對於任意一個子節點來說,均不大於其父節點的值,如此遞推,就是根節點的值是最大的,這樣的數稱為大跟堆。

如上圖,左邊就是大根堆;右邊則是小根堆,這裡必須要注意一點,只要求子節點與父節點的關係,兩個節點的大小關係與其左右位置沒有任何關係。
備註: 一般升序採用大頂堆,降序採用小頂堆

百度百科解釋: 堆排序(英語:Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

  • 7.1、演算法描述
    • 將初始待排序關鍵字序列(R1, R2, … Rn)構建成大頂堆,此堆為初始的無序區;
    • 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1, R2, … Rn-1)和新的有序區(Rn),且滿足R[1, 2, … n-1]<=R[n];
    • 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1, R2, … Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1, R2 , … Rn-2)和新的有序區(Rn-1, Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成
  • 7.2、簡單例項
    給定一個列表array=[4,6,8,5,9],對其進行堆排序(使用大根堆)。
    步驟一、構造初始堆。將給定的無序序列構造成一個大頂堆
    • 1、假設給定無序序列結構如下:
      在這裡插入圖片描述
    • 2、此時我們從最後一個非葉子節點開始(葉節點自然不用調整,第一個非葉子結點arr.length/2-1=5/2-1=1,也就是下面的6結點,從左至右,從上至下進行調整)
      注意:我們把6和9比較交換之後,必須考量9這個節點對於其子節點會不會產生任何影響,因為其是葉子節點,所以不加考慮;但是,一定要熟練這種思維,寫程式碼的時候就比較容易理解為什麼會出現一次非常重要的交換了。
      在這裡插入圖片描述
    • 3、找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換
      在真正程式碼實現中,這時候4和9交換過後,必須考慮9所在的這個節點位置,因為其上的值變了,必須判斷對其的兩個子節點是否造成了影響。這麼說不合適,實際上就是判斷其作為根節點的那棵樹,是都還滿足大根堆的原則,每一次交換,都必須要迴圈把子數部分判別清楚。
      在這裡插入圖片描述
    • 這時交換導致了子根[4,5,6]出現混亂,繼續調整,[4,5,6]中6最大,交換4和6.
      牢記上面說的規則,每次交換都要把改變了的那個節點所在的樹重新判斷一下,這裡就用上了,4和9交換了,變動了的那棵子樹就必須重新調整,一直調整到符合大根堆的規則為止。
      在這裡插入圖片描述
    • 此時,我們就將一個無序序列構造成了一個大頂堆。

步驟二、將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素,如此反覆進行交換、重建、交換。

  • 1、將對頂元素9和末尾元素4進行交換
    這裡,必須說明一下,所謂的交換,實際上就是把最大值從樹裡面拿掉了,剩下參與到排序的樹,其實只有總節點的個數減去拿掉節點個數了,所以下圖用的是虛線。
    在這裡插入圖片描述
  • 2、重新調整結構,使其滿足大根堆的定義
    在這裡插入圖片描述
  • 3、再將堆頂元素8與末尾元素5進行交換,得到第二大元素8
    在這裡插入圖片描述
  • 4、後續過程,繼續調整,交換,如此反覆進行,最終使得整個序列有序
    在這裡插入圖片描述
  • 7.3、動圖演示
    在這裡插入圖片描述
  • 7.3、程式碼實現
/**
*堆排序
*@param array      需要排序的int陣列
*@return 排序後的陣列
*/
private static int len;
public static int[] heapSort(int[] array){
    len = array.length;
    if (len < 1) {
        return array;
    }
    //1.構建一個最大堆
    buildMaxHeap(array);
    //2.迴圈將堆首位(最大值)與末位交換,然後在重新調整最大堆
    while (len > 0) {
        swap(array, 0, len - 1);
        len--;
        adjustHeap(array, 0);
    }
    return array;
}
/**
*建立最大堆
*@param array 需要構建的陣列
*/
private static void buildMaxHeap(int[] array) {
    //從最後一個非葉子節點開始向上構造最大堆
    for (int i = (len-1) / 2; i >= 0; i--) {
        adjustHeap(array ,i);
    }
}
/**
*調整使之成為最大堆
*@param array
*@param i
*/
private static void adjustHeap(int[] array, int i) {
    int maxIndex = i;
    //如果有左子樹,且左子樹大於父節點,則將最大指標指向左子樹
    if (i * 2 < len && array[i * 2] > array[maxIndex]) {
        maxIndex = i * 2;
    }
    //如果有右子樹,且右子樹大於父節點,則將最大指標指向右子樹
    if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex]) {
        maxIndex = i * 2 + 1;
    }
    //如果父節點不是最大值,則將父節點與最大值交換,並且遞迴調整與父節點交換的位置。
    if (maxIndex != i) {
        swap(array, maxIndex, i);
        adjustHeap(array, maxIndex);
    }
}
/**
*交換陣列內兩個元素
*@param array 源元素陣列
*@param i     交換位置i
*@param j     交換位置j
*/
private static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}
  • 7.4、演算法分析
    • 最佳情況:T(n) = O(nlogn)
    • 最差情況:T(n) = O(nlogn)
    • 平均情況:T(n) = O(nlogn)

八、計數排序

百度百科解釋: 計數排序是一個非基於比較的排序演算法,該演算法於1954年由 Harold H. Seward 提出。它的優勢在於在對一定範圍內的整數排序時,它的複雜度為Ο(n+k)(其中k是整數的範圍),快於任何比較排序演算法。當然這是一種犧牲空間換取時間的做法,而且當O(k)>O(nlog(n))的時候其效率反而不如基於比較的排序(基於比較的排序的時間複雜度在理論上的下限是O(nlog(n)), 如歸併排序,堆排序)

  • 8.1、演算法描述
    • 找出待排序的陣列中最大和最小的元素;
    • 統計陣列中每個值為i的元素出現的次數,存入陣列C的第i項;
    • 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
    • 反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1
  • 8.2、動圖演示
    在這裡插入圖片描述
  • 8.3、程式碼實現
/**
*計數排序(僅支援整數排序)
*@param array 需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] countingSort(int[] array) {
    //首先取出array陣列中最大,最小的兩個元素
    int maxElement = array[0];
    int minElement = array[0];
    for (int anArray : array) {
        if (anArray > maxElement) {
            maxElement = anArray;
        }
        if (anArray < minElement) {
            minElement = anArray;
        }
    }
    //定義一個額外空間陣列;長度為最大元素與最小元素之間存在的整數個數
    int[] extraArray = new int[maxElement - minElement + 1];
    Arrays.fill(extraArray, 0);
    for (int anArray : array) {
        extraArray[anArray - minElement]++;
    }
    //反向填充目標陣列
    int index = 0, i = 0;
    while (index < array.length) {
        if (extraArray[i] != 0) {
            array[index] = i + minElement;
            extraArray[i]--;
            index++;
        } else {
            i++;
        }
    }
    return array;
}
  • 8.4、演算法分析
    當輸入的元素是n 個0到k之間的整數時,它的執行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。由於用來計數的陣列C的長度取決於待排序陣列中資料的範圍(等於待排序陣列的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的陣列,需要大量時間和記憶體。
    • 最佳情況:T(n) = O(n+k)
    • 最差情況:T(n) = O(n+k)
    • 平均情況:T(n) = O(n+k)

九、桶排序

百度百科解釋: 桶排序 (Bucket sort)或所謂的箱排序,是一個排序演算法,工作的原理是將陣列分到有限數量的桶子裡。每個桶子再個別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間(Θ(n))。但桶排序並不是 比較排序,他不受到 O(n log n) 下限的影響。

  • 9.1、演算法描述
    • 人為設定一個BucketSize,作為每個桶所能放置多少個不同數值(例如當BucketSize==5時,該桶可以存放{1,2,3,4,5}這幾種數字,但是容量不限,及可以存放100個3)
    • 遍歷輸入資料,並且把資料一個一個放到對應的桶裡去;
    • 對每個不是空的桶進行排序,可以使用其它排序方法,也可以遞迴使用桶排序;
    • 從不是空的桶裡把排好序的資料拼接起來
      注意:如果遞迴使用桶排序為各個桶排序,則當桶的數量為1時要手動減小BucketSize增加下一迴圈桶的數量,否則會陷入死迴圈,導致記憶體溢位。
  • 9.2、圖片展示
    在這裡插入圖片描述
  • 9.3、程式碼實現
/**
*桶排序
*桶排序中:無序陣列有個要求,就是成員隸屬於固定(有限的)的區間,如範圍為0-9
*@param array      需要排序的int陣列
*@param bucketSize 桶的大小
*@return 排序後的陣列
*/
public static ArrayList<int[]> bucketSort(int[] array, int bucketSize) {
    //首先取出array陣列中最大,最小的兩個元素
    int maxElement = array[0];
    int minElement = array[0];
    for (int anArray : array) {
        if (anArray > maxElement) {
            maxElement = anArray;
        }
        if (anArray < minElement) {
            minElement = anArray;
        }
    }
    //計算出桶的數量
    int bucketCount = (maxElement - minElement) / bucketSize + 1;
    //定義桶
    List<List<Integer>> buckets = new ArrayList<>(bucketCount);
    for (int i = 0; i < bucketCount; i++) {
        buckets.add(new ArrayList<>());
    }
    //裝載桶
    for (int anArray : array) {
        //根據值計算出所在的桶索引
        int bucketIndex = anArray / bucketSize;
        buckets.get(bucketIndex).add(anArray);
    }
    //對每個桶利用快排(其他方式排序也是可以的)進行排序
    ArrayList<int[]> sortedList = new ArrayList<>(bucketCount);
    for (List<Integer> bucket : buckets) {
        int[] bucketArray = new int[bucket.size()];
        for (int i = 0; i < bucket.size(); i++) {
            bucketArray[i] = bucket.get(i);
        }
        sortedList.add(quickSort(bucketArray, 0, bucketArray.length - 1));
    }

    return sortedList;
}
  • 9.4、演算法分析
    桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間資料進行排序的時間複雜度,因為其它部分的時間複雜度都為O(n)。很顯然,桶劃分的越小,各個桶之間的資料越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
    • 最佳情況:T(n) = O(n+k)
    • 最差情況:T(n) = O(n+k)
    • 平均情況:T(n) = O(n2)

十、基數排序

百度百科解釋: 基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度為O (nlog®m),其中r為所採取的基數,而m為堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。

  • 10.1、演算法描述
    • 取得陣列中的最大數,並取得位數;
    • arr為原始陣列,從最低位開始取每個位組成radix陣列;
    • 對radix進行計數排序(利用計數排序使用於小範圍數的特點)
  • 10.2、動圖展示
    在這裡插入圖片描述
  • 10.3、程式碼實現
/**
*基數排序
*@param array      需要排序的int陣列
*@return 排序後的陣列
*/
public static int[] radixSort(int[] array) {
    //先獲取最大數
    int maxElement = array[0];
    for (int anArray : array) {
        if (anArray > maxElement) {
            maxElement = anArray;
        }
    }
    //計算其對應的位數
    int maxDigit = 0;
    while (maxElement != 0) {
        maxElement /= 10;
        maxDigit++;
    }
    //定義長度為10的桶radix
    List<List<Integer>> radixList = new ArrayList<>(10);
    for (int i = 0; i < 10; i++) {
        radixList.add(new ArrayList<>());
    }
    //從最低位開始取每個位組成radix陣列;
    int mod = 10, div = 1;
    for(int i=0;i<maxDigit;i++){
        for(int anArray : array){
            int num = (anArray % mod) / div;
            radixList.get(num).add(anArray);
        }

        int index = 0;
        for (int j = 0; j < radixList.size(); j++) {
            for (int k = 0; k < radixList.get(j).size(); k++) {
                array[index++] = radixList.get(j).get(k);
            }
            radixList.get(j).clear();
        }
        mod *= 10; div *= 10;
    }

    return array;
}
  • 10.4、演算法分析
    • 最佳情況:T(n) = O(n * k)
    • 最差情況:T(n) = O(n * k)
    • 平均情況:T(n) = O(n * k)
    • 基數排序有兩種方法:
      MSD 從高位開始進行排序 LSD 從低位開始進行排序
      基數排序 vs 計數排序 vs 桶排序

最後對於時間複雜度不理解的可以看一下我的對於時間複雜度理解的文章。如果你不想複製程式碼,可以直接從我的github上下載:github實現十大演算法程式碼

相關文章