【数据结构与算法】堆与堆排序
堆与堆排序
1 堆的概念
- 堆用于维护一个数集。
- 堆是一个完全二叉树
- 小根堆:每个结点都小于等于它的左右子结点(递归定义)
- 推论:每个结点都是以其为根节点的子树的最小值

2 堆的性质
- 完全二叉树的性质:

条件 | 编号(或数量) |
---|---|
完全二叉树的层数 | |
完全二叉树根节点向下走到叶子结点的最大距离 | |
最后一个非叶子结点(向下走到叶子结点最大距离为 1 的最后一个结点) | n/2 |
向下走到叶子结点最大距离为 2 的最后一个结点 | n/4 |
向下走到叶子结点最大距离为 d 的最后一个结点 | |
向下走到叶子结点最大距离为 d 的结点个数 |
- 小根堆的性质:由小根堆的定义可得每个结点都是以其为根节点的子树的最小值
这也是堆排序的思想,每一次将小根堆的根节点输出,然后维护剩下的节点作为一个小根堆。
3 堆的实现(小根堆为例)
- 使用一维数组来存储堆
- 1 号点为根结点。
- 结点
x
的左儿子是2x
,右儿子是2x+1
- 堆的基本操作:
down(x)
操作,将结点往下移动来维护一个小根堆- 结点
i
与其左右孩子2i
和2i+1
比较,如果结点i
不是三者之中的最小值,则结点i
要向下移动。 - 结点
i
与其最小的孩子t
交换位置后,然后递归down(t)
- 结点
up(x)
操作,把一个结点向上移动来维护一个小根堆- 结点
i
与它的父节点i/2
比较,如果父结点大于子结点,就将结点i
与i/2
交换 i /= 2
直到父结点小于子结点
- 结点

down
操作/// down 操作
void down(int h[], int size, int i)
{
int t = i;
if(2 * i <= size && h[i] > h[2 * i]) t = 2 * i;
if(2 * i + 1 <= size && h[t] > h[2 * i + 1]) t = 2 * i + 1;
if(t != i)
{
int temp = h[i];
h[i] = h[t];
h[t] = temp;
down(h, size, t);
}
}

