排序算法杂谈(三) —— 归并排序的非递归实现
1. 遞歸
?
在眾多排序算法中,歸并排序(Merge Sort)和快速排序(Quick Sort)都是時間復雜度為 O(nlog2n) 的高效排序。
這兩種排序有一種共性,就是運用到了遞歸的思想。
?
在程序設計中,遞歸是一個很有用的性質,用遞歸寫出來的算法,一般來說都很簡潔明了,易于理解。
例如歸并排序:
public final class MergeSort extends BasicMergeSort {@Overridepublic void sort(int[] array) {sort(array, 0, array.length - 1);}private void sort(int[] array, int left, int right) {if (left < right) {int mid = ArrayHelper.getMiddle(left, right);sort(array, left, mid);sort(array, mid + 1, right);mergeSolution.merge(array, left, right);}} }?
其中,mergeSolution.merge() 是將兩個有序數組合并成一個有序數組的接口,詳細內容可以參見:
https://www.cnblogs.com/jing-an-feng-shao/p/9038915.html
?
然而,漂亮的代碼與高效的代碼往往是相沖的。遞歸存在一個很致命的缺點。
眾所周知,Java 程序中的數據,存放在堆棧(Stack)空間中,堆棧具有訪問效率高,速度快的優點,但同時其分配的空間往往是有限的。
在遞歸過程中,會把上層的數據不斷地壓入堆棧空間中。如果遞歸地深度過大,在達到遞歸的結束條件之前,堆棧中的數據已經溢出,那么就會導致程序崩潰。
StackOverFlow 這個異常,相信讀者見到的次數不在少數。
?
?
2. 遞歸轉循環
?
所以,在這種情況下,往往就需要將遞歸結構轉化為循環結構去解決問題。
那么,怎么進行轉化呢?這時候就要從遞歸的本質出發。
?
遞歸是什么?它是?數據的傳遞 + 數據的壓棧。
所以解決了怎么傳遞數據的問題和怎么數據壓棧的問題,就能將遞歸轉化為循環,那么自然而然地想到了:
- 記錄傳遞的數據 -> 利用自定義對象 Record 記錄傳遞的數據。
- 利用堆棧的結構 -> 利用程序手動將 Stack<Record> 進行壓棧的操作。
最后,通過迭代自定義的堆棧內數據,達到遞歸地效果。
?
?
3. 歸并排序的非遞歸實現
?
首先需要明確的一點,是歸并排序在遞歸地過程中傳遞什么數據?
顯然是兩個子數組的左、中、右節點,其中中節點時刻已通過計算得出的,可有可無。
那么我們現在就可以設計對象 Record(為方便使用,這里記錄了中節點):
private static class Record {int left;int mid;int right;private Record(int left, int mid, int right) {this.left = left;this.mid = mid;this.right = right;}}?
其次,如何將 Record 顯示地入棧?
假設現在我們正在進行長度為 8 的數組排序,那么顯然,我們最終希望堆棧中的數據是這樣的(方便起見,這里的 Record 不計中節點):
?
?
這樣的堆棧結構,自上而下進行合并,就是我們期望的歸并排序的合并順序。
這樣的結構是不是很眼熟?
沒錯,看到這個圖,我們很自然地會想到 漢諾塔(Tower of Hanoi)。
那么又是很自然地,除了目標堆棧,還需要申請另外 2 個輔助空間去完成壓棧工作。
方便起見,這里的輔助空間也使用堆棧結構。
所以現在就有了:初始堆棧 stack1,目標堆棧 stack2,中轉堆棧 stack3。
?
?
于是,有如下代碼:
public class MergeSortLoop extends BasicMergeSort {private static final Stack<Record> stack1 = new Stack();private static final Stack<Record> stack2 = new Stack();private static final Stack<Record> stack3 = new Stack();@Overridepublic void sort(int[] array) {int left = 0;int right = array.length - 1;pushStack(stack1, left, right);while (!stack1.isEmpty()) {popAll(stack1, stack3);while (!stack3.isEmpty()) {Record record = stack3.pop();stack2.push(record);if (record.left < record.mid) {pushStack(stack1, record.left, record.mid);}if (record.mid + 1 < record.right) {pushStack(stack1, record.mid + 1, record.right);}}}while (!stack2.isEmpty()) {Record toBeMerged = stack2.pop();mergeSolution.merge(array, toBeMerged.left, toBeMerged.right);}}private void popAll(Stack<Record> source, Stack<Record> target) {while (!source.isEmpty()) {target.push(source.pop());}}private void pushStack(Stack<Record> stack, int left, int right) {Record record = new Record(left, ArrayHelper.getMiddle(left, right), right);stack.push(record);}private static class Record {int left;int mid;int right;private Record(int left, int mid, int right) {this.left = left;this.mid = mid;this.right = right;}} }?
?
4. 另一種堆棧模型
將遞歸結構轉為循環結構,核心思想就是構建堆棧模型。
之前寫到的這種 類-漢諾塔 堆棧模型,其結構非常漂亮,但是構造效率卻相對較低(其中需要用到 2 個額外的輔助空間,更多的入棧-出棧操作)。
其實,仔細回想歸并排序的遞歸過程,并不是依從這種 類-漢諾塔 堆棧模型的順序去遞歸的,其遞歸過程類似以下堆棧模型:
?
?
?
構造這種堆棧結構,只需要初始堆棧 stack1,目標堆棧 stack2。
?
與之前的過程相比,唯一的變化就在于第 2 步,只需要取出棧頂數據,而不是堆棧 stack1 中的所有數據。
?
轉載于:https://www.cnblogs.com/jing-an-feng-shao/p/9084928.html
總結
以上是生活随笔為你收集整理的排序算法杂谈(三) —— 归并排序的非递归实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 实时数据库工具Datab
- 下一篇: echarts我常用的参数总结