数据结构第九章:树的学习(上)

一.树的概念和结构

1.树的概念

树是一种非线性的数据结构,它是由n个有限节点组合成一个有层次关系的集合。把它叫做树是因为它的结构看起来像一棵倒挂着的树,也就是说它的结构是根朝上,叶子朝下的树。

有一个特殊的节点,称为根节点,根节点没有前驱节点。

除去根节点外,其余节点被分成M个互不相交的集合,其中每个集合又是由一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继节点

总体来说,树是递归定义的。

如上图所示,本章节所学的树结构就类似于第一张现实生活中的树,第二张图片是举了一个树结构的例子。这里需要注意的是:树形结构中,子树间不能有交集,否则就不是树形结构。下面举出一些非树结构的例子便于大家理解。

2.树结构的相关概念

学习树结构之前,我们需要知道下面关于树结构的相关概念:

  • 节点的度:一个节点含有的子树个数称为该节点的度;如上图A的节点为6.
  • 叶子节点或终端节点:度为0的节点称为叶子节点;如上图B.C.H.I.....等为叶子节点。
  • 非终端节点或分支节点:度不为0的节点;如上图的D.E.F.G为分支节点。
  • 双亲结点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如图A是B的父节点。
  • 孩子节点或子节点:一个节点含有子树的根节点称为该节点的子节点;如上图B是A的子节点。
  • 兄弟节点:具有相同父亲节点的节点互相称为兄弟节点;如上图B.C互相为兄弟节点。
  • 树的度:一棵树中,最大节点的度称为树的度;如上图树的度为6。
  • 节点的层次:从根节点开始定义,根为第一层,根的子节点为第二层,以此类推。
  • 树的高度或深度:树中节点最大的层次;如上图树的高度为4。
  • 堂兄弟节点:双亲在同一层的节点互相称为堂兄弟节点;如上图H.I互为堂兄弟节点。
  • 节点的祖先:从根到该节点所经过的分支上所有的节点;如上图A是所有节点的祖先。
  • 子孙:以某一个节点为根的子树中任意一节点都称为该节点的子孙;如上图所有节点都是A的子孙。
  • 森林:m棵互不相交的树的集合称为森林。

3.树不同种表示方法

树的结构相较于线性表来说,比较复杂。既要存储数据,还要存储节点与节点之间的关系。在实际的表示中,有很多表示的方法。例如:双亲表示法、孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们仅需要了解孩子兄弟表示法。

cpp 复制代码
typedef int DataType;
struct Node
{
 struct Node* _firstChild1; // 第一个孩子结点
 struct Node* _pNextBrother; // 指向其下一个兄弟结点
 DataType _data; // 结点中的数据
};

上述表示方法也被叫做左孩子右兄弟表示法。在树的结构体中,只存储了第一个孩子节点和兄弟节点。如上图所示:节点A只存储了第一个孩子B和其兄弟NULL节点。以此类推.......

此方法的优点:仅需要两个指针就可以表示一个树的结构,适用于子节点数量不确定或者动态变化的树;所有的树(包括多叉树)均可以转化为二叉树,可以简化算法设计;因为此种方法可以将多叉树转换为二叉树,所以此方法可以通过遍历二叉树的方法遍历多叉树。

此方法的缺点:访问某个节点的第N个节点需要遍历右兄弟链,时间复杂度为O(N);相较于直接的多叉树表示法,逻辑关系不够直观,比较抽象。

应用场景:文件系统目录的存储;需要频繁调整树结构场景;算法需要统一处理不同叉树场景。

4.树在实际中的应用

树是一种非线性的数据结构,广泛运用于计算机科学中。常见的树包括:二叉树、二叉搜索树、平衡二叉树(如AVL树,红黑树)、B树、B+树等。这些结构在数据存储、检索和排序中发挥着重要作用。

现代的操作系统普遍采用树形结构组织文件系统。如上图目录作为树的节点,文件作为叶子节点,形成层次化的存储体系。这种体系便于用户浏览和管理文件,同样支持高效的路径查找和权限控制。

数据库系统大量使用树结构实现索引功能。B树及其变种B+树是关系型数据库常用的索引结构,能够高效支持范围查询和顺序访问。

计算机网络的路由协议常借助树结构优化数据包传输。最小生成树算法用于网络拓扑,确保所有节点联通的同时最小化总成本。

以上是树在计算机科学的一部分实际应用。总的来说,树是一种重要的存储结构,以后学习完C++后,会向更深层次学习和探索。

二.二叉树的概念和结构

1.二叉树的概念

一颗二叉树是节点的有限集合,该集合由一个根节点加上两棵分别称为左子树和右子树的二叉树构成。下面给出图片加以理解。

从上图中可以看出:二叉树不存在度大于2的节点;二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

