【数据结构与算法】时间复杂度是什么?怎么优化它?

🟡时间复杂度

时间复杂度用来描述算法运行时间随输入规模增加而增加的速度。按照快慢排序常见的时间复杂度:

  1. 脑子秒想:O(1)常数 ,算法的执行时间是固定的,与输入规模无关,例如直接访问数组中的某个元素。
  2. 对半计算:O(log n)对数 ,常见于二分查找 等每次操作都能将问题规模减少一半的算法。
  3. 逐个遍历:O(n)线性 ,算法的执行时间与输入规模成正比,例如遍历数组中的所有元素。
  4. 快速排序:O(n log n)线性对数 ,常见于快速排序 、归并排序等分治的算法。
  5. 两两对比:O(n^2)平方,通常出现在双重嵌套循环中,例如简单的选择排序或者插入排序。
  6. 暴力枚举:O(2^n)指数,通常出现在递归算法的每一层可以分解成多个子问题的情况。
  7. 暴力枚举:O(n!)阶乘,通常出现在求解旅行商问题等需要考虑所有排列的问题上。
复杂度 比喻 典型算法 真实互联网场景
O(1) 直接看价格 哈希查找、数组随机访问 缓存系统、数据库索引、静态资源加载
O(log n) 猜价格,猜高低,每次减半范围 二分查找、平衡二叉树 搜索引擎、数据库查询优化、推荐系统
O(n) 逐个翻价格标签 线性扫描、KMP 匹配 日志分析、广告投放、图像处理
O(n log n) 先分批次筛选,再细排 快速排序、归并排序 大数据排序、搜索引擎排名、电商商品排序
O(n²) 一件一件比价格 冒泡排序、插入排序 社交网络好友推荐、碰撞检测、小规模数据分析
O(2ⁿ) 计算所有购物组合 回溯法、暴力递归 密码破解、路径规划、AI决策树
O(n!) 衣服搭配排列,组合数爆炸 全排列生成、旅行商问题 物流调度、任务调度、基因序列比对

🔘 中学数学理解

在上述提到的各种时间复杂度中,只有 O(n²)、O(2ⁿ) 和 O(n!) 与排列组合直接相关。其余的复杂度(如 O(1)、O(log n)、O(n)、O(n log n))并不涉及排列组合的概念。童鞋们,回顾一下中学课本吧哈哈,我举例了三个情况:

假设环科院内有 10 名员工(编号为 1 到 10)。公司需要从中选出 4 名员工参加培训,并安排其中的 2 名员工担任特定的角色(如组长和副组长),而剩下的 2 名员工不分配具体角色。

1、组合问题:如果只关心从 10 名员工中选出 4 名员工(不考虑角色分配),有多少种不同的选择方式?

  • 使用组合公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> C ( n , m ) C(n, m) </math>C(n,m):
    <math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C ( n , m ) = n ! m ! ( n − m ) ! = C ( 10 , 4 ) = 10 ! 4 ! ( 10 − 4 ) ! = 10 ⋅ 9 ⋅ 8 ⋅ 7 4 ⋅ 3 ⋅ 2 ⋅ 1 = 210 C(n, m) = \frac{n!}{m!(n-m)!}= C(10, 4) = \frac{10!}{4!(10-4)!} = \frac{10 \cdot 9 \cdot 8 \cdot 7}{4 \cdot 3 \cdot 2 \cdot 1} = 210 </math>C(n,m)=m!(n−m)!n!=C(10,4)=4!(10−4)!10!=4⋅3⋅2⋅110⋅9⋅8⋅7=210

