《数据结构进化史》
一本不像算法书的算法书。
如果你翻过市面上的算法教材,大概都见过这个场景:第一章"栈",第二章"队列",第三章"链表"……像是一份按字母表排列的零件清单。读完你学会了每一个零件怎么用,但你仍然不知道——这些零件,为什么会被发明出来?
这本小册子想换一个讲法。
这不是一本算法书,是一部"进化史"
想象你是一个远古程序员。
你面前只有一样东西:内存。一长条格子,每个格子能放一个数。仅此而已。
现在给你一个问题:请记住一串过去发生的事情,并且随时能取出"最近发生的那件"。
你会怎么做?
——你会发明栈。
然后问题升级:请记住一队人,让他们按到达顺序一个个进场。
——你会发明队列。
再升级:这队人里有 VIP,VIP 必须优先进场。
——你会发明优先级队列(堆)。
再升级:这些人不是一次来的,是一串一串到的,中间还要插队、删除……
——你会发明链表。
再升级:这些人之间有上下级关系,要一层层管理。
——你会发明树。
再升级:这些人之间不是上下级,是一群朋友圈,你要快速判断"这两个人是不是一伙的"。
——你会发明并查集。
……
你看,每一种数据结构都不是从天上掉下来的。它是前一种数据结构"解决不了某个问题"时,被逼出来的。
这本册子,就是想带你走一遍这条**"被逼出来"的路**。14 讲,就是 14 次升级。你会看到每一次升级背后那个"没办法了"的瞬间——而那个瞬间,恰恰是理解这个数据结构最好的入口。
讲给谁听?
- 如果你是算法新手:这本册子不要求你有任何数据结构基础。我假设你只会
for循环和数组。 - 如果你是准备面试的工程师:这本册子里的每一讲都对应大厂高频考点,但我不想让你"背答案",我想让你"想明白为什么答案是这个"。
- 如果你已经刷了几百道 LeetCode:欢迎带着"挑毛病"的心态读。我会尝试讲出你可能忽略的那条主线。
体例说明
每一讲大致是这样的结构:
- 故事开场:从一个真实场景或者上一讲的"遗留问题"说起,引出本讲要发明的东西。
- 第一性思考:不给你定义,先让你自己"发明"这个数据结构。
- 正式登场:告诉你前人是怎么给这个东西命名的,它的标准形态是什么。
- 三个例题:由浅入深。第一题建立肌肉记忆,第二题训练变形能力,第三题见识一次"灵光一闪"。
- 这一讲的哲学:这个数据结构到底在"用什么换什么"?它的适用边界在哪?
- 给下一讲埋个雷:本讲解决了某个问题,但留下了另一个问题——下一讲就是冲着这个问题去的。
每一讲会有两个角色对话:
- 我:带着问题的新手程序员
- 老周:隔壁工位的十年老码农,话不多,但每次一开口都让人醍醐灌顶
偶尔穿插第三种声音:【旁白】,用来补充历史背景或代码实现。
14 讲的主线地图
下面是这场"进化史"的完整路线。每一讲的标题我都重新起了——原书叫"栈:从简单栈到单调栈",我这里叫**"第 1 讲 · 倒着想问题:一个叫'栈'的小发明"**。每讲都有一个贯穿的核心隐喻。
第一部分 · 线性时代(1-3 讲)
时代主题:数据还在一条线上排队。我们先学会怎么在"一维世界"里高效地存放、取出、排序。
第 1 讲 · 倒着想问题:一个叫"栈"的小发明
核心隐喻:栈是"时间机器"——它让你永远只能看到"最近发生的那件事"。
本讲你会经历的"没办法了"时刻:
你要判断一串括号是否合法。括号可以嵌套。你尝试用一个变量记"现在有多少个左括号还没关"——好像可以?但题目一变,换成大中小三种括号混合,你的变量就不够用了。你意识到:你需要一个能"把'最近的'那个取出来"的结构。
贯穿三题:
- 括号匹配(入门)→ 大鱼吃小鱼(变形)→ 找右边第一个比我小的数(升华为"单调栈")
给下一讲埋的雷:栈只能看最近的,那如果我要按"先到先得"呢?
第 2 讲 · 排队的艺术:FIFO 与"不讲武德"的单调队列
核心隐喻:队列是"传送带"——东西从一端进,从另一端出。但如果有人在传送带上"抢位置"呢?那就是单调队列。
本讲你会经历的"没办法了"时刻:
滑动窗口里永远要知道最大值。你每次暴力扫一遍窗口?O(n·k),面试官皱眉。你想用堆?堆里的"过期元素"清不掉。你盯着传送带发呆——突然意识到:那些"明显赢不了的人"根本没必要留在队里。
贯穿三题:
- 二叉树层次遍历(基础)→ 循环队列(工程)→ 滑动窗口最大值(单调队列的威力)
给下一讲埋的雷:先来先得也不够,我想要"重要的人先办"。
第 3 讲 · 插队的学问:优先级队列与堆的"半有序"智慧
核心隐喻:堆是"懒散的管理者"——它不要求所有员工按能力排序,只要求"顶头的那个最能打"。
本讲你会经历的"没办法了"时刻:
Top-K 问题:从海量数据里找最大的 K 个。排序?O(n log n),浪费。你意识到:我根本不需要知道完整的排名,我只需要知道"当前第 K 名是谁"。
贯穿三题:
- 数据流中的第 K 大(入门)→ 合并 K 个有序链表(变形)→ 会议室调度(实战)
给下一讲埋的雷:线性结构都讲完了,但现实世界的数据不一定是数组。如果数据是一串一串的"链"呢?
第二部分 · 结构化时代(4-6 讲)
时代主题:数据开始有"关系"了。不再是一条线,而是链、是网、是层次。
第 4 讲 · 一根链子的三板斧(上):假头、虚拟节点、以及"多走一步"的智慧
核心隐喻:链表是"绳子上串珠子"——看似柔软,但一不小心就打结、断裂、丢珠子。
本讲你会经历的"没办法了"时刻:
删除链表某个节点,你写了 20 行 if/else 处理各种边界,最后还是漏了一个"删除头节点"的情况。老周一句话点醒你:"你给它加一个假的头不就完了吗。"
贯穿三题:
- 设计链表(六大基本操作)→ 理解"虚拟头"的本质 → 为什么说"链表题 80% 靠假头"
第 5 讲 · 一根链子的三板斧(下):新链表与快慢指针的"双人舞"
核心隐喻:双指针是"两个侦探"——一个跑得快,一个跑得慢,快的负责探路,慢的负责做标记。
本讲你会经历的"没办法了"时刻:
判断链表有没有环。你用 HashSet 记录访问过的节点?空间 O(n)。老周问你:"你跑过步吗?两个人在操场跑圈,一快一慢,只要有圈他们一定会相遇。"
贯穿三题:
- 反转链表(新链表思想)→ 环形链表(快慢指针)→ 两个链表的交点(双指针的另一种玩法)
给下一讲埋的雷:链是一维的关系。如果一个节点有好几个孩子呢?
第 6 讲 · 从一根链到一棵树:递归的"四种看世界方式"
核心隐喻:树是"家谱"——前序遍历是"父亲先于儿子",后序遍历是"儿子先于父亲",中序遍历是"按辈分横着排",层序遍历是"按世代一代代点名"。
本讲你会经历的"没办法了"时刻:
求二叉树的最大深度。你本能写了一个栈 + 迭代,30 行。老周在旁边用递归 3 行搞定,然后说:"树天生就是递归的,你非得用栈装迭代,那是拿着遥控器去开锁。"
贯穿三题:
- 二叉树最大深度(递归入门)→ 从前序中序恢复二叉树(递归的逆向思维)→ 二叉树的直径(后序遍历的威力)
给下一讲埋的雷:如果关系不再是父子,而是"朋友圈"呢?
第三部分 · 关系网络时代(7-8 讲)
时代主题:数据之间的关系不再是线、也不再是树,而是一张乱糟糟的网。
第 7 讲 · 朋友的朋友是朋友:并查集——用两行代码管理一个社交网络
核心隐喻:并查集是"认祖归宗"——每个人只记得自己的"爸爸",要判断两个人是不是一家人?沿着爸爸往上找,找到同一个祖宗就是。
本讲你会经历的"没办法了"时刻:
给你一万对"A 和 B 是朋友"的关系,问你这些人构成多少个朋友圈。你用 DFS 染色?可以,但如果动态添加关系呢?每次都重新染?老周:"你只需要记'谁是我爸'就够了。"
贯穿三题:
- 朋友圈计数(入门)→ 冗余连接检测(环的判断)→ 被围绕的区域(虚拟节点的妙用)
第 8 讲 · 把一堆数变整齐:归并与快排的"分治哲学"
核心隐喻:排序不是"排",是"分和合"——你把一堆打散,再合起来,自然就整齐了。
本讲你会经历的"没办法了"时刻:
冒泡排序写了 5 分钟,面试官问 O(n²) 能不能更快。你想了想归并排序——它本质是什么?你画了画,发现:归并排序就是二叉树的后序遍历。 这是新知识和旧知识第一次"化学反应"。
贯穿三题:
- 归并排序(分治入门)→ 数组中的逆序对(归并的副产品)→ 快速选择(快排的降级版)
给下一讲埋的雷:如果数据已经是有序的呢?我们能不能跳着找?
第四部分 · 搜索与决策时代(9-11 讲)
时代主题:数据已经有序,或者我们要在一个巨大的解空间里找东西。关键不是"怎么存",而是"怎么聪明地找"。
第 9 讲 · 有序即是力量:二分搜索的"开闭原则"哲学
核心隐喻:二分搜索是"猜数字游戏"——每猜一次,都把世界砍一半。砍得准,十次之内必中。
本讲你会经历的"没办法了"时刻:
你背了二分搜索的模板,但题目一变形——找"第一个大于 X 的位置"——你就又开始写 bug 了。老周把一张纸铺在桌上:"你画一条数轴。左闭右开还是左开右闭?先定下来,后面所有判断都顺了。"
贯穿三题:
- 经典二分(模板 1)→ 第一个/最后一个位置(模板 2)→ 旋转数组查找(二分的"脑筋急转弯")
第 10 讲 · 两个指针跑天下:区间问题的"左右夹逼术"
核心隐喻:双指针是"拉皮筋"——一端不动,另一端拉长;或者两端一起往里收。重点是保持皮筋的"单调性"。
本讲你会经历的"没办法了"时刻:
求一个数组的最长无重复子串。你用暴力 O(n²),能过但不优雅。老周说:"你每次窗口左边往右挪一格,右边最远能到哪?如果右边不会往左退,那就是 O(n)。"
贯穿三题:
- 最长无重复子串(最长区间)→ 和为 K 的子数组个数(定长)→ 最小覆盖子串(最短区间)
第 11 讲 · "当下最好"的赌博:贪心算法——为什么它有时对、有时错?
核心隐喻:贪心是"赌徒"——每一步都押最大的筹码。有时候稳赢,有时候输光。关键是判断这个赌局能不能赢。
本讲你会经历的"没办法了"时刻:
找零钱问题:给你 [1, 5, 10, 20],凑 36 块钱用最少硬币数。贪心行。给你 [1, 3, 4],凑 6 块钱?贪心错!(贪心: 4+1+1,最优: 3+3)。你第一次意识到:贪心不是万能的,它需要"问题本身配合"。
贯穿三题:
- 分发糖果(贪心正确性的证明)→ 跳跃游戏(贪心 vs DP 的分界线)→ 区间调度(经典模板)
给下一讲埋的雷:如果贪心错了,我们该怎么办?——那就把所有可能都试一遍。
第五部分 · 穷举与记忆时代(12-14 讲)
时代主题:聪明办法用尽了,我们开始"硬试"。但硬试也要有章法——这就是回溯、搜索、以及 DP。
第 12 讲 · 把所有路都走一遍:回溯——递归的"选择-撤销"仪式
核心隐喻:回溯是"探险队"——每走一步插一面旗,走不通就拔旗回退,再换一条路。
本讲你会经历的"没办法了"时刻:
给你 [1,2,3],列出所有排列。你写了三层嵌套 for 循环。然后题目改成 n 个数。你傻眼了——你不能写 n 层嵌套。老周说:"递归啊。选一个数,剩下的变成子问题,做完再把那个数放回去。"
贯穿三题:
- 全排列(模板题)→ 组合总和(带条件的回溯)→ N 皇后(回溯 + 剪枝的巅峰)
第 13 讲 · 两种走迷宫的姿势:DFS 和 BFS 的"性格差异"
核心隐喻:DFS 是"一根筋的探险家"——走到底再回头;BFS 是"散开的搜索队"——一圈一圈往外推。
本讲你会经历的"没办法了"时刻:
求最短路径(边权都是 1)。你用 DFS?不对,DFS 找到的不一定是最短的。老周说:"最短路径要 BFS。因为 BFS 第一次到达某个点时,路径一定最短。" 你突然理解了 BFS 的本质。
贯穿三题:
- 岛屿数量(DFS/BFS 都可)→ 单词接龙(BFS 的威力)→ 课程安排(DFS + 拓扑排序)
给下一讲埋的雷:穷举太慢了。如果子问题会重复出现,我能不能把结果存下来?
第 14 讲 · 给未来的自己留字条:动态规划——从"不可能"到"显然"
核心隐喻:DP 是"留字条"——你做过的每一道子题,都给未来的自己留一张便签:"答案在这里"。下次遇到,直接抄。
本讲你会经历的"没办法了"时刻:
斐波那契数列。递归写法 30 秒出来,但 n=50 就算到天荒地老。你印出调用树一看——同一个 f(10) 被算了几千次。你顿悟:我只要把算过的结果存下来就行了。这就是 DP 的起点。
贯穿三题:
- 凑硬币(DP 的六步破题法)→ 最长公共子序列(二维 DP)→ 股票买卖(状态机 DP)
终章思考:DP 和递归的边界在哪?和贪心的边界又在哪?
彩蛋:一张图看懂 14 讲
┌─── DP (14) "留字条"
┌── 穷举 ──┼─── BFS/DFS (13) "两种走迷宫"
│ └─── 回溯 (12) "选择-撤销"
│
┌── 决策 ┼── 贪心 (11) "当下最好"
│ └── 双指针 (10) "左右夹逼"
│
找东西 ┼── 二分 (9) "有序即力量"
│
├── 排序 (8) "分治"
│
建关系 ┼── 并查集 (7) "朋友圈"
│
├── 树 (6) "家谱"
│
串起来 ┼── 链表 (4-5) "珠子与侦探"
│
├── 堆 (3) "懒散管理者"
│
排队伍 ┼── 队列 (2) "传送带"
│
└── 栈 (1) "时间机器"
↑
从"怎么存"到"怎么找"到"怎么决策"
阅读建议
- 按顺序读。每一讲都是为下一讲做铺垫的,跳读会错过那条主线。
- 读每一讲的"没办法了"时刻。如果你没有感受到"啊,确实只能这么办"的那种顿悟,说明没读进去。
- 做三道例题。每讲的三道题分别对应"入门-变形-升华",做完前两道就够面试了,第三道是给真正想吃透的人准备的。
- 回来翻一翻。学完 14 讲再回来翻第 1 讲,你会看到不一样的东西。
目录
第一部分 · 线性时代
第二部分 · 结构化时代
- 第 4 讲 · 一根链子的三板斧(上):假头、虚拟节点、以及"多走一步"的智慧
- 第 5 讲 · 一根链子的三板斧(下):新链表与快慢指针的"双人舞"
- 第 6 讲 · 从一根链到一棵树:递归的"四种看世界方式"
第三部分 · 关系网络时代
第四部分 · 搜索与决策时代
第五部分 · 穷举与记忆时代
- 第 12 讲 · 把所有路都走一遍:回溯 —— 递归的"选择-撤销"仪式
- 第 13 讲 · 两种走迷宫的姿势:DFS 和 BFS 的"性格差异"
- 第 14 讲 · 给未来的自己留字条:动态规划 —— 从"不可能"到"显然"
开始吧。
下一页,你将遇到老周。他会问你一个问题:
"小伙子,给你一根棍子和一堆括号,你怎么判断括号匹配?"