从插入排序调试到排序后判定
本文分析你提供的 hIndex 初版和改进版,重点拆解插入排序的写回逻辑、H 指数的数学判定、数组越界风险和返回值错误。核心结论是:排序本身不是难点,关键是排序方向与判定公式必须保持一致。
官方题目: 274. H 指数。
一 题目模型与约束
给定一个研究者的论文引用次数数组 citations,其中 citationsi 表示第 i 篇论文获得的引用次数。H 指数是最大的 h,使得至少有 h 篇论文的引用次数不少于 h。
|--------|---------------------------------|
| 项目 | 题目规则 |
| 数组长度 | 1 <= citations.length <= 5000 |
| 引用次数 | 0 <= citationsi <= 1000 |
| 输入顺序 | 未排序,不能假设原数组有序 |
| H 指数定义 | 至少 h 篇论文满足 citationsi >= h |
| 目标 | 返回最大的 h |
H 指数不是数组中间位置的值,也不是某篇论文的引用次数本身。它是一个"数量与阈值同时满足"的最大整数,因此必须在排序后逐个检查"第几篇"和"该篇引用数"的关系。
二 H 指数的数学转化
如果把 citations 按从大到小排序,排序后的第 i 个元素对应"至少有 i + 1 篇论文的引用次数不低于 citationsi"。当 citationsi >= i + 1 时,前 i + 1 篇论文满足 H 指数条件,因此候选值为 i + 1。
|----------|--------------------------|----------------|
| 排序方向 | 判定写法 | 答案更新 |
| 降序 | citationsi >= i + 1 | h = i + 1 |
| 升序 | citationsn - h >= h | 从 h = 1 向上检查 |
| 固定中位数 | citationsn / 2 | 没有 H 指数依据,不能使用 |

图 1 降序排列后,前三个位置满足 citationsi >= i + 1,因此 h = 3。
三 初版代码分析
int hIndex(int* citations, int citationsSize) {
int p = 0;
for (int i = 1; i < citationsSize; i++) {
p = citationsi;
for (int j = i - 1; j >= 0; j--) {
citationsj + 1 = citationsj;
if (p <= citationsj) {
citationsj = p;
break;
}
}
}
return citationscitationsSize / 2;
}
这段代码试图实现插入排序,并把数组排成升序:从前面已经有序的部分中向前寻找第一个不小于 p 的位置。这个方向本身可以成立,但实现遗漏了插入排序最重要的收尾动作。
|--------------------------|------------------------------|------------------|
| 问题 | 最小反例或表现 | 影响 |
| p 比所有已排序元素都大时没有写回 | 1,3,6,8 插入 8 后可能丢失 8 | 元素被覆盖,数组不再保存原数据 |
| 直接返回 citationsn/2 | 0,1,4,5,6 中位值是 4,H 指数是 3 | 排序结果即使正确,答案仍可能错误 |
| 没有限制 h <= citationsSize | 5 返回 5,正确答案是 1 | 把引用次数误当成 H 指数 |
| 修改了输入数组 | 调用后 citations 顺序改变 | 题目通常允许,但应明确这是副作用 |

