二叉树的三种遍历方式(递归、非递归和Morris遍历)

原文:http://www.linuxidc.com/Linux/2015-08/122480.htm

二叉树遍历是二叉树的最基本的操作,其实现方式主要有三种:

  1. 递归遍历
  2. 非递归遍历
  3. Morris遍历

递归遍历的实现非常容易,非递归实现需要用到栈。而Morris算法可能很多人都不太熟悉,其强大之处在于只需要使用O(1)的空间就能实现对二叉树O(n)时间的遍历。

二叉树结点的定义

每个二叉树结点包括一个值以及左孩子和右孩子结点,其定义如下:

classTreeNode {

public:

int val;

TreeNode *left, *right;

TreeNode(int val) {

this->val = val;

this->left = this->right =NULL;

}

};

二叉树的遍历

二叉树的遍历,就是按照某条搜索路径访问树中的每一个结点,使得每个结点均被访问一次,而且仅被访问一次。常见的遍历次序有:

  1. 先序遍历:先访问根结点,再访问左子树,最后访问右子树
  2. 中序遍历:先访问左子树,再访问根结点,最后访问右子树
  3. 后序遍历:先访问左子树,再访问右子树,最后访问根结点

下面介绍,二叉树3种遍历方式的实现。

递归遍历

递归实现非常简单,按照遍历的次序,对当前结点分别调用左子树和右子树即可。

前序遍历

voidpreOrder(TreeNode *root) {

if(root == NULL)

return;

cout << root->val << endl;

preOrder(root->left);

preOrder(root->right);

}

中序遍历

voidinOrder(TreeNode *root) {

if(root == NULL)

return;

inOrder(root->left);

cout << root->val << endl;

inOrder(root->right);

}

后序遍历

voidpostOrder(TreeNode *root) {

if(root == NULL)

return;

postOrder(root->left);

postOrder(root->right);

cout << root->val << endl;

}

复杂度分析

二叉树遍历的递归实现,每个结点只需遍历一次,故时间复杂度为O(n)。而使用了递归,最差情况下递归调用的深度为O(n),所以空间复杂度为O(n)。

非递归遍历

二叉树遍历的非递归实现,可以借助栈。

前序遍历

  1. 将根结点入栈;
  2. 每次从栈顶弹出一个结点,访问该结点;
  3. 把当前结点的右孩子入栈;
  4. 把当前结点的左孩子入栈。

按照以上顺序入栈,这样出栈顺序就与先序遍历一样:先根结点,再左子树,最后右子树。

voidpreOrder2(TreeNode *root) {

if(root == NULL)

return;

stack<TreeNode *> stk;

stk.push(root);

while(!stk.empty()) {

TreeNode *pNode = stk.top();

stk.pop();

cout << pNode->val <<endl;

if(pNode->right != NULL)

stk.push(pNode->right);

if(pNode->left != NULL)

stk.push(pNode->left);

}

}

中序遍历

  1. 初始化一个二叉树结点pNode指向根结点;
  2. 若pNode非空,那么就把pNode入栈,并把pNode变为其左孩子;(直到最左边的结点)
  3. 若pNode为空,弹出栈顶的结点,并访问该结点,将pNode指向其右孩子(访问最左边的结点,并遍历其右子树)

voidinOrder2(TreeNode *root) {

if(root == NULL)

return;

stack<TreeNode *> stk;

TreeNode *pNode = root;

while(pNode != NULL || !stk.empty()) {

if(pNode != NULL) {

stk.push(pNode);

pNode = pNode->left;

}

else {

pNode = stk.top();

stk.pop();

cout << pNode->val<< endl;

pNode = pNode->right;

}

}

}

后序遍历

  1. 设置两个栈stk, stk2;
  2. 将根结点压入第一个栈stk;
  3. 弹出stk栈顶的结点,并把该结点压入第二个栈stk2;
  4. 将当前结点的左孩子和右孩子先后分别入栈stk;
  5. 当所有元素都压入stk2后,依次弹出stk2的栈顶结点,并访问之。

