LeetCode 274 H 指数 C 语言解题分析

从插入排序调试到排序后判定

本文分析你提供的 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 语言数组边界、输入修改副作用和比较函数实现风险。

相关推荐
B2_Proxy1 小时前
Scrapy 代理 IP 调度进阶:基于 Redis 与中间件的高可用架构实战
python·scrapy·1024程序员节
文慧的科技江湖1 小时前
2026年10月24日智慧农业行业早报:模型数到175个,棚里的账还按年算
开源·智慧农业·农业物联网·数字乡村·1024程序员节·农业机器人·农业数字化
tryCbest1 小时前
文字转语音Edge-TTS知识学习使用
1024程序员节·文字转语音·edge-tts
在世修行1 小时前
智学文枢:AI 客户端封装
python·http·ai·1024程序员节·pyside
buddyMe智能助手1 小时前
记忆还没失效,但动作已经危险:该不该先核实,是动作说了算
1024程序员节
l1t1 小时前
在windows中测试musql数据库
数据库·sqlite·go·1024程序员节
大江东去浪淘尽千古风流人物1 小时前
【HIMO】跨模态图像匹配算法:IMO方向场提取与PolarP描述符源码精讲
matlab·跨模态·遥感·图像匹配·1024程序员节·特征描述符
小南家的青蛙1 小时前
华为云DCS全产品对比
redis·1024程序员节
芝麻开门2921 小时前
c++11
c++11·1024程序员节