Java 详细讲解用堆解决Top-k问题

目录
  • 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){
    int 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<Integer> 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<Integer> BigHeap = new PriorityQueue<>(new Comparator<Integer>() {
     @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<Integer> maxHeap = new PriorityQueue<>(k,new Comparator<Integer>() {
         @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小的元素,要建一个大根堆 ( 堆顶元素就是 )。

到此这篇关于Java 详细讲解用堆解决Top-k问题的文章就介绍到这了,更多相关Java Top-k问题内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java实现TopK问题的方法

    面试中会经常遇到手撕代码的情况,而求TopK的是经常遇到的题目.下面我就用Java来实现.主要通过两种方法实现,快排思想以及堆排序的思想,两者的复杂度为O(NlogK). 基于快排的TopK实现: import java.util.Arrays; /** * 使用快排实现的TopK问题 Title: Description: Company: * * @author 郑伟 * @date 2018年4月10日下午9:28:15 */ public class TopK_PartitionSort

  • Java 详细讲解用堆解决Top-k问题

    目录 1.什么是堆? 堆结构 大根堆 VS 小根堆 大根堆(最大堆) 小根堆(最小堆) 优先级队列(PriorityQueue) 2.top-k问题解决思路 要解决 top-k 问题,我们应该先熟悉一种数据结构 - 堆(优先级队列),已经了解的朋友可以跳过哦. 1.什么是堆? 堆结构 堆其实就是一种二叉树,但是普通的二叉树是以链式结构进行储存数据的,而堆是以数组进行顺序存储数据的.那么什么样的二叉树才适合用顺序存储的方式呢? 我们假设一颗普通的二叉树可以用数组存储,那么就可以得到如下结构: 我们

  • Java详细讲解堆排序与时间复杂度的概念

    目录 一.堆排序 1.什么是堆排序 2.堆排序思想 3.代码实现 二.时间复杂度分析 1.初始化建堆 2.排序重建堆 3.总结 一.堆排序 1.什么是堆排序 (1)堆排序:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法.堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点. (2)堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆:或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

  • 使用堆实现Top K算法(JS实现)

    先来聊一聊Top K算法,具体内容如下 应用场景: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节.         假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个.一个查询串的重复度越高,说明查询它的用户越多,也就是越热门.),请你统计最热门的10个查询串,要求使用的内存不能超过1G. 必备知识: 什么是哈希表?         哈希表(Hash table,也叫散列表),是根据关键码值(K

  • Java 详细讲解分治算法如何实现归并排序

    目录 1.什么是分治算法 分治法 基本思想 2.分治算法的体现--归并排序 归并排序 基本思想 3.代码实现 1.什么是分治算法 分治法 分治法,字面意思是"分而治之",就是把一个复杂的1问题分成两个或多个相同或相似的子问题,再把子问题分成更小的子问题直到最后子问题可以简单地直接求解,原问题的解即子问题的解的合并,这个思想是很多高效算法的基础,例如排序算法(快速排序,归并排序),傅里叶变换(快速傅里叶变换)等. 基本思想 分治法的基本思想:将一个难以直接解决的大问题,分割成一些规模较小

  • Java 详细讲解线程安全与同步附实例与注释

    目录 线程安全问题 实例: 存钱取钱问题 买票问题 线程安全问题 分析问题 解决方案 线程同步 同步语句 synchronize(obj)的原理 同步方法 同步方法的本质 线程安全问题 多个线程可能会共享(访问)同一个资源 比如访问同一个对象,同一个变量,同一个文件 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题,称为线程安全问题 什么情况下会出现线程安全问题 多个线程共享同一个资源 且至少有一个线程正在执行写的操作 实例: 存钱取钱问题 分别有存钱和取钱2个线程 存钱      

  • Java详细讲解分析双指针法的使用

    目录 前言 1.判断链表是否有环 2.查找链表中间的元素 3.奇偶排序前奇后偶 4.删除排序链表的重复元素 5.三数之和 6.分割链表 7.合并两个有序的数组 8.两数之和—输入有序数组 9.长度最小的子数组 10.排序链表 前言 通常用在线性的数据结构中,比如链表和数组. 指针其实就是数据的索引或者链表的结点.两个指针朝着左右两个方向移动,直到满足搜索条件. 双指针可分为同向双指针.异向双指针.快慢指针.滑动窗口.根据需求选择双指针的模型,比如 删除数组或链表中重复的元素,同向双指针(线性表前

  • Java详细讲解异常Exception的处理

    目录 1.异常介绍 2.常见的运行时异常 1.空指针异常 2.数学运算异常 3.数组下标越界异常 4.类型转换异常 5.数字格式不正确异常 1.异常介绍 基本概念 程序执行中发生的不正常情况称为异常.(语法错误和逻辑错误不是异常). package com.demo; /** * @version 1.0 * @auther Demo龙 */ public class exception01 { public static void main(String[] args) { int num1

  • Java详细讲解依赖注入的方式

    目录 Spring中的三种依赖注入方式 可能遇到的问题 Spring中的三种依赖注入方式 Field Injection :@Autowired注解的一大使用场景就是Field Injection Constructor Injection :构造器注入,是我们日常最为推荐的一种使用方式Setter Injection: Setter Injection也会用到@Autowired注解,但使用方式与Field Injection有所不同,Field Injection是用在成员变量上,而Sett

  • Java详细讲解不同版本的接口语法和抽象类与接口的区别

    目录 什么是接口? 接口的语法: (JDK7.0) 接口的语法: (JDK8.0) 接口的语法: (JDK9.0)—(私有方法) 接口的分类 常量接口: 空接口: 函数式接口: 什么是接口? 说到接口,USB大家肯定不陌生~接口是一种标准.规范.注意:接口一旦制定好,使用者和实现者都必须遵循的标准. 接口的语法: (JDK7.0) (1) 关键字:interface (2) 语法:  interface 接口名{} (3) 接口编译之后会生成对应的 .class文件 (4) 接口不能创建对象,但

  • Java 详细讲解线程的状态及部分常用方法

    可以通过 Thread.getState 方法获得线程的状态(线程一共有 6 种状态) NEW(新建)new:尚未启动 RUNNABLE(可运行状态)runnable:正在 JVM 中运行:或者正在等待操作系统的其他资源(比如处理器) //有些编程语言会把RUNNABLE分成2种情况//1.running//2.ready//以上2种在Java中都属于RUNNABLE BLOCKED(阻塞状态) blocked:正在等待监视器锁(内部锁) WAITING(等待状态) waiting:在等待另一个

随机推荐