二叉树详解

目录

二叉树

树的概念和存储结构

树的概念

树的存储结构

二叉树的概念和结构

二叉树的概念

满二叉树

完全二叉树

二叉树性质

二叉树的存储结构

顺序表二叉树

链式二叉树

链式二叉树的遍历

二叉树实现案例

        Tree.h代码:

        tree.c讲解:

        tree.c代码:


二叉树

            为了了解二叉树,首先要了解树的概念。

树的概念和存储结构

树的概念

        节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6

        叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I等节点为叶节点

        非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G等节点为分支节点

        双亲节点或父节点若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图,A是B的父节点

        孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点

        兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点

        树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6

        节点的层次从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

        树的高度或深度树中节点的最大层次; 如上图:树的高度为4

        堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点

        节点的祖先从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先

        子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙

        森林:由m(m>0)棵互不相交的树的集合称为森林.

        注意 子树直接是不相交的。任何一颗树都能被拆为,根--子树

                    除了根节点之外,所有节点都只有一个父节点。

树的存储结构

        第一种,用一个顺序表来存储,有几个孩子就扩容几个_childSL,这种方式比较麻烦,不常用。

        第二种,左孩子右兄弟表示法(更优于第一种)。左孩子:指向第一个孩子,右兄弟:指向自己同个母亲节点的兄弟。这种表示方法比起顺序表更清晰,容易实现,更常使用。

二叉树的概念和结构

二叉树的概念

        不存在度大于2的节点的数被称为二叉树,二叉树由一个根节点,一个左子树和一个右子树组成。

满二叉树

        满二叉树的总节点数为:2^h-1,h为树的高度。所以我们假设一颗满二叉树的总节点数为N,那么可以得到:2^h-1=N,转换一下就可以得到N=log2(h+1),可以省略1,得到N=log2h,这就是我们遍历一遍满二叉树的时间复杂度。

完全二叉树

        满二叉树就是完全二叉树的一种。对于完全二叉树其前h-1层都是满的,最后一层不一定是满的(满二叉树就一种特殊完全的二叉树),但是最后一层从左到右是连续的,不能跳着存放。

        所以完全二叉树的节点个数就是:[2^(h-1),2^h-1]

二叉树性质

        1. 若规定根节点的层数为1(二叉树的前提),则一棵非空二叉树的第i层上最多有2(i-1)个结点。

        2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h-1。(由等比数列求得)

        3. 对任何一棵二叉树, 如果度为0其叶结点个数为a,度为2的分支结点个数为b,则有ab+1,即:度为0的节点个数比度为2的节点个数多一个。

        4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1)(注:是log以2

为底,n+1为对数。因为n=2h-1)

        5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

                1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点

                2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子

                3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

二叉树的存储结构

顺序表二叉树

        由于顺序表存储是不能空着的,所以若用顺序表存储二叉树的话,最好存储完全二叉树。虽然用顺序表存储树的条件比较苛刻,但这种存储方式还是有个很好的优点的,用顺序表存储完全二叉树就可以通过对下标的+-*/就可以从孩子节点访问到父亲节点或者从父亲节点访问到孩子节点。以下是用顺序表存储完全二叉树时下标的变化方式:

        左孩子的下标=父亲节点下标*2+1;                leftchild=parent*2+1;

        右孩子的下标=父亲节点下标*2+2;                rightchild=parent*2+2;

        父亲节点下标=(孩子节点-1)/2;                      parent=(child-1)/2;

        如果不是完全二叉树,用顺序表存储则会有些麻烦

        所以完全二叉树更适合顺序表存储,如果不是完全二叉树则更适合链式存储。

链式二叉树

        普通的二叉树(不是完全二叉树),一般可以用链式结构。链式结构的二叉树通常应用于搜索二叉树。

        搜索二叉树:左边的节点值比根小,右边节点值比根大。如下图所示:

        若在搜索二叉树中查找值,我们最多需要找高度次,时间复杂度也就是log2N(理想情况下),实际上会比log2N更高,这时我们就需要AVL树和红黑树来减少我们的时间复杂度,那些知识点比较复杂,这里就不做讲解。

        注:任何一颗二叉树都要拆为根、左子树、右子树来看,这种思想可以提高我们对二叉树递归方面的理解。

