树作为一种很常用的数据结构,主要包括二叉搜索数(BST)、多路搜索树(B-树)、B树根据叶子节点树分为二叉树和多叉树。根据左右节点是否高度上对称,分为平衡树和非平衡树,平衡树的一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡树。简单的说就是左右高度差不多。
树根据其组织节点的形式和添加删除的操作方式将树分成的很多种:大的种类包括:
二叉树 |
|
自平衡二叉查找树 |
|
B树 |
|
Trie |
|
空间划分树 |
|
非二叉树 |
|
其他类型 |
|
二叉树是指每个节点有两个子节点的树,自平衡二叉查找树是指在经过很多添加删除操作后树仍是平衡的,因为其添加删除操作中本身就考虑了平衡问题。
Trie树又叫做单词查找树,或字典树。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
空间划分树
1.所有非叶子结点至多拥有两个儿子(Left和Right);
2.所有结点存储一个关键字;
3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树;
如:
这样定义的二叉查找树不一定是平衡的,不平衡的二叉树在实际使用中作用不是很大。因为经过一系列的添加删除操作后,如果树不能保持平衡,很容易变成左重右轻或者右重左轻。这样的查找树效率非常低,逼近于排序数组的遍历。
1.定义任意非叶子结点最多只有M个儿子;且M>2;
2.根结点的儿子数为[2, M];
3.除根结点以外的非叶子结点的儿子数为[M/2, M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5.非叶子结点的关键字个数=指向儿子的指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i];
7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8.所有叶子结点位于同一层;
如:(M=3)
其中P1、P2、P3都代表指针。
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找;
5.自动层次控制;
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率,其最底搜索性能为O(LogN)。
搜索取值范围内的数。
B+树是专门为了在磁盘上存储而设计出来的数据结构。不考虑实际的磁盘情况,B+树对于B-树没有什么优势,甚至有劣势。但是由于磁盘的特殊性质B+树在磁盘的文件系统和数据库的应用远比B-树要合适。
B+树是B-树的变体,也是一种多路搜索树:
1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[ K[i], K[i+1])的子树(B-树是开区间);
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
如:(M=3)
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4.更适合文件索引系统;
1. 不同于B-树只适合随机检索,B+树同时支持随机检索和顺序检索,在实际中应用比较多。
2. B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引。
在实际的涉及到磁盘搜索中,一般使用B+树。原因是B+树与B-树在数据组织方面的区别。B+树的所有记录都放在叶子节点,而B-树则是分散在整个树中。看起来从上到下的搜索B-树可能不用到叶子节点就可以搜索到数据,不考虑实际的情况,确实B树能更快速的查找。
但实际的情况是磁盘是分块的,每一块的大小是有限制的,B-树每个节点既有数据又有指针,这个数据在应用中可不会只是一个整数,而且大小还经常是不一样的。这就决定了磁盘的一个块可以放少数的B-树节点,但可以放很多B+树的非叶子节点。由于搜索是一个涉及几乎全部节点的操作,B+树的非叶子节点不包含实际的数据,只作为索引使用,所以相当于通过一个短小的索引表(所占用物理空间)就可以定位出叶子节点,最终检查叶子节点只需要一次。而B-树每次查询索引都需要检查一个带有数据的节点,如此导致其走过的节点所占用的数据空间大小明显大。在磁盘中,分散的,多次读取是非常浪费磁盘性能的,导致B-树在磁盘中的查找效率远远低于B+树。有人可以说全部读取到内存,B-树的效率不就高了吗?对于数据系统,把节点全部读取到内存再处理是不现实的,因为通常节点成千上网甚至千万。
是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针,提高空间的利用率。其区别是兄弟节点如果一个太满,就将一部分数据转移到不那么满的兄弟节点。如果兄弟们都满了,就新建兄弟节点。而B+树只会在一个兄弟满的时候新建新的兄弟节点。
B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高;
BST树:二叉搜索树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点;
B-树(B树):多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键字范围的子结点;所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;
B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3;
所以在linux应用中,自然
Linux内核中实现了一个通用的B+树,这个树非常简单,以至于不适合实际的磁盘应用。以至于很多文件系统都自己又实现了一份B类树。
头文件位于 include/linux,定义文件位于 lib目录下。可以看出这是linux提供的工具代码。
Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询,用于指针与整数值的映射(如:IDR机制)、内存管理等。需要特别注意的是,linux的基树与课本里所学的数据结构的基树是完全不同的。这个数据结构设计是用来取代稀疏整数数组的。
radix树是通用的字典类型数据结构,radix树又称为PAT位树(Patricia Trie or crit bit tree)。Linux内核使用了数据类型unsigned long的固定长度输入的版本。每级代表了输入空间固定位数。
radix tree是一种多叉搜索树,树的叶子结点是实际的数据条目。每个结点有一个固定的、2^n指针指向子结点(每个指针称为槽slot),并有一个指针指向父结点。
Linux内核利用radix树在文件内偏移快速定位文件缓存页,图4是一个radix树样例,该radix树的分叉为4(22),树高为4,树的每个叶子结点用来快速定位8位文件内偏移,可以定位4x4x4x4=256页,如:图中虚线对应的两个叶子结点的路径组成值0x00000010和0x11111010,指向文件内相应偏移所对应的缓存页。
Linux radix树每个结点有64个slot,与数据类型long的位数相同,图1显示了一个有3级结点的radix树,每个数据条目(item)可用3个6位的键值(key)进行索引,键值从左到右分别代表第1~3层结点位置。没有孩子的结点在图中不出现。因此,radix树为稀疏树提供了有效的存储,代替固定尺寸数组提供了键值到指针的快速查找。
图1 一个3级结点的radix树及其键值表示
Linux基树与trie树非常类似,只是一个是用来查询定位字符串,每一个是用来查询定位整数。
红黑树是就是平衡二叉查找树的实现。单纯的查找二叉树由于多次添加删除可能不平衡,导致效率变低,所以内核中实际使用的是红黑树。
红黑树通过定义一部分节点是红的,一部分是黑的,在插入删除时根据预先制定的红黑规则对树做一些变换,以保证每次添加删除操作后树都是保持平衡的。也就是说红与黑都是为了达成保持平衡的算法所设计的附属的手段。
从图中也可以看出,红黑树也是一颗查找树。都是左子节点<本节点<右子节点。
从以上可以看出,树在实际应用的主要作用就是快速的比较并定位值。针对不同的场景和不同的应用有不同的变体。
优先排序列表
红黑树
区间树
根树
信号量和自旋锁
这些待续哈
IDR机制在Linux内核中指的是整数ID管理机制。实质上来讲,这就是一种将一个整数ID号和一个指针关联在一起的机制。这个机制最早在03年2月加入内核,当时作为POSIX定时器的一个补丁。现在,内核中很多地方都可以找到它的身影。
IDR机制适用在那些需要把某个整数和特定指针关联在一起的地方。例如,在IIC总线中,每个设备都有自己的地址,要想在总线上找到特定的设备,就必须要先发送设备的地址。当适配器要访问总线上的IIC设备时,首先要知道它们的ID号,同时要在内核中建立一个用于描述该设备的结构体,和驱动程序。将ID号和设备结构体结合起来,如果使用数组进行索引,一旦ID号很大,则用数组索引会占据大量内存空间。这显然不可能。或者用链表,但是,如果总线中实际存在的设备很多,则链表的查询效率会很低。此时,IDR机制应运而生。该机制内部采用radix tree实现,可以很方便的将整数和指针关联起来,并且具有很高的搜索效率。