现实中总存在这样一类需求:从一个集合中每次取出的数据都是最小值或者最大值。比如说股市的连续竞价阶段的撮合交易,需要每次取出卖方的最低价和买方的最高价进行撮合交易。如果用数组列表,从尾部插入时进行排序,然后取的时候从尾部取出。这个想法似乎不错。但是1964年,J.W. J. Williams发明了一种新的数据结构,将这个问题用更好的算法和数据结构解决。这个数据结构就是堆。
堆,逻辑上它是一棵满二叉树,而物理上它是一个数组。所谓的满二叉树,形象地说,就是除最下面一层外,其他层都是满的。如图所示:
那么怎么物理上是数组呢?其实很简单,就是按层扫描这棵树,然后按顺序塞入数组。数组就是这个样子:
因为第一层是1个元素,第二层是两个元素,第三层是4个。又到了找规律的阶段了。我找到了一个规律是每层有2n个元素。根据这个下一层数量是上一层两倍的关系,我又计算出来了数组元素a[n]的左子为a[2n+1],右子为a[2n+2]。
那么每个元素,获取parent的公式就是(x-1)/2,比如(2n+1 -1)/2 = n (2n+2 -1)/2 = n
上面的堆是大顶堆,所以还有小顶堆。大顶堆要求左子和右子都比parent小。小顶堆要求左子和右子都比parent大。
一 堆添加元素
堆从尾部添加元素。尾部添加元素后需要将堆进行调整,以保证堆顶是最大或者最小值。其与parent对比。比如小顶堆
添加了新元素1之后,因为1比parent小,所以需要和parent交换位置。
而且这是个循环到root的过程,因为新加的元素可能是整个数组中最小的元素。因为一直往上比较,所以这个过程叫sift up。但是不要忘了扩容。
二 堆删除元素
因为顶部是最大或最小的元素,也就是业务需要的元素。所以堆只从顶部删除元素。
堆删除元素的逻辑是这样的,先删除索引顶部的元素,先再把最后一个元素移动到顶部。也就是数组的最后一个元素,放到索引0这个位置。因为这从数组末尾移动过来的元素,并不是最大值或者最小值,所以需要进行sift down过程对堆进行调整,以保证堆顶是最大值或者最小值。
以小顶堆为例子,讲一下siftdown过程。设新移动到堆顶的元素为e。
Siftdown过程就是从堆顶开始,将两个子元素比较出一个最小值(如果只有一个子元素那么这个子元素就是最小值)。
将e和子元素中的最小值比较。如果e比子元素的最小值大,那么交换位置。
循环下去,直到不能找到子元素或者e比最小的子元素都小。
一级一级往下比较的目的是要保证每个子树都是小顶堆。
以下是我用java写的简单小顶堆实现。
package com.youngthing.heaps.binary;
/**
* 小顶堆
* created at 12/01/2022
*
* @author 俞建波
* <a href="mailto://yujianbo@chtwm.com">yujianbo@chtwm.com</a>
* @since 1.0.0
*/
public class MinHeap<T extends Comparable> {
private Comparable[] elements;
private int index;
public MinHeap() {
elements = new Comparable[8];
}
public void add(T t) {
// 先进行扩容
if (index == elements.length - 1) {
final Comparable[] array = new Comparable[(elements.length + 1) << 1];
System.arraycopy(elements, 0, array, 0, elements.length);
elements = array;
}
elements[index++] = t;
siftUp(t, index - 1);
}
private void siftUp(T t, int index) {
while (index > 0) {
int parent = (index - 1) >> 1;
T parentElement = (T) elements[parent];
if (parentElement.compareTo(t) > 0) {
// 交换
elements[index] = parentElement;
elements[parent] = t;
index = parent;
} else {
break;
}
}
}
public T remove() {
if (index == 0) {
return null;
}
T returnValue = (T) elements[0];
elements[0] = elements[index - 1];
siftDown();
elements[--index] = null;
return returnValue;
}
private void siftDown() {
// i 代表当前null值位置
int i = 0;
/*
* 一直循环往后跳
* case 1 如果i的左子节点超出了边界,那么跳出,这时候右子树一定超出边界
* case 2 如果i的左子节点未超出边界,但是右子节点超出了边界,比较并交换左子节点
* case 3 i的左右子节点均未超出边界,比较最小者,与i交换
*/
Comparable parent = elements[i];
while ((i << 1) + 1 < index) {
int leftIndex = (i << 1) + 1;
int rightIndex = leftIndex + 1;
if (rightIndex == index) {
// 交换 parent和left
if (parent.compareTo(elements[leftIndex]) > 0) {
swap(i, leftIndex);
}
break;
}
Comparable left = elements[leftIndex];
Comparable right = elements[rightIndex];
// left right 比较 小的值往上放树顶
int smallerIndex;
if (left.compareTo(right) > 0) {
smallerIndex = rightIndex;
} else {
smallerIndex = leftIndex;
}
// 和最小的进行交换
if (parent.compareTo(elements[smallerIndex]) > 0) {
swap(i, smallerIndex);
i = smallerIndex;
} else {
// 如果已经是最小,那么没必要比较了
break;
}
}
}
private void swap(int i, int smallerIndex) {
Comparable temp = elements[i];
elements[i] = elements[smallerIndex];
elements[smallerIndex] = temp;
}
}