【数据结构与算法】数据结构基础——树(上):树的存储结构,满二叉树,完全二叉树,二叉树的存储结构

目录

  • 树(Tree)
    • [1. 树的定义](#1. 树的定义)
    • [2. 树的性质](#2. 树的性质)
    • [3. 树的存储结构](#3. 树的存储结构)
      • [3.1 双亲表示法](#3.1 双亲表示法)
      • [3.2 孩子表示法](#3.2 孩子表示法)
      • [3.3 孩子兄弟表示法](#3.3 孩子兄弟表示法)
    • [4. 二叉树](#4. 二叉树)
      • [4.1 二叉树的概念](#4.1 二叉树的概念)
      • [4.2 特殊的二叉树](#4.2 特殊的二叉树)
        • [4.2.1 满二叉树](#4.2.1 满二叉树)
        • [4.2.2 完全二叉树](#4.2.2 完全二叉树)
      • [4.3 二叉树的存储结构](#4.3 二叉树的存储结构)
        • [4.3.1 顺序存储(数组)](#4.3.1 顺序存储(数组))
        • [4.3.2 链式存储](#4.3.2 链式存储)

树(Tree)

1. 树的定义

为了更好地理解树的结构,下面是一个简单的树示例:
根节点
节点B
节点C
节点D
叶子节点E
叶子节点F
叶子节点G
叶子节点H
叶子节点I

树是 n 个节点的有限集合(n >= 0),其中当 n = 0 时称为空树。一个非空树应该满足如下性质:

1)有且只有一个根节点

2)当 n > 1 时树可以分成若干个子集,每个子集都可以看成是一棵树

3)树的每个子集之间都不能相交

因为树的子集又可以看作是一棵树,子集的子集又可以看作若干棵树,所以树这个数据结构的很多操作都是通过递归来实现的。树还有几个需要注意的性质:

1)除了根节点以外,每个节点都有且只有一个前驱节点

2)除了叶子节点外,每个节点都可以有若干个后继节点

2. 树的性质

1) :每个节点拥有的子树个数称为该节点的度,度为 0 的节点叫做叶子节点,所有节点中最大的度就叫树的度

2)父节点 :树中每个节点的前驱节点就是它的父节点,也叫双亲节点

3)孩子节点 :树中每个节点的直接后继节点是它的孩子节点

4)兄弟节点 :具有同一个父节点的节点称为兄弟节点

5)子孙 :一个节点的所有子树都是它的子孙

6)节点层次 :从根节点开始为第一层,根节点的孩子为第二层,任意一个处在 x 层的节点,其孩子节点在 x + 1 层

7)树的高度 :层次最大的节点的层次就是树的高度

8)森林 (Forest):m(m >= 0)棵不相交的树的集合,树中每个节点的子树其实都是森林

为了更直观地理解树的各种性质,下面是一个标注了各种关系的树结构图:
A的子孙
兄弟节点
根节点
节点B
节点C
节点D
叶子节点E
叶子节点F
叶子节点G

图例说明:

  • 粉色节点:根节点(无父节点)
  • 蓝色节点:叶子节点(度为0)
  • 红色连线:父子关系
  • 虚线框:兄弟节点(具有相同父节点)
  • 点线框:某个节点的所有子孙

3. 树的存储结构

声明:因为树的结构实现和增删查改的操作都比较复杂,所以一般不使用树这个数据结构来存储数据,毕竟就只是存数据的话线性表才是大哥大(主要是提醒读者不用太过纠结增删查改的操作)。

3.1 双亲表示法

使用双亲表示法存储树,其每个节点除了存储数据元素外,就只存储了其父节点的位置。

单个节点的结构

cpp 复制代码
#define MAXSIZE 10000
typedef int DataType;
struct TreeNode
{
    DataType val;
    int parent;   // 存父节点所在位置的下标
};
struct Tree
{
    TreeNode data[MAXSIZE];
    int size;
};

以上每个节点就都存储了其父节点,因为根节点没有父节点,所以就约定根节点的父亲是 -1。

通过上面的双亲表示法我们发现,虽然这样的结构清晰明了,且由于除了根节点外其他节点都有父节点所以空间也没有浪费,但是通过这种方法我们只能知道每个节点的父节点,如果想要找其孩子节点就会很不方便。那 "那是不是可以在每个节点结构里加上存储孩子节点的位置的变量?"

对于上面的问题,答案当然是可以,但是在动手之前先想想要怎么存储孩子节点的位置呢?因为树的每个节点的孩子数量是不确定的,所以如何存储孩子节点又成了一个问题,方案有两种:

(1)每个节点的指针域的大小都设置成树的度的大小(但是这样的话,如果各节点度数相近时这反倒可以成为优势)

此时结构就可以看作是这样的,但从图中可以发现,当各节点的度相差很大时,就会产生很多的空间浪费。

(2)使用链表存储各个节点的孩子节点

给每个节点都开一个链表存储它的孩子节点,这样就可以很好的解决每个节点的孩子的数量不确定的情况,此时刚好就引出了下一种存储结构 -> 孩子表示法。

下面是双亲表示法的可视化示意图,展示了节点如何通过parent字段指向其父节点:
树形结构
顺序表存储
对应
对应
对应
对应
对应
下标: 0

值: A

parent: -1
下标: 1

值: B

parent: 0
下标: 2

值: C

parent: 0
下标: 3

值: D

parent: 1
下标: 4

值: E

parent: 1
A
B
C
D
E

说明:

  • 每个节点存储自己的值和父节点的下标
  • 根节点A的parent为-1
  • 节点B、C的parent为0(指向A)
  • 节点D、E的parent为1(指向B)

3.2 孩子表示法

孩子表示法是一种主要用孩子节点来表现树的各个节点关系的存储结构。

孩子表示法的核心是使用一个顺序表存储每个节点,每个节点都可以看作是一个链表的头节点,它们各自对应的链表存储了该节点所有孩子在顺序表中的位置。

结构代码如下:

cpp 复制代码
#define MAXSIZE 1000
typedef int DataType;
struct CNode   // 链表节点的结构
{
    int pos;
    CNode* next;
};
struct TreeNode   // 各个节点结构
{
    DataType data;
    CNode* firstchild;
};
struct Tree
{
    TreeNode nodes[MAXSIZE];   // 存储了每个节点
    int n, r;   // 节点数量和头节点
};

下面是孩子表示法的可视化示意图,展示了顺序表与链表的结合存储方式:
链表2: B的孩子
链表1: A的孩子
顺序表
树形结构
A
B
C
D
E
下标0: A

firstchild → 链表1
下标1: B

firstchild → 链表2
下标2: C

firstchild → NULL
下标3: D

firstchild → NULL
下标4: E

firstchild → NULL
pos: 1 (B)
pos: 2 (C)
NULL
pos: 3 (D)
pos: 4 (E)
NULL

说明:

  • 顺序表存储所有节点,每个节点包含数据和指向孩子链表的指针
  • 链表存储该节点所有孩子在顺序表中的位置
  • 节点A的孩子链表包含B(1)和C(2)
  • 节点B的孩子链表包含D(3)和E(4)
  • 叶子节点C、D、E的firstchild为NULL

3.3 孩子兄弟表示法

有了上述的孩子表示法,确实是可以解决了树的存储的问题了,但是还是会有的人觉得使用孩子表示法太复杂了。那有没有更简单一些的结构呢?自然是有的,也就是接下来要说的孩子兄弟表示法。

对于每个节点的结构,都只定义两个指针。一个指针为 child 指向该节点的第一个孩子节点,另一个指针为 brother 指向该节点的右边的兄弟节点。

cpp 复制代码
typedef struct TreeNode
{
    int data;
    struct TreeNode* child;
    struct TreeNode* brother;
} TreeNode;

这样就可以简单地存储树这个数据结构了,同时孩子兄弟表示法也是较为频繁地用来存储树的结构。

下面是孩子兄弟表示法的可视化示意图,展示了每个节点如何通过child和brother指针连接:
指针说明
红色虚线: child指针
绿色实线: brother指针
A

child→B

brother→NULL
B

child→D

brother→C
C

child→NULL

brother→NULL
D

child→NULL

brother→E
E

child→NULL

brother→NULL

对应的树形结构:
A
B
C
D
E

存储逻辑:

  • 节点A:child指向第一个孩子B,brother为NULL(无右兄弟)
  • 节点B:child指向第一个孩子D,brother指向兄弟C
  • 节点C:child为NULL(无孩子),brother为NULL(无右兄弟)
  • 节点D:child为NULL(无孩子),brother指向兄弟E
  • 节点E:child为NULL(无孩子),brother为NULL(无右兄弟)

4. 二叉树

4.1 二叉树的概念

二叉树是一种由 n 个有限节点组成的树。每棵二叉树又分为左子树和右子树,左子树和右子树又都有自己的左子树和右子树,所以二叉树大多数操作也都是用递归来实现的。

二叉树示意图 二叉树示意图 二叉树示意图

二叉树的性质

  • 二叉树的每个节点的度最大是 2
  • 二叉树的左孩子和右孩子是分顺序的,不能随意交换左右孩子,所以二叉树也是有序树

4.2 特殊的二叉树

引子:由于二叉树每个节点的情况可以是空节点只有左子树只有右子树既有左子树又有右子树 ,就这样看二叉树的结构还是太复杂了,所以对二叉树做更近一步的限制,就有了下面的两种特殊的二叉树:满二叉树完全二叉树

4.2.1 满二叉树

二叉树中除了叶子节点外,其余所有的节点的度都为 2 的二叉树称为满二叉树

如上图就是一个满二叉树。根据满二叉树的结构,又可以得出来满二叉树的性质:

满二叉树的性质

  • 以根节点为第一层,第 k 层的节点数量为 2^(k - 1)
  • 以根节点为第一层,高度为 k 的满二叉树的节点数量为 2^k - 1
  • 节点数量为 n 的满二叉树,树的高度为 log ⁡ 2 n \log_2 n log2n

对比上面出现过的树,满二叉树 就显得非常完美,但是 "它的缺点就是太过完美了",所以在实际中并不会经常使用到满二叉树。但是此时又由满二叉树衍生出来另外一种非常实用的特殊的树 --> 完全二叉树

4.2.2 完全二叉树

完全二叉树是一种特殊的二叉树,它满足以下两个条件:

  1. 结构连续性:除了最后一层外,其余所有层都是满的(即节点数达到该层最大值)
  2. 节点排列顺序 :最后一层的节点都连续、紧密地排列在左边

换句话说,完全二叉树可以看作是由一个高度为 h 的满二叉树 ,从右至左、从下至上地依次去掉若干个叶子节点后得到的二叉树。


关键特征:

  • 完全二叉树不一定是满二叉树,但满二叉树一定是完全二叉树
  • 完全二叉树的节点编号(若按层序从 1 开始编号)具有很好的规律性,这使得它非常适合使用数组(顺序表) 来高效存储和访问,这也是堆(Heap) 这种数据结构通常采用完全二叉树实现的原因

示例:

下图展示了一棵高度为 4 的完全二叉树。注意最后一层(第 4 层)的节点都靠左排列。
1
2
3
4
5
6
7
8
9

完全二叉树示意图 完全二叉树示意图 完全二叉树示意图

小提示:完全二叉树也可以看成是一个满二叉树在最后一层从右至左依次删除若干节点得到的。

4.3 二叉树的存储结构

4.3.1 顺序存储(数组)

因为二叉树没有对节点的度做限制,这就导致了它的形态有很多。而使用数组来存储变化多样的二叉树并不是很好的选择,二叉树的顺序存储结构如下:

由上图会发现,由于二叉树左右节点分配不均匀,会使得数组要空出很多位置来表示 NULL,这就导致了空间浪费,所以不考虑使用数组来实现二叉树。

但是并不是所有的二叉树都不适合使用数组来存储。因为完全二叉树除最后一层外,都是满的二叉树,最后一层的节点是从左至右依次排列的,这个性质使得它非常适合使用数组来存储。

如上图,使用数组存储完全二叉树没有任何的空间浪费。

4.3.2 链式存储

链式存储本质上就是使用链表来存储二叉树的逻辑关系。链式存储又分为二叉链和三叉链(实现红黑树、平衡树时会使用到三叉链),由于二叉树每个节点最多只有两个孩子,所以使用二叉链来存储二叉树。其结构如下:

节点结构

cpp 复制代码
typedef struct Tree
{
    int data;
    struct Tree* left;
    struct Tree* right;
} Tree;
相关推荐
冉卓电子14 小时前
MPC5604B/C MC_RGM 复位模块全解
c语言·开发语言·单片机·嵌入式硬件
南境十里·墨染春水14 小时前
数据结构——栈
数据结构
丘山望岳14 小时前
C++模板特化:类型与常量的灵活掌控
c语言·开发语言·c++
高级c14 小时前
MindIE 推理引擎架构解析
深度学习·算法·架构·cann
iiiiyu14 小时前
面向对象案例
java·大数据·开发语言·数据结构·python·编程语言
奶人五毛拉人一块14 小时前
滑动窗口算法及习题讲解
数据结构·算法·滑动窗口·子数组
菜菜的顾清寒14 小时前
力扣HOT100(28)两数相加
算法·leetcode·职场和发展
搬砖魁首14 小时前
基础能力系列 - 多线程1 - 内存序
算法·内存序·memory order
pursuit_csdn14 小时前
力扣周赛 503
java·算法·leetcode