USTC-算法设计 | 八次实验之后,我开始把“复杂度”当成一种直觉
算法设计这门课最初给我的感觉,是一张写满复杂度的表格:O(n log n) 看起来很漂亮,O(n^2) 看起来就应该被淘汰。真正把八次实验做下来,我才发现复杂度不是贴在代码旁边的标签,而是程序运行起来以后,一种可以被摸到的节奏。
数据量小时,暴力方法也能很快结束;数据量一大,原来只是多一层循环,等待时间就突然变得不讲道理。树的旋转、区间的剪枝、图的存储方式,也不再是课本上的插图,而是会直接决定程序能不能跑完。
仓库地址:USTC-Algorithm-Design-Labs。
一、快速排序:同一个 pivot,为什么能把程序带到两个世界
快速排序是我接触得最早、也最容易低估的实验。算法主体并不长:选一个 pivot,把数组分成两边,再递归处理。可一旦输入接近有序、重复元素很多,或者 pivot 选得不合适,递归树就会变得很难看。
我先写了一个直观版本,把它当作参照。然后开始看优化:小区间是否值得继续递归,分区过程中能不能少做几次交换,递归深度太深时有没有必要换一种处理方式。每个改动单独看都不大,但在大数组上运行,差异会慢慢从计时器里冒出来。
最直观的一次观察是重复元素。普通的两路划分会不断把相等元素送进递归,数据明明没有更多信息,程序却还在认真拆分。加入更合适的分区策略以后,递归结构明显收敛了一些。
这个实验让我第一次觉得“平均复杂度”不是保证书。它描述的是一种通常情况,而不是告诉我每个输入都会善待这段代码。排序算法真正难的地方,往往不是把它写出来,而是猜到输入会怎样为难它。
二、最近点对:把所有点两两相看,太慢了
最近点对问题很容易写出暴力解:枚举每一对点,算距离,保存最小值。点的数量不大时,这个办法看起来干净利落;可当点集扩张,平方级比较会迅速把实验变成等待。
分治法把平面按坐标切开,先分别求左右两边的最小距离,再处理跨越中线的点。真正容易漏掉的是最后这一步:答案不一定只在左边或右边,也可能是一左一右两个点恰好靠得最近。
我会把点按横坐标排序,再在中间区域里筛选可能影响答案的点。筛选条件写错时,程序通常不会崩溃,只会悄悄漏掉一个更近的组合。于是我保留了暴力版本做小规模交叉验证,随机生成几组点,拿两种方法的结果逐个对照。
这次实验给我的经验很实用:优化算法时不要急着删掉慢版本。慢版本虽然不能负责大数据,但它可以在小数据里当裁判,帮我确认分治过程没有把正确答案剪掉。
三、红黑树:旋转之前,我先学会接受“树会变色”
红黑树是八次实验里最让我频繁画纸的一个。二叉搜索树的查找逻辑并不难,难的是插入和删除以后,怎样保持树的高度不要一路歪下去。
红黑树靠颜色和几条约束维持平衡。新节点先按普通二叉搜索树插入,然后根据父节点、叔节点和祖父节点的颜色关系做调整。调整过程中会旋转,也会重新染色。代码里的左旋、右旋看起来只是几个指针交换,实际一旦父子关系更新顺序错了,后面每一层都会跟着错。
我调试时没有只看最终的中序遍历,而是把每个节点的 key、颜色、父指针和左右孩子一起打印出来。每插入一个数,就检查根节点颜色、红节点是否连续,以及各条路径的黑高是否一致。输出看起来有些吵,但比盯着一个“查找失败”有效多了。
删除操作又是另一种折磨。插入失败时通常很快能发现,删除失败却可能在很多次操作以后才暴露。这个实验让我明白,平衡树的价值不只在于一棵漂亮的树,而在于它允许数据不断变化,同时把最坏情况控制在可接受范围内。
四、区间树:线段不再只是两个端点
区间树把“某个数在哪里”换成了“哪些区间和它相交”。每个节点保存一个区间,同时维护子树中的最大右端点,用来判断搜索时哪些分支仍然有可能与查询区间重叠。
刚开始我会把它当成普通二叉搜索树,只按左端点排列。后来真正写查询才发现,左端点排序还不够:当左边某个区间看起来不相交时,子树里可能仍然藏着一个右端点很大的区间。max 字段就是在告诉搜索过程,“这一整棵子树还值不值得进去看看”。
插入和删除时,除了维护树的结构,还要一路更新最大右端点。这个字段如果有一处没有回溯更新,查询结果就可能少返回一条区间,而且很难从表面看出来。我最后用人工构造的嵌套区间、相邻区间和完全包含关系反复测试,才把边界情况理顺。
区间树是我第一次明显体会到辅助信息的价值。它并没有改变区间的定义,只是额外记住了一点关于子树的事实,于是查询可以少走很多不可能有答案的路径。
五、最长公共子序列:表格里的每一个格子都在问一个选择题
LCS 的实验看上去比红黑树温和很多,实际上它训练的是另一种思维。给定两个序列,如果当前字符相同,就把答案从左上角延伸;如果不同,就比较跳过左边或跳过上边哪一种更好。
我先写长度版本,再补回溯过程。只求长度时,二维表已经足够;但要把具体的公共子序列打印出来,就必须知道每个格子是从哪里转移过来的。只保存数字会让我在最后一步失去路径。
调试时我会拿很短的字符串手算表格,尤其检查重复字符和多个最优解的情况。两个序列可能有不止一个长度相同的公共子序列,程序输出哪一个并不一定唯一,关键是长度和回溯路径要自洽。
这个实验让我对动态规划有了更具体的感觉:它不是把递归换成数组,而是把重复出现的子问题保存下来,并且在每个状态里保留足够的信息,让最后的答案可以被重新走出来。
六、Huffman 编码:频率越高,为什么反而要离根越近
Huffman 编码的输入是字符频率,输出是一棵带权路径长度较小的二叉树。每次取出频率最小的两个节点合并,再把新节点放回优先队列,直到队列里只剩一棵树。
我喜欢这个实验的地方,是它把“贪心”表现得很直白。频率低的字符可以承担更长的编码,频率高的字符应该更早被访问。每次只看当前最小的两个节点,最后却能得到整体较优的编码。
真正容易出错的是编码生成和边界情况。树只有一个字符时怎么办?左边写 0、右边写 1 的约定是否始终一致?解码时遇到叶子节点以后,指针要不要回到根?这些细节不影响算法的几行核心逻辑,却会直接决定压缩后的数据能不能原样还原。
我把编码和解码放在一起测试:先统计频率、生成树、编码,再把比特流解码回字符串,最后逐字符比较。压缩率只是结果,能不能无损回来才是这个实验真正的验收标准。
七、回溯做调度:搜索空间大到让我开始认真剪枝
调度问题是八个实验里最有“试错感”的一个。回溯法会逐步给任务安排资源或顺序,遇到冲突就退回上一步换选择。小规模数据时,搜索过程很直观;任务一多,可能性会像树枝一样迅速长开。
如果只写一个没有判断的深度优先搜索,程序很快就会开始沉默。它并不是死循环,只是在认真遍历一个我根本不该全部遍历的空间。于是剪枝成为核心:当前部分方案已经超过已有上界,就不必继续;剩余任务即使全部按最乐观情况安排,也不可能得到更好答案,也可以提前返回。
我最喜欢看搜索树被剪掉的瞬间。一个看似很小的上界估计,能让成千上万条分支根本不再展开。可剪枝条件必须证明不会误杀最优解,这里不能只凭“跑得快了”判断正确。
这个实验让我把最坏复杂度看得更现实:有些问题不是靠换一种写法就能变成多项式时间,而是要依靠问题规模、约束结构和有效的搜索策略,把可接受的输入控制在可接受的时间里。
八、BFS 和存储优化:同一张图,换一种放法就换了脾气
最后一组实验回到图遍历。BFS 的逻辑很简单:从起点开始,把当前层的节点放进队列,访问一个节点时把还没见过的邻居加入队列。真正让我印象深的是,算法的复杂度不仅由遍历过程决定,也由图的存储方式决定。
邻接矩阵查询边很直接,但空间成本高;邻接表更适合稀疏图,却需要处理链表或数组索引。节点编号、visited 标记和队列实现也会影响实际表现。图不大时,这些差异几乎看不见;规模上来以后,内存访问和重复检查就不再是小事。
我会把 BFS 的访问顺序打印出来,先用一张手工画的图确认层次,再换成更大的输入看存储和运行时间。最容易犯的错误是把“入队”与“出队”时的 visited 搞混,结果同一个节点被重复加入,队列越来越胖,程序还以为自己在认真工作。
这次实验最后把我从“算法步骤”带回了“数据结构选择”。O(V+E) 只是一个复杂度表达,真正执行时,V 和 E 怎样放在内存里,才决定这条表达式会以什么速度落地。
八次实验之后,我不再只背复杂度
| 问题 | 核心方法 | 大致复杂度直觉 |
|---|---|---|
| 排序 | 分区与递归优化 | 平均 O(n log n),输入可能制造最坏情况 |
| 最近点对 | 分治与带状区域筛选 | 从两两比较降到 O(n log n) |
| 动态集合 | 红黑树 | 查找、插入、删除保持在 O(log n) 量级 |
| 区间查询 | 区间树与最大端点 | 避免遍历明显不可能的子树 |
| 序列相似 | 动态规划与回溯 | 用空间换掉重复计算 |
| 编码 | Huffman 贪心 | 用优先队列不断合并最小权值 |
| 调度 | 回溯与剪枝 | 不可能彻底消除搜索,但可以尽早放弃坏分支 |
| 图遍历 | BFS 与合理存储 | 遍历是线性的,存储方式决定实际代价 |
这张表当然不可能替代实验过程。真正让我留下印象的,是每个复杂度背后都有一个具体的输入在催我:排序数据变大以后开始变慢,红黑树少一次旋转就可能破坏约束,回溯不剪枝会迟迟没有结果,BFS 换一种存储就会消耗完全不同的内存。
如果重新做一遍,我会给每个实验补上统一的随机测试、边界用例和更清晰的性能记录,也会把“正确性证明”和“运行结果”放在一起看。算法不是只要跑出一个答案就结束了,答案对不对、为什么对、规模变大以后还是否值得,都应该被写进实验过程。
仓库地址:USTC-Algorithm-Design-Labs。
说明:本文根据个人历史课程实验和公开仓库整理,部分文字经过 AI 辅助润色;复杂度和实现细节以仓库当时的版本为准。课程资料仅用于学习回顾和个人作品整理,不用于当前课程作业或考试。

浙公网安备 33010602011771号