2.现实中的二叉树

看完上面的图片,我们很难不想到二叉树。大自然真是伟大啊!一个普通的人看到上方的图片只会看到其表面。而我们程序员会想到:这树长得真牛逼,像是一个二叉树!!!!

3.特殊的二叉树

因为二叉树的结构多样,所以在这里我们讲一下两个特殊的二叉树。一个是满二叉树,另外一个是完全二叉树。

满二叉树:一个二叉树,如果每一层的节点树都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k -1 ,则它就是满二叉树。

完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。满二叉树的叶子节点顺序必须严格从左到右的顺序。

4.二叉树的性质

  • 若限定根节点层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个节点。
  • 若规定根节点的层数为1,则深度为h的二叉树最大节点树是2^h -1。
  • 对任何一棵二叉树,如果度为0其叶节点个数为n0,度为2的节点数为n2,则有关系:n0=n2+1。
  • 若规定根节点的层数为1,具有n个节点的满二叉树的深度,h=
  • 若完全二叉树的节点总数为N,叶子节点为L,满足当N为偶数时,L=N/2;当N为奇数时,L=(N+1)/2;

5.性质选择题

  1. 某二叉树共有399个结点,其中有199个度为2的结点,则该二叉树中的叶子结点数为(B)

A 不存在这样的二叉树

B 200

C 198

D 199

上述题目会用到性质3:对任何一棵二叉树,如果度为0其叶节点个数为n0,度为2的节点数为n2,则有关系:n0=n2+1。答案选B

2.在具有2n个结点的完全二叉树中,叶子结点个数为(A)

A n

B n+1

C n-1

D n/2

上述题目会用到性质5:完全二叉树的节点总数为N,叶子节点为L时,满足当N为偶数时,L=N/2;当N为奇数时,L=(N+1)/2;所以此题选择A

4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为(B)

A 11

B 10

C 8

D 12

本题会用到性质4:若规定根节点的层数为1,具有n个节点的满二叉树的深度,h=。因为完全二叉树子节点的范围是1-,根据上面的性质可以得出这棵树的高度范围,最后得到此题选择B

5.一个具有767个节点的完全二叉树,其叶子节点个数为(B)

A 383

B 384

C 385

D 386

本题会用到性质是完全二叉树的叶子节点个数的范围是1-,根据总节点的个数可以得到叶子节点的范围,最终选择B

6.二叉树的存储结构

二叉树一般可以使用两种存储结构,一种是顺序结构,另一种是链式结构。本章只讲解链式存储结构,对于顺序结构只讲解相关概念,剩余的顺序结构内容详见《数据结构第十章:树的学习(中)》

(1)顺序存储

顺序结构就是使用数组来存储二叉树的数据,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,而现实中只有堆才会使用数组来存储,关于堆,后面的章节会详细讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一个二叉树结构。

(2)链式存储

二叉树的链式存储是指用链表表示一棵二叉树,即用链条来表示元素的逻辑关系,通常的方法是链表的每个节点由三个成员变量组成,分别是:数据和左右指针。左右指针分别用来表示该节点的左右孩子所在的链节点的存储地址。链式结构又分为二叉链和三叉链,当前学习阶段我们只会用到二叉链。

cpp 复制代码
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
 struct BinTreeNode* _pLeft; // 指向当前节点左孩子
 struct BinTreeNode* _pRight; // 指向当前节点右孩子
 BTDataType _data; // 当前节点值域
}
 
// 三叉链
struct BinaryTreeNode
{
 struct BinTreeNode* _pParent; // 指向当前节点的双亲
 struct BinTreeNode* _pLeft; // 指向当前节点左孩子
 struct BinTreeNode* _pRight; // 指向当前节点右孩子
 BTDataType _data; // 当前节点值域
};

三.二叉树的链式结构实现

1.前置说明

在学习二叉树的基本操作前,需要先创建一棵二叉树,由于目前对二叉树的理解较浅,所以手动创建一个简单的二叉树供后续的学习。在后面的章节会讲解二叉树真正的创建方法。

cpp 复制代码
typedef int BTDataType;

typedef struct BinaryTreeNode
{
 BTDataType _data;    //二叉树该节点的数据
 struct BinaryTreeNode* _left;    //该节点的左孩子节点
 struct BinaryTreeNode* _right;    //该节点的右孩子节点
}BTNode;
 
BTNode* CreatBinaryTree()
{
 BTNode* node1 = BuyNode(1);    //调用函数创建节点
 BTNode* node2 = BuyNode(2);
 BTNode* node3 = BuyNode(3);
 BTNode* node4 = BuyNode(4);
 BTNode* node5 = BuyNode(5);
 BTNode* node6 = BuyNode(6);

 node1->_left = node2;    //利用手动创建的方法,创建不同节点之间的关系
 node1->_right = node4;
 node2->_left = node3;
 node4->_left = node5;
 node4->_right = node6;
 return node1;
}

