MoreRSS

site iconLiHan | 李寒修改

研究生,中国传媒大学
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

LiHan | 李寒的 RSS 预览

Python 笔试备忘

2026-03-13 22:24:00

Featured image of post Python 笔试备忘

Python LeetCode & ACM笔试 极简速查表

一、 核心容器:初始化、拼接与类型转换

1. 列表 (List) -> 动态数组 / 栈

  • 创建与初始化:
    • 空列表: arr = []
    • 定长初始化: arr = [0] * n
    • 二维初始化: dp = [[0] * n for _ in range(m)] (严禁使用 [[0]*n]*m)
    • 推导式: arr = [x**2 for x in range(10) if x % 2 == 0]
  • 拼接与扩展:
    • + 运算符: [1, 2] + [3, 4] -> [1, 2, 3, 4] (产生新列表,耗时)
    • 原地扩展: arr.extend([3, 4]) (等价于按顺序 append,$O(K)$)
  • 操作: append(x) $O(1)$, pop() $O(1)$, arr[::-1] 翻转 $O(N)$, sort() 原地排序 $O(N \log N)$。

2. 集合 (Set) -> 哈希去重

  • 创建与初始化:
    • 空集合: s = set() (严禁使用 {},那是空字典)
    • 字面量: s = {1, 2, 3}
  • 拼接与运算:
    • 并集: s1 | s2s1.union(s2)
    • 交集: s1 & s2s1.intersection(s2)
    • 差集: s1 - s2
  • 操作: add(x) $O(1)$, remove(x) $O(1)$ (不存在会报错), discard(x) $O(1)$ (不存在不报错)。

