一篇搞懂优先级队列(堆)

下标关系(重要)


已知双亲(parent)的下标,则:

左孩子(left)下标 = 2 * parent + 1

右孩子(right)下标 = 2 * parent + 2

已知孩子(不区分左右)(child)下标,则:

双亲(parent)下标 = (child - 1) / 2(前提是在完全二叉树中根节点编号为0)

双亲(parent)下标 = child / 2(前提是在完全二叉树中根节点编号为1)

堆(heap)

======================================================================

概念


1.堆逻辑上是一棵完全二叉树

2.堆物理上是保存在数组

3.满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆

4.反之,则是小堆,或者小根堆,或者最小堆

在这里插入图片描述

5.堆的基本作用是,快速找集合中的最值,例如求最大值,最小值或者前k个最大/最小

操作–向下调整


此时我们想要将一个普通的堆转换成大根堆,那么该怎么转换呢?下面我们来看步骤

1:首先我们给出一个堆(本质上是一个完全二叉树):

在这里插入图片描述

2:然后我们要将这个完全二叉树转换成大根堆,方法是什么呢?

这块我们需要用到向下调整也就是从每一棵子树开始调整,每一棵子树是向下调整

那么首先我们从图中的最后这棵子树开始调整:

在这里插入图片描述

我们把这课小子树的父亲节点设为P,把这个父亲节点的左孩子设为C在这里插入图片描述

然后找P的左右孩子的最大值,发现是37,37>28,此时就将两者进行交换:

在这里插入图片描述

交换完毕后此时我们四号下标这个树就已经变成大根堆了

3:此时P指向我们的三号下标处,然后C指向三号下标的左子树也就是我们的七号下标处,为什么指向这里是因为我们通过之前的下标关系公式可以得出来:也就是三号下标对应的左子树下标为2 * 3+1=7,所以此时C指向了7号下标处:

在这里插入图片描述

然后此时确认一下七号下标的值和八号下标的值哪个大,发现七号下标的值为49,大于八号下标的值25,所以此时应该交换七号下标的值与三号下标的值,交换后如下所示:此时C指向18,P指向49

在这里插入图片描述

交换后还需要判断下三号下标这棵树是否已经全部调整完毕,所以此时P仍需要再往下走,指向我们的七号下标处,也就是C原来所指向的节点,而C此时指向的下标应该为2_7+1=15,结果发现我们排序后才到9,所以此时我们三号下标所代表的这课子树也完成了调整.

4:此时P又往前走,指向了我们的二号下标,细心的同学会发现P其实一直都是挨个往下减,那么C此时应该指向我们的2_2+1=5号下标:

在这里插入图片描述

然后此时判断左右子树哪个大,发现五号下标值为34,六号下标值为65,所以六号下标的地方的值大,所以C此时指向六号下标处:

在这里插入图片描述

然后发现六号下标比我们的二号下标的值大,所以此时应该交换六号下标和二号下标的值,如下所示:此时C指向19,P指向65

在这里插入图片描述

判断完成后还需要判断六号下标以下的子树是否是大根堆,所以此时让P指向我们的六号下标处,也就是C原来指向的节点,然后C此时指向的下标应该为2_6+1=13,但是我们这棵完全二叉树下标最大才到9,所以说明以65为根节点的这个子树已经调整完毕.

5:此时P继续往前走,走到了下标为1处的节点,而我们的C此时走到了下标为1_2+1=3的地方,如下所示:

在这里插入图片描述

然后下标为3的值为49,下标为4的值为37,49>37,所以C仍指向49,然后49>15,所以将49和15进行交换:此时C指向15,P指向49

在这里插入图片描述

交换完毕后此时仍要检验以49为根节点的这棵子树到底是否是一个大根堆,于是将P此时指向原来C所指向的下标为3处的15这个值,然后C指向了下标为2_3+1=7的这个下标所对应的值18.如下所示:

在这里插入图片描述

此时比较P所指向的节点的左右子树的值的大小,发现25大于18,所以此时C指向了25这个节点:

在这里插入图片描述

然后25>15,所以将P和C所指向的节点的值进行交换:

此时继续判断当前P所指向的节点下的二叉树是否是大根堆,于是P走到了C现在的节点,然后C应该走到下标为2_8+1=17的节点处,结果这棵二叉树按照层序遍历排下标最大才到9,所以以25为根节点的这棵子树算是遍历完毕了.

6:此时P继续回到我们的下标为1处开始往前走,此时P走到了0下标的位置,C此时应该走到0_2+1=1处的位置,如下图所示:

在这里插入图片描述

