C语言LeetCode题解,高效解法如何掌握?

99ANYc3cd6
预计阅读时长 21 分钟
位置: 首页 C语言 正文

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

leetcode题解 c语言
(图片来源网络,侵删)

为什么选择 C 语言刷 LeetCode?

  1. 基础扎实:C 语言是许多现代语言的基石,用 C 刷题能让你深刻理解内存管理、指针、数据结构在底层是如何实现的。
  2. 面试加分:在一些对性能和底层原理要求高的公司(如嵌入式、操作系统、内核开发等),熟练掌握 C 语言是巨大的优势。
  3. 性能挑战:C 语言是编译型语言,运行效率极高,在解决一些对时间复杂度要求苛刻的题目时,C 语言往往能提供最稳定的性能。

LeetCode C 语言环境搭建

在 LeetCode 网页上直接编写和运行 C 代码非常方便,无需本地配置。

  1. 选择语言:在提交代码的界面,确保语言选择为 C++注意:LeetCode 的 C 语言环境通常是基于 gcc 的,但在选择列表里常常标为 C++,因为它们的编译环境是共享的,你只需提交 .c 文件即可。
  2. 编写代码:直接在编辑器中编写你的 C 代码。
  3. 编译与运行:点击 "Run Code" 按钮,LeetCode 会在后台使用 gcc 编译你的代码,并使用测试用例进行测试。
  4. 提交:点击 "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 语言无法直接返回一个数组或链表,通常的做法是:
    1. 在主调函数中分配内存。
    2. 将指向该内存的指针作为参数传递给处理函数。
    3. 处理函数通过指针修改这块内存的内容。
    4. 主调函数接收并使用修改后的结果。

经典题型 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;
}

代码解析

  1. *`int returnSize**:这是一个“输出参数”,LeetCode 通过它来获取你返回的数组长度,你必须在函数内部给*returnSize` 赋值。
  2. int* result = (int*)malloc(...):因为 C 函数无法直接返回数组,所以我们用 malloc 在堆上分配了一块能容纳两个 int 的内存,并返回指向它的指针。
  3. if (result == NULL):健壮的代码必须检查 malloc 是否成功。
  4. 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;
}

代码解析

  1. 指针操作:这是 C 语言链表操作的典范,我们使用 prev, curr, next 三个指针来安全地遍历和修改链表结构。
  2. next = curr->next:这一步至关重要,在修改 curr->next 指向 prev 之前,必须先把 curr 的下一个节点保存下来,否则之后就无法找到链表的剩余部分了。
  3. curr->next = prev:这是核心的反转操作,将当前节点的 next 指针指向前一个节点。
  4. prev = curr; curr = next;:同步更新 prevcurr 指针,继续向后处理。
  5. 返回 prev:循环结束后,curr 已经走到链表末尾(NULL),而 prev 正好指向原链表的最后一个节点,也就是新链表的头节点。

示例 3:合并两个有序数组

展示了如何原地修改数组。 给你两个按非递减顺序排列的整数数组 nums1nums2,另有两个整数 mn,分别表示 nums1nums2 中的元素数目,请你将 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--];
    }
}

代码解析

  1. 原地操作:题目要求直接修改 nums1,如果从前向后合并,可能会覆盖掉 nums1 中还未处理的元素。
  2. 从后向前:我们从 nums1nums2 的有效末尾以及 nums1 的总末尾开始,将较大的数放到 nums1 的最后面,这样可以完美地避免数据覆盖问题。
  3. 边界条件while (p1 >= 0 && p2 >= 0) 确保我们只在两个数组都有元素时进行比较。
  4. 处理剩余元素:当 nums1 的元素先被全部处理完后,nums2 中剩下的元素一定是最小的,需要被整体移动到 nums1 的前面。while (p2 >= 0) 循环就完成了这个工作。

代码风格与调试建议

  1. 清晰的变量命名:使用有意义的变量名,如 prev_node, slow_ptr, fast_ptr
  2. 注释:对于复杂的逻辑(如指针操作、边界处理),添加简短的注释解释其作用。
  3. 处理边界条件:始终考虑空指针(head == NULL)、空数组(numsSize == 0)、单元素等情况。
  4. 利用 printf 调试:在本地环境中,可以在关键位置插入 printf 语句来打印变量值,观察程序执行流程,提交前记得删除或注释掉。
  5. 先暴力,再优化:如果思路不清晰,先用最直观(可能效率较低)的方法实现,确保逻辑正确,然后再思考如何优化时间和空间复杂度。

推荐学习资源

  • 书籍
    • 《C Primer Plus》:C 语言入门和进阶的圣经。
    • 《C 程序设计语言(K&R)》:C 语言之父的经典之作,简洁深刻。
    • 《数据结构与算法分析:C 语言描述》:将数据结构与 C 语言结合的经典教材。
  • 在线练习
    • LeetCode:本身就是最好的练习场。
    • HackerRank:提供大量 C 语言的专项练习题。

希望这份指南能帮助您在 LeetCode 上使用 C 语言愉快地刷题!祝您刷题顺利,早日成为算法大神!

-- 展开阅读全文 --
头像
C语言与JavaScript的核心差异与应用场景是什么?
« 上一篇 今天
linux c语言 bool
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码