今天继续更新拷打字节面试官系列:第六大部分 字符串+ 双指针

C 语言刷题内核指南(一):字符串的野性与双指针的效率革命
【硬核宣言】从“提交通过”到“原理精通”
兄弟,我看到你那份刷题手稿了。三四遍的迭代,从一开始的懵懂,到后面能迅速写出逆向双指针的妙手,这进步不是盖着玩的。你已经掌握了招式,现在,我们要看懂内功心法。
我们接下来的目标是: 暂时抛弃那些高级语言的语法糖,用最贴近硬件的 C 语言透视镜,把牛客 101 里最常用、最能提分的技巧,彻底打磨成一套底层知识体系。
本篇,作为硬核指南的开篇,我们聚焦两大主题:
-
C 语言字符串的野性与内存操控: 彻底理解 char * 和 \0 的力量。
-
双指针的哲学: 它是如何用 O(1) 的空间,把 O(N^2) 的复杂度拉到 O(N) 的。
============================================================
I. C 语言字符串的野性:指针、内存与三步反转法
在 C 语言的视角里,字符串不是什么高级对象,它就是一块以空字符 \0 结尾的连续 char 数组内存。所有的字符串算法,本质上都是在进行内存地址的读、写、移动。理解这个底层逻辑,你的代码才能真正硬核。
I.1 案例精研:字符串变形(NC141)
原问题: 将一个字符串中的单词反序,同时反转每个字符的大小写。 这个题目要求“单词反序”和“大小写反转”,是典型的复合操作。解决复合问题的第一步,是解耦。
I.1.1 核心思路:三步反转法的逻辑分解
最优雅的解决方案是三步反转法,其巧妙之处在于顺序:
-
全局大小写反转: 字符属性操作,独立且高效,先做。
-
整串反转: 将整个字符串视为一个整体,原地反转(实现单词反序的第一步)。
-
单词内反转: 遍历,对每个单词再次反转(修正单词内部顺序)。
图 I.1:字符串变形的逻辑分解 (流程描述)
-
输入字符串 s
-
--> Step 1: 大小写反转 (字符级)
-
--> Step 2: 整体反转 (全内存块)
-
--> Step 3: 单词内部反转 (双指针)
-
--> 输出结果
I.1.2 硬核 C 代码:指针、malloc 与 ASCII 魔法
在 C 语言中,我们必须自己处理内存分配 (malloc),并使用最底层的 ASCII 码操作实现大小写转换,避免使用 C 库中相对较重的函数。[--- C CODE START: string_transform_core.c ---]
#include <stdio.h> #include <stdlib.h> #include <string.h>
// --- 辅助函数 1:原地反转 [start, end] 区间 ---
void reverse(char *s, int start, int end)
{ if (s == NULL || start < 0 || end < start) return; int left = start; int right = end;
// 左右指针相向而行,原地交换,O(N) 时间,O(1) 空间
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
// --- 辅助函数 2:大小写反转(利用 ASCII 偏移)--- v
void toggleCase(char *s, int n) { if (s == NULL) return;
for (int i = 0; i < n; i++) { char c = s[i];
// 大写字母 (A-Z)
if (c >= 'A' && c <= 'Z') {
s[i] = c + ('a' - 'A'); // 大写转小写:加 32
}
// 小写字母 (a-z)
else if (c >= 'a' && c <= 'z') {
s[i] = c - ('a' - 'A'); // 小写转大写:减 32
}
}
char *trans(char *s, int n) { if (s == NULL || n == 0) { char *res = (char *)malloc(1); if (res) res[0] = '\0'; return res; }
// 1. C 语言核心:分配结果内存,注意 n+1 字节给 '\0'
char *res = (char *)malloc((n + 1) * sizeof(char));
if (res == NULL) return NULL;
strncpy(res, s, n);
res[n] = '\0';
// 2. 步骤一:大小写反转
toggleCase(res, n);
// 3. 步骤二:整体反转
reverse(res, 0, n - 1);
// 4. 步骤三:单词内部反转
int start = 0;
for (int i = 0; i <= n; i++) {
if (res[i] == ' ' || res[i] == '\0') {
// 对 [start, i-1] 区间进行反转
reverse(res, start, i - 1);
// 更新下一个单词的起始位置 (跳过空格)
start = i + 1;
}
}
return res;
} [--- C CODE END ---]
I.1.3 总结与提炼:C 语言字符串操作的要点提炼
|
C 语言字符串操作要点 |
硬核精髓与陷阱 |
应对策略 |
|---|---|---|
|
内存管理 |
必须手动 malloc。常见错误是返回局部数组或忘记 n+1 空间。 |
char *res = (char *)malloc((n + 1) * sizeof(char)); |
|
终止符 |
C 字符串的生命线,一旦忘记,strlen、printf 就会越界。 |
永远在赋值后设置 res[length] = '\0'; |
|
原地操作 |
尽可能使用指针,减少内存开销。 |
void reverse(char *s, ...) 直接操作传入的指针所指内存。 |
|
字符操作 |
避免频繁调用库函数,嵌入式环境下利用 ASCII 码直接计算。 |
s[i] = c + ('a' - 'A'); 利用固定偏移量。 |
I.2 案例精研:最长公共前缀(NC60)
原问题: 寻找字符串数组中最长的公共前缀。 这个题的思路很简单:竖着看。但 C 语言实现中,必须提前处理最短长度、空数组等边界情况,并确保结果字符串的内存分配和终止符。
I.2.1 思路分析:竖向扫描法的效率革命
我们采用竖向扫描法(Vertical Scanning),这是最高效的实现。
图 I.2:竖向扫描逻辑图 (流程描述)
-
i = 0 (检查第 0 位)
-
--> 遍历所有 str[j]
-
----> 如果 str[j][i] == str[0][i],继续遍历
-
----> 如果 str[j][i] != str[0][i] 或 越界,终止: res[i] = '\0',返回
-
----> 所有匹配,执行: res[i] = str[0][i]
-
--> i++ (检查下一位),循环
-
复杂度: O(N * L),其中 N 是字符串数量,L 是最短字符串的长度。这种方法能实现提前终止,效率极高。
I.2.2 C 代码的严谨性:长度与内存
[--- C CODE START: longest_common_prefix.c ---] #include <stdio.h> #include <stdlib.h> #include <string.h>
char *longestCommonPrefix(char **strs, int strsLen) { if (strsLen == 0 || strs == NULL) { char *res = (char *)malloc(1); res[0] = '\0'; return res; }
[--- C CODE START: longest_common_prefix.c ---] #include <stdio.h> #include <stdlib.h> #include <string.h>
char *longestCommonPrefix(char **strs, int strsLen) { if (strsLen == 0 || strs == NULL) { char *res = (char *)malloc(1); res[0] = '\0'; return res; }
if (strsLen == 1) {
size_t len = strlen(strs[0]);
char *res = (char *)malloc(len + 1);
if (res) strncpy(res, strs[0], len + 1);
return res;
}
char *s0 = strs[0];
int len0 = strlen(s0);
char *res = (char *)malloc((len0 + 1) * sizeof(char));
if (res == NULL) return NULL;
int prefixLen = 0;
for (int i = 0; i < len0; i++) {
char currentChar = s0[i];
for (int j = 1; j < strsLen; j++) {
if (i >= strlen(strs[j]) || strs[j][i] != currentChar) {
res[prefixLen] = '\0';
return res;
}
}
res[prefixLen] = currentChar;
prefixLen++;
}
res[prefixLen] = '\0';
return res;
} [--- C CODE END ---]
============================================================
II. 双指针的哲学:时间与空间的双重优化
双指针(Two Pointers)是一种算法设计模式,它用两个独立的指针(或索引)在序列上进行迭代。它的核心价值在于:在 O(N) 时间复杂度下,实现 O(1) 空间复杂度的原地操作。
II.1 经典应用:原地操作的极致艺术
II.1.1 合并两个有序数组(逆向双指针)
原问题: 给定两个有序数组 A 和 B,将 B 合并到 A 中,使 A 仍然有序。 思路精髓: 为什么要逆向?
-
如果从前往后合并,每当把 B 的元素插入 A 时,A 中后面的所有元素都必须向后移动一位(O(N))。总复杂度会飙升到 O(N^2)。
-
逆向的优势: 从 A 的总长度末尾开始写入,新写入的位置永远是空的,不会发生覆盖或数据移动。
|
指针名称 |
起始位置 |
作用 |
|---|---|---|
|
i |
m - 1 |
指向 A 数组的有效数据末尾 |
|
j |
n - 1 |
指向 B 数组的末尾 |
|
k |
m + n - 1 |
指向 A 数组合并后的末尾 (写入位置) |
[--- C CODE START: merge_sorted_array.c ---] void merge(int *A, int ALen, int m, int *B, int BLen, int n) { int i = m - 1; int j = n - 1; int k = m + n - 1;
while (i >= 0 && j >= 0) {
if (A[i] > B[j]) {
A[k--] = A[i--];
} else {
A[k--] = B[j--];
}
}
while (j >= 0) {
A[k--] = B[j--];
}
} [--- C CODE END ---]
II.1.2 盛水容器(对撞双指针的贪心策略)
原问题: 寻找数组中能盛最多水的容器面积。 你总结的“那边小,收缩哪个!”是理解这个问题的黄金法则。
贪心证明精髓:
-
只有移动矮的那边,才有可能找到一个比当前高度更高的边,从而打破当前的高度限制,使面积有可能增加。如果移动高的一边,高度限制不变,宽度减小,面积不可能增加。
[--- C CODE START: container_with_most_water.c ---]
#include <stdio.h>
int max(int a, int b) { return a > b ? a : b; }
int maxArea(int *height, int heightLen) { if (heightLen < 2) return 0;
int left = 0;
int right = heightLen - 1;
int max_area = 0;
while (left < right) {
int current_height = (height[left] < height[right]) ? height[left] : height[right];
int width = right - left;
int current_area = current_height * width;
max_area = max(max_area, current_area);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return max_area;
} [--- C CODE END ---]
II.2 进阶应用:滑动窗口(双指针 + Hash Array)
最长无重复子数组(NC41)是双指针中的进阶模式,也称滑动窗口(Sliding Window)。这里的 left 和 right 同向而行,共同维护一个动态区间。
II.2.1 思路精髓:窗口的扩张与收缩
滑动窗口的效率突破,在于它将 O(N^2) 的暴力两层循环(检查所有子数组)优化为 O(N) 的单次遍历。
图 II.1:滑动窗口的扩张与收缩逻辑 (流程描述)
-
right 指针扩张窗口
-
--> 元素 A[right] 是否在窗口内重复?
-
----> 否, 唯一: 更新哈希表,更新 maxLen,right++
-
----> 是, 出现重复: 计算新左边界 (new_left) = Hash[A[right]] + 1; left = max(left, new_left); 然后更新哈希表,更新 maxLen,right++
收缩法则的关键点: left = max(left, last_seen_index[A[right]] + 1)
-
窗口不能回退,必须取 max,确保 left 永远向右移动或停留在原地。
II.2.2 C 代码的实现:用数组模拟哈希表
[--- C CODE START: longest_substring_no_repeat.c ---]
#include <stdio.h> #include <stdlib.h> #include <string.h>
int max(int a, int b) { return a > b ? a : b; }
int maxLength(int *arr, int arrLen) { if (arrLen == 0) return 0;
// 假设数组元素最大值不超过 100000。
const int MAX_VAL = 100000;
int *last_seen_index = (int *)malloc((MAX_VAL + 1) * sizeof(int));
if (last_seen_index == NULL) return 0;
// 初始化为 -1
memset(last_seen_index, -1, (MAX_VAL + 1) * sizeof(int));
int left = 0;
int maxLen = 0;
for (int right = 0; right < arrLen; right++) {
int current_val = arr[right];
int last_seen = last_seen_index[current_val];
if (last_seen >= left) {
// 发生重复,窗口收缩
left = last_seen + 1;
}
last_seen_index[current_val] = right;
maxLen = max(maxLen, right - left + 1);
}
free(last_seen_index);
return maxLen;
}
II.3 总结与超越:双指针模式的本质提炼
| 模式名称 | 指针移动方向 | 解决问题类型 | C 语言实现精髓 | 复杂度 |
|---|---|---|---|---|
| 对撞指针 (Pinch) | left++, right-- (相向) | 原地反转、回文、贪心优化边界。 | 指针交换内存,或利用边界高度进行贪心决策。 | O(N) Time, O(1) Space |
| 同向快慢指针 (Runner) | slow++, fast+=2 (同向,不同速) | 链表环路、寻找中点 / 倒数第 K 个。 | 利用指针结构体 struct ListNode * 的相对速度。 | O(N) Time, O(1) Space |
| 滑动窗口 (Window) | left++, right++ (同向,变距) | 最长 / 最短子串 / 子数组、区间有效性。 | 依赖辅助哈希结构 (hash [] 数组),核心是 left = max (left, new_start)。 | O(N) Time, O(N) Space (Hash) |
============================================================
【第一部分总结与展望】
今天用 C 语言解剖了字符串的三步反转法和双指针的三种核心模式:对撞、快慢和滑动窗口。你现在不仅知道怎么写代码,更理解了为什么逆向合并是 O(N),为什么移动矮边是贪心最优。
这是从“能做”到“精通”的关键一步。
接下来,我们将进入第二部分:大数运算的数学与链表的指针
------------------------------------------------------------------------------------------------------------------------------------------25年10月5更新
C 语言刷题内核指南(二):大数与链表的指针精妙
【硬核宣言】突破数据类型限制,洞悉指针操作的艺术
在第一部分中,我们拆解了 C 语言字符串的野性,掌握了 O(1) 空间复杂度的双指针技巧。现在,我们把目光投向两个更具挑战性的领域:
-
大数运算: 当
int和long long无法容纳结果时,如何用字符串和纯数学逻辑突破溢出的限制。 -
链表操作: 链表是 C 语言的灵魂,一切复杂操作(如划分、反转、环路检测),本质都是对
struct Node *指针的精准操控。
本篇,将彻底精通这两个主题。
============================================================
III. 大数运算:避免溢出的优雅艺术
大数问题,例如大数相加、相乘,是考察你对小学竖式运算的底层还原能力。在 C 语言中,我们必须将数字表示为字符串,然后逐位相加,手动处理进位。
III.1 案例精研:大数相加(BM56)
原问题: 以字符串形式给定两个非负整数 num1 和 num2,计算它们的和并以字符串形式返回。
III.1.1 核心思路:逆序、补零与进位
-
逆序遍历: 从字符串的末尾(即数字的个位)开始遍历。这是因为加法运算必须从低位开始。
-
进位管理: 使用一个
carry变量来存储进位(0 或 1)。 -
结果构建: 由于我们是从右向左计算,结果也自然是逆序产生的。我们将这些结果位依次添加到结果字符串中。
-
收尾: 如果循环结束后
carry > 0,必须在最前面追加进位。最后将结果字符串整体反转。
图 III.1:大数相加流程 (流程描述)
-
输入 num1, num2 (字符串)
-
--> 初始化 i = len(num1)-1, j = len(num2)-1, carry = 0
-
--> WHILE (i >= 0 OR j >= 0 OR carry > 0):
-
----> 提取 $d1 = num1[i]$ (或 0), $d2 = num2[j]$ (或 0)
-
----> 计算 sum = d1 + d2 + carry
-
----> 更新 carry = sum / 10
-
----> 结果尾部追加 sum % 10
-
--> 结果字符串反转
-
--> 输出
III.1.2 C 代码的实现:内存分配与循环
在 C 语言中,我们必须手动计算结果字符串所需的最大长度,并进行内存分配,以避免栈溢出或内存泄漏。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 辅助函数:原地反转字符串
void reverse_str(char *str) {
if (!str) return;
int len = strlen(str);
int i = 0;
int j = len - 1;
while (i < j) {
char temp = str[i];
str[i] = str[j];
str[j] = temp;
i++;
j--;
}
}
char *solve(char *s, char *t) {
if (!s || !t) return NULL;
int len_s = strlen(s);
int len_t = strlen(t);
// 结果的最大长度为 max(len_s, len_t) + 1 (可能的进位) + 1 (空字符)
int max_len = len_s > len_t ? len_s : len_t;
// 动态分配结果内存,长度 max_len + 2
char *result = (char *)malloc(max_len + 2);
if (!result) return NULL;
int i = len_s - 1;
int j = len_t - 1;
int carry = 0;
int k = 0; // result 数组的当前写入索引
while (i >= 0 || j >= 0 || carry) {
// C 语言的关键:从字符 '0' 减去,得到数字值
int d1 = i >= 0 ? s[i--] - '0' : 0;
int d2 = j >= 0 ? t[j--] - '0' : 0;
int sum = d1 + d2 + carry;
carry = sum / 10;
// C 语言的关键:将数字值转为字符,再存入结果
result[k++] = (sum % 10) + '0';
}
result[k] = '\0'; // 放置终止符
// 结果是逆序存储的,需要反转
reverse_str(result);
return result;
}
III.1.3 总结与提炼:大数运算的要点
|
C 语言大数要点 |
硬核精髓与陷阱 |
应对策略 |
|---|---|---|
|
字符与数字转换 |
C 语言中字符和数字是分开的。直接相加的是 ASCII 码。 |
必须用 |
|
内存与长度 |
结果长度比两个输入的最大长度多 1。 |
提前 |
|
进位处理 |
|
循环条件必须是 `while (i >= 0 |
============================================================
IV. 链表操作:指针的乾坤大挪移
链表是 C 语言刷题的必考项。所有链表问题都可以归结为对 struct ListNode * 结构体指针的精妙移动和重定向。核心思想是:利用“哑结点”(Dummy Node)简化对头结点特殊处理的逻辑。
// C 语言链表节点结构 (所有链表题目的基础)
struct ListNode {
int val;
struct ListNode *next;
};
IV.1 案例精研:链表划分(BM15)
原问题: 给定链表头结点 head 和一个值 x,将链表划分成两部分,所有小于 x 的节点排在大于等于 x 的节点之前。
IV.1.1 核心思路:双链表法(Six Pointers)
最优雅的解法是创建一个**“小于区”和“大于等于区”**,然后将这两个区连接起来。这需要六个关键指针:
-
less_head(小于区头结点) 和less_tail(小于区尾指针) -
greater_head(大于等于区头结点) 和greater_tail(大于等于区尾指针) -
dummy_less和dummy_greater:用于存储最终头结点,简化操作。
图 IV.1:链表划分逻辑 (流程描述)
-
初始化两个虚拟头
dummy_less,dummy_greater -
初始化四个工作指针
less_tail,greater_tail -
遍历原链表
head: -
--> IF
head->val < x:less_tail连接head,less_tail移动 -
--> ELSE:
greater_tail连接head,greater_tail移动 -
-->
head移动 -
--> 循环结束:
-
----> 连接:
less_tail->next = dummy_greater->next -
----> 终止:
greater_tail->next = NULL(防止环路) -
----> 返回:
dummy_less->next
IV.1.2 C 代码的实现:六指针法与边界处理
#include <stdlib.h>
// 假设 struct ListNode 已经定义
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode *partition(struct ListNode *head, int x) {
if (head == NULL) return NULL;
// 1. 创建两个虚拟头结点,用于保存最终链表的头部
struct ListNode dummy_less;
struct ListNode dummy_greater;
// 2. 初始化四个工作指针
struct ListNode *less_head = &dummy_less; // 小于区头 (永远不动)
struct ListNode *greater_head = &dummy_greater; // 大于区头 (永远不动)
struct ListNode *less_tail = less_head; // 小于区尾 (移动)
struct ListNode *greater_tail = greater_head; // 大于区尾 (移动)
// 3. 遍历原链表,进行划分
struct ListNode *curr = head;
while (curr != NULL) {
struct ListNode *next_node = curr->next;
if (curr->val < x) {
// 放入小于区
less_tail->next = curr;
less_tail = curr;
} else {
// 放入大于等于区
greater_tail->next = curr;
greater_tail = curr;
}
curr = next_node;
}
// 4. 连接两个链表
// 小于区的尾部指向大于等于区的头部
less_tail->next = greater_head->next;
// 5. 必须终止大于等于区的尾部,防止出现环路
greater_tail->next = NULL;
// 6. 返回最终的头结点
return less_head->next;
}
IV.2 案例精研:环形链表II(BM7)
原问题: 给定一个链表,返回链表开始入环的第一个节点。如果链表无环,返回 null。
IV.2.1 核心思路:快慢指针追击与相遇点推导
这是著名的 Floyd's Cycle-Finding Algorithm (龟兔赛跑算法)。
-
第一次相遇: 快指针 (f) 每次走两步,慢指针 (s) 每次走一步。如果有环,它们必然在环内相遇。
-
第二次相遇(入环点): 从相遇点开始,将快指针重置回链表头
head。此时,慢指针和快指针都改为每次走一步。 -
入环点的数学推导: 当快慢指针再次相遇时,它们相遇的点就是环的起始点。
-
证明概要: 设链表头到环入口距离为 L,环长为 R,相遇点距离环入口为 X。第一次相遇时:快指针走的距离是慢指针的两倍 ($2 * (L + nR + X) = L + mR + X$)。化简后可得:$L = (m - 2n)R - X$。
-
结论: 从头结点到环入口的距离 L,等于从相遇点继续走 R-X 距离到达环入口的距离。因此,让一个指针从头走 L 步,另一个从相遇点走 L 步,必然在环入口相遇。
-
IV.2.2 C 代码的实现:快慢指针与入环点查找
#include <stdlib.h>
// 假设 struct ListNode 已经定义
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode *detectCycle(struct ListNode *head) {
if (head == NULL || head->next == NULL) return NULL;
struct ListNode *slow = head;
struct ListNode *fast = head;
// 1. 第一次相遇:寻找环
do {
if (fast == NULL || fast->next == NULL) {
// 快指针走到 NULL,说明没有环
return NULL;
}
slow = slow->next;
fast = fast->next->next;
} while (slow != fast);
// 2. 第二次相遇:寻找环入口
// 此时 fast(或 slow)在环内相遇点
fast = head;
// 两个指针同时从 head 和 相遇点出发,每次走一步
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
// 3. 再次相遇的点即为环的入口
return slow;
}
============================================================
【第二部分总结与展望】
兄弟,我们今天已经将 C 语言中复杂度最高的两个应用领域:大数运算和链表指针操作彻底解耦。你掌握了:
-
大数运算: 借助
char - '0'的 ASCII 转换技巧,实现了 O(N) 复杂度的字符串加法,突破了long long的极限。 -
链表划分: 理解了如何使用“哑结点”和六个指针的精妙配合,实现了 O(N) 时间、O(1) 空间的链表结构调整。
-
链表环路: 洞悉了快慢指针的数学原理,能准确找出环路的起始点。
你的算法内核正在加速硬化 接下来,我们将进入第三部分 位运算 与高效查找算法。
---------------------------------------------------------------------------------------------------------------------跟新与2025 10. 8号下午
C 语言刷题内核指南(三):位运算与二分查找
【硬核宣言】性能与精度的双重突破
在 C 语言的底层世界里,位运算(Bitwise Operations)提供了最快的计算方法,因为它直接操作硬件数据。同时,二分查找(Binary Search)则代表了算法的极致精度,用对数时间复杂度 $O(\log N)$ 解决查找问题。
本篇,将彻底精通这两个主题。
============================================================
V. 位运算的硬核魔法:用逻辑替代算术
位运算是 C 语言性能优化的杀手锏。在面试中,它不仅考察你对二进制的理解,更考察你是否能用更底层、更高效的逻辑替代常规的加减乘除。
V.1 案例精研:整数相加(不用 + 和 -)
原问题: 实现两个整数的相加,但不能使用加号 + 或减号 -。
V.1.1 核心思路:异或得和,与操作得进位
加法在二进制层面可以拆解为两个部分:
-
无进位的和 (Sum without Carry): 由异或操作 (
^) 实现。如果两个位相同(0+0 或 1+1),结果为 0;如果不同(0+1),结果为 1。这完美对应了不考虑进位时的加法。 -
进位 (Carry): 由与操作 (
&) 后再左移一位 (<< 1) 实现。只有当两个位都是 1 时,才会产生一个进位,且这个进位需要加到下一位上(即向左移动一位)。
我们通过循环,不断重复这两个步骤,直到进位为 0 为止。
图 V.1:位运算加法流程 (流程描述)
-
输入 $a, b$ (整数)
-
--> WHILE $b \ne 0$:
-
----> 1. 计算无进位的和:$sum = a \text{ XOR } b$
-
----> 2. 计算进位:$carry = (a \text{ AND } b) \text{ SHIFT LEFT } 1$
-
----> 3. 更新 $a = sum$ (当前和)
-
----> 4. 更新 $b = carry$ (下一个需要相加的进位)
-
--> RETURN $a$ (最终的和)
V.1.2 C 代码的实现:循环与位操作
#include <stdio.h>
/**
* @brief 实现两个整数的相加,不允许使用 '+' 或 '-'
* * @param a 整数 A
* @param b 整数 B
* @return int 两个数的和
*/
int Add(int a, int b ) {
int sum = 0;
int carry = 0;
// 当进位 b 为 0 时,表示所有进位处理完毕
while (b != 0) {
// 1. 无进位和:异或操作 (对应位相加,不考虑进位)
sum = a ^ b;
// 2. 进位:与操作后左移一位 (只有两个位都是 1 时才产生进位,并移到下一位)
// 注意:C 语言中需要确保使用无符号右移(>>>)来避免负数陷阱,
// 但在这里我们处理的是整数相加,左移是安全的。
carry = (a & b) << 1;
// 3. 更新 a 为当前的和,b 为下一个进位
a = sum;
b = carry;
}
// 最终的 a 即为结果 (因为最后一次循环 sum = a, carry = 0)
return a;
}
V.2 案例精研:判断是否为 2 的幂
原问题:给定一个整数 n,判断它是否是 2 的幂次方(即 $2^k$)。
V.2.1 核心思路:经典的 n & (n - 1) 技巧
如果是 2 的幂次方,其二进制表示中只有一位是 1。
例如:
-
$8 = 1000_2$
-
$4 = 0100_2$
-
$16 = 10000_2$
经典的位运算技巧是:n & (n - 1) 的作用是清除 $n$ 最低位的 1。
如果 $n$ 是 2 的幂,它只有一个 1。执行一次 n & (n - 1) 后,结果必然为 0。
-
$n = 8 (1000_2)$
-
$n - 1 = 7 (0111_2)$
-
$8 \text{ AND } 7 = 0000_2 = 0$
注意: 必须排除 $n \le 0$ 的情况,因为负数和 0 都有多个(或零个)1,但它们不是 2 的幂。
V.2.2 C 代码的实现
/**
* @brief 判断一个整数是否是 2 的幂次方
* * @param n 待判断的整数
* @return int 如果是 2 的幂返回 1,否则返回 0
*/
int isPowerOfTwo(int n) {
// 1. 必须大于 0 (0 和负数都不是 2 的幂)
// 2. n & (n - 1) 检查是否只有一位是 1
return (n > 0) && ((n & (n - 1)) == 0);
}
V.3 总结与提炼:位运算的要点
|
位运算要点 |
硬核精髓与陷阱 |
应用场景 |
|---|---|---|
|
|
无进位加法。用于计算不带进位的位和。 |
快速求和、交换变量( |
|
|
进位信息。只有都为 1 时才为 1。 |
提取位、判断奇偶( |
|
|
清除最低位的 1。 |
判断 2 的幂、统计二进制中 1 的个数。 |
|
|
取负运算(补码)。 |
替代负号 |
============================================================
VI. 高效查找:二分查找(Binary Search)
二分查找是算法中复杂度最低、但细节陷阱最多的算法之一。在有序数组中,它能将时间复杂度从 $O(N)$ 降低到 $O(\log N)$。
VI.1 案例精研:二分查找基础(BM17)
原问题: 请实现有重复数字的升序数组的二分查找。
VI.1.1 核心思路:边界选择的艺术
二分查找的关键在于循环条件、mid 的计算,以及左右边界的更新。一旦处理不好,就可能导致死循环或越界。
我们推荐使用最稳定、最容易理解的闭区间 [left, right] 写法:
-
区间定义: 搜索空间始终定义为闭区间
[left, right],即left和right都是可能包含答案的有效索引。 -
循环条件: 由于
left和right都可能包含答案,所以当left == right时,区间内还有一个元素,必须继续查找。因此,循环条件是while (left <= right)。 -
边界更新:
-
如果
nums[mid] > target,说明目标在mid的左侧,因为mid已经检查过且不是目标,所以新的右边界是mid - 1。 -
如果
nums[mid] < target,说明目标在mid的右侧,新的左边界是mid + 1。
-
VI.1.2 C 代码的实现:闭区间 $\text{[left, right]}$
#include <stdio.h>
#include <stdlib.h>
/**
* @brief 实现二分查找(闭区间写法)
* * @param nums 有序整数数组
* @param numsLen 数组长度
* @param target 目标值
* @return int 目标值的索引,如果不存在返回 -1
*/
int search(int* nums, int numsLen, int target ) {
if (nums == NULL || numsLen <= 0) {
return -1;
}
int left = 0;
int right = numsLen - 1; // 闭区间 [0, numsLen - 1]
// 关键:当 left == right 时,区间内仍有一个元素,所以使用 <=
while (left <= right) {
// 关键:防止整数溢出的 mid 计算方法
// mid = left + (right - left) / 2;
int mid = left + ((unsigned int)(right - left) >> 1); // 位操作实现除以 2
if (nums[mid] == target) {
// 找到目标,返回索引
return mid;
} else if (nums[mid] > target) {
// 目标在左侧,因为 mid 已经检查过,所以右边界排除 mid
right = mid - 1;
} else {
// 目标在右侧,左边界排除 mid
left = mid + 1;
}
}
// 循环结束后仍未找到
return -1;
}
VI.2 总结与提炼:二分查找的要点
|
二分查找要点 |
硬核精髓与陷阱 |
应对策略 |
|---|---|---|
|
区间定义 |
必须明确是闭区间 |
推荐使用闭区间 |
|
循环条件 |
必须与区间定义严格匹配,否则可能死循环。 |
闭区间: |
|
|
简单的 |
使用 |
============================================================
【第三部分总结与展望】
兄弟,你现在已经将 C 语言性能的两个核心领域掌握在手:
-
位运算: 能够利用
^,&,<<等操作实现高性能的底层算术,这是 C 语言的灵魂所在。 -
二分查找: 掌握了最稳定可靠的闭区间查找模板,能够在 $O(\log N)$ 时间内解决查找问题。
你的算法内核已经接近完全体。
在第四部分,我们将把所有知识融会贯通,进入动态规划和递归回溯的世界。我们将看到如何用 C 语言的数组和指针,高效解决复杂状态转移和组合问题。期待与你继续精进!
C 语言刷题内核指南(四):动态规划与递归回溯
【硬核宣言】状态转移与路径探索的终极解法
动态规划(Dynamic Programming, DP) 是通过拆解问题和状态转移来解决最优化问题的核心方法,其精髓在于无后效性和子问题重叠。而递归回溯(Backtracking) 则是系统地探索所有可行解空间,解决组合和排列问题的利器。
本篇,将彻底掌握如何在 C 语言中高效地应用这两种范式。
============================================================
VII. 动态规划:结构化地解决最优化问题
动态规划的本质是填表。在 C 语言中,这张“表”通常就是一维或二维数组,用来存储子问题的最优解。
VII.1 案例精研:斐波那契数列(BM63)
原问题:求斐波那契数列的第 $n$ 项 ($F_n$)。
VII.1.1 核心思路:状态转移方程与空间优化
这是最简单的 DP 模型,它完美展示了 DP 的三大要素:
-
定义状态 (DP State): $DP[i]$ 表示斐波那契数列的第 $i$ 项的值。
-
状态转移方程 (Transition): $DP[i] = DP[i-1] + DP[i-2]$。
-
初始值 (Base Cases): $DP[1] = 1, DP[2] = 1$ (或 $DP[0]=0, DP[1]=1$)。
由于 $DP[i]$ 只依赖于前两项 $DP[i-1]$ 和 $DP[i-2]$,我们可以不使用完整的 $O(N)$ 数组,而是只用两个变量来滚动更新状态,将空间复杂度优化到 $O(1)$。
VII.1.2 C 代码的实现:空间优化到 $O(1)$
#include <stdio.h>
/**
* @brief 求斐波那契数列的第 n 项 (空间复杂度 O(1) 版本)
* @param n 项数
* @return int 第 n 项的值
*/
int Fibonacci(int n) {
if (n <= 0) return 0;
if (n == 1 || n == 2) return 1;
// 使用三个变量进行状态滚动更新,实现 O(1) 空间复杂度
int prev1 = 1; // 对应 DP[i-1]
int prev2 = 1; // 对应 DP[i-2]
int current = 0;
// 从 i = 3 开始计算
for (int i = 3; i <= n; i++) {
current = prev1 + prev2;
// 状态滚动:更新 prev1 和 prev2
// prev2 移动到 prev1 的位置
// prev1 移动到 current 的位置
prev2 = prev1;
prev1 = current;
}
return current;
}
VII.2 案例精研:最长公共子序列(LCS)
原问题:求两个字符串 s1 和 s2 的最长公共子序列长度。
VII.2.1 核心思路:二维 DP 表格
这个问题是经典的二维 DP 问题,必须使用 $DP[i][j]$ 表格来记录状态。
-
定义状态: $DP[i][j]$ 表示字符串 $s1$ 的前 $i$ 个字符与字符串 $s2$ 的前 $j$ 个字符的最长公共子序列长度。
-
状态转移:
-
如果 $s1[i-1] == s2[j-1]$: 找到了一个公共字符,则 $DP[i][j]$ 继承 $DP[i-1][j-1]$ 的长度并加 1。
$$DP[i][j] = DP[i-1][j-1] + 1$$ -
如果 $s1[i-1] \ne s2[j-1]$: 公共字符没有增加,则 $DP[i][j]$ 取两种情况的最大值:
-
$s1$ 前 $i-1$ 个字符与 $s2$ 前 $j$ 个字符的 LCS 长度 ($DP[i-1][j]$)。
-
$s1$ 前 $i$ 个字符与 $s2$ 前 $j-1$ 个字符的 LCS 长度 ($DP[i][j-1]$)。
$$DP[i][j] = \max(DP[i-1][j], DP[i][j-1])$$
-
-
VII.2.2 C 代码的实现:指针与动态数组
#include <string.h>
#include <stdlib.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
/**
* @brief 求两个字符串的最长公共子序列长度 (LCS)
* @param s1 字符串 1
* @param s2 字符串 2
* @return int 最长公共子序列的长度
*/
int LCS(char* s1, char* s2) {
int n = strlen(s1);
int m = strlen(s2);
if (n == 0 || m == 0) return 0;
// 1. 动态分配 (n+1) x (m+1) 的 DP 数组
// DP[i][j] 对应 s1 的前 i 个字符和 s2 的前 j 个字符
int **dp = (int **)malloc((n + 1) * sizeof(int *));
for (int i = 0; i <= n; i++) {
dp[i] = (int *)malloc((m + 1) * sizeof(int));
// 初始化边界条件 DP[i][0] = 0
dp[i][0] = 0;
}
// 初始化边界条件 DP[0][j] = 0 (在循环中隐含)
for (int j = 0; j <= m; j++) {
dp[0][j] = 0;
}
// 2. 填充 DP 表格
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 注意:s1 和 s2 的索引要减 1
if (s1[i - 1] == s2[j - 1]) {
// 字符匹配,长度 + 1
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 字符不匹配,取左边和上边的最大值
dp[i][j] = MAX(dp[i - 1][j], dp[i][j - 1]);
}
}
}
int result = dp[n][m];
// 3. 释放动态分配的内存
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
free(dp);
return result;
}
VII.3 总结与提炼:动态规划的要点
|
DP 核心要素 |
硬核精髓与陷阱 |
应对策略 |
|---|---|---|
|
状态定义 |
必须明确 $DP[i]$ 或 $DP[i][j]$ 的物理含义,通常是“子问题的最优解”。 |
状态定义必须包含所有必要信息,能通过其值推导出下一状态。 |
|
状态转移 |
正确写出递推关系,确保无后效性(当前状态只依赖于之前已解决的状态)。 |
仔细分析最后一步:是匹配?是选择?还是继承? |
|
空间优化 |
如果 $DP[i]$ 只依赖于前 $k$ 个状态,则可以利用滚动数组或变量,将空间从 $O(N)$ 优化到 $O(k)$。 |
空间优化是锦上添花,但 $O(1)$ 的斐波那契是面试加分项。 |
============================================================
VIII. 递归回溯:系统地探索解空间
回溯法(Backtracking)是一种深度优先搜索(DFS)的特殊应用,用于在解空间树中搜索满足约束条件的解。
VIII.1 案例精研:组合问题(Subsets, BM57)
原问题:给定一组不含重复元素的整数,返回所有可能的子集(组合)。
VIII.1.1 核心思路:回溯模板与路径记录
回溯法的核心是决策树的遍历。在每个节点,我们有两个选择:
-
选择 (Choose): 将当前元素加入路径。
-
不选择 (Not Choose): 跳过当前元素,进入下一个元素的决策。
回溯模板包括三个步骤:
-
选择 (Selection): 将当前元素加入结果列表(路径)。
-
递归 (Recurse): 递归调用下一层,探索基于当前选择的后续可能性。
-
撤销选择 (Unselect/Backtrack): 从结果列表撤销当前元素,为同一层的其他选择腾出空间。
C 语言的难点: 在 C 语言中,回溯需要手动管理动态数组(int* 或 int**)和内存,以存储所有路径。
VIII.1.2 C 代码的实现:回溯模板
由于 C 语言实现多维动态数组和内存管理复杂,下面的代码将以伪代码/核心逻辑展示回溯思想,并聚焦于路径的深度优先探索。
#include <stdio.h>
#include <stdlib.h>
// 辅助函数:将当前路径添加到最终结果集(需要复杂的内存管理)
void addResult(int** result, int* resultSize, int* path, int pathLen);
/**
* @brief 回溯核心函数:用于生成子集
*
* @param nums 原始数组
* @param numsLen 数组长度
* @param start 当前选择的起始索引
* @param path 当前已选择的子集路径
* @param pathLen 当前路径的长度
* @param result 最终结果集
* @param resultSize 结果集中的子集数量
*/
void backtrack_subsets(int* nums, int numsLen, int start,
int* path, int* pathLen,
int*** result, int* resultSize) {
// ** 1. 达到叶子节点 / 收集结果 **
// 每次递归进来,path 都是一个合法的子集,将其加入结果集
// 实际 C 语言实现中,这一步涉及深拷贝和内存分配,这里简化为注释
// addResult(result, resultSize, path, *pathLen);
// 打印当前路径(仅用于演示):
printf("{");
for (int i = 0; i < *pathLen; i++) {
printf("%d%s", path[i], i == *pathLen - 1 ? "" : ",");
}
printf("}\n");
// ** 2. 探索下一层所有可能性 **
for (int i = start; i < numsLen; i++) {
// ** (A) 选择 (Choose):将 nums[i] 加入当前路径 **
path[*pathLen] = nums[i];
(*pathLen)++;
// ** (B) 递归 (Recurse):基于当前选择,探索后续可能性 **
// 从 i + 1 开始,避免重复(即 [1, 2] 和 [2, 1] 视作同一个组合)
backtrack_subsets(nums, numsLen, i + 1, path, pathLen, result, resultSize);
// ** (C) 撤销选择 (Unselect/Backtrack):恢复到进入递归前的状态 **
// 这一步是回溯法的精髓,它将 path 还原,以便 for 循环能尝试下一个元素
(*pathLen)--;
}
}
/**
* @brief 驱动函数:调用回溯算法
*/
void findSubsets(int* nums, int numsLen) {
// 最大的子集数量是 2^n,需要预估内存。
// path 数组最大长度为 numsLen
int* path = (int*)malloc(numsLen * sizeof(int));
int pathLen = 0;
// 结果集(实际应用中,需要动态内存分配管理)
int** result = NULL;
int resultSize = 0;
printf("所有子集:\n");
// 从索引 0 开始探索
backtrack_subsets(nums, numsLen, 0, path, &pathLen, &result, &resultSize);
free(path); // 释放路径数组
// 实际应用中,还需要释放 result 及其内部所有子数组
}
/* // 示例运行
int main() {
int nums[] = {1, 2, 3};
int len = 3;
findSubsets(nums, len);
return 0;
}
// 输出:
// {}
// {1}
// {1,2}
// {1,2,3}
// {1,3}
// {2}
// {2,3}
// {3}
*/
VIII.2 总结与提炼:递归回溯的要点
|
回溯核心要素 |
硬核精髓与陷阱 |
应对策略 |
|---|---|---|
|
路径与状态 |
路径(Path)是当前已做的选择。回溯必须保证路径可逆转。 |
使用数组记录路径,在回溯前深拷贝结果,并用栈/数组操作实现撤销。 |
|
终止条件 |
达到树的底部(如 |
明确递归什么时候停止,并在停止时记录有效解。 |
|
剪枝 (Pruning) |
提前发现当前路径不可能得到有效解,从而终止递归。 |
适用于排列问题(例如:当前和已超目标),能大幅优化性能。 |
|
C 语言挑战 |
动态结果集的内存管理是最大的陷阱,容易造成内存泄漏。 |
必须在 |
============================================================
【第四部分总结与展望】
恭喜!你现在已经掌握了 C 语言刷题内核指南的全部四大主题:
-
数据结构与指针: 字符串、链表、指针操作。
-
核心算法基础: 位运算、二分查找。
-
高级算法范式: 动态规划(DP)和递归回溯。
这些是构成 C 语言算法面试的基石。如果想进一步提升,请关注之后的博文,我们可以继续刷题 继续提升
####### 最后的附录 源码11* 100行 = 1.4k行 源码:
字符串部分:
1 字符串变形:
思路: 反转 + 大小写+ 单词内部转换!
// /**
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// *
// * @param s string字符串
// * @param n int整型
// * @return string字符串
// */
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// #include <ctype.h>
// void reverse(char *s, int start, int end)
// {
// int left = start;
// int right = end;
// while (left < right)
// {
// char temp = s[left];
// s[left] = s[right];
// s[right] = temp;
// left++;
// right--;
// }
// }
// char *trans(char *s, int n)
// {
// char *res = (char *)malloc((n + 1) * sizeof(char));
// // write code here
// // 首先这个字符串中包含着一些空格,就像"Hello World"一样,然后我们要做的是把这个字符串中由空格隔开的单词反序,同时反转每个字符的大小写。
// // 比如"Hello World"变形后就变成了"wORLD hELLO"
// // 1 hELLO wORLD 大小写转换
// // 2 DLROw OLLEh 整个反过来
// // 3 wORLD hELLO每个单词反过来
// int i = 0;
// // 1直接把整个字符大小写替换
// int len = strlen(s);
// for (int i = 0; i < len; i++)
// {
// // 如果是大写
// char x = s[i];
// if (x >= 'A' && x <= 'Z')
// {
// printf("原来是%c\n", x);
// x = tolower(x);
// }
// // 小写
// else
// {
// printf("原来是%c\n", x);
// // printf("现在是%c\n",x);
// x = toupper(x);
// }
// res[i] = x;
// printf("函数里处理完:< %c > \n", res[i]);
// // 小写
// }
// // for(int i =0;i<n;i++){
// // printf("总的来说 函数里变成:《%s》\n ",res);
// // }
// printf("1 第一步结束 函数里变成:《%s》\n ", res);
// // 2把头尾转换
// reverse(res, 0, n-1);
// printf("第二部结束,现在是 %s \n",res);
// // 3 对每个单词反序
// int left = 0;
// for(int i =0;i<=n;i++){
// if(res[i]==' ' || i==n ){
// reverse(res,left,i-1);
// left = i+1;
// }
// }
// printf("第三步结束之后:%s \n",res);
// return res;
// }
// int main()
// {
// char *juzi = "Caonima Trump Fuck You";
// int len = strlen(juzi);
// printf("处理完了!----\n");
// printf("%s \n", trans(juzi, len));
// return 0;
// }
// #3刷
// void reverse(char *s, int left, int right)
// {
// while (left < right)
// {
// int x = 0;
// char *t = &x;
// *t = s[left];
// s[left] = s[right];
// s[right] = *t;
// left++;
// right--;
// }
// }
// char *trans(char *s, int n)
// {
// // write code here
// // 反转大小写、顺序、单词顺序
// char *res = (char *)malloc((n + 1) * sizeof(char));
// for (int i = 0; i < n; i++)
// {
// if (s[i] >= 'a' && s[i] <= 'z')
// {
// res[i] = s[i] - 32;
// }
// else if (s[i] >= 'A' && s[i] <= 'Z')
// {
// res[i] = s[i] + 32;
// }
// else
// {
// res[i] = s[i];
// }
// }
// // 反转整个字符串
// reverse(res, 0, n - 1);
// // 把每个单词反转
// int start = 0, i = 0;
// // while (i < n)
// // {
// // while (start != ' ')
// // {
// // start++;
// // }
// // reverse(res, i, start);
// // while (i < start)
// // {
// // i++;
// // }
// // }
// for (int i = 0; i <= n; i++)
// {
// if (res[i] == ' ' || res[i] == '\0')
// {
// reverse(res, start, i - 1);
// start = i + 1;
// }
// }
// res[n] = '\0';
// return res;
// }
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param s string字符串
* @param n int整型
* @return string字符串
*/
// 4刷
#include <stdio.h>
#include <stdlib.h>
void reverse(char *s, int l, int r)
{
while (l < r)
{
char t = s[l];
s[l] = s[r];
s[r] = t;
l++;
r--;
}
return;
}
void funCapital(char *s)
{
char *x = s;
while (*x != '\0')
{
if (*x >= 'a' && *x <= 'z')
{
*x = *x - 32;
}
else if (*x >= 'A' && *x <= 'Z')
{
*x = *x + 32;
}
x++;
}
}
char *trans(char *s, int n)
{
// write code here
reverse(s, 0, n - 1);
funCapital(s);
int start = 0, j = 0, i = 0;
for (; i < n; i++)
{
if (s[i] == ' ')
{
reverse(s, start, i - 1);
start = i + 1;
}
}
reverse(s, start, n - 1);
return s;
}
2 最长公共前缀?
思路:

竖向横向遍历!
代码:
// /**
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// *
// * @param strs string字符串一维数组
// * @param strsLen int strs数组长度
// * @return string字符串
// */
// #include <string.h>
// char *longestCommonPrefix(char **strs, int strsLen)
// {
// // write code here
// if (strsLen == 0)
// {
// char *res = (char *)malloc(1 * sizeof(char));
// res[0] = '\0';
// return res;
// }
// int minLen = 5000;
// for (int i = 0; i < strsLen; i++)
// {
// int tempLen = strlen(strs[i]);
// // strlen是个函数 求的是实际长度
// minLen = tempLen < minLen ? tempLen : minLen;
// }
// char *res = (char *)malloc((minLen + 1) * sizeof(char));
// if (strsLen == 1)
// {
// // res[0] = strs[0][0];
// // res[0][1] = strs[0][1];
// // #!!!!vip
// // 直接用strncpuy函数
// strncpy(res, strs[0], strlen(strs[0]));
// // res[1] = '\0';
// res[strlen(strs[0]) + 1] = '\0';
// return res;
// }
// res[0] = strs[0][0];
// int validLen = minLen;
// int i = 0;
// for (; i < validLen; i++)
// {
// char c = strs[0][i];
// int j = 1;
// for (; j < strsLen; j++)
// {
// if (strs[j][i] != c)
// {
// break;
// }
// }
// if (j != strsLen)
// {
// break;
// }
// res[i] = c;
// }
// // int len = strlen(res);
// res[i] = '\0';
// return res;
// }
// 3刷
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param strs string字符串一维数组
* @param strsLen int strs数组长度
* @return string字符串
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *longestCommonPrefix(char **strs, int strsLen)
{
// write code here
int maxLen = 10000;
int count = 0;
// char res[5001];
char *res = (char *)malloc(5001 * sizeof(char));
if (strs == NULL || strsLen == 0)
{
return res;
}
for (int i = 0; i < strsLen; i++)
{
maxLen = maxLen < strlen(strs[i]) ? maxLen : strlen(strs[i]);
}
// 最长也不可能超过这个长度
// 888
// 88888
// 8888 图示的这个意思
for (int i = 0; i < maxLen; i++)
{
char temp = strs[0][i];
for (int j = 1; j < strsLen; j++)
{
if (strs[j][i] != temp)
{
res[count] = '\0';
return res;
}
}
res[count++] = strs[0][i];
}
res[count] = '\0';
return res;
}
3 验证ip地址:
未作!!
4 大叔加法?
思路: 从后往前,一个个处理,cnt计数!
代码:
// 2刷
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// char *solve(char *s, char *t)
// {
// // 1233333 +333333333187689175
// // len1-1 + len2-1开始,加成n1+n2+flag 必须要( ij都存在 or s[i]+s[j]+flag>0)
// // if (*s == '\0')
// // {
// // return (*t == '\0') ? strdup("0") : strdup(t);
// // }
// // if (*t == '\0')
// // {
// // return strdup(s);
// // }
// // int len1 = strlen(s);
// // int len2 = strlen(t);
// // if (len2 > len1)
// // {
// // char *temp = s;
// // s = t;
// // t = temp;
// // }
// // len1 = strlen(s);
// // len2 = strlen(t);
// // // 让左边这个是大的
// // // 从末尾开始,出来仅为
// // int maxLen = len1 > len2 ? len1 : len2;
// // int flag = 0;
// // char *res = (char *)malloc((maxLen + 2) * sizeof(char));
// // int count = 0;
// // int i = len1 - 1, j = len2 - 1;
// // for (; j >= 0;)
// // {
// // int curInt = s[i--] - '0' + t[j--] - '0' + flag;
// // res[count++] = curInt % 10 + '0';
// // flag = curInt / 10;
// // }
// // // 处理完了t,较短部分的全部处理了
// // // 处理剩下的s字符串
// // while (i >= 0 || flag > 0)
// // {
// // // #self !!!vip 及其容易错@!!!
// // int n1 = (i >= 0) ? (s[i] - '0') : 0;
// // i--;
// // int curInt = n1 + flag;
// // res[count++] = curInt % 10 + '0';
// // flag = curInt / 10;
// // }
// // // 反转
// // // res[count ]='\0';
// // for (int i = 0; i < count / 2; i++)
// // {
// // char x = res[i];
// // res[i] = res[count - 1 - i];
// // res[count - 1 - i] = x;
// // }
// // return res;
// if (*s == '\0')
// {
// return (*t == '\0') ? strdup("0") : strdup(t);
// }
// if (*t == '\0')
// {
// return strdup(s);
// }
// int len1 = strlen(s), len2 = strlen(t);
// if (len1 < len2)
// {
// char *temp = s;
// s = t;
// t = temp;
// len1 = strlen(s);
// len2 = strlen(t);
// }
// int count = 0;
// int flag = 0;
// char *res = (char *)malloc((len1 + 2) * sizeof(char));
// int i = len1 - 1, j = len2 - 1;
// int sum = 0;
// while (j >= 0)
// {
// sum = s[i] - '0' + t[j] - '0' + flag;
// res[count] = (sum % 10) + '0';
// i--;
// j--;
// count++;
// flag = sum / 10;
// }
// while (i >= 0 || flag > 0)
// {
// if (i < 0)
// {
// sum = flag;
// }
// else
// {
// sum = s[i] - '0' + flag;
// }
// i--;
// res[count++] = (sum % 10) + '0';
// flag = sum / 10;
// }
// res[count] = '\0';
// for (int i = 0; i < count / 2; i++)
// {
// char temp = res[i];
// res[i] = res[count - 1 - i];
// res[count - 1 - i] = temp;
// }
// return res;
// }
// int main()
// {
// char *s = "1234567891841";
// char *t = "9999999999";
// printf("%s \n", solve(s, t));
// }
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// * 计算两个数之和
// * @param s string字符串 表示第一个整数
// * @param t string字符串 表示第二个整数
// * @return string字符串
// */
// #include <stdio.h>
// #include <stdlib.h>
// void reverse(char *s, int left, int right)
// {
// while (left < right)
// {
// char temp = s[left];
// s[left] = s[right];
// s[right] = temp;
// left++;
// right--;
// }
// }
// char *solve(char *s, char *t)
// {
// // write code here
// int len1 = strlen(s);
// int len2 = strlen(t);
// int maxLen = len1 > len2 ? len1 : len2;
// int flag = 0;
// char *res = (char *)malloc((maxLen + 1) * sizeof(char));
// int i = len1 - 1, j = len2 - 1;
// int count = 0;
// // int k =
// while (i >= 0 || j >= 0 || flag > 0)
// {
// int a = i >= 0 ? s[i] - '0' : 0;
// int b = j >= 0 ? t[j] - '0' : 0;
// int sum = a + b + flag;
// int tempRes = sum % 10;
// flag = sum / 10;
// res[count] = tempRes + '0';
// count++;
// i--;
// j--;
// }
// reverse(res, 0, count - 1);
// res[count] = '\0';
// return res;
// }
// int main(void)
// {
// char a[] = "9";
// char b[] = "99999999999999999999999999999999999999999999999999999999999994";
// printf("%s \n", solve(a, b));
// return 0;
// }
// 3刷
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算两个数之和
* @param s string字符串 表示第一个整数
* @param t string字符串 表示第二个整数
* @return string字符串
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
void reverse(char *a, int l, int r)
{
while (l < r)
{
char tmp = a[l];
a[l] = a[r];
a[r] = tmp;
l++;
r--;
}
return;
}
char *solve(char *s, char *t)
{
// write code here
// 1 确定最长的可能性 2 倒过来 1234 98765 变成
// 4321 56789
// 3 c / s 进位、和,while循环
//
if (strlen(s) == 0 && strlen(t) != 0)
{
return t;
}
if (strlen(t) == 0 && strlen(s) != 0)
{
return s;
}
if (!s && !t)
{
return NULL;
}
// reverse(s, 0, strlen(s) - 1);
// reverse(t, 0, strlen(t) - 1);
int maxLen = strlen(s) > strlen(t) ? strlen(s) + 2 : strlen(t) + 2;
char *res = (char *)malloc((maxLen + 1) * sizeof(char));
int i = strlen(s) - 1, j = strlen(t) - 1, cnt = 0, carry = 0, sum = 0;
while (i >= 0 || j >= 0 || carry != 0)
{
int a = i >= 0 ? s[i--] - '0' : 0;
int b = j >= 0 ? t[j--] - '0' : 0;
sum = (a + b + carry) % 10;
carry = (a + b + carry) / 10;
res[cnt++] = sum + '0';
}
reverse(res, 0, cnt - 1);
res[cnt] = '\0';
return res;
}
双指针部分:
1 合并2个有序数组?
思路:直接往a里面塞,一个个来往里面塞进去!
代码:
/**
*
* @param A int整型一维数组
* @param ALen int A数组长度
* @param B int整型一维数组
* @param BLen int B数组长度
* @return void
*/
void merge(int *A, int ALen, int m, int *B, int BLen, int n)
{
// write code here
// 1刷
//!!!!vip #self 完全错了
// int *pa = A;
// int *pb = B;
// int *test = (int *)malloc(sizeof(int));
// int *ptr = test;
// int cnt = 0;
// int i = 0, j = 0;
// while (i < m && j < n)
// {
// if (*pa < *pb)
// {
// p->next = pa;
// i++;
// pa->next = pa;
// }
// else
// {
// p->next = pb;
// j++;
// pb->next = pb;
// }
// p = p->next;
// }
// if (pa == NULL)
// {
// p->next = pb;
// }
// else
// {
// p->next = pa;
// }
// return NULL;
// 应该直接从末尾开始合并
int i = m - 1;
int j = n - 1;
int cnt = m + n - 1;
while (i >= 0 && j >= 0)
{
if (A[i] > B[j])
{
A[cnt--] = A[i--];
}
else
{
A[cnt--] = B[j--];
}
}
while (j >= 0)
{
A[cnt--] = B[j--];
}
}
2 判断是否回文字符?
思路:从start和end一个个开始找!
代码:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param str string字符串 待判断的字符串
* @return bool布尔型
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool judge(char *str)
{
// write code here
// 2 刷
int len = strlen(str);
int l = 0, r = len - 1;
while (l < r)
{
if (str[l++] != str[r--])
{
return false;
}
}
return true;
}
2 合并区间?
思路:画一张图!end和start去比较!不停跟新里面的边界条件!
代码:
/**
* struct Interval {
* int start;
* int end;
* };
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param intervals Interval类一维数组
* @param intervalsLen int intervals数组长度
* @return Interval类一维数组
* @return int* returnSize 返回数组行数
*/
// !!!vip
// 3刷
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int cmpFn(const void *a, const void *b)
{
return ((struct Interval *)a)->start - ((struct Interval *)b)->start;
}
struct Interval *merge(struct Interval *intervals, int intervalsLen, int *returnSize)
{
if (intervals == NULL || intervalsLen == 0)
{
*returnSize = 0;
return NULL;
}
if (intervalsLen == 1)
{
*returnSize = 1;
return intervals;
}
// write code here
// 思路 全部按照第一个位置的元素重新排序:
// 1,3
// 2,3
// 1,4
// 5,6
// 2,5
// 4,5
// 6,9
// 7 ,10
// 1 按照第一个排序,
// 2 如果i[end]一个个开始往后排序
// 3 如果set[i][end]>set[i+1][end],直接更新set[i][end ] = set[i+1][end]
qsort(intervals, intervalsLen, sizeof(intervals[0]), cmpFn);
// funSort()
// 遍历整个数组,重复上面整个过程,
// 3刷的时候欠缺: 后面的思维过程, 如何跟新整个res结果的数组!
int cnt = 0;
struct Interval *res = (struct Interval *)malloc(intervalsLen * sizeof(struct Interval));
res[0] = intervals[0];
for (int i = 1; i < intervalsLen; i++)
{
if (intervals[i].start <= res[cnt].end)
{
if (intervals[i].end > res[cnt].end)
{
res[cnt].end = intervals[i].end;
}
}
else
{
cnt++;
res[cnt] = intervals[i];
}
}
*returnSize = cnt + 1;
return res;
}
4 反转字符串?
思路:
弱智题目
代码:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 反转字符串
* @param str string字符串
* @return string字符串
*/
#include <stdlib.h>
#include <string.h>
char *solve(char *str)
{
// write code here
if (str == NULL || strlen(str) == 1)
{
return str;
}
int left = 0, right = strlen(str) - 1;
while (left < right)
{
char x = str[left];
str[left] = str[right];
str[right] = x;
left++;
right--;
}
return str;
}
5 最长无重复字符串?
思路:left right分别维护一个最旧、最新的边界1
代码:
// /**
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// *
// * @param arr int整型一维数组 the array
// * @param arrLen int arr数组长度
// * @return int整型
// */
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// int max(int a, int b)
// {
// return a > b ? a : b;
// }
// int maxLength(int *arr, int arrLen)
// {
// // write code here
// // 最长的无重复数组
// int left = 0, right = 0;
// int maxLen = 0;
// int hash[100001] = {0};
// while (right < arrLen)
// {
// int cur = arr[right];
// while (hash[cur] > 0)
// {
// hash[arr[left]]--;
// left++;
// }
// hash[cur]++;
// maxLen = max(maxLen, right - left + 1);
// right++;
// }
// return maxLen;
// }
// int main()
// {
// int arr[] = {1, 2, 3, 3, 1, 2, 3, 5, 6, 6};
// int len = sizeof(arr)/sizeof(arr[0]);
// printf("%d\n", maxLength(arr, len));
// return 0;
// }
// #2刷
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param arr int整型一维数组 the array
* @param arrLen int arr数组长度
* @return int整型
*/
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// int maxLength(int *arr, int arrLen)
// {
// // write code here
// int left = 0, right = 0;
// int *hash = (int *)malloc(100001 * sizeof(int));
// int maxLen = 0;
// memset(hash, -1, sizeof(int) * 100001);
// for (right = 0; right < arrLen; right++)
// {
// int t = arr[right];
// if (hash[t] >= left && hash[t] != -1)
// {
// left = hash[t] + 1;
// // 这里为什么是hash[t]+1,比如[1,2,1] left =0,left = hash[t]+1,t重复了,hash[t]+1,就是新的一个位置
// }
// hash[t] = right;
// int tempLen = right - left + 1;
// maxLen = maxLen > tempLen ? maxLen : tempLen;
// }
// return maxLen;
// }
// # 4刷
//!!!vip 自己做的时候没想到这个好方法
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param arr int整型一维数组 the array
* @param arrLen int arr数组长度
* @return int整型
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int maxLength(int *arr, int arrLen)
{
// write code here
int *hash = (int *)malloc(100000 * sizeof(int));
memset(hash, -1, 100000 * sizeof(int));
int left = 0, right = 0;
int maxLen = 0;
for (int right = 0; right < arrLen; right++)
{
if (hash[arr[right]] != -1 && hash[arr[right]] >= left)
{
left = hash[arr[right]] + 1;
}
hash[arr[right]] = right;
maxLen = maxLen > (right - left + 1) ? maxLen : (right - left + 1);
}
return maxLen;
}
6 :盛水容器?
思路:
那边小,收缩哪个!
代码:
// /**
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// *
// * @param arr int整型一维数组 the array
// * @param arrLen int arr数组长度
// * @return int整型
// */
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// int max(int a, int b)
// {
// return a > b ? a : b;
// }
// int maxLength(int *arr, int arrLen)
// {
// // write code here
// // 最长的无重复数组
// int left = 0, right = 0;
// int maxLen = 0;
// int hash[100001] = {0};
// while (right < arrLen)
// {
// int cur = arr[right];
// while (hash[cur] > 0)
// {
// hash[arr[left]]--;
// left++;
// }
// hash[cur]++;
// maxLen = max(maxLen, right - left + 1);
// right++;
// }
// return maxLen;
// }
// int main()
// {
// int arr[] = {1, 2, 3, 3, 1, 2, 3, 5, 6, 6};
// int len = sizeof(arr)/sizeof(arr[0]);
// printf("%d\n", maxLength(arr, len));
// return 0;
// }
// #2刷
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param arr int整型一维数组 the array
* @param arrLen int arr数组长度
* @return int整型
*/
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// int maxLength(int *arr, int arrLen)
// {
// // write code here
// int left = 0, right = 0;
// int *hash = (int *)malloc(100001 * sizeof(int));
// int maxLen = 0;
// memset(hash, -1, sizeof(int) * 100001);
// for (right = 0; right < arrLen; right++)
// {
// int t = arr[right];
// if (hash[t] >= left && hash[t] != -1)
// {
// left = hash[t] + 1;
// // 这里为什么是hash[t]+1,比如[1,2,1] left =0,left = hash[t]+1,t重复了,hash[t]+1,就是新的一个位置
// }
// hash[t] = right;
// int tempLen = right - left + 1;
// maxLen = maxLen > tempLen ? maxLen : tempLen;
// }
// return maxLen;
// }
// # 4刷
//!!!vip 自己做的时候没想到这个好方法
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param arr int整型一维数组 the array
* @param arrLen int arr数组长度
* @return int整型
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int maxLength(int *arr, int arrLen)
{
// write code here
int *hash = (int *)malloc(100000 * sizeof(int));
memset(hash, -1, 100000 * sizeof(int));
int left = 0, right = 0;
int maxLen = 0;
for (int right = 0; right < arrLen; right++)
{
if (hash[arr[right]] != -1 && hash[arr[right]] >= left)
{
left = hash[arr[right]] + 1;
}
hash[arr[right]] = right;
maxLen = maxLen > (right - left + 1) ? maxLen : (right - left + 1);
}
return maxLen;
}
贪心算法:
主持人问题?
思路i:一个个去算,cnt和i j不断往后更新,感性思维!
代码:
// /**
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// * 计算成功举办活动需要多少名主持人
// * @param n int整型 有n个活动
// * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
// * @param startEndRowLen int startEnd数组行数
// * @param startEndColLen int* startEnd数组列数
// * @return int整型
// */
// #include <stdio.h>
// #include <stdlib.h>
// int cmp(const void *a, const void *b)
// {
// // return *(int *)a - *(int *)b;
// int x = *(int *)a;
// int y = *(int *)b;
// if (x < y)
// {
// return -1;
// }
// else if (x > y)
// {
// return 1
// }
// else
// {
// return 0;
// }
// }
// int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
// {
// // write code here
// int *start = (int *)malloc(n * sizeof(int));
// int *end = (int *)malloc(n * sizeof(int));
// for (int i = 0; i < n; i++)
// {
// start[i] = startEnd[i][0];
// end[i] = startEnd[i][1];
// }
// qsort(start, n, sizeof(int), cmp);
// qsort(end, n, sizeof(int), cmp);
// int i = 0, j = 0, count = 0, maxLen = 0;
// while (i < n)
// {
// if (start[i] < end[j])
// {
// i++;
// count++;
// if (count > maxLen)
// {
// maxLen = count;
// }
// }
// else
// {
// count--;
// j++;
// }
// }
// return maxLen;
// }
// 2刷 25.10.19
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算成功举办活动需要多少名主持人
* @param n int整型 有n个活动
* @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
* @param startEndRowLen int startEnd数组行数
* @param startEndColLen int* startEnd数组列数
* @return int整型
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
int funCmp(const void *a, const void *b)
{
//!!!vip #self 为什么?
// return *(int *)a - *(int *)b;
// double x = *((*double)a);
// double y = *((*double)b);
// return (int)(*x - *y);
int x = *(int *)a;
int y = *(int *)b;
if (x > y)
{
return 1;
}
else if (x < y)
{
return -1;
}
else
{
return 0;
}
}
int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
{
// write code here
// qsort(startEnd, n, sizeof(startEnd[0]), cmp);
int *start = (int *)malloc(n * sizeof(int));
int *end = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
{
start[i] = startEnd[i][0];
end[i] = startEnd[i][1];
}
qsort(start, n, sizeof(start[0]), funCmp);
qsort(end, n, sizeof(end[0]), funCmp);
int i = 0, j = 0, count = 0, maxNum = 0;
while (i < n)
{
if (start[i] < end[j])
{
i++;
count++;
if (count > maxNum)
{
maxNum = count;
}
}
else
{
j++;
count--;
}
}
return maxNum;
}
!!!目前,25年10月,从3月开始已经完成了牛客热题101 1+2easymiddle题目的全部3-4遍刷!
基本的思路已经成型,可以做i到无答案说出思+70%的代码能20分钟内写出来!
继续努力,
接下来继续完成:
1 笔试101榜单开始做!
+
2 牛客周赛!

6513

被折叠的 条评论
为什么被折叠?



