引入

在实际应用中,我们经常需要从一组对象中查找最大值最小值。当然我们可以每次都先排序,然后再进行查找,但是这种做法效率很低。哪么有没有一种特殊的数据结构,可以高效率的实现我们的需求呢,答案就是堆(heap)

堆分为最小堆和最大堆,它们的性质相似,我们以最小堆为例子。

最小堆

举例

如上图所示,就为一个最小堆。

特性

  • 是一棵完全二叉树

如果一颗二叉树的任何结点,或者是树叶,或者左右子树均非空,则这棵二叉树称做满二叉树(full binary tree)

如果一颗二叉树最多只有最下面的两层结点度数可以小于2,并且最下面一层的结点都集中在该层最左边的连续位置上,则此二叉树称做完全二叉树(complete binary tree)

  • 局部有序

最小堆对应的完全二叉树中所有结点的值均不大于其左右子结点的值,且一个结点与其兄弟之间没有必然的联系

二叉搜索树中,左子 < 父 < 右子

存储结构

由于堆是一棵完全二叉树,所以我们可以用顺序结构来存储它,只需要计算简单的代数表达式,就能够非常方便的查找某个结点的父结点和子节点,既避免了使用指针来保持结构,又能高效的执行相应操作。

  1. 结点i的左子结点为2xi+1,右子结点为2xi+2
  2. 结点i的父节点为(i-1)/2

数据结构

  1. // 本例为最小堆
  2. // 最大堆只需要修改less函数即可
  3. type Heap []int
  4. func (h Heap) swap(i, j int) {
  5. h[i], h[j] = h[j], h[i]
  6. }
  7. func (h Heap) less(i, j int) bool {
  8. return h[i] < h[j]
  9. }

如上所示,我们使用slice来存储我们的数据,为了后续方便我们在此定义了 swapless 函数,分别用来交换两个结点和比较大小。

插入-Push

如上图所示,首先,新添加的元素加入末尾。为了保持最小堆的性质,需要沿着其祖先的路径,自下而上依次比较和交换该结点与父结点的位置,直到重新满足堆的性质为止。

这样会出现两种情况,要么新结点升到最小堆的顶端,要么到某一位置时发现父结点比新插入的结点关键值小。

上面的流程代码如下:

  1. func (h Heap) up(i int) {
  2. for {
  3. f := (i - 1) / 2 // 父亲结点
  4. if i == f || h.less(f, i) {
  5. break
  6. }
  7. h.swap(f, i)
  8. i = f
  9. }
  10. }

实现了最核心的 up 操作后,我们的插入操作 push 便很简单,代码如下:

  1. // 注意go中所有参数转递都是值传递
  2. // 所以要让h的变化在函数外也起作用,此处得传指针
  3. func (h *Heap) Push(x int) {
  4. *h = append(*h, x)
  5. h.up(len(*h) - 1)
  6. }

删除-Remove

如上图所示,首先把最末端的结点填入要删除节点的位置,然后删除末端元素,同理,这样做也可能导致破坏最小堆的堆序特性。

为了保持堆的特性,末端元素需要与被删除位置的父结点做比较,如果小于父结点,就要up(详细代码看插入)如果大于父结点,就要再和被删除位置的子结点做比较,即down,直到该结点下降到小于最小子结点为止。

上面down的流程代码如下:

  1. func (h Heap) down(i int) {
  2. for {
  3. l := 2*i + 1 // 左孩子
  4. if l >= len(h) {
  5. break // i已经是叶子结点了
  6. }
  7. j := l
  8. if r := l + 1; r < len(h) && h.less(r, l) {
  9. j = r // 右孩子
  10. }
  11. if h.less(i, j) {
  12. break // 如果父结点比孩子结点小,则不交换
  13. }
  14. h.swap(i, j) // 交换父结点和子结点
  15. i = j //继续向下比较
  16. }
  17. }

实现了核心的 down 操作后,我们的 Remove 便很简单,代码如下:

  1. // 删除堆中位置为i的元素
  2. // 返回被删元素的值
  3. func (h *Heap) Remove(i int) (int, bool) {
  4. if i < 0 || i > len(*h)-1 {
  5. return 0, false
  6. }
  7. n := len(*h) - 1
  8. h.swap(i, n) // 用最后的元素值替换被删除元素
  9. // 删除最后的元素
  10. x := (*h)[n]
  11. *h = (*h)[0:n]
  12. // 如果当前元素大于父结点,向下筛选
  13. if (*h)[i] > (*h)[(i-1)/2] {
  14. h.down(i)
  15. } else { // 当前元素小于父结点,向上筛选
  16. h.up(i)
  17. }
  18. return x, true
  19. }

