0Pricing
Coding Interview Prep · 课时

从零理解大 O 记法

理解我们为何关注渐近增长,如何忽略常数和低阶项,以及如何快速读懂 Big-O。

从零理解大 O 记法 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。

为什么要衡量算法效率

两个程序可能都正确,但其中一个转瞬间就能完成,另一个却要运行数小时。时间复杂度描述了输入变大时运行时间如何增长。

# O(n) approach
def find_max_linear(nums):
    m = nums[0]
    for n in nums:
        if n > m: m = n
    return m

# O(n^2) approach (unnecessary double loop)
def find_max_quadratic(nums):
    for i in range(len(nums)):
        is_max = all(nums[i] >= nums[j] for j in range(len(nums)))
        if is_max: return nums[i]

print(find_max_linear([3, 1, 4, 1, 5, 9]))  # 9

Big-O:渐近上界

Big-O 描述了计算代价增长速度的最坏情况上界。关键在于忽略常数和较小的项,因为在规模变大时,只有主导项才重要。请查看代码。

# T(n) = 3n^2 + 5n + 100 is O(n^2)
# because the n^2 term dominates for large n

# T(n) = 2n + 1000 is O(n)
# the constant 1000 becomes negligible

# Rule: drop constants and lower-order terms
# 5n^3 + 2n^2 + n + 1  =>  O(n^3)
# 100 * log(n) + n      =>  O(n)
print('O(n^2) example: counting iterations')
n = 1000
count = sum(1 for i in range(n) for j in range(n))
print(count)  # 1_000_000 = n^2

常见的复杂度类别

从最快到最慢依次是:O(1)、O(log n)、O(n)、O(n log n)、O(n^2)、O(2^n)、O(n!)。掌握这些复杂度后,您就能在开始编写任何代码之前选择正确的方法。

import math

n = 1000
print(f'O(1):       {1}')
print(f'O(log n):   {int(math.log2(n))}')
print(f'O(n):       {n}')
print(f'O(n log n): {int(n * math.log2(n))}')
print(f'O(n^2):     {n**2}')
# O(2^n) for n=1000 is astronomically large
# O(n!) even larger

舍弃常数:为什么重要

运行 5n 步或 2n 步都属于 O(n)——常数取决于硬件,而不是算法。大 O 会舍弃这些常数,让您能够在相同标准下比较扩展性。

# Both are O(n) — different constants
def count_a(n):
    total = 0
    for i in range(n):   # n ops
        total += 1
    for i in range(n):   # n ops
        total += 1
    return total  # T(n) = 2n  =>  O(n)

def count_b(n):
    total = 0
    for i in range(5 * n):  # 5n ops
        total += 1
    return total  # T(n) = 5n  =>  O(n)

print(count_a(10), count_b(10))  # 20 50

最好、平均和最坏情况

大 O 表示最坏情况;Ω 表示最好情况;Θ 是同时约束两者的紧确界。当面试官询问“复杂度”时,他们几乎总是指最坏情况。

def linear_search(nums, target):
    for i, n in enumerate(nums):
        if n == target:
            return i  # best case: target at index 0 => O(1)
    return -1         # worst case: not found => O(n)

# Best case O(1): target is first element
print(linear_search([5,1,2,3], 5))   # 0

# Worst case O(n): target not in list
print(linear_search([1,2,3,4], 9))   # -1

O(log n):将搜索空间减半

如果算法每一步都将输入规模减半,例如二分查找,那么它就是 O(log n)。即使有十亿个项目,也只需要大约 30 步——快得惊人。请查看代码。

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    steps = 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, steps
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

import math
arr = list(range(1000))
idx, s = binary_search(arr, 999)
print(f'Found at {idx} in {s} steps (log2(1000)~={math.log2(1000):.1f})')

O(n log n):排序下界

