堆的实现(堆的插入、堆的删除等)超级全
文章目录
- 堆的实现(堆的插入、堆的删除等)超级全
- 一、前期基础知识
- 1.树结构
- ①树的定义
- ②树的相关概念
- ③二叉树
- ④满二叉树和完全二叉树
- a.满二叉树
- b.完全二叉树
- ⑤二叉树的性质
- ⑥二叉树顺序结构的存储和链式结构的存储
- a.顺序结构存储
- b.链式结构存储
- 2.堆结构
- 二、堆结构/二叉树顺序结构存储的实现
- 1.堆的初始化
- 2.堆的销毁
- 3.堆的插入
- 4.堆的删除
- 5.取堆顶数据
- 6.堆的数据个数
- 7.堆的判空
一、前期基础知识
1.树结构
①树的定义
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
②树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6。
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点。
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点。
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6。
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4。
n个结点的树,有n-1条边。
一棵树有以下三部分组成
根节点,无前驱结点。
叶结点,度为0,又称终端结点
非终端节点,分支节点,度不为0
一棵树是由根节点和n颗子树构成的,子树一定不能相交,除了根节点外,每一个结点都有且仅有一个父节点。
如果不是树结构,那就是图结构。
③二叉树
每一个结点的度不一定都是2,整个二叉树里不存在度大于2的结点。
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
④满二叉树和完全二叉树
a.满二叉树
满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2 ^ k - 1 ,则它就是满二叉树。
b.完全二叉树
完全二叉树
前(n - 1)层是满的
最后一层不满,但是从左到右必须连续排布,最少1个,最多2 ^ (n - 1)个。
⑤二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2 ^ (i - 1)个结点.
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2 ^ h - 1
- 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有n0 = n2 + 1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log2底(n + 1)。(ps: 是log以2为底,n+1为对数)
- 将二叉树的每一个结点按层次从0到n编号:
0
1 2
3 4 5 6
7 8 (7,8为3的子节点)
举个例子,这就是一个完全二叉树,0 = (1 - 1) / 2, 0 = (2 - 1) / 2,所以parent = (child - 1) / 2, leftchild = parent * 2 + 1, rightchild = leftchild + 1。
但是注意,一定要在范围里。
⑥二叉树顺序结构的存储和链式结构的存储
a.顺序结构存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
b.链式结构存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,二叉链就是两个指针+数据域,三叉链就是两个指针+指向父节点+数据域,我们暂时先不做重点讲解,后续笔者会进行讲解。
2.堆结构
1.一段连续的数组数据看作完全二叉树
2.必须是小堆或者大堆
小堆:任意一个父节点小于等于子节点
大堆:任意一个父节点大于等于子节点
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
大家不妨再思考一下,数据结构的目的到底是什么,就是为了方便我们在内存中管理数据,再加上一些可以实现对这段数据进行操作的接口函数来实现我们的目的。
二、堆结构/二叉树顺序结构存储的实现
1.堆的初始化
void HPIni(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
2.堆的销毁
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
3.堆的插入
void Swap(HPDataType* p1, HPDataType* p2)
{
assert(p1 && p2);
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void HPAdjustUp(HPDataType* p, int child)
{
assert(p);
int parent = (child - 1) / 2;
while (child > 0)
{
if (p[child] < p[parent])
{
Swap(( & p[child]), ( & p[parent]));
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * (php->capacity);
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
HPAdjustUp(php->a, (php->size - 1));
}
当你插入一个新的数据到堆里后,一定要记得你的结构实际是一个二叉树,而且以小堆为例,如果你插入的数据,比这个结点的父节点小,那就需要向上调整。
4.堆的删除
void HPAdjustDown(HP* p, int parent)
{
assert(p);
int child = parent * 2 + 1;//使用假设法
while (child < (p->size))
{
if ((child + 1 < p->size) && (p->a[child + 1] < p->a[child]))
//这个真的很重要,值得反复思考,进行交换
{
child = child + 1;
}
if (p->a[child] < p->a[parent])
{
Swap(&(p->a[child]), &(p->a[parent]));
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
void HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&(php->a[0]), &(php->a[(php->size) - 1]));
php->size--;
HPAdjustDown(php, 0);
}
相信大家一定会有一些问题,因为堆的删除,是删除堆顶元素,大家可能会疑问,为什么不能直接头删。
原因有以下两点:
1.顺序表时间复杂度是O(N),过于复杂。
2.如果头删之后,整个数组是要向前挪一位的,整棵树的结构就全部改变了,父子关系,大小关系全部乱了,需要重新改。
所以我们重新选择一个方法,就是我们将堆顶元素和最后一个元素互换,再尾删,这样可以保证两颗子树从上到下的整个顺序都没有被改变,向下调整就只需要进入一颗子树继续调整就好啦。
5.取堆顶数据
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);//使用size
return php->a[0];
}
6.堆的数据个数
int HPSize(HP* php)
{
assert(php);
return (php->size);
}
7.堆的判空
bool HPEmpty(HP* php)
{
assert(php);
/*if (php->size == 0)
{
return true;
}
else
{
return false;
}*/
return (php->size == 0);
}