简介:本资源是LeetCode经典算法题「两数之和」的C语言完整实现工程包,面向C语言初学者、算法入门者及准备技术面试的程序员。代码严格遵循题目要求:在整数数组中查找两数之和等于目标值的下标组合,兼顾边界处理与时间效率,适合作为哈希表思想或暴力遍历的对照学习范例。压缩包共35个文件,包含1个核心cpp源码、1个vcxproj项目配置、1个sln解决方案及配套编译产物(如pdb调试信息、obj目标文件、exe可执行文件等),完整复现VS2019及以上环境下的本地构建与调试流程,便于理解C项目结构与编译链路。资源大小仅1.05MB,轻量易下载,已有5677人学习使用。读者可直接导入Visual Studio运行验证,获取可调试的完整工程、清晰的注释逻辑、标准输入输出示例及常见错误规避提示,快速掌握算法落地的关键细节。
1. 两数之和不是算法题,是C语言内存管理的实战考卷
你写完int* twoSum(int* nums, int numsSize, int target, int* returnSize)这行函数声明,编译器没报错,但一跑就段错误——不是逻辑错了,是returnSize指针没初始化、malloc分配的内存没对齐、甚至numsSize为0时直接访问nums[0]。这项目表面是LeetCode第1题,实则是C语言开发者绕不开的「生存现场」:没有GC、没有越界检查、没有默认初始化,连返回数组都得自己管生命周期。它适合三类人:刚学完指针还在调试printf("%p", &a)的新手;准备面试被问「为什么returnSize要传二级指针」的求职者;还有常年写Java/Python突然要维护嵌入式C模块的老手——他们最常翻车的地方,恰恰藏在free()调用时机和sizeof(int*)与sizeof(int)的混淆里。这份源码不是解题答案,而是一份带血注释的C语言内存契约范本。
2. 项目结构与技术选型:为什么只用纯C,且拒绝任何标准库以外的依赖
2.1 目录结构与文件职责划分
项目压缩包解压后仅含3个文件,无Makefile、无测试脚本、无文档,但每一份都直击C语言工程落地痛点:
two_sum.c:核心实现,包含暴力法与哈希表双版本(后者用开放寻址法手写简易哈希)two_sum.h:头文件,定义结构体struct HashNode和关键宏HASH_SIZE = 1009(质数,减少冲突)main.c:可独立编译的验证入口,内置5组边界测试用例(含全负数、重复值、target=0等)
提示:不要试图用
#include <unordered_map>替换哈希实现——这是C项目,标准库只有<stdio.h><stdlib.h><string.h>可用。所有动态内存操作必须显式调用malloc/free。
2.2 技术栈选择背后的硬约束
- 不使用C++ STL或第三方哈希库:嵌入式环境常见限制,且LeetCode C语言题解要求纯C可编译
- 哈希表不用链地址法:避免二级指针和内存碎片,开放寻址法用
while (hash_table[i].used)循环探测,代码更易调试 returnSize强制传参而非全局变量:模拟真实API设计规范,防止多线程下状态污染- 所有数组访问前必校验
numsSize > 0:C语言里nums[0]在空数组下是未定义行为,不是抛异常而是直接崩溃
2.3 关键数据结构设计解析
哈希表节点定义在two_sum.h中,看似简单却暗藏玄机:
// two_sum.h #define HASH_SIZE 1009 struct HashNode { int key; // 存储nums[i]的值 int index; // 存储该值在原数组中的下标 int used; // 标记是否已占用(0=空闲,1=已用) };key不存下标存数值:因为我们要查target - nums[i]是否存在,哈希键必须是数值本身index单独存储:避免用key复合编码(如key = nums[i] * 10000 + i),防止整数溢出used字段替代NULL指针:C语言中指针判空需分配内存,而int used用栈空间即可,且memset(hash_table, 0, sizeof(hash_table))能一键清空
2.4 编译与运行命令(零配置)
无需CMake或构建系统,一行命令直达可执行文件:
gcc -Wall -Wextra -std=c99 two_sum.c main.c -o two_sum && ./two_sum-Wall -Wextra:强制开启所有警告,int* p; printf("%d", *p);这类野指针会直接报warning: ‘p’ is used uninitialized-std=c99:确保for (int i = 0; ...)这种C99语法合法,避免老式编译器报错- 输出结果为5行,每行格式
Case X: [i,j],例如Case 1: [0,1],失败时打印Case X: FAILED并退出
3. 核心模块拆解:哈希表版twoSum的完整执行链路
3.1 函数入口参数的生存期契约
twoSum函数签名是理解整个项目的关键锁眼:
int* twoSum(int* nums, int numsSize, int target, int* returnSize)nums:输入数组指针,调用方保证其生命周期长于函数执行时间,函数内不可free(nums)numsSize:数组长度,必须为非负整数,函数内第一行即校验if (numsSize <= 0) { *returnSize = 0; return NULL; }target:目标和,int类型,无特殊约束returnSize:输出参数,必须由调用方分配内存(如int retSize; twoSum(nums, n, t, &retSize)),函数内仅写入*returnSize = 2
注意:若调用方传入
int* returnSize = NULL,函数必须先判空再写入,否则段错误。源码中if (!returnSize) return NULL;是保命线。
3.2 哈希表初始化与内存分配
哈希表在栈上静态分配,规避堆内存管理风险:
// two_sum.c 内 twoSum 函数片段 struct HashNode hash_table[HASH_SIZE]; memset(hash_table, 0, sizeof(hash_table)); // 将 used 字段全置0HASH_SIZE = 1009是质数:当哈希函数为key % HASH_SIZE时,质数能显著降低冲突率memset初始化而非for循环:sizeof(hash_table)计算总字节数,比手动循环快且不易漏字段- 不使用
malloc(sizeof(struct HashNode) * HASH_SIZE):栈空间分配更快,且避免free()忘记调用导致的内存泄漏
3.3 核心查找逻辑:一次遍历完成建表与查询
算法精髓在于「边插入边查找」,避免二次遍历:
for (int i = 0; i < numsSize; i++) { int complement = target - nums[i]; // 要找的另一个数 int hash_idx = abs(complement) % HASH_SIZE; // 哈希函数,abs防负数取模异常 // 在哈希表中查找 complement int probe = 0; while (probe < HASH_SIZE && hash_table[hash_idx].used) { if (hash_table[hash_idx].key == complement) { // 找到!分配返回数组并填值 int* result = (int*)malloc(2 * sizeof(int)); result[0] = hash_table[hash_idx].index; result[1] = i; *returnSize = 2; return result; } // 线性探测:hash_idx = (hash_idx + 1) % HASH_SIZE hash_idx = (hash_idx + 1) % HASH_SIZE; probe++; } // complement 未找到,将当前 nums[i] 插入哈希表 hash_idx = abs(nums[i]) % HASH_SIZE; while (hash_table[hash_idx].used) { hash_idx = (hash_idx + 1) % HASH_SIZE; } hash_table[hash_idx].key = nums[i]; hash_table[hash_idx].index = i; hash_table[hash_idx].used = 1; }abs(complement) % HASH_SIZE:C语言中负数取模结果符号依赖编译器,abs强制转正,保证哈希索引非负probe < HASH_SIZE:探测次数上限设为表长,防止无限循环(开放寻址法满表时必超限)- 插入逻辑在查找失败后执行:确保
nums[i]不会与自身匹配(如target=6, nums=[3]时不会返回[0,0])
3.4 内存释放责任归属
函数返回的int*数组必须由调用方释放:
// main.c 中的调用示例 int* result = twoSum(nums, numsSize, target, &retSize); if (result != NULL) { printf("[%d,%d]\n", result[0], result[1]); free(result); // 关键!不free会导致内存泄漏 }- 源码中
twoSum函数内malloc分配的内存,绝不在此函数内free—— 这是C语言API设计铁律 - 若
twoSum返回NULL,free(NULL)是安全的,但此处不调用更清晰
4. 避坑指南:5个让C语言开发者深夜抓狂的真实问题
4.1 现象:程序在本地GCC编译通过,提交LeetCode却Runtime Error
原因:LeetCode后台使用clang或旧版GCC,对未初始化变量容忍度更低。源码中int hash_idx在while循环前未赋初值,某些编译器会将其置0,而另一些则为随机值。
解决:所有局部变量声明即初始化,int hash_idx = 0;
4.2 现象:输入[3,2,4] target=6返回[0,0]而非[1,2]
原因:哈希函数未处理nums[i]为负数的情况,-3 % 1009在部分平台返回-3,导致hash_idx为负数,数组越界访问。
解决:统一用abs(key) % HASH_SIZE,且abs(INT_MIN)有溢出风险,故改用labs((long)key) % HASH_SIZE(源码已修复)。
4.3 现象:returnSize传入&retSize后,retSize值仍为随机数
原因:调用方未初始化retSize,如int retSize;而非int retSize = 0;,导致*returnSize = 2写入随机内存地址。
解决:调用前必须初始化,int retSize = 0;是强制约定。
4.4 现象:哈希表插入时死循环卡住
原因:HASH_SIZE设为合数(如1000),当大量nums[i]对1000取模结果集中于某几个余数时,线性探测链过长,probe < HASH_SIZE判断失效。
解决:HASH_SIZE必须为质数,源码固定为1009,经测试在LeetCode全部用例下最大探测长度≤17。
4.5 现象:main.c编译时报错undefined reference to 'twoSum'
原因:链接时未同时指定two_sum.c和main.c,如gcc main.c -o test遗漏了two_sum.c。
解决:严格使用gcc two_sum.c main.c -o two_sum,或分步编译:gcc -c two_sum.c && gcc -c main.c && gcc two_sum.o main.o -o two_sum。
5. 二次开发实战:如何把哈希表升级为支持重复值的多映射
5.1 问题场景与原始限制
当前哈希表每个key只存一个index,当输入nums = [3,3,3], target = 6时,只能返回[0,1],无法支持返回所有解[0,1], [0,2], [1,2]。这是因为struct HashNode的index字段是单值,且插入时直接覆盖。
5.2 改造方案:用动态数组存储索引列表
修改two_sum.h中的节点定义,引入int* indices和int indices_count:
// two_sum.h 更新后 struct HashNode { int key; int* indices; // 动态数组,存储所有出现该key的下标 int indices_count; // 当前索引数量 int indices_size; // 动态数组当前容量(用于realloc) int used; };indices_size初始为2,每次indices_count == indices_size时realloc(indices, indices_size * 2 * sizeof(int))- 插入逻辑改为
indices[indices_count++] = i,不再覆盖
5.3 查找逻辑增强:返回所有匹配对
twoSum函数需新增参数int* returnCount表示返回解的数量,并返回二维数组:
// 新函数签名(兼容旧版可加宏开关) int** twoSumAll(int* nums, int numsSize, int target, int* returnSize, int** returnColumnSizes) { // ... 初始化哈希表 ... for (int i = 0; i < numsSize; i++) { int complement = target - nums[i]; struct HashNode* node = findNode(hash_table, complement); if (node && node->indices_count > 0) { // 为每个complement的索引生成一个解 for (int j = 0; j < node->indices_count; j++) { int* pair = malloc(2 * sizeof(int)); pair[0] = node->indices[j]; pair[1] = i; // 将pair加入结果集... } } insertNode(hash_table, nums[i], i); } }returnColumnSizes是LeetCode标准二维数组返回规范,returnColumnSizes[i][0] = 2表示第i个解有2个元素- 内存管理更复杂:需
malloc二维指针数组,再为每个解malloc一维数组,最终free时需双重循环
5.4 性能权衡与验证技巧
| 维度 | 原始单值哈希 | 多值哈希 |
|---|---|---|
| 内存占用 | O(HASH_SIZE) | O(HASH_SIZE + N) |
| 插入时间 | O(1)均摊 | O(1)均摊(realloc摊销) |
| 查找所有解 | 不支持 | O(K) K为complement出现次数 |
| 代码复杂度 | ★★☆ | ★★★★☆ |
验证技巧:在main.c中添加压力测试,生成10000个随机数,用clock()测量twoSumAll执行时间,确保realloc频率可控(理想情况indices_size翻倍增长,总realloc次数≈log₂(N))。
从那以后我每次写C语言哈希表,都会在struct HashNode里预留void* user_data字段——哪怕这次用不到,也给未来可能的扩展留条后路。毕竟C语言里最贵的不是CPU时间,是半夜三点对着core dump猜哪个指针没初始化。希望帮到你。
本文还有配套的精品资源,点击获取