經典算法排序

經典排序算法

0、排序算法說明

0.1 排序的定義

對一序列對象根據某個關鍵字進行排序。

0.2 術語說明

  • 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
  • 不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
  • 內排序:所有排序操作都在內存中完成;
  • 外排序:由於數據太大,因此把數據放在磁盤中,而排序通過磁盤和內存的數據傳輸才能進行;
  • 時間複雜度: 一個算法執行所耗費的時間。
  • 空間複雜度:運行完一個程序所需內存的大小。

0.3 算法總結

img

圖片名詞解釋:

  • n: 數據規模
  • k: “桶”的個數
  • In-place: 佔用常數內存,不佔用額外內存
  • Out-place: 佔用額外內存

0.4 算法分類

img

0.5 比較和非比較的區別

常見的快速排序、歸併排序、堆排序、冒泡排序等屬於比較排序在排序的最終結果裏,元素之間的次序依賴於它們之間的比較。每個數都必須和其他數進行比較,才能確定自己的位置。
冒泡排序之類的排序中,問題規模爲n,又因爲需要比較n次,所以平均時間複雜度爲O(n²)。在歸併排序、快速排序之類的排序中,問題規模通過分治法消減爲logN次,所以時間複雜度平均O(nlogn)。
比較排序的優勢是,適用於各種規模的數據,也不在乎數據的分佈,都能進行排序。可以說,比較排序適用於一切需要排序的情況。

計數排序、基數排序、桶排序則屬於非比較排序。非比較排序是通過確定每個元素之前,應該有多少個元素來排序。針對數組arr,計算arr[i]之前有多少個元素,則唯一確定了arr[i]在排序後數組中的位置。
非比較排序只要確定每個元素之前的已有的元素個數即可,所有一次遍歷即可解決。算法時間複雜度O(n)。
非比較排序時間複雜度底,但由於非比較排序需要佔用空間來確定唯一位置。所以對數據規模和數據分佈有一定的要求。

1、冒泡排序(Bubble Sort)

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

算法描述

  • 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
  • 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
  • 針對所有的元素重複以上的步驟,除了最後一個;
  • 重複步驟1~3,直到排序完成。

動圖演示

img

代碼實現

public class BubbleSort {
    public static void bubbleSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        int length = array.length;

        //外層循環控制比較輪數i
        for (int i=0; i<length; i++) {
            // 內層循環控制每一輪比較次數,每進行一輪排序都會找出一個較大值
            // (array.length - 1)防止索引越界,(array.length - 1 - i)減少比較次數
            for (int j=0; j< length-1-i; j++) {
                if(array[j] > array[j+1]) {
                    int tmp = array[j+1];
                    array[j+1] = array[j];
                    array[j] = tmp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        bubbleSort(array);
        System.out.println(Arrays.toString(array));
    }
}

算法分析

最佳情況:T(n) = O(n) 最差情況:T(n) = O(n2) 平均情況:T(n) = O(n2)

2、選擇排序(Selection Sort)

表現最穩定的排序算法之一,因爲無論什麼數據進去都是O(n2)的時間複雜度,所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間。

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

算法描述

n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。具體算法描述如下:

  • 初始狀態:無序區爲R[1…n],有序區爲空;
  • 第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別爲R[1…i-1]和R(i…n)。該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1…i]和R[i+1…n)分別變爲記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
  • n-1趟結束,數組有序化了。

動圖演示

img

代碼實現

public class SelectionSort_02 {
    public static void selectionSort(int[] array) {
        if (array == null || array.length<=1) {
            return;
        }
        for (int i=0 ; i<array.length -1; i++) {
            int minIndex=i;
            for (int j=i+1; j<array.length; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            if (i!=minIndex) {
                int tmp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = tmp;
            }
        }
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        selectionSort(array);
        System.out.println(Arrays.toString(array));
    }
}

算法分析

最佳情況:T(n) = O(n2) 最差情況:T(n) = O(n2) 平均情況:T(n) = O(n2)

3、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。

算法描述

一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:

