一:概述
一些工程应用只会使用教科书式的标准数据结构,但是也会有些应用需要对现有的数据结构进行少许的创新和改造,只有很少的情况会创造全新的数据结构。
二:动态顺序统计
顺序统计:n个元素中第i个顺序统计量,就是具有第i小关键字的元素。对于一个无序的集合,可以在O(n)的时间内得到任意的顺序统计量。
利用红黑树,可以在O(lg n)的时间内确定任意顺序统计量,同时也能在O(lg n)时间内确定元素的秩,红黑树中,元素的秩代表中序遍历时输出的位置。
支持快速顺序统计操作的数据结构,是在红黑树的基础上进行了少许的改造,形成了顺序统计树。顺序统计树就是红黑树,只不过树种每个节点,除了具有红黑树的key, color, left, right, p等五个性之外,还附加了另外的性质:size,它表示以x为根的子树(包括x本身)的内(不计哨兵)节点数。并且定义哨兵的size为0,所以有下面的性质:
x.size = x.left.size + x.right.size + 1
下图就是一颗顺序统计树,每个节点中,上边是key,下边是size:
1:确定给定秩的元素
利用size信息,可以在O(lg n)时间内确定给定秩的元素:OS-SELECT。它返回以x为根的子树中,指向第i小的关键字元素的指针,代码如下:
OS-SELECT(x, i)
r= x.left.size+1
if i == r
return x
else if i < r
return OS-SELECT(x.left, i)
else
return OS-SELECT(x.right, i-r)
该算法中,r表示以x为根的子树中,节点x的秩。如果i小于r,则需要在左子树中找第i小的元素,如果i大于r,则需要在右子树中找第i-r小的元素。
OS-SELECT的总时间与树的高度成正比,所以该算法的时间复杂度为O(lg n)。
2:确定一个元素的秩
给定元素x的指针,可以用OS-RANK返回元素x在树中的秩,代码如下:
OS-RANK(x)
r = x.left.size+1
y = x
while y != T.root
if y = y.p.right
r= r+y.p.left.size+1
y= y.p
returnr
在该算法中,r表示以y为根的子树中,x的秩。如果y为父节点的左孩子,则x在y为根的子树中的秩也是在y.p为根的子树中的秩。如果y为父节点的右孩子,则x在y.p为根的子树中的秩,还需要加上y.p为根的子树中,左分支的节点数和y.p本身所占的位置,才是x在y.p为根的子树中秩。所以,最后y==root时,r就是在整个树中的秩。
该算法的时间复杂度为O(lgn)。
3:对子树规模的维护
利用size属性,可以很快的计算出顺序统计量,但是,如果在统计数的改变过程中,不能在合理的时间内维护size属性,那么添加size属性将是无意义的,比如,在插入或者删除的过程中,如果维护元素size属性的时间超过了O(lg n),则size变得无意义。下面就说明,插入和删除操作,会在O(lg n)时间内完成size的维护。
插入操作,包括两个阶段,第一阶段从根节点开始沿树下降,将新节点插入到叶节点中。第二阶段是沿树上升,做一些变色或者旋转操作维护红黑树的性质。在第一阶段中,为了维护size的属性,可以将从根节点到新插入的叶子节点的路径上的每个节点的size加1即可。这个操作的时间复杂度为O(lg n)。在第二阶段,只有旋转操作才会导致size的属性变化,而红黑树的插入操作的旋转次数最多为2次。旋转操作是一种局部操作,只会使得2个节点的size属性失效,比如左旋代码中,增加下面两行就能维护size属性:
y.size = x.size
x.size = x.left.size + x.right.size +1
左旋操作如下图:
右旋操作也类似,所以对于插入操作来说,增加size属性后,插入的时间复杂度还是O(lg n)。
删除操作也是分两个阶段,第一阶段找到y,要么将y删除,要么将y上移,所以遍历一条从节点y所在位置到根节点的路径,将每个节点的size减1即可。第二阶段是为了维护红黑树性质的变色和旋转操作,因删除操作最多需要3次旋转,因而,同插入操作一样,删除操作的时间复杂度也是O(lg n)。
三:如何扩张数据结构
对基本的数据结构进行扩张,以支持一些附加功能,在算法设计中是很常见的,下面是扩张一种数据结构的基本步骤:
a:选择一种基本数据结构;
b:确定数据结构中需要维护的附加信息;
c:检验基础数据结构上的基本修改操作是否能维护附加信息;
d:设计新的操作。
比如在利用红黑树进行顺序统计的工作中,a:选择了红黑树作为基本数据结构。b:在红黑树节点中添加了size属性。c:保证插入和删除操作仍能在O(lg n)时间内维护size属性。d:设计顺序统计的操作。
当红黑树作为基本数据结构时,可以证明,某些类型的附加信息总是可以用插入和删除操作进行有效的维护:如果f是红黑树元素的附加属性,假设对于任意节点x,f的值仅仅依赖于x, x.left, x.right的信息,还可能包括x.left.f和x.right.f。那么在插入和删除操作中,可以在O(lg n)时间内维护f的属性。这是因为f属性的变动,只会影响到x的祖先,所以修改x.f,只需要更新x.p.f, x.p.p.f……。如此沿树向上即可。
四:区间树
区间[a,b]便于表示占用一连续时间的事件,例如查询由时间区间构成的数据库,找出给定时间内发生了什么事。
可以用对象i表示区间[a,b], i.low = a;i.high = b。对于两个区间i和j来说,如果:i.low <= j.high并且j.low <= i.high,则表示两个区间i和j重叠。
区间树是一种红黑树,树中的元素表示一个区间,该树支持的操作有:插入,删除,查找,其中,查找操作是返回一个指向x的指针,x与给定的区间i重叠。如果x不存在,则返回T.nil。
下图为一些列的区间:
为了支持查找重叠区间的操作,:
a:选择红黑树作为基本数据结构,每个节点表示一个区间int,节点的关键字为区间的低点,也就是x.int.low。所以该数据结构中,中序遍历就能按照low的次序列出各区间。
b:附加信息为max,max表示了以x为根的子树中,所有区间的端点high的最大值。
c:x的max值:x.max =max(x.int.high, x.left.max, x.right.max), 所以符合上面的定理,因而插入和删除操作能在O(lgn)时间内完成。
d:插入和删除操作与红黑树的一样,这里只需要设计SEARCH操作。它可以找出树中,与给定区间i重叠的节点x,如果x不存在,返回T.nil。代码如下:
INTERVAL-SEARCH(T, i)
x = T.root
while x != T.nil and i does not overlap x.int
if x.left != T.nil and x.left.max >= i.low
x= x.left
else x = x.right
return x
该算法能保证,如果树中存在与i重叠的区间,该区间一定能被找到。该算法在循环中,判断x.left != T.nil and x.left.max >= i.low,如果不满足x.left.max >= i.low,则说明左子树中的节点的high值,都小于i的low值,所以左子树中不存在与i重叠的区间,因而在右子树中。
如果满足了x.left.max >= i.low属性,则有可能左右子树中都会包含与i重叠的区间。首先在左子树中找,如果左子树中没有的话,右子树中一定也不存在,因为树的关键字为low值,左子树的low值肯定比右子树小。