十、用堆来实现优先队列

235 阅读4分钟

关注公众号: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后,也加深了我们对于二叉树左右根的各种操作。

加油,前路漫漫,那都不是事儿!

拜了个拜~