亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > C# > 正文

C#中的尾遞歸與Continuation詳解

2020-01-24 01:48:39
字體:
來源:轉載
供稿:網友

這幾天恰好和朋友談起了遞歸,忽然發現不少朋友對于“尾遞歸”的概念比較模糊,網上搜索一番也沒有發現講解地完整詳細的資料,于是寫了這么一篇文章,權當一次互聯網資料的補充。:P

遞歸與尾遞歸

關于遞歸操作,相信大家都已經不陌生。簡單地說,一個函數直接或間接地調用自身,是為直接或間接遞歸。例如,我們可以使用遞歸來計算一個單向鏈表的長度:

復制代碼 代碼如下:

public class Node
{
    public Node(int value, Node next)
    {
        this.Value = value;
        this.Next = next;
    }

    public int Value { get; private set; }

    public Node Next { get; private set; }
}

編寫一個遞歸的GetLength方法:

復制代碼 代碼如下:

public static int GetLengthRecursively(Node head)
{
    if (head == null) return 0;
    return GetLengthRecursively(head.Next) + 1;
}

在調用時,GetLengthRecursively方法會不斷調用自身,直至滿足遞歸出口。對遞歸有些了解的朋友一定猜得到,如果單項鏈表十分長,那么上面這個方法就可能會遇到棧溢出,也就是拋出StackOverflowException。這是由于每個線程在執行代碼時,都會分配一定尺寸的棧空間(Windows系統中為1M),每次方法調用時都會在棧里儲存一定信息(如參數、局部變量、返回地址等等),這些信息再少也會占用一定空間,成千上萬個此類空間累積起來,自然就超過線程的??臻g了。不過這個問題并非無解,我們只需把遞歸改成如下形式即可(在這篇文章里我們不考慮非遞歸的解法):

復制代碼 代碼如下:

public static int GetLengthTailRecursively(Node head, int acc)
{
    if (head == null) return acc;
    return GetLengthTailRecursively(head.Next, acc + 1);
}

GetLengthTailRecursively方法多了一個acc參數,acc的為accumulator(累加器)的縮寫,它的功能是在遞歸調用時“積累”之前調用的結果,并將其傳入下一次遞歸調用中――這就是GetLengthTailRecursively方法與GetLengthRecursively方法相比在遞歸方式上最大的區別:GetLengthRecursive方法在遞歸調用后還需要進行一次“+1”,而GetLengthTailRecursively的遞歸調用屬于方法的最后一個操作。這就是所謂的“尾遞歸”。與普通遞歸相比,由于尾遞歸的調用處于方法的最后,因此方法之前所積累下的各種狀態對于遞歸調用結果已經沒有任何意義,因此完全可以把本次方法中留在堆棧中的數據完全清除,把空間讓給最后的遞歸調用。這樣的優化1便使得遞歸不會在調用堆棧上產生堆積,意味著即時是“無限”遞歸也不會讓堆棧溢出。這便是尾遞歸的優勢。

有些朋友可能已經想到了,尾遞歸的本質,其實是將遞歸方法中的需要的“所有狀態”通過方法的參數傳入下一次調用中。對于GetLengthTailRecursively方法,我們在調用時需要給出acc參數的初始值:

復制代碼 代碼如下:

GetLengthTailRecursively(head, 0)

為了進一步熟悉尾遞歸的使用方式,我們再用著名的“菲波納鍥”數列作為一個例子。傳統的遞歸方式如下:
復制代碼 代碼如下:

public static int FibonacciRecursively(int n)
{
    if (n < 2) return n;
    return FibonacciRecursively(n - 1) + FibonacciRecursively(n - 2);
}

而改造成尾遞歸,我們則需要提供兩個累加器:
復制代碼 代碼如下:

public static int FibonacciTailRecursively(int n, int acc1, int acc2)
{
    if (n == 0) return acc1;
    return FibonacciTailRecursively(n - 1, acc2, acc1 + acc2);
}

于是在調用時,需要提供兩個累加器的初始值:
復制代碼 代碼如下:

FibonacciTailRecursively(10, 0, 1)

尾遞歸與Continuation
Continuation,即為“完成某件事情”之后“還需要做的事情”。例如,在.NET中標準的APM調用方式,便是由BeginXXX方法和EndXXX方法構成,這其實便是一種Continuation:在完成了BeginXXX方法之后,還需要調用EndXXX方法。而這種做法,也可以體現在尾遞歸構造中。例如以下為階乘方法的傳統遞歸定義:

