USTC-算法设计 | 八次实验之后,我开始把“复杂度”当成一种直觉

算法设计这门课最初给我的感觉,是一张写满复杂度的表格:O(n log n) 看起来很漂亮,O(n^2) 看起来就应该被淘汰。真正把八次实验做下来,我才发现复杂度不是贴在代码旁边的标签,而是程序运行起来以后,一种可以被摸到的节奏。

数据量小时,暴力方法也能很快结束;数据量一大,原来只是多一层循环,等待时间就突然变得不讲道理。树的旋转、区间的剪枝、图的存储方式,也不再是课本上的插图,而是会直接决定程序能不能跑完。

仓库地址:USTC-Algorithm-Design-Labs

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 辅助润色;复杂度和实现细节以仓库当时的版本为准。课程资料仅用于学习回顾和个人作品整理,不用于当前课程作业或考试。

posted @ 2026-09-11 00:37  何以牵尘  阅读(4)  评论(0)    收藏  举报