Go 堆数据结构使用

news2024/10/7 16:16:05

说到 container/heap 下的堆数据结构,让我们不需要从零开始实现这个数据结构。如果只是日常工作,其实还挺难用到堆的,更多的还是在写算法题的时候会用到。

基本概念

堆分为大顶堆和小顶堆,区分这两种类型方便我们处理问题。大顶堆的堆顶元素值最大,如果我们的业务场景是求TopN的最小值,我们就可以维系N个元素的大顶堆。这样的好处是,如果我们待排序的元素大于堆顶元素,直接忽略这个元素就可以。小顶堆也同理。

heap
如图,小顶堆和大顶堆的直观展示。堆的结构是一颗完全二叉树,拿小顶堆来说,父节点要始终小于它的左右子节点,但左右子节点的大小是不明确的。

我们一般通过数组来存储完全二叉树,也就是用数组来存储堆结构,具体的表示关系,关键是看数组的 0 号元素是否要存储值。我们的实现都是数组的 0 号元素作为堆顶元素。用数组表示之后,具体的元素关系如下:

heap-relation
如果我们的存储是从下标为1开始的,数组的 1 号元素表示堆顶元素,对应的计算关系就会发生变化。左孩子节点为 2i,右孩子节点为 2i+1,父节点为 i/2。不过,go 语言数组 0 号位标识堆顶元素,和图式是一致的。

基础操作

堆的基本操作包括向堆中插入一个新元素,以及删除堆顶元素。熟悉了这两个操作之后,基本也就掌握了堆的实现。本质上,就是模拟了一个空元素,然后重新调整堆结构。插入是一个不断上浮的过程,删除是一个下沉的过程,注意看下面两个过程:

插入

insert
我们向堆中插入新的元素 7,就是在完全二叉树的叶子结点上追加一个新的节点(追加在从左到右的最后),然后依次对这个新节点所在的子树做调整,使最小子树保持堆结构,最终到不需要调整结束,此时也就找到了元素 7 的最终位置。

从底层数组的角度来看的话,就是向数组中追加一个新的元素 7,然后基于父子节点关系,不断进行向上调整,指导找到 7 的最终位置。

删除堆顶

delete
删除堆顶元素和插入类似,堆顶元素可以通过访问数组的第一个元素获取到,删除堆顶元素之后,堆顶元素就空了。堆的处理思路是将最后一个叶子节点放到堆顶元素,然后,不断进行所在子树的向下调整,最终所有子树都满足堆的特性。

如果所示,在删除堆顶元素 13 之后,我们将堆的末尾元素替换到堆顶的位置,然后,不断向下调整,最终找到元素 6 的合理位置。

go heap 实现

go 在 container/heap 做了官方的实现,如果要使用堆,只需要实现下面的接口,接口中匿名嵌套了排序接口,我们也需要将排序的三个接口声明实现一遍。

// go1.16.6 版本
type Interface interface {
	sort.Interface
	Push(x interface{}) // add x as element Len()
	Pop() interface{}   // remove and return element Len() - 1.
}

特别需要注意一点,在实现接口类型时,Push和Pop接受体的声明需要选择指针类型,因为这个过程需要对原始的值做扩展,在原始值得基础上做修改。

我们实现一个基本的堆排序接口,使用基础类型 int 来模拟堆排序。下面是代码部分,这部分是基本的堆实现,如果要使用堆的话,可以在这个基础上做扩展。

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 interface{}) {
	*h = append(*h, x.(int))
}

func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0:n-1]
	return x
}

实现堆的 Interface 接口之后,就是如何使用这个结构体了。我们简单来说明一下。下面这个例子,我们初始化了一个 IntHeap 数据类型,然后调用 heap.Init 方法做了堆排序,保证初始化的数组是按照小顶堆的顺序排序的。之后获取堆的长度,依次调用 heap.Pop 方法获取堆顶元素。

之所以每次都调用 Pop 方法,是因为在 Pop 完堆顶元素之后,剩余的堆元素还需要进行排序。小顶堆只知道堆顶元素最小,去掉堆顶元素之后,左右子树还是需要做调整的。

