news 2026/10/3 14:02:55

选择排序与堆排序:从线性扫描到二叉堆的算法优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
选择排序与堆排序:从线性扫描到二叉堆的算法优化

排序算法这玩意,是数据结构绕不过去的坎。面试考、笔试考、工作中写业务代码不怎么用到但一写中间件就全回来了。我见过不少人在堆排序上栽跟头,对着一堆诡异的下标推导怀疑人生。也有一些人觉得选择排序太简单没啥好讲,可真要让他写一遍,不是边界写错就是循环条件搞反。

这篇我就把选择排序和堆排序摊开讲。这俩本质上是同一条思路的两个阶段——每轮从中选出最优解放到正确位置。区别只在于“选”的手法和“选”的成本。我尽量按照实际动手写代码的顺序来讲,中间会穿插CLRS那套循环不变量的证明思路,也会给出完整的C语言实现。篇幅可能有点长,但如果你能沉下心一口气读完,再用半小时把代码敲一遍,这几块骨头基本就啃下来了。

1. 内容整体设计与思路拆解

1.1 选择排序到底在“选”什么

选择排序的思路极其朴素:每一轮从未排序区间里挑一个最小(或最大)元素,把它放到已排序区间的末尾。重复n-1次,整个数组就有序了。

这个策略有个非常直观的生活类比:你有一堆散乱的扑克牌,每次都从里面翻出最小的那张,放到手里牌堆的底端;然后从剩下的牌里再翻最小的一张,接着放。这个过程不涉及任何元素跳跃式插入,也没有“交换链式”的连锁反应,每一步都清清楚楚:找到一个最小的,送它归位。

用白话说就是:

  1. 在第1轮,扫描整个数组,找到最小值,把它的位置和第0个位置交换。
  2. 在第2轮,从第1个位置开始往后扫,找到最小值,把它的位置和第1个位置交换。
  3. 依此类推,第i轮,从下标i开始扫描到末尾,找到最小值,和下标i交换。

到这一步为止,代码框架非常简单。但如果你只是背下这个流程,过两天就忘了。真正值得琢磨的是,为什么第i轮只需要扫描i之后的部分?答案藏在循环不变量的思想里。

在CLRS(《算法导论》)里,对选择排序的循环不变量是这样描述的:在每一轮迭代开始前,子数组A[0..i-1]已经是有序的,且其中的每个元素都不大于子数组A[i..n-1]中的任何元素。

这个不变量是理解选择排序正确性的钥匙。初始时i=0,A[0..-1]是空数组,条件自然成立。每轮结束时,我们将A[i..n-1]中的最小元素放到A[i]位置,于是A[0..i]有序且不大于剩余部分,下一轮的初始条件重新成立。到i=n-1时,整个数组有序。这个证明思路非常严密,也解释了后面堆排序的优化方向。

1.2 堆排序:同一思路的升级版

选择排序的痛点太明显了——每一轮找最小值都是线性扫描O(n),所以总复杂度必然是O(n²)。堆排序就是在这个“找最小值”的动作上动了刀:它把“每轮线性扫描n-i个元素”升级为“用二叉堆维护当前最小值,O(log n)时间取出”。于是总体复杂度从O(n²)降到了O(n log n)。

这个设计思路值得好好体会一下。程序员解决问题往往不是凭空发明新流程,而是对旧流程的某个瓶颈环节做结构性优化。选择排序的瓶颈是“找最小元素太慢”,堆排序就在这个环节上做文章:先把整个数组调整成一个“最小堆”(父节点值不大于子节点),这样堆顶天然就是全局最小值。取出堆顶后,把数组最后一个元素挪到堆顶,再执行一次“向下调整”,就能让剩余元素重新满足堆性质。如此循环n-1次,排序完成。

这里有一个特别容易被忽略的地方:堆排序使用的堆结构,并不是像std::priority_queue那样用malloc出来的独立结构,而是直接把原数组原地整理成堆。这是堆排序最大的魅力之一——O(1)的额外空间复杂度,连一个临时数组都不用。它完全靠数组下标之间的关系来表示一棵完全二叉树:下标i的父节点是(i-1)/2,左孩子是2i+1,右孩子是2i+2。