2、排列问题:如果已经选出了 4 名员工,现在需要从中选出 2 名员工并安排他们的具体角色(如组长和副组长),有多少种不同的安排方式?

  • 使用排列公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> A ( m , k ) A(m, k) </math>A(m,k):
    <math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A ( m , k ) = m ! ( m − k ) ! = A ( 4 , 2 ) = 4 ! ( 4 − 2 ) ! = 4 ⋅ 3 1 = 12 A(m, k) = \frac{m!}{(m-k)!}=A(4, 2) = \frac{4!}{(4-2)!} = \frac{4 \cdot 3}{1} = 12 </math>A(m,k)=(m−k)!m!=A(4,2)=(4−2)!4!=14⋅3=12

3、排列组合问题:如果既要从 10 名员工中选出 4 名员工,又要给其中的 2 名员工安排具体角色(如组长和副组长),有多少种不同的安排方式?那得分两步计算:

  1. 先计算从 10 名员工中选出 4 名员工的组合数 <math xmlns="http://www.w3.org/1998/Math/MathML"> C ( 10 , 4 ) C(10, 4) </math>C(10,4)。
  2. 再对选出的 4 名员工中的 2 名进行排列,安排具体角色 <math xmlns="http://www.w3.org/1998/Math/MathML"> A ( 4 , 2 ) A(4, 2) </math>A(4,2)。

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 总方案数 = C ( 10 , 4 ) ⋅ A ( 4 , 2 ) = 210 ⋅ 12 = 2520 总方案数 = C(10, 4) \cdot A(4, 2)= 210 \cdot 12 = 2520 </math>总方案数=C(10,4)⋅A(4,2)=210⋅12=2520

🔘 O(1)

无论输入多大,执行的操作次数都是固定的

比如办公室门口放着快递柜,领导让你拿一个指定包裹。你直接打开快递柜,找到编号123的包裹,拿走,1秒钟搞定。不管今天有1000个包裹还是10000个包裹,找编号的速度永远是固定的。典型例子是数组索引访问,直接通过索引获取值,无需遍历。不管数组有 10 还是 100 万个元素,它只访问一次,第 0 个元素,时间复杂度是 O(1)。

python 复制代码
def get_first_element(arr):
    """ 获取数组的第一个元素,时间复杂度 O(1) """
    return arr[101]  # 只执行一次操作,和数组大小无关

🔘 O(n)

需要遍历所有数据,数据量翻倍,执行次数也翻倍。

领导让你去仓库找出一个坏掉的包裹(比如破损的)。 仓库有100个包裹,你只能从第1个开始一个个检查过去,直到找到破损的。如果包裹有100个,最多要找100次如果有10000个,最多要翻10000次。当然也看运气。典型例子是遍历数组查找某个元素(线性查找),最坏情况下需要遍历整个数组,执行 n 次,时间复杂度是 O(n)。

python 复制代码
def linear_search(arr, target):
    """ 遍历数组查找目标元素,时间复杂度 O(n) """
    for i in range(len(arr)):  # 需要遍历整个数组
        if arr[i] == target:
            return i  # 找到了,返回索引
    return -1  # 没找到

🔘 O(log n)

每次操作能把问题规模缩小一半 适用于有序数据,通过"分治"快速锁定目标。

快递老板给你一个清单,上面是包裹编号从小到大排序好的,让你找某个编号的包裹。你先翻一半,如果中间的编号比目标大,说明目标在前半部分;再翻一半,直到找到。100个包裹大概只用翻7次!10000个包裹最多翻14次! 有点像玩"你猜我心里想的数字"的游戏,包裹越多,节省的时间越明显,比如你猜这个衣服的价格,如果是100元以内,我猜个数你说大了还是小了,我也不会从1元猜到100...越大越明显体现在如果这个衣服是2万,我总不能从1元猜到2万吧,这算法比O(n)香多了。

python 复制代码
def binary_search(arr, target):
    """ 在有序数组 arr 中查找 target,时间复杂度 O(log n) """
    left, right = 0, len(arr) - 1  # 初始化左右边界

    while left <= right:  # 当左右边界没有交错时
        mid = (left + right) // 2  # 计算中间索引
        if arr[mid] == target:
            return mid  # 找到了,返回索引
        elif arr[mid] < target:
            left = mid + 1  # 目标在右半部分
        else:
            right = mid - 1  # 目标在左半部分

    return -1  # 没找到,返回 -1

