您的位置:首页 > 理论基础 > 数据结构算法

如何扩充数据结构

2013-08-09 17:25 260 查看

如何扩充数据结构

我们在用数据结构的时候经常找不到适合的,树图堆栈这些根本满足不了我们的需要。有的时候我们不得不去设计一些数据结构,可是那会很麻烦,而且很难。所有我们在设计新的数据结构的时候经常拿现有的数据结构,然后在上边添加一些自己需要的功能,以便支持我们的新操作。这就是我们要说的数据结构的扩充。
如何扩充数据结构?我们由一个例子来引入。

OS树

之前我们说过TOPK问题,讲了很多方法,其中有一个就是快速选择QuickSelect可以在O(lgN)的时间内找到N个元素中的第i大的元素。
常规的遍历方法需要O(N)的时间才能在N个无序的序列中找到第i大的元素。这里我们来说一个别的方法,利用数据结构的扩充。
一个二叉排序树可以对N个元素的序列进行排序,可是这并不能在O(lgN)时间内找到第i大的元素。而且还会造成树的高度达到最坏的情况,即便是排序我们也要选择红黑树,不选择二叉查找树。
对于红黑树来说,可以做到很好的排序时间复杂度O(lgN),可是依旧没有办法在O(lgN)的时间内找到第i大的元素。可是如果我们在每个结点加一个域size,size[X]就是以X为根的RB树的内部结点数目(没有NIL,包括X结点本身)。如果要定义NIL[T]结点的话,则size[NIL[T]]=0.
那么对于一个结点的size域的求解的表达式就有了:
size[NIL[T]]=0; 
size[X]=size[left[X]]+size[right[X]]+1
不过我们大多数的时候是给NIL[T]做一个特殊标记,这样就不需要分类求了。大多数的时候我们处理边界值都是这么做的,给边界初始化,或者其他的方法,为的就是不用麻烦的去分类处理边界。



图中黑色结点是黑色,灰色结点代表红色,没有画NIL结点。

这样的树我们称为顺序统计树(Order Select Tree),接下来将的都是中序遍历是有序的OS树。可以有关键字相同的结点,但是其size域不一定相等。这样的树对于我们寻找第i大的数或者是第i小的元素是很简单的了。我们来看一下,OS-SELECT(x,i)返回一个指向以x为根的OS树中第i小的结点的指针。
OS-SELECT(x, i) 
1 r ← size[left[x]]+1 
2 if i = r 
3  then return x 
4 else if i< r 
5  then return OS-SELECT(left[x], i)
6 else return OS-SELECT(right[x], i - r) 
伪代码中的r就是以x为根的子树中x的秩(从小到大),的意思就是动态序,就是中序遍历的时候x的位置罢了。每一次的递归调用都是下降一层,所以这个OS-SELECT的时间复杂度是O(lgN)。
刚才说的一个RB树根结点的秩,那么如何在RB树中求一个任意一个结点的秩呢?一个结点的秩与他在OS树中的位置有关系。我们只知道每个结点的size域,而且每个结点的位置都要和根结点有关系,因为插入结点的时候我们是从根结点开始一路下降到合适的位置的,所以我们要求一个结点的秩就要从该结点开始一路上升到根结点。
假设要在T中求X的秩,如果X是根结点,直接返回size[left[X]]+1就好了。如果X不是根结点,那么首先X的位置要包括以X为根的子树的秩,也即是size[left[X]]+1。如果X结点是其父母P的右孩子,这说明左孩子left和父母P都是小于X的,所以X的秩就要加上size[left[P]]+1,如果是其父母的左孩子,那就什么都不加。这样一路升到根就像是插入的逆过程一样。
OS-RANK(T, x) 
1 r ← size[left[x]] + 1 
2 y ← x 
3 while y ≠ root[T] 
4  do if y = right[p[y]] 
5  then r ← r + size[left[p[y]]] + 1 
6  y ← p[y] 
7 return r 
不过一个OS树是在一个RB树的基础上建立而来的,一个RB树不仅仅只是静态的操作,也会有动态的插入和删除。这些动态的操作会影响OS的新操作OS-SELECT和OS-RANK吗?其实我们只要维护好每个结点正确的size域新的操作就不会受到影响。我们来分析一下。
之前我们讲过红黑树的插入和删除,这里的OS树就是多了一个size域而已。对于插入结点X来说,要一路查找下降到合适位置,这其中碰到的所有结点的size域都发生了变化,都要加上size[X],而X本身的size为1。这不会使size域不正常,但是需要O(lgN)的额外时间。插入一个结点还要调整红黑树的结点颜色,这会牵涉到旋转,旋转会导致size域发生变化吗?对于插入来说,至多旋转2次而已。旋转会使两个结点的size域失效,可是这没关系,失效的size域可以根据未失效的孩子求出来。



