堆
JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。
new出的空间在堆、优先级队列底层结构也是堆,此处的两个堆不是一个概念
- new–>堆:指的是一块具有特殊作业的内存空间
- 优先级队列底层的堆:值得是一种数据结构
概念:
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一
个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大
堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
大堆:完全二叉树中发现:每个节点都比其孩子节点大
小堆:完全二叉树中发现:每个节点都比其孩子节点小
性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
存储方式:
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,
注意:
对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
构建堆
实现Comp接口
用户可自己选择构建大堆还是小堆
interface Comp
{
public int compare(int left, int right);
}
// 按照小于的方式进行比较
class Less implements Comp{
// 0: left == right
// > 0: left > right
// < 0: left < right
public int compare(int left, int right){
return left - right;
}
}
class Greater implements Comp{
public int compare(int left, int right){
return right - left;
}
}
创建堆
public class Test {
private int[] array;
private int size;
Comp compare=null;
public Test(Comp com){
//默认的构造--将其底层容量设置为11
array=new int[11];
compare = com;
}
public Test(int initCapacity, Comp com){
if(initCapacity < 1){
//标准库:抛出一个非法参数异常
initCapacity=11;
}
array=new int[initCapacity];
compare = com;
}
public Test(int[] arr, Comp com){
//注意标准库中没有该接口---标准库中可以采用集合来构造优先级队列
array=new int[arr.length];
for(int i=0;i<arr.length;i++){
array[i]=arr[i];
size++;
}
size = arr.length;
compare = com;
//将array中的元素调整,让其满足小堆的性质
int lastLeaf=(array.length-2)>>1;
for(int root =lastLeaf;root>=0;root--){
shiftDown(root);
}
}
peek方法:
int peek(){
return array[0];
}
扩容
源码中优先级队列的扩容:
- 如果容量小于64时,是按照oldCapacity*2 + 2的方式扩容的
- 如果容量大于等于64,是按照oldCapacity*2的方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
private void grow(){
int oldC=array.length;
int newC=oldC+((oldC<64)?(oldC+2):(oldC>>1));
array=Arrays.copyOf(array,newC);
}
offer方法
首先要判断是否扩容
注意:堆在底层是将其元素按照完全二叉树的方式存储
1.将元素放入到底层的数组中---->尾插
2.检测新元素加入是否破坏堆的性质
boolean offer(int x){
grow();
array[size++]=x;
shiftUp(size-1);
return true;
}
private void shiftUp(int child){
int parent=(child-1)>>1;
while (parent>=0){
//比较孩子和双亲的大小
if(compare.compare(array[child], array[parent]) < 0){
//if(array[child]<array[parent]){
swap(child,parent);
//交换后可能使上层元素不满足小堆
child=parent;
parent=(child-1)>>1;
}else{
return;
}
}
}
poll方法
删除:删除的是堆顶的元素
方式一:直接将后序所有元素向前搬移一个位置,虽然可以删除元素,但搬移之后不满足堆的性质,效率低
方式二:
1.将堆顶元素与堆中最后一个元素进行交换,堆元素个数-1
2.将堆顶元素向下调整(交换后对顶元素的两个子树都满足小堆的条件)
//删除堆顶的元素
int poll(){
int ret=array[0];
swap(0,size-1);
size--;
shiftDown(0);
return ret;
}
isEmpty方法
boolean isEmpty(){
return 0==size;
}
向下调整
如果只有100这一个节点不满足小堆的性质,要让其满足堆的性质,只要对100进行调整
将100放在子树的某个位置---->即向下调整
1.parent表示本次需要调整的节点的下标
2.调整以parent为根的二叉树
3.注意:调整之前,一定要保证parent的左右子树一定满足小堆的性质
4.如果要检测parent是否满足小堆的性质,只需要使用parent与其孩子进行比较
6.满足小堆性质–>说明以parent为根的二叉树已经是小堆
7.不满足小堆性质–>说明parent比其孩子大,此时需要将parent与其较小的孩子进行交换交换完以后,parent较大的元素向下移动,可能导致其子树不满足小堆性质,需要继续调整其子树
private void shiftDown(int parent){
//使用child标记parent的较小的孩子
//默认情况下先让其标记做孩子,因为parent可能只有做孩子
int child =parent*2+1;
int size=array.length;
while (child<size){
//找较小的孩子
//在通过左右孩子比较前,必须要保证有孩子存在--while循环条件已经保证做孩子存在
//if(child+1<size && array[child+1]<array[child]){
if(child+1 < size && compare.compare(array[child+1], array[child]) < 0){
child += 1;
}
//检查双亲是否比较小的孩子大
if(array[parent]>array[child]){
//说明parent不满足小堆的性质--交换
swap(parent,child);
//parent较大的元素向下移动可能会导致子树不满足堆的性质
parent=child;
child=parent*2+1;
}else{
//以parent为根二叉树已经满足堆的性质
return;
}
}
private void swap(int parent,int child){
int tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
}
向上调整
将如图所示的二叉树调整成小堆
必须找到倒数第一个非叶子节点
lastleaf:最后一个节点的下标为size-1,
倒数第一格非叶子节点刚好是最后一个节点的双亲
lastleaf = (size-2)>>1
//将array中的元素调整,让其满足小堆的性质
int lastLeaf=(array.length-2)>>1;
for(int root =lastLeaf;root>=0;root--){
shiftDown(root);
}