堆(优先队列)
堆的介绍
在说到堆之前一定要先说一下优先队列,我们都知道队列是一种特殊的线性表,它允许在一端插入在另一端删除(先进先出
),而优先队列是带有优先级的队列,优先队列把进入队列中的元素分优先级,出队列时首先选择优先级最高的元素出队列,对于优先级相同的元素则按照先进先出的原则出队列。优先队列在生活中有许多的应用。
实现优先队列,可以用链表、二叉查找树、二叉堆,其中二叉堆对于优先队列的实现是相当普遍的,所以一般我们也把(二叉)堆称为优先队列。
堆本质上是一颗完全二叉树,对于完全二叉树,我们是可以将它转化为数组存储的(如下图),所以堆也可以用数组来存储。
堆的分类
堆又可以细分为最小堆
和最大堆
。对于任意一颗子树,它的父节点小于任意一个子节点(对于左节点于右节点的大小没有规定),则称这个堆为最小堆;相反,如果它的父节点大于任意一个子节点,则称这个堆为最大堆;这个特点也称为堆序属性。对于最小堆,由于最小节点在根节点上,利用这个特性我们可以快速找到min结点;对于最大堆,由于最大节点在根节点上,利用这个特性我们可以快速找到max结点,正因为这个属性,所以堆才成为实现优先队列的最普遍选择。
代码实现
为了避免冗余,接下来的代码只讲最小堆,最小堆至少需要下列两种操作:
- insert 插入
- deleteMin 删除优先级最小者
先说最小堆的插入操作
如下图所示,我们要在一个已知的最小堆 [13,21,16,19,31,19,68,65,26,32] 中插入元素X=14,我们先在下一个可用位置创建一个空穴并将14放入,如果 14 放入目前的位置上并不破坏堆的序,那么插入完成;但是由于 14 < 31,破坏了最小堆的序,所以我们将空穴的父节点中的元素与空穴进行调换,这样空穴就朝着根的方向进了一步。重复该过程直到满足堆的序。
代码:
/*
插入元素操作
1. 将元素插入堆中,要保持堆的堆序性质
2. 允许重复的元素
如何保持堆的堆序性质?
步骤:我们将新插入的元素放入下一个可用位置的空穴,如果放入后满足最小堆,则结束;
否则将空穴的父节点元素移入该空穴中,空穴则朝着根的方向移动一步,这样反复,直到满足最小根
*/
public void insert(int x){
//插入前得先判断数组的情况,如果数组现在是满的,则需要扩容
if(currentSize == array.length){
// enlargeArray(array.length +1);
array = Arrays.copyOf(array, array.length+1);
}
//将x放入到数组的空穴中
array[currentSize] = x;
//将x放入正确的位置上
siftUp(currentSize);
//有效个数++
currentSize++;
}
//由下向上调整堆的结构,专业点称为上滤(percolate up)
private void siftUp(int x) {
if(x <= 0) return;
//如果目标节点小于父节点,则将父节点与目标节点对换
while(array[x] < array[getParent(x)]){
int temp = array[x];
array[x] = array[getParent(x)];
array[getParent(x)] = temp;
//每次交换完后改变控制变量x的坐标
x = getParent(x);//将父节点的左边赋值给x
//如果最后到了堆的根节点,则跳出循环
if(x <= 0)
break;
}
}
最小堆的deleteMin()
如图,我们现有一个最小堆 [13, 14, 16, 19, 21, 19, 68, 65, 26, 32, 31] ,我们打算删除这个堆中的最小者,即位于根位置的节点,但是删除根节点后为了维持最小堆的序,我们换要进行一系列操作。第一步:删除根结点(最小值);第二步:我们将堆中的最后一个元素 31 移动到空闲的根节点处,但是因为 31 大于他的两个子节点,不符合最小堆了;所以第三步:我们将 31 与它的子节点中最小值 14 进行交换,这样31就向下移动了一层;当 31 移动到新的位置后,由于此时31还是大于它的子节点,所以我们重复第三步直到放入正确的位置
代码:
//找到最小的,根结点
public int findMin(){
//判断数组是否为空
if(isEmpty()){
throw new ArrayIndexOutOfBoundsException("数组为空,没有最小值");
}
return array[0];
}
/*
删除最小的
思路:当删除掉最小元素后,根结点处就成了空穴,为了维持最小堆,我们得进行一系列操作
首先因为删除了一个元素,为了维持最小堆,所以得移动数组的最后一个元素
最简单的做法是将最后一个元素放入空穴中,但这样显然不符合最小堆,所以我们的做法是将空穴的两个儿子中最小者放入空穴中
但这样一来,就又形成了一个空穴,我们按照上面的步骤,再将这个空穴的子节点中最小的放进来..直到最后删除成功
*/
public int deleteMin(){
int i = findMin();//找到最小值
//删除元素前得判断,是否数组就只有一个元素
if(currentSize == 1){
array = new int[DEFAULT_CAPACITY - 1];
return i;
}
//将堆中的最后一个元素放入最小值的位置(根)
array[0] = array[currentSize-1];
//有效元素--
currentSize--;
//对数组进行处理,即删除最小值后,数组长度会发生变化的
array = Arrays.copyOf(array, array.length-1);
//从上到下调整堆结构
siftDown(0);
//将最小值返回
return i;
}
//由上向下调整堆结构;专业点称为下滤(percolate down)
private void siftDown(int x) {
int j = -1;//用于记录和x交换位置的坐标
int num = array[x];//num 用来记录x坐标的数字
//当x坐标有左子节点时进入
for(; getleft(x) < currentSize; x = j){
j = getleft(x); //默认j为左子节点的坐标
//如果右子节点元素<左子节点元素,就将j变为右子节点的坐标
if(array[getright(x)] < array[getleft(x)]){
// array[x] = array[getright(x)];
j = getright(x);
}
//如果子节点元素 < 目前的父节点元素,则将子节点赋值给父节点
if(array[j] < array[x]){
array[x] = array[j];
array[j] = num;
}
else
break;
}
array[x] =num;//x已经发生了变换
}
有必要提一句:堆的每个操作的时间复杂度都为O(logN)
完整代码
package Structures;
import java.util.Arrays;
/**
* @author Emma
* @create 2020 - 03 - 21 - 23:51
* 最小堆结构
*/
public class MinHeap {
//堆结构,由一个数组和一个代表当前堆的大小的数字组成
private static final int DEFAULT_CAPACITY = 1; //默认容量
private int[] array; //堆数组
private int currentSize; //堆中的有效元素数
/*
有两种构造方法:
1.是给定堆的容量后,再将元素一个一个insert进来
2.直接将一个数组赋值到堆里
*/
//构造函数
public MinHeap() {
this(DEFAULT_CAPACITY);
}
//指定堆的容量,默认是10
public MinHeap(int capacity) {
//有效元素为0
currentSize = 0;
//创建数组
array = new int[capacity];
}
//构造函数,直接将一个数组赋值到堆里
public MinHeap(int[] data) {
//有效元素为data中的所有元素
currentSize = data.length;
//创建数组
array = new int[data.length];
//赋值
int x = 0;
for(int i : data){
array[x++] = i;
}
}
/*
插入元素操作
1. 将元素插入堆中,要保持堆的堆序性质
2. 允许重复的元素
如何保持堆的堆序性质?
步骤:我们将新插入的元素放入下一个可用位置的空穴,如果放入后满足最小堆,则结束;
否则将空穴的父节点元素移入该空穴中,空穴则朝着根的方向移动一步,这样反复,直到满足最小根
*/
public void insert(int x){
//插入前得先判断数组的情况,如果数组现在是满的,则需要扩容
if(currentSize == array.length){
// enlargeArray(array.length +1);
array = Arrays.copyOf(array, array.length+1);
}
//将x放入到数组的空穴中
array[currentSize] = x;
//将x放入正确的位置上
siftUp(currentSize);
//有效个数++
currentSize++;
}
//由下向上调整堆的结构
private void siftUp(int x) {
if(x <= 0) return;
//如果目标节点小于父节点,则将父节点与目标节点对换
while(array[x] < array[getParent(x)]){
int temp = array[x];
array[x] = array[getParent(x)];
array[getParent(x)] = temp;
//每次交换完后改变控制变量x的坐标
x = getParent(x);//将父节点的左边赋值给x
//如果最后到了堆的根节点,则跳出循环
if(x <= 0)
break;
}
}
//找到最小的,根结点
public int findMin(){
//判断数组是否为空
if(isEmpty()){
throw new ArrayIndexOutOfBoundsException("数组为空,没有最小值");
}
return array[0];
}
/*
删除最小的
思路:当删除掉最小元素后,根结点处就成了空穴,为了维持最小堆,我们得进行一系列操作
首先因为删除了一个元素,为了维持最小堆,所以得移动数组的最后一个元素
最简单的做法是将最后一个元素放入空穴中,但这样显然不符合最小堆,所以我们的做法是将空穴的两个儿子中最小者放入空穴中
但这样一来,就又形成了一个空穴,我们按照上面的步骤,再将这个空穴的子节点中最小的放进来..直到最后删除成功
*/
public int deleteMin(){
int i = findMin();//找到最小值
//删除元素前得判断,是否数组就只有一个元素
if(currentSize == 1){
array = new int[DEFAULT_CAPACITY - 1];
return i;
}
//将堆中的最后一个元素放入最小值的位置(根)
array[0] = array[currentSize-1];
//有效元素--
currentSize--;
//对数组进行处理,即删除最小值后,数组长度会发生变化的
array = Arrays.copyOf(array, array.length-1);
//从上到下调整堆结构
siftDown(0);
//将最小值返回
return i;
}
//将x坐标,由上向下调整堆结构
private void siftDown(int x) {
int j = -1;//用于记录和x交换位置的坐标
int num = array[x];//num 用来记录x坐标的数字
//当x坐标有左子节点时进入
for(; getleft(x) < currentSize; x = j){
j = getleft(x); //默认j为左子节点的坐标
//如果右子节点元素<左子节点元素,就将j变为右子节点的坐标
if(array[getright(x)] < array[getleft(x)]){
// array[x] = array[getright(x)];
j = getright(x);
}
//如果子节点元素 < 目前的父节点元素,则将子节点赋值给父节点
if(array[j] < array[x]){
array[x] = array[j];
array[j] = num;
}
else
break;
}
array[x] =num;//x已经发生了变换
}
//展示
public void show(){
for(int i : array){
System.out.print(i+" ");
}
System.out.println();
}
//获取到输入结点的父节点的坐标
private int getParent(int i){
if(i <= 0) return -1;
return (i-1)>>1;
}
//获取到输入结点的左节点的坐标
private int getleft(int i){
return (i*2)+1;
}
//获取到输入结点的y右节点的坐标
private int getright(int i){
return (i*2)+2;
}
//判断数组是否为空
private boolean isEmpty() {
return currentSize == 0 || array.length == 0;
}
//增加数组的容量
/* private void enlargeArray(int newSize){
int[] old = array;
array = new int[newSize];
//复制
int i = 0;
for(int x : old){
array[i++] = x;
}
}*/
//减少数组的容量
/*private void enSmallArray(int newSize){
int[] old = array;
array = new int[newSize];
//复制
for(int i = 0; i <newSize; i++){
array[i] = old[i];
}
}*/
public static void main(String[] args) {
//测试方法
//定义一个空堆,并删除其中的最小值
MinHeap heap1 = new MinHeap();
heap1.show();
heap1.insert(1);
// heap1.insert(2);
heap1.show();
heap1.deleteMin();
// heap1.deleteMin(); //会报错,没有问题
heap1.show();
// System.out.println(heap1.deleteMin()); //会报错,没有问题
//定义一个堆,并将一个满足最小堆的数组赋值进去,这个数组的长度比默认容量大,可以自动扩容
int[] arr = {13,21,16,19,31,19,68,65,26,32};
MinHeap heap2 = new MinHeap(arr);
heap2.show();
heap2.insert(14);
heap2.show();//插入14后show
int[] arr2 = new int[]{13, 14, 16, 19, 21, 19, 68, 65, 26, 32, 31};
MinHeap heap3 = new MinHeap(arr2);
heap3.deleteMin();
heap3.show();
}
}