快速排序 vs 二分查找的都用到了"分而治之"的思想:两者都通过某种方式将问题分成更小的部分,逐步缩小问题规模。都涉及"基准值"或"中间值",在二分查找中,我们选择数组的中间值作为基准,来判断目标值在左半部分还是右半部分。在快速排序中,我们选择一个基准值(pivot),根据它将数组分成两部分。但二分查找目标是从一个有序数组 中找到某个特定值的位置。因此前提条件是有序数组。每次比较的是中间值 ,决定继续搜索左半部分还是右半部分。而快速排序 目标是对一个无序数组 进行排序。每次选择一个基准值,将数组分成两部分(比基准值小的部分和比基准值大的部分),然后递归排序,可以说快速排序本身就是为了把无序数组变成有序数组。自然二者结果不同 二分查找 :最终结果是一个具体的索引值,表示目标值在数组中的位置。快速排序:最终结果是一个完整的有序数组。

🔘 O(n log n)

每次划分需要 O(n) 时间,总共划分 log(n) 次

你要把100个包裹从轻到重排序(比如称重分类),你先把包裹一分为二,分别称重;再把两堆继续分成四堆称重; 最后再一层层合并回来。这个过程比单纯挨个比较快,类似归并排序 ,它不是挨个比较,而是先大致分组,再合并 ,速度比 O(n²) 的冒泡排序快得多。再以衣服为例,你想从 100 件衣服里挑出最贵的 10 件,不要一个个比,但是这衣服又不是按照价格顺序排列的,那就先把衣服大致 分成两堆(比如 50 件和 50 件),再继续分(25 件、25 件、25 件、25 件),直到每一小堆只有 1-2 件,这个过程很快啊,因为不涉及比较,只是单纯不停的对半砍而已,不动脑子,然后再进行排序,最后把各堆的最贵衣服合并起来。 典型例子是快速排序 ,使用分治法,每一轮把数组分成两部分再递归排序。每次递归都把问题规模减半,拆分 log(n) 层,每层操作 O(n) 次,所以最终是 O(n log n)。

python 复制代码
def quick_sort(arr):
    """ 使用快速排序对数组排序,时间复杂度 O(n log n) """
    if len(arr) <= 1:  # 递归终止条件
        return arr  
        
    pivot = arr[len(arr) // 2]  # 选取中间元素作为基准
    left = [x for x in arr if x < pivot]   # 比基准小的放左边
    middle = [x for x in arr if x == pivot]  # 等于基准的放中间
    right = [x for x in arr if x > pivot]  # 比基准大的放右边

    return quick_sort(left) + middle + quick_sort(right)  # 递归排序并合并

🔘 O(n²)

嵌套循环,每个元素都需要和其他元素比较,导致执行次数指数增长。

比如你有 100 件衣服,要从中找出最贵的那件。你非要拿第 1 件衣服,和第 2 件比,再和第 3 件比... 一直到第 100 件。然后换第 2 件,再和剩下所有衣服比......这就是冒泡排序或选择排序 ,每次都要遍历整个列表,非常低效典型例子是冒泡排序(Bubble Sort),每次遍历都要两两比较 n(n-1)/2 次,数据稍大就会非常慢!

python 复制代码
def bubble_sort(arr):
    """ 冒泡排序,时间复杂度 O(n²) """
    n = len(arr)
    for i in range(n):  # 遍历 n 次
        for j in range(n - i - 1):  # 每次都遍历剩余未排序部分
            if arr[j] > arr[j + 1]:  # 如果当前元素比下一个大
                arr[j], arr[j + 1] = arr[j + 1], arr[j]  # 交换位置

🔘 O(2ⁿ)

递归爆炸,每个子问题都会生成多个子问题

比如你去商场买衣服,有 10 件衣服可以选,每件衣服你可以买或不买。你要列出所有可能的购物组合:你可以买 1 件(10 种可能)、你可以买 2 件(45 种可能 C10 2) 。注意区分O(n²)有两个层级的循环,每次循环都对比一个元素与其他元素,计算量增加是每增加一个元素,运算次数增加的是 n 次。O(2ⁿ)是每个决策点都要做选择,即每个元素都有"选"或"不选"的两种选择,随着元素增多,可能的决策组合呈指数增长。O(n²):相当于你一个个和别人比,看谁跑得快,人数每多一倍,你就要比多两倍。O(2ⁿ):相当于你在一个晚宴上,每个人都需要决定是否参加,所以人数每增加一倍,可能的组合数就成倍增加。典型例子是斐波那契数列。每次计算都要计算两个子问题,计算量呈指数级增长,非常慢

python 复制代码
def fibonacci(n):
    """ 递归计算斐波那契数列,时间复杂度 O(2ⁿ) """
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)  # 每次递归都会拆成两个子问题

