搜索算法可视化
搜索不仅要找到目标,还要决定先看哪里、哪些部分可以跳过。本组动画与代码同步,展示有序数组范围减半、图逐层扩展,以及启发式如何引导搜索边界。
线性查找不要求排序,从前到后检查,最坏情况读取全部元素。其他方法利用不同结构:二分用排序排除半个范围;双指针对有序数组求目标数对,从两端排除不可能的组合;本课滑动窗口要求连续子数组、非负值和正目标,左右边界都只向前走。前缀和押注数组不会变:先花一遍算出累计和,之后任意区间和都只需一次减法。BFS、DFS 则遍历图,以队列或栈决定下一点。A* 加入对剩余成本的估计,需要满足相应条件才能保留最短路保证。
如何阅读搜索算法
- 排除了什么:二分每次探测排除约一半候选;图遍历则通过已访问标记避免重复处理。
- 访问顺序是什么:BFS 用队列从近到远扩展,DFS 用栈或递归先深入再回退。邻接表下两者都是 O(V + E),轨迹却不同。
- 需要什么前提:二分必须有序。BFS、DFS 可遍历一般图,而 BFS 的逐层顺序保证无权图中的最少边路径。
- 何时可相信启发式:A* 按实际成本
g加估计h排序。h一致时(目标处为 0,每走一步下降不超过这一步的代价),本课这种不重开已关闭格子的实现也能保留最短路;高估时,第一个出队目标可能不是最优答案。
算法比较
| 算法 | 对象 | 时间 | 额外空间 | 前提或工具 | 顺序 |
|---|---|---|---|---|---|
| 线性查找 | 任意数组 | O(n) | O(1) | 无排序要求 | 从前到后 |
| 二分查找 | 有序数组 | O(log n) | O(1) | 有序 | 范围减半 |
| 双指针 | 有序数组 | O(n) | O(1) | 有序 | 两端靠拢 |
| 滑动窗口 | 连续子数组 | O(n) | O(1) | 本课非负值、正目标 | 右侧扩展、左侧收缩 |
| 前缀和 | 区间求和 | 构建 O(n),每次查询 O(1) | O(n) | 很少修改的数组 | 两条分隔线,一次减法 |
| 广度优先搜索 | 图/树 | O(V + E) | O(V) | 队列 | 从近到远 |
| 深度优先搜索 | 图/树 | O(V + E) | O(V) | 栈 | 优先深入 |
| 树遍历 | 二叉树 | O(n) | O(h) | 递归或栈 | 前序/中序/后序 |
| 连通分量 | 无向图 | O(V + E) | O(V) | 访问集合 | 扫描并填充各个岛屿 |
| A* 寻路 | 带权网格/图 | 本课 O((V + E) log V) | O(V + E) | 满足最短路要求的启发式 | 最小 g + h 优先 |
它们不全是在竞争同一个问题。二分问值是否在有序表中,图搜索问顶点如何连接。BFS 与 DFS 才是直接的顺序对照。A* 使用类似 Dijkstra 的优先队列,再加启发式,把搜索集中到目标方向。
建议学习顺序
先学线性查找。逐个检查,命中就停;最坏情况下要读完整个数组。它引出一个问题:多知道一些结构后,能跳过多少工作?
二分查找用一次比较排除半个候选范围。一百万个有序元素最多约二十次探测,对比线性查找最坏的一百万次。再学双指针,利用同样的排序,从两端移动索引。无序失败例会跳过真实数对,说明移动规则必须依赖排序。
滑动窗口让两个指针同向前进,右边加入、左边移除,每个元素至多各经历一次,因此总成本为 O(n)。本课的负数反例使单调性失效,收缩策略可能错过更短的合法窗口。
前缀和不再寻找窗口,而是预先算好所有答案:一遍 O(n) 写下累计和,任意区间和都是 prefix[r + 1] − prefix[l],不论长度都只读两个值。它的失败点是单点更新,会让之后所有累计和失效。
接着用 BFS从序列走向图,观察按层访问如何产生无权最短路,再用 DFS把队列换成栈,比较深入和扩散。树遍历把 DFS 用于二叉树,只改变在两个子调用前、之间或之后记录当前节点的时机,就得到前序、中序和后序。
连通分量在遍历外再加扫描:遇到未着色顶点就启动一次填充,启动次数等于岛屿数。孤立顶点本身也是一个分量。最后学 A*,把搜索边界从 FIFO/LIFO 改为按 g + h 排序,并通过高估启发式的反例检查最短路保证。
全部搜索可视化课程
改变输入或图,逐步执行,对照代码高亮检查访问与排除顺序。