时长:大概 1 小时(讲 40 分钟 + 动手 20 分钟)
门槛:会一点 Python 基础(会写循环和函数就够)
目标:搞懂"算法"到底是个啥,学会用「复杂度」判断一个做法是快是慢

一、先把"算法"这层窗户纸捅破

很多人一听"算法"两个字,脑子里浮现的是:面试题、大厂高薪、天书、劝退。其实算法一点都不玄乎,它的定义朴素到你会觉得"就这?":

算法,就是解决问题的一系列步骤。

对,就这么简单。你今天早上起床,已经用了好几个算法:

  1. 起床算法:睁眼 → 关闹钟 → 下床 → 洗漱 → 穿衣 → 出门。
  2. 煮泡面算法:烧水 → 撕包装 → 下面饼 → 等三分钟 → 倒调料 → 吃。
  3. 找钥匙算法:摸口袋 → 没有 → 翻包 → 没有 → 回想昨天放哪 → 找到。

这些全是算法。只要是有顺序的、能照着做的步骤,就是算法。

那问题来了——如果算法这么普通,为什么同样的活儿,有人 1 秒干完,有人要 1 小时?差别在哪?

答案只有四个字:步骤不一样。


二、同一个问题,两种算法,天差地别

我们举个特别实在的例子:在一堆数字里,找出最大的那个。

假设你手上有一张纸条,写着 [3, 9, 1, 7, 5],让你找最大数。

算法 A(笨办法)

  1. 拿第一个数 3,跟后面每一个比一遍,记下比它大的;
  2. 再拿第二个数 9,跟后面每一个比一遍;
  3. 反复比,比到最后一个。

你一共比了 4 + 3 + 2 + 1 = 10 次

算法 B(聪明办法)

  1. 先假定第一个数 3 是最大的;
  2. 挨个往后看,碰到更大的就换掉"当前最大";
  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)常数瞬间,跟数据量无关直接取数组某个位置

记住两条直觉就够了:

  1. n 前面有没有"方"(²),是生死线。O(n²) 和 O(n) 看着只差一个上标,数据一大就是"跑一天"和"跑一秒"的区别。
  2. 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:忽略常数,只看趋势

3n10n + 100n/2,这些统统算 O(n)。常数不重要,因为数据一大,趋势才是主角。

记住:判断复杂度,问自己一句——"数据量翻倍,我的步骤数大概翻几倍?"

  • 翻 1 倍 → O(n)
  • 翻 4 倍 → O(n²)
  • 几乎不翻(只多一点点)→ O(log n)

六、课后动手(20 分钟)🎯

别光看,跟着敲。这些实验都不难,但敲完你对复杂度的感觉会完全不一样。

实验 1:先照抄上面的计时代码

把第四节的 find_max_slowfind_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 个了。)


七、小结

  1. 算法 = 解决问题的步骤,一点都不神秘,你每天都在用。
  2. 复杂度 = 数据变大时,步骤数怎么涨,用大 O 表示,n² 和 n 的差别是生死之别。
  3. 选对算法,比写对代码更重要——同样 3 行代码,一条路 8 秒,另一条路 0.002 秒。

今天我们把"算法"这层窗户纸捅破了,也第一次亲手摸到了"复杂度"这个东西。接下来,我们要去见那些真正有用的数据结构——数组、链表、栈、队列——看看它们是怎么帮我们把复杂度压下来的。

别急,一步一个脚印,咱们慢慢把这条路走通。

标签: none

添加新评论