這篇談談遞歸程序設計的問題。從取名上來講是想刻意區別內容的側重點不一樣。上一篇是構造,其重點是從遞歸程序的自身結構出發,試圖用一種比較直觀的方法來完成遞歸程序的構造。這篇的重點是設計,其中的區別在於,此次是從問題自己的結構出發來完成遞歸程序的開發任務。上一篇中介紹的方法,比較簡單直觀,八股文的意味很是濃郁,而且還有一個比較大的缺點,那就是在實際使用時每每會受制與方法自己而不能解決有必定難度的問題。實際上遞歸是一種客觀存在的現象,遞歸的描述問題是對客觀世界的一種認識。本文從對問題的認識,描述和分析這些步驟來介紹一下如何完成遞歸程序的設計。node
一.問題的描述方法—巴克斯範式
在我上大學的時候,巴克斯範式出如今編譯原理的課程中,是用來定義文法的。在數據結構課程中並無介紹巴克斯範式。可是在實踐中發現,這個範式對完成遞歸程序很是有幫助。由於根據巴克斯範式,咱們能夠自動生成詞法分析程序,而這些程序就包含了各類遞歸程序及其調用。這裏不打算從編譯的角度來介紹巴克斯範式,而是借用巴克思範式的思想來幫助完成遞歸程序的開發。因此規範和嚴謹程度是遠不如巴克斯範式的。程序員
先從一個具體的例子開始引入巴克斯範式。現將前一篇「遞歸程序構造」中關於二叉樹的定義再次描述以下:
n(n≥0)個結點的有限集,它或者是空集(n=0),或者由一個根結點及兩棵互不相交的、分別稱做這個根的左子樹和右子樹的二叉樹組成。數據結構
這是一個用嚴謹的天然語言描述的定義,下面用另外一種形式等價的來描述這個定義:
<二叉樹> = null | 節點<左子樹><右子樹>
<左子樹> = <二叉樹>
<右子樹> = <二叉樹>測試
上面的定義由三行文本組成,每一行文本是一個等式,稱之爲規則,因此一共是三條規則。等號的左邊稱爲非終結符,等號的右邊表示這個非終結符的組成內容。通常非終結符用「<」和「>」兩個符號包圍。這些是巴克斯範式中的內容。spa
以第一條規則爲例,等號的右邊首先是null,這表示空,這等效於二叉樹定義中的「它或者是空集(n=0)」這段文字。最右邊的「節點<左子樹><右子樹>」表示二叉樹有一個節點及其所屬的左子樹和右子樹組成,這個描述二叉樹概念中的「由一個根結點及兩棵互不相交的、分別稱做這個根的左子樹和右子樹」這些文字對應。第二條和第三條規則表示左子樹和右子樹都是一棵二叉樹,這個和定義中的最後幾個字「二叉樹組成」相對應。最後看一下第一條規則中的字符「|」。這個字符在巴克斯範式中表示或,其含義是該字符的左邊或者右邊只能取一個。這個符號和定義中「或者」這個詞相對應。至此能夠確認上述三條規則對二叉樹的描述和定義對二叉樹的描述是等價的。設計
有了這個等價的巴克斯範式版本的二叉樹定義,咱們就可使用處理巴克斯範式的方式,或者說可使用編譯原理中詞法分析的思路來完成遞歸程序的開發了。
二. 從規則集轉換獲得遞歸程序
前一篇遞歸程序構造中使用了遍歷二叉樹的例子,這裏仍是使用相同的例子,看看從規則集是如何完成遍歷二叉樹的遞歸程序的開發的。事實上從規則集合轉換獲得遞歸程序的步驟是很簡單的,也是能夠自動化的。咱們徹底能夠開發一個程序,經過掃描規則集自動生成遞歸程序。下面介紹手工完成的具體步驟。code
首先爲每個非終結符定義方法,每個方法只用來處理對應的非終結符。上述三條規則中包含了三個非終結符,因此咱們須要三個方法,列出以下:blog
// 對應非終結符<二叉樹>,表示遍歷二叉樹
VisitBinaryTree()
// 對應非終結符<左子樹>,表示遍歷左子樹
VisitLeftBinaryTree()
// 對應非終結符<右子樹>,表示遍歷右子樹
VisitRightBinaryTree()遞歸
如今咱們獲得了三個方法,而後給這些方法定義參數。因爲三個方法都是須要遍歷,因此二叉樹的根節點必須是方法的參數,不然遍歷沒法完成。增長參數後方法以下所示:
// node是二叉樹的根節點
VisitBinaryTree(Node node)
// node是左子樹的根節點
VisitLeftBinaryTree(Node node)
// node是右子樹的根節點
VisitRightBinaryTree(Node node)開發
第二步是在各個方法中對指定的非終結符的右邊內容進行處理。首先看第一條規則。因爲規則中有一個「|」符號,表示右邊兩部份內容不能同時處理,因此顯然須要一個if語句作判斷,而後分狀況分別處理兩部分的內容。先看「|」左邊的內容null,這個含義是二叉樹爲空,若是是這樣,那麼就無需遍歷,因此對應的代碼應該以下:
if (node == null) return;
若是二叉樹不爲空,那麼須要處理「|」右邊的內容,這些內容分別是根節點,左子樹和右子樹。對於根節點的處理能夠抽象的使用一個方法ProcessNode來表示,然後面的左子樹和右子樹是非終結符,能夠直接調用處理改非終結符的方法就能夠了。修改完後代碼以下所示:
if (node == null) return; else { ProcessNode(node); VisitLeftBinaryTree(node.LeftTree); VisitRightBinaryTree(node.RightTree); }
對於第二和第三條規則,因爲右邊只有一個非終結符,因此其內部的代碼就是直接調用對應的處理該非終結符的方法就能夠了,完整的代碼以下所示:
public void VisitBinaryTree(Node node) { if (node == null) return; else { ProcessNode(node); VisitLeftBinaryTree(node.LeftTree); VisitRightBinaryTree(node.RightTree); } } public void VisitLeftBinaryTree(Node node) { VisitBinaryTree(node); } public void VisitRightBinaryTree(Node node) { VisitBinaryTree(node); }
到這裏代碼就完成了,並且仍是一個間接遞歸的版本。下面對這些規則和代碼再作一個討論,讓問題更明晰透徹一些。
三. 若干細節討論
第一個須要討論的就是間接遞歸的問題。咱們熟知的遍歷二叉樹的遞歸程序都是直接遞歸,這裏獲得倒是一個間接遞歸。其緣由不是介紹的方法有問題,而是上述規則的設計問題。能夠看到第二條和第三條規則表達含義就是<左子樹>和<右子樹>也是一棵二叉樹。補充這個規則的用意是爲了體現二叉樹定義中出現的文字「分別稱做這個根的左子樹和右子樹的二叉樹組成」,這句話代表左子樹和右子樹也是二叉樹,因此加入了上述規則。
既然非終結符<左子樹>,<右子樹>和非終結符<二叉樹>是等價的,那麼咱們能夠將規則一右邊出現的<左子樹>,<右子樹>直接用<二叉樹>代替。這樣規則一就以下所示:
<二叉樹> = null | 根節點<二叉樹><二叉樹>
仍是使用相同的推導方法,此次咱們能夠獲得直接遞歸版本的二叉樹遍歷程序,以下所示:
public void VisitBinaryTree(Node node) { if (node == null) return; else { ProcessNode(node); VisitBinaryTree(node.LeftTree); VisitBinaryTree(node.RightTree); } }
第二點是須要強調一下推導的步驟。我相信有些讀者已經發現了間接遞歸的問題,而且也可以直接修改代碼,將其改成直接遞歸。好比直接經過讀代碼就能夠發現方法VisitLeftBinaryTree和VisitRightBinaryTree什麼都沒幹,只是調用了方法VisitBinaryTree,因此就能夠直接調用VisitBinaryTree從而替換掉對方法VisitLeftBinaryTree和VisitRightBinaryTree的調用。這樣作是能夠的,尤爲在這個具體的簡單問題上。可是當規則足夠多,而且足夠複雜時問題就不太可能如此直白,如此易於觀察並獲得結論。因此強烈推薦的作法是先修改規則,而後再根據規則推導出程序,這是工程化的作法。
第三點,不是須要給全部的非終結符都定義方法,而後再重構,若是能看清問題那麼能夠直接寫出最終的代碼。這也是不太規範的一個地方。
第四點是強調一下這裏用到的規則和巴克斯範式的差別。前文已經提到巴克斯範式是一個規範而嚴謹的定義,而這裏使用的規則只是借用了巴克斯範式的思路來描述問題,不是很規範和嚴謹。好比在巴克斯範式中規則一的右邊不只表示<二叉樹>能夠由根節點,<左子樹>和<右子樹>組成,同時也表示這三者前後出現順序。可是這裏使用的規則,僅僅表示組成內容。或者說僅僅想表示二叉樹的結構,從而和二叉樹定義的描述等價。注意二叉樹定義中的描述沒有規定左子樹和右子樹出現的前後順序。因此在VisitBinaryTree方法中對處理內容的前後沒有限制。由此能夠推導出遍歷二叉樹的不一樣版本,只須要改變調用處理非終結符方法的前後順序便可。
固然根據具體的問題,能夠給規則加入其它的變化和含義,以便於等價的描述問題。這其中的取捨和尺度的把握是體現問題分析和程序設計能力的地方。下面再舉一個例子來講明這個問題。
四. 規則的設計
從前文的介紹能夠看出,只要獲得了規則,那麼推導出遞歸程序是很是容易的。
這樣開發遞歸程序的問題就轉化爲如何獲得規則了,也就是規則的設計問題。個人建議是多練習,多實踐。由於沒有一個固定的作法可讓咱們比較容易的獲得規則集,因此經過練習和實踐來提高問題的分析能力和程序的設計能力就是關鍵和捷徑了。可是在有些時候思考問題的技巧對咱們也是有輔助幫助做用的。這裏舉一個例子來講明一下,想以此擴展一下讀者的思路。這個例子是:逆轉字符串。
如何逆轉一個字符串是很是容易的,可是如何寫出遞歸版本的代碼呢?請注意寫出遞歸的關鍵是發現問題的遞歸結構,這個遞歸結構是事物自己的特性,而不是隻指咱們須要對該事物執行什麼樣的操做。這就是說逆轉操做不是關鍵,關鍵是如何找到字符串的遞歸結構或者說如何找到字符串的遞歸定義。固然這個能力須要在實踐中逐步培養。下面直接給出規則版本的定義:
<字符串> = null | <字符> | <字符><字符串><字符>
<字符> = …
先看第一條規則的右邊,null表示空串,<字符>表示只有一個字符的字符串,最後部分表示有多個字符的字符串。第二條規則定義了<字符>能夠是哪些字符,好比’a’,’b’,’c’或者’1’,’2’,’3’,之類的,因爲比較多就不全寫了。而後使用上文介紹的方法來推導,首先給<字符串>定義方法,而後分別處理右邊的內容,代碼以下所示:
public string ReverseString(string str, int start, int end) { if (start >= end) return str; else if (str == null || str.Length < 1) return str; else if (str.Length == 1) return str; else { char temp = str[start]; str[start] = str[end]; str[end] = temp; return ReverseString(str, start + 1, end - 1); } }
方法的調用以下:
ReverseString(str, 0, str.Length - 1);
ReverseString中的第一個if是加入的遞歸出口判斷,這不能從規則推導出來,須要本身加。關於遞歸的出口能夠閱讀前一篇:遞歸程序構造。另外還能夠修改規則以下:
<字符串> = null | <字符> | <字符><字符串>
<字符> = …
依據這個規則也是能夠推出遞歸程序的。
關於遞歸程序還有一些話題能夠講,好比數學概括法,遞推,遞歸程序的測試等等。這些擴展的話題留在之後再介紹了,此次就寫到這裏了。最後推廣一下個人羣244054966,歡迎正在創業的程序員加入。入羣時請寫明「csdn博文」,不然不加。