復制代碼 代碼如下:

public static int FactorialRecursively(int n)
{
    if (n == 0) return 1;
    return FactorialRecursively(n - 1) * n;
}

顯然,這不是一個尾遞歸的方式,當然我們輕易將其轉換為之前提到的尾遞歸調用方式。不過我們現在把它這樣“理解”:每次計算n的階乘時,其實是“先獲取n - 1的階乘”之后再“與n相乘并返回”,于是我們的FactorialRecursively方法可以改造成:
復制代碼 代碼如下:

public static int FactorialRecursively(int n)
{
    return FactorialContinuation(n - 1, r => n * r);
}

// 6. FactorialContinuation(n, x => x)
public static int FactorialContinuation(int n, Func<int, int> continuation)
{
    ...
}


FactorialContinuation方法的含義是“計算n的階乘,并將結果傳入continuation方法,并返回其調用結果”。于是,很容易得出,FactorialContinuation方法自身便是一個遞歸調用:
復制代碼 代碼如下:

public static int FactorialContinuation(int n, Func<int, int> continuation)
{
    return FactorialContinuation(n - 1,
        r => continuation(n * r));
}

FactorialContinuation方法的實現可以這樣表述:“計算n的階乘,并將結果傳入continuation方法并返回”,也就是“計算n - 1的階乘,并將結果與n相乘,再調用continuation方法”。為了實現“并將結果與n相乘,再調用continuation方法”這個邏輯,代碼又構造了一個匿名方法,再次傳入FactorialContinuation方法。當然,我們還需要為它補充遞歸的出口條件:
復制代碼 代碼如下:

public static int FactorialContinuation(int n, Func<int, int> continuation)
{
    if (n == 0) return continuation(1);
    return FactorialContinuation(n - 1,
        r => continuation(n * r));
}

很明顯,FactorialContinuation實現了尾遞歸。如果要計算n的階乘,我們需要如下調用FactorialContinuation方法,表示“計算10的階乘,并將結果直接返回”:

復制代碼 代碼如下:

FactorialContinuation(10, x => x)

再加深一下印象,大家是否能夠理解以下計算“菲波納鍥”數列第n項值的寫法?
復制代碼 代碼如下:

public static int FibonacciContinuation(int n, Func<int, int> continuation)
{
    if (n < 2) return continuation(n);
    return FibonacciContinuation(n - 1,
        r1 => FibonacciContinuation(n - 2,
            r2 => continuation(r1 + r2)));
}

在函數式編程中,此類調用方式便形成了“Continuation Passing Style(CPS)”。由于C#的Lambda表達式能夠輕松構成一個匿名方法,我們也可以在C#中實現這樣的調用方式。您可能會想――汗,何必搞得這么復雜,計算階乘和“菲波納鍥”數列不是一下子就能轉換成尾遞歸形式的嗎?不過,您試試看以下的例子呢?

對二叉樹進行先序遍歷(pre-order traversal)是典型的遞歸操作,假設有如下TreeNode類:

復制代碼 代碼如下:

public class TreeNode
{
    public TreeNode(int value, TreeNode left, TreeNode right)
    {
        this.Value = value;
        this.Left = left;
        this.Right = right;
    }

    public int Value { get; private set; }

    public TreeNode Left { get; private set; }

    public TreeNode Right { get; private set; }
}

于是我們來傳統的先序遍歷一下:

復制代碼 代碼如下:

public static void PreOrderTraversal(TreeNode root)
{
    if (root == null) return;

    Console.WriteLine(root.Value);
    PreOrderTraversal(root.Left);
    PreOrderTraversal(root.Right);
}


您能用“普通”的方式將它轉換為尾遞歸調用嗎?這里先后調用了兩次PreOrderTraversal,這意味著必然有一次調用沒法放在末尾。這時候便要利用到Continuation了:
復制代碼 代碼如下:

public static void PreOrderTraversal(TreeNode root, Action<TreeNode> continuation)
{
    if (root == null)
    {
        continuation(null);
        return;
    }

    Console.WriteLine(root.Value);

    PreOrderTraversal(root.Left,
        left => PreOrderTraversal(root.Right,
            right => continuation(right)));
}

