树形结构是一种广泛应用的非线性数据结构,它在计算机科学和日常生活中都有广泛的应用。比如文件系统,邮件系统,编译器语法树,决策树,网络通信,甚至机器学习当中,都有树形数据结构的影子。本文旨在梳理日常用到的各类树形结构以及其优点和劣势,让渎者对树形结构有一个深入的认知和了解。下面列举几类常见的树形结构的应用场景。
计算机中用于存储和管理文件的一种系统,它使用树形结构来组织文件和目录。每个文件和目录都存储在树的一个节点上,父节点表示上级目录,子节点表示下级目录或文件。
互联网的域名解析系统(DNS)采用了层次式树形结构,这种结构由多个层次组成,最上层为全球根服务器。每个国家、地区以及较大的单位都有自己的域名服务器。这些服务器通过复杂的树形结构相互连接,形成一个分布式的数据库网络。
我们观察一个域名,以在线打字练习网站,《巧手打字通》为例,其域名为:www.laidazi.com,就会发现它被两个点号分割成了三个部分。其中com为顶级域名,laidazi为二级域名,www为三级域名。
这种层次式树形结构的采用,不仅提高了域名解析的效率,还保证了域名解析的稳定性。即使某个域名服务器出现故障,整个DNS系统的正常运行也不会受到太大影响,因为数据分布在多个服务器上,可以通过其他可用的服务器完成解析过程。
邮件通常是通过树形结构进行组织和管理的。每个邮件可以看作是树的一个节点,回复或转发的邮件可以作为该邮件节点的子节点,形成一个树形结构。
程序代码在编译过程中,编译器使用树形结构(称为语法树)来表示源代码的结构和语义。每个语法树节点表示特定的语法结构,如一个函数、一个循环或一个条件语句。
在决策树中,每个节点表示一个决策点,每个分支表示一个可能的决策结果,叶子节点表示决策的最终结果。决策树常用于分类、预测和优化等领域。
在工程实践过程中,数组,链表,栈,队列等数据结构应该都是最常用的,它们都属于有序的数据元素组合,也被称为线性结构。它们在数据的存储,遍历,排序,分析计算等场景中发挥着重要作用。
但是这种线性结构在处理具有层次,分类,关系复杂的数据时,就会显得力不从心。而树形结构在这方面却有着显著优势。
我们也可以形象的将某些线性结构比类比成由单个子节点串联起来的特殊树形结构。
二叉树就是每个节点最多存在两个子节点。其特点是具有有序性,即左子节点总是在右子节点之前。这种特性可以用来解决排序、搜索、数据压缩等各种问题。二叉树根据节点的分布情况,又细分为完全二叉树和平衡二叉树。
完全二叉树,由于它的节点是按照层次顺序进行排列的,任意节点的左子节点的键值小于该节点的键值,右子节点的键值大于该节点的键值,这种基于二分查找的思想,使得查找操作变得相对简单高效。这种查找方式的时间复杂度为O(log n),在数据量较大时具有较高的效率,因此主要用于优化某些特定的查找操作,日常经常用到的堆排序就是使用了这种完全二叉树结构。
完全二叉树存在各个链路节点的深度参差不齐的问题,极端的情况下,就退变成类似于链表数据结构了。这就会使得数据查找的时间复杂度变为O(n)了。
平衡二叉树是一种自平衡的二叉查找树,主要弥补了完全二叉树存在的节点深度差异问题。这在需要频繁进行查找和插入操作的场景中,能够在最坏情况下保持O(log n)的查找性能,红黑树就是一种自平衡的二叉查找树,其中每个节点都有一个颜色属性(红或黑)。通过颜色和旋转规则来维护树的平衡,确保从根到叶子的最长路径不会超过最短路径的两倍。
平衡二叉树为了保证任何时刻树的高度都保持平衡,需要进行大量的左旋、右旋、高度计算等操作,这会导致插入和删除节点的速度相对较慢,因此不太适合用于大数据量的场景。
在现实应用场景中,由于二叉树的子节点个数受到限制,这会导致树的高度相对较高, 而且附带的业务数据本身也是存储到树的中间节点上的,导致对数据的读写要经历较长的节点路径。大数据场景下,不可能将所有数据都加载到内存中,因此每个树节点的访问都需要磁盘IO, 导致磁盘IO读写次数也较高,而磁盘IO的读写速度相对内存访问速度会慢很多,因此二叉树不适宜处理海量大规模数据。
B-Tree通过增加子节点的个数,减少了树的层级,可以极大的减少磁盘的IO次数,解决了平衡二叉树磁盘访问效率低的问题,B树本身也是一种自平衡树,可以自适应地动态更新数据,通过节点裂变的方式保持自平衡。
由于B-Tree中间节点(即非叶子节点)上同时存储了关键字,数据记录和子节点的索引指针数据,体积相对较大,如果都调入到内存中,这会使得内存利用率不高。在查询效率层面,由于采用中序遍历策略,每次查询会随着靠近根节点的数据而变化,不是很稳定,对于范围查询效率也不高。
B+树(B+Tree)是对B树的一种改进,它与B树的不同点主要包括:
•B+树的所有数据记录都存储在叶子节点上,中间节点只存储关键字和子节点索引指针数据;
•B+树的所有叶子节点被一个链表按照数据的从小到大的顺序串联起来。因此B+树上对数据的结果集的访问可以不用回溯到非叶子节点,直接通过叶子节点链表就可以直接收集,效率比较高。
由于数据结构调整,B+Tree在磁盘读写效率、范围查询效率和内存利用率等各个方面要优于B-Tree。
多路搜索树对插入、删除和修改操作都有比较高效的节点调整或裂变等方式来保证树的平衡性,从而保证O(lgN)的高效查询时间复杂度。但当应用场景扩展到多维度的空间索引查询时,由于父子节点兄弟节点之间的大小有序关系表现在多个维度上,节点调整或裂变操作还需要修改数量较多的离得较远的叶子节点或祖先节点(极限情况可能波及整棵树),就会导致数据动态更新效率变低。
KD-Tree(K-Dimensional Tree)是一种高维索引树形数据结构,用于在k维空间中存储和检索实例点。
上图是对6个二维平面坐标轴的数据点集合:(2,3), (5,4),(9,6),(4,7),(8,1),(7,2)的存储。
1.先按照X维度值7把空间一分为二得到x<=7 和 x>7两部分左子树和右子树;
2.再对左子树按Y维度4再一分为二;
3.按照上面两个步骤依次递归类推,即:第一层先按X维划分,第二层按Y维度划分, 第三层按X维度划分,第4层再按Y维度划分,循环往复。
KD-Tree适宜处理多维数据,查询效率较高。一个静态多维数据集合通过KD-Tree后,查询时间复杂度是O(lgN)。
KD-B-Tree(K-Dimension-Balanced-Tree)顾名思义,结合了KD-Tree和B+Tree。它主要解决了KD-Tree的二叉树形式树高较高,对磁盘IO不够友好的缺点,引入了B+树的多叉树形式,不仅降低了树高,而且全部数据都存储在叶子节点,增加了磁盘空间存储利用率。
因为KD-Tree和KD-B-Tree的分层划分是依据维度依次轮替进行的,动态更新后调整某个中间节点时,变更的当前维度的同时,也需要调整其全部子孙节点中的当前维度值,导致对树节点的访问和操作增多,操作耗时增大。因此,KD-Tree更适宜处理的是静态场景的多维海量数据的查询操作。
BKD-Tree(Block-K-Dimension-Tree ),它是一族(Block)KD-Trees。即它其实是一个森林,不再是一颗树。
该数据结构是由杜克大学的几位教授基于KD-B-Tree设计的,同时也结合了一种被称为 “logarithmic method" 的方法,使得静态数据动态化。
1.不同于KD-B-Tree的多叉树,BKD-Tree是完全二叉树。虽然二叉树不如多叉树的磁盘IO更友好,但是BKD-Tree仍然采用二叉树的形式主要原因可能在于:
•采用了完全二叉树的形式,类似于堆或优先级队列,能直接通过下标访问父节点或子节点;
•减少了存储在中间节点中的索引的存储空间,极简紧凑的中间索引节点占用空间更小,甚至中间节点可以一次性全部调入内存存储,调用内存访问索引节点效率更高。
1.采用一种被称为 “logarithmic method" 的方法使得静态数据动态化, 极大提高了动态数据更小的效率。
•采用一个二叉的KD-B-Tree的森林。新增加的数据存储在一个初始支持高效查询和动态更新的小规模数据结构上M,再通过M和多个小的二叉KD-B-Tree,以一定的策略和时机合并成大的二叉KD-B-Tree,以替换原结构的方式更新数据;
•数据删除可以采取标记删除的方式实现,从而实现了多维数值数据的高效率动态更新操作;
在Lucene中,BKD-Tree被用来解决大规模数据集的搜索和过滤操作的效率问题。为了支持高效的数值类或者多维度查询,Lucene引入了Bkd-Tree。
Trie树通过将字符串的字符作为节点进行存储,其底层使用的就是多叉树数据结构。经常用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。其主要优点是就是最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie树前缀匹配常用于搜索提示。如当输入一个关键字,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。
随机森林是一种监督式算法,由众多决策树组成,通过自助采样的方法生成众多并行式的分类器(决策树),通过“少数服从多数”的原则来确定最终结果 。随机森林使用多棵树来避免和防止单纯决策树过拟合的问题。其主要特点有以下两点:
1.随机:随机选择样本,随机选择特征;
2.集成学习:投票选举策略;
下面举个实际的例子,比如我们想根据某个人的年龄(age),性别(gender),学历(edu),行业(industry),居住地(city)共5个特征属性来预测其收入层次(高,中,低)。
随机森林中,每颗树都可以看做是一颗分类回归树(CART),假设森林中有5可CART树,总特征树N=5,取M=1(表示每个CART树对应一个不同的特征)。
基于不同的特征,每一个决策树都会给出一个收入层次的概率(特征概率值就是我们模型训练后的结果),拿图中的示例来看,预测结果如下:
1.age(20-40):7%,23%,70%;
2.gender(male):3%,27%,70%;
3.edu(大学):6%,14%,80%;
4.city(北京):15,%,20%,65%;
5.industry(互联网):5%,30%,65%;
综合取平均结果为:高收入概率-7.2%,中等收入概率-22.8%,低收入概率-70%。
随机森林采用集成学习方法,具有模型预测准确性高,抗干扰能力,可解释性强等特点,在风控,语音识别,图像分类等各种分类场景发挥着很大作用。
数据库索引通常用于加速对数据的访问,按照数据分布特点会有不同的数据结构,常见的有B-Tree,B+Tree,R-Tree,Hash Index,Bitmap Index,Bloom Filter等。下面就以mysql的Innodb引擎中使用的B+Tree索引结构为例,探究数据结构如何指导着我们上层应用的。
先看一张典型的B+Tree索引数据结构图:
索引树的深度应该越少越好,它会影响磁盘空间的占用以及索引的查询效率。影响索引深度的因素主要有以下几点:
1.索引数据的重复度,重复度越高,索引树深度相对就会增加;
2.索引字段的数据越小越好,这也会影响索引深度;
3.索引数据的量级,数据量越大,索引树深度也会增加;
InnoDB数据的最小的存储单位是Page(默认大小为16k),假设主键占8字节,指针占6个字节,一行数据记录大小为1k,那么,单个非叶子节点可以存储数据个数:16384/14=1170,单个叶子节点可容纳记录数16K/1K=16条。
数据总容量 = 根节点节点指针数量 * 每页保存的数据量。
假设一个高度为3的B+树,它可以存放的记录数:1170*1170*16=21902400条。
由此得出结论建议,如下:
1.重复度较高的字段(比如性别,状态等字段)上不适合建立独立索引,这往往会导致树的深度增加。如果确实有需要,可以考虑联合其它重复度低的属性字段,一起组合建立复合索引;
2.对于索引字段数据过大的问题,可以考虑使用前缀索引来减少索引的大小;
3.确保主键索引是唯一的,并且不包含NULL值。这有助于提高查询速度和数据的完整性;
4.数据记录长度越大,同等树深的数据容量就会越少;
5.数据量大了,可以考虑通过分表来缓解树深度过大的问题;
在B+Tree中,主要的存储结构信息,如下:
1.中间节点存储键值信息、子节点指针、引用计数;
2.叶子节点会存储键值信息、数据记录指针、删除标记;
3.叶子节点之间还通过指针相互连接,形成一个链表结构,便于顺序访问;
中间节点和叶子节点在InnoDB索引中扮演着不同的角色。中间节点用于组织和维护索引的结构,而叶子节点则存储了所有的数据记录,并提供对数据的直接访问。通过合理地设计和使用索引,可以提高数据库查询的性能和效率。
由此得出结论建议,如下:
1.使用覆盖索引:如果一个索引包含了查询所需要的所有数据,那么查询可以直接通过索引来获取数据,而不需要回表。这称为覆盖索引,可以显著提高查询效率;
2.避免使用函数或表达式:数据库查询优化器通常基于列的值来选择和使用索引。当列上的函数或表达式被应用时,优化器可能无法识别或利用这些索引,从而导致索引失效;
3.范围查找很高效:由于B+Tree叶子节点之间顺序链接的特性,使得范围查找非常高效。它能够快速定位到指定范围的起始和结束位置,然后通过线性遍历来获取所有范围内的值,从而避免了全树遍历。
4.最左前缀原则:当我们创建一个联合索引时,例如(a,b,c),实际上是创建了一个由列a、b和c组成的B+树。在这个树中,每个节点都包含了a、b和c的值,并且按照a、b和c的顺序进行排序。如果我们要查找列a和b的值,而跳过了列a直接查找列b的值,那么B+树的范围查找就无法使用了。因为B+树的范围查找是基于列的顺序进行的,如果跳过了最左边的列,那么就无法确定要查找的范围了。
5.控表的索引个数:过多的索引无论是在存储空间的占用方面,还是在索引维护的性能开销方面(锁竞争),都有不小的负面影响。索引的设计尽可能的考虑复用;
从线性结构到二叉树,是树形结构的一次飞跃。二叉树使得数据结构可以呈现层级关系,大大简化了数据组织的复杂性。平衡树在此基础上,通过平衡操作保持树的深度在可接受范围内,从而在插入、删除等操作时能保持较好的性能。
多路搜索树如B树、B+树等,进一步扩展了树形结构的应用范围。它们允许节点有多个子节点,从而在保持树的平衡性方面更加灵活。多维树则将树形结构从一维扩展到多维,使得数据结构能够更好地适应复杂数据类型和多维数据关系。
在应用方面,树形结构被广泛应用人工智能、图形学、网络协议等众多领域。掌握其底层实现原理后,能够更好的解决工程测的痛点问题。比如:在数据库系统中,索引常常采用树形结构来提高查询效率,了解其内部实现后,就能够在资源使用率和性能方面拥有更好的表现。