关注公众号:EZ大数据,每天进步一点点,感觉很爽!
今天过得很快,因为很充实。现在我们来一起看看堆的相关知识。
别急,我们先来看优先队列,之前讲过队列的相关东西。那么大家有没有考虑过,如何实现一种队列,其出队顺序和入队顺序无关,跟优先级有关系。也就是说,当出队的时候,取出最大的元素呢?
好了,不绕弯儿了,一起来看堆。首先,堆是一种完全二叉树,之前我们说过,在计算机领域,通常遇到O(logn)这样的时间复杂度,那么基本上都是跟树这种结构有关。那么说起堆,它除了是完全二叉树还有什么性质呢?
1.(最大堆)堆中的某个节点的值总是不大于其父节点的值,也就是所有的节点都大于或等于其子节点的值。根节点的元素是最大的元素。
2.堆中,层次比较低的节点的值不一定都大于层次高的节点的值
由于完全二叉树的特点,我们可以用数组来实现堆。如下图所示:
那么从上图我们也可以直接看到,对于第i个索引来说,其父节点索引:parent(i) = i/2,其左子节点索引:left child(i) = 2 * i,其右子节点索引:right child(i) = 2 * i +1。
注意,上图跟节点索引是从1开始,那么当索引从0开始时,其父节点、左子节点、右子节点索引变化为:
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("index-0 doesn't have parent.");
}
return (index - 1) / 2;
}
private int leftChild(int index) {
return 2 * index + 1;
}
private int rightChild(int index) {
return 2 * index + 2;
}
当从0开始时,parent(i)=(i-1)/2,左子节点索引:leftChild(i)=2i+1,右子节点索引:rightChild(i)=2i+2。
接下来我们看看堆的操作。
首先,对于堆的添加元素(Swift Up)来说,其原理是在最后位置处添加元素,然后一直与上层节点元素比大小,具体如下:
1.通过给定节点的索引,计算父节点,左子节点,右子节点索引;
2.向堆中添加元素,判断新添加的元素是否需要上浮,传入添加元素位置的索引;
3.当k索引位置元素比父节点索引位置元素大时,二者交换索引和元素;
实现代码如下:
// 向堆中添加元素
public void add(E e) {
data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k) {
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
data.swap(k, parent(k));
k = parent(k);
}
}
接下来,我们操作取出最大元素(Swift Down),其原理是:取出根节点位置元素,此时根节点位置为空,然后取出最后一个元素填入根节点位置,再向下进行比值操作。具体如下:
1.堆中根节点索引为0的元素是最大元素,与堆中最后位置元素互换位置
2.下沉操作开始,比大小,左右节点位置元素先比大小,然后再与现在的根节点位置元素比值,谁大,谁上
3.循环操作
具体实现如下:
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("Can not findMax when heap is empty.");
}
return data.get(0);
}
// 取出堆中的最大元素
public E extractMax() {
E ret = findMax();
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0); // 下沉操作开始,比大小
return ret;
}
private void siftDown(int k) {
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);
if (j + 1 < data.getSize() && data.get(j).compareTo(data.get(j + 1)) < 0) {
j = rightChild(k);
}
// 循环停止条件
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(k, j);
k = j;
}
其实,我们可以把任意数组整理成堆的形状。
public Array(E[] arr) {
data = (E[]) new Object[arr.length];
for (int i = 0; i < arr.length; i++) {
data[i] = arr[i];
}
size = arr.length;
}
public MaxHeap(E[] arr){
data = new Array<>(arr);
for (int i = parent(data.getSize() - 1); i>=0; i--){
siftDown(i);
}
}
那么如何替换堆中的元素呢?结合上文所说,我们可以取出最大元素后,再放入一个新的元素。但是大家可以思考下:如果我们先extractMax,再add,此时是两次O(logn)的操作。而如果我们把堆顶元素替换以后再Sift Down,那么一次O(logn)操作即可。
其实现方法如下:
// 取出堆中的最大元素,并且替换成元素e
public E replace(E e){
E ret = data.get(0);
data.set(0, e);
siftDown(0);
return ret;
}
不得不说,如果玩转数据结构,对于代码的效率提升非常大,我们可以更优雅的实现功能。
对于堆的各种操作完成后,我们就可以很EZ的实现优先队列了。
@Override
public E getFront(){
return maxHeap.extractMax();
}
@Override
public void enqueue(E e){
maxHeap.add(e);
}
@Override
public E dequeue(){
return maxHeap.extractMax();
}
原来如此,用堆来实现优先队列简直不要太爽。到这里,我突然想到,LeetCode上各种在N个元素中选出前M个元素(M是远远小于N)的题型中,不就是堆的应用吗?后续我会每天练习一道数据结构相关的题,毕竟刷题才会更加有效的掌握这些个“难题”。
关于堆的一些认知,我就暂时先总结到这里,对于d叉堆,索引堆,二项堆,斐波那契堆等,因为日常的工作中应用的也不多,我就不过多阐述了。
好了,今天关于堆的学习就到这里,理解了堆的上浮Swift Up和下沉Swift Down后,也加深了我们对于二叉树左右根的各种操作。
加油,前路漫漫,那都不是事儿!
拜了个拜~