树--苦修内功篇

本文详细梳理了二叉树的基本性质,包括节点分布、结点数量限制,以及完全二叉树的深度计算。深入讲解了二叉树的存储结构(顺序与链式),创建方法和遍历(递归与非递归),并介绍了线索化二叉树和哈夫曼树的基础概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


半年前自学数据结构,初次接触内功尚浅,觉晦涩难懂,而今发现遗忘严重,恰逢学校专业课开设,现复习总结数据结构,分为苦修内功篇和华山论剑篇,分别总结笔者认为数据结构重难点和经典例题实践。建议读者可以先阅读苦修内功篇复习相关知识点,再阅读华山论剑篇。尚功力浅显,不断积累修订中,多多包含指正。

二叉树的性质

性质1:在二叉树的第 i 层有2^(i-1)个结点

性质2:深度为k的二叉树至多有2^k-1个结点

性质3:任何一个二叉树,如果叶子结点数为N0,度为2的结点数为N2,则N0=N2+1

最初看到这个代数表达式我想一定和我一样黑人问号,what?这咋来的,但是第一感觉必然是表达式消元之后的最终结果,那就来一起从不同视角看一眼。

从结点角度看:二叉树只有度为0,1,2的结点,故N= N0+N1+N2
从分支角度看:度为1的射出一条分支,度为2的射出两条分支,故分支条数B=N1+2N2
从两个视角看: 除了根节点,所有的分支连接一个结点,故N=B+1
即:B+1=N1+2
N2+1=N0+N1+N2
得N0=N2+1

性质4:具有n个结点的完全二叉树深度为【logN】+1(以2为底)

这个我好像有一点感觉知道它怎磨来的

k-1层满二叉树n1=2^(k-1)-1个结点;
k层满二叉树有n2=2^k -1个结点;
根据完全二叉树定义,必有n1<=N<n2
即n1<=N<n2,;即取对数得结果

性质5:对一颗有n个结点的完全二叉树按层序编号后,有一下性质:

i=1,结点i为根结点;i>1,双亲结点为【i/2】
2i>n,无左孩子;否则左孩子=2i
2i+1>n,无右孩子;否则右孩子=2i+1

二叉树的存储结构

顺序存储

typedef struct{
	datatype SqBiTree[MAXSIZE+1];
	int nodemax;//最后一个结点下标
}BiTree

显然,顺序存储结构存储满二叉树很方便,但是如果比较稀疏,你怎么存储,怎么你也想到了三元组,嗯,我的第一反应也是,但是我们有更好的方法–>二叉链表

链式存储

