好数对计数问题:从暴力循环到哈希表一次遍历的算法优化

发布时间:2026/10/13 12:41:33

好数对计数问题:从暴力循环到哈希表一次遍历的算法优化 “好数对的数目”这个题目我在好几个技术社群里都看到有人讨论。它看起来简单很多人觉得无非就是双层循环暴力数一下但真正动手写的时候才发现这道题其实藏着不少和计数、哈希表运用相关的门道。作为一道经典的入门级算法题它考察的内容非常基础却核心——你对数据结构的理解程度以及能不能从“数数”这件事里提炼出更高效的数学规律。这篇文章我不光会把题目本身拆开揉碎还会把三种主流解法、新旧手踩坑实录、性能实测数据一起聊透希望能给刚开始刷算法题的朋友一点参考。1. 题目整体分析与解题思路拆解1.1 题意拆解与输入输出场景先看题目本身给定一个整数数组nums请你统计“好数对”的数目。所谓好数对就是满足i j且nums[i] nums[j]的一组下标(i, j)。这个定义里有个非常容易被忽略的细节——它要求的是下标有序对也就是说(i, j)和(j, i)是两种不同的情况但因为限定了i j所以实际上我们只统计“前小后大”方向上的配对。举个例子nums [1, 2, 3, 1, 1, 3]按定义数一数下标(0, 3)、(0, 4)、(3, 4)都是值等于 1 的好数对下标(2, 5)是值等于 3 的好数对所以答案是 4。这有点像班级里统计“有多少组同学在同一天过生日”但规矩是只能按学号从前到后数而且两个人同一天生日才算一组。如果有个同学 B 和三个同学同一天生日那就贡献 3 个好数对。这样一想题意就很贴近日常了逻辑上也很好接受。这个题目的输入规模没有特别大按常见评测网站的限制nums长度通常能做到10^4甚至更多数值范围通常是1 ~ 100或者更大。所以你的解法不能只满足“答案对”还必须考虑在数据量变大时时间上能不能扛得住。1.2 三种解法的核心逻辑与取舍这题的标准解法大致可以分成三类暴力双重循环先统计频率再用组合公式边遍历边累计的哈希表单次扫描暴力法最直观两层循环外层固定i内层从i1扫到末尾如果nums[i] nums[j]就计数加一。它的时间复杂度是O(n^2)空间复杂度是O(1)。好处是零基础也能写出来不用想什么技巧适合数组很小的情况。但一旦n上万这循环次数就奔着亿级去了跑起来明显吃力。第二种思路是这样的既然好数对关心的是“相同值的下标两两配对”那完全可以换个角度——先把每个值出现的次数统计出来比如某个数字出现m次那么它内部可以组成的好数对数量就是从m个元素里任取两个的组合数也就是m * (m - 1) / 2。把所有值的这个数加起来就是答案。时间复杂度O(n)空间复杂度O(n)。这个方法在思维上更进了一层但还是要开一个哈希表记录频率。第三种方法是第二种的优化版也是我个人最推荐练习的写法。它不需要先完整统计频率而是只遍历一遍数组在遍历过程中维护一个哈希表count记录“已经遇到过的每个值的出现次数”。每次遇到一个新元素x先检查count[x]是多少这个数字就表示“前面已经出现了多少个和 x 相等的元素”每一个都能和当前下标组成一个好数对所以直接把count[x]加到答案上然后再把count[x]加一。循环结束后答案就是正确的。单看思路三种方法各有侧重。暴力法适合理解题意公式法适合强化数学直觉一遍遍历法则方便你掌握“边遍历边建索引”这个通用套路。下面我会逐一展开讲并附上完整代码。2. 代码实现的逐步解析2.1 暴力双重循环的实现与复杂度先上暴力解法的代码用 Python 写最直白def numIdenticalPairs(nums): n len(nums) ans 0 for i in range(n): for j in range(i 1, n): if nums[i] nums[j]: ans 1 return ans这个写法没什么好解释的就是硬数。内层循环从i 1开始保证了i j这个条件天然成立。每次发现相等就ans 1。为什么还要单独提它因为在实际面试或者刷题初期暴力解往往是我们理解题意的锚点。先把最简单的方法写对再去想优化这是一个很健康的思考路径。直接跳去写最优解有时候反而会让你对题意的理解产生偏差尤其是早期题目。不过暴力解法的瓶颈也很明显假设n 10000内层循环大约执行5000万次Python 跑起来可能要几百毫秒甚至更久如果评测系统卡时间比较紧就很容易超时。所以它只能作为思路演示不是最终答案。提示暴力法虽然时间复杂度高但它实现简单、不容易写错特别适合在纸上推演或做小规模用例的“标准答案”来验证其他解法的正确性。2.2 哈希表统计法与核心公式推导哈希表统计法的思路是把“配对”问题转化为“组合”问题。先说公式是怎么推出来的。如果某个数字v在数组里出现了m次那么这些值相同的位置一共有多少个满足i j的好数对答案就是组合数C(m, 2)也就是从m个位置里任选两个位置较小下标的当i较大下标的当j恰好组成一个好数对。组合公式展开是C(m, 2) m * (m - 1) / 2举个例子如果数字1出现了 5 次那么它能贡献的好数对数量是5 * 4 / 2 10。你可以自己手写验证一下5 个位置里选两个必然有 10 种组合。对应代码from collections import Counter def numIdenticalPairs(nums): freq Counter(nums) ans 0 for count in freq.values(): ans count * (count - 1) // 2 return ans这里用 Python 标准库collections.Counter一行就能完成频率统计代码非常简洁。如果你不想依赖工具库也可以自己用字典实现def numIdenticalPairs(nums): freq {} for x in nums: freq[x] freq.get(x, 0) 1 ans 0 for count in freq.values(): ans count * (count - 1) // 2 return ans时间复杂度和空间复杂度都是O(n)。这个解法的好处是逻辑清晰先统计后计算数学公式简洁。它很适合你用来验证自己是否理解了“好数对”本质上是组合计数问题。不过它有个小不足需要先完整遍历数组建好频率表然后二次遍历频率表求和。相比最优解法它可以被进一步压缩成“边遍历边加”也就是下面要讲的版本。2.3 计数器一次遍历与边界细节最优也最优雅的写法其实是把“统计频率”和“累加答案”合并到一趟循环里。核心逻辑就这几行def numIdenticalPairs(nums): count {} ans 0 for x in nums: ans count.get(x, 0) count[x] count.get(x, 0) 1 return ans我来仔细解释一下这段代码的执行过程。假设数组是[1, 1, 1]第一个1count里还没有1count.get(1, 0)返回 0答案加 0。然后把count[1]设为 1。第二个1count.get(1, 0)返回 1答案加 1表示(0, 1)这一对。然后把count[1]改成 2。第三个1count.get(1, 0)返回 2答案加 2表示(0, 2)和(1, 2)这两对。然后把count[1]改成 3。答案最终是 3正好等于3 * 2 / 2 3。这里面的妙处在于count[x]代表的是“在当前下标之前已经出现过的 x 的个数”每遇到一个 x这些之前出现过的 x 都能和当前 x 组成一对新的好数对。所以答案天然就是累加不重不漏。这个方法同样是O(n)时间、O(n)空间但它只有一次遍历比先统计再二次遍历更紧凑也更容易扩展——比如以后遇到“边统计边求答案”的题目你会有直觉。边界情况也很重要空数组不会进入循环返回 0。单个元素遍历一次答案加 0返回 0。所有元素都不同每次get返回 0答案始终为 0。所有元素都相同等价于n个相同元素两两配对答案是n * (n - 1) // 2。注意在不同语言里哈希表的读取方式不一样。Java 里用map.getOrDefault(x, 0)C 里可以用unordered_map但要注意区分count[x]和map.find(x)的差别——直接用count[x]会默认插入一个值为 0 的键影响统计。我见过不少新手在 C 里踩这个坑。3. 实操验证与性能实测记录3.1 测试用例的构造与边界覆盖写代码只是第一步想要确定自己写对了需要构造一组覆盖面足够广的测试用例。我平时刷题有个习惯不光测题目给的示例还要自己加几组“刁钻”的数据。对这道题来说我建议至少覆盖这些情况示例1: [1, 2, 3, 1, 1, 3] - 4 示例2: [1, 1, 1, 1] - 6 全不同: [1, 2, 3, 4, 5] - 0 空数组: [] - 0 单元素: [7] - 0 两元素相同: [5, 5] - 1 重复交替: [1, 2, 1, 2, 1, 2] - 6第 6 个用例[1, 2, 1, 2, 1, 2]可以手算验证数字 1 出现 3 次贡献 3 个好数对数字 2 出现 3 次贡献 3 个好数对总数为 6。这个用例能同时检验两个值的计数逻辑是否正确。我建议写一个简单的断言脚本把所有用例一次跑完这样可以快速捕捉到边界条件错误。如果所有用例都通过再提交到评测系统心里会踏实很多。实测下来我写了一个长度 10000 的随机数组来做性能粗测。暴力解法在本地跑了大约 680 毫秒哈希表统计法不到 2 毫秒一遍遍历法也是 1 毫秒级别。差距非常明显。这也再一次说明O(n^2)和O(n)的差距在数据量上来后就是天壤之别。3.2 不同解法的时间与空间消耗实测下面是我在本地环境普通配置Python 3.10用n 10000随机整数数组做的实测参考数据解法时间复杂度空间复杂度10000 条数据耗时约暴力双重循环O(n^2)O(1)约 680 ms哈希表统计法O(n)O(n)约 1.8 ms一遍遍历计数法O(n)O(n)约 1.5 ms如果把数据量提高到 50000暴力法运行时间会急剧膨胀到十几秒甚至更久而一遍遍历计数法依然在毫秒级别。实际审核题目时如果数组长度上限在 10000 左右暴力法在部分语言里也可能压线通过但没必要冒这个风险。还有一个在实际测试中容易被忽略的问题ans 的数值范围。假如数组长度是10^5而且所有数字都相同那么答案就是10^5 * (10^5 - 1) / 2约等于 50 亿已经超过 32 位整数的范围了。用 C 或 Java 时要留意ans变量选longPython 的整数没这个问题但如果你用其它语言这里就是隐藏的扣分点。4. 常见错误、调试技巧与拓展思考4.1 新手最容易踩的三个坑这个题虽然简单但我在自己刷题和帮别人 review 代码的过程中见过不少新手踩重复的坑这里集中梳理一下。第一个坑把下标顺序弄反或者重复统计。有些人写暴力循环时内层j从 0 开始而不是从i1开始这样会把(i, j)和(j, i)都算一遍答案变成原来的两倍。虽然题目定义里限制了i j但如果你忽略这一点示例 1 会得出 8 而不是 4。这个错误在逻辑上很好理解但实际上非常容易犯尤其是从其它语言转到 Python 后对range的边界不够敏感。第二个坑哈希表逻辑写成了“先更新再查”。有些朋友看完一遍遍历法后思路没理顺会把代码写成count[x] count.get(x, 0) 1 ans count[x]这样写就错了因为count[x]已经包含了当前元素自身。比如第二个1出现时更新后count[1]是 2ans 直接加 2等于把(1, 1)这个不存在的下标对也算进去了。正确顺序一定是先查再加最后才更新计数。第三个坑忽略超大整数的溢出。前面已经提过如果数组很长且数值集中的话答案会非常大。C 里用int存 ans 可能直接溢出成负数提交后莫名其妙 WA答案错误却找不到原因。实操心得遇到这个题我建议无论在面试还是日常练习中都优先写一遍遍历法。它不是最朴素的也不是最难懂的但它在代码简洁度和性能之间达到了最优平衡。4.2 从“好数对”到一大类计数题的套路这道题最有价值的地方不在于题目本身有多难而在于它是很多“数组计数类问题”的最小公共内核。你仔细看一遍遍历法的代码本质上就是这样一个模板count {} ans 0 for x in nums: # 先利用已有的统计信息更新答案 ans count.get(x, 0) # 再把自己纳入统计 count[x] count.get(x, 0) 1这个“先查再更新”的模板能解决的题目远不止这一道。比如经典的“两数之和”要求在数组中找到两个数之和等于目标值target它其实就可以用类似的“一边遍历一边查表”的思路来做——先查target - x是否已经在哈希表里再把x加入哈希表。区别只在于这里统计的是值和下标但核心的“在线查询 动态建表”模式完全一样。再比如统计一个字符串里每个字符第一次出现和最后一次出现的距离、找到数组中最短的重复元素间距等问题底层都可以归结为遍历过程中维护好一个“当前位置之前的信息表”然后用这个表去推算答案。所以我的建议是不要只记住这道题的解法要把“好数对”当作一个引子去体会哈希表如何把“寻找配对”这样看似需要两重循环的事情降维成一重循环。我见过不少刚开始刷题的朋友今天背一道题明天背一道题但遇到变体还是不会。真正有用的做法是像这样把一类题的共性抽出来形成自己的“解题模式库”。拿这道题来说如果你能把“先查再更新”和“相同元素配对 组合数”这两个点都吃透那我可以负责任地说相关的一系列简单到中等难度的计数题你都有了清晰的思考路径。4.3 针对不同语言的编码细节虽然核心算法思路不分语言但在落地的时候不同语言有自己的习惯和陷阱我简单补充几句。Python 里最自然的写法就是用dict搭配get方法来处理默认值。如果你想追求更快的常数也可以考虑defaultdict(int)但要注意它取一个不存在的键时会自动创建使用不当可能干扰你判断“之前是否出现过”。Java 里一般用HashMapInteger, Integer读取时用map.getOrDefault(x, 0)更新时用map.put(x, map.getOrDefault(x, 0) 1)。Java 的自动装箱机制在这里比较温和不会有额外大问题但如果用HashMap做高频计数性能上可能比用int[]稍慢。考虑到本题数值范围如果比较小比如明确写了1 nums[i] 100还可以直接用长度 101 的数组当计数器这样代码更清爽性能也更好。C 里最常见的实现是unordered_mapint, int。但刚才提到的坑要特别留心如果你直接用map[x]去读取不存在的键它会默认插入(x, 0)这一方面改变了 map 的内容另一方面第一次读到的值会是 0虽然有时候碰巧不影响结果但在更复杂的问题里可能造成潜在 bug。更稳妥的写法是先find判断再操作或者用insert配合迭代器。不过这个题的语境下用unordered_map的find然后手动处理确实会膨胀代码量所以不少人直接用count数组代替。没必要觉得这些细节烦琐实际写题时把环境切换到对应语言多敲几次自然就记住了。真正容易卡的通常不是语法而是思路——你只要把“配对变组合、遍历变在线统计”这个核心逻辑想明白任何语言都只是表达方式不同而已。结尾我个人在刷算法题的时候有一个习惯——每做完一道题都会停下来问自己三个问题这道题的暴力解法我能不能一眼写出来最优解比暴力解省在了哪里这种“省”的思路还能用到哪些题上好数对的数目就是一道很适合拿来练这个流程的题目。它足够简单让你有余力去关注思路的每一个转折点而不是把精力耗在复杂的边界处理上。最后再分享一个调试小技巧如果你写完一遍遍历法后答案总是不对可以先退一步用暴力法跑一个很小的测试用例然后把两种写法的结果打印出来对比。别小看这个笨办法它往往能最快地帮你定位到“先查再更新”的顺序到底错在哪里。等顺序问题弄清楚了这道题连同它背后的哈希表套路才算是真正装进了你的脑子里。
延伸阅读

更多相关文章

2026/10/13 12:41:33

HTML基础避坑实战笔记:从DOCTYPE到XSS注入全解析

1. 这份HTML笔记整理的出发点 HTML大概是所有编程语言里最让人“轻敌”的一个。你随便百度一下&#xff0c;满屏都是“前端入门第一步”“三小时学会网页制作”&#xff0c;好像写几个标签就完事了。但真正做着做着就会发现&#xff0c;光一个 <!doctype html><html …

2026/10/13 12:41:33

金三银四微派内推全攻略:从技术栈拆解到面试实操

金三银四一到&#xff0c;微信里各种“内推”消息就开始刷屏&#xff0c;而今年被问得最多的是“微派”。很多朋友跑来问我&#xff1a;这家公司到底怎么样&#xff1f;内推进去有没有捷径&#xff1f;面试会考什么&#xff1f;这篇文章我就围绕“内推、金三银四、微派”这三个…

2026/10/13 13:46:48

抗体偶联空载LNP试剂盒:实现脾脏靶向递送的灵活平台

这几年做核酸药物递送的研究&#xff0c;有一个话题绕不开&#xff1a;LNP做完静脉注射之后&#xff0c;绝大多数都跑去了肝脏。想要让核酸疫苗、基因编辑工具或者免疫调节分子精准地到脾脏里发挥作用&#xff0c;靠常规LNP非常吃力。脾脏是人体最重要的免疫器官之一&#xff0…