此时65>49,所以C应该指向65这个节点,如下图所示:

在这里插入图片描述

又因为65>27,所以此时交换各自的值:

在这里插入图片描述

然后P此时指向我们C指向的节点,然后C应该指向2_2+1=5这个下标所对应的节点,如下所示:

在这里插入图片描述

34>19,所以C仍指向34这个节点,34>27,所以交换这两个值:

在这里插入图片描述

然后P此时继续指向C所指向的节点,因为P所指向的节点的下标为5,所以C下次指向的节点的下标应该为2*5+1=11,但是我们这棵完全二叉树的下标最大才到9,所以以65为根节点的二叉树此时调整完毕

最后我们发现我们的这棵二叉树调整完毕,已经成为了大根堆.

代码示例

废话不多说我们直接上代码,大家注意要留意我们每一行的注释,因为精髓都在这些注释里面:

/**

  • @author SongBiao

  • @Date 2021/1/18

  • 此段代码用于将一个堆转变成大根堆

*/

public class HeapDemo {

//堆的底层存储是顺序数组

public int[] elem;

//表示我们堆中的元素个数,同时也是我们堆调整时结束的标志

public int usedSize;

//初始化的时候为堆的底层数组创建一个默认的大小

public HeapDemo() {

this.elem = new int[10];

}

/**

  • 注意在这里为什么要传len

  • 传len的目的是告诉堆调整结束时的时间,因为每颗子树在进行向下调整时最后结束的条件是一样的

  • 就是当下标值大于堆中元素个数-1的时候就停止调整了

  • 所以len就代表每次调整结束的位置,而我们传入的len的值其实就是usedSize

  • adjustDown的时间复杂度为O(log2(n))

  • @param parent

  • @param len

*/

//向下调整

public void adjustDown(int parent, int len) {

//获取当前根节点的左子树的下标值

int child = 2 * parent + 1;

//child<len的时候说明有左子树,但是未必有右子树

while (child < len) {

//注意要加上child+1<len这个操作,原因是可能会没有右孩子只有左孩子

if (child + 1 < len && this.elem[child] < this.elem[child + 1]) {

child++;

}

//代码如果走到这里就代表child此时一定是左右孩子中的最大值所对应的下标

if (this.elem[child] > this.elem[parent]) {

int tmp = this.elem[child];

this.elem[child] = this.elem[parent];

this.elem[parent] = tmp;

parent = child;

child = 2 * parent + 1;

} else {

/*因为是从最后一棵树开始调整的 只要我们 找到了

this.elem[child] <= this.elem[parent],就说明后续就不需要循环了

后面的都已经是大根堆了,所以直接break即可*/

break;

}

}

}

//创建一个堆

//createBigHeap方法的时间复杂度为O(nlogn)

//其实本质上来说建立一个大根堆和建立一个小根堆方法的时间复杂度为O(n)

public void createBigHeap(int[] array) {

for (int i = 0; i < array.length; i++) {

this.elem[i] = array[i];

this.usedSize++;

}

//此处的i其实就代表了我们的图中P每次的下标

//this.usedSize - 1 - 1是为了获取我们堆中最后一棵子二叉树的父亲节点的下下标

//第一次减1是因为我们按照层序遍历拍序号的时候我们是从0开始编号的,而第二次减1是已知子节点求父节点下标的公式

//代表我们从最后一刻子树开始调整

for (int i = (this.usedSize - 1 - 1) / 2; i >= 0; i–) {

adjustDown(i, this.usedSize);

}

}

//打印我们调整后的结果

public void show() {

for (int i = 0; i < usedSize ; i++) {

System.out.print(this.elem[i]+ " ");

}

System.out.println();

}

}

测试类:

public class TestDemo {

public static void main(String[] args) {

HeapDemo demo = new HeapDemo();

//建立我们想要调整的堆

int[] array = { 27,15,19,18,28,34,65,49,25,37};

//调整前的数组结果为[27, 15, 19, 18, 28, 34, 65, 49, 25, 37]

System.out.println(Arrays.toString(array));

//创建我们的大根堆

demo.createBigHeap(array);

//调整后结果为65 49 34 25 37 27 19 18 15 28

demo.show();

}

}

注意事项:

1:createBigHeap方法为创建大根堆的方法,无论是创建大根堆还是小根堆的方法,其时间复杂度可以粗略估算为在循环中执行向下调整,所以为O(nlogn),但是实际上其时间复杂度为O(n),这个点建议大家直接死记硬背,至于感兴趣的朋友想知道这个O(n)怎么推出来的话,可以直接戳这个链接:

