Python基础系列课程 · 终章
欢迎关注微信公众号
在 Python 解释器中输入
import this
,你会看到一段由 Tim Peters 撰写的编程哲学。
它不是语法规则,而是 Python 社区二十多年来沉淀下来的设计智慧。
# 在 Python 交互式环境中
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
...
核心精神: 优雅、明确、简单 。好的代码应该像散文一样可读。
时间复杂度描述的是 算法执行时间随输入规模增长的变化趋势 ,用大 O 记号(Big-O Notation)表示。
我们不关心常数系数和低阶项,只看增长最快的项:
# 例子:计算 1 + 2 + ... + n
# 方法A:循环累加 —— O(n)
def sum_loop(n):
total = 0
for i in range(1, n + 1):
total += i
return total
# 方法B:公式计算 —— O(1)
def sum_formula(n):
return n * (n + 1) // 2
# n = 10^9 时,方法A需要循环10亿次,方法B只需1次乘法
| 复杂度 | 名称 | 示例 | n=10^5时可接受? |
|---|---|---|---|
| O(1) | 常数 | 数组随机访问、哈希表查询 | ✅ 完美 |
| O(log n) | 对数 | 二分查找 | ✅ 极好 |
| O(n) | 线性 | 遍历数组 | ✅ 很好 |
| O(n log n) | 线性对数 | 快排、归并排序 | ✅ 可接受 |
| O(n²) | 平方 | 双重循环、冒泡排序 | ❌ n大时超时 |
| O(2ⁿ) | 指数 | 递归求斐波那契 | ❌ 极慢 |
经验法则: 在编程竞赛和面试中,O(n log n) 通常是可接受的上限,O(n²) 对于 n > 10⁴ 就要警惕了。
空间复杂度描述算法执行过程中 额外占用内存随输入规模的增长趋势 。
# O(1) 空间 —— 只使用固定数量的变量
def reverse_array_inplace(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
return arr
# O(n) 空间 —— 创建了新数组
def reverse_array_new(arr):
result = [] # 额外分配了 n 个空间
for i in range(len(arr) - 1, -1, -1):
result.append(arr[i])
return result
# O(n) 空间 —— 递归调用栈
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1) # 递归深度为 n
分析代码复杂度的三个步骤:
# 例1:嵌套循环 —— O(n²)
for i in range(n): # n 次
for j in range(n): # n 次
print(i, j) # 共 n × n = n² 次
# 例2:嵌套但内层受限 —— O(n log n)
for i in range(n): # n 次
j = 1
while j < n: # log n 次(j 每次翻倍)
j *= 2
print(i, j) # 共 n × log n 次
# 例3:顺序执行 —— 取最大值
for i in range(n): # O(n)
print(i)
for j in range(n): # O(n)
for k in range(n): # O(n²)
print(j, k)
# 总复杂度 = O(n) + O(n²) = O(n²)
给定 n 个非负整数表示容器高度,每根线宽度为 1。找出两根线,使它们与 x 轴围成的容器能盛最多水。
# 输入: height = [1,8,6,2,5,4,8,3,7]
# 索引: 0 1 2 3 4 5 6 7 8
#
# 8 | █ █
# 7 | █ █ █
# 6 | █ █ █ █
# 5 | █ █ █ █ █
# 4 | █ █ █ █ █ █
# 3 | █ █ █ █ █ █ █
# 2 | █ █ █ █ █ █ █ █
# 1 | █ █ █ █ █ █ █ █ █ █ █ █ █
# 0 +---------------------------
#
# 选择索引1(高8)和索引8(高7),宽度7,高度取min(8,7)=7
# 面积 = 7 × 7 = 49(最大值)
枚举所有可能的左右边界组合,计算面积取最大值。
def max_area_brute(height):
n = len(height)
max_area = 0
# 枚举所有 i < j 的组合
for i in range(n):
for j in range(i + 1, n):
# 宽度 = j - i,高度 = 两根线中较矮的那根
width = j - i
h = min(height[i], height[j])
area = width * h
max_area = max(max_area, area)
return max_area
# 时间复杂度: O(n²) —— 双重循环
# 空间复杂度: O(1)
# 测试
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
print(max_area_brute(height)) # 输出: 49
当 n = 10⁵ 时,O(n²) 需要 10¹⁰ 次运算, 严重超时 。需要更聪明的解法。
初始化左右指针在两端。每次移动 较短的那根线 ,因为移动较长的线不可能获得更大的面积。
def max_area_two_pointer(height):
left, right = 0, len(height) - 1
max_area = 0
while left < right:
# 计算当前面积
width = right - left
h = min(height[left], height[right])
max_area = max(max_area, width * h)
# 移动较短的线(关键!)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
# 时间复杂度: O(n) —— 只遍历一次
# 空间复杂度: O(1) —— 只用两个指针
为什么移动较短的线不会错过最优解?
width × min(left_height, right_height)
# 直观理解(以 height=[1,8,6,2,5,4,8,3,7] 为例)
# 初始: left=0(h=1), right=8(h=7), area=8*1=8
# 移动 left (因为 1 < 7): left=1(h=8), right=8(h=7), area=7*7=49 ← 最优!
# 移动 right (因为 7 < 8): left=1(h=8), right=7(h=3), area=6*3=18
# 移动 right (因为 3 < 8): left=1(h=8), right=6(h=8), area=5*8=40
# ... 直到 left >= right
精髓: 每次移动都"放弃"了当前状态下所有不可能的解,逐步缩小搜索空间。这是贪心算法的典型应用。
可读性 > cleverness。代码是写给人看的,偶尔给机器执行。"There should be one obvious way to do it."
忽略常数、低阶项和硬件差异。O(2n) = O(n),O(n² + n) = O(n²)。
数组有序或需要从两端向中间逼近时,考虑双指针将 O(n²) 优化到 O(n)。
有时用空间换时间(哈希表),有时用时间换空间(原地操作)。根据场景选择。
🎉 至此,本课程所有章节学习完毕
愿大家勤勉忠恳,前程似锦