本文主要解决一个问题,如何实现二叉树的前中后序遍历,有两个要求:

1. O(1)空间复杂度,即只能使用常数空间;

2. 二叉树的形状不能被破坏(中间过程允许改变其形状)。

通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归
(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空
间或者用户自定义的stack),所以不满足要求。(用这两种方法实现的中序遍历实现可以参考这里。)

Morris Traversal方法可以做到这两点,与前两种方法的不同在于该方法只需要O(1)空间,而且同样可以在O(n)时间内完成。

要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris方法用到了线索二叉树(threaded binary tree)的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱(predecessor)和后继节点(successor),只需要利用叶子节点中的左右空指针指向某种顺序遍历下的前驱节点或后继节点就可以了。

Morris只提供了中序遍历的方法,在中序遍历的基础上稍加修改可以实现前序,而后续就要再费点心思了。所以先从中序开始介绍。

首先定义在这篇文章中使用的二叉树节点结构,即由val,left和right组成:

 struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

一、中序遍历

步骤:

1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

图示:

下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。

代码:

 void inorderMorrisTraversal(TreeNode *root) {
TreeNode *cur = root, *prev = NULL;
while (cur != NULL)
{
if (cur->left == NULL) // 1.
{
printf("%d ", cur->val);
cur = cur->right;
}
else
{
// find predecessor
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right; if (prev->right == NULL) // 2.a)
{
prev->right = cur;
cur = cur->left;
}
else // 2.b)
{
prev->right = NULL;
printf("%d ", cur->val);
cur = cur->right;
}
}
}
}

复杂度分析:

空间复杂度:O(1),因为只用了两个辅助指针。

时间复杂度:O(n)。证明时间复杂度为O(n),最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点的时间复杂度是多少,即以下两行代码:

 while (prev->right != NULL && prev->right != cur)
prev = prev->right;

直觉上,认为它的复杂度是O(nlgn),因为找单个节点的前驱节点与树的高度有关。但事实上,寻找所有节点的前驱节点只需要O(n)时间。n个节 点的二叉树中一共有n-1条边,整个过程中每条边最多只走2次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,如下图所示,其中红 色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为O(n)。

二、前序遍历

前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。

步骤:

1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。

b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

图示:

代码:

 void preorderMorrisTraversal(TreeNode *root) {
TreeNode *cur = root, *prev = NULL;
while (cur != NULL)
{
if (cur->left == NULL)
{
printf("%d ", cur->val);
cur = cur->right;
}
else
{
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right; if (prev->right == NULL)
{
printf("%d ", cur->val); // the only difference with inorder-traversal
prev->right = cur;
cur = cur->left;
}
else
{
prev->right = NULL;
cur = cur->right;
}
}
}
}

复杂度分析:

时间复杂度与空间复杂度都与中序遍历时的情况相同。

三、后序遍历

后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。

步骤:

当前节点设置为临时节点dump。

1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

图示:

代码:

 void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
{
if (from == to)
return;
TreeNode *x = from, *y = from->right, *z;
while (true)
{
z = y->right;
y->right = x;
x = y;
y = z;
if (x == to)
break;
}
} void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
{
reverse(from, to); TreeNode *p = to;
while (true)
{
printf("%d ", p->val);
if (p == from)
break;
p = p->right;
} reverse(to, from);
} void postorderMorrisTraversal(TreeNode *root) {
TreeNode dump();
dump.left = root;
TreeNode *cur = &dump, *prev = NULL;
while (cur)
{
if (cur->left == NULL)
{
cur = cur->right;
}
else
{
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right; if (prev->right == NULL)
{
prev->right = cur;
cur = cur->left;
}
else
{
printReverse(cur->left, prev); // call print
prev->right = NULL;
cur = cur->right;
}
}
}
}

复杂度分析:

空间复杂度同样是O(1);时间复杂度也是O(n),倒序输出过程只不过是加大了常数系数。

注:

以上所有的代码以及测试代码可以在我的Github里获取。

参考:

http://www.geeksforgeeks.org/inorder-tree-traversal-without-recursion-and-without-stack/
http://www.geeksforgeeks.org/morris-traversal-for-preorder/
http://stackoverflow.com/questions/6478063/how-is-the-complexity-of-morris-traversal-on
http://blog.csdn.net/wdq347/article/details/8853371
Data Structures and Algorithms in C++ by Adam Drozdek

---------------

以前我只知道递归和栈+迭代实现二叉树遍历的方法,昨天才了解到有使用O(1)空间复杂度的方法。以上都是我参考了网上的资料加上个人的理解来总结,如果有什么不对的地方非常欢迎大家的指正。

