数据结构与算法碎片积累(四)

引言:这一章主要介绍的是树结构,包括其中的基本树概念,然后重点学习二叉树。
20201210

1、树是啥?
答:
1)树的定义:树(tree)是n(n>0)个节点的有限集。当n=0时,为空树,在任一棵非空树中:
1-1)有且仅有一个特定的称为根(Root)节点;
1-2)当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1、T2、…、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree);
2)根概念的理解注意点:
2-1)n>0时,根节点是唯一的,坚决不能存在多个根节点;
2-2)m>0时,子树的个数是没有限制的,但是子树间一定没有相交的。

2、树结点那些事。
答:
1)结点分类:根结点、内部结点、叶结点;
2)度:结点拥有子树(孩子)个数目称为结点的度(Degree)。(树的度是其结点集中最大度值)

3)结点间关系:
child:结点的子树称为结点的孩子(child);
parent:上述的结点称为孩子的双亲(parent);
sibling:双亲的孩子间,互称为兄弟姐妹(sibling);
结点的祖先:从根结点到该结点所经分支所有结点。

4)结点的层次(level):
根结点属于第一层,孩子属于第二层,以此类推分层;
树中结点的最大层次称为树的深度(depth)或高度。

1)-4)例子解析
在这里插入图片描述
A为根结点;B、C为内部节点;D、E、F、G、H为叶结点;

A的度为2,B的度为3,C的度为2,所以树的度为3;

D结点的祖先就是B、A;
B结点就是A结点的孩子child;
A结点就是B结点的双亲parent;
B结点就是D、E、F的双亲parent;D、E、F就是结点B的孩子child;D、E、F互称为兄弟sibling。

根据图中的分层,可以知道树的深度depth为3

5)树的其他概念:
如果将树中结点的各子树从左到右是有序的,不可互换,则称该树有序树;否则,该树为无序树;
森林(forest)是m(m>0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合就是森林。

3、树的存储结构
1)单纯的顺序存储结构或者链式存储结构是满足不了树存储的,如果两种综合起来,倒是可以的。
2)为了实现对树结构存储,可以使用的方法:
双亲(parent)表示法、孩子(child)表示法、孩子兄弟(child-sibling)表示法、双亲孩子(parent-child)表示法。

3)代码给出其中两个情况:
双亲表示法:

//树的双亲表示法,结点结构定义
#define MAX_TREE_SIZE 100

typedef int ElemType;

typedef struct PTNode {
    
    
	ElemType data;//结点数据
	int parent;//双亲位置,存放数组的下标(代替使用指针方式)
}PTNode;

typedef struct {
    
    
	PTNode nodes[MAX_TREE_SIZE];
	int r;//根的位置
	int n;//结点数目
};

//这个结构存在很大改进方向,比如需要遍历某个结点得子结点
//或者兄弟结点,需要遍历整个树结构,效率比较低下。

双亲孩子表示法:

//双亲孩子(parent_child)表示法
#define MAX_TREE_SIZE 100
typedef char ElemType;

typedef struct CTNode {
    
    
	int child;//孩子结点的下标
	struct CTNode* next;//指向下一个孩子(parent结点相同)结点的指针
}*ChildPtr;

//表头结构
typedef struct {
    
    
	ElemType data;//存放在树中的结点的数据
	int parent;//存放双亲的下标
	ChildPtr FirstChild;//指向第一个孩子的指针
}CTBox;

//树结构
typedef struct {
    
    
	CTBox nodes[MAX_TREE_SIZE];//结点数组
	int r, n;//分别表示根位置和结点数目
};

4、二叉树那些事
答:
1)学习意义:因为二叉树的使用范围最广,极具代表意义,所以需要重点学习二叉树。
2)二叉树(Binary Tree)定义:二叉树是n(n>0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

3)二叉树特点:
有序性:左、右子树是有顺序的,次序不能颠倒;
度不大于2:每个结点最多有两棵子树(可以是0或者1棵)。

4)五种形态:
空二叉树;
只有一个(根)结点的二叉树;
根结点只有左子树的二叉树;
根结点只有右子树的二叉树;
根结点既有左子树又有右子树的二叉树。

