C++学习第七篇——树(二叉树)

树结构作为一种数据结构,在算法中有着十分重要的作用。所以本篇的篇幅可能会很长,理论知识很多。树结构是一种数据结构它由结点(node)以及连接结点的边(edge)组成。
根是一个树的特殊结点,因为根是唯一一个没有父结点的结点,叶子结点是指没有子节点的结点。
如图所示,
在这里插入图片描述
0结点是整个树的根结点,这里有一个对于树的概念就是,我们通常会将结点的子节点数称为度,从上图来看0,2两个点的度即为3,结点1,3,8三个点的度即为2,结点4,5,6,7,9,10,11,12的度即为0。
接着,从根结点到其他结点x的路径长度称为结点x的深度,在一个树中根结点的深度最深,就好比现实生活中的大树一样,根里上面的叶子是最远的。
对树结构有了大致的了解之后,那么开始对二叉树进行学习。
二叉树是指,一个树拥有一个根结点,并且所有结点的子结点数都不超过2,那么这棵树称为二叉树。
如图所示,
在这里插入图片描述
从上图来看,我们可以发现一个二叉树的左右结点看起来与二叉树十分相似,也就是说二叉树本身是一个递归下去的树形结构。
那么我们可以把它分成这样的三个部分:

  • 结点值
  • 左子树(二叉树)
  • 右子树(二叉树)
    c++建树如下
struct Tree
{
	int val,left,right;
}
struct Tree T[MAX];
//或者
int val[MAX],left[MAX],right[MAX];

树的深度(递归)

int getDepth(struct Tree a)
{
	int l=0,r=0;
	if(a.left!=0)//因为根结点为0,所以默认左右子树不能指向0
	{
		l=getDepth(T[a.left]);
	}
	if(a.right!=0)
	{
		r=getDepth(T[a.right]);
	}
	return max(l,r)+1;
}

当前结点的深度是指它左子树的深度与右子树的深度的最大值再加上本身这层1。
树的宽度(递归)

int ans;
int width(struct Tree a)
{
	int l=0,r=0;
	if(a.left!=0)
	{
		l=width(T[a.left]);
	}
	if(a.right!=0)
	{
		r=width(T[a.right]);
	}
	ans=max(ans,l+r+1);//取左子树高度+右子树高度+本身结点1,最大值存到ans
	return max(l,r)+1;
}

和高度的相比你会发现,它们之间的差距只有一行代码,我已经添加注释了。

接下来就是树的遍历了,我们先给每个结点的val赋值

for(int i=0;i<=6;i++)
{
	T[i].val=i;
}

前序遍历,根左右,对一个二叉树,我们先输出它的根,再输出它的左子树和右子树。

void preorder(struct Tree a)
{
	printf("%d ",a.val);
	if(a.left!=0)
	{
		preorder(T[a.left]);
	}
	if(a.right!=0)
	{
		preorder(T[a.right]);
	}
}

中序遍历,左根右,同理,先输出左子树,再输出根,再输出右子树。

void midorder(struct Tree a)
{
	if(a.left!=0)
	{
		midorder(T[a.left]);
	}
	printf("%d ",a.val);
	if(a.right!=0)
	{
		midorder(T[a.right]);
	}
}

后序遍历,左右根的顺序,同理。

void backorder(struct Tree a)
{
	if(a.left!=0)
	{
		backorder(T[a.left]);
	}
	if(a.right!=0)
	{
		backorder(T[a.right]);
	}
	printf("%d ",a.val);
}

二叉树的遍历会对树的每一个结点进行一次访问,因此算法复杂度为O(n)。但使用递归实现遍历算法时要注意,一旦树的节点数量庞大且分布不均,很可能导致递归深度过深。
最后是重点,构建二叉树,我们会发现前序和后序的根分别在左子树或右子树的端点处,根是一个独立的存在,左右子树通常聚集在根的左侧或根的右侧,很难分开。而中序的话,它的根隐藏在中间,我们很难从中看出哪一个是根,但是一旦找到了根之后,左右两侧的子树就划分的很明显了。
所以我们可以利用前序或者后序的找根,来到中序里划分左右子树,这样便可清晰可见。
按照上图为例,举例前中序建树
前序遍历为:0 1 3 4 2 5 6
中序遍历为:3 1 4 0 5 2 6

int pos;
vector <int> pre,mid;
	pre.push_back(0);
	pre.push_back(1);
	pre.push_back(3);
	pre.push_back(4);
	pre.push_back(2);
	pre.push_back(5);
	pre.push_back(6);
	mid.push_back(3);
	mid.push_back(1);
	mid.push_back(4);
	mid.push_back(0);
	mid.push_back(5);
	mid.push_back(2);
	mid.push_back(6);
	rec(0,7);

先给出建树代码

int rec(int l,int r)//前中
{
	if(l>=r) return 0;//当左侧>=右侧时已经不满足建树,返回0,但并不是指向根,因为根没有父节点
	int  root = pre[pos++];
	int m=distance(mid.begin(),find(mid.begin(),mid.end(),root));//找出根的位置
	T[root].left=rec(l,m);
	T[root].right=rec(m+1,r);
	return root;
} 

pos用来表示当前位置,如果是前序的话,根结点在起始,也就从0开始,反之从末尾开始往前。

下标(pos) 0 1 2 3 4 5 6
前序 0 1 3 4 2 5 6
中序 3 1 4 0 5 2 6

当我们找到一个根的时候,就会从mid中找出它的位置,在mid中以此根为左的结点组成它的左子树,以此根为右的结点组成它的右子树,接着,我们去建立左子树,那么左子树的结点便是从当前子树的最左侧l到当前根结点m之间所有的结点,右子树呢,则是从根结点的左侧m+1至当前子树的最右侧r,依次二分下去,返回当前根结点。
分析一下,第一个出现的根为0,然后我开始对m(0)左侧的3 1 4进行建树,pos++后我找到了下一个根为1,继续对m(1)左侧的3开始建树,然后返回当前的3作为1的左子树,对m(1)右侧的4开始建树,然后返回当前的4作为1的右子树,自此结束对1的左右子树建立,返回当前根1作为0的左子树。此时pos已经到了4,开始对m(0)右侧的5 2 6进行建树,找到下一个根为2,对m(2)左侧的5开始建树,返回5作为2的左子树,同理返回6作为2的右子树,2的左右子树完成后,返回当前root=2,作为0的右子树。树建立完毕,你可以尝试使用前面的遍历代码去进行求证。
如果理解了前序和中序建树,不妨试试看后序和中序建树。
对照一下我给出的,看看哪里不一样


int reb(int l,int r)
{
	if(l>=r) return 0;
	int  root = back[pos--];//在赋值阶段将pos赋值为back最后一个值的下标
	int m=distance(mid.begin(),find(mid.begin(),mid.end(),root));
	T[root].right=reb(m+1,r);
	T[root].left=reb(l,m);
	return root;
}
发布了13 篇原创文章 · 获赞 0 · 访问量 493

猜你喜欢

转载自blog.csdn.net/SmallHedgehog/article/details/105458953