文章目录
- 题目
- 标题和出处
- 难度
- 题目描述
- 要求
- 示例
- 数据范围
- 进阶
- 解法一
- 思路和算法
- 代码
- 复杂度分析
- 解法二
- 思路和算法
- 代码
- 复杂度分析
- 解法三
- 思路和算法
- 代码
- 复杂度分析
题目
标题和出处
标题:恢复二叉搜索树
出处:99. 恢复二叉搜索树
难度
5 级
题目描述
要求
给定二叉搜索树的根结点 root \texttt{root} root,该树中的恰好两个结点的值被错误地交换。请在不改变其结构的情况下恢复这个树。
示例
示例 1:
输入: root = [1,3,null,null,2] \texttt{root = [1,3,null,null,2]} root = [1,3,null,null,2]
输出: [3,1,null,null,2] \texttt{[3,1,null,null,2]} [3,1,null,null,2]
解释: 3 \texttt{3} 3 不能是 1 \texttt{1} 1 的左子结点,因为 3 > 1 \texttt{3} > \texttt{1} 3>1。交换 1 \texttt{1} 1 和 3 \texttt{3} 3 使二叉搜索树有效。
示例 2:
输入: root = [3,1,4,null,null,2] \texttt{root = [3,1,4,null,null,2]} root = [3,1,4,null,null,2]
输出: [2,1,4,null,null,3] \texttt{[2,1,4,null,null,3]} [2,1,4,null,null,3]
解释: 2 \texttt{2} 2 不能在 3 \texttt{3} 3 的右子树中,因为 2 < 3 \texttt{2} < \texttt{3} 2<3。交换 2 \texttt{2} 2 和 3 \texttt{3} 3 使二叉搜索树有效。
数据范围
- 树中结点数目在范围 [2, 1000] \texttt{[2, 1000]} [2, 1000] 内
- -2 31 ≤ Node.val ≤ 2 31 − 1 \texttt{-2}^\texttt{31} \le \texttt{Node.val} \le \texttt{2}^\texttt{31} - \texttt{1} -231≤Node.val≤231−1
进阶
使用 O(n) \texttt{O(n)} O(n) 空间复杂度的解法很简单,你可以想出使用 O(1) \texttt{O(1)} O(1) 空间的解决方案吗?
解法一
思路和算法
由于二叉搜索树的中序遍历序列是单调递增的,因此可以通过中序遍历序列找到交换了值的两个结点,然后将这两个结点的值恢复。
假设二叉搜索树有 n n n 个结点,中序遍历序列是 [ x 0 , x 1 , … , x n − 1 ] [x_0, x_1, \ldots, x_{n - 1}] [x0,x1,…,xn−1],则对于任意 0 ≤ i < n − 1 0 \le i < n - 1 0≤i<n−1 都有 x i < x i + 1 x_i < x_{i + 1} xi<xi+1。将交换了值的两个结点的原结点值记为 x j x_j xj 和 x k x_k xk,其中 j < k j < k j<k,则 x j < x k x_j < x_k xj<xk。交换结点值之后的中序遍历序列中存在逆序对,即相邻的两个结点值当中,前面的值大于后面的值,在中序遍历序列中寻找逆序对的同时定位到交换了值的两个结点。
-
如果 k − j = 1 k - j = 1 k−j=1,即 x j x_j xj 和 x k x_k xk 在中序遍历序列中相邻,则只有这两个结点值在交换之后产生一个逆序对,因此当中序遍历序列中存在一个逆序对时,逆序对的两个值对应的结点即为交换了值的两个结点。
-
如果 k − j > 1 k - j > 1 k−j>1,即 x j x_j xj 和 x k x_k xk 在中序遍历序列中不相邻,则这两个结点值在交换之后分别产生一个逆序对,即 x k > x j + 1 x_k > x_{j + 1} xk>xj+1 和 x k − 1 > x j x_{k - 1} > x_j xk−1>xj,因此当中序遍历序列中存在两个逆序对时,第一个逆序对的前一个结点和第二个逆序对的后一个结点即为交换了值的两个结点。
在定位到交换了值的两个结点之后,将这两个结点的值交换,即恢复了二叉搜索树。
具体做法是,首先对给定的二叉搜索树(在交换两个结点值之后)中序遍历,中序遍历序列中存储结点,然后遍历中序遍历序列,统计逆序对的数量以及定位到交换了值的两个结点,遍历结束之后,将两个结点的值交换,完成二叉搜索树的恢复。
代码
下面的代码为递归实现二叉搜索树中序遍历的做法。
class Solution {List<TreeNode> traversal = new ArrayList<TreeNode>();public void recoverTree(TreeNode root) {inorder(root);int index1 = -1, index2 = -1;int size = traversal.size();for (int i = 1; i < size; i++) {if (traversal.get(i).val < traversal.get(i - 1).val) {if (index1 < 0) {index1 = i - 1;} else {index2 = i;}}}TreeNode node1 = null, node2 = null;if (index2 < 0) {node1 = traversal.get(index1);node2 = traversal.get(index1 + 1);} else {node1 = traversal.get(index1);node2 = traversal.get(index2);}int val1 = node1.val, val2 = node2.val;node1.val = val2;node2.val = val1;}public void inorder(TreeNode node) {if (node == null) {return;}inorder(node.left);traversal.add(node);inorder(node.right);}
}
下面的代码为迭代实现二叉搜索树中序遍历的做法。
class Solution {public void recoverTree(TreeNode root) {List<TreeNode> traversal = new ArrayList<TreeNode>();Deque<TreeNode> stack = new ArrayDeque<TreeNode>();TreeNode node = root;while (!stack.isEmpty() || node != null) {while (node != null) {stack.push(node);node = node.left;}node = stack.pop();traversal.add(node);node = node.right;}int index1 = -1, index2 = -1;int size = traversal.size();for (int i = 1; i < size; i++) {if (traversal.get(i).val < traversal.get(i - 1).val) {if (index1 < 0) {index1 = i - 1;} else {index2 = i;}}}TreeNode node1 = null, node2 = null;if (index2 < 0) {node1 = traversal.get(index1);node2 = traversal.get(index1 + 1);} else {node1 = traversal.get(index1);node2 = traversal.get(index2);}int val1 = node1.val, val2 = node2.val;node1.val = val2;node2.val = val1;}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉搜索树的结点数。中序遍历需要访问每个结点一次,需要 O ( n ) O(n) O(n) 的时间,交换结点值需要 O ( 1 ) O(1) O(1) 的时间,因此时间复杂度是 O ( n ) O(n) O(n)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉搜索树的结点数。中序遍历的递归实现和迭代实现都需要栈空间,栈空间取决于二叉搜索树的高度,最坏情况下二叉搜索树的高度是 O ( n ) O(n) O(n),存储中序遍历序列需要 O ( n ) O(n) O(n) 的空间。
解法二
思路和算法
由于只需要定位到交换了值的两个结点,因此并不需要得到完整的中序遍历序列,只要能确定交换了值的两个结点,即可结束遍历。
中序遍历的过程中,如果遇到一个逆序对,则无法确定是否还有第二个逆序对,只有当遇到第二个逆序对时,才能确定交换了值的两个结点。因此,当第二次遇到逆序对时,定位到交换了值的两个结点,即可提前退出。
定位到交换了值的两个结点之后,将两个结点的值交换,完成二叉搜索树的恢复。
代码
class Solution {public void recoverTree(TreeNode root) {Deque<TreeNode> stack = new ArrayDeque<TreeNode>();TreeNode prev = null, curr = root;TreeNode node1 = null, node2 = null;while (!stack.isEmpty() || curr != null) {while (curr != null) {stack.push(curr);curr = curr.left;}curr = stack.pop();if (prev != null && curr.val < prev.val) {node2 = curr;if (node1 == null) {node1 = prev;} else {break;}}prev = curr;curr = curr.right;}int val1 = node1.val, val2 = node2.val;node1.val = val2;node2.val = val1;}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉搜索树的结点数。每个结点最多被访问一次。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉搜索树的结点数。空间复杂度主要是栈空间,取决于二叉搜索树的高度,最坏情况下二叉搜索树的高度是 O ( n ) O(n) O(n)。
解法三
思路和算法
莫里斯遍历是使用常数空间遍历二叉树的方法,使用莫里斯遍历对二叉搜索树中序遍历,可将空间复杂度降低到 O ( 1 ) O(1) O(1)。
莫里斯中序遍历的过程中,统计逆序对的数量以及定位到交换了值的两个结点,遍历结束之后,将两个结点的值交换,完成二叉搜索树的恢复。
由于莫里斯遍历的过程中会改变树的结构,只有当遍历结束时才能确保树的结构恢复,因此莫里斯遍历不能提前退出。
代码
class Solution {public void recoverTree(TreeNode root) {TreeNode prev = null, curr = root;TreeNode node1 = null, node2 = null;while (curr != null) {if (curr.left == null) {if (prev != null && curr.val < prev.val) {if (node1 == null) {node1 = prev;}node2 = curr;}prev = curr;curr = curr.right;} else {TreeNode predecessor = curr.left;while (predecessor.right != null && predecessor.right != curr) {predecessor = predecessor.right;}if (predecessor.right == null) {predecessor.right = curr;curr = curr.left;} else {predecessor.right = null;if (prev != null && curr.val < prev.val) {if (node1 == null) {node1 = prev;}node2 = curr;}prev = curr;curr = curr.right;}}}int val1 = node1.val, val2 = node2.val;node1.val = val2;node2.val = val1;}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉搜索树的结点数。使用莫里斯遍历,每个结点最多被访问两次。
-
空间复杂度: O ( 1 ) O(1) O(1)。