树结构是一种非常重要且广泛应用的数据结构。它以节点和边的形式组织数据,具有层次关系和递归性质。树结构在计算机科学领域中有着广泛的应用,例如文件系统、数据库索引、网络路由等。

一、什么是树

树是数据结构中的一种,其属于非线性数据结构结构的一种,我们前文所提到的数据结构多数都是线性的,这也是较为简单的数据结构,而接下来的树与图均属于非线性数据结构,也是概念极多的一类。

树是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构。

树的定义:n个节点组成的有限集合。n=0,空树;n>0,1个根节点,m个互不相交的有限集,每个子集为根的子树。

如图所示,图为一颗树:

关于数据结构树的概括_算法

二、树相关的基本术语

节点(Node):树中的节点代表数据元素。

边(Edge):树中的边表示节点之间的连接关系。

根节点(Root):树中唯一一个没有父节点的节点。

叶节点/终端节点(Leaf):没有子节点的节点称为叶节点。

子树(Subtree):树中任意一个节点及其子孙节点组成的子集。

节点的度(Degree):节点拥有的子节点数量称为节点的度。

树的度(Degree):树中所有节点的度的最大值。

层次(Level):根节点位于第一层,其子节点位于第二层,依次类推。

高度/深度(Height/Depth):树中节点的最大层次数称为树的高度或深度。

三、树的计算公式

树的节点树为所有节点度数加1(加根节点)。

度为m的树中第i层最多有m^(i-1)个节点。

高度为h的m次树至多(m^h-1)/(m-1)个节点。

具有n个节点的m次树的最小高度为logm( n(m-1) + 1 )  向上取整。

四、树的基本操作 在树结构中,常见的基本操作包括创建树、插入节点、删除节点、查找节点和遍历树等。

  1. 创建树:通过定义节点和边来创建树结构。
  2. 插入节点:在指定位置插入一个新节点。
  3. 删除节点:删除指定节点及其子树。
  4. 查找节点:根据给定的关键字查找对应的节点。
  5. 遍历树:按照一定的顺序访问树中的所有节点。常见的遍历方式有深度优先遍历(前序、中序、后序遍历)和广度优先遍历(层次遍历)。

五、常见的树结构类型

二叉树(Binary Tree):

树中每个节点最多有两个子节点的树称为二叉树。

二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

如图

关于数据结构树的概括_数据结构与算法_02

二叉树的特点

1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。

2)左子树和右子树是有顺序的,次序不能任意颠倒。

3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

二叉树的性质

二叉树第i层上的结点数目最多为 2的(i-1)次方个节点(i≥1)。

深度为k的二叉树至多有2的k次方-1个结点(k≥1)。

包含n个结点的二叉树的高度至少为log2 (n+1)。

在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。

满二叉树

满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。

满二叉树的特点有:

1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。

2)非叶子结点的度一定是2。

3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

关于数据结构树的概括_子树_03

如图为一颗满二叉树

完全二叉树

完全二叉树:对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。

关于数据结构树的概括_二叉树_04

如图为一颗完全二叉树

遍历:

以先序遍历为例就是在访问二叉树的结点的时候采用,先根,再左,再右的方式,对于一个最简单的访问而言如图,先序遍历的访问顺序就是A,B,C

关于数据结构树的概括_算法_05

由此如下的访问规律:

先序遍历:根左右

先序遍历访问顺序就是:ABDEFGCH

中序遍历:左根右

中序遍历访问顺序就是:EDFBGACH

后序遍历:左右根

后序遍历访问顺序就是:EFDGBHCA

关于数据结构树的概括_子树_06

代码实现
//树的先序遍历 Preorder traversal
void preorder(Node* node){
    if (node != NULL)
    {
        printf("%d ",node->data);
        inorder(node->left);
        inorder(node->right);
    }
}
//树的中序遍历 In-order traversal
void inorder(Node* node){
    if (node != NULL)
    {
        inorder(node->left);
        printf("%d ",node->data);
        inorder(node->right);
    }
}
//树的后序遍历 Post-order traversal
void postorder(Node* node){
    if (node != NULL)
    {
        inorder(node->left);
        inorder(node->right);
        printf("%d ",node->data);
    }
}

森林:

森林,顾名思义,就是由众多的树构成的一组数据结构,这些树本身没有什么联系,用系统的语言描述就是:森林:m(>=0)棵互不相交的树的集合

如果把一棵树当作一个独立的点,那么森林就是一个点的集合。

树转换成二叉树

将一棵普通树转换成二叉树有多种方法,其中一种常见的方法是使用左子右兄弟表示法(也称为二叉树的线索化表示)。