//二叉链表
typedef struct BiTNode{
	datatype data;
	struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

既然二叉链表可以存储二叉树,那么n叉链表就更有意思了。嗯可以,但没必要。
可以看出,一个二叉树含有n个结点,则必有2*n个指针域,有n-1个指针域指向孩子,n+1个为空。当看到这里,我脑子一嗡,坏了,按照数据结构的设计,肯定是不会浪费任何一种空间的,果然,埋下了线索化二叉树的伏笔。

二叉树创建

void CreateBiTree(BiTree root){
	char ch;
	scanf("%c",&ch);
	if(ch=='#')
		*root = NULL;
	else{
		*root(BiTree) malloc(sizeof(BiTree));
		if(!*root)
			exit(-1);
		(*root)->data = ch;
		CreateBiTree(&(*root)->lchild);
		CreateBiTree(&(*root)->rchild);
	}
}

二叉树的遍历

递归实现

前序遍历

void PreOrderTraverse(BiTree root){
	if(root==NULL)	return;
	visit(root->data);
	preOrderTraverse(root->lchild);
	preOrderTraverse(root->rchild);
}

中序遍历

void InOrderTraverse(BiTree root){
	if(root==NULL)	return;
	InOrderTraverse(root->lchild);
	visit(root->data);
	InOrderTraverse(root->rchild);
}

后序遍历

void PostOrderTraverse(BiTree root){
	if(root==NULL)	return;
	PostOrderTraverse(root->lchild);
	PostOrderTraverse(root->rchild);
	visit(root->data);
}

树的递归遍历不管理解还是实现都很简单,但是往往看似简单的东西演化出来理解实现更加复杂的思想。树的遍历简直是递归的最经典的模板,它演示了递归的执行过程,更加揭示了分治法的本质思想。其实对比学过的递归你会发现,快排也就是树的前序遍历,归并也就是树的后序遍历…也就是说,我可以将递归实现的算法都可以想象为树的递归遍历的演化,后续在华山论剑篇我再仔细唠叨。

非递归实现

其实我们都知道,递归是对非递归的简化,是将循环的过程交给计算机处理,我们只要关注当前结点怎么处理。那么非递归也就是对递归的模拟,既然是对递归的模拟,那么不用想,就是用栈来实现喽。

前序遍历

有两种思路模拟

1.本质模拟法:直接从使用栈从本质上模拟递归的实现过程,堪称经典!

过程:先序遍历过程为根左右,那么入栈顺序应该先右后左。

void preOrder(BiTree root)
{
	SeqStack *S;
	InitBiTree(S);
	BiTree p;
	if (root == NULL)	return;
	Push(S, root);
	while (!isEmpty(S)){
		Pop(S, &p);
		visit(p->data);
		if(p->rchild!=NULL)	Push(S, p->rchild);
		if(p->lchild!=NULL)	Push(S, p->lchild);
	}
}
2.直接模拟法:使用栈对递归的执行顺序进行模拟,妙啊!
void preOrder(BiTree root){
	SeqStack *S;
	InitBiTree(S);
	BiTree p = root;
	while(p!=NULL || !IsEmpty(S)){//当当前结点为空并且栈为空则整个过程结束
		while (p!=NULL)//循环来模拟先访问左子树
		{
			visit(p->data);
			Push(S, p);
			p = p->lchild;
		}
		if(!IsEmpty(S)){//判断来模拟回溯访问右子树
			Pop(S, &p);
			p = p->rchild;
		}
	}
}

总的来说,第一种更好想,第二种需要注意实现顺序的细节,都非常歪瑞古德!

中序遍历

void InOrder(BiTree root){
	SeqStack *S;
	InitBiTree(S);
	BiTree p = root;
	while(p!=NULL || !IsEmpty(S)){
		while (p!=NULL){
			Push(S, p);
			p = p->lchild;
		}
		if(!IsEmpty(S)){
			Pop(S, &p);
			visit(p->data);
			p = p->rchild;
		}
	}
}

后序遍历

void preOrder(BiTree root)
{
	SeqStack *S;
	InitBiTree(S);
	BiTree p = root,q = NULL;
	while (p != NULL || !IsEmpty(S))
	{					
		while (p != NULL){
			Push(S, p);
			p = p->lchild;
		}
		if (!IsEmpty(S)){
			Peek(S, p);
			if((p->rchild==NULL) ||(p->rchild==q)){
				Pop(S, &p);
				visit(p->data);
				q = p;
			}
			else	
				p = p->rchild;
		}
	}
}

层序遍历

当然是队列来实现了

void LevelOrder(BiTree root){
	SeqQueue *Q;
	Initqueue(Q);
	EnQueue(Q, root);
	BiTree p;
	while(!isEmpty(Q)){
		DeQueue(Q, &p);
		visit(p->data);
		if(p->lchild!=NULL)
			EnQueue(Q, p->lchild);
		if(p->rchild!=NULL)
			EnQueue(Q, p->rchild);
	}
}

手写遍历

一般在考试中需要手写几种遍历结果,这个本来挺简单的,但是没法有时候越简单越容易出错,在经历了几次失误后我尝试找了找有没有简单的方法可以减小失误率,嗯,这个方法如何

在这里插入图片描述

请添加图片描述

在这里插入图片描述
手写没出错过或者无手写需求的大佬请忘记这部分内容,我们继续

二叉树其余操作

统计二叉树中节点数

void Count(BiTree root){
	if(root){
		count++;
		preOrder(root->lchild);
		preOrder(root->rchild);
	}
}

统计二叉树叶子结点数并输出叶子结点

int leaf(BiTree root){
	int left, right;
	if(root){
		if((root->lchild==NULL)&&(root->rchild==NULL)){
			printf("%c", root->data);
			return 1;
		}
		return leaf(root->lchild) + leaf(root->rchild);
	}
}

求二叉树的高度
1.全局变量求高度

void TreeDepth(BiTree root,int high){
	if(root){
		if(high>depth)
			depth = high;
		TreeDepth(root->lchild, high + 1);
		TreeDepth(root->rchild, high + 1);
	}
}

2.递归求高度

int PostTreeDepth(BiTree root){
	if(root==NULL)
		return 0;
	int left = PostOrderTraverse(root->lchild);
	int right = PostOrderTraverse(root->rchild);
	return left > high ? left + 1 : right + 1;
}

树状打印二叉树

void PrintTree(BiTree root,int high){
	if(root==NULL)
		return;
	PrintTree(root->rchild, high + 1);
	for (int i = 0; i < high;i++)
		printf(" ");
	printf("%c\n", root->data);
	PrintTree(root->lchild, high + 1);
}

线索化二叉树

前文提到:一个二叉树含有n个结点,则必有2*n个指针域,有n-1个指针域指向孩子,n+1个为空指针域,那么线索化二叉树就是使用空指针域指向遍历结点的前序或者后继。这个设计很好,它没有浪费我们的结构设计,但是…他好像也没有什么实际应用,至少目前笔者没意识到,所以,先鸽为敬。

树的存储结构

双亲表示法

在这里插入图片描述
在这里插入图片描述
哎,之前没怎么在意,这不就是拉链法吗,有意思有意思,嗯。
在这里插入图片描述
注意孩子兄弟表示法,将树转化为二叉树就很简单了,跟着定义就行。

树,森林的遍历

在这里插入图片描述

树和森林的遍历都可以转化成对应的二叉树进行遍历,就鸽了,不愧是我。

哈夫曼树

哈夫曼的具体应用让我们华山论剑篇见,挖个坑吧!

本次树的知识点就到此为止,等等,也许我会后面再补一补线索化,平衡二叉等内容,但是以我·对自己的了解,接上可能性不大了,后续有对相关内容的思考总结尽量记录下来,鸽王本鸽没错了。关于树的应用实在是太经典了,树的递归遍历演化的各种题目也非常经典,哈夫曼编码译码也非常有趣,苦修内功,华山论剑篇再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code_Pianist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值