戳我进入知乎

2:adjustDown方法是我们向下调整的方法,也是我们的核心方法,即将一个堆如何编程大根堆的方法.注意这个方法有两个参数,一个是parent,一个是len,parent是将每次要调整的子树的根节点传入,而len的目的是为了作为调整结束的条件,即当child的下标大于len的值的时候就不再调整了.

最后我们向下调整的方法时间复杂度为O(logn),因为最坏的情况是从根一路比较到叶子,比较的次数为完全二叉树的高度

即时间复杂度为 O(log(n))

扩展

刚才我们是实现了将一个堆变成大根堆的方法,那么可不可以将一个堆变成小根堆呢?答案当然是可以的啦:我们只需要修改adjustDown方法即可,将里面的大于号和小于号改下即可:

/**

  • @author SongBiao

  • @Date 2021/1/18

  • 此段代码用于将一个堆转变成大根堆

*/

public class HeapDemo {

//堆的底层存储是顺序数组

public int[] elem;

//表示我们堆中的元素个数,同时也是我们堆调整时结束的标志

public int usedSize;

//初始化的时候为堆的底层数组创建一个默认的大小

public HeapDemo() {

this.elem = new int[10];

}

/**

  • 注意在这里为什么要传len

  • 传len的目的是告诉堆调整结束时的时间,因为每颗子树在进行向下调整时最后结束的条件是一样的

  • 就是当下标值大于堆中元素个数-1的时候就停止调整了

  • 所以len就代表每次调整结束的位置,而我们传入的len的值其实就是usedSize

  • adjustDown的时间复杂度为O(log2(n))

  • @param parent

  • @param len

*/

//向下调整

public void adjustDown(int parent, int len) {

//获取当前根节点的左子树的下标值

int child = 2 * parent + 1;

//child<len的时候说明有左子树,但是未必有右子树

while (child < len) {

//注意要加上child+1<len这个操作,原因是可能会没有右孩子只有左孩子

if (child + 1 < len && this.elem[child] > this.elem[child + 1]) {

child++;

}

//代码如果走到这里就代表child此时一定是左右孩子中的最大值所对应的下标

if (this.elem[child] < this.elem[parent]) {

int tmp = this.elem[child];

this.elem[child] = this.elem[parent];

this.elem[parent] = tmp;

parent = child;

child = 2 * parent + 1;

} else {

/*因为是从最后一棵树开始调整的 只要我们 找到了

this.elem[child] <= this.elem[parent],就说明后续就不需要循环了

后面的都已经是大根堆了,所以直接break即可*/

break;

}

}

}

//创建一个堆

//createBigHeap方法的时间复杂度为O(nlogn)

//其实本质上来说建立一个大根堆和建立一个小根堆方法的时间复杂度为O(n)

public void createBigHeap(int[] array) {

for (int i = 0; i < array.length; i++) {

this.elem[i] = array[i];

this.usedSize++;

}

//此处的i其实就代表了我们的图中P每次的下标

//this.usedSize - 1 - 1是为了获取我们堆中最后一棵子二叉树的父亲节点的下下标

//第一次减1是因为我们按照层序遍历拍序号的时候我们是从0开始编号的,而第二次减1是已知子节点求父节点下标的公式

//代表我们从最后一刻子树开始调整

for (int i = (this.usedSize - 1 - 1) / 2; i >= 0; i–) {

adjustDown(i, this.usedSize);

}

}

//打印我们调整后的结果

public void show() {

for (int i = 0; i < usedSize ; i++) {

System.out.print(this.elem[i]+ " ");

}

System.out.println();

}

}

测试类:

public class TestDemo {

public static void main(String[] args) {

HeapDemo demo = new HeapDemo();

//建立我们想要调整的堆

int[] array = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};

//调整前的数组结果为[27, 15, 19, 18, 28, 34, 65, 49, 25, 37]

System.out.println(Arrays.toString(array));

//创建我们的小根堆,并进行向下调整

demo.createBigHeap(array);

//经历过向下调整的全新的小根堆为15 18 19 25 28 34 65 49 27 37

demo.show();

}

}

堆的应用-优先级队列

=========================================================================

概念


在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次 高的对象。最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。

在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这 种数据结构就是优先级队列(Priority Queue)

内部原理


优先级队列的实现方式有很多,但最常见的是使用来构建

java集合中的优先级队列(PriorityQueue)


在这里插入图片描述

常用方法

在这里插入图片描述

