树是一种非线性的数据结构,它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像是一颗倒挂的树,也就是说它是根朝上,而叶朝下的。
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保持值域,也要保存节点和节点的关系,实际中树有很多种表示方式如:双亲表示法、孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。这里着重介绍最常用的孩子兄弟表示法(左孩子右兄弟)。
typedef int DataType; struct Node { struct Node* child; //第一个孩子节点 struct Node* brother; //指向其下一个兄弟节点 DataType data; //其中的数据域 };
一颗二叉树是节点的一个有限集合,该集合:
1.或者为空
2.或者由一个节点加上两颗分别被称为左子树和右子树的二叉树组成
从上图可以看出:
1.二叉树不存在度大于2的节点
2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
1.若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个节点。
2.若规定根节点的层数为1,则深度为h的二叉树的最大根节点树是(2^h)-1。
3.对任何一颗二叉树,如果度为0其节点个数为n0,度为2的分支节点个数为n2,则有n0=n2+1。
4.若规定根节点的层数为1,具有n个根节点的满二叉树的深度,h=log2(n+1)(log以2为底,n+1为对数)
5.对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的节点有:1、若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2、2i+1=n否则无左孩子
3、2i+1=n否则无右孩子
二叉树一般可以使用两种结构存储,一种顺序存储,一种链式存储
1、顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上一颗二叉树。
2、链式存储
二叉树的链式存储结构是指,用链来表示一颗二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的链式节点的存储地址。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
如果有一个关键码的集合K={k0,k1,k2,…,k(n-1)},把它的所有元素按完全二叉树的顺序存储方式在一个一维数组中,并满足:Ki<=K2i+1且Ki<=K2i+2(Ki>=K2i+1且Ki>=K2i+2)i=0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
在.c文件中用一个结构体创建一个堆
//创建一个小根堆,小根堆的具体实现在插入函数中引用的向上调整函数,以此不断插入就能创建堆 typedef int HPDataType; typedef struct Heap { HPDataType* a; //堆用数组实现,创建一个整形数组用来存放堆的数据 int size; //堆当前的实际大小 int capacity; //堆的容量 }HP;
- 结构体中包含了实现堆的数组a、堆的当前的实际大小与堆的容量。
- 因为堆可以插入与删除,所以堆的大小要求是可变的,即要求实现堆的数组是动态开辟的,动态开辟的空间用一个指针存储其首地址。
void HeapInit(HP* php) { assert(php); php->a = NULL; php->size = php->capacity = 0; }
- 在堆中还没有任何数据时,数组中也没有任何数据,即还没有动态开辟空间,将指针指向一个空指针。
- 此时,堆的当前大小与总容量都是0。
插入x以后依然保持堆的形状
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("ralloc fail"); exit(-1); } php->a = tmp; php->capacity = newCapacity; } //扩容完后将数据插入到堆的最后一个位置(数组的最后一个位置) php->a[php->size] = x; php->size++; //将插入的数据根据情况向上调整 AdjustUp(php->a, php->size - 1); //将数组和数组中最后一个数的下标传过去 }
- 先对接受的形参判空
- 插入之前先检查堆中是否还有多余的空间以供插入。当前大小与总容量相等时,说明堆已经满了,需要动态开辟空间。
- 堆满了有两种情况,一种是堆刚刚初始化,还没有往里面储存任何内容,这时可以先给他四个整形大小的空间(动态开辟四个整形空间的大小)(也可以多给点,看自己);另一种是,已经储存了数据,当时数组已经没有多余的空间,这时动态开辟原有空间两倍大小的空间(为什么是两倍,因为如果开多了,空间会浪费,如果开小了,需要频繁动态开辟空间,造成空间碎片化)。
- 将动态开辟空间的首地址指向数组a
- 空间足够以后,将需要插入的数据插入到数组的最后一个位置上,即堆的最后子节点,这样不会破坏堆整体的父子关系。
- 创建一个向上调整函数,将插入的子节点与其父节点进行比较,根据是大根堆还是小根堆,将子节点与父节点位置关系进行调整,以维持堆的形状。
向上调整函数
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 (child > 0) //孩子的下标等于0时,说明堆从最后一个数一路向上比较,已经到达堆顶了 { //小根堆,任意孩子的值要大于父节点的值,不是的话则要向上调整 if (a[child] < a[parent]) //改为>,这个堆结构就成为大堆了 { Swap(&a[child], &a[parent]); //修正父亲与孩子的下标,通过循环不断比较,直到成为堆的形状 child = parent; parent = (child - 1) / 2; } else { break; } } }
- 向上调整函数将整个数组与数组最后一个下标传过去,即向上调整函数是将堆中最后的元素与其父节点进行比较,依次向上调整。
- 当插入一个节点后,找到其父节点,子节点与父节点的坐标关系如下:
leftchild = parent * 2 + 1,左孩子的数组下标都是奇数
rightchild = parent * 2 + 2,右孩子的数组下标都是偶数
parent = (child - 1)/2,对于由子节点找父节点,不论是左孩子还是右孩子,都是这个公式,因为对于同一父节点的奇数子节点与偶数子节点-1除2得到的结果都是一样的。
- 对于小根堆来说,父节点小于等于子节点,如果插入的子节点小于其父节点,将子节点与其父节点进行交换,交换完毕后,修正子节点与父节点的下标,通过循环,不断往堆顶修正,直到循环终止。
- 循环终止条件,当子节点等于0了,说明已经向上调整到堆顶了。或者不满足修正条件,直接break出来。
在主函数中调用插入函数
int main() { //int a[] = { 15,18,19,25,28,34,65,49,27,37 }; int a[] = { 65,100,70,32,50,60 }; HP hp; HeapInit(&hp); int i = 0; for (i = 0; i < sizeof(a) / sizeof(int); i++) { HeapPush(&hp, a[i]); //依次插入,插入完成后就是堆的形状 } return 0; }
调试结果
- 创建一个堆 HP hp;,将堆 hp初始化HeapInit(&hp);
- 给定一组数{ 65,100,70,32,50,60 },根据插入函数依次插入,最终得到堆的形状{ 32,50,60,100,65,70 }
- 这个插入函数随着树的插入,堆也随之构建完成。
//判空函数 bool HeapEmpty(HP* php) { assert(php); return php->size == 0; } void HeapPrint(HP* php) { assert(php); assert(!HeapEmpty(php)); int i = 0; for (i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); }
- 堆的打印就是依次将数组中的元素遍历一遍,访问一个数打印一个数,知道数组访问完毕。
- 堆的打印首先堆传过来的指针进行断言,防止传过来的是空指针。
- 再对传过来的堆进行判空,如果堆中没有储存元素,也就不用再进行接下来的打印了。
//向下调整函数 void AdjustDown(HPDataType* a, int n, int parent) { int minChild = parent * 2 + 1; //先默认左边的孩子是整个小根堆中次小的孩子 while (minChild < n) { //与右孩子比较一下,找出小的那个孩子的下标 if (minChild + 1 < n && a[minChild + 1] < a[minChild]) { minChild++; } //找到次小的孩子后将其与父节点比较 if (a[minChild] < a[parent]) { Swap(&a[minChild], &a[parent]); //修正父亲与孩子的下标,通过循环不断比较,直到成为堆的形状 parent = minChild; minChild = parent * 2 + 1; } else { break; } } } //删除堆顶的元素 --找次大或者次小,小堆找次小,大堆找次大 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); }
- 删除堆顶元素后,要保证堆依然要保持堆的形状,所以不能将数组中的第一个元素删除掉,然后将数组的第二个元素提到前面来作为根节点,这样会将原本的兄弟关系变成父子关系,打乱堆的形状。将打乱的堆重新插入一遍回复堆的形状非常的浪费时间。
- 删除堆顶元素最好的方式是将堆顶元素与最后一个元素进行交换,交换后,将最后一个元素删除,这时堆顶元素就被删除了。堆顶元素被删除后,其他层级的父子关系并没有被打乱,只有新调换上来的堆顶元素不符合堆,这时创建一个现下调整函数,对于小根堆来说,找到原来堆中的次小元素,由其来作为堆顶元素(对于大根堆来说,找到原来堆中的次大元素,由其来作为堆顶元素)。
- AdjustDown(php->a, php->size, 0); 向下调整函数,将数组和数组的大小,以及堆顶元素的坐标传过去。
- 向下调整函数一个重要的功能就是找到次小或次大的节点,然后将其作为根节点。这段代码是小根堆,所以向下找次小的孩子。
- 先默认为左孩子是整个小根堆中次小的节点,将左孩子与右孩子比较一下,如果右孩子比左孩子小,就将其值赋给次小孩子。找到次小孩子后,将其与父节点进行比较,将次小孩子换到根节点上位置去。然后将父节点与子节点的坐标进行修正,通过循环,不断将父节点与子节点进行交换,直到循环终止。
- 循环终止条件,当子节点的坐标等于堆中的当前实际大小时,说明已经循环已经来到最后,所有的都已经进行比较过了。循环终止的另一条件是,当子节点与父节点不用交换,直接break出来。
在主函数中调用删除堆顶元素函数
int main() { //int a[] = { 15,18,19,25,28,34,65,49,27,37 }; int a[] = { 65,100,70,32,50,60 }; HP hp; HeapInit(&hp); int i = 0; for (i = 0; i < sizeof(a) / sizeof(int); i++) { HeapPush(&hp, a[i]); //依次插入,插入完成后就是堆的形状 } HeapPrint(&hp); HeapPop(&hp); HeapPrint(&hp); return 0; }
- 从打印结果来看,堆顶元素32被删除了,堆的最终形状依然维持着小根堆的形状。
HPDataType HeapTop(HP* php) { assert(php); assert(!HeapEmpty(php)); return php->a[0]; }
- 直接将堆顶元素return就行
在主函数中调用返回堆顶元素函数
int main() { //int a[] = { 15,18,19,25,28,34,65,49,27,37 }; int a[] = { 65,100,70,32,50,60 }; HP hp; HeapInit(&hp); int i = 0; for (i = 0; i < sizeof(a) / sizeof(int); i++) { HeapPush(&hp, a[i]); //依次插入,插入完成后就是堆的形状 } HeapPrint(&hp); HeapPop(&hp); HeapPrint(&hp); printf("%d ",HeapTop(&hp)); return 0; }
- 堆顶元素50被返回。
void HeapDestroy(HP* php) { assert(php); assert(!HeapEmpty(php)); free(php->a); php->a = NULL; php->size = 0; php->capacity = 0; }
- 因为储存堆中元素的数组是动态开辟的,所以将数组free掉,再直接置空。
- 数组为空后,再将表示堆当前的实际大小与堆的容量赋值为0.
在主函数中调用销毁堆的函数
int main() { //int a[] = { 15,18,19,25,28,34,65,49,27,37 }; int a[] = { 65,100,70,32,50,60 }; HP hp; HeapInit(&hp); int i = 0; for (i = 0; i < sizeof(a) / sizeof(int); i++) { HeapPush(&hp, a[i]); //依次插入,插入完成后就是堆的形状 } HeapPrint(&hp); HeapDestroy(&hp); HeapPrint(&hp); return 0; }
- 可以看到,当堆被销毁后,再次调用打印堆函数,打印函数中断言出现警告,说明堆中已无元素。
//返回堆当前存储数据的个数 int HeapSize(HP* php) { assert(php); return php->size; }
在主函数中调用
int main() { //int a[] = { 15,18,19,25,28,34,65,49,27,37 }; int a[] = { 65,100,70,32,50,60 }; HP hp; HeapInit(&hp); int i = 0; for (i = 0; i < sizeof(a) / sizeof(int); i++) { HeapPush(&hp, a[i]); //依次插入,插入完成后就是堆的形状 } HeapPrint(&hp); int z = HeapSize(&hp); printf("%d\n", z); return 0; }
Heap.h部分:包含了库函数的头文件以及自定义函数的声明。
#pragma once
#include
#include
#include
#include
//堆是完全二叉树
//堆的二叉树用数组表示,在数组的顺序从上至下,从左至右
//小根堆,任何节点的值小于等于孩子的值
//大根堆,任何节点的值大于等于孩子的值
//数组下标计算父子关系的公式
//leftchild = parent*2 + 1 左孩子的数组下标都是奇数
//rightchild = parent*2 + 2 右孩子的数组下标都是偶数
//parent = (child - 1)/2
//创建一个小根堆,小根堆的具体实现在插入函数中引用的向上调整函数,以此不断插入就能创建堆
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a; //堆用数组实现,创建一个整形数组用来存放堆的数据
int size; //堆当前的实际大小
int capacity; //堆的容量
}HP;
//初始化堆
void HeapInit(HP* php);
//打印堆
void HeapPrint(HP* php);
//销毁堆
void HeapDestroy(HP* php);
//插入x以后依然保持堆的形状
void HeapPush(HP* php, HPDataType x);
//向上调整
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int n, int parent);
//删除堆顶的元素
void HeapPop(HP* php);
//返回堆顶的元素
HPDataType HeapTop(HP* php);
//判空函数
bool HeapEmpty(HP* php);
//返回堆当前存储数据的个数
int HeapSize(HP* php);
Heap.c部分:实现各自定义函数的功能。
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//初始化堆
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//打印堆
void HeapPrint(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
int i = 0;
for (i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//销毁堆
void HeapDestroy(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
free(php->a);
php->a = NULL;
php->size = 0;
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 (child > 0) //孩子的下标等于0时,说明堆从最后一个数一路向上比较,已经到达堆顶了
{
//小根堆,任意孩子的值要大于父节点的值,不是的话则要向上调整
if (a[child] < a[parent]) //改为>,这个堆结构就成为大堆了
{
Swap(&a[child], &a[parent]);
//修正父亲与孩子的下标,通过循环不断比较,直到成为堆的形状
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//插入x以后依然保持堆的形状
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("ralloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
//扩容完后将数据插入到堆的最后一个位置(数组的最后一个位置)
php->a[php->size] = x;
php->size++;
//将插入的数据根据情况向上调整
AdjustUp(php->a, php->size - 1); //将数组和数组中最后一个数的下标传过去
}
//向下调整函数
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1; //先默认左边的孩子是整个小根堆中次小的孩子
while (minChild < n)
{
//与右孩子比较一下,找出小的那个孩子的下标
if (minChild + 1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
//找到次小的孩子后将其与父节点比较
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
//修正父亲与孩子的下标,通过循环不断比较,直到成为堆的形状
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除堆顶的元素 --找次大或者次小,小堆找次小,大堆找次大
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;
}
Test.c部分:主函数放在这,在主函数中调用个函数。在实现各函数时,可以用来测试各函数的功能。
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
int main()
{
//int a[] = { 15,18,19,25,28,34,65,49,27,37 };
int a[] = { 65,100,70,32,50,60 };
HP hp;
HeapInit(&hp);
int i = 0;
for (i = 0; i < sizeof(a) / sizeof(int); i++)
{
HeapPush(&hp, a[i]); //依次插入,插入完成后就是堆的形状
}
HeapPrint(&hp);
int z = HeapSize(&hp);
printf("%d\n", z);
return 0;
}