从零理解大 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])) # 9Big-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)) # -1O(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 反馈 — 无需本地设置。