保研随笔录
上一届的夏令营已经陆续结束。一年后的我,又会是怎样的状态?是拿到心仪的 offer,还是全然失败、准备九推?
与其焦虑地等待,不如从头开始提升自我。我将记录这一路的辛酸苦辣,尽管结果未必如意。
起点
从大二下学期期末开始,心里总是隐约不安。大一的全盘失败让我进退两难:科研没有什么成果,绩点甚至没上 90,只能排在专业 50%。在德育分和智育分都不太理想的情况下,我必须在保证绩点的同时做出科研成果。
大二的每一门课程都不是善茬,我的容错太低了,尽管知道很多内容一辈子也不会用上,但我必须强迫自己把这些内容缓存进自己有限的脑容量中。
这已经是我能做到的最大努力,有些老师不当人确实没办法
靓如姐保到了浙软,泽中保到了北大。熟悉的人奔向了不同的去向,我能否和他们一样有一个好的归宿?和他们相比,我欠缺了什么?还有哪些地方可以补足,又有哪些教训值得吸取?我必须认清自己。
不足与应对
| # | 不足 | 应对方案 |
|---|---|---|
| 1 | 代码能力薄弱:看得懂、有概念,但实操能力较差。平时过度依赖 AI 写代码,学院培养方案也不重视实践 | 9 月前学完基础知识;接下来的半年多坚持每天刷算法题,掌握套路与模板。一方面参加校内程序设计大赛、蓝桥杯等比赛,争取德育分;另一方面为夏令营和预推免机试做准备 |
| 2 | 项目经历几乎为 0,缺乏工程实践训练和实习经历 | 大三上学期末开始校外实习,最好是线下实习;注意维护人际关系,在 GitHub、牛客上多了解他人的实习经历 |
| 3 | 无法应对突发情况,比如申请港科,托福还没着落 | 提前准备好各种材料,尽早决定是否考雅思/托福、是否参加数竞、是否参加大英赛 |
| 4 | 课内成绩可能无法达到目标院校的门槛 | 在保证科研和其他学习任务进度的同时,尽可能提高课内成绩,同时把握好德育分 |
| 5 | 科研成果挂零 | 8 月整理思路、重新确定方向,全面推进科研进度 |
关键节点(时间线)
| 时间 | 节点 | 状态 |
|---|---|---|
| 2026.08 | 整理思路,重新确定方向,全面启动科研 | ⬜ 待办 |
| 2026.09 | 学完基础知识 | ✅ 已于 2026-08-04 完成 |
| 2026.09—2027.05 | 每天刷算法题(备战蓝桥杯、校内程序设计大赛) | ⬜ 待办 |
| 2027.01 | 大三上学期末,开始校外实习 | ⬜ 待办 |
| 2027.07 | 夏令营 | ⬜ 待办 |
| 2027.09 | 预推免 / 九推 | ⬜ 待办 |
知识索引
- 资源与规划
- 复杂度与计算机基础
- 数组
- 链表
- 哈希表
- 字符串
学习历程(持续更新)
2026-07-31(周五)
- 搜集算法学习与刷题网站:代码随想录、LeetCode 热题 100
- 信息与经验渠道:牛客(实习、面经、保研经验等)
2026-08-01(周六)
时间复杂度
- 理解大 O 表示法
- 了解不同数据规模对算法复杂度的要求
- 掌握复杂表达式的化简方法
- 理解
O(log n)中对数底数的影响 - 练习分析时间复杂度:从 个字符串中找出相同的两个字符串(假设仅有两个字符串相同)
程序为什么会超时
- 从硬件配置出发,大致了解 CPU 的执行速度
- 万赫兹
- 任何开发计算机程序的软件工程师都应该能够估计,这个程序的运行时间是一秒钟还是一年。
2026-08-04(周二)
空间复杂度
- 空间复杂度分析
- 递归算法的时间与空间复杂度分析:
- 斐波那契数列:对比
fibonacci(i - 1) + fibonacci(i - 2)与fibonacci(second, first + second, n - 1)两种递归写法 - 二分法(递归实现)的性能分析
- 递归算法的空间复杂度 ≈ 单层调用所需空间 × 最大递归深度
- 递归算法的时间复杂度 ≈ 递归调用总次数 × 单次调用的非递归工作量
- 斐波那契数列:对比
代码的内存消耗
- 固定部分:代码区、数据区
- 可变部分:栈区(自动分配与回收)、堆区(可通过
new动态分配,使用完成后应正确释放) - 内存泄漏:动态分配的内存在不再使用后未被释放
- 指针大小与寻址范围:
- 典型 32 位平台:指针大小为 4 Byte,理论寻址空间为 Byte,即 4 GB
- 典型 64 位平台:指针大小为 8 Byte,理论寻址空间为 Byte,实际可用范围受硬件和操作系统限制
- 内存对齐:
- 平台原因:并非所有硬件平台都能访问任意内存地址上的任意数据。某些平台只能在特定地址处读取特定类型的数据,否则会抛出硬件异常。为了让同一程序能够在多个平台上运行,需要进行内存对齐。
- 硬件原因:经过内存对齐后,CPU 访问内存的速度会显著提升。
- 阶段进度:基础知识学习至此结束。
2026-08-08(周六)
最近总是偷懒 😠,要克服这种心理。
数组(一)
一、数组理论
- 数组是存放在连续内存空间上的相同类型数据的集合。
- C++ 中的二维数组在地址空间上是连续的。
int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};数组地址:
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee40658340x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834二、二分查找
区间通常有两种定义:左闭右闭 [left, right],或左闭右开 [left, right)。
- 版本一:左闭右闭
[left, right]
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
// target 位于左闭右闭区间 [left, right]
int right = nums.size() - 1;
// 当 left == right 时,[left, right] 仍然有效,因此使用 <=
while (left <= right) {
// 防止溢出,等同于 (left + right) / 2
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
// target 位于左区间 [left, middle - 1]
right = middle - 1;
} else if (nums[middle] < target) {
// target 位于右区间 [middle + 1, right]
left = middle + 1;
} else {
// 找到目标值,返回下标
return middle;
}
}
// 未找到目标值
return -1;
}
};class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
// target 位于左闭右闭区间 [left, right]
int right = nums.size() - 1;
// 当 left == right 时,[left, right] 仍然有效,因此使用 <=
while (left <= right) {
// 防止溢出,等同于 (left + right) / 2
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
// target 位于左区间 [left, middle - 1]
right = middle - 1;
} else if (nums[middle] < target) {
// target 位于右区间 [middle + 1, right]
left = middle + 1;
} else {
// 找到目标值,返回下标
return middle;
}
}
// 未找到目标值
return -1;
}
};- 版本二:左闭右开
[left, right)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
// target 位于左闭右开区间 [left, right)
int right = nums.size();
// 当 left == right 时,[left, right) 无效,因此使用 <
while (left < right) {
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
// target 位于左区间 [left, middle)
right = middle;
} else if (nums[middle] < target) {
// target 位于右区间 [middle + 1, right)
left = middle + 1;
} else {
// 找到目标值,返回下标
return middle;
}
}
// 未找到目标值
return -1;
}
};class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
// target 位于左闭右开区间 [left, right)
int right = nums.size();
// 当 left == right 时,[left, right) 无效,因此使用 <
while (left < right) {
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
// target 位于左区间 [left, middle)
right = middle;
} else if (nums[middle] < target) {
// target 位于右区间 [middle + 1, right)
left = middle + 1;
} else {
// 找到目标值,返回下标
return middle;
}
}
// 未找到目标值
return -1;
}
};2026-08-10(周一)
数组(二)
三、移除元素
- 题目:27. 移除元素
- 暴力解法:使用两层
for循环。第一层遍历数组元素;发现需要移除的元素后,第二层将后续元素整体向前移动一位。- 时间复杂度:
- 空间复杂度:
- 双指针法(快慢指针法):通过快、慢指针,在一层
for循环中完成两层循环的工作。- 快指针:寻找新数组的元素,即不等于目标值的元素
- 慢指针:指向新数组中待更新的下标
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};四、有序数组的平方
- 题目:977. 有序数组的平方
- 暴力排序:将每个数平方后排序,时间复杂度为 。
- 双指针法:原数组有序,平方后的最大值只可能出现在数组两端。新建与原数组等长的
result,令k指向其末尾:- 若
A[i] * A[i] < A[j] * A[j],则执行result[k--] = A[j] * A[j] - 否则,执行
result[k--] = A[i] * A[i] - 时间复杂度:
- 空间复杂度:
- 若
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
int k = A.size() - 1;
vector<int> result(A.size(), 0);
// 最后仍需处理 i == j 时的元素,因此使用 <=
for (int i = 0, j = A.size() - 1; i <= j;) {
if (A[i] * A[i] < A[j] * A[j]) {
result[k--] = A[j] * A[j];
j--;
} else {
result[k--] = A[i] * A[i];
i++;
}
}
return result;
}
};class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
int k = A.size() - 1;
vector<int> result(A.size(), 0);
// 最后仍需处理 i == j 时的元素,因此使用 <=
for (int i = 0, j = A.size() - 1; i <= j;) {
if (A[i] * A[i] < A[j] * A[j]) {
result[k--] = A[j] * A[j];
j--;
} else {
result[k--] = A[i] * A[i];
i++;
}
}
return result;
}
};五、长度最小的子数组
- 题目:209. 长度最小的子数组
- 暴力解法:使用两层
for循环,不断寻找符合条件的子数组,时间复杂度为 。 - 滑动窗口:不断调整子数组的起始位置和终止位置,从而得到满足条件的最短子数组。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口内元素之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 使用 while 持续更新起始位置,并判断子数组是否符合条件
while (sum >= s) {
subLength = j - i + 1;
result = result < subLength ? result : subLength;
// 不断变更 i,缩小滑动窗口
sum -= nums[i++];
}
}
// result 未被更新,说明没有符合条件的子数组
return result == INT32_MAX ? 0 : result;
}
};class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口内元素之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 使用 while 持续更新起始位置,并判断子数组是否符合条件
while (sum >= s) {
subLength = j - i + 1;
result = result < subLength ? result : subLength;
// 不断变更 i,缩小滑动窗口
sum -= nums[i++];
}
}
// result 未被更新,说明没有符合条件的子数组
return result == INT32_MAX ? 0 : result;
}
};六、螺旋矩阵 II
- 题目:59. 螺旋矩阵 II
- 核心原则:坚持循环不变量,四条边均采用左闭右开的处理方式。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
int startx = 0, starty = 0; // 每一圈的起始位置
int loop = n / 2; // 需要循环的圈数
int mid = n / 2; // 矩阵中心位置
int count = 1; // 当前待填入的数值
int offset = 1; // 控制每条边遍历的长度
int i, j;
while (loop--) {
i = startx;
j = starty;
// 上边:从左到右(左闭右开)
for (; j < n - offset; j++) {
res[i][j] = count++;
}
// 右边:从上到下(左闭右开)
for (; i < n - offset; i++) {
res[i][j] = count++;
}
// 下边:从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 左边:从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
startx++;
starty++;
offset++;
}
// n 为奇数时,单独为矩阵中心赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
int startx = 0, starty = 0; // 每一圈的起始位置
int loop = n / 2; // 需要循环的圈数
int mid = n / 2; // 矩阵中心位置
int count = 1; // 当前待填入的数值
int offset = 1; // 控制每条边遍历的长度
int i, j;
while (loop--) {
i = startx;
j = starty;
// 上边:从左到右(左闭右开)
for (; j < n - offset; j++) {
res[i][j] = count++;
}
// 右边:从上到下(左闭右开)
for (; i < n - offset; i++) {
res[i][j] = count++;
}
// 下边:从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 左边:从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
startx++;
starty++;
offset++;
}
// n 为奇数时,单独为矩阵中心赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};2026-08-11(周二)
数组(三)
七、区间和
- 题目:58. 区间和(第九期模拟笔试)
- 暴力解法:对于每个查询区间,重新遍历并累加区间内的所有元素。
- 前缀和:适用于需要频繁计算区间和的问题。
p[i]表示数组vec中下标0到i的元素之和- 当
a == 0时,区间和为p[b] - 当
a > 0时,区间和为p[b] - p[a - 1] - 预处理时间复杂度:
- 单次查询时间复杂度:
- 空间复杂度:
#include <cstdio>
#include <vector>
using namespace std;
int main() {
int n, a, b;
scanf("%d", &n);
vector<int> vec(n);
vector<int> prefix(n);
int prefixSum = 0;
for (int i = 0; i < n; i++) {
scanf("%d", &vec[i]);
prefixSum += vec[i];
prefix[i] = prefixSum;
}
while (scanf("%d%d", &a, &b) == 2) {
int sum = (a == 0) ? prefix[b] : prefix[b] - prefix[a - 1];
printf("%d\n", sum);
}
return 0;
}#include <cstdio>
#include <vector>
using namespace std;
int main() {
int n, a, b;
scanf("%d", &n);
vector<int> vec(n);
vector<int> prefix(n);
int prefixSum = 0;
for (int i = 0; i < n; i++) {
scanf("%d", &vec[i]);
prefixSum += vec[i];
prefix[i] = prefixSum;
}
while (scanf("%d%d", &a, &b) == 2) {
int sum = (a == 0) ? prefix[b] : prefix[b] - prefix[a - 1];
printf("%d\n", sum);
}
return 0;
}面对大量数据的输入与输出时,使用
scanf和printf通常比默认的cin和cout耗时更少。
八、开发商购买土地
- 题目:44. 开发商购买土地(第五期模拟笔试)
- 暴力解法:使用一层
for循环枚举分割线,再用两层for循环分别累加分割线两侧的土地价值。 - 前缀和思路:
- 统计矩阵的总价值。
- 分别计算每行与每列的价值之和。
- 累加分割线一侧的价值,并用
abs(sum - 2 * cut)计算两侧价值之差。 - 分别枚举横向和纵向分割线,取最小差值。
- 时间复杂度:
- 空间复杂度:
#include <algorithm>
#include <climits>
#include <cstdlib>
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
int sum = 0;
vector<vector<int>> vec(n, vector<int>(m, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
// 统计每行的价值之和
vector<int> horizontal(n, 0);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
horizontal[i] += vec[i][j];
}
}
// 统计每列的价值之和
vector<int> vertical(m, 0);
for (int j = 0; j < m; j++) {
for (int i = 0; i < n; i++) {
vertical[j] += vec[i][j];
}
}
int result = INT_MAX;
int horizontalCut = 0;
// 分割线必须位于两行之间,因此不枚举最后一行之后的位置
for (int i = 0; i < n - 1; i++) {
horizontalCut += horizontal[i];
result = min(result, abs(sum - 2 * horizontalCut));
}
int verticalCut = 0;
// 分割线必须位于两列之间
for (int j = 0; j < m - 1; j++) {
verticalCut += vertical[j];
result = min(result, abs(sum - 2 * verticalCut));
}
cout << result << endl;
return 0;
}#include <algorithm>
#include <climits>
#include <cstdlib>
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
int sum = 0;
vector<vector<int>> vec(n, vector<int>(m, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
// 统计每行的价值之和
vector<int> horizontal(n, 0);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
horizontal[i] += vec[i][j];
}
}
// 统计每列的价值之和
vector<int> vertical(m, 0);
for (int j = 0; j < m; j++) {
for (int i = 0; i < n; i++) {
vertical[j] += vec[i][j];
}
}
int result = INT_MAX;
int horizontalCut = 0;
// 分割线必须位于两行之间,因此不枚举最后一行之后的位置
for (int i = 0; i < n - 1; i++) {
horizontalCut += horizontal[i];
result = min(result, abs(sum - 2 * horizontalCut));
}
int verticalCut = 0;
// 分割线必须位于两列之间
for (int j = 0; j < m - 1; j++) {
verticalCut += vertical[j];
result = min(result, abs(sum - 2 * verticalCut));
}
cout << result << endl;
return 0;
}九、数组总结
核心特性
- 数组是存放在连续内存空间上的相同类型数据的集合。
- 数组下标从 0 开始。
- 数组元素的内存地址连续,因此删除或添加元素时,通常需要移动其他元素。
经典方法
- 二分查找:坚持循环不变量,统一维护区间定义。
- 双指针:通过快、慢指针在一层
for循环中完成两层循环的工作。 - 滑动窗口:根据当前子数组的状态不断调整起始位置,将部分 的暴力解法优化至 。
- 模拟:坚持循环不变量,统一每一轮的处理规则。
- 前缀和:预处理累计和,使用
prefix[b] - prefix[a - 1]快速计算区间和。
知识图谱
2026-08-12(周三)
链表(一)
一、链表理论基础
定义
链表是一种通过指针串联起来的线性结构。每个节点由两部分组成:
- 数据域:存储节点中的数据
- 指针域:存储指向下一个节点的指针;最后一个节点的指针域指向
nullptr
常见类型
- 单链表:每个节点的指针域只能指向下一个节点
- 双链表:每个节点有两个指针域,分别指向前一个节点和后一个节点
- 循环链表:链表首尾相连,尾节点指向头节点
存储方式
数组元素在内存中连续分布,而链表节点通常不连续分布。链表通过节点指针,将分散在内存中的节点连接起来。
// 单链表节点
struct ListNode {
int val; // 节点中存储的元素
ListNode* next; // 指向下一个节点
ListNode(int x) : val(x), next(nullptr) {}
};// 单链表节点
struct ListNode {
int val; // 节点中存储的元素
ListNode* next; // 指向下一个节点
ListNode(int x) : val(x), next(nullptr) {}
};如果没有定义接受参数的构造函数,就不能使用
ListNode node(x)这种方式直接为节点值初始化。
基本操作
- 删除节点 D:将节点 C 的
next指向节点 E,再手动释放节点 D - 在 C 与 D 之间添加节点 F:先将 F 的
next指向 D,再将 C 的next指向 F
数组与链表的性能对比
| 数据结构 | 插入 / 删除 | 按下标查询 | 长度与适用场景 |
|---|---|---|---|
| 数组 | 长度通常预先确定,适合频繁查询 | ||
| 链表 | 已知目标位置时为 | 长度可动态变化,适合频繁增删、较少查询 |
二、移除链表元素
- 题目:203. 移除链表元素
- 直接操作原链表:头节点没有前驱节点,因此需要分别处理头节点和非头节点。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 连续删除值等于 val 的头节点,注意这里使用 while 而非 if
while (head != nullptr && head->val == val) {
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头节点
ListNode* cur = head;
while (cur != nullptr && cur->next != nullptr) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 连续删除值等于 val 的头节点,注意这里使用 while 而非 if
while (head != nullptr && head->val == val) {
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头节点
ListNode* cur = head;
while (cur != nullptr && cur->next != nullptr) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};虚拟头节点
设置虚拟头节点后,原头节点和其他节点可以使用同一套删除逻辑,避免单独处理头节点。
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;三、设计链表
- 题目:707. 设计链表
- 核心设计:使用虚拟头节点统一链表的插入与删除操作,并用
_size记录有效节点数量。
class MyLinkedList {
public:
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val) : val(val), next(nullptr) {}
};
MyLinkedList() {
_dummyHead = new LinkedNode(0);
_size = 0;
}
~MyLinkedList() {
while (_dummyHead != nullptr) {
LinkedNode* tmp = _dummyHead;
_dummyHead = _dummyHead->next;
delete tmp;
}
}
int get(int index) {
if (index < 0 || index >= _size) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while (index--) {
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cur = cur->next;
}
cur->next = newNode;
_size++;
}
void addAtIndex(int index, int val) {
if (index > _size) {
return;
}
if (index < 0) {
index = 0;
}
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
void deleteAtIndex(int index) {
if (index < 0 || index >= _size) {
return;
}
LinkedNode* cur = _dummyHead;
while (index--) {
cur = cur->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
private:
LinkedNode* _dummyHead;
int _size;
};class MyLinkedList {
public:
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val) : val(val), next(nullptr) {}
};
MyLinkedList() {
_dummyHead = new LinkedNode(0);
_size = 0;
}
~MyLinkedList() {
while (_dummyHead != nullptr) {
LinkedNode* tmp = _dummyHead;
_dummyHead = _dummyHead->next;
delete tmp;
}
}
int get(int index) {
if (index < 0 || index >= _size) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while (index--) {
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cur = cur->next;
}
cur->next = newNode;
_size++;
}
void addAtIndex(int index, int val) {
if (index > _size) {
return;
}
if (index < 0) {
index = 0;
}
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
void deleteAtIndex(int index) {
if (index < 0 || index >= _size) {
return;
}
LinkedNode* cur = _dummyHead;
while (index--) {
cur = cur->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
private:
LinkedNode* _dummyHead;
int _size;
};2026-08-13(周四)
链表(二)
四、反转链表
- 题目:206. 反转链表
- 双指针法:使用
cur遍历原链表,使用pre指向已完成反转的部分。改变cur->next前,必须先用temp保存下一个节点。 - 时间复杂度:
- 空间复杂度:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur != nullptr) {
// 保存下一个节点,避免改变 cur->next 后丢失后续链表
ListNode* temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur != nullptr) {
// 保存下一个节点,避免改变 cur->next 后丢失后续链表
ListNode* temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};五、删除链表的倒数第 N 个节点
- 题目:19. 删除链表的倒数第 N 个节点
- 快慢指针法:
- 创建虚拟头节点,使删除头节点与删除其他节点使用同一套逻辑。
fast先从虚拟头节点出发移动 步,使fast与slow之间保持 步的间隔。- 同时移动两个指针,直到
fast指向nullptr;此时slow恰好指向待删除节点的前一个节点。 - 调整指针并释放待删除节点。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
// 让 slow 最终停在待删除节点的前一个节点
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
ListNode* tmp = slow->next;
slow->next = tmp->next;
delete tmp;
ListNode* newHead = dummyHead->next;
delete dummyHead;
return newHead;
}
};class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
// 让 slow 最终停在待删除节点的前一个节点
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
ListNode* tmp = slow->next;
slow->next = tmp->next;
delete tmp;
ListNode* newHead = dummyHead->next;
delete dummyHead;
return newHead;
}
};七、链表相交
- 题目:面试题 02.07. 链表相交
- 长度对齐法:
- 分别计算链表 A 与链表 B 的长度。
- 让较长链表的指针先移动长度差,使两个指针到链表末尾的距离相同。
- 同时向后移动两个指针;首次指向同一节点时,该节点就是相交节点。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0;
int lenB = 0;
while (curA != nullptr) {
lenA++;
curA = curA->next;
}
while (curB != nullptr) {
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 保证 curA 指向较长链表,lenA 为较大长度
if (lenB > lenA) {
swap(lenA, lenB);
swap(curA, curB);
}
// 将两个链表按末尾对齐
int gap = lenA - lenB;
while (gap--) {
curA = curA->next;
}
while (curA != nullptr) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
};class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0;
int lenB = 0;
while (curA != nullptr) {
lenA++;
curA = curA->next;
}
while (curB != nullptr) {
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 保证 curA 指向较长链表,lenA 为较大长度
if (lenB > lenA) {
swap(lenA, lenB);
swap(curA, curB);
}
// 将两个链表按末尾对齐
int gap = lenA - lenB;
while (gap--) {
curA = curA->next;
}
while (curA != nullptr) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
};2026-08-14(周五)
链表(三)
八、环形链表 II
- 题目:142. 环形链表 II
- 需要解决两个问题:
- 判断链表中是否存在环。
- 如果存在环,找到环的入口节点。
判断是否存在环
使用快慢指针:fast 每次移动两个节点,slow 每次移动一个节点。如果两个指针在途中相遇,说明链表中存在环;如果 fast 或 fast->next 指向 nullptr,说明链表中不存在环。
寻找环的入口
假设:
- 从头节点到环入口的距离为
- 从环入口到快慢指针相遇点的距离为
- 从相遇点回到环入口的距离为
fast在环内绕了 圈
相遇时,slow 与 fast 经过的距离分别为:
由于 fast 的速度是 slow 的两倍:
整理可得:
因此,分别从头节点与快慢指针相遇点出发两个指针,并让它们每次各移动一个节点;二者再次相遇的位置就是环的入口。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,说明链表中存在环
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
// 从相遇点和头节点同时出发,再次相遇处就是环入口
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2;
}
}
return nullptr;
}
};class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,说明链表中存在环
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
// 从相遇点和头节点同时出发,再次相遇处就是环入口
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2;
}
}
return nullptr;
}
};九、链表总结
理论基础
- 常见类型:单链表、双链表、循环链表
- 存储方式:节点通常分散在内存中,通过指针连接
- 基本操作:链表的增加、删除、修改与查询
- 性能分析:理解数组和链表在不同使用场景中的优劣
经典方法与题目
- 虚拟头节点:统一头节点与其他节点的处理逻辑,避免单独分类讨论
- 链表基本操作:设计链表
- 迭代双指针:反转链表
- 快慢指针与虚拟头节点:删除链表的倒数第 N 个节点
- 长度对齐:链表相交
- 快慢指针:判断链表是否有环并寻找环入口
2026-08-15(周六)
哈希表(一)
哈希表用于快速判断元素是否出现、统计元素频率以及建立键值映射,是数组与字符串题目中的高频工具。
一、哈希表理论基础
基本概念
- 哈希表:根据关键码的值直接访问数据的数据结构。
- 哈希函数:通过
hashCode将键转换为数值,再映射到哈希表的索引位置。 - 哈希碰撞:不同的键经过哈希函数计算后,被映射到同一个索引位置。
哈希碰撞的解决方法
- 拉链法:将发生冲突的元素存储在同一个链表中。需要选择合适的表大小,避免空槽过多浪费内存,也避免链表过长降低查询效率。
- 线性探测法:依靠哈希表中的空槽解决冲突。发生碰撞时,按顺序继续寻找下一个空槽;因此需要保证
tableSize > dataSize。
常用哈希结构
- 数组
set(集合)map(映射)
C++ 集合容器对比
| 容器 | 底层实现 | 是否有序 | 元素能否重复 | 元素能否直接修改 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | ||
std::multiset | 红黑树 | 有序 | 是 | 否 | ||
std::unordered_set | 哈希表 | 无序 | 否 | 否 | 平均 | 平均 |
C++ 映射容器对比
| 容器 | 底层实现 | 键是否有序 | 键能否重复 | 键能否修改 | 映射值能否修改 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|---|
std::map | 红黑树 | 有序 | 否 | 否 | 是 | ||
std::multimap | 红黑树 | 有序 | 是 | 否 | 是 | ||
std::unordered_map | 哈希表 | 无序 | 否 | 否 | 是 | 平均 | 平均 |
红黑树是一种平衡二叉搜索树。为了维持内部顺序,
set中的元素以及map中的键不能直接修改;需要先删除,再插入新值。
二、有效的字母异位词
- 题目:242. 有效的字母异位词
- 暴力解法:使用两层
for循环逐一匹配字符,时间复杂度为 。 - 数组哈希:题目限定字符为小写英文字母,因此可以使用长度为 26 的
record数组记录各字母出现的次数。- 遍历字符串
s,对record[s[i] - 'a']执行加一操作。 - 遍历字符串
t,对record[t[i] - 'a']执行减一操作。 - 如果最终所有元素均为 0,则两个字符串互为字母异位词。
- 遍历字符串
- 时间复杂度:
- 空间复杂度:,因为数组长度固定为 26
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (char ch : s) {
// 只需计算字符相对 'a' 的偏移量
record[ch - 'a']++;
}
for (char ch : t) {
record[ch - 'a']--;
}
for (int count : record) {
if (count != 0) {
return false;
}
}
return true;
}
};class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (char ch : s) {
// 只需计算字符相对 'a' 的偏移量
record[ch - 'a']++;
}
for (char ch : t) {
record[ch - 'a']--;
}
for (int count : record) {
if (count != 0) {
return false;
}
}
return true;
}
};三、两个数组的交集
- 题目:349. 两个数组的交集
- 选择哈希结构:当数值范围较小时,可以使用数组作为哈希表;当数值范围不固定时,使用
unordered_set更合适。 - 解题思路:
- 使用
numsSet保存nums1中出现过的元素。 - 遍历
nums2,将同时出现在numsSet中的元素加入resultSet。 - 使用集合自动去重,最后转换为
vector返回。
- 使用
- 平均时间复杂度:
- 空间复杂度:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> numsSet(nums1.begin(), nums1.end());
unordered_set<int> resultSet;
for (int num : nums2) {
if (numsSet.find(num) != numsSet.end()) {
resultSet.insert(num);
}
}
return vector<int>(resultSet.begin(), resultSet.end());
}
};class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> numsSet(nums1.begin(), nums1.end());
unordered_set<int> resultSet;
for (int num : nums2) {
if (numsSet.find(num) != numsSet.end()) {
resultSet.insert(num);
}
}
return vector<int>(resultSet.begin(), resultSet.end());
}
};unordered_set 可以通过迭代器区间直接初始化:
unordered_set<int> numsSet(nums.begin(), nums.end());unordered_set<int> numsSet(nums.begin(), nums.end());2026-08-17(周一)
哈希表(二)
四、快乐数
- 题目:202. 快乐数
- 核心问题:计算各位数字的平方和时,结果可能进入循环。
- 集合判重:使用
unordered_set保存已经出现过的平方和。- 如果平方和变为 1,则原数是快乐数。
- 如果某个平方和再次出现,则计算进入无限循环,原数不是快乐数。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
// 计算各位数字的平方和
int getSum(int n) {
int sum = 0;
while (n != 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> seen;
while (true) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 平方和再次出现,说明已经进入循环
if (seen.find(sum) != seen.end()) {
return false;
}
seen.insert(sum);
n = sum;
}
}
};class Solution {
public:
// 计算各位数字的平方和
int getSum(int n) {
int sum = 0;
while (n != 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> seen;
while (true) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 平方和再次出现,说明已经进入循环
if (seen.find(sum) != seen.end()) {
return false;
}
seen.insert(sum);
n = sum;
}
}
};五、两数之和
- 题目:1. 两数之和
- 为什么不使用数组:数值范围可能很大而元素数量较少,直接建立数组会浪费内存。
- 为什么不使用
set:不仅需要判断目标元素是否存在,还需要保存该元素对应的下标。 - 映射哈希:使用
unordered_map存储“元素值 → 下标”。遍历当前元素nums[i]时,在哈希表中查找补数target - nums[i]。 - 平均时间复杂度:
- 空间复杂度:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> numToIndex;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
auto iter = numToIndex.find(complement);
if (iter != numToIndex.end()) {
return {iter->second, i};
}
// 未找到匹配项,记录当前元素及其下标
numToIndex.emplace(nums[i], i);
}
return {};
}
};class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> numToIndex;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
auto iter = numToIndex.find(complement);
if (iter != numToIndex.end()) {
return {iter->second, i};
}
// 未找到匹配项,记录当前元素及其下标
numToIndex.emplace(nums[i], i);
}
return {};
}
};六、四数相加 II
- 题目:454. 四数相加 II
- 分组哈希:将四个数组分为两组,把四层枚举降为两次两层枚举。
- 解题步骤:
- 使用
unordered_map统计所有a + b出现的次数,其中键为两数之和,值为出现次数。 - 遍历数组 C 与 D,计算
c + d。 - 如果
-(c + d)存在于哈希表中,将其出现次数累加到结果。
- 使用
- 平均时间复杂度:
- 空间复杂度:
class Solution {
public:
int fourSumCount(
vector<int>& A,
vector<int>& B,
vector<int>& C,
vector<int>& D
) {
// key:a + b;value:该和出现的次数
unordered_map<int, int> sumCount;
for (int a : A) {
for (int b : B) {
sumCount[a + b]++;
}
}
int count = 0;
for (int c : C) {
for (int d : D) {
auto iter = sumCount.find(-(c + d));
if (iter != sumCount.end()) {
count += iter->second;
}
}
}
return count;
}
};class Solution {
public:
int fourSumCount(
vector<int>& A,
vector<int>& B,
vector<int>& C,
vector<int>& D
) {
// key:a + b;value:该和出现的次数
unordered_map<int, int> sumCount;
for (int a : A) {
for (int b : B) {
sumCount[a + b]++;
}
}
int count = 0;
for (int c : C) {
for (int d : D) {
auto iter = sumCount.find(-(c + d));
if (iter != sumCount.end()) {
count += iter->second;
}
}
}
return count;
}
};2026-08-18(周二)
哈希表(三)
七、赎金信
- 题目:383. 赎金信
- 暴力解法:使用两层
for循环逐一匹配字符,时间复杂度为 。 - 数组哈希:使用长度为 26 的数组记录
magazine中每个字母的出现次数,再遍历ransomNote逐个消耗。- 如果
ransomNote比magazine更长,可以直接返回false。 - 如果某个字符的剩余次数小于 0,说明
magazine无法提供足够的字符。
- 如果
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if (ransomNote.size() > magazine.size()) {
return false;
}
int record[26] = {0};
for (char ch : magazine) {
record[ch - 'a']++;
}
for (char ch : ransomNote) {
record[ch - 'a']--;
if (record[ch - 'a'] < 0) {
return false;
}
}
return true;
}
};class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if (ransomNote.size() > magazine.size()) {
return false;
}
int record[26] = {0};
for (char ch : magazine) {
record[ch - 'a']++;
}
for (char ch : ransomNote) {
record[ch - 'a']--;
if (record[ch - 'a'] < 0) {
return false;
}
}
return true;
}
};八、三数之和
- 题目:15. 三数之和
- 参考:三数之和(代码随想录)
- 排序与双指针:
- 将数组排序。
- 使用一层
for循环固定a = nums[i]。 - 令
left = i + 1、right = nums.size() - 1,分别表示b与c。 - 当三数之和大于 0 时左移
right;小于 0 时右移left;等于 0 时记录答案并跳过重复元素。
- 去重原则:
- 对
a去重时,应与前一个元素比较;如果与后一个元素比较,会漏掉[-1, -1, 2]。 - 对
b与c去重应在找到有效三元组后进行,否则可能漏掉[0, 0, 0]。
- 对
- 时间复杂度:
- 额外空间复杂度:,不计返回结果,主要来自排序
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// 最小元素已经大于 0,不可能再得到和为 0 的三元组
if (nums[i] > 0) {
break;
}
// 对固定元素 a 去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.push_back({nums[i], nums[left], nums[right]});
// 对 b 与 c 去重
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
return result;
}
};class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// 最小元素已经大于 0,不可能再得到和为 0 的三元组
if (nums[i] > 0) {
break;
}
// 对固定元素 a 去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.push_back({nums[i], nums[left], nums[right]});
// 对 b 与 c 去重
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
return result;
}
};这道题的代码较长,重点记住“排序—固定一个数—双指针收缩—命中后去重”这一固定框架。
九、四数之和
- 题目:18. 四数之和
- 核心思路:三数之和使用一层循环固定一个数;四数之和使用两层循环固定两个数,再在剩余区间内使用双指针。
- 固定
nums[k]与nums[i] - 令
left = i + 1、right = nums.size() - 1 - 寻找满足
nums[k] + nums[i] + nums[left] + nums[right] == target的四元组
- 固定
- 通用扩展:五数之和、六数之和等问题,也可以继续增加固定元素的循环层数。
- 注意溢出:计算四数之和时使用
long long。 - 时间复杂度:
- 额外空间复杂度:,不计返回结果,主要来自排序
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
// 一级剪枝
if (nums[k] > target && nums[k] >= 0) {
break;
}
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); i++) {
// 二级剪枝
long long fixedSum = static_cast<long long>(nums[k]) + nums[i];
if (fixedSum > target && fixedSum >= 0) {
break;
}
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
long long sum = static_cast<long long>(nums[k])
+ nums[i]
+ nums[left]
+ nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.push_back({
nums[k], nums[i], nums[left], nums[right]
});
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return result;
}
};class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
// 一级剪枝
if (nums[k] > target && nums[k] >= 0) {
break;
}
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); i++) {
// 二级剪枝
long long fixedSum = static_cast<long long>(nums[k]) + nums[i];
if (fixedSum > target && fixedSum >= 0) {
break;
}
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
long long sum = static_cast<long long>(nums[k])
+ nums[i]
+ nums[left]
+ nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.push_back({
nums[k], nums[i], nums[left], nums[right]
});
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return result;
}
};十、哈希表总结
核心作用
- 快速判断元素是否出现
- 统计元素出现频率
- 建立元素与下标、计数等信息之间的映射
常用结构选择
- 数组:数值或字符范围较小且固定,例如字母异位词、赎金信
set/unordered_set:只需要判断元素是否存在或进行去重,例如两个数组的交集、快乐数map/unordered_map:需要保存<key, value>映射,例如元素到下标、两数之和到出现次数
相关解题模式
- 当哈希法的去重逻辑较复杂时,可以考虑“排序 + 双指针”,例如三数之和与四数之和。
2026-08-19(周三)
字符串(一)
一、反转字符串
- 题目:344. 反转字符串
- 双指针法:分别在字符串首尾设置
left和right,交换两个指针指向的字符,然后同时向中间移动,直到二者相遇。 - 时间复杂度:
- 空间复杂度:
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = static_cast<int>(s.size()) - 1;
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
};class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = static_cast<int>(s.size()) - 1;
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
};二、反转字符串 II
- 题目:541. 反转字符串 II
- 分段处理:每次令
i += 2 * k,处理当前 个字符中的前 个字符。- 剩余字符不少于 个:反转前 个字符。
- 剩余字符少于 个:反转全部剩余字符。
- 时间复杂度:
- 空间复杂度:
class Solution {
public:
void reverseRange(string& s, int start, int end) {
while (start < end) {
swap(s[start], s[end]);
start++;
end--;
}
}
string reverseStr(string s, int k) {
int n = s.size();
for (int i = 0; i < n; i += 2 * k) {
// 取前 k 个字符;不足 k 个时,反转全部剩余字符
int end = min(i + k - 1, n - 1);
reverseRange(s, i, end);
}
return s;
}
};class Solution {
public:
void reverseRange(string& s, int start, int end) {
while (start < end) {
swap(s[start], s[end]);
start++;
end--;
}
}
string reverseStr(string s, int k) {
int n = s.size();
for (int i = 0; i < n; i += 2 * k) {
// 取前 k 个字符;不足 k 个时,反转全部剩余字符
int end = min(i + k - 1, n - 1);
reverseRange(s, i, end);
}
return s;
}
};三、替换数字
- 题目:54. 替换数字
- 核心思路:先统计数字字符的数量,将字符串扩充到替换后的长度,再使用双指针从后向前填充。
oldIndex指向原字符串末尾。newIndex指向扩容后字符串末尾。- 普通字符直接复制;数字字符倒序写入
"number"。
- 为什么从后向前填充:
- 可以直接在原字符串上操作,无需额外申请一个结果字符串。
- 避免从前向后插入字符时,反复移动后续所有元素。
- 时间复杂度:
- 额外空间复杂度:,不计字符串扩容后的必要空间
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
while (cin >> s) {
int digitCount = 0;
for (char ch : s) {
if (ch >= '0' && ch <= '9') {
digitCount++;
}
}
int oldIndex = static_cast<int>(s.size()) - 1;
// 一个数字替换为 6 个字符,字符串长度因此额外增加 5
s.resize(s.size() + digitCount * 5);
int newIndex = static_cast<int>(s.size()) - 1;
while (oldIndex >= 0) {
if (s[oldIndex] >= '0' && s[oldIndex] <= '9') {
s[newIndex--] = 'r';
s[newIndex--] = 'e';
s[newIndex--] = 'b';
s[newIndex--] = 'm';
s[newIndex--] = 'u';
s[newIndex--] = 'n';
} else {
s[newIndex--] = s[oldIndex];
}
oldIndex--;
}
cout << s << endl;
}
return 0;
}#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
while (cin >> s) {
int digitCount = 0;
for (char ch : s) {
if (ch >= '0' && ch <= '9') {
digitCount++;
}
}
int oldIndex = static_cast<int>(s.size()) - 1;
// 一个数字替换为 6 个字符,字符串长度因此额外增加 5
s.resize(s.size() + digitCount * 5);
int newIndex = static_cast<int>(s.size()) - 1;
while (oldIndex >= 0) {
if (s[oldIndex] >= '0' && s[oldIndex] <= '9') {
s[newIndex--] = 'r';
s[newIndex--] = 'e';
s[newIndex--] = 'b';
s[newIndex--] = 'm';
s[newIndex--] = 'u';
s[newIndex--] = 'n';
} else {
s[newIndex--] = s[oldIndex];
}
oldIndex--;
}
cout << s << endl;
}
return 0;
}