所以,看堆排序不要把它当成一个“神秘的堆的数据结构”,它就是“用数组模拟完全二叉树”加上“选择排序的框架”。一旦你把这个映射关系印在脑子里,下面代码里的每个下标运算都有了实感。

1.3 为什么这两个算法值得放在一起学

很多人学排序是散点式地学:今天冒泡明天插入后天快排,每个算法孤立地背代码。但学算法最忌讳的就是孤立记忆——你很快就忘了,而且一旦忘了就完全不会推。

我的建议是:把选择排序和堆排序当成一条演化链来学。

  • 选择排序:每轮线性扫描找最小值。
  • 堆排序:用堆结构加速“找最小值”这个操作。

这样你记住的就不只是两段代码,而是一个“怎么优化算法瓶颈”的思维模型。以后你看到任何“每次都要取最值”的问题,第一反应就是“能不能用堆来加速”,这就触及了算法学习的核心价值。

2. 核心细节解析与实操要点

2.1 选择排序的实现,从索引边界说起

先给一段最基础的C语言实现。

#include <stdio.h> void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } void selection_sort(int arr[], int n) { int i, j, min_idx; for (i = 0; i < n - 1; i++) { min_idx = i; for (j = i + 1; j < n; j++) { if (arr[j] < arr[min_idx]) { min_idx = j; } } if (min_idx != i) { swap(&arr[i], &arr[min_idx]); } } }

这段代码需要关注三个边界细节。

第一,外层循环只需要到n-2。因为当i走到倒数第二个位置时,最后剩下的那个元素必然是全局最大,不需要再扫描。很多人写i < n,多循环一轮,虽然不影响正确性(最后一轮min_idx等于自己,swap不交换),但这是思路不清晰的表现。

第二,内层循环从i+1开始,j的终止条件是j < n而不是j <= n-1,这个就是数组下标习惯,两者等价,但写成j < n更符合C语言的常规。

第三,min_idx必须在每轮外层循环开始时重新初始化成i。它记录的是“当前找到的最小元素的下标”,而不是值本身。用一个变量存值然后直接交换位置,会丢失索引信息,这是新手最容易犯的错。

2.2 为什么选择排序“交换次数是0到n-1”

选择排序有一个常被人忽视的优点:它的交换次数最多是n-1次。因为每一轮外层循环最多做一次交换(把最小值放到位置i)。即使数组完全逆序,你也就是交换n-1次。

这意味着什么?如果你要排序的元素是“交换代价极其昂贵”的数据结构(比如数组元素是巨大的结构体,或者交换有副作用),选择排序在交换次数这一点上吊打插入排序和冒泡排序。冒泡排序最坏情况下交换次数接近n²/2,插入排序移动次数也是O(n²)级别,而选择排序无论数据怎么分布,比较次数永远是n(n-1)/2,交换次数永远是O(n)。

所以这产生了一个反直觉的结论:在“比较便宜、交换贵”的场景下,选择排序其实是个不错的选择。比如你对一组大型结构体按某个小字段排序,不想搞复杂的索引排序(先排下标数组再按序抽取),那么直接排结构体本身时,选择排序能有效控制交换造成的开销。

不过同样因为这个特性,选择排序属于不稳定排序。稳定性问题是实际工程中筛选排序算法的重要依据,后面第4部分我展开讲。

2.3 堆排序的建堆过程,先理解“堆化”

堆排序的核心是一个操作:heapify(向下调整)。它的作用是,假设某个节点的左右子树都已经满足堆性质,但这个节点本身可能不满足(比如它比自己的子节点大),那就把它和较小(或较大)的子节点交换,然后递归向下调整,直到它落到正确位置。

建堆是从最后一个非叶子节点开始,从下到上逐个执行heapify。

最后一个非叶子节点的下标怎么算?如果数组长度为n,最后一个元素的父节点就是(n-2)/2(因为最后一个元素下标是n-1,父节点是(n-1-1)/2)。举例:数组长度10,最后一个非叶子节点下标是4;数组长度11,还是4。这一块经常有人写错,建议直接记住结论:for (int i = n / 2 - 1; i >= 0; i--)来建堆。

你可能会有疑问:为什么不从下标0开始向下调整,而是从后往前?因为heapify的前提是“左右子树已经是堆”。叶子节点本身天然是堆(只有自己一个元素),从最后一个非叶子节点开始,就能保证每次调用heapify时,它的左右子树都已经调整过了。这和动态规划的“自底向上填表”是一个思路。

建堆的复杂度需要注意:很多人以为建堆是O(n log n),其实精确计算下来是O(n)。直观的原因是,随着下标的减小,每个节点heapify的成本随之降低。靠近根部的节点调整次数多,但数量少;靠近叶子的节点调整次数少甚至为1,但数量多。这个反直觉的结论在《算法导论》里有完整证明,用的方法是求和一个几何级数的上界。面试中问到“为什么建堆是O(n)”时,能把这个直观说法讲清楚就很不错了——每个元素下沉的路径长度随着它离叶子的距离缩短,大部分元素都离叶子很近,所以总代价线性级。

2.4 堆排序完整代码:升序排列用大顶堆

有些初学者会困惑一个问题:升序排序,堆顶应该是最小值吗?堆排序的常规实现恰恰相反——升序排序用的是大顶堆。你每轮把堆顶(最大值)换到数组末尾,然后缩小堆范围,把剩下的部分重新堆化。最大值从末尾开始往前填,最后得到的就是升序数组。

如果你用小顶堆,确实每轮能取出最小值,但取出后最小值必须放到结果数组的前面,这就需要额外O(n)的存储空间。为了节省空间原地排序,我们接受“逆序弹出”的方案,用大顶堆。

完整C语言实现如下。

#include <stdio.h> void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } // 向下调整,使得arr[0..n-1]中以root为根的子树满足大顶堆性质 void sift_down(int arr[], int n, int root) { int largest = root; int left = 2 * root + 1; int right = 2 * root + 2; if (left < n && arr[left] > arr[largest]) { largest = left; } if (right < n && arr[right] > arr[largest]) { largest = right; } if (largest != root) { swap(&arr[root], &arr[largest]); sift_down(arr, n, largest); } } void heap_sort(int arr[], int n) { // 建堆:从最后一个非叶子节点开始,自底向上调整 for (int i = n / 2 - 1; i >= 0; i--) { sift_down(arr, n, i); } // 一个个从堆顶取出最大元素,放到数组末尾 for (int i = n - 1; i > 0; i--) { swap(&arr[0], &arr[i]); sift_down(arr, i, 0); } }

这段代码要特别注意第26行和第34行:第26行sift_down(arr, n, i)的n是完整数组长度,因为建堆时所有元素都在堆内;第34行sift_down(arr, i, 0)的堆边界缩小了,因为末尾i位置已经放上了最终元素,不再参与堆调整。很多人把这两处的边界写混,导致排序结果出现“末尾元素被重新交换到前面”的诡异bug。

2.5 递归和迭代的选择

上面的sift_down用的是递归写法,思路清晰,工程上建议改成迭代。原因倒不完全是栈溢出——堆的深度是O(log n),n如果是一亿,深度也就27层左右,来递归的栈压力根本不算大。

真正的原因是,递归写法每次调用时函数栈开销虽然不致命,但在排序这种高频循环里,多点函数调用开销也是白花花的性能。迭代写法的逻辑和递归完全等价,可读性反而更高。

void sift_down_iter(int arr[], int n, int root) { while (1) { int largest = root; int left = 2 * root + 1; int right = 2 * root + 2; if (left < n && arr[left] > arr[largest]) { largest = left; } if (right < n && arr[right] > arr[largest]) { largest = right; } if (largest == root) { break; } swap(&arr[root], &arr[largest]); root = largest; } }

两种写法,我建议你都想一想、敲一遍。递归版本更贴近“heapify”的数学定义,适合用来理解;迭代版本更适合落地使用。面试时如果要求手写堆排序,建议直接写迭代版本,少几个函数调用栈,也少招人问“递归深度会不会溢出”这种扩展问题。

