排序算法雜談(四) —— 快速排序的非遞歸實現


1. 前提

排序算法(七) —— 快速排序

排序算法雜談(三) —— 歸並排序的非遞歸實現

 

 

2. 快速排序與歸並排序的遞歸

快速排序(Quick Sort)與歸並排序(Merge Sort)雖然都采用了遞歸地思想,但是其遞歸地本質卻有所不同。

  • 快速排序,手動划分,自然有序。
  • 歸並排序,自然兩分,手動合並。

 

快速排序,是先通過划分(partition)算法,將數組兩分,划分的過程中,比主元(pivot)小的數字全部被划分到了左側,比主元大的數字全部被划分到了右側。

然后對兩分的數組進行遞歸。當數組兩側的長度均小於等於1,那么數組就自然有序了。

歸並排序,是將原數組二等分,直到被等分的數組長度小於等於1,那么被等分的數組就有序了,然后對這等分的數組進行合並。

 

所以說,快速排序與歸並排序,正好代表了遞歸的兩種典型,如果將遞歸的過程看做是一顆二叉樹,那么:

  • 快速排序:下層遞歸的實現,依賴上層操作的結果。(只有父節點操作完成,才能對子節點進行遞歸)
  • 歸並排序:上層遞歸的操作,依賴下層遞歸的結果。(只有子節點全部操作完成,才可以操作父節點)

 

 

3. 快速排序非遞歸實現的堆棧模型 stack 與 記錄模型 record

 

快速排序這種,優先操作,然后遞歸的特點,大大簡化了構造目標堆棧模型的難度。

在歸並排序中,不難發現,其構造目標堆棧模型的過程,是不斷入棧的過程,最后一次性地處理堆棧信息。

 

相反,在快速排序中,目標堆棧是一個不斷 入棧-出棧 的過程,在出棧的過程中,就對數據進行處理,沒有必要再最后一次性處理。

而且,由於划分具有不穩定性,所以沒有辦法給出確切的堆棧模型。

 

快速排序的遞歸過程,只需要關心其左邊與右邊的坐標:

    private static class Record {
        int left;
        int right;

        private Record(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }

 

 

 4. 快速排序非遞歸的過程

 

快速排序非遞歸的執行過程中,只需要一個堆棧空間,其運行過程如下:

  • 對原數組進行一次划分,分別將左邊的 Record 和 右邊的 Record 入棧 stack。
  • 判斷 stack 是否為空,若是,直接結束;若不是,將棧頂 Record 取出,進行一次划分。
  • 判斷左邊的 Record 長度(這里指 record.right - record.left + 1)大於 1,將左邊的 Record 入棧;同理,右邊的 Record。
  • 循環步驟 2、3。

 

於是,有如下代碼:

public final class QuickSortLoop extends BasicQuickSort {

    private Stack<Record> stack = new Stack<>();

    @Override
    public void sort(int[] array) {
        int left = 0;
        int right = array.length - 1;
        if (left < right) {
            int pivot = partitionSolution.partition(array, left, right);
            if (pivot - 1 >= left) {
                stack.push(new Record(left, pivot - 1));
            }
            if (pivot + 1 <= right) {
                stack.push(new Record(pivot + 1, right));
            }
            while (!stack.isEmpty()) {
                Record record = stack.pop();
                pivot = partitionSolution.partition(array, record.left, record.right);
                if (pivot - 1 >= record.left) {
                    stack.push(new Record(record.left, pivot - 1));
                }
                if (pivot + 1 <= record.right) {
                    stack.push(new Record(pivot + 1, record.right));
                }
            }
        }
    }

    private static class Record {
        int left;
        int right;

        private Record(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }
}

 

如果 Record 模型過於簡單,可以直接通過入棧-出棧 具體的數據來簡化這個過程。

 

 

 5. 關於遞歸轉循環需要知道的事情

 

通過歸並排序和快速排序非遞歸實現的講解,似乎將其轉化為循環是一個更佳的做法,其實不然,它只適用於特定的場景。

關於這種方法,需要有如下的認知:

  • 遞歸的代碼,在很多時候比循環的代碼更加容易理解。
  • 遞歸轉循環,在效率上並沒有提高。相反,由於增加了構造堆棧模型的過程,其消耗的時間更多。
  • 只有當遞歸的層數過多,而導致 StackOverFlow 的問題出現,才考慮使用遞歸轉循環的方法。
  • 可以通過調整 JVM 參數,來達到擴充堆棧空間的目的,但是一般不推薦這么做,因為這個影響是整體的。
  • 從代碼的角度,如果循環能夠解決問題,那么就使用循環;如果遞歸能解決問題,那么就使用遞歸,沒有必要特意去做兩者的轉換。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM