文章目录
前言
- 在完成关于树与二叉树的基本知识的铺垫后,这里,我们进入二叉树的顺序结构的实现以及堆的实现
- 重点:堆的概念与实现
- 代码:C语言
一、二叉树的顺序结构
这里再提一下二叉树的顺序结构
- 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树(包括满二叉树)更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
二、堆的概念及结构
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
- 堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
三、堆的实现
- 堆的核心代码主要是:堆的向上调整已经堆的向下调整
- 堆的相关接口函数如下:
typedef int HPDateType;
typedef struct Heap
{
HPDateType* a;
int size;
int capacity;
}HP;
//交换数据
void Swap(HPDateType* p1, HPDateType* p2);
//堆的向上调整
void AdjustUp(HPDateType* a, int child);
//堆的向下调整
void AdjustDown(HPDateType* a, int size, int parent);
//堆的显示
void HeapPrint(HP* php);
//堆的初始化
void HeapInit(HP* php);
//堆的销毁
void HeapDestroy(HP* php);
//堆的插入
void HeapPush(HP* php, HPDateType x);
//堆的删除
void HeapPop(HP* php);
//获取堆顶元素
HPDateType HeapTop(HP* php);
//堆的判空
bool HeapEmpty(HP* php);
//堆的元素个数
int HeapSize(HP* php);
(一)、堆的向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
- 时间复杂度:O(N)
int array[] = {27,15,19,18,28,34,65,49,25,37};
void AdjustDown(HPDateType* a, int size, int parent)
{
int child = parent * 2 + 1;//先默认小的孩子是左孩子
while (child < size)
{
//if (child + 1 < size && a[child + 1] > a[child]) 大堆的向下调整
if (child+1 < size && a[child + 1] < a[child])// 小堆的向下调整
{
child++;
}
//if (a[child] > a[parent]) 大堆的向下调整
if (a[child] < a[parent])// 小堆的向下调整
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
(二)、堆的向上调整算法(堆的创建)
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
- 时间复杂度:O(N*logN)
int a[] = {1,5,3,8,7,6};
void AdjustUp(HPDateType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
//if (a[child] > a[parent])//大堆的向上调整
if (a[child] < a[parent])//小堆的向上调整
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
(三)、堆的插入(建堆过程)
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
void HeapPush(HP* php, HPDateType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDateType* tmp = (HPDateType*)realloc(php->a, sizeof(HPDateType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size-1);
}
(四)、堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&(php->a[0]), &(php->a[php->size - 1]));
php->size--;
AdjustDown(php->a, php->size, 0);
}
(五)、堆的初始化和销毁
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
(六)、获取堆顶元素
HPDateType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
(七)、交换两个元素和堆的显示
void Swap(HPDateType* p1, HPDateType* p2)
{
HPDateType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
(八)、堆的判空和堆的元素个数
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
四、堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
- 升序:建大堆
- 降序:建小堆
- 建堆推荐使用堆的向下调整来建立,其时间复杂度为O(N);而堆的向上调整时间复杂度为O(N*logN)
- 利用堆删除的思想进行排序
- 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
//升序 -- 建大堆
//降序 -- 建小堆
void HeapSort(int* a, int n)//时间复杂度O(N*logN)
{
//建堆方式1:O(N*logN)
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
//}建堆方式2:O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
五、TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下
- 用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
- 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
- 该方法空间效率比较高,是最优解,空间复杂度为O(K),而时间复杂度为O(k + (N-K)*logK)
- 若用排序的方法:时间复杂度O(N*logN),若N超级大,这种方法就不适合了
- 若用建立N个数的大堆:空间复杂度O(N),时间复杂度O(N + K*logN),同理,这里要考虑空间问题
下面进行该算法代码的模拟,用随机数产生10000个小于1000000的数据,储存到malloc开辟的空间中,并且将其中十个数据修改成比1000000大的数据,方便确认是否求解正确,在这些数据中,求前10大的数据。
void PrintTopK(int* a, int n, int k)
{
// 1. 建堆--用a中前k个元素建堆
int* KMinHeap = (int*)malloc(sizeof(int) * k);
assert(KMinHeap);
for (int i = 0; i < k; i++)
{
KMinHeap[i] = a[i];
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(KMinHeap, k, i);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int j = k; j < n; j++)
{
if (a[j] > KMinHeap[0])
{
KMinHeap[0] = a[j];
AdjustDown(KMinHeap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", KMinHeap[i]);
}
printf("\n");
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
assert(a);
srand(time(0));
for (int i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
PrintTopK(a, n, 10);
}
运行效果:
运用这种大思路确实能成功选出前10大的数据
学习记录:
- 本篇博客整理于2022.7.6
- 请多多指教🌹