题目0099:恢复二叉搜索树

题目描述

二叉搜索树中的两个节点被错误地交换。

请在不改变其结构的情况下,恢复这棵树。

示例1:

输入: [1,3,null,null,2]

   1
  /
 3
  \
   2

输出: [3,1,null,null,2]

   3
  /
 1
  \
   2

示例2:

输入: [3,1,4,null,null,2]

  3
 / \
1   4
   /
  2

输出: [2,1,4,null,null,3]

  2
 / \
1   4
   /
  3

进阶:

使用O(n)空间复杂度的解法很容易实现。

你能想出一个只使用常数空间的解决方案吗?

解题技巧

让我们从直截了当但却不是最优的解决方案开始。这个解决方案具有线性的时间和空间复杂性。

我们直到BST的中序遍历是升序序列。下面展示了如何计算中序遍历。

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
def inorder(r: TreeNode) -> List[int]:
    return inorder(r.left) + [r.val] + inorder(r.right) if r else []

这里被交换了两个节点,因此中序遍历是一个几乎排好序的数组,其中有两个元素被交换。识别排序数组中两个交换元素是可以在线性时间内解决的经典问题。

def find_two_swapped(nums: List[int]) -> (int, int):
    n = len(nums)
    x = y = -1
    for i in range(n - 1):
        if nums[i + 1] < nums[i]:
            y = nums[i + 1]
            # first swap occurence
            if x == -1:     
                x = nums[i]
            # second swap occurence
            else:           
                break
    return x, y

当已知两个交换节点,再次遍历树,交换该节点的值。

算法:

按中序遍历树。遍历后的数组应该是几乎排序的列表,其中只有两个元素被交换。

在线性时间内确定几乎排序数组中的两个交换元素x和y。

再次遍历树,将值x的节点改为y,将值y的节点改为x。

class Solution:
    def recoverTree(self, root: TreeNode):
        """
        :rtype: void Do not return anything, modify root in-place instead.
        """
        def inorder(r: TreeNode) -> List[int]:
            return inorder(r.left) + [r.val] + inorder(r.right) if r else []

        def find_two_swapped(nums: List[int]) -> (int, int):
            n = len(nums)
            x = y = -1
            for i in range(n - 1):
                if nums[i + 1] < nums[i]:
                    y = nums[i + 1]
                    # first swap occurence
                    if x == -1:     
                        x = nums[i]
                    # second swap occurence
                    else:           
                        break
            return x, y

        def recover(r: TreeNode, count: int):
            if r:
                if r.val == x or r.val == y:
                    r.val = y if r.val == x else x
                    count -= 1
                    if count == 0:
                        return      
                recover(r.left, count)
                recover(r.right, count)

        nums = inorder(root)
        x, y = find_two_swapped(nums)
        recover(root, 2)

复杂度分析

时间复杂度:\mathcal{O}(N)。中序遍历需要\mathcal{O}(N)的时间,判断两个交换节点:在最好的情况下是\mathcal{O}(1),在最坏的情况下是\mathcal{O}(N)

空间复杂度:\mathcal{O}(N),我们用nums数组保存了树的中序遍历列表。

算法:

在这里,我们通过迭代构造中序遍历,并在一次遍历中找到交换的节点。

迭代顺序很简单:尽可能的向左走,然后向右走一步,重复一直到结束。

若要找到交换的节点,就记录中序遍历中的最后一个节点pred(即当前节点的前置节点),并与当前节点的值进行比较。如果当前节点的值小于前置节点pred的值,说明该节点是交换节点之一。

交换的节点只有两个,因此在确定了第二个交换节点以后,可以终止遍历。

这样,就可以直接获取节点(而不仅仅是它们的值),从而实现\mathcal{O}(1)的交换时间,大大减少了步骤3所需的时间。

在java中用ArrayDeque代替栈。

class Solution:
    def recoverTree(self, root: TreeNode):
        """
        :rtype: void Do not return anything, modify root in-place instead.
        """
        stack = []
        x = y = pred = None

        while stack or root:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            if pred and root.val < pred.val:
                y = root
                if x is None:
                    x = pred 
                else:
                    break
            pred = root
            root = root.right

        x.val, y.val = y.val, x.val

