go语言源码解读之数据结构堆

概述

堆(heap),是一种计算中常用的数据结构。本文我们将探讨对的特性、实现细节以及实际应用场景。

基本概念

堆是一种特殊的完全二叉树。堆分为大顶堆与小顶堆。

  • 大顶堆的特点是,父节点的值总是大于或等于其子节点的值。

  • 小顶堆的特点是,父节点的值总是小于或等于其子节点的值。

小顶堆的首个元素是整个堆中的最小值,大顶堆的首个元素是整个堆的最大值,利用这种特性,能够快速的访问和提取一组数据中的最小或最大值的元素。

在这里插入图片描述

(图片来源于网络,如有侵权请联系纠正)

如果用数组方式实现堆,那么可以用固定规律就可以直接计算出当前节点的父节点的与子节点的索引:

  • 父节点的索引计算规则: (i - 1) / 2
  • 左子节点的索引的计算规则: i * 2 + 1
  • 右子节点的索引的计算规则: i * 2 + 2

a9e0564f6112f999862a46d06c459efa.png
(图片来源于网络,如有侵权请联系纠正)

补充:完全二叉树的定义

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

同时注意区分满二叉树与完全二叉树

如果一个二叉树的任何节点,要么是叶子节点,要么左右子树均非空,则这棵二叉树称做满二叉树(full binary tree)

在这里插入图片描述
(图片来源于网络,如有侵权请联系纠正)

堆的操作

push

push即入堆操作

  1. 先将元素添加到堆的末尾
  2. 将元素与父节点比较
  3. 假设是小顶堆,如果元素比父节点小,则元素与父节点交换;假设是大顶堆,如果元素比父节点大,则元素与父节点交换
  4. 重复此过程,直到元素达到根节点或元素满足对属性

pop

pod即出堆操作

  1. 先将根节点与堆的最后一个元素交换
  2. 再删除最后一个元素
  3. 对新的根节点执行"向下堆化",维持整个堆的特性。

go语言中的实现

堆可以用数组、链表、二叉搜索树、平衡二叉搜索树等数据结构来实现。但通常选用数组来实现堆,因为可以通过代数方式直接计算出当前节点的父节点、左孩子和右孩子的位置,而不是通过遍历查找父节点与孩子节点的位置。因此它在时间和空间上都是很高效的。

Go语言中堆的实现,可以自己直接实现,也可以利用container/heap包实现。container/heap包提供了堆操作的接口和方法

接下来对container/heap包的源码分析

heap.Interface接口的定义

type Interface interface {sort.InterfacePush(x interface{}) // add x as element Len()Pop() interface{}   // remove and return element Len() - 1.
}

它匿名嵌套了 sort.Interface 接口,sort.Interface的定义如下:

type Interface interface {Len() intLess(i, j int) boolSwap(i, j int)
}

这里继承 sort.Interface 接口,是因为在进行heapify(翻译为"堆化")时,需要使用Less()方法进行节点值的比较,使用Swap()方法进行节点位置之间的交换.

备注:下面的说明默认按小顶堆来说明

// 初始化的初始化
func Init(h Interface) {// heapify(堆化)n := h.Len()// 这里i设置为n/2-1,是因为i的右孩子节点是i*2+2 = (n/2-1)*2+2 = nfor i := n/2 - 1; i >= 0; i-- {down(h, i, n)}
}// Push pushes the element x onto the heap.
// The complexity is O(log n) where n = h.Len().
func Push(h Interface, x interface{}) {// 将x添加到堆中末尾。h.Push(x)// 再对堆末尾的元素,进行"向上堆化",也就是使新元素必须小于父节点,否则将新元素与父节点交换,一直持续到新元素成为根节点。up(h, h.Len()-1)
}// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to Remove(h, 0).
func Pop(h Interface) interface{} {// n是堆最后一个元素的的索引n := h.Len() - 1// 将堆顶与最后一个元素进行交换h.Swap(0, n)// 对堆顶元素(索引为0),进行"向下堆化"down(h, 0, n)return h.Pop()
}// Remove removes and returns the element at index i from the heap.
// The complexity is O(log n) where n = h.Len().
func Remove(h Interface, i int) interface{} {n := h.Len() - 1if n != i {h.Swap(i, n)if !down(h, i, n) {up(h, i)}}return h.Pop()
}// Fix re-establishes the heap ordering after the element at index i has changed its value.
// Changing the value of the element at index i and then calling Fix is equivalent to,
// but less expensive than, calling Remove(h, i) followed by a Push of the new value.
// The complexity is O(log n) where n = h.Len().
// Fix 的作用是当索引为i的元素的值改变后,需要对i对应的元素,进行“堆化”
func Fix(h Interface, i int) {// 先"向下堆化",再"向上堆化"if !down(h, i, h.Len()) {up(h, i)}
}

