题目:
设计一个算法实现二叉树的三种遍历(前序遍历 中序遍历 后序遍历)。
要求时间复杂度为O(n) 空间复杂度为O(1)。
思路:
空间复杂度O(1)的要求很严格。常规的递归实现是显然不能满足要求的[其空间复杂度是树的深度O(h) ]。本篇文章介绍著名的Morris遍历,该方法利用了二叉树结点中大量指向null的指针。
常规的栈结构遍历方式,遍历到某个节点之后并不能回到上层的结点,这是由二叉树本身的结构所限制的,每个结点并没有指向父节点的指针,因此需要使用栈来完成回到上层结点的步骤。
Morris遍历避免了使用栈结构,让下层有指向上层的指针,但并不是所有的下层结点都有指向上层的指针([这些指针也称为空闲指针])。
空闲指针的分配规则如下:
1. 当前子树的头结点为head,空闲指针由head的左子树中最右结点的右指针指向head结点。对head的左子树重复该步骤1,直到遍历至某个结点没有左子树,将该结点记 为node。进入步骤2。
2. 从node结点开始通过每个结点的右指针进行移动,并打印结点的值。
假设遍历到的当前结点为curNode,做如下判断:
curNode结点的左子树中最右结点(记为lastRNode)是否指向curNode。
A. 是 让lastRNode结点的右指针指向null,打印curNode的值。接着通过curNode的右指针遍历下一个结点,重复步骤2。
B. 否 将curNode为头结点的子树重复步骤1。
下面举例说明上述步骤(先以中序遍历为例),二叉树结构如下图所示:
遍历至结点1 发现其没有左子树 记为Node。
curNode : 1 打印1
curNode : 2 满足A 空闲指针由1的右指针指向2 将该空闲指针取消掉 打印2。通过2的右指针遍历到3。
curNode : 3 满足B 进行步骤1 最终打印3。
通过空闲指针遍历至4。
curNode : 4 满足A 空闲指针由3的右指针指向4 将该空闲指针取消掉 打印4。通过4的右指针遍历到6。
至此 左子树和根结点遍历完毕。
curNode : 6 满足B 进行步骤1 之后二叉树变为右图。
遍历至结点5 其没有左子树 记为Node。
curNode : 5 满足B 进行步骤1 最终打印5。
通过空闲指针遍历至6。
curNode : 6 满足A 空闲指针由5的右指针指向6 将该空闲指针取消掉 打印6。
通过6的右指针遍历到7。
curNode : 7 满足B 进行步骤1 最终打印7。
3. 步骤2最终移动到null结点 整个过程结束。
总结:
打印某个结点时,一定是在步骤2开始移动的过程中。
步骤2最开始从子树最左结点开始,在通过右指针移动过程中,只有以下两种移动方式:
①移动到某个结点的右子树【此时 左子树和根结点必定已经打印结束】
②移动到某个上层结点(即通过空闲指针移动)【此时 该上层结点的左子树整体打印完毕 开始处理根结点】
Morris先序遍历只需要将打印顺序稍微调整一下(调整至步骤1中打印)。
Morris后序遍历同样是需要将打印顺序稍微调整一下,即:逆序打印(不能使用额外的数据结构)所有结点的左子树右边界,在满足步骤2中情况A时打印。
注:
二叉树结点定义如下:
typedef int dataType;
struct Node
{
dataType val;
struct Node *left;
struct Node *right;
Node(dataType _val):
val(_val), left(NULL), right(NULL){}
};
常规的二叉树遍历方式采用栈实现,比较容易实现,下面直接给出代码。
/*************************Morris遍历二叉树*************************/
#include <iostream> using namespace std; typedef int dataType; struct Node { dataType val; struct Node *left; struct Node *right; Node(dataType _val): val(_val), left(NULL), right(NULL){} }; // Morris中序遍历 (左 -> 根 -> 右) void MorrisInOrderTraverse(Node *head) { if (head == NULL) { return; } Node *p1 = head; Node *p2 = NULL; while (p1 != NULL) { p2 = p1->left; if (p2 != NULL) { while(p2->right != NULL && p2->right != p1) { p2 = p2->right; } if (p2->right == NULL) { p2->right = p1; // 空闲指针 p1 = p1->left; continue; } else { p2->right = NULL; } } cout<<p1->val<<" "; p1 = p1->right; } } // Morris前序遍历 (根 -> 左 -> 右) void MorrisPreOrderTraverse(Node *head) { if (head == NULL) { return; } Node *p1 = head; Node *p2 = NULL; while (p1 != NULL) { p2 = p1->left; if (p2 != NULL) { while(p2->right != NULL && p2->right != p1) { p2 = p2->right; } if (p2->right == NULL) { p2->right = p1; // 空闲指针 cout<<p1->val<<" "; // 打印结点值的顺序稍微调整 p1 = p1->left; continue; } else { p2->right = NULL; } } else { cout<<p1->val<<" "; } p1 = p1->right; } } // 逆序右边界 Node* reverseEdge(Node *head) { Node *pre = NULL; Node *next = NULL; while(head != NULL) { next = head->right; head->right = pre; pre = head; head = next; } return pre; } // 逆序打印左子树右边界 void printEdge(Node *head) { Node *lastNode = reverseEdge(head); Node *cur = lastNode; while (cur != NULL) { cout<<cur->val<<" "; cur = cur->right; } reverseEdge(lastNode); } // Morris后序遍历 (左 -> 右 -> 根) void MorrisPostOrderTraverse(Node *head) { if (head == NULL) { return; } Node *p1 = head; Node *p2 = NULL; while (p1 != NULL) { p2 = p1->left; if (p2 != NULL) { while(p2->right != NULL && p2->right != p1) { p2 = p2->right; } if (p2->right == NULL) { p2->right = p1; // 空闲指针 p1 = p1->left; continue; } else { p2->right = NULL; printEdge(p1->left); } } p1 = p1->right; } printEdge(head); } void buildBinTree(Node **head) { dataType _val; cin>>_val; if (_val == -1) { *head = NULL; } else { *head = (Node*)malloc(sizeof(Node)); (*head)->val = _val; buildBinTree(&(*head)->left); buildBinTree(&(*head)->right); } } int main(void) { Node *head; buildBinTree(&head); cout<<"前序遍历序列为:"; MorrisPreOrderTraverse(head); cout<<endl; cout<<"中序遍历序列为:"; MorrisInOrderTraverse(head); cout<<endl; cout<<"后序遍历序列为:"; MorrisPostOrderTraverse(head); cout<<endl; return 0; }
/*************************Morris遍历二叉树 END*************************/
输入:
1 2 3 -1 -1 4 -1 -1 5 6 8 -1 -1 -1 7 -1 -1
输出:
致谢:
本篇文章参考自左神新书《程序员代码面试指南:IT名企算法与数据结构题目最优解》,在此表示感谢。