最新情報:全部的遞歸均可以改寫成非遞歸?

關注公衆號「彤哥讀源碼」,解鎖更多源碼、基礎、架構知識!算法

前言

本文收錄於專輯:http://dwz.win/HjK,點擊解鎖更多數據結構與算法的知識。微信

你好,我是彤哥,一個天天爬二十六層樓還不忘讀源碼的硬核男人。數據結構

上一節,咱們使用位圖介紹了12306搶票算法的實現,沒有收到推送的同窗能夠點擊上方專輯查看,或者在公主號歷史消息中查看。架構

在上一節的最後,彤哥收到最新情報,說是全部的遞歸均可以改寫成非遞歸,是否是真的呢?如何實現呢?有沒有套路呢?性能

讓咱們帶着這些問題進入今天的學習吧。學習

何爲遞歸?

所謂遞歸,是指程序在運行的過程當中調用自身的行爲。測試

這種行爲也不能無限制地進行下去,得有個出口,叫作邊界條件,因此,遞歸能夠分紅三個段:前進段、達到邊界條件,返回段,在這三個段咱們均可以作一些事,好比前進段對問題規模進行縮小,返回段對結果進行整理。this

這麼說可能比較抽象,讓咱們看一個簡單的案例:spa

如何用遞歸實現1到100的相加?.net

1到100相加使用循環你們都會解,代碼以下:

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,讓咱們看代碼實現:

private static int sumRecursive(int min, int max) {
// 邊界條件
if (min >= max) {
return min;
}
// 問題規模縮小
int sum = sumRecursive(min, max - 1);
// 加上當前值
sum += max;
// 返回
return sum;
}

是否是很簡單?還能夠更簡單:

private static int sumRecursive2(int min, int max) {
return min >= max ? min : sumRecursive2(min, max - 1) + max;
}

686?

因此,使用遞歸最重要的就是找到邊界條件,而後讓問題的規模朝着邊界條件的方向一直縮小,直到達到邊界條件,最後依次返回便可,這也是快速實現遞歸的套路。

這麼看來,使用遞歸彷佛很簡單,可是,它有沒有什麼缺點呢?

要了解缺點就得從遞歸的本質入手。

遞歸的本質是什麼?

咱們知道,JVM啓動的時候有個參數叫作-Xss,它不是表示XSS攻擊哈,它是指每一個線程可使用的線程棧的大小。

那麼,什麼又是線程棧呢?

棧你們都理解了,咱們在前面的章節也學習過了,使用棧,能夠實現計算器的功能,很是方便。

線程棧,顧名思義,就是指線程運行過程當中使用的棧。

那麼,線程在運行的過程當中爲何要使用棧呢?

這就不得不說方法調用的本質了。

舉個簡單的例子:

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()返回,程序等待結束。

仍是上圖吧:

因此,方法調用的本質,就是棧的使用。

同理,遞歸的調用就是方法的調用,因此,遞歸的調用,也是棧的使用,不過,這個棧會變得很是大,好比,對於1到100相加,就有99次入棧出棧的操做。

所以,總結起來,遞歸有如下兩個缺點:

  1. 操做耗時,由於牽涉到大量的入棧出棧操做;

  2. 有可能致使線程棧溢出,由於遞歸調用佔用了線程棧很大的空間。

那麼,咱們是否是就不要使用遞歸了呢?

固然不是,之因此使用遞歸,就是由於它使用起來很是簡單,可以快速地解決咱們的問題,合理控制遞歸調用鏈的長度,就是一個好遞歸。

既然,遞歸調用的本質,就是棧的使用,那麼,咱們能不能本身模擬一個棧,將遞歸調用改爲非遞歸呢?

固然能夠。

修改遞歸爲非遞歸的套路

仍是使用上面的例子,如今咱們須要把遞歸修改爲非遞歸,且不是使用for循環的那種形式,要怎麼實現呢?