我們現在把每次遞歸調用都作為代碼的最后一次操作,把接下來的操作使用Continuation包裝起來,這樣就實現了尾遞歸,避免了堆棧數據的堆積??梢姡m然使用Continuation是一個略有些“詭異”的使用方式,但是在某些時候它也是必不可少的使用技巧。

Continuation的改進

看看剛才的先序遍歷實現,您有沒有發現一個有些奇怪的地方?

復制代碼 代碼如下:

PreOrderTraversal(root.Left,
    left => PreOrderTraversal(root.Right,
        right => continuation(right)));

關于最后一步,我們構造了一個匿名函數作為第二次PreOrderTraversal調用的Continuation,但是其內部直接調用了continuation參數――那么我們為什么不直接把它交給第二次調用呢?如下:
復制代碼 代碼如下:

PreOrderTraversal(root.Left,
    left => PreOrderTraversal(root.Right, continuation));

我們使用Continuation實現了尾遞歸,其實是把原本應該分配在棧上的信息丟到了托管堆上。每個匿名方法其實都是托管堆上的對象,雖然說這種生存周期短的對象不會對內存資源方面造成多大問題,但是盡可能減少此類對象,對于性能肯定是有幫助的。這里再舉一個更為明顯的例子,求二叉樹的大小(Size):
復制代碼 代碼如下:

public static int GetSize(TreeNode root, Func<int, int> continuation)
{
    if (root == null) return continuation(0);
    return GetSize(root.Left,
        leftSize => GetSize(root.Right,
            rightSize => continuation(leftSize + rightSize + 1)));
}

GetSize方法使用了Continuation,它的理解方法是“獲取root的大小,再將結果傳入continuation,并返回其調用結果”。我們可以將其進行改寫,減少Continuation方法的構造次數:

復制代碼 代碼如下:

public static int GetSize2(TreeNode root, int acc, Func<int, int> continuation)
{
    if (root == null) return continuation(acc);
    return GetSize2(root.Left, acc,
        accLeftSize => GetSize2(root.Right, accLeftSize + 1, continuation));
}

GetSize2方法多了一個累加器參數,同時它的理解方式也有了變化:“將root的大小累加到acc上,再將結果傳入continuation,并返回其調用結果”。也就是說GetSize2返回的其實是一個累加值,而并非是root參數的實際尺寸。當然,我們在調用時GetSize2時,只需將累加器置零便可:
復制代碼 代碼如下:

GetSize2(root, 0, x => x)

不知您清楚了嗎?

結束

在命令式編程中,我們解決一些問題往往可以使用循環來代替遞歸,這樣便不會因為數據規模造成堆棧溢出。但是在函數式編程中,要實現“循環”的唯一方法便是“遞歸”,因此尾遞歸和CPS對于函數式編程的意義非常重大。了解尾遞歸,對于編程思維也有很大幫助,因此大家不妨多加思考和練習,讓這樣的方式為自己所用。