  • 從第一個元素開始,該元素可以認爲已經被排序;
  • 取出下一個元素,在已經排序的元素序列中從後向前掃描;
  • 如果該元素(已排序)大於新元素,將該元素移到下一位置;
  • 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  • 將新元素插入到該位置後;
  • 重複步驟2~5。

動圖演示

img

代碼實現

/**
 * 插入排序
 */
public class InsertionSort_03 {
    public static void insertionSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        int insertNum;
        for (int i=1; i<array.length; i++) {
            insertNum = array[i];
            //已經排好序的元素個數
            int j = i-1;
            while(j>=0 && array[j]>insertNum) {
                //從後往前循環,將大於insertNum的數向後移一格
                array[j+1] = array[j];
                j--;
            }
            //將需要插入的元素放在要插入的位置
            array[j+1] = insertNum;
        }
    }
    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        insertionSort(array);
        System.out.println(Arrays.toString(array));
    }
}

算法分析

最佳情況:T(n) = O(n) 最壞情況:T(n) = O(n2) 平均情況:T(n) = O(n2)

4、希爾排序(Shell Sort)

希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱爲縮小增量排序,同時該算法是衝破O(n2)的第一批算法之一。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。

希爾排序是把記錄按下表的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至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 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

過程演示

img

代碼實現

//希爾排序
public class ShellSort_04 {
    public static void shellSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        int length = array.length;

        // tmp爲臨時變量,gap增量默認是長度的一半
        // 每次變爲之前的一半,直到最終數組有序
        int tmp, gap = length / 2;

        while (gap > 0) {
            for (int i = gap; i < length; i++) {
                // 將當前的數與減去增量之後位置的數進行比較,如果大於當前數,將它後移
                tmp = array[i];
                int preIndex = i - gap;

                while (preIndex >= 0 && array[preIndex] > tmp) {
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                // 如果沒有發生移動,preIndex+gap=i,無變化
                // 如果發生移動, preIndex -= gap,
                // 則preIndex+gap=preIndex-gap+gap=i-gap
                // 當前數array[i]替換array[i-gap]
                array[preIndex + gap] = tmp;
            }
            gap /= 2;
        }
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        shellSort(array);
        System.out.println(Arrays.toString(array));
    }
}

算法分析

最佳情況:T(n) = O(nlog2 n) 最壞情況:T(n) = O(nlog2 n) 平均情況:T(n) =O(nlog2n)

5、歸併排序(Merge Sort)

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

算法描述

  • 把長度爲n的輸入序列分成兩個長度爲n/2的子序列;
  • 對這兩個子序列分別採用歸併排序;
  • 將兩個排序好的子序列合併成一個最終的排序序列。

動圖演示

img

代碼實現

//歸併排序
public class MergeSort_05 {
     public static void mergeSort(int[] array) {
         if(array==null || array.length<=1) {
            return;
         }
         sort(array, 0, array.length - 1);
     }

     public static void sort(int[] array, int left, int right) {
        if (left == right) {
            return;
        }

        int mid = left + ((right-left) >> 1);
         // 對左側子序列進行遞歸排序
         sort(array, left, mid);
         // 對右側子序列進行遞歸排序
         sort(array, mid + 1, right);
         // 合併
         merge(array, left, mid, right);
     }

     public static void merge(int[] array, int left, int mid, int right) {
        int[] tmp = new int[right-left+1];
        int i=0;
        int p1=left;
        int p2=mid+1;
        // 比較左右兩部分的元素,哪個小,把那個元素填入tmp中
         while (p1 <= mid && p2 <= right) {
             tmp[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
         }
         // 上面的循環退出後,把剩餘的元素依次填入到tmp中
         // 以下兩個while只有一個會執行
         while (p1 <= mid) {
             tmp[i++] = array[p1++];
         }
         while (p2 <= right) {
             tmp[i++] = array[p2++];
         }
         // 把最終的排序的結果複製給原數組
         for (i = 0; i < tmp.length; i++) {
             array[left + i] = tmp[i];
         }
     }
     public static void main(String[] args) {
         int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
         mergeSort(array);
         System.out.println(Arrays.toString(array));
     }
}

算法分析

最佳情況:T(n) = O(n) 最差情況:T(n) = O(nlogn) 平均情況:T(n) = O(nlogn)

6、快速排序(Quick Sort)

快速排序的基本思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。

算法描述

快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述如下:

  • 從數列中挑出一個元素,稱爲 “基準”(pivot);
  • 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作;
  • 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

動圖演示

img

代碼實現

package classicalSort;

import java.util.Arrays;

//快速排序
public class QuickSort1_06 {

