

文章目录
-
- 摘要
- 描述
- 题解答案(核心思路)
-
- [为什么普通二叉树和 BST 不一样?](#为什么普通二叉树和 BST 不一样?)
- [BST 的关键点](#BST 的关键点)
- 本题采用的策略
- [题解代码(Swift 可运行 Demo)](#题解代码(Swift 可运行 Demo))
- 题解代码分析
-
- [1. 为什么用前序遍历?](#1. 为什么用前序遍历?)
- [2. serialize 的核心逻辑](#2. serialize 的核心逻辑)
- [3. deserialize 的核心逻辑](#3. deserialize 的核心逻辑)
- [4. 为什么不需要回退 index?](#4. 为什么不需要回退 index?)
- 示例测试及结果
- 时间复杂度
- 空间复杂度
- 总结
摘要
「序列化和反序列化二叉搜索树」这道题,表面上是在考二叉树,其实真正的考点在于:你有没有真正理解 BST(Binary Search Tree)的性质。
如果把它当成普通二叉树去做,确实可以用层序遍历,加上 null 占位来解决,但那样字符串会很长,也完全没有利用 BST 的特性。而题目特意强调了一句:
编码的字符串应尽可能紧凑。
这其实是在暗示:
BST 是可以只靠遍历顺序恢复出来的。
这篇文章会从实际业务场景聊起,解释为什么 BST 可以被"无损压缩",再一步步带你用 Swift 写出一套简洁、高效、可运行的解法。

描述
题目要求我们设计一套算法,用来:
- 把一棵 二叉搜索树 序列化成字符串
- 再从这个字符串中反序列化,恢复成原来的 BST
有几个关键限制条件:
- 输入保证一定是一棵 BST
- 节点值范围是合法整数
- 节点数量最多 10⁴
- 序列化字符串要尽量紧凑
BST 有一个非常重要的性质:
- 左子树所有节点值 < 根节点
- 右子树所有节点值 > 根节点
这条规则,是我们能"少存信息、还能完整还原"的关键。
题解答案(核心思路)
为什么普通二叉树和 BST 不一样?
如果是普通二叉树,你只存前序遍历是没法还原结构的,比如:
txt
1
\
2
和
txt
1
/
2
前序遍历都是 [1,2],结构信息丢失了。
但 BST 不一样。
BST 的关键点
只要你知道:
- 遍历顺序(比如前序)
- BST 的大小关系规则
那么 不用存 null,也能恢复结构。
本题采用的策略
-
序列化
- 使用前序遍历(root → left → right)
- 只记录节点值
- 用逗号拼成字符串
-
反序列化
- 根据 BST 的区间规则递归构建
- 每个节点值都必须落在合法区间
(min, max)内 - 一次遍历即可还原整棵树
这样做的好处是:
- 字符串非常紧凑
- 时间和空间效率都很高
- 思路清晰,代码不复杂

题解代码(Swift 可运行 Demo)
下面是完整可运行的 Swift 示例,包括 TreeNode 定义和测试代码。
swift
import Foundation
public class TreeNode {
public var val: Int
public var left: TreeNode?
public var right: TreeNode?
public init(_ val: Int) {
self.val = val
self.left = nil
self.right = nil
}
}
class Codec {
// MARK: - Serialize
func serialize(_ root: TreeNode?) -> String {
var result: [String] = []
func preorder(_ node: TreeNode?) {
guard let node = node else { return }
result.append(String(node.val))
preorder(node.left)
preorder(node.right)
}
preorder(root)
return result.joined(separator: ",")
}
// MARK: - Deserialize
func deserialize(_ data: String) -> TreeNode? {
if data.isEmpty { return nil }
var values = data.split(separator: ",").map { Int($0)! }
var index = 0
func build(_ min: Int, _ max: Int) -> TreeNode? {
if index >= values.count {
return nil
}
let val = values[index]
if val < min || val > max {
return nil
}
index += 1
let node = TreeNode(val)
node.left = build(min, val)
node.right = build(val, max)
return node
}
return build(Int.min, Int.max)
}
}
题解代码分析
1. 为什么用前序遍历?
前序遍历的第一个节点一定是根节点,这对构建树非常友好。
BST 的前序遍历有一个隐含规律:
- 一段连续小于 root 的值,一定属于左子树
- 后面大于 root 的值,一定属于右子树
结合区间限制,我们就能精准判断一个值"该不该被消费"。
2. serialize 的核心逻辑
swift
func preorder(_ node: TreeNode?) {
guard let node = node else { return }
result.append(String(node.val))
preorder(node.left)
preorder(node.right)
}
这里非常直接:
- 不存 null
- 不加多余符号
- 单纯记录节点值
这也是字符串"足够紧凑"的关键原因。
3. deserialize 的核心逻辑
swift
func build(_ min: Int, _ max: Int) -> TreeNode?
这是整个算法的精髓。
含义是:
- 当前子树中,合法节点值必须在
(min, max)区间内 - 如果当前值不在区间内,说明它属于别的子树,直接返回 nil
- 如果合法,就创建节点,并递归构建左右子树
因为 index 是全局递增的,所以每个节点只会被处理一次。
4. 为什么不需要回退 index?
这是很多人一开始想不通的点。
原因在于:
- 前序遍历是严格的「根 → 左 → 右」
- 一旦某个值不在当前区间,它一定属于后续的右子树或祖先节点
- 所以 不能消费,就不动 index
这是 BST 性质带来的好处。
示例测试及结果
我们用示例一来跑一遍:
swift
let codec = Codec()
let root = TreeNode(2)
root.left = TreeNode(1)
root.right = TreeNode(3)
let data = codec.serialize(root)
print("序列化结果:", data)
let newRoot = codec.deserialize(data)
print("反序列化完成,根节点:", newRoot?.val ?? -1)
输出结果:
txt
序列化结果: 2,1,3
反序列化完成,根节点: 2
对于空树:
swift
print(codec.serialize(nil)) // ""
print(codec.deserialize("") == nil) // true
结果也是符合预期的。
时间复杂度
- 序列化:每个节点访问一次,O(n)
- 反序列化:每个节点构建一次,O(n)
总体时间复杂度:O(n)
空间复杂度
- 序列化字符串占 O(n)
- 递归调用栈最坏 O(n)(退化成链表)
总体空间复杂度:O(n)
总结
LeetCode 449 是一道非常经典的「理解型」题目。
它并不难写,但非常考察你是否真正理解了:
- BST 的结构特性
- 遍历顺序和树结构之间的关系
- 如何用"约束条件"减少冗余数据