七大排序算法C语言实现:从冒泡到快排的核心思想与工程选型
发布时间:2026/9/28 12:11:28来源:尧图网络
一提到“排序算法”刚接触数据结构的人第一反应往往是这玩意除了应付考试到底还有啥用但我在实际工作和带新人的过程中发现排序问题背后那一整套“怎么把无序变成有序”的思路才是真正值钱的东西。你见到的七大排序算法并不只是七段可以背下来的代码它们是七种不同的思考方式有人喜欢一次次交换把最大元素推到末尾有人擅长“抓牌式”的顺序插入有人引入分治把大问题拆成小问题还有人干脆借助一棵看不见的完全二叉树。这篇文章就是为两类人准备的一类是刚学完 C 语言、正打算啃数据结构的学生另一类是工作两三年、写了不少业务代码却没系统搞过排序的开发者。我会用 C 语言把七个算法的标准实现全部写出来并且每一步都解释它们背后的“初阶思想”——也就是发明这些算法的人最初到底是怎么想的。把这套思想吃透了后面再去看快速排序的优化版本、C 标准库里的内省排序、外部排序里的多路归并都会轻松得多。1. 排序问题的本质与统一的分析框架1.1 排序到底做了什么排序的定义本身很朴素把一组元素按照某个关键字重新排列使它们从小到大或从大到小呈现某种规则。难点从来不在“定义清楚”而在于“数据规模一大怎么做才能快”。比如给 100 万个整数排序和给 10 个整数排序完全是两个世界的问题。所以初阶的排序课其实教的是两件事第一每种算法把无序变成有序的手段第二当数据规模变大时这种手段会退化到多严重。这里补一个生活化类比。你可以把排序想成整理书架冒泡排序是反复检查相邻两本书的顺序发现放反了就交换选择排序是每次在乱书堆里找出最小的一本放到正确的位置插入排序则像你手里抓到一本新书直接把它插进已经排好的书堆里。理解这些类比比死记代码重要得多因为面试官真正想听的是你能不能讲清楚“这个算法比那个算法好在哪”。1.2 初阶思想绕不开的三个评价维度每个排序算法都能通过三个维度来度量这也是初学者需要尽快建立的分析框架时间复杂度数据规模增大时算法执行的基本操作数量增长速度。这里要区分最好情况、最坏情况和平均情况不能只说一个“O(n²)”就完事。空间复杂度排序过程中有没有开辟额外的临时空间是原地排序还是需要借助一个和数据规模同量级的辅助数组。稳定性排序前相等的元素排序后它们之间的相对顺序有没有被打破。能保持的就是稳定排序否则是不稳定排序。稳定性这一点最容易被初学者无视但它有非常实际的应用。想象你在排一个学生成绩表成绩相同的人需要按学号排。如果只做一次“按成绩排序”而这个排序算法不稳定学号的内部顺序就可能在排序过程中被打乱。更常见的场景是先按姓名排一遍再按部门排稳定的排序可以保证第二次排序后同一个部门的人仍然按姓名有序。所以稳定性的价值丝毫不比时间复杂度低。1.3 为什么很多排序算法都停在 O(nlogn)很多初学者第一次看到归并排序和快速排序的 O(nlogn) 时会问为什么不能做到 O(n)这个问题的答案涉及一个很漂亮的理论结果凡是靠“比较两个元素大小”来决定顺序的排序算法都有一个信息论意义上的下界。每次比较最多产生两个结果大或小n 个元素的序列一共有 n! 种可能排列要让算法在所有情况下都能正确区分这些排列至少需要 log₂(n!) 次比较。用斯特林公式近似一下log₂(n!) 大约等于 nlog₂n。换句话说如果两个元素之间只会比较大小那么平均情况下的时间复杂度不可能比 O(nlogn) 更好。冒泡、选择、插入是 O(n²)但它们和 O(nlogn) 的差距恰恰是后面几种算法优化的出发点。这也解释了为什么不能凭空要求“更快”除非你借助计数、基数这类非比较手段。这也是为什么一些教材会把计数排序、桶排序也算进所谓“八大排序算法”里但初阶阶段先把七种比较排序吃透最重要。2. O(n²)三兄弟冒泡、选择、插入——最直观的入门思维2.1 冒泡排序让大元素像气泡一样上浮冒泡排序的思想一句话就能讲清楚相邻的元素两两比较如果前面的比后面的大就交换。这样扫描一趟之后当前序列里最大的元素就被“冒泡”到了最后。接着再扫描前面未排序的部分第二轮把次大的元素送到倒数第二的位置。重复 n-1 趟之后整个序列有序。下面这段 C 代码加了一个常见的优化如果某一趟全程没有发生任何交换说明序列已经有序可以直接退出。void bubble_sort(int a[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { int tmp a[j]; a[j] a[j 1]; a[j 1] tmp; swapped 1; } } if (!swapped) break; } }这段代码的复杂度很好算外层循环最多 n-1 次内层循环次数随 i 递减总的比较次数约等于 n²/2。最好情况下如果传入的数组本身有序加了标志位后第一趟扫描发现没有交换直接结束复杂度就变成 O(n)。最坏和平均情况都是 O(n²)。空间上只用了常数个临时变量是原地排序。稳定性也没有问题因为只有a[j] a[j1]时才交换相等的元素不会改变先后顺序。但说实话冒泡排序在实际工程里几乎不会单独使用它的教学价值远大于实用价值。它最大的好处是让初学者第一次直观看到“交换排序”这一大类算法的运作方式。我带新人时常说冒泡排序是第一道门走过这道门你才算勉强摸到了“排序算法”的边。2.2 选择排序每次挑出最小的放到该放的位置选择排序的思路非常像我们日常挑选物品的习惯第一趟在全部 n 个元素里找出最小值把它和下标 0 的元素交换第二趟在剩下的 n-1 个元素里找出最小值把它和下标 1 的元素交换依次类推。void selection_sort(int a[], int n) { for (int i 0; i n - 1; i) { int min_idx i; for (int j i 1; j n; j) { if (a[j] a[min_idx]) { min_idx j; } } if (min_idx ! i) { int tmp a[i]; a[i] a[min_idx]; a[min_idx] tmp; } } }选择排序一定要进行 n(n-1)/2 次比较所以它的最好、最坏、平均复杂度都是 O(n²)。但它的交换次数很少每趟最多一次总共最多 n-1 次。如果交换两个元素的开销远大于比较开销比如你在排序一个由大结构体组成的数组选择排序就有它的独特价值。这里必须提醒一个坑经典的选择排序是不稳定的。考虑数组[5(甲), 5(乙), 1]第一轮找到最小值 1把它与下标 0 的 5(甲) 交换得到[1, 5(乙), 5(甲)]。原来的 5(甲) 本来在 5(乙) 前面现在却排到了后面。很多书上只说“选择排序不稳定”却不解释为什么这个例子可以让你彻底记住原因跨越式交换很容易把相等的元素搬乱。2.3 插入排序像整理扑克牌一样把新牌插进去插入排序的思路在打扑克牌时体现得最自然你左手里的牌已经是有序的摸到一张新牌时把它从右往左和手里的牌比较找到合适的位置插进去。映射到数组上就是把数组看成两部分左边是已排序区域右边是待排序区域每轮把右边第一个元素插入左边正确的位置。void insertion_sort(int a[], int n) { for (int i 1; i n; i) { int key a[i]; int j i - 1; while (j 0 a[j] key) { a[j 1] a[j]; j--; } a[j 1] key; } }插入排序的代码很短但含金量不低。最内层的 while 循环实际做的事情是“把比 key 大的元素整体右移一位”一旦遇到不大于 key 的元素就停止然后把 key 放进去。注意这里的移动是逐位搬运而不是交换所以插入排序是稳定排序。总复杂度在平均和最坏情况下是 O(n²)但在输入几乎已经有序时内层循环几乎不用移动复杂度能降到 O(n)。这个特性让它特别适合作为其他高级排序在小区间上的收尾工具等讲到快速排序时你会再次遇到它。2.4 三个 O(n²) 排序的横向小结三个算法同是 O(n²)但性格完全不同。冒泡排序交换次数太多教学意义更大选择排序比较次数固定但交换少适合“比较便宜、交换昂贵”的场景插入排序在近乎有序的数据上表现最好而且稳定、实现简单。用一张表可以看得更清楚算法平均时间复杂度最好情况最坏情况空间复杂度稳定性冒泡排序O(n²)O(n)O(n²)O(1)稳定选择排序O(n²)O(n²)O(n²)O(1)不稳定插入排序O(n²)O(n)O(n²)O(1)稳定选型上我的建议很简单如果数据规模小比如几百到几千条而且基本有序直接插入排序就够了如果数据规模大就别在 O(n²) 里打转往下看分治和堆结构。3. 希尔排序从 O(n²) 到 O(nlogn) 的第一道分水岭3.1 增量思想为什么跳着排反而更快希尔排序是插入排序的改进而且改进的思路非常自然。你回头看插入排序它的内层循环每次只把元素左移一位。如果一个很大的元素在最前面它要经过很多轮比较才能挪到正确位置。希尔排序的做法是先让元素在“大跨度”上大体到位再缩小跨度精确调整。具体说选定一个增量 gap把相距 gap 的元素分到同一个组分别做插入排序。然后 gap 缩小继续分组排序直到 gap1最后整个数组有序。这背后的思想是插入排序在近乎有序的数据上效率极高接近 O(n)。希尔排序通过前面几轮的粗调让数据变得“大体有序”最后一轮标准的插入排序就能跑得很快。初看好像在同一个序列上反复排序多做了很多无用功但实验证明如果 gap 序列选得好虽然每一轮都做了多次插入排序总复杂度却远低于 O(n²)。这就是“化整为零、逐步逼近”的妙处。3.2 希尔排序的 C 语言实现最简单的 gap 序列就是每次取一半n/2、n/4……直到 1。void shell_sort(int a[], int n) { for (int gap n / 2; gap 0; gap / 2) { for (int i gap; i n; i) { int tmp a[i]; int j i; while (j gap a[j - gap] tmp) { a[j] a[j - gap]; j - gap; } a[j] tmp; } } }注意看这段代码的内层其实就是一个以 gap 为步长的插入排序。原来写插入排序时用的是j - 1、a[j - 1]现在全部换成j - gap、a[j - gap]。理解这一点你就明白为什么很多教材说希尔排序是“插入排序的推广”。3.3 稳定性与复杂度的不确定性希尔排序的关键变量是 gap 序列。经典的 n/2 序列实现简单但最坏情况还是 O(n²)如果改用更讲究的序列比如 Hibbard 序列1, 3, 7, 15, ...或 Sedgewick 序列平均复杂度可以压到 O(n^(3/2)) 甚至 O(n^(5/4))。业界并没有一个统一公认的“最优增量序列”这也是它不如快速排序和归并排序普及的原因之一复杂度和实现细节强绑定很难给出一个稳定可靠的性能承诺。稳定性方面希尔排序是不稳定的。因为分组跨越式移动会让相等元素的相对顺序被打乱这和选择排序不稳定是同一个原因。实际工程里希尔排序在数据量不大、内存极紧张的嵌入式环境里偶有使用但大多数现代语言的标准库已经不会用它做默认排序。初学它重点不是拿去实战而是理解“增量 插入”的巧妙组合这能帮你建立排序优化的第一直觉。4. 归并排序分治思想的教科书级示范4.1 分治三步骤分解、解决、合并归并排序是“分治思想”最标准的载体。所谓分治就是把一个复杂的大问题拆成若干个规模更小的同类子问题子问题解决了再合并回来。归并排序的三个步骤分别是分解把数组从中间一分为二左右两个子数组分别递归地排序解决递归进行到子数组只剩一个元素时它天然有序合并把两个已经有序的子数组合并成一个整体有序的数组。这里最值得琢磨的一句话是归并排序的核心操作在“合并”而不在“分解”。分解本身只是机械地取中点真正花时间的是把两个有序数组合并成一个有序数组。这一点恰好和快速排序相反后面你会看到对比。4.2 合并过程与完整 C 实现合并的难点在于不能直接原地完成至少常见的二路归并不行需要开一块临时空间。合并时用两个指针分别指向两个子数组的头部谁小就先把谁放进结果数组指针后移某一方用完了就把另一方剩余元素整体拷入。void merge(int a[], int left, int mid, int right) { int n1 mid - left 1; int n2 right - mid; int L[n1], R[n2]; // 教学用变长数组工程中可复用统一缓冲区 for (int i 0; i n1; i) L[i] a[left i]; for (int j 0; j n2; j) R[j] a[mid 1 j]; int i 0, j 0, k left; while (i n1 j n2) { if (L[i] R[j]) { a[k] L[i]; } else { a[k] R[j]; } } while (i n1) a[k] L[i]; while (j n2) a[k] R[j]; } void merge_sort(int a[], int left, int right) { if (left right) return; int mid left (right - left) / 2; merge_sort(a, left, mid); merge_sort(a, mid 1, right); merge(a, left, mid, right); }注意mid的写法是left (right - left) / 2而不是(left right) / 2。后者在 left 和 right 都接近 INT_MAX 时可能溢出前者可以安全避开。这个习惯从一开始就应该养成。合并时if (L[i] R[j])这一行里的“等号”不是可有可无。写成小于等于相等元素会优先取左子数组里的保证稳定性如果写成小于相等元素的相对顺序就会被打乱归并排序就变成不稳定的了。这是初学者最容易忽略的细节。4.3 归并排序的价值和代价归并排序最突出的优点有两个一是无论最好、最坏还是平均情况复杂度都是严格的 O(nlogn)完全不依赖输入数据的初始顺序二是它是七种经典排序里性能最好的稳定排序。因为这两个特性它经常被用于要求排序结果稳定、数据量又大的场景比如链表排序、外部排序中的多路归并。代价也很明显需要额外 O(n) 的辅助空间。递归调用本身还要用到 O(logn) 的栈空间。在内存很充裕的普通应用里这不是问题但在嵌入式设备上可能就成了短板。另外递归实现每次都要开辟临时数组频繁分配和释放内存会带来不小开销工程实现往往会复用一块缓冲区来消除这个浪费。5. 快速排序实践中使用最频繁的比较排序5.1 枢轴与划分分治的另一种姿势快速排序同样使用分治思想但和归并排序选择了完全相反的路。归并把精力花在“合并”上因为它的分解太简单快速排序把精力花在“划分”上因为合并根本不需要做——只要左右两个子数组各自有序整个数组自然有序。快排的思路是从序列里挑出一个枢纽元素pivot然后一趟扫描把比 pivot 小的元素放在它左边、比它大的放在右边接着对左右两侧分别递归排序。挑选 pivot 的方式直接决定性能。固定取最后一个元素最简单但一旦输入是已经有序或接近有序的数据每次划分都会出现极不平衡的左右子数组导致递归深度退化成 O(n)总复杂度退化到 O(n²)。实际工程里常见三种解法随机选 pivot从数学期望上打破最坏情况和特定输入之间的绑定三数取中取左端、中间、右端三个元素的中位数作为 pivot对已经排好序的输入特别友好小区间改用插入排序当子数组长度小于某个阈值比如 10 到 20时递归带来的开销已经大于插入排序本身直接转插入排序收尾。C 标准库里不少实现就是这么干的。5.2 简单的 C 实现Lomuto 划分法直观又容易写适合初学。它以数组最后一个元素为 pivot用下标 i 维护“已确定小于 pivot”的区域的边界void swap_int(int *x, int *y) { int tmp *x; *x *y; *y tmp; } int partition(int a[], int low, int high) { int pivot a[high]; int i low - 1; for (int j low; j high; j) { if (a[j] pivot) { i; swap_int(a[i], a[j]); } } swap_int(a[i 1], a[high]); return i 1; } void quick_sort(int a[], int low, int high) { if (low high) { int pi partition(a, low, high); quick_sort(a, low, pi - 1); quick_sort(a, pi 1, high); } }工程实现通常会在进入partition前先随机交换一下边界元素或者做三数取中来降低最坏情况的概率。注意划分语句用的是 pivot而不是 pivot这样可以让相等元素尽量待在右侧减少交换次数。5.3 稳定性、复杂度和一个典型坑快速排序是不稳定的原因和选择排序类似划分过程会把远处的元素进行跨越式交换相等元素的相对顺序无法保证。复杂度上平均和最好情况都是 O(nlogn)空间复杂度主要来自递归调用栈平均 O(logn)最坏 O(n)。这里必须单独提一个常见坑快排写起来短但要写得让所有边界情况都正确并不容易。递归时pi已经放在了正确位置所以左区间是[low, pi - 1]、右区间是[pi 1, high]。千万别把pi本身包含进任何一个递归区间否则可能出现无限递归。我见过不少初学的人在这里卡住调试一晚上都找不到问题。6. 堆排序用完全二叉树完成原地排序6.1 堆结构与堆排序的整体流程堆排序的思路有点另类它不急着直接排序列而是先把整个数组看作一棵完全二叉树的层序遍历结果然后把它整理成一个大顶堆——也就是每个父节点的值都不小于子节点。这样堆顶就是全序列的最大值。接下来的操作分三步循环把堆顶元素和当前堆的最后一个元素交换堆的有效长度减一最后一个元素已经排定不再参与后续调整对新的堆顶调用堆调整heapify让剩余部分重新满足大顶堆性质。重复这个过程最大值会被一个个送到序列末尾最终得到一个升序数组。用数组表示堆时父子下标关系是左孩子2*i 1右孩子2*i 2父节点(i - 1) / 2。这套下标关系是整个堆排序代码的地基错一步后面全部乱套。6.2 C 实现与关键步骤分析先写出堆调整函数。它假定 i 的左右子树都已经是大顶堆只有 a[i] 自己可能不满足父节点约束因此需要把它不断下移到合适位置void heapify(int a[], int n, int i) { int largest i; int l 2 * i 1; int r 2 * i 2; if (l n a[l] a[largest]) largest l; if (r n a[r] a[largest]) largest r; if (largest ! i) { swap_int(a[i], a[largest]); heapify(a, n, largest); } } void heap_sort(int a[], int n) { for (int i n / 2 - 1; i 0; i--) { heapify(a, n, i); } for (int i n - 1; i 0; i--) { swap_int(a[0], a[i]); heapify(a, i, 0); } }建堆为什么要从n/2 - 1开始倒着做因为n/2 - 1是最后一个非叶子节点只有从下往上调整才能保证“i 的左右子树已经是大顶堆”这个前置条件被满足。堆排序最大的优点是原地完成只需要 O(1) 的辅助空间。建堆过程整体是 O(n)因为节点越向下层需要调整的路径越短随后每次交换都要执行一次 O(logn) 的 heapify总计 O(nlogn)。最坏、平均、最好都是 O(nlogn)这一点是它相对快速排序的主要优势快排最坏会退化堆排序不会。堆排序的稳定性无须多说它必然是不稳定的每次把堆顶元素和末尾元素交换完全可能让两个相等的元素颠倒位置。7. 七大排序的横向对比与工程选型7.1 一眼看完的总对比表在动手选型之前我建议先把下面这张表背下来。它回答的是七大排序算法最关键的问题平均情况多快、最坏能不能扛住、费多少空间、稳定不稳定。排序算法平均时间复杂度最好情况最坏情况空间复杂度稳定性冒泡排序O(n²)O(n)O(n²)O(1)稳定选择排序O(n²)O(n²)O(n²)O(1)不稳定插入排序O(n²)O(n)O(n²)O(1)稳定希尔排序视增量序列而定常见 O(n^(3/2)) 附近O(n) 量级O(n²)简单增量序列O(1)不稳定归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定快速排序O(nlogn)O(nlogn)O(n²)O(logn) 递归栈不稳定堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定如果你喜欢记忆框架可以这样归纳冒泡、选择、插入是 O(n²) 里的三件套其中只有插入排序在有序场景下最有价值希尔是插入排序的跳跃版本复杂度介于两者之间但不稳定归并、快排、堆是进军 O(nlogn) 的三驾马车归并稳定但费空间快排平均性能最好但最坏情况会退化堆排序能在最坏情况下依然保证 O(nlogn) 并且不费额外空间。7.2 实战选型的判断顺序实际选型时我会按下面这个顺序过一遍。第一数据规模。规模小到几百甚至几千条直接插入排序就够了不要为了秀技术引入快速排序的递归和划分。过度设计有时候比低效更可怕。规模大了优先考虑 O(nlogn) 一族。第二数据分布。如果已知数据基本有序插入排序能跑出接近 O(n) 的效果同样的数据交给快速排序如果固定选最后一个元素当枢轴反而可能是最坏情况。希尔排序也适合近乎有序的数据但实现复杂度和调参难度更高。第三稳定性需求。如果后面的流程还依赖相等元素保持原顺序比如数据库先按用户 ID 排序再按活跃时间排序那稳定排序里的归并排序几乎是唯一候选。没有稳定性需求时默认选快速排序因为它的常数因子通常最小这也是 C、Java 标准库在处理普通场景时都倾向快排的原因。第四内存约束。要求原地排序、不能开大数组归并排序立刻出局堆排序和快排都能原地完成但如果要求最坏情况也稳定在 O(nlogn)堆排序胜出。7.3 面试题里的常见变体面试里常听到“如果你是 C 标准库的作者你会怎么选型”。这个问题考的不是记忆而是上面这一套权衡。典型的答案是主排序用快排的三数取中加随机化版本当递归分区规模小于一定阈值时改用插入排序如果要求稳定切换到归并排序。有些库还会加一道保险当递归深度超过 logn 的某个倍数时就用堆排序兜底这种混合方案在业界有个正式名字叫内省排序。理解了七种排序各自的优缺点后你就能看懂它的每一步设计选择。8. 写排序代码最容易踩的坑与调试心得8.1 边界条件永远要先想清楚排序算法的代码普遍不长但也正因为短边界条件更容易被忽略。我在代码评审时见过最多的问题就是数组长度为 0 或 1 时的行为。插入排序的循环从下标 1 开始如果 n0第一轮循环i1直接访问a[1]越界。归并排序如果不写if (left right) return;子数组长度只剩一个元素时还会继续递归调用。这些都是只要在写代码前用一秒钟想一下“最小规模的输入长什么样”就能避免的事。8.2 稳定性不是看名字就能判断的很多初学者会把“插入排序是稳定排序”记成“选择排序也稳定”。这里我建议用一个小例子自测序列[3(甲), 3(乙), 2]走一轮选择排序结果是什么如果结果里 3(甲) 跑到了 3(乙) 后面就说明这个实现打破了稳定性。自己手推一遍比死记结论可靠得多。8.3 递归深度的隐患快速排序在已经有序的数组上如果固定取最后一个元素为枢轴递归深度会达到 O(n)。当 n 大到十万级别时极可能在 C 语言里触发栈溢出。我之前帮一个同学排查程序闪退最后定位到就是快排在有序数据上爆栈。解决办法不是说“别用快排”而是写代码时就加入随机化枢轴或三数取中。归并排序的递归深度是确定的 O(logn)不太担心这个问题但它的辅助数组分配次数值得注意可以在递归外层分配一块统一缓冲区每次合并都往里写避免反复调用malloc。8.4 堆排序的下标细节是重灾区堆排序的下标计算必须小心翼翼。heapify过程中l和r可能越界所以无论访问a[l]还是a[r]之前都必须先判断l n、r n。另外第二个循环里每交换一次堆的有效长度就减一传给heapify的第二个参数必须是当前的i而不能一直是最初的n。我第一次手写堆排序时就是漏了这两个细节结果运行结果永远差一位。8.5 合并时的等号决定稳定性前面在归并排序里强调过if (L[i] R[j])的等号问题这里再强调一次。许多初学者写完归并后测试用例都通过了但一旦输入包含大量相等元素稳定性测试就失败原因几乎都出在这里。同样插入排序的循环条件写a[j] key还是a[j] key也会直接影响稳定性前者稳定后者会把相等的 key 继续往左搬变成不稳定。8.6 调试技巧先手算“纸面排序”再小数据单步跟踪我在带新人时总会给一个建议学排序算法不要一上来就开 IDE 断点调试先用笔在纸上写一组 6 到 8 个数手动跑一遍每一步的数组形态。这个过程会让你真正理解“哪一步把元素移到了哪里”。纸面跑通了再对照自己写的代码问题通常一眼就能暴露出来。如果纸面结果和代码结果不一致把两个过程一行行列出来对比差异点就是 bug。这种训练对准备面试尤其值钱因为面试官让你手写快排时你不可能开着一个调试器。最后再分享一个小技巧学习七种排序算法时不妨把它们按“耗时操作”分组记忆。冒泡耗时在交换选择耗时在比较插入耗时在搬移归并耗时在合并快排耗时在划分堆排序耗时在堆化。抓到每一个算法最核心的那个动作你会发现所有代码都是围绕这个动作展开的。我当年就是这么把数据结构课啃下来的希望这个方法也能帮你少走点弯路。
网站建设高端定制企业官网