我是很喜歡算法的,打算寫一個數據結構與算法系列的記錄,不少都用到了遞歸,因此這星期打算說說你們都以爲很簡單,可是我以爲並不簡單的遞歸,畢竟不少人對遞歸的認識也就是本身調用本身,可是我以爲這並不深刻,或者說這並非遞歸的實質,或者說解釋遞歸解釋的並很差。
首先咱們準備一顆二叉樹:算法
咱們須要定義一些術語,咱們所使用的數據結構由結點組成,結點包含的連接能夠爲空也能夠指向其餘結點.
在二叉樹中,每一個結點只能有一個父節點(只有一個例外,也就是根節點,它沒有父節點),並且每一個結點只有兩個連接, 分別指向本身的左子結點和右子結點.儘管連接指向的是結點,但咱們能夠將每一個連接看作指向了另外一顆二叉樹, 而這棵樹的根節點就是被指向的節點.所以,咱們能夠將二叉樹定義爲一個空連接 ,或者是一個有左右兩個連接的結點,每一個連接都指向一顆(獨立的)二叉樹。----《算法》第四版。
前中後序遍歷(這是本篇文章的要討論的核心問題之一,由前中後序遍從來討論遞歸,而後再到荷蘭國旗問題和快速排序)
結點代碼:數組
public class TreeNode { int data; TreeNode leftNode; TreeNode rightNode; public TreeNode(int data) { this.data = data; } }
二叉樹:數據結構
public class BinaryTree { TreeNode root; public BinaryTree(TreeNode root) { this.root = root; } /** * 前序遍歷 */ public void frontShow(){ } /** * 中序遍歷 */ public void middleShow(){ } /** * 後序遍歷 */ public void afterShow(){ } }
雖然百度不招人喜歡,可是百度百科的一些詞條編輯的仍是不錯的.
從二叉樹的遞歸定義可知,一棵非空的二叉樹由根結點及左、右子樹這三個基本部分組成。所以,在任一給定結點上,能夠按某種次序執行三個操做:less
(1) 訪問結點自己(N),
(2) 遍歷該結點的左子樹(L),
(3) 遍歷該結點的右子樹(R)。函數
前序遍歷: 根節點 左子樹 右子樹
中序遍歷: 左子樹 根節點 右子樹
後序遍歷: 左子樹 右子樹 根節點工具
其實組合的話是有六種的,可是討論遞歸的話,前中後序就足夠了.this
咱們如今來任意的給一顆二叉樹,不用代碼。咱們來寫出前中後序遍歷的結果:spa
前序遍歷: 3 8 5 2 6 1 7
中序遍歷: 5 8 2 3 1 6 7
後序遍歷: 5 2 8 1 7 6 3翻譯
前中後序遍歷的進一步解釋:3d
當我到達一個結點,先打印出來,再去訪問其餘子結點就是前序遍歷 當我到達一個結點,不是先打印當前結點,而是接着訪問該節點的左子節點,某個節點沒有子節點(或者說子結點是null) 我就打印當前節點,這就是中序遍歷 之前序遍歷爲例,我首先打印根節點,而後判斷它的左子節點是否爲空,若是非空,打印該節點,而後接着進行這樣的操做, 翻譯成代碼就是這樣的:
在BinaryTree中的代碼:
public void frontShow(){ System.out.println(root.data); if (root.leftNode != null){ root.leftNode.frontShow(); } if (root.rightNode != null){ root.rightNode.frontShow(); } }
結點中的代碼也是這樣的,
public void frontShow(){ System.out.println(root.data); if (root.leftNode != null){ root.leftNode.frontShow(); } if (root.rightNode != null){ root.rightNode.frontShow(); } }
中後序代碼把打印順序調整一下就能夠了,其實這個很好理解,不是多麼酷炫的事情.
如下一個問題在我我的看來纔是值得值得思考的,就是程序在遍歷二叉樹的過程,每一個節點通過了幾回。
其實這個問題,也是十分簡單,你按着程序走就能夠了.
首先來到3,接着來到8,接着來到5,而後發現5的左右子節點都是空的,而後回到....
這裏再介紹另外一種在形式上略有不一樣的前序遍歷方式:
public void frontShow(TreeNode root){ if (root == null){ return; } System.out.println(data); frontShow(root.leftNode); frontShow(root.rightNode); }
這種形式上的前中後序遍歷:
每一個節點會到達三次,前序遍歷就是第一次碰到的時候打印出來,中序是第二次,後序是第三次
各位有興致的話能夠本身寫一寫,畫一畫.
請注意上面那張圖,雖然他十分的醜,可是你們不以爲它像一個棧嗎? 遞歸就是程序在幫你壓棧.
那既然遞歸是系統在幫你壓棧,那非遞歸版我就本身壓棧,不讓程序幫咱們壓棧就能夠了嗎?
咱們先用下圖來演示非遞歸版的遍歷:
許多時候,問題的規模是會影響咱們的判斷,爲了解決問題,咱們能夠先把問題的規模先下降到看起來容易解決的地步,再試着去解決問題,若是解決了, 咱們再逐步的擴大問題的規模
荷蘭國旗問題: 給定一個數組arr和一個數num,請把小於num的數放在num的左邊,大於num的數放在num右邊。
等於num的數放在中間
例子: 輸入: arr [5,7,5,8,1,9,10] ,num =5
輸出: [1, 5, 5, 8, 9, 10, 7]]
思路:
> 準備一個變量less和more。 1. less 區域內(即數組下標 <= less)所有是小於num的, 2. more區域內(即數組下標>more)所有是大於num 3. 咱們須要一個變量來幫助咱們遍歷數組。 在一開始的時候,less = -1 , more = 數組的長度。 這表明剛開始這兩個區域還不存在 當arr[curr] < num 的時候, less區域的下一個數和arr[curr]交換。而後less右移一個位置, curr右移一個位置 當arr[curr] > num 的時候, more區域的上一個數和arr[curr]交換。而後more左移一個位置, 此時咱們是沒法保證從more區域的上一個數,到底是大於num仍是小於num, 所以咱們仍然須要將num和這個數進行比較. 以上的思路翻譯成代碼是用循環來完成的,那麼循環結束的條件是什麼呢? curr的左側是less區域, 當排序完成的時候, (less,curr]區域爲等於num的區域, 可是當curr + 1 = more 時,這個時候arr[curr+1]仍是未進行判斷的, 咱們仍然須要對arr[curr+1] 和num進行比較
那這跟快速排序有什麼問題呢?咱們來思考一下快速排序(這裏討論是基礎版的快速排序)。快速排序是一種分治的排序算法,它將一個數組分紅兩個子數組,將兩部分獨立的排序。荷蘭國旗就至關於快速排序中的切分過程。快速排序的關鍵就在於這個切分過程。
快速排序的思想就是:
首先根據num,將數組切分紅兩個子數組,而後再對這兩個子數組進行切分,當子數組的數量小於2,默認數組即爲有序。 理解這個切分過程對快速排序來講十分的重要
這也是我在寫算法的時候考慮的一個問題,
答案有兩種: 1. 嚴謹一點的話就是數學建模,經過數學來證實你算法的正確性。 2. 利用計算機這個工具來印證咱們算法的正確性,意思就是試,找到一個雖然是正確的,可是時間複雜度不那麼好的正確算法。 經過隨機函數產生大量的樣本,比較你的算法和時間複雜度不那麼好的正確算法所產生的結果。這種方法
好比說對於排序算法,
首先利用隨機函數不斷的產生各類各樣的數組,而且向數組中填充隨機數.
而後將數組複製一份,給正確的算法用
而後比較兩個算法的結果.