性能优化大作战:从 O(N*M) 到 O(N),我的哈希表奇遇记(1865. 找出和为指定值的下标对)

性能优化大作战:从 O(N*M) 到 O(N),我的哈希表奇遇记 😎

嘿,各位奋斗在一线的开发者伙伴们!我是一个热衷于在代码世界里探险的程序员。今天,我想和大家分享一次我在项目中遇到的性能瓶颈以及如何用一个小小的技巧化险为夷的经历。这不仅仅是一个技术分享,更是一次思维上的"升级"之旅。

我遇到了什么问题

故事发生在一个我们正在开发的电商平台的促销活动模块。产品经理提出了一个听起来很酷炫的功能:"心动伙伴价"。

规则是这样的:我们有一批固定的"活动商品"(价格列表A),同时,系统里有成千上万的用户,每个用户的钱包里都有动态变化的余额(余额列表B)。当某个"活动商品价格" A[i] 加上某个"用户余额" B[j] 恰好等于一个特定的"幸运数字" T(比如 888元)时,这个用户就能以一个超级折扣价买下这个商品。

我的任务就是,在活动页面上实时显示出,当前有多少个这样的 (商品, 用户) 幸运组合。

听起来不难,对吧?我最初的想法也很直接:

java 复制代码
// 伪代码,展示最初的思路
int count = 0;
for (int productPrice : productPrices) {
    for (int userBalance : userBalances) {
        if (productPrice + userBalance == target) {
            count++;
        }
    }
}

这方案在我的开发环境里跑得飞快,毕竟测试数据才几十条。但一上线,问题就来了。我们的"活动商品"大概有几百种,但平台的用户量是数十万级别的,而且用户的余额会因为充值、消费而频繁变动!每次调用这个计数接口,服务器CPU都直接飙红,页面加载要转好几秒的圈圈,用户体验简直是灾难。😭

这就是典型的 "暴力解法" 带来的性能灾难。双重循环的时间复杂度是 O(N * M),在我们的场景里就是 几百 * 几十万,每次计算都是千万级别的操作,这谁顶得住啊!

恍然大悟的瞬间:哈希表登场!💡

就在我焦头烂额,准备向DBA大佬求助,看看能不能用数据库索引硬抗的时候,我突然回想起了刷 LeetCode 时遇到的一个经典问题。这个问题和我的场景简直一模一样!

题目链接 :1865. 找出和为指定值的下标对

题目核心 :给你两个数组 nums1 和 nums2。nums1 是静态的,nums2 的元素可以被修改。你需要高效地计算出满足 nums1[i] + nums2[j] == tot 的数对数量。

关键提示解读:

  • nums1.length <= 1000 (商品数量,比较少)
  • nums2.length <= 10^5 (用户数量,非常多)
  • add 和 count 函数最多调用 1000 次 (查询和更新都很频繁)

这个提示简直是在大声告诉我:性能瓶颈在于如何处理庞大且动态的 nums2!

我的"恍然大悟"时刻来了!对于 A[i] + B[j] == T 这个等式,我们完全可以变换一下: B[j] = T - A[i]。

这意味着,对于每一个商品价格 A[i],我们其实是在寻找一个特定数值 的余额 B[j]。在巨大的 userBalances 数组里大海捞针一样地找一个数?这不就是哈希表(HashMap)最擅长的工作吗!🚀

我的解决方案:空间换时间

我决定重构我的服务类,用哈希表来存储用户余额的分布情况。

设计思路
  1. 预处理 :在服务启动时,或者当 userBalances 列表初始化时,我不再只是简单地存一个列表。我用一个 HashMap<Integer, Integer> 来存储每个余额值出现了多少次 。我们称之为 balanceFrequencies。Key 是余额值,Value 是拥有这个余额的用户数量。

  2. 处理更新 (add 方法):当一个用户的余额发生变化时(比如从 200 变成了 300),我需要同步更新我的数据。这是一个"踩坑"点!我一开始只更新了数组,忘了更新哈希表,导致计数一直不准。正确的做法是:

    • 找到旧余额 oldBalance,在 balanceFrequencies 中把它对应的计数减 1。
    • 更新数组中的用户余额。
    • 找到新余额 newBalance,在 balanceFrequencies 中把它对应的计数加 1。 这样,我的频率地图就永远保持最新状态了。😉
  3. 高效计数 (count 方法):这是最酷的部分!现在要计算幸运组合数,我不再需要遍历庞大的用户余额列表。我只需要:

    • 遍历那个小得多的"活动商品"价格列表 productPrices。
    • 对于每个 productPrice,计算出我需要的目标余额 targetBalance = T - productPrice。
    • 直接在我的 balanceFrequencies 哈希表中查找这个 targetBalance 出现了多少次。这个操作的平均时间复杂度是 O(1)!
    • 把查到的次数累加起来,就得到了最终结果。

这样一来,整个计数操作的时间复杂度从 O(N * M) 奇迹般地降到了 O(N)(N是商品数量)!

上代码!

下面是我用 Java 实现的 PromotionCounter 服务类,完美复刻了 LeetCode 的解题思路。