注1:事實上,在C#中,即使您實現了尾遞歸,編譯器(包括C#編譯器及JIT)也不會進行優化,也就是說還是無法避免StackOverflowException。我會在不久之后單獨討論一下這方面問題。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
国产91久久婷婷一区二区| 欧美日韩午夜激情| 欧美精品videosex牲欧美| 97精品一区二区三区| 精品国产一区二区三区四区在线观看| 欧洲成人免费视频| 欧美肥老妇视频| 欧美在线视频a| 国产视频精品一区二区三区| 日韩电视剧免费观看网站| 国产欧美精品一区二区| 国产精品狠色婷| 国内揄拍国内精品少妇国语| 成人av资源在线播放| 国产视频久久网| www.亚洲人.com| 色妞色视频一区二区三区四区| 欧美激情综合色综合啪啪五月| 国产欧美一区二区白浆黑人| 欧美国产精品日韩| 久久精品夜夜夜夜夜久久| 97久久超碰福利国产精品…| 色综合色综合久久综合频道88| 亚洲国产精品嫩草影院久久| 国产精品自产拍高潮在线观看| 国产精品久久久久久久久免费| 国产精品久久婷婷六月丁香| 亚洲电影中文字幕| 亚洲精品动漫久久久久| 91av在线精品| 国产日韩中文字幕| 7m精品福利视频导航| 欧美另类极品videosbestfree| 久久艳片www.17c.com| 国产精品丝袜一区二区三区| 欧美一级高清免费播放| 久久久久久久久久亚洲| 日韩高清有码在线| 57pao成人国产永久免费| 2019中文字幕在线免费观看| 久久成人一区二区| 欧美噜噜久久久xxx| 中文字幕视频一区二区在线有码| 亚洲国产成人久久| 欧美一区二区三区艳史| 日韩欧美一区二区三区| 午夜精品一区二区三区在线视| 色婷婷av一区二区三区在线观看| 国产精品成人观看视频国产奇米| 青草青草久热精品视频在线网站| 欧美性高跟鞋xxxxhd| 国产精品一区二区三区免费视频| 久久福利视频导航| 国产精自产拍久久久久久| 日韩av影片在线观看| 国产精品一区二区三区成人| 亚洲精品日韩丝袜精品| 国产精品电影在线观看| 日韩精品极品视频| 欧美色道久久88综合亚洲精品| 色妞在线综合亚洲欧美| 色综合色综合久久综合频道88| 欧美成人在线网站| 萌白酱国产一区二区| 不卡av日日日| 海角国产乱辈乱精品视频| 亚洲欧美国产精品| 久久久精品电影| 国产精品视频久久久| 亚洲精品电影网在线观看| 久久久天堂国产精品女人| 亚洲欧美日韩直播| 国模精品视频一区二区| 国产不卡av在线| 亚洲国产精品电影| 国产精品第1页| 久久影院在线观看| 国产亚洲精品综合一区91| 国产精品国产三级国产aⅴ浪潮| 成人黄色在线观看| 亚洲999一在线观看www| 在线成人一区二区| 亚洲欧美日韩国产精品| 日韩国产高清视频在线| 亚洲精品视频免费在线观看| 久久大大胆人体| 97婷婷大伊香蕉精品视频| 最新69国产成人精品视频免费| 国产精品扒开腿做爽爽爽视频| 日韩精品中文字幕视频在线| 日韩欧美国产激情| 欧美激情综合色综合啪啪五月| 91禁外国网站| 亚洲欧美一区二区三区久久| 久久久久久国产| 最近2019好看的中文字幕免费| 久久99热精品| 国产精品久久av| 亚洲free嫩bbb| 91免费看片在线| 日韩电影大片中文字幕| 亚洲色图美腿丝袜| 欧美日韩久久久久| 北条麻妃一区二区在线观看| 亚洲免费av电影| 91亚洲精品视频| 日韩视频中文字幕| 欧美中文在线观看| 欧美国产在线电影| 国产精品自产拍高潮在线观看| 色偷偷噜噜噜亚洲男人的天堂| 日韩中文字幕网站| 免费av一区二区| 51精品国产黑色丝袜高跟鞋| 7777kkkk成人观看| 热门国产精品亚洲第一区在线| 亚洲一区国产精品| 国产一区在线播放| 亚洲乱码国产乱码精品精天堂| 中文字幕日韩av综合精品| 欧美激情在线视频二区| 亚洲人精品午夜在线观看| 色悠悠久久久久| 91九色精品视频| 奇米成人av国产一区二区三区| 日韩中文字幕在线播放| 777777777亚洲妇女| 日韩电影在线观看免费| 日韩欧美有码在线| 欧美激情2020午夜免费观看| 91精品国产91久久久久久最新| 成人久久18免费网站图片| 国产精品久久国产精品99gif| 亚洲新声在线观看| 91精品国产91久久久久久最新| 中文字幕亚洲无线码在线一区| 最近2019年手机中文字幕| 色偷偷91综合久久噜噜| 欧美高清性猛交| 久久国产一区二区三区| 亚洲最大成人免费视频| 亚洲最大av网| 日本sm极度另类视频| 国产成人激情视频| 国产精品视频网站| 日韩欧美国产黄色| 国产成一区二区| 久久精品成人欧美大片| 亚洲最大的av网站| 性色av香蕉一区二区| 欧美野外猛男的大粗鳮| 国产91精品网站| 亚洲a∨日韩av高清在线观看| 欧美成人网在线| 丝袜美腿精品国产二区| 一区二区三区视频在线| 欧美日韩aaaa| 亚洲性生活视频在线观看| 国产成人av在线| wwwwwwww亚洲| 高清欧美性猛交xxxx黑人猛交| 久久av在线看| 国产精品国语对白| 国产一区二区三区在线|