目录
一.树
树的定义
1.树就是不包含回路的连通无向图,本质是图
2.一棵树中的任意两个结点 有且仅有唯一的一条路径 连通
3.一棵树如果有n个结点,那么它一定恰好有n-1条边
树的参数
二.二叉树
二叉树是一种特殊的树
二叉树的概念
- 每个结点最多有两个儿子,左边的叫做左子树,右边的叫做右子树
- 包括两种特殊结构,满二叉树和完全二叉树
满二叉树
- 二叉树中每个内部结点都有两个儿子
- n层满二叉树的结点个数:2^0+2^1+...+2^(n-1)=2^n-1
- 当有n个结点时,完全二叉树的高度为logn+1
完全二叉树
- 最后一层的结点是连续的,个数不确定(满二叉树也是完全二叉树)
- n层完全二叉树的结点个数范围:[2^(n-1)-1+1,2^n-1]=[2^(n-1),2^n-1]
- 当有n个结点时,完全二叉树的高度为logn+1(和满二叉树一样,完全二叉树少的那些结点不影响高度的计算)
二叉树的特性
使用数组存储二叉树时:
结点坐标表示:设父节点为parent,子结点为child
- parent=(child-1)/2
- 左孩子:child=parent*2+1
- 右孩子:child=parent*2+2
存储特点:
- 逻辑结构为树形结构,实际不存在
- 物理结构为在内存中连续存放
- 数组存储只适合完全二叉树(否则会浪费很多空间)
三.堆
堆的定义
- 堆总是完全二叉树
- 堆中的某个结点总是小于等于/大于等于它的父结点
堆的创建
我们的初步想法就是,向要模拟堆的数组中一个一个的添加数据,当该子结点与它的父结点相比不符合我们所要构建的特性,则将他们交换位置,直到该结点到最上层为止
//这里的hp就是模拟堆的数组,a是存储输入数据的数组,child是从哪个子结点开始处理 void adjustup(Heap* hp,HPDataType* a,int child) { assert(a); //防止传入错误 assert(!HeapEmpty(hp)); //判断该堆是否为空 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; } } }
这里是建立大堆的向上调整的代码,大堆要保证父结点比子结点大
堆的其他功能实现
- 先将准备工作做了,这里是我们的头文件+主函数
#include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int HPDataType; //方便修改数组类型 #define MALLOC(type,num) (type*)malloc(num*sizeof(type)) #define REALLOC(obj,type,num) (type*)realloc(obj,num*sizeof(type)) typedef struct Heap { HPDataType* _a; //模拟堆的数组 int _size; //用于记录堆的结点个数 int _capacity; //容量 }Heap; void HeapCreate(Heap* hp, HPDataType* a, int n);// 堆的构建 void HeapDestory(Heap* hp); // 堆的销毁 void HeapPush(Heap* hp, HPDataType x); // 堆的插入 void adjustup(Heap* hp,HPDataType* a, int child); //向上调整 void HeapPop(Heap* hp); // 堆的删除 void adjustdown(Heap* hp,HPDataType* a, int n,int child); //向下调整 HPDataType HeapTop(Heap* hp); // 取堆顶的数据 int HeapSize(Heap* hp); // 堆的数据个数 int HeapEmpty(Heap* hp); // 堆的判空 void print(Heap* hp, int n); //打印函数 void swap(HPDataType* x, HPDataType* y); //交换数字 void test() { //初始数据 HPDataType arr[100] = { 0 }; int n = 0; scanf("%d", &n); for (int i = 0; i < n; i++) { scanf("%d", &arr[i]); } //初始化堆 Heap* hp = init(n); HeapCreate(hp, arr, n); print(hp, HeapSize(hp)); //删除 HeapPop(hp); print(hp, HeapSize(hp)); //插入 HeapPush(hp, 100); print(hp, HeapSize(hp)); HPDataType tmp = HeapTop(hp); printf("%d\n", tmp); HeapDestory(hp); } int main() { test(); //最大堆 return 0; }
堆的构建和向上调整
Heap* init(int n) { Heap* hp = MALLOC(Heap, 1); //为存储数组地址和堆参数的结构体分配空间 hp->_a = MALLOC(HPDataType, n); //为数组分配n个元素的空间 hp->_capacity = n; hp->_size = 0; return hp; } void HeapPush(Heap* hp, HPDataType x) { assert(hp); //防止传入错误 hp->_a[hp->_size++] = x; //size为下一个元素的下标,因此直接在该处赋值 adjustup(hp,hp->_a, hp->_size-1); //每传入一个元素,都要向上调整一次,维护我们最大堆的特性 if (hp->_size == hp->_capacity) { //size同时也是元素个数,当个数==容量时需要扩容 HPDataType* tmp = REALLOC(hp->_a, HPDataType, hp->_capacity * 2); hp->_a = tmp; hp->_capacity *= 2; } } void HeapCreate(Heap* hp, HPDataType* a, int n) { assert(hp); //防止传入错误,因为后面的代码需要使用 assert(a); for (int i = 0; i < n; i++) { HeapPush(hp, a[i]); } }
堆的删除和向下调整
有了插入数据,那么我们什么时候需要用到删除数据呢?
- 由于堆的特性,我们可以知道, 堆顶元素是该堆中最大或最小的数据,因此堆顶元素是最有价值的!
- 但如果直接拿出去的话,会改变原先的结点之间的关系,需要重新建立堆,也就是要重新开辟一块空间来存储新的堆(过于繁琐了哈~)
- 因此聪明的人类想出一个解决办法,我们可以将堆顶元素与堆中最后一个元素互换,并且将size--,就可以将堆顶元素剔出堆的范围内而不需要新的堆存储) =)
- 这里其实就是堆排序的主要思路捏 =)
void siftdown(HPDataType* a, int n, int parent) { assert(a); int child = parent * 2 + 1; //左孩子 int flag = 0; while (child < n && flag == 0) { //维护大堆 HPDataType tmp = parent; //tmp记录最大值的下标 if (a[child] > a[tmp]) { tmp = child; } if (child + 1 < n) { //有右孩子 if (a[child + 1] > a[tmp]) { tmp = child + 1; } } if (tmp == parent) { //如果父结点最大,则该停止循环了 flag = 1; } else { swap(&a[tmp], &a[parent]); //更新父子结点下标 parent = tmp; child = parent * 2 + 1; } } // while (child < n && flag == 0) { //维护小堆 // HPDataType tmp = parent; // if (a[child] < a[tmp]) { // tmp = child; // } // if (child + 1 < n) { //有右孩子 // if (a[child + 1] < a[tmp]) { // tmp = child + 1; // } // } // if (tmp == parent) { // flag = 1; // } // else { // swap(&a[tmp], &a[parent]); // parent = tmp; // child = parent * 2 + 1; // } // } } void HeapPop(Heap* hp) { assert(hp); assert(!HeapEmpty(hp)); //空堆不可以删除 swap(&hp->_a[0], &hp->_a[hp->_size - 1]); //交换头尾数值,使其可以进行向下维护 hp->_size--; //将最大的数值剔出堆 adjustdown(hp,hp->_a,HeapSize(hp),0); //从头维护 }
主要功能的函数已经实现啦,剩下的就是些细枝末节
也许你会好奇为什么有些函数只有短短一行,这样也需要分装成一个函数吗?
(主要就是为了方便观看,我们可以简单的通过阅读函数名来知道此处在干些什么,如果只是干巴巴的代码,很容易看不懂,阅读性也不好)
HPDataType HeapTop(Heap* hp) { //取堆顶元素 assert(hp); assert(!HeapEmpty(hp)); return hp->_a[0]; } int HeapSize(Heap* hp) { //返回堆中元素个数 return hp->_size; } int HeapEmpty(Heap* hp) { //判断是否为空堆 return hp->_size == 0; } void swap(HPDataType* x, HPDataType* y) { //交换数据 HPDataType t; t = *x; *x = *y; *y = t; } void print(Heap* hp,int n) { //分装的打印函数 assert(hp); for (int i = 0; i < n; i++) { printf("%d ", hp->_a[i]); } printf("\n"); } void HeapDestory(Heap* hp) { //别忘了释放开辟的内存空间 assert(hp); free(hp->_a); free(hp); }