【算法竞赛学习笔记】基础概念篇:算法复杂度
前言
本文为个人学习的算法学习笔记,学习笔记,学习笔记,不是经验分享与教学,不是经验分享与教学,不是经验分享与教学,若有错误各位大佬轻喷(T^T)。主要使用编程语言为Python3,各类资料题目源于网络,主要自学途径为蓝桥云课,侵权即删。
一、算法复杂度概述
算法复杂度是衡量算法效率的核心指标,主要分为两类:
- 时间复杂度:衡量算法执行时间随数据规模增长的变化趋势(关注 “快不快”)
- 空间复杂度:衡量算法占用的额外存储空间随数据规模增长的变化趋势(关注 “省不省空间”)
复杂度分析不依赖具体硬件 / 语言,仅关注 “数据规模” 与 “资源消耗” 的关系,是评估算法优劣的通用标准。
二、时间复杂度
2.1 定义
时间复杂度(渐进时间复杂度)表示算法执行次数(或时间)随数据规模 n 增长的趋势,而非实际执行时间。核心思想:当 n 足够大时,公式中的低阶项、常量、系数对增长趋势影响极小,可忽略,仅保留 “最大量级”。
示例:从代码到时间复杂度
文档中两个经典代码案例的 Python 实现与分析:
- 单循环(线性级)
def sum_arr(arr):sum_val = 0 # 1次(常量)for i in range(len(arr)): # 初始化1次,循环n次 → 共n+1次sum_val = sum_val + arr[i] # n次return sum_val # 1次(常量)
- 总执行次数:1 + (n+1) + n + 1 = 2n + 3
- 忽略低阶项(3)和系数(2),时间复杂度为 O(n)(线性级)
- 双循环(平方级)
def sum_double_loop(arr):sum_val = 0 # 1次(常量)for i in range(len(arr)): # 外层循环:n+1次for j in range(len(arr)): # 内层循环:n*(n+1)次sum_val = sum_val + arr[i] + arr[j] # n²次return sum_val # 1次(常量)
- 总执行次数:1 + (n+1) + n*(n+1) + n² + 1 = 2n² + 2n + 3
- 忽略低阶项(2n+3)和系数(2),时间复杂度为 O(n²)(平方级)
2.2 常见时间复杂度及示例
不同复杂度的增长速度差异极大,下表整理了核心类型及 Python 典型场景:
复杂度 | 描述 | 增长速度 | 典型示例 |
---|---|---|---|
O(1) | 常数级 | 最快(无增长) | 基本运算(加减乘除)、固定次数循环(如循环 1000 次,与 n 无关) |
O(log n) | 对数级 | 很慢 | 二分查找、while 循环中变量 “倍增 / 倍减”(如 i=i*2,直到 i>n) |
O(n) | 线性级 | 较慢 | 单循环(遍历列表、顺序查找) |
O(n log n) | 线性对数级 | 中等 | 高效排序算法(Python 内置 sorted ()、归并排序) |
O(n²) | 平方级 | 较快 | 双重循环(如冒泡排序、求 “和谐对” 问题的暴力解法) |
O(n³) | 立方级 | 很快 | 三重循环(如三维列表遍历、矩阵多重乘法) |
O(2ⁿ) | 指数级 | 极快 | 暴力递归解决子集问题(如 n 个元素的所有子集,共 2ⁿ个) |
O(n!) | 阶乘级 | 最快(不可接受) | 暴力解决全排列问题(如 n 个元素的所有排列,共 n! 种) |
关键示例详解
- O (1):常数级无论数据规模 n 多大,执行次数固定:
def constant_sum(n):sum_val = 0for i in range(10000): # 固定10000次,与n无关sum_val += ireturn sum_val
- 额外执行次数不随 n 变化,时间复杂度为 O(1)
- O (log n):对数级核心特征:循环次数随 n 增长,但增长速度是 “对数级”(底数可忽略,因 log₂n 与 log₃n 仅差系数):
def log_n_example(n):i = 1count = 0while i <= n: # 循环次数k满足2ᵏ > n → k≈log₂ni *= 2count += 1return count
- 当 n=8 时,循环 4 次(1→2→4→8→16),log₂8=3,量级为 log n;
- 当 n=10²⁴时,循环仅约 80 次,远快于 O (n)
2.3 复杂度增长速度对比
不同复杂度的差距随 n 增大呈指数级扩大,以 n=20 为例:
复杂度 | n=20 时的执行次数量级 | 能否接受 |
---|---|---|
O(1) | 1 | 完全接受 |
O(log n) | ~5 | 完全接受 |
O(n) | 20 | 完全接受 |
O(n log n) | ~100 | 完全接受 |
O(n²) | 400 | 接受 |
O(2ⁿ) | ~100 万 | 勉强接受 |
O(n!) | ~2.4×10¹⁸ | 完全不可接受 |
结论:O (2ⁿ) 和 O (n!) 仅适用于 n≤20 的极小数据,工程中优先选择 O (n log n) 及更优复杂度的算法
2.4 最好、最坏、平均时间复杂度
当算法执行次数受 “输入数据分布” 影响时,需区分三种复杂度(大多数情况无需区分,仅当复杂度有量级差距时需关注)。
示例:列表查找元素
# 在列表arr中查找x,返回索引(未找到返回-1)
def find_element(arr, n, x):pos = -1for i in range(n):if arr[i] == x:pos = ibreak # 找到后立即退出return pos
-
最好时间复杂度:最理想情况的执行次数若 x 是列表第一个元素(arr [0]=x),循环仅执行 1 次 → O(1)。
-
最坏时间复杂度:最糟糕情况的执行次数若 x 是列表最后一个元素,或 x 不存在,循环执行 n 次 → O(n)。
-
平均时间复杂度:所有可能输入的执行次数的平均值假设 x 在列表中每个位置的概率为 1/n,且 x 不存在的概率为 1/2:
- 存在时:平均执行次数 = (1+2+...+n)/n = (n+1)/2
- 不存在时:执行次数 = n
- 总平均:(1/2)×(n+1)/2 + (1/2)×n = (3n+1)/4 → 忽略系数和低阶项,O(n)
2.5 时间复杂度分析原则
1. 最大循环原则(主导项原则)
算法的整体复杂度由 “执行次数最多的部分” 决定,忽略次要部分。示例:
def dominant_loop(n):# 1. 循环100次(O(1),次要)sum1 = 0for p in range(1, 101):sum1 += p# 2. 循环n次(O(n),次要)sum2 = 0for q in range(1, n):sum2 += q# 3. 双重循环n²次(O(n²),主导)sum3 = 0for i in range(1, n+1):for j in range(1, n+1):sum3 += i * jreturn sum1 + sum2 + sum3
- 主导项是 O (n²),整体复杂度为 O(n²)
2. 加法原则(独立操作叠加)
若算法由多个 “独立” 部分组成(无嵌套),总复杂度为各部分复杂度的最大值(而非求和)。公式:若 T1 (n)=O (f (n)),T2 (n)=O (g (n)),则 T (n)=T1 (n)+T2 (n)=O (max (f (n),g (n)))。
示例:
def add_principle(n_arr, m_arr):# 部分1:遍历n_arr(O(n))sum_val = 0for num in n_arr:sum_val += num# 部分2:遍历m_arr(O(m))for num in m_arr:sum_val += numreturn sum_val
- 总复杂度为 O(max(len(n_arr), len(m_arr)))(若两列表长度相近,可简化为 O (n))
3. 乘法原则(嵌套操作叠加)
若算法存在 “嵌套” 操作(如循环内调用另一个算法),总复杂度为各部分复杂度的乘积。公式:若 T1 (n)=O (f (n)),T2 (n)=O (g (n)),则 T (n)=T1 (n)×T2 (n)=O (f (n)×g (n))。
示例:
# 内层函数:O(k)(k为输入参数,此处k≤n)
def inner_func(k):sum_val = 0for i in range(1, k):sum_val += ireturn sum_val# 外层循环:O(n),嵌套调用inner_func(O(n))
def multiply_principle(n):ret = 0for i in range(1, n):ret += inner_func(i)return ret
- 总复杂度为 O(n × n) = O(n²)
三、空间复杂度
3.1 定义
空间复杂度(渐进空间复杂度)表示算法占用的额外存储空间随数据规模 n 增长的趋势,仅关注 “额外空间”(即算法执行过程中主动申请的空间,不包含输入数据本身的空间)。
与时间复杂度类似,空间复杂度也忽略低阶项和系数,仅保留最大量级。
3.2 常见空间复杂度及示例
1. O (1):常数级空间
算法仅使用 “固定数量” 的额外变量,与 n 无关。示例:
def constant_space(n):sum_val = 0 # 固定1个变量for i in range(n): # 固定1个变量isum_val += ireturn sum_val
- 额外空间仅 2 个变量(sum_val、i),与 n 无关 → O(1)
2. O (n):线性级空间
算法申请的额外空间随 n 线性增长(如列表、字典等)。示例:
def linear_space(n):i = 0arr = [0] * n # 申请大小为n的列表(额外空间随n增长)for i in range(n):arr[i] = i * isum_val = 0for i in range(n-1, -1, -1):sum_val += arr[i]return sum_val
- 额外空间主要是大小为 n 的列表 arr → O(n)
3.3 注意事项
- 空间复杂度不包含 “输入数据” 的空间(如函数参数中的列表),仅计算 “算法执行中主动创建” 的空间;
- 递归算法的空间复杂度需考虑 “调用栈深度”:若递归深度为 n(如斐波那契递归),则空间复杂度为 O (n)(因调用栈需保存 n 个栈帧)。
四、复杂度核心小结
时间复杂度
- 核心:执行时间随 n 的增长趋势,关注 “最大量级”;
- 常见类型(按优劣排序):O (1) < O (log n) < O (n) < O (n log n) < O (n²) < O (2ⁿ) < O (n!);
- 分析原则:最大循环原则、加法原则、乘法原则;
- 特殊情况:仅当复杂度有量级差距时,才区分最好 / 最坏 / 平均复杂度。
空间复杂度
- 核心:额外存储空间随 n 的增长趋势,关注 “额外空间”;
- 常见类型:O (1)(常数空间)、O (n)(线性空间);
- 递归算法需关注 “调用栈深度” 对空间的影响。