当然我们PriorityQueue这个实现类还有很多其他方法,例如其自己实现的向上调整方法siftUp或者向下调整方法siftDown等等,包括其自增长方法grow等等,感兴趣同学可以自己下来看看

接下来我们可以看到我们的优先级队列PriorityQueue实现了我们的Queue接口,下面我们先来看一段代码:

public class TestDemo {

public static void main(String[] args) {

Queue priorityQueue = new PriorityQueue<>();

//每次插入都会进行向上调整,保证最后是一个小根堆

priorityQueue.offer(15);

priorityQueue.offer(45);

priorityQueue.offer(35);

priorityQueue.offer(2);

priorityQueue.offer(44);

priorityQueue.offer(67);

priorityQueue.offer(78);

priorityQueue.offer(89);

//输出结果为2

System.out.println(priorityQueue.peek());

//我们把当前的队头元素弹出

priorityQueue.poll();

//输出结果为15

System.out.println(priorityQueue.peek());

}

}

按照正常队列来说我们使用peek方法获取队头元素应该为15,而此处的输出结果竟然为2,是所有插入到优先级队列中最小的那个数字,从这里我们可以得出结论,在集合中,我们的优先级队列的底层其实默认为一个小根堆,即我们的优先级队列每次存元素的时候,一定都会保证数据进入堆中后,依然可以维持成一个小堆.,每次取出一个元素的时候,也一定会保证剩下的元素调整为一个小堆.

扩展(将底层默认的小根堆改为大根堆)

我们知道java集合当中的优先级队列PriorityQueue其底层默认是一个小根堆,但是我们就想让其变成大根堆,该怎么办呢?

此时就用到了我们的比较器,也就是我们的Comparator接口,如下所示:

在这里插入图片描述

那么下面我们直接来看代码示例:

public class TestDemo {

public static void main(String[] args) {

//此时我们PriorityQueue底层默认是一个小根堆,此时我们想要将其变成一个大根堆就需要用到Comparator接口

//括号里面用到的其实是匿名内部类

Queue qu = new PriorityQueue<>(new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

//这样比较的话就是大根堆

return o2-o1;

}

});

//每插入一个元素就要进行向上调整

qu.offer(3);

qu.offer(1);

qu.offer(2);

qu.offer(40);

qu.offer(6);

qu.offer(5);

qu.offer(7);

qu.offer(9);

qu.offer(13);

qu.offer(12);

//因为此时调整结果为40

System.out.println(qu.poll());

//结果为13

System.out.println(qu.poll());

//结果为12

System.out.println(qu.poll());

}

}

注意事项

1:想要将PriorityQueue底层默认的小根堆改为大根堆,那么就需要使用到我们的Comparator接口,注意代码中的写法属于匿名内部类的写法,后面我们会讲到,大家在这里只需要记住该写法即可

2:需要注意的是,我们的return方法返回的是o2-o1,只有返回这个的时候我们PriorityQueue底层默认才是大根堆,当返回o1-o2的时候底层默认是小根堆,这种写法大家也只需要死记硬背即可.

实战模拟


虽然我们java集合中已经封装了对于优先级队列入队和出队的操作,但是在这里我们还是自己来动手实现一下入队和出队操作,方便大家的理解

注意:在集合中的优先级队列(堆)的出队和入队操作都是针对于其底层的小根堆来操作的,本文所有的例子都是以大根堆进行操作的,但是原理其实是相同的,希望大家注意.

入队列(向上调整)(入堆)

当我们向优先级队列中插入元素的时候,我们的优先级队列一定会再次进行调整,变成新的大根堆或者小根堆.所以此处我们来实现这个调整的过程,我们此处还是接着刚才我们所调整过后的大根堆,在它的基础上我们来继续做实验:

在这里插入图片描述

此处我们想在现在这个大根堆中插入100这个值,那么首先先把100这个值插入数组的最后面:

在这里插入图片描述

然后开始进行我们的调整,与之前我们堆变成大根堆不同的是,之前的堆完全不是一个大根堆,而我们现在的这个堆是在一个本身就是大根堆的基础上添加了元素后的堆,那么将这个堆调整成大根堆的过程与之前将一个完全不是大根堆的一个堆调整成大根堆的过程在调整的过程上还是有点区别的前者为向上调整,后者为向下调整,下面来看向上调整的过程:

1:

在这里插入图片描述

首先是让P指向我们的37,然后C指向我们的100,在程序中我们是只知道新插入的100这个值的下标,那么怎么获得其双亲结点37的下标呢?其实非常简单,就是使用之前的下标公式parent = (child-1)/2便可以获得,然后100>37,直接互换如下图所示:

