一、基础知识
-
堆的概念:
① 是一棵完全二叉树
② 大堆:树中以及子树,父节点的值 >= 孩子节点的值
小堆:树中以及子树,孩子节点的值 >= 父亲节点的值
-
存储结构:数组存储
-
两组计算:
① 已知父节点下标 p a r e n t parent parent ,求左孩子 l e f t c h i l d leftchild leftchild 右孩子 r i g h t c h i l d rightchild rightchild 节点的下标: l e f t c h i l d = p a r e n t × 2 + 1 leftchild = parent × 2 +1 leftchild=parent×2+1, r i g h t c h i l d = p a r e n t × 2 + 2 rightchild =parent×2+2 rightchild=parent×2+2
② 已知某一孩子节点下标 c h i l d child child ,求父节点 p a r e n t parent parent 下标: p a r e n t = ( c h i l d − 1 ) / 2 parent =(child -1)/2 parent=(child−1)/2
二、接口
结构体
typedef int DataType;
typedef struct Heap
{
DataType* a;
int size;//放入数组元素的个数
int capacity;//总容量
}HP;
2.1 初始化
//初始化
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
2.2 销毁
//销毁
void HeapDeatroy(HP* hp)
{
assert(hp);
free(hp->a);
hp->size = hp->capacity = 0;
}
2.3 判断堆是否为空
//判断堆是否为空
bool HeapIsEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
2.4 判断堆是否为满
//判断堆是否为满
bool HeapIsFull(HP* hp)
{
assert(hp);
return hp->size == hp->capacity;
}
2.5 取堆顶元素
//取堆顶元素
DataType HeapTop(HP* hp)
{
assert(hp);
assert(!HeapIsEmpty(hp));
return hp->a[0];
}
2.6 入堆
//交换函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//入堆
void HeapPush(HP* hp,DataType x)
{
assert(hp);
//扩容
//Full同时包括size==0 capacity==0 的情况
if (HeapIsFull(hp))
{
int newCapacity = (hp->capacity == 0) ? 4 : hp->capacity * 2;
DataType* tmp = realloc(hp->a, sizeof(DataType) * newCapacity);
if (tmp == NULL)//开辟失败
{
printf("fail!");
exit(0);
}
hp->a = tmp;
hp->capacity = newCapacity;//赋值新容量
}
//将新节点加入堆中
hp->a[hp->size] = x;
hp->size++;
//向上调整
AdjustUp(hp->a, hp->size - 1);//调整数组内容和待调整的位置
}
void AdjustUp(DataType*a,int child)
{
//用公式计算父节点
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[parent] < a[child])
{
//交换
Swap(&a[parent], &a[child]);
//更换父节点和孩子节点的下标,继续比较
child = parent;
parent = (child - 1) / 2;
}
else
{
//此时当前路径(从根到叶子节点)满足大堆要求直接break
break;
}
}
}
2.7 出堆
出堆规定为:删掉顶节点
//交换函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//出堆
void HeapPop(HP* hp)
{
assert(hp);
assert(!HeapIsEmpty(hp));
//尾与顶交换
Swap(&hp->a[0], &hp->a[hp->size - 1]);
//删尾
hp->size--;
//向下调整
AdjustDown(hp->a, 0,hp->size);//调整数组内容和待调整的位置
}
void AdjustDown(DataType*a,int parent,int size)
{
//用公式计算左孩子的节点下标(可能没有右孩子)
int child = parent * 2 + 1;
while (child < size)
{
//变成大的那个孩子
//判断右孩子是否存在 + 判断右孩子是否大于左孩子
if (child + 1 < size && a[child] < a[child + 1])
{
++child;//切换成右孩子
}
if (a[child] > a[parent])
{
//交换
Swap(&a[parent], &a[child]);
//更换父节点和孩子节点的下标,继续比较
parent=child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
总结:
AdjustUp
中parent
不能越界,因为计算parent
(parent=(child-1)/2)时候永远大于等于0
AdjustDown
中child
可以越界
越界的区别决定了两者while循环的终止条件
AdjustUp
向上调整是沿着当前节点到根的这条路径,所以比大小是一对一
AdjustDown
向下调整是要在三角形区进行比较,所以比大小是一对一或一对多建堆的时间复杂度为: O ( N ) O(N) O(N)
向下/向上调整法时间复杂度: O ( N l o g N ) O(NlogN) O(NlogN)
三、应用
3.1 TopK问题
3.2 堆排序
思路:
如果利用堆实现排序(升序为例),可以假设方案一:
① 建立小堆
② 取出一个元素(当前堆中最小)
③ 重复①②再建立小堆,再取出一个;直到取完
方案一缺陷:
当每次取出一个元素,都会破坏本身排好的堆;时间复杂度 O ( N 2 ) O(N^2) O(N2)
方案二:
① 建立大堆
② 将堆顶元素(当前堆中最大)与堆尾元素互换位置
③ 将堆向下调整,在调整时不调整最后一个元素
④ 重复②③ 互换,去尾调整;直到都去完
方案二优势:时间复杂度 O ( N + N l o g N ) O(N+NlogN) O(N+NlogN)
//交换函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//打印函数
void Print(int *a,int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
void HeapSort(int *a,int size)
{
//方法一:向上调整法建立大堆
for (int i = 0; i < size; i++)
{
AdjustUp(a, i);
}
//方法二:向下调整法建立大堆
for (int i = (size - 1 - 1) / 2; i > 0; i--)
{
AdjustDown(a, i, size);
}
for (int i = size - 1; i > 0; i--)
{
//堆顶(此时堆中最大元素)与堆尾元素互换位置
Swap(&a[0], &a[i]);
//堆顶元素向下调整
AdjustDown(a, 0, i);
}
Print(a, size);//测试
}
int main()
{
int a[] = { 56,12,45,10,9,25 };
int size = sizeof(a) / sizeof(int);
HeapSort(a, size);
return 0;
}
说明:
向上调整法建立大堆,其实顺序不是从叶子往根,而是从根节点这一层(或根节点下一层)开始调整
向下调整法建立大堆,其实顺序不是从根往叶子,而是从叶子节点这一层(或叶子节点上一层)开始调整