MoreRSS

site iconLiHan | 李寒修改

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

Inoreader Feedly Follow Feedbin Local Reader

LiHan | 李寒的 RSS 预览

随想260331二零二五

2026-03-31 22:37:00

Featured image of post 随想260331二零二五

卧铺又谈去一年

春天都快过去了,这时候才提笔 2025 的总结,确是晚了,巧在又是在开往武汉的列车上,蜷缩在卧铺里,气氛到这里了,不写点什么是说不过去了。

光与暗

再无这般 CUC

2025 的六月清晰地把全年一分为二,上半年还是校园里年轻的白杨,下半年就深锁秦岭了。

说实话,现在回想起来,还是不敢相信短短半年时间里,居然留下了那么多回忆。一月和 lcy 的 粤旅250111算是毕业前狂欢的预告;三月和 cwt 吃遍 CUC 周边面馆、逛遍超市;四月的王府井、雍和宫还有万恶之源的青边口长城让我爱上了旅行;五月除了驻京办计划,珠江绿洲的顶层才是我心中的广州塔;速通完天津,六月还有东猴顶的日出……

珠江绿洲楼顶的夜里,我记得和 cwt 感叹为何直到毕业前一个月才发现了这个好地方,穿梭在老陕面馆和超市间,我们也后悔太迟开始享受生活,4年的本科生活除却失败的恋爱,真成了头轻尾巴尖重了。

我将我的魂魄留在了通惠河畔,这里也是 cwt 面试上如今令我羡慕的工作的地方,7月在武汉与重庆的小聚,26年初在河南的重逢,也让我的青春打赢了两场复活赛。

可是冰冷的现实总在每一次重温毕业随想时提醒起我: 再无青春,再无这般 CUC

恰如所料的 NWPU

在最后的日子里,对未来的恐惧也牢牢地占据了脑袋的一小片。从保研后的调研来看,在 NWPU 的日子非但不会好过,甚至可谓“龙场悟道”的一场修行。随着开学日子的逼近,纵使同 lcy 打了一个假期三角洲,暂时麻痹了青春的怀念,总得面对研究生的现实了。

记得在通惠河畔最后一次痛哭时,我料想硕士三年是剥夺一切娱乐与情感的苦修。不幸的是,现实恰如我料的一般,离地铁站的17km 拦住了我几乎所有的娱乐,一个接一个的横向与宛如铁笼的校园管理,更是让我彻底窒息:我非旦没有想错,甚至乐观了不止一点

仅仅三个月时间就打破了 CUC 四年带给我的自信,仅仅一个学期的煎熬,让我对西安的滤镜跌得粉碎。我再也不去幻想读博当教授,我只想早点就业脱离魔窟,找个轻松安逸的工作赶赶青春的尾巴。

忆往昔

外公的去世在高二那年,也许宇宙的内存容量有限,自那以后我获得了他释放的内存,自我意识清晰了起来。

今天出门的时候,看着姥姥不舍的样子,幸好我的内心还有几分波纹。记得姥姥说过,每次我和父母离开西安时,她都会守在窗台望向出小区门的必经之路,这两年倒是很少走这个门出了,我们都习惯从地下直穿到地铁站,我想她该好久没看到我们的背影了吧。

于是按下了电梯一楼的按钮,我站在楼下拼命地朝着十楼的角度挥手,房间里关着灯,街边的路灯也没有过年时候亮了,我更卖力地挥着:学历越来越高,也许前途越来越好,为什么连多陪姥姥几日都做不到呢?

我讨厌这个世界。

N谈未来

没啥好谈的,结论很清晰了,刷力扣、做项目、背面经、抢实习,早点就业,早点自由,早点完成所有的遗憾。

刚入学的时候,带着 CUC 给我的自信,我选了班长又选研会主席, openvpn 晾了一年终究是给我整会了,十月国庆在绿树上号三角洲时,上月游戏时长只有 6h。

lyx 带着我开始搞科研,说真的,他的努力我是真的佩服,从早干到凌晨,一连着就是几个礼拜,可惜我确实不是这块的料,也许又可能是努力晚了,随着我决定就业,科研和读博当教授之路算是彻底放弃了。

十月还拉着 lyx 爬了山,秦岭连绵,本想着后面两周一座山,脚伤让我又是半年走不好路。1月的时候,硬撑着和 1228 河南相见,险些腿断在龙门石窟,直到半个月前才算好了彻底。

经历了人生第一次挂科,也意味着我研究生生活彻底的崩坏,lihan 不得不现实起来,放弃了读博】放弃了恋爱、放弃了打游戏(好吧晚上还会打打),能偷偷学学技术,找个互联网私企外企都不自信了。

玩好

玩就玩好吧,总不能真让 NWPU 毁了青春,多少挣扎一下吧。

————2026.3.31 午夜再于返鄂列车上

Clash Verge 在局域网内通过端口代理流量

2026-03-28 16:40:00

Featured image of post Clash Verge 在局域网内通过端口代理流量

问题

Clash Verge 在局域网内通过端口代理流量时,虚拟机或局域网内设备无法 ping 通代理端口。

原因

  1. 防火墙未放行
  2. 代理端口选择错误
  3. 未打开 Clash Verge 的局域网访问权限
  4. 订阅配置覆盖了本地配置,导致始终绑定在 127.0.0.1 而非 0.0.0.0

解决

  1. 确保防火墙已放行 Clash Verge 的代理端口(默认为 7897 或 7890)。
  2. 检查 Clash Verge 的配置文件,确认代理端口设置正确
  3. 在 Clash Verge 的设置中,打开局域网连接,确保允许来自局域网的连接。
  4. 右键订阅,选择编辑文件,找到bind-address,将其值改为0.0.0.0,保存后重启 Clash Verge。
  5. 在目标设备的命令行中修改代理,命令为export http_proxy=http://<Clash Verge所在设备的IP地址>:<代理端口>,并 ping google.com 来测试是否成功。

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 重叠
  • palindrome 回文

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,作为本计划的状态流转记录工具。