目录
堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i= 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质
- 堆中的某个结点总是不大于(或不小于)其父结点的值
- 堆是一颗完全二叉树
堆的存储
由于堆是一颗完全二叉树,我们可以采用层序的规则来高效的存储
如图所示,如果对于非完全二叉树采用这样的方式存储,会导致许多的存储空间白白浪费 ,空间利用率较低.
假设i为节点在数组中的下标,则有:
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
堆的创建
大根堆:二叉树根节点,比他左右子树的值都大
小根堆:二叉树根节点,比他左右子树的值都小
以{ 27,15,19,18,28,34,65,49,25,37 } 数组为例,我们分析大根堆的创建:在调整时,先找到左右孩子中最大的值,再与父结点比较,如果孩子结点的值大于父结点,父子交换。
1.我们首先按照层序的方式构建完全二叉树
2.我们找到最后一个度不为零的结点,首先调整最后一棵子树,然后向上遍历依次调整
对于完全二叉树来说,用数组存储,数组的最后一个元素一定是二叉树最后一棵度为零的结点(叶子结点),那么他的父结点就是我们找的最后一刻度不为零的结点。parent = (i - 1) / 2;随后 parent--,操作剩下的子树;
具体的图解演示
代码实现
属性定义
由于堆是一个完全二叉树并且由数组存储,所以我们定义属性如下:
private int[] elementDate;//数组
private int size; //有效值
private int DEFAULT_CAPACITY = 10;//定义一个默认值
public Heap() {
this.elementDate =new int[DEFAULT_CAPACITY];
this.size = 0 ;
}
public Heap(int [] array) {
// 重新复制数组,防止外部修改数组对heap里数据的影响
this.elementDate = Arrays.copyOf(array, array.length);
this.size = elementDate.length;
// 向下调整为堆结构
int parent = (size - 1 - 1) / 2; //找到最后一个度不为零的结点
for (int i = parent; i >= 0 ; i--) {
// 向下调整的方法
shiftDown(i);
}
}
向下调整
private void shiftDown(int parent) {
//非空校验
if(parent < 0){
return;
}
//由父结点找到左孩子结点
int child = parent * 2 + 1;
while(child < size){
//判断是否有右孩子
if(child + 1 < size){
//如果右孩子的值大于左孩子,让child指向右孩子
if(elementDate[child + 1] > elementDate[child]){
child++;
}
}
//如果父节点大于孩子结点,直接退出循环
if(elementDate[parent] >= elementDate[child]){
break;
}
//孩子结点大于父结点,交换位置
swap(elementDate,parent,child);
//重置父亲,孩子结点
parent = child;
child = parent * 2 + 1;
}
}
private void swap(int[] elementDate, int parent, int child) {
int temp = elementDate[parent];
elementDate[parent] = elementDate[child];
elementDate[child] = temp;
}
堆的插入
插入一个新的元素之后,与他的父节点比较,如果没有父节点大,我们调整完成,否则,与父节点交换,向上调整
public void offer(int value){
//判断数组是否已满
if(isFull()){
//扩容
elementDate =Arrays.copyOf(elementDate,elementDate.length * 2);
}
//在size位置插入新的元素
elementDate[size] = value;
size++;
//向上调整
shiftUp(size - 1);
}
private boolean isFull() {
return size == elementDate.length;
}
向上调整
private void shiftUp(int child) {
//判断是否越界
if(child > size){
return;
}
// 找到父节点
int parent = (child - 1) / 2;
while(child > 0 && parent >= 0){
//两者进行判断
if(elementDate[parent] >= elementDate[child]){
break;
}
//交换
swap(elementDate,parent,child);
//重置父亲,孩子结点
child = parent;
parent = (child - 1) / 2;
}
}
删除结点
注意:堆的删除一定删除的是堆顶元素。具体如下:
1. 将堆顶元素对堆中最后一个元素交换
2. 将堆中有效数据个数减少一个
3. 对堆顶元素进行向下调整
public int poll(){
//非空校验
if(isEmpty()){
throw new RuntimeException("数组为空");
}
//记录堆顶元素
int value = elementDate[0];
//交换第一个和最后一个元素
swap(elementDate,0,size - 1);
//有效值减一
size--;
//向下调整
shiftDown(0);
return value;
}
private boolean isEmpty() {
return size == 0;
}
优先队列(PriorityQueue)
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适。在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的
常用接口介绍
优先队列的实现
用堆作为底层结构封装优先级队列,所以上述我们实现的堆的增删等一系列操作,就是我们模拟实现优先队列