树的基础知识

本文介绍了树的基本概念,包括有根树、叶节点和内部节点的定义,以及节点的度和深度。重点讲解了二叉树的性质,如层节点数、度为2的节点与叶节点的关系。还详细阐述了二叉树的表达方式、节点深度的计算以及二叉树的遍历方法,包括前序、中序和后序遍历。最后讨论了如何根据前序和中序遍历反推二叉树结构。

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

树的基本知识

树的结构

树结构是一种数据结构,由节点以及连接节点的边构成。

1
2
3
4
5
6
7
8
9
10

1:如果一棵树具有根节点,那么称其为有根树
2:没有子节点的节点称其为叶节点(5, 7,8,9,10)
3:除叶节点外的,称为内部节点
4:有根树中节点拥有的子节点个数称为该节点的度,如1的度为3
5:从根节点到节点x的路径长度称为节点x的深度,从节点x到叶节点的距离称为节点x的高。

二叉树

如果一棵树具有根节点,且每个节点的子节点的个数都不超过2,那么该树就是二叉树(各个节点之间不含有相同子节点)

二叉树的性质

1:第i层的最大节点数为2^(i-1),i>=1;
2:深度为k的二叉树所拥有的节点数不超过2^k-1;(等比数列求和)
3:对于任何非空二叉树,叶节点的数目是度为2的节点的总数加一。
证明:
我们从根节点出发,此时度为2的节点为0,叶节点为1;如果增加一个节点,那么度为2的节点数不会增加,叶节点数也还是1,如果增加两个节点,度为2的节点数为1,但是叶节点数为2.无论如何,要想增加一个度为2的节点,势必增加2个度为0的点,但是两者之差恒为1
4:对于二叉树的任意一个节点,设其下标为x,左子树为2x,右子树为2x+1。
证明:

1
2
3
4
5
6
7

对于节点3而言,由于二叉树是从上到下,从左到右进行编号,那么在对3的子树进行编号前,1,2的子节点一定已经完成了编号(且全都具备2个子节点),那么22+3号节点本身+3的左节点=2(n-1)+2=2*n;

二叉树的表达

法一:链表法

#include<iostream>
using namespace std;
struct node
{
	int v;
	node* left;
	node* right;
};
node* root;
void createtree(int x)
{
	root = new node;
	root->v = x;
	root->left = NULL;
	root->right = NULL;
}
int main(void)
{
	createtree(0);
	return 0;
}

法二:模拟链表存储

#include<iostream>
using namespace std;
const int maxn = 1000;
int tree[maxn];
int root=1;
int Left[maxn];
int Right[maxn];
void createtree(int v)
{
	tree[root] = v;
	Left[root] = 0;
	Right[root] = 0;
}
int main(void)
{
	createtree(1);
	return 0;
}

计算节点的深度

int setdepth(int u,int d)
{
    d=0;
    while(p[u]!=NULL)
    {
       u=t[u].p;
       d++;
    }
    return d;
}

递归计算所有节点的深度

void setdepth(int u,int p)
{
    D[u]=p;
    if(T[u].left)
    {
       setdepth(T[u].left,p+1);
    }
    if(T[u].right)
    {
       setdepth(T[u].right,p+1);
    }
}

二叉树的遍历

中序遍历(inorder)

前序遍历即按照左子树-->根节点-->右子树的顺序遍历二叉树
void preorder(int root)
{
    if(T[root].left!=0)
    {
       preorder(T[root].left]);
    }
    printf("%d",T[root].value);
    if(T[root].right!=0)
    {
       preorder(T[root].right);
    }
}

前序遍历(preorder)

中序遍历即按照根节点-->左子树-->右子树的顺序遍历二叉树
void inorder(int root)
{
    printf("%d",T[root].v);
    if(T[root].left!=0)
    {
       inorder(T[root].left);
    }
    if(T[root].right!=0)
    {
        inorder(T[root].right);
    }
}

