定义
堆是一种树状的数据结构,堆有很多种,比如本次的主题二叉堆,对叉堆,索引堆,二项堆,斐波那契堆...... 二叉堆,又叫完全二叉堆,顾名思义它是由
完全二叉树的形式组成的. 提供三个接口: 1:添加元素 2:获取最大值 3:删除最大值
特性
- 逻辑结构由完全二叉树组成
- 物理结构一般由数组组成
- 任意节点的值,都大于等于(小于等于)子节点的值
- 由特性二决定了它的每个节点必须具有可比较性
规律总结
规律一: 索引i和节点总数n之间的规律
结合图2,我们来看看,索引
i和节点总数n之间的规律,这个很重要,后面进行节点的添加会用到这些,floor()向下取整:
- 1:如果
i=0,则它是根节点 - 2:如果
i>0,它的父节点索引为:floor((i-1)/2) - 3:如果
2i<=n-2,它的左子树索引为:2i+1 - 4:如果
2i>n-2,它无左子树 - 5:如果
2i<=n-3,它的右子树索引为:2i+2 - 6:如果
2i>n-3,它无右子树
规律二: 非叶子节点的数量==第一个叶子节点的索引==floor(n/2)
接口实现
添加元素O(logn)
从上图我们可以总结添加元素的流程是个循环执行过程:
node(待插入元素)
- 1:添加元素的第一步是把
node插入到数组尾部 - 2:
node的值>父节点值,与父节点交换位置,继续循环 - 3:如果
node<=父节点的值或者node没有父节点,则终止循环 整个过程叫做上滤
/**
* 添加元素
*
* @param element
*/
private void add(int element) {
//添加到数组尾部
this.elements[size++] = element;
//上滤
siftUp(size - 1);
}
/**
* 对index位置的元素进行上滤操作
*
* @param index 索引位置
*/
private void siftUp(int index) {
//获取index位置元素的值
int value = elements[index];
while (index > 0) {
//获取父节点索引值:见==规律一第2条==
int parentIndex = (index - 1) >> 1;
//获取父节点的值
int parentValue = elements[parentIndex];
//如果父节点的值大,则循环结束
if (parentValue >= value) {
break;
}
//来到这里,说明父节点的值小,进行交换
int tmp = elements[index];
elements[index] = elements[parentIndex];
elements[parentIndex] = tmp;
//交换之后,index要指向父节点,继续循环
index = parentIndex;
}
}
添加元素的优化
上面的siftUp(int index) 方法中,我们判断如果父节点的值小,我们就进行交换操作,其实这里可以进行优化,我们可以先把要添加的元素的值保存起来,等到最后确定了要添加元素的最终位置,才把要添加的元素放上去.
/**
* 对index位置的元素进行上滤操作
*
* @param index 索引位置
*/
private void siftUp2(int index) {
//保存新添加元素的值
int value = elements[index];
while (index > 0) {
//获取父节点索引
int parentIndex = (index - 1) >> 1;
//获取父节点值
int parentValue = elements[parentIndex];
//如果父节点的值大,则循环结束
if (parentValue > value) {
break;
}
//来到这里,说明父节点的值小,父节点值下挪
elements[index] = parentValue;
// 重新赋值index
index = parentIndex;
}
//循环结束,确定了最终位置就是index
elements[index] = value;
}
删除最大值(大顶堆)O(logn)
由大顶堆的性质,可以看出,删除最大值,其实就是把数组头元素删除了,是不是就这么简单,我们来看删除过程.
总结删除的过程如下:
- 1:用尾部的值替换头部的值
- 2:删除尾部的值
- 3:如果
node<最大子节点的值,与最大子节点的值交换 - 4:如果
node>=最大子节点的值,或者没有子节点,退出循环
整个过程叫做下滤,
- 第一:
如果是叶子节点的话,就不需要进行下滤操作,比如上图的38,47,21,14因为没有子节点,所以不需要下滤. - 第二:
第一个叶子节点之后的所有节点都不需要下滤. - 第三: 那问题来了我们怎么获取第一个叶子节点的索引呢?请看
规律二. - 第四: 需要
下滤的节点,要么只有左子节点,要么左右子节点都有,所以,肯定有左子节点,看节点72,43,68
/**
* 删除最大值
*
* @return 最大值
*/
private int remove() {
//头部元素,最大值
int maxValue = elements[0];
//尾部索引
int lastIndex = --size;
//把尾部的值赋值给头部
elements[0] = elements[lastIndex];
//删除尾部元素
elements[lastIndex] = 0;
//下滤操作
siftDown(0);
return maxValue;
}
/**
* 对index位置数据进行下滤操作
*
* @param index 索引位置
*/
private void siftDown(int index) {
//获取头部元素的值
int value = elements[index];
//获取第一个叶子节点的索引,规律二==floor(n/2)
int firstIndex = size >> 1;
//只有是非叶子节点才进行下滤
while (index < firstIndex) {
//index节点有二种情况,1:只有左子节点 2:左右子节点都有
//因为肯定有左子节点,看规律一第3点
int leftIndex = (2 << 1) + 1;
//获取左子节点数值
int leftValue = elements[leftIndex];
//右子节点索引=左子节点索引+1
int rightIndex = leftIndex + 1;
//只有右子节点左右<整个数组大小,说明index也有右子节点
if (rightIndex < size) {
int rightValue = elements[rightIndex];
//获取左右子节点最大的那一个
if (rightValue > leftValue) {
leftValue = rightValue;
leftIndex = rightIndex;
}
}
//如果节点数值>=左右子节点数值,退出循环
if (value >= leftValue) {
break;
}
//将子节点数值赋值给index位置数值
elements[index] = leftValue;
//重新设置index
index = leftIndex;
}
//确定最终的位置
elements[index] = value;
}
获取最大值(大顶堆)O(1)
最大值
=数组头元素
批量建堆
就是给定一个数组,里面的元素是随机的,怎么把这个数组变成一个二叉堆
自上而下的上滤(O(nlogn))
第一个节点不需要上滤
/**
* 批量建堆
*/
private void heapify() {
// 自上而下的上滤,从第二个元素开始
for (int i = 1; i < size; i++) {
siftUp(i);
}
}
自下而上的下滤(O(n))
从第一个非叶子节点开始往前一个个下滤
/**
* 批量建堆
*/
private void heapify() {
// 自下而上的下滤,第一个叶子节点的索引=size/2,所以往前推一个就是第一个非叶子节点
for (int i = (size >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
}
结论:平时我们在批量建堆的过程中还是选择自下而上的下滤