《算法与数据结构》学习笔记 4-3 在最大堆中添加元素,Shift Up
这一小节,我们解决这样一个问题:向最大堆中添加一个元素,对应优先队列中”入队”这个操作,同时还要保持最大堆的性质,即根元素是堆中最大的元素,并且除了根节点以外任意一个节点不大于它的父亲节点。
我们将要介绍的这个操作叫做 Shift Up 。
什么是 Shift Up?
下面罗列了 Shift Up 这个操作的关键点:
- 新加的元素放在数组的末尾;
- 进行一系列的操作维护最大堆的定义;
- 新加入的元素调整元素的位置:只与父节点比较(不与兄弟孩子比较);
- 如果比父节点大,就交换位置,否则,可以停止了,这个元素就放在当前位置。
为什么要在数组的末尾添加一个元素呢?可不可以在开头、中间?
首先,在开始或者中间插入元素,要把后面的元素后移,数组开头的元素具有特殊性质,相比起来,放在末尾维护堆的性质平均开销较少。所以在数组的末尾加入元素是最自然的想法。
如果在数组末尾添加元素,时间复杂度是 O(1)。但是在数组的末尾添加了一个元素,此时的数组就不满足堆的定义和性质,我们须要进行一系列的操作,去维护堆的定义和性质。
如何维护堆的定义和性质:通过 Shift Up 把新添加的元素放置到合适的位置
Shift Up 操作描述如下:
在数组的最后位置添加一个元素,新加入的元素只和父节点比较大小(无须和它的兄弟节点比较),只要比父节点大(严格大于),就往上走,否则停止,这个新添加的元素就放置在合适的位置,同时也调整了部分元素的位置。循环这样做,这样的过程叫做 Shift Up。
Shift Up 代码实现
代码实现:
/**
* 在堆的尾部增加一个元素,将这个元素执行 shift up 操作,保持最大堆的性质
*
* @param element
* @return
*/
public void insertElement(int element) {
// 首先确保还有位置能够放添加进来的元素
assert count + 1 <= capacity;
this.count = this.count + 1;
data[count] = element;
shiftUp(count);
}
/**
* 将索引为 k 的元素逐渐上移,直到满足最大堆的定义
* 还可以优化,把多次的交换工作变成多次的赋值
* 对索引是 h 的元素执行 shiftUp 操作
*
* @param h
*/
private void shiftUp(int h) {
// 当 h = 1 的时候,元素已经在堆顶,shiftUp 操作没有意义
// data[h / 2] 表示的是 data[h] 这个元素的父亲节点
while (h > 1 && data[h / 2] < data[h]) { // h>1 别忘了
swap(data, h / 2, h);
// h = h/2 ,把当前考查的索引变成父亲节点的索引
h /= 2;
}
}
特别说明:
1. 有索引就必须要考虑索引的边界问题,就是这里说的 h>1
,因为当 h = 1
的时候,元素已经在堆顶, Shift Up 操作没有意义;
- 像插入排序一样,我们先给出一个 Shift Up 等价的写法,把 while 循环里面的判断写到循环体中如判断,我个人认为下面的代码逻辑更加清晰一点,更能表达 Shift Up 的语义,更突出了”当当前元素小于等于其父亲节点的时候,终止循环”这个逻辑。
private void shiftUp(int h) {
while (h > 1) { // h>1 别忘了
if(data[h / 2] < data[h]){
swap(data, h / 2, h);
h /= 2;
}else{
break;
}
}
}
Shift Up 的过程可以转化为多次赋值
我们理解下面这种写法,可以通过插入排序的优化那一部分介绍的内容来理解。过程其实很简单,先把这个要 Shift Up 的元素存起来,只要遇到父亲元素比这个存起来的元素小,就把父亲节点的元素覆盖到当前元素,等到条件不满足的时候,就把存起来的元素值赋给当前元素即可。
Java 代码实现:
private void shiftUp(int h) {
int temp = data[h];
while (h > 1) {
if (data[h / 2] < temp) {
data[h] = data[h / 2];
h /= 2;
} else {
break;
}
}
data[h] = temp;
}
下一节,我么介绍什么是从最大堆中取出一个元素,以及如何取出这个元素。