目录
此处可能会有疑问,左右孩子的父节点计算为什么可以归纳为一个结论了?
一、 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
二、 堆的概念及结构
如果有一个关键码的集合k ={ },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:
且且i = 0,
2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树(完全二叉树是从满二叉树中最后一层连续删除若干结点(只能从最右侧删除)后得到的二叉树。)。
要满足且且的原因。
三、数组存储、顺序存储的规律
如果要用数组存储二叉树,那么必须要符合顺序存储中父子存储的规律
此处可能会有疑问,左右孩子的父节点计算为什么可以归纳为一个结论了?
- 一个左子节点索引 leftchild 和一个右子节点索引 rightchild,并且它们共享同一个父节点时,这意味着 rightchild = leftchild + 1。现在,如果你用上述公式来计算它们的父节点索引:
- 对于左子节点:parent = (leftchild - 1) / 2
- 对于右子节点:parent = (rightchild - 2) / 2但因为 rightchild = leftchild + 1,所以:
- parent = ((leftchild + 1) - 2) / 2
- parent = (leftchild - 1) / 2
- 并且由于(int)3/(int)2 = (int)1,这一向下取整的性质,所以在这一计算过程中不会出现浮点数的情况
- 你可以看到,无论你是从左子节点还是右子节点开始计算,你都得到了相同的父节点索引。
但是数组存储二叉树是有要求的。如果不符合该规律,那么得设置空节点去代替缺失的节点(因为要满足下标的规律才能方便查找),那么使用太多的空节点会造成空间的浪费。
结论:数组存储只适合完全二叉树和满二叉树
四、大小堆解释
堆并非是一定有序的 :左孩子与右孩子之间没有大小关系
- 大堆:在最大堆中,父节点的值总是大于或等于其子节点的值。但是,左孩子和右孩子之间并没有固定的大小关系。也就是说,左孩子可以大于、小于或等于右孩子,这都不会违反最大堆的定义。
- 也就是说,对于给定的节点i,其值应满足:array[i] >= array[2i + 1] 且 array[i] >= array[2i + 2]。
- 小堆:在最小堆中,父节点的值总是小于或等于其子节点的值。同样地,左孩子和右孩子之间的大小关系是不确定的。
- 也就是说,对于给定的节点i,其值应满足:array[i] <= array[2i + 1] 且 array[i] <= array[2i + 2]。
- 这里的“2i + 1”和“2i + 2”分别表示节点i的左子节点和右子节点在数组中的位置(假设数组是从0开始索引的)。
这种特性使得堆成为一种非常有效的数据结构,特别是在实现优先队列等应用中。堆可以在对数时间内完成插入和删除最大(或最小)元素的操作,这是因为它不需要保持整个结构的完全排序。
举个例子:
10
/ \
5 8
/ \ / \
2 3 6 7
在这个堆中,父节点的值总是大于或等于其子节点的值。但是,你可以看到左孩子和右孩子之间的大小关系是不一致的。例如,5的左孩子是2,右孩子是3,而8的左孩子是6,右孩子是7。这里并没有规定左孩子必须大于或小于右孩子。
五、大小堆的实现(向上和向下调整法)
void Swap(HPDataType* px,HPDataType* py)
{
*py ^= *px;
*px ^= *py;
*py ^= *px;
}
5.11向上调整法
目的:
当向堆中插入新元素时,为了维护堆的性质,需要对该元素进行向上调整。向上调整法就是从新插入的节点开始,通过与其父节点的比较和交换,确保该节点的值不大于(对于大根堆)或不小于(对于小根堆)其父节点的值。
步骤:
- 插入数据
- 与自己的父亲比较
- 交换/不交换
- 交换:孩子来到父亲位置,父亲来到自己父亲的位置。
判断条件:a[child] > a[parent]
结束循环条件:child > 0 (确保不是根节点)
时间复杂度:O(logN),其中N是堆中元素的数量。因为每次调整都涉及沿着树的一条路径向上移动,而树的深度为logN。
void AdjustUP(HPDataType* a, int n, int parent)参数的意义:
- HPDataType是一个自定义的数据类型,代表堆中存储的数据的类型int,a是一个指向HPDataType类型数组的指针,这个数组存储了堆中的所有元素。
- child表示当前要进行向上调整的节点的索引。在堆排序中,当我们向堆中插入一个新的元素时,这个新元素通常被放置在数组的末尾,然后可能需要通过向上调整来确保它满足堆的性质。child就是这个新插入元素的索引。
void AdjustUp(HPDataType* 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 = (parent - 1) / 2;
}
else {
break;
}
}
}
5.12向上调整法时间复杂度计算
可得高度与向上调整的关系
时间复杂度
5.21向下调整法
目的:
当从堆中移除元素(通常是堆顶元素)后,为了维护堆的性质,需要对剩余的元素进行重新调整。向下调整法就是从父节点开始,通过与其子节点的比较和交换,确保父节点的值不大于(对于大根堆)或不小于(对于小根堆)其子节点的值。
步骤:
- 删除堆顶元素
- 堆顶元素与最后一个元素交换
- 删除最后一个元素
- 堆顶元素与左右两个孩子(最小/最大的孩子比较)
- 判断交换/不交换
- 交换:父亲来到孩子位置,孩子来到自己孩子的位置
判断条件:child + 1 < n && a[child + 1] < a[child]
结束循环条件:child < n(确保左孩子存在)
时间复杂度:O(logN),其中N是堆中元素的数量。因为每次调整都涉及沿着树的一条路径向下移动,而树的深度为logN。
如何删除堆顶数据后插入数据?
向下调整法
如果直接挪动覆盖:操作的时间复杂度太大,关系太乱,不如重新建堆
向下调整法:
void AdjustDown(HPDataType* a, int n, int parent)参数的意义:
- HPDataType是一个自定义的数据类型,代表堆中存储的数据的类型int,a是一个指向HPDataType类型数组的指针,这个数组存储了堆中的所有元素。
- n表示堆中当前最后一个元素的下标。在堆排序的过程中,堆的大小可能会变化,因为我们会不断地从堆中移除元素。这个参数确保我们知道何时停止向下调整,即当child索引超过最后一个下标时。
- parent表示当前要调整的节点的索引。在堆排序中,当我们从堆中移除堆顶元素并与堆的最后一个元素交换时,我们需要对新的堆顶元素进行向下调整以确保堆的性质得到维护。parent就是这个需要进行调整的节点的索引。
// 向下调整算法(用于删除或构建堆时维护堆)
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1; // 获取左孩子索引
while (child < n) // 确保左孩子存在
{
// 如果右孩子存在且大于左孩子,则选择右孩子
if (child + 1 < n && a[child + 1] > a[child])
{
++child; // 选择右孩子
}
// 如果孩子节点大于父节点,则交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child; // 移动到孩子节点
child = parent * 2 + 1; // 获取新的左孩子索引
}
else {
break; // 不需要交换,退出循环
}
}
}
5.22向下调整法的时间复杂度计算
可得高度与向下调整次数的关系
可得时间复杂度:
六、堆排序的实现
有一个数列,请用堆排序升序排列
如果使用向下调整法建小堆,先把0视为堆根,0和3交换,然后当3视为堆根时:
所以要建大堆:
堆排序的时间复杂度与向上调整法建堆时差不多
子节点大于父节点时交换,建大堆,升序,保证父节点小于子节点
子节点小于父节点时交换,建小堆,降序,保证父节点大于子节点
代码如下:
#include<bits/stdc++.h>
using namespace std;
void Swap(int* px, int* py)
{
*py ^= *px;
*px ^= *py;
*py ^= *px;
}
- 该函数是堆排序的核心,用于调整堆的结构,确保其满足堆的性质(父节点小于其子节点,这是小根堆;反之则是大根堆。这里的代码是小根堆的实现)。
- 接收三个参数:一个整数数组a、数组的长度n以及要调整的父节点的索引parent。
- 首先,计算左孩子的索引child。
- 然后,通过循环,比较父节点和孩子节点的大小。如果存在右孩子且右孩子的值小于左孩子,则选择右孩子作为更小的孩子。
- 如果更小的孩子的值小于父节点,则交换它们的值,并将parent移动到新的位置,再次检查新的子节点。
- 如果子节点不小于父节点,则循环终止,调整完成。
// 向下调整算法(用于删除或构建堆时维护堆)
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1; // 获取左孩子索引
while (child < n) // 确保左孩子存在
{
// 如果右孩子存在且大于左孩子,则选择右孩子
if (child + 1 < n && a[child + 1] < a[child])
{
++child; // 选择右孩子
}
// 如果孩子节点大于父节点,则交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child; // 移动到孩子节点
child = parent * 2 + 1; // 获取新的左孩子索引
}
else {
break; // 不需要交换,退出循环
}
}
}
- 首先,对数组a建立一个小根堆。从最后一个非叶子节点开始(即索引为(n-1-1)/2的节点),调用AdjustDown函数调整每个子树。
- 一旦堆建立完毕,进入循环:将堆顶元素(数组的第一个元素)与堆的最后一个元素交换,然后重新调整剩下的元素为堆,但每次调整的范围都减小一个(即排除掉最后一个元素)。
- 循环继续,直到堆的大小为1,此时数组已经完全排序。
void HeapSort(int* a, int n)
{
// a数组直接建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
// O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);// 首尾交换
AdjustDown(a, end, 0);// 向下调整
--end;
}
}
这个函数首先通过AdjustDown函数将数组转化为最大堆。然后,它反复地将堆的根节点(即最大元素)与堆的最后一个节点交换,并重新调整堆,直到整个数组被排序。
int main()
{
int a[] = { 3,6,1,5,8,9,2,7,4,0 };
HeapSort(a, sizeof(a) / sizeof(int));
for (int i = 0; i < 10; i++)
printf("%d", a[i]);
return 0;
}
七、Top-k问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>
void Swap(int* px, int* py)
{
/*HPDataType tmp = *px;
*px = *py;
*py = tmp;*/
*py ^= *px;
*px ^= *py;
*py ^= *px;
}
void createNData()
{
// 造数据
int n = 10000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
int x = rand();
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
// 向下调整算法(用于删除或构建堆时维护堆)
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1; // 获取左孩子索引
while (child < n) // 确保左孩子存在
{
// 如果右孩子存在且大于左孩子,则选择右孩子
if (child + 1 < n && a[child + 1] < a[child])
{
++child; // 选择右孩子
}
// 如果孩子节点大于父节点,则交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child; // 移动到孩子节点
child = parent * 2 + 1; // 获取新的左孩子索引
}
else {
break; // 不需要交换,退出循环
}
}
}
void topk()
{
printf("请输入k:>");
int k = 0;
scanf("%d", &k);
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
int val = 0;
int* minheap = (int*)malloc(sizeof(int) * k);
if (minheap == NULL)
{
perror("malloc error");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &minheap[i]);
}
// 建k个数据的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minheap, k, i);
}
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{
// 读取剩余数据,比堆顶的值大,就替换他进堆
if (x > minheap[0])
{
minheap[0] = x;
AdjustDown(minheap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", minheap[i]);
}
fclose(fout);
}
int main()
{
//createNData();
topk();
return 0;
}
今天就先到这了!!!
看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。