题目来源
题目描述
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
void recoverTree(TreeNode* root) {
}
};
题目解析
三个指针:找到这两个错误节点
用三个指针:
- first、second分别指向第一个和第二个错乱位置的节点
- pre指向当前节点的中序遍历的前一个节点
- 如果对所有的节点值都不一样的搜索二叉树进行中序遍历,那么出现的节点值会一直升序。如果,如果有两个节点位置错了,就会出现降序
- 如果出现了两次降序
- 第一个错误的节点为第一次降序时较大的节点
- 第二个错误的节点为第二次降序时较小的节点
- 举个例子:
- 正确的中序遍历顺序: {1、2、3、4、5}
- 遍历得到的中序遍历:{1、5、3、4、2}
- 第一次降序为5->3,所以第一个错误的节点为5
- 第二次降序为4->2,所以第二个错误的节点为2
- 把5和2交换即可恢复
- 如果出现了一次降序
- 第一个错误的节点为本次降序时较大的节点
- 第二个错误的节点为本次降序时较小的节点
- 举个例子:
- 正确的中序遍历顺序: {1、2、3、4、5}
- 遍历得到的中序遍历:{1、2、4、3、5}
- 只有一次降序4->3,所以第一个错误节点为4,第二个错误节点为3
- 把4和3交换过来即可恢复
- 寻找两个错误节点的过程可以总结为:第一个错误节点为第一次降序是较大的节点;第二个错误节点为最后一次降序是较小的节点
- 所以,关键点中序遍历,实现如下:
即:中序遍历BST,依次访问的节点值是递增的,错误的BST会破坏递增性,从而能定位出错误。错误有两种
- 错误1:出现了两对不满足前小后大,需要交换第一对的第一个元素与第二对的第二个元素。
- 错误2:只出现一对不满足前小后大,交换这一对元素即可。
- 只用比较前后访问的节点值,prev 保存上一个访问的节点,当前访问的是 root 节点。
- 每访问一个节点,如果prev.val>=root.val,就找到了一对“错误对”。
- 检查一下它是第一对错误对,还是第二对错误对。
- 遍历结束,就确定了待交换的两个错误点,进行交换。
class Solution {
TreeNode *pre = NULL, *first = NULL, *second = NULL;
void inorder(TreeNode* root){
if(root == NULL){
return;
}
inorder(root->left);
if (pre != NULL && pre->val > root->val) {
if (first == NULL) first = pre;
second = root;
}
pre = root;
inorder(root->right);
}
public:
void recoverTree(TreeNode* root) {
inorder(root);
swap(first->val, second->val);
}
};
其迭代写法
class Solution {
public:
void recoverTree(TreeNode* root) {
TreeNode *pre = NULL, *first = NULL, *second = NULL, *p = root;
stack<TreeNode*> st;
while (p || !st.empty()) {
while (p) {
st.push(p);
p = p->left;
}
p = st.top(); st.pop();
if (pre) {
if (pre->val > p->val) {
if (!first) first = pre;
second = p;
}
}
pre = p;
p = p->right;
}
swap(first->val, second->val);
}
};
使用栈写法:
class Solution {
public:
std::vector<TreeNode*> getTwoErrNodes(TreeNode* root) {
std::vector<TreeNode*> errs(2, nullptr);
if(root == nullptr){
return errs;
}
std::stack<TreeNode*> stack;
TreeNode *prev = nullptr, *curr = root;
while (!stack.empty() || curr != nullptr){
if(curr != nullptr){
stack.push(curr);
curr = curr->left;
}else{
curr = stack.top(); stack.pop();
if(prev != nullptr && prev->val > curr->val){
errs[0] = errs[0] == nullptr ? prev : errs[0];
errs[1] = curr;
}
prev = curr;
curr = curr->right;
}
}
return errs;
}
};
在结构上交换这两个错误节点
如果想要在结构上交换这两个错误节点,需要先找到这两个错误节点的父节点。
- 假设第一个错误节点是e1,e1的父节点是e1P,e1的左孩子为e1L,e1的右孩子为e1R
- 假设第二个错误节点是e2,e1的父节点是e2P,e2的左孩子为e2L,e2的右孩子为e2R
在结构上交换两个节点,实际上是把两个节点互换环境。即:
- e2成为e1P的孩子,e1L和e1R成为e2的孩子节点
- e1成为e2P的孩子,e2L和e2R成为e1的孩子节点
但是,注意要处理很多特殊情况:
- 比如,如果 e1 是头节点,则意味着 e1P 为 null,那么让 e2 成为 e1P 的孩子节点时,关于 e1P的任何 left 指针或 right 指针操作都会发生错误,因为 e1P 为 null 则根本没有 Node 类型节点的结构。
- 再如,如果 e1 本身就是 e2 的左孩子节点,即 e1==e2L,那么让 e2L 成为 e1 的左孩子节点时,e1 的 left 指针将指向 e2L,将会指向自己,这会让整棵二叉树发生严重的结构错误。
换句话说,我们必须理清楚 e1 及其上下环境之间的关系,e2 及其上下环境之间的关系,以及两个环境之间是否有联系。有以下三个问题和一个特别注意是必须关注的。
- 情况一:e1和e2是否有一个头结点?如果有,谁是头
- 情况二:e1和e2是否相邻?如果相邻,谁是谁的父节点
- 情况三:e1和e2分别是各自父节点的左孩子还是右孩子
特别注意:因为是在中序遍历时先找到e1,后找到e2,所以e1 一定不是e2 的右孩子节点,e2 也一定不是e1 的左孩子节点。
以上三个问题与特别注意之间相互影响,情况非常复杂。经过仔细整理,共有 14 种情况,每一种情况在调整 e1 和 e2 各自的拓扑关系时都有特殊处理。
- e1 是头节点,e1 是e2 的父节点,此时e2 只可能是e1 的右孩子节点。
- e1 是头节点,e1 不是e2 的父节点,e2 是e2P 的左孩子节点。
- e1 是头节点,e1 不是e2 的父节点,e2 是e2P 的右孩子节点。
- e2 是头节点,e2 是e1 的父节点,此时e1 只可能是e2 的左孩子节点。
- e2 是头节点,e2 不是e1 的父节点,e1 是e1P 的左孩子节点。
- e2 是头节点,e2 不是e1 的父节点,e1 是e1P 的右孩子节点。
- e1 和e2 都不是头节点,e1 是e2 的父节点,此时e2 只可能是e1 的右孩子节点,e1 是e1P 的左孩子节点。
我们先来找到e1和e2的父节点
std::vector<TreeNode*> getTwoErrParents(TreeNode* root, TreeNode *e1, TreeNode* e2){
std::vector<TreeNode*> parents(2, nullptr);
if(root == nullptr){
return parents;
}
std::stack<TreeNode*> stack;
TreeNode *prev = nullptr, *curr = root;
while (!stack.empty() || curr != nullptr){
if(curr != nullptr){
stack.push(curr);
curr = curr->left;
}else{
curr = stack.top(); stack.pop();
if(curr->left == e1 || curr->right == e1){
parents[0] = curr;
}
if(curr->left == e2 || curr->right == e2){
parents[1] = curr;
}
curr = curr->right;
}
}
return parents;
}
递归
用中序遍历树,并将所有的节点都存到一个一维向量中,然后对存节点值的一维向量排序,再将拍好的数组按照顺序赋给节点。
class Solution {
void inorder(TreeNode* root, std::vector<TreeNode *> &list, std::vector<int> &vals){
if(root == NULL){
return;
}
inorder(root->left, list, vals);
list.push_back(root);
vals.push_back(root->val);
inorder(root->right, list, vals);
}
public:
void recoverTree(TreeNode* root) {
std::vector<TreeNode *>list;
std::vector<int> vals;
inorder(root, list, vals);
sort(vals.begin(), vals.end());
for (int i = 0; i < vals.size(); ++i) {
list[i]->val = vals[i];
}
}
};
这种最一般的解法可以针对任意个数目的节点错乱的情况
Morris 遍历
这道题的真正符合要求的解法应该用的 Morris 遍历,这是一种非递归且不使用栈,空间复杂度为 O(1) 的遍历方法