C数据结构:二叉树的基本操作

 

 

树基本知识

根结点:非空树的第一个结点,也就是所有结点的曾曾曾…祖先

叶子结点:就是每个分支的终端结点。

双亲结点:孩子的父母就称为双亲结点。
(个人理解:分支的妈妈就是双亲结点,至于为什么树都称为双亲,我认为是为了二叉树的方便使用,因为二叉树只有两个孩子,两个孩子的母亲,就是双亲结点。)

兄弟结点
1、所在层次一样。
2、双亲结点一样,也就是需要是同一个妈生的才是兄弟。这些都称为兄弟结点。

堂兄弟结点
1、所在层次一样。
2、双亲结点不一样,必须是不同妈生的才是堂兄弟。这些结点称为堂兄弟结点。

结点的度:顾名思义就是每一个结点所拥有的的度,度就是该结点的孩子有多少个(分支有多少),比如叶子的度就是0,该定义和树的度结合使用。

树的度:顾名思义就是一棵树的度,并且是树中各个结点的度的最大值。
简单来说一棵树中找到分支最多的那一个结点代表树的度。

数的深度:所含层次数,即结点的最大层次,也可以称为树的高度。

二叉树的性质

二叉树顾名思义每一个结点最多有两个孩子,即左右孩子(分支)
因此结构体内的date域要有两个。

