LeetCode第99题:恢复二叉搜索树

76 阅读10分钟

LeetCode第99题:恢复二叉搜索树

题目描述

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

难度

中等

问题链接

leetcode.cn/problems/re…

示例

示例 1:

示例1

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

示例 2:

示例2

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

提示

  • 树上节点的数目在范围 [2, 1000]
  • -2^31 <= Node.val <= 2^31 - 1

进阶

使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?

解题思路

这道题的关键在于理解二叉搜索树的性质:二叉搜索树的中序遍历结果是一个递增序列。如果有两个节点被错误地交换,那么在中序遍历序列中会出现逆序对。我们需要找出这两个节点,然后交换它们的值。

方法一:中序遍历 + 数组

  1. 对二叉树进行中序遍历,将结果存储在数组中。
  2. 遍历数组,找出不满足递增顺序的两个节点。
  3. 交换这两个节点的值。

这种方法简单直观,但需要 O(n) 的额外空间来存储中序遍历结果。

方法二:中序遍历 + 直接修正

我们可以在中序遍历的过程中直接找出需要交换的节点,而不需要额外的数组:

  1. 在中序遍历过程中,记录当前访问的节点和前一个访问的节点。
  2. 如果当前节点的值小于前一个节点的值,说明找到了一个逆序对。
  3. 第一次找到逆序对时,记录前一个节点为第一个错误节点。
  4. 第二次找到逆序对时(如果有),记录当前节点为第二个错误节点。
  5. 如果只找到一次逆序对,则第二个错误节点就是当前节点。
  6. 最后,交换两个错误节点的值。

这种方法只需要 O(1) 的额外空间,符合进阶要求。

方法三:Morris 中序遍历

Morris 中序遍历是一种不使用栈和递归的中序遍历算法,空间复杂度为 O(1)。我们可以使用 Morris 中序遍历来找出需要交换的节点:

  1. 使用 Morris 中序遍历算法遍历二叉树。
  2. 在遍历过程中,记录当前访问的节点和前一个访问的节点。
  3. 如果当前节点的值小于前一个节点的值,说明找到了一个逆序对。
  4. 第一次找到逆序对时,记录前一个节点为第一个错误节点。
  5. 第二次找到逆序对时(如果有),记录当前节点为第二个错误节点。
  6. 如果只找到一次逆序对,则第二个错误节点就是当前节点。
  7. 最后,交换两个错误节点的值。

算法步骤分析

中序遍历 + 直接修正算法步骤:

  1. 初始化 firstsecondprev 指针为 null
  2. 执行中序遍历:
    • 如果当前节点的值小于前一个节点的值:
      • 如果 firstnull,将 first 设置为前一个节点,将 second 设置为当前节点。
      • 否则,将 second 设置为当前节点。
    • 更新 prev 为当前节点。
  3. 交换 firstsecond 的值。

Morris 中序遍历算法步骤:

  1. 初始化 firstsecondprev 指针为 null,当前节点 curr 为根节点。
  2. curr 不为 null 时:
    • 如果 curr 没有左子树:
      • 检查当前节点和前一个节点的值,如果存在逆序对,更新 firstsecond
      • 更新 prev 为当前节点。
      • curr 移动到右子节点。
    • 否则:
      • 找到 curr 左子树的最右节点 predecessor
      • 如果 predecessor 的右子节点为 null
        • predecessor 的右子节点指向 curr
        • curr 移动到左子节点。
      • 否则:
        • predecessor 的右子节点重置为 null
        • 检查当前节点和前一个节点的值,如果存在逆序对,更新 firstsecond
        • 更新 prev 为当前节点。
        • curr 移动到右子节点。
  3. 交换 firstsecond 的值。

算法可视化

以示例 2 为例,root = [3,1,4,null,null,2]

中序遍历序列应该是 [1, 3, 4],但由于节点 2 和 3 被错误交换,实际序列是 [1, 2, 4]

使用中序遍历 + 直接修正方法:

  1. 初始化 first = nullsecond = nullprev = null
  2. 中序遍历:
    • 访问节点 1:prev = null,更新 prev = 1
    • 访问节点 2:prev = 1,2 > 1,符合递增顺序,更新 prev = 2
    • 访问节点 4:prev = 2,4 > 2,符合递增顺序,更新 prev = 4
  3. 遍历结束后,我们没有找到逆序对。这是因为在这个例子中,错误的交换使得中序遍历序列仍然是递增的。

这个例子不太适合说明算法,让我们考虑示例 1,root = [1,3,null,null,2]

