二叉树(一)--特性+堆

目录

一.树

树的定义

 树的参数

二.二叉树

二叉树的概念

满二叉树

完全二叉树

 二叉树的特性

使用数组存储二叉树时:

三.堆 

堆的定义

堆的创建

堆的其他功能实现

堆的构建和向上调整

堆的删除和向下调整


一.树

树的定义

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);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值