数据结构——二叉树

        各位看官好,这是鄙人的第十篇博客,今天我们来聊聊二叉树。那么肯定会有不了解的小白会问为什么叫做二叉树,是什么树,由什么组成嘞,有什么作用嘞等等。大家放心鄙人会一一为大家解读的。

二叉树是一个什么树

        首先我们解决为什么称它为树,我们只需要一张图片就可以解决。

53fe72f4c9234d9c8c70758a3e1941f7.png

       这样大家可以看出来了吧,这是一个树的树枝虽然是倒过来的。并且是由n(n>=0)个有限节点组成一个有层次关系的集合。这样大家就先初步的了解了二叉树是个什么树了吧。二叉树就是结点的一个有限集合,这个集合可能为空的也可能在一个结点上有2个对称的数。

小白:那这个树有什么特殊的吗?

鄙人:当然了,特点一:一个结点上最多有两个数,就是一个结点上最多分出来两个结点。特点二:二叉树的子树有左右之分,其子树的次序不能颠倒。这就是二叉树的两个特点了。

小白:那这样的话,是不是二叉树只有这一种。

鄙人:那当然不是,二叉树有还有两种特殊的数。满二叉树:如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

8fc6458632d94988937dbf1a23f5d567.png

完全二叉树:由满二叉树而引出来的,若设二叉树的深度为h,除第h层外,其他各层(1~h-1)的结点数都达到最大个数(即1~h-1层为一个满叉树),第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

7b90c8690ce6472a931a88ebb35d032e.png

这两种就是二叉树的两种特殊的形式了,分别是满二叉树和完全二叉树。

小白:那二叉树怎么存储数据嘞?

鄙人:二叉树的存储有两种方式,一是顺序存储,二是链式存储。首先顺序存储是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。接着是链式存储用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。也许大家读了之后还是会感到比较陌生,但是到后面见的多了自然也就更加了解了。

鄙人:既然都说了二叉树的存储结构了,那么二叉树的性质也不能少吧。并且二叉树的性质还不少。有五个。性质一:在二叉树的第i层上至多有2i-1个结点(i>=1)。这什么意思嘞,比如说,现在只有一层,那么根结点就是2的1次方减1结果为1.有两层的时候就是2的2次方减一结果为2,其他的以此类推。

4ef6355c8ce94144845f223f04b81c56.png

性质2:深度为k的二叉树至多有2k-1个结点(k>=1)。在第几层最多有2的几次方减1个结点。如在第一层最多有2的一次方减一个结点。第二层最多有2的二次方减1个结点。

82ee7f44374f451791d6fa067a2ad734.png 

性质3:对于任何一颗二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。这个嘞,就是说如果有两个分之的结点相加为n1,有一个结点的相加为n2,但如果没有分之结点的相加结果为n3。这样最后的n1+n2+n3=总结点数。这里n1为4,n2为1,n3为5相加总结点为10。

e4aa634b40554d9e88996e66ad5fd7a4.png

 

性质4:具有n个节点的完全二叉树深为log2x+1(其中x表示不大于n的最大整数)。(其中x表示不大于n的最大整数)。 由满二叉树的定义我们可以知道,深度为k的满二叉树的结点数n一定是2 k -1。 

性质5:如果对一颗有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第一层到[log2n]+1层,每层从左到右),对任一结点i(1<=i<=n)。

(1)如果i=1,则结点i是二叉树的根,无双亲,如果i>1,则其双亲结点是结点[i/2]
(2)如果2i>n,则结点i无左孩子(结点i为叶子结点)否则左孩子是结点2i。
(3)如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1.

       这样大家应该了解了二叉树的五个性质了吧。

小白:博主博主,堆是什么东西嘞?

鄙人:堆嘞,大家可以看到这个字,就是一堆东西,那么我们将一堆数据看作是完全二叉树的结构存储在一维数组中,如果这棵完全二叉树能满足所有的父亲节点都不大于它的孩子节点,那么这样的数据结构就叫做小堆,相反,如果所有父亲节点都不小于它的孩子节点,那么这个数据结构就叫做大堆。

5ae714ec14364d24819eacabe3fd75ec.jpeg

小白:那堆是怎么实现的?

鄙人:大致的思路:将堆顶节点和最后一个节点交换,然后删除最后一个节点。 交换节点键值后就不满足二叉堆的特性了,所以需要重新调整。 根节点为父节点,与两个子节点键值进行比较,找到键值最大的节点后交换父节点和该节点的位置。 位置交换后以最大值所在节点看作父节点,继续与子节点比较。 当父节点均大于子节点或到达叶子节点时,即完成堆化过程。

arr.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
 
typedef int HPDataType;
 
typedef struct Heap
{
	HPDataType* a;
	size_t size;
	size_t capacity;
}HP;
 
void HeapInit(HP* php);
void HeapDestory(HP* php);
void HeapPrint(HP* php);
void Swap(HPDataType* pa, HPDataType* pb);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);

arr.c

#include "arr.h"
 
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}
void HeapDestory(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}
 
//按数组打印
void HeapPrint(HP* php)
{
	assert(php);
	for (size_t i = 0; i < php->size; ++i)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}
 
void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
 
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
//多少个数据
size_t HeapSize(HP* php)
{
	assert(php);
	return php->size;
}
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	//这个比较取决于大小堆
	//小堆
	//最后一次比较,是parent是0,进行比较,当再次进行调整后。就不需要进行了,此时的child等于0,parent也是0[因为size_t是正整数】
	//-1/2还是等于0
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;//跳出循环
		}
	}
}
 
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	数据插入数组后
	//先判断是否有地方进行扩容
	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : (2 * (php->capacity));
		//开辟空间,要有一个临时变量进行开辟,否则如果开辟失败,里面的数据就都找不到了
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("malloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}
	php->a[php->size] = x;
	(php->size)++;//先插入,后size++,此时size这个下标的位置并没有值
	向上调整的算法,成为堆
	size_t child = (php->size) - 1;
	AdjustUp(php->a, child);
}

堆插入:先插入一个数字到数组的尾上【插入的这个数字后,可能不满足堆的概念】,再进行向上调整算法,直到满足堆

void AdjustDown(HPDataType* a, size_t root, size_t size)
{
	//找出小的
	//注意:可能没有右孩子
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		//避免越界
		if (child + 1 < size && a[child] > a[child + 1])
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;//跳出循环
		}
	}
}
 
void HeapPop(HP* php)
{
	assert(php);
	//当删除数据的时候,要判断有没有值
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, 0, php->size);
}

堆删除:每次都只能删除堆顶元素。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于根结点数据的“下沉”过程。

bool HeapEmpty(Hp* php)
{
	assert(php);
	return php->size == 0;
}

堆判空:如果当前节点的左子节点为空,但右子节点不为空,则该树不是完全二叉树。 b. 如果当前节点的左子节点和右子节点都不为空,将它们加入队列中。 c. 如果当前节点的左子节点为空,且右子节点为空,则之后的所有节点都必须是叶子节点(即没有子节点)。

层历遍历:先入第一层——根,上一层出来,带入下一层。除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

7f394f70ee514a19808f3dbe9a2e7804.jpeg

        这些大概就是二叉树的一些基本知识了,当然小白们看第一遍肯定还是有很多不明白的地方,但随着以后的学习自然也会更加清楚的。

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值