堆的概念
堆是一棵完全二叉树,一般使用数组来存储。通俗来讲堆其实就是利用数组来维护一个完全二叉树。
按照堆的特点可以把堆分为大顶堆和小顶堆
-
大顶堆:堆的每个结点的值都大于或等于其左右孩子结点的值
-
小顶堆:堆的每个结点的值都小于或等于其左右孩子结点的值
根据堆的概念(利用数组维护的完全二叉树),可以推导出:
假设 节点A 在数组 tree 的索引为 i
则
(1)A节点的左节点索引:leftIdx = (i+1)*2 -1
(2)A节点的右节点索引:rightIdx = (i+1)*2
(3)A的父节点索引:parentIdx = (i-1)/2
堆的构建
堆的构建可以看成是空堆中逐渐插入数据,因此构建堆,应该先实现堆的插入方法。
往堆中插入节点
往堆中插入数据时,可能会破坏大顶堆(或小顶堆),根节点大于左右节点的性质,因此需要做出调整。
堆的插入流程如下:
- 将插入的数组置于数组的尾部
- 从尾部开始比较当前节点(一开始为插入的节点)与父节点是否满足大顶堆(或小顶堆)的性质,不满足则交换父子节点。
- 重复2步骤,直到满足大顶堆性质或到达堆顶
示例代码:
/**
* 插入的流程:
* 1.先把元素放到数组最后
* 2.与父元素比较,若父元素小于子元素则交换父子元素,直到父元素大于等于子元素
* @param n
*/
public void add(int n){
if(size>=capacity){
throw new RuntimeException("堆达到最大容量");
}
//新节点的索引
int curIdx = size;
//将新节点插入数组尾部
table[curIdx] = n;
//获得父节点索引,具体代码在参考后文的完整示例
//父节点索引 pIdx = (curIdx-1)/2
int pIdx = getParent(curIdx);
/**
* 调整堆,使其符合大顶堆的性质,时间复杂度O(logn)
* 与父元素比较,若父元素小于子元素则交换父子元素,直到父元素大于等于子元素
*/
while(table[curIdx]>table[pIdx]){
//子节点值大于父节点,交换父子节点
int t = table[pIdx];
table[pIdx] = table[curIdx];
table[curIdx] = t;
curIdx = pIdx;
pIdx = getParent(curIdx);
}
size++;
}
在清楚堆的插入过程之后,堆的构建就是直接调用插入方法,往堆中存放数据。
同时由上述可知,往堆中添加元素的时间复杂度为 O(logN),因此构建堆的时间复杂度为n次插入,即 O(NlogN)
删除堆顶节点
删除堆顶元素的流程:
- 用堆的最后一个元素(数组中的最后一个元素),代替堆顶元素(数组的第一个元素)
- 判断当前元素(一开始为堆顶),是否小于左右子元素,小于则用左右子元素中较大的元素和当前元素进行交换
- 重复步骤2,直接节点大于左右元素或没有左右元素
示例代码:
/**
* 删除并返回堆顶元素
* 删除流程:
* 1.把最后一个元素代替删除位置的元素
* 2.与子元素比较,把较大的子元素与当前元素交换,直到当前元素大于左右子元素
* @return 堆顶元素
*/
public int remove(){
//待删除的堆顶元素
int v = table[0];
//用堆的最后一个元素(数组中的最后一个元素),代替堆顶元素(数组的第一个元素)
table[0] = table[size-1];
size--;
//调正堆,使堆满足大顶堆的性质
//当前元素索引,一开始为堆顶元素
int curIdx = 0;
while(true){
//获得当前元素的左右节点索引,具体代码参考后面完整示例
int lf = getLeftChild(curIdx);
int rt = getRightChild(curIdx);
if(lf==-1&&rt==-1){
//没有左右元素,无需调整
break;
}
//较大的子元素索引
int swapIdx;
if(lf==-1||rt==-1){
swapIdx = lf==-1?rt:lf;
}else{
swapIdx = table[rt]>table[lf]?rt:lf;
}
if(table[curIdx]>table[swapIdx]){
//父元素大于左右子元素,结束交换
break;
}
//用较大的子元素代替父元素
int t = table[curIdx];
table[curIdx] = table[swapIdx];
table[swapIdx] = t;
curIdx = swapIdx;
}
//返回被删除的元素
return v;
}
完整代码
/**
* @author Darren
* @date 2021/6/24 15:54
* 简单大顶堆的实现
*/
public class Heap {
//堆的容量
int capacity;
//已存放的节点数
int size;
//存放完全二叉树结构的数组
int[] table;
Heap(int capacity){
this.capacity = capacity;
table = new int[capacity];
}
/**
* 插入的流程:
* 1.先把元素放到数组最后
* 2.与父元素比较,若父元素小于子元素则交换父子元素,直到父元素大于等于子元素
* @param n
*/
public void add(int n){
if(size>=capacity){
throw new RuntimeException("堆达到最大容量");
}
//当前元素的索引
int curIdx = size;
//将新节点插入数组尾部
table[curIdx] = n;
int pIdx = getParent(curIdx);
/**
* 调整堆,使其符合大顶堆的性质,时间复杂度O(logn)
* 与父元素比较,若父元素小于子元素则交换父子元素,直到父元素大于等于子元素
*/
while(table[curIdx]>table[pIdx]){
int t = table[pIdx];
table[pIdx] = table[curIdx];
table[curIdx] = t;
curIdx = pIdx;
pIdx = getParent(curIdx);
}
size++;
}
/**
* 删除堆顶元素
* 删除流程:
* 1.把最后一个元素代替删除位置的元素
* 2.与子元素比较,把较大的子元素与当前元素交换,直到当前元素大于左右子元素
* @return
*/
public int remove(){
//待删除的堆顶元素
int v = table[0];
//用堆的最后一个元素(数组中的最后一个元素),代替堆顶元素(数组的第一个元素)
table[0] = table[size-1];
size--;
//调正堆,使堆满足大顶堆的性质
//当前元素索引,一开始为堆顶元素
int curIdx = 0;
while(true){
int lf = getLeftChild(curIdx);
int rt = getRightChild(curIdx);
if(lf==-1&&rt==-1){
//没有左右元素,无需调整
break;
}
//较大的子元素索引
int swapIdx;
if(lf==-1||rt==-1){
swapIdx = lf==-1?rt:lf;
}else{
swapIdx = table[rt]>table[lf]?rt:lf;
}
if(table[curIdx]>table[swapIdx]){
//父元素大于左右子元素,结束交换
break;
}
//用较大的子元素代替父元素
int t = table[curIdx];
table[curIdx] = table[swapIdx];
table[swapIdx] = t;
curIdx = swapIdx;
}
return v;
}
public int getLeftChild(int i){
int index = (i+1)*2-1;
//index>=size 说明没有左子节点
return index<size?index:-1;
}
public int getRightChild(int i){
int index = (i+1)*2;
//index>=size 说明没有右子节点
return index<size?index:-1;
}
public int getParent(int i){
return (i-1)/2;
}
}