从算法到人工智能 · 第 1 课:算法不是玄学,它是"做事的步骤"
时长:大概 1 小时(讲 40 分钟 + 动手 20 分钟)
门槛:会一点 Python 基础(会写循环和函数就够)
目标:搞懂"算法"到底是个啥,学会用「复杂度」判断一个做法是快是慢
一、先把"算法"这层窗户纸捅破
很多人一听"算法"两个字,脑子里浮现的是:面试题、大厂高薪、天书、劝退。其实算法一点都不玄乎,它的定义朴素到你会觉得"就这?":
算法,就是解决问题的一系列步骤。
对,就这么简单。你今天早上起床,已经用了好几个算法:
- 起床算法:睁眼 → 关闹钟 → 下床 → 洗漱 → 穿衣 → 出门。
- 煮泡面算法:烧水 → 撕包装 → 下面饼 → 等三分钟 → 倒调料 → 吃。
- 找钥匙算法:摸口袋 → 没有 → 翻包 → 没有 → 回想昨天放哪 → 找到。
这些全是算法。只要是有顺序的、能照着做的步骤,就是算法。
那问题来了——如果算法这么普通,为什么同样的活儿,有人 1 秒干完,有人要 1 小时?差别在哪?
答案只有四个字:步骤不一样。
二、同一个问题,两种算法,天差地别
我们举个特别实在的例子:在一堆数字里,找出最大的那个。
假设你手上有一张纸条,写着 [3, 9, 1, 7, 5],让你找最大数。
算法 A(笨办法):
- 拿第一个数 3,跟后面每一个比一遍,记下比它大的;
- 再拿第二个数 9,跟后面每一个比一遍;
- 反复比,比到最后一个。
你一共比了 4 + 3 + 2 + 1 = 10 次。
算法 B(聪明办法):
- 先假定第一个数 3 是最大的;
- 挨个往后看,碰到更大的就换掉"当前最大";
- 走完一遍,手上留下的就是最大数。
你一共比了 4 次。
5 个数,差距还不明显。但想象纸条上有 100 万个数字:
- 算法 A 要比大约 5000 亿次(数学上叫 n² 量级);
- 算法 B 只要比 100 万次(n 量级)。
同样一台电脑,算法 A 要跑好几个小时,算法 B 一眨眼就完事。这就是算法的价值——不是能不能做出来,而是"多快做出来"。
三、复杂度:衡量"快慢"和"占地"的标尺
先说一句总纲:复杂度其实有两把尺子——
- 时间复杂度:算法跑得快不快,也就是"要花多少步"。
- 空间复杂度:算法占不占内存,也就是"要额外用多少空间"。
这一节先把重点放在"快不快"上,"占不占地"放到本节末尾细说。
上面那个"比多少次",在算法里有个专业名词叫时间复杂度,用大 O 记号来表示。别被这个符号吓到,它说白了就是一句话:
当数据量 n 越来越大时,你的步骤数大概按什么速度涨。
最常见的几档,从慢到快排个队:
| 记号 | 名字 | 直观感受 | 举个现实例子 |
|---|---|---|---|
| O(n!) | 阶乘 | 爆炸,几万就崩 | 暴力解"旅行商"问题 |
| O(2ⁿ) | 指数 | 很恐怖 | 暴力枚举所有组合 |
| O(n²) | 平方 | 数据翻倍,时间翻 4 倍 | 上面那个笨办法 A |
| O(n log n) | 线性对数 | 挺快,能扛百万级 | 快排、归并排序 |
| O(n) | 线性 | 快,扫一遍就行 | 上面那个聪明办法 B |
| O(log n) | 对数 | 飞快,数据翻倍只多一步 | 二分搜索 |
| O(1) | 常数 | 瞬间,跟数据量无关 | 直接取数组某个位置 |
记住两条直觉就够了:
- n 前面有没有"方"(²),是生死线。O(n²) 和 O(n) 看着只差一个上标,数据一大就是"跑一天"和"跑一秒"的区别。
- log n 是算法世界的神。数据从 1 万变 1 亿,O(log n) 只是从 14 步变成 27 步。这就是为什么"二分查找"能那么快。
空间复杂度:别忘了"占地"这件事
光快还不够,算法还得看它吃多少内存。这个"吃内存"的量,就是空间复杂度,同样用大 O 表示。它回答的是:
当数据量 n 越来越大时,你要额外占用的内存大概按什么速度涨。
常见的几档,跟时间那边长得很像:
| 记号 | 含义 | 例子 |
|---|---|---|
| O(1) | 只占固定那么点内存 | 用一个临时变量记最大值 |
| O(n) | 内存随数据一起涨 | 把 n 个数据复制一份存起来 |
| O(n²) | 内存涨得飞快 | 建一个 n×n 的二维表 |
怎么判断:看你的算法有没有新建一大片跟 n 有关的东西。
- 只用几个临时变量 → O(1)
- 建了一个长度是 n 的列表 / 哈希表 → O(n)
- 建了一个 n×n 的二维表 → O(n²)
时间 vs 空间,往往是一对可以交换的筹码:想快,通常就得多占内存;想省内存,通常就得多花时间。后文(第 17 课动态规划)会反复出现的"用空间换时间",说的就是这笔买卖。现在你只要记住:谈复杂度,永远同时问两句——快不快?占不占地?
后面几课我们会反复跟这几档打交道,现在你只要有个"谁快谁慢"的感觉就行。
四、亲手感受一下:用 Python 计时
光说不练假把式。咱们写两段代码,跑一跑,亲眼看 n² 和 n 差多少。
准备:先造一堆数据
import time
# 造 n 个数字,从 0 到 n-1
n = 20000
nums = list(range(n))算法 A:O(n²) 的笨办法
def find_max_slow(nums):
for i in range(len(nums)): # 外层走 n 次
is_max = True
for j in range(len(nums)): # 内层又走 n 次
if nums[j] > nums[i]: # 只要有人比它大,它就不是最大
is_max = False
break
if is_max:
return nums[i] # 这个一定是最大的这段代码里,外层循环 n 次、内层最多也 n 次,所以是典型的 O(n²)。
算法 B:O(n) 的聪明办法
def find_max_fast(nums):
current = nums[0]
for x in nums: # 只走一遍
if x > current:
current = x
return current这个只扫一遍,是 O(n)。
计时对比
start = time.time()
find_max_slow(nums)
print("笨办法耗时:", round(time.time() - start, 4), "秒")
start = time.time()
find_max_fast(nums)
print("聪明办法耗时:", round(time.time() - start, 4), "秒")你大概率会看到类似这样的结果(数值随机器略有浮动):
笨办法耗时: 8.5 秒
聪明办法耗时: 0.002 秒差了 4000 多倍。 而代码呢?笨办法也就多了 3 行而已。这就是"复杂度"这个东西的威力——不是会不会写的问题,是选哪条路的问题。
小提示:n = 20000 如果让你的电脑跑笨办法太慢或太快,可以自己调。调到"笨办法要等好几秒"那个量级,感受最直观。五、复杂度怎么"看"出来(不背公式)
很多人以为判断复杂度要算数学,其实日常代码你扫一眼就能估个大概。给你三个土办法:
土办法 1:数循环嵌套层数
- 一层循环 → 通常是 O(n)
- 两层嵌套循环 → 通常是 O(n²)
- 三层嵌套 → 通常 O(n³),基本别这么写
土办法 2:看每次循环后,范围有没有砍半
- 每次把范围砍一半(比如
mid = (low + high) // 2)→ 这就是 O(log n),快得很 - 每次只往前走一步 → O(n)
土办法 3:忽略常数,只看趋势
3n、10n + 100、n/2,这些统统算 O(n)。常数不重要,因为数据一大,趋势才是主角。
记住:判断复杂度,问自己一句——"数据量翻倍,我的步骤数大概翻几倍?"
- 翻 1 倍 → O(n)
- 翻 4 倍 → O(n²)
- 几乎不翻(只多一点点)→ O(log n)
六、课后动手(20 分钟)🎯
别光看,跟着敲。这些实验都不难,但敲完你对复杂度的感觉会完全不一样。
实验 1:先照抄上面的计时代码
把第四节的 find_max_slow 和 find_max_fast 跑一遍,记下两个耗时,感受一下差距。
实验 2:把 n 翻倍,看时间怎么变
把 n = 20000 改成 n = 40000,再跑一次。对比:
- 聪明办法的时间大概翻了 2 倍(因为 O(n));
- 笨办法的时间大概翻了 4 倍(因为 O(n²))。
把这个现象记下来,这就是"复杂度"最直观的样子。
实验 3:写一个 O(n²) 的"找重复"
给你一个列表,判断里面有没有重复数字。先别管效率,用最笨的"两两比较"写出来:
def has_duplicate(nums):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] == nums[j]:
return True
return False数一下:这是几层循环?时间复杂度是多少?
实验 4(挑战):试试 O(log n) 的二分查找
在一个已经排好序的列表里找某个数,用"每次砍一半"的思路写一个二分查找,然后自己造 100 万个有序数字,看看找一次要多久:
def binary_search(nums, target):
low, high = 0, len(nums) - 1
while low <= high:
mid = (low + high) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1 # 没找到思考:为什么它能在 100 万个数字里,只用 20 步左右就找到目标?(提示:每次砍一半,100 万砍 20 次就剩 1 个了。)
七、小结
- 算法 = 解决问题的步骤,一点都不神秘,你每天都在用。
- 复杂度 = 数据变大时,步骤数怎么涨,用大 O 表示,n² 和 n 的差别是生死之别。
- 选对算法,比写对代码更重要——同样 3 行代码,一条路 8 秒,另一条路 0.002 秒。
今天我们把"算法"这层窗户纸捅破了,也第一次亲手摸到了"复杂度"这个东西。接下来,我们要去见那些真正有用的数据结构——数组、链表、栈、队列——看看它们是怎么帮我们把复杂度压下来的。
别急,一步一个脚印,咱们慢慢把这条路走通。