算法类型与描述形式

算法不只是「一段逻辑」。不同类型的算法,表达重点完全不同:有的重点在执行步骤,有的在状态变化,有的在递归关系,有的在数学关系。表达形式必须匹配表达重点——流程图、状态图、递归树、公式推导各有适用场景。「一个算法必须有流程图」是错的,形式由类型决定。


一、从两个算法开始

同样是「一个函数实现的算法」,二分查找和快速排序画成流程图,观感完全不同。

二分查找——重点是步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
开始

设置 left=0, right=n-1

left <= right ?
├─ 否 → 返回 -1
└─ 是 → mid=(left+right)/2

a[mid]==target ?
├─ 是 → 返回 mid
└─ 否 → a[mid]<target ?
├─ 是 → left=mid+1
└─ 否 → right=mid-1

回到 left<=right 判断(循环)

流程图画出来清清楚楚:初始化 → 判断 → 执行 → 循环。

快速排序——重点是递归关系:

1
2
3
4
5
void quickSort(int a[], int l, int r) {
int p = partition(a, l, r);
quickSort(a, l, p - 1); // 递归左半
quickSort(a, p + 1, r); // 递归右半
}

硬画流程图会变成一堆乱箭头。它真正的表达重点是分治结构——递归树更合适:

1
2
3
4
5
          quickSort(0,7)
/ \
quickSort(0,3) quickSort(5,7)
/ \ / \
... ... ... ...

同样是一个函数,二分查找该画流程图,快速排序该画递归树。差别不在函数,在算法的类型。


二、为什么类型决定形式

算法的类型,本质是表达重点不同:

1
2
3
4
5
6
7
8
表达重点          → 该用什么形式
──────────────────────────────────────
执行步骤 → 流程图(先做什么、再做什么、怎么判断)
状态变化 → 状态图 / 状态机(有哪些状态、怎么转移)
递归关系 → 递归树(一层层怎么展开)
数据如何变化 → 数据结构图(数据长什么样、怎么被转换)
数学关系 → 公式推导(输入和输出之间的数学关系)
选择策略 → 决策树(每一步在什么条件下选哪条路)

判断标准一句话:

算法描述的是「步骤」就画流程图;描述的是「状态」就画状态图;描述的是「递归」就画递归树;描述的是「数学」就写公式。

不是「一个函数 = 必须有流程图」,而是「算法的表达重点 = 它该用的形式」。


三、常见算法类型 → 最佳描述形式对照

算法类型 表达重点 推荐描述
顺序处理 执行步骤 流程图
if/else 判断 分支路径 流程图
循环算法 反复执行 流程图
搜索算法 步骤 + 状态变化 流程图 + 状态变化
排序算法 数据的动态变化过程 动态过程图
递归算法 递归展开关系 递归树
动态规划 子问题之间的转移 状态转移表
图算法 节点/边的结构变化 图结构变化
贪心算法 每步的选择策略 决策过程图
字符串算法 匹配状态 状态机
数学算法 输入输出间的数学关系 公式推导
数据结构操作 数据结构的形态变化 数据结构变化图

举例说明几个差异最大的:

动态规划——状态转移表。 画流程图没法表达「子问题怎么复用」。状态转移表一行一状态:

1
2
3
4
          金额 0  1  2  3  4  5
硬币 1 0 1 2 3 4 5
硬币 2 0 1 1 2 2 3
硬币 5 0 1 1 2 2 1

表本身就是算法——每个格子怎么由前面格子算出,比任何流程图都清楚。

字符串匹配——状态机。 KMP 的核心不是步骤,而是「当前匹配到哪个状态、失配时跳到哪」:

1
2
3
4
     匹配'A'        匹配'B'        匹配'A'
──→ 状态0 ──→ 状态1 ──→ 状态2 ──→ 状态3
│失配 │失配
└──────────────┴────→ 回退到已匹配前缀的状态

数学算法——公式推导。 比如求最大公约数,欧几里得的表达重点是数学关系,不是执行步骤:

1
2
gcd(a, b) = gcd(b, a mod b)
gcd(a, 0) = a

公式两行说完的事,流程图反而要画一堆框。


四、公式也是算法的一种表达,不是另一类东西

「算法 = 代码」是误解。同一个算法,在不同抽象层级有不同的表达:

1
2
3
4
5
数学公式     → 算法思想(最抽象)
伪代码 → 接近实现的步骤描述
流程图 → 控制结构的图形化
状态图/递归树 → 结构关系的图形化
代码实现 → 可运行的最终形态

比如二分查找:

1
2
3
4
5
6
7
8
9
10
公式/伪代码:
while left <= right:
mid = (left+right)/2
if a[mid]==target: return mid
if a[mid]<target: left=mid+1
else: right=mid-1

流程图:见第一节的图

代码:binarySearch(a, target)

同一种算法,多种表达形式。 选择哪种,取决于当时要讲清楚什么——讲步骤用流程图,讲证明用公式,讲实现用代码。


五、描述算法的最短完整骨架

实际工程里分析一个算法函数,通常不是只画一张图,而是走一个完整骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
算法名称        ← 它是什么

问题定义 ← 解决什么

输入 / 输出 ← 参数和结果

核心思想 ← 为什么这么做

数据结构 ← 用什么保存数据

执行过程 ← 流程图 / 状态图 / 递归树(按类型选)

伪代码 ← 接近实现的步骤

代码实现 ← 可运行形态

复杂度分析 ← O(n), O(log n)

测试案例 ← 怎么验证

以 groupAnagrams 为例:

1
2
3
4
5
6
问题定义:把字母相同、排列不同的字符串分到一组
输入输出:vector<string> → vector<vector<string>>
核心思想:排序后相同的字符串互为变位词
数据结构:unordered_map<string, vector<string>>
执行过程:遍历 → 排序生成key → 放入map → 返回value集合(流程图合适)
复杂度:时间 O(n·k·logk),空间 O(n·k)

不同算法在这个骨架上,唯一会换的就是「执行过程」那一格——按类型换成对应形式。


六、在工程中的位置

这条线不是孤立的:

1
2
3
4
5
6
7
模块三要素(主线)       数据 + 算法 + 接口

算法(本线) 分类型,每种类型选表达形式

功能函数 数据结构 + 算法 + 原子化接口

七条流 流程/状态/数据 本来就是算法的运行时形态
  • 主线讲「模块里有算法」——本线讲「算法怎么被分析、怎么表达」;
  • 七条流讲「程序运行时的流」——算法图是设计时的表达,流是运行时的观察,同一结构的两面;
  • 文档写作时,「算法类型 → 描述形式」直接决定一章怎么画图、怎么写。

收束

1
2
3
4
5
6
7
8
9
10
11
12
13
算法 ≠ 必须有流程图

算法的类型 = 表达重点

步骤 → 流程图 递归 → 递归树
状态 → 状态图 数学 → 公式
转移 → 状态转移表 选择 → 决策树

同一个算法可有多种表达(公式/伪代码/流程图/代码)

分析算法的骨架:思想 → 数据结构 → 执行过程 → 复杂度 → 测试

执行过程那一格,按算法类型选形式

流程图只是算法的形式之一。 形式由类型决定,类型由表达重点决定——这才是算法描述的第一原则。