☰
LeetCode两数之和C语言源码:暴力解与哈希表实现及调试避坑指南
2026/10/6 3:24:54 网站建设 项目流程

简介:这份资源是面向C语言初学者与LeetCode刷题者的「两数之和」题解源码包,针对在整数数组中查找和为目标值的两个元素这一经典算法问题,提供可直接编译运行的完整工程实现,帮助读者理解暴力枚举与哈希思路在C语言下的落地写法。压缩包共35个文件,约1.05MB,包含1个cpp源文件、1个sln解决方案与vcxproj工程文件、1个rc资源脚本,以及pdb、obj、ilk、idb等编译调试中间产物和tlog、log、cache等构建日志,另有manifest、filters、user等工程配置项,整体为Visual Studio项目的完整目录结构,便于直接打开调试或对照学习。目前已有5675人学习下载,适合刚接触算法题、需要参考可运行C语言实现与工程组织方式的读者,可从中获取题目求解代码、项目搭建范例与调试排错思路。

1. 从一道“两数之和”说起:这份 C 语言源码到底能帮你省下多少调试时间

如果你正在准备 LeetCode 第一题,或者刚学完 C 语言数组和指针,想找一个能直接编译、能跑通、还能看清每一步内存变化的参考实现,这份《LeetCode 两数之和 C 语言源码》就是冲着你来的。两数之和本身逻辑不复杂:给一个整数数组和一个目标值,找出和为目标值的两个下标。但真正用 C 写出来,很多人会卡在返回数组的内存分配、下标顺序、重复元素处理上。这份源码把暴力解和哈希思路都摊开,适合刚接触 LeetCode 的 C 语言学习者,也适合想复习指针与动态内存的从业者。下面我按“能编译、能验证、能改参数”的路线拆一遍,你照着走就能复现。

2. 两数之和的 C 实现骨架:从函数签名到返回数组的内存归属

2.1 为什么 LeetCode 的 C 函数签名长这样

LeetCode 上 C 语言版本的twoSum签名通常是:

int* twoSum(int* nums, int numsSize, int target, int* returnSize);

第一次看到这个签名的人容易懵:为什么返回int*,还要多一个returnSize?原因在于 C 语言函数不能直接返回数组,只能返回指向数组首元素的指针。而调用方需要知道你到底返回了几个元素,所以用returnSize这个指针把长度“带出去”。nums是输入数组,numsSize是元素个数,target是目标和。这四个参数里,returnSize是唯一一个你必须在函数内部写值的参数,不写就会导致判题系统读不到长度而报错。

常见做法是:先想清楚你要返回两个下标,所以*returnSize = 2,然后动态申请一个长度为 2 的int数组,把下标塞进去,最后返回这个数组的首地址。这里有个血泪经验:不要返回局部数组。局部数组在函数结束后栈内存会被回收,判题系统拿到的是野指针,结果就是随机通过或随机崩溃,这种玄学问题排查起来非常费时间。

2.2 暴力解:两层循环的边界与返回顺序

先看最稳妥的暴力实现,适合新手先跑通:

#include <stdlib.h> 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; } } } // 没找到时返回空,长度置 0 *returnSize = 0; free(result); return NULL; }

逻辑说明:外层i从 0 到numsSize - 2,内层j从i + 1到numsSize - 1,保证不会重复使用同一个元素,也保证返回的下标是i < j的顺序。参数说明:numsSize - 1这个边界是为了防止i越界后内层没有元素可配。malloc申请两个int的空间,如果申请失败就把*returnSize置 0 并返回NULL,避免调用方拿到无效指针。找到后立刻赋值并返回,不再继续循环。

注意:LeetCode 的判题系统通常不要求你释放result,它会在判题结束后统一处理。但如果你在自己本地写测试程序,记得在打印完结果后free(result),否则会有内存泄漏。这个细节在本地调试时经常被忽略,跑多了内存占用会慢慢涨。

2.3 哈希思路在 C 里怎么落地:手写简易开放寻址

