Panchant's Blog!

Home
Blog
  • Archive
  • Cats.
  • Tags
Mine
  • Friends
About
Search
0

保研随笔录

Published on 7/31/2026
Updated on 8/19/2026
Thoughts
Estimated reading 45.01 minutes
28133 words

保研随笔录

上一届的夏令营已经陆续结束。一年后的我,又会是怎样的状态?是拿到心仪的 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预推免 / 九推⬜ 待办

知识索引

  • 资源与规划
    • 算法、刷题与保研经验资源
  • 复杂度与计算机基础
    • 时间复杂度
    • 程序为什么会超时
    • 空间复杂度与递归分析
    • 代码的内存消耗
  • 数组
    • 基础:数组理论
    • 查找:二分查找
    • 双指针:移除元素、有序数组的平方
    • 滑动窗口:长度最小的子数组
    • 模拟:螺旋矩阵 II
    • 前缀和:区间和、开发商购买土地
    • 总结:数组总结
  • 链表
    • 基础:链表理论基础
    • 删除:移除链表元素
    • 实现:设计链表
    • 反转:反转链表
    • 双指针:删除链表的倒数第 N 个节点、链表相交
    • 环检测:环形链表 II
    • 总结:链表总结
  • 哈希表
    • 基础:哈希表理论基础
    • 数组哈希:有效的字母异位词、赎金信
    • 集合:两个数组的交集
    • 判重:快乐数
    • 映射:两数之和、四数相加 II
    • 双指针:三数之和、四数之和
    • 总结:哈希表总结
  • 字符串
    • 双指针:反转字符串
    • 分段反转:反转字符串 II
    • 原地扩容:替换数字

学习历程(持续更新)

2026-07-31(周五)

  • 搜集算法学习与刷题网站:代码随想录、LeetCode 热题 100
  • 信息与经验渠道:牛客(实习、面经、保研经验等)

2026-08-01(周六)

时间复杂度

  • 理解大 O 表示法
  • 了解不同数据规模对算法复杂度的要求
  • 掌握复杂表达式的化简方法
  • 理解 O(log n) 中对数底数的影响
  • 练习分析时间复杂度:从 nnn 个字符串中找出相同的两个字符串(假设仅有两个字符串相同)

程序为什么会超时

  • 从硬件配置出发,大致了解 CPU 的执行速度
  • 1 GHz=1000 MHz1\ \mathrm{GHz} = 1000\ \mathrm{MHz}1 GHz=1000 MHz
  • 1 MHz=106 Hz=1001\ \mathrm{MHz} = 10^6\ \mathrm{Hz} = 1001 MHz=106 Hz=100 万赫兹
  • 任何开发计算机程序的软件工程师都应该能够估计,这个程序的运行时间是一秒钟还是一年。

2026-08-04(周二)

空间复杂度

  • 空间复杂度分析
  • 递归算法的时间与空间复杂度分析:
    • 斐波那契数列:对比 fibonacci(i - 1) + fibonacci(i - 2) 与 fibonacci(second, first + second, n - 1) 两种递归写法
    • 二分法(递归实现)的性能分析
    • 递归算法的空间复杂度 ≈ 单层调用所需空间 × 最大递归深度
    • 递归算法的时间复杂度 ≈ 递归调用总次数 × 单次调用的非递归工作量

代码的内存消耗

  • 固定部分:代码区、数据区
  • 可变部分:栈区(自动分配与回收)、堆区(可通过 new 动态分配,使用完成后应正确释放)
  • 内存泄漏:动态分配的内存在不再使用后未被释放
  • 指针大小与寻址范围:
    • 典型 32 位平台:指针大小为 4 Byte,理论寻址空间为 2322^{32}232 Byte,即 4 GB
    • 典型 64 位平台:指针大小为 8 Byte,理论寻址空间为 2642^{64}264 Byte,实际可用范围受硬件和操作系统限制
  • 内存对齐:
    1. 平台原因:并非所有硬件平台都能访问任意内存地址上的任意数据。某些平台只能在特定地址处读取特定类型的数据,否则会抛出硬件异常。为了让同一程序能够在多个平台上运行,需要进行内存对齐。
    2. 硬件原因:经过内存对齐后,CPU 访问内存的速度会显著提升。
  • 阶段进度:基础知识学习至此结束。

2026-08-08(周六)

最近总是偷懒 😠,要克服这种心理。

数组(一)

一、数组理论
  • 数组是存放在连续内存空间上的相同类型数据的集合。
  • C++ 中的二维数组在地址空间上是连续的。