3. 字典 (Dict) -> 哈希表

  • 创建与初始化:
    • 空字典: d = {}dict()
    • 默认字典: d = defaultdict(int) (访问不存在的键返回默认值)(from collections import defaultdict
    • 键值对: d = {'a': 1, 'b': 2}
  • 拼接与合并:
    • Python 3.9+: d3 = d1 | d2 (合并字典,同名键覆盖)
    • 原地更新: d1.update(d2)
  • 操作: d.get(key, default) (安全读取), key in d $O(1)$。

4. 字符串 (String) -> 不可变字符序列

  • 切片 (极其高频,极速操作):
    • s[:4] # “leet” (前 4 个字符)
    • s[-4:] # “code” (后 4 个字符)
    • s[::-1] # “edocteel” ($O(N)$ 极速反转)
  • 查找与判断:
    • s.find("etc") # 返回 2 (子串起始索引,找不到返回 -1,笔试首选)
    • s.startswith("le")# True
    • s.endswith("de") # True
    • s.isalnum() # True (判断是否只包含字母和数字,双指针判断回文时必用)
    • s.isdigit() # False (判断是否纯数字)
  • 替换与清理 (注意:必须重新赋值):
    • s2 = " a b c "
    • s2.strip() # “a b c” (默认袪除首尾所有空白符)
    • s2.replace(" ", "") # “abc” (极其好用的全量替换)

5. 元组 (Tuple) -> 不可变序列 / 可哈希键

  • 创建与初始化:
    • 空元组: t = ()
    • 单元素元组: t = (1,) (⚠️ 必须带逗号,否则 (1) 会被当作整数运算)
    • 常规元组: t = (1, 2, 3)
  • 核心特性与用途:
    • 可哈希 (Hashable): 因为绝对不可变,它是唯一能作为 Dict 键 (Key) 或存入 Set 的序列(List 绝对不行)。
    • 多维状态记录: BFS 搜索时记录坐标 visited.add((r, c));DP 记忆化 memo[(idx, weight)] = res
  • 操作与解包:
    • 极速解包: r, c = (0, 1) (多变量同时赋值)
    • 索引与切片: t[0], t[::-1] (与列表行为一致,但不能修改元素)
1
2
3
4
# enumarate 同时获取索引和值
arr = ['a', 'b', 'c']
for i, val in enumerate(arr):
 print(f"Index: {i}, Value: {val}")

6. 容器互相转换矩阵

转换方向 语法 说明
List -> Set set(arr) 瞬间去重,常用于降维打击 $O(N)$ 查找
Set -> List list(s) 集合转回数组,常用于去重后排序
String -> List list("abc") 变为 ['a', 'b', 'c'],因为字符串不可变,需转列表修改
List -> String "".join(arr) 高效拼接,arr 内部必须全是字符串元素
Dict -> List list(d.keys())
list(d.values())
分别提取字典的键或值构成列表
List <-> Tuple tuple(arr)
list(t)
列表转元组以使其可哈希(存入 Set/Dict),或元组转列表以修改内容

二、 ACM 模式标准输入输出

处理国内笔试题(如牛客网)必备,避免 input() 导致超时或读取异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import sys

# 1. 万能读取法 (应对空格、换行混乱的输入)
# 将所有输入全部读入,打平为一个一维列表,后续通过迭代器或指针逐步提取
data = sys.stdin.read().split()
if not data: exit()
iterator = iter(data)
n = int(next(iterator)) # 提取第一个数

# 2. 单行读取 (极速版)
def get_ints():
 return list(map(int, sys.stdin.readline().split()))

# 3. 矩阵读取 (读取 n 行 m 列)
n, m = get_ints()
matrix = [get_ints() for _ in range(n)]

# 4. 高效输出
# print 在大量输出时很慢,改用 sys.stdout.write
sys.stdout.write(" ".join(map(str, result_list)) + "\n")

三、 高频核心数据结构 (collections & heapq)

Python 循环结构极简速查

1. for 循环 (基于 range)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 基础遍历 [0, n-1]
for i in range(n): pass

# 区间遍历 [1, n-1]
for i in range(1, n): pass

# 倒序遍历 [n-1, 0] (左闭右开,步长-1)
for i in range(n - 1, -1, -1): pass

# 步长遍历 (每次+2)
for i in range(0, n, 2): pass

避坑:Python 中在 for 循环内修改 i 无效,会被下一轮自动重置。如需动态调整指针必须用 while

2. 数据遍历 (无索引依赖)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. 仅遍历值
for val in arr: pass

# 2. 同时获取索引和值 (高频)
for i, val in enumerate(arr): pass

# 3. 多序列并行遍历 (以最短为准)
for v1, v2 in zip(arr1, arr2): pass

# 4. 集合遍历 (无序)
for x in my_set: pass

# 5. 字典遍历
for k in my_dict: pass # 仅遍历键 (默认行为)
for v in my_dict.values(): pass # 仅遍历值
for k, v in my_dict.items(): pass # 同时遍历键值 (高频)
# 注意:严禁在遍历 Set/Dict 时修改其大小 (add/remove/pop),否则报错

3. while 循环

1
2
3
4
5
6
7
# 1. 标准边界控制 (如二分查找)
while left <= right: pass

# 2. 容器判空 (代替 C++ 的 !q.empty())
# 空列表 []、空集合 set() 均等价于 False
while q:
 node = q.popleft()

4. 控制流特有语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# for...else / while...else 结构 (省去 bool 标记)
for x in arr:
 if x == target:
 break # 触发 break,直接跳出,不执行 else
else:
 # 循环自然跑完(未被 break 中断)时执行
 return "Not Found"

# 基础控制符
continue # 跳过本次循环剩余代码,进入下一次
break # 强制跳出当前所在的一层循环
pass # 空语句占位符 (等价于 C++ 的 {})

5. 常用数据结构操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 双端队列 (BFS 核心)
from collections import deque
q = deque([1, 2])
q.append(3) # 尾部入队 O(1)
curr = q.popleft() # 头部出队 O(1) 

# 2. 自动初始化字典 (图论建图、归类核心)
from collections import defaultdict
adj = defaultdict(list) # 默认值为 []
adj[0].append(1) # 免去 if 0 not in adj 的判断

# 3. 词频统计器
from collections import Counter
cnt = Counter("anagram") # {'a': 3, 'n': 1, 'g': 1, 'r': 1, 'm': 1}

# 4. 堆 / 优先队列 (Dijkstra、TopK 核心)
import heapq
arr = [3, 1, 2]
heapq.heapify(arr) # 原地建小顶堆 O(N)
heapq.heappush(arr, 0) # 插入 O(log N)
min_val = heapq.heappop(arr) # 弹出最小值 O(log N)
heapq.heappushpop(arr, 4) # 插入后弹出最小值 O(log N)
# 大顶堆技巧:压入 -x,弹出时再取负

四、 内置函数与语法糖

  • 极值与数学: float('inf'), float('-inf')pow(a, b, mod) 快速幂取模算法(比自己手写快)。divmod(a, b) 同时返回商和余数。
  • ASCII 转换: ord('a') -> 97, chr(97) -> ‘a’。
  • 字符串高频处理:
    • s.split() (默认处理所有连续空白符并丢弃空串)。
    • s.isalnum() (判断是否全为字母或数字,回文串常用)。
    • s.find(sub) (找子串,找不到返回 -1,严禁使用 index() 因为会抛异常报错)。
  • 高阶遍历:
    • for i, val in enumerate(arr): (同时拿索引和值)。
    • for x, y in zip(arr1, arr2): (双数组齐头并进)。
  • 排序:
    • arr.sort() # 默认升序,原地排序,适用于数字/字符串
    • arr.sort(reverse=True) # 降序排序
    • arr.sort(key=lambda x: x[0]) # 按元素的第一个字段排序(如区间、元组、二维数组)
    • arr.sort(key=lambda x: (x[0], -x[1])) # 多条件排序:优先按第一元素升序,相同则按第二元素降序
    • sorted(arr) # 返回新排序后的列表,不改变原数组
    • sorted(arr, key=..., reverse=...) # 一切排序技巧均可用
    • 例:intervals.sort(key=lambda x: x[0]) # 区间按左端点排序,区间合并/覆盖问题高频
  • 三元运算符(条件表达式):
    • 语法:a if 条件 else b (等价于 C/C++/Java 的 条件 ? a : b
    • 示例:res = x if x > 0 else -x # 取绝对值

五、 常用算法代码骨架

1. 二分查找 (标准库版)

自带的 bisect 库是 $O(\log N)$,直接替代手写二分。

1
2
3
4
5
6
import bisect
arr = [1, 2, 2, 4]
# 找第一个 >= x 的位置 (左侧插入点)
idx_left = bisect.bisect_left(arr, 2) # 返回 1
# 找第一个 > x 的位置 (右侧插入点)
idx_right = bisect.bisect_right(arr, 2) # 返回 3

2. 图论 BFS 模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
step = 0
visited = set([start])
q = deque([start])
while q:
 size = len(q)
 for _ in range(size): # 按层扩展
 curr = q.popleft()
 if curr == target: return step
 for nxt in graph[curr]:
 if nxt not in visited:
 visited.add(nxt)
 q.append(nxt)
 step += 1

3. 并查集 (Union-Find)

 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
class UnionFind:
 def __init__(self, size):
 # 初始化:每个人的老总都是自己
 self.parent = [i for i in range(size)]
 # 记录以 i 为根的集合的大小,初始都是 1
 self.size = [1] * size
 # 连通分量的数量(有多少个独立的帮派)
 self.count = size

 def find(self, i):
 # 路径压缩 (Path Compression)
 if self.parent[i] != i:
 self.parent[i] = self.find(self.parent[i])
 return self.parent[i]

 def union(self, i, j):
 root_i = self.find(i)
 root_j = self.find(j)

 # 已经是同一个老总,无需合并
 if root_i == root_j:
 return False

 # 按大小合并 (Union by Size):小树挂在大树下面
 if self.size[root_i] < self.size[root_j]:
 self.parent[root_i] = root_j
 self.size[root_j] += self.size[root_i]
 else:
 self.parent[root_j] = root_i
 self.size[root_i] += self.size[root_j]

 # 合并成功,独立帮派数量减一
 self.count -= 1
 return True

 def is_connected(self, i, j):
 # 判断两人是否连通
 return self.find(i) == self.find(j)

4. DPS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def dfs_template(node, state_var):
 # 1. 递归终止条件 (Base Case)
 # 遇到空节点、越界,或者满足特定结算条件时,立刻向上层返回
 if not node:
 return
 if is_target_reached(node):
 process_result(node, state_var)
 return

 # 2. 处理当前层逻辑 (Process Current Node)
 # 根据题意,修改状态变量或记录当前节点的信息
 update_state(state_var, node)

 # 3. 递归下探 (Drill Down)
 # 遍历所有可能的下一个节点(如树的左右子树,图的邻居节点)
 # 注意:下探时必须传递更新后的状态变量
 dfs_template(node.left, next_state(state_var))
 dfs_template(node.right, next_state(state_var))

 # 4. 恢复当前层状态 (Restore State / Backtrack) - 可选
 # 如果 state_var 是全局变量或可变对象(如列表),在回溯时需要撤销第2步的修改
 # 如果 state_var 是按值传递(如数字相加),则不需要此步
 revert_state(state_var, node)

5. 01背包

 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
def zero_one_knapsack(weights, values, capacity):
 n = len(weights)

 # 1. 定义与初始化 DP 数组
 # dp[j] 表示:当背包容量为 j 时,能获得的最大价值(或最小代价)
 # 如果求最大值,初始化为 0;如果求最小值,初始化为 float('inf')
 dp = [0] * (capacity + 1)

 # base case: 容量为 0 时,价值为 0
 dp[0] = 0

 # 2. 外层循环:遍历每一个物品
 for i in range(n):
 w = weights[i]
 v = values[i]

 # 3. 内层循环:遍历背包容量(核心难点:必须倒序!)
 # 倒序的原因:dp[j] 的更新依赖于上一层状态的 dp[j-w]。
 # 如果正序遍历,dp[j-w] 会在当前物品这一层被提前更新,导致同一个物品被重复放入(这就变成了完全背包问题)。
 for j in range(capacity, w - 1, -1):

 # 4. 状态转移方程 (State Transition)
 # 决策:是不放这个物品(保持 dp[j]),还是腾出 w 的空间放入这个物品(dp[j - w] + v)?
 # 根据题意取 max 或 min
 dp[j] = max(dp[j], dp[j - w] + v)

 # 5. 返回最终状态
 return dp[capacity]

树图什么的

现实校准:LeetCode 考什么,不考什么

在你听到“红黑树”、“B+树”等高大上的名词时,需要首先明确工业界底层开发与 LeetCode 算法应试的绝对分界线

  • 红黑树 (Red-Black Tree) / AVL 树: 它们是自平衡二叉搜索树,为了解决普通二叉搜索树退化成链表的问题而生。但在 LeetCode 面试中,几乎绝对不会让你从零手写一棵红黑树(代码量极大且极易出错,通常需要几百行严密的旋转逻辑)。如果你在题目中需要用到自平衡树的特性(即保持动态有序并支持 $O(\log N)$ 查找),在 C++ 中直接使用 std::set / std::map,在 Java 中使用 TreeMap,在 Python 中则依赖第三方库 sortedcontainers 或使用 bisect 模块结合列表模拟。
  • LeetCode 的核心靶点: 是利用基础的二叉树、二叉搜索树 (BST)、字典树 (Trie) 以及基础图论(无向图/有向图的遍历、拓扑排序),来考察你的递归思维 (DFS)层级思维 (BFS)

以下是为你剥离冗余后,针对 LeetCode 场景的树与图 Cheat Sheet


1. 二叉搜索树 (BST - Binary Search Tree)

辩证本质: 树形结构中的“二分查找”。它将线性数组的查找效率从 $O(N)$ 降维至 $O(\log N)$,但代价是每次插入/删除都需要动态维护其严格的大小关系。

  • 核心物理规则: 对于任何一个节点,其左子树所有节点的值必小于该节点;其右子树所有节点的值必大于该节点。
  • 最强推论(必考点): 对 BST 进行中序遍历 (Inorder Traversal:左-根-右),得到的必定是一个严格递增的有序数组
  • 经典题型: 验证二叉搜索树(98)、二叉搜索树中第K小的元素(230)、修剪二叉搜索树(669)。

Python 模板:利用中序遍历验证 BST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class TreeNode:
 def __init__(self, val=0, left=None, right=None):
 self.val = val
 self.left = left
 self.right = right

def isValidBST(root: TreeNode) -> bool:
 # 辩证点:不要单纯只判断左孩子小于根、右孩子大于根。
 # 必须传递一个不断收紧的上下界,确保整棵子树都符合规则。
 def dfs(node, lower, upper):
 if not node:
 return True
 if node.val <= lower or node.val >= upper:
 return False
 # 往左走,上限变成当前节点的值;往右走,下限变成当前节点的值
 return dfs(node.left, lower, node.val) and dfs(node.right, node.val, upper)

 return dfs(root, float('-inf'), float('inf'))

2. 字典树 / 前缀树 (Trie)

辩证本质: 典型的“空间换时间”。通过将字符串拆解为字符并构建多叉树,将海量字符串的匹配时间复杂度,从 $O(N \times L)$ 极致压缩到 $O(L)$($L$ 为单个单词的长度),代价是需要极其庞大的节点内存。

  • 核心特征词: “前缀匹配”、“搜索联想”、“单词搜索”、“异或最大值(01字典树)”。
  • 物理结构: 根节点为空,每个节点包含一个字典(或长度为 26 的数组)指向下一个字符,以及一个布尔标志位表示“是否为单词结尾”。
  • 经典题型: 实现 Trie (前缀树)(208)、单词搜索 II(212 - Trie + DFS 的终极杀器)。

Python 模板:Trie 的标准实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TrieNode:
 def __init__(self):
 self.children = {} # 存储子节点,例如 {'a': TrieNode, 'b': TrieNode}
 self.is_end = False # 标识此处是否构成一个完整的单词

class Trie:
 def __init__(self):
 self.root = TrieNode()

 def insert(self, word: str) -> None:
 node = self.root
 for char in word:
 if char not in node.children:
 node.children[char] = TrieNode()
 node = node.children[char]
 node.is_end = True

 def search(self, word: str) -> bool:
 node = self.root
 for char in word:
 if char not in node.children:
 return False
 node = node.children[char]
 return node.is_end # 必须是单词结尾才算找到

3. 图 (Graph) 基础:无向图 vs 有向图

辩证本质: 树其实是图的一种极度受限的特例(树是没有环的连通无向图)。图抛弃了“父子关系”的严格层级,允许数据之间存在任意的网状联系。

  • 无向图 (Undirected Graph): 边是双向的。A 认识 B,B 必然认识 A。
    • 核心陷阱: 极易形成死循环死锁(如 A 走向 B,B 又走向 A)。
    • 破解之道: 无论 DFS 还是 BFS,必须使用 visited 集合来记录走过的节点,坚决不走回头路。
  • 有向图 (Directed Graph): 边是单向的。A 关注了 B,B 不一定关注 A。
    • 核心考点: 成环检测拓扑排序

4. 拓扑排序 (Topological Sort) - 针对有向无环图 (DAG)

辩证本质: 将一个存在依赖关系的网状图,拉平为一条线性的执行序列。如果图中存在环(如 A 依赖 B,B 依赖 A),则拓扑排序必然失败(发生死锁)。

  • 核心特征词: “课程表”、“编译依赖”、“任务调度”、“是否存在先后矛盾”。
  • 物理模型 (Kahn 算法):
    1. 统计所有节点的入度 (In-degree)(即有多少条边指向它,代表它被多少个前置条件卡着)。
    2. 将所有入度为 0 的节点(没有任何前置依赖的任务)放入队列。
    3. 弹出节点执行,并将其指向的所有邻居节点的入度减 1。
    4. 如果邻居入度降为 0,则加入队列。
  • 经典题型: 课程表(207)、课程表 II(210)。

Python 模板:利用 BFS (Kahn算法) 实现拓扑排序

 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
from collections import deque, defaultdict

def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
 # 1. 初始化邻接表和入度数组
 graph = defaultdict(list)
 in_degree = [0] * numCourses

 # 2. 构建图 (prerequisites[i] = [a, b] 表示选 a 必须先学 b,即 b -> a)
 for course, pre in prerequisites:
 graph[pre].append(course)
 in_degree[course] += 1

 # 3. 将所有入度为 0 的节点(即不需要任何先修课的课程)入队
 queue = deque([i for i in range(numCourses) if in_degree[i] == 0])

 # 4. 记录已完成的课程数量
 completed_count = 0

 while queue:
 node = queue.popleft()
 completed_count += 1

 # 将依赖该课程的所有后续课程入度减 1
 for neighbor in graph[node]:
 in_degree[neighbor] -= 1
 # 一旦前置依赖全部解除,加入队列准备学习
 if in_degree[neighbor] == 0:
 queue.append(neighbor)

 # 辩证点:如果完成的数量等于总课程数,说明不存在环,可以学完。
 # 如果最后还有课程没学完(入度死活降不到0),说明图里必定存在互相依赖的“死锁环”。
 return completed_count == numCourses

六、 根据数据范围推断时间复杂度 (防超时指南)

机试时,先看题目给定的变量范围 $N$,直接反推该用什么算法:

  • $N \le 20$: 回溯法、状态压缩 DP $\rightarrow O(2^N)$ 或 $O(N!)$
  • $N \le 400$: 三重循环、Floyd 算法 $\rightarrow O(N^3)$
  • $N \le 2000$: 两重循环、普通 DP $\rightarrow O(N^2)$
  • $N \le 10^5$: 排序、堆、二分查找、线段树 $\rightarrow O(N \log N)$
  • $N \le 10^6$: 双指针、滑动窗口、单调栈、哈希表 $\rightarrow O(N)$
  • $N \ge 10^9$: 数学规律、快速幂、二分答案 $\rightarrow O(\log N)$ 或 $O(1)$

七、Leetcode Python 数据结构速成

已完成联网核对。前文提到的“有效的字母异位词”题号有误(LeetCode 24 实际为“两两交换链表中的节点”,242 才是“有效的字母异位词”),下表已修正。

这是一份纯粹用于检验 Python 数据结构熟练度的闭卷通关表:

题目号及名称 核心知识点 闭卷 AC 验收标准
125. 验证回文串 字符串过滤、切片翻转 熟练使用推导式 [c.lower() for c in s if c.isalnum()],用切片 s == s[::-1] 一行判断,不写双指针。
349. 两个数组的交集 Set 的初始化与交集运算 严禁手写两层循环,必须一句话搞定:return list(set(nums1) & set(nums2))
128. 最长连续序列 Set 的 $O(1)$ 极速查找 将数组转为 set,彻底理解并利用 num in num_set 的 $O(1)$ 特性防超时。
242. 有效的字母异位词 词频统计 (Counter) 严禁手写循环统计,直接引入 Counter,使用 return Counter(s) == Counter(t) 一行秒杀。
49. 字母异位词分组 defaultdict、Tuple 作为哈希键 熟练写出 d = defaultdict(list),深刻理解必须将排序后的字符串转为 tuple 才能作为字典的 key。
169. 多数元素 字典遍历 / API 获取极值 熟练手写字典遍历寻找最大值,或直接使用 Counter(nums).most_common(1)[0][0]
20. 有效的括号 List 作为栈的使用 熟练使用 append()pop(),并用静态字典 {')': '(', ']': '[', '}': '{'} 优雅映射替代大量 if-else
102. 二叉树的层序遍历 deque 实现 BFS 倒背如流 q = deque([root])q.popleft(),严禁在此处使用 list.pop(0)
200. 岛屿数量 多维坐标的 Tuple 打包与 Set 去重 在二维 BFS/DFS 中,极其熟练地将坐标打包成元组存入访问记录:visited.add((r, c))
215. 数组中的第K个最大元素 heapq 核心 API 熟练使用 heapq.heapify 以及维护大小为 $K$ 的小顶堆(heappushheappop)。
347. 前 K 个高频元素 大顶堆技巧与复杂结构嵌套 结合 Counter 统计频率,并熟练写出“取负数塞入小顶堆模拟大顶堆”的操作:heappush(hp, (-freq, num))
56. 合并区间 列表的高阶排序 (lambda) 不看资料直接写出按左端点升序排列的定制规则:intervals.sort(key=lambda x: x[0])

【本文为 AI 生成】Hugo + Pjax 实现无刷新博客体验:从音乐播放中断谈起

2026-03-11 14:23:00

Featured image of post 【本文为 AI 生成】Hugo + Pjax 实现无刷新博客体验:从音乐播放中断谈起

前言

在搭建个人博客的过程中,我一直有一个执念:希望网页底部的音乐播放器能够像网易云音乐那样,在页面切换时永不中断

传统的静态博客(如 Hugo 生成的站点)每一次点击链接,浏览器都会重新加载整个页面 (Full Page Reload)。这意味着:

  1. DOM 树被销毁重建。
  2. 所有 JavaScript 状态丢失。
  3. 音频/视频标签被重置 —— 这就是为什么音乐会停。

为了解决这个问题,我们需要引入 SPA (Single Page Application) 的概念,或者更轻量级的方案 —— Pjax (PushState + Ajax)

核心技术方案:Pjax

Pjax 的工作原理非常直观:

  1. 拦截 <a> 标签的点击事件。
  2. 使用 Ajax 请求新页面的 HTML 内容。
  3. 解析新 HTML,只提取我们需要更新的部分(例如主要内容区 .main-container)。
  4. 使用 history.pushState 修改浏览器的 URL地址栏,使其看起来像正常跳转。
  5. 替换 DOM 中的内容区。

通过这种方式,页脚 (Footer)侧边栏 (Sidebar) 可以保持不变,驻留在其中的音乐播放器自然也就不会中断了。

1. 引入 Pjax

首先在 <head> 中引入 Pjax 库(推荐使用 pjax 库而非老旧的 jquery-pjax):

1
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>

2. 定制化配置 (The Tricky Part)

这是最关键的一步。为了保证 Stack 主题的正常渲染,如果你直接替换整个 body,播放器还是会挂掉。我们需要精准打击

我在 layouts/partials/head/custom.html 中进行了如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var pjax = new Pjax({
 selectors: [
 "title",
 ".main-container", // 只替换内容区!
 "body" // 这里的处理很有讲究,见下文
 ],
 switches: {
 "body": function(oldEl, newEl, options) {
 // 我们只更新 body 的 class (用于切换暗色模式或页面特定样式)
 // 绝不替换 body 的 innerHTML,否则页脚脚本会被杀掉
 oldEl.className = newEl.className;
 },
 ".main-container": Pjax.switches.innerHTML, // 标准替换
 "title": Pjax.switches.outerHTML
 }
});

关键点:不仅要通过 CSS 选择器指定更新区域,还要自定义 switch 函数,确保 body 标签只更新属性而不重置内容。

踩坑与填坑

实现 Pjax 只是第一步,真正的挑战在于副作用

坑一:脚本不执行 (Mastodon 动态消失)

现象:跳转到 Timeline 页面,Mastodon 动态加载不出来。 原因:通过 innerHTML 插入的 HTML 片段中如果包含 <script> 标签,浏览器出于安全和规范考虑,通常不会执行它们

解决方案: 我们将初始化代码封装为全局函数,并在 Pjax 完成事件 (pjax:complete) 中手动调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Check & Init Mastodon Logic
window.initMastodon = function() {
 if (!document.getElementById('mt-container')) return;
 // ... 初始化代码 ...
};

// 监听 Pjax 完成
document.addEventListener('pjax:complete', function () {
 window.initMastodon();
 // 其他需要重载的脚本,如 Google Analytics
 if (typeof gtag === 'function') {
 gtag('config', 'MEASUREMENT_ID', {'page_path': location.pathname});
 }
});

坑二:播放器状态丢失

现象:虽然使用了 Pjax,但用户有时会习惯性按 F5 刷新,或者 Pjax 请求超时回退到普通跳转,这时候音乐还是会断,且进度归零。

解决方案:状态持久化 (State Persistence)。

利用 localStorage 在播放器每秒更新时记录状态:

1
2
3
4
5
6
7
setInterval(() => {
 if (!ap.audio.paused) {
 localStorage.setItem('aplayer_time', ap.audio.currentTime);
 localStorage.setItem('aplayer_index', ap.list.index);
 localStorage.setItem('aplayer_paused', 'false');
 }
}, 1000);

在页面加载时(无论是 Pjax 还是普通加载),尝试恢复状态:

1
2
3
4
5
const savedTime = localStorage.getItem('aplayer_time');
if (savedTime) {
 ap.seek(parseFloat(savedTime));
 if (savedPaused === 'false') ap.play();
}

这里还有一个细节:audio 元素必须在元数据加载后才能 seek,所以需要监听 loadedmetadatacanplay 事件。

总结

通过引入 Pjax 并配合精细的生命周期管理,我们成功在静态博客上实现了类似 SPA 的流畅体验:

  1. 音乐不间断:Footer 区域脱离了页面刷新的生命周期。
  2. 加载极速:只请求部分 HTML,带宽消耗更低。
  3. 体验降级:即使 Pjax 失败,完善的状态恢复机制也能保证用户体验不割裂。

折腾博客的乐趣往往不在于写文章本身,而在于通过解决这些具体的技术问题,窥探现代 Web 开发的冰山一角。

面向外企就业的 LeetCode/NeetCode 记录

2026-03-10 10:24:00

Featured image of post 面向外企就业的 LeetCode/NeetCode 记录

LeetCode 刷题方法

路线图

算法清单

外企基石,必须闭眼手撕:

  • 哈希表
  • 双指针
  • 滑动窗口
  • 二分查找
  • 链表
  • 二叉树(含二叉搜索树 BST)

工程进阶,拉开差距的关键:

  • 回溯算法(排列/组合/子集)
  • 堆/优先队列
  • 栈与队列
  • 图论基础(仅限 BFS / DFS / 拓扑排序)

高级护城河,面试冲刺期再看:

  • 动态规划(仅限 1D DP 和基础背包/最长公共子序列)
  • 字典树 (Trie)
  • 并查集 (Union-Find)
  • 前缀和技巧

题单

设计模式

1. 单例模式 (Singleton) —— C++ 面试必考八股

  • 这是什么:保证一个类只有一个实例,并提供一个全局访问点。
  • 外企考点:不仅会让你手写,还会结合 C++ 问你“线程安全的单例怎么写?”(Meyers Singleton,利用局部静态变量的特性)。
  • 如何融合进你的项目:你的 GPU 监控探针(Agent)运行在 5090 服务器上,它需要读取配置信息(比如本机的 IP、监控频率)。这个 ConfigManager 配置管理器,就必须是一个单例。你在简历上面试时可以说:“为了避免多次读取配置文件造成 I/O 浪费,我用 C++11 的 std::call_once(或局部静态变量)实现了一个线程安全的单例配置中心。”

2. 观察者模式 (Observer) —— 监控系统的灵魂

  • 这是什么:一个对象状态改变,所有依赖它的对象都会收到通知。(也就是发布-订阅机制)。
  • 外企考点:解耦。面试官常问:“如果我加一个新的监控维度,你的代码需要大改吗?”
  • 如何融合进你的项目:你的 5070 Ti 是一直在变化的(温度忽高忽低,显存忽大忽小)。你可以把本地的显卡作为一个 Subject(被观察者),把负责发送网络请求的模块、负责写本地日志的模块作为 Observer(观察者)。显存一旦超过阈值,主动“通知”这些模块报警,而不是让它们写个 while(true) 死循环去轮询。

3. 工厂模式 (Factory) —— 告别满屏的 if-else

  • 这是什么:把创建对象的具体逻辑隐藏起来,提供一个统一的接口。
  • 外企考点:考察你对开闭原则(对扩展开放,对修改封闭)的理解。
  • 如何融合进你的项目:你的调度系统未来不仅能收集 5090 的数据,可能还要收集 AMD 显卡,甚至普通 CPU 的数据。写一个 DeviceFactory,传入 “NVIDIA”,它就吐出一个调用 NVML API 的对象;传入 “CPU”,它就吐出一个读取 /proc/cpuinfo 的对象。

4. 策略模式 (Strategy) —— 调度系统的核心

  • 这是什么:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。
  • 外企考点:极其高频的设计题。比如“设计一个打车软件的计费模块”。
  • 如何融合进你的项目:你的核心是算力调度。怎么调度?你可以有“轮询策略(Round Robin)”、“最低显存优先策略(Least Loaded)”。把这些策略抽象成接口,运行时动态插拔。这就是顶级的基础设施架构。

刷题记录

NeetCode Roadmap

Contains Duplicate (LeetCode 217)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 最经典的哈希表题。用一个 Set 来记录我们见过的数字,一边遍历一边查这个数字在不在 Set 里。
class Solution:
 def containsDuplicate(self, nums: List[int]) -> bool:
 seen = set() # 创建一个空的哈希集合

 for num in nums:
 if num in seen: # 查字典:这个数字我之前见过吗?
 return True # 见过!直接返回 True
 seen.add(num) # 没见过,把它记在小本本上

 return False # 全都查完了也没重复,返回 False

# time complexity:O(n)
# space complexity:O(n)
1
2
3
4
5
6
7
# Pythonic 的一行解法,利用 set() 去重特性,直接比较原数组长度和去重后 Set 的长度。
class Solution:
 def containsDuplicate(self, nums: List[int]) -> bool:
 return len(nums) != len(set(nums))

# time complexity:O(n)
# space complexity:O(n)
  • 如果你的数组非常大,且重复元素大概率出现在很靠前的位置,解法 1 更优,因为它能“及时止损”。
  • 如果你的数组没有重复元素,或者重复元素在最后面,解法 2 更优,因为底层 C 语言的执行速度会碾压 Python 的 for 循环。
summary:
  1. 时间复杂度 (Time Complexity)
  • $O(1)$ 常数级:终极目标。代码特征:直接通过索引取值 nums[0],或者在字典/集合中查值 if key in my_dict:
  • $O(\log n)$ 对数级:极快。代码特征:二分查找。每次操作砍掉一半(如 while left < right: 配合 mid)。
  • $O(n)$ 线性级:外企最爱的标准解法。代码特征:一层无嵌套的 for 循环,从头到尾扫一遍。
  • $O(n \log n)$:通常是排序的极限。代码特征:代码里调用了 nums.sort(),或者用了归并/快速排序。
  • $O(n^2)$ 平方级:面试危险信号。代码特征:两层嵌套的 for 循环。面试官通常会让你把它优化到 $O(n)$。
  1. 空间复杂度 (Space Complexity)
  • $O(1)$:只新建了几个变量(如双指针 left, right),没有开辟随数据规模增大的新空间。
  • $O(n)$:新建了一个和原数组一样大的字典(哈希表)、集合(Set)或新数组。外企极度喜欢用空间换时间。
  1. Pythonic 循环

抛弃 for(int i=0; i<n; i++),只记以下三种:

  • 只取值for num in nums:
  • 只取索引for i in range(len(nums)):
  • 既要索引又要值for i, num in enumerate(nums):
  1. 核心标准库 (绝不重复造轮子)
  • collections.Counter (计数器)
  • 场景:完成词频/元素频率统计。直接替代 count[i] = count[i] + 1 循环。
  • 用法
1
2
from collections import Counter
count = Counter(nums)
  • collections.deque (双端队列)
  • 场景:做 BFS(广度优先搜索)必用。两头增删都是 $O(1)$,绝不用普通 list 做队列。
  • 用法
1
2
3
4
from collections import deque
q = deque([1, 2])
q.append(3) # 尾部进队
q.popleft() # 头部出队
  • heapq (优先队列 / 堆)
  • 场景:解决 “Top K” (前K个最大/最小) 问题。替代 C++ 的 std::priority_queue
  • 用法
1
2
3
4
import heapq
heapq.heapify(nums) # $O(n)$ 原地建堆
heapq.heappush(nums, 2) # 压入元素
val = heapq.heappop(nums) # 弹出最小值
Valid Anagram (LeetCode 242)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

## 1
class Solution:
 def isAnagram(self, s: str, t: str) -> bool:
 if len(s)!=len(t):
 return False
 temp = [0] * 26

 for i in range(len(s)):
 temp[ord(s[i])-ord('a')]=temp[ord(s[i])-ord('a')]+1
 temp[ord(t[i])-ord('a')]=temp[ord(t[i])-ord('a')]-1

 for i in range(26):
 if temp[i]!=0:
 return False

 return True

# time complexity:O(m+n)
# space complexity:O(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 2 神奇 Python 小函数

from collections import Counter
return Counter(s) == Counter(t)

# time complexity:O(m+n)
# space complexity:O(1)

# 3 或者

return sorted(s) == sorted(t)

# time complexity:O(nlogn+mlogm)
# space complexity:O(1)/O(n+m)
summary:
  1. ord(char) —— 字符转索引
  • 用法index = ord(char) - ord('a')
  • 场景:将 ‘a’-‘z’ 映射到 0-25,配合数组做计数。
  • 相关用法chr(code)ord() 的逆运算,如 chr(97) 返回 'a',常用于将计算后的索引还原为字符。
  1. sorted(s) —— 排序法
  • 用法return sorted(s) == sorted(t)
  • 场景:快速验证变位词(Anagram)。两个字符串排序后应完全一致。
  • 相关用法list.sort(reverse=True)sorted() 返回新列表,支持所有可迭代对象;list.sort() 是列表的原地排序(无返回值)。
  1. Counter —— 词频统计
  • 用法return Counter(s) == Counter(t)
  • 场景:工程代码中一行解决词频统计,底层是 Hash Map。
  • 相关用法Counter(s).most_common(k)。返回前 k 个高频元素,解决 Top K 问题的神器。
  1. [0] * n —— 数组快速初始化
  • 用法count = [0] * 26
  • 场景:建立固定长度的计数桶(Bucket),比 dict 更快更省空间。
  • 相关用法[[0] * n for _ in range(m)]。构建二维数组(矩阵)的正确写法,必须用列表推导式以避免引用拷贝错误。
Two Sum (LeetCode 1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 直接暴力双循环遍历
class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 for i in range(len(nums)):
 for j in range(i + 1, len(nums)):
 if nums[i] + nums[j] == target:
 return [i, j]
 return []
# time complexity:O(n^2)
# space complexity:O(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 双指针排序

class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 A = []
 for i, num in enumerate(nums):
 A.append([num, i])

 A.sort()
 i, j = 0, len(nums) - 1
 while i < j:
 cur = A[i][0] + A[j][0]
 if cur == target:
 return [min(A[i][1], A[j][1]),
 max(A[i][1], A[j][1])]
 elif cur < target:
 i += 1
 else:
 j -= 1
 return []

# time complexity:O(nlogn)
# space complexity:O(n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Hash Map 两遍遍历
class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 indices = {} # val -> index

 for i, n in enumerate(nums):
 indices[n] = i

 for i, n in enumerate(nums):
 diff = target - n
 if diff in indices and indices[diff] != i:
 return [i, indices[diff]]
 return []
# time complexity:O(n)
# space complexity:O(n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Hash Map 一遍遍历
class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 prevMap = {} # val -> index

 for i, n in enumerate(nums):
 diff = target - n
 if diff in prevMap:
 return [prevMap[diff], i]
 prevMap[n] = i

# time complexity:O(n)
# space complexity:O(n)
summary:
  1. nums.index(val) —— 列表查找
  • 用法idx = nums.index(target - n)
  • 场景:查找值对应的索引。面试大忌:在 for 循环里用它,时间复杂度直接退化为 $O(n^2)$。因为它底层是线性扫描。
  • 相关用法hash_map[key]。面试官想看的是用哈希表将查找降维到 $O(1)$。
  1. Two Pointers —— 双指针法
  • 用法l, r = 0, len(nums) - 1,配合 while l < r
  • 场景仅适用于已排序数组。如果是乱序数组求 Two Sum,排序需 $O(n \log n)$ 且会打乱原始索引(需额外记录),不如哈希表法 $O(n)$ 优。
  • 相关用法nums.sort()。双指针的前置条件通常是数组有序。
  1. range(i + 1, n) —— 内层循环起点
  • 用法for j in range(i + 1, len(nums)):
  • 场景:暴力解法(Brute Force)中,内层循环必须从 i + 1 开始。
  • 相关意义:两层含义:一是避免自己加自己(题目限制);二是去重,避免计算了 (A, B) 又计算 (B, A)
  1. enumerate(nums) —— 枚举遍历
  • 用法for i, n in enumerate(nums):
  • 场景:需要同时获取索引 (index) 和值 (value)。这是 Two Sum 这类题的标准起手式。
  • 相关用法range(len(nums))(只拿索引)、for n in nums(只拿值)。不要在需要索引时手动维护一个 count 变量,太土。
Group Anagrams (LeetCode 49)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# sorting 
class Solution:
 def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
 res = defaultdict(list)
 for s in strs:
 sortedS = ''.join(sorted(s))
 res[sortedS].append(s)
 return list(res.values())
# time complexity:O(m∗nlogn)
# space complexity:O(m*n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# hashmap
class Solution:
 def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
 res = defaultdict(list)
 for s in strs:
 count = [0] * 26
 for c in s:
 count[ord(c) - ord('a')] += 1
 res[tuple(count)].append(s)
 return list(res.values())

# time complexity:O(m*n)
# space complexity:O(m*n)
summary:
  1. tuple(mutable_obj) ——不仅是转换
  • 用法key = tuple(count)
  • 场景字典的 Key 必须是不可变类型 (Immutable)。列表 (List) 是可变的,不能作为 Key,必须转化为元组 (Tuple) 才能作为哈希表的键。
  • 相关用法frozenset。不可变集合,也可以做 Key。
  1. list(d.values()) —— 字典值导出
  • 用法return list(res.values())
  • 场景:题目只需要返回分好组的列表(List of Lists),不需要 Key。
  • 相关坑点:Python 3 中 d.values() 返回的是一个视图对象 (View),必须用 list() 强转才能符合题目要求的返回类型 List[List[str]]
  1. ''.join(sorted(s)) —— 字符串标准化
  • 用法key = "".join(sorted(s))
  • 场景:将字符串排序并重组为用作 Key 的不可变对象。sorted(s) 返回的是列表 ['a', 'e', 't'](可变,不可做 Key),必须用 join 拼回字符串 "aet"
  • 相关用法tuple(sorted(s))。如果不拼回字符串,转成元组也可以做 Key。
Top K Frequent Elements (LeetCode 347)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Counter + most_common
class Solution:
 def topKFrequent(self, nums: List[int], k: int) -> List[int]:
 counts=Counter(nums)
 t1=counts.most_common(k)
 t2=[]
 for i in t1:
 t2.append(i[0])
 return t2

# time complexity:O(nlogn)
# space complexity:O(n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Sorting

class Solution:
 def topKFrequent(self, nums: List[int], k: int) -> List[int]:
 count = {}
 for num in nums:
 count[num] = 1 + count.get(num, 0)

 arr = []
 for num, cnt in count.items():
 arr.append([cnt, num])
 arr.sort()

 res = []
 while len(res) < k:
 res.append(arr.pop()[1])
 return res

# time complexity:O(nlogn)
# space complexity:O(n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Min-Heap

class Solution:
 def topKFrequent(self, nums: List[int], k: int) -> List[int]:
 count = {}
 for num in nums:
 count[num] = 1 + count.get(num, 0)

 heap = []
 for num in count.keys():
 heapq.heappush(heap, (count[num], num))
 if len(heap) > k:
 heapq.heappop(heap)

 res = []
 for i in range(k):
 res.append(heapq.heappop(heap)[1])
 return res
# time complexity:O(nlogk)
# space complexity:O(n+k)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#Bucket Sort
class Solution:
 def topKFrequent(self, nums: List[int], k: int) -> List[int]:
 count = {}
 freq = [[] for i in range(len(nums) + 1)]

 for num in nums:
 count[num] = 1 + count.get(num, 0)
 for num, cnt in count.items():
 freq[cnt].append(num)

 res = []
 for i in range(len(freq) - 1, 0, -1):
 for num in freq[i]:
 res.append(num)
 if len(res) == k:
 return res
# time complexity:O(n)
# space complexity:O(n)
summary:
  1. count.get(num, 0) —— 字典计数初始化
  • 用法count[num] = 1 + count.get(num, 0)
  • 场景:统计频次时,若 key 首次出现就给默认值 0,避免 KeyError
  • 相关用法defaultdict(int)。效果等价,写法更短:count[num] += 1
  1. arr.pop()[1] —— 取“当前最大频次元素”
  • 用法:先把 [cnt, num] 放进数组并升序排序,再 arr.pop() 拿末尾最大频次对。
  • 含义arr.pop() 返回 [cnt, num][1] 取的是 num
  • 场景:Sorting 解法中每次弹出一个最高频元素,直到拿满 k 个。
  1. freq = [[] for _ in range(len(nums) + 1)] —— 桶数组初始化
  • 用法:创建 n + 1 个桶,索引代表“出现次数”,桶里存“出现该次数的数字”。
  • 为什么是 n + 1:元素最高频次可能是 n(全相同),所以要开到下标 n
  • 相关用法:二维数组初始化统一用列表推导式,避免 [[...]] * n 引用拷贝坑。
  1. Bucket Sort —— Top K 的线性解法
  • 流程:先 count 频次,再把数字丢进 freq[cnt],最后从高频桶倒序取数直到凑够 k
  • 复杂度:时间 O(n),空间 O(n)
  • 适用场景:数据量大、追求线性复杂度时优先。
  1. Heap (Min-Heap) —— Top K 的工程常用解法
  • 思路:维护一个大小为 k 的最小堆,堆里存 (freq, num)
  • 操作:每来一个新元素先 heappush,若堆超 kheappop,最终堆内即 Top K。
  • 复杂度:时间 O(n log k),空间 O(n + k);当 k << n 时常比全排序更优。

words list

  • integer 整数
  • distinct 不同的
  • duplicate 重复的
  • constraint 约束条件
  • approach 方法
  • time/space complexity 时间/空间复杂度
  • valid 有效的
  • anagram 字谜(回文构筑法)
  • indices 索引
  • assume 假设
  • pitfall 陷阱
  • Brute Force 暴力解法
  • sublist 子列表
  • group 分组
  • enumerate 枚举
  • Bucket Sort 桶排序
  • heap 优先队列,堆
  • min-heap 最小堆
  • such that 使得
  • algorithm 算法
  • discard 放弃
  • intersection 交集
  • union 并集
  • intuition 直觉
  • Throughout 自始至终
  • consecutive 连续的
  • sequence 序列
  • implement 实施
  • parenthesis 括号
  • binary tree 二叉树
  • traversal 遍历
  • level order traversal 层序遍历
  • interval 区间
  • overlap 重叠

tips

1. 语言与阵地

  • 语言死绑 Python:追求极简 API 和开发速度,避开 C++ 繁琐的模板代码。
  • 平台死绑美区:只在 LeetCode.com 刷,强迫适应全英文题目与约束条件。

2. 路线与开销

  • 唯一指定路线:只刷 NeetCode Roadmap。打通它 = 自动通关 Blind 75 + NeetCode 150。
  • 绝不打周赛:外企不考竞赛级偏门题,别浪费周末断联日。
  • 零氪金原则:绝不买 $297 NeetCode Premium。只在收到面试通知的前一个月,买 $35 LeetCode 官方会员突击公司专属题库 (Company Tags)。

3. 核心刷题 SOP (15分钟法则)

  • Step 1 (闭卷 15 分钟):拿到题自己想,没思路或写不出立刻停手,绝生死磕。
  • Step 2 (开卷偷师):看 NeetCode 免费视频,学 Python 模板和英文思路 (Think Out Loud)。
  • Step 3 (半闭卷手敲):关掉视频,凭记忆自己敲到 AC。允许随时查 Python API(如字典操作、内置函数),现阶段不要为默写语法浪费时间。

4. 兜底退路与国内大厂补丁 (Pivot 方案)

  • 唯一需要加练的(考前 1 个月突击):外企不考中间件的死记硬背。但如果在 2027 年春招/秋招前决定转面国内互联网业务岗,需额外花 1个月 狂背“国产特色八股文”:MySQL 底层(B+树索引)、Redis(缓存穿透/雪崩)、以及各类高并发锁机制。 仅作为临时补丁,勿在日常备战中分散精力。

5. NeetCode 的正确打开方式

NeetCode 的 Visualize the algorithm step by step 功能非常便于理解算法流程,至少对我来说,可视化地过一遍算法流程,就很清晰了。

链接

funny

Throughout heaven and earth I alone am the dumb one, I tried to solve this with trie.

2027秋招外企计划

2026-03-06 13:50:00

Featured image of post 2027秋招外企计划

前言

经历了一学期的研究生生活,lihan 变得保守了很多,也不再执着于读博与当教授,外企的技术岗是当前的首选目标了。兼顾当前课业与课题组压力以及身心健康,制定了一个目标明确的学习计划,计划的核心是提升自己的技术能力,兼顾项目开发与实习经历以及英语能力,以健康的身体和积极的心态迎接秋招。

计划 ———— Lihan 的外企核心 SDE 备战「自动机」执行纲领

🎯 终极目标 (The North Star): 2027年秋招/暑期实习斩获一线外企(Microsoft, Amazon, NVIDIA等)核心 Infra/后端 开发岗 Offer,实现 965 工作制、高时薪,拥有绝对的生活主导权。


📅 第一部分:宏观路由与异常接管 (Macro Roadmap)

主线任务由算法与 C++/AI Infra 工程双轮驱动。前端开发仅作为前置 3 天的工具测试,不占主线资源。

📌 前置点火任务:MVP 启动冲刺 (限时 3 天)

  • 任务:借助 AI IDE 快速完成一个基于 Next.js 的 Todo WebApp,仅用于记录本计划的状态流转。
  • 强制约束:限时 3 天。绝不系统性学习官方文档,超时一分钟也必须强制封板,立刻转入算法与 C++ 主线。

🗺️ 两年半推进时间线

  • 阶段一:基建与探路期 (研一下,至 2026 年 8 月)

  • 算法:Python 刷通 LeetCode 经典 150 题(按分类),训练全英文“边写边说”能力。

  • 工程:启动「异构 GPU 算力监控系统」。用 C++ 配合 NVML 写出轻量级数据采集探针,跑通本地与远端 5090 服务器的底层硬件状态拉取。

  • 🛑 异常接管 (期末突击):学期末提前划出 3 周 纯粹用于期末复习。此期间【高能输出】状态冻结,一切为了保住 GPA,考完立刻解冻。

  • 阶段二:并发与重构攻坚期 (研二上,2026.09 - 2027.02)

  • 算法:二刷错题本,定向突击目标外企高频题库。

  • 工程:引入 Python FastAPI 或 Go 重构通信网关,解决跨网络并发与数据持久化。

  • 🛑 异常接管:同样预留 3 周 应对研二上期末考试。

  • 阶段三:狙击与收网期 (研二下,2027.03 - 2027.09)

  • 动作:精修全英文简历,开启 BQ(行为面试)的英文 Mock Interview。海投外企暑期实习。

  • 终局:依靠 3 个月的暑期实习斩获 Return Offer,或携极具深度的 C++ 底层项目降维打击秋招。


⚙️ 第二部分:核心状态机引擎 (State Machine Logic)

你的每一天只会处于以下五种状态之一,绝不允许出现“边学边玩”的模糊中间态。

  • 🟢 状态 A [高能输出]: 全神贯注执行算法刷题与 C++/Python 硬核底层开发。断绝外部通讯。
  • 🔵 状态 B [防御敷衍]: 处理导师横向任务与学校日常课程。大脑降频,能水则水,绝不多花一分精力。
  • 🟡 状态 C [合法狂欢]: 当日设定的【高能输出】任务达标后自动触发。毫无负罪感地打游戏、看剧。
  • 🟣 状态 D [战略停机]: 提前规划好的不插电日(如周日)。去山里徒步摄影,彻底抽离。禁止临时起意的旷工。
  • 🔴 状态 E [兜底模式]: 极度疲惫时触发。完成“最小可行性任务”(如盲打 1 道 LeetCode 简单题)后直接睡觉,保持惯性不断裂。

⏳ 第三部分:每日资源调度表 (Daily Scheduler 8:30-24:00)

针对每周只有四节课的现状,将上课时间直接视为【🔵 防御敷衍】模块。如果在课上,则挂机听讲,大脑后台构思代码或复习计网/OS理论。

时间块 状态机 核心执行逻辑与输入/输出 时长
08:30 - 09:30 系统冷开机 洗漱、早餐。 浏览开源社区或科技资讯,平缓启动大脑。 1.0h
09:30 - 11:30 🟢 高能输出 算法与理论底座 (Python/CS理论)
精力峰值时段。刷 1-2 道 LeetCode,强制英文口述思路。 2.0h
11:30 - 14:00 物理断电 午餐 + 深度午休。 雷打不动睡 40-60 分钟。 2.5h
14:00 - 17:00 🟢 高能输出 AI Infra 底层工程实战 (C++)
操作本地高配机器与远端服务器,死磕内存管理与网络并发。 3.0h
17:00 - 19:00 硬件维护 体能训练与晚餐。 健身房或操场,切换轨道,缓解颈椎压力。 2.0h
19:00 - 21:30 🔵 防御敷衍 导师横向与学校课业
降维处理杂活与作业。到点准时停手。(若此时在上课,则此模块平移至上课时段) 2.5h
21:30 - 23:00 🟢 / 🟡 判定 复盘或触发合法狂欢
若当日高能任务 100% 达成,立刻启动游戏;未达成则做最后冲刺与英文 Bug 复盘。 1.5h
23:00 - 24:00 内存清理 降温与休眠。 洗澡、看闲书、刷 B 站。24:00 强制熄灯休眠。 1.0h

学习记录

招聘信息

以下记录就业相关信息,以备查阅:

  • 2026 米哈游 城市专场 【程序/测试】
    • 日期:2026年03月25日 19:00-20:30(GMT+8)
    • 面向:28届 实习生
    • 地点:西安+西工大长安校区宣讲 启真楼1楼104
    • 岗位:测试
    • 链接:https://mp.weixin.qq.com/s/w9bp4LJTU6QW6lG9kIsHaw
    • 宣讲链接:https://mp.weixin.qq.com/s/Ds46VVli7xdG3VKowFYGKQ
    • 参与:线下面试前问卷+网申简历

TODO Web APP 开发

利用 AI 同时熟悉现代前端开发框架,快速搭建一个 Todo Web APP,作为本计划的状态流转记录工具。

现代前后端WEBAPP开发_方向系统学习记录

2026-02-06 01:00:00

Featured image of post 现代前后端WEBAPP开发_方向系统学习记录

现代前后端 WEBAPP 开发_方向系统学习记录

1. 基础概念

1.1 现代前后端 WEBAPP 思维

  • 前端与后端高度解耦,通过 API/HTTP/Socket 通信
  • 前端使用组件化、模块化框架(React + Next.js + TS)
  • 后端服务微服务化或插件化,每个工具独立容器化
  • 统一开发环境与生产环境,避免“本地可用、服务器报错”

1.2 核心技术理解

技术 作用
Windows 操作系统与 GUI 编辑器环境
WSL2 Linux 核心环境,和服务器一致
nvm Node 版本管理器,保证项目 Node 版本一致
Node.js JS/TS 运行环境
pnpm 高效包管理器,安装依赖与管理脚手架
Next.js + React + TS 现代前端框架,支持组件化与页面路由
Docker 插件化工具容器化,保证隔离和可移植性

2. 学习路线规划

阶段 学习目标 依赖
技术工具 [x] 开发环境搭建:Windows + WSL2 + VS Code Remote-WSL,Docker 容器化思维 WSL2 安装与配置、VS Code Remote-WSL、Docker&&K8s学习笔记
[ ] Node.js + TypeScript 入门:Node 运行机制、nvm 版本管理、npm / pnpm / yarn 使用、TypeScript 基础 Nodejs+TypeScript_技术工具学习记录
[ ] Next.js 现代前端开发:React + Next.js + TS、组件化开发、页面路由与 API 路由、插件化平台思维 Next.js 官方文档、React 官方文档、UI 组件库(Ant Design / Tailwind UI)、Docker 容器化工具
基础知识 了解前后端解耦与现代 WEBAPP 架构 HTTP/HTTPS、RESTful API、GraphQL、微服务与插件化服务思维
掌握模块化与依赖管理概念 CommonJS / ES Module、模块加载机制、包管理器原理
熟悉前端组件化设计与状态管理 React 组件设计模式、状态管理(Zustand / Redux / Context API)
产出学习 能搭建插件化 WEBAPP 平台,整合前端页面与 Docker 插件工具 Node + Next.js + Docker API 调用、前端组件化开发、插件化工具集成
通过小项目练习技术链 小工具插件(音乐播放器、计算器、小游戏、AI Agent)容器化 + 前端调用
建立完整开发与部署流程能力 Windows → WSL2 → nvm → Node → pnpm → Next.js → Docker 插件化平台

3. 技术依赖


4. 相关资料链接


5. 学习过程记录

2026-02-06

  • 理解现代前后端解耦 + 插件化平台思想
  • 搭建 Windows + WSL2 开发环境,VS Code Remote-WSL 测试

2026-02-07

  • 安装 nvm,切换 Node 18,理解 Node 运行机制
  • 熟悉 npm / pnpm / yarn 区别,选择 pnpm 作为包管理器

2026-02-08

  • 使用 pnpm create next-app 初始化 Next.js + TS 项目
  • 理解模块系统:.js / .mjs / CJS / ESM
  • 开始规划插件化工具目录结构

2026-02-09

  • 每个工具独立写 Dockerfile,测试容器启动与端口映射
  • 前端通过 API 路由调用 Docker 插件,实现初步平台功能
  • 整个开发流程在 WSL2 + Docker 环境下稳定运行

6. 技术流程图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Windows (GUI + 编辑器)
WSL2 (Linux 环境)
nvm (Node 版本管理)
Node.js (JS/TS 运行)
pnpm (包管理与项目脚手架)
Next.js + React + TypeScript (前端页面 + 组件化开发)
Docker (插件化工具容器化,隔离服务)
现代前后端 WEBAPP 工具平台

核心思维:环境统一 → Node 版本稳定 → 高效依赖管理 → 组件化前端 → 容器化服务 → 插件化平台


【Node.js + TypeScript】学习笔记

2026-02-06 01:00:00

Featured image of post 【Node.js + TypeScript】学习笔记

【Node.js + TypeScript】实操笔记

1. 简单介绍

  • Node.js 是基于 V8 引擎的 JavaScript 运行环境,可在服务器端执行 JS/TS
    • nvm 是 Node 版本管理工具,方便切换不同项目的 Node 版本
    • pnpm 是高效的包管理器,替代 npm/yarn,节省磁盘空间和安装时间
  • TypeScript 是 JavaScript 的超集,提供类型系统和编译时检查
  • 常见应用场景:Web 后端服务、CLI 工具、前端构建工具、插件化平台

2. 前置技术依赖

  • 熟悉 JavaScript 基础语法
  • 对 Linux / WSL2 命令行基础了解

3. 环境配置

  • 系统环境:Windows + WSL2(推荐 Linux 内核一致性)
  • Node 版本管理:nvm 安装 Node,切换版本
  • 包管理器:推荐 pnpm,安装与验证:

  • VS Code 配置

    • 安装 Remote-WSL 插件
    • 安装 TS / Node 插件,启用自动类型提示

4. 基础用法

4.1 Node.js 基础

4.1.1 Node.js 安装与基于 nvm 的版本管理

4.1.1.1 Node.js 安装
  • 类似于 python 的 conda,nvm(Node Version Manager)是一个用于管理多个 Node.js 版本的工具。它允许你在同一台机器上安装和切换不同版本的 Node.js,非常适合开发者在不同项目中使用不同版本的 Node.js。

  • 类似于 python 的 pip,pnpm 是一个高效的 JavaScript 包管理器,提供了更快的安装速度和更少的磁盘空间占用。它通过使用符号链接来共享依赖项,从而避免了重复安装相同的包。

Node.js 官网 提供了 Node.js 的安装命令,在选择了 系统环境Node 版本管理器包管理器Node.js 版本后,可以直接复制命令在终端中执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 以在 WSL(Ubuntu24.04)下安装使用 nvm 进行版本管理并使用 pnpm 管理包的 Node.js 25.x 为例:

# 下载并安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# 代替重启 shell
\. "$HOME/.nvm/nvm.sh"

## 若无效报错则重启 shell 继续后面的步骤

# 下载并安装 Node.js:
nvm install 25

# 验证 Node.js 版本:
node -v # Should print "v25.6.1".

# 安装 Corepack:
npm install -g corepack

# 下载并安装 pnpm:
corepack enable pnpm

# 验证 pnpm 版本:
pnpm -v

值得注意的是,nvm 和 pnpm 的安装步骤可能会因操作系统和环境的不同而有所差异,详见[nvm 和 pnpm 的安装注意事项](# 7.1-nvm-和-pnpm-的安装注意事项)。

4.1.1.2 Node.js 常用命令
4.1.1.3 nvm 常用命令
 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
# 列出已安装的 Node.js 版本及别名
nvm ls

# 列出在线可安装的 Node.js 版本
nvm ls-remote

# 安装指定版本的 Node.js
nvm install <version>
## nvm install 25

# node 为最新版本的别名
nvm install node

# 设置别名
nvm alias my_alias v14.4.0

# 使用指定版本的 Node.js
nvm use <version>

# 卸载指定版本的 Node.js
nvm uninstall <version>

# 显示当前使用的 Node.js 版本
nvm current

# 获取 nvm 的安装路径
nvm which <version>

# 在指定版本下运行简单命令
nvm run <version> -- <command>
# 在指定版本下运行复杂命令
nvm exec <version> <command>

4.1.2 基于 pnpm 的包管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14



---

## 5. 实践案例

* **搭建小型 API 服务**

 ```ts
 import express, { Request, Response } from 'express';
 const app = express();
 app.get('/ping', (req: Request, res: Response) => res.send('pong'));
 app.listen(3000, () => console.log('Server running on port 3000'));
  • Docker 容器化运行

    1
    2
    3
    4
    5
    6
    
    FROM node:18-alpine
    WORKDIR /app
    COPY package.json pnpm-lock.yaml ./
    RUN npm install -g pnpm && pnpm install
    COPY . .
    CMD ["npx", "ts-node", "src/index.ts"]
    
  • 实践经验

    • 使用 pnpm 速度快、占用少
    • 在 WSL2 中开发,Docker 与 Linux 环境一致

6. 常见问题与解决办法


7. Tips

7.1 nvm 和 pnpm 的安装注意事项

由于 Node.js 25.x 版本的最新更新,Corepack 在 Node.js 25.x 中已被移除,因此需要手动安装 Corepack 来使用 pnpm 包管理器。

在 Node.js 25.x 中安装 pnpm 的步骤比 Node.js 24.x 多了一步:

``bash

安装 Corepack:

npm install -g corepack

1
2
3
4
5
6
7

---

## 8. 参考资料

- [Node.js 官网](https://nodejs.org/)
- [nvm GitHub 仓库](https://github.com/nvm-sh/nvm)