说起“认识复杂度和简单排序算法”很多人第一反应是“这都讲烂了”。可我在带新人和面试的时候发现能把复杂度说清楚、能把冒泡排序/选择排序/插入排序写得滴水不漏的人远比想象中少。今天我想用一篇平时聊天式的文章聊聊怎么看复杂度怎么把这三个简单排序算法彻底吃透包括它们的时间复杂度推导、稳定性、代码边界以及实际工作里到底什么时候该用它们。这篇文章不打算堆公式而是用推演、代码和踩坑记录带你入门适合刚接触算法的同学也适合当复习提纲。1. 为什么每个程序员都应该先学会“度量”算法1.1 复杂度不是考试题而是工程决策的尺子算法领域有个老生常谈的问题两段代码都能完成同样的功能比如对一个数组排序你怎么知道哪个更好凭感觉靠“我机器上跑起来挺快”都不太靠谱。机器性能会影响绝对耗时数据量大小也会影响结论。一段代码在100个数据时跑得飞快不代表在100万条数据时还能撑住。复杂度的作用就是帮我们跳过具体机器、具体数据直接衡量算法本身随输入规模增长而变化的趋势。你可以把复杂度理解为“算力账单”。它不统计你用了多少毫秒而是统计你大概执行了多少次核心操作。这个次数是100、是1000还是一亿直接决定了程序在真实场景里能不能用。比如双层循环处理几万条数据可能就要几秒用户可不会等。认识复杂度就是在动手写代码之前先估算好“这笔账单我付不付得起”。而且复杂度分析不是理论家的游戏。做后端接口优化、处理日志、合并列表、去重、排序几乎每天都在和它打交道。有了复杂度意识你看到一段代码就能预判它会不会在数据量上来之后变成性能瓶颈这是职业程序员的基本功。1.2 没有复杂度意识会踩什么坑我见过不少真实案例。A同学写了个功能把一组用户ID和一组订单ID做匹配两层for循环当时测试数据只有几百条运行结果“秒出”。等上了生产环境数据量涨到几十万条接口直接超时数据库连接被占满监控告警响成一片。问题就出在“双重循环匹配”这件事的复杂度是O(n*m)数据量各涨100倍计算量涨10000倍。这不是优化一两行代码能救回来的需要换思路。类似的问题在排序场景里更明显。假设你的业务需要对一个大数组做排序如果只调用排序函数而不知道它底层是O(n^2)还是O(n log n)你很难解释“为什么数据一多就卡”。最典型的例子是某些初学者在循环里调用sort方法外层还有一个循环整体复杂度直接变成O(n^2 log n)非常恐怖。理解复杂度之后你会习惯性地把“循环次数”和“每次操作成本”分开计算这才是估算法性能的正确姿势。2. 复杂度的核心概念大O表示法并不难2.1 时间复杂度的本质操作次数随输入规模变化的趋势大O表示法被很多初学者当成拦路虎其实它的核心思想非常简单忽略常数、忽略低阶项只保留增长速度最快的那一部分。为什么可以忽略常数因为当输入规模n足够大的时候常数项的影响会越来越小。比如一个算法执行100n次操作另一个算法执行0.5n^2次操作当n10时前者是1000后者是50后者反而快当n1000时前者是10万后者是50万差距开始反转当n10000时前者100万后者5000万谁快谁慢一目了然。所以我们说前者的复杂度是O(n)后者是O(n^2)。用生活里的类比就是你从A地到B地开车速度比走路快了几十倍但如果你选错了路线绕了一个大圈再快的车也追不上。算法里的“常数因子”就是车速“复杂度级别”就是路线距离。分析算法时我们首先关心的是路线对不对而不是车速多快。再看一个代码层面的例子。假设你要从数组中找一个数最简单的方法是逐个遍历def find_in_list(arr, target): for x in arr: if x target: return True return False最坏情况下目标不在数组里需要遍历完整个数组执行n次比较复杂度O(n)。这个“最坏情况”很重要我们后面还会反复提到。2.2 空间复杂度代码跑起来占了多少“内存本子”时间复杂度关心时间空间复杂度则关心额外内存。相当于你做一道数学题有些解法只需要在草稿纸的角落写几个步骤有些解法需要另外拿一整本草稿本。计算机里的算法也是一样有些排序算法在数组内部交换元素不需要额外的存储结构有些算法则需要复制一份数组或开一个辅助数组内存开销就上去了。空间复杂度的表示方法与大O一致。原地排序的选择排序、插入排序额外空间通常是O(1)因为只用几个临时变量。归并排序这类算法需要额外数组空间复杂度是O(n)。在内存紧张、数据量大的嵌入式或服务端场景空间复杂度可能比时间复杂度更敏感。初学者容易只盯着耗时而忽略内存但等你真的处理几十GB数据时空间限制会逼着你去改算法。2.3 常见复杂度阶梯O(1)、O(log n)、O(n)、O(n log n)、O(n^2)、O(2^n)我习惯把常见复杂度看成一个“难度阶梯”每往上爬一级数据规模变大后的代价完全不同。放一张表方便对照复杂度通俗解释典型场景O(1)无论数据多少操作次数固定数组按下标访问、哈希表查找O(log n)数据翻倍操作次数只增加一点点二分查找、平衡二叉树查找O(n)数据翻倍操作次数约翻倍单层遍历、线性查找O(n log n)数据翻倍操作次数略多于翻倍快速排序、归并排序O(n^2)数据翻倍操作次数变成4倍冒泡排序、选择排序、插入排序最坏情况O(2^n)数据规模稍大就彻底崩盘穷举子集、某些暴力递归重点看O(log n)它被低估得很厉害。n从1000涨到100万O(n)的耗时涨了1000倍而O(log n)的耗时只从10变成20。这就是为什么有序数组里能用二分查找就绝不用线性查找。而O(n^2)为什么在数据量大时不可接受因为n100万时n^2是10的12次方哪怕一次操作只要1纳秒也需要1000秒。复杂度阶梯的意义就是让你对大数敏感而不是只看“现在数据少”。2.4 最好、最坏、平均不要被“平均”骗了复杂度不能只谈一个数因为同一算法在不同输入下表现可能天差地别。比如插入排序对已经排好序的数组它非常快对完全逆序的数组它又非常慢。所以我们通常区分最好情况、最坏情况和平均情况。面试和工程中最该关注的是最坏情况因为它代表性能底线。如果一个系统在最坏情况下会超时那就不能上线。平均情况则更接近“典型应用”但计算平均往往要假设输入分布比较复杂。初学者常犯的错是把最好情况当成算法的真实水平比如听说某排序“接近O(n)”以为所有数组都能享受这个速度实际上只有数据基本有序才成立。复杂度的表达式也有小陷阱O(n m)表示两个输入规模独立比如遍历两个不同长度的数组O(n*m)表示嵌套循环涉及的两组数据分别增长计算量就会相乘。分析代码时要先分清哪些循环是并列的、哪些是嵌套的。3. 简单排序算法逐个拆解3.1 冒泡排序把最大值一点一点“冒”到末尾冒泡排序可能是很多人学的第一个排序算法。它的思路特别直观从头到尾遍历数组比较相邻两个元素如果前一个比后一个大就交换位置。这样一轮走完后最大的元素就像泡泡一样冒到了整个数组的末尾。下一轮再从头开始但末尾已经是最大元素就不需要再管它所以内层循环范围可以缩小一个。我用Python写一个标准实现并且加了一个常见的优化def bubble_sort(arr): n len(arr) for i in range(n - 1): swapped False for j in range(n - 1 - i): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] swapped True if not swapped: break return arr这个版本里的swapped标志很关键。如果某一轮从头到尾都没有发生交换说明数组已经有序再往后遍历只是浪费时间直接退出循环。加了这行优化后冒泡排序在“近乎有序”的数据上会快很多。很多人写冒泡排序会忘记内层循环的边界。外层的i控制轮数最多n-1轮就能排好内层range(n-1-i)是因为每一轮结束末尾已经排好i1个元素不需要再比较它们。这里如果写成range(n-1)代码也不会越界但会增加很多无用比较丢失了每一轮缩短范围的语义。冒泡排序的代码虽然简单却很适合用来理解“交换”和“相邻比较”。它像是把排序过程可视化地呈现出来每一轮都有一大一小两个数在相邻位置“打架”大数一路向后赢。数字规模小的时候这个算法还能用规模一大它就会被慢吞吞的O(n^2)拖垮。3.2 选择排序每次选最小的放前面就像整理书柜选择排序的思路比冒泡更“理性”我不需要在每一轮都做很多交换只要扫描一遍找出最小元素放到当前位置上。具体做法是先在整个数组里找到最小值的下标和数组第一个位置交换然后在剩下的n-1个元素里继续找最小放到第二个位置以此类推直到全部排好。def selection_sort(arr): n len(arr) for i in range(n - 1): min_idx i for j in range(i 1, n): if arr[j] arr[min_idx]: min_idx j arr[i], arr[min_idx] arr[min_idx], arr[i] return arr选择排序的“选”体现在min_idx上。它不需要像冒泡那样频繁交换每一轮只做一次最终交换最多n-1次交换。但你千万别以为它因此比冒泡快因为它的核心开销在“比较”上每一轮都要把未排序区间的所有元素都扫一遍比较次数仍然是标准的123...(n-1)也就是O(n^2)。有一个很反直觉的点选择排序的时间复杂度对任何输入都一样。哪怕数组已经有序它仍然要走完所有比较不能像冒泡加个标志就提前退出。这是它的一大弱点。但它也有好处交换次数少如果“交换元素”本身是一个很昂贵的操作比如交换的是复杂对象选择排序反而可能占优势。同时要注意选择排序是不稳定的。不稳定是什么意思举一个反例数组 [5, 8, 5, 2]如果第一个5和2交换后两个5的先后顺序可能就变了。在某些业务场景里排序的稳定性很重要后文我会详细说。3.3 插入排序像整理扑克牌已排序区逐渐扩大如果你玩过扑克牌一定体验过“摸一张新牌把它插到手里已排好序的牌堆中的正确位置”。插入排序就是这个过程。它把数组分成两部分左边是已排序区间右边是未排序区间。初始时认为第一个元素自己就是有序的然后从第二个元素开始依次把每个元素往左移动插到它该待的位置。def insertion_sort(arr): n len(arr) for i in range(1, n): key arr[i] j i - 1 while j 0 and arr[j] key: arr[j 1] arr[j] j - 1 arr[j 1] key return arr注意这里用的是“后移元素”而不是“交换”。先把待插入的key保存下来然后将比key大的元素逐个向右挪一位给key腾位置最后再写入。这种写法比反复交换高效一些也更容易理解“插入”的本质。插入排序有一个非常有价值的性质面对“几乎有序”的数组它接近O(n)。为什么因为当数据本身已经排得差不多时while循环往往执行一两次就停了甚至不执行。这在实际业务里很常见某个列表大部分有序只有少数几个元素位置不对。这时候插入排序可能是所有简单排序里表现最好的。它的稳定性也很好因为当arr[j]等于key时while循环的条件是arr[j] key而不是所以相等元素不会被移动相对顺序保持不变。这一点在后续处理复杂数据时很有用。3.4 三种简单排序的横向对比把三个排序放在一起看差别会非常清晰排序算法最好时间复杂度最坏时间复杂度额外空间稳定性特点冒泡排序O(n)O(n^2)O(1)稳定相邻比较可提前退出选择排序O(n^2)O(n^2)O(1)不稳定比较次数固定交换少插入排序O(n)O(n^2)O(1)稳定对几乎有序数据极快表格里的“最好O(n)”是我用加了标志位的冒泡排序实现以及插入排序对有序数组而言的结果。选择排序没有“最好情况下的退化”因为它必须老老实实走完所有比较。从常数因子看插入排序在一般的小规模数据里往往比冒泡和选择都快因为它平均后的比较和移动次数更少而且局部性也好。所以很多语言标准库在底层排序时遇到很小的子数组会切换成插入排序来收尾。简单排序并不等于“永远没用”只是你不能在百万级数据上指望它们。4. 手把手实操从伪代码到可运行实现4.1 环境准备与代码框架理论学习再多不如敲一遍代码。我建议你打开任何一个Python环境或者直接在在线解释器里完成。我们不需要额外依赖只用random和time也能做很多验证。先准备一个辅助函数用来生成随机数组以及一个验证函数检查排序结果是否正确import random def random_array(n, low0, high1000): return [random.randint(low, high) for _ in range(n)] def is_sorted(arr): return all(arr[i] arr[i 1] for i in range(len(arr) - 1))is_sorted利用Python的all和生成器做一次性判断比手写for循环更干净。测试时我们可以把排序后的数组和Python内置的sorted结果对比也可以直接用is_sorted验证。这部分看似简单但它是后面一切实验的安全网。很多人写完排序函数后随便跑一两个例子就宣布成功结果边界条件一多就崩。所以我建议从第一步开始就建立验证意识用随机数据生成器覆盖各种输入情况包括空数组、单元素数组、重复值数组。4.2 实现三种排序函数并测试下面的代码把三种排序放在一起方便直接运行对比def bubble_sort(arr): n len(arr) for i in range(n - 1): swapped False for j in range(n - 1 - i): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] swapped True if not swapped: break def selection_sort(arr): n len(arr) for i in range(n - 1): min_idx i for j in range(i 1, n): if arr[j] arr[min_idx]: min_idx j arr[i], arr[min_idx] arr[min_idx], arr[i] def insertion_sort(arr): n len(arr) for i in range(1, n): key arr[i] j i - 1 while j 0 and arr[j] key: arr[j 1] arr[j] j - 1 arr[j 1] key我故意让函数直接修改传入的列表没有返回新列表因为这样更贴近“原地排序”的概念。如果你习惯返回副本可以先复制一份再排序但要注意区分传引用还是传值。测试代码可以这样写test_cases [ [], [1], [5, 2, 9, 1, 5, 6], [3, 2, 1], [1, 2, 3, 4, 5], [100, 50, 25, 12, 6] ] for sort_func in [bubble_sort, selection_sort, insertion_sort]: for case in test_cases: arr case[:] sort_func(arr) assert is_sorted(arr), f{sort_func.__name__} failed on {case} print(f{sort_func.__name__} passed)这样跑下来你可以直观地感受冒泡、选择和插入的差异。注意函数之间的先后顺序不重要但每次都要用case[:]复制一份原数组否则第一个排序函数会把数组排好后面测试的就是已排序数组了。这是新手特别容易踩的坑。4.3 用数据规模观察“算法复杂度”的实际威力光通过测试还不够你还需要亲眼看一看O(n^2)到底是怎么随着n增长的。用time.perf_counter可以粗略计时import time def time_sort(sort_func, arr): start time.perf_counter() sort_func(arr) return time.perf_counter() - start for n in [1000, 2000, 4000, 8000]: data random_array(n) t time_sort(insertion_sort, data[:]) print(fn{n}, insertion_sort time{t:.4f}s)如果按O(n^2)的规律n从1000涨到2000理论上耗时应该变成约4倍从2000涨到4000也约4倍。实测可能有些偏差因为数据随机性、CPU缓存和Python解释器的开销都会影响但大趋势应该符合预期。你可以把三种排序都测一遍会发现冒泡和选择在n8000时可能已经需要一两秒插入排序会好一些。我还喜欢用“对数坐标”观察复杂度直接看日志趋势但这是后话。对于入门阶段比较不同n下的耗时倍率已经足够让你建立“复杂度”的直觉。强烈建议你亲手跑几个不同规模而不是只看文章里的表格。5. 复杂度分析实战一步步推导三种排序的耗时公式5.1 冒泡排序的复杂度推导很多人会背“冒泡排序O(n^2)”但说不清为什么。我把推导过程拆开。冒泡排序最基础的比较次数是第一轮比较n-1次第二轮n-2次第三轮n-3次直到最后一轮比较1次。所以比较总数是(n-1) (n-2) ... 1 n(n-1)/2去掉低阶项和系数就是O(n^2)。最坏情况下数组完全逆序每次比较都要交换交换次数也等于比较次数仍然是O(n^2)。最好情况下数组本来就有序加入swapped标志后第一轮比较完发现没有交换直接退出比较次数n-1所以最好情况是O(n)。没有标志位的冒泡排序就算遇到有序数组依然傻傻跑完n(n-1)/2次比较复杂度照样O(n^2)。从这个例子可以看出实现细节会真正影响复杂度不是嘴上说说。很多人面试时把冒泡排序复杂度背成“始终O(n^2)”其实不准确最优实现下最好情况是O(n)。准确的说法应该是“最坏和平均是O(n^2)最好可以到O(n)”。冒泡的额外空间我前面说过是O(1)因为它只在数组内部做交换临时变量就是那几根手指头。5.2 选择排序的复杂度推导选择排序的比较次数非常好推第一轮要找最小值需要比较n-1次第二轮要从剩下n-1个元素里找最小值比较n-2次依此类推最后一个元素不需要再比。总比较次数同样是(n-1) (n-2) ... 1 n(n-1)/2所以选择排序的时间复杂度固定是O(n^2)。有意思的是它没有最好、最坏的差别因为无论数组已经有序还是完全逆序每一轮都必须把剩余区间完整扫描一遍才能确认最小值。这是它的“死板”之处。但它的交换次数很固定每轮最多交换1次总共最多n-1次。这在某些场景是优点。比如你顺序排的是体积很大的对象交换需要拷贝大量字段而比较只是读几个字段那么选择排序的“少量交换”可能比冒泡排序的有大量交换更划算。当然这要权衡不能只看复杂度级别。空间复杂度O(1)不额外借数组。稳定性上它是三个里唯一不稳的原因前面举例说过为把最小值放到前面可能跨越多个相同元素破坏了它们原本的相对顺序。5.3 插入排序的复杂度推导与最好情况插入排序的复杂度最有趣因为它的表现和初始数据顺序强相关。每一轮i都需要把第i个元素插到前i个元素中的某个位置。最坏情况下数组从大到小排列每次插入时新元素都要一路挪到最前面移动次数是12...(n-1)n(n-1)/2同时每次都要做一次比较所以最坏O(n^2)。平均情况大约也是一半位置常数上有差别但数量级仍为O(n^2)。最好情况就是数组已经有序。此时对每个ikey只需要和左边最近的一个元素比较一次发现不用插入直接进入下一轮。比较次数是n-1移动次数为0复杂度O(n)。这正是插入排序的魅力输入数据越接近有序它越“超常发挥”。这也是为什么很多高级排序算法会在小区间切换成插入排序。插入排序的空间复杂度也是O(1)只在key和下标上做手脚。稳定性为稳定因为严格使用比较相等时不动。5.4 稳定性分析的意义稳定性这个词听起来抽象但在真实业务中作用很大。它的定义是如果数组中有两个相等元素a和b排序前a在b前面排序后a仍然在b前面那么这个排序算法就是稳定的。什么时候需要稳定排序一个经典场景是按多个字段排序。比如先给一批员工按部门排序再按姓名排序。如果第二次排序使用稳定排序那么姓名相同的人仍然保持部门顺序如果使用不稳定排序部门顺序就可能被打乱。数据库里经常需要这种多级排序行为所以底层排序算法往往精心选择稳定的变体。冒泡和插入稳定选择不稳定。理解“为什么不稳定”比记住结论更重要选择排序会跨越多个元素把最小值换到前面这个过程可能让相同元素的相对位置翻转。而冒泡只交换相邻元素且相等时不交换插入只在遇到严格大于自己的元素时才向后挪所以相等元素自然保持原序。面试时如果被问到建议直接手写一个反例说明。6. 工程里的选择简单排序到底够不够用6.1 什么时候该用简单排序看到O(n^2)先别急着否定。工程是权衡的艺术不是纸面复杂度最优就能赢。简单排序依然有它们的适用场景。第一个场景是数据量小。当你只需要排几十个、几百个元素时任何高级排序的性能优势都不明显反而是插入排序的简洁性和低常数可能获胜。某些标准库在递归排序到子数组大约几十个元素时会切到插入排序就是这个道理。第二个场景是“数据几乎有序”。比如一个排行榜每天只更新少量数据其余顺序没变或者用户上传的列表只在末尾插入了一个新元素。这时候插入排序可能达到O(n)比很多O(n log n)的算法跑得还快。我自己写业务代码时如果确认数组基本有序会直接用插入排序省心又高效。第三个场景是内存极其受限。O(1)额外空间的原地排序在嵌入式、驱动、单片机等环境很有价值。再加上实现简单、不易出错这类排序反而显得可靠。6.2 从简单排序到更优算法的思维跃迁简单排序的问题在于使用了太多的“盲目比较”。冒泡每一轮都在相邻移动选择每一轮都要从头到尾扫描插入则把每个元素和前面所有元素比较。当n变大这些操作都撑不住。更优的排序算法比如快速排序、归并排序核心思路是“分而治之”把一个大规模问题拆成两个小规模问题分别解决后再合并。这样单次的成本虽然也有常数但整体规模从n^2降到n log n增长斜率完全不同。我建议初学者先别急着学那些优化太复杂的算法。把冒泡、选择、插入这三兄弟吃透你会理解什么是嵌套循环、什么时候该追求“有序区间”、为什么“几乎有序”是个宝贵特性。等你会推导它们的复杂度了再去看快速排序的分区思想和归并排序的合并过程会轻松非常多。算法学习就像搭积木复杂度是底层积木简单排序是第一层积木。7. 常见问题与排查技巧实录7.1 为什么我的冒泡排序没有提前退出却还是O(n^2)这是初学者经常混淆的点。加了swapped标志后冒泡排序在最好情况可以O(n)但在最坏情况还是O(n^2)。很多人误以为“加了标志就能变成O(n)”这是不对的。标志只是一个提前终止机制只有当数组确实基本有序时才有用。如果数组是逆序或随机的几乎每轮都有交换标志不起作用复杂度还是原来那样。分析复杂度时一定要明确说的是“最好”还是“最坏”。在面试里你可以先给出最坏复杂度再补充说明优化后的最好情况这样显得完整。7.2 排序代码常见的边界错误循环结束条件差一位我见过最多的错误是内层循环边界写错。比如冒泡排序内层写成range(n-1)虽然不会越界但会导致很多多余比较而且“i”没有起到缩小范围的作用。选择排序的外层写成range(n)其实最后只剩一个元素时不需要再选写成range(n-1)即可但写成range(n)也不算错只是多了一轮空转。插入排序的while条件j 0如果写成j 0会漏掉和arr[0]的比较结果排序不完整。所以我给新人的建议是写完排序函数后一定要测n0、n1、n2以及完全逆序和完全有序的数组。我的随机测试习惯是for n in range(20)对每个n随机生成几十个数组全部和sorted结果对比能覆盖绝大多数边界。7.3 如何用随机数据验证排序正确性简单排序的正确性很好验证不用手动构造一堆用例。用random模块生成随机数组再用内置sorted做基准对比几乎可以覆盖所有正常情况。for _ in range(1000): n random.randint(0, 50) data random_array(n) expected sorted(data) for sort_func in [bubble_sort, selection_sort, insertion_sort]: arr data[:] sort_func(arr) assert arr expected, (sort_func.__name__, data, arr) print(all tests passed)这里arr expected是列表精确比较比is_sorted更严格。每次循环都重新生成随机数据跑1000次基本上不会漏掉边界。我甚至建议你把这套代码放进自己以后的算法练习模板里每次写新排序都先过一遍。另一个小技巧是把测试失败时的原始数据和排序结果一起打印出来。很多bug一眼就能看清是边界问题还是交换逻辑问题比盲目猜测快得多。7.4 面试里关于“简单排序”的高频追问不要以为简单排序不会在面试里出现。我遇到过很多面试官从冒泡排序切入然后连续追问复杂度、稳定性、优化空间。下面是几个高频问题建议先自己想答案再看解析为什么选择排序不稳定答因为它在选择最小值和当前位置交换时可能跨越相同元素导致相对顺序变化。插入排序在什么情况下最快答数组已经有序或基本有序时接近O(n)。数据量很小的时候插入排序为什么可能比快速排序还快答快速排序有递归调用和分区开销常数因子较大插入排序实现简单、局部性好常数极小在小数组上有优势。冒泡排序加上swapped标志后最好情况时间复杂度是多少答O(n)。这些追问考验的不是背题能力而是你对算法本质的理解。能用一句话解释“为什么”比背出结论有用得多。我个人在刷题和带新人的这些年见过太多一上来就啃复杂排序算法的人结果连为什么O(n^2)都说不清楚。如果你现在刚开始学算法别怕慢先把这三个简单排序的代码默写出来再把复杂度推导亲自算一遍。这套动作看似基础但真的会让后边的算法学习顺畅很多。最后再分享一个小技巧学排序的时候准备一个小数组比如 [5, 2, 9, 1, 5, 6]用手在纸上模拟每一轮变化。写代码遇到困难时回到手动模拟往往一眼就能找到错在哪。
阅读完成 · 觉得有帮助?