《算法撕裂者》系列0 - TopK问题

零、前言

首先说说我写这篇系列文的原因吧,最近准备面试在刷算法题,题也刷了不少,但遇到新题总是不能很快的想到好的解决方案,总是被算法猛撕。想想还是没有总结到位,于是从今天开始《算法撕裂者》这系列的文章,抽空常更新文章,争取成为算法撕裂者。腹肌都能撕裂的我不信撕不裂算法,干就完了~ 加油~ 奥利给!
在这里插入图片描述

一、算法必学:经典的 Top K 问题

什么是 Top K 问题?简单来说就是在一堆数据里面找到前 K 大(当然也可以是前 K 小)的数。

这个问题也是十分经典的算法问题,不论是面试中还是实际开发中,都非常典型。面对这种问题,你知道怎么解决吗?

二、很容易想到的方案之排序法

既然是要前 K 个,最简单粗暴的方法就是排序咯,来~ 下面有常见的十种排序方法,任您挑任您选!
在这里插入图片描述
但是就算高效的排序也需要在平均 O(nlogn)的时间复杂度找到结果。而且还有个很大的问题就是当数据量很庞大的时候,使用这种方法要消耗大量的内存空间,甚至一台机器的内存都无法完成的情况。

三、分布式思想处理海量数据

既然考虑到海量数据,那我们可以往分布式的方向去思考。

我们可以将数据分散在多台机器中,然后每台机器并行计算各自的 TopK 数据,最后汇总,再计算得到最终的 TopK 数据

这种数据分片的分布式思想在面试中非常值得一提,在实际项目中也十分常见

四、最经典的方法之堆

面对 Top K 问题,最经典的解法是利用 。(堆是数据结构里的知识,学习算法的一个重要前提需要了解常用的数据结构,既然大胆点开了算法文章,这里默认大家对前提知识已经了解,在此就不多做介绍,不懂请自行百度,后期有时间我再写相关文章。)

这里简单给大家介绍一下堆,以防陷入知识盲区的旁友。

堆(数据结构):堆可以被看成是一棵树【树就不用多说了吧,对,就是你家门外的那棵树 (手动滑稽)】

最大堆(也叫大根堆、大顶堆):根结点的键值是所有堆结点键值中最大者,且每个结点的值都比其孩子的值大。

最小堆(也叫小根堆、小顶堆):根结点的键值是所有堆结点键值中最小者,且每个结点的值都比其孩子的值小。
在这里插入图片描述
维护一个容量为 K 的最小堆(需求而定,同理最大堆),依次将数据放入堆中,当堆的容量满了时,只需将堆顶元素与下一个元素作比较,如果下一个元素比堆顶元素大,则将当前堆顶元素删除,并将该元素插入到堆中。遍历完全部数据后,Top K 的数据也就在堆中了。

至于是构造最小堆还是最大堆,就根据需求而定:

如果是要前 K 个最大的元素,那就构造最小堆。
(逻辑就是,要前 K 个最大值时,如果待添加的元素大于堆中的最小值,就可以添加。 )

如果是要前 K 个最小的元素,那就构造最大堆。
(逻辑就是,要前 K 个最小值时,如果待添加的元素小于堆中的最大值,就可以添加。 )

用堆实现的好处:

  1. 即使海量数据,我们使用一台计算机就可以解决。因为我们不需要一次性将全部数据取出来,可以一次只取一部分,因为我们只需要将数据一个个拿来与堆顶比较。
  2. 时间复杂度,整个操作中,遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK),加起来就是 O(nlogK) 的复杂度,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此是十分高效的。

五、纸上得来终觉浅,绝知此事要躬行。

Talk is cheap ,Show me the code !

开胃前菜已经品过了,接下来上两道硬菜。

1、LeetCode面试题–最小的 K 个数

LeetCode传送门

题目描述:

输入整数数组 arr ,找出其中最小的 k 个数。
例如,输入451627388个数字,
则最小的4个数字是1234
示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

这个问题就是求前 K 个最小的数,所以使用的是最大堆

废话少说,放码过来

package Array;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * 最小的 K 个数
 * 本题知识点: 数组、堆
 */
public class $40_GetLeastNumbers_Solution {

    public static void main(String[] args) {
        $40_GetLeastNumbers_Solution solution = new $40_GetLeastNumbers_Solution();
        int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
        int k = 4;
        ArrayList<Integer> res = solution.GetLeastNumbers_Solution(arr, k);
        System.out.println("res:" + res); // [4,3,1,2]
    }

