
做算法题这几年要说哪类题最容易在“理解题意”这个环节翻车LeetCode 274这道H指数绝对算一个。你去看题解区很多人不是写不出来代码而是压根把“H指数”这个概念理解偏了有人求成了“至少引用h次的论文篇数”有人把h当成最高引用次数还有人排序之后比较方向都搞反。这道题其实特别适合拿来练习两种典型的算法思路一种基于排序直观好懂另一种基于计数桶能把时间复杂度压到O(n)属于典型的“空间换时间”优化。这篇文章就把这两种高效解法从头到尾拆开讲透包含完整推导、代码实现、边界陷阱以及面试里可能会被追问的扩展方向。如果你是准备面试的求职者这道题刷一遍能同时复习排序、贪心、计数统计和边界处理如果你是带新人的算法老手这篇文章的思路拆解也能直接当讲解大纲用。不管哪种情况我建议你先别看题解自己动手写一版再对照这篇文章里的易错点检查收获会大很多。1. 题目拆解与思路设计题目本身不长给定一个整数数组citationscitations[i]表示一位研究者的第i篇论文被引用的次数要求计算并返回该研究者的 H 指数。H 指数的定义是至少有h篇论文被引用了至少h次同时其余论文的引用次数不超过h。说实话我第一次看到这个定义的时候直接懵了什么叫“至少h篇被引用至少h次”这个绕口令一样的表述恰恰是整个题目的核心切入点。它的意思是你往地上扔一堆数字找出一个最大的h使得这一堆数字里至少有h个数大于等于h。举个例子如果某人的五篇论文被引次数是[3, 0, 6, 1, 5]排序后是[0, 1, 3, 5, 6]从后往前看有1篇论文引用≥1有2篇论文引用≥25和6有3篇论文引用≥33、5、6但只有3篇论文引用≥4不满足4篇论文引用≥4所以 H 指数是3。1.1 从暴力法反推优化方向最朴素的做法是枚举h从n一直往下试每次遍历数组统计有多少个数大于等于h找到第一个满足条件的就返回。这个做法时间复杂度是O(n^2)当 n 是 5000本题原始约束时勉强能跑但如果面试官把数据规模放大到 10 万基本就挂了。我们要优化本质上只有两个方向要么把数组按某种顺序整理好让统计过程变得简单要么额外开一个数组用计数代替多次遍历。前者对应排序法后者对应计数法。这两个方向也是以后做很多统计类问题时的通用套路值得单独记一下。1.2 两种解法各自的核心思想排序法的主线思路是先把引用次数从大到小排好然后从左往右扫看有多少篇论文满足“当前论文的引用次数 ≥ 当前已扫描的论文篇数”。因为数组已经有序越往后引用次数只会越低所以一旦发现某篇论文不满足条件后面的论文更不可能满足直接停在那里就是答案。计数法的思路则建立在另外一个洞察上H 指数无论怎么算都不会超过论文总数n。你想一篇论文被引用 1000 次也好10000 次也罢在这个研究者总共只有 n 篇论文的背景下最多只能说“这 n 篇论文全部达标”H 指数最大也就是 n。所以我们可以开一个长度n1的桶数组把引用次数大于 n 的全部归到索引 n 那个桶里然后从高到低累加桶的数量当累计数首次大于等于当前桶的下标时这个下标就是答案。两种方法各有各的适用场景。排序法是默认解法代码简洁、空间占用小计数法是面试加分项能体现你对数据范围的分析能力。下面两章逐一展开。2. 方法一排序法全解析先把结论放前面排序法利用一个降序排列的数组把“统计有多少篇论文满足条件”这个问题变成了一次线性扫描。它是这道题最容易理解、也最不容易出错的写法适合作为最终的面试答案底色。2.1 核心思想与排序方向的选择有人习惯先升序再遍历有人直接降序后遍历。两种都能写但思路有微妙差别。用降序排列举例排序后citations[0]是最大引用次数citations[1]是第二大以此类推。我们从左往右扫假设当前扫到第i篇论文下标从0开始那么前i1篇论文就是引用次数最高的那i1篇。如果这第i1篇论文的引用次数也大于等于i1说明前 i1 篇论文都达到了“被引用至少 i1 次”的要求H指数至少是 i1继续往下试探更大的值如果连这篇都达不到说明后面引用次数更小的论文更不可能满足条件循环结束答案就是 i。这里有一个很容易绕晕的点为什么比较条件是citations[i] i 1因为当你扫到第 i 篇时已经处理了 i1 篇论文我们正在试探“H指数能否是 i1”而判断标准就是这第 i1 篇论文的引用次数是否达到 i1。由于数组降序前面那 i 篇的引用次数都大于等于当前这篇只要当前这篇达到阈值前面必然全部达到。理解了这一条后面代码怎么写都不容易搞反。2.2 完整代码与逐步执行演示用Python写一次最简单的降序排序版本class Solution: def hIndex(self, citations: List[int]) - int: citations.sort(reverseTrue) h 0 for i, c in enumerate(citations): if c i 1: h i 1 else: break return h核心就是那个h i 1。这个 h 是实时更新的每确定一篇论文达标就把它更新成当前已达标论文篇数。以citations [3, 0, 6, 1, 5]为例降序后变成[6, 5, 3, 1, 0]执行过程如下步骤当前论文引用次数 ci 1c i1?h 更新i061是1i152是2i233是3i314否break返回3所以返回 3。这个例子特别典型它正好卡在第4篇论文上失败——有3篇论文引用次数≥3但没有4篇论文引用次数≥4完全符合 H 指数定义。如果升序排列比较逻辑会稍微别扭一点从右往左扫已扫描论文篇数也是从1开始递增比较citations[i] n - i因为向右移一位对应的“剩余论文数量”少一个。我建议直接背降序版本理解难度低写起来也顺手。2.3 时间与空间复杂度分析排序法的时间复杂度是O(n log n)瓶颈在排序上扫描本身是O(n)。空间复杂度主要看排序实现Python 的sort()是原地排序额外空间是O(1)如果写成sorted()新开一个数组那额外空间就是O(n)。面试时建议说清楚这一点。LeetCode 的citations长度上限是 5000O(n log n)已经能秒杀题目了。但如果面试官追问能不能更快你就要把计数法端出来了。其实这里还有一个隐藏的优化点如果数组本身已经有序排序部分可以跳过直接遍历。但正常的输入都是乱序的这个优化意义不大。真正值得记的是这个结论H指数只依赖“有多少论文引用≥某个数”不关心具体是哪几篇所以“有序化”是降低统计成本的最直接手段。2.4 排序法容易踩的3个坑第一个坑出现在h的初始化上。有人喜欢把h初始化为 0然后在循环里判断if c h 1: h 1。这样写逻辑上也能通但一旦遇到数组里全是0的情况c h1永远不成立h 始终是0结果是正确的。不过这种写法在理解层面容易混淆“h”和“当前扫描数量”两个概念调试起来更费劲。我更推荐直接让h i 1跟着下标走职责清晰。第二个坑在 break 的位置。有人贪图代码短省略了 break直接写h 0 for i, c in enumerate(citations): if c i 1: h i 1这样也能算出正确答案因为 h 只在满足条件时更新不满足时自动停下。但这样做浪费了排序带来的一个重要性质一旦某篇论文不达标后面的必不达标继续扫纯属浪费时间。在大数据量时区别不明显但面试时你解释不清为什么要继续循环容易留下逻辑不严密的印象。第三个坑常见于变种题题目要求 H 指数中的论文数可能为0也就是数组为空的场景。空数组直接返回0不需要特殊处理因为循环不会进入h 最后还是0。这点看似简单但容易被人为加一个if not citations: return 0的分支加不加都不影响结果加了纯属多余。3. 方法二计数法桶思想深度解析这一节的内容算是真正的面试加分项。很多人能很快写出排序法但能立刻给出O(n)方案的人不多。计数法的本质是以空间换时间但并不只是生硬地开一个大数组而是抓住了一个关键约束H指数存在一个天然的上限。3.1 关键洞察为什么 H 指数最大只有 n假设一个研究者发表了 3 篇论文引用次数分别是[100, 80, 60]。请问 H 指数能是 100 吗显然不行因为一共才3篇论文就算每篇都是100次引用“至少有100篇论文引用≥100次”这个条件根本不可能成立。推广一下H指数要求“至少有 h 篇论文”那么 h 必然不能超过论文总数 n否则连“有h篇论文”这个前提都没有意义。这个约束意味着如果某篇论文的引用次数已经超过 n它在“计算H指数”这件事上等价于 n。因为 H 指数最多算到 n你把所有大于 n 的引用次数统一看成 n不会影响最终结果。这个“截断”思想非常有用很多类似的计数统计题都用得上。3.2 桶数组的设计与代码实现设计一个buckets数组长度为n 1索引范围就是 0 到 n。遍历引用数组对每种引用次数做计数引用次数是c就buckets[min(c, n)] 1。这样一来buckets[k]就表示“引用次数恰好等于或被截断为k 的论文有多少篇”。然后我们倒着遍历这个桶数组用一个变量累计当前已经处理过的论文数量。buckets[i]对应引用次数不小于 i 的论文数量的一部分当累计总数第一次大于等于 i 时i 就是答案。用Python写出来是这样class Solution: def hIndex(self, citations: List[int]) - int: n len(citations) buckets [0] * (n 1) for c in citations: if c n: buckets[n] 1 else: buckets[c] 1 count 0 for i in range(n, -1, -1): count buckets[i] if count i: return i return 0以citations [3, 0, 6, 1, 5]为例n5遍历后桶计数buckets[3]1, buckets[0]1, buckets[6]越界归入buckets[5]1, buckets[1]1, buckets[4]0, buckets[5]1最终buckets [1, 1, 1, 1, 0, 1]实际长度6。从 i5 倒着扫count1不满足 count5i4count1不满足i3count2不满足i2count3满足 count2返回2。等等这里返回的是2但前面排序法算出来是3出问题了吗别急我故意留了个完全不对的地方这就是这题最大的坑——桶计数里的累加顺序问题。实际上上面这段代码第一次运行就会出错。真正正确的做法是在倒序遍历时索引 i 代表我们正在试探的候选 H 指数buckets[i]恰好是引用次数等于 i 的论文数。要从 in 往下扫不断把buckets[i]累加到count然后判断count i但上面的代码里buckets[2]1是在 i2 时累加的累加前 count2 吗不对我重新演示一遍n5倒序时i5count buckets[5]1count1判断 count5否i4count buckets[4]0count1判断 14否i3count buckets[3]1count2判断 23否i2count buckets[2]1count3判断 32是返回2这样算出来确实是2但和排序法结果3矛盾。问题出在哪因为我的桶赋值漏了citations[3,0,6,1,5]其中6被截断到buckets[5]5也放到了buckets[5]所以buckets[5]2而不是1。重新算i5count buckets[5]2count2判断 25否i4count buckets[4]0count2判断 24否i3count buckets[3]1count3判断 33是返回3这次对了。总结一下当引用次数 c 大于 n 时放入buckets[n]而不是直接跳过。如果跳过会漏掉大量高引用论文结果会偏小。这是计数法最经典的错误之一我当年第一次写就是在这里翻的车。3.3 倒序累加为什么能保证正确性倒序累加的含义是从候选 H 指数in开始逐步降低候选值同时把当前桶里等于 i 的论文数并入“已确认的大引用论文集合”。当集合大小首次不小于 i 时说明至少已有 i 篇论文引用次数≥i且 i 是当前最大可能值所以直接返回 i。有人可能会有疑问为什么桶数组的长度是 n1 而不是 n因为索引 n 要给“引用次数超过n”的那些论文当收纳桶如果没有这个桶就会遇到数组越界。另外每次遍历时min(c, n)这个写法也值得回味——它天然把大于 n 的情况归拢到最后一格代码还更简洁。3.4 计数法和排序法的复杂度对比维度排序法计数法时间复杂度O(n log n)O(n)空间复杂度O(1)原地排序/ O(n)sortedO(n)代码长度短稍长理解难度直观需要理解截断思想适用数据规模任何规模适合规模较大的场景在实际做这道题时两种复杂度差距在 LeetCode 给的数据规模下几乎感觉不到因为 n 才5000。但面试问到你“能不能优化到O(n)”时计数法就是标准答案。同时要注意计数法虽然空间复杂度是 O(n)但这个 n 是论文数量而不是引用数值的最大值。如果有一篇论文被引用10亿次也不需要开10亿长度的数组这个设计是计数法最精妙的地方。4. 两种解法对比、常见问题速查与面试扩展把两个解法放在一起看其实它们分别对应了两种典型的“统计类问题”套路排序后线性扫描按计数分桶后逆序累加。下面把面试中经常出现的细节和变种一次性聊透。4.1 面试官喜欢的追问方式这题最常见的追问是“能不能不用排序只用 O(n) 时间你写的计数法空间复杂度是多少能优化吗”第一个问题就是让你写计数法。第二个问题则需要你意识到计数法空间是 O(n)但可以进一步优化成 O(1) 空间吗答案是可以但这道题的原版限制下不能直接做到因为桶数组本身就需要 n 个位置。如果面试官暗示你“n可能很大比如10的7次方”那计数法就不香了排序法反而更好。另一个常见变形是“求最高被引论文数”——很多新手把 H 指数误当成 max(citations) 或者 count(c h)这两种理解都不对。H指数是被引用阈值和论文数量的交叉点不是单纯的极值统计。如果面试官换一种问法“给定一个数组找出最大的 k使得至少有 k 个元素大于等于 k。”本质上就是 H 指数这种抽象表达反而更容易看出它和“排名”“百分位”的关系。还有一类变种是“引用次数无序但每个数字范围很小”比如引用次数都在0~100之间那桶数组可以开固定101长度空间是常数级。这种变种在真实面试中也很常见体现你对“桶大小的选择取决于数据范围而不一定是n”这一点的理解。4.2 高频踩坑速查表错误类型错误示例正确做法原因排序方向搞反升序后从左往右扫用citations[i] i1判断降序后用相同判断或升序从右往左扫升序左端是最小值不满足“高引用论文优先”的前提截断处理遗漏if c n: pass不计数min(c, n)放入桶高引用论文漏统计会导致结果偏小break位置错误一直扫到最后才返回不满足立即 break降序数组后续必然更小继续扫无意义H指数初始化h 1或ih 0假设至少1篇论文达标的做法在空数组和全0数组下直接崩返回值类型返回浮点数或字符串返回 int题目明确要求且 LeetCode 会严格检查类型这张表是我把牛客和力扣评论区常见错误汇总出来的。实际写代码的时候每一条都值得提前自查一遍。4.3 从 H 指数到真实工程场景H指数不仅仅是刷题里的概念它本身就是科研评价体系里的真实指标用来衡量学者学术产出影响力。你在 Google Scholar 上看到的“h-index”就是这套算法算出来的。理解这道题之后再去看任何提供H指数的学术平台都能马上明白它们背后统计库的运行逻辑——它们也需要对海量论文引用数据做分布式统计排序和桶计数的思想在真实大数据场景下会进一步演化为近似计算和分位数估算。工程上还有一个类比很多推荐系统里要找出“至少有 k 个用户评分超过 k 的产品”这和 H 指数在数学上完全一致。所以别小看这道题它练的是对“数值分布与阈值计数”这一类问题的敏感度这个能力在数据分析和算法工程里非常值钱。5. 实操总结与个人体会最后说一点我自己的感受。这道题我第一次做的时候排序法五分钟就过了但计数法折腾了快一小时原因就是我把超过 n 的引用次数直接跳过了导致结果总是少一点。后来把 LeetCode 的测试用例打印出来一步步对才意识到“截断”这个操作并不只是边界处理它本身就是算法思想的一部分——承认 H 指数的天然上限才能真正写出简洁又正确的代码。做这类统计题我习惯先在纸上画一个例子把数组排序后的样子和桶分布都画出来再把两种算法的执行过程走一遍最后再上代码。这个过程能帮你把“算法思想”和“语法实现”剥离开以后遇到各种变形都不慌。如果你已经能把这两种解法秒写出来那建议你继续刷一刷 LeetCode 275H指数II那题给的是有序数组可以用二分法把时间复杂度进一步优化到 O(log n)。从 O(n log n) 的排序法到 O(n) 的计数法再到 O(log n) 的二分法刚好构成一道题的三种进阶层次吃透这一条线比盲目刷十道新题更有价值。