后序遍历(postorder)

后序遍历即按照左子树-->右子树-->根节点的顺序遍历二叉树
void postorder(int root)
{
    if(T[root].left!=0)
    {
       postorder(T[root].left);
    }
    if(T[root].right!=0)
    {
       postorder(T[root].right);
    }
    printf("%d",root);
}

完整测试代码:

#include<iostream>
using namespace std;
const int maxn = 100;
int root = 0;
int Left[maxn];
int Right[maxn];
int tree[maxn];
void inorder(int root)
{
	if (Left[root] != 0)
	{
		preorder(Left[root]);
	}
	printf("%d ", tree[root]);
	if (Right[root] != 0)
	{
		preorder(Right[root]);
	}
}
void preorder(int root)
{
	printf("%d ", tree[root]);
	if (Left[root] != 0)
	{
		inorder(Left[root]);
	}
	if (Right[root] != 0)
	{
		inorder(Right[root]);
	}
}
void postorder(int root)
{
	if (Left[root] != 0)
	{
		postorder(Left[root]);
	}
	if (Right[root] != 0)
	{
		postorder(Right[root]);
	}
	printf("%d ", tree[root]);
}
int main(void)
{
	root = 1;
	for (int i = 1; i <= 7; i++)
	{
		tree[i] = i;
		if (2 * i <= 7)
		{
			Left[i] = 2 * i;
		}
		if (2 * i + 1 <= 7)
		{
			Right[i] = 2 * i + 1;
		}
	}
	preorder(1);
	cout << endl;
	inorder(1);
	cout << endl;
	postorder(1);
	return 0;
}

推断二叉树

根据前序和中序遍历反推二叉树

举个例子

1
2
3
4
5
6
7

在这张图中:
前序遍历是 1 2 4 5 3 6 7
中序遍历是 4 2 5 1 6 3 7
由于前序遍历按照 根节点–>左子树–>右子树的顺序
中序遍历按照 左子树–>根节点–>右子树的顺序
首先,1是根节点,在中序遍历中找到1,下标为4,那么左子树的大小为3,右子树的大小也为3。
从而可以确定,左子树在前序遍历中表现为2 4 5,在中序遍历中表现为4 2 5,右子树在前序遍历表现为 3 6 7,在中序遍历中表现为6 3 7。成功将两个子树分解出来。

我们可以很容易的发现,前序遍历的第一项就是根节点,而我们在中序遍历找到根节点的位置后,根节点位置的左边部分就是左子树,右边的部分就是右子树。
由此我们知道,前序遍历提供根节点的位置信息,中序遍历提供子树的大小长度。而子树的大小在数组中又表现为下标之差的绝对值。因此,我们通过递归可以不断的把树分解,从而重构二叉树。

void rebuild(int pre, int in, int n, int root)
{
    pre参数的意思是左子树在preorder数组的起始位置,in是左子树在inorder数组
    的起始位置,n表示子树的大小,root表示现在正在处理的节点在二叉树中的序号
	if (n <= 0)//如果子树的大小小于等于0,终止递归
	{
		return;
	}
	tree[root] = preorder[pre];//找到根节点,按顺序赋值
	int i;
	for (i = in; i <= 7; i++)//在中序遍历中寻找根节点的下标
	{
		if (inorder[i] == preorder[pre])
		{
			break;
		}
	}
	int lefttree = i - in;//左子树的大小
	int righttree = n - lefttree - 1;//右子树的大小
	rebuild(pre + 1, in, lefttree, 2 * root);
	rebuild(pre + lefttree + 1, i + 1, righttree, 2 * root + 1);
}

根据中序和后序遍历反演二叉树

原理类似,只不过提供根节点位置的变成了postorder数组,由于postorder的顺序是左子树–>右子树–>根节点,所以我们每次获取根节点值的时候跟之前的代码不同.

