从杂乱的用户评分到核心洞见:一个“和谐”算法如何帮我定位爆款产品(594. 最长和谐子序列)

0 阅读7分钟

😎 从杂乱的用户评分到核心洞见:一个“和谐”算法如何帮我定位爆款产品

大家好,今天想跟大家聊聊一个我在工作中遇到的真实案例,它看似是一个简单的数据分析需求,却让我对一个基础算法——哈希表——有了全新的认识。故事的起点,是我们的产品经理(PM)提出了一个棘手的问题。

一、我遇到了什么问题?PM的灵魂拷问

我在一家电商公司工作,我们的平台上有成千上万的商品,每个商品都有大量的用户评分(1到5星)。有一天,PM找到我,眉头紧锁地说:“我想知道哪些商品是我们的‘潜力爆款’,哪些是‘口碑两极分化’的。光看平均分完全没用啊!”

他说的没错。一个平均分3.0的商品,可能是所有用户都给了3星(评价中庸),也可能是一半用户给了1星,另一半给了5星(口碑崩盘)。

PM进一步阐述了他的想法:“一个真正的好产品,它的评分应该是高度集中的,比如绝大多数用户都给了4星和5星。我们能不能找到‘评分最集中的相邻分数组合’,并且这个组合覆盖的用户量是最大的?”

我立刻get到了他的点。他要找的,其实就是由 x 星和 x+1 星组成的、人数最多的那个用户群体。比如,对于商品A,它的所有评分记录是 [4, 5, 4, 4, 5, 5, 5, 1],那么由4星和5星组成的这个群体共有7人。这就是它的“核心好评用户群”。

这个问题,瞬间让我想起了 LeetCode 上的那道题:594. 最长和谐子序列

  • 和谐子序列:最大值和最小值之差正好是1。
  • 我的问题:找到由 x 星和 x+1 星构成的最大用户群。

这不就是一回事嘛!看来刷题真的能转化为生产力!💪

二、我是如何用[哈希表]解决的

踩坑实录:差点就写了个O(N^2)的循环地狱 🤦‍♂️

我的第一反应可能和很多同学一样:暴力出奇迹!用两层循环,遍历所有评分,看看能不能凑成和谐对子。但我们的商品可能有几十万条评分,N 这么大,N的平方简直是灾难,服务器分分钟被我干趴下。

恍然大悟的瞬间 💡

我突然意识到,PM并不关心是哪个用户在哪天给的评分,他只关心每种评分(1星、2星...)各有多少个。评分的先后顺序、谁评的,都无关紧要。

题目的“子序列”这个词也是在给我暗示!子序列不要求元素连续,这说明我们只需要关注值的频率,而不需要关注值的位置

一旦想明白这点,思路就豁然开朗了:这不就是一个经典的频率统计问题吗?而解决频率统计问题的神器,就是哈希表(HashMap)

解法一:哈希表计数法 (空间换时间的最优解)

这个方法分两步走,非常清晰:

  1. 扫一遍,全记下:遍历所有的评分数据,用一个HashMap来记录每个星级(1星、2星...)出现了多少次。
  2. 看一遍,找邻居:再遍历一遍我们刚建好的HashMap,对于每个星级 x,去查一下它的“邻居” x+1 在不在表里。如果在,就把它们的次数加起来,然后和我手里记录的“最大和谐用户群”比一下,取个大的。

关键API和它们的作用:

  • Map<Integer, Integer> frequencyMap = new HashMap<>();
    • 为什么用 HashMap? 题目的提示说了,数字范围可能很大。在我的实际场景里,虽然评分是1-5,但如果处理的是用户ID或者订单金额,那范围就大了去了。HashMap可以无视这个范围,高效地存储键值对。
  • map.put(num, map.getOrDefault(num, 0) + 1);
    • getOrDefault 真的是个宝藏API!它一句话就干了两件事:“如果map里已经有num这个键,就返回它的值;如果没有,就返回我指定的默认值0”。这让我的代码从 if-else 的繁琐中解脱出来,变得超级简洁。
  • frequencyMap.keySet()
    • 当我们统计完频率后,我们只需要关心有哪些星级出现过,所以遍历它的键集合keySet()就足够了。

