2026蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:堆排序与优先队列解析

引言

在少儿编程的各类赛事里,排序与优先级调度几乎是必考的底层能力。无论是蓝桥杯软件赛(C/C++·Python 程序设计,第十八届报名季 2026-10-08---12-18、省赛 2027-04-10)、全国青少年信息素养大赛算法应用主题赛,还是中国电子学会软件编程(Python/C++)考级,都经常出现「把数据整理有序」「取前 k 名」「实时维护最大/最小值」这类题目。

要高效完成这些任务,**堆(Heap)与优先队列(Priority Queue)**是最趁手的工具。今天我们用一道原创题,把堆排序的原理、手写实现,以及优先队列的经典应用一次讲透,并给出 C++ 与 Python 双版本。

题目 / 项目目标

校园科技节成绩整理

科技节结束后,班主任手里有一份 n 名同学的比赛得分(整数,可能重复)。请你完成两件事:

  1. 全体排序:把全部得分按从小到大的顺序整理好(要求用堆排序实现);
  2. 表彰前 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²)),在赛事中非常可靠。

易错点(务必注意)

  1. 下标从 0 还是 1 开始 :本文用「下标从 0 开始」,所以左孩子是 2i+1、右孩子是 2i+2。一旦你改成「从 1 开始」存数据,公式要变成 2i 与 2i+1,混用会直接数组越界或算错父子关系。
  2. 下沉时漏比右孩子:只和左孩子比,忘记比较右孩子,会导致堆序破坏,排序结果错乱。
  3. 建堆起点写错 :必须从最后一个非叶子节点 (n/2 - 1) 开始向上下沉;从 0 或 1 开始都建不出正确的堆。
  4. 升序用大根堆、降序用小根堆:堆排序升序依赖「反复把最大值放末尾」,所以必须用大根堆;用反了会得到降序甚至乱序。
  5. 下沉的越界判断 :访问 a[l]、a[r] 前务必判断 l < n / r < n,否则会越界崩溃。
  6. 语言默认堆方向不同 :Python 的 heapq 是最小堆 ,C++ 的 priority_queue 默认是最大堆 (要用 greater<int> 才是最小堆)。跨语言刷题时这个差异极易栽跟头。
  7. 多组测试未清空:竞赛多为多测,堆/数组如果定义在循环外,记得每组重新初始化,否则上一组的数据会污染这一组。

进阶

  • 对顶堆求动态中位数 :用「大顶堆存较小一半 + 小顶堆存较大一半」可 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 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
Eiceblue1 小时前
用 Python 给 Word 文档加水印:文字、图片与批量处理
开发语言·vscode·python·word
j7~1 小时前
【C++ 面试笔记】(篇二):类与数据抽象 & STL 容器与算法
开发语言·c++·面试·stl容器和算法·类和数据抽象
Android小码家1 小时前
framework&framework-minus-apex&android.jar的区别
android·python·jar
lee_curry1 小时前
第十周完整学习笔记:继续升级为“检索更准、权限安全、索引可维护、失败可恢复、长期可运行”的企业级 RAG 系统
python·ai·langchain·agent·langgraph
Fuyo_11191 小时前
深入解析 C++ unordered_set 与 unordered_map 的核心特性与性能实战
开发语言·c++
码匠许师傅1 小时前
【C++三方组件】FFmpeg:音视频处理的瑞士军刀
c++·ffmpeg·音视频
SZYSK2 小时前
OpenHarmony 上读卡、推理和双屏刷新抢 CPU:核验机与消费机的调度差
物联网·算法·harmonyos·1024程序员节
ShineWinsu2 小时前
对于Qt:QPushButton常见属性的解析
开发语言·c++·qt·api·gui·按钮·qpushbutton
Ivanqhz2 小时前
MoE + LLM 应用
数据库·人工智能·pytorch·python·深度学习·算法·1024程序员节