    public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        int length = input.length;
        if (k > length || k == 0) {
            return result;
        }
        // 构造最大堆(Java PriorityQueue 默认是实现最小堆)
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k,
                // 助记 默认升序 o1 - o2
                // 	   降序则为 o2 - o1
//                (o1, o2) -> o2 - o1  // Java8 后支持 lambda 表达式
                new Comparator<Integer>() {
                    @Override
                    public int compare(Integer o1, Integer o2) {
                        return o2.compareTo(o1); // 并非从大到小排序,确保维持队首值为最大值
                    }
                }
        );
        for (int i = 0; i < length; i++) {
            // 如果队列中元素个数未到k,直接添加
            if (maxHeap.size() != k) {
                maxHeap.offer(input[i]);
            }
            // 达到k数,判断下一个值是否小于队首值,小于则将队首值删除,将此值添加
            else if (input[i] < maxHeap.peek()) {
                Integer temp = maxHeap.poll();
                temp = null; //GC回收
                maxHeap.offer(input[i]);
            }
        }
        // 遍历最大堆,取出结果
        for (Integer integer : maxHeap) {
            result.add(integer);
        }
        return result;
    }
}

个人认为注释还是写得挺详细的,就不多文字解释了。理解了前面的思路,这个代码应该也不难懂,最好自己手敲一遍在IDE中调式运行,看着嗯嗯嗯我懂~ 敲完就哎哎哎卧槽!看代码和写代码是两回事!
在这里插入图片描述

2、LeetCode面试题–前 K 个高频元素

LeetCode传送门

题目描述:

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

这个问题很明显,就是求前 K 个频率最多元素,所以我们使用最小堆

这道题麻烦点的就是多了统计频率的步骤,我们使用Map来统计key的频率value,然后构造最小堆比较的时候是根据频率value比较

下面开撸

package Array;

import java.util.Comparator;
import java.util.List;
import java.util.LinkedList;
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.TreeMap;

/**
 * @description: 前 K 个高频元素  (中等)
 * 本题知识点:数组、哈希、堆
 * @author: Kevin
 * @createDate: 2020/2/24
 * @version: 1.0
 */
public class TopKFrequent {

    public static void main(String[] args) {
        TopKFrequent solution = new TopKFrequent();
        int[] nums = {1, 1, 2, 3, 3, 3, 4, 5, 5, 5, 5};
        int k = 3;
        List<Integer> res = solution.TopKFrequent(nums, k);
        System.out.println(res); //[5, 3, 1]
    }

    // 解决方法
    public List<Integer> TopKFrequent(int[] nums, int k) {
        // 1. 使用 map 统计频率
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int num : nums) {
            if (map.containsKey(num)) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }

        // 2. 根据频率,将频率添加到优先队列(Java默认实现最小堆)
        PriorityQueue<Integer> pq = new PriorityQueue<>(
//                (o1, o2) -> map.get(o1) - map.get(o2)
                new Comparator<Integer>() {
                    @Override
                    public int compare(Integer o1, Integer o2) {
                        // 按照频率,小的在前 (即 默认升序)
                        return map.get(o1) - map.get(o2);
                    }
                }
        );
        for (int key : map.keySet()) {
            // 如果队列中元素个数未到k,直接添加
            if (pq.size() < k) {
                pq.add(key);
            }
            // 达到k数,判断下一个值是否大于队首值,大于则将队首值删除,将此值添加
            else if (map.get(key) > map.get(pq.peek())) {
                pq.poll();
                pq.add(key);
            }
        }

        List<Integer> res = new LinkedList<>();
        while (!pq.isEmpty()) {
            res.add(pq.poll());
        }
        Collections.reverse(res); // 翻转数组,输出结果频率由高到低
        return res;
    }
}

六、总结(文末有彩蛋!!!)

总结一下这篇文章,我们先提出了TopK问题,以及解决思路,排序虽然简单但是效率和空间都不佳;分布式是个好思想但是需要物资;最经典的方法则是利用堆。然后介绍了最大堆和最小堆以及什么情况用什么堆。最后用两个算法面试题实操运用。

为了帮助大家记忆,我特地作了首助记诗, 献丑奉上。在这里插入图片描述
要是觉得不错的话666点赞来一波呀~ 你们的鼓励就是我的动力!谢谢!

最最后~~

推广一下我的个人公众号,用作个人分享生活、分享技术、分享资源的。最近在找工作比较忙,抽空不定期更新,欢迎大家关注一波~ 微信搜索 “KeviniFree” 或扫下图二维码(公众号首发文章)
在这里插入图片描述
还有欢迎大家去全球最大的同性交友网站GitHub,我的不少代码和项目都将其开源分享,供大家学习交流。喜欢就Follow & Star me !!!

End-------------------------------感谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值