3. 实操过程与核心环节实现

3.1 用CLRS循环不变量视角再看选择排序

第2部分给的代码是能跑的,但如果你想彻底搞懂它,我强烈建议你照着CLRS的套路走一遍循环不变量的三步证明。这是把代码从“背下来”变成“推导出来”的分水岭。

  • 初始化:当i=0时,子数组A[0..-1]为空。空数组当然有序,而且“其中任何元素不大于A[0..n-1]中任何元素”这个命题是空洞成立的(不存在这样的元素),所以不变量成立。
  • 保持:假设某轮开始时,A[0..i-1]有序且都不大于剩余的A[i..n-1]。内层循环会在A[i..n-1]中找到最小元素的下标min_idx。把A[min_idx]和A[i]交换后,A[i]现在是剩余部分的最小值,而A[i-1](已排序部分的最大值)不大于A[i]。因此A[0..i]有序,且其中的元素都不大于新的剩余部分A[i+1..n-1],下一轮的不变量成立。
  • 终止:当i=n-1时,A[0..n-2]有序,并且它们都不大于A[n-1]。于是整个数组有序。

这个证明里最精妙的地方在于“剩余部分”这个集合的定义随i变化。它帮你随时把握住一个核心事实:选择排序每一轮只保证一个元素落到最终位置,之前放好的元素绝对不会再被动过。这和插入排序截然不同,插入排序每轮会“挤动”已排序区间来腾位置。

3.2 手动模拟堆排序一轮流程

光看证明容易飘,来手推一轮堆排序,把下标和值都摆出来。

假设数组是:[4, 10, 3, 5, 1],长度为5。

第一步建堆。最后一个非叶子节点下标是5/2-1=1,对应元素10。它的左孩子是下标3(值5),右孩子是下标4(值1),10比两者都大,不需要调整。接着看下标0元素4:左孩子下标1(值10),右孩子下标2(值3),最大的是10,于是交换4和10,数组变成[10, 4, 3, 5, 1]。但交换后下标1的4可能还违反堆性质,继续调整:它的左孩子下标3(值5),右孩子下标4(值1),最大是5,交换4和5,数组变成[10, 5, 3, 4, 1]。此时堆结构是大顶堆。

排序开始:

  • i=4:交换堆顶10和末尾1,数组变成[1, 5, 3, 4, 10],堆范围缩小到前4个元素,对堆顶1做sift_down。1的左孩子5、右孩子3,最大是5,交换,得到[5, 1, 3, 4, 10];继续调整,1的左孩子是4(下标3),右孩子越界,交换1和4,得到[5, 4, 3, 1, 10]。前4个元素重新成堆,数组末尾10已经就位。
  • i=3:交换堆顶5和下标3的1,数组变成[1, 4, 3, 5, 10],堆范围是前3个元素。调整堆顶1:左孩子4、右孩子3,交换1和4,得到[4, 1, 3, 5, 10]。前3个元素成堆。
  • i=2:交换堆顶4和下标2的3,数组变成[3, 1, 4, 5, 10],堆范围是前2个元素。调整堆顶3:左孩子1,不变。
  • i=1:交换堆顶3和下标1的1,数组变成[1, 3, 4, 5, 10],排序完成。

手动推一遍,你对“堆排序的每一轮做了什么”会有极强的感知。你会发现,堆排序的交换次数在前几轮看起来毫无章法,但每一轮堆顶元素都会被放到它最终该在的位置。

3.3 复杂度分析:选择排序看比较数,堆排序看堆化路径

选择排序的比较次数是固定的n(n-1)/2,不随数据分布变化。不管数据是本身就有序的,还是完全逆序的,每轮都要完整扫描剩余区间。这导致了它缺乏“自适应”特性——即便你给它一个已经排好的数组,它依然要做同样的比较量。这个特性有时候是缺点(没利用输入数据的有序性),有时候是优点(最坏情况也不会恶化到比n²级别更多)。

交换次数前文已提,最多n-1次。

堆排序的时间复杂度拆成两部分:

  • 建堆:O(n)。
  • n-1次“交换+向下调整”:每次调整最坏O(log n),所以是O(n log n)。

