一篇质量非常高的关于二叉树遍历的帖子,转帖自http://noalgo.info/832.html

二叉树遍历(递归、非递归、Morris遍历)

2015年01月06日 |  分类:数据结构 |  标签:二叉树遍历 |  评论:8条评论 |  浏览:6,603次

二叉树遍历是二叉树中最基本的问题,其实现的方法非常多,有简单粗暴但容易爆栈的递归算法,还有稍微高级的使用栈模拟递归的非递归算法,另外还有不用栈而且只需要常数空间和线性时间的神奇Morris遍历算法,本文将对这些算法进行讲解和实现。

递归算法

二叉树节点使用以下数据结构进行表示,包括关键字、左儿子、右儿子属性和一个带默认参数的构造函数。
struct成员的默认属性为public,于是可以直接访问。

struct Node
{
int val;
Node *left, *right;
Node(int v = 0, Node *l = NULL, Node *r = NULL) : val(v), left(l), right(r) {}
};

二叉树的递归算法非常简单,设置好递归出口之后,根据遍历的顺序,对当前节点的左右子递归调用自身即可。其前序、中序、后序遍历的代码如下。

void preorder1(Node *root) //递归前序遍历
{
if (root == NULL) return;
printf("%d ", root->val);
preorder1(root->left);
preorder1(root->right);
} void inorder1(Node *root) //递归中序遍历
{
if (root == NULL) return;
inorder1(root->left);
printf("%d ", root->val);
inorder1(root->right);
} void postorder1(Node *root) //递归后序遍历
{
if (root == NULL) return;
postorder1(root->left);
postorder1(root->right);
printf("%d ", root->val);
}

栈模拟非递归算法

递归算法的本质是利用函数的调用栈进行,实际上我们可以自行使用栈来进行模拟,这样的算法空间复杂度为O(h),h为二叉树的高度。

前序遍历

首先把根节点入栈,然后在每次循环中执行以下操作:

  • 此时栈顶元素即为当前的根节点,弹出并打印当前的根节点。
  • 把当前根节点的右儿子和左儿子分别入栈(注意是右儿子先入栈左儿子后入栈,这样的话下次出栈的元素才是左儿子,这样才符合前序遍历的顺序要求:根节点->左儿子->右儿子)。

下面是代码实现。

void preorder2(Node *root)//非递归前序遍历
{
if (root == NULL) return; stack<Node *> stk;
stk.push(root);
while (!stk.empty())
{
Node *p = stk.top(); stk.pop();
printf("%d ", p->val);
if (p->right) stk.push(p->right);
if (p->left) stk.push(p->left);
}
}

后序遍历

因为后序遍历的顺序是:左子树->右子树->根节点,于是我们在前序遍历的代码中,当访问完当前节点后,先把当前节点的左子树入栈,再把右子树入栈,这样最终得到的顺序为:根节点->右子树->左子树,刚好是后序遍历倒过来的版本,于是把这个结果做一次翻转即为真正的后序遍历。而翻转可以通过使用另外一个栈简单完成,这样的代价是需要两个栈,但就复杂度而言,空间复杂度仍然是O(h)。

void postorder2(Node *root)//非递归后序遍历
{
if (root == NULL) return; stack<Node *> stk, stk2;
stk.push(root);
while (!stk.empty())
{
Node *p = stk.top(); stk.pop();
stk2.push(p);
if (p->left) stk.push(p->left);
if (p->right) stk.push(p->right);
}
while(!stk2.empty())
{
printf("%d ", stk2.top()->val);
stk2.pop();
}
}

中序遍历

中序遍历稍微复杂,使用一个指针p指向下一个待访问的节点,p初始化为根节点。在每次循环中执行以下操作:

  • 如果p非空,则把p入栈,p变为p的左儿子。
  • 如果p为空,说明已经向左走到尽头了,弹出当前栈顶元素,进行访问,并把p更新为其右儿子。

下面是代码实现。

void inorder2(Node *root)//非递归中序遍历
{
stack<Node *> stk;
Node *p = root;
while (p != NULL || !stk.empty())
{
if (p != NULL)
stk.push(p), p = p->left;
else
{
p = stk.top(); stk.pop();
printf("%d ", p->val);
p = p->right;
}
}
}