在这里插入图片描述

2:然后新的C就指向我们当前P的位置,也就是4这个下标处,然后我们P需要指向的下标为1这个下标处,还是利用双亲结点公式便可以获取到P所应指向的结点:

在这里插入图片描述

此时100>49,则交换两个值:

在这里插入图片描述

3:此时C指向1下标处,P指向0下标处 :

在这里插入图片描述

发现100>65,所以继续交换:

在这里插入图片描述

最后C指向P,C此时的下标为0,而P指向的下标为(0-1)/2=负数,负数不存在,那么P为负数就代表最终我们调整完毕,此时我们的优先级队列在插入元素后,又及时调整成了一个大根堆,接下来我们来看代码实现吧。

代码预期结果:

调整完后的大根堆按照层序遍历的结果为:

100 65 34 25 49 27 19 18 15 28 37

代码实现

首先实现我们的入队方法push和向上调整的adjustUp方法

package heap;

import java.util.Arrays;

public class HeapDemo {

//堆的底层存储是顺序数组

public int[] elem;

//表示我们堆中的元素个数,同时也是我们堆调整时结束的标志

public int usedSize;

//初始化的时候为堆的底层数组创建一个默认的大小

public HeapDemo() {

this.elem = new int[10];

}

/**

  • 注意在这里为什么要传len

  • 传len的目的是告诉堆调整结束时的时间,因为每颗子树在进行向下调整时最后结束的条件是一样的

  • 就是当下标值大于堆中元素个数-1的时候就停止调整了

  • 所以len就代表每次调整结束的位置,而我们传入的len的值其实就是usedSize

  • adjustDown的时间复杂度为O(log2(n))

  • @param parent

  • @param len

*/

//向下调整

public void adjustDown(int parent, int len) {

//获取当前根节点的左子树的下标值

int child = 2 * parent + 1;

//child<len的时候说明有左子树,但是未必有右子树

while (child < len) {

//注意要加上child+1<len这个操作,原因是可能会没有右孩子只有左孩子

if (child + 1 < len && this.elem[child] < this.elem[child + 1]) {

child++;

}

//代码如果走到这里就代表child此时一定是左右孩子中的最大值所对应的下标

if (this.elem[child] > this.elem[parent]) {

int tmp = this.elem[child];

this.elem[child] = this.elem[parent];

this.elem[parent] = tmp;

parent = child;

child = 2 * parent + 1;

} else {

/*因为是从最后一棵树开始调整的 只要我们 找到了

this.elem[child] <= this.elem[parent],就说明后续就不需要循环了

后面的都已经是大根堆了,所以直接break即可*/

break;

}

}

}

//创建一个堆

//createBigHeap方法的时间复杂度为O(nlogn)

//其实本质上来说建立一个大根堆和建立一个小根堆方法的时间复杂度为O(n)

public void createBigHeap(int[] array) {

for (int i = 0; i < array.length; i++) {

this.elem[i] = array[i];

this.usedSize++;

}

//此处的i其实就代表了我们的图中P每次的下标

//this.usedSize - 1 - 1是为了获取我们堆中最后一棵子二叉树的父亲节点的下下标

//第一次减1是因为我们按照层序遍历拍序号的时候我们是从0开始编号的,而第二次减1是已知子节点求父节点下标的公式

//代表我们从最后一刻子树开始调整

for (int i = (this.usedSize - 1 - 1) / 2; i >= 0; i–) {

adjustDown(i, this.usedSize);

}

}

/**

  • push方法作用为向一个大根堆中插入元素,并将插入元素后的堆继续调整为大根堆

*/

public void push(int val) {

if (isFull()) {

//如果堆底层的数组满了就进行扩容为原来的二倍

this.elem = Arrays.copyOf(this.elem, this.elem.length * 2);

}

this.elem[this.usedSize] = val;

//此时插入一个元素后原来的usedSize为10,现在变成了11

this.usedSize++;

//进行向上调整

//此时这个元素插入到了数组的最后一个位置处,传入的应该是其下标为usedSize-1

adjustUp(this.usedSize-1);

}

//判断当前堆是否已满

public boolean isFull() {

return this.usedSize == this.elem.length;

}

//向上调整

public void adjustUp(int child) {

int parent = (child - 1) / 2;

while (child > 0) {

if (this.elem[child] > this.elem[parent]) {

int tmp = this.elem[child];

this.elem[child] = this.elem[parent];

this.elem[parent] = tmp;

child = parent;

parent = (child - 1) / 2;

} else {

break;

}

}

}

//打印我们调整后的结果

public void show() {

for (int i = 0; i < usedSize; i++) {

System.out.print(this.elem[i] + " ");

}

System.out.println();

}

}

