服务计算:最小堆

传送门

Go Online 传送门


堆的定义

堆是一种经过排序的完全二叉树或满二叉树,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来处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值