上一篇讲的是「你在学科的哪一格」。这一篇往下钻一层,讲那句人人都背过的话。
程序 = 数据结构 + 算法是 Wirth 在 1976 年提出来的。它到今天仍然是对的,但它被当成了另一句话在用。
一、先看两本书的写法
有意思的是,同一个等式,不同教材的写法不一样:
| 出处 | 原文的写法 |
|---|---|
| 《数据结构——从概念到C++实现(第 4 版)》第 4 页 | 算法 + 数据结构 = 程序 |
| 《数据结构——从概念到C实现(第 2 版)》第 2 页 | 数据结构 + 算法 = 程序 |
同一句话,加号两边调了个位置。
这不是哪本印错了。两本书都在讲同一件事,只是强调的重点不同:一本想先说算法,一本想先说结构。
但这里正好可以问一句:顺序重要吗?
加号本身是可交换的,但**「先想哪个」非常重要**。
两本书紧接着都说了同一句:一个「好」程序,首先要把问题抽象出一个适当的数据模型(结构),然后基于它设计算法。
结构在前,算法在后。这是设计顺序,不是加法交换律。
二、误读在哪
绝大多数人把这句话理解成:
程序由两个部件组成,像「桌子 = 桌面 + 桌腿」——一块是数据结构,一块是算法,拼起来就是程序。
Wirth 说的不是部件构成,是设计过程。
差别在哪?听起来很虚,但它直接影响你怎么写程序:
| 如果理解成「部件」 | 如果理解成「顺序」 |
|---|---|
| 先想算法,再挑个数据结构装数据 | 先看数据的形状,再想怎么处理它 |
| 数据结构只是个容器 | 数据结构的形状决定了算法能长什么样 |
| 换语言换库无所谓 | 换存储方式等于换问题难度 |
同一个需求,两种理解会导致两种完全不同的代码。
举个最常见的:查一万人里有没有某个人。
- 「部件」理解:先写一个循环去比——算法定了,数据结构用数组。
- 「顺序」理解:先问这些人怎么组织——如果按身份证号能建索引,查找就是一次的事,根本不需要循环。
第二种想法不是「优化」,是在设计阶段就选了另一条路。 等写完循环再优化,已经晚了。
三、加号是误导性的:两半不是相加,是相乘
这是我认为这句话里最容易漏掉的一层。
同一个算法,换个存储方式,复杂度会差一个量级:
| 操作 | 用数组 | 用链表 |
|---|---|---|
| 取第 k 个元素 | O(1) | O(n) |
| 在头部插入 | O(n) | O(1) |
| 查找某个值 | O(n) | O(n) |
算法没有变一个字,只是数据换了个放法,取第 k 个元素就从「一步」变成「走 k 步」。
所以「数据结构 + 算法」这个加号,会让人以为是两块独立的成本相加。实际是乘积关系:结构选错,算法再好也白搭;结构选对,笨算法也能跑得动。
一句实操的经验:
性能问题,八成的根子在结构,不在算法。
四、这个等式漏了什么
1976 年的程序和 2026 年的程序,不是同一种东西。
Wirth 那个年代,程序基本是:读入数据 → 计算 → 输出结果。单线程、独占机器、跑完就结束。
今天的程序里有五样东西,等式里没有:
1. 并发
多个执行流同时改同一份数据。这不是「算法」也不是「数据结构」,这是顺序的不确定性。你要处理的是「谁先谁后」,而不是「算得快不快」。
2. I/O 与阻塞
程序大部分时间在等——等磁盘、等网络、等用户。一个「算法上 O(n)」的程序,可能 99% 的时间卡在等磁盘。这时候优化算法是白费劲。
3. 持久化状态
程序死了,数据还在。这意味着你要考虑「写到一半断电」之后是什么状态。这是事务、日志、幂等的问题,不是数据结构能解决的。
4. 网络对端
对面是另一个程序,它可能慢、可能超时、可能返回垃圾、可能永远不回。分布式系统里有个残酷的事实:你永远无法确认对面收到了。 这类问题叫「两军问题」,有严格的证明说明它无解(不是不会做,是做不了)。
5. 人
界面、错误提示、可恢复性、可理解性。程序是给人用的,这一条决定了它能不能被用起来。
所以:等式没错,是不完整。
一句判据,我用它判断过很多次:
你觉得程序「写完了」,但它还不能用——缺的通常不是算法,也不是数据结构,是这五样里的一个。
五、那这句话还有什么用
有人可能会说:既然它不完整,是不是就别讲了?
恰恰相反。它仍然是最好的入门框架,因为它是所有框架里最简洁的一个。
它的价值不在「完整」,在「可操作」:
- 拿到一个问题,先问:数据长什么样? (数组?树?图?还是根本没结构?)
- 再问:在它上面做什么? (查?排?最短路径?)
- 最后才问:用什么语言写?
顺序颠倒,代价很大。 先写代码再想结构,等于先盖墙再画图纸。
六、一个反例,说明顺序错了会怎样
经典的「最大子段和」问题:给定一列数(有正有负),求和最大的连续一段。
按「先算法后结构」的思路,你会写三重循环:枚举起点、枚举终点、算这段的和。复杂度 O(n³)。数据一大就跑不动。
按「先看结构」的思路,会先注意到一件事:「这一段的和」是可以从前一段推出来的——不需要每次重算。
于是三重循环塌成两重(O(n²)),再往下想,还能塌成一重(O(n))。
同一份数据,同一个需求,四种写法:O(n³) → O(n²) → O(n log n) → O(n)。
这不是「优化」,这是从看问题的角度不同长出来的不同解法。
下一篇讲怎么判断这些复杂度,下下篇讲这四种写法背后的四种设计范式——以及为什么最快的那个解法可能是错的。
参考
- 《数据结构——从概念到C++实现(第 4 版)》第 4 页;《数据结构——从概念到C实现(第 2 版)》第 2 页(等式的两种写法)。
- 「五样漏掉的东西」「加号是乘积」「八成根子在结构」是我自己的归纳,不是原书内容。
- 两军问题的严格证明见《计算机学科概论(第 3 版)》第 41 页。