数据结构学习笔记|5.树和二叉树

目录

0. 前言

 1.树和二叉树的定义

1)树的定义

2)树的表示方法

3)树的基本术语

2.二叉树

1)基本定义

2)二叉树与树的区别

3)二叉树的种类

a.满二叉树

b.完全二叉树 

4)二叉树的性质

5)二叉树性质的推导 

 6)二叉树的存储

a.顺序存储

b.链式存储

3.对二叉树的操作

1)二叉树的遍历

a.遍历二叉树的定义

b.遍历二叉树的方法种类

c.代码实现(递归方法)

2)二叉树的创建

3)二叉树的复制

4)二叉树的深度计算

5)二叉树的结点个数计算

 3.线索二叉树

1)二叉链结构的改进

2)线索二叉树的结构

3)线索二叉树的线索化

         4)利用线索进行遍历

5)构建线索的意义

a.时空复杂度的改良

b.充分利用信息


0. 前言

树结构是一种重要的非线性结构。它是一种层次结构,如人类社会的族谱,公司中人员分配的管理都可以用树来形象表示。树在计算机中也得到广泛应用,在操作系统中,树来表示文件目录的组织结构。

 1.树和二叉树的定义

1)树的定义

树(Tree)是n(n>=0)个结点的有限集,它或为空树(n=0);或非空树(有结点的树)

 对于非空树T的特点

  1. 有且仅有一个称之为根的结点
  2. 除根节点以外其余结点可分为m(m>0)个互不相交的有限集T1,T2...Tm,其中每个集合本身也是一棵树,称之为根的子树(SubTree)

 

 如图,a是只有根节点的树,而b是一般的树。

2)树的表示方法

树的定义本身就是一个递归的定义(即在树中包含树),树可以有其他表现形式,可以有嵌套集合,广义表的形式,还有凹入表示法(类似书的编目),表示的方法多样。一般来说,分等级的分类都可用层次结构来表示,也就是说,都可以用树表示

 嵌套集合

 广义表

凹入表示法

3)树的基本术语

  1. 结点: 树中的一个独立单元,包含独立的数据元素,及指向其他子树的分支,例如,上面树中的A,B,C,D结点。
  2. 结点的度: 结点拥有的子树个数是结点的度,例如,A的度为3,B的度为2。
  3. 树的度:树的度是树内各结点的度的最大值,例如,上图树的度为3(B,C,D)(H,I,J)。
  4. 叶子:度为0的结点称为叶子或终端结点,如,结点K,L,F,G,M,I,J是树的叶子
  5. 双亲和孩子,兄弟,祖先...等各种关系: 树中结点的关系理解可以像一个家庭,例如,双亲和孩子,一个结点的子树是该结点的孩子,该结点是这些子树的双亲;兄弟,同一个双亲的孩子之间互称兄弟...等等关系。
  6. 层次:结构的层次从根开始定义起,根为第一层,根的孩子为第二层。
  7. 树的深度:树中结点的最大层次数称为树的深度或高度,图中所示树的深度为4。
  8. 有序树和无序树:如果树中的结点各子树看成从左至右有次序的(不能互换结点位置),则该树称为有序树,否则是无序树。
  9. 森林:是m(m>=0)棵不相交的树的集合,对于树中的结点而言,子树的集合为森林。

2.二叉树

1)基本定义

二叉树(Binary Tree)是n(n>=0)个结点所构成的集合,它或为空树(n=0);或非空树

对于非空二叉树有以下特点

  1. 有且仅有一个称之为根的结点(树的基本特点)
  2. 除根结点外其余结点分为两个互不相交的子集T1和T2,分别为T的左子树和右子树,且T1和T2本身都是二叉树

2)二叉树与树的区别

二叉树本身就是树,它也是递归性质,主要区别有以下两点

  1. 二叉树的每个结点至多有2棵子树(二叉树不存在度大于2的结点,且二叉树的度最多为2)
  2. 二叉树是有序树,有左右之分,且次序不能颠倒