cpp
int array[2][3] = {
    {0, 1, 2},
    {3, 4, 5}
};
int array[2][3] = {
    {0, 1, 2},
    {3, 4, 5}
};

数组地址:

text
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
二、二分查找

区间通常有两种定义:左闭右闭 [left, right],或左闭右开 [left, right)。

  • 版本一:左闭右闭 [left, right]
cpp
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)
cpp
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 循环。第一层遍历数组元素;发现需要移除的元素后,第二层将后续元素整体向前移动一位。
    • 时间复杂度:O(n2)O(n^2)O(n2)
    • 空间复杂度:O(1)O(1)O(1)
  • 双指针法(快慢指针法):通过快、慢指针,在一层 for 循环中完成两层循环的工作。
    • 快指针:寻找新数组的元素,即不等于目标值的元素
    • 慢指针:指向新数组中待更新的下标
    • 时间复杂度:O(n)O(n)O(n)
    • 空间复杂度:O(1)O(1)O(1)
cpp
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. 有序数组的平方
  • 暴力排序:将每个数平方后排序,时间复杂度为 O(nlog⁡n)O(n \log n)O(nlogn)。
  • 双指针法:原数组有序,平方后的最大值只可能出现在数组两端。新建与原数组等长的 result,令 k 指向其末尾:
    • 若 A[i] * A[i] < A[j] * A[j],则执行 result[k--] = A[j] * A[j]
    • 否则,执行 result[k--] = A[i] * A[i]
    • 时间复杂度:O(n)O(n)O(n)
    • 空间复杂度:O(n)O(n)O(n)
cpp
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 循环,不断寻找符合条件的子数组,时间复杂度为 O(n2)O(n^2)O(n2)。
  • 滑动窗口:不断调整子数组的起始位置和终止位置,从而得到满足条件的最短子数组。
    • 时间复杂度:O(n)O(n)O(n)
    • 空间复杂度:O(1)O(1)O(1)
cpp
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
  • 核心原则:坚持循环不变量,四条边均采用左闭右开的处理方式。
cpp
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]
    • 预处理时间复杂度:O(n)O(n)O(n)
    • 单次查询时间复杂度:O(1)O(1)O(1)
    • 空间复杂度:O(n)O(n)O(n)
cpp
#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 循环分别累加分割线两侧的土地价值。
  • 前缀和思路:
    1. 统计矩阵的总价值。
    2. 分别计算每行与每列的价值之和。
    3. 累加分割线一侧的价值,并用 abs(sum - 2 * cut) 计算两侧价值之差。
    4. 分别枚举横向和纵向分割线,取最小差值。
  • 时间复杂度:O(nm)O(nm)O(nm)
  • 空间复杂度:O(nm+n+m)O(nm+n+m)O(nm+n+m)
cpp
#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 循环中完成两层循环的工作。
  • 滑动窗口:根据当前子数组的状态不断调整起始位置,将部分 O(n2)O(n^2)O(n2) 的暴力解法优化至 O(n)O(n)O(n)。
  • 模拟:坚持循环不变量,统一每一轮的处理规则。
  • 前缀和:预处理累计和,使用 prefix[b] - prefix[a - 1] 快速计算区间和。

知识图谱

数组知识图谱

↑ 返回知识索引

2026-08-12(周三)

链表(一)

一、链表理论基础

定义

链表是一种通过指针串联起来的线性结构。每个节点由两部分组成:

  • 数据域:存储节点中的数据
  • 指针域:存储指向下一个节点的指针;最后一个节点的指针域指向 nullptr

常见类型

  • 单链表:每个节点的指针域只能指向下一个节点
  • 双链表:每个节点有两个指针域,分别指向前一个节点和后一个节点
  • 循环链表:链表首尾相连,尾节点指向头节点

存储方式

数组元素在内存中连续分布,而链表节点通常不连续分布。链表通过节点指针,将分散在内存中的节点连接起来。

cpp
// 单链表节点
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

数组与链表的性能对比

数据结构插入 / 删除按下标查询长度与适用场景
数组O(n)O(n)O(n)O(1)O(1)O(1)长度通常预先确定,适合频繁查询
链表已知目标位置时为 O(1)O(1)O(1)O(n)O(n)O(n)长度可动态变化,适合频繁增删、较少查询
二、移除链表元素
  • 题目:203. 移除链表元素
  • 直接操作原链表:头节点没有前驱节点,因此需要分别处理头节点和非头节点。
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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;
    }
};

虚拟头节点

设置虚拟头节点后,原头节点和其他节点可以使用同一套删除逻辑,避免单独处理头节点。

