1. 程式人生 > >最新情報:所有的遞迴都可以改寫成非遞迴?

最新情報:所有的遞迴都可以改寫成非遞迴?

![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200812230900725-1001373334.jpg) # 前言 > 本文收錄於專輯:[http://dwz.win/HjK](http://dwz.win/HjK),點選解鎖更多資料結構與演算法的知識。 你好,我是彤哥,一個每天爬二十六層樓還不忘讀原始碼的硬核男人。 上一節,我們使用點陣圖介紹了12306搶票演算法的實現,沒有收到推送的同學可以點選上方專輯檢視,或者在公主號歷史訊息中檢視。 在上一節的最後,彤哥收到最新情報,說是所有的遞迴都可以改寫成非遞迴,是不是真的呢?如何實現呢?有沒有套路呢? 讓我們帶著這些問題進入今天的學習吧。 # 何為遞迴? 所謂遞迴,是指程式在執行的過程中呼叫自身的行為。 這種行為也不能無限制地進行下去,得有個出口,叫做`邊界條件`,所以,遞迴可以分成三個段:前進段、達到邊界條件,返回段,在這三個段我們都可以做一些事,比如前進段對問題規模進行縮小,返回段對結果進行整理。 這麼說可能比較抽象,讓我們看一個簡單的案例: **如何用遞迴實現1到100的相加?** 1到100相加使用迴圈大家都會解,程式碼如下: ```java public class Sum { public static void main(String[] args) { System.out.println(sumCircle(1, 100)); } private static int sumCircle(int min, int max) { int sum = 0; for (int i = min; i <= max; i++) { sum += i; } return sum; } } ``` 那麼,如何使用遞迴實現呢? # 如何快速實現遞迴? 首先,我們要找到這道題的邊界條件,1到100相加,邊界條件可以是1,也可以是100,如果從1開始,那麼邊界條件就是100,反之亦然。 找到了邊界條件之後,就是將問題規模縮小,對於這道題,計算1到100相加,那麼,能不能先計算1到99相加再把100加上呢?肯定是可以的,這樣問題的規模就縮小了,直到,問題規模縮小為1到1相加為止。 OK,讓我們看程式碼實現: ```java private static int sumRecursive(int min, int max) { // 邊界條件 if (min >= max) { return min; } // 問題規模縮小 int sum = sumRecursive(min, max - 1); // 加上當前值 sum += max; // 返回 return sum; } ``` 是不是很簡單?還可以更簡單: ```java private static int sumRecursive2(int min, int max) { return min >= max ? min : sumRecursive2(min, max - 1) + max; } ``` 686? 所以,使用遞迴最重要的就是找到邊界條件,然後讓問題的規模朝著邊界條件的方向一直縮小,直到達到邊界條件,最後依次返回即可,這也是快速實現遞迴的套路。 這麼看來,使用遞迴似乎很簡單,但是,它有沒有什麼缺點呢? 要了解缺點就得從遞迴的本質入手。 # 遞迴的本質是什麼? 我們知道,JVM啟動的時候有個引數叫做`-Xss`,它不是表示XSS攻擊哈,它是指每個執行緒可以使用的執行緒棧的大小。 那麼,什麼又是執行緒棧呢? 棧大家都理解了,我們在前面的章節也學習過了,使用棧,可以實現計算器的功能,非常方便。 執行緒棧,顧名思義,就是指執行緒執行過程中使用的棧。 那麼,執行緒在執行的過程中為什麼要使用棧呢? 這就不得不說方法呼叫的本質了。 舉個簡單的例子: ```java private static int a(int num) { int a = 1; return a + b(num); } private static int b(int num) { int b = 2; return c(num) + b; } private static int c(int num) { int c = 3; return c + num; } ``` 在這段程式碼中,方法a() 呼叫 方法b(),方法b() 呼叫 方法c(),在實際執行的過程中,是這樣處理的:呼叫方法a()時,發現需要呼叫方法b()才能返回,那就把方法a()及其狀態儲存到棧中,然後呼叫方法b(),同樣地,呼叫方法b()時,發現需要先呼叫方法c()才能返回,那就把方法b()及其狀態入棧,然後呼叫方法c(),呼叫方法c()時,不需要額外呼叫別的方法了,計算完畢返回,返回之後,從棧頂取出方法b()及當時的狀態,繼續執行方法b(),方法b()執行完畢,返回,再從棧中取出方法a()及當時的狀態,計算完畢,方法a()返回,程式等待結束。 還是上圖吧: ![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200812230901044-178555665.jpg) 所以,方法呼叫的本質,就是棧的使用。 同理,遞迴的呼叫就是方法的呼叫,所以,遞迴的呼叫,也是棧的使用,不過,這個棧會變得非常大,比如,對於1到100相加,就有99次入棧出棧的操作。 ![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200812230901309-684295708.jpg) 因此,總結起來,遞迴有以下兩個缺點: 1. 操作耗時,因為牽涉到大量的入棧出棧操作; 2. 有可能導致執行緒棧溢位,因為遞迴呼叫佔用了執行緒棧很大的空間。 那麼,我們是不是就不要使用遞迴了呢? 當然不是,之所以使用遞迴,就是因為它使用起來非常簡單,能夠快速地解決我們的問題,合理控制遞迴呼叫鏈的長度,就是一個好遞迴。 既然,遞迴呼叫的本質,就是棧的使用,那麼,我們能不能自己模擬一個棧,將遞迴呼叫改成非遞迴呢? 當然可以。 # 修改遞迴為非遞迴的套路 還是使用上面的例子,現在我們需要把遞迴修改成非遞迴,且不是使用for迴圈的那種形式,要怎麼實現呢? 首先,我們要自己模擬一個棧; 然後,找到邊界條件; 最後,朝著邊界條件的方向縮小問題規模; OK,上程式碼: ```java private static int sumNonRecursive(int min, int max) { int sum = 0; // 宣告一個棧