注意:上述不是二叉树真正创建方法,后续会讲解真正的二叉树创建方法。

2.二叉树的遍历

(1)前序、中序及后序遍历

学习二叉树结构,最简单的就是二叉树的遍历。所谓二叉树的遍历就是按照某种特定的规则,依次对二叉树的节点进行相应的操作,并且每个节点只操作一次。访问节点所做的操作依赖于具体的实际问题。遍历是二叉树的最重要运算之一,也是二叉树上进行其他运算的基础。

按照规则,二叉树的遍历有:前序遍历、中序遍历、后序遍历的递归结构遍历。

  1. 前序遍历:访问根节点的操作发生在遍历其左右子树之前。
  2. 中序遍历:访问根节点的操作发生在遍历左右子树之间。
  3. 后序遍历:访问根节点的操作发生在遍历其左右子树之后。

下面是三种顺序遍历的代码实现:

cpp 复制代码
// 二叉树前序遍历 
void PreOrder(BTNode* root);
{
  if(root==NULL)
  {
    printf("NULL");
    return;
  }
  printf("%c",root->data);
  PreOrder(root->lift);
  PreOrder(root->right);
}

// 二叉树中序遍历
void InOrder(BTNode* root);
{
  if(root==NULL)
  {
    printf("NULL");
    return;
  }
  InOrder(root->lift);
  printf("%c",root->data);
  InOrder(root->right);
}


// 二叉树后序遍历
void PostOrder(BTNode* root);
{
  if(root==NULL)
  {
    printf("NULL");
    return;
  }
  PostOrder(root->lift);
  PostOrder(root->right);
  printf("%c",root->data);
}

上述先序遍历代码的具体解释:利用递归的思想,先遍历根节点,后遍历左子树,接下来遍历右子树。左子树又可以分为根节点和左右子树。右子树同理,以此类推。最终的根节点为NULL时,说明遍历完毕,返回即可。

(2)层序遍历

除了先序遍历、中序遍历、后序遍历,还可以对二叉树进行层序遍历。设二叉树的根节点所在的层数为1,层序遍历就是从二叉树根节点出发,首先访问第一层的根节点,然后从左到右访问第二层上的节点,接着是第三层的节点,以此类推,自上而下,从左至右逐层访问树的节点的过程就是层序遍历。

下面是层序遍历的代码实现

cpp 复制代码
void LevelOrder (BTNode * root)
{
  Queue q;    //创建一个对列
  QueueInit(&q);    //对队列进行初始化
  if(root)    //如果根节点为NULL
    QueuePush(&q,root);    //将此根节点入队    
  while(!QueueEmpty(&q))    //当二叉树非空时进入while循环
  {    
    BTNode *front =QueueFront(&q);    //保存队头的二叉树元素的数据
    QueuePop(&q);    //将队头的二叉树数据出队
    printf("%c",front->data);        //打印刚刚出队的队头二叉树元素的数据
    if(front->left)    //如果根节点的左子树不为NULL
      QueuePush(&q,front->left);    //将此元素数据入队
    if(front->right)    //如果根节点的右子树不为NULL
      QueuePush(&q,front->right);
  }
  printf("\n");
  QueueDestory(&q);    //销毁队
}

上述代码的具体解释:层序遍历需要用到队列。用到的思想是一层节点带入一层节点。首先将根节点入队,随后判断左右孩子节点是否为空,不为空,则将其入队。最后将根节点出队。就达成了层序遍历的效果。

相关推荐
我是大咖2 小时前
二维数组与数组指针
java·数据结构·算法
行业探路者3 小时前
健康宣教二维码是什么?主要有哪些创新优势?
人工智能·学习·音视频·二维码·产品介绍
草莓熊Lotso3 小时前
Python 入门超详细指南:环境搭建 + 核心优势 + 应用场景(零基础友好)
运维·开发语言·人工智能·python·深度学习·学习·pycharm
June bug4 小时前
【实习笔记】Fiddler学习笔记
笔记·学习·fiddler
我怕是好4 小时前
学习STM32 ESP8266
stm32·嵌入式硬件·学习
爱编码的傅同学4 小时前
【今日算法】Leetcode 581.最短无序连续子数组 和 42.接雨水
数据结构·算法·leetcode
JeffDingAI4 小时前
【Datawhale学习笔记】动手学RNN及LSTM
笔记·rnn·学习
wm10434 小时前
代码随想录第四天
数据结构·链表
CoderCodingNo5 小时前
【GESP】C++六级考试大纲知识点梳理, (3) 哈夫曼编码与格雷码
开发语言·数据结构·c++