树
树的基本知识
树的结构
树结构是一种数据结构,由节点以及连接节点的边构成。
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。
证明:
对于节点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 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 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;
}