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 參數,來達到擴充堆棧空間的目的,但是一般不推薦這么做,因為這個影響是整體的。
- 從代碼的角度,如果循環能夠解決問題,那么就使用循環;如果遞歸能解決問題,那么就使用遞歸,沒有必要特意去做兩者的轉換。