经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

这篇具有很好参考价值的文章主要介绍了经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:

本篇文章以 TopK 问题为引,具体阐述了 PriorityQueue 实现的基本逻辑——堆 数据结构,以及PriorityQueue 的常用方法。如有问题欢迎看官朋友指正,如果觉得文章还不错的话,求点赞、收藏、评论 三连。

重点:

  • 堆的基本实现逻辑
  • PriorityQueue 运用和源码分析
  • TopK 问题的解法


1 初识TopK问题

1.1 是什么

常见的题型是,在大小为 n 的 array 数组 中,

  • 找出 前 k 个 最大 / 最小的元素

  • 找出 第 k 个 最大 / 最小的元素

1.2 如何解决

无论是找到前k个,还是找到第k个,TopK 问题本质上都是找到 前k个 有序的数据集合
(文章做过些许的修改, 如有误导了各位看官朋友, 楼主深表歉意)

1)sort
  • 如果是 基本数据类型,直接 Arrays.sort(array) 排序数组
  • 如果是 引用数据类型,元素指向特定的对象时,在 Arrays.sort() 中再传入 Comparator 比较器再排序数组
  • 然后拿到前k个有序的数据集合即可
    经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构
2)快排

调用 Arrays 中自带的sort方法虽然方便,但是笔试中一般不推荐使用

  • 我们可以自主实现 快速排序的方式堆数组进行排序,快排的时间复杂度最好为 O(N * logN),最坏为 O(N^2),同时快速排序后续还可以有更多的优化空间 (三数取中、区间优化、同类聚集等等),由于篇幅原因我们就不展开了

经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构
经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

3)优先级队列

优先级队列是一种能够快速找到集合最值的数据结构, 对数据较少时进行出队入队时间复杂度很低,非常适合 TopK 问题 的解决, 相较于快速排序的 O(N * logN), O(N*logK) 数据越多优势
越明显

  • 对于 TopK 问题我们可以充分利用优先级队列(堆)的特性将时间复杂度做到 O(N*logK),使排序的效果大大提升,但在这之前我们先来回顾一下有关于堆的知识吧(观众老爷们稍安勿躁),基础扎实的铁子可以直接跳转 4 解决TopK问题


2 堆

2.1 是什么

优先级队列底层是使用堆数据结构实现

  • 物理上 是保存在 数组 中, 逻辑上是一棵采用顺序存储方式的 完全二叉树(避免空间的浪费),通过从 0 开始的下标标定二叉树的每个节点
  • 结点的值都大于其子树中结点的值,叫做大堆 / 大根堆 / 最大堆
  • 结点小于子树节点叫 小堆 / 小根堆 / 最小堆,左右子节点的大小关系不能确定
  • 通过首元素能够快速找到集合中的最值
    经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

2.2 堆和二叉树的顺序存储

​ 如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一 个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。

1) 存储方式
  • 堆在逻辑上就是一颗完全二叉树

  • 层序遍历 方式 用 数组保存 二叉树结构,这种方法主要用于堆的表示顺序存储存储完全二叉树,非完全二叉树会有空间的浪费

2)下标关系

起始下标为 0

(1) 已知父亲节点下标i

  • 左孩节点下标:2*i + 1
  • 右孩节点下标:2*i + 2

(2) 已知孩子节点下标i

  • 父亲节点下标 = (child - 1)/ 2

经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

2.3 堆的相关操作

在介绍Java 集合框架提供的 PriorityQueue 类之前,我们必须先来了解一下优先级队列常用方法背后的基本逻辑是如何的,由于优先级队列底层是使用堆数据结构实现,所以下面我们通过简单模拟实现 建堆、插入、删除、返回首元素 的操作进行详细介绍

1)建堆——向下调整

(建大根堆为例)

(1)思路:

  • 传入数组,拷贝元素

  • 最后一个节点的父节点 开始一步步向前遍历,遍历到的每一个父节点 当作一颗新树的根节点,调整该树

  • (创建新的)调整一棵树,从根节点开始,不断进行向下调整

    • 找到 根节点的左右孩子中较大节点
    • 根节点循环同 较大的子节点比较,进行**交换**
    • 直到循环条件中根节点满足**child < len** 或者 在循环内部不满足 根节点 小于 子节点,该树调整完成

(2)解法:

