谈树的中序遍历
树的中序遍历有2种传统做法,一个是直接递归(利用系统栈),一个是非递归(自己手动开一个栈)。不过,无论哪种方法,都需要用到栈,空间复杂度和树高成正比,最坏情况下为$O(n)$。
LC-94就考了中序遍历。这里贴一个非递归写法,方便以后复习思路。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> stk;
while(root || stk.size()) {
while(root) {
stk.push(root);
root = root->left;
}
if(stk.size()) {
root = stk.top();
stk.pop();
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};
为何要用栈
二叉树在中序遍历时,位于上方的点不一定比下方的点先遍历。这个很显然,比如根节点就是在左子树的节点遍历完之后再遍历。但指针是只能向下走的,没有往上走的路,因此我们需要用栈来记录上方待遍历的节点地址。在递归实现中,这叫回溯;在非递归实现中,这叫弹出栈顶元素。
那能不能把这部分空间优化掉呢?
首先考虑什么时候需要回溯。当一个节点的前驱节点不是它的左孩子时,它的前驱节点就需要回溯。而前驱节点在哪?直观上来看,应该是左子树里面中序遍历的最后一个元素,也就是当前节点先向左走一步,然后一直向右走到底的那个位置!
抛开栈,牵线搭桥+过河拆桥
让需要回溯的前驱节点的右孩子链到当前节点,这是Morris算法的核心。这个思路其实很reasonable,因为中序遍历的后继节点的确就是访问当前节点的右子树。通过预先搭桥再遍历的方式,就可以省掉一个栈了。
整体算法流程如下:
- 如果当前节点的左孩子为空,那么遍历一下当前节点,然后当前节点更新为右孩子;
- 如果当前节点的左孩子不空,那么找到当前节点的前驱节点
last
(1) 如果last
的右孩子为空,说明这是第一次到达这里,表示左子树还没被遍历过,那么把last
的右孩子更新为当前节点(牵线搭桥),然后当前节点进入左子树遍历;
(2) 如果last
的右孩子不空,说明这是第二次到达这里,表示左子树已经遍历过了,那么把当前节点遍历一下,然后记得把last
的右孩子恢复为空(过河拆桥),然后当前节点进入右子树遍历。贴一张原理图,方便日后手动模拟。
代码
class Solution {
public:
void recoverTree(TreeNode* root) {
TreeNode* first = nullptr, *second = nullptr, *last = nullptr;
while(root) {
if(root->left) {
auto p = root->left;
while(p->right && p->right != root) p = p->right;
if(p->right) {
if(last && last->val >= root->val) {
if(!first) first = last, second = root;
else second = root;
}
p->right = nullptr;
last = root;
root = root->right;
} else {
p->right = root;
root = root->left;
}
} else {
if(last && last->val >= root->val) {
if(!first) first = last, second = root;
else second = root;
}
last = root;
root = root->right;
}
}
swap(first->val, second->val);
}
};
- Post link: https://scnujackychen.github.io/2021/03/12/LC-99/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub IssuesGitHub Discussions