堆是古往今来唯一被泱泱中华的文人骚客们题诗作赋过的数据结构。什么,看官您不信?咱可有诗为证。诗曰:
堆实际上是一棵完全二叉树。为了维持“上头尖来下头粗”的形状和有序性,我们规定老子要在儿子的上面,而且比儿子的键值要小。丫丫个呸,这简直就是大头儿子小头爸爸嘛。这样,它和二叉查找树比较起来有个好处,就是假如你想知道堆中“头”最小的爸爸是谁,压根不用找,最顶上那个就是。毛主席教导我们,看问题要一分为二。凡事有好就有坏,堆所带来的坏处就是除了可以很轻松的查找堆中最小值外,基本上不能做别的了。当然,插入和删除这种基本操作还是可以做的。
堆的插入操作就是暂时认为要插入的值是“头”最大的儿子,把它先放入最下层下一个可用位置上。然后让其与老子比较,看谁的“头”大。持续这个过程,让其一直朝着最顶层的根祖宗方向往上冒,直到找到合适位置。这个过程称为“percolate up”。
而删除操作则反其道而行之。我们知道,找到最小项很容易,难点在于如何删除它。当最小项“死亡”后,我们会发现,在根位置找不到“头”最小的祖宗了,形成了一个空结点。这还了得。为了维持“上头尖来下头粗”的形状,我们得找个祖宗放到该位置。于是我们把堆中最后一项放入空结点。如果最后一项应该放在这,操作就完成了。然而,这几乎不可能,除非堆的大小是2或3。那么只能沿着儿子结点往下移动了。但是有两个儿子哎,选哪个?废话,当然是选“头”较小的那个啦,这样才显得像父子,看见“大头”就来气。就这样,一直移动到合适位置。这个过程则称为“percolate down”。
那我们该怎么找父亲儿子呢?这就得依靠完全二叉树的特性了。完全二叉树是完全填满的树,只有最底层可能有例外。最底层按从左到右的次序填入,中间不能有空结点。如图所示:
图1
图2
图3
图4
图1是一棵满二叉树;图2是一棵完全二叉树;图3和图4都不是完全二叉树,因为这两个中间都有空结点。很显然,满二叉树也是完全二叉树。看看图1和图2是不是很像“上头尖来下头粗”的一坨,:-)。
完全二叉树有许多有用的特性。比如,N个结点的完全二叉树的高度至多是 logN 。不需要 left 和 right 链接等。这样,我们可以将一棵完全二叉树以按层遍历的次序存储在数组中(这符合原则4)。我们把根祖宗放入数组索引 1 的位置,其他结点依次放入。为什么不放在索引 0 呢?因为除了根结点外,每个结点都有父节点。为了避免特殊情况,我们保留位置 0 为哑结点,使其可以当作根结点的父亲。此外,还有其他好处,不再一一列举。这样,我们对数组中任意索引 i 的结点,均可以在索引 2i 处找到它的左儿子。如果该位置超出了树中结点数,那我们就知道它没有左儿子。同理,它的右儿子在索引 2i+1 处。当然,我们也要测试其是否存在。它老子则在 i/2 索引处。
如果只需要插入,删除和找最小值三种操作,那么我们应该毫不犹豫的使用堆。那什么时候会有这种情况?比方说,喜欢我的MM有一个加强排。这么多人每天都围在你身边嗡嗡来嗡嗡去,咋应付啊?于是,我定个标准:每次,我都只找队伍里面最清纯的MM。这下,整个世界清净了。以后,当遇见新美女时,就把她放到队伍合适的位置。那些跟咱Say 88的MM,则一概从队伍里面开除。这个时候,这个队伍用堆来排列最合适不过。这听起来好像优先级队列啊。没错,堆还有一个别名,正是优先级队列。
很遗憾,.NET并没有提供优先级队列集合。我们只能自己编写,下面是完整源代码。