3)二叉树的种类

a.满二叉树

满二叉树是深度为k,且含有2^k-1个结点的二叉树(结点个数拉满的二叉树),如图为深度为4的满二叉树。编号约定从结点起,自上而下,从左往右

 满二叉树的基本关系

b.完全二叉树 

深度为k,有n个结点的完全二叉树,要依据树中的每一个结点都应该与深度为k的满二叉树中编号从1至n的结点一一对应,称之为完全二叉树,否则是非完全二叉树

 完全二叉树的结点与满二叉树一一对应

 该树的6,7结点不对应,是非完全二叉树

  该树的6结点不对应,是非完全二叉树

 完全二叉树的特点

假设完全二叉树的层次为k

  1. 完全二叉树的前k-1层一定是满二叉树
  2. 叶子结点只可能出现在第k层和第k-1层

4)二叉树的性质

  1. 在层次为k的二叉树上,该层最多有2^(k-1)个结点个数
  2. 在深度为k(k>=1)的二叉树上,最多有2^k-1个结点(满二叉树)
  3. 对于任意的二叉树,假设度为0的结点有n0个,度为2的结点有n2个,则关系为:n0 = n2+1
  4. 具有n个结点的完全二叉树,则深度为[log2(n)]+1 (运算符[x]的意义是不超过x的最大正数)
  5. 对于完全二叉树,根据满二叉树的性质逆推,则有以下性质,假设某结点的序号为i
    1. 如果i为1,则无双亲,对于i>1,则双亲结点为[i/2]
    2. 如果2i>n,则说明该结点没有左孩子,否则左孩子的结点为2i
    3. 如果2i+1>n,则说明该结点没有右孩子,否则右孩子的结点为2i+1

5)二叉树性质的推导 

性质3推导

 性质4推导

 6)二叉树的存储

a.顺序存储

用顺序存储来存储二叉树,用一维数组来储存二叉树。要反应逻辑关系,得按照一定规律来存储

按照满二叉树的对应结点的索引位置来存储二叉树,例如,下面数组中用0表示该位置没有元素

完全二叉树的顺序存储

 非完全二叉树的顺序存储

 很明显,用顺序存储二叉树,不仅有的空间无法使用,而且动态管理十分麻烦,顺序存储二叉树十分麻烦。

 b.链式存储

链式存储二叉树,不仅很好的满足了二叉树的逻辑关系,也容易实现动态管理,树用链表存储,十分好。对于二叉树,链式存储为了方便分为两种,二叉链和三叉链

对于二叉树的结点,最基本应该有数据域(存放数据),左孩子指针,右孩子指针。

 二叉链能够实现二叉树的所有功能,存储小,简洁,但是不便于访问双亲节点

 三叉链表相比二叉链表,多了一个双亲指针,便于访问双亲结点

 为了简洁性,使用二叉链表这个存储结构表示二叉树,二叉链表的代码为

typedef char elem; //定义基本元素类型

//二叉树的基本数据类型定义
typedef struct BTNode
{
	elem data; //数据域
	struct BTNode* rchild, * lchild; //左孩子和右孩子的指针
}BTNode,*LBTree;

 

3.对二叉树的操作

1)二叉树的遍历

a.遍历二叉树的定义

遍历二叉树(Traversing Binary Tree)是指按照某条搜索路径寻访树中的每个结点,使得每个结点均被访问一次,而且仅被访问一次。遍历二叉树是二叉树的基本操作。

 b.遍历二叉树的方法种类

遍历二叉树可分为,前序遍历,中序遍历和后序遍历。 其中前序,中序和后序是指结点的数据的访问顺序,前序遍历是先访问结点的数据,中序是中间再访问,后序是最后在访问结点数据。如果没有其他规则,默认,先访问左子树,再访问右子树。

举例