Morris遍历

Morris遍历的神奇之处在于它是非递归的算法,但并不需要额外的O(h)的空间,而且复杂度仍然是线性的。这样的算法最关键的问题是当访问完一棵子树后,如何回到其对于的根节点再继续访问右子树呢?Morris是通过修改二叉树某些节点的指针来做到的。

中序遍历

按照定义,在中序遍历中,对于一棵以root为根的二叉树,当访问完root的前驱节点后,需要回到root节点进行访问,然后再到root的右儿子进行访问。于是,我们可以每次访问到一棵子树时,找到它的前驱节点,把前驱节点的右儿子变为当前的根节点root,这样当遍历完前驱节点后,可以顺着这个右儿子回到根节点root。

但问题是修改了该前驱节点的右儿子后什么时候再改回来呢?

  • 当第一次访问以root为根的子树时,找到它的前驱pre,此时pre的右儿子必定为空,于是把这个右儿子设置为root,以便以后根据这个指针回到root节点。
  • 当第二次回到以root为根的子树时,再找到它的前驱pre,此时pre的右儿子已经被设置成了当前的root,这时把该右儿子重新设置成NULL,然后继续进行root的右儿子的遍历。于是完成了指针的修改。

在这样的情景下,寻找当前节点的前驱节点时,不仅需要判断其是否有右儿子,而且还要判断右儿子是否为当前的root节点,跟普通情况下的寻址前驱节点稍微多了一个条件。

由于在每次遍历一个节点的时候都需要寻找其前驱节点,而寻找前驱节点的时间一般与树的高度相关,这样看上去算法的复杂度应该为O(nlogn)才对。但由于其只需要对有左儿子的节点才寻找前驱,于是所有寻找前驱时走过的路加起来至多为一棵树的节点数,例如在下文的例子中,只需要对以下节点寻找前驱:

  • 节点4:寻找路径为:2-3
  • 节点2:寻找路径为:1
  • 节点6:寻找路径为:5

于是寻找前驱加上遍历的运算量之和至多为2*n,n为节点个数,于是算法的复杂度为仍然为O(n)。

其实现代码如下:

void inorder3(Node *root)//Morris中序遍历
{
Node *p = root;
while (p != NULL)
{
if (p->left == NULL)
printf("%d ", p->val), p = p->right;
else
{
Node *pre = p->left;
while (pre->right != NULL && pre->right != p)
pre = pre->right; if (pre->right == NULL) //第一次访问,修改pre的右儿子
pre->right = p, p = p->left;
else //第二次访问,改回pre的右儿子
pre->right = NULL, printf("%d ", p->val), p = p->right;
}
}
}

前序遍历

前序遍历和中序遍历类似,只是在遍历过程中访问节点的顺序稍有不同。即在第一次访问一棵子树时,就要先对根节点进行访问,于是printf输出语句被放到了if判断中第一次访问的分支中。

其代码如下:

void preorder3(Node *root)//Morris前序遍历
{
Node *p = root;
while (p != NULL)
{
if (p->left == NULL)
printf("%d ", p->val), p = p->right;
else
{
Node *pre = p->left;
while (pre->right != NULL && pre->right != p)
pre = pre->right; if (pre->right == NULL) //第一次访问,修改pre的右儿子
pre->right = p, printf("%d ", p->val), p = p->left;
else //第二次访问,改回pre的右儿子
pre->right = NULL, p = p->right;
}
}
}

后序遍历

后序遍历稍微复杂,但其遍历的基本顺序也是和前/中序遍历类似,只是在打印的时候做了一个翻转。考虑下文例子中的后序遍历结果:1 3 2 5 7 6 4。其可以这样进行拆分并进行解释:

  • 1:最左下角的结果节点
  • 3 2:节点2、3的倒序
  • 5:右儿子的最左下角的节点
  • 7 6 4:右边一列节点4、6、7的倒序

