算法复杂度:那些神秘符号背后的故事
算法复杂度那些神秘符号背后的故事 开篇为什么需要这套数学语言想象一下你要向朋友描述不同汽车的油耗❌没有统一标准“我的车挺省油的”“他的车特别费油”“那辆车还行吧”✅有统一标准“我的车百公里6升”“他的车百公里12升”“那辆车百公里8升”算法复杂度就是计算机世界的油耗标准让我们能客观比较不同算法的效率 第一部分为什么要发明 O 符号 历史背景1976年计算机科学家Donald Knuth高德纳在他的经典著作《计算机程序设计艺术》中正式引入了大O符号。为什么要发明它问题1同样的算法在不同电脑上速度不一样同一程序运行时间 - 在老电脑上10秒 - 在新电脑上1秒 - 在超级电脑上0.1秒 怎么公平比较问题2数据量不同运行时间也不同排序算法A - 排10个数0.001秒 - 排1000个数0.5秒 排序算法B - 排10个数0.002秒 - 排1000个数0.3秒 哪个算法更好✅ 解决方案关注增长趋势而不是具体时间大O符号的核心思想不关心具体跑多快只关心数据变多时速度会怎样变化 第二部分O、n、log 是怎么来的O- “Order of”数量级O 来自数学中的 “Order”意思是级别或量级正式名称Big O Notation大O记号法 学名渐近上界Asymptotic Upper Bound 简单说O 告诉你算法的天花板有多高为什么用 O 而不用其他字母来自德语 “Ordnung”秩序、等级数学家 Paul Bachmann 在1894年首次使用后来被计算机科学广泛采用n- “number”数量n 代表输入数据的规模为什么用小写 n - 数学传统用 n 表示自然数或计数 - 英文单词number数量、n itemsn个项目 - 约定俗成所有教材都用 n形成标准实际含义排序算法n 要排序的元素个数 搜索算法n 要搜索的范围大小 图算法n 节点或边的数量log- “对数”Logarithmlog 是数学家 John Napier 在1614年发明的 为什么叫对数英文Logarithm 拆解logos比例 arithmos数字 中文翻译对数 对应的指数 直观理解 loglog 回答这个问题需要乘以/除以多少次才能达到目标// log₂(8) 3 的意思2×2×28乘了3次 或者8÷2÷2÷21除了3次// 在计算机中常用的是 log₂以2为底因为计算机喜欢二分法每次排除一半 为什么排序算法中经常出现 log n因为聪明的算法都在用分治法Divide and Conquer例子从1000个数中找一个数 ❌ 笨方法O(n)逐个检查最多查1000次 ✅ 聪明方法O(log n) 第1次排除500个剩500个 第2次排除250个剩250个 第3次排除125个剩125个 ... 只需 log₂(1000) ≈ 10 次 第三部分这些公式是怎么计算出来的1️⃣O(1)- 常数时间怎么看出来的functiongetFirst(arr){returnarr[0];// 只有1步操作}// 不管数组多大都是1步 → O(1)设计原理操作次数不随 n 变化数学表达f(n) 1简化为O(1)2️⃣O(n)- 线性时间怎么看出来的functionsum(arr){lettotal0;for(leti0;iarr.length;i){// 循环n次totalarr[i];}returntotal;}// 数组有n个元素就要加n次 → O(n)设计原理操作次数 n数学表达f(n) n简化为O(n)3️⃣O(n²)- 平方时间怎么看出来的functionbubbleSort(arr){for(leti0;iarr.length;i){// 外层n次for(letj0;jarr.length-1;j){// 内层n次if(arr[j]arr[j1]){swap(arr[j],arr[j1]);}}}}// 嵌套循环n × n n² → O(n²)设计原理操作次数 n × n n²数学表达f(n) n²简化为O(n²)4️⃣O(log n)- 对数时间怎么看出来的functionbinarySearch(arr,target){letleft0,rightarr.length-1;while(leftright){letmidMath.floor((leftright)/2);if(arr[mid]target){returnmid;}elseif(arr[mid]target){leftmid1;// 只在右半部分找}else{rightmid-1;// 只在左半部分找}// 每次循环搜索范围减半}}// 每次排除一半 → O(log n)推导过程假设需要 k 次操作 第1次n ÷ 2 第2次n ÷ 4 第3次n ÷ 8 ... 第k次n ÷ 2^k 1 解方程n ÷ 2^k 1 得到2^k n 取对数k log₂(n) 所以是 O(log n)5️⃣O(n log n)- 线性对数时间怎么看出来的以归并排序为例functionmergeSort(arr){// 第1步拆分递归 log n 层if(arr.length1)returnarr;letmidMath.floor(arr.length/2);letleftmergeSort(arr.slice(0,mid));// 递归左半letrightmergeSort(arr.slice(mid));// 递归右半// 第2步合并每层需要 n 次操作returnmerge(left,right);}// 分析// - 递归深度log n 层每次减半// - 每层合并n 次操作所有元素都要参与// - 总操作n × log n → O(n log n)可视化理解原始数组: [5, 2, 8, 1, 9, 3, 7, 4] 第1层拆分 ← 第1层 [5,2,8,1] [9,3,7,4] 第2层拆分 ← 第2层 [5,2] [8,1] [9,3] [7,4] 第3层拆分 ← 第3层 [5] [2] [8] [1] [9] [3] [7] [4] 第3层合并 ← 合并8个元素需要n次操作 [2,5] [1,8] [3,9] [4,7] 第2层合并 ← 合并4组需要n次操作 [1,2,5,8] [3,4,7,9] 第1层合并 ← 合并2组需要n次操作 [1,2,3,4,5,7,8,9] 总共 - 拆分了 log₂(8) 3 层 - 每层合并需要 n 8 次操作 - 总计8 × 3 24 次 ≈ n log n数学推导设 T(n) 为排序n个元素的时间 T(n) 2T(n/2) n 解释 - 2T(n/2)递归处理左右两半 - n合并两个有序数组需要n次操作 用主定理Master Theorem求解 T(n) O(n log n)6️⃣快速排序的复杂度分析✅ 最好/平均情况O(n log n)理想情况每次选的队长都在中间 [5, 2, 8, 1, 9, 3, 7, 4] 选队长5 [2, 1, 3, 4] [5] [8, 9, 7] ↓ ↓ 继续分 继续分 递归深度log n 层 每层处理n 个元素 总计n log n❌ 最坏情况O(n²)最坏情况每次选的队长都是最大或最小 [1, 2, 3, 4, 5, 6, 7, 8] 已经有序 选队长1 [] [1] [2, 3, 4, 5, 6, 7, 8] ↓ 选队长2 [] [2] [3, 4, 5, 6, 7, 8] ↓ 继续... 递归深度n 层退化成链表 每层处理n, n-1, n-2, ..., 1 个元素 总计n (n-1) (n-2) ... 1 n(n1)/2 ≈ n² 第四部分为什么这样设计 设计原则1忽略常数因子算法A需要 3n 次操作 算法B需要 100n 次操作 理论上都是 O(n)但实际差距很大啊 ✅ 大O的设计哲学 - 关注增长趋势不是绝对速度 - 常数因子取决于硬件、编译器优化等 - n 很大时n 和 n² 的差距远大于 3n 和 100n 的差距举例当 n 1,000,000 时 - 3n 3,000,000 - 100n 100,000,000 - n² 1,000,000,000,000 看到了吗n² 比 100n 大了1万倍 所以常数因子不重要关键是 n 还是 n² 设计原则2只看最高阶项某个算法的操作次数 f(n) 3n² 100n 50 为什么简化为 O(n²) 因为 n 很大时 - n 1000 3n² 3,000,000 100n 100,000 50 50 3n² 占了99.9%低阶项可以忽略数学依据lim(n→∞) (3n² 100n 50) / n² 3 当 n 趋向无穷大时3n² 主导了整个表达式 设计原则3区分最好、平均、最坏情况为什么要分析三种情况 快速排序例子 - 最好O(n log n) ← 数据随机分布 - 平均O(n log n) ← 大多数情况 - 最坏O(n²) ← 数据已有序且选第一个为队长 实际意义 - 最好情况了解算法潜力 - 平均情况日常使用的预期性能 - 最坏情况保证不会慢于这个界限 设计原则4稳定性的重要性什么是稳定性 相同值的元素排序后保持原来的相对顺序 例子按成绩排序学生 原始[(小明,85), (小红,90), (小刚,85)] 稳定排序结果 [(小明,85), (小刚,85), (小红,90)] ↑小明还在小刚前面保持原顺序 不稳定排序可能 [(小刚,85), (小明,85), (小红,90)] ↑顺序变了 为什么重要 多条件排序时先按班级再按成绩稳定性保证结果正确 设计原则5空间复杂度的权衡时间复杂度算法要多快 空间复杂度算法要多大内存 常见权衡 - 快速排序时间O(n log n)空间O(log n) ← 省空间 - 归并排序时间O(n log n)空间O(n) ← 费空间但稳定 - 插入排序时间O(n²)空间O(1) ← 最省空间 没有完美的算法只有适合场景的选择 第五部分排序算法复杂度对比表算法最好平均最坏空间稳定设计思想快速排序O(n log n)O(n log n)O(n²)O(log n)❌分治法选队长分区归并排序O(n log n)O(n log n)O(n log n)O(n)✅分治法拆分后合并插入排序O(n)O(n²)O(n²)O(1)✅逐个插入已排序序列TimSortO(n)O(n log n)O(n log n)O(n)✅自适应混合策略 第六部分如何自己推导复杂度 步骤1数循环次数// 例1单层循环for(leti0;in;i){console.log(i);}// 执行n次 → O(n)// 例2双层循环for(leti0;in;i){for(letj0;jn;j){console.log(i,j);}}// 执行n×n次 → O(n²) 步骤2看递归深度// 例1二分查找functionbinarySearch(n){if(n1)return;binarySearch(n/2);// 每次减半}// 递归深度log n → O(log n)// 例2归并排序functionmergeSort(n){if(n1)return;mergeSort(n/2);// 左半mergeSort(n/2);// 右半merge(n);// 合并n个元素}// 深度log n每层工作n → O(n log n) 步骤3用主定理Master Theorem适用于分治算法的复杂度计算形式T(n) aT(n/b) f(n) 其中 - a子问题数量 - n/b每个子问题的规模 - f(n)合并操作的代价 常见情况 1. T(n) 2T(n/2) n → O(n log n) 归并排序 2. T(n) 2T(n/2) O(1) → O(n) 遍历二叉树 3. T(n) T(n/2) O(1) → O(log n) 二分查找 第七部分实际应用建议 如何选择排序算法// JavaScriptarr.sort();// 内置排序通常是TimSort或快速排序// Pythonsorted(arr)# TimSort// JavaArrays.sort(arr);// 双轴快速排序基本类型或 TimSort对象// Cstd::sort(arr.begin(),arr.end());// introsort混合排序⚠️ 常见陷阱// ❌ 错误认为 O(n²) 一定比 O(n log n) 慢// 当 n 很小时常数因子更重要if(n50){insertionSort(arr);// O(n²) 但常数小实际更快}else{quickSort(arr);// O(n log n)}// ✅ 正确根据数据特点选择if(isNearlySorted(arr)){insertionSort(arr);// 接近有序时 O(n)}else{mergeSort(arr);// 一般情况 O(n log n)} 总结记住这些就够了 核心概念O 是什么算法效率的度量衡关注增长趋势不是绝对时间n 是什么输入数据的规模要处理的元素个数log n 是什么每次排除一半的操作次数分治法的标志为什么这样设计忽略硬件差异关注可扩展性提供理论上限 实用口诀O(1) → 一步到位雷打不动 O(log n) → 每次减半聪明绝顶 O(n) → 一个不少逐个检查 O(n log n)→ 分治合并排序标配 O(n²) → 双重循环小心变慢 O(2ⁿ) → 能不用就不用太慢了 延伸阅读《算法导论》Introduction to Algorithms- CLRS《计算机程序设计艺术》- Donald Knuth在线可视化工具https://visualgo.net/en/sorting现在你知道了这些看似神秘的符号其实是计算机科学家为了让算法比较更公平、更科学而设计的通用语言。下次看到 O(n log n)你就知道这是在说“这个算法很聪明用了分治法每个元素都要做对数次操作”
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2643531.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!