堆 是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
将 n 个元素的序列 { k1,k2,…,kn },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足以下条件时称作堆

若将和此序列对应的一维数组( 即以一维数组做此序列的存储结构) 看成是 一个完全二叉树。
堆的性质
大根堆:
堆顶元素为序列中 n 个元素的最大值 (即完全二叉树中根节点值最大)
小根堆:
堆顶元素为序列中 n 个元素的最小值 (即完全二叉树中根节点值最小)

用到了树的性质 5
详见:二叉树博客
向下调整的前提:堆顶左右子树都同为大(小)堆,而堆顶不满足条件,
如给出一个数组,将其看作完全二叉树结构,以调整成小堆为例:将其向下调整直到满足堆的特性
int heap[] = {27,15,19,18,28,34,65,49,25,37};
代码:
// 向下调整
void AdjustDown(int arr[], int parent, int size){
int child = parent * 2 + 1; // 左孩子
while (child < size){ // 如果进入循环表示至少有左节点(完全二叉树)
// 1.找孩子中值较小的节点
if (child + 1 < size && arr[child + 1] < arr[child])
child += 1;
// 2. 检测双亲是否满足堆特性
if (arr[parent] > arr[child]){
Swap(&arr[parent], &arr[child]);
// 继续向下调整,直到所有节点满足堆特性
parent = child;
child = parent * 2 + 1;
}
else { return; }
}
}
在堆进行插入时,将新元素放入存储结构末尾,即插入堆尾(最后一个叶子节点之后),如果插入后不满足堆得特性那么需要从插入节点开始向上调整到满足堆

// 小堆的向上调整
void AdjustUp(int arr[], int size)
{
int child = size - 1;
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent]){
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else { return; }
}
}
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是让 所有非叶子节点依次下沉。
从倒数的第一个非叶子节点的子树开始向上调整,一直调整到根节点的树,就可以调整成堆。
建堆的时间复杂度为O(N)。
其时间复杂度推导过程:
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
例子:
如图中要删除72,先用堆中最后一个元素来35替换72,再将35下沉到合适位置,最后将叶子节点删除。“结点下沉”“下滤”
是一种利用 树 形数据结构进行 选择排序 的排序算法,通过堆来进行选择数据。
初始建堆所需 的比较次数较多,因此 记录数较少时不宜采用。
堆排序步骤:
建堆
● 升序排列:建大堆
● 降序排列:建小堆
堆排序,为什么升序排列要建大堆,降序排列要建小堆
利用堆删除思想来进行排序
排升序序列 建大堆
void HeapSort(int arr[], int size)
{
// 1. 建堆 升序--->大堆 降序--->小堆
for (int i = (size - 2) / 2; i >= 0; --i)
AdjustDown(arr, i, size);
// 2. 利用堆删除的思想排序
int end = size - 1;
while (end > 0){
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end); // 向下调整
--end;
}
}
typedef int HDataType;
// int(*Compare)(int left, int right);
typedef int(*COM)(int, int);
typedef struct Heap
{
HDataType* array;
int capacity;
int size;
COM Compare;
}Heap;
// 交换
void Swap(int* a, int* b){
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
// 向下调整
void AdjustDown(Heap* hp, int parent)
{
int child = parent * 2 + 1;
int size = hp->size;
while (child < size) {
// 找parent小的孩子
if (child + 1 < size && hp->Compare(hp->array[child + 1], hp->array[child]))
child += 1;
// 检测parent是否满足堆的特性
if (hp->Compare(hp->array[child], hp->array[parent])){
Swap(&hp->array[parent], &hp->array[child]);
// 需要继续往下调整
parent = child;
child = parent * 2 + 1;
}
else { return; }
}
}
// 向上调整
void AdjustUp(Heap* hp){
int child = hp->size - 1;
int parent = (child - 1) / 2;
while (child > 0){
if (hp->Compare(hp->array[child], hp->array[parent])){
Swap(&hp->array[child], &hp->array[parent]);
child = parent;
parent = (child - 1) / 2;
}
else { return; }
}
}
// 检查容量,扩容
static void CheckCapacity(Heap* hp){
assert(hp);
if (hp->size == hp->capacity){
int newCapacity = hp->capacity * 2;
// 1. 申请新空间
int* temp = (HDataType*)malloc(sizeof(HDataType)*newCapacity);
if (NULL == temp){
assert(0);
return;
}
// 2. 拷贝元素
for (int i = 0; i < hp->size; ++i)
temp[i] = hp->array[i];
// 3. 释放旧空间
free(hp->array);
hp->array = temp;
hp->capacity = newCapacity;
}
}
// 堆的构建
void HeapCreate(Heap* hp, HDataType a[], int n, COM com){
// 1. 初始化hp
hp->array = (HDataType*)malloc(sizeof(HDataType)*n);
if (NULL == hp->array){
assert(0);
return;
}
hp->capacity = n;
hp->size = 0;
// 2. 将数组中的元素拷贝到堆结构中
memcpy(hp->array, a, n*sizeof(HDataType));
hp->size = n;
hp->Compare = com;
for (int root = (n - 2) / 2; root >= 0; root--)
AdjustDown(hp, root);
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
if (hp->array)
{
free(hp->array);
hp->array = NULL;
hp->capacity = 0;
hp->size = 0;
}
}
// 堆的插入
void HeapPush(Heap* hp, HDataType x)
{
// 检测容量是否足够
CheckCapacity(hp);
// 先将元素放入到堆中
hp->array[hp->size] = x;
hp->size++;
// 将新插入的元素往上调整
AdjustUp(hp);
}
// 堆的删除
void HeapPop(Heap* hp)
{
if (HeapEmpty(hp)) return;
Swap(&hp->array[0], &hp->array[hp->size - 1]);
hp->size -= 1;
AdjustDown(hp, 0);
}
// 取堆顶的数据
HDataType HeapTop(Heap* hp)
{
assert(!HeapEmpty(hp));
return hp->array[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
assert(hp);
return 0 == hp->size;
}
// COM比较(建大堆、建小堆)
int Less(HDataType left, HDataType right)
{
return left < right;
}
int Greater(HDataType left, HDataType right)
{
return left > right;
}
TOP-K:指从大量数据(源数据)中获取最大(或最小)的K个数据。
即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下全部加载到内存中)。最佳的方式就是用堆来解决。
求解过程如下:
时间复杂度为O(n*log₂K),空间复杂度为K
//topk问题
void PrintTopK(int* arr, int n, int k)
{
// 1. 建堆--用a中前k个元素建堆(依次一个一个插入)
Heap hp;
HeapCreate(&hp);
for(int i = 0; i < k; ++i)
HeapPush(&hp,arr[i]);
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k; i < n; i++){
if (arr[i] > HeapTop(&hp)){
HeapPop(&hp);
HeapPush(&hp, arr[i]);
}
}
HeapPrint(&hp);
HeapDestroy(&hp); // 销毁堆
}
例题:
我们可以先取下标 0~k-1 的局部数组,用它来维护一个大小为K的数组,然后遍历后续的数字,进行比较后决定是否替换。这时候堆排序就派上用场了。我们可以将前K个数字建立为一个最小(大)堆,如果是要取最大的K个数,则在后续遍历中,将数字与最小堆的堆顶数字进行比较,若比它大,则进行替换,然后再重新调整为最大堆。整个过程直至所有数字遍历完为止。
时间复杂度为O(n*log₂K),空间复杂度为K。
更适合用 STL中提供的容器适配器优先级队列解决
官方手册:cplusplus.com/reference/queue/priority_queue/

优先级队列priority_queue是C++ STL库中提供的一种容器适配器,其底层容器默认为vector,又使用了堆算法将vector中元素构造成堆的结构;
默认情况下priority_queue是大堆
如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。
TOP-K例题:
剑指offer40:最小的K个数
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> res;
if(k == 0 || k > input.size()) return res;
priority_queue<int> pq;
for(int i : input){
if(pq.size() < k) pq.push(i);
else{
if(i < pq.top()){
pq.pop();
pq.push(i);
}
}
}
while(!pq.empty()){
res.push_back(pq.top());
pq.pop();
}
return res;
}
};
class Solution {
public:
/*
int findKthLargest(vector& nums, int k) {
sort(nums.begin(), nums.end(), greater()); // 降序排
return nums[k-1];
}
*/
int findKthLargest(vector<int>& nums, int k) {
// 区间构造优先级队列
priority_queue<int> pq(nums.begin(), nums.end());
// 将前k-1个元素删除
for(int i = 0; i < k-1; ++i)
pq.pop();
return pq.top(); //堆顶元素
}
};
堆的动图与部分图片来源于网络,以及《漫画算法》—程序员小灰