up
操作/// up 操作
void up(int h[], int size, int i)
{
while(i / 2 && h[i / 2] > h[i])
{
int temp = h[i];
h[i] = h[i / 2];
h[i / 2] = temp;
i /= 2;
}
}
- 使用基本操作
up
和down
就可以完成如下操作:
/// - 插入操作
/// 新增元素插在末尾,然后对他进行 up 操作
heap[++ size] = x;up(size);
/// - 最小值
/// 根节点即为最小值
heap[1];
/// - 删除最小值
/// 用堆的最后一个元素覆盖根节点,然后堆的尺寸减 1
/// 然后对根节点执行 down 操作
heap[1] = heap[size];size--;
down(1);
/// - 删除任意一个元素
/// 用堆的最后一个元素覆盖要删除的元素,然后堆的尺寸减 1
/// 然后对覆盖后的结点执行 down 操作
heap[k] = heap[size];size--;
down(k);up(k);
/// - 修改一个元素的值
/// 修改结点元素值后,对其执行 down 操作或 up 操作
/// 这里减少了一个判断,down() 和 up() 都写上,只会执行其中一个。
heap[k] = x;down(x);up(x);
4 堆排序
- 推排序用于对一维数组的元素进行排序
down
操作将一维数组转换为小根堆:down
只对非叶子结点操作才有意义,由完全二叉树的性质,最后一个非叶子结点的编号为n / 2
- 因此,从
i = n/2
到i = 0
的结点进行down
操作,这样就能得到小根堆。
for(int i = size / 2; i; i--) down(i);
- 输出最小值,然后删除最小值,循环。
while(size)
{
printf("%d ", h[1]);
h[1] = h[size]; size--;
down(1);
}
- 堆排序实现
void heap_sort(int h[], int size)
{
for(int i = size / 2; i; i--) down(i);
while(size)
{
printf("%d ", h[1]);
h[1] = h[size]; size--;
down(h, size, 1);
}
}
C++ 使用 vector 实现小根堆模板类
#pragma once
#include <vector>
#include <stdexcept>
#include <string>
using namespace std;
template <class T>
class MinHeap
{
/// @brief 用来存储堆的容器, 第一个位置不会使用
vector<T> m_elems = vector<T>(1);
public:
inline size_t size() const {
return m_elems.size() - 1;
}
inline bool empty() const {
return m_elems.size() <= 1;
}
inline void push(const T& elem) {
// 1. 将新添加的元素放在堆的末尾
m_elems.emplace_back(elem);
// 2. 然后对末尾元素执行 up 操作
up(m_elems.size() - 1);
}
inline T top() const {
if (this->empty()) throw logic_error("The MinHeap is empty.");
return m_elems[1];
}
void pop() {
if (this->empty()) throw logic_error("The MinHeap is empty.");
// 1. 用堆中最后一个元素覆盖根节点, 然后删除末尾的结点
m_elems[1] = m_elems.back();
m_elems.pop_back();
// 2. 然后对根结点执行 down 操作
down(1);
}
void erase(const int& index) {
if (index >= m_elems.size()) throw out_of_range(to_string(index) + " is out of range! ");
// 1. 用堆末尾的元素覆盖要删除的元素, 堆元素个数 -1
m_elems[index] = m_elems.back();
m_elems.pop_back();
// 2. 然后对 index 位置的结点执行 down 操作
down(index);
}
private:
void up(const int& index) {
int tmp = index;
// 当父结点比自己大时, 执行 up 操作
while (tmp / 2 && m_elems[tmp / 2] > m_elems[tmp]) {
swap(m_elems[tmp / 2], m_elems[tmp]);
tmp /= 2;
}
}
void down(const int& index) {
int tmp = index, left = index * 2, right = index * 2 + 1;
// 1. 求的 index, index * 2, inded * 2 + 1 三者中最小元素的下标
if (left < m_elems.size() && m_elems[index] > m_elems[left])
tmp = left;
if (right < m_elems.size() && m_elems[tmp] > m_elems[right])
tmp = right;
// 2. 如果最小元素的下标与 index 不相等
if (index != tmp) {
// 交换 index 和 tmp 对应的值
swap(m_elems[index], m_elems[tmp]);
// 继续执行 down 操作
down(tmp);
}
}
};
C++ 优先队列
- 与一般的队列容器
queue
First in,First out 不同, 优先队列priority_queue
特点是 First in,Largest out. priority_queue
其实就是堆数据结构, Largest 指的是元素的优先级最高, 通过指定元素优先级的比较规则,priority_queue
即可大根堆也可是小根堆, 还可存放非数值的其他元素.
语法
template <typename T,
typename Container=std::vector<T>,
typename Compare=std::less<T> >
class priority_queue{
//......
}
-
typename T
:指定存储元素的具体类型; -
typename Container
:指定priority_queue
底层使用的基础容器,默认使用vector
容器, 还可以是queue
-
typename Compare
: 指定容器中评定元素优先级所遵循的排序规则,默认使用std::less<T>
按照元素值从大到小进行排序,还可以使用std::greater<T>
按照元素值从小到大排序,但更多情况下是使用自定义的排序规则。
参考自: C++:std::greater()、std::less()、自定义比较函数的规则
对于 std::greater()、std::less() 排序和建堆的效果
- 对于排序
less<T>
升序排列greater<T>
降序排列- 对于建堆
less<T>
大根堆greater<T>
小根堆
基本使用
#include <iostream>
#include <queue>
using namespace std;
int main() {
vector<int> nums{ 3, 6, 2, 1, 12, 0, 4, 17, 2 };
priority_queue<int> heap1;
priority_queue<int, vector<int>, greater<int>> heap2(greater<int>(), nums);
for (auto elem : nums) heap1.push(elem);
cout << "默认为大根堆:" << endl;
while (!heap1.empty()){
cout << heap1.top() << " ";
heap1.pop();
}
printf("\n\n");
cout << "小根堆:" << endl;
while (!heap2.empty()) {
cout << heap2.top() << " ";
heap2.pop();
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix