二叉树的遍历通常有三种方法:
1.迭代
2.栈实现非迭代
3.Morris traversal实现非迭代
这三种方法的比较参照这篇中序遍历的题目。
Morris traversal利用了线索二叉树,将空间复杂度降为O(1),本篇继续探讨下它的其他遍历的实现。
线索二叉树
动机
1.在一个以链表形式存储的二叉树中,有n个节点,则有(n-1)个节点间指针,而节点结构体用于存储指针的则有2n个,造成了一定了浪费。
2.遍历树时费劲。
举个例子:

定义
节点中空的左指针指向其前驱节点,空的右指针指向后继节点。
举个例子:

问题又来了,如何判断指针指向的是子节点 还是 前驱/后继 呢?
于是结构体升级,增加了一个tag用于区别这两种情况。变成了:

举个例子:(tag为1表示前驱/后继,为0表示孩子)

Morris traversal中,就是利用了线索二叉树中记录后继节点的思想。
Morris Traversal
接下来,具体分析Morris traversal的实现
中序
这里再次阐述一遍中序的实现,与前面链接的例子有一处不同(在处理节点间指向关系上),目的是与其他顺序遍历尽量保持一致,方便比较。
算法步骤:
1.初始化当前节点curr为根节点root。
2.while(curr != NULL):
if(curr->left == NULL):
a) 输出curr的值;
b) curr = curr->right;
else:
找到curr左子树的最右节点a(curr在中序遍历中的前驱节点):
if(a->right == NULL):a->right = curr,curr = curr->left;//还未线索化
if(a->right == curr):a->rihgt = NULL,输出curr的值,curr = curr->right; //已线索化过了
举个例子:

代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root){
vector<int> head;
TreeNode* cur;
cur = root;
while (cur != NULL) {
if (cur->left == NULL) {
head.push_back(cur->val);
cur = cur->right; /* 将右孩子作为当前节点 */
}
else {
/* 查找cur节点的前驱节点 */
TreeNode *node = cur->left;
while (node->right != NULL && node->right != cur)
node = node->right;
if (node->right == NULL) { /* 还没有线索化,则建立线索 */
node->right = cur;
cur = cur->left;
}
else { /* 如果已经线索化了,则访问节点,并删除线索 */
head.push_back(cur->val);
node->right = NULL;
cur = cur->right;
}
}
}
return head;
}
};
先序
先序与中序差别只有一处,在输出节点值的时间上。
算法步骤:
1.初始化当前节点curr为根节点root。
2.while(curr != NULL):
if(curr->left == NULL):
a) 输出curr的值;
b) curr = curr->right;
else:
找到curr左子树的最右节点a(curr在中序遍历中的前驱节点):
if(a->right == NULL):a->right = curr,输出curr的值(此处是与中序唯一不同),curr = curr->left;
if(a->right == curr):a->rihgt = NULL,curr = curr->right;
举个例子:

代码:(未调试,可能有bug)
vector<int> preorderTraversal(TreeNode* root){
vector<int> head;
TreeNode* cur;
cur = root;
while (cur != NULL) {
if (cur->left == NULL) {
head.push_back(cur->val);
cur = cur->right; /* 将右孩子作为当前节点 */
}
else {
/* 查找cur节点的前驱节点 */
TreeNode *node = cur->left;
while (node->right != NULL && node->right != cur)
node = node->right;
if (node->right == NULL) { /* 还没有线索化,则建立线索 */
head.push_back(cur->val);
node->right = cur;
cur = cur->left;
}
else { /* 如果已经线索化了,则访问节点,并删除线索 */
node->right = NULL;
cur = cur->right;
}
}
}
return head;
}
后序
后序是先输出完左右孩子后再输出父节点,较其他顺序更复杂。
算法步骤:
1.添加一个头节点dump,其左孩子指向根节点root,初始化当前节点curr为dump。
2.while(curr != NULL):
if(curr->left == NULL):
curr = curr->right;
(不同点,没有输出)
else:
找到curr左子树的最右节点a(curr在中序遍历中的前驱节点):
if(a->right == NULL):a->right = curr,curr = curr->left;
if(a->right == curr):a->rihgt = NULL,逆序输出从curr的左孩子到a这条路上所有节点(不同点),curr = curr->right;
举个例子:

代码:(未调试,可能有bug)
vector<int> postorderTraversal(TreeNode* root){
vector<int> head;
TreeNode* cur;
TreeNode dump = {-1, NULL, NULL};
dump.left = root;
cur = &dump;
while (cur != NULL) {
if (cur->left == NULL) {
cur = cur->right; /* 将右孩子作为当前节点 */
}
else {
/* 查找cur节点的前驱节点 */
TreeNode *node = cur->left;
while (node->right != NULL && node->right != cur)
node = node->right;
if (node->right == NULL) { /* 还没有线索化,则建立线索 */
node->right = cur;
cur = cur->left;
}
else { /* 如果已经线索化了,逆序输出,并删除线索 */
print(cur->left, node, head);
node->right = NULL;
cur = cur->right;
}
}
}
return head;
}
void print(TreeNode *from, TreeNode *to, vector<int> head){
reverse(from, to);
TreeNode cur = to;
while(cur != from){
head.push_back(cur->val);
}
reverse(to, from);
}
void reverse(TreeNode *from, TreeNode *to){
if(from == to) return;
TreeNode *x = from;
TreeNode *y = from->right;
TreeNode *z;
while(x != to){
z = y->right;
y->right = x;
x = y;
y = z;
}
}
逆序时一定是一条只有right的子树,因为前面while循环是一直指向right的。而逆序的原因是因为后序遍历要先输出完孩子节点。
算法为了保持空间复杂度是O(1),不能创建新的空间,因此就在逆序的时候需要两次调转节点指向关系。这一点显得有一些不优雅。
更多
最高赞关于树的Morris traversal分析更深入:https://www.zhihu.com/question/21556707

本文详细介绍了二叉树的Morris遍历,包括中序、先序和后序遍历的实现,利用线索二叉树降低空间复杂度至O(1)。通过分析算法步骤和示例代码,帮助理解这一非迭代遍历方法。
7101

被折叠的 条评论
为什么被折叠?



