第2章:当资源有了边界(时间与空间复杂度)
兔狲教授的亲切开场
科学家的第一课——推理是有代价的。在开始计算之前,我们需要问问:这要花多少时间?需要多少空间?就像踏上旅程前要检查干粮和行囊,好的思考者总是先评估资源的边界。
核心议题:我们用什么结构来描述世界?
一周后的午后,珠江畔飘来温润的春雨,轻轻敲打着黑石屋书房的玻璃窗。红砖拱窗上,雨滴蜿蜒而下,像极了电路板上的走线。窗外,康乐园的榕树新叶滴着水珠,木棉花在雨中显得更加鲜红。小小猪还在琢磨着上一章的NAND门电路,小海豹则在整理关于布尔的历史笔记。
兔狲教授泡了一壶凤凰单丛,潮汕工夫茶的蜜香在室内氤氲开来,与窗外春雨的气息、红砖墙的泥土味交织在一起。壁炉虽未生火,但1914年建造时的烟道似乎仍留存着历史的温度。
“上节课我们讨论了如何表达世界,”兔狲教授缓缓开口,“从电报的滴嗒声到手电筒的开关,我们用二元逻辑简化了世界。”
小海豹放下笔,若有所思:“教授,我有一个问题。如果我们已经可以用0和1表达世界,那么接下来的问题是什么?”
小小猪从电路板上抬起头,“是啊,就像有了字母,接下来要考虑怎么组织这些字母来写文章?”
兔狲教授微笑:“很好的问题。这就是今天的核心议题:我们用什么结构来描述世界?”
他走到白板前,写下两个词:
时间 —— 空间
“当我们描述任何事物时,”兔狲教授转身面对两人,“都要考虑这两个维度。比如描述一本书:
- 时间:读完它需要多少小时?
- 空间:它占据书架多少厘米?
在计算的世界里,这两个维度变成了时间复杂度和空间复杂度——描述算法如何‘占用’时间和空间的结构。”
全宇宙图书馆的遐想:结构的考验
“还记得上节课我们说到的‘全宇宙图书馆’吗?”兔狲教授问。
小小猪眼睛一亮,“记得!教授说要在全宇宙的书里找一本特定的书……这怎么可能呢?”
小海豹补充道:“宇宙中大约有10^80个原子。如果每本书……”
“让我们先简化一下,”兔狲教授微笑着打断,“假设图书馆有N本书。你想找一本特定的书——《推理科学的起源》。”
两种搜索:暴力与智慧
暴力搜索的沉重代价
小小猪不假思索地说:“那就一本一本找呗!从第一排第一个书架开始。”
“这就是暴力搜索(Brute-force Search),”兔狲教授点头,“也是最直观的方法。让我们算算这要多久。”
他在白板上写下:
假设检查一本书需要1秒钟。
- 100本书:最多100秒 ≈ 1.7分钟(可以接受)
- 10,000本书:最多10,000秒 ≈ 2.8小时(一顿饭的时间)
- 1,000,000本书:最多1,000,000秒 ≈ 11.6天(一次短途旅行)
- 1,000,000,000本书:最多31.7年(半个人生)
小小猪张大了嘴,“如果是全宇宙的图书……”
“这就是现实,”兔狲教授平静地说,“算法的效率不是可有可无的装饰,而是生死攸关的选择。”
兔狲教授的评语
暴力搜索对应的时间复杂度是 O(n)——操作次数与数据规模n成正比。当n很大时,“成正比”可能意味着“不可能完成”。
二分搜索:减半的艺术
小海豹思索片刻,问道:“如果图书馆的书是按顺序排列的呢?比如按书名拼音排序。”
兔狲教授眼睛一亮,“好问题!这时候我们可以用二分搜索(Binary Search)——一种聪明得多的策略。”
他画出一个简单的流程图:
从中间翻开一本书
↓
是目标书吗? → 是 → 成功!
↓ 否
目标在左边还是右边?
↓
舍弃另一半,重复过程“每次检查,我们都把搜索范围减半,”兔狲教授解释,“就像不断对折一张纸。”
小小猪迅速计算:“16本书的话……第一次剩8本,第二次剩4本,第三次剩2本,第四次就找到了!只要4次!”
“对,”兔狲教授赞许道,“100本书只需要7次,10亿本书只需要30次。”
他在白板上画出对比表:
| 书籍数量 | 暴力搜索次数 | 二分搜索次数 |
|---|---|---|
| 10 | 10 | 4 |
| 100 | 100 | 7 |
| 1,000 | 1,000 | 10 |
| 1,000,000 | 1,000,000 | 20 |
| 1,000,000,000 | 1,000,000,000 | 30 |
小海豹轻声说:“这个差距……是指数级的。一边是线性增长,一边是对数增长。”
进阶科普:对数时间的数学原理
二分搜索的时间复杂度是 O(log₂ n),源于每次将问题规模减半。
数学上,设初始规模为n,经过k次减半后规模为1:
这里的log₂ n增长极其缓慢:
- log₂ 10 ≈ 3.32
- log₂ 100 ≈ 6.64
- log₂ 1,000,000 ≈ 19.93
- log₂ 1,000,000,000 ≈ 29.90
这种“缓慢增长”是算法设计的圣杯之一。许多高效算法(如平衡二叉搜索树、快速排序的期望复杂度)都蕴含了对数思想。
思想模型:资源意识——描述世界的结构
兔狲教授走到窗边,望着窗外的雨丝。“回到我们最初的问题:我们用什么结构来描述世界?”
他转身面对两人,指向白板上早已写下的两个词:
时间 —— 空间
“这就是答案,”兔狲教授说,“在计算的世界里,我们用时间结构和空间结构来描述一切。这引出了我们今天最重要的思想模型:资源意识。”
“计算不是发生在真空中的魔法。它消耗两种宝贵的资源:时间和空间。”
时间复杂度:增长的阶
“我们刚才讨论的搜索次数,就是时间复杂度,”兔狲教授说,“但计算机科学家不关心‘具体多少秒’,而是关心增长的趋势。”
他在白板上写下一串符号:
- O(1):常数时间。无论多少数据,操作时间不变
- O(log n):对数时间。二分搜索,每次减半
- O(n):线性时间。暴力搜索,与数据量成正比
- O(n log n):线性对数时间。许多高效排序算法
- O(n²):平方时间。嵌套循环比较所有对
- O(2ⁿ):指数时间。某些组合问题,灾难性的增长
“这就是大O记号(Big O notation),”兔狲教授解释,“它描述的是算法最坏情况下的渐进行为。”
小小猪指着O(2ⁿ)问:“这个‘指数时间’有多可怕?”
兔狲教授画了一个简单的表格:
| n | O(n) | O(n²) | O(2ⁿ) |
|---|---|---|---|
| 10 | 10 | 100 | 1,024 |
| 20 | 20 | 400 | 1,048,576 |
| 30 | 30 | 900 | 1,073,741,824 |
| 40 | 40 | 1,600 | 约1.1万亿 |
| 50 | 50 | 2,500 | 约1.1千万亿 |
“看到吗?”兔狲教授说,“当n=50时,O(2ⁿ)已经是一个天文数字。这就是为什么我们说指数复杂度的问题在实际中往往无解。”
空间复杂度:记忆的代价
小海豹若有所思:“除了时间,存储信息也需要空间。”
“正是,”兔狲教授点头,“这就是空间复杂度。”
他举了个例子:“假设你要排序一副扑克牌。有两种方法:
- 原地排序:就在桌面上移动牌,几乎不需要额外空间(O(1)空间)
- 非原地排序:需要另一个空桌子来放中间结果(O(n)空间)
“空间和时间常常需要权衡,”兔狲教授继续说,“有时我们可以用更多空间换取更快时间,这叫‘空间换时间’。反之,在内存有限的设备上,我们可能选择较慢但省空间的算法。”
进阶科普:大O记号的历史与哲学
大O记号由德国数学家保罗·巴赫曼在1894年引入,后由埃德蒙·兰道推广。符号O源于德语“Ordnung”(阶),描述函数的增长阶。
大O记号的核心哲学是抽象与简化:
- 忽略常数因子:3n和100n都是O(n)
- 忽略低阶项:n² + 1000n + 50000是O(n²)
- 关注最坏情况:为算法性能提供保证
这种简化不是“不精确”,而是有目的的抽象。就像地图不需要1:1的比例尺一样,复杂度分析不需要精确到时钟周期。
值得思考的是:大O记号反映的是渐近行为,对于小规模数据,常数因子可能更重要。这就是为什么有时“理论上更优”的算法在实践中不如简单算法。
正交计算图:看见复杂度的光谱
兔狲教授打开投影仪,一幅规整的计算图出现在屏幕上。
“这是不同复杂度类别的正交计算图,”兔狲教授指着图说,“从左到右:输入规模n,经过不同复杂度类别,产生不同的增长曲线。”
小小猪盯着图,“O(1)到常数,O(log n)到对数……这个O(2ⁿ)到指数,连线突然变长了!”
“这就是关键,”兔狲教授说,“指数增长就像脱缰的野马——开始时微不足道,转眼间就冲破一切限制。”
小海豹仔细看着图的层级结构,“教授,这个图的设计……每个复杂度类别都在同一层级,输出曲线也在同一层级,很有秩序。”
“这就是正交计算图的力量,”兔狲教授微笑,“它用视觉语言告诉我们:复杂度不是模糊的感觉,而是清晰可分的类别。”
进阶科普:复杂度类别的严格定义
计算机科学中,复杂度类别有严格的形式定义:
- P(多项式时间):存在多项式时间算法解决的问题
- NP(非确定性多项式时间):解可以在多项式时间内验证的问题
- EXP(指数时间):需要指数时间的问题
- LOGSPACE:只需要对数空间的问题
著名的P vs NP问题(千禧年七大数学难题之一)问的是:P是否等于NP?如果是,那么所有容易验证解的问题也都容易找到解。
目前普遍认为P ≠ NP,这意味着有些问题本质上就比另一些问题困难。这种“本质困难性”的发现,是20世纪理论计算机科学最深刻的洞见之一。
现实世界的复杂度困境
案例一:社交网络的蝴蝶效应
小小猪拿出手机,“像微信这样的社交网络,怎么找到‘可能认识的人’?”
“简单的算法是O(n²)的,”兔狲教授说,“假设你有100个好友,每个好友有100个好友。要找到共同好友,需要比较100×100=10,000次。”
他顿了顿,“但如果每人有1,000个好友呢?100万次操作。实际社交网络有数亿用户,O(n²)根本不可行。”
“那怎么办?”小小猪问。
“实际系统使用近似算法、索引技术和分布式计算,”兔狲教授说,“牺牲一点精确性,换取可行性。这是工程中常见的妥协。”
案例二:地图导航的迷宫
小海豹打开地图应用,“导航软件怎么找到最短路径?”
“最简单的暴力搜索是O(n!),”兔狲教授说,“n是路口数量。对于20个路口的地图,20! ≈ 2.4×10¹⁸种可能路径。”
小小猪倒吸一口凉气,“这……算到宇宙末日也算不完。”
“所以需要更聪明的算法,”兔狲教授说,“比如Dijkstra算法(O(n²))或A*算法(用启发式函数指导搜索)。这些算法不保证找到所有可能路径,但保证在可行时间内找到最优或近似最优解。”
兔狲教授的评语
现实世界的问题往往需要在理想与可行之间找到平衡。完美的解可能不存在,或者代价太高。好的算法设计师懂得:有时,足够好的解就是最好的解。
空间与时间的永恒舞蹈——描述世界的结构互动
兔狲教授回到白板前,画了一个坐标轴。
“让我们回到最初的问题,”他说,“我们用什么结构来描述世界?答案是:时间和空间。但这两者不是孤立的,它们相互关联、相互制约。”
他指着坐标轴:“时间和空间就像天平的两端,增加一端,往往可以减少另一端。这就是描述世界的两种基本结构如何互动。”
空间换时间:查表法
“比如计算斐波那契数列,”兔狲教授举例,“朴素递归是O(2ⁿ)的,极其缓慢。但如果我们用一个数组存储已经计算过的结果——”
他在白板上写:
fib = [0, 1] # 存储空间
for i in range(2, n+1):
fib[i] = fib[i-1] + fib[i-2] # 查表而非重复计算“——就变成了O(n)时间,O(n)空间。用一点存储空间,换来巨大的时间节省。”
时间换空间:流式处理
“反过来,”兔狲教授继续说,“在内存有限的设备(如嵌入式系统)上,我们可能选择流式处理——一次只处理一小部分数据,虽然可能多花时间,但大大节省空间。”
小海豹若有所思:“这就像读书时,是买下整本书(占用书架空间),还是每次去图书馆借(花费时间)?”
“很好的比喻!”兔狲教授赞道,“资源分配的本质是权衡。没有免费的午餐,只有基于约束的优化选择。”
进阶科普:时空权衡的数学形式
时空权衡可以形式化为时间-空间折衷定理。对于许多问题,存在一个连续谱:
设解决问题需要时间T和空间S,则通常有:
其中f(n)是问题固有的复杂度函数。
例如,在排序中:
- 快速排序:期望O(n log n)时间,O(log n)空间(递归栈)
- 归并排序:O(n log n)时间,O(n)空间(需要辅助数组)
- 堆排序:O(n log n)时间,O(1)空间(原地排序)
选择哪种算法,取决于具体场景:数据规模、内存限制、是否需要稳定性等。没有绝对的最好,只有最适合。
关键要点
兔狲教授的总结:描述世界的两种结构
当我们用计算来描述世界时,时间和空间是我们最基本的描述框架。
- 推理有代价:时间和空间是计算的硬约束,好的思考者首先评估资源边界
- 复杂度类别:O(1)、O(log n)、O(n)、O(n²)、O(2ⁿ)代表不同的增长阶,差异可能天壤之别
- 大O哲学:关注渐近趋势而非具体数字,进行有目的的抽象
- 时空权衡:时间和空间常常可以相互转换,选择取决于具体约束
- 可行性思维:现实问题往往需要在意与可行之间找到平衡,完美主义可能是效率的敌人
代码实践:复杂度分析的Python实现
"让我们用代码来理解不同时间复杂度的区别,"兔狲教授说,"从直观到抽象,从具体操作到渐近分析。"
不同时间复杂度的算法实现
import time
import matplotlib.pyplot as plt
# O(1) 常数时间复杂度
def constant_time_operation(n):
"""常数时间操作:无论输入多大,执行时间相同
参数:
n: 输入规模
返回:
总是返回42(示例操作)
"""
return 42 # 无论n多大,这个操作的时间都相同
# O(log n) 对数时间复杂度
def binary_search_iterative(arr, target):
"""二分查找:每次将搜索空间减半
参数:
arr: 已排序的数组
target: 要查找的目标值
返回:
目标值在数组中的索引,如果不存在则返回-1
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # 防止溢出
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # 搜索右半部分
else:
right = mid - 1 # 搜索左半部分
return -1
# O(n) 线性时间复杂度
def linear_search(arr, target):
"""线性查找:逐个检查每个元素
参数:
arr: 数组
target: 要查找的目标值
返回:
目标值在数组中的索引,如果不存在则返回-1
"""
for i, value in enumerate(arr):
if value == target:
return i
return -1
# O(n log n) 线性对数时间复杂度
def merge_sort(arr):
"""归并排序:分而治之的排序算法
参数:
arr: 待排序的数组
返回:
排序后的数组
"""
if len(arr) <= 1:
return arr
# 分:将数组分成两半
mid = len(arr) // 2
left_half = merge_sort(arr[:mid])
right_half = merge_sort(arr[mid:])
# 治:合并两个有序数组
return merge(left_half, right_half)
def merge(left, right):
"""合并两个有序数组"""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# 添加剩余元素
result.extend(left[i:])
result.extend(right[j:])
return result
# O(n²) 平方时间复杂度
def bubble_sort(arr):
"""冒泡排序:重复比较相邻元素
参数:
arr: 待排序的数组
返回:
排序后的数组
"""
n = len(arr)
for i in range(n):
# 最后i个元素已经排序好
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j] # 交换
return arr
# O(2ⁿ) 指数时间复杂度
def fibonacci_recursive(n):
"""递归计算斐波那契数(指数时间复杂度示例)
参数:
n: 斐波那契数的位置
返回:
第n个斐波那契数
"""
if n <= 1:
return n
return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
# O(2ⁿ) 的改进版本:动态规划(下一章详细讲)
def fibonacci_dp(n):
"""动态规划计算斐波那契数(线性时间复杂度)
参数:
n: 斐波那契数的位置
返回:
第n个斐波那契数
"""
if n <= 1:
return n
fib = [0] * (n + 1)
fib[1] = 1
for i in range(2, n + 1):
fib[i] = fib[i - 1] + fib[i - 2]
return fib[n]时间复杂度对比实验
def measure_time_complexity():
"""测量不同时间复杂度算法的实际运行时间"""
sizes = [10, 100, 1000, 10000]
results = {}
for size in sizes:
# 准备测试数据
sorted_arr = list(range(size))
unsorted_arr = list(range(size, 0, -1)) # 倒序排列
# O(1) - 常数时间
start = time.time()
constant_time_operation(size)
o1_time = time.time() - start
# O(log n) - 对数时间(在有序数组中查找中间值)
target = size // 2
start = time.time()
binary_search_iterative(sorted_arr, target)
olog_time = time.time() - start
# O(n) - 线性时间(查找最后一个元素)
start = time.time()
linear_search(sorted_arr, size - 1)
on_time = time.time() - start
# O(n log n) - 线性对数时间(对倒序数组排序)
start = time.time()
merge_sort(unsorted_arr[:]) # 使用副本避免原地修改
onlogn_time = time.time() - start
# O(n²) - 平方时间(对倒序数组排序)
# 注:对于大n,bubble_sort会很慢,所以小一点
if size <= 1000:
start = time.time()
bubble_sort(unsorted_arr[:])
on2_time = time.time() - start
else:
on2_time = None # 太大,跳过
# O(2ⁿ) - 指数时间(计算斐波那契数)
# 注:指数时间增长太快,只测试小n
if size <= 30:
start = time.time()
fibonacci_recursive(size)
o2n_time = time.time() - start
else:
o2n_time = None
results[size] = {
'O(1)': o1_time,
'O(log n)': olog_time,
'O(n)': on_time,
'O(n log n)': onlogn_time,
'O(n²)': on2_time,
'O(2ⁿ)': o2n_time
}
print(f"\n输入规模 n = {size}:")
for complexity, t in results[size].items():
if t is not None:
print(f" {complexity}: {t:.6f}秒")
else:
print(f" {complexity}: 太大,跳过")
return results
# 运行实验
print("不同时间复杂度算法运行时间对比实验")
print("=" * 50)
results = measure_time_complexity()复杂度可视化工具
def plot_complexity_growth():
"""可视化不同时间复杂度的增长趋势"""
n_values = list(range(1, 101))
# 计算不同复杂度函数的增长
constant = [1] * len(n_values)
logarithmic = [np.log2(n) for n in n_values]
linear = n_values
linearithmic = [n * np.log2(n) for n in n_values]
quadratic = [n ** 2 for n in n_values]
exponential = [2 ** n for n in n_values]
plt.figure(figsize=(12, 8))
plt.plot(n_values, constant, label='O(1) - 常数', linewidth=2)
plt.plot(n_values, logarithmic, label='O(log n) - 对数', linewidth=2)
plt.plot(n_values, linear, label='O(n) - 线性', linewidth=2)
plt.plot(n_values, linearithmic, label='O(n log n) - 线性对数', linewidth=2)
plt.plot(n_values, quadratic, label='O(n²) - 平方', linewidth=2)
# 指数增长太快,单独显示小范围
plt.plot(n_values[:10], exponential[:10], label='O(2ⁿ) - 指数 (n≤10)', linewidth=2)
plt.xlabel('问题规模 (n)', fontsize=12)
plt.ylabel('时间复杂度增长', fontsize=12)
plt.title('不同时间复杂度的增长趋势对比', fontsize=14)
plt.legend()
plt.grid(True, alpha=0.3)
plt.yscale('log') # 使用对数刻度更好地显示差异
print("复杂度增长趋势图已生成。关键观察:")
print("1. O(1)和O(log n)增长最慢,适合大规模问题")
print("2. O(n)和O(n log n)是大多数实际问题的理想选择")
print("3. O(n²)在大规模时会急剧变慢")
print("4. O(2ⁿ)即使在小规模也增长极快,必须避免")
# 注意:需要numpy和matplotlib库
try:
import numpy as np
plot_complexity_growth()
except ImportError:
print("\n提示:要运行可视化代码,请先安装numpy和matplotlib")
print("安装命令: pip install numpy matplotlib")
print("\n" + "=" * 50)
print("兔狲教授的复杂度思考题:")
print("1. 如果n=100万,O(n²)算法需要多久?假设每步操作1纳秒")
print("2. 二分查找(O(log n))比线性查找(O(n))快多少倍?当n=10亿时呢?")
print("3. 为什么归并排序(O(n log n))比冒泡排序(O(n²))更适合大规模数据?")"通过这些代码,"兔狲教授总结道,"我们不仅理解了不同时间复杂度的概念,更学会了资源意识——在开始编码前,先思考:这要花多少时间?需要多少空间?这是科学推理的第一步。"
兔狲教授的思考题
实践探索(适合小小猪)
复杂度实验:编写两个程序:一个O(n²)的嵌套循环,一个O(n)的单循环。测试当n=1000, 10000, 100000时的运行时间。感受指数差异。
算法设计:设计一个“找数组最大值”的算法。先写一个O(n²)版本(比较所有对),再改进为O(n)版本(遍历一次)。体会算法改进的力量。
现实观察:观察身边的软件或设备(如电梯、红绿灯、搜索引擎),猜测它们可能使用了什么时间复杂度的算法。为什么这么选择?
历史探究(适合小海豹)
符号溯源:研究大O记号的历史。为什么选择字母O?保罗·巴赫曼和埃德蒙·兰道各自贡献了什么?
问题演化:调查“P vs NP问题”的来龙去脉。为什么这个问题如此重要?目前有哪些进展和假说?
思想脉络:复杂度理论如何从图灵的可计算性理论发展而来?两者之间有什么深刻的联系?
综合思考
伦理困境:假设你设计一个医疗诊断系统。一个算法准确率99.9%但需要10分钟,另一个准确率99%但只需1秒。你如何选择?为什么?
极限挑战:如果摩尔定律继续有效(每18个月计算能力翻倍),能解决指数复杂度的问题吗?用数学证明你的观点。
创造练习:设计一个“复杂度寓言”——用故事形式解释O(1)、O(log n)、O(n)、O(n²)、O(2ⁿ)的区别。(提示:可以比喻为不同速度的交通工具)
未来想象:量子计算宣称能解决某些指数复杂度问题(如大数分解)。研究这是如何可能的?这会对现有复杂度理论产生什么冲击?
下一步预告
雨渐渐停了,夕阳的金光透过云层,在珠江江面上洒下一片碎金。康乐园的暮色温柔,木棉花在晚霞中显得格外沉静。
“今天我们从‘描述世界的结构’这个问题开始,”兔狲教授收拾着功夫茶具,“探索了时间和空间作为计算的基本框架,看到了推理的代价,看到了资源边界的存在。”
小小猪还在摆弄着计时器,测试不同算法的实际运行时间,电脑屏幕的光映在他专注的脸上。
小海豹合上笔记本,轻声说:“教授,如果有些问题,无论我们多聪明,无论计算机多快,都本质上无法在合理时间内解决呢?”
兔狲教授的动作停顿了一下,茶壶悬在半空。
“你说到了关键,”他缓缓道,“下一章,我们要探讨的正是这个问题:有些问题,连最强大的计算机也无法解决。”
小小猪猛地抬头,“什么?还有计算机解决不了的问题?”
“不是‘现在’解决不了,”兔狲教授纠正,“而是永远解决不了。这是计算的本质边界。”
小海豹眼睛一亮,“图灵的停机问题?”
“是的,”兔狲教授微笑,“我们将探索可计算性的极限——什么是计算机的终极边界。”
窗外,最后一缕阳光消失在地平线,远处珠江上的轮船亮起灯火。黑石屋的书房里,三个思考者的影子被灯光拉得很长,投在红砖墙上。
小小猪的笔记:测试结果让我震惊!O(n²)算法在n=10000时跑了5秒,O(n)算法只用了0.005秒。1000倍的差距!理论上的差异在现实中如此真实。
小海豹的笔记:读了大O的历史,发现它最初用于数论中素数分布的研究。数学的各个分支总是这样奇妙地连接着。
兔狲教授的结语:记住,好的算法设计师首先要有的就是资源意识。在开始编码前,先问问:这要花多少时间?需要多少空间?但更重要的是:这个问题本身有高效的解吗?我们慢慢来,理解了最重要。