综合就是O(n log n)。注意堆排序是“最坏情况也是O(n log n)”的排序算法,这点比快速排序要强——快速排序最坏是O(n²)(比如已经有序的数据配合糟糕的枢纽元选择)。当然快排在工程实践上依然是默认王者,原因涉及缓存局部性、常数因子和随机化手段,这在第4部分细说。

堆排序的额外空间复杂度是O(1),这是它的巨大优势。很多内存受限的嵌入式环境、驱动模块内部排序,都会倾向用堆排序而不是归并排序,就因为它不需要额外数组。

3.4 用C语言完整跑一遍并验证

把选择排序和堆排序放在同一个demo程序里对比验证:

#include <stdio.h> #include <stdlib.h> void print_array(int arr[], int n) { for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int arr1[] = {64, 25, 12, 22, 11}; int n1 = sizeof(arr1) / sizeof(arr1[0]); int arr2[] = {64, 25, 12, 22, 11}; int n2 = sizeof(arr2) / sizeof(arr2[0]); selection_sort(arr1, n1); printf("selection sorted: "); print_array(arr1, n1); heap_sort(arr2, n2); printf("heap sorted: "); print_array(arr2, n2); return 0; }

输出应该都是11 12 22 25 64。如果你改一下入参,比如传一个完全降序的数组{5, 4, 3, 2, 1},或者传一个带重复值的数组{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5},看看两组算法输出是否稳定一致,能进一步帮你排查实现中的边界问题。

说到调试,我强烈建议你写一个小的随机测试框架,生成足够多的随机数组,分别调用selection_sort和heap_sort,然后和C标准库的qsort结果对比,任何不一致都说明你的实现有bug。这一步是工程级验证,不是学校作业级的“跑一个用例就交差”。我用这种方法抓到过自己在sift_down边界条件上的一个隐蔽错误,眼看输出99%有序但最后一个元素错位。

4. 常见问题与排查技巧实录

4.1 选择排序是稳定排序吗?为什么?

不是稳定排序。

先解释稳定性:如果数组中有两个相等元素,排序后它们的相对顺序保持不变,这种算法就叫稳定的。选择排序每轮会选中剩余区间中的最小值,并把它和当前i位置元素交换。问题就出在这个“交换”上——它可能把一个靠前的元素直接换到后面去。

举例:数组[5a, 3, 5b, 1],其中5a、5b我是用来区分两个值相等的5。第一轮找到最小值1,下标3,和下标0的5a交换,数组变成[1, 3, 5b, 5a]。你看,本来5a在5b前面,排序后5b跑到5a前面了,相对顺序被破坏。

选择排序的稳定性问题在工程上会导致一个连锁反应:如果你先按“部门”排序,再按“薪资”排序,希望薪资相同的记录依然保持部门排序的结果,那么第二趟排序算法必须稳定。选择排序做不到,所以这种“多关键字排序”场景下你会换用归并排序或插入排序。

4.2 堆排序也不是稳定排序,原因不同

堆排序的不稳定性来自两个层面:

一是父子节点交换时可能跨越多个位置。大顶堆堆顶元素和数组末尾元素交换时,如果堆里还有另一个与堆顶等值的元素,它可能被换到前面,也可能留在原地,相对顺序没法控制。

二是sift_down过程中,一个节点往下“沉”时,可能经过若干与它等值的节点,把它们顶到上面去。比如大顶堆里有个节点值是5,它往下沉时路过另一个值为5的兄弟节点,为了维持堆结构,两者的相对位置就会变动。

所以,堆排序虽然时间复杂度漂亮、原地排序,但一旦涉及“相同关键字顺序要保留”的需求,它直接出局。

4.3 堆排序 vs 快速排序:为什么工程上快排才是默认选择?

你要是看过各种排行榜,会发现C标准库的qsort、C++的std::sort,底层都不是堆排序,而是快排的变体(某些情况下混入插入排序)。这是为什么?