具体步骤如下:

  1. 将树的根节点作为二叉树的根节点。
  2. 对于树的每一个子节点,递归地进行以下操作:
  • 将子节点的第一个孩子作为二叉树节点的左子节点。
  • 将子节点的其他孩子依次作为左子节点的右兄弟节点。

下面通过一个例子来说明这个过程。假设有以下的树:

A
     / | \
    B  C  D
   / \   / \
  E   F G   H

将这棵树转换成二叉树的过程如下:

  1. 将树的根节点 A 作为二叉树的根节点。
  2. 对于根节点 A,它的第一个子节点是 B,将 B 转换成二叉树节点的左子节点。
  3. 对于 B 节点,它的第一个孩子是 E,将 E 作为 B 节点的左子节点。
  4. 对于 B 节点,它的第二个孩子是 F,将 F 作为 E 节点的右兄弟节点。
  5. 对于 B 节点,它的第三个孩子是空,没有其他的孩子节点。
  6. 回到根节点 A,它的第二个子节点是 C,将 C 转换成根节点 A 的左子节点的右兄弟节点。
  7. 对于 C 节点,它的第一个孩子是空,没有其他的孩子节点。
  8. 回到根节点 A,它的第三个子节点是 D,将 D 转换成根节点 A 的左子节点的右兄弟节点。
  9. 对于 D 节点,它的第一个孩子是 G,将 G 作为 D 节点的左子节点。
  10. 对于 D 节点,它的第二个孩子是 H,将 H 作为 G 节点的右兄弟节点。
  11. 对于 D 节点,它的第三个孩子是空,没有其他的孩子节点。

最终得到的二叉树如下:

A
       /
      B
     / \
    E   F
         \
          C
           \
            D
           / \
          G   H

通过这种方式,我们将普通树转换成了二叉树。在转换后的二叉树中,每个节点的左子节点指向其在原树中的第一个孩子,右子节点指向其在原树中的下一个兄弟节点。注意,转换后的二叉树可能会出现很多空节点,表示原树中某些节点没有对应的兄弟节点。

二叉树转换成树

将二叉树转换成普通树,即恢复二叉树的多子节点结构,可以使用后序遍历的方式实现。

具体步骤如下:

  1. 对于二叉树的每个节点,如果它没有左子节点,则跳过该节点不处理;如果它有左子节点但没有右子节点,则将该左子节点作为该节点唯一的子节点。
  2. 对于二叉树的每个节点,如果它有右子节点,则递归地将其右子节点转换为多个兄弟节点并将其作为其左子节点的右兄弟节点。
  3. 重复步骤2,直到遍历完整棵二叉树。

下面通过一个例子来说明这个过程。假设有以下的二叉树:

A
      / \
     B   C
    / \   \
   D   E   F
          /
         G

将这棵二叉树转换成普通树的过程如下:

  1. 对于节点 A,它没有右子节点,跳过不处理。
  2. 对于节点 B,它有右子节点 C,则将 C 转换为 B 的兄弟节点。
  3. 对于节点 C,它没有右子节点,跳过不处理。
  4. 对于节点 D,它没有右子节点,跳过不处理。
  5. 对于节点 E,它没有右子节点,跳过不处理。
  6. 对于节点 F,它有右子节点 G,则将 G 转换为 F 的兄弟节点。

最终得到的普通树如下:

A
      /
     B
    / \
   D   E
        \
         C
          \
           F
            \
             G

通过这种方式,我们将二叉树转换成了普通树,恢复了原始的多子节点结构。

森林转换成二叉树

将森林(多棵树的集合)转换成二叉树的过程可以通过将每棵树的根节点依次作为二叉树的根节点,然后处理每个树的子节点,将其转换为二叉树的左子节点和兄弟节点。以下是具体步骤:

  1. 对于森林中的第一棵树,将其根节点作为二叉树的根节点。
  2. 对于该树的子节点,递归地进行以下操作:
  • 将子节点的第一个孩子作为二叉树节点的左子节点。
  • 将子节点的其他孩子依次作为左子节点的右兄弟节点。
  1. 对于森林中的每个后续树,将其根节点转换为二叉树节点的右兄弟节点。

下面通过一个例子来说明这个过程。假设有以下的森林:

Tree 1             Tree 2
       A                  E
     / | \                \
    B  C  D                F
   /                      / \
  E                      G   H

