插入、冒泡、歸併、堆排序、快排總結

一個暱稱而已T發表於2017-09-13

1、插入排序

這裡寫圖片描述
這裡寫圖片描述

    public static void insertionSort(int[] array) {
        int l = array.length;
        int j;
        for (int i = 1; i < l; i++) {
            int temp = array[i];
            //temp <= array[j - 1] 會因為等於多一次比較
            for (j = i; j > 0 && temp < array[j - 1]; j--)
                array[j] = array[j - 1];
            array[j] = temp;
        }
    }

空間消耗 O(1) (臨時儲存array[i])
平均時間複雜度 O(n^2)
最好情況 O(n) (已經排序,內層for迴圈的檢測總是立即判斷不成立而終止)
最壞情況 O(n^2) (需排序的為逆序)

如果目標是把n個元素的序列升序排列,那麼採用插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞情況就是,序列是降序排列,那麼此時需要進行的比較共有n(n-1)/2次。插入排序的賦值操作是比較操作的次數加上 (n-1)次。平均來說插入排序演算法的時間複雜度為O(n^2)。

(可以使用二分查詢法進行優化)


2、氣泡排序

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

可以是將最小的冒上去,或者最大的沉下去。

這裡寫圖片描述

//改進後的,增加了一個判斷標誌
public static void bubbleSort(int[] array) {
        boolean flag = true;
        //若flag 為false表明剩下的序列是有序的了
        for (int i = 0; i < array.length && flag; i++) {
            flag = false;
            for (int j = array.length - 1; j > i; j--) {
                if (array[j] < array[j - 1]) {
                    int temp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = temp;
                    flag = true;//表明有資料交換
                }
            }
        }

空間消耗 O(1) (用於交換相鄰資料)
平均時間複雜度 O(n^2)
最好情況 O(n) (改進後的,避免對已經有序的序列重複進行迴圈比較,未改進的為O(n^2) )
最壞情況 O(n^2) (需排序的為逆序)

當最好情況下,即需排序的陣列本身是有序的,根據改進的程式碼。可以推斷出有n-1次比較,沒有資料交換。當最壞的時候,即需排序的陣列本身是逆序的,此時需要比較n(n-1)/2次,並做等量數量級的記錄移動。

3、歸併排序

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

(1)遞迴實現

    public static void mergeSort(int[] array) {
        int[] tempArr = new int[array.length];
        mergeSort(array, tempArr, 0, array.length - 1);
    }

    private static void mergeSort(int[] array, int[] tempArr, int left, int right) {
        if (left < right) {
            int center = (left + right) / 2;
            //遞迴將左邊的歸併為有序
            mergeSort(array, tempArr, left, center);
            //遞迴將右邊的歸併為有序
            mergeSort(array, tempArr, center + 1, right);
            //將左右兩個子序列歸併到一起
            merge(array, tempArr, left, center + 1, right);
        }
    }

    private static void merge(int[] array, int[] tempArr, int leftPos, int rightPos, int rightEnd) {
        int leftEnd = rightPos - 1, tmpPos = leftPos, num = rightEnd - leftPos + 1;

        while (leftPos <= leftEnd && rightPos <= rightEnd) {
            if (array[leftPos] < array[rightPos]) tempArr[tmpPos++] = array[leftPos++];
            else tempArr[tmpPos++] = array[rightPos++];
//            tempArr[tmpPos++] = array[array[leftPos] < array[rightPos] ? leftPos++ : rightPos++];
        }


        while (leftPos <= leftEnd)
            tempArr[tmpPos++] = array[leftPos++];

        while (rightPos <= rightEnd)
            tempArr[tmpPos++] = array[rightPos++];

        for (int i = 0; i < num; i++, rightEnd--)
            array[rightEnd] = tempArr[rightEnd];
    }

空間消耗 O(n+log n)
平均時間複雜度 O(n log n)
最好情況 O(n log n)
最壞情況 O(n log n)
這裡寫圖片描述

(2)非遞迴實現

    public static void mergeSort(int[] arr) {
        int len = arr.length;
        int k = 1;

        while(k < len)
        {
            mergePass(arr, k, len);
            k *= 2;
        }
    }

    //mergePass方法負責將陣列中的相鄰的有k個元素的序列進行歸併
    private static void mergePass(int[] arr, int k, int n) {
        int i = 0;

        //從前往後,將2個長度為k的子序列合併為1個
        //n - 2*k + 1中加 1 的原因是陣列的下表是從 0 開始的
        //且需要保證兩兩合併的序列(非落單的序列)長度為k
        while(i < n - 2*k + 1)
        {
            merge(arr, i, i + k-1, i + 2*k - 1);
            i += 2*k;
        }

        //這段程式碼保證了,將那些“落單的”長度不足兩兩merge的部分和前面merge起來。
        if(i < n - k )
        {
            merge(arr, i, i+k-1, n-1);
        }

    }

        //merge函式實際上是將兩個有序陣列合併成一個有序陣列
        private static void merge(int[] arr, int low, int mid, int high) {
        //temp陣列用於暫存合併的結果
        int[] temp = new int[high - low + 1];
        int i = low;
        int j = mid+1;
        int k = 0;

        //將記錄由小到大地放進temp陣列
        for(; i <= mid && j <= high; k++)
        {
            if(arr[i] < arr[j])
                temp[k] = arr[i++];
            else
                temp[k] = arr[j++];
        }

        //接下來兩個while迴圈是為了將剩餘的(比另一邊多出來的個數)放到temp陣列中
        while(i <= mid)
            temp[k++] = arr[i++];

        while(j <= high)
            temp[k++] = arr[j++];

        //將temp陣列中的元素寫入到待排陣列中
        for(int l = 0; l < temp.length; l++)
            arr[low + l] = temp[l];
    }

空間消耗 O(n)

避免了遞迴時需要的深度為 log n 的棧空間

4、堆排序

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

在下面的篇部落格中說得挺好的,具體的實現細節可以參考,但是是用JS實現程式碼的:
http://bubkoo.com/2014/01/14/sort-algorithm/heap-sort/

    public static void heapSort(int[] arr) {
        //構建初始最大堆
        for (int i = arr.length/2-1; i >=0; i--) {
            buildMaxHeap(arr,i,arr.length);
        }

        for (int i = arr.length-1; i > 0 ; i--) {
            //交換堆頂最大值和最後一個葉子結點
            int tmp = arr[0];
            arr[0] = arr[i];
            arr[i] = tmp;

            //重新構建剩下的序列的最大堆
            buildMaxHeap(arr,0,i);
        }
    }

    //構建最大堆
    public static void buildMaxHeap(int[] array,int index,int heapSize) {
        int iMax, iLeft, iRight;
        while (true) {
            iMax = index;
            iLeft = 2 * index + 1;
            iRight = 2 * (index + 1);

            if (iLeft < heapSize && array[index] < array[iLeft]) {
                iMax = iLeft;
            }
            if (iRight < heapSize && array[iMax] < array[iRight]) {
                iMax = iRight;
            }
            if (iMax != index) {
                int tmp = array[iMax];
                array[iMax] = array[index];
                array[index] = tmp;

                index = iMax;
            } else {
                break;
            }
        }
    }

空間消耗 O(1)
平均時間複雜度 O(n log n)
最好情況 O(n log n)
最壞情況 O(n log n)

堆排序複雜度分析:

    它的執行時間主要是消耗在初始構建堆和在重建堆時的反覆篩選上。
    在構建堆的過程中,因為我們是完全二叉樹從最下層最右邊的非終端結點開始構建,將它與其孩子進行比較和若有必要的互換,對於每個非終端結點來說,其實最多進行兩次比較和互換操作,因此整個構建堆的時間複雜度為O(n)。
    在正式排序時,第i次取堆頂記錄重建堆需要用O(logi)的時間(完全二叉樹的某個結點到根結點的距離為⌊log2i⌋+1),並且需要取n-1次堆頂記錄,因此,重建堆的時間複雜度為O(nlogn)。
    所以總體來說,堆排序的時間複雜度為O(nlogn)。由於堆排序對原始記錄的排序狀態並不敏感,因此它無論是最好、最壞和平均時間複雜度均為O(nlogn)。這在效能上顯然要遠遠好過於冒泡、簡單選擇、直接插入的O(n2)的時間複雜度了。 
    空間複雜度上,它只有一個用來交換的暫存單元,也算是非常的不錯。不過由於記錄的比較與交換是跳躍式進行,因此堆排序也是一種不穩定的排序方法。
    另外,由於初始構建堆所需的比較次數較多,因此,它並不適合待排序序列個數較少的情況。 

5、快速排序

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

參考部落格:http://bubkoo.com/2014/01/12/sort-algorithm/quick-sort/

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

    public static void sort(int[] array,int left,int right) {
        if (left > right) {
            return;
        }
        int storeIndex = partition(array, left, right);
        sort(array, left, storeIndex - 1);
        sort(array, storeIndex + 1, right);
    }

    private static int partition(int[] array, int left, int right) {
        int storeIndex = left;
        int pivot = array[right]; // 直接選最右邊的元素為基準元素
        for (int i = left; i < right; i++) {
            if (array[i] < pivot) {
                swap(array, storeIndex, i);
                storeIndex++; // 交換位置後,storeIndex 自增 1,代表下一個可能要交換的位置
            }
        }
        swap(array, right, storeIndex); // 將基準元素放置到最後的正確位置上
        //之後,以基準元素為分界點,左邊是小於它的,右邊是大於等於它的

        return storeIndex;
    }

    private static void swap(int[] array, int i, int k) {
        int temp = array[i];
        array[i] = array[k];
        array[k] = temp;
    }

空間消耗 O(log n)
平均時間複雜度 O(n log n)
最好情況 O(n log n)
最壞情況 O(n^2)

最壞情況發生在每次劃分過程產生的兩個區間分別包含n-1個元素和1個元素的時候(設輸入的表有n個元素)。
最好情況為如果每次劃分過程產生的區間大小都為n/2。

相關文章