二叉树的顺序结构及堆的概念及结构实现
二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆的概念及结构
在这里我们先学习一下堆,堆是一种特殊的二叉树形式
如果有一个关键码的集合K = { N1,N2 ,N3 ,…, },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: Ni<= N(2i+1)且 Ni<= N(2i+2)( Ni>= N(2i+1)且Ni >=N(2i+2) ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
★堆中某个节点的值总是不大于或不小于其父节点的值;
★堆总是一棵完全二叉树。
堆的实现
1、堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int arr[] = {27,15,19,18,28,34,65,49,25,37};
后面在讲到堆的插入接口函数时,还会提到向上调整算法
2、堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
此时调换1和8的位置时,8的左子树堆结构被破坏,所以在每一次发生元素交换的时候,都需要递归调用重新构造堆的结构
最后构造的大堆:8,7,6,5,1,3
3、堆的插入
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
★将堆顶元素和堆中最后一个元素进行交换
★删除最后一个元素
★将堆顶的元素向下调整,直到满足堆特性为止
4、堆的实现
这里堆的实现我们使用的是顺序表结构
堆的结构体及接口定义
// 大堆
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void AdjustUp(int* a, int child);//向上调整
void AdjustDown(int* a, int n, int parent);//向下调整
void Swap(HPDataType* px, HPDataType* py);//交换函数
void HeapInit(HP* hp);//堆的初始化
void HeapDestroy(HP* hp);// 堆的销毁
void HeapPush(HP* hp, HPDataType x);// 堆的插入
void HeapPop(HP* hp);// 堆的删除
HPDataType HeapTop(HP* hp);// 取堆顶的数据
void HeapPrint(HP* hp);//堆的打印
bool HeapEmpty(HP* hp);// 堆的判空
int HeapSize(HP* hp);// 堆的数据个数
堆的接口实现
交换函数(Swap)
代码如下:
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
这里的交换函数不是接口函数,仅为了方便其他接口函数调用
向上调整(AdjustUp)
代码如下:
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;
}
}
}
这里的向上调整函数就是指定一个元素与其父亲比较,如果孩子小于父亲,就交换
常用于小堆的插入与堆排序。
向下调整(AdjustDown)
代码如下:
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;
}
}
}
这里的向下调整函数就是指定一个元素与其父亲比较,如果孩子小于父亲,就交换
同样常用于小堆的插入与堆排序,和向上调整的不同的就是方向。
堆的初始化(HeapInit)
代码如下:
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
堆的销毁(HeapDestroy)
代码如下:
void HeapDestroy(HP* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
堆的插入(HeapPush)
代码如下:
void HeapPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
size_t newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = realloc(hp->a, sizeof(HPDataType)*newCapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = newCapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
堆的插入,首先创建内存空间,然后插入元素,size++就不说了;
重点讲一下这里的向上调整,因为是小数往上调,所以这里的调用是用于小堆的建立;
如果要改成大堆,那么就要将向上调整函数的判断改为大于;
修改后代码如下:
if (a[child] > a[parent])
堆的删除(HeapPop)
代码如下:
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);
}
堆的删除是删除堆顶的元素,但是需要注意的是并不是直接将堆顶元素直接删除
而是将堆顶元素和最后一个元素交换,再进行size–
再将换上去的最后的元素重新向下调整到相应位置
这样做的目的是为了保持堆的基本结构,否则可能堆结构可能不成立。
取堆顶的数据(HeapTop)
代码如下:
HPDataType HeapTop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->a[0];
}
直接返回第一个元素即可
堆的打印(HeapPrint)
代码如下:
void HeapPrint(HP* hp)
{
for (int i = 0; i < hp->size; ++i)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
常用的for循环对顺序表进行元素遍历逐个打印
堆的判空(HeapEmpty)
代码如下:
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
这里使用的是bool值,当然你也可以使用int类型
堆的数据个数(HeapSize)
代码如下:
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
堆排序的简易例子
代码如下:
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
for (int end = n - 1; end > 0; --end)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
}
}
int main()
{
int a[] = { 70, 56, 30, 25, 15, 10, 75, 33, 50, 69 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
这里我们主要使用向下调整的方法来实现,因为上面对堆的删除是用于小堆
所以这里调用向下调整后,该数组为降序,排序后打印如下:
75 70 69 56 50 33 30 25 15 10
如果要进行升序排序,我们只需将向下调整函数的部分符号修改即可
修改如下:
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;
}
}
}
排序后打印如下:
10 15 25 30 33 50 56 69 70 75
结语
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!