例如,访问表达式(a + b*(c-d) - e/f)的二叉树

 前序遍历:- + a * b - c d / e f

 中序遍历:a + b * c - d - e / f

 后序遍历:  a b c d - * + e f / -

可以发现,前序遍历刚好是波兰表达式,中序遍历是中缀表达式(1+1是中缀表达式),而后续遍历是逆波兰表达式。

c.代码实现(递归方法)

前序遍历

//先序遍历法,展示二叉表
int LBTreeShow_P(const LBTree* T)
{
	if (*T == NULL) //如果T的该结点是NULL,返回上一层
		printf("#");
	else //如果T的结点不为NULL,则打印,并且遍历左子树再遍历右子树
	{
		printf("%c", (*T)->data);
		LBTreeShow_P(&((*T)->lchild)); //遍历左子树
		LBTreeShow_P(&((*T)->rchild)); //遍历右子树
	}
	return 1;
}

//先序遍历法,展示二叉表
int LBTreeShow_Pre(const LBTree* T)
{
	LBTreeShow_P(T);
	printf("\n");
	return 1;
}

中序遍历(交换前序的代码位置)

//中序遍历
int LBTreeShow_I(const LBTree* T)
{
	if (*T == NULL)
		printf("#");
	else
	{
		LBTreeShow_I(&((*T)->lchild));
		printf("%c", (*T)->data);
		LBTreeShow_I(&((*T)->rchild));
	}
	return 1;
}

//中序,是先左子树,中间,后边是右子树
int LBTreeShow_In(const LBTree* T)
{
	LBTreeShow_I(T);
	printf("\n");
	return 1;
}

后序遍历

//后续遍历子树,先是左子树,再是右子树,再是结点
int LBTreeShow_A(const LBTree* T)
{
	if (*T == NULL)
		printf("#");
	else //如果结点有意义
	{
		LBTreeShow_A(&((*T)->lchild));
		LBTreeShow_A(&((*T)->rchild));
		printf("%c", (*T)->data);
	}
	return 1;
}

//后续遍历子树,先是左子树,再是右子树,再是结点
int LBTreeShow_Post(const LBTree* T)
{
	LBTreeShow_A(T);
	printf("\n");
	return 1;
}

递归可以用栈来写非递归遍历的方法,这里就不举例了。

2)二叉树的创建

学会了遍历,可以用任意一种遍历方法来创建二叉树。

//先序遍历法创建二叉链树,递归法
int LBTreeCreat(LBTree* T)
{
	//如果二叉链树输入正确,问题是输入缓冲区中还有一个换行符,需要清掉
	//但是如果输入错误,则字符会读取到换行符,就导致异常输入的问题了
	char ch = getchar();
	if (ch == '#') //如果ch是#,则T直接为NULL
		*T = NULL;
	else
	{
		//将元素放入结点中
		*T = (BTNode*)malloc(sizeof(BTNode));
		(*T)->data = ch;
		LBTreeCreat(&((*T)->lchild)); //遍历左结点
		LBTreeCreat(&((*T)->rchild)); //遍历右结点
	}
	return 1;
}

//先序遍历法,创建二叉表,清掉换行符
int LBTreeCreat_Pre(const LBTree* T)
{
	LBTreeCreat(T);
	char ch = getchar(); //清掉换行符
	return 1;
}

3)二叉树的复制

//遍历复制二叉树
int LBTreeCopy(LBTree* T1, const LBTree* T2)
{
	if (*T2 == NULL)
		*T1 = NULL;
	else
	{
		*T1 = (BTNode*)malloc(sizeof(BTNode));
		(*T1)->data = (*T2)->data;
		LBTreeCopy(&((*T1)->lchild), &((*T2)->lchild)); //复制左子树
		LBTreeCopy(&((*T1)->rchild), &((*T2)->rchild)); //复制右子树
	}
	return 1;

 4)二叉树的深度计算

