Morris Traversal (线索二叉树 Threaded binary tree)
参考来源:http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
Pre-order
前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。
步骤:
- 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
- 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。 - 重复以上1、2直到当前节点为空。
// LeetCode, Binary Tree Preorder Traversal
// Morris 先序遍历,时间复杂度 O(n),空间复杂度 O(1)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
TreeNode* cur=root;
while(cur!=NULL){
if(cur->left==NULL){
result.push_back(cur->val);
cur=cur->right;
}
else { /* 查找前驱 */
TreeNode* temp=cur->left;
while(temp->right!=NULL&&temp->right!=cur){
temp=temp->right;
}
if(temp->right==NULL){ /* 还没线索化,则建立线索 */
result.push_back(cur->val); /* 仅这一行的位置与中序不同 */
temp->right=cur;
cur=cur->left;
}
else{ /* 已经线索化,则删除线索 */
temp->right=NULL;
cur=cur->right;
}
}
}
return result;
}
};
复杂度分析:
时间复杂度与空间复杂度都与中序遍历时的情况相同。
In-order
步骤:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
// LeetCode, Binary Tree Inorder Traversal
// Morris 中序遍历,时间复杂度 O(n),空间复杂度 O(1)
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
TreeNode* cur=root;
while(cur!=NULL){
if(cur->left==NULL){
result.push_back(cur->val);
cur=cur->right;
}
else {
/* 查找前驱 */
TreeNode* temp=cur->left;
while(temp->right!=NULL&&temp->right!=cur){
temp=temp->right;
}
if(temp->right==NULL){ /* 还没线索化,则建立线索 */
temp->right=cur;
cur=cur->left;
}
else{ /* 已经线索化,则访问节点,并删除线索 */
result.push_back(cur->val);
temp->right=NULL;
cur=cur->right;
}
}
}
return result;
}
};
复杂度分析:
空间复杂度: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)。
Post-order
后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤:
当前节点设置为临时节点dump。
1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
// LeetCode, Binary Tree Postorder Traversal
// Morris 后序遍历,时间复杂度 O(n),空间复杂度 O(1)
class Solution {
public:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> result;
TreeNode dummy(-1);
TreeNode *cur, *prev = nullptr;
std::function < void(const TreeNode*)> visit = [&result](const TreeNode *node) {
result.push_back(node->val);
};
dummy.left = root;
cur = &dummy;
while (cur != nullptr) {
if (cur->left == nullptr) {
prev = cur; /* 必须要有 */
cur = cur->right;
}
else {
TreeNode *node = cur->left;
while (node->right != nullptr && node->right != cur)
node = node->right;
if (node->right == nullptr) { /* 还没线索化,则建立线索 */
node->right = cur;
prev = cur; /* 必须要有 */
cur = cur->left;
}
else { /* 已经线索化,则访问节点,并删除线索 */
visit_reverse(cur->left, prev, visit);
prev->right = nullptr;
prev = cur; /* 必须要有 */
cur = cur->right;
}
}
} return result;
}
private:
// 逆转路径
static void reverse(TreeNode *from, TreeNode *to) {
TreeNode *x = from, *y = from->right, *z;
if (from == to) return;
while (x != to) {
z = y->right;
y->right = x;
x = y;
y = z;
}
}
// 访问逆转后的路径上的所有结点
static void visit_reverse(TreeNode* from, TreeNode *to, std::function< void(const TreeNode*) >& visit) {
TreeNode *p = to;
reverse(from, to);
while (true) {
visit(p);
if (p == from) break;
p = p->right;
} reverse(to, from);
}
};
复杂度分析:
空间复杂度同样是O(1);时间复杂度也是O(n),倒序输出过程只不过是加大了常数系数。