Init(h Interface): 对一个未排序的切片构建堆。这是通过down方法实现的,down方法确保元素下沉到正确的位置,维持堆的性质。for循环的第一个索引i设置为n/2-1,是因为i的右孩子节点是i*2+2 = (n/2-1)*2+2 = n,即数组最后一个元素索引,这里需要注意避免数组索引越界

Push(h Interface, x interface{}):元素被添加到切片的末尾,然后通过up方法上浮到正确的位置。

Pop(h Interface) interface{}:堆顶元素(切片的第一个元素)被移动到切片末尾并返回,然后新的堆顶元素通过down方法恢复堆的性质。

Remove(h Interface, i int) interface{}:类似Pop,但可以移除指定位置的元素。此操作需要综合updown方法来调整堆。

Fix(h Interface, i int): 如果堆中某个元素被外部修改了(比如优先级改变),Fix方法会根据这个修改后的新值重新调整堆。

up()方法是heap接口重要的方法,作用是将j索引对应元素,与父节点比较,如果比父节点大,就与父节点交换,重复这个过程一直持续到j对应元素成为根节点或者j对应元素大于父节点。

func up(h Interface, j int) {for {// 通过当前节点j,用代数方式计算出父节点i := (j - 1) / 2 // parent// 如果父节点的索引与当前节点j相等就退出// 或者如果当前节点已经大于登录了父节点,则退出。说明j已经在其应该在的位置了if i == j || !h.Less(j, i) {break}// 否则,此时父节点i比当前节点j小,那么就将父节点与当前节点交互h.Swap(i, j)// 交换后,j的索引指向父节点,等待下一次循环j = i}
}

down()方法是heap接口重要的方法,作用是将i当前节点与孩子比较,如果孩子节点当前节点小,就将孩子节点与当前节点互换。重复这个过程一直到其孩子节点大于当前节点。

func down(h Interface, i0, n int) bool {i := i0for {// j1是左孩子j1 := 2*i + 1// 判断j1左孩子是否从数组溢出if j1 >= n || j1 < 0 { // j1 < 0 after int overflowbreak}j := j1 // left child// j2是i当前节点的右孩子// 如果j2右孩子小于j1左孩子,j就指向右孩子。(这一步的意思是让j指向左右孩子中最小的那个元素)if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {j = j2 // = 2*i + 2  // right child}// 如果j左右孩子(可能是左孩子也可能右孩子),比i当前节点大,说明i已经满足堆属性了,则退出循环if !h.Less(j, i) {break}// 此时孩子节点小于当前节点,就将当前节点与孩子节点交换h.Swap(i, j)// 交换完后,当前索引指向孩子节点i = j}return i > i0
}
  • 时间复杂度

Push/pop: O(LogN)

堆的实际应用场景

  • 优先级队列: 例如任务调度器,即按任务优先级大小进行调度的任务调度器。告警系统中,按消息重要性优先发送重要信息等。
  • 延时队列: 延迟队列是利用优先级队列,根据元素中的时间戳的先后顺序进行弹出。client-go中的workqueue就是一种延时队列,后面会写篇文章专门分析下workqueue的延时队列的源码,探讨其实现机制
  • 堆排序: 利用堆排序,时间复杂度是O(NlogN)
  • 网络路由;最短路径算法与最小生成树

利用堆实现优先级队列

优先级队列与堆的关系:

  • 优先级队列是一种抽象数据类型,它可以看作是一个队列,但其中的元素是有优先级的。优先级队列的操作通常包括插入元素(通常称为入队),删除具有最高优先级的元素(通常称为出队),以及查看最高优先级的元素(通常称为获取队头元素)。

  • 堆是一种具体数据结构。堆的操作通常包括插入元素(通常称为插入),删除具有最高优先级的元素(通常称为删除),以及查看最高优先级的元素(通常称为获取堆顶元素)。

在实际应用中,优先级队列和堆常常被一起使用。例如,操作系统中的任务调度器通常使用优先级队列来管理一组任务,其中的每个任务都有一个优先级。堆可以用于实现优先级队列,例如,我们可以使用最大堆来实现最大优先级队列,使用最小堆来实现最小优先级队列。
总的来说,优先级队列和堆都是处理一组元素的工具,但它们的用途和实现方式有所不同。优先级队列是一种抽象数据类型,而堆是一种数据结构,可以使用堆来实现优先级队列。

示例: 使用go语言的heap.Interface实现一个优先级队里

package mainimport ("container/heap""fmt"
)type Item struct {priority    intname string
}// 优先级队列底层用数组实现
type PriorityQueue []*Item// 先实现heap.Interfach的五个接口Len(),Less(),Swap(),Push(),Pop()
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {// 优先级值(priority)越大越优先(大顶堆), 也就是完全二叉树中,上层的节点的优先级大于下层节点的优先级值return pq[i].priority > pq[j].priority
}
func (pq PriorityQueue) Swap(i, j int) {pq[i], pq[j] = pq[j], pq[i]
}func (pq *PriorityQueue) Push(x interface{}) {Item := x.(*Item)*pq = append(*pq, Item)
}func (pq *PriorityQueue) Pop() interface{} {old := *pqn := len(old)Item := old[n-1]*pq = old[0 : n-1]return Item
}func main() {pq := make(PriorityQueue, 0)heap.Init(&pq)// 插入元素Items := []Item{{1, "low"},{3, "high"},{2, "medium"},}for i := range Items {heap.Push(&pq, &Items[i])}// 按优先级从大到小的弹出元素for pq.Len() > 0 {Item := heap.Pop(&pq).(*Item)fmt.Printf("Item: %s\n", Item.name)}
}-----------输出----------
Item: 高优先级
Item: 中优先级
Item: 低优先级

堆排序

为了简化堆排序的实现,这里借用go标准包container/heap.go中的down函数,能快速实现一个数组的堆化。

heapSort()函数的逻辑是,先将arr建堆,再从arr堆的堆顶依次弹出堆顶元素,并将元素放入res结果数组,完成循环之后res数组就是排序后的数组

package mainimport "fmt"// down 是go标准包container/heap.go中的函数,实现一个数组中某个元素的heapify
func down(h []int, i0, n int) bool {i := i0for {// j1是左孩子j1 := 2*i + 1// 判断j1左孩子是否从数组溢出if j1 >= n || j1 < 0 { // j1 < 0 after int overflowbreak}j := j1 // left child// j2是i当前节点的右孩子// 如果j2右孩子小于j1左孩子,j就指向右孩子。(这一步的意思是让j指向左右孩子中最小的那个元素)if j2 := j1 + 1; j2 < n && (h[j2] < h[j1]) {j = j2 // = 2*i + 2  // right child}// 如果j左右孩子(可能是左孩子也可能右孩子),比i当前节点大,说明i已经满足堆属性了,则退出循环if h[j] > h[i] {break}// 此时孩子节点小于当前节点,就将当前节点与孩子节点交换h[i], h[j] = h[j], h[i]// 交换完后,当前索引指向孩子节点i = j}return i > i0
}// heapsort 实现对一个数组的堆排序
func heapSort(arr []int) []int {// 构建堆:将数组arr转为一个堆for i := len(arr)/2 - 1; i >= 0; i-- {down(arr, i, len(arr))}// res 用于存放从堆弹出元素并放入结果res := []int{}// 弹出堆的第一个元素,放入res// 然后将最后一个元素放到第一个位置,并重新构建堆for len(arr) > 0 {item := arr[0]res = append(res, item)arr[0] = arr[len(arr)-1]arr = arr[:len(arr)-1]down(arr, 0, len(arr))}return res}func main() {arr := []int{3, 5, 8, 2, 1}arr = heapSort(arr)fmt.Println("after sort:", arr) // after sort: [1 2 3 5 8]arr = []int{3, 3, 3, 3, 3}arr = heapSort(arr)fmt.Println("after sort:", arr) // after sort: [3 3 3 3 3]arr = []int{5, 4, 3, 2, 1}arr = heapSort(arr)fmt.Println("after sort:", arr) // after sort: [1 2 3 4 5]
}

除了自己手写一个堆排序算法之外,可以直接使用go标准库的sort/sort.go包heapSort(),可以快速实现堆排序。算法逻辑实现这里就不再赘述。


// siftDown implements the heap property on data[lo:hi].
// first is an offset into the array where the root of the heap lies.
func siftDown(data Interface, lo, hi, first int) {root := lofor {child := 2*root + 1if child >= hi {break}if child+1 < hi && data.Less(first+child, first+child+1) {child++}if !data.Less(first+root, first+child) {return}data.Swap(first+root, first+child)root = child}
}func heapSort(data Interface, a, b int) {first := alo := 0hi := b - a// Build heap with greatest element at top.for i := (hi - 1) / 2; i >= 0; i-- {siftDown(data, i, hi, first)}// Pop elements, largest first, into end of data.for i := hi - 1; i >= 0; i-- {data.Swap(first, first+i)siftDown(data, lo, i, first)}
}

结语

通过上述分析,我们分析了heap.go源码的实现,进而基于堆我们实现了一个任务优先级队列,我们可以看到Go语言中堆的实现既简洁又高效。从实际工作应用来看,用堆实现的优先级队列使用场景更广泛,需要重点理解和掌握

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/401428.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DVWA-IDS测试(特殊版本)

起因 浏览DVWA历史更新记录发现有版本带有IDS插件&#xff0c;可以用于平时没有相关设备等场景演示用&#xff0c;所以开启本次测试。 下载 官方最新版本是移除了IDS插件&#xff0c;原因是“从不使用”&#xff0c;所以需要下载移除该插件之前的版本。 https://github.com/…

Excel中使用SUMIF函数对指定区域满足条件的进行求和

1.使用 SUMIF 函数对 范围 中符合指定条件的值求和。 例如&#xff0c;如果某列中含有数字&#xff0c;你只需对大于 5 的数值求和。 可使用以下公式&#xff1a;SUMIF(B2:B25,">5") 2.将条件应用于一个区域并对其他区域中的对应值求和。 例如&#xff0c;公式 S…

时钟缓冲器的相关知识

时钟缓冲器是比较常用的器件&#xff0c;其主要功能作用有时钟信号复制&#xff0c;时钟信号格式转换&#xff0c;时钟信号电平转换等。我们下面简单了解下&#xff1a; 1.时钟信号复制 例如ICS553芯片&#xff0c;其将单路输入时钟信号复制4份进行输出&#xff0c;输出信号具…

debian 常用命令

1、修改环境变量 /etc/profile export PATH/usr/local/bin:$PATHsource /etc/profile ## 生效临时改变export PATH/usr/local/bin:$PATH或者改变当前用户的vim ~/.bashrcsource ~/.bashrc // 生效 2、清除当前登录的历史操作 history -c 3、解压缩 压缩基本的命令格式 …

SD卡电路设计基础

一、定义 SD卡按尺寸分类可分为三类:标准 SD 卡、Mini SD 卡和 Micro SD 卡。其中Mini SD 卡比较少见&#xff0c;标准 SD 卡因为体积较大主要用在数码相机等对体积要求不严格的地方,我们最常用的是 Micro SD 卡&#xff0c;原名Trans-flash Card (TF 卡)。 Micro SD 作用:一…

天书般的Tree工具类

3.1 JAVA中树形对象的定义 在JAVA中树形结构是通过对象嵌套方式来定义的&#xff0c;如MenuVo对象中有一个子对象subMenus&#xff1a; Data public class MenuVo {private Long id;private Long pId;private String name;private Integer rank0;private List<MenuVo> s…

OpenHarmony UI动画-recyclerview_animators

简介 带有添加删除动画效果以及整体动画效果的list组件库 下载安装 ohpm install ohos/recyclerview-animatorsOpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考如何安装OpenHarmony ohpm 包 使用说明 引入组件库 import { RecyclerView } from "ohos/recycler…

【图论】并查集(Union-find Sets)

文章目录 前言一、并查集(Union-find Sets)基本概念基本操作步骤 二、并查集的操作步骤1. 初始化 init2. 查询 find、合并 union&#xff08;未进行路径压缩&#xff09;3. 查询 find、合并 union&#xff08;路径压缩&#xff09; 三、Kruskal 算法中 环 的判断并查集的使用 总…

宝塔面板屏蔽 Censys,防止源站 IP 泄露

Censys 搜索引擎很强大。Censys 每天都会扫描 IPv4 地址空间&#xff0c;以搜索所有联网设备并收集相关的信息&#xff0c;并返回一份有关资源&#xff08;如设备、网站和证书&#xff09;配置和部署信息的总体报告。 在 IP 前加上 https 访问时&#xff0c;Nginx 会自动返回该…

Redis缓存——缓存更新策略和常见的缓存问题

一.什么是缓存&#xff1f; 前言&#xff1a;什么是缓存? 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码 前言&#xff1a;为什么要使用缓存&#xff1f; 一句话:因为速度快,好用 缓存数据存储于代码中,而代码运行在内存…

Unity新输入系统 之 InputAction(输入配置文件最基本的单位)

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正​ 首先你应该了解新输入系统的构成结构&#xff1a;Unity新输入系统结构概览-CSDN博客 Input System - Unity 手册 1.In…

【SpringCloud】RabbitMQ——五种方式实现发送和接收消息

SpringAMQP SpringAMQP是基于RabbitMQ封装的一套模板&#xff0c;并且还利用SpringBoot对其实现了自动装配。 SpringAmqp的官方地址&#xff1a;https://spring.io/projects/spring-amqp SpringAMQP提供了三个功能&#xff1a; 自动声明队列、交换机及其绑定关系基于注解的…

【CONDA】库冲突解决办法

如今&#xff0c;使用PYTHON作为开发语言时&#xff0c;或多或少都会使用到conda。安装Annaconda时一般都会选择在启动终端时进入conda的base环境。该操作&#xff0c;实际上是在~/.bashrc中添加如下脚本&#xff1a; # >>> conda initialize >>> # !! Cont…

Java基础之循环嵌套

循环嵌套 在一个循环内部可以嵌套另一个或多个循环。 外部循环每执行1次&#xff0c;内层循环会执行1轮(全部)。 案例1&#xff1a; 连续3天&#xff0c;每天都要表白5次。 package com.briup.chap03;public class Test03_Nest {public static void main(String[] args) {…

XMGoat:一款针对Azure的环境安全检测工具

关于XMGoat XMGoat是一款针对Azure的环境安全检测工具&#xff0c;XM Goat 由 XM Cyber Terraform 模板组成&#xff0c;可帮助您了解常见的 Azure 安全问题。每个模板都是一个用于安全技术学习的靶机环境&#xff0c;包含了一些严重的配置错误。 在该工具的帮助下&#xff0c…

File的概述和构造方法

一.路径&#xff1a; 相对路径开头不带盘符。 二.File&#xff1a; 1.File对象&#xff1a; File对象就表示一个路径&#xff0c;可以是文件的路径&#xff0c;也可以是文件夹的路径&#xff0c; 这个路径可以是存在的&#xff0c;也可以是不存在的。 2.File对象常见的构造…

SpringCloud完整教程

一下内容为本人在听黑马程序员的课程时整理的 微服务技术栈 ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ 1、微服务框架 1.1、认识微服务 1.1.1、服务架构演变 **单体架构&#xff1a;**将业务的所有功能集中在一个项目中开发&#xff0c;打包成…

华为云Api调用怎么生成Authorization鉴权信息,StringToSign拼接流程

请求示例 Authorization 为了安全&#xff0c;华为云的 Api 调用都是需要在请求的 Header 中携带 Authorization 鉴权的&#xff0c;这个鉴权15分钟内有效&#xff0c;超过15分钟就不能用了&#xff0c;而且是需要调用方自己手动拼接的。 Authorization的格式为 OBS 用户AK:…

Linux系统移植——开发板烧写

目录&#xff1a; 目录&#xff1a; 一、什么是EMMC分区&#xff1f; 1.1 eMMC分区 1.2 分区的管理 二、相关命令介绍&#xff1a; 2.1 mmc 2.1.1 主要功能 2.1.2 示例用法 2.2 fdisk 2.2.1 基本功能 2.2.2 交互模式常用命令 2.2.3 注意事项 三、U-BOOT烧写 3.1 mmc命令 3.2 f…

【Linux入门】Linux环境搭建

目录 前言 一、发行版本 二、搭建Linux环境 1.Linux环境搭建方式 2.虚拟机安装Ubuntu 22.02.4 1&#xff09;安装VMWare 2&#xff09;下载镜像源 3&#xff09;添加虚拟机 4&#xff09;换源 5&#xff09;安装VM Tools 6)添加快照 总结 前言 Linux是一款自由和开放…