弹出-Pop

当i=0时,Remove 就是 Pop

  1. // 弹出堆顶的元素,并返回其值
  2. func (h *Heap) Pop() int {
  3. n := len(*h) - 1
  4. h.swap(0, n)
  5. x := (*h)[n]
  6. *h = (*h)[0:n]
  7. h.down(0)
  8. return x
  9. }

初始化-Init

在我们讲完了堆的核心操作 updown 后,我们来讲如何根据一个数组构造一个最小堆。

其实我们可以写个循环,然后将各个元素依次 push 进去,但是这次我们利用数学规律,直接由一个数组构造最小堆。

首先,将所有关键码放到一维数组中,此时形成的完全二叉树并不具备最小堆的特征,但是仅包含叶子结点的子树已经是堆。

即在有n个结点的完全二叉树中,当 i>n/2-1 时,以i结点为根的子树已经是堆。

  1. func (h Heap) Init() {
  2. n := len(h)
  3. // i > n/2-1 的结点为叶子结点本身已经是堆了
  4. for i := n/2 - 1; i >= 0; i-- {
  5. h.down(i)
  6. }
  7. }

测试

  1. func main() {
  2. var h = heap.Heap{20, 7, 3, 10, 15, 25, 30, 17, 19}
  3. h.Init()
  4. fmt.Println(h) // [3 7 20 10 15 25 30 17 19]
  5. h.Push(6)
  6. fmt.Println(h) // [3 6 20 10 7 25 30 17 19 15]
  7. x, ok := h.Remove(5)
  8. fmt.Println(x, ok, h) // 25 true [3 6 15 10 7 20 30 17 19]
  9. y, ok := h.Remove(1)
  10. fmt.Println(y, ok, h) // 6 true [3 7 15 10 19 20 30 17]
  11. z := h.Pop()
  12. fmt.Println(z, h) // 3 [7 10 15 17 19 20 30]
  13. }

完整代码

Github

堆排序

在讲完堆的基础知识后,我们再来看堆排序就变得非常简单。利用最小堆的特性,我们每次都从堆顶弹出一个元素(这个元素就是当前堆中的最小值),即可实现升序排序。代码如下:

  1. // 堆排序
  2. var res []int
  3. for len(h) != 0 {
  4. res = append(res, h.Pop())
  5. }
  6. fmt.Println(res)

优先队列

优先队列是0个或者多个元素的集合,每个元素都有一个关键码,执行的操作有查找,插入和删除等。

优先队列的主要特点是支持从一个集合中快速地查找并移出具有最大值或最小值的元素。

堆是一种很好的优先队列的实现方法。

参考资料

  • 《数据结构与算法》张铭 王腾蛟 赵海燕 编著
  • GO SDK 1.13.1 /src/container/heap

最后

本文是自己的学习笔记,在刷了几道LeetCode中关于堆的题目后,感觉应该系统的学习和总结一下这一重要的数据结构了。

强烈建议看Go的源码中关于heap的实现,仔细感受面向接口编程的思想,和他们的代码风格以及质量。