任何基于比较的排序在最坏情况下都至少需要 O(n log n)——这是一个严格的数学下界。因此,先排序再扫描的总体复杂度是 O(n log n),而不是 O(n^2)。代码展示了归并排序。

# Merge sort: O(n log n)
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(a, b):
    res, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: res.append(a[i]); i+=1
        else:             res.append(b[j]); j+=1
    return res + a[i:] + b[j:]

print(merge_sort([5,2,8,1,9,3]))  # [1,2,3,5,8,9]

摊销复杂度

摊销分析会将成本平均分摊到许多次操作上。Python 的 append 的摊销复杂度是 O(1):通常会立即完成,偶尔发生的 O(n) 扩容成本则会分摊到所有 append 操作中。

# Dynamic array append is O(1) amortised
import sys

lst = []
capacities = []
for i in range(16):
    lst.append(i)
    capacities.append(sys.getsizeof(lst))

# Size jumps show reallocation events
for i, c in enumerate(capacities):
    if i > 0 and capacities[i] != capacities[i-1]:
        print(f'Realloc at i={i}, new size={c} bytes')

识别代码中的复杂度

一个快速规则是:数循环次数。一个循环是 O(n),两个嵌套循环是 O(n^2),每次减半的循环是 O(log n)。独立的遍历会执行 add 运算;只有嵌套循环才会相乘。请查看代码。

# Two independent passes: O(n) + O(n) = O(n)
def two_passes(nums):
    total = sum(nums)           # O(n)
    mean = total / len(nums)
    diffs = [abs(n - mean) for n in nums]  # O(n)
    return max(diffs)           # O(n)
# Overall: O(n) -- NOT O(n^2)

# Nested loops: O(n) * O(n) = O(n^2)
def all_pairs(nums):
    pairs = []
    for i in range(len(nums)):       # O(n)
        for j in range(i+1, len(nums)): # O(n)
            pairs.append((nums[i], nums[j]))
    return pairs  # O(n^2)

空间复杂度基础

空间复杂度会跟踪除输入之外额外使用的内存。原地反转是 O(1);哈希表是 O(n)。当您用空间换取时间时,一定要同时说明两者。

# O(1) space: reverse in-place
def reverse_inplace(arr):
    l, r = 0, len(arr) - 1
    while l < r:
        arr[l], arr[r] = arr[r], arr[l]
        l += 1; r -= 1

# O(n) space: create reversed copy
def reverse_copy(arr):
    return arr[::-1]

a = [1, 2, 3, 4, 5]
reverse_inplace(a)
print(a)  # [5, 4, 3, 2, 1]

面试中如何讨论复杂度

即使面试官没有询问,也要主动说明复杂度:“时间复杂度是 O(n log n),空间复杂度是 O(n)。”然后再提供更快的方案。这个习惯能体现真正的资深程度。

# Example of explaining complexity step by step
def two_sum(nums, target):
    # O(n) time: one pass through nums
    # O(n) space: hash map stores up to n elements
    seen = {}  # value -> index
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:   # O(1) lookup
            return [seen[complement], i]
        seen[n] = i
    return []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

快速检查

快速检查一下——展示您对大 O 和复杂度类别的掌握程度。只有一道题,您一定可以做到。🎯

课程回顾

回顾:大 O表示舍弃常数后的最坏情况增长率;您已经了解从 O(1) 到 O(n!) 的复杂度类别;独立循环相加,嵌套循环相乘。

常见问题解答

「从零理解大 O 记法」课时是免费的吗?

是的 — 「从零理解大 O 记法」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。

「从零理解大 O 记法」这节课中我会学到什么?

理解我们为何关注渐近增长,如何忽略常数和低阶项,以及如何快速读懂 Big-O。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Coding Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「从零理解大 O 记法」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Coding Interview Prep 课中编写并运行代码吗?

能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 从零理解大 O 记法
  2. 分析循环与嵌套循环
  3. 递归与递归树法
  4. 空间复杂度与权衡
← 返回 Coding Interview Prep