文本目录:
❄️一、优先级队列:
➷ 1、概念:
❄️二、优先级队列的模拟实现:
➷ 1、堆的概念:
➷ 2、堆的性质:
➷ 3、堆的创建:
▶ 向下调整:
➷ 4、堆的插入和删除:
▶ 堆的插入:
☞ 思路:
☞ 代码:
▶ 堆的删除:
☞ 思路:
☞ 代码:
➷ 5、堆的判空和判满:
➷ 6、堆的对顶数据:
➷ 7、堆的总代码:
❄️总结:
❄️一、优先级队列:
➷ 1、概念:
我们在前面是介绍过队列的,队列是一种先进先出的数据结构,但是在有些情况下呢,我们操作的数据有优先级,这时候,在我们出队列的时候呢,可能需要先出优先级高的先出队列,那么在这种情况下呢,我们的队列就是有些不合适了,比如呢,我们在 玩游戏的时候呢,来个电话,是不是手机先处理电话,之后再处理游戏去。
在这种情况下,数据结构提供了两种最基本的操作:一个事返回优先级最高的对象,另一个是添加新的对象。这种操作呢就是 优先级队列——Priority Queue
❄️二、优先级队列的模拟实现:
在 JDK1.8 中呢,我们的 Priority Queue 的底层使用了堆的数据结构,而对于 堆呢就是在 完全二叉树 的基础上进行一些调整的。
➷ 1、堆的概念:
如果有一个关键码的集合 K = {K0,K1,K2,.......,Kn - 1},把它的所有数据都按照 完全二叉树的顺序存储方式存储 在一个一维数组中,并且满足:
Ki <= K2i + 1 且 Ki <= K2i + 2 (Ki >= K2i + 1 且 Ki >= K2i + 2) i = 0,1,2,3,4,5.......,
则称为 小堆(大堆)。
在这里我们 将根节点最大的堆称为 最大堆 或者 大根堆
将根节点最小的堆称为 最小堆 或者 小根堆
➷ 2、堆的性质:
• 堆中某个节点的值总是 不大于 或者 不小于 其父节点的值。
• 堆是一颗完全二叉树。
我们来看看 大根堆 和 小根堆 的逻辑结构和存储结构:
我们的存储方式是采用 层序存储 的 规则来进行存储到数组中。这样就非常的高效了。
我们要注意如果不是 完全二叉树 的情况下呢,我们的不能使用 层序遍历的存储规则,这样会浪费空间,所以如果不是完全二叉树不能这样做。
➷ 3、堆的创建:
我们在创建堆之前呢,我们先来做一些准备工作,我们要注意我们的堆的底层使用的是数组进行实现的。
我们将这些数据变成 大根堆 或者 小根堆 但是呢,我们要如何才能创建 大根堆 或者 小根堆 呢?
在创建 大根堆 或者 小根堆 之前呢,我们先把这个数组的值呢变成 完全二叉树 来看看:
我们红色的字体就是我们的数组的下标。这个就是我们这组数据的 完全二叉树 的逻辑结构。
那么我们要如何创建堆呢?这里呢我们要了解到一个知识点——向下调整。
▶ 向下调整:
我们这里是 从最后一颗子树进行调整,找到最后一颗子树的根节点,再比较这个根节点的左右节点谁大,我们把根节点和这个最大值进行调整,这样循环调整每一颗子树。这样就可以调整成大根堆。
这里呢,我们直接看图来理解这个问题:
这就是我们的大根堆的调整方法从 完全二叉树 调整。
这里我们需要一些必须要求的东西:
1、首先要找到最后一颗子树的位置,也就是最后一颗子树的根节点。
这里我们使用一个公式:(数组的长度 - 1 - 1)/ 2 就是我们的最后一颗子树的根节点。我们使用 parent 标记这个节点。
2、我们如何找到我们的下一颗子树的根节点位置?
这个就比较简单了,我们直接 parent-- 就可以了。直至我们的 parent 下标 >= 0 即可比如:
3、我们要知道我们的子树的 左子树 ,为什么是左子树呢?因为是由 完全二叉树 调整而来,如果 没有左子树就没有右子树,所以只需要找到左子树即可。
那么怎样去找呢?这里也有公式:child = parent * 2 + 1 ,就是我们左子树。但是这个呢我们还要找其左右子树最大的值,不是直接对child进行交换的。并且要注意我们的 child 要 小于 有效的数组长度。
4、我们怎样知道我们的调整结束了?
这里我们不能调整一次就结束,这里的结束条件就是,当我们的 左子树这个节点的下标比有效数组长度长的时候就是结束的时候了。
每次我们要对 child 和 parent 进行调整。parent = child child = parent * 2 + 1
我们来演示一变看看如何进行的:
这就是我们的 向下调整 成 大根堆 的操作。来看看这个 向下调整 的代码:
/**
*
* @param parent 每颗子树调整时候的起始位置
* @param usedSize 用来判断 每颗子树什么时候结束的
*/
private void shiftDown(int parent,int usedSize) {
int child = parent * 2 + 1;//parent根节点的左子树的节点
while (child < usedSize) {
//判断有没有右子树,如果有就把 child 设置为最大的值的下标
if (child + 1 < usedSize && elem[child + 1] > elem[child]) {
//右子树比左子树大把 child + 1,就是右子树
child += 1;
}
if (elem[parent] < elem[child]) {
//进行交换
swap(elem,child,parent);
//调整完,把 parent 和 child 进行调整位置
parent = child;
child = parent * 2 + 1;
}else {
//这是 parent下标的值大于child,跳出
break;
}
}
}
private void swap(int[] elem,int i,int j) {
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
这个呢就是我们的 向下调整 的 创建大根堆,对于我们的要是 创建 小根堆 的话就是把其小的数值进行调整就可以了,就是反过来进行调整。
我们这个时候来看看对于堆的创建,了解到 向下调整 之后呢,就是非常的容易了,我们呢就是把每一个 parent 进行 向下调整,这里就用到循环遍历就可以了。
我们来看代码:
//堆的创建
public void createHeap() {
for (int parent = (this.usedSize - 1 - 1) / 2; parent >= 0 ; parent--) {
shiftDown(parent,this.usedSize);
}
}
Ok啊,这样我们的 大根堆的创建 就创建完成了。
➷ 4、堆的插入和删除:
▶ 堆的插入:
☞ 思路:
对于我们的堆的插入的操做呢,我们的插入一定是在最后一个位置插入的,这个位置,我们一定是知道的,就是我们的 usedSize 这个下标的位置插入数据。我们就可以根据这个位置求出其插入的值的根节点的位置。
我们来看图来理解:
这个 90 就是我们新插入的值,我们再对其进行 向上调整,我们是知道插入值的下标,就可以计算到根节点的下标,就可以进行 向上调整了 。
再对其进行向上调整就可以了。
这里我们也要注意当我们的数组使用满了后,我们就需要扩容了。
这个就是插入的操作了。我们来看看代码是如何编写的:
☞ 代码:
//堆的插入
public void offer(int val) {
if (usedSize == elem.length) {
//满了。2倍扩容
this.elem = Arrays.copyOf(this.elem, 2 * this.elem.length);
}
//插入操作
this.elem[usedSize] = val;
//这个时候 usedSize 位置就是我们的子树的坐标位置
shiftUp(usedSize);
this.usedSize++;
}
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (parent >= 0) {
//进行向上调整
if (this.elem[parent] < this.elem[child]) {
//交换
swap(elem,child,parent);
//parent 和 child 调整位置
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}
OK,这个呢就是我们的插入操作的代码了之后我们来看看删除操作是如何做到的。
▶ 堆的删除:
☞ 思路:
这个删除操作还是很简单的,我们需要删除优先级高的数据,所以我们删除的就是 0 下标的值
我们呢有三个步骤:
1、把堆的 0 下标的数据 和 usedSize - 1 这个下标的数据进行交换,就是第一个数据和最后一个 数据进行交换。
2、我们把有效数组长度减一。
3、我们就是 0 这个下标不是大根堆的结构,所以我们对 0 这个下标进行 向下调整 操作。
这个呢就是我们删除的思路了,我们接下来看看其代码:
☞ 代码:
//堆的删除操作
public int poll() {
//删除优先级最高的就是0下标的值,有三个步骤:
//1、把堆的第一个数据和最后一个数据进行交换
//2、把有效数据长度进行减1
//3、把 0 下标的值进行向下调整
if (usedSize == 0) {
//堆为空,结束
return -1;
}
int val = elem[0];
//1、
swap(elem,0,usedSize - 1);
//2、
usedSize--;
//3、
shiftDown(0,usedSize);
return val;
}
➷ 5、堆的判空和判满:
这两个操作就非常的简单了,我们直接来看代码:
//判空
public boolean isEmpty() {
return usedSize == 0;
}
//判满
public boolean isFull() {
return usedSize == this.elem.length;
}
➷ 6、堆的对顶数据:
这个同样非常的简单,我们直接返回下标为 0 的值就可以了。
//返回堆顶的数据
public int peek() {
if (isEmpty()) {
return -1;
}
return this.elem[0];
}
这样呢我们所有的基本操作就都完成了,我们来看一下总代码:
➷ 7、堆的总代码:
import java.util.Arrays;
public class TestHeap {
public int[] elem;
public int usedSize;
public TestHeap() {
//初始话
this.elem = new int[10];
}
public void initElem(int[] array) {
//把elem里的值设置为我们输入的值
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
}
}
/**
* 建堆的时间复杂度为O(N)。
* 大根堆
* @param parent 每颗子树调整时候的起始位置
* @param usedSize 用来判断 每颗子树什么时候结束的
*/
private void shiftDown(int parent,int usedSize) {
int child = parent * 2 + 1;//parent根节点的左子树的节点
while (child < usedSize) {
//判断有没有右子树,如果有就把 child 设置为最大的值的下标
if (child + 1 < usedSize && elem[child + 1] > elem[child]) {
//右子树比左子树大把 child + 1,就是右子树
child += 1;
}
if (elem[parent] < elem[child]) {
//进行交换
swap(elem,child,parent);
//调整完,把 parent 和 child 进行调整位置
parent = child;
child = parent * 2 + 1;
}else {
//这是 parent下标的值大于child,跳出
break;
}
}
}
private void swap(int[] elem,int i,int j) {
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
//大根堆的创建
public void createHeap() {
for (int parent = (this.usedSize - 1 - 1) / 2; parent >= 0 ; parent--) {
shiftDown(parent,this.usedSize);
}
}
//堆的插入
public void offer(int val) {
if (isFull()) {
//满了。2倍扩容
this.elem = Arrays.copyOf(this.elem, 2 * this.elem.length);
}
//插入操作
this.elem[usedSize] = val;
//这个时候 usedSize 位置就是我们的子树的坐标位置
shiftUp(usedSize);
this.usedSize++;
}
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (parent >= 0) {
//进行向上调整
if (this.elem[parent] < this.elem[child]) {
//交换
swap(elem,child,parent);
//parent 和 child 调整位置
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}
//堆的删除操作
public int poll() {
//删除优先级最高的就是0下标的值,有三个步骤:
//1、把堆的第一个数据和最后一个数据进行交换
//2、把有效数据长度进行减1
//3、把 0 下标的值进行向下调整
if (isEmpty()) {
//堆为空,结束
return -1;
}
int val = elem[0];
//1、
swap(elem,0,usedSize - 1);
//2、
usedSize--;
//3、
shiftDown(0,usedSize);
return val;
}
//返回堆顶的数据
public int peek() {
if (isEmpty()) {
return -1;
}
return this.elem[0];
}
//判空
public boolean isEmpty() {
return usedSize == 0;
}
//判满
public boolean isFull() {
return usedSize == this.elem.length;
}
}
❄️总结:
OK,这篇博客呢就到这里就结束了,这篇博客我们介绍的是 优先级队列 的底层操作代码,下一篇博客我们来看看 Java 自带的 优先级队列吧~~并且还有一些关于我们的优先级队列的习题,敬请期待吧!!!拜拜·~~~