第4章 树(下)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lc250123/article/details/82896471

伸展树

具有这样特性的树叫伸展树:它保证从空树开始,任意连续 M 次对树的操作最多花费 O(MlogN) 时间。如果任意特定操作可以有最坏时间界 O(N),而我们仍然要求一个 O(logN) 的摊还时间界,那么很清楚,只要有一个结点被访问,它就必须被移动。

因为在许多应用中当一个结点被访问时,它就很可能不久再被访问。而且伸展树还不要求保留高度或平衡信息,因此可以在某种程度上节省空间并简化代码。所以伸展树很有应用场景。

用伸展的方法移动被访问的结点,分以下两种情形:

之字形:

一字形:

对结点 X 的访问会引起右边的结果。

伸展树有几种变体,我们以后讨论。


B 树

考虑这样的情况,有许多数据,内存装不下,那么就意味着必须把数据结构放到磁盘上。而磁盘的的访问时间一般都比较长,拿7200 转/min 的磁盘来说,1 转占用 1/120s,即 8.3ms,平均认为转到一般发现要寻找的信息,即 4.1ms,加上平均寻道时间,一般的访问时间会在 12ms 左右。如果数据存储在硬盘上,以前面的数据结构来说,也就 AVL 树效率最高。拿 1000W 的数据来说,需要的访问次数大约为 log10000000 = 24 次。用时约 12*24 = 288ms,这还是我们对系统拥有完整控制资源的情况下。

如何使访问次数低于 24,很明显我们要构造 M 叉查找树。在二叉树中我们需要一个键来决定到底取用两个分支中的哪个,而在 M 叉查找树中需要 M-1 个键来决定选取哪个分支,同时需要保证 M 叉查找树以某种方式得到平衡。

扫描二维码关注公众号,回复: 3425326 查看本文章

实现这种想法的一种方法是使用 M 阶 B 树:

(1)数据项存储在树叶上。

(2)非叶结点存储直到 M-1 个键,以指示搜索的方向;键 i 代表子树 i+1 中的最小的键。

(3)树的根要么是一片树叶,要么其儿子数在 2 和 M 之间。

(4)除根外,所有非叶结点的儿子数在 \left \lceil M/2 \right \rceil 和 M 之间。(保证其不会退化为二叉树)

(5)所有的树叶都在相同的深度上并有 \left \lceil L/2 \right \rceil 和 L 之间个数项,稍后描述 L。

以下是一个 5 阶 B 树的一个例子(L = 5):

如何确定 L 值:

      一个结点代表一个磁盘区块,假设一个区块容纳 8192 字节,一个键值假设为 32 字节(比如身份证号占 17 字节),在一个 M 阶的 B 树中,我们有 M-1 个键,总数占 32M-32 字节,然后有 M 个分支,由于每个分支基本上都是别的区块,因此我们可以假设一个分支就像一个指针,占 4 个字节,这样总共 36M-32 字节,那么不超过 8192 的最大 M 值为 228。假设一个数据记录占 256 字节,那么一个区块最多能装 32 个记录,如是 L=32。这样就保证每片树叶有 16 到 32 个数据记录以及每个内部结点(除根外)至少以 114 种方式分叉。如果有 1000W 记录,那么至多存在 1e7 / 16 = 625000 片树叶。在最坏情况下树叶将在第 4 层上(近似 log_{(M/2)}N)。同时我们也可以将根节点和下一层存放在内存以提升速率。

对 B 树的插入和删除:

插入时要考虑树叶是否装满,满后分裂为两片树叶,考虑父节点儿子个数是否已满,父节点已满就分裂父节点,同理往上,如果达到根结点满,就分裂根结点,然后添加一个新根。还有一种方法处理儿子过多的情况,就是在相邻结点有空间时把一个儿子过继过去。

删除时要考虑叶结点小于 \left \lceil L/2 \right \rceil 的情况,我们可以在没有达到 L 值时合并一个邻项来矫正,如果最终导致根结点只有一个儿子,就删除根节点。如下是删除 99 后的情况:


树在标准库中的应用

下面两个 STL 容器都是由自顶向下红黑树实现的:

set 容器:set 是一个排序后的容器,该容器不允许重复。

map 容器:map 用来存储排序后的由键和值组成的项的集合。键必须唯一,但是多个键可以对应同一个值。因此,值不需要唯一。在 map 中的键保持逻辑排序后的顺序。

map 中需要注意的地方:

(1)map 中有一个重要的操作符重载: ValueType & operator[] ( const KeyType & key );  其有改变 map 本身的功能,如果 map 中存在这个 key,就返回指向相应的值的引用。如果 map 中不存在 key,就在 map 中插入一个默认的值,然后返回指向这个插入的默认值的引用。所以如果函数中传入的是 const map 就不要使用这个功能。

map<string, double> salaries;

salaries["Pat"] = 7500.00;
cout << salaries["Pat"] << endl;
cout << salaries["Jan"] << endl;

输出:
7500.00
0

 

猜你喜欢

转载自blog.csdn.net/lc250123/article/details/82896471
今日推荐