前几天在网上学习了 AVL 树的概念和实现方法,但光在网上看文章来学习,多少有点片面,很多细节网上的文章解释的不是很清楚,就比如删除节点该如何操作。所以,翻出了我吃灰多年的《算法导论》,其中讲到了二叉搜索树(BST)和红黑树,虽然没有 AVL,但是举一反三,也能对 AVL 有更深刻的理解。
我分了两篇文章,分别记录 BST 和红黑树的笔记。
什么是 BST#
二叉搜索树 (Binaray Search Tree)
顾名思义,一棵二叉搜索树是以一棵二叉树来组织的,树中的每个节点,除了 Key 和承载的数据外,还有属性 parent
、 left
、 right
,分别指向父节点、左子树和右子树。如果某个子节点或父节点不存在,则相应的属性为 Nil。根节点是唯一一个父节点为 Nil 的节点
二叉搜索树中,对任何一个节点 N,其左子树的 key 最大不超过 N.key,右子树的 key 最小不低于 N.key
在一个高度为 h 的 BST 中,元素的插入、删除、查找都可以在 O(h) 时间内完成
BST API#
《算法导论》中介绍了 BST 的一些常规操作 API,我用 Golang 简单了实现了一遍。
先看看基础骨架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// Node 节点
type Node struct {
key int
parent, left, right *Node
}
// NewNode 创建一个新的节点
func NewNode(key int) *Node {
return &Node{key: key}
}
// NewBST 创建一个 BST
func NewBST() *BST {
return &BST{}
}
// BST 二叉搜索树 binary search tree
type BST struct {
root *Node
}
// Search 查找 BST 中的节点
func (b *BST) Search(key int) *Node { return nil }
// Max BST 中最大的节点
func (b *BST) Max(n *Node) *Node { return nil}
// Min BST 中最小的节点
func (b *BST) Min(n *Node) *Node { return nil }
// Successor 查找一个节点的后继节点
func (b *BST) Successor(n *Node) *Node { return nil }
// Predecessor 查找一个节点的前驱节点
func (b *BST) Predecessor(n *Node) *Node { return nil }
// Insert 插入一个节点到 BST 中
func (b *BST) Insert(n *Node) {}
// Delete 从 BST 中删除一个元素
func (b *BST) Delete(n *Node) {}
// Transplant 替换一个节点,这个方法用以辅助 Delete 函数
func (b *BST) Transplant(n *Node, v *Node) {}
|
TL;DR#
如果不想看下面的长篇大论,可以直接点击这里看源码
我们经常需要查找存储在 BST 中的关键字。除了 Search 操作外,BST 还支持 Minimum(最小节点)
、Maximum(最大节点)
、Successor(后继节点)
、Predecessor(前驱节点)
的查询操作。
这些 API 都可以使用递归来实现,但使用迭代可以得到更好的运行效率,因为不用维护递归的调用栈。
Search(搜索)#
我们从根节点出发,不断的把节点的 key 和传入的 key 相比较,如果两者相等,则这是节点就是要查找的节点。如果节点的 key 比传入的 key 小,则结果应该在当前节点的右边,反之则在左边。直到遍历到树的叶子节点,如果还没有结果则表示没有找到对应着 key 的 Node。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Search 查找 BST 中的节点
func (b *BST) Search(key int) *Node {
node := b.root
for node != nil {
if node.key == key {
break
}
if key > node.key {
node = node.right
} else {
node = node.left
}
}
return node
}
|
Max(最大节点)#
因为 BST 的性质,最大的节点一定是树的最右侧的节点,所以我们只需要给出最右侧的节点即可。
1
2
3
4
5
6
7
8
9
10
11
|
// Max BST 中最大的节点
func (b *BST) Max(n *Node) *Node {
if n == nil {
return nil
}
node := n
for node.right != nil {
node = node.right
}
return node
}
|
Min(最小节点)#
和 Max 方法相反,Min 只需要给出最左侧的节点即可。
1
2
3
4
5
6
7
8
9
10
11
|
// Min BST 中最小的节点
func (b *BST) Min(n *Node) *Node {
if n == nil {
return nil
}
node := n
for node.left != nil {
node = node.left
}
return node
}
|
Successor(后继节点)#
如果树中所有节点的 key 都各不相同,则一个节点 X 的后继节点,是所有大于 X.key 的节点中最小的节点
当查找一个节点的后继时,我们需要使用中序遍历的顺序来查找它的后继。而这又分两种情况:
- 当一个节点的右子树不为空时,后继节点为右子树中的最左节点(右子树中的最小节点)
- 当节点 X 没有右子树,但有一个后继节点 Y,那么后继节点 Y 是左子树是 X 的祖先节点中的第一个节点
文字解释太难以理解了,我们使用中序遍历的顺序来理解下面的一个 BST
1
2
3
4
5
6
7
8
9
|
7
/ \
6 8
/ \
2 9
/ \
1 4
/ \
3 5
|
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// Successor 查找一个节点的后继节点
func (b *BST) Successor(n *Node) *Node {
if n == nil {
return nil
}
if n.right != nil {
return b.Min(n.right)
}
var (
node = n
successor = n.parent
)
for successor != nil && successor.right == node {
node = successor
successor = successor.parent
}
return successor
}
|
Predecessor(前驱节点)#
查找前驱节点的逻辑和上面的后继节点逻辑镜像翻转即可,逻辑是一样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// Predecessor 查找一个节点的前驱节点
func (b *BST) Predecessor(n *Node) *Node {
if n.left != nil {
return b.Max(n.left)
}
var (
node = n
predecessor = n.parent
)
for predecessor != nil && predecessor.left == node {
node = predecessor
predecessor = predecessor.parent
}
return predecessor
}
|
BST 的插入相对简单,因为没有自平衡机制,只需要按照 BST 的规则插入即可。
但是随着元素的不断加入,树的高度也在变化,在极端情况下,如果 n 个元素严格按照递增的顺序被插入(1,2,3…),那么这棵树就变成了一个高度为 n-1(不包含 root 节点) 的链表了,这样的 BST 效率是极差的,所以才会有 AVL、红黑树这样的平衡树。
但这篇文章只讨论 BST,平衡机制先不聊了。
循环片段找到插入的节点 n 的 preant,然后分别修改他们的 parent 指针的 children 指针实现插入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// Insert 插入一个节点到 BST 中
func (b *BST) Insert(n *Node) {
if b.root == nil {
b.root = n
return
}
var (
node = b.root
parent = node.parent
)
for node != nil {
// 已存在的节点,不处理
if node.key == n.key {
return
}
parent = node
if n.key > node.key {
node = node.right
} else {
node = node.left
}
}
if n.key > parent.key {
parent.right = n
} else {
parent.left = n
}
n.parent = parent
}
|
删除节点大概是 BST 中最麻烦的操作了,因为会分好几种情况,我们一个一个的说
1. 没有子节点#
当要删除的节点 N 没有子节点时,这是最简单的操作,只需要斩断 N.parent 和 N 的联系即可
如下面的例子,要删除节点 1 不需要什么操作,直接删除即可
2. 只有左子节点#
当要删除的节点 N 有左子节点,但没有右子节点时,我们让 N.left 取代 N 的位置
1
2
3
4
5
6
7
8
9
|
+---------+-------+
| delete node 2 |
+---------+-------+
| 3 | 3 |
| / | / |
| 2 | 1 |
| / | |
| 1 | |
+---------+-------+
|
3. 只有右子节点#
和第 2 条相反,当节点 N 有右子节点但没有左子节点时,让 N.right 取代 N 的位置
1
2
3
4
5
6
7
8
9
|
+---------+-------+
| delete node 2 |
+---------+-------+
| 1 | 1 |
| \ | \ |
| 2 | 3 |
| \ | |
| 3 | |
+---------+-------+
|
4. 有左右子节点#
当节点 N 同时拥有左右子节点时,情况稍微复杂点。
我们首先找到节点 N 的后继节点 Y,因为 N 有右子节点,后继节点 Y 一定是 N 节点的右子树中的最小节点,并且 Y 没有左子节点。这个时候又分两种情况:
- 如果 Y 不是 N 的右子节点,我们首先要让 Y.right -> Z 替换 Y 的位置,同时将 Y.right 指向 N.right,然后让 Y 替换 N 的位置
1
2
3
4
5
6
7
8
9
10
11
|
+-----------+----------+----------+
| step 1 | step 2 | step 3 |
+-----------+----------+----------+
| 2(N) | 2(N) | 3(Y) |
| / \ | / \ | / \ |
| 1 5 | 1 5 | 1 5 |
| / | / | / |
| 3(Y) | 4(Z) | 4(Z) |
| \ | | |
| 4(Z) | 3(Y) | 2(N) |
+-----------+----------+----------+
|
1
2
3
4
5
6
7
8
9
|
+------------+----------+
| step 1 | step 2 |
+------------+----------+
| 2(N) | 3(Y) |
| / \ | / \ |
| 1 3(Y) | 1 4 |
| \ | |
| 4 | 2(N) |
+------------+----------+
|
理论到此为止,我们来实现代码。上面提到后继节点 Y 替换节点 N,我们需要实现 Transplant
方法
这个方法没有处理 v 的 child node,需要调用方自己处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// Transplant 替换一个节点,这个方法用以辅助 Delete 函数
func (b *BST) Transplant(n *Node, v *Node) {
if n == b.root {
b.root = v
if v != nil {
v.parent = nil
}
return
}
if n.parent.left == n {
n.parent.left = v
} else {
n.parent.right = v
}
if v != nil {
v.parent = n.parent
}
}
|
然后我们利用这个方法来实现 Delete
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Delete 从 BST 中删除一个元素
func (b *BST) Delete(n *Node) {
if n.left == nil {
b.Transplant(n, n.right)
return
}
if n.right == nil {
b.Transplant(n, n.left)
return
}
successor := b.Successor(n)
if successor != n.right {
// 后继节点不是 n 的直属右节点的话,这个后继节点一定没有左节点
// 所以我们不需要处理 successor.left
b.Transplant(successor, successor.right)
successor.right = n.right
successor.right.parent = successor
}
b.Transplant(n, successor)
successor.left = n.left
successor.left.parent = successor
}
|
国际惯例,写完代码要用测试用例来验证。
测试的代码太多了,就不贴上了,可以点击上面提到的源码链接查看。
测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
$ go test -v -cover ./...
=== RUN TestBST_Delete
=== RUN TestBST_Delete/only_left_child_node
=== RUN TestBST_Delete/only_right_child_node
=== RUN TestBST_Delete/no_child_node
=== RUN TestBST_Delete/has_left_and_right_child_nodes
=== RUN TestBST_Delete/has_non-right_successor_node
--- PASS: TestBST_Delete (0.00s)
--- PASS: TestBST_Delete/only_left_child_node (0.00s)
--- PASS: TestBST_Delete/only_right_child_node (0.00s)
--- PASS: TestBST_Delete/no_child_node (0.00s)
--- PASS: TestBST_Delete/has_left_and_right_child_nodes (0.00s)
--- PASS: TestBST_Delete/has_non-right_successor_node (0.00s)
=== RUN TestBST_Insert
=== RUN TestBST_Insert/1
=== RUN TestBST_Insert/2
=== RUN TestBST_Insert/3
=== RUN TestBST_Insert/4
=== RUN TestBST_Insert/5
--- PASS: TestBST_Insert (0.00s)
--- PASS: TestBST_Insert/1 (0.00s)
--- PASS: TestBST_Insert/2 (0.00s)
--- PASS: TestBST_Insert/3 (0.00s)
--- PASS: TestBST_Insert/4 (0.00s)
--- PASS: TestBST_Insert/5 (0.00s)
=== RUN TestBST_Successor
=== RUN TestBST_Successor/has_right_child_node
=== RUN TestBST_Successor/has_multi_right_child_nodes
=== RUN TestBST_Successor/no_right_child_nodes
=== RUN TestBST_Successor/no_right_child_nodes_1
=== RUN TestBST_Successor/no_successor
--- PASS: TestBST_Successor (0.00s)
--- PASS: TestBST_Successor/has_right_child_node (0.00s)
--- PASS: TestBST_Successor/has_multi_right_child_nodes (0.00s)
--- PASS: TestBST_Successor/no_right_child_nodes (0.00s)
--- PASS: TestBST_Successor/no_right_child_nodes_1 (0.00s)
--- PASS: TestBST_Successor/no_successor (0.00s)
=== RUN TestBST_Search
=== RUN TestBST_Search/#00
=== RUN TestBST_Search/#01
--- PASS: TestBST_Search (0.00s)
--- PASS: TestBST_Search/#00 (0.00s)
--- PASS: TestBST_Search/#01 (0.00s)
=== RUN TestBST_Predecessor
=== RUN TestBST_Predecessor/has_left_child
=== RUN TestBST_Predecessor/has_multi_left_child_nodes
=== RUN TestBST_Predecessor/no_left_child_node
=== RUN TestBST_Predecessor/no_predecessor
--- PASS: TestBST_Predecessor (0.00s)
--- PASS: TestBST_Predecessor/has_left_child (0.00s)
--- PASS: TestBST_Predecessor/has_multi_left_child_nodes (0.00s)
--- PASS: TestBST_Predecessor/no_left_child_node (0.00s)
--- PASS: TestBST_Predecessor/no_predecessor (0.00s)
=== RUN TestBST_Max
=== RUN TestBST_Max/#00
=== RUN TestBST_Max/#01
=== RUN TestBST_Max/#02
--- PASS: TestBST_Max (0.00s)
--- PASS: TestBST_Max/#00 (0.00s)
--- PASS: TestBST_Max/#01 (0.00s)
--- PASS: TestBST_Max/#02 (0.00s)
=== RUN TestBST_Min
=== RUN TestBST_Min/#00
=== RUN TestBST_Min/#01
=== RUN TestBST_Min/#02
--- PASS: TestBST_Min (0.00s)
--- PASS: TestBST_Min/#00 (0.00s)
--- PASS: TestBST_Min/#01 (0.00s)
--- PASS: TestBST_Min/#02 (0.00s)
PASS
coverage: 97.4% of statements
ok github.com/ningzio/algorithm/bst 0.367s coverage: 97.4% of statements
|
BST 排序#
前面我们提到过,BST 可以在 O(h) 时间内完成插入一个节点,并且我们可以通过中序遍历输出排序后的结果。
在理想的情况下(平衡二叉树),一棵树的高度为节点数量 n 的对数 log n ,所以 h = log n
我们每次插入一个元素,需要 log n 的时间,那么插入 n 个元素的时间复杂度为 O(n * log n)
所以,在理想的情况下,我们可以使用 BST 实现在 O(n * log n) 时间内完成对一组随机序列的排序,然后使用中序遍历输出所有严肃
但是还有另外一个问题,如何处理重复元素?BST 的设计初衷是为了查找(如其名),一般不允许有重复的 Key,就如上面的写法,遇到重复的 Key 会直接覆盖。但是如果我们用 BST 来做排序的话,肯定是要处理重复元素的。
书中也给出了几种方案:
简单且实用方案#
为节点 X 新增一个 .L([]*Node)
属性,当遇到相同的值 Y 时,将 Y 添加到 X.L
中,当需要输出排序结果时,将 X.L
中的内容一并输出。
花里胡哨解法#
为节点 X 新增一个 .b
属性。当遇到相同值 Y 时:
- 如果
X.b
的属性为 false,将 X.b 的值修改为 true,并让 X 成为 Y 的左子节点
- 如果
X.b
的属性为 true,将 X.b 的值修改为 false,并让 X 成为 Y 的右子节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
+---------------+----------------+----------------+----------------+------------------+----------------+
| insert x | insert x1 | inset x2 | insert x3 | insert y (y > x) | insert x4 |
+---------------+----------------+----------------+----------------+------------------+----------------+
| x | x1 | x1 | x3 | x3 | x3 |
| | / | / | / \ | / \ | / \ |
| | x | x2 | x2 x1 | x2 x1 | x4 x1 |
| | | \ | \ | \ \ | / \ \ |
| | | x | x | x y | x2 x y |
| | | | | | |
| x.b = false | x1.b = false | x1.b = true | x3.b = false | x3.b = false | x3.b = false |
| | x.b = true | x2.b = false | x1.b = true | x1.b = true | x1.b = true |
| | | x.b = true | x2.b = false | x2.b = false | x4.b = false |
| | | | x.b = true | x.b = true | x2.b = true |
| | | | | | x.b = true |
+---------------+----------------+----------------+----------------+------------------+----------------+
|
这样可以保证相同的节点可以均衡的分配到树中,而不会向着一个方向一直倾斜而变成了一个链表。
树的遍历#
树的三种遍历方式,我们来看看分别使用三种遍历方式得到的结果
1
2
3
4
5
6
|
5
/ \
3 7
/ / \
1 6 9
|
前序遍历: [5, 3, 1, 7, 6, 9]
跟节点 -> 左子节点 -> 右子节点
中序遍历: [1, 3, 5, 6, 7, 9]
左子节点 -> 根节点 -> 右子节点
后序遍历: [1, 3, 5, 6, 9, 7]
左子节点 -> 右子节点 -> 根节点