资讯动态

渐进符号一网打尽:从大O到Θ彻底搞懂算法复杂度

发布时间:2026/10/9 6:28:13 来源:尧图企业网站定制
说实话大多数人对“算法设计与分析”的第一道坎就卡在渐进符号这堆数学记号上。大O、Ω、Θ、小o、小ω教材里一个个定义抛过来看起来像天书越看越迷糊。我当时学的时候也一样总觉得这些符号长得差不多又好像哪都不一样更别提做题时会用、能算、能判断对错了。这篇文章我就把我自己从“背定义”到“真正理解”再到能拿来分析算法复杂度、做笔试填空题的完整思路写出来。我会把渐进符号拆成“数学定义 生活直觉 实操判断”三层配合具体例子和避坑经验让没学过的人也能看懂让正在备考的人能直接用上。1. 为什么渐进符号这么难懂病根不在符号本身渐进符号难绝大多数人不是栽在数学基础而是栽在理解角度。教材上来就写大O的定义是存在正常数c和n0使得对所有n≥n0有0≤f(n)≤c·g(n)。一眼扫过去很多人脑子里就两个疑问这个c是什么这个n0又是什么为什么好好的算法分析非要搞出这么两个奇奇怪怪的参数如果不把这个问题解决后面所有内容都是空中楼阁。1.1 先把“n足够大”这个直觉建立起来我先说你最熟悉的生活场景。假设你评价两个人谁的饭量大一个人能吃两碗饭另一个人能吃三碗饭你是不是直接说“第二个人饭量大”但如果是喝汤呢第一个人喝三碗汤第二个人喝一碗汤你会说“第一个人更能喝汤”。可如果你拿这个问题去问一个只看“饭量”不看“汤量”的人他可能就糊涂了。算法分析也一样。我们说一个算法的时间复杂度是O(n²)不是说它每一步都恰好执行n²次而是说当输入规模n足够大的时候它的执行时间不会超过某个“n²级别”的量。这里有两个关键词第一个是“足够大”第二个是“级别”。“足够大”对应定义里的n0。n比较小的时候什么乱七八糟的常数、低阶项都可能影响很大但算法分析关心的是大规模输入下的表现所以那些小规模下的特殊情况可以暂时忽略。n0存在的意义就是划一条线从这条线往后函数就开始“露出真面目”了。“级别”对应定义里的c。c是一个正的常数它允许我们忽略“系数”差别。比如3n²和100n²在渐进分析眼里它们都是“n²级别”的因为100这个系数是个固定值不会随着n变化。真正的差别在于“增长趋势”100n²增长得再快也快不过n³而3n²不管系数多小也改变不了它和n²同属一个级别的事实。我之前看到有人打过一个比方大O就像给算法“封顶划线”你开一辆车速最高120km/h的车哪怕实际飙到119.9我们就说你的车速“不超过120级别”如果你开一辆最高200km/h的车我们就说“不超过200级别”。两个车可能今天实际一个跑80、一个跑90但评价“最高性能”时级别决定了上限。这个类比虽然不完全严谨但对建立直觉足够了。1.2 渐进符号到底在回答什么问题继续用直觉往下套。假设你有两台机器跑同一个算法机器A上执行时间是 f₁(n) n² 5n机器B上执行时间是 f₂(n) 0.5n² 100n你现在想知道“把输入规模n从1000涨到10000哪个机器表现更好”。如果只拿n1000代入A算出来是1005000B算出来是600000你会觉得B更好。但当n涨到10000A是100050000B是500100000反而是A更好。为什么因为n²的增长速度压过了线性项5n和100n最终谁在这个“增长故事”里唱主角取决于最高次项。渐进符号的价值就是把这种“在n走向无穷大时谁主导谁”的关系用一套严谨的记号表达出来。它不关心你在n1000那一下的绝对耗时因为那个数字跟硬件、编译器、输入数据都有关它关心的是“趋势”趋势才是这个算法本身固有的属性。这就是渐进分析背后的核心思想丢掉细节抓住增长。1.3 渐进符号难懂的另一层原因符号太多语义太密还有一个让新手晕头转向的原因就是符号家族太庞大了。大O看起来像个字母O读音却叫“Big-Oh”Θ听起来像中文的“西塔”Ω像“欧米伽”小o和大O长得几乎一样就是胖瘦不同。定义里还全是不等号、存在量词、任意量词直接把人劝退。但其实你可以把这些问题拆开来看这一套符号本质上就是在给两个函数的关系做“分级评价”。我们平时说一个人跑步水平会分“不超过、至少、刚好、严格低于、严格高于”几个描述。渐进符号一个个对应过来大O上界最多不超过Ω下界至少不低于Θ紧界两边夹住恰好同阶小o严格上界弱于小ω严格下界强于一旦有了这个语义框架再看教材定义就不需要死记硬背了而是“哦原来它在描述一种边界关系”。下面的章节我会把这五兄弟逐个拿出来连同它们的生活类比、数学定义、典型例子全部过一遍。2. 五个渐进符号逐个拆解先懂直觉再啃定义这一章我按“大O → Ω → Θ → 小o → 小ω”的顺序走顺序是有讲究的先掌握最常用的上界和下界再理解“紧界”其实是上界和下界的交集最后把小o、小ω当成“严格版本”的补充。循序渐进比一上来就摊开五个定义好得多。2.1 大O算法分析里最常用的“上界”大O的定义我建议你先接受这个简化版说法存在正的常数c和n0使得当n≥n0时f(n)总是不超过c·g(n)就记作f(n)O(g(n))。读法“f的增长速度不超过g的增长速度”。这里的“不超过”是核心。举例f(n)3n²2n1g(n)n²。你能不能找到常数c和n0让3n²2n1 ≤ c·n²对足够大的n都成立当然可以n≥1时3n²2n1 ≤ 3n²2n²n²6n²取c6、n01就行。于是f(n)O(n²)。事实上它也是O(n³)、O(2ⁿ)因为“不超过”是一个很宽松的说法只要g增长得不比f慢都成立。这也是大O最容易被诟病的地方它只给“上限”不给你“精确值”。实际分析算法时大O是最常用的。因为绝大多数时候我们关心的是“这个算法最坏情况下耗时会控制在什么级别”而不需要精确知道它到底快多少。你把一个O(n²)算法优化成O(n log n)这是质的飞跃但你把一个O(n²)算法改成“更好的O(n²)”——比如常数减半——虽然实际可能快一倍但在渐进分析里两者是同一个级别。生活类比大O就像“限速牌”。你知道了限速120就保证你不会超过120但具体跑80还是100不在大O关心的范围内。算法最坏情况下的“限速”就是大O的级别。2.2 Ω对称的“下界”判断最优的武器Ω的定义一句话就能说清存在正的常数c和n0使得当n≥n0时f(n)总是至少为c·g(n)就记作f(n)Ω(g(n))。读法“f的增长速度至少不低于g的增长速度”。如果说大O是“最好不过如此”那Ω就是“再差也有底线”。比如f(n)3n²2n1显然有3n²2n1 ≥ n²于是f(n)Ω(n²)。同样它也是Ω(n)甚至Ω(1)——因为g哪怕选择长得特别慢的函数f也能稳稳压住它。有人会觉得Ω用得少其实不是。在设计算法时我们常常需要用Ω来判断“某个问题复杂度有没有可能更低”。比如排序问题的下界就是Ω(n log n)基于比较的排序这意味着你再怎么优化也不可能造出一个基于比较的、比n log n还快的通用排序算法。知道下界你就不至于浪费时间在不可能的优化方向上。2.3 Θ真正意义上的“同阶等价”Θ的定义需要同时满足大O和Ω若f(n)O(g(n))且f(n)Ω(g(n))则f(n)Θ(g(n))。读法“f和g的增长率是同一级别”。为什么我需要单独强调Θ因为实际工程里大家嘴上常说“这个算法是O(n²)”心里想的其实是“这个算法是Θ(n²)”。大O只是说“不超过n²级别”它也可能是n log n级别而Θ是“恰好在n²这个级别”两边都被夹住。判断的时候带上Θ会更严谨也更清晰。比如插入排序最坏情况是O(n²)但不代表它平均也是Θ(n²)而归并排序最坏、最好、平均都是Θ(n log n)。只要记住一句话Θ 大O Ω它把“上界”和“下界”收拢到了一起。这也是我建议做题时优先确认Θ关系的原因——如果题目问“求该算法的时间复杂度”正确答案通常写成Θ形式只有当你只能确定上界、无法确定下界时才退而求其次写大O。2.4 小o严格的“弱于”小o的定义和大O很像只是把“≤”换成了“”的感觉但数学上更严格地说是对任意正常数c都存在n0使得当n≥n0时f(n)c·g(n)。换句话说无论你给多小的正数cf都迟早被g压下去。我读一下这个定义的潜台词如果f(n)o(g(n))那么g不仅是不比f慢而是比f快得多。举个例子n² o(n³)因为你可以对任何c比如c0.0001都能找到足够大的n让n²0.0001·n³。但n²不是o(n²)因为如果c0.5n²就不可能小于0.5n²。换句话说“严格小于”和“小于等于”是有本质区别的。小o在算法分析里出现的频率没有大O高但在数学推导和复杂度比较里很有用它表达的是“一个函数最终被另一个碾压”的意思。2.5 小ω严格的“强于”小o的镜像小ω的定义自然就是小o的镜像对任意正常数c都存在n0使得当n≥n0时f(n)c·g(n)。读法“f严格快于g”。比如n³ω(n²)但n³不是ω(n³)。小ω在面试、考试里偶尔会作为判断题出现你只要掌握“它是小o的反向版本”就行。2.6 五个符号横向对比表为了让这五个符号的关系更一目了然我整理了一个对比表你可以把它存下来做题前扫一眼。符号名字直观含义类比关系类型O大O不超过上界限速牌f ≤ gΩ大Ω不低于下界最低工资f ≥ gΘ西塔同阶紧界同一个段位f ≈ go小o严格弱于被碾压f gω小ω严格强于碾压别人f g注意这里我用了“f ≤ g”这种简写法真正严格的表述应该是“f的增长速率与g相比不超过”而不是数值上f(n)≤g(n)。因为3n²和2n²数值上3n²永远更大但3n²Θ(2n²)它们隶属同一级别。所以只要你看到“级别”两个字就不会被表象迷惑。3. 从理论到实操怎么判断一个算法的复杂度级别看完定义和对比表你可能还是觉得“道理都懂但拿到一段代码我还是不知道该怎么判断它的复杂度”。这很正常理解定义和会实际分析之间还差着大量的练习和方法论。这一章我就给你一套可以直接“抄作业”的分析流程配合具体例子走一遍完整判断过程。3.1 方法一盯住循环结构看它跑几层、每次规模怎么变绝大部分非递归算法的时间复杂度都藏在循环和递归里。判断一个循环的复杂度核心是看“循环变量从哪开始、每次怎么变、到哪结束”。先看最简单的单层循环for (i 1; i n; i) { // 每次执行O(1)的操作 }这个循环从1跑到n执行n次每轮O(1)总复杂度O(n)。但如果循环体里还有一层循环呢for (i 1; i n; i) { for (j 1; j n; j) { // 每次执行O(1)的操作 } }外层n次内层每轮都完整跑n次总共n×n n²次复杂度O(n²)。这就是很多人嘴里的“双层循环就是O(n²)”但要注意这个结论只在内外层都和n同规模时成立。如果内层循环的次数是n/2或者外层只循环到n/2结论会变成O(n²/4)但在渐进意义下依然是O(n²)因为常数因子被吞掉了。更有意思的是这种模式for (i 1; i n; i * 2) { // 每次执行O(1)的操作 }这个循环的i从1开始每次翻倍1, 2, 4, 8, …直到超过n。循环次数是“以2为底n的对数”再加1所以复杂度是O(log n)。同理把i改成i * 3复杂度是O(log₃n)但在渐进分析里log底数不影响结论都会被写作O(log n)。这种“循环规模指数级跳变”的模式是新手最容易漏掉的地方。看到i就写O(n)看到i*2才应该瞬间反应成O(log n)。3.2 方法二递归结构用递推关系分支图辅助理解递归的复杂度判断会更绕。比如归并排序的经典写法递归地把数组分成两半对每一半分别排序线性时间合并结果它的递推式是T(n) 2T(n/2) O(n)。怎么解这个递推式最朴素的方法是展开第一次展开T(n) 2T(n/2) O(n)第二次展开T(n) 4T(n/4) O(n) O(n)第k次展开T(n) 2^k·T(n/2^k) k·O(n)递归到T(1)时n/2^k1所以klog₂n。代入得到T(n) n·T(1) O(n log n)最终就是O(n log n)。这里的直观解释是每一层递归把所有数据的总工作量加起来是O(n)一共有log n层所以总复杂度是O(n log n)。归并排序是“每层线性层数为对数”的典型代表。如果你不想每次都手推递推式可以记住几个常见模式递推形式复杂度结论典型例子T(n) T(n/2) O(1)O(log n)二分查找T(n) 2T(n/2) O(n)O(n log n)归并排序T(n) 2T(n/2) O(1)O(n)遍历求最大值T(n) T(n-1) O(1)O(n)线性递归T(n) T(n-1) O(n)O(n²)选择排序的递归版这张表比死记主定理更实用先覆盖高频模式再扩展特殊形式。3.3 方法三用“增长率排序”做快速判断和比较有些场景不要求你精确解出复杂度而是让你比较两个复杂度谁更快。这时候你脑子里需要有一张“增长率阶梯图”。按从慢到快的顺序1, log n, √n, n, n log n, n², n³, …, 2ⁿ, n!, nⁿ这张阶梯图有很多有趣的细节。比如√n和log n的关系看起来√n也不大但它比log n增长得快多了。比如n log n和n²的关系n log n虽然比n高一点但永远赶不上n²的增长。判断时不要只因为有个“n²”就简单认为它一定是最慢的得看它的阶在阶梯上处于什么位置。举个具体例子现在有两个算法一个复杂度是O(2ⁿ)另一个是O(n¹⁰)。你会选哪个如果只看“指数”和“多项式”的字眼有人会犹豫。但放在阶梯图上2ⁿ是指数级n¹⁰是多项式级当n足够大时任何多项式都比任何指数慢死得慢所以n¹⁰在渐进意义下更好。所以选O(n¹⁰)的算法。再比如O(n²)和O(n log n)后者更好因为n log n除以n²等于(log n)/n极限是0说明n log n增长更慢。3.4 常见复杂度实际量级感受用数字刺激一下直觉光说“增长更快”是抽象的我用具体数字让你感受一下。假设计算机每秒能执行10⁹次基本操作n10⁶O(n)需要0.001秒O(n²)需要1000秒约17分钟O(2ⁿ)直接爆炸到宇宙末日级别。n10⁹O(n log n)大概需要30秒左右O(n²)则需要10¹⁰秒超过300年。n10⁵O(n²)是10⁸秒不对10¹⁰次操作约10秒还能忍但n10⁶就彻底崩了。所以为什么渐进分析这么重要因为一个从O(n²)优化到O(n log n)的算法在面对大数据量时不是“快了一点”而是“从不可用变成可用”。这就是渐进符号对实际工程的指导意义。4. 常见误区与做题技巧这些坑我都踩过最后这部分我把平时教学和做题中最常遇到的误区和易错点整理出来每条都结合真实场景说清楚能帮你少走很多弯路。4.1 误区一把“最好情况”“最坏情况”和渐进符号混为一谈很多初学者会说“快排的最好情况是O(n log n)最坏情况是O(n²)所以平均是O(n log n)”。这个说法本身不算错但容易造成误解大O描述的是“函数级别”它不关心是最好情况还是最坏情况。比如快速排序无论最好还是最坏给定输入后都会有一个确定的运行时间函数而大O是对这个函数的“上界描述”。严格点说“快速排序最坏情况下运行时间的渐近上界是O(n²)”会比“快速排序是O(n²)”更精确因为平均情况和最好情况它确实也能达到O(n log n)。做题时我的建议是如果题目说“平均情况复杂度”你就写Θ(n log n)如果说“最坏情况复杂度”那就写Θ(n²)。不要把最好、最坏、平均这三个维度混在同一个渐进描述里。还有一点容易忽略渐进分析只描述“趋势”不描述“具体输入模式”。同一个算法在有序数组和无序数组上的表现可能天差地别这和渐进符号本身无关和数据分布有关。所以用渐进符号描述算法时先要想清楚你在描述“哪个情况下的哪个指标”。4.2 误区二把“O(...)”理解成数相等教材里经常写“f(n)O(g(n))”这个等号其实是历史遗留的写法更准确的说法是“f(n)属于O(g(n))这个集合”。因为O(g(n))表示的是所有“不超过g级别”的函数组成的集合而f是这个集合里的一个元素。这个误解带来的最大问题是有人会认为“f(n)O(n²)”就意味着f(n)最终会和某个n²相等。显然不对3n²2n1在数值上永远不会等于n²。更严谨的写法应该是f(n)∈O(n²)但计算机算法领域的传统就是写等号你习惯了就好但心里要明白这个“等号”不是严格意义上的相等。同理当你看到“2ⁿO(n!)”时不要觉得矛盾2ⁿ确实不超过n!级别因为n!的增长比指数还快。只是这个“”可以读成“属于”整句就是“2ⁿ属于O(n!)”。4.3 误区三忽略log底数的影响渐进符号里有个约定俗成的简化log₂n、log₁₀n、ln n全部写作log n因为换底公式会让它们只差一个常数倍在O、Ω、Θ的定义下常数倍被吞了。所以做题时不管题目里的log是几进制你都可以统一当log n处理。但要注意这个简化只对“对数级”和“对数级”之间成立你要是拿logn和√n比那还是得看增长率阶梯不能靠换底公式。我见过有人问“log₂n和log n²什么关系”答案是log n² 2log n系数2被吞掉所以两者同阶。但log n²和(log n)²不一样(log n)²的增长率比log n要高一个层次虽然它还是多项式级别里最温和的之一。4.4 误区四只记结论不求推导导致不会变形有同学背下了“二分查找是O(log n)”但考试题改成“每次查找不是砍一半而是把搜索范围变成原来的三分之二复杂度是多少”就懵了。其实底层逻辑很简单每一步后问题规模变成原来的k倍k1那么执行步数就是“以1/k为底n的对数”依然是对数级。所以“三分之二版二分”的复杂度还是O(log n)只是底数变成了3/2。这才是渐进符号真正要培养的能力看到规模变化模式就能反推复杂度级别。死记公式碰到变形题就露馅。4.5 实操技巧三步法快速判断复杂度我自己做题的时候总结过一套三步法分享给你第一步找循环入口。嵌套循环看层级单循环看步长变化。 第二步找每一步的规模缩减方式。如果是i规模是线性缩减每层执行n次如果是i*2或i/2是对数缩减执行log n次如果是递归分解成子问题写递推式。 第三步把各层操作量相乘。如果内层依赖外层变量比如ji那涉及累加和通常结果是n(n1)/2级别的Θ(n²)而不是固定的n²。举个例子for (i 1; i n; i) { for (j 1; j i; j) { // O(1)操作 } }内层循环次数是123…n n(n1)/2渐进级别是Θ(n²)。不能简单套“双层循环就是O(n²)”的结论但结果恰好一致。如果内层是jn/i那总次数就是n·(1 1/2 1/3 … 1/n) ≈ n·ln n变成Θ(n log n)。这种“看似双层循环实则调和级数”的题目在考试中经常出现值得单独留心。4.6 实操技巧关于主定理的速记版递归式的通用求解最靠谱的工具是主定理Master Theorem。它处理的是形如T(n) a·T(n/b) f(n)的递推式表示把规模为n的问题分成a个子问题每个规模n/b分解加合并的代价是f(n)。主定理通过比较f(n)和n^(log_b a)的增长级别来决定最终结果如果f(n)增长得比n^(log_b a)慢复杂度由叶子节点主导结果是Θ(n^(log_b a))如果f(n)和n^(log_b a)同阶结果是Θ(n^(log_b a)·log n)如果f(n)增长得比n^(log_b a)快很多并且满足正则条件结果是Θ(f(n))举个例子T(n)2T(n/2)O(n)。这里a2b2n^(log₂2)n¹f(n)n正好和n¹同阶所以结果是Θ(n log n)。和前面我们手动展开的结果完全一致。T(n)T(n/2)O(1)a1,b2,n^(log₂1)n⁰1f(n)1同阶结果是Θ(log n)。这正好是二分查找。主定理不适合所有情况比如f(n)不是多项式级别的、或者子问题规模不是精确的n/b时需要换别的工具。但对应付绝大多数算法课、考研题和面试题它已经足够好用了。4.7 避坑清单考场上/做题中容易翻车的几个点下面几条是我觉得特别容易被忽略但常常出题的细节都列出来循环变量从1开始还是从0开始不改变复杂度结论但改变n0的取值和精确次数。分析时不用太纠结写渐进级别不受影响。嵌套循环里如果内层循环边界是外层变量的函数一定不能用“m层循环就是O(n^m)”的口诀要先算累加和。递归式和循环不一样你可能需要用递推展开法、主定理、甚至递归树三种工具。遇到T(n)T(n/2)T(n/2)O(1)等价于2T(n/2)O(1)结果是Θ(n)但如果是T(n)T(n/3)T(2n/3)O(n)复杂度是Θ(n log n)而不是O(n log n)实际是Θ(n log n)递归树的层数会变最深层数log_{3/2}n依旧是对数级总工作量每层线性。渐进符号里的“n”一般指输入规模。不同场景下输入规模的定义不同排序里是元素个数图算法里是顶点数V和边数E有些题目会强调“n是输入数据的位数”这时复杂度要小心可能变成伪多项式。看到题目给的变量定义先看清楚再动手。最后再说几句我自己的体会是渐进符号这东西真不是靠背定义能学会的你得把它当成一门外语多读、多写、多用才能建立那种“看一眼就知道大概什么级别”的直觉。当初我学的时候把常见复杂度从慢到快默写了无数遍每次看到一段代码就在心里默默走一遍循环分析流程。后来做多了才慢慢觉得那些符号不再是一堆天书而是描述算法效率最简洁的语言。如果这篇文章能帮你跨过“渐进符号难理解”这个坎哪怕只解决了一部分困惑我写这些字就值了。最后再分享一个我到现在还在用的小技巧刷算法题时每道题提交之前先自己在草稿纸上写出它的时间复杂度跟题解做对照错了就复盘哪里分析错了坚持一段时间你的复杂度直觉会变得非常敏锐。算法设计与分析这条路上渐进符号只是第一步但把这第一步踩实了后面的路会顺很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