You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.
到这里可以看到,只要删除节点,就先把这个节点与最后一个节点进行交换,然后在对这个节点进行调整。但是这里地方有点特殊,可能需要下沉操作后还需要上浮操作。因为删除的这个节点并不一定是顶部节点或者叶子节点。为什么需要有!down(h, i, n) 这个判断,因为如果一个节点进行了下沉操作,则说明该节点比子节点还要大,由于其他节点之前都是按照最小顶堆放置的,也就是说,如果该节点比子节点还要大,那么它就肯定比自己的父节点还要大。所以这里就做了优化,如果这个节点进行了下沉操作,则不需要进行上浮了,否则再进行上浮。其实判断是否有过上浮操作再判断是否需要下沉是一样的。
Heap
最近学习图的单源最短路径算法(我看的是Dijkstra算法)的时候,有看到讲解算法的博主在里面使用最小堆来记录每个顶点距离原点的最小距离,然后每次从最小堆顶中取出最小距离的点,再对邻接表进行循环。因为
Java
的提供优先队列的实现是没有更新的接口,所有博主自己写了一套,Go
语言的标准库里面也是有Heap
的实现的,提供了相应的方法。Go
语言的实现只是引子,重点还是思路。最小堆或者最大堆的核心操作
不管是插入、删除和更新操作,最重要的就是维持最小堆或者最大堆的特性(每个节点都比左右子节点小或者大)。所以先看看,当一个节点被操作后,违反了这个特性,那么就接下来的步骤就是从该节点开始调整整棵树了。比如有这样的一棵树:
假设图中被红框标注的那个节点刚被修改了,这个节点的值相比父节点较小,所以已经不满足了最小堆的定义了,就需要调整了。调整的分为两类:1、上浮; 2、下沉。像上面这种情况,就考虑上浮。
上浮的步骤
如下图,但把根节点删除的时候,就需要把最后一个节点替换到根节点,然后再对根节点进行下沉,以维持最小(大)堆的平衡。
下浮的步骤
Heap的源码
将指定类型初始化为最小(大)顶堆
Init
函数接收一个类型为Interface
的形参(h
),我们先看一下Interface
的结构它是由sort.interface
和Push、Pop
函数组成,也就是这个实现这个接口的类型需要有:Len()
、Less(i, j int) bool
、Swap(i, j int)
、Push(x interface{})
和Pop() interface{}
这几个方法。现在我们继续说
Init
函数,该函数很简单,就一个循环,但是起点比较奇怪,然后再遍历到的节点进行下沉(down函数)操作。首先看为什么是从n/2-1
这个节点开始。对于一个完全二叉树,如果节点个数为
n
那么它的所以叶子节点就是n/2
到n
包含两者,这些节点都是叶子节点。那么n/2-1
也就是从后往前第一个有子节点的节点。叶子节点没有子节点,不需要做下沉操作,所以循环的开始应该是从后往前第一个有子节点的节点开始进行下沉操作。添加元素
将元素添加到尾部,这里是调用的类型为
Interface
变量h
的Push
方法,也就是使用的时候需要手动去实现的,然后将最后这个元素(最后一个子节点)进行上浮操作。删除元素
这里将第一个元素与最后一个元素进行交换也就是把顶部的节点和最后一个子节点进行了交换,然后再对交换后的顶部节点进行下沉操作来维护最小顶堆的平衡。然后再把最后一个叶子节点删除并返回,也就是被交换下来的之前的顶部节点。
移除某一个节点
到这里可以看到,只要删除节点,就先把这个节点与最后一个节点进行交换,然后在对这个节点进行调整。但是这里地方有点特殊,可能需要下沉操作后还需要上浮操作。因为删除的这个节点并不一定是顶部节点或者叶子节点。为什么需要有
!down(h, i, n)
这个判断,因为如果一个节点进行了下沉操作,则说明该节点比子节点还要大,由于其他节点之前都是按照最小顶堆放置的,也就是说,如果该节点比子节点还要大,那么它就肯定比自己的父节点还要大。所以这里就做了优化,如果这个节点进行了下沉操作,则不需要进行上浮了,否则再进行上浮。其实判断是否有过上浮操作再判断是否需要下沉是一样的。调整某个节点的特性
与删除差不多,就是对该节点进行下沉或者上浮。
下沉操作的实现
首先有一个循环,循环的结束条件为节点没有子节点了或者节点经过比较不需要和子节点进行交换。现在看一下逻辑:
j1 := 2*i + 1
j2 := j1 + 1; j2 < n && h.Less(j2, j1)
,如果判断成立,则节点和右子节点进行比较,否则和左节点进行比较j = j2 // = 2*i + 2 // right child
。if !h.Less(j, i) { break } h.Swap(i, j)
上浮操作实现
相对于下沉操作,上浮的操作相对简单,因为只需要比较该节点与父节点的值,父节点只有一个。
循环的结束条件需要注意一下,当节点为根的时候,也就是
j
为0的时候,(0-1)/2
是为0的,也就是最后计算出来的i
也是为0的,所有i == j
成立,循环结束。所有循环结束的条件是,当前节点没有父节点了(根节点)或者不需要和父节点进行交换(节点的值大于父节点的值)。使用Heap
既然写到这里了,那就看看
go
里面怎么使用它吧,我对go
不是很熟,只是用来练习一些数据结构和算法而已,下面的一些代码并不是最佳实践。先定义一个类型,实现
Interface
需要的几个方法这里定义了一个类型
PriorityQueue
,本质是一个int
型切片,然后实现了需要实现的几个方法,这样就可以使用heap
提供的方了。使用
heap
的方法总结
就这样吧,重点是上浮和下沉两个操作。有了最小(大)堆,还可以利用它进行排序(堆排序),也就是每次
Pop
顶元素就行。下一篇准备写求图的单源最短路径的经典算法:
Dijkstra
。blog地址
The text was updated successfully, but these errors were encountered: