二叉树的存储结构
- 导读
- 一、存储结构
- 二、顺序存储结构
- 三、链式存储结构
- 结语
导读
大家好,很高兴又和大家见面啦!!!
在前面的内容中,我们已经认识了树这种新的数据结构以及二叉树这种特殊的树。
与前面我们学习的线性表不同,在树这种新的数据结构中,数据与数据之间呈现的是一对多和多对一的关系,在逻辑上就像一棵树一样从树根开始往外发散式的分布,因此我们将数据之间呈现的这种逻辑关系称为树形结构。
而二叉树作为一种特殊的树形结构,二叉树中的数据与数据之间呈现的是一对二和二对一的关系,每一个数据向下都会对应两个数据;同理,每两个数据向上都会对应一个数据。不管是向下还是向上,对于一个数据而言,与它对应的这两个数据可能都为空,也可能是其中一个为空。
在今天的内容中我们将会介绍一下对于二叉树这种特殊的逻辑结构,在物理上又应该如何对这些数据进行存储。
一、存储结构
对于计算机而言,数据在计算机中对树形结构进行存储时,并不可能做到像逻辑结构一样的树形存储,因此在实际的存储过程中,我们只能够像线性表一样将数据以顺序存储或链式存储的方式存入内存中。
这时可能有朋友就会奇怪了,既然不能做到真正的树形存储,为什么会有树这种数据结构呢?
对于这个问题,我个人的理解是——不同的数据结构之间的差异主要体现在对数据的实际操作上。
例如在线性结构中,因为数据在逻辑上是一对一的进行存储的,因此我们在对线性结构中查找数据时,可以通过一个元素找到与它对应的前驱数据和后继数据;而对于树形结构而言,我们则可以通过一个数据找到与其对应的父结点和孩子结点。
在前面的介绍中我们有提到过计算机的内存空间主要是三块分区:
- 存储局部变量、函数等数据的栈区
- 存储由
malloc/calloc
这些动态函数申请的动态数据的堆区 - 存储像全局变量、静态数据以及常量等数据的静态区
不管是哪一块分区,其内存空间都是一块连续的存储空间,因此数据在实际的存储过程中并不会出现像树形结构这种特殊的结构。而在数据的实际操作过程中,我们则可以根据数据在逻辑上的关系对数据进行增删改查等相关的操作。
在理解了树与线性表的区别后,下面我们就来详细介绍一下二叉树的两种存储结构。
二、顺序存储结构
顺序存储结构我们并不陌生了,所谓的顺序存储就是通过一块地址连续的存储单元来存放数据。
在线性表的顺序存储中,对于逻辑上相邻的两个元素,我们在存储时需要使其在物理位置上也相邻;而在二叉树的顺序存储中,由于数据之间呈现的是一对多和多对一的关系,因此在实际存储中,二叉树中的各个数据在物理位置上不一定相邻。
从上图中可以看到对于线性表而言,因为元素在逻辑上是一对一的关系,因此在进行顺序存储时,每一个元素都能与其逻辑相邻的元素做到物理位置上也相邻;但是在二叉树中,因为元素在逻辑上是一对多的关系,对于元素
a
3
a_3
a3来说,逻辑上与其相邻的有三个元素
a
1
a_1
a1、
a
5
a_5
a5和
a
6
a_6
a6,但是通过顺序存储的话我们会发现这三个元素是无法做到同时与
a
3
a_3
a3在物理位置上也相邻的。
在这种情况下,我们应该如何来对二叉树中的元素进行顺序存储呢?
在前面我们有提到过,对于不同的数据结构而言,它们的区别主要体现在对数据结构中的数据元素的具体操作上。因此不管是逻辑结构还是存储结构,都是为了能够方便程序猿对这些数据进行相应的操作。
对于二叉树而言,为了体现数据之间的逻辑关系,我们可以通过元素之间的逻辑关系来对其进行顺序存储。在上一篇中我们有介绍过满二叉树和完全二叉树,在这两种二叉树中,如果给每个结点从上到下,从左到右依次进行编号的话,结点与结点的编号之间是存在一定的联系的。
例如,在从0开始编号且结点数量为 n ( n > = 1 ) n(n>=1) n(n>=1) 的完全二叉树中,对于编号为 i i i 的结点与其父结点以及孩子结点的关系如下:
- 若该结点存在父结点,则其父结点的编号为 ( i − 1 ) / 2 (向下取整) (i-1)/2(向下取整) (i−1)/2(向下取整)。若 i i i 为奇数,则该结点为其父结点的左孩子;若 i i i 为偶数,则该结点为其父结点的右孩子;
- 若该结点存在左孩子,则其左孩子的结点编号为 2 i + 1 ( 2 i + 1 < = n ) 2i+1(2i+1<=n) 2i+1(2i+1<=n);若该结点存在右孩子,则其右孩子的结点编号为 2 i + 2 ( 2 i + 2 < = n ) 2i+2(2i+2<=n) 2i+2(2i+2<=n) ;
如果将这些结点的编号作为其对应的数组空间的下标,那我们是不是就可以在对这些数据进行顺序存储时,通过其下标来找到与其在逻辑上相邻的各个元素了呢?
因此二叉树的顺序存储指的是通过一块地址连续的存储单元从根结点出发,自上而下,从左至右的存储完全二叉树上的结点元素。
在上图中可以看到,对于任意一棵二叉树,我们在进行顺序存储时都可以按照完全二叉树的形式进行存储。
这种存储方式的优点在于我们可以通过数组下标来反映二叉树中各个结点之间的逻辑关系,如果存储的是一棵完全二叉树或者满二叉树,还能起到节省存储空间的效果。
但是该存储方式的缺点也比较明显,对于一棵普通的二叉树而言,更多的是如同上图用例中一样,树中每一层的结点数量都不是最多的结点数量,都会存在些许的空缺。
在这种情况下,如果我们要满足二叉树各个结点之间的逻辑关系,那只能在空缺的结点上增加不存在的空结点,反映到内存中则是将这些空结点的空间给空出来,这样不会起到节省空间的效果,反而还会浪费大量的空间。
下面我们通过一个实例来感受一下这个缺陷。
假设有一棵高度为h结点的数量为h的二叉树,那此时如果我们要通过顺序存储的方式来存储二叉树的话,我们则需要向内存空间申请 2 h − 1 2^h-1 2h−1的空间数量,但是在这些空间中实际被使用的空间也仅有h个,其余的空间都是被浪费掉的。如下所示:
当 h = 10 h=10 h=10 时,此时我们需要申请1023个内存空间,而实际使用的空间只有10个,此时多余的1013个空间都是被浪费掉的。
如果树中结点存储的数据其类型为
long long
型,在64位的机器上,该数据类型需要8个字节的空间,在这种情况下我们则需要浪费8104个字节的空间,也就是8KB左右的内存空间,而实际使用的空间只有80个字节的空间,也就是不到0.1KB的空间。那么我要存储这不到0.1KB大小的数据,我需要额外的浪费8KB的空间,这显然是不合适的。当树的高度增加到100/1000/10000……可想而知,此时浪费的空间相比于实际使用的空间来说是非常巨大的,
因此,二叉树的顺序存储结构更适合对满二叉树和完全二叉树这种空间利用率更高的特殊二叉树。
三、链式存储结构
对于一般的二叉树而言,顺序存储的空间利用率低下,因此二叉树一般采用的都是链式存储的形式,通过链表的结点来存储二叉树中的每个结点。
在二叉树中,结点的结构通常包括3个部分:存储数据的数据域data
、指向左子树的左指针域lchild
以及指向右子树的右指针域rchild
。
在二叉树的链式存储中,数据域的数量和指针域的数量并没有明确的规定,因此可以存在多个数据域和指针域。
上图所示的只包含左指针域和右指针域的结点所组成的链表我们将其称为二叉链表。二叉链表在内存中存储时,每个结点都可以通过其左右指针域找到其对应的左右子树:
在二叉链表中,当一个结点的指针域为空指针时,表示该结点没有对应的左右子树,当指针域都为空时,则表示该结点为叶子结点。
但是因为二叉链表只能够找到结点的左右子树,因此如果我们需要遍历一棵二叉树的全部结点,我们只能传入该二叉树的根结点。
当我们需要找到一个结点的父结点时,对于二叉链表来说,就比较为难了。为了即能够找到一个结点的左右子树,还能找到该结点的父结点,此时我们便可在结点中增加一个指向父结点的指针parent
:
像这种同时拥有指向左右子树的左右指针和指向父结点的父指针的结点组成的链表我们将其称为三叉链表。
对于一棵二叉树而言,除了根结点没有父结点外,其余的结点都有且仅有唯一的一个父结点,因此,在三叉链表中,从任意一个结点开始,都能够找到二叉树中的所有结点:
对于二叉链表与三叉链表而言,这两种链表在基本操作的实现上就有一定的区别。例如当我想查找整个二叉树的全部结点时,如果使用的是二叉链表,此时我们只能从根结点出发才能够完成所有结点的查找工作;而使用三叉链表时,我们可以从任意结点出发,都能够完成所有结点的查找工作。
而使用不同的存储结构时,对应操作的实现也会有所不同。例如当我们使用顺序存储结构来实现查找二叉树中的全部结点时,我们可以根据结点的下标之间的关系来完成整棵二叉树的查找工作;当我们使用链式存储结构来实现时,我们则需要根据结点的指针域来完成整棵二叉树的查找工作。
因此对于二叉树的具体操作的算法的实现,我们需要根据实际的应用场合来选择合适的存储结构。比如对满二叉树和完全二叉树进行操作时,我们选择顺序存储的方式会更加方便;对已知根结点的一般的二叉树,需要访问其左右子树时,我们只需要选择二叉链表;如需要频繁访问父结点时,选择三叉链表则更为合适。
结语
在今天的内容中,我们详细介绍了二叉树的两种存储结构——顺序存储和链式存储:
- 当我们要存储一棵完全二叉树时,我们可以通过顺序存储的方式来存储完全二叉树中的各个结点,这样能够最大程度的节省内存空间;
- 当我们要存储一棵普通的二叉树是,我们更多的是采用链式存储的方式来存储二叉树中的各个结点,以此来提高空间的利用率;
二叉树的顺序存储结构是依靠完全二叉树中各个结点的编号之间的关系得以实现各个数据之间的逻辑关系;而链式存储结构是直接通过结点的指针域来实现二叉树中各个结点的逻辑关系。
在链式存储中,我们主要介绍了两种链式存储的方式——二叉链表和三叉链表:
- 二叉链表是通过左右指针域来找到结点所对应的左右子树,适合已知根结点,需要对其左右子树进行操作的场合;
- 三叉链表是通过左右指针域来找到结点对应的左右子树,通过父指针域来找到结点对应的父结点,适合需要频繁访问父结点和不知道根结点但需要访问树中所有结点的场合;
对于二叉树的不同的存储结构,以及不同的实现方式,其使用的场所也会有所区别,因此我们需要根据实际的应用场合来选择合适的存储结构。
今天的内容到这里就全部结束了,在下一篇内容中,我们将介绍二叉树的基本操作以及C语言的实现,大家记得关注哦!!!如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以分享给身边需要的朋友。最后感谢大家的支持,咱们下一篇再见!!!