暴力解时间复杂度 O(n²),数据量一大就超时。两数之和的经典优化是用哈希表把查找降到 O(1)。但 C 标准库没有现成的哈希表,常见做法是手写一个简易开放寻址哈希表,或者用排序加双指针。这里给一个适合 LeetCode 提交的开放寻址版本,表大小取numsSize * 2以上,减少冲突:

#include <stdlib.h> #include <string.h> typedef struct { int key; // 数值 int index; // 原数组下标 } HashEntry; int* twoSum(int* nums, int numsSize, int target, int* returnSize) { int tableSize = numsSize * 2 + 1; HashEntry* table = (HashEntry*)calloc(tableSize, sizeof(HashEntry)); int* result = (int*)malloc(2 * sizeof(int)); if (table == NULL || result == NULL) { *returnSize = 0; free(table); free(result); return NULL; } // 用 -1 标记空槽,因为下标不会是负数 for (int i = 0; i < tableSize; i++) { table[i].index = -1; } for (int i = 0; i < numsSize; i++) { int need = target - nums[i]; // 计算哈希位置,处理负数取模 int pos = ((need % tableSize) + tableSize) % tableSize; // 线性探测查找 need while (table[pos].index != -1) { if (table[pos].key == need) { result[0] = table[pos].index; result[1] = i; *returnSize = 2; free(table); return result; } pos = (pos + 1) % tableSize; } // 没找到就把当前数插入 int insertPos = ((nums[i] % tableSize) + tableSize) % tableSize; while (table[insertPos].index != -1) { insertPos = (insertPos + 1) % tableSize; } table[insertPos].key = nums[i]; table[insertPos].index = i; } *returnSize = 0; free(table); free(result); return NULL; }

逻辑说明:遍历数组时,先算need = target - nums[i],然后在哈希表里找need。如果找到,说明之前某个下标的值和当前值加起来等于目标,直接返回那两个下标。如果没找到,就把当前值和下标插入哈希表,供后面的元素查找。参数说明:tableSize取numsSize * 2 + 1是为了降低装载因子,减少线性探测的冲突次数。calloc会把内存清零,但这里手动把index设为 -1 更直观,因为 0 是合法下标。负数取模用((x % n) + n) % n是 C 语言里常见的修正写法,直接x % n在 x 为负时会得到负余数,导致数组越界。

提示:哈希表版本在 LeetCode 上提交时,注意calloc和malloc的返回值都要判空。虽然判题环境很少内存不足,但养成习惯后,本地跑大数据集时不会突然崩溃。

3. 本地编译与验证:用 gdb 和断言把两数之和跑透

3.1 写一个能复现的测试主函数

光有twoSum函数没法直接跑,需要补一个main来喂数据。下面这个测试程序覆盖了普通情况、重复元素、负数、无解四种场景:

#include <stdio.h> #include <stdlib.h> // 把上面的 twoSum 函数贴在这里 void printResult(int* nums, int numsSize, int target) { int returnSize = 0; int* res = twoSum(nums, numsSize, target, &returnSize); printf("target=%d, ", target); if (res != NULL && returnSize == 2) { printf("index=[%d, %d], value=[%d, %d]\n", res[0], res[1], nums[res[0]], nums[res[1]]); free(res); } else { printf("no solution\n"); } } int main() { int a[] = {2, 7, 11, 15}; printResult(a, 4, 9); // 期望 [0,1] int b[] = {3, 2, 4}; printResult(b, 3, 6); // 期望 [1,2] int c[] = {3, 3}; printResult(c, 2, 6); // 期望 [0,1] int d[] = {-1, -2, -3, -4}; printResult(d, 4, -7); // 期望 [2,3] int e[] = {1, 2, 3}; printResult(e, 3, 100); // 期望 no solution return 0; }

逻辑说明:printResult封装了调用和打印,避免在main里重复写returnSize和free。参数说明:每个测试用例都手动指定了期望结果,方便你对照。编译命令用gcc -g -Wall -o two_sum two_sum.c,-g保留调试符号,-Wall打开所有警告。如果编译时出现implicit declaration of function 'malloc',说明忘了#include <stdlib.h>。

3.2 用 gdb 看 returnSize 有没有被正确写入

本地跑通后,如果 LeetCode 上还是报错,常见原因是returnSize没写对。用 gdb 可以直接看函数返回后这个值是多少:

gcc -g -Wall -o two_sum two_sum.c gdb ./two_sum

进入 gdb 后:

break twoSum run next print *returnSize continue

逻辑说明:break twoSum在函数入口下断点,next单步执行,print *returnSize查看指针指向的值。如果函数返回后*returnSize还是 0 或者随机值,说明你在某个分支忘了赋值。参数说明:-g是 gdb 调试的前提,没有它只能看到地址,看不到变量名。这个排查方法在 LeetCode 报“运行时错误”但本地能跑时特别有用,因为很多判题系统的错误信息不告诉你具体哪一行。

3.3 用断言把边界条件钉死

如果你想把这份源码改成自己的练习模板,建议在测试里加assert:

#include <assert.h> void test_two_sum() { int nums[] = {2, 7, 11, 15}; int returnSize = 0; int* res = twoSum(nums, 4, 9, &returnSize); assert(returnSize == 2); assert(res[0] == 0 && res[1] == 1); free(res); }

逻辑说明:assert在条件不满足时直接终止程序并打印文件和行号,比printf更适合自动化验证。参数说明:returnSize == 2检查长度,res[0] == 0 && res[1] == 1检查下标顺序。注意assert在NDEBUG宏定义下会被忽略,所以发布版本里不要依赖它做业务判断。

4. 避坑与排查:两数之和 C 源码里最容易翻车的五个点

4.1 返回局部数组导致随机通过

现象:本地跑没问题,LeetCode 上有时通过有时报错,甚至同一份代码两次提交结果不同。原因:函数里写了int result[2];然后返回result,局部数组在栈上,函数结束后内存被回收,判题系统读到的可能是旧数据也可能是垃圾值。解决:一律用malloc或calloc在堆上申请返回数组,并在确认不需要后释放。如果判题系统不要求释放,至少保证申请成功后再写入。

4.2 忘记给 returnSize 赋值

现象:LeetCode 报“输出长度错误”或直接返回空。原因:函数里找到了答案,但只写了result[0]和result[1],没有写*returnSize = 2。判题系统不知道你返回了几个元素,可能按 0 处理。解决:在每一个return之前检查*returnSize是否已经赋值。常见做法是在函数开头就写*returnSize = 0,找到答案后再改成 2。

4.3 哈希表负数取模越界

现象:数组里有负数时程序崩溃或结果错误。原因:C 语言里-7 % 5结果是-2,不是3,直接拿这个负余数当数组下标会越界。解决:用((x % n) + n) % n修正,或者先把所有数加上一个偏移量变成非负。这个坑在 LeetCode 的负数测试用例里非常常见,很多人第一次写哈希版本都会中招。

4.4 重复元素时返回了下标相同的两个位置

现象:输入[3, 3],目标 6,返回[0, 0]而不是[0, 1]。原因:哈希表插入和查找的顺序没处理好,先查后插可以避免同一个元素被用两次。如果先插后查,当前元素可能匹配到自己。解决:在循环里先查找need,确认找不到再把当前元素插入哈希表。这样保证找到的另一个元素一定来自之前的下标。

4.5 忘记包含 stdlib.h 导致 malloc 隐式声明

现象:编译警告implicit declaration of function 'malloc',运行时指针被截断成int,在 64 位系统上崩溃。原因:没有#include <stdlib.h>,编译器默认malloc返回int,而实际返回的是 64 位指针。解决:所有用到malloc、calloc、free的文件都加上#include <stdlib.h>。编译时开-Wall能提前发现这个警告,不要忽略它。

5. 进阶技巧:把两数之和改成通用查找模板

5.1 用函数指针支持不同的匹配策略

两数之和的本质是“找两个元素满足某种关系”。如果你想把这份源码扩展成练习模板,可以把匹配条件抽成函数指针:

typedef int (*MatchFunc)(int a, int b, int target); int default_match(int a, int b, int target) { return a + b == target; } int* findPair(int* nums, int numsSize, int target, MatchFunc match, 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 (match(nums[i], nums[j], target)) { result[0] = i; result[1] = j; *returnSize = 2; return result; } } } *returnSize = 0; free(result); return NULL; }

