堆的概念
性质
堆一般分为大根堆和小根堆,该篇文章我统一以大根堆为例
大根堆:
- 堆总是一棵完全二叉树
- 每个节点都大于它的子节点,因此它的根节点是最大节点
结构
逻辑结构:
逻辑结构它是一颗完全二叉树
存储结构:
堆的存储一般用线性的数组存储
这里数组下标为0的位置不存数据
父节点与孩子节点的下标有如下规律:
- 若父节点下标为
n
,那么左子节点为2n
,右子节点为2n+1
- 若子节点下标为
n
,则父节点的下标一定是n/2
堆的相关操作
二叉堆的操作大致分三种:
- 调整堆(构建堆)
- 添加元素
- 取出堆顶元素
调整堆(构建堆)
例如给定一串数字: 5,25,15,30,70,50
逻辑结构为:
让这个完全二叉树变成一个大根堆,就是让每个子树都变成大根堆的过程,因此我们自右向左、自下向上保证每一棵子树都为大根堆
- 首先找到第一棵拥有孩子节点的子树,即下标为
size/2
的节点,size
指堆的大小
调整后
- 上一棵子树已满足大根堆,继续按照自右向左、自下向上的原则,依次保证每一棵子树为大根堆
调整后
上一棵子树已满足大根堆,继续按照自右向左、自下向上的原则,依次保证每一棵子树为大根堆
调整后
调整后
最终结果:
添加元素
添加元素是堆调整中唯一往上调整shiftUp的环节
因为添加元素是在前面的堆已经调好的情况下添加的,因此我们只需要
从添加节点位置开始,自下向上一个方向进行调整即可
假如添加的元素为80,先把80添加到尾部,再依次向上调整。如果当前节点大于父节点就交换位置,向上调整(shiftUp),依次向上调整直到满足当前节点小于父节点或者当前节点为根节点为止
- 将数据添加到大根堆的尾部
调整后
调整后
取出堆顶元素
堆只能从堆顶取元素。对于大根堆,每次取出的元素就是最大的元素。方法就是用堆中最后一个元素,覆盖堆顶元素,然后去掉最后一个元素。再次从上往下调整(shiftDown)。
- 先取出堆顶元素80
- 然后尾节点50覆盖顶节点80,删除尾部节点50
继续调整:
调整结束!
代码实现
创建一个大根堆类
里面维护一个数组、堆的大小等信息
class MaxHead {
private int[] headArr; // 存放数据的数组 (0下标不存值)
private int headSize=0; // 堆的大小
private int maxSize = 1000; // 数组最大值, 默认值1000
// 构造函数
public MaxHead(int maxSize) {
this.maxSize = maxSize;
this.headArr = new int[maxSize];
}
public MaxHead() {
this.headArr = new int[this.maxSize];
}
}
封装几个通用方法
打印数组信息:
/**
* 打印数组信息
* @param arr
*/
public static void print(int[] arr) {
for (int item: arr)
System.out.print(item+" ");
}
交换数组中两个数据的位置:
/**
* 交换数组中两个数据的位置
* @param arr
* @param idx1
* @param idx2
*/
public static void swap(int[] arr, int idx1, int idx2) {
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
向下调整(核心):
/**
* 向下调整 (因为向下调整的边界是head的大小 因此需要headSize这个参数)
* @param headArr:堆数组
* @param curIndex:当前节点的下标
* @param headSize: 堆的大小
*/
public void shiftDown(int[] headArr, int curIndex, int headSize) {
while (curIndex*2 <= headSize) { // 存在左孩子(存在子节点)
int left = 2*curIndex;
// 最大子节点下标默认值: 左孩子下标
int maxSon = left;
int right = left+1;
// 存在右孩子节点 且 右孩子大于左孩子
if (right <= headSize && headArr[right] > headArr[left]) {
maxSon = right;
}
// 如果当前节点小于最大的孩子节点 则交换位置
if (headArr[curIndex] < headArr[maxSon]) {
swap(headArr, curIndex, maxSon);
// 交换之后 当前节点下移
curIndex = maxSon;
}else
break;
}
}
向上调整(核心):
/**
* 向上调整
* @param headArr
* @param curIndex
*/
public static void shiftUp(int[] headArr, int curIndex) {
while (curIndex >1) { // 如果curIndex=1,则为根节点, 上移结束
// 父节点为 当前节点/2 curIndex>>1 等同于 curIndex/2
int fatherIndex = curIndex >> 1;
if (headArr[fatherIndex] < headArr[curIndex]) { // 父节点比当前节点小(不满足大根堆),向上调整
swap(headArr, curIndex, fatherIndex);
// 交换之后 当前节点上移
curIndex = fatherIndex;
} else
break;
}
}
调整堆代码
/**
* 根据传来数组,原地将他调整为一个大根堆
* @param arr
*/
public static void buildHead(int[] arr) { // 注意:传来的数组,index=0不存值
int headSize = arr.length-1;
for (int i=headSize/2; i>=1; i--) { // headSize所在的下标 即第1个存在孩子节点的子树节点下标
// 依次调整每一棵子树, 令其满足大根堆
shiftDown(arr, i, headSize);
}
}
添加元素代码
/**
* 添加元素
* @param value
*/
void put(int value) {
if (headSize == maxSize-1) {
System.out.println("堆已满!");
return;
}
/**
* 1、堆的大小+1 ++headSize
* 2、将插入的数放入堆尾 headArr[headSize] = num
*/
headArr[++headSize] = value;
// 操作数刚开始的下标是堆尾元素下标 curIndex = headSize
int curIndex = headSize;
// 重新调整为大根堆 (shiftUp)
shiftUp(this.headArr, curIndex);
}
取出堆顶元素代码
/**
* 获取堆顶元素 并移除
* @return
*/
int pop() {
if (headSize == 0) {
System.out.println("堆已空!");
return -1;
}
// 获取堆顶元素
int topValue = headArr[1];
/**
* 1、将堆尾部元素覆盖给arr[1]位置 headArr[1] = headArr[headSize]
* 2、将堆的大小减1 headSize--
*/
headArr[1] = headArr[headSize--];
// 因为队尾元素值已经覆盖到根节点位置 接下来的操作数便是这个值
int curIndex = 1;
// 重新调整为小根堆 (shiftDown)
shiftDown(this.headArr,curIndex, headSize);
return topValue;
}
完整代码
import java.util.Scanner;
class MaxHead {
private int[] headArr; // 存放数据的数组 (0下标不存值)
private int headSize=0; // 堆的大小
private int maxSize = 1000; // 数组最大值, 默认值1000
// 构造函数
public MaxHead(int maxSize) {
this.maxSize = maxSize;
this.headArr = new int[maxSize];
}
public MaxHead() {
this.headArr = new int[this.maxSize];
}
/**
* 打印数组信息
* @param arr
*/
public void print(int[] arr) {
for (int item: arr)
System.out.print(item+" ");
}
/**
* 交换数组中两个数据的位置
* @param arr
* @param idx1
* @param idx2
*/
public void swap(int[] arr, int idx1, int idx2) {
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
/**
* 向下调整 (因为向下调整的边界是head的大小 因此需要headSize这个参数)
* @param headArr:堆数组
* @param curIndex:当前节点的下标
* @param headSize: 堆的大小
*/
public void shiftDown(int[] headArr, int curIndex, int headSize) {
while (curIndex*2 <= headSize) { // 存在左孩子(存在子节点)
int left = 2*curIndex;
// 最大子节点下标默认值: 左孩子下标
int maxSon = left;
int right = left+1;
if (right <= headSize) { // 存在右孩子节点
if (headArr[right] > headArr[left])
maxSon = right;
}
// 如果当前节点小于最大的孩子节点 则交换位置
if (headArr[curIndex] < headArr[maxSon]) {
swap(headArr, curIndex, maxSon);
// 交换之后 当前节点下移
curIndex = maxSon;
}else
break;
}
}
/**
* 向上调整
* @param headArr
* @param curIndex
*/
public void shiftUp(int[] headArr, int curIndex) {
while (curIndex >1) { // 如果curIndex=1,则为根节点, 上移结束
// 父节点为 当前节点/2 curIndex>>1 等同于 curIndex/2
int fatherIndex = curIndex >> 1;
if (headArr[fatherIndex] < headArr[curIndex]) { // 父节点比当前节点小(不满足大根堆),向上调整
swap(headArr, curIndex, fatherIndex);
// 交换之后 当前节点上移
curIndex = fatherIndex;
} else
break;
}
}
/**
* 根据传来数组,原地将他调整为一个大根堆
* @param arr
*/
public void buildHead(int[] arr) { // 注意:传来的数组,index=0不存值
int headSize = arr.length-1;
for (int i=headSize/2; i>=1; i--) { // headSize所在的下标 即第1个存在孩子节点的子树节点下标
// 依次调整每一棵子树, 令其满足大根堆
shiftDown(arr, i, headSize);
}
}
/**
* 添加元素
* @param value
*/
void put(int value) {
if (headSize == maxSize-1) {
System.out.println("堆已满!");
return;
}
/**
* 1、堆的大小+1 ++headSize
* 2、将插入的数放入堆尾 headArr[headSize] = num
*/
headArr[++headSize] = value;
// 操作数刚开始的下标是堆尾元素下标 curIndex = headSize
int curIndex = headSize;
// 重新调整为大根堆 (shiftUp)
shiftUp(this.headArr, curIndex);
}
/**
* 获取堆顶元素 并移除
* @return
*/
int pop() {
if (headSize == 0) {
System.out.println("堆已空!");
return -1;
}
// 获取堆顶元素
int topValue = headArr[1];
/**
* 1、将堆尾部元素覆盖给arr[1]位置 headArr[1] = headArr[headSize]
* 2、将堆的大小减1 headSize--
*/
headArr[1] = headArr[headSize--];
// 因为队尾元素值已经覆盖到根节点位置 接下来的操作数便是这个值
int curIndex = 1;
// 重新调整为小根堆 (shiftDown)
shiftDown(this.headArr,curIndex, headSize);
return topValue;
}
public static void main(String[] args) {
MaxHead maxHead = new MaxHead(20);
for (int i=0; i<10; i++) {
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
maxHead.put(num);
for (int j=1; j<=maxHead.headSize; j++) {
System.out.print(maxHead.headArr[j]+" ");
}
System.out.println();
}
for (int i=0; i<10; i++) {
int minNum = maxHead.pop();
System.out.println("最大值为:"+minNum);
for (int j=1; j<=maxHead.headSize; j++) {
System.out.print(maxHead.headArr[j]+" ");
}
System.out.println();
}
}
}