引言
在少儿编程的各类赛事里,排序与优先级调度几乎是必考的底层能力。无论是蓝桥杯软件赛(C/C++·Python 程序设计,第十八届报名季 2026-10-08---12-18、省赛 2027-04-10)、全国青少年信息素养大赛算法应用主题赛,还是中国电子学会软件编程(Python/C++)考级,都经常出现「把数据整理有序」「取前 k 名」「实时维护最大/最小值」这类题目。
要高效完成这些任务,**堆(Heap)与优先队列(Priority Queue)**是最趁手的工具。今天我们用一道原创题,把堆排序的原理、手写实现,以及优先队列的经典应用一次讲透,并给出 C++ 与 Python 双版本。
题目 / 项目目标
校园科技节成绩整理
科技节结束后,班主任手里有一份
n名同学的比赛得分(整数,可能重复)。请你完成两件事:
- 全体排序:把全部得分按从小到大的顺序整理好(要求用堆排序实现);
- 表彰前 k 名 :奖状有限,只表彰得分最低的
k名同学(即前 k 小)。请用尽量高效的方法输出这k个分数(不要求有序,但禁止为了取前 k 个而把整个数组排一遍序)。
下面我们来拆解:如何用「堆」这一数据结构,把这两件事都优雅地解决。
核心考点
- 完全二叉树与堆 :用数组即可表示一棵完全二叉树,节点
i的左右孩子分别是2i+1与2i+2(下标从 0 开始)。 - 大根堆 / 小根堆:每个节点的值都不小于(或不大于)它的孩子,堆顶就是最大(或最小)值。
- 下沉调整
siftDown:把可能违反堆性质的节点一路往下交换,直到重新满足堆序,是堆的核心操作,时间复杂度O(log n)。 - 建堆
buildHeap:从最后一个非叶子节点(n/2 - 1)自底向上依次下沉,整体只需O(n)(不是O(n log n))。 - 堆排序 :升序排序用大根堆,反复把堆顶(最大)换到当前末尾、缩小堆再下沉,得到
O(n log n)的原地排序。 - 优先队列取前 k 小 :建最小堆后连续弹出
k次堆顶,时间O(n + k log n),远优于全排序后再取。
解法 / 拆解
任务一:堆排序(升序,原地,大根堆)
思路分两步:先把数组调成大根堆,再把堆顶依次和末尾交换并下沉,最大的元素就「沉」到了末尾。
cpp
// C++ 堆排序(升序,原地,大根堆)
#include <iostream>
#include <vector>
using namespace std;
// 下沉调整:以 i 为根的子树,保证根 >= 左右孩子
void siftDown(vector<int>& a, int n, int i) {
while (true) {
int l = i * 2 + 1, r = i * 2 + 2, mx = i;
if (l < n && a[l] > a[mx]) mx = l; // 先和左孩子比
if (r < n && a[r] > a[mx]) mx = r; // 再和右孩子比(别漏了右孩子)
if (mx == i) break; // 已经满足堆序,结束
swap(a[i], a[mx]);
i = mx; // 继续往下检查
}
}
void heapSort(vector<int>& a) {
int n = a.size();
// 建堆:从最后一个非叶子节点自底向上下沉,O(n)
for (int i = n / 2 - 1; i >= 0; --i) siftDown(a, n, i);
// 排序:堆顶(最大)换到末尾,缩小堆再下沉,O(n log n)
for (int end = n - 1; end > 0; --end) {
swap(a[0], a[end]);
siftDown(a, end, 0);
}
}
python
# Python 堆排序(升序,原地,大根堆,用标准库 heapq 是最小堆,这里手写保持一致)
def sift_down(a, n, i):
while True:
l, r, mx = i * 2 + 1, i * 2 + 2, i
if l < n and a[l] > a[mx]:
mx = l
if r < n and a[r] > a[mx]:
mx = r
if mx == i:
break
a[i], a[mx] = a[mx], a[i]
i = mx
def heap_sort(a):
a = list(a)
n = len(a)
for i in range(n // 2 - 1, -1, -1): # 建堆 O(n)
sift_down(a, n, i)
for end in range(n - 1, 0, -1): # 排序 O(n log n)
a[0], a[end] = a[end], a[0]
sift_down(a, end, 0)
return a
任务二:前 k 小(最小堆,优先队列)
最小堆的堆顶永远是全局最小。把全部数建成最小堆(建堆 O(n)),然后连续弹出 k 次堆顶,就得到前 k 小,总耗时 O(n + k log n)。
cpp
// C++ 前 k 小(最小堆 / 优先队列)
#include <queue>
#include <vector>
using namespace std;
vector<int> topKSmallest(vector<int>& a, int k) {
// greater<int> 表示最小堆;范围构造内部调用 make_heap,建堆 O(n)
priority_queue<int, vector<int>, greater<int>> pq(a.begin(), a.end());
vector<int> res;
for (int i = 0; i < k && !pq.empty(); ++i) {
res.push_back(pq.top()); // 堆顶最小
pq.pop();
}
return res;
}
python
# Python 前 k 小(标准库 heapq 就是最小堆)
import heapq
def top_k_smallest(a, k):
h = list(a)
heapq.heapify(h) # 建最小堆 O(n)
res = []
for _ in range(min(k, len(h))): # 弹 k 次 O(k log n)
res.append(heapq.heappop(h))
return res
小贴士:如果只想要「最小的 k 个且要求有序」,把上面结果再
sort一下即可;如果k接近n,直接全排序反而更简单,这就是下面要说的「何时用堆」。
时间 / 空间复杂度
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
建堆 buildHeap |
O(n) |
O(1) |
堆排序 heapSort |
O(n log n) |
O(1)(原地) |
| 取前 k 小(最小堆) | O(n + k log n) |
O(n)(堆占空间) |
| 单次插入 / 弹出 | O(log n) |
--- |
堆排序与快速排序、归并排序同属 O(n log n),但它是原地、且最坏情况也稳定 O(n log n) (快排最坏会退化成 O(n²)),在赛事中非常可靠。
易错点(务必注意)
- 下标从 0 还是 1 开始 :本文用「下标从 0 开始」,所以左孩子是
2i+1、右孩子是2i+2。一旦你改成「从 1 开始」存数据,公式要变成2i与2i+1,混用会直接数组越界或算错父子关系。 - 下沉时漏比右孩子:只和左孩子比,忘记比较右孩子,会导致堆序破坏,排序结果错乱。
- 建堆起点写错 :必须从最后一个非叶子节点
(n/2 - 1)开始向上下沉;从 0 或 1 开始都建不出正确的堆。 - 升序用大根堆、降序用小根堆:堆排序升序依赖「反复把最大值放末尾」,所以必须用大根堆;用反了会得到降序甚至乱序。
- 下沉的越界判断 :访问
a[l]、a[r]前务必判断l < n/r < n,否则会越界崩溃。 - 语言默认堆方向不同 :Python 的
heapq是最小堆 ,C++ 的priority_queue默认是最大堆 (要用greater<int>才是最小堆)。跨语言刷题时这个差异极易栽跟头。 - 多组测试未清空:竞赛多为多测,堆/数组如果定义在循环外,记得每组重新初始化,否则上一组的数据会污染这一组。
进阶
- 对顶堆求动态中位数 :用「大顶堆存较小一半 + 小顶堆存较大一半」可
O(log n)插入、O(1)取中位数,是处理「数据流实时中位数」的标准解法(与本文堆思想一脉相承)。 - 堆优化的最短路 :Dijkstra 算法用优先队列维护「当前距离最小的节点」,把朴素
O(n²)降到O((V+E) log V),是图论必会优化。 - 多路归并 :合并
K个有序数组/链表时,用最小堆每次取K个队首里最小的那个,时间O(N log K)。 - 可并堆(左偏树 / 配对堆) :当题目需要「合并两个堆」时,普通二叉堆做不到
O(log n)合并,需要左偏树等结构。 - 优先队列 vs 排序:取前 k 个、实时维护极值、Top-K 频次统计这类问题用堆;需要全量有序才用排序。选错数据结构会在大数据量下超时。
小结与互动
今天我们围绕一道「校园科技节成绩整理」原创题,把堆排序 (建堆 O(n) + 排序 O(n log n))和优先队列取前 k 小 (O(n + k log n))完整走了一遍,并给出了 C++ 与 Python 双版本。记住最关键的三个点:建堆从 n/2-1 向上、下沉别漏右孩子、升序用大根堆。
你平时在刷题时,是先写 sort 一把梭,还是会特意用堆来优化?有没有哪道题因为「没用堆」而超时过?欢迎在评论区聊聊你的经历,我们下期见。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。