大家好,我是蓝胖子,我一直相信编程是一门实践性的技术,其中算法也不例外,初学者可能往往对它可望而不可及,觉得很难,学了又忘,忘其实是由于没有真正搞懂算法的应用场景,所以我准备出一个系列,囊括我们在日常开发中常用的算法,并结合实际的应用场景,真正的感受算法的魅力。

今天我们就来看看堆这种数据结构。

源码已经上传到github

  1. https://github.com/HobbyBear/codelearning/tree/master/heap

原理

在详细介绍堆之前,先来看一种场景,很多时候我们并不需要对所有元素进行排序,而只需要取其中前topN的元素,这样的情况如果按性能较好的排序算法,比如归并或者快排需要n*log( n)的时间复杂度,n为数据总量,排好序后取出前N条数据,而如果用堆这种数据结构则可以在n*log(N)的时间复杂度内找到这N条数据,N的数据量远远小于数据总量n。

接着我们来看看堆的定义和性质,堆是一种树状结构,且分为最小堆和最大堆,最大堆的性质有父节点大于左右子节点,最小堆的性质则是父节点小于左右子节点。如下图所示:

并且堆是一颗完全二叉树,完全二叉树的定义如下:

  1. 若设二叉树的深度为h,除第 h 层外,其它各层 (1h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

因为结点都集中在左侧,所以我们可以从上到下,从左到右对堆中节点进行标号,如下图所示:

从0开始对堆中节点进行标号后,可以得到以下规律:

  1. 父节点标号 = (子节点标号-1)/2
  2. 左节点标号 = 父节点标号 *2 + 1
  3. 右节点标号 = 父节点标号 *2 + 2

有了标号和父子节点的标号间的关系,我们可以用一个数组来保存堆这种数据结构,下面以构建一个最大堆为例,介绍两种构建堆的方式。

HeapInsert

heapInsert的方式是从零开始,逐个往堆中插入数组中的元素,并不断调整新的节点,让新节点的父节点满足最大堆父节点大于其子节点的性质,这个调整的过程被称作ShiftUp。当数组中元素全部插入完成时,就构建了一个最大堆。代码如下:

  1. func HeapInsert(arr []int) *Heap {
  2. h := &Heap{arr: make([]int, 0, len(arr))}
  3. for _, num := range arr {
  4. h.Insert(num)
  5. }
  6. return h
  7. }

Heapify

heapify的方式是假设数组已经是一个完全二叉树了,然后找到树中的最后一个非叶子节点,然后通过比较它与其子节点的大小关系,让其满足最大堆的父节点大于其子节点的性质,这样的操作被称作ShifDown,对每个非叶子节点都执行ShifDown操作,直至根节点,这样就达到了将一个普通数组变成一个堆的目的。

如果堆的长度是n,那么最后一个非叶子节点是 n/2 -1 ,所以可以写出如下逻辑,

  1. func Heapify(arr []int) *Heap {
  2. h := &Heap{arr: arr}
  3. lastNotLeaf := len(arr)/ 2 -1
  4. for i:= lastNotLeaf;i >= 0; i-- {
  5. h.ShiftDown(i)
  6. }
  7. return h
  8. }

取出根节点

取出根节点的逻辑比较容易,将根节点结果保存,之后让它与堆中最后一个节点交换位置,然后从索引0开始进行ShiftDown操作,就又能让整个数组变成一个堆了。

  1. func (h *Heap) Pop() int {
  2. num := h.arr[0]
  3. swap(h.arr, 0, len(h.arr)-1)
  4. h.arr = h.arr[:len(h.arr)-1]
  5. h.ShiftDown(0)
  6. return num
  7. }

ShiftUp,ShiftDown实现

下面我将shiftUp和shiftDown的源码展示出来,它们都是一个递归操作,因为在每次shiftUp或者shiftDown成功后,其父节点或者子节点还要继续执行shifUp或shiftDown操作。

  1. // 从标号为index的节点开始做shifUp操作
  2. func (h *Heap) ShiftUp(index int) {
  3. if index == 0 {
  4. return
  5. }
  6. parent := (index - 1) / 2
  7. if h.arr[parent] < h.arr[index] {
  8. swap(h.arr, parent, index)
  9. h.ShiftUp(parent)
  10. }
  11. }
  12. // 从标号为index的节点开始做shifDown操作
  13. func (h *Heap) ShiftDown(index int) {
  14. left := index*2 + 1
  15. right := index*2 + 2
  16. if left < len(h.arr) && right < len(h.arr) {
  17. if h.arr[left] >= h.arr[right] && h.arr[left] > h.arr[index] {
  18. swap(h.arr, left, index)
  19. h.ShiftDown(left)
  20. }
  21. if h.arr[right] > h.arr[left] && h.arr[right] > h.arr[index] {
  22. swap(h.arr, right, index)
  23. h.ShiftDown(right)
  24. }
  25. }
  26. if left >= len(h.arr) {
  27. return
  28. }
  29. if right >= len(h.arr) {
  30. if h.arr[left] > h.arr[index] {
  31. swap(h.arr, left, index)
  32. h.ShiftDown(left)
  33. }
  34. }
  35. }

堆的原理以及实现O(lgn)的更多相关文章

  1. fibonacci-Heap(斐波那契堆)原理及C++代码实现

    斐波那契堆是一种高级的堆结构,建议与二项堆一起食用效果更佳. 斐波那契堆是一个摊还性质的数据结构,很多堆操作在斐波那契堆上的摊还时间都很低,达到了θ(1)的程度,取最小值和删除操作的时间复杂度是O(l ...

  2. Linux 堆溢出原理分析

    堆溢出与堆的内存布局有关,要搞明白堆溢出,首先要清楚的是malloc()分配的堆内存布局是什么样子,free()操作后又变成什么样子. 解决第一个问题:通过malloc()分配的堆内存,如何布局? 上 ...

  3. 漫谈 C++ 的 内存堆 实现原理

    如果我来设计 C++ 的 内存堆 , 我会这样设计 : 进程 首先会跟 操作系统 要 一块大内存区域 , 我称之为 Division , 简称 div . 然后 , 将这块 div 作为 堆 , 就可 ...

  4. poppo大根堆的原理与实现。

    大根堆的定义:1 大根堆是一个大根树 2 大根堆是一个完全二叉树 所以大根堆用数组表示是连续的,不会出现空白字段. 对于大根堆的插入 对于大根堆的插入,可以在排序前确定大根堆的形状,可以确定元素5从位 ...

  5. binary-heap(二叉堆)原理及C++代码实现

    二叉堆可以看做一个近似的完全二叉树,所以一般用数组来组织. 二叉堆可以分为两种形式:最大堆和最小堆.最大堆顾名思义,它的每个结点的值不能超过其父结点的值,因此堆中最大元素存放在根结点中.最小堆的组织方 ...

  6. 利用DWORD SHOOT实现堆溢出的利用(先知收录)

    原文链接:https://xz.aliyun.com/t/4009 1.0 DWORD SHOOT是什么捏? DWORD SHOOT指能够向内存任意位置写入任意数据,1个WORD=4个bytes,即可 ...

  7. 浅析PriorityBlockingQueue优先级队列原理

    介绍 当你看本文时,需要具备以下知识点 二叉树.完全二叉树.二叉堆.二叉树的表示方法 如果上述内容不懂也没关系可以先看概念. PriorityBlockingQueue是一个无界的基于数组的优先级阻塞 ...

  8. 【CTF】日志 2019.7.13 pwn 堆溢出基础知识

    十六进制两位表示一个字节 堆溢出 先上堆图: 堆的数据结构 一般情况下,物理相邻的两个空闲 chunk 会被合并为一个 chunk struct malloc_chunk { INTERNAL_SIZ ...

  9. 八大排序算法Java实现

    本文对常见的排序算法进行了总结. 常见排序算法如下: 直接插入排序 希尔排序 简单选择排序 堆排序 冒泡排序 快速排序 归并排序 基数排序 它们都属于内部排序,也就是只考虑数据量较小仅需要使用内存的排 ...

  10. 八大排序算法总结与java实现(转)

    八大排序算法总结与Java实现 原文链接: 八大排序算法总结与java实现 - iTimeTraveler 概述 直接插入排序 希尔排序 简单选择排序 堆排序 冒泡排序 快速排序 归并排序 基数排序 ...

随机推荐

  1. WPF入门教程系列二十八 ——DataGrid使用示例MVVM模式(5)

    WPF入门教程系列目录 WPF入门教程系列二--Application介绍 WPF入门教程系列三--Application介绍(续) WPF入门教程系列四--Dispatcher介绍 WPF入门教程系 ...

  2. 闺蜜机 StanbyME 产品随想

    今天媳妇告诉我,现在小度这边推出一款叫 "闺蜜机"的可用移动的IPAD设备,我点开链接一看,就感觉兴趣不大,不就是一款把屏幕做的更大些的IPAD了吗? 有哪些更多创新呢?为什么会需 ...

  3. 尚医通-day14【创建订单】(内附源码)

    页面预览 订单详情 订单列表 第01章-创建订单 生成订单分析 生成订单方法参数:就诊人id与 排班id 生成订单需要获取就诊人信息(微服务远程调用service-user) 获取排班信息与规则信息( ...

  4. Java判断一个数是不是质数

    判断一个数是不是质数 做这个题之前我们需要先进行了解什么是质数 质数:只能被1和它本身整除的数 举一个简单的例子:数字5是不是质数呢? 我们可以进行分析: 解题思路:5可以分为1 2 3 4 5,我们 ...

  5. 前端:Uncaught TypeError: Cannot set property 'value' of null;

    1.在写JS代码中的绑定事件时(将JS与HTML标签分开时),发现会有如下错误提示: Uncaught TypeError: Cannot set property 'onclick' of null ...

  6. Python安装time库失败?不是吧阿sir你还不知道内置模块不用下载吧

    嗨嗨,今天给python安装time库,一直报错,换源等办法都试过了 直到我看到 Python中有以下常用模块不用单独安装 random模块 sys模块 time模块 os系统操作 re正则操作 js ...

  7. 防火墙(iptables与firewalld)

    防火墙 iptables 疏通和堵 进行路由选择前处理的数据包:prerouting 处理流入的数据包:input 处理流出的数据包:output 处理转发的数据包:forward 进行路由选择后处理 ...

  8. Redis设计

    目录 过期键删除策略 持久化 RDB AOF AOF重写 主从复制 完整重同步和部分重同步 哨兵Sentinel 哨兵对redis服务器集群的监听 执行者选举 故障转移 选择新的主服务器流程 过期键删 ...

  9. MyBatis 常用工具类

    SQL 类 MyBatis 提供了一个 SQL 工具类,使用这个工具类,我们可以很方便在 Java 代码动态构建 SQL 语句 String newSql = new SQL() ({ SELECT( ...

  10. 代码随想录算法训练营第四天|力扣24.两两交换链表节点、力扣19.删除链表的倒数第N个结点、力扣面试02.07链表相交、力扣142.环形链表

    两两交换链表中的节点(力扣24.) dummyhead .next = head; cur = dummyhead; while(cur.next!=null&&cur.next.ne ...