天天看點

常見排序算法學習總結

一、選擇排序(Selection Sort)

無論什麼資料進去都是O(n2)的時間複雜度

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

算法描述

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

  1. 初始狀态:無序區為R[1…n],有序區為空;
  2. 第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個的新無序區;
  3. n-1趟結束,數組有序化了。

代碼實作

  /**
     * 選擇排序
     */
    public static int[] selectionSort(int[] array) {
        if (array.length == 0)
            return 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; //将最小數的索引儲存
            }
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
        return array;
    }
           

複雜度分析

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

二、插入排序(Insertion Sort)

插入排序(Insertion-Sort) 的算法描述是一種簡單直覺的排序算法。它的工作原理是通過建構有序序列,對于未排序資料,在已排序序列中從後向前掃描,找到相應位置并插入。插入排序在實作上,通常采用in-place排序(即隻需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反複把已排序元素逐漸向後挪位,為最新元素提供插入空間。

算法描述

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

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

代碼實作

  /**
     * 插入排序,尋找元素arr[i]合适的插入位置
     */
    public static int[] insertionSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            // 寫法1
//            for( int j = i ; j > 0 ; j -- )
//                if( arr[j] < arr[j-1] ) )
//                    swap( arr, j , j-1 );
//                else
//                    break;

            // 寫法2
//            for( int j = i; j > 0 && arr[j] < arr[j-1] ; j--)
//                swap(arr, j, j-1);

            // 寫法3
            int e = arr[i];
            int j;// j儲存元素e應該插入的位置
            for(j = i ; j > 0 && arr[j-1] > e ; j--) {
                arr[j] = arr[j - 1];
            }
            arr[j] = e;
        }
        return arr;
    }
    
    private static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }
           

複雜度分析

最佳情況:T(n) = O(n)

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

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

三、冒泡排序

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

算法描述

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

代碼實作

1    /**
 2      * 冒泡排序
 6      */
 7     public static int[] bubbleSort(int[] array) {
 8         if (array.length == 0)
 9             return array;
10         for (int i = 0; i < array.length; i++)
11             for (int j = 0; j < array.length - 1 - i; j++)
12                 if (array[j + 1] < array[j]) {
13                     int temp = array[j + 1];
14                     array[j + 1] = array[j];
15                     array[j] = temp;
16                 }
17         return array;
18     }
           

複雜度分析

最佳情況:T(n) = O(n)

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

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

四、希爾排序(Shell Sort)

它與插入排序的不同之處在于,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。希爾排序是把記錄按下表的一定增量分組,對每組使用直接插入排序算法排序;随着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,算法便終止。

算法描述

希爾排序的基本步驟,在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2,(n/2)/2…1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但其實這個增量序列不是最優的。此處我們做示例使用希爾增量。

先将整個待排序的記錄序列分割成為若幹子序列分别進行直接插入排序,具體算法描述:

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

代碼實作

/**
     * 希爾排序
     */
    public static int[] ShellSort(int[] array) {
        int len = array.length;
        int temp, gap = len / 2;
        while (gap > 0) {
            for (int i = gap; i < len; i++) {
                temp = array[i];
                int preIndex = i - gap;
                while (preIndex >= 0 && array[preIndex] > temp) {
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                array[preIndex + gap] = temp;
            }
            gap /= 2;
        }
        return array;
    }
           

複雜度分析

最佳情況:T(n) = O(nlog2 n)

最壞情況:T(n) = O(nlog2 n)

平均情況:T(n) =O(nlog2n)

五、歸并排序(Merge Sort)

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

算法分析

  1. 把長度為n的輸入序列分成兩個長度為n/2的子序列;
  2. 對這兩個子序列分别采用歸并排序;
  3. 将兩個排序好的子序列合并成一個最終的排序序列。

代碼實作

/**
     * 歸并排序
     */
    // 将arr[l...mid]和arr[mid+1...r]兩部分進行歸并
    private static void merge(int[] arr, int l, int mid, int r) {

        int[] aux = Arrays.copyOfRange(arr, l, r+1);
//        int[] aux = new int[r-l+1];
//        for (int i = l; i <= r ; i++) {
//            aux[i - l] = arr[i];
//        }

        // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
        int i = l, j = mid+1;
        for( int k = l ; k <= r; k ++ ){

            if( i > mid ){  // 如果左半部分元素已經全部處理完畢
                arr[k] = aux[j-l]; j ++;
            }
            else if( j > r ){   // 如果右半部分元素已經全部處理完畢
                arr[k] = aux[i-l]; i ++;
            }
            else if( aux[i-l] < aux[j-l] ){  // 左半部分所指元素 < 右半部分所指元素
                arr[k] = aux[i-l]; i ++;
            }
            else{  // 左半部分所指元素 >= 右半部分所指元素
                arr[k] = aux[j-l]; j ++;
            }
        }
    }

    // 遞歸使用歸并排序,對arr[l...r]的範圍進行排序
    private 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){

        int n = arr.length;
        sort(arr, 0, n-1);
    }
           

複雜度分析

最佳情況:T(n) = O(n)

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

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

六、快速排序(Quick Sort)

通過一趟排序将待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分别對這兩部分記錄繼續進行排序,以達到整個序列有序。

算法描述

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

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

代碼實作