于是我们可以在中序遍历过程中,当第二次访问到一个节点时,把它的左儿子到它的前驱节点的路径上的节点进行翻转打印,即可得到后序遍历的结果。但这样的话根节点到最右下角那一列会访问不到,增加一个辅助节点作为新的根节点,把原有根节点作为其左儿子即可。

其实现代码如下:

void reverse(Node *p1, Node *p2)//使用right指针翻转p1到p2节点
{
if (p1 == p2) return; Node *pre = p1, *p = p1->right;
while (true)
{
Node *tem = p->right;
p->right = pre;
if (p == p2) break;
pre = p, p = tem;
}
} void print(Node *p1, Node *p2)//逆序打印p1到p2节点
{
reverse(p1, p2);
for (Node *p = p2; ; p = p->right)
{
printf("%d ", p->val);
if (p == p1) break;
}
reverse(p2, p1);
} void postorder3(Node *root)//Morris后序遍历
{
Node dummy(-1, root, NULL), *p = &dummy;
while (p != NULL)
{
if (p->left == NULL)
p = p->right;
else
{
Node *pre = p->left;
while (pre->right != NULL && pre->right != p)
pre = pre->right; if (pre->right == NULL)
pre->right = p, p = p->left;
else
pre->right = NULL, print(p->left, pre), p = p->right;
}
}
}

代码测试

在下面的主函数中,我们对以下简单的二叉树进行测试。

     4
/ \
2 6
/ \ / \
1 3 5 7

主函数代码如下:

#include <cstdio>
#include <stack>
using namespace std; int main()
{
Node a1(1), a3(3), a5(5), a7(7);
Node a2(2, &a1, &a3), a6(6, &a5, &a7);
Node a4(4, &a2, &a6); preorder1(&a4); printf("\n"); //4 2 1 3 6 5 7
preorder2(&a4); printf("\n"); //4 2 1 3 6 5 7
preorder3(&a4); printf("\n"); //4 2 1 3 6 5 7
printf("\n"); inorder1(&a4); printf("\n"); //1 2 3 4 5 6 7
inorder2(&a4); printf("\n"); //1 2 3 4 5 6 7
inorder3(&a4); printf("\n"); //1 2 3 4 5 6 7
printf("\n"); postorder1(&a4); printf("\n"); //1 3 2 5 7 6 4
postorder2(&a4); printf("\n"); //1 3 2 5 7 6 4
postorder3(&a4); printf("\n"); //1 3 2 5 7 6 4
}

除非注明,文章来自NoAlGo博客原创,转载请保留链接:二叉树遍历(递归、非递归、Morris遍历)

 

