Heap
堆
一个优先级队列。PriorityQueue
便是根据堆来实现的
找出前K个数(从大到小),构建一个容量为K的小堆,遍历序列,如果元素比堆顶的元素大,则替换之,然后下沉,维护堆。
还有一个应用场景,堆排序。这都是以前
堆的数据存储、增删改查遍历操作实现
堆是一个完全二叉树,何为完全二叉树,1.所有的叶节点都出现在最下边两层,不就是平衡二叉树吗。任一两条路径的高度差不超过1。
最大堆:节点的值大于子节点的值(子节点之间不一定满足何种顺序),因此根节点是最大的
最小堆:节点的值小于子节点的值,因此根节点是最小的
堆整体趋势上是有序的,但不严格有序,上一层所有节点值大于下一层所有节点的值,但是一层内节点间是无序的
堆的数据存储也可以用节点,和树一样通过维护节点关系来维护,但是因为堆是一个完全二叉树的前提,父子节点间的节点数量满足一定的关系,所以可以用数组来存储。找父节点、左节点和右节点可以通过计算数组下标来获取。这是可以用数组来存储的必要条件。如果用节点关系的话,每个节点需要维护左右节点和父节点的引用,增加存储空间,二是同一排的节点之间没有直接的引用,不便于遍历。所以用数组存储各个节点。
通过一个节点获取左、右、父节点的下标计算公式推导。如果获取前一个或后一个直接将下标减1或加1即可。
定位一个父节点,比如在堆中的第c
排,第l
个。推导其左节点下标和自己的下标关系。
![](https://img.haomeiwen.com/i5487261/79ece01f4a7a64a9.jpg)
上图即为节点在堆中的位置和映射到数组的位置关系。
一个标准的堆,数字代表节点在堆中坐标,横坐标记为c
,纵坐标记为l
,前提准备,需要用到等比数列表达式和求和公式,这里不展开。
对于第c层,这一层的节点数量为,
1到c层总节点数量为,推导
、
和
跟
的关系
N前面的节点数量
,则
(下标从0开始)
N到N左之间的节点数量
,
可以看出N和N左之间的节点数量和N之前的节点数量是一样的,记为
则
即
通过父节点找两个子节点的关系
则(下标从0开始)
通过上面例子,假定父节点为,则c=3,l=c
计算得
跟示意图吻合
通过子节点找父节点,因为子节点可能是左节点,也有可能是右节点。最终我们将要推导出一个公式,两个子节点的坐标参数都能计算出唯一的父节点坐标,先根据左节点和右节点分别求出父节点的下标计算公式
通过左节点
通过右节点
备注:此处的计算结果按照程序中的除法概念,只取整数。因为堆中没有直接维护节点间的关系,对于任意子节点,无从得知其是左节点还是右节点。所以这个两个公式哪个可以作为两种子节点下标计算父节点下标的通用公式?
按照堆是一个完全二叉树设定,从第二层开始,每一层的节点数量都是上一层的2倍,即每一层的节点数都是偶数。所以任意一个右节点都处于第奇数个位置上,即在数组的下标一定是偶数,同理得左节点在数组中的下标一定是奇数。即,
。假设用左节点的公式来计算右节点下标,即
因为,所以
,多加的1作为余数被丢弃掉了
所以
即可以用左节点公式来计算右节点下标。
反之,用右节点公式计算左节点,因为(刚好除尽),但是
,不能被整除,且整数减少了1。所以右节点计算公式不能计算左节点。
综上,通过任意子节点的下标计算父节点的下标能且只能通过左节点公式来计算。
以上父子节点下标互相计算公式,通过JDK PriorityQueue
源码也能得到印证
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;// 通过子节点下标计算父节点下标
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least 通过父节点下标计算左节点下标
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
以小顶堆为例
新增操作
先将值追加到堆尾,即放入到数组的size下标处,然后递归跟父节点比较,如果值比父节点小,则向上筛选,即跟父节点互换,代码实现如下
private Integer[] queue;
//节点个数
public void add(int value) {
//放入堆底
int k = size;
queue[k] = value;
size++;
if (k == 0) {
return;
}
//向上翻
siftUp(k, value);
}
private void siftUp(int k, int value) {
while (k > 0) {
int parent = (k - 1) >>> 1;//为什么是无符号右移
if (queue[parent] == null) {
throw new IllegalArgumentException("parent is null when add " + value);
}
if (value < queue[parent]) {
//对换
queue[k] = queue[parent];
queue[parent] = value;
//继续往上找
k = parent;
}else {
break;
}
}
}
删除操作
先把最后一个元素放置在待删除的位置,然后下调,直至子节点比当前节点都大或者没有子节点了,代码实现如下
public boolean remove(int v) {
int p = indexOf(v);
if (p == -1) {
return false;
}
removeAt(p);
return true;
}
private int indexOf(Integer o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
public int removeAt(int i) {
int s = --size;//先得到末尾元素下标,同时将size减1
int value=queue[i];
int v = queue[s];
queue[s] = null;
if (s == i) {//删除的刚好是末尾这个元素
return value;
}
//将最后一个元素放置在待删除的位置
queue[i] = v;
//下调操作
siftDown(i);
return value;
}
private void siftDown(int k) {
int half = size >>> 1;
while (k < half) {
//跟子节点比较
int least= k * 2 + 1;//先假设较小节点是左节点
int c=queue[least];
int right=least+1;
if (right< size && queue[right]<c){
c=queue[right];
least=right;
}
if (queue[k]<c){
break;
}
//交换
queue[least]=queue[k];
queue[k]=c;
k=least;
}
}
遍历操作
直接遍历数组即可。
网友评论