第一个栈的入栈顺序是:根结点,左孩子和右孩子;于是,压入第二个栈的顺序是:根结点,右孩子和左孩子。因此,弹出的顺序就是:左孩子,右孩子和根结点。

voidpostOrder2(TreeNode *root) {

if(root == NULL)

return;

stack<TreeNode *> stk, stk2;

stk.push(root);

while(!stk.empty()) {

TreeNode *pNode = stk.top();

stk.pop();

stk2.push(pNode);

if(pNode->left != NULL)

stk.push(pNode->left);

if(pNode->right != NULL)

stk.push(pNode->right);

}

while(!stk2.empty()) {

cout << stk2.top()->val<< endl;

stk2.pop();

}

}

另外,二叉树的后序遍历的非递归实现,也可以只使用一个栈来实现。

voidpostOrder2(TreeNode *root) {

if(root == NULL)

return;

stack<TreeNode *> stk;

stk.push(root);

TreeNode *prev = NULL;

while(!stk.empty()) {

TreeNode *pNode = stk.top();

if(!prev || prev->left == pNode ||prev->right == pNode) {  // traversedown

if(pNode->left)

stk.push(pNode->left);

else if(pNode->right)

stk.push(pNode->right);

/* else {

cout << pNode->val<< endl;

stk.pop();

}

*/

}

else if(pNode->left == prev) {  // traverse up from left

if(pNode->right)

stk.push(pNode->right);

}

/* else if(pNode->right == prev) { //traverse up from right

cout << pNode->val<< endl;

stk.pop();

}

*/

else {

cout << pNode->val<< endl;

stk.pop();

}

prev = pNode;

}

}

复杂度分析

二叉树遍历的非递归实现,每个结点只需遍历一次,故时间复杂度为O(n)。而使用了栈,空间复杂度为二叉树的高度,故空间复杂度为O(n)。

Morris遍历

Morris遍历算法最神奇的地方就是,只需要常数的空间即可在O(n)时间内完成二叉树的遍历。O(1)空间进行遍历困难之处在于在遍历的子结点的时候如何重新返回其父节点?在Morris遍历算法中,通过修改叶子结点的左右空指针来指向其前驱或者后继结点来实现的。

中序遍历

  1. 如果当前结点pNode的左孩子为空,那么输出该结点,并把该结点的右孩子作为当前结点;
  2. 如果当前结点pNode的左孩子非空,那么就找出该结点在中序遍历中的前驱结点pPre
    • 当第一次访问该前驱结点pPre时,其右孩子必定为空,那么就将其右孩子设置为当前结点,以便根据这个指针返回到当前结点pNode中,并将当前结点pNode设置为其左孩子;
    • 当该前驱结点pPre的右孩子为当前结点,那么就输出当前结点,并把前驱结点的右孩子设置为空(恢复树的结构),将当前结点更新为当前结点的右孩子
  3. 重复以上两步,直到当前结点为空。

voidinOrder3(TreeNode *root) {

if(root == NULL)

return;

TreeNode *pNode = root;

while(pNode != NULL) {

if(pNode->left == NULL) {

cout << pNode->val<< endl;

pNode = pNode->right;

}

else {

TreeNode *pPre = pNode->left;

while(pPre->right != NULL&& pPre->right != pNode) {

pPre = pPre->right;

}

if(pPre->right == NULL) {

pPre->right = pNode;

pNode = pNode->left;

}

else {

pPre->right = NULL;

cout << pNode->val<< endl;

pNode = pNode->right;

}

}

}

}

因为只使用了两个辅助指针,所以空间复杂度为O(1)。对于时间复杂度,每次遍历都需要找到其前驱的结点,而寻找前驱结点与树的高度相关,那么直觉上总的时间复杂度为O(nlogn)。其实,并不是每个结点都需要寻找其前驱结点,只有左子树非空的结点才需要寻找其前驱,所有结点寻找前驱走过的路的总和至多为一棵树的结点个数。因此,整个过程每条边最多走两次,一次使定位到该结点,另一次是寻找某个结点的前驱,所以时间复杂度为O(n)。

如以下一棵二叉树。首先,访问的是根结点F,其左孩子非空,所以需要先找到它的前驱结点(寻找路径为B->D->E),将E的右指针指向F,然后当前结点为B。依然需要找到B的前驱结点A,将A的右指针指向B,并将当前结点设置为A。下一步,输出A,并把当前结点设置为A的右孩子B。之后,会访问到B的前驱结点A指向B,那么令A的右指针为空,继续遍历B的右孩子。依次类推。

前序遍历

与中序遍历类似,区别仅仅是输出的顺序不同。

void preOrder3(TreeNode*root) {

if(root == NULL)

return;

TreeNode *pNode = root;

while(pNode) {

if(pNode->left == NULL) {

cout << pNode->val<< endl;

pNode = pNode->right;

}

else {

TreeNode *pPre = pNode->left;

while(pPre->right != NULL&& pPre->right != pNode)

pPre = pPre->right;

if(pPre->right == NULL) {

pPre->right = pNode;

cout << pNode->val<< endl;

pNode = pNode->left;

}

else {

pPre->right = NULL;

pNode = pNode->right;

}

}

}

}

后序遍历

  1. 先建立一个临时结点dummy,并令其左孩子为根结点root,将当前结点设置为dummy;
  2. 如果当前结点的左孩子为空,则将其右孩子作为当前结点;
  3. 如果当前结点的左孩子不为空,则找到其在中序遍历中的前驱结点
    • 如果前驱结点的右孩子为空,将它的右孩子设置为当前结点,将当前结点更新为当前结点的左孩子;
    • 如果前驱结点的右孩子为当前结点,倒序输出从当前结点的左孩子到该前驱结点这条路径上所有的结点。将前驱结点的右孩子设置为空,将当前结点更新为当前结点的右孩子。
  4. 重复以上过程,直到当前结点为空。

voidreverse(TreeNode *p1, TreeNode *p2) {

if(p1 == p2)

return;

TreeNode *x = p1;

TreeNode *y = p1->right;

while(true) {

TreeNode *temp = y->right;

y->right = x;

x = y;

y = temp;

if(x == p2)

break;

}

}

voidprintReverse(TreeNode *p1, TreeNode *p2) {

reverse(p1, p2);

TreeNode *pNode = p2;

while(true) {

cout << pNode->val <<endl;

if(pNode == p1)

break;

pNode = pNode->right;

}

reverse(p2, p1);

}

voidpostOrder3(TreeNode *root) {

if(root == NULL)

return;

TreeNode *dummy = new TreeNode(-1);

dummy->left = root;

TreeNode *pNode = dummy;

while(pNode != NULL) {

if(pNode->left == NULL)

pNode = pNode->right;

else {

TreeNode *pPrev = pNode->left;

while(pPrev->right != NULL&& pPrev->right != pNode)

pPrev = pPrev->right;

if(pPrev->right == NULL) {

pPrev->right = pNode;

pNode = pNode->left;

}

else {

printReverse(pNode->left,pPrev);

pPrev->right = NULL;

pNode = pNode->right;

}

}

}

}