将这个森林转换成二叉树的过程如下:

  1. 对于第一棵树,将根节点 A 转换为二叉树的根节点。
  2. 对于节点 A,它的第一个子节点是 B,将 B 转换为 A 节点的左子节点。
  3. 对于节点 B,它的第一个孩子是 E,将 E 作为 B 节点的左子节点。
  4. 对于节点 B,它没有其他的孩子节点。
  5. 回到根节点 A,它的第二个子节点是 C,将 C 转换为 A 的左子节点的右兄弟节点。
  6. 对于节点 C,它没有孩子节点。
  7. 回到根节点 A,它的第三个子节点是 D,将 D 转换为 A 的左子节点的右兄弟节点。
  8. 对于节点 D,它没有孩子节点。

此时第一棵树已经转换完成,得到的二叉树如下:

A
     / | \
    B  C  D
   /
  E
  1. 对于第二棵树,将根节点 E 转换为二叉树的右兄弟节点。
  2. 对于节点 E,它的第一个孩子是 F,将 F 作为 E 节点的右兄弟节点。
  3. 对于节点 E,它没有其他的孩子节点。

最终得到的二叉树如下:

A
     / | \
    B  C  D
   /
  E 
   \
    F

通过这种方式,我们将森林转换成了二叉树。在转换后的二叉树中,每个节点的左子节点指向其在原森林中的第一个孩子,右子节点指向其在原森林中的下一个兄弟节点。

二叉树转换成森林

将二叉树转换成森林的过程可以通过反向操作,即从左子节点和右兄弟节点创建新的树。以下是具体步骤:

  1. 对于给定的二叉树,选择其中一个节点作为森林的根节点。
  2. 如果该节点有左子节点,则将其左子节点视为一棵独立的子树,并将其从原来的二叉树中剥离。
  3. 如果该节点有右兄弟节点,则将其右兄弟节点视为另一棵独立的子树,并将其从原来的二叉树中剥离。
  4. 递归地对左子树和右兄弟子树重复步骤2和3,直到所有的节点都被处理。

下面通过一个例子来说明这个过程。假设有以下的二叉树:

A
     /  \
    B    C
   /    / \
  D    E   F
         / \
        G   H

将这棵二叉树转换成森林的过程如下:

  1. 选择节点 A 作为森林的第一棵树的根节点。
  2. 节点 A 有左子节点 B 和右子节点 C,将它们分别视为两棵独立的子树,并剥离原二叉树。
  3. 对于左子树 B,它没有左子节点,也没有右兄弟节点,不再产生新的子树。
  4. 对于右子树 C,它的左子节点是 E,将 E 视为一棵独立的子树,并剥离原二叉树。
  5. 对于子树 E,它没有左子节点,也没有右兄弟节点,不再产生新的子树。
  6. 对于右子树 C 的剩余部分 F,将 F 视为另一棵独立的子树,并剥离原二叉树。
  7. 对于子树 F,它的左子节点是 G,将 G 视为一棵独立的子树,并剥离原二叉树。
  8. 对于子树 F 的剩余部分 H,将 H 视为另一棵独立的子树,并剥离原二叉树。

最终得到的森林如下:

Tree 1                   Tree 2               Tree 3
   A                       B                     C
                          /                     / \
                         D                     E   F
                                                / \
                                               G   H

通过这种方式,我们将二叉树转换成了森林。在转换后的森林中,每棵树都与原二叉树中的某个节点相关联。

DFS深度优先搜索算法:

树的深度优先搜索(DFS)是一种用于遍历或搜索树结构的算法。它从根节点开始,沿着一条路径尽可能深地搜索,直到达到叶子节点或无法继续下去为止。然后回溯到上一层节点,继续搜索其他路径,直到遍历完整棵树。

下面我会使用一个简单的示例来讲解树的深度优先搜索算法的实现过程。

假设我们有以下的树结构:

A
     / \
    B   E
   / \   \
  C   D   F

我们将使用递归的方式实现深度优先搜索算法。

首先,我们定义一个函数 DFS(node),其中 node 代表当前节点。算法的基本思想是:

  1. 检查当前节点是否为空,如果为空则返回。
  2. 访问当前节点。
  3. 递归调用 DFS() 函数遍历当前节点的左子树。
  4. 递归调用 DFS() 函数遍历当前节点的右子树。

按照这个思路,我们可以实现深度优先搜索算法的代码如下:

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def DFS(node):
    if node is None:
        return
    
    # 访问当前节点
    print(node.val)
    
    # 递归遍历左子树
    DFS(node.left)
    
    # 递归遍历右子树
    DFS(node.right)

# 构建树结构
root = TreeNode('A')
root.left = TreeNode('B')
root.right = TreeNode('E')
root.left.left = TreeNode('C')
root.left.right = TreeNode('D')
root.right.right = TreeNode('F')

# 调用深度优先搜索算法
DFS(root)