    /**
     * 將數組的某一段元素進行劃分,小的在左邊,大的在右邊
     */
    public static int divide(int[] array, int start, int end) {
        //每次都以最右邊的元素作爲基準值
        int base = array[end];
        //start一旦等於end,就說明左右兩個指針合併到了同一位置,可以結束此輪循環。
        while (start < end) {
            while (start < end && array[start] <= base)
                //從左邊開始遍歷,如果比基準值小,就繼續向右走
                start++;
            //上面的while循環結束時,就說明當前的array[start]的值比基準值大,應與基準值進行交換
            if (start < end) {
                //交換
                int temp = array[start];
                array[start] = array[end];
                array[end] = temp;
                //交換後,此時的那個被調換的值也同時調到了正確的位置(基準值右邊),因此右邊也要同時向前移動一位
                end--;
            }
            while (start < end && array[end] >= base)
                //從右邊開始遍歷,如果比基準值大,就繼續向左走
                end--;
            //上面的while循環結束時,就說明當前的array[end]的值比基準值小,應與基準值進行交換
            if (start < end) {
                //交換
                int temp = array[start];
                array[start] = array[end];
                array[end] = temp;
                //交換後,此時的那個被調換的值也同時調到了正確的位置(基準值左邊),因此左邊也要同時向後移動一位
                start++;
            }

        }
        //這裏返回start或者end皆可,此時的start和end都爲基準值所在的位置
        return end;
    }

    //排序
    public static void quickSort(int[] a, int start, int end){
        if(start > end){
            //如果只有一個元素,就不用再排下去了
            return;
        }
        else{
            //如果不止一個元素,繼續劃分兩邊遞歸排序下去
            int partition = divide(a, start, end);
            quickSort(a, start, partition-1);
            quickSort(a, partition+1, end);
        }

    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        quickSort(array,0,14);

        System.out.println(Arrays.toString(array));
    }
}
package classicalSort;

import java.util.Arrays;

//快速排序
//左右兩邊沒找到一次就互換,有點浪費時間,不如當左邊大於右邊小於時再互換
public class QuickSort2_06 {

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


    private static void quickSort(int[] array, int left, int right) {
        if (array == null || left >= right || array.length <= 1) {
            return;
        }
        int mid = partition(array, left, right);
        quickSort(array, left, mid);
        quickSort(array, mid + 1, right);
    }

    private static int partition(int[] array, int left, int right) {
        int temp = array[left];
        while (right > left) {
            while (temp <= array[right] && left < right) {
                --right;
            }

            if (left < right) {
                array[left] = array[right];
                ++left;
            }

            while (temp >= array[left] && left < right) {
                ++left;
            }
            if (left < right) {
                array[right] = array[left];
                --right;
            }
        }
        array[left] = temp;
        return left;
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        quickSort(array);
        System.out.println(Arrays.toString(array));
    }
}

算法分析

最佳情況:T(n) = O(nlogn) 最差情況:T(n) = O(n2) 平均情況:T(n) = O(nlogn)

7、堆排序(Heap Sort)

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

什麼是堆?

堆是一種非線性結構,(本篇隨筆主要分析堆的數組實現)可以把堆看作一個數組,也可以被看作一個完全二叉樹,通俗來講堆其實就是利用完全二叉樹的結構來維護的一維數組

按照堆的特點可以把堆分爲大頂堆小頂堆

大頂堆:每個結點的值都大於等於其左右孩子結點的值

小頂堆:每個結點的值都小於等於其左右孩子結點的值

堆的概念

如果有一個關鍵碼的集合K={K(0),K(1),K(2)……K(n-1)},把它的所有元素按完全二叉樹的順序存儲方式存儲在一個一維數組中,並滿足:K(i)<=k(2* i+1)且 K(i)<=k(2* i+2)(K(i)>=k(2* i+1)且 K(i)>=k(2*i+2))i=0,1,2……則稱爲小堆(或大堆)。

img
堆存儲在下標爲0開始的數組中,因此在堆中給定下標爲i的結點時:

如果i=0,結點i是根結點,沒有雙親結點;否則結點i的雙親結點爲(i-1)/2。
如果2* i+1<=n-1,則結點i的左孩子爲2i+1,否則結點i無左孩子。
如果2
i+2<=n-1,則結點i的右孩子爲2*i+2,否則結點i無右孩子。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oRIswSnX-1592742610823)(C:/Users/luche/AppData/Roaming/Typora/typora-user-images/image-20200616220227901.png)]