如果是左旋,仅仅在之前说过的代码中添加两行而已,时间代价O(1)。
size[y]=size[x]
size[x]=size[left[x]]+size[right[x]]+1
删除的话也是从一路查找下降的X的位置,删除之后,一路下降碰到的结点的size域都要减去size[X],代价是O(lgN)。而颜色的调整会发生旋转,这和刚才插入是一样的,不会改变size域。
所以来说OS树的动态操作不会改变OS树结点的size域,我们可以很好的维护,那么我们的新操作就不会受到影响。

扩充的步骤

说完了OS树,那我们来总结一下,如何对数据结构进行扩充。
对一种数据结构的扩充过程可分为四个步骤:
1、选择基础数据结构; (选择红黑树)
2、确定要在基础数据结构中添加哪些信息; (加入size域)
3、验证可用基础数据结构上的基本修改操作来维护这些新添加的信息; (插入和删除可以维护size域)
4、设计新的操作。 (OS--SELECT和OS--RANK
不过这只是一个模式罢了,不是必须遵循的步骤,很多时候都是采用的试探法,这些步骤的顺序都是可以颠倒和并行的。就像刚才OS树就不是按这个步骤来的。这里只是给出一个参考罢了。不要那么的循规蹈矩啊!

对于在红黑树上的扩充,我们这有一个定理,只要满足,就可以在红黑树上扩充:
定理:设域f对含n个结点的红黑树进行扩充的域,且假设某结点x的域f的内容可以仅用结点x,left[x]和right[x]中的信息计算,包括f[left[x]]和f[right[x]]。这样,在插入和删除操作中,我们可以在不影响这两个操作O(lgN)渐近性能的情况下,对T的所有结点的f值进行维护。

区间树

既然红黑树很好扩充,那么我们再来扩充一个,区间树。我们假设区间都是闭区间,我们用一个闭区间[t1,t2](t1<=t2)表示一个区间树的元素X。则区间X表示为int[X],t1是低端点,t2是高端点,即low[int[X]]=t1,high[int[x]]=t2.
区间树是在红黑树基础上进行扩充得到的支持以区间为元素的动态集合的操作的树。其中每个节点的关键值是区间的左端点(key[int[x]]=low[int[X]])。通过建立这种特定的结构,可是使区间的元素的查找和插入都可以在O(lgn)的时间内完成。
这只是第一步选择基础的数据结构红黑树。
我们需要的操作是INTERVAL-SEARCH(T,i)给定一个区间i,然后从区间树T中找到一个和i区间重叠的区间返回指针,否则返回NIL。(要注意的是T中可能有很多和i重叠的区间,我们找到一个即返回。)
首先要明白重叠是什么,重叠有好多种情况,对于区间i和i丿。




图中(a)表示两个区间重叠的情况,其他的(b)、(c)表示没有重叠的情况。

也即是对于两个区间i和j,如果low[i]<=high[j] and low[j]<=high[i],那i和j就是重叠了,其他情况都不是重叠。
这便是第四步,设计新的操作INTERVAL-SEARCH(T,i)。
要实现新的操作,这些还不够,我们呢无法求出所要的结果。我们需要在区间树的结点里添加一些域,我们可以添加一个max域,max[x]是以x为根的子树中所有元素的高端点的最大值,这样就可以了。当然添加什么域不是随便就能想出来的,肯定需要好多次的试探,加入max域可以使i区间在和结点区间比较是否重叠的时候选择是进入左子树还是右子树。对于区间树每一个结点X的max域max[X]=max(high[int[X]],high[left[X]],high[right[X]])
这便是第二步添加域max[x],以x为根的子树中所有元素的高端点的最大值。
这样一个区间树就可以出来了,途中黑色是黑色,灰色是红色。




我们先来实现一下新的操作,刚才说根据max域可以判断出进入哪个子树。如何判断?判断i区间是否和T中元素重叠,从根结点开始一路下降。如果左子树的max值小于low[i],则说明左子树中肯定不存在和i重叠的区间,转到右子树;否则,转到左子树。我们为什么要选择max域代表高端点最大值而不是低端点的最大值呢?因为这是根据重叠的性质来的,如果是低端点的最大值,即便是max<low[i],这说明不了要进入哪个子树中。而如果是高端点的最大值的话,max<low[i],说明肯定在右子树中,即便不在右子树中,也肯定不在左子树中。如果max>=low[i],这说明肯定要进入左子树,因为即便是不在左子树中,肯定也不在右子树。这都是因为左子树的max要小于左子树的max。
INTERVAL-SEARCH(T, i)
1 x ← root[T]
2 while x≠NIL[T] and (low[i]>high[int[x]]or low[int[x]]>high[i])
3      do if left[x]≠NIL[T] and max[left[x]]>=low[i]
4             then x ← left[x]
5         else x ← right[x]
6 return x
时间复杂度很明显是O(lgN)
如何维护添加的域max,和之前说的OS树是一样的道理,插入X和删除X需要向上升或向下降O(lgN),则max[X]=max(high[int[X]],high[left[X]],high[right[X]])。而旋转和之前也是一样,这里不再说了。
这便是第三步,维护结点的域。
OK,说完了。

转载请注明出处:http://blog.csdn.net/liangbopirates/article/details/9858563
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