测试类:

public class TestDemo {

public static void main(String[] args) {

HeapDemo demo = new HeapDemo();

//建立我们想要调整的堆

int[] array = { 27,15,19,18,28,34,65,49,25,37};

//调整前的数组结果为[27, 15, 19, 18, 28, 34, 65, 49, 25, 37]

System.out.println(Arrays.toString(array));

//创建我们的大根堆

demo.createBigHeap(array);

//此时向我们已经创建好的大根堆内再次插入一个元素100

demo.push(100);

//调整后结果为100 65 34 25 49 27 19 18 15 28 37

demo.show();

}

}

关于向上调整方法adjustUp的时间复杂度为O(log2(n)).

出队列(出堆)

在优先级队列中出队列的那个元素一定是这个队列中优先级最高的那个元素,而在集合中的优先级队列因为其底层是一个小根堆,所以每次出队的元素一定是所有元素中值最小的那个,因为本文是按照大概堆实现的,所以出队的那个元素一定是值最大的那个元素.

我们还是拿之前的大根堆来举例子,模拟出队的过程:

在这里插入图片描述

此时对于这个大根堆来说,当我们执行出队操作的时候,出队的元素一定是65,并且出队后仍需要保持当前的堆是一个大根堆

那么我们的思路是这样的:

1:将我们第一个元素和最后一个元素进行交换:交换后删除最后一个元素即完成了我们出队列的第一步操作jiaohaun

2:当然我们出队可不能光说是将元素出队了就没事了,同时还要保证我们剩下的堆仍然是一个大根堆,这就到了我们的第二步,进行向下调整,并且只需要调整0号下标就好了

所以最终当我们将65出队后,进行完向下调整后,最后的结果如下所示:

在这里插入图片描述

下面我们来完成我们的出队方法poll

代码示例

package heap;

public class HeapDemo {

//堆的底层存储是顺序数组

public int[] elem;

//表示我们堆中的元素个数,同时也是我们堆调整时结束的标志

public int usedSize;

//初始化的时候为堆的底层数组创建一个默认的大小

public HeapDemo() {

this.elem = new int[10];

}

/**

  • 注意在这里为什么要传len

  • 传len的目的是告诉堆调整结束时的时间,因为每颗子树在进行向下调整时最后结束的条件是一样的

  • 就是当下标值大于堆中元素个数-1的时候就停止调整了

  • 所以len就代表每次调整结束的位置,而我们传入的len的值其实就是usedSize

  • adjustDown的时间复杂度为O(log2(n))

  • @param parent

  • @param len

*/

//向下调整

public void adjustDown(int parent, int len) {

//获取当前根节点的左子树的下标值

int child = 2 * parent + 1;

//child<len的时候说明有左子树,但是未必有右子树

while (child < len) {

//注意要加上child+1<len这个操作,原因是可能会没有右孩子只有左孩子

if (child + 1 < len && this.elem[child] < this.elem[child + 1]) {

child++;

}

//代码如果走到这里就代表child此时一定是左右孩子中的最大值所对应的下标

if (this.elem[child] > this.elem[parent]) {

int tmp = this.elem[child];

this.elem[child] = this.elem[parent];

this.elem[parent] = tmp;

parent = child;

child = 2 * parent + 1;

} else {

/*因为是从最后一棵树开始调整的 只要我们 找到了

this.elem[child] <= this.elem[parent],就说明后续就不需要循环了

后面的都已经是大根堆了,所以直接break即可*/

break;

}

}

}

//创建一个堆

//createBigHeap方法的时间复杂度为O(nlogn)

//其实本质上来说建立一个大根堆和建立一个小根堆方法的时间复杂度为O(n)

public void createBigHeap(int[] array) {

for (int i = 0; i < array.length; i++) {

this.elem[i] = array[i];

this.usedSize++;

}

//此处的i其实就代表了我们的图中P每次的下标

//this.usedSize - 1 - 1是为了获取我们堆中最后一棵子二叉树的父亲节点的下下标

//第一次减1是因为我们按照层序遍历拍序号的时候我们是从0开始编号的,而第二次减1是已知子节点求父节点下标的公式

//代表我们从最后一刻子树开始调整

for (int i = (this.usedSize - 1 - 1) / 2; i >= 0; i–) {

adjustDown(i, this.usedSize);

}

}

//出队方法

public int poll() {

if (isEmpty()) {

throw new RuntimeException(“队列为空”);

}

//使用ret保存我们要删除的元素

int ret = this.elem[0];

//第一步,将第一个元素和最后一个元素进行交换

int tmp = this.elem[0];

this.elem[0] = this.elem[this.usedSize - 1];

this.elem[this.usedSize - 1] = tmp;

//–后的usedSize的值为9

this.usedSize–;

//第二步,将剩下的堆仍然变成大根堆,进行向下调整,并且只对0号下标进行调整

adjustDown(0, this.usedSize);

return ret;

}

//判断当前的堆是否为空

public boolean isEmpty() {

return this.usedSize == 0;

}

//打印我们调整后的结果

public void show() {

for (int i = 0; i < usedSize; i++) {

System.out.print(this.elem[i] + " ");

}

System.out.println();

}

}

