刷题笔记(十一)--栈和队列:Top-K问题

系列文章目录

刷题笔记(一)–数组类型:二分法
刷题笔记(二)–数组类型:双指针法
刷题笔记(三)–数组类型:滑动窗口
刷题笔记(四)–数组类型:模拟
刷题笔记(五)–链表类型:基础题目以及操作
刷题笔记(六)–哈希表:基础题目和思想
刷题笔记(七)–字符串:经典题目
刷题笔记(八)–双指针:两数之和以及延伸
刷题笔记(九)–字符串:KMP算法
刷题笔记(十)–栈和队列:基础题目

前言

这一部分呢,是针对于队列来说,因为TOP-K问题要用到这个,也就是优先级队列。老规矩,还是一点一点来。

什 么 是 T O P − K 问 题 ? \color{red}什么是TOP-K问题? TOPK

简单说一下,所谓TOP-K问题,就是在一份海量的数据中去寻找前K个最大或者最小的值。

可能会有人说,那我直接使用快排等排序算法不直接吗?这里注意一下,是海量的数据。如果说数据量不是很大,那么快排等排序算法当然是可以使用的。可是如果说数据量过大的时候,这个时候我们就要考虑的时候时间和空间的双重效率了,所以就要使用我们的优先级队列。

什 么 是 优 先 级 队 列 ? \color{red}什么是优先级队列?

所谓优先级队列,就是披着队列外衣的堆。优先级队列对外的接口只能是从队头取元素,从队尾加元素,所以从这一点来看它就是一个队列。而内部的元素就是自动依照元素的权值排列。

那 么 什 么 是 堆 呢 ? \color{red}那么什么是堆呢?

是不是好像有点越说越多了,

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

所以通俗一点,堆就是一棵完全二叉树的数组对象。

题录

215. 数组中的第K个最大元素.

题目链接如下:

215. 数组中的第K个最大元素

题目截图如下:
在这里插入图片描述
这道题就是一个基本的对于堆的掌握能力的考察,没有什么特别需要注意的地方,关于堆的构造我之前也写过相关的博客。

public class 数组中的第K个最大元素 {
    public int findKthLargest(int[] nums, int k) {
        int end = nums.length - 1;
        //总的思路就是构造一个大根堆,然后不停的删减元素,删除K个之后,此时的0号下标就是要求的
        for (int i = (end - 1) / 2; i >= 0; i--) {
            shiftDowm(nums,i,end);
        }
        //开始删减元素,删减K个之后就结束了
        while(k-- > 1){
            swap(nums,0,end);
            end--;
            shiftDowm(nums,0,end);
        }
        return nums[0];
    }