//计算树的深度
int LBTreeDepth(const LBTree T)
{
	if (T == NULL)
		return 0;
	else
	{
		int m = LBTreeDepth(T->lchild);
		int n = LBTreeDepth(T->rchild);
		return (m > n) ? m + 1 : n + 1;
	}
}

  5)二叉树的结点个数计算

//统计树的结点个数
int LBTreeCount(const LBTree T)
{
	if (T == NULL)
		return 0;
	else
		return LBTreeCount(T->lchild) + LBTreeCount(T->rchild) + 1;
}

//统计树中度为0的结点个数
int LBTreeCount0(const LBTree T)
{
	if (T == NULL) //如果T为空节点直接返回
		return 0;
	else if (T->lchild == NULL && T->rchild == NULL) //如果T的度为0的结点
		return 1;
	else //如果是其他情况,说明度为1或者2,则计算俩结点的个数返回
		return LBTreeCount0(T->lchild) + LBTreeCount0(T->rchild);
}

//统计树中度为1的结点个数
int LBTreeCount1(const LBTree T)
{
	if (T == NULL) //如果T为空结点,直接返回0
		return 0;
	else if ((T->lchild != NULL && T->rchild == NULL) || (T->lchild == NULL && T->rchild != NULL))
		return LBTreeCount1(T->lchild) + LBTreeCount1(T->rchild) + 1;
	else
		return LBTreeCount1(T->lchild) + LBTreeCount1(T->rchild);
}

//统计树中度为2的结点个数
int LBTreeCount2(const LBTree T)
{
	if (T == NULL)
		return 0;
	else if (T->lchild && T->rchild)
		return LBTreeCount2(T->lchild) + LBTreeCount2(T->rchild) + 1;
	else
		return LBTreeCount2(T->lchild) + LBTreeCount2(T->rchild);

 3.线索二叉树

线索二叉树(Thread Binary Tree)是按照前序,中序或后续的顺序将二叉树串联起来,为每个结点找到了前驱和后继,将非线性关系转化成线性的一种二叉树,其中按照某种规律,将结点连接起来,就是将二叉树线索化。

 1)二叉链结构的改进

对于有n个结点的二叉树,如果用二叉链结构,则一定会有n+1个连接都指向了NULL(假设将NULL看为一个结点,则非NULL结点的度肯定为2,而NULL结点度为0,由二叉树的性质可以知道),如何利用好这n+1个NULL链接?

2)线索二叉树的结构

将NULL的链接可以指向结点的前驱和后继,为了标志该指针到底是指向子树还是前驱和后继结点,可以设立标志,区分,这就是线索二叉树的标志。

代码实现

typedef char elem; //声明元素类型

typedef enum{chd,pnt} PTag; //子代标签

//线索二叉树
typedef struct ThrBTNode
{
	elem data; //数据域
	struct ThrBTNode* lchild, * rchild; //左子树和右子树的标签
	PTag ltag, rtag; //左标签和右标签
	//若ltag为chd,则指代结点的左子树
	//若rtag为pnt,则ltag指向前继结点,rtag指向后继节点
}ThrBTNode,*ThrBTree;

3)线索二叉树的线索化

在遍历过程中,可以将结点项连接起来,例如下图,中序遍历线索化连接

 如果设立头结点,可以实现双向线索链表,即可以从第一个结点遍历,也可以从最后一个结点遍历

代码实现,采取中序遍历线索化

static ThrBTNode* pre = NULL; //静态全局变量,用于线索查找函数

