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

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

源码已经上传到github

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

原理

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

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

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

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

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

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

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

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

HeapInsert

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

func HeapInsert(arr []int) *Heap {
h := &Heap{arr: make([]int, 0, len(arr))}
for _, num := range arr {
h.Insert(num)
}
return h
}

Heapify

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

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

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

取出根节点

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

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

ShiftUp,ShiftDown实现

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

// 从标号为index的节点开始做shifUp操作
func (h *Heap) ShiftUp(index int) {
if index == 0 {
return
}
parent := (index - 1) / 2
if h.arr[parent] < h.arr[index] {
swap(h.arr, parent, index)
h.ShiftUp(parent)
}
} // 从标号为index的节点开始做shifDown操作
func (h *Heap) ShiftDown(index int) {
left := index*2 + 1
right := index*2 + 2
if left < len(h.arr) && right < len(h.arr) {
if h.arr[left] >= h.arr[right] && h.arr[left] > h.arr[index] {
swap(h.arr, left, index)
h.ShiftDown(left)
}
if h.arr[right] > h.arr[left] && h.arr[right] > h.arr[index] {
swap(h.arr, right, index)
h.ShiftDown(right)
}
}
if left >= len(h.arr) {
return
}
if right >= len(h.arr) {
if h.arr[left] > h.arr[index] {
swap(h.arr, left, index)
h.ShiftDown(left)
}
}
}

堆的原理以及实现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. 现代C++学习指南-标准库

    在[上一章](https://www.yuque.com/docs/share/adb5b1e4-f3c6-46fd-ba4b-4dabce9b4f2a?# <现代C++学习指南-类型系统> ...

  2. 在线免费ChatGPT,官方api

    作为一款强大的语言模型,ChatGPT在自然语言处理领域享有较高声誉.现在,您可以在我们的在线平台上免费体验ChatGPT的功能了! 经过不断地优化和改进,我们的在线聊天机器人已经能够针对各种话题展示 ...

  3. Blazor前后端框架Known-V1.2.2

    V1.2.2 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行. 概述 基于C#和Blazor实现的快速开发框架,前后端分离,开箱即用. 跨平台,单 ...

  4. CentOS 7相关操作

    防火墙操作 开启防火墙 sudo systemctl start firewalld.service 查看防火墙状态 sudo systemctl status firewalld.service 关 ...

  5. 河南省CCPC大学生程序设计竞赛赛后总结yy

    这次的ccpc总体来说,取得的成绩并不理想,首先是题目解决的数量较少,其次是罚时太多了.开始也是找到了签到题,按理说应该不难拿下,虽然大家解决这道签到题都不是很快,但是我们小队在比赛已经过去两个小时左 ...

  6. 论文翻译: FREEVC:朝着高质量、无文本、单次转换声音的目标迈进

    原文:FREEVC: TOWARDS HIGH-QUALITY TEXT-FREE ONE-SHOT VOICE CONVERSION 原文地址:https://ieeexplore.ieee.org ...

  7. MAUI中Windows的标题栏颜色怎么设置

    如下图所示,MAUI中Windows下的标题栏是灰色的,如何设置颜色,找了很久,在GitHub上的issue中找到了答案, 找到/Platforms/Windows/App.xaml <maui ...

  8. Unity UGUI的RawImage(原始图片)组件的介绍及使用

    Unity UGUI的RawImage(原始图片)组件的介绍及使用 1. 什么是RawImage组件? RawImage是Unity UGUI中的一个组件,用于显示原始图片.与Image组件不同,Ra ...

  9. DNS与CDN技术

    参考链接: CDN原理简单介绍 浅析:DNS解析和CDN加速 DNS报文格式解析

  10. Unity的IFilterBuildAssemblies:深入解析与实用案例

    Unity IFilterBuildAssemblies Unity IFilterBuildAssemblies是Unity引擎中的一个非常有用的功能,它可以让开发者在构建项目时自定义哪些程序集需要 ...