1. 从嵌入式到算法:我的LeetCode刷题心路历程
作为一名嵌入式开发工程师,我习惯了与寄存器、硬件接口和底层驱动打交道。直到开始准备面试,才发现算法能力同样重要。面对LeetCode题库,我决定用纯C语言刷题,既巩固基础,又提升解决问题的能力。这篇文章记录了我从22题到43题的解题思路和代码实现,希望能给同样用C刷题的朋友一些参考。
提示:如果你是算法新手,建议先练习牛客网的算法入门题,再挑战LeetCode中等难度题目。
2. 链表与字符串处理:基础但重要
2.1 两数相加:链表操作入门
这道题要求我们将两个用链表表示的数字相加。关键点在于处理不同长度的链表和进位问题。
c复制struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) {
struct ListNode dummy; // 虚拟头节点简化操作
struct ListNode *tail = &dummy; // 尾指针用于构建结果链表
int carry = 0; // 进位标志
while(l1 || l2 || carry) {
int sum = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry;
carry = sum / 10;
struct ListNode *newNode = malloc(sizeof(struct ListNode));
newNode->val = sum % 10;
newNode->next = NULL;
tail->next = newNode;
tail = tail->next;
l1 = l1 ? l1->next : NULL;
l2 = l2 ? l2->next : NULL;
}
return dummy.next;
}
注意事项:
- 使用虚拟头节点可以避免处理头节点的特殊情况
- 记得处理最后的进位
- 内存分配后要检查是否成功(实际面试中可能会被问到)
2.2 无重复字符的最长子串:滑动窗口经典应用
这道题考察如何找到不含重复字符的最长子串,滑动窗口(双指针)是解决这类问题的标准方法。
c复制int lengthOfLongestSubstring(char* s) {
int hash[128] = {0}; // ASCII码哈希表
int max_len = 0;
int left = 0, right = 0;
int n = strlen(s);
while(right < n) {
if(hash[s[right]] == 0) {
hash[s[right]] = 1;
max_len = fmax(max_len, right - left + 1);
right++;
} else {
hash[s[left]] = 0;
left++;
}
}
return max_len;
}
优化技巧:
- 使用128大小的数组直接映射ASCII字符
- 右指针扩展窗口,左指针收缩窗口
- 实时更新最大长度,避免二次遍历
3. 动态规划与回溯算法:中等难度核心
3.1 最长回文子串:中心扩展法
回文串问题有多种解法,中心扩展法在时间和空间复杂度上都有不错的表现。
c复制int expandAroundCenter(char* s, int left, int right) {
while(left >= 0 && right < strlen(s) && s[left] == s[right]) {
left--;
right++;
}
return right - left - 1;
}
char* longestPalindrome(char* s) {
if(strlen(s) < 1) return "";
int start = 0, end = 0;
for(int i = 0; i < strlen(s); i++) {
int len1 = expandAroundCenter(s, i, i); // 奇数长度
int len2 = expandAroundCenter(s, i, i+1); // 偶数长度
int len = fmax(len1, len2);
if(len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
char* result = malloc(end - start + 2);
strncpy(result, s + start, end - start + 1);
result[end - start + 1] = '\0';
return result;
}
关键点:
- 需要同时考虑奇数和偶数长度的回文串
- 中心扩展时注意边界条件
- 结果字符串需要正确终止('\0')
3.2 电话号码的字母组合:回溯算法实践
回溯法是解决组合问题的利器,这道题展示了如何递归生成所有可能的组合。
c复制char* phoneMap[] = {"", "", "abc", "def", "ghi", "jkl",
"mno", "pqrs", "tuv", "wxyz"};
char** result;
char* path;
int pathLen;
int resultSize;
void backtrack(char* digits, int index) {
if(index == strlen(digits)) {
result[resultSize] = malloc(pathLen + 1);
strcpy(result[resultSize], path);
result[resultSize][pathLen] = '\0';
resultSize++;
return;
}
char* letters = phoneMap[digits[index] - '0'];
for(int i = 0; i < strlen(letters); i++) {
path[pathLen++] = letters[i];
backtrack(digits, index + 1);
pathLen--;
}
}
char** letterCombinations(char* digits, int* returnSize) {
if(strlen(digits) == 0) {
*returnSize = 0;
return NULL;
}
int total = 1;
for(int i = 0; i < strlen(digits); i++) {
total *= strlen(phoneMap[digits[i] - '0']);
}
result = malloc(total * sizeof(char*));
path = malloc(strlen(digits) + 1);
pathLen = 0;
resultSize = 0;
backtrack(digits, 0);
*returnSize = resultSize;
return result;
}
回溯三要素:
- 选择列表:当前数字对应的字母
- 路径:已选择的字母组合
- 结束条件:处理完所有数字
4. 数组与矩阵操作:常见面试考点
4.1 下一个排列:理解字典序
这道题要求找出数组的下一个排列,需要理解字典序的概念和排列的生成规律。
c复制void nextPermutation(int* nums, int numsSize) {
int i = numsSize - 2;
while(i >= 0 && nums[i] >= nums[i+1]) {
i--;
}
if(i >= 0) {
int j = numsSize - 1;
while(j > i && nums[j] <= nums[i]) {
j--;
}
swap(&nums[i], &nums[j]);
}
reverse(nums, i + 1, numsSize - 1);
}
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
void reverse(int* nums, int start, int end) {
while(start < end) {
swap(&nums[start], &nums[end]);
start++;
end--;
}
}
算法步骤:
- 从后向前找第一个升序对(i, i+1)
- 从后向前找第一个大于nums[i]的数nums[j]
- 交换nums[i]和nums[j]
- 反转i+1到末尾的部分
4.2 旋转图像:矩阵操作技巧
旋转图像需要原地操作,不能使用额外空间,考察对矩阵的理解和操作能力。
c复制void rotate(int** matrix, int matrixSize, int* matrixColSize) {
// 先转置矩阵
for(int i = 0; i < matrixSize; i++) {
for(int j = i; j < matrixSize; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 再水平翻转每一行
for(int i = 0; i < matrixSize; i++) {
for(int j = 0; j < matrixSize/2; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[i][matrixSize-1-j];
matrix[i][matrixSize-1-j] = temp;
}
}
}
数学原理:
- 顺时针旋转90度 = 转置 + 水平翻转
- 逆时针旋转90度 = 转置 + 垂直翻转
- 旋转180度 = 水平翻转 + 垂直翻转
5. 动态规划进阶:经典问题解析
5.1 最大子数组和:Kadane算法
Kadane算法是解决最大子数组和问题的高效方法,时间复杂度O(n),空间复杂度O(1)。
c复制int maxSubArray(int* nums, int numsSize) {
int max_sum = nums[0];
int current_sum = nums[0];
for(int i = 1; i < numsSize; i++) {
current_sum = fmax(nums[i], current_sum + nums[i]);
max_sum = fmax(max_sum, current_sum);
}
return max_sum;
}
算法思想:
- 当前子数组和如果是正数,就对后续子数组和有贡献
- 如果是负数,就从下一个元素重新开始计算
- 始终保持记录最大和
5.2 编辑距离:字符串动态规划
编辑距离是衡量两个字符串相似度的重要指标,在自然语言处理中有广泛应用。
c复制int minDistance(char* word1, char* word2) {
int m = strlen(word1), n = strlen(word2);
int** dp = malloc((m+1) * sizeof(int*));
for(int i = 0; i <= m; i++) {
dp[i] = malloc((n+1) * sizeof(int));
}
for(int i = 0; i <= m; i++) dp[i][0] = i;
for(int j = 0; j <= n; j++) dp[0][j] = j;
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(word1[i-1] == word2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = 1 + fmin(dp[i-1][j-1], fmin(dp[i-1][j], dp[i][j-1]));
}
}
}
int result = dp[m][n];
for(int i = 0; i <= m; i++) free(dp[i]);
free(dp);
return result;
}
状态转移方程:
- 字符相等:dp[i][j] = dp[i-1][j-1]
- 字符不等:dp[i][j] = 1 + min(替换, 删除, 插入)
6. 刷题经验与技巧分享
6.1 如何高效刷题
- 分类练习:按算法类型(如动态规划、回溯、双指针等)集中练习
- 反复练习:难题要多次重写,直到完全理解
- 总结模板:每种算法类型总结出自己的代码模板
- 时间管理:初期可以放宽时间限制,后期要严格计时
6.2 C语言刷题的特殊技巧
- 内存管理:C语言需要手动管理内存,面试时要特别注意
- 工具函数:提前准备常用的swap、max、min等工具函数
- 数组处理:C语言数组没有边界检查,要特别注意越界问题
- 指针操作:理解指针的指向和内存分配是关键
6.3 面试准备建议
- 沟通思路:即使没完全解出来,也要清晰地表达思考过程
- 边界检查:主动考虑输入的特殊情况(空值、边界值等)
- 复杂度分析:能够分析算法的时间和空间复杂度
- 测试用例:写完代码后自己设计测试用例验证
刷题是一个循序渐进的过程,从我的经验来看,坚持每天解决2-3道中等难度题目,两个月后会有明显提升。最重要的是理解算法背后的思想,而不是死记硬背代码。希望这些解题思路和代码实现能对你的刷题之路有所帮助!