堆 堆排序 优先队列 图文详解(Golang实现)
引入
在实际应用中,我们经常需要从一组对象中查找最大值或最小值。当然我们可以每次都先排序,然后再进行查找,但是这种做法效率很低。哪么有没有一种特殊的数据结构,可以高效率的实现我们的需求呢,答案就是堆(heap)
堆分为最小堆和最大堆,它们的性质相似,我们以最小堆为例子。
最小堆
举例
如上图所示,就为一个最小堆。
特性
- 是一棵完全二叉树
如果一颗二叉树的任何结点,或者是树叶,或者左右子树均非空,则这棵二叉树称做满二叉树(full binary tree)
如果一颗二叉树最多只有最下面的两层结点度数可以小于2,并且最下面一层的结点都集中在该层最左边的连续位置上,则此二叉树称做完全二叉树(complete binary tree)
- 局部有序
最小堆对应的完全二叉树中所有结点的值均不大于其左右子结点的值,且一个结点与其兄弟之间没有必然的联系
二叉搜索树中,左子 < 父 < 右子
存储结构
由于堆是一棵完全二叉树,所以我们可以用顺序结构来存储它,只需要计算简单的代数表达式,就能够非常方便的查找某个结点的父结点和子节点,既避免了使用指针来保持结构,又能高效的执行相应操作。
结点i的左子结点为2xi+1,右子结点为2xi+2
结点i的父节点为(i-1)/2
数据结构
// 本例为最小堆
// 最大堆只需要修改less函数即可
type Heap []int
func (h Heap) swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h Heap) less(i, j int) bool {
return h[i] < h[j]
}
如上所示,我们使用slice来存储我们的数据,为了后续方便我们在此定义了 swap
和 less
函数,分别用来交换两个结点和比较大小。
插入-Push
如上图所示,首先,新添加的元素加入末尾。为了保持最小堆的性质,需要沿着其祖先的路径,自下而上依次比较和交换该结点与父结点的位置,直到重新满足堆的性质为止。
这样会出现两种情况,要么新结点升到最小堆的顶端,要么到某一位置时发现父结点比新插入的结点关键值小。
上面的流程代码如下:
func (h Heap) up(i int) {
for {
f := (i - 1) / 2 // 父亲结点
if i == f || h.less(f, i) {
break
}
h.swap(f, i)
i = f
}
}
实现了最核心的 up
操作后,我们的插入操作 push
便很简单,代码如下:
// 注意go中所有参数转递都是值传递
// 所以要让h的变化在函数外也起作用,此处得传指针
func (h *Heap) Push(x int) {
*h = append(*h, x)
h.up(len(*h) - 1)
}
删除-Remove
如上图所示,首先把最末端的结点填入要删除节点的位置,然后删除末端元素,同理,这样做也可能导致破坏最小堆的堆序特性。
为了保持堆的特性,末端元素需要与被删除位置的父结点做比较,如果小于父结点,就要up(详细代码看插入)如果大于父结点,就要再和被删除位置的子结点做比较,即down,直到该结点下降到小于最小子结点为止。
上面down的流程代码如下:
func (h Heap) down(i int) {
for {
l := 2*i + 1 // 左孩子
if l >= len(h) {
break // i已经是叶子结点了
}
j := l
if r := l + 1; r < len(h) && h.less(r, l) {
j = r // 右孩子
}
if h.less(i, j) {
break // 如果父结点比孩子结点小,则不交换
}
h.swap(i, j) // 交换父结点和子结点
i = j //继续向下比较
}
}
实现了核心的 down
操作后,我们的 Remove
便很简单,代码如下:
// 删除堆中位置为i的元素
// 返回被删元素的值
func (h *Heap) Remove(i int) (int, bool) {
if i < 0 || i > len(*h)-1 {
return 0, false
}
n := len(*h) - 1
h.swap(i, n) // 用最后的元素值替换被删除元素
// 删除最后的元素
x := (*h)[n]
*h = (*h)[0:n]
// 如果当前元素大于父结点,向下筛选
if (*h)[i] > (*h)[(i-1)/2] {
h.down(i)
} else { // 当前元素小于父结点,向上筛选
h.up(i)
}
return x, true
}
弹出-Pop
当i=0时,Remove
就是 Pop
// 弹出堆顶的元素,并返回其值
func (h *Heap) Pop() int {
n := len(*h) - 1
h.swap(0, n)
x := (*h)[n]
*h = (*h)[0:n]
h.down(0)
return x
}
初始化-Init
在我们讲完了堆的核心操作 up
和 down
后,我们来讲如何根据一个数组构造一个最小堆。
其实我们可以写个循环,然后将各个元素依次 push
进去,但是这次我们利用数学规律,直接由一个数组构造最小堆。
首先,将所有关键码放到一维数组中,此时形成的完全二叉树并不具备最小堆的特征,但是仅包含叶子结点的子树已经是堆。
即在有n个结点的完全二叉树中,当 i>n/2-1
时,以i结点为根的子树已经是堆。
func (h Heap) Init() {
n := len(h)
// i > n/2-1 的结点为叶子结点本身已经是堆了
for i := n/2 - 1; i >= 0; i-- {
h.down(i)
}
}
测试
func main() {
var h = heap.Heap{20, 7, 3, 10, 15, 25, 30, 17, 19}
h.Init()
fmt.Println(h) // [3 7 20 10 15 25 30 17 19]
h.Push(6)
fmt.Println(h) // [3 6 20 10 7 25 30 17 19 15]
x, ok := h.Remove(5)
fmt.Println(x, ok, h) // 25 true [3 6 15 10 7 20 30 17 19]
y, ok := h.Remove(1)
fmt.Println(y, ok, h) // 6 true [3 7 15 10 19 20 30 17]
z := h.Pop()
fmt.Println(z, h) // 3 [7 10 15 17 19 20 30]
}
完整代码
堆排序
在讲完堆的基础知识后,我们再来看堆排序就变得非常简单。利用最小堆的特性,我们每次都从堆顶弹出一个元素(这个元素就是当前堆中的最小值),即可实现升序排序。代码如下:
// 堆排序
var res []int
for len(h) != 0 {
res = append(res, h.Pop())
}
fmt.Println(res)
优先队列
优先队列是0个或者多个元素的集合,每个元素都有一个关键码,执行的操作有查找,插入和删除等。
优先队列的主要特点是支持从一个集合中快速地查找并移出具有最大值或最小值的元素。
堆是一种很好的优先队列的实现方法。
参考资料
- 《数据结构与算法》张铭 王腾蛟 赵海燕 编著
- GO SDK 1.13.1 /src/container/heap
最后
本文是自己的学习笔记,在刷了几道LeetCode中关于堆的题目后,感觉应该系统的学习和总结一下这一重要的数据结构了。
强烈建议看Go的源码中关于heap的实现,仔细感受面向接口编程的思想,和他们的代码风格以及质量。
堆 堆排序 优先队列 图文详解(Golang实现)的更多相关文章
- ElasticSearch实战系列八: Filebeat快速入门和使用---图文详解
前言 本文主要介绍的是ELK日志系统中的Filebeat快速入门教程. ELK介绍 ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是 ...
- CentOS 6.3下Samba服务器的安装与配置方法(图文详解)
这篇文章主要介绍了CentOS 6.3下Samba服务器的安装与配置方法(图文详解),需要的朋友可以参考下 一.简介 Samba是一个能让Linux系统应用Microsoft网络通讯协议的软件, ...
- Cocos2d-x win7 + vs2010 配置图文详解
Cocos2d-x win7 + vs2010 配置图文详解 下载最新版的cocos2d-x.打开浏览器,输入cocos2d-x.org,然后选择Download,本教程写作时最新版本为cocos2d ...
- zookeeper的安装(图文详解。。。来点击哦!)
zookeeper的安装(图文详解...来点击哦!) 一.服务器的配置 三台服务器: 192.168.83.133 sunshine 192.168.83.134 sunshineMin 19 ...
- Hadoop集群搭建安装过程(三)(图文详解---尽情点击!!!)
Hadoop集群搭建安装过程(三)(图文详解---尽情点击!!!) 一.JDK的安装 安装位置都在同一位置(/usr/tools/jdk1.8.0_73) jdk的安装在克隆三台机器的时候可以提前安装 ...
- Hadoop集群搭建安装过程(二)(图文详解---尽情点击!!!)
Hadoop集群搭建安装过程(二)(配置SSH免密登录)(图文详解---尽情点击!!!) 一.配置ssh无密码访问 ®生成公钥密钥对 1.在每个节点上分别执行: ssh-keygen -t rsa(一 ...
- Linux虚拟机安装(CentOS 6.5,图文详解,需要自查)
Linux虚拟机的安装(图文详解) 下篇会接续Hadoop集群安装(以此为基础) 一.安装准备 VMWorkstation.linux系统镜像(以下以CentOS6.5为例) 二.安装过程详解 关闭防 ...
- 分享我开发的网络电话Android手机APP正式版,图文详解及下载
分享我开发的网络电话Android手机APP正式版,图文详解及下载 分享我开发的网络电话Android手机APP正式版 实时语音通讯,可广域网实时通讯,音质清晰流畅! 安装之后的运行效果: 第一次安装 ...
- 图文详解Unity3D中Material的Tiling和Offset是怎么回事
图文详解Unity3D中Material的Tiling和Offset是怎么回事 Tiling和Offset概述 Tiling表示UV坐标的缩放倍数,Offset表示UV坐标的起始位置. 这样说当然是隔 ...
随机推荐
- JVM 启动调优总结
启动命令 格式: java -jar 命令行参数 jar包路径 .示例如下 java -Dfile.encoding=utf-8 -jar -XX:MetaspaceSize=128m -XX:Max ...
- 用Python将处理数据得到的csv文件分类(按顺序)保存
用Python中的os和numpy库对文件夹及处理数据后得到的文件进行分类保存: import numpy as np import os for m in range(699,0,-35): cur ...
- Sqlsever新增作业执行计划傻瓜式操作
开启数据库代理,启动不了请检查数据库服务的代理是否开启 建议Sqlserver2008以上的版本 完整步骤如下: 查看效果: 10秒以后再来查询:发现数据有多了一些,是不是很简单,嘻嘻!
- Java设计模式_七大原则
简介 单一职责原则.对类来说,即一个类应该只负责一项职责. 开闭原则.对扩展开放,对修改关闭.在程序需要进行扩展的时候,不能去修改原有代码,使用接口和抽象类实现一个热插拔的效果. 里氏替换原则.任何基 ...
- 如何让多个不同类型的后端网站用一个nginx进行反向代理实际场景分析
前段时间公司根据要求需要将聚石塔上服务器从杭州整体迁移到张家口,刚好趁这次机会将这些乱七八糟的服务器做一次梳理和整合,断断续续一个月迁移完成 大概优化掉了1/3的机器,完成之后遇到了一些问题,比如曾今 ...
- 平时服务正常,突然挂了,怎么重启都起不来,查看日志Insufficient space for shared memory file 内存文件空间不足
Java HotSpot(TM) 64-Bit Server VM warning: Insufficient space for shared memory file: /tmp/hsperfd ...
- [考试反思]0719NOIP模拟测试6 + 0722NOIP模拟测试7
连续爆炸,颇为愉快. 第6次:Rank #4 第7次:Rank #9 对于第6次考试,个人比较满意,因为T1只是差了一个卡常. 因为在考试前两天刚讲了矩阵,满脑子都是矩阵,还想到了循环矩阵优化. 整个 ...
- 【vue】在VS Code中调试Jest单元测试
在VS Code中调试Jest单元测试 添加调试任务 打开 vscode launch.json 文件,在 configurations 内加入下面代码 "configurations&qu ...
- Scrapy进阶知识点总结(六)——中间件详解
概述 查看scrapy官网的框架图,可以看出中间件处于几大主要组件之间,类似于生产流水线上的加工过程,将原料按照不同需求与功能加工成成品 其中4,5处于下载器与引擎之间的就是下载中间件,而spider ...
- 因为 GitHub Actions 我发现了 Jake Wharton 的一个仓库
本文微信公众号「AndroidTraveler」首发. 背景 昨天(2019-11-14)上去 GitHub 上面一看,结果来了个下面的提示: 点进去一看: 看来是自动化构建相关的. 那就试一下,选了 ...