原题:链接
二叉搜索树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
示例1:
示例2:
进阶:
你能想出一个只使用常数空间的解决方案吗?
这是一道难度为hard的题,可以通过这道题熟悉递归并了解中序遍历的几种方法。
暂时先不考虑进阶的要求,我们先捋一下思路。
首先这是一个二叉搜索树,因为意外有两个点被交换了,所以我们需要尽可能保持树的结构去找到那两个点。其实我们并没有必要真正找到那两个点在内存中的地址,由题目要求可知,我们只需找到错误交换的那两个点,然后交换它们的value值即可。怎么找呢?我们需要遍历一遍这二叉树,然后通过遍历得到的顺序,找到我们需要的两个点,按顺序分别记为first、second。
可以证实,对于二叉搜索树而言,其中序遍历的结果为一个升序的序列。因此就可以通过观察当前数中序序列,找出被交换的两个点。
例如:
2
/ \
3
1
很明显其中序遍历结果为[3, 2, 1]。从升序的角度我们就可以确定first节点就是值为3的那个节点,second节点就是值为1的那个节点。
一般地,如果遍历中当前节点cur.val < 上一个节点pre.val,我们的first节点就是这个pre节点。而在first节点确定后,我们的second节点就是cur节点。用代码描述如下:
if not first and cur.val < pre.val:
first = pre
if first and cur.val < pre.val:
second = cur
这个问题解决了,剩下的就是将上面的逻辑整合到二叉搜索树的中序遍历中。
这里列举学习一下中序遍历的三种方法,递归版本、迭代版本和Morris版本。
很明显的就是递归版本和迭代版本,但会有O(h)(h为树的高度)空间复杂度,而进阶的O(1)的空间复杂度要求则需要使用Morris中序遍历了。
解法一
递归版本
def recoverTree(root):
# 初始化
# 递归过程的全局变量通过类属性保存
self.first = None
self.second = None
self.pre = TreeNode(float('-inf'))
def inorder_recursion(root):
# 中序模板
if not root:
return
inorder_recursion(root.left)
# root为遍历到的当前节点
# 比较操作
if not self.first and root.val < self.pre.val:
self.first = self.pre
if self.first and root.val < self.pre.val:
self.second = root
# 记录上一个节点
# 用于递归返回时比较
self.pre = root
inorder_recursion(root.right)
# 中序遍历并取得first和second
inorder_recursion(root)
# 交换即可
if self.first and self.second:
self.first.val, self.second.val = self.second.val, self.first.val
解法二
迭代版本
def recoverTree(root):
# 初始化
first = None
second = None
pre = TreeNode(float('-inf'))
stack = []
cur = root
while cur or stack:
# 迭代版本模板
while cur:
stack.append(cur)
cur = cur.left
# 获取当前节点
cur = stack.pop()
# 比较操作
if not first and cur.val < pre.val:
first = pre
if first and cur.val < pre.val:
second = cur
# 记录上一个节点
pre = cur
# 考查右儿子
cur = cur.right
# 交换即可
if first and second:
first.val, second.val = second.val, first.val
解法三
Morris版本
我们先理清一下Morris中序遍历的过程:
- 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
- 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点(此处要避免与当前节点相等造成死循环):
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
重复以上直到当前节点为空。
自己刚开始看的时候也是很头大,但不要害怕。其实画画图,一步一步尝试就能大概理清了。
Morris中序遍历通过寻找前驱节点,达到一种类似于消除左子树并右展开为链表的感觉。感兴趣可以参考这道LeetCode题:Click me
def recoverTree(root):
# 初始化
first = None
second = None
preNode = TreeNode(float('-inf'))
cur = root
while cur:
# Morris遍历的模板
if cur.left:
# 如果左儿子不为空
pre = cur.left
# 找到左子树最右的节点
while pre.right and pre.right != cur:
pre = pre.right
# 如果此节点无右儿子
if not pre.right:
pre.right = cur
cur = cur.left
continue
# 如果有右儿子
# 记得置空右儿子
# 恢复树的结构
pre.right = None
# 比较操作
# cur为当前节点
if not first and cur.val < preNode.val:
first = preNode
if first and cur.val < preNode.val:
second = cur
# 记录上一个节点
preNode = cur
cur = cur.right
# 交换即可
if first and second:
first.val, second.val = second.val, first.val
其实中序遍历有很多实际的应用,我暂且能想到的就是以前看过利用二叉搜索树中序求解并写出一个四则运算表达式(24 Point Game)。后续如有更新或者补充会加上,记录自己的学习历程。over!!!