题目描述
给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
示例 1:
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
示例 2:
输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
提示:
树上节点的数目在范围 [2, 1000] 内
-231 <= Node.val <= 231 - 1
进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?
分析
我们知道,BST的中序遍历序列是递增的。交换BST的两个节点,对其中序遍历序列的影响等价于交换两个数的位置。
比如:1 2 3 4 5 6,交换2 5得到1 5 3 4 2 6,我们首先要思考的就是对于给定的序列,怎么找出是哪两个数发生了交换。可以发现,由于2 5的交换,使得原本递增的序列不再递增,第一个出现递减的是5 3,一个很大的数跑到前面了,显然5就是其中一个数,也就是说第一次发生递减的较大的那个数是交换的数;我们再来考察最后一次或者说第二次出现递减的位置,也就是4 2,一个较小的数2跑到了后面,显然是交换得来的,也就是另一个交换的数是最后一次递减位置的较小的那个数。
再比如交换相邻的数2 3得到1 3 2 4 5 6,我们发现只有一个递减对3 2,这时候这个递减对就是交换的数。
总结下两种情况,设x和y是交换的两个数,则在交换后的序列中,第一次发生递减的数对中较大者是x,最后一个发生递减的数对中较小者是y。我们可以遍历交换后的序列的同时,发现逆序对了,如果x还没有赋值,说明是第一次遇见逆序对 ai ai+1,就将ai赋给x。不管是不是第一次遇见逆序对,都将ai+1赋给y,这样y最终的值一定取决于最后的逆序对。
下面说下本题的解题思路。
方法一
对BST做下中序遍历,按遍历顺序存下所有节点,找到两个发生交换的节点,交换其值即可。
怎么按顺序存节点?
我们最开始学习中序遍历时,就是dfs(左),访问,dfs(右),也就是说遍历左右子树中间位置一定是按顺序出现节点的,所有的操作写在中间即可。
class Solution {
public:
vector<TreeNode*> res;
void dfs(TreeNode* root) {
if(!root) return;
dfs(root->left);
res.push_back(root);
dfs(root->right);
}
void recoverTree(TreeNode* root) {
dfs(root);
int n = res.size();
TreeNode* x = NULL;
TreeNode* y = NULL;
for(int i = 0;i < n - 1;i++) {
if(res[i]->val > res[i+1]->val) {
x = res[i];
break;
}
}
if(!x) return;
for(int i = n - 1;i >= 1;i--){
if(res[i]->val < res[i-1]->val){
y = res[i];
break;
}
}
swap(x->val, y->val);
}
};
方法二
之前的方法是将BST的中序遍历结果存下来再交换,但是我们在遍历过程中已经是按照中序的顺序遍历的了,存储操作显然浪费空间。
只需要维护下当前遍历到节点在中序遍历中的前驱节点pre,在遍历过程中如果发现某节点的前驱比它大,尝试更新下x和y就可以了。
怎么保存中序遍历的前驱?
在中序遍历模板中,dfs左子树和右子树之间,访问u之后保存下pre = u,那么下次再次遍历到该位置的代码就是遍历到了下一个节点,自然之前存的pre就是当前u的前驱了。
class Solution {
public:
TreeNode *pre = NULL;
TreeNode* x = NULL;
TreeNode* y = NULL;
void dfs(TreeNode* root) {
if(!root) return;
dfs(root->left);
if(pre && pre->val > root->val) {
if(!x) x = pre;
y = root;
}
pre = root;
dfs(root->right);
}
void recoverTree(TreeNode* root) {
dfs(root);
swap(x->val, y->val);
}
};
方法三(Morris遍历)
即使是方法二,没有自己使用额外空间,却还是在dfs的过程中用掉了系统栈里面O(n)的空间,没有做到题目要求的O(1)的额外空间,想要就地解决问题,就必须使用Morris遍历了。
我们在数据结构中学过线索化二叉树,也就是将空的指针域利用起来。现在,我们对节点u的左孩子(如果有)的右子树的最右边节点建立索引,使其右指针指向其在中序遍历中的下一个节点u,也就是说u的中序前驱就是其左孩子的最右边节点,这是显而易见的。
dfs过程中之所以需要用到栈,就是遍历到没有左孩子的节点时候需要回溯,往上继续找下一个节点,这时我们提前建立好了索引,就不再需要保存回溯信息了。Morris的主要原理就是线索化二叉树,主遍历流程按照正常中序遍历逻辑,先一路向左遍历到最左边的节点,然后再向右遍历,这是主遍历的逻辑。线索化二叉树的逻辑贯穿其中,如果遍历到的节点有左孩子,就尝试用一个指针p沿着左孩子的右子树一路向右,尝试为其建立索引。等到了我们的root真正遍历到建立索引的节点时,就有了明显的前驱后继关系,可以尝试比较更新解了,进入到后继节点免去回溯的操作。
更新前驱pre有两种情况,以下面的树为例:
先回忆下上面说的遍历顺序,使用主遍历指针root以及更新索引的指针p。遍历一个节点,首先用指针p尝试找到其前驱,建立索引,成功建立则将主指针root向左移动,主指针root总的移动顺序是先向左走到不能走了再向右。
遍历节点1,为5建立索引指向1;遍历节点2,为4建立索引指向2,遍历节点4,。
root=4,由于4没有左孩子,就需要向右遍历了,4是从根节点向左遍历的最左边节点,此时向右遍历就可以更新pre = 4了。root就沿着右孩子也就是索引进入2,
root = 2,由于2有左孩子,我们再次用p进入2的左孩子的最右边节点4,此时4存在索引,所以不需要建立索引,root只有建立了索引才能向左,所以此时root不必向左,而应该向右进入5。注意这是p第二次进入索引节点,在root = 2的情况下,马上就要进入下一个节点,此时可以更新pre = 2了,这次更新是在索引节点已经建立的情况下p再次进入索引节点。root向右进入5,4的索引没用了,就可以清空4的索引。
root=5,由于5没有左孩子,尝试向右转向,root再次向右,所以需要更新pre,所以更新下pre = 5,然后就可以继续沿着5的右孩子索引进入1了。
root = 1,p再次进入索引节点5,清空5的索引的同时更新pre = root = 1,右转进入3.
root = 3,还是右转进入的3,所以再次更新pre = 3,3没有左孩子,尝试向右走,也没有索引节点,算法结束。
从上面例子可以发现,当root右转或者p进入已经建立索引的节点时可以更新pre,同时尝试更新x和y。BST中的节点在没有左子树时,第一次向右拐弯前就是真正的访问时机;有左子树时,也就存在前驱节点,当p进入已经建立索引的前驱节点时,root右拐弯前就是该节点的真正访问时机。总而言之,节点u存在左子树,左子树遍历完进入右子树前访问u;节点u不存在左子树,进入右子树前访问u,这也就是中序遍历的顺序。
class Solution {
public:
TreeNode *pre = NULL;
TreeNode* x = NULL;
TreeNode* y = NULL;
void recoverTree(TreeNode* root) {
while(root) {
if(root->left) {//为其寻找前驱索引节点
auto p = root->left;
while(p->right && p->right != root) p = p->right;
if(p->right == NULL) {//建立线索
p->right = root;
root = root->left;
}
else{//索引存在root就可以向右了
if(pre && pre->val > root->val) {
y = root;
if(!x) x = pre;
}
pre = root;
p->right = NULL;
root = root->right;
}
}
else{
if(pre && pre->val > root->val) {//向右更新pre
y = root;
if(!x) x = pre;
}
pre = root;
root = root->right;
}
}
swap(x->val,y->val);
}
};