二叉树的三种遍历方式(递归、非递归和Morris遍历)相关推荐

  1. 二分查找算法的两种实现方式:非递归实现和递归实现

    二分查找的条件是对一组有序数组的查找,这一点很容易忘记,在使用二分查找的时候先要对数组进行排序. 先说一下二分查找的思路:一个有序数组,想要查找一个数字key的下标,首先算出中间下标mid,利用mid ...

  2. 二叉树三种遍历方式的非递归实现

    树的递归实现方式很简单,下面介绍三种遍历的非递归实现. 树的遍历有个特点,那就是在处理具体问题时,绝大多数情况下是在当前循环.或函数(或是子树)的根节点来处理的,能够注意到当前根节点是如何从上个根节点 ...

  3. 二叉树的三种遍历(递归与非递归) + 层次遍历

    <转载于  >>> > 二叉树是一种非常重要的数据结构,很多其他数据机构都是基于二叉树的基础演变过来的.二叉树有前.中.后三种遍历方式,因为树的本身就是用递归定义的,因此 ...

  4. 实现二叉树的三种非递归遍历算法

    [问题描述] 编写程序,实现二叉树的三种非递归遍历算法:先序非递归,中序非递归,后序非递归. [输入形式] 输入建树序列. [输出形式] 输出三种遍历序列. [样例输入] A B C # # # # ...

  5. 二叉树的四种遍历方式——前序、中序、后序、层序遍历(递归+非递归实现)

    如果N代表根节点,L代表根节点的左子树,R代表根节点的右子树,则根据遍历根节点的先后次序有以下遍历方式: 1. NLR:前序遍历(Preorder Traversal 亦称先序遍历)--访问根结点-- ...

  6. 详解二叉树的三种遍历方式(递归、迭代、Morris算法)

    详解二叉树的三种遍历方式(递归.迭代.Morris算法) 最重要的事情写在前面:遍历顺序不一定就是操作顺序!!! 递归解法 首先,一颗二叉树它的递归序列是一定的,导致其前中后序不同的原因只不过是访问节 ...

  7. 算法练习day10——190328(二叉树的先序、 中序、 后序遍历, 包括递归方式和非递归方式、找到一个节点的后继节点、二叉树的序列化和反序列化)

    1.实现二叉树的先序. 中序. 后序遍历, 包括递归方式和非递归方式 1.1 访问节点的顺序 节点访问顺序如下图所示: 访问顺序:1 2 4 4 4 2 5 5 5 2 1 3 6 6 6 3 7 7 ...

  8. c语言中二叉树中总结点,C语言二叉树的三种遍历方式的实现及原理

    二叉树遍历分为三种:前序.中序.后序,其中序遍历最为重要.为啥叫这个名字?是根据根节点的顺序命名的. 比如上图正常的一个满节点,A:根节点.B:左节点.C:右节点,前序顺序是ABC(根节点排最先,然后 ...

  9. 二叉树几种遍历算法的非递归实现

    二叉树遍历的非递归实现 相对于递归遍历二叉树,非递归遍历显得复杂了许多,但换来的好处是算法的时间效率有了提高.下面对于我学习非递归遍历二叉树算法的过程进行总结 为了便于理解,这里以下图的二叉树为例,分 ...

最新文章

  1. php$后面加点有什么用,css和js后加问号和数字有什么用
  2. manacher最长回文子串
  3. mysql 修改表名的方法:sql语句
  4. 请重视!服务器这几个“异常”可能性预警
  5. 1221. 分割平衡字符串
  6. 花生油和菜籽油哪个更健康?
  7. Android应用开发提高篇(1)-----获取本地IP
  8. 24.Android-实现黑名单电话拦截
  9. win10无线投屏花屏
  10. HTML5从入门到精通电子书pdf下载
  11. 计算机表格斜杠怎么打,excel表格打斜杠的方法步骤图详解
  12. EUI库 - 自动布局
  13. 【UV打印机】理光喷头组合说明(16H)
  14. 【设计】工业设计公司设计师的原则
  15. 计算机辅助设计实训报告范文,电子实训报告总结范文
  16. 浅谈马氏距离【Mahalonobis Distance】
  17. 【推荐+转摘】如何又快又好的做出一份优质PPT
  18. Linux下修改虚拟机的root密码
  19. sql 处理数据字段为空 如果为空转换成别的值
  20. 装机员万能驱动v1.0.0.1官方版

热门文章

  1. 测试opencl软件,OpenCL应用测试
  2. 安装openssh-server报Depends: openssh-client (= 1:6.6p1-2ubuntu2.8)错误
  3. 【红日靶场系列】ATTCK红队评估3
  4. 《全数据时代的炼金师》读书笔记(二)
  5. 魔术师乔布斯,炼金术士苹果——苹果公司第四季度报表简评
  6. 有趣的HTML实例(二) 404页面
  7. 应试技巧丨英语写作忘词了?我有办法
  8. Server U 的使用
  9. C++ MFC万能的类向导
  10. 屏幕小于6英寸的手机_2019有哪些小屏手机 8款6英寸以下小屏全面屏手机推荐