PriorityQueue完整源码
1
using System;
2
using System.Collections.Generic;
3
4
namespace Lucifer.DataStructure
5
{
6
public class PriorityQueue<T>
7
{
8
#region 私有变量
9
10
private int theSize;
11
private T[] theArray;
12
private IComparer<T> theComparer;
13
14
private const int defaultCapacity = 100;
15
16
#endregion
17
18
#region 构造函数
19
20
public PriorityQueue()
21
: this(Comparer<T>.Default)
22
{
23
}
24
25
public PriorityQueue(IComparer<T> comparer)
26
{
27
this.theArray = new T[defaultCapacity];
28
this.theComparer = comparer;
29
}
30
31
public PriorityQueue(IEnumerable<T> collection)
32
{
33
this.theComparer = Comparer<T>.Default;
34
35
ICollection<T> currentCollection = collection as ICollection<T>;
36
37
if (currentCollection != null)
38
{
39
this.theSize = currentCollection.Count;
40
this.theArray = new T[(theSize + 2) * 11 / 10];
41
currentCollection.CopyTo(theArray, 1);
42
}
43
else
44
{
45
int i = 1;
46
foreach (T item in collection)
47
{
48
this.theArray[i++] = item;
49
theSize++;
50
}
51
}
52
53
this.BuildHeap();
54
}
55
56
#endregion
57
58
#region 公有方法
59
/// <summary>
60
/// 添加 item 到优先级队列中。
61
/// </summary>
62
/// <param name="item">要添加的值。</param>
63
public void Add(T item)
64
{
65
if (theSize + 1 == theArray.Length)
66
{
67
Array.Resize<T>(ref theArray, theArray.Length == 0 ? defaultCapacity : (theArray.Length * 2));
68
}
69
/*
70
* 向上过滤。
71
*/
72
int hole = ++theSize;
73
theArray[0] = item;
74
for (; theComparer.Compare(item, theArray[hole / 2]) < 0; hole /= 2)
75
{
76
theArray[hole] = theArray[hole / 2];
77
}
78
theArray[hole] = item;
79
}
80
81
/// <summary>
82
/// 清空优先级队列。
83
/// </summary>
84
public void Clear()
85
{
86
Array.Clear(theArray, 0, theSize);
87
this.theSize = 0;
88
}
89
90
/// <summary>
91
/// 移除优先级队列中最小项。
92
/// </summary>
93
/// <returns>返回优先级队列中被移除的最小项。</returns>
94
public T Remove()
95
{
96
T item = this.GetMin();
97
theArray[1] = theArray[theSize--];
98
this.PercolateDown(1);
99
100
return item;
101
}
102
103
/// <summary>
104
/// 获得优先级队列中最小项。
105
/// </summary>
106
/// <returns>返回优先级队列中最小项。</returns>
107
/// <exception>抛出 InvalidOperationException 异常,当队列为空时。</exception>
108
public T GetMin()
109
{
110
if (this.IsEmpty())
111
{
112
throw new InvalidOperationException("The queue is empty.");
113
}
114
115
return theArray[1];
116
}
117
118
/// <summary>
119
/// 判断优先级队列是否为空。
120
/// </summary>
121
/// <returns>如果是空队列,返回 true 。否则,返回 false 。</returns>
122
public bool IsEmpty()
123
{
124
return theSize == 0;
125
}
126
127
#endregion
128
129
#region 公有属性
130
131
/// <summary>
132
/// 获得优先级队列中项的数量。
133
/// </summary>
134
public int Count
135
{
136
get
137
{
138
return theSize;
139
}
140
}
141
142
#endregion
143
144
#region 私有方法
145
/*
146
* 构造一个堆。
147
*/
148
private void BuildHeap()
149
{
150
for (int i = theSize / 2; i > 0; i--)
151
{
152
this.PercolateDown(i);
153
}
154
}
155
156
/*
157
* 向下过滤。
158
*/
159
private void PercolateDown(int hole)
160
{
161
int child;
162
T item = theArray[hole];
163
for (; hole * 2 <= theSize; hole = child)
164
{
165
child = hole * 2;
166
/*
167
* 父结点并不总是有两个子结点。
168
*/
169
if (child != theSize && theComparer.Compare(theArray[child + 1], theArray[child]) < 0)
170
{
171
child++;
172
}
173
174
if (theComparer.Compare(theArray[child], item) < 0)
175
theArray[hole] = theArray[child];
176
else
177
break;
178
}
179
theArray[hole] = item;
180
}
181
182
#endregion
183
}
184
}
此外,还有一些别的堆。比如斜堆,偶堆,斐波那契堆等,不再阐述。有兴趣的朋友可以参考其他资料。
Heap啊,Heap,
上头尖来下头粗。
有朝一日倒过来,
下头尖来上头粗。
堆实际上是一棵完全二叉树。为了维持“上头尖来下头粗”的形状和有序性,我们规定老子要在儿子的上面,而且比儿子的键值要小。丫丫个呸,这简直就是大头儿子小头爸爸嘛。这样,它和二叉查找树比较起来有个好处,就是假如你想知道堆中“头”最小的爸爸是谁,压根不用找,最顶上那个就是。毛主席教导我们,看问题要一分为二。凡事有好就有坏,堆所带来的坏处就是除了可以很轻松的查找堆中最小值外,基本上不能做别的了。当然,插入和删除这种基本操作还是可以做的。
堆的插入操作就是暂时认为要插入的值是“头”最大的儿子,把它先放入最下层下一个可用位置上。然后让其与老子比较,看谁的“头”大。持续这个过程,让其一直朝着最顶层的根祖宗方向往上冒,直到找到合适位置。这个过程称为“percolate up”。
而删除操作则反其道而行之。我们知道,找到最小项很容易,难点在于如何删除它。当最小项“死亡”后,我们会发现,在根位置找不到“头”最小的祖宗了,形成了一个空结点。这还了得。为了维持“上头尖来下头粗”的形状,我们得找个祖宗放到该位置。于是我们把堆中最后一项放入空结点。如果最后一项应该放在这,操作就完成了。然而,这几乎不可能,除非堆的大小是2或3。那么只能沿着儿子结点往下移动了。但是有两个儿子哎,选哪个?废话,当然是选“头”较小的那个啦,这样才显得像父子,看见“大头”就来气。就这样,一直移动到合适位置。这个过程则称为“percolate down”。
那我们该怎么找父亲儿子呢?这就得依靠完全二叉树的特性了。完全二叉树是完全填满的树,只有最底层可能有例外。最底层按从左到右的次序填入,中间不能有空结点。如图所示:




图1是一棵满二叉树;图2是一棵完全二叉树;图3和图4都不是完全二叉树,因为这两个中间都有空结点。很显然,满二叉树也是完全二叉树。看看图1和图2是不是很像“上头尖来下头粗”的一坨,:-)。
完全二叉树有许多有用的特性。比如,N个结点的完全二叉树的高度至多是 logN 。不需要 left 和 right 链接等。这样,我们可以将一棵完全二叉树以按层遍历的次序存储在数组中(这符合原则4)。我们把根祖宗放入数组索引 1 的位置,其他结点依次放入。为什么不放在索引 0 呢?因为除了根结点外,每个结点都有父节点。为了避免特殊情况,我们保留位置 0 为哑结点,使其可以当作根结点的父亲。此外,还有其他好处,不再一一列举。这样,我们对数组中任意索引 i 的结点,均可以在索引 2i 处找到它的左儿子。如果该位置超出了树中结点数,那我们就知道它没有左儿子。同理,它的右儿子在索引 2i+1 处。当然,我们也要测试其是否存在。它老子则在 i/2 索引处。
如果只需要插入,删除和找最小值三种操作,那么我们应该毫不犹豫的使用堆。那什么时候会有这种情况?比方说,喜欢我的MM有一个加强排。这么多人每天都围在你身边嗡嗡来嗡嗡去,咋应付啊?于是,我定个标准:每次,我都只找队伍里面最清纯的MM。这下,整个世界清净了。以后,当遇见新美女时,就把她放到队伍合适的位置。那些跟咱Say 88的MM,则一概从队伍里面开除。这个时候,这个队伍用堆来排列最合适不过。这听起来好像优先级队列啊。没错,堆还有一个别名,正是优先级队列。
很遗憾,.NET并没有提供优先级队列集合。我们只能自己编写,下面是完整源代码。


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

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

此外,还有一些别的堆。比如斜堆,偶堆,斐波那契堆等,不再阐述。有兴趣的朋友可以参考其他资料。
谨以此记录成长的脚步,同时和大家一起分享快乐。