尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

堆排序的核心:为什么升序用大顶堆?大小堆区别与工程实战解析

发布时间:2026/9/24 18:47:47

资讯中心
01
ARTICLE

堆排序的核心:为什么升序用大顶堆?大小堆区别与工程实战解析

堆排序的核心:为什么升序用大顶堆?大小堆区别与工程实战解析
还没想清楚为什么给数组排个序非要学堆排序的人十有八九会在面试或实战里栽跟头。我见过太多人代码背得滚瓜烂熟一问“为什么升序排序要用大顶堆而不是小顶堆”当场卡壳。这其实是一个非常好的信号背代码的人没理解堆排序的本质而理解堆排序本质的人根本不需要背。这件事我必须讲清楚因为堆排序算法跟快排、归并排完全不是一个思路它背后是“基于完全二叉树的优先级组织”。而“大小堆的区别”恰恰是无数人在面试和工程应用里反复踩坑的雷区。这篇文章我会直接把堆排序拆开揉碎从大小堆的定义到建堆、下沉、排序的完整实现再到TopK、优先队列等真实工程场景全程用实际案例说话。1. 先从“选数”说起堆排序在排序家族里的定位1.1 排序算法的选型困境拿到一组数据要排序最直白的做法是冒泡和插入排序——代码短、思路简单但数据量一旦过万O(n²)的复杂度就会让人崩溃。实测过 10 万条随机整数时冒泡排序在我笔记本上跑了大概 22 秒而快排只需要几十毫秒。这是数量级的差距不是硬件能救回来的。于是工程上真正能打的排序算法集中在三剑客快速排序、归并排序、堆排序。三者都能做到 O(n log n) 的平均复杂度但各有各的性格。算法平均时间复杂度最坏时间复杂度额外空间稳定性快速排序O(n log n)O(n²)O(log n)不稳定归并排序O(n log n)O(n log n)O(n)稳定堆排序O(n log n)O(n log n)O(1)不稳定光看这张表你可能会想堆排序最坏情况是 O(n log n)空间又是 O(1)这不是完美算法吗但现实工程里标准库的 sort 函数普遍用快排如 C 的 introsort 混合策略这背后的原因在后面章节会详细讲这里先记住一个结论堆排序不是一个“日常排序首选”但它是一个“必须在特定场景下能打的算法”。1.2 堆排序最特殊的地方时间稳定且原地完成快排的平均性能确实好它的常数因子小数据局部性好在绝大多数情况下是排序之王。但快排有一个让人不舒服的地方最坏情况 O(n²)。虽然随机化可以大大降低这个概率但在某些特殊场景下比如大量重复元素、极端有序数据快排依然可能踩中性能深坑。归并排序则是稳定且保证 O(n log n)但它有一个致命问题需要 O(n) 的额外空间。在大数据量场景比如要对 10 亿条记录排序额外申请等量空间的开销非常肉疼。堆排序恰恰补上了这两个空缺——无论数据长什么样它就是严格跑 O(n log n)而且完全在原地交换数组元素额外空间基本是 O(1)。这个“稳定性中的稳定性”是它的重要价值。代价是什么它不稳定相等元素的相对顺序可能被打乱而且不像快排那样有缓存友好的连续访问模式所以常数因子偏大实际运行速度往往比快排慢一些。1.3 花时间学堆排序图的不是排序本身说实话日常业务代码里你几乎不会手写堆排序直接用系统排序函数就够了。堆排序的真正价值在于它背后的数据结构——二叉堆。理解了堆排序你就理解了优先队列、TopK 问题、定时器调度、Dijkstra 最短路径算法的底层逻辑。这些可都是实打实的高频面试题和工程组件。所以接下来先从堆本身入手搞清楚大顶堆和小顶堆到底差在哪儿再回头看堆排序就顺理成章了。2. 大小堆的区别藏在“谁站在树顶”里2.1 大顶堆与小顶堆的本质定义堆是一棵完全二叉树所谓大顶堆也叫大根堆、最大堆就是满足父节点的值总是大于或等于任意一个子节点的堆。这样就保证了整棵树的根节点是全局最大值。小顶堆则恰好相反每个父节点都小于或等于它的子节点根节点是全局最小值。需要特别强调一个容易误解的点堆只约束了父节点和子节点之间的大小关系它并没有约束兄弟节点之间的大小顺序。换句话说堆只在“垂直方向”有序在“水平方向”是没有顺序的。这个特性我用一个生活场景来类比。想象一个班级里要选一个身高最高的人当领队。大顶堆的做法是每两个相邻的层级之间上面的人一定比下面的人高但同一层级左边的人未必比右边的人矮。你只需要顺着根节点往下找就能快速知道谁是最高的人但对全班人的身高做严格排名堆做不到。2.2 构建方式几乎一样比较符号决定一切构建大顶堆和小顶堆的实现代码几乎一模一样唯一的区别就是一个比较符号。用 Python 示意# 大顶堆的下沉调整父节点与较大的子节点交换 def sift_down_big(arr, n, i): largest i left 2 * i 1 right 2 * i 2 if left n and arr[left] arr[largest]: largest left if right n and arr[right] arr[largest]: largest right if largest ! i: arr[i], arr[largest] arr[largest], arr[i] sift_down_big(arr, n, largest) # 小顶堆的下沉调整父节点与较小的子节点交换 def sift_down_small(arr, n, i): smallest i left 2 * i 1 right 2 * i 2 if left n and arr[left] arr[smallest]: smallest left if right n and arr[right] arr[smallest]: smallest right if smallest ! i: arr[i], arr[smallest] arr[smallest], arr[i] sift_down_small(arr, n, smallest)注意只有在比较大小那几行不同逻辑骨架完全一致。很多实际应用比如优先队列需要动态维持最大值或最小值只需改一个符号就能在最大值优先和最小值优先之间切换这是堆结构非常优雅的地方。2.3 为什么升序排序要用大顶堆而不是小顶堆这是一个经典问题。直觉上既然是升序排序从小到大似乎应该用一个能“吐”出最小值的小顶堆。顺序读取堆顶不就是从小到大输出吗这个直觉本身没有错但遗漏了一个关键约束堆排序是原地排序不能借助额外的结果数组。升序排序的目标是把小值放前面大值放后面。如果用小顶堆根节点是当前最小值你可以把它拿出来放到数组前面但拿掉根节点后剩下的元素怎么继续维持堆要么你允许额外 O(n) 的辅助空间堆排序就不再是原地算法了要么让数组剩余部分继续构成堆但前面已经排好的位置会挡住调整的空间。解决这个矛盾的办法就是反过来用大顶堆。大顶堆的根节点是当前最大值把它和堆末尾元素交换最大值就直接落到了数组的最后一个位置。然后缩小堆的范围让剩余元素继续构成一个“缩水版”的大顶堆重复这个过程每轮把当前最大值送到数组末尾最后数组自然变成升序。一个真实的模拟数组 [4, 10, 3, 5, 1]先构建大顶堆堆顶是 10。把 10 和末尾的 1 交换数组变成 [1, 5, 3, 4, 10]。此时 10 已经固定在最后一个位置接下来对前四个元素 [1, 5, 3, 4] 重新调整成堆堆顶是 5再把 5 和当前末尾的 4 交换得到 [4, 1, 3, 5, 10]…… 这个过程一直重复最终数组就是升序的。所以记忆口诀是面向上浮用最小堆面向下沉用最大堆。升序排序用大顶堆降序排序用小顶堆。2.4 大小堆在 TopK 问题里的“角色互换”TopK 问题比排序本身更常见也更考人对大小堆的理解是否透彻。我面试过不少人问“一个包含 1 亿个整数的数组中找出最大的 100 个数”很多人的第一反应是排序然后取前 100 个。这个方案虽然对但不是最优的因为需要把所有数据读进内存并排序而堆可以只维护 100 个元素的堆空间友好得多。如果要求最大的 100 个数用小顶堆。堆顶是当前这 100 个数里最小的那个相当于一个“守门员门槛”。遍历整个数据流遇到比堆顶大的元素就把堆顶淘汰新元素入堆。这样堆里始终保存着迄今为止最大的 100 个数。这里存在一个反直觉的地方求最大却用最小堆。原因是堆顶是扇形小组的门槛只有新元素比门槛高才有资格进入这个“精英圈”。反过来求最小的 100 个数就用大顶堆堆顶是当前最小 100 个数里最大的那个。新元素如果比堆顶小说明它比精英圈里最菜的那个还强就替换它。需求使用的堆堆顶含义判断条件最大的 K 个数小顶堆当前 K 个数的最小值新元素 堆顶则替换最小的 K 个数大顶堆当前 K 个数的最大值新元素 堆顶则替换这个“角色互换”如果不先搞懂大小堆的本质到了真实场景十有八九会搞反。3. 手写堆排序的两个关键动作下沉与建堆3.1 完全二叉树用数组连续存储的秘密堆是一个完全二叉树这个特性保证了我们可以用数组来紧凑地存储它不需要额外的指针。完全二叉树的定义是除了最后一层其他层都是满的最后一层的节点又尽量从左往右排列。正因如此父子节点之间的下标关系是固定的。假设数组索引从 0 开始对于下标为i的节点左子节点下标2 * i 1右子节点下标2 * i 2父节点下标(i - 1) // 2比如数组 [10, 5, 3, 4, 1]下标 0 的元素是根它的左子节点是下标 1值为 5右子节点是下标 2值为 3。下标 1 的父节点是 (1-1)//2 0也就是值为 10 的根节点。这种紧凑结构让堆排序省掉了所有链表式的指针开销直接操作数组下标就能完成一切这也是它空间复杂度能做到 O(1) 的根本原因。3.2 下沉操作维护堆性质的灵魂堆排序所有动作的核心是一个操作——把某个节点“下沉”到正确的位置。具体逻辑是这样的如果当前节点的值比它更大或更小的子节点小或大就把它和那个子节点交换然后继续在新位置往下检查直到叶子节点或满足堆性质为止。以大顶堆为例下沉的每一步都是找左、右子节点中的较大值跟当前节点比如果当前节点更小就交换。def heapify(arr, n, i): 对下标为 i 的节点执行下沉操作。 n 是当前堆的有效长度。 while True: largest i left 2 * i 1 right 2 * i 2 if left n and arr[left] arr[largest]: largest left if right n and arr[right] arr[largest]: largest right if largest i: break arr[i], arr[largest] arr[largest], arr[i] i largest这里我用了 while 循环而不是递归主要是为了在大规模数据上避免递归深度过大。理论上递归也能写但工程习惯更倾向于迭代版本而且迭代版本对新手来说更容易检查边界条件。3.3 建堆为什么必须从最后一个非叶子节点开始把一个乱序数组变成堆需要从下往上逐个做下沉操作。最后一个非叶子节点的下标是 n // 2 - 1这里 n 是数组长度索引从 0 开始。从它开始往前遍历到根节点依次执行 heapify。为什么要从下面开始倒着做道理很朴素因为 heapify 的下沉操作假设当前节点的左、右子树已经是合法的堆了。如果你从根节点开始下沉而下面的子树乱成一锅粥下沉完根节点下面的乱序依然存在问题根本没有解决。反过来从最底层往上每次处理一个节点时它的两个子树都已经堆化了一遍就能把所有节点调整到位。数组中最后一个非叶子节点就是最后一个元素的父节点它后面全是叶子本身就满足“子树已经是堆”的前提。所以从它开始往前逐步堆化是唯一可行的自底向上策略。建堆的代码def build_heap(arr): n len(arr) for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i)3.4 排序阶段交换 缩小堆范围 下沉建完堆之后数组已经满足大顶堆性质堆顶就是全局最大值。排序阶段就是一个反复执行“选最大值放到末尾”的过程把堆顶下标 0和堆末尾下标 n-1交换这样最大值到数组末尾位置固定下来。把堆的有效长度减一也就是把刚放好的最大值排除在堆之外。对新堆的根节点刚交换过来的元素做下沉操作重新恢复堆性质。重复 n-1 次数组就全部有序了。完整实现def heap_sort(arr): n len(arr) # 第一阶段建堆 build_heap(arr) # 第二阶段逐个取出堆顶 for i in range(n - 1, 0, -1): arr[0], arr[i] arr[i], arr[0] heapify(arr, i, 0) return arr def heapify(arr, n, i): while True: largest i left 2 * i 1 right 2 * i 2 if left n and arr[left] arr[largest]: largest left if right n and arr[right] arr[largest]: largest right if largest i: break arr[i], arr[largest] arr[largest], arr[i] i largest def build_heap(arr): n len(arr) for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i)这段代码可以直接运行。我强烈建议你自己跑几个测试用例空数组、单元素数组、逆序数组、全部元素相同的数组、随机大数组每个用例都能帮你发现问题。4. 堆排序的完整模拟以 [4, 10, 3, 5, 1] 为例4.1 建堆过程的逐步拆解纸上谈兵不如手推一遍。我用一个非常经典的数组 [4, 10, 3, 5, 1] 来逐步追踪堆排序的每一步。这个数组足够小方便看清每个交换背后的逻辑。数组下标关系先列出来下标 0: 值 4下标 1: 值 10下标 2: 值 3下标 3: 值 5下标 4: 值 1数组长度为 5最后一个非叶子节点的下标是 5 // 2 - 1 1。所以建堆从下标 1 开始。第一步heapify(arr, 5, 1)下标 1 的值是 10。它的左子节点是下标 3值 5右子节点是下标 4值 1。10 比两个子节点都大所以这个位置已经满足堆性质不需要交换。第二步heapify(arr, 5, 0)下标 0 的值是 4。它的左子节点是下标 1值 10右子节点是下标 2值 3。左子节点 10 是三者中最大的所以把 4 和 10 交换。数组变成 [10, 4, 3, 5, 1]。交换后下标 1 的新值是 4。需要继续下沉吗看它的子节点下标 3值 5比它大。所以 4 和 5 交换。数组变成 [10, 5, 3, 4, 1]。此时下标 3 是叶子节点下沉结束。整个数组已经是一个合法的大顶堆了堆顶是最大值 10。建堆阶段完成。可以看到建堆经历了两层交换但整体操作次数是 O(n) 级别的。关于建堆为什么是 O(n) 而不是 O(n log n)后面会有详细分析。4.2 排序过程的完整追踪建堆完成后进入排序阶段。第一轮交换堆顶和末尾堆范围是 [0, 4]堆顶是 10末尾下标 4 是 1。交换后数组为 [1, 5, 3, 4, 10]。10 已经固定在最后一个位置。现在堆的有效长度变成 4对新堆的根节点下标 0值 1执行 heapify。子节点中左子节点下标 1值 5和右子节点下标 2值 3中5 更大1 和 5 交换。数组变为 [5, 1, 3, 4, 10]。继续下沉下标 1值 1的子节点是下标 3值 44 大于 1交换。数组变为 [5, 4, 3, 1, 10]。此时下标 3 是叶子节点下沉结束。堆结构恢复。第二轮交换堆顶和当前末尾当前堆范围是 [0, 3]堆顶是 5末尾下标 3 是 1。交换后数组为 [1, 4, 3, 5, 10]。堆有效长度变成 3对堆顶下标 0值 1重新下沉。子节点左子节点下标 1值 4和右子节点下标 2值 3中4 更大交换 1 和 4。数组变为 [4, 1, 3, 5, 10]。下标 1值 1是叶子此时堆有效范围是 [0,2]下标 1 的子节点下标 3 已经不在堆范围内了下沉结束。第三轮交换堆顶和当前末尾当前堆范围是 [0, 2]堆顶是 4末尾下标 2 是 3。交换后数组为 [3, 1, 4, 5, 10]。堆有效长度变成 2对堆顶下标 0值 3下沉。子节点下标 1值 1不满足“比 3 大”的条件无需交换。下沉结束。第四轮交换堆顶和当前末尾当前堆范围是 [0, 1]堆顶是 3末尾下标 1 是 1。交换后数组为 [1, 3, 4, 5, 10]。堆有效长度变为 1此时堆中只剩一个元素排序完成。最终数组[1, 3, 4, 5, 10]完美升序。如果你自己动手把这个过程在纸上画一遍堆排序的整个逻辑就刻在脑子里了。强烈建议不要跳这一步。4.3 为什么建堆是 O(n) 而不是 O(n log n)很多人下意识地认为建堆要对 n 个节点各做一次下沉下沉一次最坏 O(log n)所以建堆应该是 O(n log n)。这个直觉是错的因为不是每个节点下沉时都能达到 log n 的深度。可以这样理解完全二叉树中绝大多数节点集中在树的底部。倒数第一层有大约 n/2 个叶子节点它们根本不需要下沉倒数第二层有大约 n/4 个节点它们最多下沉 1 次倒数第三层有大约 n/8 个节点最多下沉 2 次。总的操作次数近似为T(n) (n/4) * 1 (n/8) * 2 (n/16) * 3 ...这个求和结果收敛于 n也就是说建堆的总代价是线性的O(n)。这是一个经典的分析结论虽然初看反直觉但数学上完全站得住。排序阶段就简单了每次把堆顶换到末尾然后对堆顶做一次下沉。下沉的代价是 O(log n)一共进行 n-1 轮所以排序阶段是 O(n log n)。两阶段叠加堆排序的总复杂度是 O(n log n)。空间复杂度所有操作都在原数组上完成额外只用了几个临时变量O(1) 空间。关于最坏情况堆排序没有任何退化路径——不管输入是逆序、顺序、还是全相同它都是 O(n log n)。这一点对某些对时间上限有硬性要求的系统非常关键。5. 堆排序工程实践中的坑与建议5.1 稳定性问题为什么堆排序“不守序”稳定性是一个很容易被你忽略但在真实业务里可能捅出娄子的特性。所谓稳定排序是指如果两个元素的值相等排序后它们的相对顺序保持不变。堆排序的稳定性和它的工作方式直接冲突。看一下排序阶段堆顶元素和堆末尾元素交换时如果这两个元素值相等交换后它们的位置就发生了对调更重要的是堆排过程中元素会多次在不同层级的路径上跳跃相同元素的相对顺序完全可能被打乱。举个例子有一条学生记录数组按“分数”排序其中两条记录分数都是 80 分本来张三排在李四前面。如果用堆排序排序后张三和李四的顺序无法保证可能在一次交换中张三跳到了李四后面。这在实际业务里是硬伤。比如数据仓库里对某列排序时希望同一分数的记录按原输入顺序输出堆排序做不到。遇到这种场景应当选择归并排序或者使用稳定排序算法的库函数。5.2 优先队列和堆排序的关系一个容易被混淆的概念面试中经常有人把优先队列和堆排序混为一谈。它们确实共享同一个底层数据结构——二叉堆但关注点完全不同。优先队列是动态的。它可以随时插入新元素也可以随时取出堆顶的最大值或最小值。插入和取出的时间复杂度都是 O(log n)。很多语言的内置实现都可以直接使用比如 Python 的 heapq、Java 的 PriorityQueue、C 的 priority_queue。堆排序是静态的。它针对一个完整数组先建堆再一个个“拔掉”堆顶最终完成全量排序。如果你要实现一个任务调度器任务有优先级随时可能来新任务那需要的是优先队列而不是堆排序。如果你要对一个静态的数据集排个序那才考虑堆排序。两者共用堆的核心操作但是使用场景和代码形态完全不同。用 heapq 实现优先队列很简单import heapq # 小顶堆堆顶是最小值 pq [] heapq.heappush(pq, 5) heapq.heappush(pq, 2) heapq.heappush(pq, 8) print(heapq.heappop(pq)) # 输出 2 print(heapq.heappop(pq)) # 输出 5如果需要支持大顶堆可以存负数或自定义比较类这是工程里非常实用的技巧。5.3 什么时候该用堆排序什么时候还是老老实实用内部排序直接给结论日常业务中能用语言标准库的排序函数就绝不要手写堆排序。标准库的排序如 C std::sort、Python sorted、Java Arrays.sort在绝大多数场景下性能优于手写堆排序因为它们的底层实现做了大量优化包括数据分割、局部性利用、小数组切换插入排序等。但有几个特定场景堆排序确实是更理性的选择内存极其受限的环境。嵌入式系统、单片机环境内存只有几十 KB无法承受归并排序额外的 O(n) 空间这时堆排序的 O(1) 额外空间就是救命稻草。对最坏时间有硬性要求的系统。比如某个实时调度模块它不允许排序突然飙升到 O(n²)。快排在这种场景可能因为糟糕的枢轴选择而退化堆排序保证每个case都稳定在 O(n log n)这一点很有价值。做 TopK 或动态数据流处理。虽然这不是排序而是堆的最典型应用。比如对 10 亿条日志找访问量最高的 K 个 IP堆是标准解法。除此之外常规业务里不太需要堆排序这一点要讲清楚不要给读者留下“堆排序是万能的”错误印象。5.4 手写堆排序最容易翻车的三个细节写堆排序的时候我已经数不清看过多少个人在下面三个细节上翻车了。这里单独拎出来作为避坑指南。边界条件判断错误。在 heapify 中判断子节点是否存在时很多人会把left n写成left n导致访问越界。记住下标从 0 开始合法的索引范围是[0, n-1]所以判断条件必须是 n而不是 n。同理右子节点判断也是right n。下沉方向写反。建大顶堆时应在子节点中找较大的那个建小顶堆时找较小的那个。有些人想当然地认为“升序排序用大顶堆所以下沉时一直跟右子节点比较”这是错误的。必须同时比较左右两个子节点后再与父节点比三个元素中取最大或最小作为交换目标。用递归实现 heapify 导致栈溢出。这在数据规模很大时是非常现实的问题。堆的高度虽然只有 log n但如果递归实现深度依然可达十几层理论上不会爆栈但是仍然存在某些语言递归调用开销过大、拖慢速度的问题。更重要的是用迭代实现养成习惯后改造成其他堆变体如索引堆、斐波那契堆会更顺手。建议的测试方法写一个计数器对一个随机数组排序后再写一个验证函数检查每个元素是否比前一个大再对逆序、全相等、含重复大数组分别验证。这样能把你代码里的所有潜在bug逼出来。6. 堆排序在真实场景中的进阶用法6.1 动态数据流的中位数维护堆排序本身是静态的但它衍生的技巧在动态问题里特别好用。一个经典问题是设计一个数据结构支持不断插入新数据并且能随时返回当前所有数据的中位数。解法是维护两个堆一个大顶堆存较小的一半一个小顶堆存较大的一半。插入时先判断该进哪个堆再根据两个堆的大小关系做调整保证两个堆的规模差不超过 1。这样大顶堆的堆顶就是较小一半的最大值小顶堆的堆顶就是较大一半的最小值中位数由这两个堆顶直接算出来。这个过程之所以高效正是因为在堆里插入和删除堆顶都是 O(log n)整体复杂度比每次插入后重新排序的 O(n log n) 好了一个数量级。我在处理实时监控指标时就经常用这种双堆结构来快速算耗时分布。6.2 多路归并排序中的堆优化归并排序合并两个有序序列很容易但当你要合并 k 个有序序列时用普通方法每轮要扫描 k 个序列的头部选最小值整体复杂度是 O(k * n)。更好的做法是维护一个小顶堆堆顶就是当前 k 个头元素中最小的那个弹出堆顶后从它所属的那个序列再取下一个元素入堆。这样每轮选最小值的开销从 O(k) 降到 O(log k)在大规模 k 路归并场景比如数据库外部排序下效果极其显著。这种思想在搜索引擎的倒排索引合并、大型日志文件的排序合并中也都有广泛使用。理解了堆的底层逻辑这些高级用法都是水到渠成的事。6.3 定时器与调度器中的应用系统里常见的定时器都是一个优先队列按照触发时间排序最近要触发的任务永远在堆顶。新任务到达时插入堆 O(log n)每次触发只需要取堆顶复杂度非常可控。我曾经在一个消息中间件里见过手写的std::priority_queue的妙用延迟消息按到期时间入堆消费者线程只需要看堆顶就能决定是否处理如果堆顶还没到期整个线程可以安全地 sleep。这比遍历一个无序数组判断哪些任务到期高效太多了。这个场景严格来说不是堆排序但它用的就是“堆”这个数据结构的灵魂。7. 写到最后堆排序的价值不在排序本身堆排序在整个算法体系里位置特殊。它不像快排那样题目刷得多就能背得很熟也不像归并排序那样规则一眼就能看懂。它需要你先理解完全二叉树在数组中的存储方式再理解下沉和上浮的调整逻辑最后还需要想清楚为什么升序要用大顶堆、TopK 取最大却要用小顶堆。每个点单独拆开都不难但组合在一起就是一个非常考验基础功的题目。我自己在实际项目里很少直接用堆排序做常规排序但在做 TopK、流式中位数、多路归并和定时器时天天都在和堆打交道。理解堆排序最大的收获其实是建立了“如何在一个动态变化的集合里快速拿到极值”的直觉这个直觉在系统设计里的价值远超排序本身。如果你正在准备面试或想深入理解数据结构的实际应用建议按这个顺序消化先手写一遍大顶堆的建堆和堆排序再改一个符号让它变成小顶堆然后试着用堆解决 TopK 问题最后挑战一下双堆求中位数。这条路走通之后你再看优先队列、Dijkstra 最短路径、A* 搜索这些内容都会有豁然开朗的感觉。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。