目录
3、Java中的堆(PriorityQueue、PriorityBlockingQueue)
堆的定义:
1、堆是一个完全二叉树
2、堆的每个节点都大于等于(或者小于等于)左右子节点,即大于等于左子树和右子树。节点大于等于所有子节点叫做大顶堆,小于等于所有子节点叫做小顶堆。
1、堆的特点
首先堆是一个完全二叉树,再回顾一下完全二叉树的特点,下面的操作都会基于这些特点。堆一般也是使用数组进行表示,我们一般会将数组下标为0的位置空着,从下标1开始存储数据。并且所有节点都满足,如果父节点的下标为 i :
左子节点的下标为 2 * i, 右子节点的下标为 2 * i + 1; 如果我们从数组下标为0开始存储数据的话,左子节点会增加一次加法运算,左子节点的下标为:2 * i + 1,右子节点的下标为 2 * i + 2。 即时间换空间的思想(或反之)。
并且如果完全二叉树的节点个数为N,那么叶子节点的个数为 N/2。并且叶子节点都存储在数组的后半部分,入下图:
并且一个堆可以有不同的存在形式,只要满足上面的特点即可,比如 1,3,4,5四个数,可以组成多种不同形式的堆:
2、堆的操作
最为数据结构是堆,其实本身就是优先级队列,在java中就是PriorityQueue和PriorityBlockingQueue,那么使用场景就是所有优先级队列的场景,还有就是经常用于处理Top K的问题(链接地址:)。数据结构的存在都是为了解决现实中的问题,那么数据结构堆需要对外提供添加一个元素、移除堆顶元素(查看),并且在添加和删除后任然要保证是堆。即满足上面的两点:完全二叉树、节点大于等于(或小于等于)所有子节点。
1、堆化(添加元素)
往堆中添加一个元素的操作,我们叫做堆化,堆化的过程可以从上往下堆化,也可以从下往上堆化。添加是我们一般使用从下往上堆化的方式,先将添加的元素放到数组剩余的第一位,这种方式可以保证添加节点后肯定还是完全二叉树。此时需要从下往上判断是否满足堆的特性,如果是大顶堆就判断父节点值是否大于等于子节点。判断的路径就是当前元素的下标比如为 i,判断路径就是 i / 2, i / 4, i / 2² 。过程如下图:
用代码实现,上面图的打印结果:
[0, 6, 5, 1, 3, 4, 0]
[0, 6, 5, 2, 3, 4, 1]
public class Heap {
public static void main(String[] args) {
Heap heap = new Heap(6);
int[] arr = {6, 5, 1, 3, 4,};
for (int i = 0; i < arr.length; i++) {
heap.insert(arr[i]);
}
System.out.println(Arrays.toString(heap.arr));
heap.insert(2);
System.out.println(Arrays.toString(heap.arr));
// [0, 6, 5, 1, 3, 4, 0]
// [0, 6, 5, 2, 3, 4, 1]
}
/** 用数组存储堆元素 */
public int[] arr;
/** 堆的最大值,即数组的长度,当前不考虑扩容问题 */
private int maxLength;
/** 当前堆已经存储的个数 */
private int currentLength;
public boolean insert(int value) {
if (currentLength >= maxLength) {
return false;
}
// 先赋值到空闲数组的最后一位,此时一定满足完全二叉树
++currentLength;
arr[currentLength] = value;
int index = currentLength;
// 判断还没有到堆顶下标为1, 并且当前值大于 index/2 为父节点的值
while(index/2 > 0 && arr[index] > arr[index / 2]) {
// 交换元素
HeapSort.swap(arr, index, index/2);
// 下标缩小一半,继续
index = index / 2;
}
return true;
}
public Heap(int capacity) {
// 留出下标为0的位置
this.arr = new int[capacity + 1];
maxLength = capacity;
this.currentLength = 0;
}
}
2、移除堆顶元素
删除元素右要防止结构发生变化,那么类似添加操作,只使用数组有值的最后一个下标元素顶替堆顶元素【下标为1的元素】,再往下进行堆化。从堆顶开始判断左右子节点是否比自己大,将最大的进行交换。如果交换了,就将指针指向交换了的元素,继续前面的判断(与自己的左右子节点进行对比)。
代码实现入下:
public Boolean remove() {
if (currentLength == 0) {
return false;
}
// 将最后一个叶子节点放到堆顶位置,计数器减1
arr[1] = arr[currentLength];
--currentLength;
heapify(arr, currentLength, 1);
return true;
}
private void heapify(int[] a, int currentLength, int i) { // 自上往下堆化
while (true) {
// 从堆顶1,开始往下堆化, maxPos记录要替换的节点
int maxPos = i;
// 左子下标在数组有值范围内判断, 如果当前节点值小于左子节点
if (i*2 <= currentLength && a[i] < a[i*2]) {
maxPos = i*2;
}
// 右子下标在数组有值的范围内判断,如果上面的父节点或者左子节点的最大值 比右子节点小
if (i*2+1 <= currentLength && a[maxPos] < a[i*2+1]) {
maxPos = i*2+1;
}
// 上面两个判断都没进,即父节点大于左右子节点,停止
if (maxPos == i) {
break;
}
// 将父节点与左子或右子替换
HeapSort.swap(arr, i, maxPos);
// 将堆化的指针指向刚替换过的左子或右子,看看其下面是否还需要替换
i = maxPos;
}
}
3、Java中的堆(PriorityQueue源码分析)
堆其实就是优先队列,只是叫法不同而已,解决的都是相同的问题使用场景。在Java中提供了非线程安全的PriorityQueue和线程安全的PriorityBlockingQueue(前面分析过该类型为阻塞队列+优先级队列【堆】)。现在只是对数据结构进行分析,并且查看Java中堆的实现过程和堆化(从上往下和从下往上)的过程。所以下面对非线程安全的PriorityQueue源码分析,如下:
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
/** 默认的初始化数组的长度 */
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 该队列从下表0开始存储,父节点下标[n]的左子节点下标[2*n+1]和右子节点下标[2*(n+1)] ,
* 默认是小顶堆,存储的泛型对象,左右子节点需要比父节点大,就是使用Comparator回调进行比较
* 没有使用private 修饰就是为了简化内部类的访问
*/
transient Object[] queue;
/** 队列的总容量 */
private int size = 0;
/** 计算使用的个数,也是:没有使用private 修饰就是为了简化内部类的访问 */
transient int modCount = 0; // non-private to simplify nested class access
/** 比较器,如果比较器为 Null则使用自然排序 */
private final Comparator<? super E> comparator;
/******************************* 添加元素,从下往上堆化的过程 *********************************/
/** 添加元素,使用的add方法兼容AbstractQueue接口 */
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
// 判断是否扩容,需要则扩容为原来的二倍
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 如果堆为空,添加到堆顶(即下标为0的位置)
if (i == 0)
queue[0] = e;
else // 否则与我们自己写的一样,往上堆化
siftUp(i, e);
return true;
}
/** 往上堆化,使用比较器或自然排序只是判断值大小时使用,其他都一样,下面只分析比较器的从下往上堆化 */
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
/** 添加元素时,从下往上heapify的过程 */
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
// 带符号右移,获取父节点的下标
int parent = (k - 1) >>> 1;
// 获取父节点的值,并且使用比较器的回调方法比较
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break; // while的退出条件就是已经满足堆的大小比较
// swap父子节点的值,只是这样就一次性将X直接赋值到最终位置上 queue[k] = x;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
/****************************** 移除堆顶元素,从上往下堆化的过程 *******************************/
/** 移除堆顶元素 */
public E poll() {
if (size == 0)
return null;
// 获取叶子节点的最后一个值的下标
int s = --size;
modCount++;
// 获取堆顶元素
E result = (E) queue[0];
// 获取叶子节点的最后一个值
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
// 与上图一样,将最后一个叶子节点的值替换堆顶元素,
// 再从上往下进行堆化
siftDown(0, x);
return result;
}
/** 同样,比较器只是用于判断值的大小,下面只分析带比较器的堆化过程 */
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
/** 从上往下堆化的过程 */
private void siftDownUsingComparator(int k, E x) {
// 完全二叉树本身后半部分是叶子节点不需要堆化
int half = size >>> 1;
while (k < half) {
// 获取子节点的下标
int child = (k << 1) + 1;
// 获取左子元素
Object c = queue[child];
// 获取右子节点下标
int right = child + 1;
// 判断右子下标必要越过总长度,并且他先比较了左右子节点的最大值,
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
// 那
if (comparator.compare(x, (E) c) <= 0)
break;
// 同样现在只是内部调整,计算出堆顶元素X 最后应该去的位置k
queue[k] = c;
k = child;
}
// 最后一步直接将x放置到最后应该去的位置上
queue[k] = x;
}
/** 扩容,扩容为原来的两倍 */
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
}