目录
一:前言
二叉树可用两种方式存储:顺序结构和链式结构,本文讲的是顺序结构。但是普通二叉树不适合使用数组这种顺序结构,因为可能会造成大量空间浪费。而完全二叉树适合用这种顺序结构。
我们一般将堆这种二叉树用顺序结构来存储。
注意:这里的堆是数据结构,不是操作系统中的堆,操作系统中的是管理内存的一块区域分段!
二:堆的性质和分类
堆分为大根堆(父节点大于等于孩子节点)和小根堆,或者大堆和小堆,名字不重要你理解就好。
堆的性质:1:堆中节点值总是不大于或不小于父亲节点的值。
2:堆总是一颗完全二叉树。
三:堆的实现
一:所需文件
我们一般用三个文件来完成堆的实现:头文件放堆的类型定义、接口函数声明、引用的头文件,heap.c放实现的代码,test.c用于测试
二:结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;//动态开辟的数组
int size;
int capacity;
}HP;
命名按你喜好就行
三:初始化和销毁
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
四:向上调整算法和向下调整算法
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) 这个不行 parent不会小于0
while (child > 0)
{
if (a[child] > a[parent])// 这个大于还是小于号看你要建什么堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
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[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
向下调整算法中child+1<n是为了是防止右孩子不存在
看代码逻辑大家肯定能明白,这就是在调整孩子和父亲的位置,只不过是用哪个算法的问题。
这里讲结论:插入用向上调整算法,建堆(向下的话时间复杂度O(N))和删除用向下调整算法。
五:插入
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
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(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
七:检查空不空和取堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
//
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
八:堆大小
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
四:应用
一:堆排序
void HeapSort(int* a, int n)
{
HP hp;
HeapInit(&hp);
// N*logN
for (int i = 0; i < n; ++i)
{
HeapPush(&hp, a[i]);
}
// N*logN
int i = 0;
while (!HeapEmpty(&hp))
{
int top = HeapTop(&hp);
a[i++] = top;
HeapPop(&hp);
}
HeapDestroy(&hp);
}
那么堆排序可不可以这样呢?可以是可以,但是有几个弊端:1.首先得有一个堆,太麻烦2.空间复杂度高+拷贝数据。所以可以这样
void HeapSort(int* a, int n)
{
// 升序 -- 建大堆
// 降序 -- 建小堆
// 建堆--向下调整建堆 --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;
}
}
这里n-1-1是从最后节点的父节点开始的意思
注意:升序建大堆,降序建大堆。
原因:就升序如果建小堆,选出一个最小的数放第0个位置,次小的放第1个,剩下的数看作堆,但是堆的关系都乱了,只能重新建堆,太麻烦。你可以试试。
二:TopK
就是选前k个最小或最大的数。
结论:如果选前K个最大的建小堆 否则反之
原因:就选K个最大的如果建大堆,那要popK次,如果数据非常大,那就很麻烦了。
改进:1.前K个数建小堆
2.后面的数入堆,如果比堆顶数据大就替换他进堆,最后堆的数据就是K个最大的
感谢大家看到这,祝大家共同进步!