// output: 1,2,2,3,3,4,5,5,6,9,10,20,
func main() {
	h := &IntHeap{3, 2, 20, 5, 3, 1, 2, 5, 6, 9, 10, 4}
	heap.Init(h)

	length := h.Len()
	for i := 0; i < length; i++ {
		fmt.Printf("%d,", heap.Pop(h).(int))
	}
}

当然,我们也可以不使用 heap.Init 去做堆的初始化排序,我们可以向一个空元素的堆中依次插入元素来构建堆,效果其实是一样的。因为每次执行 heap.Push 就是在做堆排序。

func main() {
	nums := []int{3, 2, 20, 5, 3, 1, 2, 5, 6, 9, 10, 4}
	h := &IntHeap{}

	for _, val := range nums {
		heap.Push(h, val)
	}

	length := h.Len()
	for i := 0; i < length; i++ {
		fmt.Printf("%d,", heap.Pop(h).(int))
	}
}

性能比较

在 go 中如果不使用堆排序,我们要获取 TopN 个最大值元素,一般的做法是对数据集做好排序,然后,截取排序好的前 N 个元素。

如果使用堆排序,性能会更好吗?如果还以取 TopN 个最大值元素为例,我们可以构造一个 N 个元素的小顶堆,如果发现待排序的元素比堆顶元素小,可以直接舍弃。如果待排序的元素比堆顶元素大,执行 heap.Push 堆排序,完成之后并 heap.Pop 堆顶元素。

快排的方法,调用 sort.Slice 对整形数组直接排序,然后取 TopN

func FindTopNWithSort(nums []int, n int) []int {
	sort.Slice(nums, func(i, j int) bool {
		return nums[i] > nums[j]
	})

	result := make([]int, n)
	copy(result, nums[:n])
	return result
}

下面是堆排序方式,为了跟堆顶元素做比较,我们在 IntHeap 结构体上扩展新的方法 Top 来获取堆顶元素。这样能减少一些无效的堆排序。
heap.top

func FindTopNWithHeap(nums []int, n int) []int {
	h := &IntHeap{}
	for _, val := range nums {
		// 根据堆顶元素做提前退出
		if h.Len() == n && h.Top().(int) > val {
			continue
		}

		heap.Push(h, val)
		if h.Len() > n {
			heap.Pop(h)
		}
	}

	return func() []int {
		initialLen := h.Len()
		result := make([]int, initialLen)
		for i := initialLen; i > 0; i-- {
			result[i-1] = heap.Pop(h).(int)
		}
		return result
	}()
}

我们首先验证这两个方法的正确性,确保可以返回 TopN 的最大值,然后在使用 benchmark 做性能测试。这里随机构造 600 个整数,然后用两种方法取 Top 30 的最大值

var nums []int
var result []int

func TestMain(m *testing.M) {
	maxVal := 6000
	rand.Seed(time.Now().Unix())

	nums = make([]int, maxVal)
	for i := 0; i < len(nums); i++ {
		nums[i] = rand.Intn(maxVal)
	}
	m.Run()
}

func BenchmarkFindTopNWithSort(b *testing.B) {
	k := 30
	nums2 := make([]int, len(nums))
	copy(nums2, nums[:len(nums)])
	b.ResetTimer()

	for n := 0; n < b.N; n++ {
		result = FindTopNWithSort(nums2, k)
	}
}

func BenchmarkFindTopNWithHeap(b *testing.B) {
	k := 30
	nums2 := make([]int, len(nums))
	copy(nums2, nums[:len(nums)])
	b.ResetTimer()

	for n := 0; n < b.N; n++ {
		result = FindTopNWithHeap(nums2, k)
	}
}

两种方式执行5次,可以发现,heap 的执行情况要稍微好一点。如果将待排序的数据集由600调整成6000,heap 的排序效果会更明显的变好一些。

heap.bechmark
回归到一般的业务工作中,我们构造待排序的数据集其实也非常花费时间,如果使用堆排序,可以省去一部分构造待排序数据集的时间开销。不过,还是需要合理评估,如果数据集本身特别小,只有几十个元素的排序,可能堆排序还会特别拉胯

