伸展树
具有这样特性的树叫伸展树:它保证从空树开始,任意连续 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 叉查找树以某种方式得到平衡。
实现这种想法的一种方法是使用 M 阶 B 树:
(1)数据项存储在树叶上。
(2)非叶结点存储直到 M-1 个键,以指示搜索的方向;键 i 代表子树 i+1 中的最小的键。
(3)树的根要么是一片树叶,要么其儿子数在 2 和 M 之间。
(4)除根外,所有非叶结点的儿子数在 和 M 之间。(保证其不会退化为二叉树)
(5)所有的树叶都在相同的深度上并有 和 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 层上(近似 )。同时我们也可以将根节点和下一层存放在内存以提升速率。
对 B 树的插入和删除:
插入时要考虑树叶是否装满,满后分裂为两片树叶,考虑父节点儿子个数是否已满,父节点已满就分裂父节点,同理往上,如果达到根结点满,就分裂根结点,然后添加一个新根。还有一种方法处理儿子过多的情况,就是在相邻结点有空间时把一个儿子过继过去。
删除时要考虑叶结点小于 的情况,我们可以在没有达到 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