【数据结构】非线性表----树详解

2024-08-05 09:51:15 浏览数 (3)

是一种非线性结构,它是由**n(n>=0)**个有限结点组成一个具有层次关系的集合。具有层次关系则说明它的结构不再是线性表那样一对一,而是一对多的关系;随着层数的增加,每一层的元素个数也在不断变化(由上一层和该层的对应关系决定)。

关于树的名称的由来,是因为它的结构类型很像现实中的树倒过来,故称作——树。

根据树的名称,也对其所包含的元素进行了命名和定义。

树的基本术语

1.结点:包含一个数据元素及若干指向其子树的分支;

2.结点的度:一个结点拥有的子树的数目;

3.叶子或终端结点:度为0的结点;

4.非终端结点或分支结点:度不为0的结点;

5.树的度:树内各结点的度的最大值;(需要注意,树的度和结点的度的定义是有略微差异的)

除此之外,还使用人类亲缘关系进行了一系列描述:(注:下图的关系与节点名称无关,仅代表树的结构)

6.孩子结点或子结点:结点的子树的根称为该结点的孩子结点或子结点;

7.双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的双亲结点或父结点;

8.兄弟结点:同一个双亲的孩子之间互称兄弟;

9.祖先结点:从根到该结点所经分支上的所有结点;

10.子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙;

11.结点的层次:从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第L层.则其子树的根就在第L 1层;

12.堂兄弟结点:其双亲在同一层的结点互为堂兄弟;

13.树的深度或高度:树中结点的最大层次;

14.森林:由m(m>=0)棵互不相交的树的集合称为森林;

15.有序树和无序树:树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树;

16.路径和路径长度:路径是由树中的两个结点之间的结点序列构成的。而路径长度是路径上所经过的边的个数

树的基本特征

针对树,我们有很多可以说的点。这里我们主要从其结构特征来进行总结。

  • 任何一棵树都是由根和子树构成的——子树又是一棵树——树是递归定义的
  • 树的根结点没有前驱;除根结点外的所有结点有且只有一个前驱(也就是一个父节点)
  • 树中所有结点有零个或多个后继
  • 子树是不相交的。
  • 当不再有子树的时候就说明该树已经到了尾结点。
树的公式

事实上,基于树的数据结构,可以衍生出很多数学公式来对其进行计算,但是这里只介绍基本的两个(其他重要的公式会在二叉树中进行介绍)

节点数量
  • 对于一棵有 n 个节点的树,节点数量公式为:n = I L 其中,I 是内节点数量,L 是叶节点数量。
边数
  • 一棵有 n 个节点的树有 n−1 条边。
树的构建

树的存储结构主要有以下几种方法,每种方法都有其优缺点和适用场景:

1. 父节点表示法(Parent Representation)

每个节点记录其父节点的编号或指针。这种方法使用一个数组,其中每个元素表示节点,其值是该节点的父节点的索引。

结构

代码语言:javascript复制
parent[i] 表示第 i 个节点的父节点

示例

代码语言:javascript复制
节点:  0  1  2  3  4  5
父节点: -1  0  0  1  1  2  (-1 表示根节点)

优点:简单,适合快速查找父节点。

缺点:查找子节点较慢,需要遍历整个数组。

2. 孩子表示法(Child Representation)

每个节点记录其所有子节点。这种方法使用一个链表或数组,其中每个节点包含一个指向其子节点列表的指针。

结构

代码语言:javascript复制
children[i] 表示第 i 个节点的子节点列表

示例

代码语言:javascript复制
节点:   0  1  2  3  4  5
子节点: [1,2] [3,4] [5] [] [] []

优点:适合快速查找子节点。

缺点:查找父节点较慢,需要记录父节点指针或进行额外处理。

以上两种方法的缺点和优点是互补的。那么是否有更为方便的方法呢?

3. 孩子兄弟表示法(Left-Child Right-Sibling Representation)

当我们需要定义很多孩子的时候,我们可以使用左孩子右兄弟的定义法

也就是说根节点只指向它的第一个孩子结点,而其他的孩子节点就交给第一个孩子节点去指向

将树转换为二叉树,每个节点记录其最左子节点和右兄弟节点。这种方法使用两个指针,一个指向最左子节点,另一个指向右兄弟。

结构

代码语言:javascript复制
节点: node
左孩子: leftChild
右兄弟: rightSibling

示例

代码语言:javascript复制
节点:    0
        / 
       1   2
      / 
     3   4
转换为:
节点:    0
       /  
      1    2
     /
    3
     
      4

优点:能将任意树转换为二叉树,利用二叉树的遍历和处理方法。

缺点:需要两个指针,内存开销较大。

4. 邻接表表示法(Adjacency List Representation)

通常用于存储图结构,但也可以用于树。每个节点记录其相邻节点(即子节点和父节点)。

结构

代码语言:javascript复制
adjList[i] 表示第 i 个节点的相邻节点列表

示例

代码语言:javascript复制
节点:   0  1  2  3  4  5
相邻节点: [1,2] [0,3,4] [0,5] [1] [1] [2]

优点:适合处理树和图的遍历和搜索。

缺点:需要额外处理以区分子节点和父节点。

5. 邻接矩阵表示法(Adjacency Matrix Representation)

使用一个二维数组表示节点之间的连接关系。通常用于稠密图,但在某些情况下也可用于树。

结构

代码语言:javascript复制
adjMatrix[i][j] 表示节点 i 和节点 j 之间是否有边(0 或 1)

示例

代码语言:javascript复制
节点:  0  1  2  3  4  5
邻接矩阵:
[0, 1, 1, 0, 0, 0]
[1, 0, 0, 1, 1, 0]
[1, 0, 0, 0, 0, 1]
[0, 1, 0, 0, 0, 0]
[0, 1, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 0]

优点:简单,适合快速查找任意两个节点之间是否有边。

缺点:存储空间开销大,不适合稀疏树。

这些存储结构各有特点,选择哪种方法主要取决于具体应用需求,例如查找子节点还是父节点更频繁,内存开销是否是主要考虑因素等。

树的初始化和一些基本操作
代码语言:javascript复制
#include <stdio.h>
#include <stdlib.h>

// 定义树节点结构
typedef struct TreeNode {
    int data;
    struct TreeNode* child1;
    struct TreeNode* child2;
    struct TreeNode* child3;
    struct TreeNode* child4;
    ...
} TreeNode;

//树的初始化
TreeNode* createNode(int data)
{
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    if(!newNode){
        printf("failed!n");
        return NULL;
    }
    newNode->data = data;
    newNode->child1 = NULL;
    newNode->child2 = NULL;
    ...
    return newNode;
}

//插入、查找、遍历...
TreeNode* insertTree(TreeNode* parent,int data);
TreeNode* findTree(TreeNode* parent,int data);
TreeNode* inorderTraversal(TreeNode* parent);
...
总结

单纯的树实际上用处不大(子节点过多)。但是对于文件系统、目录以及某些分层过多的系统,使用的就是树。

通常在优化的数据结构中,使用更多的是叫做二叉树的数据结构

这是基于树的数据结构,一个根节点只有两个孩子结点,在下一节我们将会对二叉树进行剖析,敬请期待。

0 人点赞