代码实现:

/*
 * 思路:用哈希表统计频率,再遍历哈希表寻找相邻数对,计算最大长度。
 */
class Solution {
    public int findLHS(int[] nums) {
        // key: 评分星级, value: 该星级的用户数
        Map<Integer, Integer> frequencyMap = new HashMap<>();

        // 1. 统计所有评分的频率
        for (int num : nums) {
            frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
        }

        int maxLen = 0;
        // 2. 遍历所有出现过的星级
        for (int key : frequencyMap.keySet()) {
            // 寻找它的“好邻居” x+1
            if (frequencyMap.containsKey(key + 1)) {
                // 如果找到了,计算这个“和谐”组合的总人数
                int currentLen = frequencyMap.get(key) + frequencyMap.get(key + 1);
                // 更新我们的“最大核心用户群”记录
                maxLen = Math.max(maxLen, currentLen);
            }
        }
        return maxLen;
    }
}
解法二:排序 + 滑动窗口法 (另一种思路)

如果面试官追问:“如果内存很宝贵,不让你用哈希表这么大的额外空间怎么办?” 别慌,我们还有后手!

我们可以先对评分数组进行排序。排序后,相同星级的评分就会聚在一起。然后,我们就可以用一个“滑动窗口”来解决问题。想象一下尺子,我们在排好序的数组上移动这把尺子,保证尺子两端的值的差不大于1,然后找到尺子能覆盖的最长那一段。

代码实现:

/*
 * 思路:排序后,问题转化为寻找最长的、最大值与最小值之差为1的连续子数组。
 */
import java.util.Arrays;

class Solution2 {
    public int findLHS(int[] nums) {
        // 1. 先让所有评分排排坐
        Arrays.sort(nums);

        int maxLen = 0;
        int left = 0; // 窗口的左边界
        // 2. right指针不断向右移动,扩大窗口
        for (int right = 0; right < nums.length; right++) {
            // 如果窗口内的最大值和最小值之差 > 1,说明窗口太大了,左边该收缩了
            while (nums[right] - nums[left] > 1) {
                left++;
            }
            // 只有当差值正好为1时,才是一个有效的和谐序列
            if (nums[right] - nums[left] == 1) {
                maxLen = Math.max(maxLen, right - left + 1);
            }
        }
        return maxLen;
    }
}

这个方法虽然在时间上慢一点(因为排序),但在空间上更优。

四、解读力扣上的“提示”

  • 1 <= nums.length <= 2 * 10^4: 数组长度最多2万。O(N^2)(大约4亿次操作)肯定会超时。O(N log N)(大约 20000 * log(20000) ≈ 28万次)和 O(N)(2万次)都是秒过。这印证了我们选择这两种解法的正确性。
  • -10^9 <= nums[i] <= 10^9: 数字范围巨大。这个提示几乎是明示我们:“不要用数组下标来计数,快用哈希表!”

五、举一反三:这个模式还能用在哪?

这个“统计频率,寻找相邻关系”的模式非常实用!

  1. 用户行为分析:分析用户连续登录天数,找到 x 天和 x+1 天登录的用户群体,这可能代表了我们最忠实的用户。
  2. 物联网(IoT)数据处理:分析传感器在一分钟内收集的温度数据,找到温度波动在1度内的最长数据序列,可以用来判断设备是否工作稳定。
  3. 金融风控:分析用户的交易金额,如果大量交易集中在两个非常相近的金额上(如499元和500元),可能是在规避某种金额上限的监管策略。

六、更多练手机会

掌握了这个模式,不妨试试下面这些题目,它们能让你对哈希表的应用更加得心应手:

希望这次的分享,能让你看到算法是如何在真实世界中发光发热的。下次当你面对一堆看似杂乱的数据时,别忘了,一个巧妙的算法可能就是你找到宝藏的那把钥匙!

编码愉快!🚀