测试类:

public class TestDemo {

public static void main(String[] args) {

HeapDemo demo = new HeapDemo();

//建立我们想要调整的堆

int[] array = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};

//调整前的数组结果为[27, 15, 19, 18, 28, 34, 65, 49, 25, 37]

System.out.println(Arrays.toString(array));

//创建我们的大根堆,并进行向下调整

demo.createBigHeap(array);

//经历过向下调整的全新的大根堆为65 49 34 25 37 27 19 18 15 28

demo.show();

//此时出队列的结果应该为65

System.out.println(demo.poll());

//出队后调整的结果为49 37 34 25 28 27 19 18 15

//可以看出出队后会自行调整,继续变成一个大根堆

demo.show();

}

}

堆的其他应用-TopK 问题(经常出现)

===================================================================================

拜托,面试别再问我TopK了!!!

关键记得,找前 K 个最大的,要建 K 个大小的小堆

基本方法


要求找前K个最小的元素 当然是要建大堆

要求找前K个最大的元素,当然是要建小堆

假设此时有10个元素,K=3

此时会引申出来两个问题:

1:为什么找前K个最小的元素 要建大堆?

为什么找前K个最大的元素,要建小堆?

2:堆的大小是多少:答:堆的大小为K的大小,也就是说K的值为多少,我们所建立的堆中的元素就有多少个

举例1(找前k个最小/最大的元素)

此时我们十个元素,如下所示:

在这里插入图片描述

此时我们要寻找这个堆里前3个最大的元素,那么此时堆的大小就为3,并且我们要建的是一个小根堆,下面来看步骤:

1:既然堆的大小为3,那么就让27,15,19这三个元素先组成小堆:组成的小堆如下所示:

在这里插入图片描述

2:下来到了18,然后让18与我们当前优先级队列的队头元素,也就是15去进行比较,发现18>15,那么就先让堆顶元素出堆,出堆的方法我们之前在出队列的时候已经讲过了,也就是交换我们的15与我们最后一个元素19,然后让15出堆即可:

在这里插入图片描述

发现此时仍是一个小堆后,将18入堆:

在这里插入图片描述

然后再调整为一个小根堆:

在这里插入图片描述

3:然后此时到了28,还是按照刚才18与15的比较方式去进行比较,现在需要比较的是28和18,还是刚才的方法往下比较即可,注意需要一直比较到37才能结束,此处我们就省略中间比较的过程,最后在我们堆中留下来的三个元素就是我们当前这个堆中前三个最大的元素:大家下来可以自己在草稿纸上自行演算,最后我们得出的结果为:

在这里插入图片描述

插入37后我们还会调整一次:最终结果如下所示:

在这里插入图片描述

下面大家想一想关于找堆中前K个最大/最小个元素的这个方法的时间复杂度是多少:

首先我们所建立的堆的大小是我们K值的大小,也就是说这个堆中的元素有K个,那么堆的高度为log2(K),又因为这个比较是在不断遍历元素与堆进行比较的,所以最终的时间复杂度为nlog2(k)(n为我们最原始堆中的元素个数)

代码实现

此处我们来实现找前K个最大的元素:来看我们的代码:

public class TestDemo {

public static void topK(int[] array, int k) {

//此时我们PriorityQueue底层默认是一个小根堆

//此时创建一个大小为K的小根堆,因为是找前K个最大的元素

Queue qu = new PriorityQueue<>(k, new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

//这样比较的话就是小根堆

return o1 - o2;

}

});

//2:遍历数组

for (int i = 0; i < array.length; i++) {

if (qu.size() < k) {

qu.offer(array[i]);

} else {

if (qu.peek() < array[i]) {

qu.poll();

qu.offer(array[i]);

}

}

}

for (int i = 0; i < k; i++) {

System.out.println(qu.poll());

}

}

