传送门
堆的定义
堆是一种经过排序的完全二叉树或满二叉树,n个元素的序列{k1,k2,…,kn},当且仅当满足如下关系时被成为堆(1)Ki <= k2i 且 ki <= k2i-1或 (2) Ki >= k2i 且 ki >= k2i-1(i = 1,2,…[n/2])当满足(1)时,为最小堆,当满足(2)时,为最大堆。
满二叉树即除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
最小堆就是在二叉堆的基础上,符合了每个结点都比他的子结点要小的规则
我们会遇到这两种情况,这也是最小堆中特别要注意的两种实现:
上浮:从下至上恢复堆的顺序,在当某个节点的优先级上升时;
下沉:从上至下恢复的顺序,当某个节点的优先级下降的时候;
GO语言实现最小堆
(1)初始化
从一个无序序列初始化为一个堆的过程就是一个反复“筛选”的过程。由完全二叉树的性质可以知,一个有n个节点的完全二叉树的最后一个非叶节点是节点[n/2],堆的初始化过程就从这个[n/2]节点开始。上图为如下无序数组的初始化:
{49,38,65,97,76,13,27,50}
首先,未处理的数组对应的堆为图1模样。从第四个节点开始([8/2]=4),因为50 < 97,故要交换两节点,交换后还要继续对其新的左子树进行类似输出后那样的筛选。易见其左子树只有节点97,已经为最佳情况,故可以继续堆的初始化,如图2。再考虑第三个节点,因为13 < 27 < 65,即节点13为当前的最小节点,故与节点65交换,并对新的左子树进行筛选,其也为最佳情况,故可继续堆的初始化,结果如图3。然后考虑第二个节点,因为38 < 50 < 76,故已经为最优情况,不用调整。最后再考虑第一个节点,根节点。因为 13 < 38 < 49,故需要将根节点49与其右孩子节点13交换,交换后还要继续对其新的右子树进行类似输出后那样的筛选,可见右子树还需要调整,因为 27 < 49 < 65,故将节点49与节点27交换。此时已经处理完了根节点,初始化结束。最终结果如图5.
相关函数:
Down函数,下沉:
将堆nodes的第i个元素下沉,n为nodes的长度。每次选出一个比较小的子节点,父子结点交换,直到父节点比子结点小就可以退出循环了。
/ 需要down(下沉)的元素在数组中的索引为i,n为heap的长度,将该元素下沉到该元素对应的子树合适的位置,从而满足该子树为最小堆的要求
func down(nodes []Node, i, n int) {
fmt.Println("before down node", i, " value ", nodes[i], " :", nodes)
parent := i //parent node
child := i*2 + 1; //left child
for child < n {
if child+1 < n && nodes[child+1].Value < nodes[child].Value { //选择最小的子节点
child += 1 //right child
}
if nodes[parent].Value < nodes[child].Value { //父节点小过子节点,终止循环
break
}
fmt.Println("node exchange: ", parent, " value ", nodes[parent], " and ", child, " value ", nodes[child])
nodes[parent].Value, nodes[child].Value = nodes[child].Value, nodes[parent].Value //父子结点交换
parent, child = child, child*2 + 1
}
fmt.Println("after down node ", i, " value ", nodes[i], " :", nodes)
}
Init函数,初始化最小堆
从最后一个非叶子节点往前遍历,下沉每一元素
// 用于构建结构体数组为最小堆,需要调用down函数
func Init(nodes []Node) {
//第一个非叶子节点为n/2
for i := len(nodes)/2; i >= 0; i -- {
down(nodes, i, len(nodes))
}
return
}
(2)Pop 操作
将堆顶元素弹出,并且使得堆还是最小堆
弹出后将堆低的元素移动到堆顶,进行下沉操作即可
// 弹出最小元素,并保证弹出后的结构体数组仍然是一个最小堆
func Pop(nodes []Node) (Node, []Node) {
tmp := Node{nodes[0].Value} //最小元素为堆顶元素
length := len(nodes)-1 //新堆长度
nodes[0].Value = nodes[length].Value //把原堆最后一个数放到堆顶
nodes = nodes[:length] //切片更新
down(nodes, 0, length) //进行下沉操作
return tmp, nodes //返回最小元素
}
(3)Push操作
添加一个元素到堆的最后面
相关函数
Up函数,将元素上浮
将元素上浮到合适的位置
// 用于保证插入新元素(j为元素的索引,数组末尾插入,堆底插入)的结构体数组之后仍然是一个最小堆
func up(nodes []Node, j int) {
parent := 0 //父节点
for j > 0 {
if j%2 == 0 { //当前结点是右子节点}
parent = j/2 - 1
} else { //左子节点
parent = j/2
}
//子节点比父节点小,交换位置
if nodes[j].Value < nodes[parent].Value {
nodes[parent], nodes[j] = nodes[j], nodes[parent]
j = parent //将当前节点转到父节点
} else { //安全起见,逆向遍历完j,只是插入的话其实可以break了
j --
}
}
}
Push函数,添加元素,并保持最小堆性质
将添加的元素上浮即可
// 保证插入新元素时,结构体数组仍然是一个最小堆,需要调用up函数
func Push(node Node, nodes []Node) []Node {
nodes = append(nodes, node) //加到数组里
up(nodes, len(nodes)-1) //上浮
return nodes
}
(4)Remove操作
将元素从堆中删除,和Pop类似,只是把堆底的元素放掉删除掉的元素的位置。
// 移除数组中指定索引的元素,保证移除后结构体数组仍然是一个最小堆
func Remove(nodes []Node, node Node) []Node {
for i := 0; i < len(nodes); i++ { //循环,同时检测重复值
if nodes[i].Value == node.Value {
new_length := len(nodes)-1
nodes[i].Value = nodes[new_length].Value //将最后一个元素放到改位置
nodes = nodes[:new_length] //切片更新
down(nodes, i, new_length) //下沉操作
}
}
return nodes
}
测试
(1)初始化
结果跟一开始提到的图5一样
(2)Pop操作
Pop后的结果为
(3)Push操作
Push一个结点55
(4)Remove操作
Remove掉结点49
总结
本次实验内容还是挺简单的,只是助教一开始给的代码的函数签名没有返回[]Node类型,这可能会出现一个奇怪的现象:在Pop函数和Remove函数里对nodes进行切片,发现在main函数里len(nodes)并没有减少。
另外可能是xcode的原因,在vscode运行程序需要一次性把输入全打进去(在Go Online则不用),不然会把换行符当作输入0来处理。