二叉树非递归遍历,不使用栈(前序,中序,后续)
2015-04-02 09:32
375 查看
二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有前序、中序以及后序三种遍历方法。因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。而对于树的遍历若采用非递归的方法,它们都有使用栈和不使用栈的方法,其中不使用栈的非递归遍历又是找工作面试中经常被问的问题,所以在这里做一总结:
一.使用栈的非递归遍历
主要思想就是利用栈来暂存下一个需要遍历的节点,然后不断退栈.
1.前序遍历
2.中序遍历
找工作面试的时候,实现二叉树的前中后序遍历中,面试官会有以下两个要求:
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只提供了中序遍历的方法,在中序遍历的基础上稍加修改可以实现前序,而后续就要再费点心思了。所以先从中序开始介绍。
1.中序遍历
步骤:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:
下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。
复杂度分析:
空间复杂度:O(1),因为只用了两个辅助指针。
时间复杂度:O(n)。证明时间复杂度为O(n),最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点的时间复杂度是多少,即以下两行代码:
直觉上,认为它的复杂度是O(nlgn),因为找单个节点的前驱节点与树的高度有关。但事实上,寻找所有节点的前驱节点只需要O(n)时间。n个节点的二叉树中一共有n-1条边,整个过程中每条边最多只走2次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,如下图所示,其中红色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为O(n)。
2.前序遍历
前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。
步骤:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:
后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤:
当前节点设置为临时节点dump。
1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:
方法1:
(注:不使用栈的代码参考http://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html)
参考:
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
struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} };
一.使用栈的非递归遍历
主要思想就是利用栈来暂存下一个需要遍历的节点,然后不断退栈.
1.前序遍历
class Solution { public: vector<int> preorderTraversal(TreeNode *root) { vector<int> result; stack<TreeNode *> st; TreeNode *tempNode; if(root == NULL) return result; st.push(root); while(!st.empty()) { tempNode = st.top(); st.pop(); result.push_back(tempNode->val); if(tempNode->right) st.push(tempNode->right); if(tempNode->left) st.push(tempNode->left); } return result; } };
2.中序遍历
class Solution { public: vector<int> inorderTraversal(TreeNode *root) { stack<TreeNode*> st; vector<int> result; TreeNode* temp; if(root==NULL) return result; temp=root; while(temp) { st.push(temp); temp=temp->left; } while(!st.empty()) { temp=st.top(); result.push_back(temp->val); st.pop(); if(temp->right) { temp=temp->right; while(temp) { st.push(temp); temp=temp->left; } } } return result; } };3.后续遍历
class Solution { public: vector<int> postorderTraversal(TreeNode *root) { TreeNode *tempNode; stack<TreeNode*> st; stack<int> sign; vector<int> result; int right; if(root==NULL) return result; tempNode = root; while(tempNode) { st.push(tempNode); sign.push(0); tempNode = tempNode->left; } while(!st.empty()) { tempNode = st.top(); right = sign.top(); if(tempNode->right && right==0) { sign.pop(); sign.push(1); tempNode = tempNode->right; while(tempNode) { st.push(tempNode); sign.push(0); tempNode = tempNode->left; } } else { st.pop(); sign.pop(); result.push_back(tempNode->val); } } return result; } };二.不使用栈的非递归遍历(Morris Traversal)
找工作面试的时候,实现二叉树的前中后序遍历中,面试官会有以下两个要求:
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只提供了中序遍历的方法,在中序遍历的基础上稍加修改可以实现前序,而后续就要再费点心思了。所以先从中序开始介绍。
1.中序遍历
步骤:
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)。
2.前序遍历
前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。
步骤:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:
void preorderMorrisTraversal(TreeNode *root) {3.后续遍历
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直到当前节点为空。
图示:
方法1:
void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.方法2:
{
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(0);
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;
}
}
}
}
class Solution { public: vector<int> postorderTraversal(TreeNode *root) { TreeNode *dump = new TreeNode(0); dump->left = root; vector<int> ret; int L, R; TreeNode *cur = dump, *tmp = NULL, *helper = NULL; while (cur) { if (!cur->left) { cur = cur->right; } else { tmp = cur->left; while (tmp->right != NULL && tmp->right != cur) tmp = tmp->right; if (tmp->right != cur) { tmp->right = cur; cur = cur->left; } else { //print L = ret.size(); helper = cur->left; while (helper != cur) { ret.push_back(helper->val); helper = helper->right; } R = ret.size() - 1; //reverse while (L < R) { swap(ret[L++], ret[R--]); } tmp->right = NULL; cur = cur->right; } } } delete dump; return ret; } };
(注:不使用栈的代码参考http://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html)
参考:
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
相关文章推荐
- Java 二叉树的前序、中序、后续遍历 递归和迭代实现
- 二叉树先序、中序、后续遍历(非递归实现)
- 递归和非递归俩种方法实现二叉树的前序、中序、后续遍历
- 二叉树的创建 先序 中序 后续 递归和非递归遍历
- 二叉树的前序,中序,后续,递归及非递归遍历的python实现
- 二叉树的各种遍历(先序、中序、后续、层次)——递归、非递归
- 二叉树先序、中序、后续遍历递归以及非递归java实现
- 二叉树前序、中序和后序的遍历方法(递归、用栈和使用线索化)
- 二叉树的建立,非递归前序、中序、后续遍历
- 【Java语言学习】之递归、非递归前序、中序、后续遍历二叉树
- 二叉树的前序、中序、后续遍历,递归、非递归实现
- 二叉树的实现&&递归和非递归方式前序、中序、后续遍历&&发现一个节点中序遍历的下一节点
- 二叉树前序、中序、后续遍历(递归实现)
- 二叉树的前序/中序/后续遍历(递归+非递归)
- 二叉树非递归遍历(前序,中序,后续遍序)
- java 创建二叉树 前序、中序、后续 递归遍历树 和 非递归遍历,递归求树高,递归求树节点数
- 二叉树前序,中序,后续遍历(递归和非递归)
- 给出二叉树的先序和中序遍历,递归求解后序遍历
- 二叉树--已知前序遍历和中序遍历,输出后续遍历
- 二叉树--已知前序遍历和中序遍历,输出后续遍历