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