/**
     * 快速排序方法
     */
    public static int[] QuickSort(int[] array, int start, int end) {
        if (array.length < 1 || start < 0 || end >= array.length || start > end) return null;
        int smallIndex = partition(array, start, end);
        if (smallIndex > start)
            QuickSort(array, start, smallIndex - 1);
        if (smallIndex < end)
            QuickSort(array, smallIndex + 1, end);
        return array;
    }
    /**
     * 快速排序算法——partition
     */
    public static int partition(int[] array, int start, int end) {
        int pivot = (int) (start + Math.random() * (end - start + 1));
        int smallIndex = start - 1;
        swap(array, pivot, end);
        for (int i = start; i <= end; i++)
            if (array[i] <= array[end]) {
                smallIndex++;
                if (i > smallIndex)
                    swap(array, i, smallIndex);
            }
        return smallIndex;
    }

    /**
     * 交換數組内兩個元素
     */
    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
           

複雜度分析

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

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

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

七、堆排序(Heap Sort)

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

算法描述

  1. 将初始待排序關鍵字序列(R1,R2….Rn)建構成大頂堆,此堆為初始的無序區;
  2. 将堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
  3. 由于交換後新的堆頂R[1]可能違反堆的性質,是以需要對目前無序區(R1,R2,……Rn-1)調整為新堆,然後再次将R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。

代碼實作

//聲明全局變量,用于記錄數組array的長度;
    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;
    }
    /**
     * 建立最大堆
     */
    public static void buildMaxHeap(int[] array) {
        //從最後一個非葉子節點開始向上構造最大堆
        //for循環這樣寫會更好一點:i的左子樹和右子樹分别2i+1和2(i+1)
        for (int i = (len/2- 1); i >= 0; i--) {
            adjustHeap(array, i);
        }
    }
    /**
     * 調整使之成為最大堆
     */
    public 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);
        }
    }
           

複雜度分析

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

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

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

八、計數排序(Counting Sort)

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

算法描述

找出待排序的數組中最大和最小的元素;

  1. 統計數組中每個值為i的元素出現的次數,存入數組C的第i項;
  2. 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
  3. 反向填充目标數組:将每個元素i放在新數組的第C(i)項,每放一個元素就将C(i)減去1。

代碼實作

/**
     * 計數排序
     */
    public static int[] CountingSort(int[] array) {
        if (array.length == 0) return array;
        int bias, min = array[0], max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max)
                max = array[i];
            if (array[i] < min)
                min = array[i];
        }
        bias = 0 - min;
        int[] bucket = new int[max - min + 1];
        Arrays.fill(bucket, 0);
        for (int i = 0; i < array.length; i++) {
            bucket[array[i] + bias]++;
        }
        int index = 0, i = 0;
        while (index < array.length) {
            if (bucket[i] != 0) {
                array[index] = i - bias;
                bucket[i]--;
                index++;
            } else
                i++;
        }
        return array;
    }
           

複雜度分析

當輸入的元素是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)

桶排序是計數排序的更新版。它利用了函數的映射關系,高效與否的關鍵就在于這個映射函數的确定。假設輸入資料服從均勻分布,将資料分到有限數量的桶裡,每個桶再分别排序(有可能再使用别的排序算法或是以遞歸方式繼續使用桶排序進行排

算法分析

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

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

代碼實作

/**
     * 桶排序
     */
    public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
        if (array == null || array.size() < 2)
            return array;
        int max = array.get(0), min = array.get(0);
        // 找到最大值最小值
        for (int i = 0; i < array.size(); i++) {
            if (array.get(i) > max)
                max = array.get(i);
            if (array.get(i) < min)
                min = array.get(i);
        }
        int bucketCount = (max - min) / bucketSize + 1;
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        ArrayList<Integer> resultArr = new ArrayList<>();
        for (int i = 0; i < bucketCount; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }
        for (int i = 0; i < array.size(); i++) {
            bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
        }
        for (int i = 0; i < bucketCount; i++) {
            if (bucketSize == 1) { // 如果帶排序數組中有重複數字時
                for (int j = 0; j < bucketArr.get(i).size(); j++)
                    resultArr.add(bucketArr.get(i).get(j));
            } else {
                if (bucketCount == 1)
                    bucketSize--;
                ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
                for (int j = 0; j < temp.size(); j++)
                    resultArr.add(temp.get(j));
            }
        }
        return resultArr;
    }
           

複雜度分析

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

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

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

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

十、基數排序(Radix Sort)

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

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

算法描述

  1. 取得數組中的最大數,并取得位數;
  2. arr為原始數組,從最低位開始取每個位組成radix數組;
  3. 對radix進行計數排序(利用計數排序适用于小範圍數的特點);

代碼實作

/**
     * 基數排序
     */
    public static int[] RadixSort(int[] array) {
        if (array == null || array.length < 2)
            return array;
        // 1.先算出最大數的位數;
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            max = Math.max(max, array[i]);
        }
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        int mod = 10, div = 1;
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
        for (int i = 0; i < 10; i++)
            bucketList.add(new ArrayList<Integer>());
        for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
            for (int j = 0; j < array.length; j++) {
                int num = (array[j] % mod) / div;
                bucketList.get(num).add(array[j]);
            }
            int index = 0;
            for (int j = 0; j < bucketList.size(); j++) {
                for (int k = 0; k < bucketList.get(j).size(); k++)
                    array[index++] = bucketList.get(j).get(k);
                bucketList.get(j).clear();
            }
        }
        return array;
    }
           

複雜度分析

最佳情況:T(n) = O(n * k)

最差情況:T(n) = O(n * k)

平均情況:T(n) = O(n * k)

基數排序有兩種方法:

MSD 從高位開始進行排序

LSD 從低位開始進行排序

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

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

基數排序: 根據鍵值的每位數字來配置設定桶

計數排序: 每個桶隻存儲單一鍵值

桶排序: 每個桶存儲一定範圍的數值