氣泡排序、快速排序(遞迴&非遞迴)、堆排序演算法比較淺析

CliuGeek發表於2017-11-28

最近演算法老師讓大家抓緊時間完成演算法作業,總共四個題目。這幾個題目都很不錯,我計劃著把這四個作業寫成四篇部落格,一是可以記錄下來供大家分享學習,二是可以提升自己對於這幾個演算法的理解。


任務要求:實現排序類,公共函式包括氣泡排序、快速排序(遞迴)、堆排序。將遞迴演算法改寫為非遞迴的,進行比較。


演算法思想:

(1) 氣泡排序:臨近資料的關鍵碼進行兩兩比較,按照順序(從小到大或者從大到小)進行交換,這樣一趟走過去之後,擁有最大或者最小的關鍵碼的資料被交換到了最後一個位置;然後,再從頭開始進行兩兩比較,直到倒數第二位結束;依照上面的步驟,直到確定第一個位置的資料或者這一組的資料經過一趟冒泡之後不再發生變化時,氣泡排序結束。這樣得出的結果序列就是我們預期要得到的有序序列。
(2) 快速排序(遞迴):任取待排序物件中的某個物件作為基準,按照該物件的關鍵碼大小,將整個物件序列劃分為左右兩個子序列。其中,左側子序列中所有的關鍵碼都小於或等於基準物件的關鍵碼;右側子序列中所有物件的關鍵碼都大於基準物件的關鍵碼。此時基準物件所在的位置也就是該物件在該序列中最終應該安放的位置。然後遞迴的在左右子序列中重複實施上述的方法,直至所有的物件都放到相應的位置上為止。
(3) 快速排序(非遞迴):遞迴的快排演算法是編譯器自動用棧來實現的,當遞迴層次比較深的時候,需要佔用比較大的程式棧空間,會造成程式棧溢位的危險。因此我們可以自己用棧模擬遞迴過程,即每次從棧頂取出一部分做劃分之後,都把新的兩部分的起始位置分別入棧。
(4) 堆排序:利用大頂堆(或者小頂堆)堆頂記錄的是最大關鍵字(最小關鍵字)這一特性,實現對序列的排序。以大頂堆為例。首先,將初始待排序關鍵字序列(R1,R2…Rn)構建成一個大頂堆,此時的堆為初始的無序區。然後,將堆頂的元素R1與最後一個元素Rn交換,此時得到新的無序區(R1,R2…Rn-1)和新的有序區(Rn),且此時滿足R1,R2…Rn-1 <= Rn。經過如上的交換之後,新的頂堆可能違反大頂堆的性質,因此需要對新產生的無序區(R1,R2…Rn-1)進行調整使之成為新的大頂堆,然後再將R1與Rn-1(無序區中最後一個元素)進行交換。不斷重複以上過程直至有序區中的元素個數達到n-1個為止,完成整個排序過程。

設計思路:

(1) 氣泡排序:氣泡排序需要用到雙重迴圈。首先,實現內層迴圈,即進行相鄰元素之間的比較,該調換順序的調換順序,不用調換順序的進行下一位的比較。其次,再實現外層迴圈,根據氣泡排序的思想,每次內迴圈結束後,都會有一個最小值(或者最大值)到達它最終應該到達的位置,因此下一趟內迴圈的比較次數就減少一次,於是我們應該在外迴圈中控制內層迴圈的次數。內層迴圈結束退出後,外層迴圈減一再進入內迴圈。為了防止有序序列已經出現的時候,我們的迴圈還在執行,不妨設定一個標誌位,初始為0,如果在內部迴圈中發生了交換則將其置為1。如果內層迴圈結束後,我們的標誌位仍為0,則表示我們的序列已經有序,無需再繼續執行下去,此時便可以返回當前的有序序列了。
(2) 快速排序(遞迴):快排的設計思路基於分治法。首先,選取一個元素作為切分元素pivot,然後基於pivot把陣列切分為三部分:左子陣列(元素小於或者等於pivot),pivot,右子陣列(元素大於pivot)。然後,遞迴地對左右兩個子陣列進行快速排序,只要保證左右兩個子陣列分別有序,整個陣列也就有序了。
(3) 快速排序(非遞迴):自己構造一個棧,棧中存放待排序陣列的起始位置和終止位置,然後將整個陣列的起始位置s和終止位置e入棧,然後從s到e依次對出棧資料進行排序,找到基準資料最終的位置p。在基準位置左側,判斷起始位置s是否小於基準位置p-1,如果小於則將起始位置和新的終點(p-1)位置進棧。同理,在基準位置右側,判斷e是否大於基準位置p+1,若大於則將p+1作為起始位置,e作為終點位置進棧。在這兩個新的棧中找到各自的基準資料的最終位置。如此往復直至棧空。
(4) 堆排序:堆排序中關鍵的就是建立大頂堆的過程和調整過程,而調整過程是最為重要的。從最末非葉節點(根據樹的結構,不難得出這個位置為Array.length/2向下取整)開始,然後依次往前調整;然後我們再討論調整過程,首先和當前節點的左子女進行比較,用一個值largest記錄比較出來的比較大的那個值的index,然後再讓這個largest所代表的值與該節點的右子女進行比較,依舊用largest記錄較大值的index,最後比較當前節點與這個largest所代表的值,如果相等說明我們當前節點就是這個三節點兩層子樹的最大值,如果不相等,我們應該將這個值與我們當前的節點(子樹父節點)交換。按照以上步驟建立大頂堆,然後將大頂堆堆頂的值與該樹的最後一個節點進行交換,此時便分出來兩個區,也就是我們演算法思想中提到的無序區和有序區。交換之後的,再對新的Array進行新一輪的調整。為了方便,我同時也實現了一個陣列的工具類,它有兩個功能,一是列印Array;另一個功能就是實現兩個數的交換功能。