首先,咱們要本身模擬一個棧;

而後,找到邊界條件;

最後,朝着邊界條件的方向縮小問題規模;

OK,上代碼:

private static int sumNonRecursive(int min, int max) {
int sum = 0;
// 聲明一個棧
Stack<Integer> stack = new Stack<Integer>();
stack.push(max);
while (!stack.isEmpty()) {
if (max > min) {
// 要計算max,先計算max-1
stack.push(--max);
} else {
// 問題規模縮小到必定程度,計算返回
sum += stack.pop();
}
}
return sum;
}

好了,是否是很簡單,其實跟遞歸的套路是同樣的,只不過改爲本身模擬棧來實現。

這個例子可能不是那麼明顯,咱們再舉個二叉樹遍歷的例子來看一下。

public class BinaryTree {

Node root;

// 插入元素
void put(int value) {
if (root == null) {
root = new Node(value);
} else {
Node parent = root;
while (true) {
if (value <= parent.value) {
if (parent.left == null) {
parent.left = new Node(value);
return;
} else {
parent = parent.left;
}
} else {
if (parent.right == null) {
parent.right = new Node(value);
return;
} else {
parent = parent.right;
}
}

}
}
}

// 先序遍歷
void preTraversal(Node x) {
if (x == null) return;
System.out.print(x.value + ",");
preTraversal(x.left);
preTraversal(x.right);
}

static class Node {
int value;
Node left;
Node right;

public Node(int value) {
this.value = value;
}
}

public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
binaryTree.put(3);
binaryTree.put(1);
binaryTree.put(2);
binaryTree.put(7);
binaryTree.put(8);
binaryTree.put(5);
binaryTree.put(4);
binaryTree.put(6);
binaryTree.put(9);
binaryTree.put(0);

binaryTree.preTraversal(binaryTree.root);
}
}

我這裏隨手寫了一顆二叉樹,並實現了其先序遍歷,這個測試用例中的二叉樹長這個樣子:

因此,這個二叉樹的先序遍歷結果爲3,1,0,2,7,5,4,6,8,9,

能夠看到,使用遞歸先序遍歷二叉樹很是簡單,並且代碼清晰易懂,那麼,它如何修改成非遞歸實現呢?

首先,咱們要本身模擬一個棧;

而後,找到邊界條件,爲節點等於空時;

最後,縮小問題規模,這裏是先把右子樹壓棧,再把左子樹壓棧,由於先左後右;

好了,來看代碼實現:

// 先序遍歷非遞歸形式
void nonRecursivePreTraversal(Node x) {
// 本身模擬一個棧
Stack<Node> stack = new Stack<Node>();
stack.push(x);
while (!stack.isEmpty()) {
Node tmp = stack.pop();
// 隱含的邊界條件
if (tmp != null) {
System.out.print(tmp.value + ",");
// 縮小問題規模
stack.push(tmp.right);
stack.push(tmp.left);
}
}
}

掌握了這個套路是否是把遞歸改寫爲非遞歸很是簡單,不過,改寫以後的代碼顯然沒有遞歸那麼清晰。

好了,遞歸改寫爲非遞歸的套路咱們就講到這裏,不知道你Get到了沒有呢?你也能夠找個遞歸本身來改寫試試看。

後記

本節,咱們從遞歸的概念入手,學習瞭如何快速實現遞歸,以及遞歸的本質,最後,學習了遞歸改寫爲非遞歸的套路。

本質上,這也是棧這種數據結構的常規用法。

既然講到了棧,不講隊列是否是有點過度?

因此,下一節,遍歷各類源碼的彤哥將介紹如何實現高性能的隊列,想了解其中的套路嗎?還不快點來關注我!

關注公號主「彤哥讀源碼」,解鎖更多源碼、基礎、架構知識。




本文分享自微信公衆號 - 彤哥讀源碼(gh_63d1b83b9e01)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索