堆
堆 - 是一种完全二叉树
堆的概念及结构
如果有一个关键码的集合K = { k0, k1,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
大堆:树中一个树及子树中,任何一个父亲都大于等于孩子
小堆:树中一个树及子树中,任何一个父亲都小于等于孩子
数据结构和操作系统都有栈和堆,数据结构的堆是完全二叉树 他们二者没有关系,他们是两个学科中不同物种
堆的性质
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树
堆:
逻辑结构:我们想象出来的 ->完全二叉树
物理结构:实实在在在内存中存储的结构 ->数组
习题练习
1.下列关键字序列为堆的是:()
A 100,60,70,50,32,65->
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32
堆的实现
运用到二叉树顺序存储时的结论:
这里我们以建大堆为例子
大堆:树中一个树及子树中,任何一个父亲都大于等于孩子
堆的结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
初始化
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
释放
**堆是用顺序表存储 **
释放动态开辟的数组,然后把指针置空,防止造成野指针
void HeapDestory(HP* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
插入数据
空间不够->扩容
HeapPush要求,插入数据之后还是堆(大堆/小堆保持不变)
在数组末尾插入数据:堆插入数据堆其它结点没有影响,只是可能会影响从他到根节点路径上节点关系
插入数据会影响原来堆的结构,树已经是堆了 然后插入一个元素后 这个元素就变成了树中的最后一个叶子 这个叶子节点可能会破坏堆的特性 那就得往上调整
void HeapPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
hp->a = tmp;
hp->capacity = newcapacity;
}
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
交换两个数
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
向上调整算法
向上调整有固定的路径可循,从该结点开始到根节点
循环结束条件:
1.插入的结点比 从它到根结点的所有结点的值都要大,则要一直调整,child调到根跳出循环
或者:
2.插入的结点比 从它到根结点的部分结点的值大,当调整到某一个位置,孩子结点的值比父亲结点的值小,就不用调整了,走else跳出循环
void AdjustUp(int* a,int child)
{
assert(a);
int parent = (child - 1) / 2;
while(child > 0)
{
if(a[child] > a[parent])
{
Swap(&a[child],&a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
向下调整算法
建小堆前提:建根结点的左右子树都是小堆
向下调整没有固定的路径可循,和左孩子交换还是和右孩子交换?
->由于是向下调整建小堆,所以和左右孩子中小的孩子交换
循环结束条件
1.调整到叶子了(度为0的结点)
or :2.调整到父亲结点的值比孩子的值小
由于不知道左孩子大还是右孩子大,所以可以假设左孩子更大,如果不是 : 左孩子+1就是右孩子
(左孩子:parent * 2 + 1 右孩子: parent * 2 + 2 ,二者坐标差1)
注意:要防止最后一层只有左孩子没有右孩子导致越界的情况:要判断:child + 1 < n ==> 右孩子下标要在数组范围内
如果child+1 >= n说明没有右孩子
void AdjustDown(int* a,int n , int parent)
{
assert(a);
int child = parent * 2 + 1;
while(child < n)
{
if(child+1<n && a[child+1] < a[child])
{
child +=1;
}
if(a[child] < a[parent])
{
Swap(&a[child],&a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
打印
直接遍历打印数组元素即可
void HeapPrint(HP* hp)
{
assert(hp);
int i = 0;
for(i = 0;i<hp->size;i++)
{
printf("%d ",hp->a[i]);
}
printf("\n");
}
删除堆顶元素
==错误想法:==直接把后面的元素往前移动覆盖
导致的后果:堆的结构变了,不一定是原来的大堆/小堆 而且往前覆盖时间复杂度:0(N)
如:原来是大堆:
优质思路:
- 数组的最后一个元素和第一个元素交换,
- 然后去掉数组最后一个元素(这样就去掉了原来堆顶的元素),这样不会改变原来堆的结构
- 然后就可以对根结点使用向下调整法 进行调整(向下调整法前提:左右子树是大堆/小堆)
除次之外:出堆顶数据,要抱枕堆中至少有一个元素(堆不为空)
void HeapPop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0],&hp->a[hp->size-1]);
hp->size--;
AdjustDown(hp->a,hp->size,0);
}
得到堆顶数据
返回类型为我们自己typedef重新定义的类型
要保证堆中有数据
HPDataType HeapTop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->a[0];
}
堆是否为空
size标志的就是堆中元素个数,如果为0,说明堆为空
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
堆中元素个数
size标志的就是堆中元素个数,指向数组最后一个元素的下一个位置
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
什么时候用向上调整,什么时候用向下调整
首先明确一点:向上调整和向下调整都可以构建堆
删除堆顶数据:
- 先交换首尾元素
- 去掉末尾元素(原来的堆顶元素)
- 交换后,最上面的根位置可能导致整棵树不满足堆的结构,但是它的左子树和右子树都是堆
- 所以就可以使用向下调整构建成堆
向下调整前提:左右子树都是堆(大堆/小堆) 要建什么堆,就要保证左右子树是什么堆
删除:从根节结点调到叶子 / 调到满足堆结构, 所以用的是向下调整
插入元素:
- 在数组最后一个位置插入元素。
- 树原本已经是堆了,然后插入一个元素后 这个元素就变成了树中的最后一个叶子
- 这个叶子结点可能会破坏堆的特性
- 所以需要向上调整
插入:从叶子调到根结点/调到满足堆,所以用向上调整
向上调整函数接口:
void AdjustUp(int* a,int child)
向下调整函数接口:
void AdjustDown(int* a,int n , int parent)
- a:要调整的数组
- n:堆中元素个数(认为堆中有多少个元素)
- parent:从哪个位置往下调整
向上/向下调整的时间复杂度分析
以向下调整为例
最坏情况:调整高度次,即从根结点一直调整到叶子
以向上调整为例
最坏情况:调整高度次,即从叶子结点一直调整到根
所以一次调整的时间复杂度为:O(高度)
共有N个元素,则总的时间复杂度是:0(高度*N)
完全二叉树高度分析:
完全二叉树结点范围:[2(h-1),2h -1 ]
2^(h -1) = N ==> h= logN +1 2^h -1 = N ==> h = log (N+1) (log 以2为底的
完全二叉树高度范围:[ logN +1 , log (N+1) ]
结论:
+1可以忽略, N:结点个数
所以调整一次:O(logN)
调整N次: O(N*logN)
建堆的时间复杂度分析
堆是完全二叉树,而满二叉树也是完全二叉树,为了简化,使用满二叉树来证明(时间复杂度本来看的 就是近似值,多几个节点不影响最终结果):
向下调整建堆 - O(N)
例子:
以向下调整为例子
最坏情况:每一层的每一个结点都要向下调整到叶子结点 从倒数第二层开始调,叶子结点(最后一层)不需要调整
如何求出T(n) ->错位相减法
因为有n个结点,高度为h 2^h - 1 = n ->h = log(n+1)
代入T(n)得到:
因此:向下调整建堆的时间复杂度为O(N)。
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"Heap.h"
void TEST1()
{
HP hp;
HeapInit(&hp);
int a[] = {9,8,9,0,2,4,1,7,5 };
int i = 0;
int sz = sizeof(a) / sizeof(a[0]);
for (i = 0; i < sz; i++)
{
HeapPush(&hp, a[i]);
HeapPrint(&hp);
printf("HeapSize = %d\n", HeapSize(&hp));
}
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
printf("HeapSize = %d\n", HeapSize(&hp));
printf("\n");
}
HeapDestory(&hp);
}
int main()
{
TEST1();
}
Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"Heap.h"
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
void HeapDestory(HP* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
void AdjustUp(int* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
hp->a = tmp;
hp->capacity = newcapacity;
}
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
void AdjustDown(int* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] < a[child])
{
child += 1;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPrint(HP* hp)
{
assert(hp);
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
void HeapPop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
HPDataType HeapTop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->a[0];
}
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* hp);
void HeapDestory(HP* hp);
void HeapPrint(HP* hp);
HPDataType HeapTop(HP* hp);
void HeapPop(HP* hp);
void HeapPush(HP* hp,HPDataType x);
bool HeapEmpty(HP* hp);
int HeapSize(HP* hp);
|