二叉树遍历,递归,栈,Morris的更多相关文章

  1. 二叉树中序遍历,先序遍历,后序遍历(递归栈,非递归栈,Morris Traversal)

    例题 中序遍历94. Binary Tree Inorder Traversal 先序遍历144. Binary Tree Preorder Traversal 后序遍历145. Binary Tre ...

  2. 二叉树的遍历(递归,迭代,Morris遍历)

    二叉树的遍历: 先序,中序,后序: 二叉树的遍历有三种常见的方法, 最简单的实现就是递归调用, 另外就是飞递归的迭代调用, 最后还有O(1)空间的morris遍历: 二叉树的结构定义: struct ...

  3. 数据结构(3) 第三天 栈的应用:就近匹配/中缀表达式转后缀表达式 、树/二叉树的概念、二叉树的递归与非递归遍历(DLR LDR LRD)、递归求叶子节点数目/二叉树高度/二叉树拷贝和释放

    01 上节课回顾 受限的线性表 栈和队列的链式存储其实就是链表 但是不能任意操作 所以叫受限的线性表 02 栈的应用_就近匹配 案例1就近匹配: #include <stdio.h> in ...

  4. 额外空间复杂度O(1) 的二叉树遍历 → Morris Traversal,你造吗?

    开心一刻 一天,有个粉丝遇到感情方面的问题,找我出出主意 粉丝:我女朋友吧,就是先天有点病,听不到人说话,也说不了话,现在我家里人又给我介绍了一个,我该怎么办 我:这个问题很难去解释,我觉得一个人活着 ...

  5. 数据结构二叉树的递归与非递归遍历之java,javascript,php实现可编译(1)java

    前一段时间,学习数据结构的各种算法,概念不难理解,只是被C++的指针给弄的犯糊涂,于是用java,web,javascript,分别去实现数据结构的各种算法. 二叉树的遍历,本分享只是以二叉树中的先序 ...

  6. C++编程练习(17)----“二叉树非递归遍历的实现“

    二叉树的非递归遍历 最近看书上说道要掌握二叉树遍历的6种编写方式,之前只用递归方式编写过,这次就用非递归方式编写试一试. C++编程练习(8)----“二叉树的建立以及二叉树的三种遍历方式“(前序遍历 ...

  7. 二叉树——遍历篇(递归/非递归,C++)

    二叉树--遍历篇 二叉树很多算法题都与其遍历相关,笔者经过大量学习.思考,整理总结写下二叉树的遍历篇,涵盖递归和非递归实现. 1.二叉树数据结构及访问函数 #include <stdio.h&g ...

  8. c++实现二叉树层序、前序创建二叉树,递归非递归实现二叉树遍历

    #include <iostream> #include <cstdio> #include <stdio.h> #include <string> # ...

  9. 二叉树的创建、遍历(递归和非递归实现)、交换左右子数、求高度(c++实现)

    要求:以左右孩子表示法实现链式方式存储的二叉树(lson—rson),以菜单方式设计并完成功能任务:建立并存储树.输出前序遍历结果.输出中序遍历结果.输出后序遍历结果.交换左右子树.统计高度,其中对于 ...

随机推荐

  1. 【BZOJ】1665: [Usaco2006 Open]The Climbing Wall 攀岩(spfa)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1665 这题只要注意到“所有的落脚点至少相距300”就可以大胆的暴力了. 对于每个点,我们枚举比他的x ...

  2. Java序列化(转载)

    引用自:http://developer.51cto.com/art/201506/479979_all.htm 关于 Java 对象序列化您不知道的 5 件事 数年前,当和一个软件团队一起用 Jav ...

  3. Tweened Animations 渐变动作

    Tweened Animations 渐变动作 Animations分两类: 第一类:渐变的(Tweened): 淡入淡出(Alpha),旋转(Rotate),移动(Translate),缩放(Sca ...

  4. 主干(trunk)、分支(branch )、标记(tag) 用法示例 + 图解

    以svn为例,git的master相当于trunk,dev分支相当于branches --------------------------------------------------------- ...

  5. Android中的&lt;include&gt;标签和&lt;merge&gt;标签

    android开发中经常会碰到某一个布局的复用:直接拷贝粘贴并不是是有效的策略,这时候就能够借助<include>标签和<merge>标签来完毕. 官方文档: http://d ...

  6. mysql数据库sql优化——子查询优化

    1.什么是子查询.表关联查询: 子查询:是指在主sql语句中的select或where子句中使用select查询语句:select a.name,(select b.name from b where ...

  7. 【BZOJ4260】Codechef REBXOR Trie树+贪心

    [BZOJ4260]Codechef REBXOR Description Input 输入数据的第一行包含一个整数N,表示数组中的元素个数. 第二行包含N个整数A1,A2,…,AN. Output ...

  8. 【Python基础】装饰器的解释和用法

    装饰器的用法比较简单,但是理解装饰器的原理还是比较复杂的,考虑到接下来的爬虫框架中很多用到装饰器的地方,我们先来讲解一下. 函数 我们定义了一个函数,没有什么具体操作,只是返回一个固定值 请注意一下缩 ...

  9. 从global到mooncake迁移SQL Azure

    之前遇到了问题,在此备注一下: 因为两个环境基本上可以认为是隔离的,所以迁移过程基本上只有通过导出.导入的方式(也是官方推荐的方式): 1.从global上进行数据库的export操作(扩展名bacp ...

  10. 通过手机浏览器打开APP或者跳转到下载页面.md

    目录 通过手机浏览器打开APP或者跳转到下载页面 添加 schemes 网页设置 参考链接 通过手机浏览器打开APP或者跳转到下载页面 以下仅展示最简单的例子及关键代码 由于硬件条件有限,仅测试了 A ...