性质1:二叉树的第 i 层上至多有 2 i − 1 2^i{^-}{^1} 2i1个结点。( i ≥ 1 i \ge 1 i1
该性质在满二叉树的情况下得出

因为每一层上都是2的次方个数,二叉树每一个结点都拥有左右孩子,每增加一层就,该层的结点都会增加两个孩子,所以是2的次方个数。
二叉树严格遵守左右之分,这是二叉树所特有的,和树不一样。

性质2:深度为 k 的二叉树至多有 2 k 2^k 2k - 1个结点。( k ≥ 1 k \ge 1 k1
该性质在满二叉树的情况下得出

我们得知了性质1,每一层的结点数,那么我们现如今又知道了层数k,只要我们用求和公式把每一层结点数加起来即可。
又因为性质1的每一层之间是等比数列,所以只要用等比数列求和公式即可轻松得出结果。

性质3:对于任何一棵二叉树T,如果其叶子数为n 0 _0 0,其具有左右孩子的结点数为n 2 _2 2
那么有:n 0 _0 0 = n 2 _2 2 + 1。

我的理解有两种描述:(不严谨,仅做辅助记忆)
①当只有一个根节点连着两个孩子结点,那么就是一个双亲加两片叶子,也就是根节点1+1 = 叶子数,从而得知该性质是成立。
②假如有2个双亲结点,那么对应的就会有3片叶子。为什么会这样?
原因是根节点必然会连着另一个双亲结点和一片叶子,而另一个双亲结点必然是连着两片叶子,这样才能对得上2个双亲对应三片叶子。

满二叉树

满二叉树,名如其物,每一层都是满的。
简单来说就是k层,当最后一层也是满叶子结点即称满二叉树。
利用性质2:当最后一层的叶子结点数 = 2 k − 1 2^k{^-}{^1} 2k1的时候就是满二叉树。
当最后一层k层的叶子结点数是 2 k − 1 2^k{^-}{^1} 2k1的时候,也从而得知其k层往上的结点数也都是满的,也能用 2 k − 1 2^k{^-}{^1} 2k1计算得出当前层数的结点数。

完全二叉树

完全二叉树晦涩难懂
用我的理解:
对满二叉树进行按层数进行编号(如下如图),每一层从左到右进行递增编号码,并且在完全二叉树中这些编号是确定的。
完全二叉树就是必须让这些编号是递一递增的,不能中间缺一个,但是你可以从尾部一个一个减掉,比如减掉15这个叶子结点后仍然是完全二叉树,满二叉树也是一个完全二叉树。
但是如果你跳过15,不是从15开始慢慢减1,那么就是中间缺了一块,就不是完全二叉树了。
满二叉树
总结结论:
因为我们如果想变化完全二叉树,必须是从后的k层的最右边开始操作,上图的15就是最尾部。
如果满二叉树,想要添加一个结点仍想要让他是完全二叉树,就得另起一层,仍旧是从左到右进行添加。
想要减少一个结点仍想要让他是完全二叉树,就从最后一层的最右边开始减。
因此我们知道完全二叉树必然是左孩子层数大于等于右孩子的层数,毕竟我们减孩子是从右边减,加孩子的时候要么层数相等,如果满叶子了,要加一层的时候,又变成了从左孩子开始加。如此一来,一直都是左孩子的层数大于等于右孩子的层数。

性质4、5的解释

性质4:具有n个结点数的完全二叉树的深度为:[ l o g 2 n log_2n log2n ]+ 1
(log求出的数是取小于以二为底的次方数,取的就是小于该次方数的数。
求出n = 2 3 . 5 ^3{^.}{^5} 3.5,我们取得是3,而不是4)

我的解释:首先拿满二叉树来作为例子,因为满二叉树也是完全二叉树。我们知道一个满二叉树的总结点数是: 2 k 2^k 2k-1,log 2 _2 2( 2 k 2{^k} 2k-1)的结果必然小于k ,因为 2 k 2^k 2k-1取以2为低的对数后必然小于k, 性质解释了,要取小于k的数,我们就取k-1,那么完成后,性质的公式还有一个+1,k-1+1正好就是等于k, k就是该满二叉树的深度。

性质5:如果对一棵树有n个结点的完全二叉树,其深度是[ l o g 2 n log_2n log2n] + 1,的结点按层照完全二叉树的编号排好顺序,那么对于任意一个编号为 i 的结点都有 ↓

(1)如果 i = 1, 则结点 i 是二叉树的根, 无双亲;
如 果 \qquad 如果 i > 1, 则其双亲结点:[ i 2 \frac{i}{2} 2i ]
我的理解:因为性质4的铺垫,取小于k的数,所以在这里同样也是。
如下图,i > 1的时候,双亲结点除以2一定等于他的左孩子,所以 i / 2 的时候,
有小数点就去掉小数点,要较小值,其值就是 i 的双亲结点。
满二叉树

(2)如果2i > n , 则结点 i 为叶子结点, 无左孩子;
否 则 \qquad 否则 其左孩子为 2i。
我的理解:其实很容易理解,因为(1)中解释道,左孩子i /2 就等于双亲结点,那么我们反过来想想,双亲结点乘以2就应该等于他的左孩子,因此,假设在完全二叉树的情况下,2i 不等于总结点数n 的话,肯定就证明了 i 这个双亲结点没有左孩子,又因为是完全二叉树,该结点没有了左孩子必然没有右孩子。所以如果2i等于n的时候,利用(1)的结论可得知,结点为 i 的时候,他的叶子是2i,这里的 i 和(1)中不一样,(1)中是叶子结点为i,(2)这里 i说的是双亲结点为i ,所以2i 就是其叶子的左孩子结点。

(3)如果2i +1 > n ,则结点 i + 1 没有右孩子;
否 则 \qquad 否则 其右孩子结点为2i + 1。
结论(3)是由(2)得知的,如果2i = n,那么2i + 1 必然是 i 这个结点有左孩子没有右孩子。
如果 2i + 1 = n 很显然是个奇数,奇数必然是右孩子。

顺序存储结构(利用性质4、5)

这两个性质主要是应用于当二叉树是用顺序存储结构。
之所以完全二叉树可以用顺序存储结构来存储和建立,得益于他的编号是有顺序的,因此这也是为什么前面强调完全二叉树必须是从上到下从左到右依次逐1递增来编号的。

通过用一个顺序数组即可表示出完全二叉树。
通过一个顺序存储结构的数组也可以还原出一个完全二叉树。

数组大小 2 k 2^k 2k - 1

缺点:很明显,如果最后一层只有最左边的节点有一个左孩子,那么这一层剩下的空间就浪费了,因为我们知道层数深度是直接开辟 2 k 2^k 2k - 1个空间,当 k 非常大的时候就会很浪费空间,所以一般只有满二叉树的时候是刚好能用完这些空间。

不是完全二叉树是否可以用顺序存储结构来存储一个二叉树?
答案是完全可以的,研究完全二叉树是为了抛砖引玉,解释一些有规律的,即使不是完全二叉树,我们照样可以给该二叉树进行按顺序编号,只是他中间会有缺漏,并不是完全二叉树罢了。
这时候细细想想,一般的二叉树按照满二叉树的有序编号来编写,这时候中间缺漏的东西会更多,也就意味着数组中浪费的空间更多。

链式存储结构

链式存储结构是重点,也是最常用的存储二叉树的方式。
建立二叉树、遍历二叉树、计算二叉树的结点数和叶子数都是利用递归算法的思想进行操作。

结点结构体

typedef struct _BiTree{
	
	char num;
	struct _BiTree* right;
	struct _BiTree* left;
	
}BiTree, *to_BiTree; //链式二叉树结构体 

建立二叉树

有一个细节千万不能漏掉。
因为递归算法会直接调用函数本身
当你用scanf进行录入的时候切记一定要把多余的空格吸收掉,不然会导致建立二叉树失败。
原因很简单:因为缓冲区不会把你的回车键吸收,而是自动的把回车键给下个调用函数的scanf接收。

void Create_tree(to_BiTree* Tree)
{
 	char n;
 	char temp;
	scanf("%c", &n);
	temp = getchar();//吸收空格 很重要 
	if(n =='#') 
	{
		(*Tree) = NULL;
	} 
	else
	{
		(*Tree) = (to_BiTree)malloc(sizeof(BiTree));
		(*Tree)->num = n;
		Create_tree(&(*Tree)->left);
		Create_tree(&(*Tree)->right);
	}
	
	return;
 }

先序遍历

根 左 右

void First_traverse(to_BiTree Tree)
{
	if(!Tree) return;
	printf("%c", Tree->num);
	First_traverse(Tree->left);
	First_traverse(Tree->right);
	
 } 

中序遍历

左 根 右

void Mid_traverse(to_BiTree Tree)
{
	if(!Tree) return;
	Mid_traverse(Tree->left);
	printf("%c", Tree->num);
	Mid_traverse(Tree->right);
}

后序遍历

左 右 根

void End_traverse(to_BiTree Tree)
{
	if(!Tree) return;
	End_traverse(Tree->left);
	End_traverse(Tree->right);
	printf("%c", Tree->num);
}
 

层次遍历

顾名思义就是一层一层的遍历。
该算法要用到队列的思想。
因为我们要实现一层一层的遍历必须要记录该层次的结点,然后遍历,然后用该层的结点进入下一层。
也就是先进先出的意思,第一层先入队,然后出队,然后第二层进队,然后出队 … \dots

队列的结构体代码

#define MAXSIZE 50
typedef struct _SqQueue{
	
	struct _BiTree* date[MAXSIZE];
	int front;//头指针 
	int rear;//尾指针 
}SqQueue;

层次遍历代码

void Level_traverse(to_BiTree Tree) //层次遍历
{
	SqQueue Sq;
	to_BiTree temp;
	InitQueue(&Sq);
	EnQueue(&Sq, Tree);
	while(IsSqEmpty(Sq))
	{
		DeQueue(&Sq, &temp);
		//printf("測試測試");
		printf("%c",temp->num);
		if(temp->left) EnQueue(&Sq, temp->left);
		if(temp->right) EnQueue(&Sq, temp->right);
	 } 
}
void InitQueue(SqQueue*Sq)//队列初始化
{
	Sq->front = 0;
	Sq->rear = 0;
}

bool IsSqEmpty(SqQueue Sq)//判断队列是否为空
{
	if(Sq.rear == Sq.front) return false;
	else return true;
}

void EnQueue(SqQueue*Sq, to_BiTree Tree) //进队
{
	Sq->date[Sq->rear] = Tree;
	Sq->rear++;
	
 } 

void DeQueue(SqQueue*Sq, to_BiTree* temp) //出队
{
	(*temp) = Sq->date[Sq->front];
	Sq->front++;
}  

复制二叉树

int Copy_Tree(to_BiTree* NewT, to_BiTree Tree)
{
	
	if(!Tree) 
	{
		(*NewT) = NULL;
		return 0;
	}
	else
	{
		(*NewT) = (to_BiTree)malloc(sizeof(BiTree));
		(*NewT)->num = Tree->num;
		Copy_Tree(&(*NewT)->left, Tree->left);
		Copy_Tree(&(*NewT)->right, Tree->right);
	}
}

计算二叉树深度

int Depth_Tree(to_BiTree Tree)
{
	int m, n;
	if(!Tree) return 0;
	else
	{
		m = Depth_Tree(Tree->left);
		n = Depth_Tree(Tree->right);
		//m>n初始化的时候是0,所以第一次搜索到叶子的孩子为NULL的时候,都会返回一个0,回到叶子就是return 1 
		if(m>n) return (m+1);
		else return (n+1);
	}
}

计算二叉树的结点总个数

int NodeCount_Tree(to_BiTree Tree)
{
	if(!Tree) return 0;
	else return NodeCount_Tree(Tree->left) + NodeCount_Tree(Tree->right) + 1;
 } 

计算二叉树的叶子结点数

int LeavesCount_Tree(to_BiTree Tree)
{
	if(!Tree) return 0;
	if(!Tree->left && !Tree->right) return 1;
	else return LeavesCount_Tree(Tree->left) + LeavesCount_Tree(Tree->right);
	//因为最后那个叶子总是没有左右孩子的,所以总会返回1,因此这里不用额外+1 
}

释放申请的空间

void Free_Tree(to_BiTree* Tree)
{
	to_BiTree temp = (*Tree);
	if(!(*Tree)) return;
	Free_Tree(&(*Tree)->left);
	Free_Tree(&(*Tree)->right);
	free((*Tree));
	(*Tree) = NULL;
 } 

程序的完整代码

可以试着按照该样例进行录入: ABC##DE#G##F###

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
typedef struct _BiTree{
	char num;
	struct _BiTree* right;
	struct _BiTree* left;
}BiTree, *to_BiTree; //链式二叉树结构体 

#define MAXSIZE 50
typedef struct _SqQueue{
	struct _BiTree* date[MAXSIZE];
	int front;//头指针 
	int rear;//尾指针 
}SqQueue;


void Create_tree(to_BiTree* Tree);//生成二叉树

void First_traverse(to_BiTree);//先序遍历 

void Mid_traverse(to_BiTree Tree);//中序遍历 

void End_traverse(to_BiTree Tree);//后序遍历 

void Level_traverse(to_BiTree Tree);//层次遍历 
void InitQueue(SqQueue*);//初始化队列 
bool IsSqEmpty(SqQueue Sq);//判断队列是否为空 
void EnQueue(SqQueue*,to_BiTree);//入队 
void DeQueue(SqQueue*, to_BiTree*);//出队 

int Copy_Tree(to_BiTree*, to_BiTree);//把旧树复制到一个新树中,第一个参数是新书,第二个参数是旧树 

int Depth_Tree(to_BiTree);//计算树的深度 
 
int NodeCount_Tree(to_BiTree);//计算树的结点数 

int LeavesCount_Tree(to_BiTree Tree);//计算树的叶子的节点数。注意:若一个结点有两个叶子,那就是一个结点 

int main()
{
	
	to_BiTree Tree = NULL, NewT = NULL;
	/*
	让用户输入数据生成二叉树
	1:对二叉树进行三种不同的遍历
	2:实现非递归的三种遍历方式
	a:首先实现中序的非递归算法,实现成功后再实现剩下的两种非递归的遍历二叉树方式 
	*/ 
	//ABC##DE#G##F### 
	Create_tree(&Tree);
	printf("以下是三种不同的遍历方式");
	printf("\n先序遍历:");
	First_traverse(Tree);
	printf("\n中序遍历:");
	Mid_traverse(Tree);
	printf("\n后序遍历:");
	End_traverse(Tree);
	printf("\n层次遍历:"); 
	Level_traverse(Tree);
	
	Copy_Tree(&NewT, Tree);
	printf("\n(复制)中序遍历:");
	Mid_traverse(NewT);
	
	int count = Depth_Tree(Tree);
	printf("\n树的深度:%d",count);
	
	count = NodeCount_Tree(Tree);
	printf("\n树的总结点数:%d", count);
	
	count = LeavesCount_Tree(Tree);
	printf("\n树的叶子的总结点数:%d", count); 
	Free_Tree(&Tree); 
	if(Tree) 
	{
		printf("\n先序遍历:");//如果没有释放完全还有漏的话,用一个先序遍历测试一下情况是怎样,但很明显我是不会失败的 hhhh
		First_traverse(Tree);
	 }  
	 else
	 {
	 	printf("\n树已被释放为空"); 
	 }
	
	return 0;
 } 
 
void Create_tree(to_BiTree* Tree)
{
 	char n;
 	char temp;
	scanf("%c", &n);
	temp = getchar();//吸收空格 很重要 
	if(n =='#') 
	{
		(*Tree) = NULL;
	} 
	else
	{
		(*Tree) = (to_BiTree)malloc(sizeof(BiTree));
		(*Tree)->num = n;
		Create_tree(&(*Tree)->left);
		Create_tree(&(*Tree)->right);
	}
	
	return;
 }

void First_traverse(to_BiTree Tree)
{
	if(!Tree) return;
	printf("%c", Tree->num);
	First_traverse(Tree->left);
	First_traverse(Tree->right);
	
 } 
 
void Mid_traverse(to_BiTree Tree)
{
	if(!Tree) return;
	Mid_traverse(Tree->left);
	printf("%c", Tree->num);
	Mid_traverse(Tree->right);
}
 
void End_traverse(to_BiTree Tree)
{
	if(!Tree) return;
	End_traverse(Tree->left);
	End_traverse(Tree->right);
	printf("%c", Tree->num);
}
 
void Level_traverse(to_BiTree Tree)
{
	SqQueue Sq;
	to_BiTree temp;
	InitQueue(&Sq);
	EnQueue(&Sq, Tree);
	while(IsSqEmpty(Sq))
	{
		DeQueue(&Sq, &temp);
		//printf("測試測試");
		printf("%c",temp->num);
		if(temp->left) EnQueue(&Sq, temp->left);
		if(temp->right) EnQueue(&Sq, temp->right);
	 } 
}
 
void InitQueue(SqQueue*Sq)
{
	Sq->front = 0;
	Sq->rear = 0;
}

bool IsSqEmpty(SqQueue Sq)
{
	if(Sq.rear == Sq.front) return false;
	else return true;
}

void EnQueue(SqQueue*Sq, to_BiTree Tree)
{
	Sq->date[Sq->rear] = Tree;
	Sq->rear++;
	
 } 

void DeQueue(SqQueue*Sq, to_BiTree* temp)
{
	(*temp) = Sq->date[Sq->front];
	Sq->front++;
}  
 
int Copy_Tree(to_BiTree* NewT, to_BiTree Tree)
{
	
	if(!Tree) 
	{
		(*NewT) = NULL;
		return 0;
	}
	else
	{
		(*NewT) = (to_BiTree)malloc(sizeof(BiTree));
		(*NewT)->num = Tree->num;
		Copy_Tree(&(*NewT)->left, Tree->left);
		Copy_Tree(&(*NewT)->right, Tree->right);
	}
}
 
int Depth_Tree(to_BiTree Tree)
{
	int m, n;
	if(!Tree) return 0;
	else
	{
		m = Depth_Tree(Tree->left);
		n = Depth_Tree(Tree->right);
		if(m>n) return (m+1);//m>n初始化的时候是0,所以第一次搜索到叶子的孩子为NULL的时候,都会返回一个0,回到叶子就是return 1 
		else return (n+1);
	}
}
 
int NodeCount_Tree(to_BiTree Tree)
{
	if(!Tree) return 0;
	else return NodeCount_Tree(Tree->left) + NodeCount_Tree(Tree->right) + 1;
 } 
 
int LeavesCount_Tree(to_BiTree Tree)
{
	if(!Tree) return 0;
	if(!Tree->left && !Tree->right) return 1;
	else return LeavesCount_Tree(Tree->left) + LeavesCount_Tree(Tree->right);
	//因为最后那个叶子总是没有左右孩子的,所以总会返回1,因此这里不用额外+1 
}
void Free_Tree(to_BiTree* Tree)
{
	to_BiTree temp = (*Tree);
	if(!(*Tree)) return;
	Free_Tree(&(*Tree)->left);
	Free_Tree(&(*Tree)->right);
	free((*Tree));
	(*Tree) = NULL;
 }

收获

二叉树整体给我的感觉就是递归递归递归,
让我对递归算法有了更好的理解,
其次是一个意外的收获,高中时期没能彻底的理解对数的概念,
反倒研究二叉树的时候茅塞顿开。
(数学渣渣想学好计算机真的难┭┮﹏┭┮)

posted @ 2022-07-02 17:13  竹等寒  阅读(14)  评论(0编辑  收藏  举报  来源