程式程式碼:

(1) 氣泡排序:

public class BubbleSort {
       public static void main(String[] args) {
        // TODO Auto-generated method stub
        int A[] = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
        BubbleSort(A, 10);
        for (int i = 0; i < 10; i++) {
            System.out.print(A[i] + " ");
        }
    }

       public static void BubbleSort(int A[], int n) {
        boolean flag = false;
        int temp;
        for (int i = 0; i < n - 1; i++) {
            flag = false;
            for (int j = n - 1; j > i; j--)
                if (A[j - 1] > A[j]) {
                    temp = A[j - 1];
                    A[j - 1] = A[j];
                    A[j] = temp;
                    flag = true;
                }
            if (flag == false)
                return;
        }
    }
    }

(2) 快速排序:

public class QuickSort {
        public static void main(String[] args) {
            int A[] = { 1, 5, 8, 2, 4, 6, 3, 7, 9, 0 };
            int low = 0;
            int high = A.length - 1;
            QuickSort(A, low, high);
            for (int i = 0; i < A.length; i++) {
                System.out.print(A[i] + " ");
        }
    }

    public static void QuickSort(int A[], int low, int high) {
        if (low < high) {
            int pivotpos = Partition(A, low, high);
            QuickSort(A, low, pivotpos - 1);
            QuickSort(A, pivotpos + 1, high);
        }
    }

    public static int Partition(int A[], int low, int high) {
        int pivot = A[low];
        while (low < high) {
            while (low < high && A[high] >= pivot)
                --high;
            A[low] = A[high];
            while (low < high && A[low] <= pivot)
                ++low;
            A[high] = A[low];
        }
        A[low] = pivot;
        return low;
    }
    }

(3) 快速排序非遞迴方式實現:

public class QuickSortNonRecursion {
        public static void main(String[] args) {
            int A[] = { 1, 5, 8, 2, 4, 6, 3, 7, 9, 0 };
            int low = 0;
            int high = A.length - 1;
            nonRec_quickSort(A, low, high);
            for (int i = 0; i < A.length; i++) {
                System.out.print(A[i] + " ");
        }
    }

    public static void nonRec_quickSort(int[] a, int start, int end) {
        Stack<Integer> stack = new Stack<>();
        if (start < end) {
            stack.push(end);
            stack.push(start);
            while (!stack.isEmpty()) {
                int l = stack.pop();
                int r = stack.pop();
                int index = partition(a, l, r);
                if (l < index - 1) {
                    stack.push(index - 1);
                    stack.push(l);
                }
                if (r > index + 1) {
                    stack.push(r);
                    stack.push(index + 1);
                }
            }
        }
        System.out.println(Arrays.toString(a));
    }

    public static int partition(int[] a, int start, int end) {
        int pivot = a[start];
        while (start < end) {
            while (start < end && a[end] >= pivot)
                end--;
            a[start] = a[end];
            while (start < end && a[start] <= pivot)
                start++;
            a[end] = a[start];
        }
        a[start] = pivot;
        return start;
    }
    }

(4) 堆排序:

核心程式碼:

public class HeapSort {

    public static void main(String[] args) {
        int[] array = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -2, -3 };

        System.out.println("Before heap:");
        ArrayUtils.printArray(array);

        heapSort(array);