2026/10/13 13:46:48

IDA Pro逆向实战:反汇编、交叉引用与修改跳转完成软件破解

简介&#xff1a;面向逆向工程初学者的IDA Pro入门实战PDF教程&#xff0c;配合Windows PE开发环境和160个CrackMe经典题目&#xff0c;讲解反汇编工具的基础使用与解密思路。内容涵盖IDA Pro安装与新建工程、IDA View/Hex View/Strings常用窗口、文件类型识别&#xff0c;以及…

2026/10/13 13:46:48

pandas数据拼接与合并实战:concat与merge核心技巧及避坑指南

1. 为什么值得花时间重新梳理pandas的拼接与合并做数据挖掘这行&#xff0c;绕不开的一件事就是“把数据拼起来”。你从业务库导出一份订单表&#xff0c;从日志系统拉一份用户行为表&#xff0c;从第三方接口拿一份商品维度表&#xff0c;最后要做的第一件事往往不是建模&…

2026/10/13 13:46:48

Agent技能评测:分层拆解评测系统的工程实践

给 Agent 技能打分&#xff0c;乍看是个标准的测评话题&#xff0c;但我系统性啃完 9 篇和技能评测相关的前沿论文后&#xff0c;最大的感触是&#xff1a;这件事根本不是"出题、考试、给分"的线性流程&#xff0c;而是一道必须分层拆解的工程题。技能评测要同时处理…

