Chapter 15

再谈编程

Python 之禅 & 算法时间复杂度

Python基础系列课程 · 终章

欢迎关注微信公众号

coder程 查令十街84号

本章目录

1 Python之禅 10 min
2 时间复杂度分析 20 min
3 经典算法实战:最大盛水容器 15 min
4 课程总结与展望 10 min

Python之禅 (The Zen of 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.
...

核心精神: 优雅、明确、简单 。好的代码应该像散文一样可读。

Python之禅 · 核心要义

Beautiful is better than ugly.
优美胜于丑陋。代码首先是写给人看的,其次才是给机器执行。
Explicit is better than implicit.
明确胜于隐晦。不要写"聪明"的代码,要写别人能看懂的代码。
Simple is better than complex.
简单胜于复杂。能用一行解决的,不要用十行;能用十行解决的,不要用复杂的设计模式。
Readability counts.
可读性很重要。这是 Python 最被强调的原则。

Python之禅 · 更多智慧

There should be one-- and preferably only one --obvious way to do it.
做一件事应该只有一种——最好只有一种——显而易见的方式。
Now is better than never. Although never is often better than *right* now.
现在做比不做好,但盲目动手还不如不做。先思考,再编码。
If the implementation is hard to explain, it's a bad idea.
如果实现很难解释,那就是个坏主意。好代码自带说明书。
Namespaces are one honking great idea -- let's do more of those!
命名空间是个绝妙主意,我们应当多加利用!

什么是时间复杂度?

时间复杂度描述的是 算法执行时间随输入规模增长的变化趋势 ,用大 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. 找到最内层循环的操作(基本操作)
  2. 计算该操作执行的总次数
  3. 保留最高阶项,忽略常数系数
# 例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) —— 只用两个指针

双指针法的正确性证明

为什么移动较短的线不会错过最优解?

# 直观理解(以 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

精髓: 每次移动都"放弃"了当前状态下所有不可能的解,逐步缩小搜索空间。这是贪心算法的典型应用。

重难点回顾

🎯 Python之禅的核心

可读性 > cleverness。代码是写给人看的,偶尔给机器执行。"There should be one obvious way to do it."

🎯 Big-O 只看增长趋势

忽略常数、低阶项和硬件差异。O(2n) = O(n),O(n² + n) = O(n²)。

🎯 双指针的适用场景

数组有序或需要从两端向中间逼近时,考虑双指针将 O(n²) 优化到 O(n)。

🎯 时间与空间的权衡

有时用空间换时间(哈希表),有时用时间换空间(原地操作)。根据场景选择。

课程总结

  • Python之禅教会我们:优雅、明确、简单是代码的最高标准
  • 时间复杂度是衡量算法效率的通用语言,O(1) < O(log n) < O(n) < O(n log n) < O(n²)
  • 分析复杂度时关注最高阶项,忽略常数系数
  • 双指针技巧可以将暴力 O(n²) 优化到 O(n),关键在于"移动哪一边"
  • 算法优化的本质是:找到冗余计算并消除它

🎉 至此,本课程所有章节学习完毕
愿大家勤勉忠恳,前程似锦

返回首页 ▶