Morris: 二叉树遍历的更好方案
平台:C++
Morris算法是什么
一种用于二叉树的遍历算法,可以将传统的栈遍历和递归遍历带来的最差 O ( n ) O(n) O(n)的空间复杂度降低到 O ( 1 ) O(1) O(1)
Morris’s in General
Morris算法抛弃了栈结构(Stack)来做DFS(Depth First Search),转而利用指针的连接来存取前驱节点
来源题解:Leetcode#538官方题解
给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。
例如:
输入: 原始二叉搜索树:
5
/ \
2 13
输出: 转换为累加树:
18
/ \
20 13
常规思路
由于给出的是二叉搜索树,右子树节点的值永远大于根节点,因此可以很容易想到中序遍历
遍历得到数组后再逆序相加,再填回原树
逆中序遍历
上面的方法行之有效,但是由于操作繁杂冗余,会消耗大量的时间,因此我们可以把逆序相加与遍历相结合,便诞生了逆中序遍历的方法
Morris
解决了时间复杂度,是否还有优化的可能?
Morris算法就是这把钥匙。
代码概览(C++):
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
TreeNode* getPredecessor(TreeNode* node) {
//Discard the stack to find the predecessor
//Get the most left node of right sub-trees as the predecessor
TreeNode* pred = node->right;
while (pred->left != nullptr && pred->left != node) {
pred = pred->left;
}
return pred;
}
TreeNode* convertBST(TreeNode* root) {
int sum = 0;
//Initialise
TreeNode* node = root;
//
TreeNode* pred;
while (node != nullptr) {
if (node->right == nullptr) {
//Process the node
sum += node->val;
node->val = sum;
//Proceed to the left node
node = node->left;
} else {
//Get the most left node of right sub-trees as the predecessor
pred = getPredecessor(node);
//connect the predecessor with the node to keep the predecessor
if (pred->left == nullptr) {
pred->left = node;
// move node to the right node
node = node->right;
} else {
// discard the link
pred->left = nullptr;
//process the node
sum += node->val;
node->val = sum;
node = node->left;
}
}
}
return root;
}
};
接下来我们分步讲解Morris’s算法
-
初始化node为root,记录sum值为0
并找到前驱节点
TreeNode* getPredecessor(TreeNode* node) { //Discard the stack to find the predecessor //Get the most left node of right sub-trees as the predecessor TreeNode* pred = node->right; while (pred->left != nullptr && pred->left != node) { pred = pred->left; } return pred; }
若前驱节点无左子节点,将前驱节点的左指针改到当前节点,这里是一个后续操作的伏笔
并将node移动到右子节点pred = getPredecessor(node); if (pred->left == nullptr) { pred->left = node; // move node to the right node node = node->right; }
-
继续找到node的前驱节点,并连接到node(逻辑同1)
将node移动到右子节点
-
此时右子节点到头了,应该对该右子节点进行处理
刚才埋下的伏笔即连接前驱节点,将发挥功效。
通过此时node已经有了左指针,node可以顺着左指针回到父节点
if (node->right == nullptr) { //Process the node sum += node->val; node->val = sum; //Proceed to the left node node = node->left; }
-
接下来要做这些事
- 销毁前驱节点的左指针
- 对父节点进行处理
- 将node移动到其左子节点
else {
// discard the link
pred->left = nullptr;
//process the node
sum += node->val;
node->val = sum;
}
-
同样的对打通了左子节点的11节点进行操作
-
回溯到根节点进行操作
-
对左子树对操作大同小异,无非是从2节点开始找前驱节点,在此省略
最后结果累加树为
49
/ \
54 33
/ \ / \
55 52 44 20
总结
Morris算法的思想,是充分利用数据结构的闲置资源(这里则是nullptr指针)。
因此,在算法的优化过程中,若是以压缩空间复杂度为目标,可以寻找数据结构本身存在的闲置资源。
祝愿大家早日寻得内心的美好