参考文章:

  1. The Generic Way to Implement a Heap in Golang
  2. Usage of the Heap Data Structure in Go (Golang), with Examples

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Docker安装Zookeeper教程(超详细)

生命无罪&#xff0c;健康万岁&#xff0c;我是laity。 我曾七次鄙视自己的灵魂&#xff1a; 第一次&#xff0c;当它本可进取时&#xff0c;却故作谦卑&#xff1b; 第二次&#xff0c;当它在空虚时&#xff0c;用爱欲来填充&#xff1b; 第三次&#xff0c;在困难和容易之…

第六章:关系数据理论

一、问题的提出、范式 1、【多选题】下列说法中正确的是&#xff1a; 正确答案&#xff1a; ABCD 2、【多选题】关系模式R&#xff08;项目序号&#xff0c;项目代码&#xff0c;项目名称&#xff09;&#xff0c;项目序号是码。一个项目代码只有一个项目名称。下列说法不正确…

文献检索报告

文献检索第一篇检索作业总结第一章检索任务1.1检索课题1.2确定选题所属学科1.3中英文检索词第二章检索策略与结果2.1检索中文期刊文献2.1.1 CNKI中国期刊全文数据库2.1.2 维普期刊全文数据库2.1.3 万方期刊数据库2.1.4 超星期刊全文2.2检索中文学位论文2.2.1 CNKI博硕学位论文数…

virtio-net发包流程分析

virtio-net发包流程分析 virtio-net发包流程前端驱动部分 总流程 start_xmit|---->free_old_xmit_skbs /* 释放backend处理过的desc */|---->xmit_skb /* 调用xmit_skb函数将网络包写入virtqueue */| |---->sg_set_buf /* 数据包头部填入scatterlist */| | |---->…

手撕红黑树、三种情况就可玩转红黑

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录一、红黑树概念二、红黑树性质三、红黑树 插入①变色&#xff08;c红 p红 g黑 u存在且红&#xff09;②旋转&#xff08;c红 p红 g黑 u存在且…

熟人服务器被黑,五种实战方法强化linux服务器安全性!

公司护网行动,五种实战方法,下面直接上实操: 1.修改ssh端口为59527,并开放防火墙端口 修改ssh配置文件 /etc/ssh/sshd_config,将端口号修改为59527.同时保留ssh默认的22端口,为了防止修改端口号失败以后,远程登录不上服务器 2.修改firewall配置 默认情况下,防火墙是…

JVM——垃圾回收

垃圾回收 1、如何判断对象可以回收? 一、引用计数法 当一个对象被其他变量引用时&#xff0c;使其计数1&#xff08;若被引用两次&#xff0c;计数为2&#xff09;&#xff0c;若某个变量不在引用它时&#xff0c;使其计数-1&#xff1b;当这个对象引用计数变为0时意味着不…

吴恩达【神经网络和深度学习】Week1——深度学习概述

文章目录1、What is a neural network?2、Supervised Learning with Neural Networks2.1、Examples2.2、The classification of data3、Why is Deep Learning taking off&#xff1f;4、Quiz课程笔记整理按照所讲章节的标题来完成1、What is a neural network? 以房价预测模型…

基于HTML5 技术的开放自动化HMI

人机交互接口&#xff08;HMI&#xff09;是自动化系统中不可或缺的一部分。传统的做法是提供一个HMI 显示屏&#xff0c;并且通过组态软件来配置显示屏的功能&#xff0c;通过modbus 或者以太网与PLC 连接。 现在&#xff0c;事情变得复杂了许多&#xff0c;用户不仅需要通过专…

干货 | 关于PCB中的“平衡铜”,一文全部说明白

平衡铜是PCB设计的一个重要环节&#xff0c;对PCB上闲置的空间用铜箔进行填充&#xff0c;一般将其设置为地平面。 平衡铜的意义在于&#xff1a; 对信号来说&#xff0c;提供更好的返回路径&#xff0c;提高抗干扰能力&#xff1b;对电源来说&#xff0c;降低阻抗&#xff0c;…