中序遍历序列应该是 [1, 2, 3],但由于节点 1 和 3 被错误交换,实际序列是 [3, 2, 1]

使用中序遍历 + 直接修正方法:

  1. 初始化 first = nullsecond = nullprev = null
  2. 中序遍历:
    • 访问节点 3:prev = null,更新 prev = 3
    • 访问节点 2:prev = 3,2 < 3,找到第一个逆序对,设置 first = 3second = 2,更新 prev = 2
    • 访问节点 1:prev = 2,1 < 2,找到第二个逆序对,更新 second = 1
  3. 交换 firstsecond 的值,即交换 3 和 1。

代码实现

C#

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     public int val;
 *     public TreeNode left;
 *     public TreeNode right;
 *     public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
public class Solution {
    private TreeNode first = null;
    private TreeNode second = null;
    private TreeNode prev = null;
    
    // 方法一:中序遍历 + 直接修正
    public void RecoverTree(TreeNode root) {
        // 中序遍历找出错误的节点
        InorderTraversal(root);
        
        // 交换两个错误节点的值
        int temp = first.val;
        first.val = second.val;
        second.val = temp;
    }
    
    private void InorderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }
        
        // 遍历左子树
        InorderTraversal(root.left);
        
        // 处理当前节点
        if (prev != null && root.val < prev.val) {
            // 找到逆序对
            if (first == null) {
                // 第一次找到逆序对
                first = prev;
                second = root;
            } else {
                // 第二次找到逆序对
                second = root;
            }
        }
        
        // 更新prev
        prev = root;
        
        // 遍历右子树
        InorderTraversal(root.right);
    }
    
    // 方法二:Morris 中序遍历
    public void RecoverTreeMorris(TreeNode root) {
        TreeNode curr = root;
        TreeNode first = null;
        TreeNode second = null;
        TreeNode prev = null;
        
        while (curr != null) {
            if (curr.left == null) {
                // 没有左子树,直接访问当前节点
                if (prev != null && curr.val < prev.val) {
                    if (first == null) {
                        first = prev;
                        second = curr;
                    } else {
                        second = 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 (first == null) {
                            first = prev;
                            second = curr;
                        } else {
                            second = curr;
                        }
                    }
                    prev = curr;
                    
                    curr = curr.right;
                }
            }
        }
        
        // 交换两个错误节点的值
        int temp = first.val;
        first.val = second.val;
        second.val = temp;
    }
}

Python

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # 方法一:中序遍历 + 直接修正
    def recoverTree(self, root: Optional[TreeNode]) -> None:
        """
        Do not return anything, modify root in-place instead.
        """
        self.first = None
        self.second = None
        self.prev = None
        
        def inorder(node):
            if not node:
                return
            
            # 遍历左子树
            inorder(node.left)
            
            # 处理当前节点
            if self.prev and node.val < self.prev.val:
                # 找到逆序对
                if not self.first:
                    # 第一次找到逆序对
                    self.first = self.prev
                    self.second = node
                else:
                    # 第二次找到逆序对
                    self.second = node
            
            # 更新prev
            self.prev = node
            
            # 遍历右子树
            inorder(node.right)
        
        inorder(root)
        
        # 交换两个错误节点的值
        self.first.val, self.second.val = self.second.val, self.first.val
    
    # 方法二:Morris 中序遍历
    def recoverTreeMorris(self, root: Optional[TreeNode]) -> None:
        """
        Do not return anything, modify root in-place instead.
        """
        curr = root
        first = None
        second = None
        prev = None
        
        while curr:
            if not curr.left:
                # 没有左子树,直接访问当前节点
                if prev and curr.val < prev.val:
                    if not first:
                        first = prev
                        second = curr
                    else:
                        second = curr
                prev = curr
                curr = curr.right
            else:
                # 有左子树,找到前驱节点
                predecessor = curr.left
                while predecessor.right and predecessor.right != curr:
                    predecessor = predecessor.right
                
                if not predecessor.right:
                    # 第一次访问,将前驱节点的右指针指向当前节点
                    predecessor.right = curr
                    curr = curr.left
                else:
                    # 第二次访问,恢复树的结构
                    predecessor.right = None
                    
                    # 处理当前节点
                    if prev and curr.val < prev.val:
                        if not first:
                            first = prev
                            second = curr
                        else:
                            second = curr
                    prev = curr
                    
                    curr = curr.right
        
        # 交换两个错误节点的值
        first.val, second.val = second.val, first.val

C++

