1. 题目背景与需求分析
今天我们来拆解LeetCode第1305题"两棵二叉搜索树中的所有元素"。这道题看似简单,但其中蕴含着几个值得深入探讨的二叉树操作技巧。题目要求我们将两棵二叉搜索树(BST)中的所有元素提取出来,并按升序排列返回。
二叉搜索树的性质大家应该不陌生:对于任意节点,其左子树所有节点的值都小于该节点的值,右子树所有节点的值都大于该节点的值。这个性质决定了BST的中序遍历结果必然是一个有序序列。题目给出的两个BST可能结构完全不同,但我们需要将它们的所有元素合并成一个有序列表。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法思路
2.1 直观解法:暴力合并后排序
最直接的思路是:
- 遍历两棵BST,收集所有节点值
- 将收集到的所有值合并到一个列表中
- 对这个列表进行排序
- 返回排序后的结果
这种方法的时间复杂度主要取决于排序步骤。假设两棵树共有n个节点,那么时间复杂度为O(n log n),空间复杂度为O(n)。
虽然这种方法能通过测试用例,但显然没有充分利用BST本身的有序性质,效率上还有提升空间。
2.2 优化思路:利用BST的中序特性
更聪明的做法是利用BST的中序遍历特性:
- 分别对两棵BST进行中序遍历,得到两个有序列表
- 合并这两个有序列表(类似归并排序中的合并步骤)
这种方法的时间复杂度为O(n),因为中序遍历和有序列表合并都是线性时间操作,空间复杂度同样为O(n)。
3. 中序遍历+归并的详细实现
3.1 中序遍历的实现
中序遍历的递归实现最为直观:
python复制def inorder_traversal(root, result):
if not root:
return
inorder_traversal(root.left, result)
result.append(root.val)
inorder_traversal(root.right, result)
对于每棵树,我们初始化一个空列表,然后进行中序遍历:
python复制list1 = []
inorder_traversal(root1, list1)
list2 = []
inorder_traversal(root2, list2)
3.2 有序列表的合并
有了两个有序列表后,我们可以使用双指针法进行合并:
python复制def merge_two_sorted_lists(list1, list2):
merged = []
i = j = 0
while i < len(list1) and j < len(list2):
if list1[i] < list2[j]:
merged.append(list1[i])
i += 1
else:
merged.append(list2[j])
j += 1
# 添加剩余元素
merged.extend(list1[i:])
merged.extend(list2[j:])
return merged
3.3 完整解法代码
将上述两部分组合起来:
python复制class Solution:
def getAllElements(self, root1: TreeNode, root2: TreeNode) -> List[int]:
def inorder(root, result):
if not root:
return
inorder(root.left, result)
result.append(root.val)
inorder(root.right, result)
list1 = []
inorder(root1, list1)
list2 = []
inorder(root2, list2)
return merge_two_sorted_lists(list1, list2)
def merge_two_sorted_lists(list1, list2):
merged = []
i = j = 0
while i < len(list1) and j < len(list2):
if list1[i] < list2[j]:
merged.append(list1[i])
i += 1
else:
merged.append(list2[j])
j += 1
merged.extend(list1[i:])
merged.extend(list2[j:])
return merged
4. 迭代法中序遍历的实现
递归解法虽然简洁,但在处理极大树时可能会遇到栈溢出问题。我们可以使用迭代法实现中序遍历:
4.1 迭代法中序遍历代码
python复制def inorder_iterative(root):
result = []
stack = []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
4.2 使用迭代法的完整解法
python复制class Solution:
def getAllElements(self, root1: TreeNode, root2: TreeNode) -> List[int]:
def inorder_iterative(root):
result = []
stack = []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
list1 = inorder_iterative(root1)
list2 = inorder_iterative(root2)
return self.merge_two_sorted_lists(list1, list2)
def merge_two_sorted_lists(self, list1, list2):
# 合并逻辑同上
...
5. 进一步优化:边遍历边合并
前面的解法需要先完整遍历两棵树,存储所有节点值,然后再合并。我们可以进一步优化,实现边遍历边合并,这样在某些情况下可以提前终止遍历,节省时间和空间。
5.1 使用生成器实现惰性遍历
python复制def inorder_generator(root):
stack = []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
yield current.val
current = current.right
5.2 生成器版本的合并
python复制class Solution:
def getAllElements(self, root1: TreeNode, root2: TreeNode) -> List[int]:
gen1 = inorder_generator(root1)
gen2 = inorder_generator(root2)
val1 = next(gen1, None)
val2 = next(gen2, None)
result = []
while val1 is not None and val2 is not None:
if val1 < val2:
result.append(val1)
val1 = next(gen1, None)
else:
result.append(val2)
val2 = next(gen2, None)
# 处理剩余元素
while val1 is not None:
result.append(val1)
val1 = next(gen1, None)
while val2 is not None:
result.append(val2)
val2 = next(gen2, None)
return result
这种方法的空间复杂度可以降低到O(h1 + h2),其中h1和h2分别是两棵树的高度,因为只需要存储当前的遍历路径。
6. 复杂度分析与对比
让我们对比一下几种解法的复杂度:
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 暴力法 | O(n log n) | O(n) | 实现简单,但效率低 |
| 递归中序+合并 | O(n) | O(n) | 利用了BST性质 |
| 迭代中序+合并 | O(n) | O(n) | 避免递归栈溢出 |
| 生成器边遍历边合并 | O(n) | O(h) | 最优空间复杂度 |
在实际面试中,推荐使用迭代中序+合并的方法,因为它既高效又避免了递归可能带来的问题。
7. 测试用例与边界条件
编写测试用例时,需要考虑以下边界情况:
- 一棵树为空,另一棵树非空
- 两棵树都为空
- 两棵树有重复元素
- 两棵树的元素完全不相交(所有元素都小于或大于另一棵树的元素)
- 大树与小树的组合(测试性能)
示例测试用例:
python复制def test_getAllElements():
# 测试用例1:普通情况
root1 = TreeNode(2, TreeNode(1), TreeNode(4))
root2 = TreeNode(1, TreeNode(0), TreeNode(3))
assert Solution().getAllElements(root1, root2) == [0,1,1,2,3,4]
# 测试用例2:一棵树为空
root1 = TreeNode(1, None, TreeNode(2))
root2 = None
assert Solution().getAllElements(root1, root2) == [1,2]
# 测试用例3:两棵树都为空
assert Solution().getAllElements(None, None) == []
# 测试用例4:有重复元素
root1 = TreeNode(1, TreeNode(1), TreeNode(1))
root2 = TreeNode(1, TreeNode(1), TreeNode(1))
assert Solution().getAllElements(root1, root2) == [1,1,1,1,1,1]
8. 常见错误与调试技巧
在实现这道题时,容易犯的几个错误:
-
忘记处理空树情况:没有检查root是否为None就直接开始遍历,导致NullPointerException。
-
中序遍历实现错误:特别是迭代法中,容易混淆压栈和弹出的顺序。记住原则:一直向左走到底,然后回溯处理节点,再转向右子树。
-
合并逻辑错误:在合并两个有序列表时,容易漏掉某个列表的剩余元素。确保在其中一个列表遍历完后,将另一个列表的剩余元素全部加入结果。
调试技巧:
- 对于小树,可以手动模拟中序遍历过程,验证每一步的结果
- 打印中间结果,比如单独打印每棵树的中序遍历结果,确保这部分正确
- 对于合并逻辑,可以用简单的两个小列表手动模拟合并过程
9. 扩展思考:处理大量数据的场景
如果题目中的BST非常大,无法一次性装入内存,我们可以如何调整解法?
这种情况下,生成器版本的解法就显示出优势了。我们可以:
- 将BST存储在磁盘上,按需读取节点
- 使用外部归并排序的思想,分批读取和处理数据
- 考虑使用B+树等适合外部存储的树结构替代BST
这种场景下,空间复杂度优化就显得尤为重要,生成器版本的O(h)空间复杂度在这种情况下会是更好的选择。
10. 实际应用场景
这道题的解法在实际中有哪些应用场景呢?
- 数据库索引合并:当我们需要合并两个有序索引时,类似的算法会被使用
- 日志合并:合并两个按时间排序的日志文件
- 大数据处理:在MapReduce等大数据处理框架中,归并排序是核心操作之一
- 版本控制系统:合并两个有序的变更历史记录
理解这种基本的合并算法,对于处理各种有序数据的合并场景都有重要意义。