//中序对二叉树进行线索化的基本函数
Status ThrBTreeThread_I(ThrBTree* T)
{
	//出现的问题,最后一个结点的右子树并没有线索化也没有检查
	if (*T) //如果T不为NULL
	{
		ThrBTreeThread_I(&((*T)->lchild)); //首先遍历左子树
		//对结点进行线索化
		if ((*T)->lchild == NULL) //如果T的左子树指针为NULL,则更改
		{
			(*T)->ltag = pnt;
			(*T)->lchild = pre; //将左子树指针连接到前继结点
		}
		else
			(*T)->ltag = chd;
		if (pre) //如果Pre不为空,则继续判断
		{
			if (pre->rchild == NULL) //如果pre的右子树指针为NULL,则更改
			{
				pre->rtag = pnt;
				pre->rchild = *T;
			}
			else //否则pre的rtag为chd
				pre->rtag = chd;
		}
		pre = *T;

		ThrBTreeThread_I(&((*T)->rchild)); //最后遍历右子树
	}
    return OK;
}

//中序对二叉树进行线索化
Status ThrBTreeThread_In(ThrBTree* T)
{
	pre = NULL; //重置全局变量Pre
	ThrBTreeThread_I(T);
	pre->rtag = pnt;
	pre->rchild = NULL;
    return OK;
}

//头结点对二叉树进行线索化,构建双向循环链表,中序线索化
Status ThrBTreeThreadHead_In(ThrBTNode** head, ThrBTree* T)
{
	pre = NULL; //重置全局变量NULL
	*head = (ThrBTNode*)malloc(sizeof(ThrBTNode));
	(*head)->rchild = *head; //将头结点的后继位置
	(*head)->rtag = pnt; //将rtag设置为child
	if (*T == NULL) //如果树为空,则head指向自己
	{
		(*head)->ltag = pnt;
		(*head)->lchild = *head;
	}
	else
	{
		(*head)->ltag = chd;
		(*head)->lchild = *T;
		ThrBTreeThread_In(T);
		pre->rchild = *head; //更改最后一个结点的后继位置,改为head
		(*head)->rchild = pre; //同时更改头结点的右子树后继结点指向
	}
    return OK;
}

 4)利用线索进行遍历

 中序线索遍历,实现条件较为简单,因此用中序线索较好

代码实现

/遍历头结点的线索二叉树,线索遍历,中序遍历
Status ThrBTreeShow_HT(const ThrBTree HT)
{
	ThrBTNode* p = HT->lchild; //p首先是HT的根结点,HT的第一个结点是头结点
	//找到遍历的第一个结点
	while (p->ltag == chd) //如果p还有左孩子,说明p并不是第一个起始的结点
		p = p->lchild; //直到p没有左孩子,说明p才是第一个结点
	while (p != HT) //当p为HT时,遍历结束,因为最后HT的最后一个结点的后继是HT
	{
		printf("%c", p->data); //打印结点p
		//找到结点p的下一个位置
		if (p->rtag == pnt) //如果rtag为pnt,说明p的rchild是后继位置
			p = p->rchild;
		else //如果不是,说明p有右子树,应该是右子树的最左下角位置
		{
			ThrBTNode* temp = p->rchild;
			while (temp->ltag == chd) //如果temp还有左孩子,说明temp并不是最左下角
				temp = temp->lchild;
			p = temp; 
		}
	}
	printf("\n");
	return OK;
}

  5)构建线索的意义

a.时空复杂度的改良

对于遍历结点,二叉树和线索二叉树的时间复杂度都为O(n),都要遍历每一个结点。

对于空间复杂度,二叉树的空间复杂度为O(n),而线索二叉树的空间复杂度为O(1),线索二叉树大大改良了空间的占用。

b.充分利用信息

对于二叉树的指针NULL,如果不用线索二叉树结构,则会有n+1个指针指向NULL,浪费了空间。而且根据某一顺序找前驱和后继也麻烦。

而用线索二叉树,不仅将n+1个指针利用了起来,指向结点的前驱和后继,这样更方便。

因此,线索二叉树改良了空间复杂度,将NULL指针指向前驱和后继,充分利用了空间。

猜你喜欢

转载自blog.csdn.net/DADONGOOO/article/details/128787070
今日推荐