void restablish(int lpost,int rpost,int in,int n,int root)
{
	if (n <= 0)
	{
		return;
	}
	tree[root] = postorder[rpost];
	int i;
	for (i = in; i < in + n; i++)
	{
		if (inorder[i] == postorder[rpost])
		{
			break;
		}
	}
	int lefttree = i - in;
	int righttree = n - lefttree - 1;
	restablish(lpost, lpost + lefttree - 1, in, lefttree, 2 * root);
	restablish(rpost - righttree, rpost - 1, i + 1, righttree,2 * root + 1);
}

根据前序和中序遍历写出后序遍历

当然可以通过之前反演二叉树的代码直接反演出二叉树,然后再对二叉树进行后序遍历,但是这样过于麻烦。思想是一样,我们甚至不用储存二叉树中的内容,直接调整输出的顺序就可以了。

void invert(int pre, int in, int n)
{
	if (n <= 0)
	{
		return;
	}
	else
	{
		int i;
		for (i = in; i <in + n; i++)
		{
			if (inorder[i] == preorder[pre])
			{
				break;
			}
		}
		int lefttree = i - in;
		int righttree = n - lefttree - 1;
		invert(pre + 1, in, lefttree);
		invert(pre + lefttree +1, i + 1, righttree);
		printf("%d ", preorder[pre]);
	}
}

根据中序和后序遍历写出前序遍历

void invert2(int post, int in, int n)
{
	if (n <= 0)
	{
		return;
	}
	else
	{
		printf("%d ", postorder[post]);
		int i;
		for (i = in; i < in + n; i++)
		{
			if (inorder[i] == postorder[post])
			{
				break;
			}
		}
		int lefttree = i - in;
		int righttree = n - lefttree - 1;
		invert2(post - righttree-1, i - lefttree, lefttree);
		invert2(post - 1, i + 1, righttree);
	}
}

前序加后序能够确定二叉树吗?

1
2
3
4
5
6
7

前序遍历:1 2 4 5 3 6 7 根节点–>左子树–>右子树
中序遍历:4,2,5,1,6,3,7 左子树–>根节点–>右子树
后序遍历:4 5 2 6 7 3 1 左子树–>右子树–>根节点
前序遍历中根节点在数组的最左边,后序遍历中根节点在数组的最右边,我们只能确定根节点在数组中的位置,但是无法通过根节点的位置来分割二叉树,如果使用递归函数去解析二叉树,会出现问题,原因是无法确定左子树和右子树的大小,也就无法通过分割数组的方法来区分左右子树。

完整测试代码

#include<iostream>
using namespace std;
int preorder[10] = {1,2,4,5,3,6,7};
int inorder[10] = {4,2,5,1,6,3,7};
int postorder[10] = { 4,5,2,6,7,3,1 };
void invert1(int pre, int in, int n)
{
	if (n <= 0)
	{
		return;
	}
	else
	{
		int i;
		for (i = in; i <in + n; i++)
		{
			if (inorder[i] == preorder[pre])
			{
				break;
			}
		}
		int lefttree = i - in;
		int righttree = n - lefttree - 1;
		invert1(pre + 1, in, lefttree);
		invert1(pre + lefttree +1, i + 1, righttree);
		printf("%d ", preorder[pre]);
	}
}
void invert2(int post, int in, int n)
{
	if (n <= 0)
	{
		return;
	}
	else
	{
		printf("%d ", postorder[post]);
		int i;
		for (i = in; i < in + n; i++)
		{
			if (inorder[i] == postorder[post])
			{
				break;
			}
		}
		int lefttree = i - in;
		int righttree = n - lefttree - 1;
		invert2(post - righttree-1, i - lefttree, lefttree);
		invert2(post - 1, i + 1, righttree);
	}
}
int main(void)
{
	invert1(0,0,7);
	printf("\n");
	invert2(6, 0, 7);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我还是忘不了

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

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

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

打赏作者

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

抵扣说明:

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

余额充值