堆是一种重要的数据结构,掌握堆的基本原理对理解和使用堆都有更好的效果。这里模拟实现堆基本组成要素。
函数声明
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int DataType;
typedef struct HeapNode
{
DataType* HeapArray;
int capacity;
int size;
}HPNode;
//创建并初始化一个堆节点
HPNode* HeapNodeCreat();
//打印堆
void HeapPrint(HPNode* php);
//入堆
void HeapPush(HPNode* php, DataType x);
//出堆
void HeapPop(HPNode* php);
//检查堆是否为空
bool IsEmpty(HPNode* php);
//返回堆顶元素
DataType HeapTop(HPNode* php);
//销毁堆
void HeapDestroy(HPNode* php);
//小堆,向上调整
void AdjustUp(DataType* php, int size);
//小堆,向下调整
void AdjustDown(DataType* php, int size, int parent);
//交换
void Swap(DataType* p1, DataType* p2);
创建并初始化堆
HPNode* HeapNodeCreat()
{
HPNode* php = (HPNode*)malloc(sizeof(HPNode));
if (php == NULL)
{
perror("malloc");
exit(-1);
}
php->HeapArray = NULL;
php->capacity = 0;
php->size = 0;
return php;
}
遍历打印堆元素
void HeapPrint(HPNode* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->HeapArray[i]);
}
printf("\n");
}
入堆
void HeapPush(HPNode* php, DataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 1 : 2 * php->capacity;
DataType* tmp = (DataType*)realloc(php->HeapArray, newcapacity * sizeof(DataType));
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
php->HeapArray = tmp;
php->capacity = newcapacity;
}
php->HeapArray[php->size] = x;
//向上调整
AdjustUp(php->HeapArray, php->size);
php->size++;
}
入堆需要借用向上调整函数,而堆的内容中最重要的就是向上调整和向下调整这两个函数,这是堆的核心部分,也是实现堆、用堆的原理解决问题的关键。这里每次入堆都需要检查数组容量,按照惯例,如果容量不够则扩2倍的容量。
向上调整
//小堆,向上调整
void AdjustUp(DataType* HeapArray, int size)
{
int child = size;
int parent = (child - 1) / 2;
while (child)
{
//如果子节点比父节点小,则交换两个节点的值
if (HeapArray[child] < HeapArray[parent])
{
Swap(&HeapArray[child], &HeapArray[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
这里的调整是适用于小堆,如果需要实现大堆,仅需要将调整函数中的<符号替换为>符号即可。而向上调整函数借用了“交换”函数Swap。
Swap函数
void Swap(DataType* p1, DataType* p2)
{
DataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
注意Swap函数的参数是两个地址,只有传地址才能正确地交换传入的两个值,因为函数形参的修改并不改变实参。
出堆
void HeapPop(HPNode* php)
{
assert(php);
assert(!IsEmpty(php));
//交换堆顶元素和堆底元素
php->HeapArray[0] = php->HeapArray[php->size - 1];
php->size--;
//向下调整
AdjustDown(php->HeapArray, php->size, 0);
}
同样地,出堆需要借用向下调整函数,与向上调整函数类似,向下调整函数十分重要,理解向下调整函数是实现出堆的前提。
向下调整
//小堆,向下调整,需要传入根的下标
void AdjustDown(DataType* HeapArray, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//找出两个子节点中最小的一个
if (child < size - 1 && HeapArray[child + 1] < HeapArray[child])
{
child++;
}
//如果父节点比小的那个子节点小,则交换
if (HeapArray[child] < HeapArray[parent])
{
Swap(&HeapArray[child], &HeapArray[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
这里,向下调整函数也是用于实现小堆,如果需要用于大堆的使用,仅需要将函数中的<符号替换为>符号即可。
检查堆是否为空
bool IsEmpty(HPNode* php)
{
assert(php);
return php->size == 0;
}
返回堆顶元素
DataType HeapTop(HPNode* php)
{
assert(php);
return php->HeapArray[0];
}
销毁堆
void HeapDestroy(HPNode* php)
{
assert(php);
free(php->HeapArray);
free(php);
}
以上就是堆的基本实现。
下面是堆排序的实现,时间复杂度O(nlogn),空间复杂度O(1)
目前为止,堆排序在理论上是最快的一类排序算法,利用以上关于堆的基本原理,可以实现堆排序这个算法。
1、给定一个数组,将数组排成升序或者降序。
2、如何在已有的数组空间上完成堆排序?
3、使用堆排序将数组排成升序,需要将数组处理为大堆还是小堆?
首先,应该明确,要是用堆排序排升序,需要先将数组处理为大堆。在大堆的基础上,进行堆排序,每次将最大的元素即堆顶元素放到最后并将堆的大小减1,再调整剩余元素组成的堆为大堆。重复这个过程直到剩余最后一个元素即可完成排序。
调整堆为大堆应该如何实现?
思路1:在现有数组上进行向上调整,若根节点是第一层,则从第二层开始进行向上调整。循环遍历,直到最后一个元素。
思路2:在现有数组上进行向下调整,若根是第一层,则向下调整从最后一个内点开始,循环遍历,直到根节点。最后一个内点是最后一层最后一个元素的父亲节点。
可以得出使用思路1建大堆的时间复杂度为O(nlogn),使用思路2建大堆的时间复杂度为O(n)。故这里采用思路二来模拟实现堆排序。
堆排序
//----------堆排序——升序----------
void HeapSort(DataType* arr, int size)
{
//将数组转换成大堆
int root = (size - 1 - 1) / 2;
while (root >= 0)
{
AdjustDown(arr, size, root);
root--;
}
//堆排序
int count = size;
while (count)
{
DataType tmp = arr[0];
arr[0] = arr[count - 1];
arr[count - 1] = tmp;
count--;
AdjustDown(arr, count, 0);
}
}
这里直接复用了上面的向上调整函数。每次迭代将堆顶元素与堆尾元素交换并将堆的大小减1,在向下调整,直到只剩下最后一个元素。
堆还有一个十分常见的用法,即最大Top-k问题。在数组中,选出最大的k个元素。这类问题可以使用堆来解决。
最大Top-k问题
void MaxTop_k(DataType* arr, int size, int k)
{
//创建一个大小为k的数组
DataType* HeapArr = (DataType*)malloc(k * sizeof(DataType));
if (HeapArr == NULL)
{
perror("malloc");
exit(-1);
}
//将k个数据放入数组中
for (int i = 0; i < k; i++)
{
HeapArr[i] = arr[i];
}
//将数组转换为小堆
for (int root = (k - 1 - 1) / 2; root >= 0; root--)
{
AdjustDown(HeapArr, k, root);
}
//剩下的数据,如果比堆顶的数据大,则从堆顶入堆
for (int i = k; i < 10000; i++)
{
if (HeapArr[0] < arr[i])
{
HeapArr[0] = arr[i];
AdjustDown(HeapArr, k, 0);
}
}
}
在解决这个问题前,需要明确一点,能否直接对n个数进行排序然后取前k个元素,或者每次就建立n个数的堆再取堆顶元素,迭代k次。显然,当数据量特别大的时候,这是不合理的,因为这会耗费大量的内存,比如100亿个整型元素中找出最大的前100个元素。所以这里需要变通一下。
思路如下:如果要实现最大的Top-k,那么需要先建一个k个元素的小堆。从数组中取前k个元素来建立即可。但是为什么要建立小堆?这里只需要知道,在这k个元素组成的小堆中最小的元素就是堆顶元素。每次拿数组中的其他元素与堆顶元素相比,如果比堆顶元素大,则替换堆顶元素,在进行向下调整。最后你会发现,堆顶的元素一定是比堆下面的元素小且比其余所有元素大的一个元素,即堆中就是前k个最大的元素了。
最大Top-k问题
//----------最大TOPk----------
void MaxTop_k(DataType* arr, int size, int k)
{
//创建一个大小为k的数组
DataType* HeapArr = (DataType*)malloc(k * sizeof(DataType));
if (HeapArr == NULL)
{
perror("malloc");
exit(-1);
}
//将k个数据放入数组中
for (int i = 0; i < k; i++)
{
HeapArr[i] = arr[i];
}
//将数组转换为小堆
for (int root = (k - 1 - 1) / 2; root >= 0; root--)
{
AdjustDown(HeapArr, k, root);
}
//剩下的数据,如果比堆顶的数据大,则从堆顶入堆
for (int i = k; i < size; i++)
{
if (HeapArr[0] < arr[i])
{
HeapArr[0] = arr[i];
AdjustDown(HeapArr, k, 0);
}
}
}
解决这个问题开辟了k个空间,对k个元素的小堆建立还是使用的向下调整。时间复杂度为O(k +(n - k) logk)。