public void adjustDown(int root, int len){
        int parent = root;
        int child = root * 2 + 1;

        while(child < len){
            //找到较大的孩子节点
            if(child + 1 < elem.length && this.elem[child] < this.elem[child+1] ){
                child++;
            }

            //从根节点开始向下遍历比较
            if(this.elem[child] > this.elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child;
                child = parent * 2 + 1;
            }else{
                break;
            }
        }
}


public void creatHeap(int[] array){
        this.elem = new int[array.length];
        for(int i = 0; i < array.length; i++){
            this.elem[i] = array[i];
            usedSize++;
        }

        //从最后一个节点的父节点开始调整
        for(int i = (elem.length - 1 - 1) / 2; i >= 0; i--){
            adjustDown(i, this.elem.length);
        }
}

(3)时间复杂度

​ 以满二叉树为例:

经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构
经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

  • 即时间复杂度为 O(N)
2) 插入元素——向上调整

(1)思路:

  • 判满增容
  • 调整,找到父节点,比较 交换或break,循环向上

(2)解决方法:

public void push(int val){
        //判满
        if(isFull()){
            this.elem = Arrays.copyOf(this.elem, 2*this.elem.length);
        }

        this.elem[usedSize] = val;
        usedSize++;

        adjustUp(this.usedSize - 1);

    }
    
    public boolean isFull(){
        return this.usedSize == this.elem.length;
    }
    
    public void adjustUp(int root){
        int parent = (root - 1) / 2;
        int child = root;

        //从根节点的父节点循环向上调整
        while(parent >= 0){
            if(elem[child] > elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child - 1) / 2;

            }else{
                break;
            }
        }
    }

(3)时间复杂度:

  • 最坏情况下,向上调整至首元素,调整次数为二叉树的高度 - 1

  • O(logN)

3) 删除元素——向下调整

(1)思路:

  • 判空

  • 将0下标元素和最后一个元素交换,删除最后一个元素,usedSize–

  • adjustDown(0,this.usedSize )向下调整

(2)解决方法:

public void poll(){
        //判空
        if(this.usedSize == 0){
            throw new MyHeapIsEmpty("堆为空");
        }

        int len = this.elem.length;
        int tmp = elem[0];
        elem[0] = elem[len - 1];
        elem[len - 1] = tmp;
        this.usedSize--;

        //向下调整
        adjustDown(0,len - 1);
    }

public void adjustDown(int root, int len){
        int parent = root;
        int child = root * 2 + 1;

        while(child < len){
            //找到较大的孩子节点
            if(child + 1 < elem.length && this.elem[child] < this.elem[child+1] ){
                child++;
            }

            //从根节点开始向下遍历比较
            if(this.elem[child] > this.elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child;
                child = parent * 2 + 1;
            }else{
                break;
            }
        }
    }

(3)时间复杂度:

  • 最坏情况下,替换的元素向下调整至二叉树最底层

  • O(logN),调整次数为二叉树的高度 - 1

4) 返回队首元素

1)思路

  • 判空 再返回

2)解法:

public int peek(){
        if(this.usedSize == 0){
            throw new MyHeapIsEmpty("堆为空");
        }

        return this.elem[this.usedSize - 1];
    }

2.4 堆排序

1)思路:

  • 创建大堆

  • 不断进行首尾元素交换,每次交换后向下调整同时 end–

2)解法:

public static void heapSort(int[] array){
        //先创建大堆
        creatHeap(array);
        int end = array.length - 1;

        //不断首尾交换,向下调整
        while(end > 0){
            int tmp = array[0];
            array[0] = array[end];
            array[end] = tmp;
            siftDown(array, 0, end);
            end--;
        }
    }

public static void creatHeap(int[] array){
        for(int i = (array.length - 1 - 1) / 2 ; i >= 0; i--){
            siftDown(array, i, array.length);
        }
    }

public static void siftDown(int[] array, int root, int len){
        int parent = root;
        int child = parent * 2 + 1;

        while(child < len) {
            if (child + 1 < len && array[child] < array[child + 1]) {
                child++;
            }

            if (array[child] > array[parent]) {
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                parent = child;
                child = parent * 2 + 1;
            } else {
                break;
            }
        }
    }

3)时间复杂度:

  • 创建大堆时间复杂度为 O(N),首尾交换向下调整时间复杂度为 N * logN,

  • 两者是并列关系,最终时间复杂度为 O(N*logN)


3 优先级队列

由 Java 提供的 PriorityQueue 和 PriorityBlockingQueue 两种优先级队列供我们使用,都继承于 Queue集合类

我们主要介绍PriorityQueue,下面主要介绍常用的构造方法 、成员变量,结合部分源码剖析一下优先级队列的 offer 成员方法,其他的成员方法我们只需会使用即可

3.1 构造方法PriorityQueue()

  • PriorityQueue()

  • PriorityQueue(int initialCapacity)

    • 指定初始容量
  • public PriorityQueue(Comparator<? super E> comparator)

    • 传入比较器
  • public PriorityQueue(int initialCapacity, Comparator<? super E> comparator)

    • 指定initialCapacity 不能 < 1,否则会抛出异常

3.2 成员变量

  • Object[] queue

  • int size

  • Comparator<? super E> comparator

3.3 成员方法

方法名 详情
boolean offer (E e) 插入元素,元素为空抛出异常
E peek () 得到集合中的最值(优先级最高元素)
E poll () 删除集合中的最值(优先级最高元素)
int size () 得到元素个数
boolean isEmpty () 判断队列是否为空
void clear () 清除元素
1)offer 源码

经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

(1)扩容判断
经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

(2)第一次插入
直接赋值
经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构
(3)非第一次插入

经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

​ 是否有比较器


  • 经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

  • 没有比较器,传入的对象也要重写 Compare 方法

    经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

  • 如果传入的是 Integer类,其重写compareTo方法将插入元素x 和 父元素 e 进行比较,是 默认建立小根堆

    this.value 表示插入元素

    经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构
    经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

3.4 PriorityQueue 创建大根堆

PriorityQueue 默认创建的是小根堆,但是在具体的应用场景中我们必须创建大根堆来寻求题解(诸如经典 TopK 问题中找到数组中前K个小的数字)

  • 已有类型,构造优先级队列时直接 传入比较器,重写compare时,return o2 - o1(o2表示父元素),即父元素值大于传入元素值直接停止向上调整
  • 自定义类型,记得 实现Comparable接口,重写compareTo方法
    • 建立大根堆 return o2 - o1,
    • 建立小根堆 return o1 - o2



4 解决TopK问题

4.1 解决TopK问题

以找前k个最小的元素为例

1)思路:
  • 找前 k 小的元素,创建大根堆maxHeap

    • 在构造PriorityQueue 时 需要传入比较器,因为PriorityQueue默认是创建小根堆
      如果构造的时候传入k,要先 判断k是否 <1
  • 遍历array数组,当 i < k,放入array[i] 到maxHeap

  • 遍历array数组,当 i >= k,获取 堆中最大元素 tmp,tmp 同array[i]比较

    • 当 array[i] < tmp,先出队除去tmp,再入队将array[i] 放入maxHeap中
2)解法:
  • 前k个最小的元素
public static void main(String[] args) {
        int[] array = {12,34,32,12,43,65,34,75,25,86,24,64,35};
        int[] ans = topK(array, 4);
        System.out.println("原数组" + Arrays.toString(array));
        System.out.println("前4个最小的数"+ Arrays.toString(ans));
    }


    public static int[] topK(int[] array, int k) {
        //创建大堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });

        //遍历数组
        for(int i = 0; i < array.length; i++){
            //初始化入队
            if(i < k){
                maxHeap.offer(array[i]);
            }else{
            //判断队头元素
                int tmp = maxHeap.peek();
                if(tmp > array[i]){
                    maxHeap.poll();
                    maxHeap.offer(array[i]);
                }
            }
        }

        //maxHeap顺序放入ans数组返回
        int[] ans = new int[k];
        int count = 0;
        while(!maxHeap.isEmpty()){
            ans[count++] = maxHeap.poll();
        }

        return ans;
    }
  • 结果经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构
3)时间复杂度
  • 最坏情况下,初始化入队遍历 N 个元素都需要更换队头元素操作,出队入队时间复杂度为 2*logK,
  • 所以时间复杂度为 O(N * logK)

4.2 TopK相关练习

查找和最小的 K 对数字

1)思路
  • 实例化元素类型为 List的堆,调用带有**k, 比较器 2个参数的构造方法**

  • 两层循环拿到每一个对值,判断是否**入队** 或者 更换队头元素

    • 创建临时 List list 接收对值
    • 判断list 是否**放入堆** 中 或者 更换队头元素
  • 遍历堆元素放入 List<List> 的 List文章来源地址https://www.toymoban.com/news/detail-421283.html