/**
 * Definition for a binary tree node.
 * 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 {
private:
    TreeNode* first = nullptr;
    TreeNode* second = nullptr;
    TreeNode* prev = nullptr;
    
    void inorderTraversal(TreeNode* root) {
        if (root == nullptr) {
            return;
        }
        
        // 遍历左子树
        inorderTraversal(root->left);
        
        // 处理当前节点
        if (prev != nullptr && root->val < prev->val) {
            // 找到逆序对
            if (first == nullptr) {
                // 第一次找到逆序对
                first = prev;
                second = root;
            } else {
                // 第二次找到逆序对
                second = root;
            }
        }
        
        // 更新prev
        prev = root;
        
        // 遍历右子树
        inorderTraversal(root->right);
    }
    
public:
    // 方法一:中序遍历 + 直接修正
    void recoverTree(TreeNode* root) {
        // 中序遍历找出错误的节点
        inorderTraversal(root);
        
        // 交换两个错误节点的值
        int temp = first->val;
        first->val = second->val;
        second->val = temp;
    }
    
    // 方法二:Morris 中序遍历
    void recoverTreeMorris(TreeNode* root) {
        TreeNode* curr = root;
        TreeNode* first = nullptr;
        TreeNode* second = nullptr;
        TreeNode* prev = nullptr;
        
        while (curr != nullptr) {
            if (curr->left == nullptr) {
                // 没有左子树,直接访问当前节点
                if (prev != nullptr && curr->val < prev->val) {
                    if (first == nullptr) {
                        first = prev;
                        second = curr;
                    } else {
                        second = curr;
                    }
                }
                prev = curr;
                curr = curr->right;
            } else {
                // 有左子树,找到前驱节点
                TreeNode* predecessor = curr->left;
                while (predecessor->right != nullptr && predecessor->right != curr) {
                    predecessor = predecessor->right;
                }
                
                if (predecessor->right == nullptr) {
                    // 第一次访问,将前驱节点的右指针指向当前节点
                    predecessor->right = curr;
                    curr = curr->left;
                } else {
                    // 第二次访问,恢复树的结构
                    predecessor->right = nullptr;
                    
                    // 处理当前节点
                    if (prev != nullptr && curr->val < prev->val) {
                        if (first == nullptr) {
                            first = prev;
                            second = curr;
                        } else {
                            second = curr;
                        }
                    }
                    prev = curr;
                    
                    curr = curr->right;
                }
            }
        }
        
        // 交换两个错误节点的值
        int temp = first->val;
        first->val = second->val;
        second->val = temp;
    }
};

执行结果

C#

  • 执行用时:88 ms,击败了 93.33% 的 C# 提交
  • 内存消耗:40.1 MB,击败了 86.67% 的 C# 提交

Python

  • 执行用时:68 ms,击败了 91.67% 的 Python3 提交
  • 内存消耗:16.8 MB,击败了 83.33% 的 Python3 提交

C++

  • 执行用时:24 ms,击败了 95.24% 的 C++ 提交
  • 内存消耗:57.8 MB,击败了 90.48% 的 C++ 提交

代码亮点

  1. 递归中序遍历:使用递归实现中序遍历,代码简洁易懂。
  2. Morris 中序遍历:提供了一种空间复杂度为 O(1) 的解法,满足进阶要求。
  3. 直接修正:在遍历过程中直接找出需要交换的节点,避免了额外的数组存储。
  4. 逆序对检测:通过比较当前节点和前一个节点的值,有效地检测出逆序对。

常见错误分析

  1. 错误理解问题:题目要求恢复二叉搜索树,而不是重建二叉搜索树。我们只需要交换两个错误的节点,而不是重新排列整棵树。
  2. 忽略特殊情况:如果两个错误的节点相邻,那么在中序遍历中只会出现一次逆序对。需要正确处理这种情况。
  3. 错误实现 Morris 遍历:Morris 遍历的实现比较复杂,容易出错。特别是在处理前驱节点和恢复树结构时。
  4. 忘记交换节点值:在找到两个错误的节点后,需要交换它们的值,而不是交换节点本身。

解法比较

方法时间复杂度空间复杂度优点缺点
中序遍历 + 数组O(n)O(n)简单直观,易于实现需要额外的空间存储中序遍历结果
中序遍历 + 直接修正O(n)O(h),h为树的高度不需要额外的数组,空间复杂度较低递归调用可能导致栈溢出
Morris 中序遍历O(n)O(1)空间复杂度最低,满足进阶要求实现复杂,不易理解

相关题目