C 语言在 LeetCode 上有其独特的优势和挑战,这份指南将从环境搭建、解题技巧、代码风格、经典题型等多个方面,结合具体实例,助您高效地使用 C 语言刷题。

(图片来源网络,侵删)
为什么选择 C 语言刷 LeetCode?
- 基础扎实:C 语言是许多现代语言的基石,用 C 刷题能让你深刻理解内存管理、指针、数据结构在底层是如何实现的。
- 面试加分:在一些对性能和底层原理要求高的公司(如嵌入式、操作系统、内核开发等),熟练掌握 C 语言是巨大的优势。
- 性能挑战:C 语言是编译型语言,运行效率极高,在解决一些对时间复杂度要求苛刻的题目时,C 语言往往能提供最稳定的性能。
LeetCode C 语言环境搭建
在 LeetCode 网页上直接编写和运行 C 代码非常方便,无需本地配置。
- 选择语言:在提交代码的界面,确保语言选择为
C++。注意:LeetCode 的 C 语言环境通常是基于gcc的,但在选择列表里常常标为C++,因为它们的编译环境是共享的,你只需提交.c文件即可。 - 编写代码:直接在编辑器中编写你的 C 代码。
- 编译与运行:点击 "Run Code" 按钮,LeetCode 会在后台使用
gcc编译你的代码,并使用测试用例进行测试。 - 提交:点击 "Submit" 提交你的最终代码,系统会给出 "Accepted" (通过) 或 "Wrong Answer" (错误答案) 等结果。
本地开发(强烈推荐): 虽然网页版方便,但强烈建议你在本地配置好 C 语言环境(如 GCC/Clang + VS Code / Vim),进行编码、调试和测试,再复制到 LeetCode 提交,本地调试能极大提高效率。
C 语言刷题核心技巧与陷阱
指针是灵魂,也是魔鬼
C 语言的精髓在于指针,在 LeetCode 中,几乎所有与数组、链表、树相关的题目都离不开指针。
- 数组名与指针:在函数参数中,
int nums[]和int *nums是等价的,它们都退化为指向数组首元素的指针。 - 指针算术:
ptr + 1会根据ptr的类型移动一个类型大小的距离。int *ptr + 1会移动sizeof(int)个字节。 - 不要越界:C 语言不会检查数组越界,访问越界内存会导致未定义行为,可能程序崩溃,也可能得到错误结果,这是最常见的错误之一。
内存管理
C 语言需要手动管理内存,这是最大的挑战之一。
malloc/calloc/free:- 当你需要动态创建数组、链表节点、树节点时,必须使用
malloc(或calloc) 分配内存。 - 使用完毕后,必须用
free释放内存,否则会造成内存泄漏,LeetCode 的测试用例之间通常是隔离的,单个用例的泄漏不会影响下一个,但这是一个坏习惯,在实际生产环境中是致命的。
- 当你需要动态创建数组、链表节点、树节点时,必须使用
malloc的头文件:#include <stdlib.h>malloc失败:malloc可能会因内存不足而返回NULL,在使用返回的指针前,一定要检查是否为NULL,否则解引用空指针会导致程序崩溃。
字符串处理
C 语言的字符串是以 '\0' 结尾的字符数组。
strlen:计算字符串长度,直到遇到'\0',头文件#include <string.h>。strcpy/strncpy:字符串拷贝。strncpy可以指定拷贝的最大长度,更安全。strcmp:比较两个字符串。- 注意:C 字符串操作函数都非常容易出错,一定要确保目标缓冲区足够大,并且字符串以
'\0'
传递与返回
- 修改数组/链表:由于数组在函数参数中退化为指针,你在函数内部对指针的解引用操作(如
*ptr = 10;或ptr[i] = 10;)会直接修改原数组的内容,这是 C 语言实现“引用传递”效果的方式。 - 返回数组/链表:C 语言无法直接返回一个数组或链表,通常的做法是:
- 在主调函数中分配内存。
- 将指向该内存的指针作为参数传递给处理函数。
- 处理函数通过指针修改这块内存的内容。
- 主调函数接收并使用修改后的结果。
经典题型 C 语言实现示例
示例 1:两数之和
这是一个经典的哈希表题,但为了展示 C 语言的指针特性,我们先从暴力解法开始。
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
暴力解法 C 代码:
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
// *returnSize 是 LeetCode 要求的,用于告诉调用者返回的数组长度
*returnSize = 2;
// 必须为结果分配内存
int* result = (int*)malloc(2 * sizeof(int));
if (result == NULL) {
// 分配失败,返回 NULL 或处理错误
*returnSize = 0;
return NULL;
}
for (int i = 0; i < numsSize; i++) {
for (int j = i + 1; j < numsSize; j++) {
if (nums[i] + nums[j] == target) {
result[0] = i;
result[1] = j;
return result; // 找到后直接返回
}
}
}
// 如果没找到,释放内存并返回 NULL
free(result);
*returnSize = 0;
return NULL;
}
代码解析:
- *`int returnSize
**:这是一个“输出参数”,LeetCode 通过它来获取你返回的数组长度,你必须在函数内部给*returnSize` 赋值。 int* result = (int*)malloc(...):因为 C 函数无法直接返回数组,所以我们用malloc在堆上分配了一块能容纳两个int的内存,并返回指向它的指针。if (result == NULL):健壮的代码必须检查malloc是否成功。free(result):如果最终没有找到解,我们在返回前释放掉之前分配的内存,避免泄漏。
示例 2:反转链表
完美展示了 C 语言指针操作的精妙之处。
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表头节点。
C 语言代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head) {
// 三个关键指针:prev, curr, next
struct ListNode *prev = NULL;
struct ListNode *curr = head;
while (curr != NULL) {
// 1. 暂存下一个节点,防止断链
struct ListNode *next = curr->next;
// 2. 反转当前节点的指针
curr->next = prev;
// 3. 指针后移,为下一次反转做准备
prev = curr;
curr = next;
}
// 当循环结束时,curr 为 NULL,prev 指向新的头节点
return prev;
}
代码解析:
- 指针操作:这是 C 语言链表操作的典范,我们使用
prev,curr,next三个指针来安全地遍历和修改链表结构。 next = curr->next:这一步至关重要,在修改curr->next指向prev之前,必须先把curr的下一个节点保存下来,否则之后就无法找到链表的剩余部分了。curr->next = prev:这是核心的反转操作,将当前节点的next指针指向前一个节点。prev = curr; curr = next;:同步更新prev和curr指针,继续向后处理。- 返回
prev:循环结束后,curr已经走到链表末尾(NULL),而prev正好指向原链表的最后一个节点,也就是新链表的头节点。
示例 3:合并两个有序数组
展示了如何原地修改数组。
给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目,请你将 nums2 合并到 nums1 中,使合并后的数组同样按非递减顺序排列。
C 语言代码 (从后向前合并):
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
// 从后向前合并,避免覆盖 nums1 中的元素
int p1 = m - 1; // nums1 的有效元素末尾
int p2 = n - 1; // nums2 的末尾
int p = m + n - 1; // 合并后数组的末尾
// 从后向前比较,将较大的元素放到 nums1 的末尾
while (p1 >= 0 && p2 >= 0) {
if (nums1[p1] > nums2[p2]) {
nums1[p--] = nums1[p1--];
} else {
nums1[p--] = nums2[p2--];
}
}
// nums2 还有剩余元素(说明它们都比 nums1 的最小元素还小),直接复制到 nums1 前面
// nums1 先耗尽,这个循环会把 nums2 剩下的全部复制过去
// nums2 先耗尽,这个循环不会执行,因为 p2 < 0
while (p2 >= 0) {
nums1[p--] = nums2[p2--];
}
}
代码解析:
- 原地操作:题目要求直接修改
nums1,如果从前向后合并,可能会覆盖掉nums1中还未处理的元素。 - 从后向前:我们从
nums1和nums2的有效末尾以及nums1的总末尾开始,将较大的数放到nums1的最后面,这样可以完美地避免数据覆盖问题。 - 边界条件:
while (p1 >= 0 && p2 >= 0)确保我们只在两个数组都有元素时进行比较。 - 处理剩余元素:当
nums1的元素先被全部处理完后,nums2中剩下的元素一定是最小的,需要被整体移动到nums1的前面。while (p2 >= 0)循环就完成了这个工作。
代码风格与调试建议
- 清晰的变量命名:使用有意义的变量名,如
prev_node,slow_ptr,fast_ptr。 - 注释:对于复杂的逻辑(如指针操作、边界处理),添加简短的注释解释其作用。
- 处理边界条件:始终考虑空指针(
head == NULL)、空数组(numsSize == 0)、单元素等情况。 - 利用
printf调试:在本地环境中,可以在关键位置插入printf语句来打印变量值,观察程序执行流程,提交前记得删除或注释掉。 - 先暴力,再优化:如果思路不清晰,先用最直观(可能效率较低)的方法实现,确保逻辑正确,然后再思考如何优化时间和空间复杂度。
推荐学习资源
- 书籍:
- 《C Primer Plus》:C 语言入门和进阶的圣经。
- 《C 程序设计语言(K&R)》:C 语言之父的经典之作,简洁深刻。
- 《数据结构与算法分析:C 语言描述》:将数据结构与 C 语言结合的经典教材。
- 在线练习:
- LeetCode:本身就是最好的练习场。
- HackerRank:提供大量 C 语言的专项练习题。
希望这份指南能帮助您在 LeetCode 上使用 C 语言愉快地刷题!祝您刷题顺利,早日成为算法大神!
