树(数据结构)

一、树,森林,二叉树的简单介绍

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的最大整数)

\max \left ( \log _{2}n \right )-1 

6)一颗有 n 个结点的完全二叉树,对于任意一个结点 i,结点的顺序为从上往下,从左往右:

对于一个拥有左右孩子的结点来说,其左孩子为 2i,右孩子为 2i + 1。

如果 i = 1,那么此结点为二叉树的根结点,如果 i > 1,那么其父结点就是 \max \left ( i/2 \right ),比如第 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。

如果这种题只给一个前序和后序的结果,是可能无法推出二叉树结构的,但本题可以,不再推导。

三、高级的二叉树结构

不再书写,以下可以看此网址

柏码 - 让每一行代码都闪耀智慧的光芒!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值