复杂度分析

时间复杂度:最好的情况下是\mathcal{O}(1);最坏的情况下是交换节点之一是最右边的叶节点时,此时是\mathcal{O}(N)

空间复杂度:最大是\mathcal{O}(H)来维持栈的大小,其中H指的是树的高度。

算法:

方法二的迭代可以转换为递归方式。

递归中序遍历很简单:遵循Left->Node->Right方向,即对左子节点进行递归调用,然后判断该节点是否被交换,然后对右子节点执行递归调用。

class Solution:
    def recoverTree(self, root):
        """
        :type root: TreeNode
        :rtype: void Do not return anything, modify root in-place instead.
        """
        def find_two_swapped(root: TreeNode):
            nonlocal x, y, pred
            if root is None:
                return

            find_two_swapped(root.left)
            if pred and root.val < pred.val:
                y = root
                # first swap occurence
                if x is None:
                    x = pred 
                # second swap occurence
                else:
                    return
            pred = root
            find_two_swapped(root.right)

        x = y = pred = None
        find_two_swapped(root)
        x.val, y.val = y.val, x.val

复杂度分析

时间复杂度:最好的情况下是\mathcal{O}(1);最坏的情况下是交换节点之一是最右边的叶节点时,此时是\mathcal{O}(N)。 空间复杂度:最大是\mathcal{O}(H)来维持递归调用堆栈的大小,其中H指的是树的高度。

算法:

我们已经讨论了迭代和递归中序遍历,但是这两种遍历尽管使用了\mathcal{O}(H)的空间去存储栈空间,但是都有较大的时间复杂度。我们可以通过牺牲性能来届生空间。

Morris的遍历思想很简单:只遍历树而不是用空间。

怎么能够做到呢?在每个节点上,你必须决定下一个遍历的方向:遍历左子树或者右子树。如果不适用额外的空间,怎么指的左子树已经遍历完成了呢?

Morris算法的思想是在节点和它的直接前驱之间设置一个临时的链接:predecessor.right=root,从该节点开始,找到它的直接前驱并验证是否存在链接。

如果没有链接,设置连接并走向左子树。

如果有连接,断开连接并走向右子树。

这里有一个小问题要处理:如果该节点没有左孩子,即没有左子树,则我们直接走向右子树。

class Solution:
    def recoverTree(self, root):
        """
        :type root: TreeNode
        :rtype: void Do not return anything, modify root in-place instead.
        """
        # predecessor is a Morris predecessor. 
        # In the 'loop' cases it could be equal to the node itself predecessor == root.
        # pred is a 'true' predecessor, 
        # the previous node in the inorder traversal.
        x = y = predecessor = pred = None

        while root:
            # If there is a left child
            # then compute the predecessor.
            # If there is no link predecessor.right = root --> set it.
            # If there is a link predecessor.right = root --> break it.
            if root.left:       
                # Predecessor node is one step left 
                # and then right till you can.
                predecessor = root.left
                while predecessor.right and predecessor.right != root:
                    predecessor = predecessor.right

                # set link predecessor.right = root
                # and go to explore left subtree
                if predecessor.right is None:
                    predecessor.right = root
                    root = root.left
                # break link predecessor.right = root
                # link is broken : time to change subtree and go right
                else:
                    # check for the swapped nodes
                    if pred and root.val < pred.val:
                        y = root
                        if x is None:
                            x = pred 
                    pred = root

                    predecessor.right = None
                    root = root.right
            # If there is no left child
            # then just go right.
            else:
                # check for the swapped nodes
                if pred and root.val < pred.val:
                    y = root
                    if x is None:
                        x = pred 
                pred = root

                root = root.right

        x.val, y.val = y.val, x.val

复杂度分析:

时间复杂度:\mathcal{O}(N),我们访问每个节点两次。

空间复杂度:\mathcal{O}(1)