    public void shiftDowm(int[] nums,int parent,int end){
        //这里是定义的左孩子
        int child = parent * 2 + 1;
        while(child <= end){
            //首先要进行判断,父节点要和左右子树里面大的哪一个进行交换
            if(child + 1 <= end && nums[child] < nums[child + 1]){
                child = child + 1;
            }
            if(nums[child] > nums[parent]){
                swap(nums,parent,child);
                parent = child;
                child = parent * 2 + 1;
            }else{
                break;//如果说当前父节点的值大于子节点,那么就证明当前已经是大根堆,所以停止调整。
            }
        }
    }
    public void swap(int[] nums,int i,int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

347. 前 K 个高频元素

题目链接如下:

347. 前 K 个高频元素

题目截图如下:

在这里插入图片描述

拿到这个题目的时候,第一反应就是用一个map的结构来存储键值对,然后根据键来对其进行排序。但是这里突然间没有一个很好的结构来储存排序中的这些键值对,最后没办法打开题解,发现看的有点懵,嗷~是好久没有看关于Map和Set的知识点了。所以这篇博客新加了一个复习篇章,就是复习一下我已经遗忘的知识点。

public classK个高频元素 {
    public int[] topKFrequent(int[] nums, int k) {
        //首先定义一个Map用来存储每个元素和其出现的顺序
        Map<Integer,Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int key = nums[i];
            map.put(key, map.getOrDefault(key,0) + 1);
        }

        //然后把这些映射关系全部返回,用一个set接收起来
        Set<Map.Entry<Integer,Integer>> sets = map.entrySet();
        PriorityQueue<Map.Entry<Integer,Integer>> minQue = new PriorityQueue<>(
                //其实这一行是完全可以省略的,因为优先级队列本来就是默认小根堆
                (o1,o2) -> (o1.getValue() - o2.getValue())
        );
        //然后就是遍历所有的映射关系,根据值来进行堆排序。
        for(Map.Entry<Integer,Integer> set:sets){
            minQue.add(set);
            if(minQue.size() > k){
                minQue.poll();
            }
        }
        int[] arr = new int[k];
        //然后堆当中剩余的键值对一个一个往出删,删一个存一个的key
        for(int i = k - 1;i >= 0;i--){
            arr[i] = minQue.poll().getKey();
        }
        return arr;
    }
}

所以这道题在熟悉Map操作的基础上,还是很容易做出来的。

复习

<1>关于比较器

这个部分的书写是因为发现自己对于大根堆和小根堆的建造,印象很模糊很模糊了。(十分钟后)笔者这会已经感觉自己对之前的知识遗忘的很厉害,为啥呢,就是笔者之前第一次在了解关于比较器的知识点的时候是在String类这部分的时候,然后复习着复习就感觉情况不那么简单,确实有些知识点遗忘了。

1.关于CompareTo

这个知识点其实就是笔复习时候发现的,就先使用这个知识点引申出以下的内容。

在这里插入图片描述

红框一:两个字符串相同,返回0
红框二:一个字符被另一个字符包含,返回两个字符串的长度差值
红框三:两个字符串不同,返回两个字符串中第一个不同字符的字典序差值。注意:字典序差值!字典序差值!字典序差值!!

2.关于Comparable和Comparator

这两个都是用来实现元素排序的,但是二者还是有着很本质的区别的。

1. 字 面 含 义 \color{red}1.字面含义 1.

Comparable是“比较”的意思,而Comparator是比较器的意思。前者是able结尾,也就是说它自身就有这着某种能力,而后者是or结尾,表示自己是比较的参与者。

2. 用 法 不 同 \color{red}2.用法不同 2.

他们都是顶级的接口,但是所有的方法和用法是完全不同的。

Comparable用法:此接口只有一个方法,就是ComparaTo,实现Comparable接口并且重写CompareTo方法就可以实现某个类的排序。它支持Collections.sort和Arrays.sort的排序。
CompareTo方法说明:此方法接收的参数P是要比较的对象,就是用当前的对线和要对比的对象进行比较,然后返回一个int类型的值,正序从小到大的排序规则是【使用当前对象的值减去要对比的对象的值】,倒序从大到小的排序规则是【使用要比较的对象的值减去当前对象的值】。当然了,如果说当前类没有继承Comparable接口,那么就不能使用Collection.sort方法进行排序。

Comparator用法:当前接口的实现排序的方法不是CompareTo,而是Compare
拓展Comparator匿名类:Comparator除了可以通过创建自定义比较器外,还可以通过匿名类的方式,更快捷,便捷的完成自定义比较器的功能。

3. 使 用 场 景 不 同 \color{red}3.使用场景不同 3.使

1.如果说要使用Comparable接口,就必须要修改原有的类,也就是你要排序的那个类,就要在那个类中实现Comparable接口并且重写CompareTo方法,所以从这点来看Comparable接口更像是“对内”进行排序的接口。
2.Comparator则不需要修改原来的类,也就是说即使这个类是第三方提供,我们依然可以通过创建新的自定义比较器Comparator来实现对第三方类的排序功能。也就是说通过Comprartor接口可以实现和原有类的解耦,在不修改原有类的情况下实现排序功能。所以从里来看Comparator可以看作是“对外”提供排序的接口。

<2>关于map的复习

这一部分的复习内容是笔者在看题解的时候,发现自己对于map的方法有了很大的遗忘,所以这一部分在这里也复习一下。

1.什么是map

map是一个接口
在这里插入图片描述
可以看到,该接口并没有继承Collection。该类存储的是键值对<K,V>,并且key一定是唯一的,不能重复。

2.关于Map.Entry(K,V)

Map.Entry(K,V)是Map内部实现的用来存放<key,value>键值对映射关系的内部类。该内部类主要提供了关于<key,value>的获取方式,以及key的比较方式等等。常用的方法有如下:
在这里插入图片描述
这里的话主要是这个Set<Map.Entry<K, V>> entrySet()不是很好理解,所以可以对应着我们上面的347题理解。然后对于这些还有一些地方需要说明的

1.Map是一个接口,不可以直接实例化对象,只能实现其实现类TreeMap或者HashMap
2.key唯一并且不能为null,但是value可以为空也可以重复
3.Map中的key可以全部分离出来存储在set中。(key不可重复)
4.Map中的value可以全部分离出去,存在Collection的任何一个子集合中(value可能重复。)
5.value可以修改,但是key不能修改,如果要改只能删除后再插入。

### 使用二分法查找第 K 小元素 对于在已排序数组中查找第 K 小的元素,可以应用二分查找方法。这种方法不仅高效而且逻辑清晰。 #### 非递归方式实现 为了在一个升序排列的整型数组 `arr` 中找到第 k 小的元素,可以通过调整标准的二分查找算法来完成这一目标: ```cpp int findKthSmallest(int arr[], int n, int k) { // 创建一个小顶堆用于存储前k个最小值 priority_queue<int, vector<int>, greater<int>> minHeap; for (int i = 0; i < n; ++i){ minHeap.push(arr[i]); } // 提取堆中的前(k-1)个元素 for (int i = 0; i < k - 1; ++i){ minHeap.pop(); } // 堆顶即为所求的第k小元素 return minHeap.top(); } ``` 上述代码实际上并未直接使用二分查找而是借助了优先队列(最小堆),这是因为直接通过二分查找定位到具体位置较为复杂。不过,在某些情况下也可以基于二分思想设计专门针对此类问题的解决方案[^3]。 #### 更高效的二分策略 另一种更贴近传统意义上的“二分”,是将整个过程视为对可能的结果空间进行划分的过程。这里的关键在于理解我们不是在原始输入序列上做二分操作,而是在潜在解的空间里执行二分搜索。这通常涉及到设定合理的上下界并不断缩小区间直到收敛于正确答案。 考虑一个优化版本,它并不依赖额外的数据结构如堆: ```cpp // 寻找无重复元素数组中的第k小数 double findKthElement(vector<double>& nums, int k) { double low = *min_element(nums.begin(), nums.end()); double high = *max_element(nums.begin(), nums.end()); while(low <= high){ double mid = low + ((high-low)/2); // 统计不大于mid的数量count以及最靠近mid的最大值preMid int count=0; double preMid=-DBL_MAX; for(auto num : nums){ if(num<=mid){ count++; preMid=max(preMid,num); } } if(count==k) return preMid; if(count<k) low=mid+EPSILON;// EPSILON是一个很小正数防止浮点误差 else high=mid-EPSILON; } throw invalid_argument("Invalid Input"); } ``` 这段代码展示了如何利用二分查找技术在一个给定范围内精确地锁定住所需的数值。注意这里的处理对象被假定是没有重复项的情况下的实数集合,并且引入了一个极小量 `EPSILON` 来规避由于计算机内部表示带来的精度损失问题[^4]。 #### 复杂度分析 当采用纯粹的二分查找思路解决问题时,每次迭代都会使候选集减半,因此理论上可以在 O(log N) 的时间内得到结果。然而实际性能还取决于具体的实现细节其他因素的影响,比如是否存在大量相同键值等特殊情形下可能会退化成线性扫描的时间开销[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值