多叉树的前序遍历_二叉树的非递归遍历的思考
封面圖來自wikipedia
1 簡介
二叉樹的深度優先遍歷(前序遍歷、中序遍歷、后序遍歷)是一個比較基本的操作。如果使用遞歸的做法,很容易寫出相應的程序;而如果使用非遞歸的做法,雖然也能寫出相應的代碼,但是由于三種非遞歸的遍歷沒有統一的格式,比較難記住。在這里,介紹一種統一格式的非遞歸寫法。
2 遞歸做法
先介紹一下二叉樹的三個深度優先遍歷的基本概念:
- 前序遍歷:先訪問根節點,然后前序遍歷左子樹,最后前序遍歷右子樹。
- 中序遍歷:先中序遍歷左子樹,然后訪問根節點,最后中序遍歷右子樹。
- 后序遍歷:先后序遍歷左子樹,然后后序遍歷右子樹,最后訪問根節點。
根據概念很容易寫出對應的遞歸遍歷代碼
2.0 數據結構定義
struct TreeNode{TreeNode* left;TreeNode* right;int val; };2.1 前序遍歷
vector<int> preorder(TreeNode* root, vector<int>& res) {if (!root) return res;res.push_back(root->val);preorder(root->left, res);preorder(root->right, res);return res; }2.2 中序遍歷
vector<int> inorder(TreeNode* root, vector<int>& res) {if (!root) return res;inorder(root->left, res);res.push_back(root->val);inorder(root->right, res);return res; }2.3 后序遍歷
vector<int> postorder(TreeNode* root, vector<int>& res) {if (!root) return res;postorder(root->left, res);postorder(root->right, res);res.push_back(root->val);return res; }3 非遞歸做法
先列出代碼,后面再寫下代碼的思想以及自己的理解。
可以看出三種遍歷的寫法,除了三句執行入棧的代碼,順序不一樣,其他都是一致的,實現了格式的統一。
3.1 前序遍歷
void preorder(TreeNode *root, vector<int>& res) {stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));s.push(make_pair(root, true));}} }3.2 中序遍歷
void inorder(TreeNode *root, vector<int>& res) {stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root->right, false));s.push(make_pair(root, true));s.push(make_pair(root->left, false));}} }3.3 后序遍歷
void postorder(TreeNode *root, vector<int>& res) {stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root, true));s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));}} }4 算法思想
4.1 簡要說明
下面以前序遍歷為例子,簡單說說我自己的理解。先總結下自己的理解:
前序遍歷的規則:“根節點-左子樹遞歸-右子樹遞歸”,等價于下面兩個規則
4.2 詳細解釋
接下來嘗試對上面的話解釋一下。
回看前序遍歷的概念,可以發現它制定了遍歷的規則:先是根節點,然后遞歸遍歷左子樹,最后遞歸遍歷右子樹,我們表示成“根節點-左子樹-右子樹”。這個好像不太直觀,我們想想這個規則能不能表示成其他等價規則。首先想到的一點是:
- (a) 對于樹中的每一個節點,它以及它的兩個子節點的訪問順序必須是 “節點-左子節點-右子節點”。
這個很容易理解。對于一個節點來說,它的左子節點是左子樹的根節點,右子節點是右子樹的根節點,既然要求 “節點-左子樹-右子樹”,那么必要條件就有 “節點-左子節點-右子節點”。其次,遞歸遍歷使得對于每個節點,都有這樣的要求。
但是這個只是必要條件,并不能唯一確定節點訪問順序。舉個例子,假設有下面一棵二叉樹,那么它的前序遍歷是 “1-2-4-5-3-6-7”。假設我們只是規定了 “節點-左子節點-右子節點” 這個規則,那么我們便規定了下面三個序列的次序:“1-2-3”、“2-4-5”、“3-6-7”,(即:3 必須在 2 之后訪問,2 必須在 1 之后訪問...)然而我們沒有規定這三個序列之間的相對次序,那么符合條件的次序就有很多了,比如 “1-2-3-4-5-6-7”、“1-2-3-6-7-4-5”,“1-2-4-3-6-5-7” 等等。
圖1 - 二叉樹例子仔細思考了一下,出現上面這些序列的原因是:我們沒有規定左子樹 “2-4-5” 與右子樹 “3-6-7” 兩個子樹之間的相對順序。比如第一個例子 “1-2-3-4-5-6-7”,在左子樹只訪問根節點 “2” 之后,就去訪問右子樹的根節點 “3”,之后再訪問左子樹剩下的部分,最后再訪問右子樹剩下的部分。
我們知道正確的做法是:先訪問完所有左子樹的節點,再訪問所有右子樹的節點。于是得到第二條規則:
- (b) 對于樹中的每一個節點,只有當左子樹的節點全部訪問完,才能訪問右子樹的節點。
有了上述兩條規則,遍歷順序便被唯一確定了。當然我不知道怎么嚴謹地證明這個結論。
回頭再思考一下上面兩個規則,第一個規則規定了節點與它的兩個子節點(子樹)之間的順序,而第二個規則規定了兩個子樹之間的順序。
5 代碼對算法的實現
來看看代碼怎么實現我們上面說的兩點規則的。為了方便,我把代碼搬了下來。
// 前序遍歷 void preorder(TreeNode *root, vector<int>& res) {stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));s.push(make_pair(root, true));}} }- (1) 首先注意到,代碼使用了棧,在元素入棧的時候,三條語句確定了一個節點與它的兩個子節點之間的順序。對所有的節點進行這個操作,便實現了規則(a)。
- (2) 由于棧的 “后進先出” 特性,根據入棧的順序,相比左子節點,右子節點會在棧更深的位置,所以后續會先訪問左子節點。訪問左子節點的時候,會將它的子節點壓入棧,因此所有的左子樹的節點都會比原本右子節點更先訪問到。因此,棧的本身結構保證了所有的節點都執行了規則(b)。
- (3) 代碼中對每個節點使用了一個標記位,開始第一次入棧時,都標記為 false,只有當第二次入棧時,節點以及它的子節點順序確定,才被標記成 true。換句話說,false 表示了當前節點與其子節點的順序還沒確定下來,true 表示當前節點與其子節點的順序已經確定下來,因此可以被訪問了。這個保證了樹中的 “所有” 節點都執行了規則 (a)。
下面是算法執行的示意圖,便于大家理解算法流程。
圖2 - 前序遍歷流程圖7 總結
我們將樹的遍歷的規則轉化為兩條等價的規則,其中一條確定了節點與子節點之間的遍歷順序,另一條確定了子節點之間的遍歷順序。之后,借助棧的特性,實現了上述兩條規則,即實現了樹的遍歷。
算法的優點是將遍歷順序與算法邏輯之間的分離,于是使用哪一種遍歷順序,不影響算法本身的邏輯。換一句話說,不管是哪一種遍歷順序,代碼的整體框架是一樣的,只需稍微改變跟順序相關的幾句代碼,就ok了。除此之外,很容易推廣到多叉樹。
算法的缺點嘛,對于每個節點都需要入棧兩次,同時對于每個節點都需要分配一個標志位,但是我覺得瑕不掩瑜。
8 參考資料
在寫作的過程中,參考了以下一些資料,在此表示感謝
https://blog.csdn.net/sdulibh/article/details/50573036自己水平有限,哪里寫錯了,歡迎指正,虛心接受大家的意見。
如果覺得我的文章對你有幫助,歡迎點贊、收藏、關注呀,以激勵我更好地分享呀~
總結
以上是生活随笔為你收集整理的多叉树的前序遍历_二叉树的非递归遍历的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 买套房子多少钱啊?
- 下一篇: 为什么要返回softmax_为什么sof