cpp
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
三、设计链表
  • 题目:707. 设计链表
  • 核心设计:使用虚拟头节点统一链表的插入与删除操作,并用 _size 记录有效节点数量。
cpp
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 保存下一个节点。
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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 个节点
  • 快慢指针法:
    1. 创建虚拟头节点,使删除头节点与删除其他节点使用同一套逻辑。
    2. fast 先从虚拟头节点出发移动 n+1n+1n+1 步,使 fast 与 slow 之间保持 n+1n+1n+1 步的间隔。
    3. 同时移动两个指针,直到 fast 指向 nullptr;此时 slow 恰好指向待删除节点的前一个节点。
    4. 调整指针并释放待删除节点。
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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. 链表相交
  • 长度对齐法:
    1. 分别计算链表 A 与链表 B 的长度。
    2. 让较长链表的指针先移动长度差,使两个指针到链表末尾的距离相同。
    3. 同时向后移动两个指针;首次指向同一节点时,该节点就是相交节点。
  • 时间复杂度:O(n+m)O(n+m)O(n+m)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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
  • 需要解决两个问题:
    1. 判断链表中是否存在环。
    2. 如果存在环,找到环的入口节点。

判断是否存在环

使用快慢指针:fast 每次移动两个节点,slow 每次移动一个节点。如果两个指针在途中相遇,说明链表中存在环;如果 fast 或 fast->next 指向 nullptr,说明链表中不存在环。

寻找环的入口

假设:

  • 从头节点到环入口的距离为 xxx
  • 从环入口到快慢指针相遇点的距离为 yyy
  • 从相遇点回到环入口的距离为 zzz
  • fast 在环内绕了 nnn 圈

相遇时,slow 与 fast 经过的距离分别为:

dslow=x+yd_{\text{slow}}=x+ydslow​=x+y
dfast=x+y+n(y+z)d_{\text{fast}}=x+y+n(y+z)dfast​=x+y+n(y+z)

由于 fast 的速度是 slow 的两倍:

2(x+y)=x+y+n(y+z)2(x+y)=x+y+n(y+z)2(x+y)=x+y+n(y+z)

整理可得:

x=(n−1)(y+z)+zx=(n-1)(y+z)+zx=(n−1)(y+z)+z

因此,分别从头节点与快慢指针相遇点出发两个指针,并让它们每次各移动一个节点;二者再次相遇的位置就是环的入口。

  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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红黑树有序否否O(log⁡n)O(\log n)O(logn)O(log⁡n)O(\log n)O(logn)
std::multiset红黑树有序是否O(log⁡n)O(\log n)O(logn)O(log⁡n)O(\log n)O(logn)
std::unordered_set哈希表无序否否平均 O(1)O(1)O(1)平均 O(1)O(1)O(1)

C++ 映射容器对比

容器底层实现键是否有序键能否重复键能否修改映射值能否修改查询效率增删效率
std::map红黑树有序否否是O(log⁡n)O(\log n)O(logn)O(log⁡n)O(\log n)O(logn)
std::multimap红黑树有序是否是O(log⁡n)O(\log n)O(logn)O(log⁡n)O(\log n)O(logn)
std::unordered_map哈希表无序否否是平均 O(1)O(1)O(1)平均 O(1)O(1)O(1)

红黑树是一种平衡二叉搜索树。为了维持内部顺序,set 中的元素以及 map 中的键不能直接修改;需要先删除,再插入新值。

二、有效的字母异位词
  • 题目:242. 有效的字母异位词
  • 暴力解法:使用两层 for 循环逐一匹配字符,时间复杂度为 O(n2)O(n^2)O(n2)。
  • 数组哈希:题目限定字符为小写英文字母,因此可以使用长度为 26 的 record 数组记录各字母出现的次数。
    • 遍历字符串 s,对 record[s[i] - 'a'] 执行加一操作。
    • 遍历字符串 t,对 record[t[i] - 'a'] 执行减一操作。
    • 如果最终所有元素均为 0,则两个字符串互为字母异位词。
  • 时间复杂度:O(n+m)O(n+m)O(n+m)
  • 空间复杂度:O(1)O(1)O(1),因为数组长度固定为 26
cpp
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 更合适。
  • 解题思路:
    1. 使用 numsSet 保存 nums1 中出现过的元素。
    2. 遍历 nums2,将同时出现在 numsSet 中的元素加入 resultSet。
    3. 使用集合自动去重,最后转换为 vector 返回。
  • 平均时间复杂度:O(n+m)O(n+m)O(n+m)
  • 空间复杂度:O(n)O(n)O(n)
cpp
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 可以通过迭代器区间直接初始化:

cpp
unordered_set<int> numsSet(nums.begin(), nums.end());
unordered_set<int> numsSet(nums.begin(), nums.end());

↑ 返回知识索引

2026-08-17(周一)

哈希表(二)

四、快乐数
  • 题目:202. 快乐数
  • 核心问题:计算各位数字的平方和时,结果可能进入循环。
  • 集合判重:使用 unordered_set 保存已经出现过的平方和。
    • 如果平方和变为 1,则原数是快乐数。
    • 如果某个平方和再次出现,则计算进入无限循环,原数不是快乐数。
  • 时间复杂度:O(log⁡n)O(\log n)O(logn)
  • 空间复杂度:O(log⁡n)O(\log n)O(logn)
cpp
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]。
  • 平均时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)
cpp
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
  • 分组哈希:将四个数组分为两组,把四层枚举降为两次两层枚举。
  • 解题步骤:
    1. 使用 unordered_map 统计所有 a + b 出现的次数,其中键为两数之和,值为出现次数。
    2. 遍历数组 C 与 D,计算 c + d。
    3. 如果 -(c + d) 存在于哈希表中,将其出现次数累加到结果。
  • 平均时间复杂度:O(n2)O(n^2)O(n2)
  • 空间复杂度:O(n2)O(n^2)O(n2)
cpp
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 循环逐一匹配字符,时间复杂度为 O(n2)O(n^2)O(n2)。
  • 数组哈希:使用长度为 26 的数组记录 magazine 中每个字母的出现次数,再遍历 ransomNote 逐个消耗。
    • 如果 ransomNote 比 magazine 更长,可以直接返回 false。
    • 如果某个字符的剩余次数小于 0,说明 magazine 无法提供足够的字符。
  • 时间复杂度:O(n+m)O(n+m)O(n+m)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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. 三数之和
  • 参考:三数之和(代码随想录)
  • 排序与双指针:
    1. 将数组排序。
    2. 使用一层 for 循环固定 a = nums[i]。
    3. 令 left = i + 1、right = nums.size() - 1,分别表示 b 与 c。
    4. 当三数之和大于 0 时左移 right;小于 0 时右移 left;等于 0 时记录答案并跳过重复元素。
  • 去重原则:
    • 对 a 去重时,应与前一个元素比较;如果与后一个元素比较,会漏掉 [-1, -1, 2]。
    • 对 b 与 c 去重应在找到有效三元组后进行,否则可能漏掉 [0, 0, 0]。
  • 时间复杂度:O(n2)O(n^2)O(n2)
  • 额外空间复杂度:O(log⁡n)O(\log n)O(logn),不计返回结果,主要来自排序
cpp
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。
  • 时间复杂度:O(n3)O(n^3)O(n3)
  • 额外空间复杂度:O(log⁡n)O(\log n)O(logn),不计返回结果,主要来自排序
cpp
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,交换两个指针指向的字符,然后同时向中间移动,直到二者相遇。
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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,处理当前 2k2k2k 个字符中的前 kkk 个字符。
    • 剩余字符不少于 kkk 个:反转前 kkk 个字符。
    • 剩余字符少于 kkk 个:反转全部剩余字符。
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)
cpp
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"。
  • 为什么从后向前填充:
    1. 可以直接在原字符串上操作,无需额外申请一个结果字符串。
    2. 避免从前向后插入字符时,反复移动后续所有元素。
  • 时间复杂度:O(n)O(n)O(n)
  • 额外空间复杂度:O(1)O(1)O(1),不计字符串扩容后的必要空间
cpp
#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;
}

↑ 返回知识索引

Related Post
Comments
author-avatar
Panchant
A student, and a runner.
29
Arts.
12
Cats.
58
Tags
Customed
This is the custom content section
Anything can be placed here
TOC
  1. 保研随笔录
  2. 起点
  3. 不足与应对
  4. 关键节点(时间线)
  5. 知识索引
  6. 学习历程(持续更新)
  7. 2026-07-31(周五)
  8. 2026-08-01(周六)
  9. 2026-08-04(周二)
  10. 2026-08-08(周六)
  11. 2026-08-10(周一)
  12. 2026-08-11(周二)
  13. 2026-08-12(周三)
  14. 2026-08-13(周四)
  15. 2026-08-14(周五)
  16. 2026-08-15(周六)
  17. 2026-08-17(周一)
  18. 2026-08-18(周二)
  19. 2026-08-19(周三)
© 2025-2026 By Panchant
由 Astro v2.8.3 构建 | 主题 HsuBlog
Build with by Panchant
Search
Extended SearchHelloWorld
You can use a unix-like format: Extended Search