ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

H指数算法题:从排序到二分查找的O(log n)优化

H指数算法题:从排序到二分查找的O(log n)优化 1. 从被引用次数说起H指数到底在衡量什么第一次遇到H指数这道题很多人会愣一下——这名字听着像学术圈的指标怎么跑到算法题里来了。其实它的定义非常朴素给定一个整数数组citations其中citations[i]表示某位研究者第 i 篇论文被引用的次数我们需要找到一个最大的 h使得至少有 h 篇论文的被引用次数都不低于 h。这个定义读起来有点绕但换个说法就清楚了。假设你手上有 5 篇论文引用次数分别是[3, 0, 6, 1, 5]。我们按从高到低排一下[6, 5, 3, 1, 0]。现在从前往后数第 1 篇有 6 次引用至少 1 篇满足≥1第 2 篇有 5 次至少 2 篇满足≥2第 3 篇有 3 次至少 3 篇满足≥3第 4 篇只有 1 次不满足≥4。所以 H 指数就是 3。我在带新人刷题的时候发现大部分人卡住不是因为不会写代码而是没真正理解这个至少 h 篇不低于 h的双重约束。它同时限制了数量和数值两个维度这才是 H 指数问题的核心难点也是后面所有优化思路的出发点。1.1 为什么暴力解法容易写错最直觉的做法是把数组降序排列然后从 h n 开始往下试对每个 h 检查前 h 个元素是否都 ≥ h。这个思路没错但新手经常在边界上翻车。比如数组是[100]答案是 1 而不是 100数组是[0]答案是 0。还有人会把前 h 个元素都 ≥ h错写成前 h 个元素之和 ≥ h这就完全跑偏了。另一个常见错误是排序方向搞反。如果升序排列判断逻辑就得反过来写很容易绕晕。我的建议是统一降序处理让前 h 个和第 h 个的语义保持一致减少心智负担。1.2 排序解法的复杂度瓶颈在哪排序解法的时间复杂度是 O(n log n)空间复杂度取决于排序实现。对于 n 在几千以内的数据这完全够用。但题目如果给出 n 高达 10^5 甚至更大并且暗示数组已经有序那排序就是浪费——你本可以利用有序这个前提把复杂度压到 O(log n)。这就是这道题被归类为二分查找变体的原因。它考察的不是你会不会二分而是你能不能识别出答案具有单调性这个隐藏条件。H 指数有一个关键性质如果 h 是可行的那么所有小于 h 的值也都可行如果 h 不可行那么所有大于 h 的值都不可行。这种单调性正是二分查找的适用信号。2. 二分查找变体的识别信号单调性藏在哪很多人学二分查找只记住了有序数组里找目标值这个模板一旦题目换个马甲就认不出来。H 指数这道题的价值就在于它逼你去思考二分查找的本质到底是什么答案不是数组有序而是搜索空间具有单调性。只要你能构造出一个判定函数check(h)使得它随着 h 增大而从 true 单调变为 false或反过来就能二分。数组是否有序只是实现这个判定的一种手段不是必要条件。2.1 把判定函数设计成二分的抓手对于 H 指数我们可以定义check(h)返回是否存在至少 h 篇论文的引用次数 ≥ h。在数组已升序排列的前提下这个判定可以 O(1) 完成——只需要看倒数第 h 个元素即索引n - h处的值是否 ≥ h。因为升序数组里如果倒数第 h 个都 ≥ h那它后面的 h 个元素自然都 ≥ h。这个 O(1) 判定函数是整道题的点睛之笔。有了它二分查找的搜索空间就是[0, n]每次取中点 mid调用 check(mid)根据结果收缩边界。总复杂度 O(log n)非常优雅。我见过有同学非要每次 check 都去遍历一遍数组那样复杂度退化成 O(n log n)虽然也能过但完全没体现出二分的优势面试官一眼就能看出你没抓住重点。2.2 搜索区间的开闭选择与死循环陷阱二分查找最容易出 bug 的地方就是区间定义。我个人的习惯是统一用左闭右闭[left, right]循环条件是left right更新时left mid 1或right mid - 1。这套写法逻辑自洽不容易死循环。但 H 指数有个特殊之处答案可能是 0也可能是 n。如果搜索区间设成[0, n-1]就会漏掉答案为 n 的情况比如[100, 100]答案是 2。所以区间必须设成[0, n]让 n 也在候选范围内。还有一个细节当 check(mid) 为 true 时说明 mid 可行但可能存在更大的可行值所以要记录答案并向右收缩left mid 1当 check(mid) 为 false 时说明 mid 太大向左收缩right mid - 1。这个记录 收缩的模式比直接返回 mid 更稳妥因为它能处理多个可行值取最大的场景。3. 从零手写一遍升序数组上的 O(log n) 实现光说原理不够我们直接把代码敲出来。假设输入数组已经按升序排列这是二分变体能够成立的前提。如果题目给的是无序数组要么先排序退化成 O(n log n)要么用计数排序的思路做到 O(n)这个后面再展开。3.1 核心代码逐行拆解def hIndex(citations): n len(citations) left, right 0, n ans 0 while left right: mid left (right - left) // 2 # 升序数组中倒数第 mid 个元素索引为 n - mid if mid 0 or citations[n - mid] mid: ans mid left mid 1 else: right mid - 1 return ans逐行看几个关键点。mid left (right - left) // 2这种写法是为了防止left right溢出虽然在 Python 里整数不会溢出但养成习惯没坏处换到 C 或 Java 就是必须的。mid 0这个判断是边界保护。当 mid 为 0 时n - mid等于 n会越界。而 h 0 永远可行至少 0 篇论文引用 ≥ 0这是废话但逻辑上成立所以直接判定为 true。citations[n - mid] mid是核心判定。为什么是n - mid因为升序数组里最大的 mid 个元素位于索引n-mid到n-1。如果这 mid 个元素中最小的那个即索引n-mid都 ≥ mid那这 mid 个全部满足条件。3.2 手动跑一遍验证逻辑拿[0, 1, 3, 5, 6]举例n 5。初始 left0, right5。第一轮 mid2检查citations[5-2]citations[3]5 2成立ans2left3。第二轮 left3, right5mid4检查citations[5-4]citations[1]1 4不成立right3。第三轮 left3, right3mid3检查citations[5-3]citations[2]3 3成立ans3left4。第四轮 left4 right3循环结束返回 3。和暴力解法的结果一致验证通过。这个过程里你能清楚看到二分是如何一步步逼近最优解的。每一轮都把搜索空间砍半5 个元素只用了 4 轮就收敛换成 10^5 个元素也只需要约 17 轮。3.3 无序数组的两种处理策略如果题目没有保证数组有序你有两条路可走。第一条是老老实实排序然后套上面的二分总复杂度 O(n log n)。第二条是用计数排序的思想开一个大小为 n1 的桶数组把每个引用次数映射进去超过 n 的统一算作 n然后从后往前累加找到第一个满足累计篇数 ≥ 当前引用次数的位置。计数排序的解法复杂度是 O(n)空间 O(n)在 n 很大且引用次数分布集中的场景下优势明显。但它牺牲了空间而且代码量更大。我的经验是面试时如果面试官没特别要求先写排序二分的版本清晰易懂如果追问能否优化到线性再补充计数排序的思路。4. 那些年我在H指数上踩过的坑这道题看起来简单但我在不同阶段反复栽过跟头。下面这几个坑几乎每个刷题的人都会遇到至少一个。4.1 边界值 0 和 n 的处理最常见的错误是忘记 h 可以等于 0。当所有论文引用次数都是 0 时答案是 0。如果你的二分区间从 1 开始就会返回错误结果。另一个极端是 h 可以等于 n比如[10, 10, 10]答案是 3。如果区间上界设成 n-1就会漏掉这个情况。我的做法是区间固定为[0, n]让两个边界都在搜索空间内然后用ans变量记录最后一次可行的 mid。这样无论答案是 0 还是 n都能正确返回。4.2 升序降序判断逻辑的镜像关系如果你选择降序排列判定逻辑就变成检查索引h-1处的元素是否 ≥ h。因为降序数组里前 h 个元素是最大的 h 个第 h 个索引 h-1是其中最小的。这个镜像关系很容易记混我建议在纸上画一个 5 元素的例子两种排序各推一遍把对应关系固化下来。提示无论升序还是降序核心都是找到第 h 个元素判断它是否 ≥ h。升序看索引 n-h降序看索引 h-1记住这个对应关系就不会乱。4.3 二分收缩方向写反导致的死循环这是最隐蔽的坑。如果 check(mid) 为 true 时你写成了right mid而 mid 的计算又是向下取整那么当 left 和 right 相邻时mid 会一直等于 left区间无法收缩程序死循环。避免这个问题的办法是true 时收缩到mid 1或mid - 1确保每次迭代区间至少缩小 1。我个人的模板是 true 时left mid 1false 时right mid - 1配合left right的循环条件从来没出过死循环。4.4 把至少h篇理解成恰好h篇这个坑属于理解层面的。题目要求的是至少有 h 篇论文的引用次数 ≥ h不是恰好 h 篇。如果你按恰好去写判定会漏掉很多可行解。比如[5, 5, 5]h3 时三篇都 ≥ 3满足至少 3 篇但如果按恰好 3 篇 ≥ 3去理解虽然结果一样逻辑上却容易在别的例子上出错。正确的理解是只要满足条件的论文数量 ≥ hh 就可行。判定函数要检查的是够不够而不是刚刚好。5. 复杂度对比与不同解法的适用场景把这道题的所有解法摆在一起对比能帮你建立更完整的认知。不同解法没有绝对优劣关键看数据规模和题目约束。解法时间复杂度空间复杂度适用场景暴力枚举O(n²)O(1)n 很小教学演示排序 线性扫描O(n log n)O(1) 或 O(n)通用场景代码简单排序 二分O(n log n)O(1) 或 O(n)数组已有序时退化为 O(log n)计数排序O(n)O(n)n 很大引用次数范围集中从表格能看出如果数组已经有序排序二分里的排序步骤可以省掉直接变成 O(log n)这是最优解。如果数组无序但 n 很大计数排序的 O(n) 更有优势。如果只是日常刷题或面试排序线性扫描的写法最不容易出错性价比最高。5.1 什么时候该用二分什么时候不该用二分的适用条件是搜索空间单调 判定函数可快速求值。H 指数完美满足这两点。但如果判定函数本身就需要 O(n) 时间那二分带来的 O(log n) 外层循环反而让总复杂度变成 O(n log n)和直接扫描没区别甚至更慢。所以每次想用二分之前先问自己两个问题搜索空间是否单调判定函数能否在 O(1) 或 O(log n) 内完成两个都是 yes才值得上二分。5.2 计数排序解法的实现要点计数排序的思路值得展开说一下因为它在很多统计类题目里都能复用。核心是开一个大小为 n1 的数组bucket遍历 citations对于每个引用次数 c如果 c n 就把bucket[n]加一否则把bucket[c]加一。然后从 n 往下遍历用一个变量count累加已经遍历过的论文数当count i时i 就是答案。这个解法的巧妙之处在于它把排序这个 O(n log n) 的操作换成了 O(n) 的桶计数代价是需要额外 O(n) 空间。在引用次数普遍较小、分布集中的真实场景里这个 trade-off 非常划算。6. 从这道题延伸出去二分变体的通用套路H 指数只是二分变体家族里的一个成员。掌握它的解题框架后你会发现一大类题目都能用同样的思路解决。6.1 答案二分的通用模板所谓答案二分就是不对数组本身二分而是对答案的可能取值范围二分。模板大致如下def solve(nums): left, right 最小可能答案, 最大可能答案 ans 初始值 while left right: mid left (right - left) // 2 if check(nums, mid): ans mid left mid 1 # 或 right mid - 1取决于求最大还是最小 else: right mid - 1 return ans这个模板的关键在于check函数的设计。它需要根据具体题目来定制但核心思想都是判断某个候选答案是否可行。H 指数的 check 是是否有至少 mid 篇论文引用 ≥ mid其他题目的 check 可能是能否在 mid 天内完成所有任务容量为 mid 时能否装下所有货物等等。6.2 几道可以对照练习的同类题如果你想巩固这个套路可以找这几类题练手分割数组求最小最大和、爱吃香蕉的珂珂、在 D 天内送达包裹的能力。它们的共同点是答案在一个连续区间内且具有单调性判定函数可以快速求值。练的时候不要只写代码要在纸上把搜索区间、判定函数、收缩方向这三件事写清楚。我见过太多人代码能跑通但说不清为什么一到面试就露馅。6.3 二分查找的调试技巧最后分享一个我常用的调试方法在二分循环里打印每一轮的 left、right、mid 和 check 结果。对于小规模输入手动对照预期输出能快速定位是区间设错、判定写错还是收缩方向反了。另一个技巧是构造极端用例全 0 数组、全相同值数组、严格递增数组、答案在边界的数组。这四类用例能覆盖绝大多数边界 bug。我在本地测试时这四类用例是必跑的跑完基本就能确认逻辑没问题。注意二分查找的 bug 往往在特定输入下才暴露不要只测一两个用例就提交。养成构造边界用例的习惯能帮你省下大量排查时间。7. 我在实际刷题中的几点体会刷题刷到一定阶段你会发现真正拉开差距的不是会不会写某个算法而是能不能快速识别题目属于哪一类。H 指数这道题给我的最大启发是不要被题目的表面描述吓住先把它翻译成数学语言再看这个数学结构有没有可利用的性质。至少 h 篇论文引用 ≥ h翻译过来就是存在 h 个元素都 ≥ h这是一个关于计数和阈值的约束。一旦识别出这个结构单调性就自然浮现二分也就顺理成章了。另外我建议每道题至少写两种解法。H 指数我写过排序扫描、排序二分、计数排序三个版本写完之后对时间换空间和空间换时间的理解会深刻很多。这种理解不是看题解能获得的必须自己动手敲一遍、跑一遍、改一遍。最后说个细节这道题在面试里经常被用来考察你能不能主动优化。面试官先让你写个 O(n log n) 的然后问如果数组已经有序呢这时候如果你能立刻反应出 O(log n) 的二分印象分会很高。所以平时练习时多问自己一句如果条件变了我的解法还能不能更快这个习惯比多刷十道题更有价值。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进