public static void main(String[] args) {

int[] array = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};

//此时我们传入的k值为3,也就是找前三个最大的元素

//最后的结果得出前三个最大元素为37,49,65

topK(array, 3);

}

}

如果此时我们想找前K个最小的元素,方法该怎么写呢?来看代码:

public class TestDemo {

public static void topK(int[] array, int k) {

//此时我们PriorityQueue底层默认是一个小根堆

//此时创建一个大小为K的大根堆

Queue qu = new PriorityQueue<>(k, new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

//这样比较的话就是大根堆

return o2 - o1;

}

});

//2:遍历数组

for (int i = 0; i < array.length; i++) {

if (qu.size() < k) {

qu.offer(array[i]);

} else {

//注意此处改成大于号

if (qu.peek() > array[i]) {

qu.poll();

qu.offer(array[i]);

}

}

}

for (int i = 0; i < k; i++) {

//每次出队列都会继续构建大根堆

System.out.println(qu.poll());

}

}

public static void main(String[] args) {

int[] array = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};

//最后的结果得出前三个最大元素为19,18,15

topK(array, 3);

}

}

因为是找前K个最小的元素,所以每次队头元素与数组遍历元素在进行比较的时候,当数组遍历的元素小于队头元素的时候,将队头元素删掉,并将我们数组遍历的元素入堆.并且在最开始建堆的时候一定建的是大堆,所以是return o2-o1。

举例2(找第K个最大/最小元素)

找第K个最大元素

下面仍是给定刚才的数组,找出这个数组第K个最大元素,其实仍然是紧接着刚才找前K个最大元素的代码,当找到前K个最大元素 后,此时我们的堆顶元素就是我们第K个最大的元素

在这里插入图片描述

例如刚才的数组,想要找到第三大的元素,我们拿肉眼可以看出来第三大的元素为37,那么要想找到第三大的元素,首先需要找到前三个最大的元素,我们在之前已经找到关于这个数组前三个最大的元素为37,49,65,堆图为:

在这里插入图片描述

可以看到37此时为堆顶元素,所以当数组遍历完成后,堆顶元素37就是第三大的元素

代码示例

public class TestDemo {

public static void topK(int[] array, int k) {

//此时我们PriorityQueue底层默认是一个小根堆

//此时创建一个大小为K的小根堆

Queue qu = new PriorityQueue<>(k, new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

//这样比较的话就是小根堆

return o1 - o2;

}

});

//2:遍历数组

for (int i = 0; i < array.length; i++) {

if (qu.size() < k) {

qu.offer(array[i]);

} else {

//注意此处改成小于号

if (qu.peek() < array[i]) {

qu.poll();

qu.offer(array[i]);

}

}

}

//打印我们第三个最大元素为37

System.out.println(qu.peek());

}

public static void main(String[] args) {

int[] array = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};

//最后的结果为37

topK(array, 3);

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

分享

1、算法大厂——字节跳动面试题

2、2000页互联网Java面试题大全

3、高阶必备,算法学习

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

public class TestDemo {

public static void topK(int[] array, int k) {

//此时我们PriorityQueue底层默认是一个小根堆

//此时创建一个大小为K的小根堆

Queue qu = new PriorityQueue<>(k, new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

//这样比较的话就是小根堆

return o1 - o2;

}

});

//2:遍历数组

for (int i = 0; i < array.length; i++) {

if (qu.size() < k) {

qu.offer(array[i]);

} else {

//注意此处改成小于号

if (qu.peek() < array[i]) {

qu.poll();

qu.offer(array[i]);

}

}

}

//打印我们第三个最大元素为37

System.out.println(qu.peek());

}

public static void main(String[] args) {

int[] array = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};

//最后的结果为37

topK(array, 3);

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-8MVTESCU-1712885732260)]
[外链图片转存中…(img-qSDeuzNb-1712885732261)]
[外链图片转存中…(img-gXdyS9rv-1712885732261)]
[外链图片转存中…(img-Ti91Ceh6-1712885732261)]
[外链图片转存中…(img-hL9FcolM-1712885732262)]
[外链图片转存中…(img-zOZCCfa7-1712885732262)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-nAPe1OBi-1712885732263)]

分享

1、算法大厂——字节跳动面试题

[外链图片转存中…(img-GmvMmqJA-1712885732263)]

2、2000页互联网Java面试题大全

[外链图片转存中…(img-A7QZJalE-1712885732263)]

3、高阶必备,算法学习

[外链图片转存中…(img-cVPQnuVN-1712885732264)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-SR3GWuDU-1712885732264)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值