二叉树的非递归遍历算法

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

⼆叉树的⾮递归遍历算法
我们都知道,⼆叉树常见的操作之⼀就是遍历,leetcode上很多对⼆叉树的操作都是基于遍历基础之上的。

⼆叉树的遍历操作分为深度优先遍历和⼴度优先遍历,深度优先遍历包括前序、中序、后序三种遍历⽅式;⼴度优先遍历⼜叫层序遍历。

这两种遍历⽅式都是⾮常重要的,树的层序遍历⼀般是没法通过递归来实现,其遍历过程需要借助⼀个队列来实现,本质上就是图的BFS的⼀种特例,可以⽤来求⼆叉树的⾼度、宽度等信息。

我们今天要讲的是⼆叉树的前、中、后序遍历的⾮递归算法。

⼆叉树前、中、后序遍历的递归算法形式上上是⾮常简洁明了的,只要你熟练使⽤递归这⼀⽅法,应该都能写出来。

⽽⾮递归算法则显得不那么容易。

但是⾮递归算法也⾮常重要,有⼀些题⽤⾮递归算法解会简单⼀点。

其实,我觉得我们⼈类的思维就是典型的⾮递归形式的,所以,要想实现⼀个⾮递归算法,其实就是把我们脑中的求解过程转换成相应的代码。

前序⾮递归
例如,对于下⾯这棵⼆叉树,我们要对其进⾏⾮递归前序遍历,该如何做?
前序遍历,就是先访问根节点,再访问左⼦树,再访问右⼦树,左⼦树和右⼦树的访问过程也是这样递归进⾏。

那么我们⼀开始是依次访问1、2、4、6。

到了6的时候我们为什么要停下来呢?因为这时候我们发现,6的左⼦树为空,相当于它的左⼦树已经访问完了,按照前序遍历的定义,这个时候我们应该访问其右⼦树,我们发现6的右⼦树也为空,相当于它的右⼦树访问完了。

这个时候我们该怎么办呢?
没错,这个时候说明以6为根节点的⼦树都已经访问完了,我们应该往上回溯,回到6的⽗亲结点。

从这⾥,我们发现了⼏个问题:
我们每个结点都要经过3次
第⼀次是从该结点的根结点往下⾛到该结点的时候经过的
第⼆次是从其左⼦树返回到该结点时经过的
第三次是从其右⼦树返回到该结点时经过的。

前序遍历⼀开始是不断地往左下⾓⽅向⾛,每经过⼀个结点就访问它,直到到达最左下⾓位置停下来。

每当从左孩⼦返回到根结点的时候,需要判断⼀下右⼦树是不是为空,不为空的话才去访问右⼦树。

由于存在回溯,所以访问过程需要⽤到栈,⽤来记录我们依次经过的结点。

初步思考之后,我们可以写出如下代码:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode* p = root;
if (p) {
res.push_back(p->val);
s.push(p);
}
while (!s.empty()) {
p = s.top();
while (p->left) {
res.push_back(p->left->val);
s.push(p->left);
p = p->left;
}
if (p->right) {
res.push_back(p->right->val);
s.push(p->right);
p = p->right;
}
else {
p = s.top(); s.pop();
}
}
return res;
}
res数组是⽤来记录我们访问序列的,p指向我们当前经过的结点,s⽤来记录我们⾛过的结点序列。

这个代码乍⼀看好像和我们刚才思考的过程是⼀样的,但是你⼀运⾏就会出现问题,⾸先,我们来看⼀下这个循环
p = s.top();
while (p->left) {
res.push_back(p->left->val);
s.push(p->left);
p = p->left;
}
我们上⾯讲过,⼆叉树中每个结点都会经过三次,我们这⾥⼀直在往左下⾓⽅向⾛,每⾛⼀步记录⼀个结点到栈⾥⾯,并且访问该结点。

但是,你有没有发现,还是针对我们之前那棵树:
当我们访问完6回到4的时候,这个循环条件同样⼜成⽴了,它⼜会重复⼀次往左下⾓⾛,这并不是我们想要的,那么怎么办?
你可以想⼀下,这种情况是什么条件才会触发的?
没错,就是我们刚从左⼦树返回的时候,⾔外之意就是说,我们上⼀个访问的结点是左孩⼦。

所以为了避免这种情况发⽣,我们必须记录⼀下上⼀个访问过的结点是什么?
为此我们新加⼀个pre指针,指向上⼀个访问过的结点,这个结点需要我们每⾛⼀步都更新⼀次。

同时我们会发现,当我们从右⼦树返回的时候,我们也不能进⼊上⾯那个循环。

⽐如对于之前那个图,当我们从7返回到4的时候,我们不应该再次往4的左孩⼦⽅向⾛。

还有⼀个类似的问题,就是我们往右孩⼦⽅向⾛不仅仅需要右孩⼦⾮空,⽽且需要保证上⼀个访问过的结点不是右孩⼦。