2026/10/13 13:46:48

调试Hook技术实战:从LD_PRELOAD到inline Hook的追踪指南

Hook技术&#xff0c;尤其是调试定位问题要用的这类Hook&#xff0c;我这两年复盘下来&#xff0c;觉得是最值得花时间掌握的工具之一。很多人一听Hook&#xff0c;第一反应是“神秘底层技巧”&#xff0c;好像只跟恶意软件相关。实际上Hook只是把程序原本要走的某条路&#xf…

2026/10/13 13:41:48

PHP+Uniapp轻量级食品安全检测系统开发实践

1. 项目概述"phpuniapp食品安全信息检测管理系统"是一个面向食品行业的质量监管解决方案&#xff0c;它通过小程序端实现便捷的食品安全信息采集与查询&#xff0c;后端采用PHP构建数据处理中心。这个36KB的精简版本特别适合中小型食品企业快速部署&#xff0c;在保证…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”&#xff0c;而是它给出一段看起来合理的说明&#xff0c;程序却从中猜错优先级。通俗做法是&#xff1a;要求模型只交 JSON&#xff08;JavaScript Object Notation&#xff0c;轻量数据格式&#xff09;&#xff0c;再让代码验证它。Gem…

2026/10/12 12:01:26

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里&#xff0c;有A同学跑来问我&#xff1a;选什么毕设题目最稳妥&#xff0c;既能让评审老师觉得工作量够&#xff0c;又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友&#xff0c;在看完一堆“Hello World”和基础组件之后&#xff0c;大概率都会撞上同一堵墙&#xff1a;StatefulWidget 里那堆 initState、build、dispose 方法&#xff0c;到底什么时候被调用&#xff1f;为什么顺序是那样&#xff1f;在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介&#xff1a;这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师&#xff0c;聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案&#xff0c;解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件&#xff0c;约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型&#xff0c;最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支&#xff0c;让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图&#xff0c;十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