Appearance
树与二叉树
树是数据结构中第一个真正「非线性」的结构。它用层次化的方式组织数据,在文件系统、数据库索引、编译器语法树、路由表等场景无处不在。二叉树是树中最简单也最重要的形式,它的遍历、搜索、平衡操作是面试的高频考点。本篇将从树的基本概念讲起,覆盖二叉树的四种遍历(前中后序 + 层序)、Morris 遍历的 O(1) 空间技巧、二叉搜索树的增删查、AVL 和红黑树概念,以及 Go 标准库 container/heap 的用法,最后用 LeetCode 真题收尾。理解树,是打开后续堆、图、字典树等结构大门的钥匙。
一、树的基本概念
树是一种由节点组成的层次结构,满足:
- 有且仅有一个根节点(root),没有父节点。
- 其余节点有且仅有一个父节点。
- 节点之间通过边连接,无环。
常见术语:
- 节点(Node):树的基本单元。
- 根(Root):顶层的节点。
- 叶节点(Leaf):没有子节点的节点。
- 深度(Depth):从根到该节点的边数(根深度为 0)。
- 高度(Height):从该节点到最深叶节点的边数(叶节点高度为 0)。
- 度(Degree):节点的子节点个数。
- 层(Level):根为第 1 层,往下递增。
二叉树是每个节点最多有两个子节点(左、右)的树。满二叉树:每个节点要么是叶,要么有两个子。完全二叉树:除最后一层外全满,最后一层从左到右连续。
二、二叉树
1. 实现:节点结构体
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// 辅助:按层序构建完全二叉树(用 -1 表示空节点,简化测试)
func buildTree(vals []int) *TreeNode {
if len(vals) == 0 || vals[0] == -1 {
return nil
}
root := &TreeNode{Val: vals[0]}
queue := []*TreeNode{root}
i := 1
for len(queue) > 0 && i < len(vals) {
node := queue[0]
queue = queue[1:]
if i < len(vals) && vals[i] != -1 {
node.Left = &TreeNode{Val: vals[i]}
queue = append(queue, node.Left)
}
i++
if i < len(vals) && vals[i] != -1 {
node.Right = &TreeNode{Val: vals[i]}
queue = append(queue, node.Right)
}
i++
}
return root
}
func main() {
root := buildTree([]int{1, 2, 3, 4, 5, -1, 6})
fmt.Println("根节点:", root.Val)
fmt.Println("左子:", root.Left.Val)
fmt.Println("右子:", root.Right.Val)
}2. 遍历:前序、中序、后序(递归 + 迭代)
前序(根左右)、中序(左根右)、后序(左右根)是三种深度优先遍历。中序遍历二叉搜索树会得到有序序列。
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// 递归前序
func preorderRecursive(root *TreeNode) []int {
var result []int
var dfs func(*TreeNode)
dfs = func(node *TreeNode) {
if node == nil {
return
}
result = append(result, node.Val)
dfs(node.Left)
dfs(node.Right)
}
dfs(root)
return result
}
// 递归中序
func inorderRecursive(root *TreeNode) []int {
var result []int
var dfs func(*TreeNode)
dfs = func(node *TreeNode) {
if node == nil {
return
}
dfs(node.Left)
result = append(result, node.Val)
dfs(node.Right)
}
dfs(root)
return result
}
// 递归后序
func postorderRecursive(root *TreeNode) []int {
var result []int
var dfs func(*TreeNode)
dfs = func(node *TreeNode) {
if node == nil {
return
}
dfs(node.Left)
dfs(node.Right)
result = append(result, node.Val)
}
dfs(root)
return result
}
// 迭代前序:用栈
func preorderIterative(root *TreeNode) []int {
var result []int
if root == nil {
return result
}
stack := []*TreeNode{root}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, node.Val)
// 先压右后压左,保证左先出
if node.Right != nil {
stack = append(stack, node.Right)
}
if node.Left != nil {
stack = append(stack, node.Left)
}
}
return result
}
// 迭代中序:用栈
func inorderIterative(root *TreeNode) []int {
var result []int
var stack []*TreeNode
cur := root
for cur != nil || len(stack) > 0 {
// 一路向左压栈
for cur != nil {
stack = append(stack, cur)
cur = cur.Left
}
cur = stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, cur.Val)
cur = cur.Right
}
return result
}
// 迭代后序:前序「根左右」改成「根右左」再反转
func postorderIterative(root *TreeNode) []int {
var result []int
if root == nil {
return result
}
stack := []*TreeNode{root}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, node.Val)
if node.Left != nil {
stack = append(stack, node.Left)
}
if node.Right != nil {
stack = append(stack, node.Right)
}
}
// 反转得到「左右根」
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result
}
func buildTree(vals []int) *TreeNode {
if len(vals) == 0 || vals[0] == -1 {
return nil
}
root := &TreeNode{Val: vals[0]}
queue := []*TreeNode{root}
i := 1
for len(queue) > 0 && i < len(vals) {
node := queue[0]
queue = queue[1:]
if i < len(vals) && vals[i] != -1 {
node.Left = &TreeNode{Val: vals[i]}
queue = append(queue, node.Left)
}
i++
if i < len(vals) && vals[i] != -1 {
node.Right = &TreeNode{Val: vals[i]}
queue = append(queue, node.Right)
}
i++
}
return root
}
func main() {
root := buildTree([]int{1, 2, 3, 4, 5, -1, 6})
fmt.Println("前序(递归):", preorderRecursive(root))
fmt.Println("前序(迭代):", preorderIterative(root))
fmt.Println("中序(递归):", inorderRecursive(root))
fmt.Println("中序(迭代):", inorderIterative(root))
fmt.Println("后序(递归):", postorderRecursive(root))
fmt.Println("后序(迭代):", postorderIterative(root))
}3. 层序遍历(BFS)
层序遍历用队列实现,前面栈与队列篇已演示过。这里给出返回每一层的版本:
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func levelOrder(root *TreeNode) [][]int {
var result [][]int
if root == nil {
return result
}
queue := []*TreeNode{root}
for len(queue) > 0 {
size := len(queue)
level := make([]int, 0, size)
for i := 0; i < size; i++ {
node := queue[0]
queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, level)
}
return result
}
func main() {
root := &TreeNode{Val: 1,
Left: &TreeNode{Val: 2, Left: &TreeNode{Val: 4}, Right: &TreeNode{Val: 5}},
Right: &TreeNode{Val: 3, Right: &TreeNode{Val: 6}}}
fmt.Println("层序:", levelOrder(root))
}4. Morris 遍历(O(1) 空间)
Morris 遍历利用叶节点的空指针临时指向后继,实现 O(1) 空间的中序遍历。核心:对每个节点,找到其左子树的最右节点,把它的右指针指向当前节点,遍历完左子树后能回到当前节点。
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func morrisInorder(root *TreeNode) []int {
var result []int
cur := root
for cur != nil {
if cur.Left == nil {
result = append(result, cur.Val)
cur = cur.Right
} else {
// 找前驱节点(左子树最右)
prev := cur.Left
for prev.Right != nil && prev.Right != cur {
prev = prev.Right
}
if prev.Right == nil {
// 建立临时链接
prev.Right = cur
cur = cur.Left
} else {
// 已访问过,恢复树结构
prev.Right = nil
result = append(result, cur.Val)
cur = cur.Right
}
}
}
return result
}
func main() {
root := &TreeNode{Val: 4,
Left: &TreeNode{Val: 2, Left: &TreeNode{Val: 1}, Right: &TreeNode{Val: 3}},
Right: &TreeNode{Val: 6, Left: &TreeNode{Val: 5}}}
fmt.Println("Morris 中序:", morrisInorder(root)) // [1 2 3 4 5 6]
}Morris 遍历的时间复杂度仍是 O(n),但空间复杂度从 O(h) 降到 O(1)。代价是会临时修改树结构(虽然最终恢复),且代码相对复杂。
三、二叉搜索树(BST)
BST 满足:左子树所有节点值 < 根 < 右子树所有节点值。它的中序遍历是有序的,查找/插入/删除平均 O(log n)。
1. 插入、删除、查找
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
type BST struct {
root *TreeNode
}
// 插入
func (b *BST) Insert(val int) {
b.root = insert(b.root, val)
}
func insert(node *TreeNode, val int) *TreeNode {
if node == nil {
return &TreeNode{Val: val}
}
if val < node.Val {
node.Left = insert(node.Left, val)
} else if val > node.Val {
node.Right = insert(node.Right, val)
}
return node
}
// 查找
func (b *BST) Search(val int) bool {
return search(b.root, val)
}
func search(node *TreeNode, val int) bool {
if node == nil {
return false
}
if val == node.Val {
return true
}
if val < node.Val {
return search(node.Left, val)
}
return search(node.Right, val)
}
// 删除:分三种情况
func (b *BST) Delete(val int) {
b.root = deleteNode(b.root, val)
}
func deleteNode(node *TreeNode, val int) *TreeNode {
if node == nil {
return nil
}
if val < node.Val {
node.Left = deleteNode(node.Left, val)
} else if val > node.Val {
node.Right = deleteNode(node.Right, val)
} else {
// 找到目标
if node.Left == nil {
return node.Right
}
if node.Right == nil {
return node.Left
}
// 两个孩子:用右子树最小值替换
minNode := node.Right
for minNode.Left != nil {
minNode = minNode.Left
}
node.Val = minNode.Val
node.Right = deleteNode(node.Right, minNode.Val)
}
return node
}
// 中序遍历验证有序性
func (b *BST) Inorder() []int {
var result []int
var dfs func(*TreeNode)
dfs = func(n *TreeNode) {
if n == nil {
return
}
dfs(n.Left)
result = append(result, n.Val)
dfs(n.Right)
}
dfs(b.root)
return result
}
func main() {
bst := &BST{}
values := []int{5, 3, 7, 1, 4, 6, 8}
for _, v := range values {
bst.Insert(v)
}
fmt.Println("中序(有序):", bst.Inorder())
fmt.Println("查找 4:", bst.Search(4))
fmt.Println("查找 9:", bst.Search(9))
bst.Delete(3)
fmt.Println("删除 3 后:", bst.Inorder())
}2. 验证 BST
判断一棵树是否是合法 BST。错误做法:只比较 node 与左右孩子;正确做法:传递上下界。
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func isValidBST(root *TreeNode) bool {
return validate(root, nil, nil)
}
// min、max 用指针以便区分「未设置」和「值为 0」
func validate(node *TreeNode, min, max *int) bool {
if node == nil {
return true
}
if min != nil && node.Val <= *min {
return false
}
if max != nil && node.Val >= *max {
return false
}
return validate(node.Left, min, &node.Val) && validate(node.Right, &node.Val, max)
}
func main() {
root := &TreeNode{Val: 5,
Left: &TreeNode{Val: 1},
Right: &TreeNode{Val: 7, Left: &TreeNode{Val: 6}, Right: &TreeNode{Val: 8}}}
fmt.Println("是合法 BST?", isValidBST(root)) // true
}四、平衡二叉树
普通 BST 在数据有序插入时会退化成链表,操作变 O(n)。平衡树通过旋转保持高度在 O(log n)。
1. AVL 树
AVL 树要求每个节点的左右子树高度差不超过 1。插入删除后通过四种旋转(LL、RR、LR、RL)恢复平衡。这里给出 AVL 节点和插入的核心代码:
go
package main
import "fmt"
type AVLNode struct {
Val int
Left *AVLNode
Right *AVLNode
height int
}
func height(n *AVLNode) int {
if n == nil {
return 0
}
return n.height
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func updateHeight(n *AVLNode) {
n.height = 1 + max(height(n.Left), height(n.Right))
}
func balanceFactor(n *AVLNode) int {
return height(n.Left) - height(n.Right)
}
// 右旋
func rotateRight(y *AVLNode) *AVLNode {
x := y.Left
t := x.Right
x.Right = y
y.Left = t
updateHeight(y)
updateHeight(x)
return x
}
// 左旋
func rotateLeft(x *AVLNode) *AVLNode {
y := x.Right
t := y.Left
y.Left = x
x.Right = t
updateHeight(x)
updateHeight(y)
return y
}
func insert(n *AVLNode, val int) *AVLNode {
if n == nil {
return &AVLNode{Val: val, height: 1}
}
if val < n.Val {
n.Left = insert(n.Left, val)
} else if val > n.Val {
n.Right = insert(n.Right, val)
} else {
return n
}
updateHeight(n)
bf := balanceFactor(n)
// LL
if bf > 1 && val < n.Left.Val {
return rotateRight(n)
}
// RR
if bf < -1 && val > n.Right.Val {
return rotateLeft(n)
}
// LR
if bf > 1 && val > n.Left.Val {
n.Left = rotateLeft(n.Left)
return rotateRight(n)
}
// RL
if bf < -1 && val < n.Right.Val {
n.Right = rotateRight(n.Right)
return rotateLeft(n)
}
return n
}
func inorder(n *AVLNode) []int {
var result []int
if n == nil {
return result
}
result = append(result, inorder(n.Left)...)
result = append(result, n.Val)
result = append(result, inorder(n.Right)...)
return result
}
func main() {
var root *AVLNode
// 顺序插入 1..7,普通 BST 会退化成链表,AVL 保持平衡
for _, v := range []int{1, 2, 3, 4, 5, 6, 7} {
root = insert(root, v)
}
fmt.Println("AVL 中序:", inorder(root))
fmt.Println("根节点:", root.Val, "高度:", root.height)
}2. 红黑树概念
红黑树是另一种广泛使用的平衡树(C++ 的 std::map、Java 的 TreeMap、Go 的 syscall 等)。它的平衡条件比 AVL 宽松:不要求严格平衡,只要求最长路径不超过最短路径的两倍。
红黑树五条性质:
- 每个节点是红色或黑色。
- 根是黑色。
- 叶节点(NIL)是黑色。
- 红节点的子节点必须是黑色(不能有连续红节点)。
- 任一节点到其叶节点的所有路径包含相同数量的黑节点。
红黑树插入删除通过变色 + 旋转维护平衡,旋转次数比 AVL 少,适合写多读少的场景。Go 标准库 container/list 没用红黑树,但 runtime 内部、golang.org/x/exp/slices 排序等地方有相关思想。
五、Go 标准库:container/heap
container/heap 实现了最小堆,提供 heap.Interface 接口。基于它可以构建优先队列,下一篇会专门讲堆。这里先看一个最小堆的简单用法:
go
package main
import (
"container/heap"
"fmt"
)
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func main() {
h := &IntHeap{5, 3, 8, 1, 9}
heap.Init(h)
heap.Push(h, 0)
fmt.Println("堆顶:", (*h)[0])
for h.Len() > 0 {
fmt.Printf("%d ", heap.Pop(h))
}
fmt.Println()
}六、LeetCode 经典题解析
1. 二叉树的最大深度(LC 104)
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
left := maxDepth(root.Left)
right := maxDepth(root.Right)
if left > right {
return left + 1
}
return right + 1
}
func main() {
root := &TreeNode{Val: 3,
Left: &TreeNode{Val: 9},
Right: &TreeNode{Val: 20, Left: &TreeNode{Val: 15}, Right: &TreeNode{Val: 7}}}
fmt.Println("最大深度:", maxDepth(root)) // 3
}2. 对称二叉树(LC 101)
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func isSymmetric(root *TreeNode) bool {
if root == nil {
return true
}
return isMirror(root.Left, root.Right)
}
func isMirror(a, b *TreeNode) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Val == b.Val && isMirror(a.Left, b.Right) && isMirror(a.Right, b.Left)
}
func main() {
root := &TreeNode{Val: 1,
Left: &TreeNode{Val: 2, Left: &TreeNode{Val: 3}, Right: &TreeNode{Val: 4}},
Right: &TreeNode{Val: 2, Left: &TreeNode{Val: 4}, Right: &TreeNode{Val: 3}}}
fmt.Println("对称?", isSymmetric(root)) // true
}3. 翻转二叉树(LC 226)
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func invertTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
root.Left, root.Right = root.Right, root.Left
invertTree(root.Left)
invertTree(root.Right)
return root
}
func preorder(root *TreeNode) []int {
if root == nil {
return nil
}
return append(append([]int{root.Val}, preorder(root.Left)...), preorder(root.Right)...)
}
func main() {
root := &TreeNode{Val: 4,
Left: &TreeNode{Val: 2, Left: &TreeNode{Val: 1}, Right: &TreeNode{Val: 3}},
Right: &TreeNode{Val: 7, Left: &TreeNode{Val: 6}, Right: &TreeNode{Val: 9}}}
fmt.Println("翻转前:", preorder(root))
invertTree(root)
fmt.Println("翻转后:", preorder(root))
}4. 二叉搜索树验证(LC 98)
前面已实现 isValidBST,注意要用上下界而非只比较父子。
5. 二叉搜索树第 K 小(LC 230)
利用中序遍历有序的特性,中序遍历到第 k 个即为答案。
go
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func kthSmallest(root *TreeNode, k int) int {
var result int
var dfs func(*TreeNode) bool
dfs = func(node *TreeNode) bool {
if node == nil {
return false
}
if dfs(node.Left) {
return true
}
k--
if k == 0 {
result = node.Val
return true
}
return dfs(node.Right)
}
dfs(root)
return result
}
func main() {
root := &TreeNode{Val: 5,
Left: &TreeNode{Val: 3, Left: &TreeNode{Val: 2, Left: &TreeNode{Val: 1}}, Right: &TreeNode{Val: 4}},
Right: &TreeNode{Val: 6}}
fmt.Println("第 3 小:", kthSmallest(root, 3)) // 3
}七、复杂度分析
| 操作 | 普通 BST(平均/最坏) | AVL/红黑树 | 说明 |
|---|---|---|---|
| 查找 | O(log n) / O(n) | O(log n) | 最坏退化为链表 |
| 插入 | O(log n) / O(n) | O(log n) | |
| 删除 | O(log n) / O(n) | O(log n) | |
| 遍历 | O(n) | O(n) | |
| 空间 | O(n) | O(n) | |
| Morris 遍历空间 | O(1) | O(1) | 临时改树 |
八、小结
树是层次化数据的自然表示,本篇要点:
- 基本概念:根、叶、深度(从根算)、高度(从叶算)、度。二叉树每个节点最多两个孩子。
- 遍历:前序(根左右)、中序(左根右)、后序(左右根)有递归和迭代两种写法;层序用队列。中序遍历 BST 得到有序序列。
- Morris 遍历:利用叶节点空指针临时指向后继,实现 O(1) 空间中序遍历,代价是临时修改树结构。
- BST:左 < 根 < 右,查找/插入/删除平均 O(log n),但可能退化。删除节点分无子、一子、双子三种情况,双子用右子树最小值替换。
- 平衡树:AVL 严格平衡(高度差 ≤ 1),读多写少;红黑树宽松平衡,写多读少。Go 标准库
container/heap用于堆场景。 - 验证 BST 必须用上下界,不能只比父子;第 K 小 用中序遍历到第 k 个即停。
树的学习难点在于递归思维的训练。建议每道题都先想清楚「当前节点要做什么、左右子树递归返回什么」,这就是「递归函数的定义」。
下一篇我们将聚焦堆——一种用数组实现的特殊完全二叉树,它是优先队列的标准实现,也是 Top K 问题、堆排序的核心。