于是,引⼊这个pre指针之后,我们之前的代码可以改成下⾯这样的
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode* p = root, * pre = p;
if (p) {
res.push_back(p->val);
s.push(p);
}
while (!s.empty()) {
p = s.top();
while (p->left && p->left != pre && p->right != pre) {//没有从左边返回,也没有从右边返回
res.push_back(p->left->val);
s.push(p->left);
pre = p;
p = p->left;
}
if (p->right && p->right != pre) {
res.push_back(p->right->val);
s.push(p->right);
pre = p;
p = p->right;
}
else {
pre = s.top();
s.pop();
}
}
return res;
}
这次我们的代码终于没有任何问题了。

讲完⼆叉树的前序⾮递归算法,我们再讲讲中序和后序⾮递归算法。

其实理解了前序,我们只需要做⼀些微⼩的修改就⾏,整体过程还是类似的。

中序⾮递归
还是以我们上⾯那棵树为例,我们来讲讲中序遍历过程
中序遍历,就是先访问左⼦树,再访问根节点,最后访问右⼦树,左⼦树和右⼦树的访问过程也是这样递归进⾏。

那么我们⼀开始访问的是哪个元素呢?
没错,由于我们先是对左⼦树进⾏递归,所以我们⼀开始访问的应该是最左下⾓位置的元素
这个和前序是有⼀点区别的,前序是往左下⾓⽅向⾛,每⾛⼀步访问⼀个结点。

中序是先⼀下⼦⾛到左下⾓,然后再访问最左下⾓位置的元素。

访问完6,接下来该怎么办?没错因为6的左⼦树⼀定为空,否则它就不可能是第⼀个被访问的,这时候我们应该判断⼀下6的右⼦树是不是为空,如果右⼦树不为空的话,我们要对它的右⼦树重复刚才的过程。

对于上⾯那棵树,它的右⼦树是空的,于是相当于以6为根结点的⼦树都已经访问完了,这个时候我们应该回到6的⽗亲结点,并且访问其⽗亲结点。

然后重复刚才的过程。

整个过程代码如下:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode* p = root, * pre = p;
if (p) s.push(p);
while (!s.empty()) {
p = s.top();
while (p->left && p->left != pre && p->right != pre) {//没有从左边返回,也没有从右边返回
s.push(p->left);
pre = p;
p = p->left;
}
if (p->right != pre) res.push_back(p->val);
if (p->right && p->right != pre) {
s.push(p->right);
pre = p;
p = p->right;
}
else {
pre = s.top();
s.pop();
}
}
return res;
}
可以和前⾯的前序遍历做个对⽐,我们发现,有以下⼏个区别:
对于下⾯这个循环,也就是在我们往左下⾓⽅向⾛的时候,我们并没有访问中途的结点
//中序
while (p->left && p->left != pre && p->right != pre) {//没有从左边返回,也没有从右边返回
s.push(p->left);
pre = p;
p = p->left;
}
前序遍历过程我们是⾛⼀步,就访问⼀个(访问的过程就是把该结点的值添加到res数组中去)
//前序
while (p->left && p->left != pre && p->right != pre) {//没有从左边返回,也没有从右边返回
res.push_back(p->left->val);
s.push(p->left);
pre = p;
p = p->left;
}
对于下⾯的代码,对应的是我们往右⼦树⽅向⾛之前,如果我们是不是从右孩⼦返回的(避免从右孩⼦返回后重复访问当前结点),我们应该先访问当前结点,再往它的右⼦树⽅向⾛。

if (p->right == pre) res.push_back(p->val);
if (p->right && p->right != pre) {
s.push(p->right);
pre = p;
p = p->right;
}
-对于下⾯的代码,对应的是我们⾛到了最左下⾓位置的时候,我们需要确认到达这个位置时,上⼀个访问过的结点不是右⼦树,因为从右⼦树返回的时候,我们当前结点是早就访问过的,中序遍历根结点访问顺序是优先于右⼦树。

else {
p = s.top(); s.pop();
if (p->right != pre) res.push_back(p->val);
pre = p;
}
整体来看,我们相⽐前序遍历只是在结点访问顺序上做了⼀些修改,整体代码逻辑是基本⼀样的。

后序⾮递归
后序遍历,是先访问左⼦树,再访问右⼦树,最后访问根结点,整体逻辑和上⾯前序以及中序都是⼀样的,我就直接解释⼀下后续⾮递归的代码了
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode* p = root, * pre = p;
if (p) s.push(p);
while (!s.empty()) {
p = s.top();
while (p->left && p->left != pre && p->right != pre) {//没有从左边返回,也没有从右边返回
s.push(p->left);
pre = p;
p = p->left;
}
if (p->right && p->right != pre) {
s.push(p->right);
pre = p;
p = p->right;
}
else {
p = s.top(); s.pop();
pre = p;
res.push_back(p->val);
}
}
return res;
}
由于是最后访问根结点,所以后序遍历的代码在形式上会更加简单。

我们想⼀想,在后序遍历过程中,什么时候才会输出⼀个结点,那么必然是访问完⼀个结点的左⼦树和右⼦树之后才会访问该结点。

那么当访问完⼀个结点的左右⼦树,我们上⾯的代码其实就是进⼊到了那个
else {
p = s.top(); s.pop();
pre = p;
res.push_back(p->val);
}
这个我就不多解释了,搞不清楚的可以去⾃⼰调试⼀下。

对于我们上⾯那棵树,我们的三种遍历序列依次为:。

相关文档
最新文档