5)二叉树特性体现:三个结点的话,普通树就两种情况,两层或者三层;而二叉树有5种情况:
在这里插入图片描述
其中,上图的左开始数的2,3图是特殊二叉树,斜树。

5、满二叉树
答:
1)定义:在一棵二叉树中,如果所有分支点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
2)特点:
叶子只能出现在最底层;
非叶子节点的度一定是2;
在同样深度的二叉树中,满二叉树的结点个数一定是最多的,同时叶子也是最多的。

6、完全二叉树
1)定义:对应一棵具有n个结点的二叉树,按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同,则这棵二叉树称为完全二叉树。
例子:
在这里插入图片描述
右图相对左图来说,右图属于完全二叉树

2)满二叉一定是完全二叉树,但完全二叉树不一定是满二叉树。

3)加深完全二叉树理解:
满二叉树:
在这里插入图片描述
在这里插入图片描述
7、二叉树的5性质
答:
1)在二叉树的第i层上至多有2^(i-1)个结点(i>=1);
2)深度为k的二叉树至多有(2^k)-1个结点(k>=1);

3)对任何一棵二叉树T,如果终端结点(叶子点)数为n0,度为2的结点(树)为n2,则有关系,n0=n2+1;
4)具有n个结点的完全二叉树的深度为[log2n]+1(底为2的对数取下取整数+1,如2.5下取+1,那么等于2+1);

5)如果一棵有n个结点的完全二叉树的结点按层序编号,对任意结点i(1<=i<=n)有以下特性:
特性一:
如果i=1,则结点i时二叉树的根,无双亲;如果i>1,则其双亲是结点i/2 取下限(值);
特性二:
如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i;
特性三:
如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1。
在这里插入图片描述
8、二叉树的存储结构
答:
1)顺序存储结构,一维数组,按照满二叉树形式编号,存储二叉树中的各个位置对应的结点;但是对应斜树情况,浪费严重,故不提倡使用。
2)国际惯例,二叉树采用的是链式存储结构:

//二叉链表
typedef char ElemType;

typedef struct BiTNode {
    
    
	ElemType data;//数据域
	struct BiTNode* lchild, * rchild;//指针域,指向自身,类似递归样式
}BiTNode,*BiTree;

9、二叉树遍历方式
答:前序遍历、中序遍历、后序遍历、层序遍历;前、中、后序遍历方式是根据根结点位置决定。

1)前序遍历:若二叉树为空,则返回空操作,否则,先访问根结点,然后前序遍历左子树,再前序遍历右子树。(个人巧记:根起,先左后右,同级没有往上级开始重复)
在这里插入图片描述
2)中序遍历:
若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。
在这里插入图片描述
3)后序遍历:
若树为空,则空操作返回,否则,从左到右,先叶子后结点的方式访问先左后右子树,最后访问根结点。
在这里插入图片描述
4)层序遍历:
若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,从左到右顺序对结点逐个访问。
在这里插入图片描述
10、二叉树结构建立代码怎么实现?
答:
1)二叉树遍历概要小结:
前、中、后序遍历方式是根据根结点位置决定。
前序遍历,根结点在前,首先被遍历,然后遍历左子树,接着遍历右子树;
中序遍历,先遍历左子树,然后根结点,然后右子树遍历;
后序遍历,先遍历左子树,接着右子树遍历,最后根结点遍历。

计算机只会处理线性序列,而上述的遍历方式都不是线性序列,所以,需要将其变为某种意义上的线性序列,足以让计算机进行处理。

2)二叉树结构创建注意,一般情况下,我们提倡使用前序遍历方式输入:

#include<stdio.h>
#include <malloc.h>

//结点
typedef char ElemType;

typedef struct BiTNode {
    
    
	char data;
	struct BiTNode* lchild, * rchild;
}BiTNode,*BiTree;

//创建二叉树,约定用户遵循前序遍历方式输入数据
void CreateBiTree(BiTree* T) {
    
    
	char c;
	scanf_s("%c",&c);
	if (' ' == c) //这里要注意,如果该结点的下面对应的没有子结点的,需要使用
		          //空格代替,例如前序方式输入:AB D  CE 输入
	{
    
    
		*T = NULL;
	}
	else
	{
    
    
		*T = (BiTNode*)malloc(sizeof(BiTNode));
		(*T)->data = c;
		CreateBiTree(&(*T)->lchild);//这里加&取地址原因是,这里child是指针
		CreateBiTree(&(*T)->rchild);//也就是检查了指针平衡问题
	}
}//利用递归方式创建完成了二叉树的创建
//这里采用递归的原因是,因为我们不需要知道树的深度,这也是不使用迭代原因

//访问二叉树结点的具体操作
void visit(char c,int level) {
    
    
	printf("%c位于第%d层\n", c, level);
}

//遍历二叉树,前序遍历方式
void PreOrderTravere(BiTree T, int level) //level记录保存所在的层数
{
    
    
	if (T)
	{
    
    
		//前序遍历
		visit(T->data, level);
		PreOrderTravere(T->lchild, level + 1);//遍历左子树
		PreOrderTravere(T->rchild, level + 1);//遍历右子树

		后序遍历
		//PreOrderTravere(T->lchild, level + 1);//遍历左子树
		//PreOrderTravere(T->rchild, level + 1);//遍历右子树
		//visit(T->data, level);

		中序遍历
		//PreOrderTravere(T->lchild, level + 1);//遍历左子树
		//visit(T->data, level);
		//PreOrderTravere(T->rchild, level + 1);//遍历右子树
	}
}

int main() {
    
    
	int level = 1;
	BiTree T = NULL;

	CreateBiTree(&T);
	PreOrderTravere(T, level);

	return 0;
}//测试时候,一定要按照给定的遍历方式输入,然后没有元素的左右结点需要给
 //空格代替输入

11、二叉树结构代码改进版本_线索二叉树。
答:
1)改进原因:普通二叉树的结点由两个指针和一个数据域组成,容易造成时间空间的浪费;同时,不要前驱后继方式遍历,导致遍历效率较低;

2)改进版本:线索二叉树。
采用中序遍历方式(前序、后序遍历方式都不能满足前后继指针保存),刚好保存前驱后继指针的功能,在结点结构上增加左右标签,提示左右孩子指针到底保存的是孩子,还是前驱后继地址。

3)线索二叉树代码实现:
(构造二叉树结构还是使用前序遍历方式输入的,但是,就是在使用中序遍历方式,添加前驱后继指针定义下了很大功夫)

#include<stdio.h>
#include<stdlib.h>

typedef char ElemType;

//线索存储标志位
//Link(0);表示* lchild, * rchild指向的是孩子的指针
//Thread(1);表示* lchild, * rchild是指向前驱后继(的线索)
typedef enum {
    
    Link,Thread} PointerTag;

typedef struct BiThrNode {
    
    
	char data;
	struct BiThrNode* lchild, * rchild;
	PointerTag ltag;
	PointerTag rtag;
}BiThrNode,*BiThrTree;

//全局变量,始终指向刚刚访问过的结点
BiThrTree pre;

//创建一棵二叉树,约定用户遵守前序遍历方式输入数据
void CreateBiThrTree(BiThrTree *T) {
    
    
	char c;

	scanf_s("%c", &c);
	if (' ' == c) //注意输入的结点没有孩子时,需要使用空格代替
	{
    
    
		*T = NULL;
	}
	else {
    
    
		*T = (BiThrNode *)malloc(sizeof(BiThrNode));
		(*T)->data = c;
		(*T)->ltag = Link;
		(*T)->rtag = Link;

		//递归调用,创建左右子树
		CreateBiThrTree(&(*T)->lchild );
		CreateBiThrTree(&(*T)->rchild);
	}
}
//通过前序遍历方式创建二叉树(推荐的),可以使用其他遍历方式访问,如中序遍历

