简介:这份资源是 LeetCode 经典入门题「两数之和」的 C 语言实现源码,面向正在刷题、准备算法面试或学习 C 语言基础语法的开发者。题目要求在整数数组中找出和为目标值的两个元素下标,并假设每组输入只对应唯一答案、同一元素不可重复使用,是理解哈希查找与暴力枚举思路的典型练习。压缩包共 35 个文件,约 1.05MB,除核心 cpp 源码外,还包含 sln 解决方案、vcxproj 工程配置、rc 资源脚本以及 pdb、obj、ilk 等编译调试产物,另有 manifest、tlog、cache 等构建日志文件,整体是一套可直接用 Visual Studio 打开运行的完整工程。目前已有 5675 人学习下载,适合希望对照可运行代码理解数组遍历、下标返回与边界处理的初学者,也能作为课程作业或面试复习的参考实现。
1. 两数之和 C 语言源码:从暴力枚举到哈希表的完整落地
LeetCode 第一题「两数之和」几乎是所有人刷题的起点,但真正把它写成一份能过编译、能跑测试、能扛住边界数据的 C 语言源码,很多人第一次都会翻车。原因不在算法本身,而在于 C 没有内置哈希表、没有 vector、没有自动内存管理,所有东西都得自己搭。这篇笔记就围绕「LeetCode 两数之和 C 语言源码」这个具体目标,把暴力解法、排序双指针、开放寻址哈希表三种实现路径拆开讲清楚,每一份代码都能直接编译运行。适合刚学完 C 语言基础、想拿这道题练手又不想只抄个答案的人,也适合刷 LeetCode 热门 100 题时想把底层实现搞明白的读者。下面所有代码基于 C99 标准,用 gcc 编译,不依赖任何第三方库。
2. 先把题目约束吃透:为什么 C 语言实现比 Python 麻烦三倍
2.1 题目本身的三个硬约束
两数之和的题面很短:给定一个整数数组nums和一个目标值target,找出数组中和为target的两个数,返回它们的下标。但落到 C 语言里,有三个约束必须先想清楚。
第一,返回值类型。LeetCode 的 C 语言接口签名是int* twoSum(int* nums, int numsSize, int target, int* returnSize),返回的是一个int*,同时通过returnSize指针回传数组长度。这意味着你必须用malloc动态分配返回数组,不能返回栈上局部数组。
第二,数据范围。题目保证只有一个有效答案,且同一个元素不能使用两次。但数组里可能有负数、有重复值,target也可能是负数。这些都会影响哈希函数的设计。
第三,时间复杂度要求。暴力双重循环是 O(n²),在 n 达到 10⁴ 量级时就会超时。LeetCode 的测试用例规模决定了你必须把复杂度压到 O(n) 或 O(n log n)。
2.2 C 语言没有哈希表,这是最大的坑
Python 里一行dict就解决的事,在 C 里要自己写哈希表。常见做法有两种:开放寻址法和链地址法。对于两数之和这道题,因为只需要存key=数值, value=下标,且插入后只查不删,开放寻址法更简单,缓存友好度也更高。
我一般会用线性探测的开放寻址哈希表,表大小取大于2*numsSize的最小 2 的幂次,这样可以用位运算代替取模,速度更快。哈希函数用经典的乘法散列,把负数映射到正数区间。
提示:如果面试时被要求手写,先跟面试官确认是否允许用
uthash这类头文件库。多数国内面试要求纯手写,那就老老实实自己实现。
2.3 三种解法的选型对比
| 解法 | 时间复杂度 | 空间复杂度 | 是否需要额外内存 | 适用场景 |
|---|---|---|---|---|
| 暴力双重循环 | O(n²) | O(1) | 否 | n < 1000,面试热身 |
| 排序 + 双指针 | O(n log n) | O(n) | 需要存原下标 | 需要返回数值而非下标 |
| 哈希表一次遍历 | O(n) | O(n) | 需要哈希表 | LeetCode 正式提交 |
选型逻辑很直接:LeetCode 提交用哈希表,面试手写如果时间紧可以先写暴力再优化,排序双指针适合「返回数值」变体题。下面三节分别给出完整源码。
3. 暴力解法与排序双指针:两份能直接编译的 C 源码
3.1 暴力双重循环的最小可运行版本
先看最朴素的写法,这份代码虽然会超时,但它是理解题目和验证环境的基础。
#include <stdio.h> #include <stdlib.h> /** * 暴力解法:双重循环枚举所有下标对 * nums: 输入数组 * numsSize: 数组长度 * target: 目标和 * returnSize: 输出参数,返回结果数组长度 */ int* twoSum(int* nums, int numsSize, int target, int* returnSize) { // 分配返回数组,固定两个元素 int* result = (int*)malloc(2 * sizeof(int)); if (result == NULL) { *returnSize = 0; return NULL; } // 外层固定第一个数,内层找第二个数 for (int i = 0; i < numsSize - 1; i++) { for (int j = i + 1; j < numsSize; j++) { // 用减法代替加法,避免整数溢出 if (nums[i] + nums[j] == target) { result[0] = i; result[1] = j; *returnSize = 2; return result; } } } // 没找到,返回空 *returnSize = 0; free(result); return NULL; } int main(void) { int nums[] = {2, 7, 11, 15}; int target = 9; int returnSize = 0; int* res = twoSum(nums, 4, target, &returnSize); if (res != NULL) { printf("[%d, %d]\n", res[0], res[1]); free(res); } return 0; }逻辑说明:外层循环从 0 到numsSize-2,内层从i+1到numsSize-1,保证不重复使用同一元素。判断条件用nums[i] + nums[j] == target,这里有个细节——如果数组元素接近INT_MAX,加法会溢出,更稳妥的写法是nums[j] == target - nums[i]。
参数说明:returnSize是输出参数,调用方传入地址,函数内部写入结果长度。malloc分配的内存在调用方负责free,这是 LeetCode C 接口的约定。
编译命令:gcc -std=c99 -Wall -o two_sum_brute two_sum_brute.c,-Wall打开所有警告,能帮你发现未使用变量和类型不匹配。
3.2 排序加双指针:返回数值的变体写法
如果题目变体要求返回两个数值而不是下标,排序双指针是更优雅的选择。但要注意,排序会打乱原下标,所以如果还要返回下标,必须额外保存原始位置。
#include <stdio.h> #include <stdlib.h> // 用于排序的结构体,保存数值和原始下标 typedef struct { int value; int index; } Node; // qsort 的比较函数 int cmp(const void* a, const void* b) { return ((Node*)a)->value - ((Node*)b)->value; } int* twoSumSorted(int* nums, int numsSize, int target, int* returnSize) { Node* nodes = (Node*)malloc(numsSize * sizeof(Node)); for (int i = 0; i < numsSize; i++) { nodes[i].value = nums[i]; nodes[i].index = i; } qsort(nodes, numsSize, sizeof(Node), cmp); int left = 0, right = numsSize - 1; int* result = (int*)malloc(2 * sizeof(int)); while (left < right) { int sum = nodes[left].value + nodes[right].value; if (sum == target) { result[0] = nodes[left].index; result[1] = nodes[right].index; *returnSize = 2; free(nodes); return result; } else if (sum < target) { left++; } else { right--; } } *returnSize = 0; free(nodes); free(result); return NULL; }逻辑说明:先把每个元素和它的原始下标打包成Node,按数值排序。然后左右指针向中间收缩,和小于目标就左指针右移,大于就右指针左移。找到后返回的是原始下标,不是排序后的位置。
参数说明:qsort的比较函数必须返回int,这里直接相减在数值范围小的时候没问题,但如果数值接近INT_MIN和INT_MAX,相减会溢出,工程代码里应该写成(a > b) - (a < b)的形式。
这份代码的时间瓶颈在排序,O(n log n),比哈希表慢,但胜在不需要自己实现哈希结构,代码量少,适合快速验证。
4. 手写开放寻址哈希表:O(n) 解法的完整源码
4.1 哈希表结构设计与哈希函数选择
开放寻址法的核心是一个数组,每个槽位存key和value,用used标记是否占用。插入时如果槽位被占,就线性往后找空位。查找时从哈希位置开始,遇到空槽说明不存在。
表大小取大于等于2 * numsSize的最小 2 的幂次,这样可以用hash & (size - 1)代替取模。哈希函数用乘法散列:
// 把任意整数映射到 [0, size) 区间 static unsigned int hash(int key, int size) { unsigned int x = (unsigned int)key; x = ((x >> 16) ^ x) * 0x45d9f3b; x = ((x >> 16) ^ x) * 0x45d9f3b; x = (x >> 16) ^ x; return x & (size - 1); }这个函数来自经典的整数散列,对负数也有效,因为先转成了unsigned int。0x45d9f3b是一个经验证分布均匀的乘数。
4.2 完整哈希表解法源码与逐行说明
#include <stdio.h> #include <stdlib.h> #include <limits.h> // 哈希表槽位 typedef struct { int key; // 数值 int value; // 下标 int used; // 是否占用 } HashSlot; // 哈希函数 static unsigned int hash(int key, int size) { unsigned int x = (unsigned int)key; x = ((x >> 16) ^ x) * 0x45d9f3b; x = ((x >> 16) ^ x) * 0x45d9f3b; x = (x >> 16) ^ x; return x & (size - 1); } // 查找 key,找到返回下标,没找到返回 -1 static int hashFind(HashSlot* table, int size, int key) { unsigned int idx = hash(key, size); while (table[idx].used) { if (table[idx].key == key) { return table[idx].value; } idx = (idx + 1) & (size - 1); // 线性探测 } return -1; } // 插入 key-value,假设 key 不重复 static void hashInsert(HashSlot* table, int size, int key, int value) { unsigned int idx = hash(key, size); while (table[idx].used) { idx = (idx + 1) & (size - 1); } table[idx].key = key; table[idx].value = value; table[idx].used = 1; } int* twoSum(int* nums, int numsSize, int target, int* returnSize) { // 计算表大小:大于等于 2*numsSize 的最小 2 的幂 int size = 1; while (size < 2 * numsSize) { size <<= 1; } HashSlot* table = (HashSlot*)calloc(size, sizeof(HashSlot)); if (table == NULL) { *returnSize = 0; return NULL; } int* result = (int*)malloc(2 * sizeof(int)); if (result == NULL) { free(table); *returnSize = 0; return NULL; } for (int i = 0; i < numsSize; i++) { // 先查再插,避免自己配自己 int complement = target - nums[i]; int found = hashFind(table, size, complement); if (found != -1) { result[0] = found; result[1] = i; *returnSize = 2; free(table); return result; } hashInsert(table, size, nums[i], i); } free(table); free(result); *returnSize = 0; return NULL; } int main(void) { int nums[] = {3, 2, 4}; int target = 6; int returnSize = 0; int* res = twoSum(nums, 3, target, &returnSize); if (res != NULL) { printf("[%d, %d]\n", res[0], res[1]); free(res); } return 0; }逻辑说明:遍历数组,对每个元素先算补数target - nums[i],在哈希表里查补数是否存在。如果存在,说明之前某个元素和当前元素配对成功,直接返回。如果不存在,把当前元素和下标插入哈希表。先查后插的顺序保证了不会用同一个元素两次。
参数说明:calloc会把所有槽位的used初始化为 0,比malloc加memset更简洁。表大小取2 * numsSize是为了控制负载因子在 0.5 以下,减少探测次数。hashFind里的while (table[idx].used)循环在表未满时一定会终止,因为负载因子小于 1。
编译运行:gcc -std=c99 -O2 -Wall -o two_sum_hash two_sum_hash.c && ./two_sum_hash,输出[1, 2]。
4.3 用 stdio.h 和 limits.h 做边界自测
写完代码不能只跑一个用例。我一般会写一个简单的测试函数,覆盖负数、重复值、两元素、大数值几种情况。
#include <stdio.h> #include <limits.h> void test(int* nums, int n, int target) { int returnSize = 0; int* res = twoSum(nums, n, target, &returnSize); if (res) { printf("target=%d -> [%d, %d]\n", target, res[0], res[1]); free(res); } else { printf("target=%d -> no answer\n", target); } } int main(void) { int a[] = {-3, 4, 3, 90}; test(a, 4, 0); // 负数场景 int b[] = {3, 3}; test(b, 2, 6); // 重复值 int c[] = {INT_MAX, 1, -1}; test(c, 3, 0); // 大数值边界 return 0; }limits.h提供INT_MAX和INT_MIN,用来构造极端输入。负数场景验证哈希函数对负数的处理,重复值验证先查后插的顺序,大数值验证减法代替加法是否避免了溢出。
5. 避坑与排查:两数之和 C 源码最常见的 5 个翻车点
5.1 返回栈上数组导致段错误
现象:本地跑没问题,提交到 LeetCode 报 runtime error 或者返回乱码。
原因:写了int result[2]; return result;,局部数组在函数返回后栈帧被回收,指针悬空。
解决:必须用malloc分配,调用方负责free。LeetCode 的判题系统会自己释放,但本地测试要记得手动free。
5.2 哈希表大小取太小导致死循环
现象:程序卡死,或者查找函数无限循环。
原因:表大小等于numsSize,负载因子达到 1,线性探测找不到空槽,while循环永不退出。
解决:表大小至少取2 * numsSize,并且保证是 2 的幂次,这样(idx + 1) & (size - 1)才能正确回绕。
5.3 先插后查导致自己配自己
现象:输入[3, 2, 4],target=6,返回[0, 0]。
原因:先把当前元素插入哈希表,再查补数,当target = 2 * nums[i]时,查到的就是自己。
解决:严格按「先查后插」的顺序,查到了直接返回,查不到才插入当前元素。
5.4 整数溢出导致判断失效
现象:输入包含INT_MAX和正数,加法溢出变成负数,漏掉正确答案。
原因:nums[i] + nums[j]溢出,结果未定义。
解决:用nums[j] == target - nums[i]代替加法判断,或者把元素转成long long再运算。
5.5 忘记处理 returnSize 导致判题失败
现象:本地输出正确,LeetCode 报「输出长度不匹配」。
原因:没有给*returnSize赋值,判题系统读到的是随机值。
解决:每个返回路径都要设置*returnSize,找到答案设 2,没找到设 0。
6. 进阶技巧:用 gdb 单步验证哈希表探测过程
写完哈希表解法,光看输出对不对还不够,我想知道探测次数和槽位分布。这时候 gdb 比 printf 更好用。编译时加-g -O0保留调试信息:
gcc -std=c99 -g -O0 -o two_sum_hash_dbg two_sum_hash.c gdb ./two_sum_hash_dbg进入 gdb 后,在hashInsert函数上下断点,用commands自动打印每次插入的 key 和最终槽位:
(gdb) break hashInsert (gdb) commands > silent > printf "insert key=%d value=%d\n", key, value > continue > end (gdb) run这样能看到每个元素落在哪个槽位,如果发现大量连续占用,说明哈希函数分布不好或者表太小。我一般会观察idx的初始值和最终值差多少,差值就是探测次数。正常情况下平均探测次数应该小于 2,如果超过 5,就要考虑换哈希函数或者加大表。
另一个技巧是用valgrind检查内存泄漏:
valgrind --leak-check=full ./two_sum_hash哈希表解法里table和result都要释放,漏掉任何一个 valgrind 都会报 definitely lost。我自己的习惯是每写一个malloc就在旁边注释谁负责free,这样 review 的时候一眼能看出问题。
最后说个习惯:这道题我前后写过不下十遍,每次换语言或者换实现方式都会重新踩一遍坑。哈希表大小取 2 的幂、先查后插、减法防溢出,这三条是我用血泪经验换来的,希望帮到你。
本文还有配套的精品资源,点击获取