一、树,森林,二叉树的简单介绍
1.树的概念
1)定义:树是由节点和边组成的图形结构,每个节点可以连接到其他节点,形成一棵树形结构。
如图为一个简单的树。
2)子树: 子树是由某个节点及其所有后代节点组成的树。如图,B结点所延伸的树即为A结点的子树。
3)根节点:延伸子树的结点为这个数的根节点。如图,A结点为B、C、D三个子树的根节点。
4)结点的度:结点拥有的子树数称为结点的度。
5)叶子:度为0的结点称为叶子结点(或终端结点)。
6)树的度:树内各结点的度的最大值。
7)树的深度:树中结点的最大层次。
2.森林的概念
定义:m(m>=0)颗互不相交的树的集合。
3.二叉树的概念
1)定义:二叉树是一种特殊的树,每个结点至多只有两颗子树(即二叉树的度只能为2)
2)二叉树的基本形态
3)满二叉树:它的每个节点要么没有子节点,要么有两个子节点。同时,它的每一层节点数都是满的(叶子结点无空缺)。
4)完全二叉树:除了最后一层节点不满外,其它层节点数都达到最大值,并且最后一层所有节点都连续靠左排列(连续编号,只能缺一角)
二、二叉树
1.二叉树、树、森林之间的转换
1)树 => 二叉树
最左边的子节点 => 父结点的左子树
其兄弟结点 => 其右子树
例:如图,这是一个不同的树
我们采用一种简单的连线法,如下图
将每一层的兄弟结点连接起来。
除了最左侧的子结点,被新线连接的子结点销毁掉原有的旧线。
所有旧线相连的子结点为左结点,新线相连为右结点。
结论:通过观察,无论什么树,其转化为二叉树后,根结点都没有右子树
2)二叉树 => 树
将上述方法反推即可。
将所有的右结点与其祖先结点相连,删除旧线即可得到
3)森林 => 二叉树
例:如下图,为一个由三颗树组成的森林
先利用之前的方法,将上面的所有树转换为二叉树
然后,我们以第一颗树为主树,将其他的树从根结点其次向右连接
结论:相比于树的转换,森林转换为二叉树后,根结点有右子树
4)二叉树 => 森林
仍然根据上述方法反推。
同时,如果不确定将二叉树如何转换,可以判断根结点是否有右子树,来确认如何转换
2.二叉树的性质
1)二叉树的节点数为n,它的边数为 n-1。
2)在二叉树中,第 i 层上最多有 2 ^ (i -1) 个节点。
3)如果二叉树的深度为k,至多有 2^k - 1 个结点。
4)对于任意的一棵二叉树,如果其度为0的结点数为n0,度为2的结点数为n2,则:
n0 = n2 + 1
5)如果一个完全二叉树的节点数为 n,则它的深度为:(max:取不小于x的最大整数)
6)一颗有 n 个结点的完全二叉树,对于任意一个结点 i,结点的顺序为从上往下,从左往右:
对于一个拥有左右孩子的结点来说,其左孩子为 2i,右孩子为 2i + 1。
如果 i = 1,那么此结点为二叉树的根结点,如果 i > 1,那么其父结点就是 ,比如第 3个结点的父结点为第1个节点,也就是根结点。
如果 2i > n,则结点 i 没有左孩子,比如下面图中的二叉树,n 为 5,假设此时 i = 3,那么 2i = 6 > n = 5 说明第三个结点没有左子树。
如果 2i + 1 > n,则结点 i 没有右孩子。
如上图所示,可以仔细观察一下性质6
3.二叉树综合案例(关于性质)
1)如果有 n 个相同的结点,可以组成多少种不同的二叉树
思路:动态规划
可知,没有结点和只有1个结点的情况:dp[0] = dp[1] = 1
2个结点:dp[2] = dp[0] * dp[1] + dp[1] * dp[0]
3个结点:dp[3] = dp[0] * dp[2] + dp[1] * dp[1] + dp[2] * dp[0]
依次类推,可以使用双循环打表,下面是代码:
#include<iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int dp[1001] = {0};
dp[0] = dp[1] = 1;
for (int i = 2; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
dp[i] += dp[-1 + j] * dp[i - j];
}
}
cout << dp[n];
return 0;
}
2)一颗完全二叉树有 n 个结点,请问它有多少个叶子结点
思路:利用性质5,求深度k,然后求前 k - 1 层的全部结点数,分别求倒数第一层和第二层的叶子结点。下面为代码演示:
#include<iostream>
#include<cmath>
using namespace std;
int main()
{
int n;
cin >> n;
// 求完全二叉树的深度
int k = log(n) / log(2) + 1;
// 求前k-1层的叶子结点
int sum = pow(2, k - 1) - 1;
// 求倒数第一、二层的叶子结点
int x = n - sum;
int y = pow(2,k-2) - (x + 1) / 2;
cout << x + y;
return 0;
}
4.二叉树的构建
1)以数组形式创建
如图,根据二叉树的编号,依次存入数组
利用性质6,可以通过这种形式来找到对应结点
但是,性质6只能应用于完全二叉树,所以这种方式是有缺陷的
2)以链式结构的形式创建
typedef struct TreeNode
{
char element; // 元素
int num; // 编号
struct TreeNode* left; // 左子树
struct TreeNode* right; // 右子树
}Node;
5.二叉树的遍历
1)前序遍历
规则:先遍历左子树并输出,遇空后返回上一个结点,再向右遍历输出
代码思路1:递归
void preOrder1(Node* node)
{
if (node == NULL)
return;
cout << node->element << endl;
preOrder1(node->left);
preOrder1(node->right);
}
代码思路2:非递归 -- 栈(stack) 代码逻辑非常麻烦,不建议使用
void preOrder2(Node* node)
{
stack<Node*>s;
s.push(node);
while (s.size() != 1 || s.top() != NULL)
{
while (s.top() != NULL)
{
cout << s.top()->element << endl;
s.push(s.top()->left);
}
s.pop();
Node* tmp = s.top()->right;
s.pop();
s.push(tmp);
}
}
如代码所示,使用双层循环来实现左右子树的遍历。
先填入根结点,第一层循环的结束条件为:栈中的结点只有1个时,此节点为NULL。
第二层循环用于遍历左子树,直到没有左结点为止。
然后遍历右子树,将为NULL的尾结点出栈。
此时,栈顶元素的左子树遍历完,删除栈顶元素,压入栈顶的右结点。
当栈中只有1个NULL结点时,遍历结束。
2)中序遍历
规则:先遍历左子树,然后在返回时依次输出,然后遍历右子树输出
代码思路1:递归
void inOrder1(Node* node)
{
if (node == NULL)
return;
inOrder1(node->left);
cout << node->element << endl;
inOrder1(node->right);
}
和前序排序的思路基本一致,在左子树遍历结束后打印输出即可。
代码思路2:非递归 -- 栈(stack)
void inOrder2(Node* node)
{
stack<Node*>s;
s.push(node);
while (s.size() != 1 || s.top() != NULL)
{
while (s.top() != NULL)
{
s.push(s.top()->left);
}
s.pop();
cout << s.top()->element << endl;
Node* tmp = s.top()->right;
s.pop();
s.push(tmp);
}
}
在有效元素出栈时,将其输出打印
3)后序遍历
规则:先遍历左子树,然后遍历右子树,最后再依次输出
代码思路1:递归
void postOrder1(Node* node)
{
if (node == NULL)
return;
postOrder1(node->left);
postOrder1(node->right);
cout << node->element << endl;
}
代码思路2:非递归 -- 栈(stack)
非常麻烦,需要标记结点,且遍历时不能随意删去结点,当标记显示左右子树都遍历完成
4)层序遍历
规则:根据层次依次输出
1)代码思路1:递归
不易实现,需要根据当前层次分别打印输出左右结点,按照层次顺序递归
2)代码思路2:非递归 -- 队列(queue)
void levelOrder(Node* node)
{
queue<Node*>q;
q.push(node);
while (!q.empty())
{
Node* tmp = q.front();
q.pop();
cout << tmp->element << endl;
if (tmp->left)
q.push(tmp->left);
if (tmp->right)
q.push(tmp->right);
}
}
每次循环,将队首结点出队并打印输出,再将其的左右结点入队
5.二叉树综合案例(关于遍历)
1)一颗二叉树,前序遍历的结果是ABCDE,中序遍历的结果是BADCE,那么后序遍历的结果是什么?
首先,前序遍历可看出A为二叉树的根结点,再看中序遍历,即可得知B为A的左结点,且左子树只有一个结点B。
已知B为A的左结点,根据前序遍历可以看出C为A的右结点,根据先左后右即可看出D为C的左结点,E为C的右结点。
这样就可知后序遍历的结果:BDECA。
如果这种题只给一个前序和后序的结果,是可能无法推出二叉树结构的,但本题可以,不再推导。
三、高级的二叉树结构
不再书写,以下可以看此网址