🔘 O(n!)

全排列、旅行商问题,枚举所有可能的情况。

复杂度是最恐怖的,只能用在 n 很小的情况,比如 n ≤ 10。,n = 10 时就有 3,628,800 种可能,n = 20 直接卡死。老板让你安排10个客户的送货顺序,而且必须找出最快的一条路线。10个包裹需要 10! = 3628800 条路线去试!再比如你要决定 10 套衣服的搭配顺序,每天穿不同的。 第一天你可以穿 10 件中的任何一件,第二天可以穿剩下的 9 件,第三天可以穿剩下的 8 件......总共的搭配方式是: 10! = 10 × 9 × 8 × 7 × 6 × 5 × 4 × 3 × 2 × 1 = 3,628,800。10 件衣服就有几百万种搭配 ,如果是 20 件衣服,那就基本算不动了((20! \approx 2.4 × 10^{18}))。这就是旅行商问题(TSP) ,也是排列组合问题,计算量大到爆炸。:

python 复制代码
from itertools import permutations

def traveling_salesman(cities, distances):
    """ 暴力求解旅行商问题,时间复杂度 O(n!) """
    min_distance = float('inf')
    best_route = None

    for route in permutations(cities):  # 生成所有城市的排列
        distance = sum(distances[route[i]][route[i + 1]] for i in range(len(route) - 1))  # 计算总距离
        if distance < min_distance:  # 记录最短路径
            min_distance = distance
            best_route = route

    return best_route, min_distance

🟡算法优化是什么?

算法工程师的工作核心之一就是优化任务的时间复杂度,但并不是所有问题都能选择最快的算法,有些问题本身就是计算量极大,只能寻找可接受的近似解。

🔘常见优化方案

场景 原始复杂度 理论最优复杂度 为什么不能更快? 优化方案
旅行商问题(TSP) O(n!) O(n²2ⁿ) 需要枚举所有城市间的路径组合,规模爆炸 近似算法(如遗传算法、模拟退火)快速逼近优解
抖音推荐系统 O(n²) O(n log n) 用户与视频特征数量乘积太大,逐一计算不可行 矩阵分解/ANN
社交网络最短路径(如微信好友推荐) O(n³) O(m + n log n) 图节点和边数巨大,暴力遍历所有路径太慢 Dijkstra/A*
AI 训练(GPT、BERT) O(n³) O(n²) 超大数据量和参数矩阵乘法,计算量激增 并行计算/模型剪枝
自动驾驶路径规划 O(2ⁿ) O(n²) 障碍物和路径组合指数增长,需全局最优 动态规划/Dijkstra
搜索引擎关键词匹配 O(n²) O(n log n) 海量网页逐一比对关键词,线性扫描太慢 倒排索引:预建词-文档映射表,查询时直接定位相关文档。
在线支付欺诈检测 O(n²) O(n log n) 每笔交易需与历史记录比对,复杂度随量级增 特征工程/流式计算:在实时流中提取关键特征,降低全量比对需求。