运行以上代码,输出结果为:

A
B
C
D
E
F

可以看到,深度优先搜索算法按照先访问根节点,然后递归遍历左右子树的顺序,依次访问了树中的所有节点。

需要注意的是,虽然我们使用了递归实现,但在实际应用中,如果树的层级较深,递归可能导致堆栈溢出。为了避免这种情况,可以使用栈(Stack)数据结构来代替递归,实现非递归的深度优先搜索算法。

哈夫曼树(Huffman Tree):

哈夫曼树(Huffman Tree),又名:赫夫曼树,也称为最优二叉树(Optimal Binary Tree),是一种带权路径长度最短的树结构。它主要用于数据压缩和编码的应用中,通过将出现频率较高的字符用较短的编码表示,而出现频率较低的字符用较长的编码表示,从而实现数据的高效存储和传输。

在哈夫曼树中,每个字符都被赋予一个权值,通常是该字符在文本中出现的频率或概率。构建哈夫曼树的过程可以通过以下步骤完成:

  1. 创建森林:将每个字符作为一个独立的树(只包含一个节点)加入到森林中。
  2. 选择权值最小的两棵树作为左右子树创建一个新的树,并将新树插入到森林中。
  3. 重复步骤2,直到森林中只剩下一棵树,即哈夫曼树构建完成。

在哈夫曼树中,每个字符对应的编码可以通过从根节点到每个叶子节点的路径上的0和1来表示。如果从根节点到叶子节点的路径上经过的左子节点用0表示,经过的右子节点用1表示,那么叶子节点的路径就是对应字符的编码。

构建哈夫曼树

在构建哈夫曼树时,只需要遵循一个原则,那就是权重越大的结点距离树根越近。

因此,在构建过程中,有如下的规律:

关于数据结构树的概括_数据结构与算法_07

首先,选出我们数据中最小的两个数据,构建成二叉树的左孩子和右孩子,而根的数据为两者之和

关于数据结构树的概括_算法_08

关于数据结构树的概括_数据结构与算法_09

其次,将刚才合成的数据作为右孩子,左孩子从未处理的数据中选出最小的一个,作为左孩子,他们的根同样为左右孩子的权值和

关于数据结构树的概括_树_10

不断重复上述的步骤,直到将所有的数据全部处理完并构建出二叉树,这棵二叉树就是我们的哈夫曼树。

 如图这颗哈夫曼树的WPL值为:WPL= 8*1+ 6*2 + 1*3 + 4*3 = 273

由上文的分析可知,构建哈夫曼树时,我们需要根据各个结点的权重值,筛选出其中值最小的两个结点,构建二叉树,其代码为:

void CreateHuffmanTree(HuffmanTree *HT, int *w, int n) {
    if(n <= 1)
        return; // 如果只有一个编码就相当于0
    int m = 2*n-1; // 哈夫曼树总节点数,n就是叶子结点
    *HT = (HuffmanTree)malloc((m+1) * sizeof(HTNode)); // 0号位置不用
    HuffmanTree p = *HT;
// 初始化哈夫曼树中的所有结点
    for(int i = 1; i <= n; i++) {
        (p+i)->weight = *(w+i-1);
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
//从树组的下标 n+1 开始初始化哈夫曼树中除叶子结点外的结点
    for(int i = n+1; i <= m; i++) {
        (p+i)->weight = 0;
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
//构建哈夫曼树
    for(int i = n+1; i <= m; i++) {
        int s1, s2;
        Select(*HT, i-1, &s1, &s2);    //查找内容,需要用到查找算法
        (*HT)[s1].parent = (*HT)[s2].parent = i;
        (*HT)[i].left = s1;
        (*HT)[i].right = s2;
        (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
    }
}

六、树的应用场景 树结构在计算机科学中有着广泛的应用,以下是一些常见的应用场景:

  1. 文件系统:操作系统中的文件系统通常使用树结构来组织文件和目录的层次关系。
  2. 数据库索引:数据库中的索引结构(如B+树)基于树的特性,用于快速查找和排序数据。
  3. 表达式求值:通过将表达式转化为树形结构来求值,例如二叉表达式树。
  4. 图算法:图算法中常用的深度优先搜索(DFS)和广度优先搜索(BFS)等基于树的遍历算法。
  5. 网络路由:网络中的路由算法使用树结构来确定数据包的转发路径。
  6. Huffman编码:一种用于数据压缩的编码方式,基于树的构建和遍历。
  7. 区间查询:线段树等树形数据结构常用于区间查询问题,例如最小值查询、区间和查询等。

图片以及部分代码来源: https://www.dotcpp.com/