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