2)解法
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        //创建大堆
        PriorityQueue<List<Integer>> maxHeap = new PriorityQueue<>(k, new omparator<List<Integer>>() {
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                return (o2.get(0) + o2.get(1)) - (o1.get(0) + o1.get(1));
            }
        });

        //遍历数组
        for(int i = 0; i < nums1.length; i++){
            for(int j = 0; j < nums2.length; j++){
                //直接放入堆
                if(maxHeap.size() < k){
                    List<Integer> list = new ArrayList<>();
                    list.add(nums1[i]);
                    list.add(nums2[j]);
                    maxHeap.offer(list);
                //更换队头元素
                }else{
                    List<Integer> top = maxHeap.peek();
                    int topval = top.get(0) + top.get(1);
                    if(topval > nums1[i] + nums2[j]){
                        maxHeap.poll();
                        List<Integer> list = new ArrayList<>();
                        list.add(nums1[i]);
                        list.add(nums2[j]);
                        maxHeap.offer(list);
                    }
                }
            }
        }

        //放回 List<List<Integer>>
        List<List<Integer>> ret = new ArrayList<>();
        for(int i = 0; i < k && !maxHeap.isEmpty(); i++){
            ret.add(maxHeap.poll());
        }

        return ret;
    }

到了这里,关于经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【OSTEP】调度: 多级反馈队列 (MLFQ) | 优先级提升 | 饥饿问题 | 愚弄调度问题 | MLFQ 调优

      💭 写在前面 本系列博客为复习操作系统导论的笔记,内容主要参考自: Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau, Operating Systems: Three Easy PiecesA. Silberschatz, P. Galvin, and G. Gagne, Operating System Concepts, 9th Edition, John Wiley Sons, Inc., 2014, ISBN 978-1-118-09375-7.Microsoft. MSDN(Microsoft Developer

    2024年02月06日
    浏览(24)
  • Linux_进程的优先级&&环境变量&&上下文切换&&优先级队列

    什么是优先级? 指定一个进程获取某种资源的先后顺序 本质是进程获取cpu资源的优先顺序 为什么要有优先级 进程访问的资源(CPU)是有限的 操作系统关于调度和优先级的原则:分时操作系统,基本的公平,如果进程因为长时间不被调整,就造成了饥饿问题 Linux的优先级特

    2024年04月09日
    浏览(41)
  • 优先级队列

    目录  前言: 1、PriorityQueue的特性 .2 PriorityQueue常用接口介绍 Ⅰ、PriorityQueue常见的构造方法  Ⅱ、常用的方法 Ⅲ、PriorityQueue的扩容方式:  3、应用 普通的队列是一种 先进先出 的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元

    2024年02月02日
    浏览(30)
  • 优先级队列【C++】

    优先队列(priority_queue)也是队列的一种,priority_queue的接口是和queue的接口是相同的。所以两者的使用语法也是相同的。我们直接看优先队列(priority——queue)的底层实现原理。 默认情况下priority_queue是大堆。 priority_queue的底层实际上就是堆,模拟实现priority_queue之前,需要

    2024年02月10日
    浏览(32)
  • 「数据结构」优先级队列

    🎇 个人主页 :Ice_Sugar_7 🎇 所属专栏 :Java数据结构 🎇 欢迎点赞收藏加关注哦! 优先级队列底层是用堆实现的 ,关于堆的实现,之前的文章已经详细介绍过了,文章链接:二叉树1:堆的实现 方法 功能 PriorityQueue() 创建一个空的优先级队列,默认容量是11 PriorityQueue(int i

    2024年02月20日
    浏览(32)
  • rabbitmq的优先级队列

    在我们系统中有一个 订单催付 的场景,我们的客户在天猫下的订单 , 淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tianmao商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,

    2024年02月11日
    浏览(31)
  • Java优先级队列-堆

    大家好,我是晓星航。今天为大家带来的是 Java优先级队列(堆) 的讲解!😀 使用数组保存二叉树结构,方式即将二叉树用 层序遍历 方式放入数组中。 一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。 这种方式的主要用法就是堆的表示。 已知双亲(parent)的下

    2023年04月16日
    浏览(29)
  • 【JAVA】优先级队列(堆)

    羡慕别人就让自己变得更好! 优先级队列(堆)可用于topK问题 有大小根堆 注意堆的模拟实现 坚持真的很难但是真的很酷! 队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列。 此时,数据结

    2024年02月15日
    浏览(39)
  • RabbitMQ优先级队列的使用

    生产者 消费者

    2024年02月16日
    浏览(31)
  • java 堆(优先级队列)详解

    如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki = K2i+1 且 Ki= K2i+2 (Ki = K2i+1 且 Ki = K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小

    2024年02月08日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包