逻辑说明:MatchFunc接收两个数组元素和目标值,返回是否匹配。default_match就是两数之和的a + b == target。参数说明:findPair的签名和twoSum类似,但多了一个match参数。这样你可以传a * b == target变成“两数之积”,或者传a - b == target变成“两数之差”,不用重写循环。

5.2 用哈希表把通用查找也降到 O(n)

上面的通用版本还是 O(n²)。如果匹配条件可以写成“给定 a,判断 b 是否满足”,就能用哈希表优化。比如两数之和里,给定nums[i],需要找target - nums[i]。通用做法是提供一个need函数:

typedef int (*NeedFunc)(int current, int target); int sum_need(int current, int target) { return target - current; } int* findPairHash(int* nums, int numsSize, int target, NeedFunc need, int* returnSize) { int tableSize = numsSize * 2 + 1; HashEntry* table = (HashEntry*)calloc(tableSize, sizeof(HashEntry)); int* result = (int*)malloc(2 * sizeof(int)); if (table == NULL || result == NULL) { *returnSize = 0; free(table); free(result); return NULL; } for (int i = 0; i < tableSize; i++) { table[i].index = -1; } for (int i = 0; i < numsSize; i++) { int want = need(nums[i], target); int pos = ((want % tableSize) + tableSize) % tableSize; while (table[pos].index != -1) { if (table[pos].key == want) { result[0] = table[pos].index; result[1] = i; *returnSize = 2; free(table); return result; } pos = (pos + 1) % tableSize; } int insertPos = ((nums[i] % tableSize) + tableSize) % tableSize; while (table[insertPos].index != -1) { insertPos = (insertPos + 1) % tableSize; } table[insertPos].key = nums[i]; table[insertPos].index = i; } *returnSize = 0; free(table); free(result); return NULL; }

逻辑说明:NeedFunc根据当前值和目标值算出“需要的另一个值”。两数之和里就是target - current。参数说明:findPairHash的哈希逻辑和前面一样,只是把target - nums[i]换成了need(nums[i], target)。这样你改一行need函数就能复用整个哈希框架。

5.3 验证方法:用随机数据对拍暴力解和哈希解

改完通用版本后,怎么确认没写错?我一般会写一个对拍程序,随机生成数组,分别跑暴力解和哈希解,比较结果是否一致:

#include <stdio.h> #include <stdlib.h> #include <time.h> void random_test() { srand((unsigned)time(NULL)); for (int t = 0; t < 1000; t++) { int n = rand() % 20 + 2; int* nums = (int*)malloc(n * sizeof(int)); for (int i = 0; i < n; i++) { nums[i] = rand() % 41 - 20; // -20 到 20 } int target = rand() % 41 - 20; int size1 = 0, size2 = 0; int* r1 = twoSum(nums, n, target, &size1); int* r2 = findPairHash(nums, n, target, sum_need, &size2); if (size1 != size2) { printf("size mismatch at test %d\n", t); free(nums); free(r1); free(r2); return; } if (size1 == 2) { int sum1 = nums[r1[0]] + nums[r1[1]]; int sum2 = nums[r2[0]] + nums[r2[1]]; if (sum1 != target || sum2 != target) { printf("value mismatch at test %d\n", t); free(nums); free(r1); free(r2); return; } } free(nums); free(r1); free(r2); } printf("all random tests passed\n"); }

逻辑说明:随机生成 2 到 21 个元素,值在 -20 到 20 之间,目标值也在同一范围。分别调用暴力解和哈希解,比较返回长度和实际和值。参数说明:rand() % 41 - 20生成闭区间[-20, 20]。srand((unsigned)time(NULL))用当前时间做种子,保证每次运行数据不同。这个对拍方法能覆盖负数、重复元素、无解等情况,比手动写几个用例更可靠。

从那以后我每次改哈希表相关的代码,都会先跑一遍随机对拍,确认暴力解和优化解结果一致再提交。这个习惯帮我省下了大量在判题系统上反复试错的时间。希望这份两数之和的 C 源码和排查思路能帮到你,把第一题真正跑透。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询