//中序遍历方式访问创建的二叉树
//又称  中序遍历线索化
void InThreading(BiThrTree T) {
    
    
	if (T) {
    
    
		InThreading(T->lchild);//递归左孩子线索化(一直检索,直到到左边最底
							   //层结点)

		//结点处理
		if (!T->lchild) //表示没有指向孩子时,对ltag进行修改
						//如果该结点没有左孩子,设置itag为Thread,并把lchild
						//指向刚刚访问的结点
		{
    
    
			T->ltag = Thread;
			T->lchild = pre;//这里体现了,先假设存在,然后调用;最后,逻辑
							 //链接一起,原来假设存在的真的存在了(递归思路)
		}

		if (!pre->rchild) //表示刚刚走过的结点没有右孩子,那么修改rtag并
						  //使得其,rchild指向下一个地址
		{
    
    
			pre->rtag = Thread;
			pre->rchild = T;//上一个rchild指向刚刚访问的结点
		}

		pre = T;//走完这些步骤,把刚访问的结点交给pre,下面开始新的结点

		InThreading(T->rchild);//递归右孩子线索化
	}
}//这里结点处理很难理解,一个是前驱时候,选择的是左指针,选择当前的结点进行判断;
 //另一个后驱时候,选择的是右指针,选择上一节点作为判断。这是为啥呢?

//从另一个角度思考,我们拥有的是当前结点信息,以及刚走过的结点信息。
//找前驱指向时,我们目的是,将当前结点的左孩子指针指向其前驱结点,也就是刚走过结点,
//这理解简单;
//找后驱指向时,我们希望可以找到当前结点的后一结点位置,然后直接让结点右指针直接指向
//后一结点,但是,我们手里面并没有当前结点后一结点位置信息,所以行不通这样子;但是,
//反过来考虑,刚走过的结点的后一结点不就是当前节点吗?好了,找到方法了,我们可以把
//刚走过的结点的右孩子指针指向当前节点,可以达到右孩子指针指向后继结点目的。


//这函数解决pre还没有初始化就调用导致报错问题
void InOrderThreading(BiThrTree* p ,BiThrTree T) {
    
    
	*p = (BiThrTree)malloc(sizeof(BiThrNode));
	(*p)->ltag = Link;
	(*p)->rtag = Thread;
	(*p)->rchild = *p;
	if (!T) //如果是空树,那么,指向本身
	{
    
    
		(*p)->lchild = *p;
	}
	else {
    
    
		(*p)->lchild = T;
		pre = *p;//初始化pre

		InThreading(T);
		pre->rchild = *p;
		pre->rtag = Thread;
		(*p)->rchild = pre;
	}
}//这个还没有理解

void visit(char c) {
    
    
	printf("%c", c);
}

//上述的中序遍历二叉树采用的是递归方式
//下面依旧是采用中序遍历二叉树,非递归方式访问
void InOrderTraverse(BiThrTree T) {
    
    
	BiThrTree p;
	p = T->lchild;

	while (p!=T)
	{
    
    
		while (p->ltag == Link) {
    
    
			p = p->lchild;
		}//直到走到左子树最底层结点,左叶子
		visit(p->data);

		while (p->rtag==Thread&&p->rchild!=T)//表示有后继
		{
    
    
			p = p->rchild;//访问后继
			visit(p->data);//打印后继数据
		}

		//收尾工作
		p = p->rchild;
	}
}

int main()
{
    
    
	printf("请使用前序遍历方式(注意,无孩子点采用空格代替)输入:\n");
	BiThrTree p,T = NULL;

	CreateBiThrTree(&T);
	InOrderThreading(&p,T);

	printf("中序遍历(非递归)的输出结果为:\n");
	InOrderTraverse(p);//传进去的是头指针
	
	printf("\n");

	return 0;
}
//这节要学会断点调试来调bug
//注释写了不少,感觉在递归面前,我像个尘民,还是很迷(惑)。

小结:
了解基本树的概念以及性质,重点学习二叉树。记得构建二叉树时候,使用的是前序遍历方式输入的;为了使得二叉树可以前驱后继双向检索,引出了线索二叉树。线索二叉树构建时,开始输入还是前序遍历方式,后面需要定义部分的结点前驱后继指针,需要使用中序遍历方式进行初始化这部分结点左右孩子指针,变为前驱后继指针。毫无疑问,都是使用了递归方式来解决,即使写了很多注释,但还是觉得很难,看来还是我太菜了。

#########################
不积硅步,无以至千里
好记性不如烂笔头
感谢授课老师
截图权力归原作者所有

猜你喜欢

转载自blog.csdn.net/qq_45701501/article/details/110992230