第一,缓存局部性。快排的分区操作是对数组进行连续扫描,访问模式是线性的,CPU缓存命中率很高。堆排序的访问模式是跳跃式的——你总是在下标i、2i+1、2i+2这些地方跳来跳去,缓存命中率差得多。现代计算机的瓶颈早已不是算法复杂度,而是内存访问模式。

第二,常数因子。堆排序每轮sift_down平均需要比较2到3次(找左孩子、右孩子的最大值),而快排每轮只需要比较1次。n趋于无穷大时,堆排序的2nlog₂n和快排的1.39nlog₂n差距在基准常数上就已经体现出来了。

第三,最坏情况规避。快排最坏O(n²),但这个最坏情况通常可以通过随机化枢纽元来避免到“几乎不会发生”。堆排序的最坏情况就是平均情况,但如果常数因子差,这个保证意义就打了折扣。

所以堆排序的应用场景主要集中在:需要最坏情况保证且内存极度有限、不希望有额外数组分配的场景。比如实时系统、嵌入式内核里的某些排序模块。其余的通用排序,快排和归并是主流。

4.4 建堆用“从下往上”和“从上往下”有什么区别

有读者可能会问:建堆能不能从下标0开始逐个sift_down?能,但那样复杂度会退化到O(n log n)而不是O(n)。原因是,从根开始调整时,你没法保证子节点已经满足堆性质,每个节点都可能在向下沉的过程中多次穿越整个树高。而反过来从底部开始,每个节点的调整路径短,总代价低。这个差别在处理百万级数据时非常明显。

4.5 递归堆化的栈溢出与性能

我在真机上测过,对一个100万随机整数排序,递归堆化和迭代堆化的时间差大约在10%到20%之间。几十毫秒对单次排序无所谓,但如果你在一个循环中调用无数次排序,差距就累积起来了。

另外有个冷知识:堆排序在数据“已经接近有序”时表现并不比乱序好多少。因为它每轮总要重新堆化,而堆化的代价和当前堆的层数强相关,数据分布对堆高度的影响很有限。堆排序对输入数据的“适应性”几乎为零,这是它不如插入排序“见好就收”的地方。

4.6 查找最小值还是最大值的细节

堆排序里如果你要降序排序,逻辑就要反过来——用小顶堆,每轮把最小值放到末尾。实现上只需要把所有比较运算符从大于号改成小于号即可。我见过有人为了省这点改动,强行在降序时还维持大顶堆,然后按索引逆序输出,结果数组输出倒是降序,但堆结构不伦不类,下一步维护时必然踩坑。

想清楚你要的是升序还是降序,再决定堆的类型,别偷懒。

5. 从选择到堆:一个思维模型的延伸

把选择排序和堆排序打通后,你可以把这个思维模型迁移到很多算法问题上。

最经典的是Top K问题:要从10亿个数里找出最大的前100个,如果先全排序,复杂度是O(n log n);但用最小堆维护当前Top 100,每来一个新数,只要比堆顶大就替换堆顶并重新堆化,复杂度是O(n log k),k固定为100时几乎等于O(n)。这就是堆排序思维的直接应用。

另一个应用是“动态数据流的中位数”:维护一个大顶堆存较小的一半数据,一个小顶堆存较大的一半数据,新元素来了就判断该进哪边,必要时调整两边堆的大小差异。这也是堆结构的经典场景。

所以,我劝你学算法的时候,不要背代码,而是去抓“这个结构在解决什么瓶颈问题”。选择排序告诉你“线性扫描找最小值是低效的”,堆排序告诉你“树形结构可以把最值查询优化到log级别”。带着这个思维去刷题,你会发现自己对很多数据结构的理解都更上了一层。

我在讲面试培训时经常问学生一个问题:给你一个流式数据,要求随时都能取出当前数据的中位数,你的方案是什么?能做出这个题的人,几乎都先懂堆排序的原理。排序算法学得好不好,不在于能不能默写代码,而在于能不能把里面的思想迁移到新问题上。

6. 写在最后的一些小经验

排序算法是那种看起来简单,但非常检验“工程细致度”的东西。我见过不少资深开发者在面试时手写堆排序翻车,原因就是sift_down的边界条件和递归退出条件没有理清楚。