算法描述

  • 將初始待排序關鍵字序列(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,則整個排序過程完成。

動圖演示

img

代碼實現

/**
 * 堆排序
 * 堆頂array[0]
 * 左子節點array[2i+1]
 * 右子節點array[2i+2]
 */
public class HeapSort_07 {
    public static void heapSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        int length = array.length;

        // 1.構建大頂堆
        // 從下至上,從右至左進行調整,則第一個非葉子結點爲2i+1=length-1;即i=length/2-1
        for (int i = length / 2 - 1; i >= 0; i--) {
            //從第一個非葉子結點從下至上,從右至左調整結構
            adjustHeap(array, i, length);
        }
        //2.調整堆結構+交換堆頂元素與末尾元素
        for (int j = length - 1; j > 0; j--) {
            //將堆頂元素與末尾元素進行交換
            swap(array, 0, j);
            //重新對堆進行調整
            adjustHeap(array, 0, j);
        }

    }

    //調整大頂堆(僅是調整過程,建立在大頂堆已構建的基礎上)
    private static void adjustHeap(int[] array, int i, int length) {
        //先取出當前元素i
        int temp = array[i];
        //從i結點的左子結點開始,也就是2i+1處開始
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            //如果左子結點小於右子結點,k指向右子結點
            if (k + 1 < length && array[k] < array[k + 1]) {
                k++;
            }
            //如果子節點大於父節點,將子節點值賦給父節點(不用進行交換)
            if (array[k] > temp) {
                array[i] = array[k];
                i = k;
            } else {
                break;
            }
        }
        //將temp值放到最終的位置
        array[i] = temp;
    }

    //交換元素位置
    private static void swap(int[] array, int a, int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        heapSort(array);

        System.out.println(Arrays.toString(array));
    }
}

算法分析

最佳情況:T(n) = O(nlogn) 最差情況:T(n) = O(nlogn) 平均情況:T(n) = O(nlogn)

8、計數排序(Counting Sort)

計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素是待排序數組A中值等於i的元素的個數。然後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。

算法描述

  • 找出待排序的數組中最大和最小的元素;
  • 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項;
  • 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
  • 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。

動圖演示

img

代碼實現

public class CountingSort_08 {
    public static void countingSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        int length = array.length;
        int max = array[0];
        int min = array[0];
        for (int i=0; i<length; i++) {
            if (max < array[i]) {
                max = array[i];
            }

            if (min > array[i]) {
                min = array[i];
            }
        }
        // 最大最小元素之間範圍[min, max]的長度
        int offset = max - min + 1;
        // 1. 計算頻率,在需要的數組長度上額外加1
        int[] count = new int[offset + 1];
        for (int i=0; i<length; i++) {
            // 使用加1後的索引,有重複的該位置就自增
            count[array[i] - min + 1]++;
        }
        // 2. 頻率 -> 元素的開始索引
        for (int i=0; i<offset; i++) {
            count[i+1] += count[i];
        }
        // 3. 元素按照開始索引分類,用到一個和待排數組一樣大臨時數組存放數據
        int[] aux = new int[length];
        for (int i = 0; i < length; i++) {
            // 填充一個數據後,自增,以便相同的數據可以填到下一個空位
            aux[count[array[i] - min]++] = array[i];
        }
        // 4. 數據回寫
        for (int i = 0; i < length; i++) {
            array[i] = aux[i];
        }
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        countingSort(array);

        System.out.println(Arrays.toString(array));
    }
}

算法分析

當輸入的元素是n 個0到k之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。

最佳情況:T(n) = O(n+k) 最差情況:T(n) = O(n+k) 平均情況:T(n) = O(n+k)

9、桶排序(Bucket Sort)

桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。

桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)

算法描述

  • 人爲設置一個BucketSize,作爲每個桶所能放置多少個不同數值(例如當BucketSize==5時,該桶可以存放{1,2,3,4,5}這幾種數字,但是容量不限,即可以存放100個3);
  • 遍歷輸入數據,並且把數據一個一個放到對應的桶裏去;
  • 對每個不是空的桶進行排序,可以使用其它排序方法,也可以遞歸使用桶排序;
  • 從不是空的桶裏把排好序的數據拼接起來。

注意,如果遞歸使用桶排序爲各個桶排序,則當桶數量爲1時要手動減小BucketSize增加下一循環桶的數量,否則會陷入死循環,導致內存溢出。

圖片演示

img

代碼實現

package classicalSort;

import java.util.*;

//桶排序
public class BucketSort_09 {

    public static void bucketSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 建立桶,個數和待排序數組長度一樣
        int length = array.length;

        LinkedList<Integer>[] bucket = (LinkedList<Integer>[]) new LinkedList[length];