java 复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * 促销活动计数器服务
 * 思路:用哈希表缓存用户余额的频率,实现快速计数。
 * 核心:遍历小数组,查询大数组(的哈希表),这是性能优化的关键!
 */
public class PromotionCounter {

    // 活动商品价格列表(对应 nums1)
    private final int[] productPrices;
    // 用户余额列表(对应 nums2),需要保留它以支持更新
    private final int[] userBalances;
    // 关键武器:用户余额的频率地图!
    // 为什么用 HashMap?因为它提供了平均 O(1) 的查找和插入,太适合这个场景了!
    // 不用 TreeMap 是因为它虽然有序,但操作是 O(logK),在这里没必要。
    private final Map<Integer, Integer> balanceFrequencies;

    public PromotionCounter(int[] productPrices, int[] userBalances) {
        this.productPrices = productPrices;
        this.userBalances = userBalances;
        this.balanceFrequencies = new HashMap<>();

        // 服务初始化时,预先计算好所有用户余额的频率
        System.out.println("🚀 服务启动,正在建立用户余额频率地图...");
        for (int balance : userBalances) {
            balanceFrequencies.put(balance, balanceFrequencies.getOrDefault(balance, 0) + 1);
        }
        System.out.println("✅ 频率地图建立完毕!");
    }

    /**
     * 当用户余额变动时调用此方法(对应 add)
     * @param userIdex 用户的索引
     * @param amount 增加的金额
     */
    public void updateUserBalance(int userIdex, int amount) {
        // 1. 先从频率地图中"注销"旧的余额
        int oldBalance = userBalances[userIdex];
        balanceFrequencies.put(oldBalance, balanceFrequencies.get(oldBalance) - 1);

        // 2. 更新实际的用户余额
        userBalances[userIdex] += amount;
        int newBalance = userBalances[userIdex];

        // 3. 在频率地图中"登记"新的余额
        // getOrDefault 是个好东西,能避免空指针和if-else判断,代码更清爽!
        balanceFrequencies.put(newBalance, balanceFrequencies.getOrDefault(newBalance, 0) + 1);
        System.out.println("😉 用户 " + userIdex + " 的余额已更新!");
    }

    /**
     * 计算幸运组合的数量(对应 count)
     * @param targetSum 幸运数字
     * @return 组合数量
     */
    public int countLuckyPairs(int targetSum) {
        int count = 0;
        // 遍历那个小得多的商品价格列表
        for (int price : productPrices) {
            int targetBalance = targetSum - price;
            // O(1) 复杂度查找!
            count += balanceFrequencies.getOrDefault(targetBalance, 0);
        }
        return count;
    }
}

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

这个"用哈希表预处理一个数据集,以加速与另一个数据集的匹配查询"的模式,在开发中非常实用:

  1. 社交App兴趣匹配:一个静态的"兴趣标签"列表,一个庞大且动态的"用户已选标签"列表。快速计算出两个用户有多少个共同兴趣。
  2. 游戏玩家匹配系统 :你有若干个固定的"游戏房间等级",需要为海量在线玩家(每个人有自己的积分)快速找到可以进入哪个等级的房间(例如 房间等级要求 - 玩家积分 < 阈值)。
  3. 风控系统 :一个已知的"风险IP"列表,需要实时检测海量的用户登录IP,看是否存在 IP_A + IP_B(某种转换后的数值)等于某个风险值的情况。

更多练习,成为高手!

想彻底掌握这个技巧?没有什么比多练习更有效了。下面这几道 LeetCode 题目都是这个思想的绝佳练习:

希望我的这次经历能给大家带来一些启发。记住,当遇到性能问题时,退一步,分析数据规模和操作特性,也许一个简单的数据结构就能让你的程序焕然一新!😎

相关推荐
旺仔仔仔19 分钟前
机器人项目实战(5):从单次抓取,到接收指令连续做事
算法
旺仔仔仔28 分钟前
机器人项目实战(2):识别物体、提取点云,再到抓取姿态
算法
姜鱼问生30 分钟前
密码明明一样,为什么 GPG 说“错的“?
后端·算法·架构
启观川44 分钟前
数据结构与算法-第 3 章 常用算法-贪心算法
数据结构·笔记·python·算法·贪心算法
对空六课1 小时前
指标口径生效时间的版本查询实现
大数据·服务器·前端·算法·数据分析
对空六课1 小时前
RFM分层边界变化的回溯计算
大数据·服务器·前端·数据库·人工智能·算法·数据分析
一勺思维1 小时前
我为什么把 Agent 的核心逻辑从 LangChain 里搬出来手写
算法
碧海银沙音频科技研究院1 小时前
基于炬芯ATS308X的LE aduio LC3编解码算法功能实现
算法
小蒋观天下1 小时前
厂区人员徘徊游荡判别——基于时序轨迹速度的逗留行为分级算法落地
大数据·人工智能·深度学习·算法·机器学习·ai大模型
程序员清风2 小时前
堆与优先队列:从 Top K 到任务调度
java·数据结构·算法