图 2 初版在 p 大于所有已排序元素时移动了数据,却没有把 p 放入最后空位。
标准插入排序的最后一步必须是 citationsj + 1 = p。当前代码只有在 p <= citationsj 时才写 citationsj = p;如果 p 一直较大,内层循环结束时没有任何写回。
四 改进版分析
int hIndex(int* citations, int citationsSize) {
int p = 0;
for (int i = 1; i < citationsSize; i++) {
p = citationsi;
for (int j = i - 1; j >= 0; j--) {
if (p > citationsi - 1) {
citationsj + 1 = citationsj;
if (p <= citationsj) {
citationsj = p;
break;
}
}
}
}
for (int i = 0; i < citationsSize; i++) {
if (i + 1 > citationsi) {
return citationsi - 1;
}
}
return 0;
}
改进版把排序和答案判定拆成了两个阶段,这是结构上的进步,但两个阶段的条件仍然不正确。
|--------|----------------------------------------------------------------------|----------------------------------------|
| 位置 | 问题 | 反例 / 后果 |
| 排序条件 | p > citationsi - 1 不是"向前移动"的通用条件,且 citationsi - 1 会在移动过程中变化 | 1,3,2 不能稳定排好 |
| 排序收尾 | 仍没有无条件执行 citationsj + 1 = p | p 可能丢失,数组出现重复值 |
| H 判定 | i + 1 > citationsi 时返回 citationsi - 1 | 0,1,3,5,6 在 i=0 访问 citations-1 |
| 返回对象 | 返回引用次数而不是 h | 100,1 可能返回 100,正确答案是 1 |
| 全满足分支 | 循环结束直接 return 0 | 1,1,1 正确答案 1,却会返回 0 |
其中 citationsi - 1 在 i = 0 时是明确的越界访问。即便没有触发 i = 0,返回 citationsi - 1 也把"引用次数"当成了"论文数量",变量语义不一致。
五 推荐实现一 正确的降序插入排序
int hIndex(int* citations, int citationsSize) {
for (int i = 1; i < citationsSize; ++i) {
int p = citationsi;
int j = i - 1;
while (j >= 0 && citationsj < p) {
citationsj + 1 = citationsj;
--j;
}
citationsj + 1 = p;
}
int h = 0;
for (int i = 0; i < citationsSize; ++i) {
if (citationsi >= i + 1) {
h = i + 1;
} else {
break;
}
}
return h;
}
这个版本保留了你原来"先排序、再判断"的学习路线,但把排序方向改为降序,使 H 指数判定可以直接写成 citationsi >= i + 1。插入排序的移动条件和最终写回位置也被明确拆开。
- while 条件只负责把更小的元素向右移动。
- 循环结束后统一执行 citationsj + 1 = p,覆盖 p 在最前面、中间或最后面的所有情况。
- 扫描时 h 保存满足条件的论文数量,不返回某个 citations 元素。
六 推荐实现二 使用 qsort
static int compare_desc(const void* a, const void* b) {
int x = *(const int*)a;
int y = *(const int*)b;
if (x < y) return 1;
if (x > y) return -1;
return 0;
}
int hIndex(int* citations, int citationsSize) {
qsort(citations, citationsSize, sizeof(int), compare_desc);
int h = 0;
for (int i = 0; i < citationsSize; ++i) {
if (citationsi >= i + 1) {
h = i + 1;
} else {
break;
}
}
return h;
}
如果目标是代码简洁和稳定,qsort 版本更合适。比较函数没有使用 y - x,避免在更宽泛的整数输入下产生减法溢出;题目给定范围内 int 足够安全。
七 正确性证明
设数组已经按降序排列。对于任意位置 i,前 i + 1 个元素都不小于 citationsi。
- 若 citationsi >= i + 1,则至少有 i + 1 篇论文的引用次数不少于 i + 1,因此 h = i + 1 是合法候选。
- 若 citationsi < i + 1,则从 i 开始的后续元素都不大于 citationsi,后续位置不可能重新满足 citationsk >= k + 1,因此可以停止。
- 扫描过程中保存最大的合法候选 h,最终得到的 h 既满足定义,又不可能继续增大,因此是最大 H 指数。
八 复杂度与副作用
|----------|--------------------|-----------|----------|-------------------|
| 实现 | 排序复杂度 | 扫描复杂度 | 额外空间 | 备注 |
| 初版 | 意图为 O(n^2),但实现不完整 | 无有效判定 | O(1) | 会丢失 p,且中位数不是 H 指数 |
| 改进版 | 最坏 O(n^2),仍不正确 | O(n) | O(1) | 存在越界和错误返回 |
| 降序插入排序 | 最坏 O(n^2) | O(n) | O(1) | 适合保留插入排序练习 |
| qsort 版本 | 平均 / 典型 O(n log n) | O(n) | 由库实现决定 | 工程代码更简洁 |
两种推荐实现都会原地修改 citations。题目允许这样做,但如果调用方还需要原始顺序,应先复制数组;这属于接口副作用,而不是 H 指数算法本身的必要条件。
九 测试矩阵
|-----------------|----------|--------------|
| 输入 | 期望 H | 覆盖点 |
| 3,0,6,1,5 | 3 | 官方示例,普通情况 |
| 1,1,1 | 1 | 所有论文引用数相同 |
| 0,0,0 | 0 | 全为零 |
| 5 | 1 | 单元素,答案受 n 限制 |
| 100,1 | 1 | 不能返回引用次数 100 |
| 0,1,3,5,6 | 3 | 排序后 i=0 边界 |
| 1,2,3,4,5 | 3 | 中位数不能直接当答案 |
| 10,10,10,10 | 4 | 答案不能超过论文总数 |
| 0,2,0,1 | 1 | 零值和无序输入 |
十 结论与建议
初版的插入排序缺少最后写回 p 的动作,即使修复排序,也不能通过直接返回 citationscitationsSize / 2 得到 H 指数。改进版虽然增加了第二阶段,但排序条件、i = 0 边界、返回值含义和全满足分支仍然存在错误。
建议先使用"降序排序 + citationsi >= i + 1"的版本建立正确模型;如果更重视工程简洁性,则使用 qsort 版本。无论采用哪种排序,都要把"论文数量 h"和"引用次数 citationsi"分开处理。
十一 来源与证据范围
官方题目页面: LeetCode 274 H 指数,访问日期:2026 年 10 月 24 日。
个人代码来源:用户在本次对话中直接粘贴,证据类型为 user pasted。本文评价的是题目约束下的算法行为,并补充了 C 语言数组边界、输入修改副作用和比较函数实现风险。