        // 待排序數組中的最大值
        int maxValue = Arrays.stream(array).max().getAsInt();
        // 根據每個元素的值,分配到對應範圍的桶中
        for (int i = 0; i < array.length; i++) {
            int index = toBucketIndex(array[i], maxValue, length);
            // 沒有桶才建立桶(延時)
            if (bucket[index] == null) {
                bucket[index] = new LinkedList<>();
            }
            // 有桶直接使用
            bucket[index].add(array[i]);
        }

        // 對每個非空的桶排序,排序後順便存入臨時的List,則list中已經有序)
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < length; i++) {
            if (bucket[i] != null) {
                Collections.sort(bucket[i]);
                temp.addAll(bucket[i]);
            }
        }

        // 將temp中的數據寫入原數組
        for (int i = 0; i < length; i++) {
            array[i] = temp.get(i);
        }
    }

    //映射函數,將值轉換爲應存放到的桶數組的索引
    private static int toBucketIndex(int value, int maxValue, int length) {
        return (value * length) / (maxValue + 1);
    }

    public static void main(String[] args) {
        int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        bucketSort(array);

        System.out.println(Arrays.toString(array));
    }
}

算法分析

桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,因爲其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。

最佳情況:T(n) = O(n+k) 最差情況:T(n) = O(n+k) 平均情況:T(n) = O(n2)

10、基數排序(Radix Sort)

基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,複雜度爲O(kn),爲數組長度,k爲數組中的數的最大的位數;

基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以是穩定的。

算法描述

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

動圖演示

img

代碼實現

//基數排序
public class RadixSort_10 {

    public static void radixSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        int length = array.length;

        // 每位數字範圍0~9,基爲10
        int radix = 10;
        int[] aux = new int[length];
        int[] count = new int[radix + 1];
        // 以關鍵字來排序的輪數,由位數最多的數字決定,其餘位數少的數字在比較高位時,自動用0進行比較
        // 將數字轉換成字符串,字符串的長度就是數字的位數,字符串最長的那個數字也擁有最多的位數
        int x = Arrays.stream(array).map(s -> String.valueOf(s).length()).max().getAsInt();

        // 共需要d輪計數排序, 從d = 0開始,說明是從個位開始比較,符合從右到左的順序
        for (int d = 0; d < x; d++) {
            // 1. 計算頻率,在需要的數組長度上額外加1
            for (int i = 0; i < length; i++) {
                // 使用加1後的索引,有重複的該位置就自增
                count[digitAt(array[i], d) + 1]++;
            }
            // 2. 頻率 -> 元素的開始索引
            for (int i = 0; i < radix; i++) {
                count[i + 1] += count[i];
            }

            // 3. 元素按照開始索引分類,用到一個和待排數組一樣大臨時數組存放數據
            for (int i = 0; i < length; i++) {
                // 填充一個數據後,自增,以便相同的數據可以填到下一個空位
                aux[count[digitAt(array[i], d)]++] = array[i];
            }
            // 4. 數據回寫
            for (int i = 0; i < length; i++) {
                array[i] = aux[i];
            }
            // 重置count[],以便下一輪統計使用
            for (int i = 0; i < count.length; i++) {
                count[i] = 0;
            }

        }
    }

    // 根據d,獲取某個值的個位、十位、百位等,d = 0取出個位,d = 1取出十位,
    // 以此類推。對於不存在的高位,用0補

    private static int digitAt(int value, int d) {
        return (value / (int) Math.pow(10, d)) % 10;
    }
}
   }
        // 2. 頻率 -> 元素的開始索引
        for (int i = 0; i < radix; i++) {
            count[i + 1] += count[i];
        }

        // 3. 元素按照開始索引分類,用到一個和待排數組一樣大臨時數組存放數據
        for (int i = 0; i < length; i++) {
            // 填充一個數據後,自增,以便相同的數據可以填到下一個空位
            aux[count[digitAt(array[i], d)]++] = array[i];
        }
        // 4. 數據回寫
        for (int i = 0; i < length; i++) {
            array[i] = aux[i];
        }
        // 重置count[],以便下一輪統計使用
        for (int i = 0; i < count.length; i++) {
            count[i] = 0;
        }

    }
}

// 根據d,獲取某個值的個位、十位、百位等,d = 0取出個位,d = 1取出十位,
// 以此類推。對於不存在的高位,用0補

private static int digitAt(int value, int d) {
    return (value / (int) Math.pow(10, d)) % 10;
}

}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章