Android 13 源码获取与构建

文章目录1. 环境准备1.1 基本信息1.2 系统初始化1.2.1 更新 Ubuntu 软件包1.2.2 安装 git 工具1.2.3 安装依赖包(Ubuntu 18.04)1.2.4 修改默认python版本1.2.5 安装 repo 工具2. 源码下载完成2.1 创建源码目录2.2 初始化源码仓库2.3 开始下载源码2.4 Android 13 源码目录3. 构建…

m可见光通信的空间调制(sm)误码率matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 可见光通信技术&#xff08;Visible Light Communication&#xff0c;VLC&#xff09;是指利用可见光波段的光作为信息载体&#xff0c;在空气中直接传输光信号的通信方式。可见光通信技术绿色低…

virtio vring原理

vring原理 在 virtio 设备上进行批量数据传输的机制被称为 virtqueue 。每个设备可以拥有零个或多个 virtqueue &#xff0c;当 Driver 想要向设备发送数据时&#xff0c;它会填充 Descriptor Table 中的一项&#xff08;或将几项链接在一起&#xff09;&#xff0c;并将描述符…

圣诞树拼图游戏unity制作

2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ 一、前言 提示&#xff1a;使用unity来制作一个拼图游戏&#xff0c;图片便是圣诞树。 二、创意名 圣诞树拼图游戏 三、效果展示 圣诞树拼图游戏最终效果。 游戏中效果如图&#xff1a; 游戏拼图完成后效果如图&am…

vue实现随机生成分享海报(内容动态)

大家好&#xff0c;我是雄雄。 前言 昨天写了篇文章&#xff1a;自己整理的vue实现生成分享海报&#xff08;含二维码&#xff09;&#xff0c;看着网上的没实现 主要是介绍了如何使用vue实现&#xff0c;动态分享内容为海报&#xff0c;且附带二维码&#xff0c;扫描二维码能…

shell脚本四剑客之awk详解

文章目录awk的介绍awk能够干什么awk的格式工作原理&#xff1a;记录和域内建变量的用法1. FS2. OFS3.RS4. ORS5. NF6. NRBEGIN 和END语句块常见案例1. 使用NR行号提取ip2. 打印UID小于10的账号名称和UID信息3. 数学运算4. AWK打印硬盘设备名称&#xff0c;默认以空格为分割&…

UDP用户数据报协议(计算机网络-运输层)

目录 UDP 概述 UDP 的主要特点 UDP 的问题 UDP的多路分用模型 UDP 的首部格式 UDP 概述 用户数据报协议&#xff08;User Datagram Protocol&#xff0c;UDP&#xff09; UDP 只在 IP 的数据报服务之上增加了很少一点的功能&#xff0c;即端口的功能和差错检测的功能 虽…

计算机网络——网络层功能概述

网络层 网络层的主要任务是把分组从源端传到目的端&#xff0c;为分组交换网上的不同主机提供通信服务。网络层的传输单位成为数据报。 数据报是一组比较长的数据&#xff0c;分组则是将数据报划分为不同的片段 网络层的第一个功能&#xff1a;路由的选择和分组的转发。 网络层…

python词云图词频统计

目录 一&#xff1a;安装必要的库 二&#xff1a;数据分析 条形图可视化 三&#xff1a;数据分析 词频统计 词云图可视化 一&#xff1a;安装必要的库 导入必要的库 import collections # 词频统计库 import os import re # 正则表达式库 import urllib.error # 指定url&…

WRF进阶:antro_emiss工具处理全球大气人为排放(EDGRA_HTTPs)/人为排放清单前处理

本内容视频版讲解&#xff1a;全球人为排放处理 介绍 一般人为数据的排放前处理使用pre_chen_src工具&#xff0c;然而pre_chen_src处理后的文件并不是WRF所能读取的文件格式&#xff0c;需要使用onvert_emiss.exe&#xff0c;生成WRF需要的人为排放的nc数据。 在WRF-chem3.6…