Java 详细讲解用堆解决Top

网友投稿 275 2022-08-04


Java 详细讲解用堆解决Top

目录1、什么是堆?堆结构大根堆 VS 小根堆大根堆(最大堆)小根堆(最小堆)优先级队列(PriorityQueue)2、top-k问题解决思路总结:

要解决 top-k 问题,我们应该先熟悉一种数据结构 - 堆(优先级队列),已经了解的朋友可以跳过哦。

1、什么是堆?

堆结构

堆其实就是一种二叉树,但是普通的二叉树是以链式结构进行储存数据的,而堆是以数组进行顺序存储数据的。那么什么样的二叉树才适合用顺序存储的方式呢?

我们假设一颗普通的二叉树可以用数组存储,那么就可以得到如下结构:

我们可以看到,当二叉树中间有空值时,数组的存储空间会被浪费,那么什么情况下空间才不会被浪费呢? 那就是完全二叉树。

从以上结构中,我们不能用链式结构的指针来访问孩子节点或者父亲节点,只能通过对应下标来访问,其实也比较简单。

例如下图:

已知 2 节点的下标是1,那么

他的左孩子下标是:2 * 2 + 1 = 3

他的右孩子下标是:2 * 2 + 2 = 4

相反,已知 1 节点的下标是3,3 节点的下标是4,那么

1 节点的父亲节点下标是:(3 - 1) / 2 = 1

3 节点的父亲节点下标是:(4 - 1) / 2 = 1

大根堆 VS 小根堆

大根堆(最大堆)

大根堆保证,每颗二叉树的根节点都 大于 左右孩子节点

从最后一棵子树的根节点开始调整,来到每颗子树的根节点,使得每棵子树都向下调整为大根堆,最后再向下做最后调整,保证二叉树整体是大根堆(这个调整主要是为了后面的堆排序)。

具体调整过程如下:

怎么用代码实现呢?

我们首先从最后一棵子树调整,那么就要拿到最后一颗子树的根节点 parent ,我们知道数组最后一个节点下标是 len - 1,而这个节点是最后一棵子树的左孩子或者右孩子,根据孩子下标就可以拿到根节点下标( parent ) ,parent-- 就可以让每颗子树都进行调整,直到来到根节点,再向下调整最后一次,便可以得到大根堆。

// 将数组变成大根堆结构

public void createHeap(int[] arr){

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

elem[i] = arr[i];// 放入elem[],假设不需要扩容

usedSize++;

}

// 得到根节点parent, parent--依次来到每颗子树的根节点,

for (int parent = (usedSize-1-1)/2; parent >= 0; parent--) {

// 依次向下搜索,使得每颗子树都变成大根堆

shiftDown(parent,usedSize);

}

}

// 向下搜索变成大根堆

public void shiftDown(int parent,int len){

eGroZint child = parent*2+1;// 拿到左孩子

while (child < len){

// 如果有右孩子,比较左右孩子大小,得到较大的值和父节点比较

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

child++;

}

// 比较较大的孩子和父节点,看是否要交换

int max = elem[parent] >= elem[child] ? parent : child;

if (max == parent) break;// 如果不需要调整了,说明当前子树已经是大根堆了,直接 break

swap(elem,parent,child);

parent = child;// 继续向下检测,看是否要调整

child = parent*2+1;

}

}

public void swap(int[] arr,int i,int j){

int temp = arr[i];

arr[i] = arr[j];

arr[j] = temp;

}

小根堆(最小堆)

小根堆保证,每颗二叉树的根节点都 小于 左右孩子节点

调整过程同上。

优先级队列(PriorityQueue)

在java中,提供了堆这种数据结构(PriorityQueue),也叫优先级队列,当我们创建一个这样的对象时,就得到了一个没有添加数据的 小根堆 ,我们可以向里面添加或者删除元素,每向里面删除或者添加一个元素,系统会整体进行一次调整,重新又调整为小根堆。

// 默认得到一个小根堆

PriorityQueue smallHeap = new PriorityQueue<>();

smallHeap.offer(23);

smallHeap.offer(2);

smallHeap.offer(11);

System.out.println(smallHeap.poll());// 弹出2,剩余最小的元素就是11,会被调整到堆顶,下一次弹出

System.out.println(smallHeap.poll());// 弹出11

// 如果需要得到大根堆,在里面传一个比较器

PriorityQueue BigHeap = new PriorityQueue<>(new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

return o2 - o1;

}

});

2、top-k问题解决思路

例:有一堆元素,让你找出前三个最小的元素。

思路一: 将数组从小到大排序,拿到数组前3个元素。但是可以发现这样时间复杂度太高,不可取。

思路二: 将元素全部放入一个堆结构中,然后弹出三个元素,每次弹出的元素都是当前堆最小的,那么弹出的三个元素就是前最小的三个元素。

这种思路可以做,但是假设我有1000000个元素,只弹出前三个最小的元素,那么就要用到大小为1000000的堆。这么做空间复杂度太高,不建议用这种方法。

思路三:

我们需要得到三个最小的元素,那么就建一个大小为3的堆,假设目前的堆结构中刚好放满了3个元素,那么这三个元素就是当前最小的三个元素。假设第四个元素是我们想要的元素之一,那么前三个至少有一个元素不是我们想要的,就需要弹出,那么弹出谁呢?

我们要得到的是前三个最小的元素,所以当前堆结构中最大的元素一定不是我们想要的,所以这里我们建一个大根堆。弹出该元素,然后放入第四个元素,直到遍历完整个数组。

这样我们就得到了只含有前三个最小元素的堆,并且可以看到堆的大小一直都是3,而不是有多少数据就建多大的堆,然后再依次弹出元素就行了。

// 找前 k个最小的元素

public static int[] topK(int[] arr,int k){

// 创建一个大小为 k的大根堆

PriorityQueue maxHeap = new PriorityQueue<>(k,new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

return o2 - o1;

}

});

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

if (i < k){

// 放入前 k 个元素

maxHeap.offer(arr[i]);

}else{

// 从第 k+1个元素开始进行判断是否要入堆

if (maxHeap.peek() > arr[i]){

maxHeap.poll();

maxHeap.offer(arr[i]);

}

}

}

int[] ret = new int[k];

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

ret[i] = maxHeap.poll();

}

return ret;

}

以上就是top-k问题的基本思路,其他的类似问题也是这样解。

总结:

1、如果求前K个最大的元素,要建一个小根堆。

2、如果求前K个最小的元素,要建一个大根堆。

3、如果求第K大的元素,要建一个小根堆 ( 堆顶元素就是 )。

4、如果求第K小的元素,要建一个大根堆 ( 堆顶元素就是 )。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:详解Java分布式缓存系统中必须解决的四大问题(java分布式缓存框架)
下一篇:ElasticSearch如何设置某个字段不分词浅析(elasticsearch 不分词)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~