引入

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

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

最小堆

举例

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

特性

  • 是一棵完全二叉树

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

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

  • 局部有序

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

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

存储结构

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

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

数据结构

// 本例为最小堆// 最大堆只需要修改less函数即可type Heap []intfunc (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 []intfor len(h) != 0 {  res = append(res, h.Pop())}fmt.Println(res)

优先队列

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

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

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

参考资料

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

最后

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

golang 排序_堆 堆排序 优先队列 图文详解(Golang实现)相关推荐

  1. java虚拟机工作原理图_超“强”的图文详解-JVM虚拟机底层原理与调优实战

    今天我和大家分享一篇文章,文章上半部分为JVM底层原理 下半部分为调优实战 文章有点长,需要点耐心哦! 如果觉得看文章太难理解,就点击下面我投稿B站的jvm视频讲解. 还配有视频讲解:解密BATJ一线 ...

  2. 微信抢红包的方案_微信抢红包方法图文详解

    微信抢红包不知道大家是否都熟悉,照顾点新人,很多朋友只是听说微信红包却不知道怎么抢?微信抢红包一夜爆红,受到众多朋友的喜爱,功能上可以实现发红包.查收发记录和提现,一起看看微信抢红包技巧详解吧! 一. ...

  3. adprw指令教程_三菱adprw指令图文详解

    技术文档 主体内容:可以认为是页面最想表达的内容总和.对于内容详情页来说,主体内容指从标题开始至正文内容结束,翻页区域也被视为主体内容,文章后的评论.分享.推荐等不视为主体内容. 首屏:用户点击搜索结 ...

  4. 目录树 删除 数据结构_数据结构:B树和B+树的插入、删除图文详解

    B树 1.1B树的定义 B树也称B-树,它是一颗多路平衡查找树.我们描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数.当m取2时,就是我们常见的二叉搜索树. ...

  5. 广联达2018模板算量步骤_老师傅带你学造价,广联达GTJ2018图文详解,小白也能学会的软件...

    在GTJ2018问世之前,土建造价人员有三个软件是必会的,一个是GGJ主打钢筋算量,一个是GCL主打土建算量,还有一个是GBQ主要是套定额用来计价的软件. 那时候如果计算一个工程的工程量,首先要用GG ...

  6. 电脑连接电视方法详解_查看电脑配置的几种方法(图文详解)

    很多朋友想要了解自己电脑详细的配置的时候,一般都是通过第三方的工具检测的.那么有没有其他更好的方法可以在win系统下查看电脑配置呢?今天我就给大家分享一下如何查看电脑配置. 查看电脑配置的几种方法图文 ...

  7. tracepro应用实例详解_建筑安装工程造价,高清PPT图文详解,小白也能学会的简单步骤...

    建筑安装工程造价,高清PPT图文详解,小白也能学会的简单流程 工程造价的直意就是工程的建造价格,是指进行某项工程建设所花费的全部费用.工程造价在工程中是很关键的存在,是工程能够取得的关键:对工程建设的 ...

  8. 前端中全部盒子靠左对齐_图文详解ul中li内容垂直居中和水平居中的方法

    在页面布局时,经常会用到li标签,它可用于列表,导航,选项卡等等,那你知道如何让ul中的li居中吗?这篇文章就和大家讲讲如何让ul中的li水平居中以及如何让li内容垂直居中.感兴趣的朋友继续往下看吧. ...

  9. html js不触发_图文详解鼠标事件CSS:hover和JS:mouseover的区别

    在工作中为了使页面更具有吸引力,前端开发人员经常会在页面中加上鼠标移入和移出的效果.鼠标移入移出的设置,一般有两种方法,一种是单纯用CSS中的hover伪类,另一种可以用JS 中的DOM事件,即onm ...

最新文章

  1. 交换机***工具——Yersinia
  2. iOS开发指南 从Hello World到App Store上架 第5版
  3. OSPF 报文 链路状态请求报文 LSR
  4. java.lang.IllegalStateException: 启动子级时出错
  5. 英雄联盟手游:大神开发提莫打野,伤害爆表,玩家纷纷效仿
  6. VC++实现获取DNS服务器
  7. Windows Server 笔记之网络负载平衡(NLB)和服务质量(QoS)简介
  8. Sklearn参数详解--决策树
  9. python学习笔记之装饰器、递归、算法(第四天)
  10. 基于sklearn的LogisticRegression鸢尾花多类分类实践
  11. android button imagebutton 区别,android - 可点击的ImageView和ImageButton之间的区别
  12. 3Dslicer中 PET/CT 模块:PET Standard Uptake Value Computation
  13. 单生产者/单消费者 的 FIFO 无锁队列
  14. Android音频系统之AudioFlinger(一)
  15. 淘宝京东查看价格历史的chrome插件
  16. 智能供应链预测的应用
  17. 你真的了解行盒模型吗?
  18. 手机号电子邮箱怎么填写?
  19. linux系统scsi硬盘,Linux系统中SCSI硬盘的热拔插
  20. 怎么退出自适应巡航_定速巡航与自适应定速巡航到底有什么不同?

热门文章

  1. 美国顶尖大学的博士是怎样练成的?
  2. 科技部部长:不唯论文,不是讲不要论文
  3. 【福利】周志华教授专著《集成学习:基础与算法》上市,豆瓣满分森林书破解AI实践难题...
  4. 颠覆认知!25张动图,让你重新认识地球
  5. springboot 项目实战 基本框架搭建(IDEA)
  6. android监听方法的耗时时间,Android从网络获取北京时间以及动态的监听时间
  7. lex和yacc环境配置
  8. 数据结构实验之栈六:下一较大值(二)
  9. 图像格式jpg、jpeg、jpe、gif、png、png等有何不同?ps中那种图片格式可以保留图层?
  10. GCC编译器和GDB调试器常用选项