链式二叉树的遍历

        二叉树的遍历分为4种:

                前序遍历:根-->左子树-->右子树;根在左子树右子树之前遍历。

                中序遍历:左子树-->跟-->右子树;根在左子树右子树之间遍历。

                后续遍历:左子树-->右子树-->跟;根在左子树右子树之后遍历。

                层序遍历:通过使用队列来实现一层一层访问。

        链式二叉树的遍历最好都用递归来进行,我们拿一组链式二叉树的前序遍历来举例。

        前序遍历代码如下:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
    //如果检测到NULL的节点,就要返回递归上一层
	if (root == NULL)
	{
		printf("# ");
		return;
	}
    //可以不是打印,这里当做访问当前节点的例子
	printf("%c ", root->_data);
    //上面访问完当前节点,就要递归左子树和右子树,依次往下遍历
	BinaryTreePrevOrder(root->_left);
	BinaryTreePrevOrder(root->_right);
}

        前序遍历递归内存分配过程模拟:

        前序遍历,又称为深度优先遍历(dfs)

        层序遍历,又称为广度优先遍历(bfs)。层序遍历案例:扫雷递归扩展或qq共同好友推荐。

二叉树实现案例

        Tree.h代码:
#include <stdio.h>
#include <assert.h>
#include <string.h>

typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType _data;
	struct BinaryTreeNode* _left;
	struct BinaryTreeNode* _right;
}BTNode;

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate1(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树层数
int leveSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

        我们用链式结构来组成一颗二叉树。用ABD##E#H##CF##G##(前序遍历)组成的二叉树结构如图所示:

        tree.h文件设置完成后,我们要根据ABD##E#H##CF##G##来创建树。

        tree.c讲解:

        用BinaryTreeCreate1函数来创建树遇到‘#’就返回NULL,否则就创建一个树节点,存储当前字符,然后对这个树节点的左右子树进行递归。

        用BinaryTreeDestory实现树的销毁,树的销毁就要用到后续遍历了,只有我们在后序遍历时,左右子树都返回NULL,(代表当前节点是叶子节点)我们才free当前节点。

        返回二叉树节点个数很简单,只要不为NULL,就返回递归左子树+递归右子树+1。

        返回叶子节点个数要根据叶子节点特性(左子树=右子树=NULL),判断递归时遇到的节点是否是叶子节点,如果是,返回1,然后返回左右子树的叶子数加起来的总和。

        返回K层节点个数就需要先往下递归K-1次,直到K=1时,就说明我们递归到了我们原输入的K层,这时候如果当前层的节点不为NULL就返回1,然后还是返回左右子树的K层节点数量。

        返回二叉树层数的话就需要把后续递归的值,存储到一个变量中返回,然后返回左右子树中更深的那颗子树+1;+1是代表加上自己这层。

        查找二叉树值为X的节点,也需要把递归返回的节点存储起来。正常情况若没有找到节点,就会递归到root的NULL中,就会返回NULL,若在某次递归途中找到等于X的节点,就返回当前节点的地址,然后对左右子树返回的地址进行判断,若返回的其中一个不是NULL就说明找到了,直接返回找到的节点给上一个递归,从而实现层层返回。

        前序中序后序都比较相似也比较容易实现,前面已经对前序遍历进行过讲解,这里就不再赘述。

          层序遍历需要用到队列,用到队列的先进先出原则。

        首先,我们要先把第一个节点存入队列中,然后再进循环(因为循环继续条件是队列不为空)。进循环后,先把队首的树节点存储起来,再pop节点,(我们pop的节点时队列节点,树节点并没有动,所以我们还是能够通过pop前存储的front查找我们应该访问的节点)然后通过我们存储的front去寻找左右非空节点。总结来说,就是pop一个节点的同时push被pop节点的左右非空节点。

         注意:队列里的节点类型要改成树的节点类型,即队列里的每个元素存储的是树的节点。这样的好处在于我们可以通过提前储存要pop的节点,然后我们pop元素出队列后还可以访问到。

        层序遍历如果加个leveSize就可以实现一层一层打印。先把leveSize设置成1,代表树的第一个节点,然后pop元素 leveSize次,就说明把当前层的数据全出完了,然后只要队列不为空,就再重新给leveSize赋值队列当前元素个数,再一层一层的pop,就可以实现一层一层访问打印。

        判断二叉树是否是完全二叉树的前提也是层序遍历。(我们要对层序遍历进行改变,不管front的左右子树是否是NULL,都push进队列中)完全二叉树通过层序遍历的话一定是连续的(NULL一定在最后,NULL后续一定没有元素),如果当我们层序遍历遇到NULL时,退出循环,这时候若我们队列中除去NULL如果还有其他树节点,那就说明当前二叉树不是完全二叉树。

        tree.c代码:
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate1(BTDataType* a, int n, int* pi)
{
	if ((*pi) >= n)
	{
		printf("访问超出数组\n");
		exit(-1);
	}
	if (a[(*pi)] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->_data = a[(*pi)++];
	node->_left = BinaryTreeCreate1(a, n, pi);
	node->_right = BinaryTreeCreate1(a, n, pi);
	return node;
}

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
		return;
	//用后续遍历,直到左右子树都返回NULL才free节点
	BinaryTreeDestory((*root)->_left);
	BinaryTreeDestory((*root)->_right);
	free(*root);
}

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root&&root->_left == NULL&&root->_right == NULL)
		return 1;
	return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1 && root != NULL)
		return 1;
	return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{

	if (root == NULL)
		return NULL; 
	if (root->_data == x)
		return root;
	//把左右存储起来,方便后续判断然后返回地址
	//单纯的||只能返回true和false,无法返回地址
	BTNode* left = BinaryTreeFind(root->_left, x);
	BTNode* right = BinaryTreeFind(root->_right, x);
	//如果返回的left和right中有一个不是NULL,就进入判断,返回不是NULL的地址
	if (left || right)
	{
		if (left)
			return left;
		else
			return right;
	}
}

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	printf("%c ", root->_data);
	BinaryTreePrevOrder(root->_left);
	BinaryTreePrevOrder(root->_right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	BinaryTreeInOrder(root->_left);
	printf("%c ", root->_data);
	BinaryTreeInOrder(root->_right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	BinaryTreePostOrder(root->_left);
	BinaryTreePostOrder(root->_right);
	printf("%c ", root->_data);
}
// 层序遍历
//用队列实现
void BinaryTreeLevelOrder1(BTNode* root)
{
	//创建一个队列
	Queue ql;
	QueueInit(&ql);
	//先提前放个元素进去,方便后面进入while循环
	//把队列传输元素的变量类型改为树的结点类型,这样好方便传输值(值就是树的结点)
	Queuepush(&ql, root);
	//由于后面需要用到BinaryTreeFind函数,先提前备份root
	while (!QueueEmpty(&ql))
	{
		//用树结构体的指针存放要出列的数据
		BTNode* front = Queuetop(&ql);
		//pop是队列里的,树里的节点还在,所以front还能访问到被pop出来的树的结点
		Queuepop(&ql);
		printf("%c ", front->_data);
		if (front->_left != NULL)
		{
			Queuepush(&ql, front->_left);
		}
		if (front->_right != NULL)
		{
			Queuepush(&ql, front->_right);
		}
	}
	QueueDestory(&ql);
}
//一层一层访问打印
void BinaryTreeLevelOrder1(BTNode* root)
{
	//创建一个队列
	Queue ql;
	QueueInit(&ql);
	//先提前放个元素进去,方便后面进入while循环
	//把队列传输元素的变量类型改为树的结点类型,这样好方便传输值(值就是树的结点)
	Queuepush(&ql, root);
	//由于后面需要用到BinaryTreeFind函数,先提前备份root
	int leveSize = 1;
	while (!QueueEmpty(&ql))
	{
		while (leveSize--)
		{
			//用树结构体的指针存放要出列的数据
			BTNode* front = Queuetop(&ql);
			//pop是队列里的,树里的节点还在,所以front还能访问到被pop出来的树的结点
			Queuepop(&ql);
			printf("%c ", front->_data);
			if (front->_left != NULL)
			{
				Queuepush(&ql, front->_left);
			}
			if (front->_right != NULL)
			{
				Queuepush(&ql, front->_right);
			}
		}
		printf("\n");
		leveSize = QueueSize(&ql);
	}
	QueueDestory(&ql);
}
// 判断二叉树是否是完全二叉树 返回1 是完全二叉树 0不是
int BinaryTreeComplete(BTNode* root)
{
	Queue ql;
	QueueInit(&ql);

	Queuepush(&ql, root);

	while (!QueueEmpty(&ql))
	{
		BTNode* front = Queuetop(&ql);
		Queuepop(&ql);

		//遇到NULL就退出循环
		if (front == NULL)
			break;

		//不管是不是NULL都加进队列
		Queuepush(&ql, front->_left);
		Queuepush(&ql, front->_right);
	}
	while (!QueueEmpty(&ql))
	{
		BTNode* front = Queuetop(&ql);
		Queuepop(&ql);
		if (front != NULL)
			return 0;
	}
	return 1;
	QueueDestory(&ql);
}
//二叉树层数
int leveSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leftnum = leveSize(root->_left);
	int rightnum = leveSize(root->_right);
	return leftnum>rightnum ? leftnum + 1 : rightnum + 1;
}

  • 23
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值