给你一个查漏补缺清单,我面试前也会拿它自测:

  • 选择排序的外层循环结束条件是n-1还是n?为什么可以到n-1而不是n?
  • 内层循环的起始下标是i+1还是i?如果从i开始会有什么影响(多比较一次,不改变结果但浪费)?
  • 堆排序里n/2-1是怎么来的?如果不是偶数长度,这个公式还成立吗?
  • sift_down的终止条件是什么?什么时候可以安全退出?
  • 堆排序的swap之后,为什么缩小堆范围从i开始而不是i+1?

这些问题如果你能一口气回答出来,说明你是真的理解了,不是背的。

最后分享一个我的调试技巧:写排序算法时,不要用大数组测试,用{3, 3, 3}、{2, 1}、{1, 2}、{}这种极端小用例起步。等小用例全通过,再用随机大数组和标准库对比。一个空数组就能逼出很多实现里的“非空假设”bug——如果你的排序函数一上来就访问arr[0],空数组直接就崩了。

数据结构和算法这条路没有捷径,但可以走得更聪明。把选择排序和堆排序当成一条线来学,你会发现很多在其他地方零散见过的东西——二叉堆、优先队列、堆化、Top K——全都串起来了。这就是我写这篇长文的初衷。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 14:00:34

Bonree Ants流式引擎:毫秒级实时指标计算实践指南

简介&#xff1a;Bonree Ants流式大数据处理引擎是一套面向Windows平台开发者的轻量级时序数据流式计算框架&#xff0c;适用于需要快速构建准实时指标计算、动态基线预警及多粒度批量分析能力的中高级Java工程师与大数据初学者。资源包共136个文件&#xff0c;含110个核心Java…

作者头像 李华
网站建设 2026/10/3 13:56:14

2026企业AI办公工具选型指南:从方法论到主流平台盘点

企业采购AI办公工具时&#xff0c;很多管理者习惯于直接对比功能清单&#xff0c;以功能数量、品牌知名度作为决策依据&#xff0c;或是单纯参考同行业采购案例快速敲定产品。这种选型思路容易忽略工具与业务场景的适配问题&#xff0c;不少企业上线AI平台后&#xff0c;工具停…

作者头像 李华
网站建设 2026/10/3 13:55:17

微信小程序开发:模板

微信小程序的template,本质是wxml层面的可复用模块片段。它的核心意义是&#xff1a;把重复的wxml结构抽出来&#xff0c;通过传入不同的数据来复用&#xff0c;减少重复代码&#xff0c;提升维护效率。 通过官方的文档了解 &#xff1a;wxml模块<template>用法, 补充一…

作者头像 李华
网站建设 2026/10/3 13:55:11

初步理解 AOP

初步理解 AOP AOP&#xff08;Aspect-Oriented Programming&#xff0c;面向切面编程&#xff09;是对 OOP&#xff08;面向对象编程&#xff09;的补充。它要解决的核心问题是&#xff1a;把那些散落在各个业务方法中的重复逻辑&#xff08;如日志、事务、权限、缓存&#xff…

作者头像 李华
网站建设 2026/10/3 13:55:02

Flex-π——比Fast-WAM更快的多流世界-动作模型:可按需融合RGB和“从RGB中获取的3D pointmap和DINO语义”,联合生成潜在的未来视觉特征和动作

前言 过去一年&#xff0c;具身智能领域在模型架构上展开激烈范式之争 一派坚守 VLA&#xff08;视觉-语言-动作模型&#xff09;&#xff0c;认为端到端直接从图像映射到机械臂动作&#xff0c;结构精炼、推理极快&#xff0c;是高频实时闭环控制的唯一可行方案&#xff1b;…

作者头像 李华
网站建设 2026/10/3 13:52:12

开源项目可视化--一键生成架构图丨Github项目分享

开源仓库转交互式的系统架构图Diagram网页版&#xff1a; GitDiagram - Visualize Any GitHub Repository 开源仓库转换成一个结构化的、易于 AI 理解的文本&#xff0c;将代码库的原始内容完整地呈现给LLM&#xff1a; https://gitingest.com/ 开源仓库打包Repomix&#x…

作者头像 李华