堆
定义
- 堆通常是一个可以被看做一棵树的数组对象,所有它没有使用父指针或者子指针。
- 二叉堆是一棵完全二叉树。
- 堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
- 最大堆:父节点的值比每一个子节点的值都要大。子树也是最大堆。
最小堆:父节点的值比每一个子节点的值都要小。子树也是最小堆
堆与二叉搜索树
节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。
内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额为是我内存。堆仅仅使用一个数据来村塾数组,且不使用指针。
平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足对属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。
搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。
堆的操作
这里以最大堆为例
插入
插入其实就是把插入结点放在堆最后面,然后与父亲比较,如果父亲值小于它,那么它就和父亲结点交换位置,重复该过程,直到插入节点遇到一个值比它大的父亲或者它成为树根结点。
以最大堆为栗子,在堆中插入值为20的结点(红色结点代表新进入)
20明显大于它的父亲结点值,所以和7交换位置,交换后新的父亲值还是比它小,继续交换
一步一步与它前面比它小的父亲结点交换位置,最后20成为根结点
最小堆同理,只不过要求父亲结点要比它小,如果父亲结点大于它,就得交换
删除
删除通常是指删除最大堆中的最大值或者最小堆中的最小值,也就是树根。
以删除最大堆树根为例子,删除其实就是整个堆中少了一个结点,不妨把位于最后面的一个结点当成新的树根,然后与它左右孩子比较,与值最大并且值大于它的孩子进行交换,直到它的孩子都是小于它的,或者变成树叶。
还是用最大堆为例,删除树根20,把最后的结点8提到树根
然后就按说明步骤,找到一个位置,它的孩子都小于它或者他变成树叶
堆的特性
如果从数组的第一个节点开始存放数据的话,当前节点的父节点、左孩子、右孩子的索引就会有如下的关系:
父节点的索引:(index-1)/2 (index为当前节点的索引)
左孩子的索引:index*2+1
右孩子的索引:index*2+2
右孩子的索引 = 左孩子的索引 + 1
如上图堆,采用数据存储为:
[ 10, 7, 2, 5, 1 ]
堆的实现
- 定义
public class MyHeap {
Integer[] data; //存放数据
int size; //堆的大小
public MyHeap() {
data = new Integer[10]; //默认数组大小为10
}
}
- 获取父节点
/**
* 返回堆中索引为index的节点的父节点的索引
* @param index
* @return
*/
private int parent(int index) {
if (index == 0)
System.out.println("index-0 doesn't have parent");
return (index - 1) / 2;
}
- 返回左孩子
/**
* 返回堆中索引为index的节点的左孩子的索引
* @param index
* @return
*/
private int leftChild(int index) {
return index * 2 + 1;
}
- 返回右孩子
/**
* 返回堆中索引为index的节点的右孩子的索引
* @param index
* @return
*/
private int rightChild(int index) {
return index * 2 + 2;
}
- 交换数据
/**
* 交换索引为i、j的值
* @param i
* @param j
*/
private void swap(int i, int j) {
Integer t = data[i];
data[i] = data[j];
data[j] = t;
}
- 向上调整
/**
* 如果一个节点比它的父节点大(最大堆),那么需要将它同父节点交换位置。
* @param k
*/
private void shiftUp(int k){
while (k > 0){ //k不为树根
if(data[parent(k)].compareTo(data[k]) < 0){ //如果比父节点大
swap(k,parent(k));
}else{
break;
}
k = parent(k);
}
}
- 向下调整
/**
* 如果一个节点比它的子节点小(最大堆),那么需要将它向下移动。
* @param k
*/
private void shiftDown(int k){
while (leftChild(k) < size){ //存在子节点
int j = leftChild(k);
if(j + 1 < size && data[j].compareTo(data[j+1]) < 0){ //如果右孩子存在,且大于左孩子
j = rightChild(k);
}
if(data[k].compareTo(data[j]) < 0){ //如果比子节点小
swap(k,j);
}else {
break;
}
k = j;
}
}
- 将数组转化为堆
/**
* 将任意数组转化为堆
* @param array
*/
public void heapify(Integer[] array){
if(array == null && array.length == 0)
return;
data = array;
size = array.length;
//先将数组直接看成是一个完全二叉树,然后找到该树的最后一个节点的父节点。然后从这个节点开始到根节点结束,执行shiftDown操作。时间复杂度为O(n)
for (int i = parent(size - 1); i >= 0; i--) {
shiftDown(i);
}
}
- 数组扩容
/**
* 数组扩容
* @param size
*/
private void ensureCapacity(int size) {
int len = data.length;
if(size>len)
{
//数组拷贝扩容
data = Arrays.copyOf(data, size);
}
}
- 添加元素
/**
* 添加元素
* @param element
*/
public void add(Integer element){
ensureCapacity(size+1);
data[size++] = element;
shiftUp(size-1);
}
- 删除堆顶元素
/**
* 删除堆顶元素
*/
public Integer remove(){
if(size == 0)
return null;
Integer e = data[0];
swap(0,size-1); //将数组第一个元素与最后一个元素交换
data[size-1] = null; //删除数组最后一个元素
size--;
shiftDown(0);
return e;
}
- 删除指定元素
/**
* 删除指定位置的元素
* @param index
* @return
*/
public Integer remove(int index){
if(size == 0 || index > size - 1 || index < 0)
return null;
Integer e = data[index];
if(index == size - 1){
data[size-1] = null;
size--;
}else{
swap(index,size-1);
data[size-1] = null;
size--;
if(data[index].compareTo(data[parent(index)]) > 0)
shiftUp(index);
else
shiftDown(index);
}
return e;
}
优先队列
- 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。
- 优先队列通常采用堆数据结构来实现。
- Java中的优先队列PriorityQueue就是采用数组实现的二叉堆(最小堆)来实现。
JDK中优先队列的实现
- 入队(二叉堆添加元素)
/**
* Inserts the specified element into this priority queue.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws ClassCastException if the specified element cannot be
* compared with elements currently in this priority queue
* according to the priority queue's ordering
* @throws NullPointerException if the specified element is null
*/
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;
if (i == 0)
queue[0] = e;
else
siftUp(i, e); //向上调整
return true;
}
- 出队(删除堆顶元素)
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;
}
最大堆和最小堆实现的区别在于shiftUp、shiftDown中判断条件相反
由于Object数组元素无法比较大小,可将泛型元素强制转换为Comparable<? super E>类型
参考链接:
https://blog.csdn.net/szu_crayon/article/details/81812946
https://www.jianshu.com/p/6b526aa481b1
https://blog.csdn.net/qq_37044026/article/details/86714130