堆 堆排序 优先队列 图文详解(Golang实现)的更多相关文章

  1. ElasticSearch实战系列八: Filebeat快速入门和使用---图文详解

    前言 本文主要介绍的是ELK日志系统中的Filebeat快速入门教程. ELK介绍 ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是 ...

  2. CentOS 6.3下Samba服务器的安装与配置方法(图文详解)

    这篇文章主要介绍了CentOS 6.3下Samba服务器的安装与配置方法(图文详解),需要的朋友可以参考下   一.简介  Samba是一个能让Linux系统应用Microsoft网络通讯协议的软件, ...

  3. Cocos2d-x win7 + vs2010 配置图文详解

    Cocos2d-x win7 + vs2010 配置图文详解 下载最新版的cocos2d-x.打开浏览器,输入cocos2d-x.org,然后选择Download,本教程写作时最新版本为cocos2d ...

  4. zookeeper的安装(图文详解。。。来点击哦!)

    zookeeper的安装(图文详解...来点击哦!) 一.服务器的配置 三台服务器: 192.168.83.133   sunshine 192.168.83.134   sunshineMin 19 ...

  5. Hadoop集群搭建安装过程(三)(图文详解---尽情点击!!!)

    Hadoop集群搭建安装过程(三)(图文详解---尽情点击!!!) 一.JDK的安装 安装位置都在同一位置(/usr/tools/jdk1.8.0_73) jdk的安装在克隆三台机器的时候可以提前安装 ...

  6. Hadoop集群搭建安装过程(二)(图文详解---尽情点击!!!)

    Hadoop集群搭建安装过程(二)(配置SSH免密登录)(图文详解---尽情点击!!!) 一.配置ssh无密码访问 ®生成公钥密钥对 1.在每个节点上分别执行: ssh-keygen -t rsa(一 ...

  7. Linux虚拟机安装(CentOS 6.5,图文详解,需要自查)

    Linux虚拟机的安装(图文详解) 下篇会接续Hadoop集群安装(以此为基础) 一.安装准备 VMWorkstation.linux系统镜像(以下以CentOS6.5为例) 二.安装过程详解 关闭防 ...

  8. 分享我开发的网络电话Android手机APP正式版,图文详解及下载

    分享我开发的网络电话Android手机APP正式版,图文详解及下载 分享我开发的网络电话Android手机APP正式版 实时语音通讯,可广域网实时通讯,音质清晰流畅! 安装之后的运行效果: 第一次安装 ...

  9. 图文详解Unity3D中Material的Tiling和Offset是怎么回事

    图文详解Unity3D中Material的Tiling和Offset是怎么回事 Tiling和Offset概述 Tiling表示UV坐标的缩放倍数,Offset表示UV坐标的起始位置. 这样说当然是隔 ...

随机推荐

  1. JVM(8) 线程安全与锁优化

    面向过程编程:程序编写以算法为核心,程序员会把数据和过程分别作为独立的部分来考虑,数据代表问题空间的客体,程序代码则用于处理这些数据.这种思维方式直接站在计算机的角度去抽象问题和解决问题,称为面向过程 ...

  2. 数据结构(四十五)选择排序(1.直接选择排序(O(n²))2.堆排序(O(nlogn)))

    一.选择排序的定义 选择排序的基本思想是:每次从待排序的数据元素集合中选取最小(或最大)的数据元素放到数据元素集合的最前(或最后),数据元素集合不断缩小,当数据元素集合为空时排序过程结束.常用的选择排 ...

  3. incompatible implicit declaration of built-in function 'fabs'

    形如: float a = -3.0; float b = fabs(a); 形参数据类型和实参数据类型完全一致,却还报警告: incompatible implicit declaration of ...

  4. 设计时需要考虑的问题(webAPI)

    1.根据api接口访问路径定义好controller和action. 2.记录操作日志.包含接口入参.出参.异常以及重要的节点数据(数据库返回.第三方接口返回.重要的私有变量值) 3.入参合法性检查. ...

  5. 基于AOP和Redis实现对接口调用情况的监控及IP限流

    目录 需求描述 概要设计 代码实现 参考资料 需求描述 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下: 需求一:实现对每个接口,每天的调用次数做记录: 需求二:如果某 ...

  6. Windows 10 + kali Linux 双系统安装教程(详细版)

    准备工具如下: kali Linux 镜像 准备一4G以上的U盘 制作U盘启动盘工具- Win32DiskImager 添加引导工具-EasyBCD 留出一个空的盘,哪个盘的空间比较大可以压缩出大概2 ...

  7. 使用promise封装ajax

    直接上代码: function Ajax(method, headers, url, data, progress = null) { return new Promise(function (res ...

  8. 【模板】prufer序列

    如何构造一个prufer序列? 我们给一棵无根树的节点编上号,每次找到一个编号最小的度为1节点,删除它,并输出与它连接的点的编号,直到只剩下两个节点. 这样,我们就构造出来了一个prufer序列. 通 ...

  9. CSPS模拟 85

    WWB大佬的bitset映射真是太强了! %%% T1 观察样例,猜规律. T2 对题目的翻译工作用了很长时间 翻译错了好几次.. 观察到奇环没法染色,选的边必须把奇环弄断 如果在偶环上,偶环就变得没 ...

  10. spring boot打包成war包的页面该放到哪里?

    背景 经常有朋友问我,平时都是使用spring mvc,打包成war包发布到tomcat上,如何快速到切换到spring boot的war或者jar包上? 先来看看传统的war包样式是什么样子的? 1 ...