        System.out.println("After heap sort:");
        ArrayUtils.printArray(array);

    }

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

        buildMaxHeap(array);
        for (int i = array.length - 1; i >= 1; i--) {
            ArrayUtils.exchangeElements(array, 0, i);

            maxHeap(array, i, 0);
        }
    }

    private static void buildMaxHeap(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        int half = array.length / 2;
        for (int i = half; i >= 0; i--) {
            maxHeap(array, array.length, i);
        }
    }

    private static void maxHeap(int[] array, int heapSize, int index) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;

        int largest = index;
        if (left < heapSize && array[left] > array[index]) {
            largest = left;
        }
        if (right < heapSize && array[right] > array[largest]) {
            largest = right;
        }
        if (index != largest) {
            ArrayUtils.exchangeElements(array, index, largest);
            maxHeap(array, heapSize, largest);
        }
    }
    }

工具類:

public class ArrayUtils {
    public static void printArray(int[] array) {
        System.out.print("{");
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i]);
            if (i < array.length - 1) {
                System.out.print(",");
            }
        }
        System.out.println("}");
    }

    public static void exchangeElements(int[] array, int index1, int index2) {
        int temp = array[index1];
        array[index1] = array[index2];
        array[index2] = temp;
    }
    }

測試例及執行結果:

(1) 氣泡排序:
{10,9,8,7,6,5,4,3,2,1} ==> {1,2,3,4,5,6,7,8,9,10}
{1,5,8,2,4,6,3,7,9,0} ==> {0,1,2,3,4,5,6,7,8,9}
(2) 快速排序(遞迴與非遞迴測試例相同):
{10,9,8,7,6,5,4,3,2,1} ==> {1,2,3,4,5,6,7,8,9,10}
{1,5,8,2,4,6,3,7,9,0} ==> {0,1,2,3,4,5,6,7,8,9}
(3) 堆排序:
Before heap:
{9,8,7,6,5,4,3,2,1,0,-1,-2,-3}
After heap sort:
{-3,-2,-1,0,1,2,3,4,5,6,7,8,9}

分析:

(1) 氣泡排序:
最好情況:物件的初始排列是按關鍵碼從小到大排好序時,此演算法只執行一次氣泡排序,做n-1次的關鍵碼比較,不需要移動物件。
最壞情況:演算法執行了n-1次冒泡,第i次(1<=i<=n)做了n-i次關鍵碼比較,執行了n-i次物件交換。因此這種情況下,總的關鍵碼比較次數KCN和物件移動次數RMN為:

KCN&RMN公式

空間複雜度:需要一個附加物件以實現物件值的對換。
氣泡排序是一個穩定的排序演算法。
(2) 快速排序(遞迴):
快速排序的趟數取決於遞迴樹的深度。

最理想情況下時間複雜度分析:每次劃分後,該物件的左右兩側的子序列長度相同。總的時間複雜度T(n)= o(n log2n )
最壞情況下時間複雜度分析:每次劃分後,有一個子序列是空的,此時T(n)=O(n2)
平均情況:T(n)= o(n log2n )

最好情況下空間複雜度分析:由於演算法是遞迴的,需要一個棧存放每層遞迴呼叫時的指標和引數。遞迴的深度就是樹的深度,因此此時的儲存開銷為o(log2n)。
最壞情況下空間複雜度分析:此時遞迴樹為單支樹,儲存開銷為o(n)。

快速排序是不穩定的排序演算法。
(3) 快速排序(非遞迴):
該演算法對比遞迴演算法,它使用的棧是我們自己建立的棧。而遞迴方法使用的是程式自動產生的棧,棧中包含了函式呼叫時的引數和函式中的區域性變數。如果區域性變數很多或者函式內部有呼叫了其他函式,這樣一來,棧就會變得很大,每次遞迴呼叫都要操作很大的棧,效率自然會下降。而我們自己建立的棧,不管程式的複雜度如何,都不會影響程式的效率。
該演算法的平均情況的時間複雜度仍是o(n log2n )。
(4) 堆排序:
時間複雜度分析:初始化大頂堆過程每個非葉節點都用到了調整過程的演算法,因此該層迴圈所用時間為:

Heap

n 其中,i 是層序號,2i 是第 i 層的最大結點數,(k-i-1)是第 i 層結點能夠 移動的最大距離。
化簡後:

HeapInform

調整迴圈過程:該迴圈的計算時間為O(nlog2n)。所以,堆排序的時間複雜度為O(nlog2n)。
空間複雜度分析:附加儲存主要在調整過程中佔用,執行交換需要使用一個臨時儲存空間,因此空間複雜度為O(1)。
堆排序是一個不穩定的排序演算法。

以上是作業的主要內容,如果有不正確或者有異議的地方,還希望大家不吝指正。

相關文章