堆通常是一个可以被看作一棵树的数组对象。堆具体的实现一般不通过指针域,而是通过构建一个堆树组与二叉树的父子节点进行对应,因此堆总是一颗完全二叉树。
对于任意一个父节点的序号N来说(这里N从0算),它的节点的序号一定是2N+1,2N+2...因此可以直接用数组来表示一个堆。
不仅如此,堆还有一个性质:堆中某个节点的值总是不大于或不小于其父节点的值。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆常用来实现优先队列,在面试中经常考的问题都是与排序有关,比如堆排序、topK问题等。由于堆的根节点是序列中最大或最小值,因而可以在建堆以及重建堆的过程中,筛选出数据序列中的极值,从而达到排序或者挑选topK值的目的。
案例PriorityBlockingQueue中二叉堆的使用
put过程:
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();//加锁
int n, cap;
Object[] array;
//如果元素个数大于等于数组长度,开始扩容
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
//一开始构建队列的时候传入comparator
Comparator<? super E> cmp = comparator;
if (cmp == null)
//使用元素自带的comparator进行比较,并排序
siftUpComparable(n, e, array);
else
//使用comparator进行比较,并排序
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;//增加元素个数
notEmpty.signal();//唤醒take线程
} finally {
lock.unlock();//解锁
}
return true;
}
//新元素堆内上浮实现
private static <T> void siftUpComparable(int k, T x, Object[] array) {
//尝试转化插入对象为Comparable实例
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
// 新元素x的数组下标为k,对应的父节点的下标为(k-1)/2
int parent = (k - 1) >>> 1;
Object e = array[parent];
//如果子节点已经比父节点还要大,不需要再跟上层节点比较,新元素上浮结束
if (key.compareTo((T) e) >= 0)
break;
//如果子节点已经比父节点小,父节点下沉,新元素上浮一次
array[k] = e;
k = parent; //新元素上浮后继续与新的父节点比较大小,直到k=0或者新的父节点小于新元素
}
array[k] = key;//新元素在堆中插入正确的位置。
}
private void tryGrow(Object[] array, int oldCap) {
//扩容期间需要解锁,能够让其他线程继续执行
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;//新数组
//cas标识,如果为0,表示当前没有线程对数组扩容
if (allocationSpinLock == 0 &&
//cas方式设置值0->1
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
//开始扩容
//计算新的数组长度
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
//计算容量不能超过最大大小
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];//创建新的数组用来保存元素
} finally {
allocationSpinLock = 0;//重置标识,表示扩容完毕
}
}
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock();//重新加锁
//如果数组还是原来的数组
if (newArray != null && queue == array) {
queue = newArray;//设置为新数组
System.arraycopy(array, 0, newArray, 0, oldCap);//转移元素到新数组
}
}
take过程
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//加锁
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;//队列为空
else {
Object[] array = queue;
E result = (E) array[0];//取出第一个,二叉堆中第一个最小
E x = (E) array[n];
array[n] = null;//设置为空
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
//空元素堆内下沉实现
private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>) x;
int half = n >>> 1; // loop while a non-leaf half最后一个有子节点的父节点下标
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];
int right = child + 1;
if (right < n
&& ((Comparable<? super T>) c)
.compareTo((T) array[right]) > 0)
c = array[child = right]; //比较出左右子节点更小的那个子节点
if (key.compareTo((T) c) <= 0) //如果左右子节点的最小值大于数组末尾的值,那么数组末尾的值直接放到父节点,空节点下沉结束
break;
array[k] = c; // 如果子节点最小值小于数据末尾的值,子节点上浮到父空节点
k = child; //空节点下滑到最小子节点的位置
}
array[k] = key; // 最后空节点填充数组最后的值
}
}