leet-code-99

99. 恢复二叉搜索树

难度 中等


给你二叉搜索树的根节点 root ,该树中的恰好两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。

示例 1:

1
2
3
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 3 使二叉搜索树有效。

示例 2:

1
2
3
输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 3 使二叉搜索树有效。

个人题解

由于是搜索二叉树,中序遍历的结果是一个单调不减的数组。因此我们对给定的二叉树进行中序遍历,用链表储存遍历结果,再对链表中所有元素进行遍历,找到不符合单调不减的两个数记录为ij。之后再次遍历二叉树,对这两个节点进行交换,由于不能改变结构,因此只需要交换value
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {

List<Integer> nums = new ArrayList<>();

public void recoverTree(TreeNode root) {
mid(root);
int[] n = findTwoSwapped();
recover(root,2,n[0],n[1]);
}

public void mid(TreeNode node){
if(node == null)
return;
mid(node.left);
nums.add(node.val);
mid(node.right);
}

public int[] findTwoSwapped() {
int n = nums.size();
int index1 = -1, index2 = -1;
for (int i = 0; i < n - 1; ++i) {
if (nums.get(i + 1) < nums.get(i)) {
index2 = i + 1;
if (index1 == -1) {
index1 = i;
} else {
break;
}
}
}
int x = nums.get(index1), y = nums.get(index2);
return new int[]{x, y};
}

public void recover(TreeNode node,int num,int num1,int num2){
if(node==null)
return;
else if(node.val==num1||node.val==num2){
node.val = node.val == num1?num2:num1;
if(--num==0)
return;
}
recover(node.left,num,num1,num2);
recover(node.right,num,num1,num2);
}

}

时间复杂度O(N)
空间复杂度O(N)


我个人的解法是通过链表来查询出错的两个节点,但很显然,我们可以在第一次遍历的时候就能查出错误节点,但本人能力有限,因此查看题解,发现有更优解:

官方题解

方法二:隐式中序遍历
具体来说,由于我们只关心中序遍历的值序列中每个相邻的位置的大小关系是否满足条件,且错误交换后最多两个位置不满足条件,因此在中序遍历的过程我们只需要维护当前中序遍历到的最后一个节点 pred,然后在遍历到下一个节点的时候,看两个节点的值是否满足前者小于后者即可,如果不满足说明找到了一个交换的节点,且在找到两次以后就可以终止遍历。

这样我们就可以在中序遍历中直接找到被错误交换的两个节点 xy,不用显式建立 nums 数组。

中序遍历的实现有迭代和递归两种等价的写法,在本方法中提供迭代实现的写法。使用迭代实现中序遍历需要手动维护栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public void recoverTree(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
TreeNode x = null, y = null, pred = null;

while (!stack.isEmpty() || root != null) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if (pred != null && root.val < pred.val) {
y = root;
if (x == null) {
x = pred;
} else {
break;
}
}
pred = root;
root = root.right;
}

swap(x, y);
}

public void swap(TreeNode x, TreeNode y) {
int tmp = x.val;
x.val = y.val;
y.val = tmp;
}
}

复杂度分析

时间复杂度:最坏情况下(即待交换节点为二叉搜索树最右侧的叶子节点)我们需要遍历整棵树,时间复杂度为 O(N),其中 N 为二叉搜索树的节点个数。
空间复杂度:O(H),其中 H 为二叉搜索树的高度。中序遍历的时候栈的深度取决于二叉搜索树的高度。

方法三:Morris 中序遍历

思路与算法

方法二中我们不再显示的用数组存储中序遍历的值序列,但是我们会发现我们仍需要 O(H) 的栈空间,无法满足题目的进阶要求,那么该怎么办呢?这里向大家介绍一种不同于平常递归或迭代的遍历二叉树的方法:Morris 遍历算法,该算法能将非递归的中序遍历空间复杂度降为 O(1)

Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 xx):

  1. 如果x无左孩子,则访问 x 的右孩子,即 x = x.x.right
  2. 如果 x 有左孩子,则找到 x 左子树上最右的节点(即左子树中序遍历的最后一个节点,x 在中序遍历中的前驱节点),我们记为 predecessor。根据 predecessor 的右孩子是否为空,进行如下操作。
    • 如果predecessor 的右孩子为空,则将其右孩子指向 x,然后访问 x 的左孩子,即 x = x.left
    • 如果 predecessor 的右孩子不为空,则此时其右孩子指向 x,说明我们已经遍历完 x 的左子树,我们将 predecessor 的右孩子置空,然后访问 x 的右孩子,即 x = x.right
  3. 重复上述操作,直至访问完整棵树。

其实整个过程我们就多做一步:将当前节点左子树中最右边的节点指向它,这样在左子树遍历完成后我们通过这个指向走回了 xx,且能再通过这个知晓我们已经遍历完成了左子树,而不用再通过栈来维护,省去了栈的空间复杂度。

了解完这个算法以后,其他地方与方法二并无不同,我们同样也是维护一个 pred 变量去比较即可,具体实现可以看下面的代码,这里不再赘述。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Solution {
public void recoverTree(TreeNode root) {
TreeNode x = null, y = null, pred = null, predecessor = null;

while (root != null) {
if (root.left != null) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root.left;
while (predecessor.right != null && predecessor.right != root) {
predecessor = predecessor.right;
}

// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor.right == null) {
predecessor.right = root;
root = root.left;
}
// 说明左子树已经访问完了,我们需要断开链接
else {
if (pred != null && root.val < pred.val) {
y = root;
if (x == null) {
x = pred;
}
}
pred = root;

predecessor.right = null;
root = root.right;
}
}
// 如果没有左孩子,则直接访问右孩子
else {
if (pred != null && root.val < pred.val) {
y = root;
if (x == null) {
x = pred;
}
}
pred = root;
root = root.right;
}
}
swap(x, y);
}

public void swap(TreeNode x, TreeNode y) {
int tmp = x.val;
x.val = y.val;
y.val = tmp;
}
}

复杂度分析

  • 时间复杂度:O(N)O(N),其中 NN 为二叉搜索树的高度。Morris 遍历中每个节点会被访问两次,因此总时间复杂度为 O(2N)=O(N)O(2N)=O(N)。
  • 空间复杂度:O(1)O(1)。

转载来自恢复二叉搜索树


leet-code-99
http://example.com/2022/06/19/leet-code-99/
Author
John Doe
Posted on
June 19, 2022
Licensed under