14.1 动态顺序统计
14.1-1
对于图14-1中的红黑树T,执行OS-SELECT(T.root,10)的过程为:26->17->21->19->20。
14.1-2
对于图14-1中的红黑树T和关键字x.key为35的结点x,执行OS-RANK(T,x)的过程:1->3->16。
14.1-3
OS-SELECT的非递归版本:
OS-SELECT(x, i)
r = x.left.size + 1
while i != r
if i < r
x = x.left
else x = x.right
i = i - r
r = x.left.size + 1
return x
14.1-4
一个递归过程OS-KEY-RANK(T,k),以一棵顺序统计树T和一个关键字k作为输入,返回k在由T表示的动态集合中的秩。假设T的所有关键字都不相同。
OS-KEY-RANK(T, k)
if k == T.key
return T.root.left.size + 1
elseif k < T.key
return OS-KEY-RANK(T.left, k)
else return T.root.left.size + 1 + OS-KEY-RANK(T.right, k)
14.1-5
给定n个元素的顺序统计树中的一个元素x和一个自然数i,在 O ( lg n ) O(\lg{n}) O(lgn)的时间内确定x在该树线性序中的第i个后继。
DETERMINE-SUCCESSOR(x, i)
while i > 0
if i <= x.right.size
x = x.right
while i <= x.left.size
x = x.left
i = i - x.left.size - 1
else x = x.p
i = i - x.right.size - 1
return x
14.1-6
在OS-SELECT或OS-RANK中,注意到无论什么时候引用结点的size属性都是为了计算一个秩。相应地,假设每个结点都存储它在以自己为根的子树中的秩。
- 在插入时维护这个信息:
由13.3节可知,红黑树上的插入操作包括两个阶段。第一阶段从根开始沿树下降,将新结点插入作为某个已有结点的孩子。第二阶段沿树上升,做一些变色和旋转操作来保持红黑树性质。
在第一阶段中为了维护结点在以自己为根的子树中的秩,对由根至叶子的路径上遍历的每一个结点x,如果新结点将插入到它的左子树中,则它的rank加1,否则它的rank不变。由于一条遍历的路径上共有 O ( lg n ) O(\lg{n}) O(lgn)个结点,故维护rank属性的额外代价为 O ( lg n ) O(\lg{n}) O(lgn)。
在第二阶段,对红黑树结构上的改变仅仅是由旋转所致,旋转次数至多为2。此外,旋转是一种局部操作:它仅会使两个结点rank属性失效,而围绕旋转操作的链就是与这两个结点关联。参照13.2节的LEFT-ROTATE(T,x)代码,增加下面一行:
y.rank = y.rank + x.rank
对RIGHT-ROTATE做相应地改动。
因为在红黑树的插入过程中至多进行两次旋转,所以在第二阶段更新rank属性只需要
O
(
1
)
O(1)
O(1)的额外时间。因此,对一棵有n个结点的顺序统计树插入元素所需要的总时间为
O
(
lg
n
)
O(\lg{n})
O(lgn),从渐进意义上看,这与一般的红黑树是一样的。
- 在删除时维护这个信息:
红黑树上的删除操作也包括两个阶段:第一阶段对搜索树进行操作,第二阶段做至多三次旋转,其他对结构没有任何影响(见13.4节)。第一阶段中,要么将结点y从树中删除,要么将它在树中上移。为了更新结点在以自己为根的子树中的秩,我们只需要遍历一条由结点y(从它在树中的原始位置开始)至根的简单路径,如果当前结点是下个结点的左孩子,则下个结点的rank减一,否则下个结点的rank不变。因为在n个结点的红黑树中,这样一条路径的长度为 O ( lg n ) O(\lg{n}) O(lgn),所以第一阶段维护rank属性所耗费的额外时间为 O ( lg n ) O(\lg{n}) O(lgn)。第二阶段采用与插入相同的方式来处理删除操作中的 O ( 1 ) O(1) O(1)次旋转。所以对有n个结点的顺序统计树进行插入和删除操作,包括维护rank属性,都只需要 O ( lg n ) O(\lg{n}) O(lgn)的时间。
14.1-7
在
O
(
n
lg
n
)
O(n\lg{n})
O(nlgn)时间内,利用顺序统计树对大小为n的数组中的逆序对(见思考题2-4)进行计数。
将数组中的n个元素依次插入顺序统计树中,插入过程中,假设第i个插入到树中的元素的秩为r,则i-r是在i之前插入但大于i的元素个数,也即逆序对数。对r进行累加,即可得到总的逆序对数。
14.2如何扩张数据结构
14.2-1
通过为结点增加指针的方式,在扩张的顺序统计树上,支持每一动态集合查询操作MINIMUM、MAXIMUM、SUCCESSOR和PREDECESSOR在最坏时间 O ( 1 ) O(1) O(1)内完成。顺序统计树上的其他操作的渐进性能不应受影响:
- MINIMUM:用一个指针指向顺序统计树中的最小元素。每次向树中插入元素时与其进行比较,如果新插入的元素更小,则将MINIMUM指向新插入的元素,否则不做修改。如果删除MINIMUM,则将MINIMUM指向它的后继。
- MAXIMUM:用一个指针指向顺序统计树中的最大元素。每次向树中插入元素时与其进行比较,如果新插入的元素更大,则将MAXIMUM指向新插入的元素,否则不做修改。如果删除MAXIMUM,则将MAXIMUM指向它的前驱。
- SUCCESSOR:给每个结点增加指向后继的指针。每次向树中插入或删除结点时,根据插入或删除结点的父结点的SUCCESSOR,可以在时间内 O ( 1 ) O(1) O(1)维护插入或删除结点的SUCCESSOR。
- PREDECESSOR:给每个结点增加指向前驱的指针。每次向树中插入或删除结点时,根据插入或删除结点的父结点的PREDECESSOR,可以在时间内 O ( 1 ) O(1) O(1)维护插入或删除结点的PREDECESSOR。
14.2-2
能在不影响红黑树任何操作的渐进性能的前提下,将结点的黑高作为树中结点的一个属性来维护:
插入和删除操作只对从根结点到某一叶结点的路径上的结点进行修改,即
O
(
lg
n
)
O(\lg{n})
O(lgn)个结点。修改每个结点的黑高只需要
O
(
1
)
O(1)
O(1)时间。因此,总的修改时间为
O
(
lg
n
)
O(\lg{n})
O(lgn)。
14.3 区间树
14.3-1
作用于区间树的结点且在 O ( 1 ) O(1) O(1)时间内更新max属性的过程LEFT-ROTATE的伪代码:
LEFT-ROTATE(T, x)
y = x.right
x.right = y.left
if y.left ≠ T.nil
y.left.p = x
y.p = x.p
if x.p == T.nil
T.root = y
elseif x == x.p.left
x.p.left = y
else x.p.right = y
y.left = x
x.p = y
y.max = x.max
x.max = max(x.int.high, x.left.max, x.right.max)
14.3-2
改写INTERVAL-SEARCH的代码,使得当所有区间都是开区间时,它也能正确地工作。
INTERVAL-SEARCH(T, i)
x = T.root
while x ≠ T.root 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
14.3-3
给出一个有效的算法,对一个给定的区间i,返回一个与i重叠且具有最小低端点的区间;或者当这样的区间不存在时返回T.nil。
MIN-INTERVAL-SEARCH(T, i)
x = INTERVAL-SEARCH(T, i)
while x ≠ T.nil and i overlap x.left.int
x = x.left
return x
14.3-4
给定一棵区间树T和一个区间i,描述如何在 O ( m i n ( n , k lg n ) ) O(min(n, k\lg{n})) O(min(n,klgn))时间内列出T中所有与i重叠的区间,其中k为输出的区间数。(提示:一种简单的方法是做若干次查询,并且在这些查询操作中修改树,另一种略微复杂点的方法是不对树进行修改。)
ALL-INTERVAL-SEARCH(T, i)
QUEUE Q
ENQUEUE(Q, T.root)
ARRAY A
while !QUEUE-EMPTY(Q)
x = DEQUEUE(Q)
if i overlap x.int
PUSH(A, x)
if x.left ≠ T.nil and x.left.max ≥ i.low
ENQUEUE(Q, x.left)
if x.right ≠ T.nil and x.int.low ≤ i.high
ENQUEUE(Q, x.right)
return A
14.3-5
对区间树T和一个区间i,修改有关区间树的过程来支持新的操作INTERVAL-SEARCH-EXACTLY(T,i),它返回一个指向T中结点x的指针,使得x.int.low=i.low且x.int.high=i.high;或者,如果T不包含这样的区间时返回T.nil。所有的操作(包括INTERVAL-SEARCH-EXACTLY)对于包含n个结点的区间树的运行时间都应为 O ( lg n ) O(\lg{n}) O(lgn)。
INTERVAL-SEARCH-EXACTLY(T, i)
x = T.root
while x ≠ T.root and x.int.low ≠ i.low and x.int.high ≠ i.high
if x.int.low ≥ i.low
x = x.left
else x = x.right
return x
14.3-6
步骤1:基础数据结构
选择一棵红黑树,其中集合中的数字仅作为结点的键存储。
SEARCH只是在二叉搜索树上的TREE-SEARCH,它在红黑树上运行的时间为
O
(
lg
n
)
O(\lg{n})
O(lgn)。
步骤2:附加信息
红黑树通过每个节点x中的以下属性进行扩充:
- x.min-gap包含以x为根的子树中的最小间隙。它有以x为根的子树中两个最接近的数字之差的大小。如果x是叶子(它的子节点都是T.nil),则让 x . m i n − g a p = ∞ x.min-gap=\infty x.min−gap=∞。
- x.min-val包含以x为根的子树中的最小值。
- x.max-val包含以x为根的子树中的最大值。
步骤3:对信息的维护
添加到树中的三个属性可以各自根据结点及其子结点中的信息计算。因此,根据定理14.1,可以在插入和删除操作期间对树的所有结点的属性进行维护,并且不影响这两个操作的KaTeX parse error: Expected '}', got 'EOF' at end of input: O(\lg{n)渐进时间性能:
- x . m i n − v a l = { x . l e f t . m i n − v a l if there is a left subtree, x . k e y otherwise, x.min-val=\left\{ \begin{array}{ll} x.left.min-val & \textrm{if there is a left subtree,}\\ x.key & \textrm{otherwise,} \end{array} \right. x.min−val={x.left.min−valx.keyif there is a left subtree,otherwise,
- x . m a x − v a l = { x . r i g h t . m a x − v a l if there is a right subtree, x . k e y otherwise, x.max-val=\left\{ \begin{array}{ll} x.right.max-val & \textrm{if there is a right subtree,}\\ x.key & \textrm{otherwise,} \end{array} \right. x.max−val={x.right.max−valx.keyif there is a right subtree,otherwise,
- x . m i n − g a p = m i n { x . l e f t . m i n − g a p ( ∞ if no left subtree), x . r i g h t . m i n − g a p ( ∞ if no right subtree), x . k e y − x . l e f t . m a x − v a l ( ∞ if no left subtree), x . r i g h t . m i n − v a l − x . k e y ( ∞ if no right subtree). x.min-gap=min\left\{ \begin{array}{ll} x.left.min-gap & \textrm{($\infty$ if no left subtree),}\\ x.right.min-gap & \textrm{($\infty$ if no right subtree),}\\ x.key-x.left.max-val & \textrm{($\infty$ if no left subtree),}\\ x.right.min-val-x.key & \textrm{($\infty$ if no right subtree).} \end{array} \right. x.min−gap=min⎩⎪⎪⎨⎪⎪⎧x.left.min−gapx.right.min−gapx.key−x.left.max−valx.right.min−val−x.key(∞ if no left subtree),(∞ if no right subtree),(∞ if no left subtree),(∞ if no right subtree).
事实上,定义min-val和max-val属性的原因是为了可以从结点及其子结点的信息中计算min-gap。
步骤4:设计新的操作
MIN-GAP简单地返回树根的min-gap。因此,它的运行时间为
O
(
1
)
O(1)
O(1)。
思考题
14-1 最大重叠点
a. 证明:假设在区间的端点中没有最大重叠点。最大重叠点p在m个区间的内部。实际上,p位于这m个区间的交集的内部。现在看一下这m个区间的交集的一个端点
p
′
p'
p′。点
p
′
p'
p′和p被相同数目区间所覆盖,因为它也在这m个区间的交集中,所以
p
′
p'
p′也是最大重叠点。此外,
p
′
p'
p′是某个区间的端点(否则交集不会在那里结束)。这与我们的假设相矛盾,即在区间的端点中没有最大重叠点。因此,最大重叠点一定是其中一个区间的端点。
b. 使红黑树记录所有的端点。也就是说,要插入一个区间,将分别插入其左右端点。左端点关联
p
(
e
)
=
+
1
p(e)=+1
p(e)=+1值,右端点关联
p
(
e
)
=
−
1
p(e)=-1
p(e)=−1值。当多个端点具有相同的值时,在插入具有该值的任何右端点之前,先插入具有该值的所有左端点。
令
e
1
,
e
2
,
…
,
e
n
e_{1},e_{2},\dots,e_{n}
e1,e2,…,en是区间对应端点的排序序列。对于
1
⩽
i
⩽
j
⩽
n
1\leqslant i\leqslant j\leqslant n
1⩽i⩽j⩽n,令
s
(
i
,
j
)
=
p
(
e
i
)
+
p
(
e
i
+
1
)
+
⋯
+
p
(
e
j
)
s(i,j)=p(e_{i})+p(e_{i+1})+\dots+p(e_{j})
s(i,j)=p(ei)+p(ei+1)+⋯+p(ej)。我们想找到一个使
s
(
i
,
j
)
s(i,j)
s(i,j)最大化的
i
i
i。
对于树中的每个节点
x
x
x,令
l
(
x
)
l(x)
l(x)和
r
(
x
)
r(x)
r(x)分别是以
x
x
x为根的子树中最左端点和最右端点在排序顺序中的索引。所以,以
x
x
x为根的子树中包含端点
e
l
(
x
)
,
e
l
(
x
)
+
1
,
…
,
e
r
(
x
)
e_{l(x)},e_{l(x)+1},\dots,e_{r(x)}
el(x),el(x)+1,…,er(x)。
每个节点
x
x
x存储三个新属性。我们存储
x
.
v
=
s
(
l
(
x
)
,
r
(
x
)
)
x.v=s(l(x),r(x))
x.v=s(l(x),r(x)),这是以
x
x
x为根的子树中所有节点的值之和。我们还存储
x
.
m
x.m
x.m,对于
i
∈
{
l
(
x
)
,
l
(
x
)
+
1
,
…
,
r
(
x
)
}
i\in\{l(x),l(x)+1,\dots,r(x)\}
i∈{l(x),l(x)+1,…,r(x)},表达式
s
(
l
(
x
)
,
i
)
s(l(x),i)
s(l(x),i)取得的最大值。最后,我们存储
x
.
o
x.o
x.o作为使
x
.
m
x.m
x.m取得最大值的
i
i
i值。对于哨兵,我们定义
T
.
n
i
l
.
v
=
T
.
n
i
l
.
m
=
0
T.nil.v=T.nil.m=0
T.nil.v=T.nil.m=0。我们可以通过自下而上的方式计算这些属性,以满足定理14.1的要求:
x
.
v
=
x
.
l
e
f
t
.
v
+
p
(
x
)
+
x
.
r
i
g
h
t
.
v
,
x.v=x.left.v+p(x)+x.right.v,
x.v=x.left.v+p(x)+x.right.v,
x
.
m
=
m
a
x
{
x
.
l
e
f
t
.
m
(max is in x’s left subtree),
x
.
l
e
f
t
.
v
+
p
(
x
)
(max is at x),
x
.
l
e
f
t
.
v
+
p
(
x
)
+
x
.
r
i
g
h
t
.
m
(max is in x’s right subtree).
x.m=max\left\{ \begin{array}{ll} x.left.m & \textrm{(max is in x's left subtree),}\\ x.left.v+p(x) & \textrm{(max is at x),}\\ x.left.v+p(x)+x.right.m & \textrm{(max is in x's right subtree).} \end{array} \right.
x.m=max⎩⎨⎧x.left.mx.left.v+p(x)x.left.v+p(x)+x.right.m(max is in x’s left subtree),(max is at x),(max is in x’s right subtree).
一旦理解了如何计算
x
.
m
x.m
x.m,就可以直接从
x
x
x及其两个子结点中的信息计算
x
.
o
x.o
x.o。因此,我们可以实现如下操作:
- INTERVAL-INSERT:插入两个结点,区间的每个端点对应一个结点。
- FIND-POM:返回端点由 T . r o o t . o T.root.o T.root.o表示的区间。
(注意,因为我们正在构建所有端点的二叉搜索树,然后确定
T
.
r
o
o
t
.
o
T.root.o
T.root.o,所以我们不需要从树中删除任何结点。)
因为定义新属性的方式,定理14.1表明每个操作的运行时间是
O
(
lg
n
)
O(\lg{n})
O(lgn)。实际上,FIND-POM只需要
O
(
1
)
O(1)
O(1)的运行时间。
14-2 Josephus排列
a. 假设
m
m
m是常数,描述一个
O
(
n
)
O(n)
O(n)时间的算法,使得对于给定的
n
n
n,能够输出
(
n
,
m
)
−
J
o
s
e
p
h
u
s
(n,m)-Josephus
(n,m)−Josephus排列。
我们使用循环列表,其中每个元素都有两个属性,
k
e
y
key
key和
n
e
x
t
next
next。在开始时,我们初始化列表按顺序包含键
1
,
2
,
…
,
n
1,2,\dots,n
1,2,…,n。这个初始化需要
O
(
n
)
O(n)
O(n)的时间,因为每个元素只有一个固定的工作量(即设置它的
k
e
y
key
key和
n
e
x
t
next
next属性)。我们通过让最后一个元素的
n
e
x
t
next
next属性指向第一个元素来使列表循环。
然后我们从头开始扫描列表。我们输出然后删除每个第m个元素,直到列表变空。输出序列是
(
n
,
m
)
−
J
o
s
e
p
h
u
s
(n,m)-Josephus
(n,m)−Josephus排列。此过程每个元素占用
O
(
m
)
O(m)
O(m)的时间,总时间为
O
(
m
n
)
O(mn)
O(mn)。因为
m
m
m是常数,所以我们得到
O
(
m
n
)
=
O
(
n
)
O(mn)=O(n)
O(mn)=O(n)的运行时间。
b. 假设
m
m
m不是常数,描述一个
O
(
n
lg
n
)
O(n\lg{n})
O(nlgn)时间的算法,使得对于给定的
n
n
n,能够输出
(
n
,
m
)
−
J
o
s
e
p
h
u
s
(n,m)-Josephus
(n,m)−Josephus排列。
可以直接使用第14.1节中的顺序统计树。假设我们处于排列中的特定位置,并且假设它是第
j
j
j大的剩余人物。假设剩下
k
⩽
n
k\leqslant n
k⩽n个人。然后我们将删除人
j
j
j,减去
k
k
k以反映移除了此人,然后看第
(
j
+
m
−
1
)
(j+m-1)
(j+m−1)大的剩余人物(减去1因为我们刚刚删除了第
j
j
j大的人)。但这假设
j
+
m
⩽
k
j+m\leqslant k
j+m⩽k。如果假设不成立,那么使用一点模数运算,如下所示:
详细地说,我们使用顺序统计树T,然后调用OS-INSERT,OS-DELETE,OS-RANK和OS-SELECT过程:
JOSEPHUS(n, m)
initialize T to be empty
for j = 1 to n
create a node x with x.key == j
OS-INSERT(T, x)
j = 1
for k = n downto 1
j = ((j + m - 2) mod k) + 1
x = OS-SELECT(T.root, j)
print x.key
OS-DELETE(T, x)
需要 O ( n lg n ) O(n\lg{n}) O(nlgn)时间来构建顺序统计树T,然后调用了 O ( n ) O(n) O(n)次顺序统计树的过程,每个过程调用都需要 O ( lg n ) O(\lg{n}) O(lgn)的时间。因此,总时间为 O ( n lg n ) O(n\lg{n}) O(nlgn)。