使用 Neovim 刷题 LeetCode

预推免

众所周知预推免是有机试的,因此就不得不提前准备一下,否则以我稀烂的算法水平,就算被捞起来那也过于耻辱了。

先来写点别的吧,就是预推免机试本身。因为发的邮件上提到了,原则上我是不能透露机试的题目的,所以我就讲点别的。

先是机试前的准备,记录已经很详细了所以我也不多补充什么了:

2026.9.14

  • 之前还想过系统的事情,要不要用 Linux,本来是决定第一次尝试一下,但已经是开着的 Windows 了,那没辙。哦其实早点进去挺好的,可以早点调配系统,我当时有点脑抽了忘了这回事,于是是等到最后一点才进的。
  • 那当然就是用 VS Code 了,打开后是一个 C 的目录,我就开始默写模板了。不过其实我当时建了一个 template.cpp 想着要的时候再复制,这个没必要,因为我后面直接提前建了题目文件并复制了模板,只是忘记把输入输出一起建了。写的时候还有点忘记了,靠着 LSP 写了 ios::sync_with_stdio(false),然后下一句写了 cin::tie(nullptr),结果 LSP 报错,运行也不行。后面才搞明白应该是 cin.tie(nullptr)
  • 进去的时候本来把 Vim 插件开了,不过很快又关了。因为我好久没配过了,不知道咋搞了,进入 Normal 会习惯 jk,还有一个致命的问题就是剪贴板,想要提交代码的话很麻烦,剪贴板不互通,最后我就放弃了,要换位的时候用鼠标。
  • 当时 Code Runner 插件可以直接跑,但我看命令行有点问题,感觉参数有点太少了点,于是尝试去配置。不过因为太久没弄过 VS Code 的 C++ 配置,我也有点淡忘了,折腾了好一阵子,差点没弄完,最后还有几分钟开始的时候才搞定。
  • 首先是一个写代码的配置吧,我有点忘记了,但这个还好,直接把标准改成了 c++23。因为我看 OJ 系统,居然支持 C++23,说实话非常意外惊喜。但其实我也没怎么用高级语法,本来用了 this 推导,但不知道为何还是本地显示有问题,我就不管了直接传 self 进去。
  • 有点忘记了,不知道是前面搞的还是修复后搞的,总之我尝试加过 ASan 参数,也就是 -fsanitize=address,undefined,结果报错找不到,遂放弃。但其实我后面的代码似乎也没涉及边界的处理,所以也用不到。
  • 然后主要是那个运行,一直失败,然后我这个报错也看不明白,急死我了,四处改改。不想用 Code Runner 是因为我看它的命令行没什么参数,可能不太符合我的要求,例如说我可能会用 C++20 及以上的语法呀什么的,还有其他的如警告提示等,然后我也没怎么折腾过这个的配置,不清楚怎么设置(虽然后面也尝试看了下设置)。所以说就想用默认的编译运行。
  • 不过最后还是发现原因了,原因是用的 gcc 而不是 g++……因为我 VS Code 打开的这个项目本身是 C 的,配置也是 GCC,虽然我一进来就改了,但其实要改好几个地方,我东改改西改改漏了点,最后才发现有一个二进制文件位置用的是 gcc.exe,改成了 g++.exe 就好了,那时候距离开始也就几分钟了,极限,极限。
  • 剩下时间就是尝试 Hello World 了,不过当时有点神经了,发现虽然没报错了,但也没输出,感觉有点诡异。不过很快就发觉了是 freopen 的缘故,感觉有点麻烦就把那块注释了。但开考后又启用了,因为真的好方便,我真的要吹爆了。

已经很久没打开过 VS Code 了,但是真到考试的时候那还是会选择 VS Code 的。

南大的 OJ 感觉体验确实很不错,尤其是听说其他地方有断网写代码,然后用 U 盘拷过去运行的情况,就更觉现代化了(好歹没让手写……)。

不过当时更让我惊讶的是直接就上了 C++23,我本地的 C++ 标准也是从一开始的 C++17,到后面为了用一些方便的写法(也同时是学习新语法),一路升到了 C++23。只是其实还是没怎么用,如 Ranges 等。

提前准备的模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

#ifdef LOCAL
  #define dbg(...) fprintf(stderr, __VA_ARGS__)
#else
  #define dbg(...) ((void)0)
#endif

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
#ifdef LOCAL
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    return 0;
}

第一次用这个 freopen 就是直接在机试上用了,挺好用的。然后就很自然地「三屏」,左边是代码、中间是输出结果、右边是题目,同时还解放了终端可以输出额外的调试信息。

2026.9.14

  • 然后就是开始后不久我就能 get 到 freopen 了,疑似有点太爽了。之前应该也是分屏,一边题目一边代码,不过其实我自己应该是更习惯左边题目右边代码的,平时练习也是这样的,但这次却是左边代码右边题目,可能也是一开始分的时候就是这样的然后也没有调。然后开始后不久我就再领悟到了,把左边的代码本身再左右分区,右侧放 1.out1.in,常驻显示 1.out,因为 1.in 在编写过程、还没正确的情况下只用弄一下。左侧代码弄大一点,点右侧的时候还会自动扩张,点回去就也会变回去。这样真的好爽,不用反复自己输入或粘贴,而且可以把终端解放出来,显示调试信息。当然我也没把所有样例都弄下来本地保存,实际上测试的时候依旧是每个复制展示一下。虽然说 Linux Bash 有学过一点,但 Windows CMD 那就算了吧。

不过 dbg 倒没有很好用,加上我不太记得格式化字符串怎么写了,就更麻烦了。但这也是没有办法的办法了,毕竟不是本地环境。

2026.9.14

  • 在我没搞明白移位的具体情况的时候也有尝试过调试,不过调试我也是标准输入和 dbg 一起用,我记得第一题似乎用标准输出更多,然后就是后面自己注释掉,因为 bitset 嘛,我不太知道格式化该怎么写,就直接用我知道的标准输出了。当然因为我开得比较大,本地上就用宏调小了。不过后面也有问题,因为后面也给了数据很大的样例,本地也在前面通过后调大来检验了,最后就是完全一致。
  • 当然其实也可能可以用 C++23 的 print,像是 OJ 上的示例程序就是用这个的。但因为我也不太熟就不敢第一次去尝试,而且也不知道跟前面的关闭同步有没有冲突。***

就浅浅提一下作为一个引子吧。

JetBrains CLion 时期

野望与选择

其实我的算法练习可以追溯到去年这个时候。说早吧挺早,说晚吧挺晚。

说晚是因为入学的时候我没练算法,上课的时候我没练算法,考试的时候我没练算法,结果等到《数据结构与算法》课都上完挺久了,才做了我的 LeetCode 第一题。

挺早[1]则是相对当前的时间节点,以及我最初的「预想」算比较早的了。我那会也是因为有了一些紧迫感,被焦虑推着去计划了下算法练习。

2025.9.5

  • 下午 *** 聊了下他们那保研的事情,***
    • 于是乎一下午查了一下保研有关的资料,现在大致算是知道怎么运作了。
    • 然后就感慨绩点没啥用。***
    • 但是重中之重其实是后面的内容,即使推免了,后面还有笔试、机试、面试什么的。
    • ***,机试和面试可以说是直戳我命门。机试我太紧张和犯低级错误了,练得也比较少,因为我不太喜欢算法吧。面试就更不用说了,哥们我还没面试过呢,更别说其中可能的英文了。
    • 唉,行路难。我觉得这学期还是练一下算法吧,毕竟写代码真的太少了,项目不做,总得写点代码。

以及当时退课的期望:

2025.9.7

  • 退掉了,希望以此换来的是这学期能练一下算法。大概算了一下学分应该问题不大,实在不行大四上学期应该也能补完。

写到这想起来个有意思的事情,截至目前,大学前三年我一共修了 149.5 学分,算上大四一年的两次形策课就正好凑齐 150 学分了。不过其实还有毕业设计的 6 学分会打破这个完美的数字。

然后 2025.9.10 我就基本是完成了我的 LeetCode 第一题(各种意义上)。

当时练习 LeetCode 一共有两个选择,一个是 VS Code,一个是 JetBrains CLion。从子标题上看可以知道我当时选择的是后者,不过如果是现在的我穿越回去的话也许更倾向前者。

我当时的想法大约就是,这两个机房都会提供(不过现在我其实并不确信了,当时会这样想也是受到了自己所处四周环境的影响),CLion 作为一个重型 IDE,功能应该还是比(没有定制化配置过的)VS Code 要全面与强大的。此外还有一个在当时比较重要的原因就是,我认为 CLion 的 Vim 插件比 VS Code 的要好用一点。

现在看来其实还是比较荒谬的理由。前者,VS Code 在 LSP 的帮助下其实也差不了多少,虽然没有 Clangd 插件,因此体验上还有些许差距;而后者,再怎么配,终究也是残次品,尤其是它不会默认把剪贴板配好,导致如果想要获得一个比较好的体验的话,那就得记一下配置,然后在机房里慌慌张张地配置。要是偶然忘了,那就得提交的时候记事本打开来复制一下了。这些都是亲身经历,所言非虚。

当然 CLion 也有它的调试器?虽然我有点忘了什么样子了,但 VS Code 根据使用的记忆差距大概也并不算大。加上根据我自己练习的经历,用调试器还是太少了,都是靠其他方式(打印)进行调试的。正好前几天看到 Rust debugging survey 2026 results | Rust Blog,使用频率上打印调试是毋庸置疑的第一。

反而是 CLion 的内存占用对我的轻薄本来说,确实是一大不可忽视的负担。

由于我已经卸载了 CLion,没法第一手地拿到相关配置,因此只能通过一些旧纪录进行回顾。

出发

我当时使用的应该是 leetcode-editor Plugin for JetBrains IDEs | JetBrains Marketplace 这个插件,因为选择了 CLion,也是在 Windows 上进行练习,毕竟一个 JetBrains 已经是内存巨兽了,再活跃个 WSL 也吃不消。

最初的配置大概就是 CLion 的默认配置吧,具体可见初始化的提交 25b9edd,大概就是配了 .clang-format, .clang-tidy 等,使用 CMake 管理(尽管我不会写 CMakeLists.txt),最初的版本也是 C++17,以及忽略了爬取的题目信息(即 doc)。

最初还准备了一个 utils.h,当时大概就有了优化本地调试体验的想法。最初只有一个函数 printVector,用于调试 vector

1
2
3
4
5
6
7
8
template<typename T>
void printVector(const vector<T>& vec) {
    cout << "[";
    for (int i = 0; i < vec.size(); ++i) {
        cout << vec[i] << (i == vec.size() - 1 ? "" : ", ");
    }
    cout << "]" << endl;
}

根据第一题的样子,大致也可以了解到我当时写题目的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 2025-09-10 17:49:33

#include <bits/stdc++.h>

using namespace std;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> table;
        for (int i = 0; i < nums.size(); i++) {
            auto result = table.find(target - nums[i]);
            if (result != table.end()) {
                return {result->second, i};
            }
            table[nums[i]] = i;
        }
        return {};
    }
};
//leetcode submit region end(Prohibit modification and deletion)

#include "../../../utils.h"

int main(void) {
    Solution s;
    vector<int> t1 = {1, 2};
    s.twoSum(t1, 3);
    return 0;
}

上面的区域称为 Import 区。最上面标记了时间,然后使用了万能头,并将 std 命名空间内容直接引入。

最下面的区域就是 Main 区,因为 LeetCode 风格的练习与 ACM 风格不一样,ACM 风格要求自行处理输入输出,而 LeetCode 风格的题目(大多)输入输出格式以函数签名的形式给出,这免去了自行处理输入输出的琐碎事。当然,自然也就缺乏了处理输入输出的学习与经验。

因此 LeetCode 风格的题解基本上是以 Solution 类的格式表示,若需要本地运行调试,就需要自己准备一个 main 函数了。

总之,9.10 开始,就开始了正式的,我的第一小段机试练习。

毕竟还是要上课,所以最初的计划也就是一周抽个几天的早上练一下得了。

2025.9.12

  • 昨天的时候大致想过,算法安排是跟 *** 一致,周三、五、七,***。暂时是这样想的,这两天整个晚上做项目,是自己的项目,例如说 Focust 之类的。

写着写着就会遇到新的情况,于是就为工具函数添砖 Java,如加入了 printMatrix 打印矩阵

后面我又觉得可以继续进行改进,记得大约是与 DS 进行了交流。STL 容器中除了 vector 外还有一些也可以加上打印的支持。此外用 printVector 什么的还是太麻烦了,遂直接实现了 << 操作符。

同时像上面的例子,我要测试的时候不得不 vector<int> t1 = {1, 2} 这样,写了几次后就倦了,能否直接写在调用里面呢?在上面也进行了支持。当然这是十分危险的,因为很多题目签名传入的都是其引用,而直接在调用处构造又没有命名绑定,很容易造成悬垂引用。不过只要约束自己、知道自己在干什么倒也还好了。

这样子后就可以直接 s.twoSum(VEC(1, 2), 3) 了,很是方便。当然也加入了如 VECSTR, MAT 等,用于其他情形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
class LValueVec {
    public:
    std::vector<T> data;
    LValueVec(std::initializer_list<T> il) : data(il) {}
    operator std::vector<T> &() { return data; }
};

template<typename T>
std::ostream &operator<<(std::ostream &os, const LValueVec<T> &lv) {
    os << lv.data;
    return os;
}

//! 用以方便地对非 const 的 vector<T>& 的函数参数进行传递。
//! 这可能会造成危险的悬垂引用,慎用!
//! 不应修改或引用这样传入的临时 vector。
#define VEC(...) LValueVec<int>({__VA_ARGS__})
#define VECSTR(...) LValueVec<std::string>({__VA_ARGS__})
#define MAT(...) LValueVec<std::vector<int>>({__VA_ARGS__})

后面还有 stringvector<char> 之间的转换 S2V, V2S等。

做到链表的时候,反复抄注释里面给的定义我也是烦了。于是后面也将其写入了 utils.h 当中,同时也加入了一些辅助调试的工具函数,如可以用链式的 tree->visualize() 开启可视化打印,直观呈现树的形状。从代码中还能看出来为了应付 Windows 终端编码问题,而在代码中设置为 UTF-8 编码,确保树的形状完美地呈现。

整理与笔记

不过当时练习的习惯其实非常差劲,早些时候还会看看题解,等到后面的时候就是过了得了,也不管是不是最优解、代码写得好不好。因此其实也没学到什么。

2025.9.14

  • 记得整理,有一些解法确实不太好。

于是我打算需要文档记录一些题解和想法,这样才能更好地学习与理解记忆。

2025.9.15

  • 晚上稍微弄了点算法周报的模板,但是未完成,待继续。

我很沉浸于为达最初计划目的地时铺设地砖的过程,往往乐在其中,超过了向目标进发。当然,这个过程基本上也是伴随着拖延,墨迹。

2025.9.21

  • 算法模板极限,晚上很晚才开始认真投入弄,现在就差 Bases 了。

下面是我最终准备的笔记模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<%*
const LeetCodeManager = tp.user.LeetCodeManager;
const AlgorithmProblem = tp.user.LeetCodeAlgorithmProblem;
const problem = await new AlgorithmProblem(tp, LeetCodeManager).create();
%>---
aliases:
  - LeetCode <% problem.formattedId %>
tags:
  - 算法/LeetCode
<%* problem.tags.forEach(tag => { %>
  - 算法/标签/<% tag %>
<%* }) -%>
leetcode_problem_id: <% problem.id %>
leetcode_title_slug: <% problem.title_slug %>
leetcode_title: <% problem.title_cn %>
leetcode_difficulty: <% problem.difficulty %>
leetcode_source: <% problem.url %>
leetcode_creation_time: 
leetcode_finish_time: 
leetcode_time_use: 
leetcode_independent_solution: true # true|false
creation_time: <% problem.creation_time %>
update_time: <% problem.creation_time %>
leetcode_problem_status: 进行中 # 进行中|已完成|待复习
---

# 📝 [LeetCode T<% problem.formattedId %>:<% problem.title_cn %>](<% problem.url %>)

> [!ABSTRACT] 核心摘要
> - **核心问题**:用一句话重新描述题目,萃取出问题的本质。
> - **核心思路**:一句话点明解题用的核心算法模式或数据结构。
> - **关键逻辑/状态转移**:DP 题写状态转移方程,回溯题写决策树模型,双指针写指针移动逻辑等。
> - **关键点/易错点**:边界条件、特殊 case、优化技巧等。
> - **复杂度**:时间和空间复杂度。

> [!INFO]+ 具体题目
> ![[<% problem.description_filename %>]]

模板本身很简单,关键其实在于其内核,也就是 LeetCodeManagerLeetCodeAlgorithmProblem。下面是相应的接口,完整代码就不放了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class LeetCodeManager {
  constructor(tp) {}

  /**
   * 异步加载和解析题目数据库 JSON 文件。
   * 使用缓存机制,只有在数据未加载时才读取文件。
   * @returns {Promise<boolean>} 如果成功加载则返回 true,否则返回 false。
   */
  async #loadProblems() {}

  /**
   * 格式化题号,确保为 4 位,不足则补零。
   * @param {string|number} id - 题目ID.
   * @returns {string} - 例如,1 -> "0001", 123 -> "0123".
   */
  formatProblemId(id) {}

  /**
   * 根据题目 ID 获取题目详细信息。
   * @param {string|number} id - LeetCode 题目的编号。
   * @returns {Promise<object|null>} 返回题目信息对象,如果找不到则返回 null。
   */
  async getProblemById(id) {}

  /**
   * 根据题目信息生成标准的文件名。
   * @param {object} problem - 从 getProblemById 获取的题目对象。
   * @returns {string} - 标准化的文件名,例如 "LeetCode-T0001-TwoSum.md"。
   */
  generateProblemFileName(problem) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LeetCodeAlgorithmProblem {
  constructor(tp, LeetCodeManager) {}

  /**
   * 通过 LeetCode GraphQL API 获取题目的详细描述(中文)。
   * @param {string} titleSlug - 题目的 title_slug,例如 "two-sum"。
   * @returns {Promise<string|null>} 成功则返回 HTML 字符串,否则返回 null。
   */
  async #fetchProblemDescription(titleSlug) {}

  /**
   * 生成描述文件的标准文件名。
   * @param {object} problem - 题目对象。
   * @returns {string} - 描述文件名,例如 "0001-Two-Sum-Desc.md"。
   */
  #generateDescriptionFileName(problem) {}

  /**
   * 创建单个题目复盘笔记的核心函数。
   * @returns {Promise<object|null>} 返回包含题目信息的对象,用于模板填充。
   */
  async create() {}
}

然后创建出来的笔记模板大概就是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
---
aliases:
  - LeetCode 0002
tags:
  - 算法/LeetCode
  - 算法/标签/递归
  - 算法/标签/链表
  - 算法/标签/数学
leetcode_problem_id: 2
leetcode_title_slug: add-two-numbers
leetcode_title: 两数相加
leetcode_difficulty: 🟠 Medium
leetcode_source: https://leetcode.cn/problems/add-two-numbers/
leetcode_creation_time: 2025-09-24 20:12
leetcode_finish_time: 2025-09-24 20:39
leetcode_time_use: 27min
leetcode_independent_solution: true
creation_time: 2025-09-27 16:37
update_time: 2025-09-27 16:37
leetcode_problem_status: 进行中
value:
density:
---

# 📝 [LeetCode T0002:两数相加](https://leetcode.cn/problems/add-two-numbers/)

> [!ABSTRACT] 核心摘要
> - **核心问题**:用一句话重新描述题目,萃取出问题的本质。
> - **核心思路**:一句话点明解题用的核心算法模式或数据结构。
> - **关键逻辑/状态转移**:DP 题写状态转移方程,回溯题写决策树模型,双指针写指针移动逻辑等。
> - **关键点/易错点**:边界条件、特殊 case、优化技巧等。
> - **复杂度**:时间和空间复杂度。

> [!INFO]+ 具体题目
> ![[LeetCode-T0002-AddTwoNumbers-Desc]]

当时大概就是做一道题,然后就去创建一个这样的笔记,并填入诸如时间这样的元信息。

还准备了一个「周报」,下面是相应模板与 Bases 代码,用于统计一周内做的题:

2025.9.24

  • 一个个将之前做的题目建立了复盘文件,累死了。
  • 除了一道题目创建和完成时间不是同一天外,其他的用时都比较精准。因此后面要是没做完、不打算当日继续做、且最多只是完成了数据读取、构造等初级,并未涉及逻辑部分的话,直接到时候删掉重做。要是单纯因为一天做不完,直接写时间 1h+ 就行了。
  • 后面大概就是做一道立刻搞一道,或者是一天完后立刻弄。
  • 弄每周的时候才发现原来 Bases 的 filters 少写了一个 s……
  • 此外还有一个,完成时间字段多了个 HH-mm,结果它就认为比 week_end 大了,超出了范围。解决方法就是先 format 成 YYYY-MM-DD 再 date()
  • 感觉算法题 Bases 有点卡,这才几道题啊……
  • 代码写错了,结果造成了 .md.md 的局面。本来是想取消创建或创建失败时去除该文件的,但看来 Templater 不太可行?硬来的话应该可以通过 tp.app,但我懒得折腾了,尽可能避免取消创建和创建失败吧。我先加个 gitignore。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
---
tags:
  - 算法/每周回顾
leetcode_problems_solved: 347,144,145,94
leetcode_problems_count: 4
leetcode_practice_week: 7
week_start: 2025-10-20
week_end: 2025-10-26
creation_time: 2025-10-26 20:07
update_time: 2025-10-26 20:07
value:
density:
---

# 🚀 算法练习周度复盘 | 第 7 周(2025-10-20 至 2025-10-26)

## 📊 本周概览

![[LeetCode-Problems-Summary.base]]

## 🔍 题目解析

![[LeetCode-T0347-TopKFrequentElements]]

---

![[LeetCode-T0144-BinaryTreePreorderTraversal]]

---

![[LeetCode-T0145-BinaryTreePostorderTraversal]]

---

![[LeetCode-T0094-BinaryTreeInorderTraversal]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
filters:
  and:
    - file.hasTag("算法/LeetCode")
formulas:
  problem_title: link(file.asLink(), leetcode_problem_id + "-" + leetcode_title)
  problem_difficuly: leetcode_difficulty.slice(0, 2)
  is_independent: if(leetcode_independent_solution, "✅", "❌")
  source_link: link(leetcode_source, "原题")
  problem_tags: file.tags.filter(value.startsWith('#算法/标签'))
properties:
  formula.problem_title:
    displayName: 题目
  formula.problem_difficuly:
    displayName: 难度
  formula.problem_tags:
    displayName: 标签
  formula.source_link:
    displayName: 链接
  note.leetcode_time_use:
    displayName: 耗时
  formula.is_independent:
    displayName: 自主
views:
  - type: table
    name: 本周题目回顾
    filters:
      and:
        - date(leetcode_finish_time.format("YYYY-MM-DD")) >= date(this.week_start)
        - date(leetcode_finish_time.format("YYYY-MM-DD")) <= date(this.week_end)
    order:
      - formula.problem_title
      - formula.problem_difficuly
      - leetcode_time_use
      - formula.problem_tags
      - formula.source_link
      - formula.is_independent
    sort:
      - property: leetcode_finish_time
        direction: ASC
    rowHeight: medium
  - type: table
    name: 总题目回顾
    order:
      - formula.problem_title
      - formula.problem_difficuly
      - leetcode_time_use
      - formula.problem_tags
      - formula.source_link
      - formula.is_independent
    sort:
      - property: leetcode_finish_time
        direction: ASC
    rowHeight: medium

显示效果:

笔记之后就是 Anki 了,我也萌生了使用 Anki 辅助记忆的念头。

2025.9.27

  • Anki 也可以用于算法练习记忆,不过应当设置得比较简练才行。
    • 我在想能不能综合一下题解,在题解一部分用一定规则写下比较短的核心总结,然后可以用脚本填充其他字段快速制卡。毕竟题干部分是没有在模板中的,需要额外写程序规划。这可能要开始写点题解后进一步思考。

不过上面的这个思考,最后不断推迟,在今年三月的时候才在「程序」上取消了。

结局

然后可以快进到结局了。上面的笔记,除了最开始的测试用途外,一次也没写过,「光荣」地成为了我的无用功之一。

此外我这一阶段的算法练习也最终停在了 2025.10.22,在那天进一步更新了工具函数后,再也没用到更新后的内容了。

翻了一下那会的记事,结合这个时间节点,毫不意外啊只能说——我去搞 Focust 了。名义上就是暂停一阵子算法练习,实际上那段日子整个生活都暂停了。一直到非常久了后,才最终在名义上也停止了。翻看记录,这个「非常久」是 2025.11.23。

这一阶段的算法练习,除了给我折腾扑通了几下子外,似乎也确实没产生什么实际的意义。毕竟题目也是瞎糊弄的,过了就过了,也没有看题解去理解和改进写法。笔记更是徒有一个模板,没有任何产出。

寒假的时候也久违地想到了算法,不过最终是毫无动静,下面便是唯一的记录了:

2026.2.2

  • 要准备一下日程了,寒假还是练练算法比较好。

类似的故事在大三下学期也有上演。三月份开学的时候也是想到,是时候捡起来算法练习了,那会提升了 Anki 的优先级——也可能是因为代码部分实际上已经比较完善了——在思考着笔记与 Anki 之间的整合,毕竟我一点也不想每道题笔记写一点,然后 Anki 制卡还要再写一点。这期间产出了一些文档,包含规范与一些速查表等,我记得是有两份多篇,但我也一直没来得及看,就放在我的「下载」文件夹,直到今天才删掉。恢复看了两眼,感觉也挺一坨的。

于是又是一晃头,一学期又要过了,临近期末,我又觉得是时候考虑恢复一下算法练习了,差不多是 2026.5.11 那会。结果重新启动的任务待办,被一次又一次取消,最后在过了一个月后的 2026.6.9 后,又一次终止了。这一次,一题都没做。

Neovim 时期

以下内容最新基于 3ea53cb

灵感?

当然,上面的内容都不是本文的正题,尽管已经有了五千多字。毕竟本文的标题是「使用 Neovim 刷题 LeetCode」,Neovim 还没出现呢。

最初的念头是在期末考试复习的最后关头出现的,当时不想复习,灵光乍现有了念头,记录了下来。

2026.6.28

  • 唉,这个软件质量管理是真不想背,中午也没回去午休,继续摆,到现在三点半才来写,写完后继续背吧。
    • 昨天的时候想到,七月的时候可以试试用 Neovim 做 LeetCode,就不必用笨重的 CLion 了。找到了一个 Neovim 插件可以满足,有其他的需求也可以到时候 fork 改改。另外还有一个问题是注意到 gr 映射重复了,按理来说 gr 开了一个组,不过现在立刻触发了。

很快啊,gr 这个在考完试后几天就搞掉了。大概就是 Neovim 0.12 提供了 gr* 的默认 LSP 映射,不过我用 LazyVim 已经有了且习惯了其他的映射(且功能大概更强),于是就删掉了。具体可见 |lsp-defaults|

还有上面的提交还将之前 Vim 的 <leader>o/O 映射——也就是不改变光标位置向下/上插入一个空行——改为了 <leader>oj/k,虽然说到现在还不是很习惯,但也是为了映射的命名空间着想。前者 oO 的命名空间就直接废了,而后者还有大把的映射空槽可供考虑。

不过与其说这个阶段在准备用 Neovim 刷 LeetCode,不如说是在我自己在调配着 Neovim 玩。看了相当多插件的文档并实际测试与调整配置,玩得很开心。

崭新的仓库

既然抛弃了 CLion 而转向 Neovim,自然也可以使用 WSL 了,不用被 Windows 折磨。

七月初我经过与 Agent 深入的交流,规划并重新设计了一下题解仓库的形式。

在之前,根目录散落着一些文件,而题解本身是在 leetcode/editor/cn 中,这似乎是插件配置上的原因。迁移后题解本身就在 solutions 中,也更加清晰了。

调试层面

此外,之前单一的 utils.h 也被我拆分为了多个工具库,并进行了大面积的扩展。utils.h 只承担「重导出」的职责,具体的任务被拆解在 lc/ 子目录中(含义为 LeetCode)。

1
2
3
4
5
6
7
8
9
#pragma once

#include <bits/stdc++.h>

#include "lc/check.h"
#include "lc/dbg.h"
#include "lc/nodes.h"
#include "lc/parse.h"
#include "lc/print.h"

具体而言,拆分为了 check, dbg, nodes, parse, print 这五个部分。

nodes 部分就不用说了,其实就是给予链表、二叉树定义。特别地,LeetCode 样例中的二叉树给的是其层序遍历表示(含空节点),中间可能会有 null 的存在,于是提供了 vector<optional<int>> 的转换方式。当然,也继承了我之前的 tree->visualize() 链式开关打印。

print 部分也很简单,同时使用了 C++ 的一些模板语法,可能还有 concepts 等,简化了写法,不用像我之前每种容器的模板写一次了。

然后是 check 部分。之前本地判断正误我都是手动执行并肉眼对比输入输出。但是我觉得这样还是太累了,于是就准备了这样一个工具库,将调用与预期结果直接写在 main 中,执行后就会自动判断正误,如果不同也会显示出来。具体而言其实现方式是,将输出转换为字符串,然后进行对比,找到第一个不同的地方。

提供了下面的宏方法:

1
2
3
4
5
6
CHECK(actual, expected)                       // 基础比对
CHECK("demo", actual, expected)               // 带用例标签(下面的版本也有)
CHECK_ANYORDER(actual, expected)              // 排序后比对(任意顺序均可的题)
CHECK_INPLACE(call, modified, expected)       // void 函数改引参:先调用后比对
CHECK_FIRSTK(k_call, vec, expected)           // 返回 k、取前 k 个比对
CHECK_FIRSTK_ANYORDER(k_call, vec, expected)  // 前者的任意顺序版(后面添加的)

除此以外,还有中间的调试。打印就是最好的调试,但是 C++ 很多类型默认没法打印,或者打印的形式不符合预期。因此 dbg 还提供了一个 DBG 宏,用非常自然的形式对中间变量进行快照。

下面 utils.h 回归测试与其输出(有修改),DBG 宏的输出是在 stderr 中,而 CHECK 则是在 stdout 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "../utils.h"

int main() {
  /* --- parse.h:UDL 字面量 --- */
  CHECK("[1,3,-1]"_vi, std::vector<int>{1, 3, -1});
  CHECK("[[1,2],[3],[]]"_vvi, std::vector<std::vector<int>>{{1, 2}, {3}, {}});
  CHECK(R"(["ab","c"])"_vs, std::vector<std::string>{"ab", "c"});
  CHECK("[1.5,2]"_vd, std::vector<double>{1.5, 2.0});
  CHECK("[true,false]"_vb, std::vector<bool>{true, false});
  CHECK("[]"_vi, std::vector<int>{});
  CHECK("abc"_vc, std::vector<char>{'a', 'b', 'c'});

  /* --- 链表/树:构造与打印一致性 --- */
  CHECK("[1,2,3]"_list, "[1,2,3]"_list);
  CHECK("[]"_list == nullptr, true); // 空链表语义:nullptr
  CHECK("[1,null,2,3]"_tree, "[1,null,2,3]"_tree);
  CHECK("[]"_tree == nullptr, true);

  /* --- print.h:泛型容器 --- */
  CHECK(lc_check::stringify(std::deque<int>{1, 2}), "[1, 2]");
  CHECK(lc_check::stringify(std::map<int, int>{{1, 2}}), "[(1, 2)]");
  CHECK(lc_check::stringify(std::make_tuple(1, 'a')), "(1, a)");
  CHECK(lc_check::stringify(std::optional<int>{}), "null");
  CHECK(0.1 + 0.2, 0.3);
  CHECK(1024.0, 1024); // 混用整型不因序列化格式差异误报
  CHECK(lc_check::stringify(2.0), "2.00000");

  /* --- check.h:标签 / anyorder / inplace / firstk --- */
  CHECK("labeled case", 1 + 1, 2);
  CHECK_ANYORDER(std::vector<int>{3, 1, 2}, "[1,2,3]"_vi);
  CHECK_ANYORDER("anyorder labeled", std::vector<int>{2, 1}, "[1,2]"_vi);

  auto v = "[0,1,0,3,12]"_vi;
  auto move_zeroes = [](std::vector<int> &nums) {
    std::stable_partition(nums.begin(), nums.end(),
                          [](int x) { return x != 0; });
  };
  CHECK_INPLACE(move_zeroes(v), v, "[1,3,12,0,0]"_vi);

  auto v2 = "[3,2,2,3]"_vi;
  auto remove_val = [](std::vector<int> &nums, int val) {
    return static_cast<int>(std::remove(nums.begin(), nums.end(), val) -
                            nums.begin());
  };
  CHECK_FIRSTK(remove_val(v2, 3), v2, "[2,2]"_vi);

  /* --- dbg.h:冒烟(stderr 输出,不参与断言,编译运行即视为通过) --- */
  auto nums = "[1,2]"_vi;
  DBG(nums, nums.size() + 1, std::make_pair(1, 2));

  /* --- S2V/V2S --- */
  CHECK(V2S(S2V("abc")), "abc");

  // 故意失败的用例,默认不编译
#ifdef CHECK_FAIL_DEMO
  CHECK(1, 2);
  CHECK("fail with label", "[1,2]"_vi, "[1,3]"_vi);
  CHECK_ANYORDER(std::vector<int>{1, 1}, "[1,2]"_vi);
#endif
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
PASS [1, 3, -1]
PASS [[1, 2], [3], []]
PASS [ab, c]
PASS [1.50000, 2.00000]
PASS [1, 0]
PASS []
PASS [a, b, c]
PASS [1 -> 2 -> 3]
PASS 1
PASS [1, null, 2, 3]
PASS 1
PASS [1, 2]
PASS [(1, 2)]
PASS (1, a)
PASS null
PASS 0.30000
PASS 1024.00000
PASS 2.00000
PASS labeled case 2
PASS [1, 2, 3]
PASS anyorder labeled [1, 2]
PASS [1, 3, 12, 0, 0]
PASS k=2 [2, 2]
[test_check.cpp:54] nums = [1, 2], nums.size() + 1 = 3, std::make_pair(1, 2) = (1, 2)
PASS abc
FAIL scripts/test_check.cpp:62
  expected: 2
  actual:   1
            ^
FAIL fail with label scripts/test_check.cpp:63
  expected: [1, 3]
  actual:   [1, 2]
                ^
FAIL scripts/test_check.cpp:64
  expected: [1, 2]
  actual:   [1, 1]
                ^

== 24 passed, 3 failed ==

而最后一个就是 parse,提供了 UDL (User-defined Literals, 用户定义字面量)。之前的 VEC(1, 2) 虽然已经可以了,但是 LeetCode 那边提供的格式却是 [1, 2]。数组还好,可以直接复制中间的内容,但像是链表、二叉树,要怎么办呢?

为了让我可以直接复制提供的样例,就加入了 UDL,直接用对应的类型包裹,然后会自动进行解析。这也就是上面看到的 ""_vi 等了。

1
2
3
4
5
6
7
8
"[1,3,-1]"_vi           // vector<int>
"[[1,2],[3]]"_vvi       // vector<vector<int>>
R"(["ab","c"])"_vs      // vector<string>
"[1.5,2.0]"_vd          // vector<double>
"[true,false]"_vb       // vector<bool>
"[1,null,2,3]"_tree     // TreeNode*(BFS 序,null 为空位)
"[1,2,3]"_list          // ListNode*(空 "[]" 返回 nullptr)
"abc"_vc                // vector<char>

此外还学到了 C++11 中加入的 raw string,即上面的 R"(...)"

由此便提供了近乎完美的本地调试体验,夸张一点的说法就是,写题、调试甚至成为了「享受」。

工具层面

上面主要是调试之中的体验优化。还有一些其他琐碎的要点。

首先是 .clangd 的修改。

1
2
3
4
5
6
7
8
9
10
11
If:
  PathMatch: .*\.h
CompileFlags:
  Add: [-x, c++-header]
---
If:
  PathMatch: solutions/.*\.cpp
Diagnostics:
  UnusedIncludes: None
CompileFlags:
  Add: [-Wno-unused-variable]

为了兼容(虽然说其实只需要一个全局替换),还是沿用了 utils.h 等的 .h 的写法,但这就让 Clangd 服务器把它当成了 C 的头文件,会有一些不必要的警告。因此加入了规则,这是一个 C++ 项目,即便用了 .h 还是要当成 C++ 的头文件。

然后就是借题,去掉了一些干扰性质的警告,如没有直接使用的头文件、没用的变量等。不过后者其实在 compile_flags.txt 中也有写,唔,不知道能否让 Clangd 也获悉?

.clang-tidy 基本上是从之前继承过来,然后根据实际体验调整关闭了一些警告。

后面也调整了一下 .clang-format,明确设置为了 LLVM 格式,主要是额外根据实际情况加了规则。

还有一个,之前用的是 CMake,但我不会用,没怎么学过。加上其实我要的功能没那么复杂,就是编译某个题解文件、运行它,最多还有个调试而已。于是我就想用更为熟悉的 Just。便使用一些脚本并迁移到了 just 的工具链,最初提供了下面的一些方法(有改动):

1
2
3
4
5
6
7
8
Available recipes:
    run F      # 编译运行(LeetCode 题解不释放节点,关闭 LeakSanitizer 防误报)
    debug F    # 无 sanitizer 编译供 gdb/DAP;stdout 最后一行为产物路径(nvim 键位读取)
    build-all  # 全量编译(迁移验证 / utils.h 改动回归)。只编译不运行
    test-utils # utils.h 回归测试
    stats      # 重生成 README 进度表
    note F     # 从模板创建 Typst 笔记(notes/<id>.<slug>.typ)
    commit     # 半自动 jj 提交

例如可以用 just run 1 运行第一题,这也是后面在编辑器层面的「执行」中实际使用的命令。

1
2
3
4
PASS [0, 1]
PASS [0, 1]

== 2 passed, 0 failed ==

just debug 则会生成对应的 .dbg 文件,用以给 gdb 进行调试。

just stats 则会根据进度更新 README 进度表。在练习尾声的时候,我将其作为了提交的依赖项,这样可以每次提交前都更新一下进度,不用手动更新。

just commit 就是提交了。我的提交不是简单的提交,而是有一定的规则。

像之前的格式就是 W7D1(4)[10.22]: 完成 T347, 144, 145, 94。因为我当时的练习模式就是一周练固定几天,于是就有了这样的方式:

  1. W7 代表这是第 7 周;
  2. D1 代表这是当周规则上的第一天,或者也可以简单理解为该周第几次练习;
  3. (4) 代表这一天做了 4 道题;
  4. [10.22] 代表这一天实际代表的天是 10.22。不能用 commit 或 author 日期代替,因为我时常会忘记提交;
    • 隐含了年份 2025,也许也是因为认为我坚持不到一年,真是精准又犀利的判断呐……
    • 不过认真说当时应该是深思熟虑过的,因为一年后推免都结束了大概,没必要额外注明年份。
  5. 然后是完成的题目,其中最开始以 T 开头,顺序按照真正的完成顺序。

这个格式虽然在我这时候已经不适用了,但规则本身是必要的,因此进行了一些调整,改成了 2026-08-13(5): 完成 T28, 206, 24, 19, 142

  1. 2026-08-13 直接以日期开头;
  2. (5) 依旧代表做了几题;
  3. 后面完全一致。

2026.8.12

  • 对了明天还要改一下默认的 description 模板,不打算用周模式了,直接用当天日期。尽量早上完成。

而这个信息,其实完全都可以自动生成的。在之前我就是手写,毕竟周数和天数都不太好解析,然后还有完成顺序的依赖。但是换成现在就很简单了,只需要获取天数,统计变化即可。

因为最开始我是在重做部分旧题,就使用 mtime 进行排序,后面开始做新题后就改成了 ctime。但其实我偶尔会翻提交,这个时间顺序有的不太准确。可能跟我编辑的顺序有关,但我也不太在意了。

笔记层面

当然,我还是忘不了我那笔记呀。摸了一个 Typst 的笔记模板,同时还加入了上面的 just note 命令支持。

直接看 showcase.typ 吧:

结果当然还是一次都没用过,又是成了摆设。

leetcode.nvim 准备

上面讲的都是题解仓库那块的准备,与之并行的便是 Neovim 编辑器一侧的准备了。

差不多是期末一周后,实现了基础的 Neovim LeetCode 支持,且我审核了一下代码并进行了简单的测试。

我期末那会提到的插件便是 kawre/leetcode.nvim: A Neovim plugin enabling you to solve LeetCode problems.,在上面这个提交中也是进行了一些基础工作。

选择的映射命名空间是 <leader>k,其中 k 可以认为是取 LeetCode 意(音)。

当然,比较牵强,实则是选了一个没有被占用的,因为 <leader>l<leader>c 都已经有了占用。虽然说按照前面的原则,<leader>l/L 对应的 LazyVim 映射并不常用且占据了一整个命名空间,但考虑到现在映射也不是很紧张,加上这个 LeetCode 其实用处非常局限,就依旧保留了,作为以后更为常用且广泛的用处的选择之一。不用大写也是因为在用到的时候比较频繁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
map("<leader>kk", "<cmd>Leet menu<cr>", "主面板")
map("<leader>kc", "<cmd>Leet console<cr>", "测试用例控制台")
map("<leader>ki", "<cmd>Leet info<cr>", "题目信息")
map("<leader>kt", "<cmd>Leet tabs<cr>", "已开题目")
map("<leader>ky", "<cmd>Leet yank<cr>", "复制提交区")
map("<leader>kL", "<cmd>Leet lang<cr>", "切换语言")
map("<leader>kr", "<cmd>Leet run<cr>", "远程样例测试")
map("<leader>ks", "<cmd>Leet submit<cr>", "提交")
map("<leader>kR", "<cmd>Leet random<cr>", "随机一题")
map("<leader>kl", "<cmd>Leet list<cr>", "题单")
map("<leader>kD", "<cmd>Leet daily<cr>", "每日一题")
map("<leader>ko", "<cmd>Leet open<cr>", "浏览器打开")
map("<leader>kS", "<cmd>Leet last_submit<cr>", "恢复上次提交代码")
map("<leader>ke", "<cmd>Leet reset<cr>", "重置代码部分")
map("<leader>kI", "<cmd>Leet inject<cr>", "重新注入")
map("<leader>kd", "<cmd>Leet desc toggle<cr>", "题面开关")
map("<leader>kC", function() ... end, "重新生成样例 CHECK")
map("<leader>kb", function() ... end, "本地编译运行")
map("<leader>kg", function() ... end, "编译并调试")
map("<leader>kn", function() ... end, "笔记")

上面就是我经过调整的、截止目前使用的全部相关映射了。除了「随机一题」和「每日一题」外,好像都或多或少有用过。function 就是调用相应的 just 命令或外部脚本。

还是从上面那个提交开始写起。根据我的配置,可以使用 nvim leetcode.nvim 来启动 LeetCode 刷题的专属会话。

模板实例如下(有改动):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// @leet imports start
// Created: 2025-09-10 17:49:33

#include "../utils.h"

using namespace std;
// @leet imports end

// @leet start
class Solution {
public:
  vector<int> twoSum(vector<int> &nums, int target) {
    unordered_map<int, int> mp;
    for (int i = 0; i < nums.size(); i++) {
      auto it = mp.find(target - nums[i]);
      if (it != mp.end()) {
        return {it->second, i};
      }
      mp[nums[i]] = i;
    }
    return {};
    // __builtin_unreachable(); // GCC/Clang
  }
};
// @leet end

int main(void) {
  Solution s;
  CHECK(s.twoSum(""_vi, 3), "[0,1]"_vi);
  CHECK(s.twoSum("[2,2,1]"_vi, 4), "[0,1]"_vi);
  return 0;
}

可以看到其实还是分成了三个区域,只是 Import 区有了额外的标识符,且主 Code 区标识符也发生了变化。

旧题解的迁移发生在一次提交中,有意思的是,迁移后净代码量反而减小了。不过这应该是我删掉了原来的 .clang-format,用了 LLVM 格式的缘故。

虽然说可以直接 nvim leetcode.nvim 直接开启练习,但并不是所有工作都能在 Neovim 里面完成的。当然,终端方面的事情可以使用 Neovim 的内置终端,但这还不够,例如说你想保持着上一次运行的结果,但同时还要新的终端等。

此外,由于插件设置了 winfixbuf,缓冲区会被固定,无法直接通过正常的缓冲区操作切换缓冲区,自然也没法自由地看其他文件了。对于题目,可以用 <leader>kt 切换打开过的题目,也可以用 <leader>kl 打开题单进行搜索,但对于其他的,就需要其他的 Neovim 实例了。例如说我还需要开着机试的计划等等。

虽然开一个终端标签页可以一定程度上解决这个问题,但显然不够好,而且重启后也无法恢复。自然,我就想到了 Zellij。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function leetcode() {
    local repo="..."
    if [[ -n "$ZELLIJ" ]]; then
        (cd "$repo" && nvim leetcode.nvim)
        return
    fi
    local line
    line=$(zellij list-sessions -n 2>/dev/null | grep '^leetcode ')
    if [[ -n "$line" ]]; then
        if [[ "$line" != *EXITED* ]]; then
            zellij attach leetcode
            return
        fi
        zellij delete-session leetcode >/dev/null 2>&1
    fi
    zellij --session leetcode --new-session-with-layout leetcode
}

这便是我写在 ~/.zshrc 的配置了,定义了一个 leetcode 函数,然后它会检测当前是否有正在运行的 LeetCode 练习会话,有的话就会附着上去,否则的话会进行重置(直接恢复原貌)。

这里还定义了名为 leetcode 的布局,在最开始就是简单的两个标签页,其中一个运行 nvim leetcode.nvim,是练习的主力,另一个就放着题单。这样的话即便是关机了之类的,只要重新输入 leetcode,就能立刻恢复并进入 LeetCode 的练习状态。

上面加入了笔记功能,虽然最后是没有用到,但在当时我还是为其准备了一个 <leader>kn 的映射,用以在 Zellij 中打开一个 stacked pane:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
map("<leader>kn", function()
  local file = current_solution()
  if not file then
    return
  end
  local res = vim.system({ "just", "note", file }, { cwd = repo_root(), text = true }):wait()
  if res.code ~= 0 then
    vim.notify(res.stderr or "创建笔记失败", vim.log.levels.ERROR, { title = "leetcode" })
    return
  end
  local note_path = vim.trim(res.stdout)
  local in_zellij = vim.env.ZELLIJ ~= nil
  if in_zellij then
    local cmd = {
      "zellij",
      "action",
      "new-pane",
      "--stacked",
      "--name",
      "note",
      "--",
      "nvim",
      note_path,
    }
    vim.fn.jobstart(cmd, { detach = true })
    vim.notify("已在 Zellij 新 pane 打开笔记", vim.log.levels.INFO, { title = "leetcode" })
  end
end, "笔记")

而在正式开始练习前,我进行的最后一项准备,大概就是调试了。

虽然我上面写打印就是最好的调试,但我在最初还是有想过用调试器的。可以说 Neovim 唯一体验上落下风、不如 VS Code, CLion 的就是调试了。这个确实不好用。

测试过程中,我注意到步入的时候有时候会不小心步入了「无关代码」,如标准库函数,还有就是我自定义的这些辅助调试的库函数。因此进行了一些自定义,排除了自动步入这些函数,只会在自己写的题解中漫步

不过最后还是没怎么进行调试。一来效率太低了,重新启动然后执行到目标位置,都够加入 DBG 观察需要的变量执行好几次了。二来 TUI 中的调试确实很怪,UI 也很不舒服。反而是为了加入这样的支持弄了一些全局 hack 的东西,让我有点不舒服。

最后的准备工作在 7.9 差不多就完成了。

演进

然后可以跳过一个月,直接到 8.12 了。

没错啊没错,准备工作做完了以后就晾在那里了,差点又成为了花瓶。直到一个月以后才「不能再这样下去了」,重新捡了起来。

2026.8.12

  • 难绷,好久没登录了,LeetCode Cookie 直接过期了。还得重新搞。
    • 下午快两点终于开始做了。第一题重做二分查找,感觉做了快半个钟,乐。不过还算顺利最终。印象中写得比之前简明(之前 28 行,这次 25 行),可以看看标答。嗯,然后很快就要长休息了……看了看标答确实更好,我觉得手抄一下吧,长长记性。不知道要不要 Anki 记录一下。
  • 算法练习做了九道题,很不错了。按我最初设想的一题一分,都可以拿到十分了(哦不太行,今天 RSS 没清完),当然因为是重做含金量没那么高就是了,虽然我也全忘记了。
  • 还有就是实际练习过程中遇到一些问题,也顺手解决了,例如一些配置、工具库函数等。
  • 下午倒是认认真真开始练习算法,差不多做了四个小时吧,当时应该是做了「数组和双指针」的五道题,还有「字符串」第一题很简单。
  • 晚上回来摆了一会,然后去看纪录片,后面做了「字符串」剩下三题,最后一题依旧用了暴力,去看视频学了一下 KMP,感觉我已经会了,明天先自己写一下。

写本文重新打开的时候,Cookie 也过期了。

格式化

最开始重设 .clang-format 时,除了设置样式基准为 LLVM 外,还有一个就是弄了 AllowShortFunctionsOnASingleLine: None,也就是不允许函数单行。

这是因为创建一个新题后,它的函数体本来就是空的嘛,如果习惯性地在开始的时候保存了一下,那就会自动格式化为 {} 了,还得进去后回车,有点不便。

而 8.14 的时候修改了这一个行为,允许空函数单行。这是为什么呢?因为那会写到一种新题型了,并非提供 Solution 类,而是给了一个要实现了类,而构造函数是空的,这样格式化后 {, } 不在同一行,有点丑陋。

那岂不是又恢复了默认行为?其实不是的,只是将 None 改为了 Empty。之前很大一部分不便不仅仅来源于空函数变成了单行,还来源于单行函数也变成了单行,在你写了一行变量初始化的时候顺手保存一下,都会变成一行,这其实才是更为不爽的一点。相较而言,可以容忍空函数成为单行。

自动导入头文件

写着写着,又遇到一个问题。Clangd 会自动帮我在 #include "../utils.h 后加入对应函数的头文件。这其实是不必要的,因为我在 utils.h 中已经加入了万能头了。

而经过我的调查,这个是写在了 LazyVim 的配置当中,命令行参数加入了 --header-insertion=iwyu,因此即便配置 .clangd 也没有效果。

还有一种解决方法就是在设置里面覆盖掉默认,但我觉得也太不优雅点了,何况我只想对这个工作目录生效而已。

2026.8.14

  • 神了,做最后一题想要关闭自动 import,我明明全都引入了默认,结果关不掉,因为 LazyVim 默认设置好了,除非我覆盖。唉唉,没辙了,感觉关掉全部引入得了。

当然在调查过程中我还认识到一个选项 exrc,可以创建 .nvim.lua 作为项目级别的 Neovim 配置,

不过权衡之下我还是没有选择,因为这样其实有安全隐患。上面调试方面的配置其实也是因为这样,才不得不选择在全局的配置中进行丑陋的 hack。而这里,我认为算是小问题,就放弃了。

最终就是保留了这个行为,自动引入就自动引入吧,也当让我认识认识了。

Anki 与新笔记

「贼心」不死,在上面的时候我提到过,刚开始写了没多久,我就认为还是有必要记忆一下解法的。因此又动了 Anki 的心思。

2026.8.12

  • 此外最后一个小时还开了 Claude 做 Anki 卡片的计划,大致商量完了,让它自己实现了,希望明早起来看不会消耗太多额度吧。

最初的想法就是这样,问题和代码都是由脚本自动生成,而其他字段,如提示、思路、笔记、相关知识等,就待我复习的时候,手动进行补充。不必长篇大论,随手记即可,因为我写题、看解答的时候确实会有一些想要记下来的冲动,目标就是把这个冲动落到实处。

不过这次倒是没有成为摆设,而是很快就投入了使用。我自动导入卡片,然后在预览的时候一张一张过,有的有了点念头后就即可记录下来。

2026.8.14

  • 晚上回来立刻就开始做了,是这几天晚上最早恢复的了,因为去吃饭的时候就在想了。不过可惜的是好像还是没做出来,后面看了思路后自己写了一遍,过了,然后再学标答改。然后最后一点时间导入 Anki 并加笔记,不过没啥时间了。

当然,也会遇到一些问题,然后深入探究一番。

2026.8.15

  • 下午折腾好久 Anki。
    • 加入了代码的白天模式支持,现在更适配主题一些了。
    • 反倒是字体花了好久时间,太特么恶心了。一直用 JetBrainsMono NF,结果不行,各种折腾,试过了 JetBrainsMono NFMJetBrains Mono 等都不行。后面终于试了一下 JetBrainsMono Nerd Font 才行。这个字体名字问题真的太恶心了。尤其是 Flash 还给我过个脚本检测是可以的。
    • 然后刚刚我忘记了 NFM 有啥用,去删掉了。刚删完才想到 gVim 要用,果然如此,灰溜溜装回来。折腾到现在四点出头,我还没搞完笔记呢。

有意思的是,后面配 Neovide 的时候字体名称也是 JetBrainsMono Nerd Font。NF 也是受到了 gVim 的影响,它就是这样配的。

而很快我又对这个模式不满意了。这个模式是一天练完后再将想法记录下来,但其实这时候很多早已烟消云散了。

此外很不方便的一点是,因为有的题有多解,而且我觉得都有记录的价值,而默认就是只有一种解法会同步进去,因此在一开始我就是手动将第二个解法复制过去,然后进行批注。但带来的一个问题就是破坏了单一知识来源,额外解的代码有时候看的时候又觉得不够完美了,这时候又得同时改代码和卡片两处,让我很是不爽。

因此我决定全面贯彻落实「单一知识来源」的思想,将笔记也全部集中在代码中,Anki 卡片直接从代码生成,不必后面自行添加内容。如有需要更新,更新代码即可,然后同步过去。

2026.8.16

  • 下午开始搞 Anki,我打算继续优化一下,看看能不能直接在原文件里面写好了,直接推送到 Anki 不用改,这样也符合单一知识来源。

最后实现的模板如下。一共有下面的字段:

  1. 问题:LeetCode 题面,剥去了部分内容(如样例)避免太过冗长;
  2. 解答:代码与相应的思路、笔记;
  3. 提示:有时会提示额外思考一些题目无关、做题过程中学到的内容;
  4. 笔记:额外的笔记信息;
  5. 相关知识:顾名思义,没用过;
  6. 解数:解的数目,用以提示要思考几种解法;
  7. 随记:唯一一个不会同步的字段,用以实现「仅 Anki 端」的内容补充,不过也没用过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// @leet start
class Solution {};            // 主解法(提交远程的那份,照旧剥壳)
// @leet end

// @card hint
// 正面可见的提示(Markdown,连续 // 行,空行写成单独的 //)。

// @card idea 迭代 BFS
// 主解法的名字 + 思路(≈ 旧「思路」字段,渲染为「解法 1 · 迭代 BFS」)。

// @alt 递归 DFS
// 该解法的说明文字(紧跟标记行的连续 // 行)。
class SolutionDFS {              // 一律 class 形态,辅助函数写在 class 内
public:
                              // 可在 main 里 CHECK,与主解法同等测试
};
// @alt end

// @card note
// 全题级笔记(语言技巧、易错点等)。

// @card ref
// 相关知识(导引其他题目/知识卡)。

给一个实际例子就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @leet start
class Solution {
public:
  vector<vector<int>> combinationSum3(int k, int n) { ... }
};
// @leet end

// @card hint
// 如何快速枚举所有组合?

// @alt Gosper's Hack
// ……
class SolutionGosper {
public:
  vector<vector<int>> combinationSum3(int k, int n) { ... }
};
// @alt end

// @card note
// ……

显示效果如下(补全了没用过的两个字段用于完整的显示效果,同时背面缩小了比例为了显示更完整):

右边的多解也是可以折叠的。

然后这个笔记字段还特别有小巧思,它不固定在左栏或右栏,优先选择右栏,但如果加入它会溢出而出现滚动条,它就会跑到更矮的一栏。因此上图中它在左栏。

同时加入了 snippets,方便使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
local snippets = {
  s("cd", {
    t("// @card "),
    c(1, {
      t("hint"),
      sn(nil, { t("idea "), i(1, "解法名") }),
      t("note"),
      t("ref"),
    }),
    t({ "", "// " }),
    i(0),
  }),
  s("alt", {
    t("// @alt "),
    i(1, "解法名"),
    t({ "", "// " }),
    i(2, "说明"),
    t({ "", "class Solution" }),
    i(3, "Name"),
    t({ " {", "public:", "  " }),
    i(0),
    t({ "", "};", "// @alt end" }),
  }),
  s("nodbg", t("#define DBG(...) ((void)0)")),
}

后面还加入了一个代码折叠的功能。有的时候会有很多冗余的写法,这些对于代码不可或缺,但是对复习的时候意义就不大了,因为模式重复。因此后面还添加了 // @fold 的支持,可以在卡片中折叠某部分代码,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
  vector<int> findMode(TreeNode *root) {
    vector<int> ans;
    int maxCount = 0;
    int count = 0;
    TreeNode *last = nullptr;
    // @fold
    auto visit = [&](TreeNode *node) {
      if (last && node->val == last->val) {
        count++;
      } else {
        count = 1;
      }
      last = node;
      if (count > maxCount) {
        maxCount = count;
        ans = {node->val};
      } else if (count == maxCount) {
        ans.push_back(node->val);
      }
    };
    // @fold end
    auto inorder = [&](auto &&self, TreeNode *node) {
      if (!node) {
        return;
      }
      self(self, node->left);
      visit(node);
      self(self, node->right);
    };
    inorder(inorder, root);
    return ans;
  }
}

显示效果如下:

可见 visit lambda 的内容被折叠成了 ...,因为两个解法都有用到,但又不是重点,我将其抽到了「笔记」字段当中。

折叠也可以有相应的摘要代替默认行为[2],不过我没用过。

对于已有笔记的迁移,则是在这个提交中完成的。好在那时候还不算多,迁移没那么困难。

以及由于字段可能比较长,超过了限制,会被自动折叠,于是也调整了 .clang-format 的设置

C++ 版本

最开始用的是 C++17,但确实很希望用上一些新语法,可以减少一些样板代码。

C++20 Ranges(以及后面版本的补充)自不必说,我还特地学了一下。不过其实我也没用几次,因为怕机试没有也不敢多用。下面是其中一个使用的例子,其实格式化效果挺差的:

1
2
3
4
5
6
auto valid = [&](int k) -> bool {
  return ranges::fold_left(piles | views::transform([&](auto a) {
                             return (a + k - 1) / k;
                           }),
                           0L, plus<>()) <= h;
};

不过还有一些小偷懒还是常做的,比如说 C++20 加入了基于范围的 for 循环初始化语句

1
2
3
4
for (int prefixSum = 0; auto n : nums) {
  prefixSum += n;
  ...
}

还有 bit_width 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int fib(int n) {
  if (n == 0) {
    return 0;
  }
  auto p = 0, q = 1; // F(0), F(1)
  // 或 sizeof(n) * 8 - 1 - __builtin_clz(n)
  auto bit = 1 << (bit_width((unsigned)n) - 1);
  while (bit) {
    auto r = p * ((q << 1) - p);
    auto s = p * p + q * q;
    if (n & bit) {
      p = s, q = r + s;
    } else {
      p = r, q = s;
    }
    bit >>= 1;
  }
  return p;
}

然后这一轮刷题,我不太喜欢脱离目标函数出去写工具函数。因为这样写得不流畅,你要先离开当前函数体,然后写完了再回来写调用,我感觉不够线性。因此我就大量地使用了 lambda 函数。

而 lambda 函数要写递归,你就得将 self 作为一个参数手动传入,非常难受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Node *connect(Node *root) {
  auto build = [&](auto &&self, Node *node, Node *parent) -> void {
    if (!node) {
      return;
    }
    if (parent && parent->left == node) {
      node->next = parent->right;
    } else if (parent && parent->next) {
      node->next = parent->next->left;
    }
    self(self, node->left, node);
    self(self, node->right, node);
  };
  build(build, root, nullptr);
  return root;
}

当然,其实可以用函数容器 std::function,就可以不写 this 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vector<vector<int>> findSubsequences(vector<int> &nums) {
  int size = nums.size();
  vector<vector<int>> ans;
  vector<int> temp;
  function<void(int)> dfs = [&](int idx) {
    if (idx == size) {
      if (temp.size() > 1) {
        ans.push_back(temp);
      }
      return;
    }
    if (temp.empty() || nums[idx] >= temp.back()) {
      temp.push_back(nums[idx]);
      dfs(idx + 1);
      temp.pop_back();
    }
    if (temp.empty() || nums[idx] != temp.back()) {
      dfs(idx + 1);
    }
  };
  dfs(0);
  return ans;
}

但是其会进行类型擦除,性能上不如 lambda。

C++23 添加了 deducing this 特性,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
TreeNode *sortedArrayToBST(vector<int> &nums) {
  auto build = [&](this auto &&self, pair<int, int> lr) -> TreeNode * {
    if (lr.first == lr.second) {
      return nullptr;
    }
    int mid = (lr.first + lr.second) / 2;
    auto node = new TreeNode(nums[mid]);
    node->left = self({lr.first, mid});
    node->right = self({mid + 1, lr.second});
    return node;
  };
  return build({0, nums.size()});
}

无需在里面 self(self, ...),在外面也无需 dfs(dfs, ...) 了,这应该是我最常用的高版本特性了。

最终两次跳跃,升到了 C++23

2026.8.17

  • 今天算法练习改成 C++23 了,我太喜欢用 lambda 写递归了,懒得在外面写,结果这样就要 self(self, ...)。写多了也写烦了,就用 C++23 的 this 推导了。不过是先升到 C++20 用 std::bit_witdh,当然是一解,后面优化的解法没用。然后再换成 C++23。但还是不能用太多比较新的语法,例如说 range 等,万一写熟悉了后面实际机试没那么新版本就完球了。只是这里 lambda 我写多了倒也无所谓了。

自动生成样例

我写着写着又又又烦了,虽然说已经有了 UDL,但样例还让我自己复制也太麻烦了吧。既然都给了,能自动生成吗?于是就去做了

模板中的 // TESTAUTOGEN 会被自动替换为解析出来的样例,这下不用手抄了,爽。

虽然有的时候解析会有异常,例如说样例的格式过于复杂等——最初实现的时候没考虑太复杂的,也没有太复杂的实例——或者说不知为何样例数目解析有点问题,但总的来说还是裨益多多。

不过我后期的时候其实也挺少本地测试了,因为我发现 LeetCode 可以远程测试。我之前一直不怎么喜欢远程测试的,因为如果要调试的话也不方便。但是 LeetCode 的远程测试居然可以自定义用例,甚至不用给结果,会自动帮你判。

因此后面的时候,除非真的出现问题,否则都是优先走 fast-path,也就是直接远程测试先,过了就万岁交了。

自动移除 DBG

DBG 非常好用,但是我时常会遇到这样的情况,调试完改好了,兴奋地再交一发感觉这次一定过了,结果忘记注释或删掉调试语句,然后就是 LeetCode 远程不认识 DBG,编译就报错了,白白多了一次失败。

像一般机试的方式,那就是在本地定义一个 LOCAL 宏,然后在没定义的时候将其变成一个空语句。但我这里的 LeetCode 练习却不行,因为将 DBG 定义为空,一定得出现在 Code 区域中,否则 LeetCode 压根不知道。而这很难被我所控制,每次都写不仅不优雅,而且也不方便。

前面写到 snippets 时里面有一个从未使用过的 snippet nodbg,可以直接将 DBG 定义为 ((void)0),算是正解之前尝试的无奈之举。

我最初的想法其实是提交前有一个 Hook 检查是否出现了 DBG,有的话就终止提交并报错,提醒我自己删掉。不过这样一来,如果交了还是错的,那我还得重新加回来(虽然可以撤销)。

跟 Agent 交流过程中实现了一个更好的方式,那就是不只是检测,还同时将提交内容中的 DBG 替换成 (void)0,而本地保持不变,这样本地不需要任何修改。

最开始的思路是普通的字符串匹配,不过终究还是不够优雅。后面使用了 Tree-sitter,实现语义级别的匹配。妈妈再也不用担心我把调试语句一起上传上去了。

同时这个替换考虑得还挺周全,如果跨越了多行,还会补相应的空行,这样一来行号信息也是正确的。嵌套什么的就更不必说了。

Zellij 演进

Zellij 的配置方面也是在不断演进的。

有了 Anki 后,我就需要三个 tabs 了,分别就是代码、其他资料(如机试题单计划)、Anki。

这些基本上就是固定的,写在了布局当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
layout {
    cwd ".../LeetCode"
    new_tab_template {
        pane
        pane size=1 borderless=true {
            plugin location="zellij:compact-bar" {
                tooltip "C-F1"
            }
        }
    }
    tab name="leetcode" focus=true hide_floating_panes=true {
        pane stacked=true {
            pane command="nvim" cwd="solutions" expanded=true {
                args "leetcode.nvim"
            }
            pane name="shell"
        }
        floating_panes {
            pane command="btm"
        }
        pane size=1 borderless=true {
            plugin location="zellij:compact-bar" {
                tooltip "C-F1"
            }
        }
    }
    tab name="..." cwd="..." hide_floating_panes=true {
        pane command="nvim" {
            args "..." // 机试计划
        }
        floating_panes {
            pane cwd="..." command="..." {
                args ... // 其他命令
            }
        }
        pane size=1 borderless=true {
            plugin location="zellij:compact-bar" {
                tooltip "C-F1"
            }
        }
    }
    tab name="anki" cwd="anki" {
        pane 
        pane size=1 borderless=true {
            plugin location="zellij:compact-bar" {
                tooltip "C-F1"
            }
        }
    }
}

第一个 tab 里面还有一个隐藏的浮动窗口,执行的是 btm。因为有时候不小心无限循环了[3],这时候在 Neovim 的终端里面没法直接 Ctrl + C 结束进程(我可能要去调查一下如何解决?),这时候我就得去 bottom 手动杀掉进程了。

因此就在布局中也直接提前打开了,有备无患。唯一比较可惜的是,没法在布局中发送按键,因为 bottom 打开也不是直接就能杀进程的,得先按一个 e 展开进程列表。

不过看上面写着,好像有很多重复的部分,例如说那个 zellij:compact-bar 是一个每个面板都需要的状态栏,重复了好几次。其实理想的写法是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
layout {
    cwd ".../LeetCode"
    default_tab_template hide_floating_panes=true {
        children
        pane size=1 borderless=true {
            plugin location="zellij:compact-bar" {
                tooltip "C-F1"
            }
        }
    }
    tab name="leetcode" focus=true {
        pane stacked=true {
            pane command="nvim" cwd="solutions" expanded=true {
                args "leetcode.nvim"
            }
            pane name="shell"
        }
        floating_panes {
            pane command="btm"
        }
    }
    tab name="..." cwd="..." {
        pane command="nvim" {
            args "..." // 机试计划
        }
        floating_panes {
            pane cwd="..." command="..." {
                args ... // 其他命令
            }
        }
    }
    tab name="anki" cwd="anki" {
        pane 
    }
}

然而很遗憾的是,行不通,有 bug。

2026.8.21

  • 有点变态,昨天 Zellij 发新版了,于是更新后给 Windows 也装了,打算同步调整一下设置,WSL 的老设置要加新功能、Windows 的设置要按需调整,调整过程中转移去调 LeetCode 的布局模板了。

第二个问题我修复了,不过意外的是很快合并了。看来作者比较忙,要是第一时间没看到的话就基本很难合了,因为我几个月前也有一个改动极小的 PR。

杂项

还有一些杂项,一并讲了。

上面将卡片信息一并整合在代码中,结果就有了警告,多行注释推荐用 /* ... */。呃呃了,关掉。

这段时间我也不是完全投入在算法练习中,也有时候会折腾一些别的。例如说学习了解了一下 fzf,然后发现 fzf 相关命令是可以配置的。

例如说我现在配置如下:

1
2
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_CTRL_T_COMMAND=$FZF_DEFAULT_COMMAND

大概就是默认用 fd 搜索文件,且包含隐藏文件等。然后 Shell 中可以用 Ctrl + T 模糊搜索目标文件,不用手打或一个一个 Tab

不过在 LeetCode 这里我改了一下,改成了 fd --follow,排掉了隐藏文件。

这其实稍微有点不便,不过当时的思路大概是这样的:学到了 Ctrl + T,但是里面有好多如 .git 里面的文件,不好。再学到了 FZF_CTRL_T_COMMAND,然后了解到可以用 mise 控制项目级别的环境变量,遂设置了。最后感觉这个也值得全局设置,于是再给 ~/.zshrc(现 ~/.zshenv)添加了。

mise 其实也早有耳闻,但一直没什么机会用一下,因此这次也算是尝试了一下。

结语

其实 Neovim 时期的机试练习也就是从 8.12 开始,到 9.8 结束,不到一个月。而其中真正练习了的天数,其实也就 18 天。

但是在这 18 天中,我做了 151 张卡(也就是做了 151 题),并学习了其中 135 张[4],快速补全了机试的基础概念,以至于我不会在机房座位里面发楞,盯着题目啊吧啊吧。

而且与一年前不同的是,在这段练习的时间当中,我居然大多时候是快乐且充实的,学习(基础的)算法,还挺有趣。此外因为我根据自己的需求深度定制了体验,尽管依旧有少数缺憾,但我也很少为其他琐碎的事情所烦扰,敲着预先准备的一个又一个映射,流畅地像在奏乐,情绪价值也是给满。否则的话,我也不可能有 18/28 天真正在练习。这还像话吗,我的效率能过半?。

虽然说最开始的目的就是为了预推免的机试,同时也为未来潜在的机试练习做基础设施的准备。但我觉得更为宝贵、我从中收获更大的,也许仍是在其中的探索吧。我不断地完善了 Neovim 的配置,一次编辑、一次跳跃,都让我欣喜不已。

也许这和之前很多次没有区别,终究只是在重压之下逃避的自娱自乐之举,但我坚信在这期间分泌的多巴胺不是虚假的,我在其中品尝到的甘甜,与高中折腾时并无差距。只要我依旧可以理解那时候的快乐,那我就可以说我还尚未迷失自我,就可以说我还是那个我。


  1. 当然还是相对我自己的其他计划。 ↩︎

  2. 默认行为是,如果注解是在空行,那么会将下/上一行作为边界,如果是在后面,则是将所在行注解前面作为边界。然后头尾两行与中间 ... 拼接代替。 ↩︎

  3. 比如说链表、树出现了环,然后打印。 ↩︎

  4. 为什么没有全部学习呢?因为 9.8 最后一次练习后一直到预推免中间这段时间我直接开摆了,毛事都没做,更不用说清卡了,于是最后一天做的 16 题就没有学习了。在那之后唯一做的事情就是考前抄写了一下模板。 ↩︎