下面是几个常见但能极大优化的例子

🔘线性查找 → 二分查找

低效做法(O(n)): 直接遍历数组找目标值。
高效做法(O(log n)): 如果数组是有序的 ,可以用二分查找减少大量无意义的比较。

python 复制代码
# O(n) 线性查找(低效)
def linear_search(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1  # 没找到

# O(log n) 二分查找(高效)
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1  # 没找到

🔘冒泡排序 → 快速排序

低效做法(O(n²)): 冒泡排序,每次遍历整个 整个 整个 数组,把最大值冒到最后。
高效做法(O(n log n)): 快速排序,每次选一个基准值, 避免了无意义的遍历交换

python 复制代码
# O(n²) 冒泡排序(低效)
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]  # 交换

# O(n log n) 快速排序(高效)
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]  # 选一个基准值
    left = [x for x in arr if x < pivot]
    mid = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + mid + quick_sort(right)

🔘递归 → 动态规划

低效做法(O(2ⁿ)): 递归计算斐波那契数,重复计算太多。
高效做法(O(n)):动态规划 ,递归会重复计算,而动态规划用数组存储中间结果,避免不必要的计算。

python 复制代码
# O(2ⁿ) 递归斐波那契(低效)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

# O(n) 动态规划(高效)
def fib_dp(n):
    dp = [0, 1]  # 记录已计算的值
    for i in range(2, n + 1):
        dp.append(dp[i - 1] + dp[i - 2])
    return dp[n]

🔘 两数之和(暴力法 → 哈希表)

低效做法(O(n²)): 两层循环找两个数的和。
高效做法(O(n)):哈希表存已经遍历过的数,快速查找。

python 复制代码
# O(n²) 暴力法(低效)
def two_sum(arr, target):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] + arr[j] == target:
                return [i, j]

# O(n) 哈希表(高效)
def two_sum_hash(arr, target):
    num_map = {}  # 记录数值 -> 索引
    for i, num in enumerate(arr):
        diff = target - num
        if diff in num_map:
            return [num_map[diff], i]  # 直接查找,无需遍历
        num_map[num] = i  # 存入字典
相关推荐
IT猿手2 小时前
2025最新群智能优化算法:海市蜃楼搜索优化(Mirage Search Optimization, MSO)算法求解23个经典函数测试集,MATLAB
开发语言·人工智能·算法·机器学习·matlab·机器人
IT猿手4 小时前
2025最新群智能优化算法:山羊优化算法(Goat Optimization Algorithm, GOA)求解23个经典函数测试集,MATLAB
人工智能·python·算法·数学建模·matlab·智能优化算法
Dream it possible!7 小时前
LeetCode 热题 100_字符串解码(71_394_中等_C++)(栈)
c++·算法·leetcode
修己xj8 小时前
算法系列之深度优先搜索寻找妖怪和尚过河问题的所有方式
算法
开心比对错重要8 小时前
leetcode69.x 的平方根
数据结构·算法·leetcode
美狐美颜sdk9 小时前
什么是美颜SDK?从几何变换到深度学习驱动的美颜算法详解
人工智能·深度学习·算法·美颜sdk·第三方美颜sdk·视频美颜sdk·美颜api
m0_461502699 小时前
【贪心算法1】
算法·贪心算法
Doopny@9 小时前
数字组合(信息学奥赛一本通-1291)
数据结构·算法·动态规划
君莫愁。9 小时前
【Unity】搭建基于字典(Dictionary)和泛型列表(List)的音频系统
数据结构·unity·c#·游戏引擎·音频
原来是猿10 小时前
蓝桥备赛(13)- 链表和 list(上)
开发语言·数据结构·c++·算法·链表·list