原创文章,欢迎转载,转载请注明出处:http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html。

[转载]Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)的更多相关文章

  1. Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)——无非是在传统遍历过程中修改叶子结点加入后继结点信息(传统是stack记录),然后再删除恢复

    先看看线索二叉树 n个结点的二叉链表中含有n+1(2n-(n-1)=n+1)个空指针域.利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索 ...

  2. Morris Traversal 方法遍历二叉树(非递归、不用栈,O(1)空间)

    http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html

  3. Morris Traversal方法遍历

    实现二叉树的遍历且只需要O(1)的空间. 参考:http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html

  4. 【LeetCode-面试算法经典-Java实现】【145-Binary Tree Postorder Traversal(二叉树非递归后序遍历)】

    [145-Binary Tree Postorder Traversal(二叉树非递归后序遍历)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a bin ...

  5. 【LeetCode-面试算法经典-Java实现】【144-Binary Tree Preorder Traversal(二叉树非递归前序遍历)】

    [144-Binary Tree Preorder Traversal(二叉树非递归前序遍历)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a bina ...

  6. c/c++二叉树的创建与遍历(非递归遍历左右中,破坏树结构)

    二叉树的创建与遍历(非递归遍历左右中,破坏树结构) 创建 二叉树的递归3种遍历方式: 1,先中心,再左树,再右树 2,先左树,再中心,再右树 3,先左树,再右树,再中心 二叉树的非递归4种遍历方式: ...

  7. c/c++叉树的创建与遍历(非递归遍历左右中,不破坏树结构)

    二叉树的创建与遍历(非递归遍历左右中,不破坏树结构) 创建 二叉树的递归3种遍历方式: 1,先中心,再左树,再右树 2,先左树,再中心,再右树 3,先左树,再右树,再中心 二叉树的非递归4种遍历方式: ...

  8. C++版 - LeetCode 144. Binary Tree Preorder Traversal (二叉树先根序遍历,非递归)

    144. Binary Tree Preorder Traversal Difficulty: Medium Given a binary tree, return the preorder trav ...

  9. 数据结构之二叉树篇卷三 -- 二叉树非递归遍历(With Java)

    Nonrecursive Traversal of Binary Tree First I wanna talk about why we should <code>Stack</c ...

随机推荐

  1. SQL基础--查询之五--查询语句一般格式

    SQL基础--查询之五--查询语句一般格式

  2. mysql 取当前日期对应的周一或周日

    select subdate(curdate(),date_format(curdate(),'%w')-1)//获取当前日期在本周的周一 select subdate(curdate(),date_ ...

  3. POJ2891:Strange Way to Express Integers(解一元线性同余方程组)

    写一下自己的理解,下面附上转载的:若a==b(modk);//这里的==指的是同余,我用=表示相等(a%k=b)a-b=kt(t为整数)以前理解的错误思想:以前认为上面的形式+(a-tb=k)也是成立 ...

  4. 关于Oracle误操作--数据被Commit后的数据回退恢复(闪回)

    今天操作Oracle数据库时,做一个Update数据时,不小心少写了个where,看这粗心大意的. 于是乎,把所有的员工工号都给更新成一个同一个工号了.这是一个悲催的故事. 因为工号是Check了好多 ...

  5. Rails的HashWithIndifferentAccess

    ruby 2.0 引入了keyword arguments,方法的参数可以这么声明 def foo(bar: 'default') puts bar end foo # => 'default' ...

  6. ACM ICPC, Damascus University Collegiate Programming Contest(2018) Solution

    A:Martadella Stikes Again 水. #include <bits/stdc++.h> using namespace std; #define ll long lon ...

  7. Linux /python --- zipinfo命令

    Linux zipinfo命令用于列出压缩文件信息. 执行zipinfo指令可得知zip压缩文件的详细信息. zipinfo [-12hlmMstTvz][压缩文件][文件...][-x <范本 ...

  8. javascript-高级用法

    22.1 安全的类型检测 为什么:typeof 不靠谱, 无法将数组从对象中区分出来, instanceof 有特殊情况,在iframe存在的情况下无法判断另一个iframe内的数组 如何做:Obje ...

  9. 20145301赵嘉鑫《网络对抗》逆向及Bof基础

      20145301赵嘉鑫<网络对抗>逆向及Bof基础 实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函数会简单回 ...

  10. VC++使用FindFirstFile,FindNextFile遍历一个文件夹

    转载:http://www.cnblogs.com/chenkunyun/archive/2012/03/24/2415727.html 方法一. //遍历文件夹函数 void TraverseFol ...