在包含n个元素的无序集合中,寻找第i个顺序统计量的时间复杂度为O(n)。通过建立一种特定的结构,可以使得任意的顺序统计量都可以在O(lgn)的时间内找到。这就是下面会提到的基于红黑树的顺序统计树。
相比于基础的数据结构,顺序统计树增加了一个域size[x]。这个域包含以x为根的子树的节点数(包含x本身)。size域满足等式:
size[x] = size[left[x]] + size[right[x]] + 1
再根据红黑树的排序特性,我们就可以O(lgn)的时间内完成下面的操作。
顺序统计树如下图所示:
查找第i小的元素
实现OS-SELECT(x, i)返回以x为根的子树中包含第i小关键字的节点的指针。根据排序树的性质,我们知道左子树的键值要小于根节点的键值,右节点的键值要大于根节点,这相当于静态的顺序统计量的PARTITION已经完成。同时我们知道左右子树的大小,我们就可以确定在哪个分支进行接下来的查找。
OS-SELECT(x, i) 整个过程如下:
- OS-SELECT(x, i)
- r ← size[left[x]]+1
- if i=r
- then return x
- elseif i<r
- then return OS-SELECT(left[x], i)
- else return OS-SELECT(left[x], i-r)
每次调用,必定会下降一层,故OS-SELECT的时间复杂度为O(lgn)
确定一个元素的秩
这里的秩指的是节点x在线性序中的位置。根据排序树的性质,也就是节点x在中序遍历中的位置。利用中序遍历的特性就可以得到。
OS-RANK(T, x) 整个过程如下:
- OS-RANK(T, x)
- r ← size[left[x]]+1
- y ← x
- while y ≠ root[T]
- do if y = right[p[y]]
- then r ← r + size[left[y]]+1
- y ← p[y]
每次必然至少上升一层,故OS-RANK的时间复杂度为O(lgn)
建立顺序统计树的时间为O(nlgn),那和一般的静态查找O(n)相比,岂不是更加复杂?其实,这两种方法应用的场合不一样,如果只查找一次或几次,静态查找比较快速,如果多次查找(查找次数和n具有可比性),那么顺序统计树就体现出它的优点了。另外,顺序统计树还可以方便快速(O(lgn)时间内)的支持元素的插入和删除,这两点是静态顺序统计量方法无法比拟的。
问题思考,如何利用顺序统计树来解决一些问题呢?
14.1-5 给定n个元素的顺序统计树中的一个元素x和一个自然数i,如何在O(nlgn)时间内,确定x该树的线性序中第i个后继?
分析与解答:
首先利用元素x得到它的秩r,然后查找第i+r小的元素即可.
OS-RANK(T, x, i)的整个过程如下:
- OS-RANK(T, x, i)
- r ← OS-RANK(T, x)
- return OS-SELECT(x, i+r)
14.1-7 说明如何在O(nlgn)的时间内,利用顺序统计树对大小为n的数组中的逆序对进行计数)
分析与解答:
如果这n个元素的数组记为a1, a2, a3, ... , ai , ... , an,那么我们可以依次求出以第i个元素ai结尾的逆序对<aj, ai>,j<i 的个数vi。
那么总的逆序对的个数为v
v = v1 + v2+ ... + vn
可以这样考虑,动态集合a1, a2, ... , ai中我们可以求出ai的秩(也就是说ai在排序后的序列中的位置),若为其秩为r,则逆序对的数量
vi = i - r
如此我们便可以迭代的求取。
- INVERSION(A)
- T ← Θ
- v ← 0
- for i ← 1 to n
- do OS-INSERT(T, A[i])
- r ← OS-RANK(T, A[i])
- v ← v + (i-r)
- return v
14-2 Josephus排列
Josephus问题的定义如下:假设n个人排成环形,且有一正整数m<=n。从某个指定的人开始,沿环报数,每遇到第n个人就让其出列,且报数进行下去。这个过程一直进行到所有人都出列为止。每个人出列的次序定义了整数1, 2, ..., n的(n, m)-Josephus排列。例如,(7, 3)-Josephus排列为<3, 6, 2, 7, 5, 1, 4>。
a)假设m为常数。请描述一个O(n)时间的算法,使之给定的整数n,输出(n, m)-Josephus排列
b)假设m不是常数。请描述一个O(nlgn)时间的算法,使之给定的整数n和m,输出(n, m)-Josephus排列
分析与解答:
a)每个人对应一个元素,共n个元素,键值为编号。将这n个元素构成一个循环双链表,那么每次让一个人出列的时间复杂度为O(m)总的时间复杂度为O(nm),由于m是常数,则为O(n)的时间复杂度。
b)若m不是常数,则正好可以使用顺序统计树来动态的进行处理。每次选择出列一个元素,在顺序统计树中将其删除,并重新查找,迭代进行。如果之前删除的是当前集合的第j个位置的元素,那么下一次删除的是剩余集合的j-1+m个位置的元素,并对剩余集合的元素个数取模。整个过程如下
- JOSEPHUS(n , m)
- T ← Θ
- for i ← 1 to n
- do create a node x with key[x]=i
- OS-INSERT(T,x)
- k ← n
- j ← m
- while k > 2
- do x ← OS-SELECT(root[T], j)
- print key[x]
- OS-DELETE(T, x)
- k ← k − 1
- j ← (( j + m − 2) mod k) + 1
算法导论习题解答系列停了一年了,现在重新拾起,好多算法已经忘了,有的记得大概,但是真正的用代码实现却很难下手。
14.1-3 写出OS-SELECT的非递归形式
一般递归形式改写为非递归形式要用到while,有时还要用到栈结构。
OS-SELECT(x, i){ r = size[left[x]] + 1; while (r != i) { if (i < r) { x = left[x]; r = size[left[x]] + 1; } else { x = right[x]; i = i -r; r = size[left[x]] + 1; } } return x;}
14.1-4 写出一个递归过程OS-KEY-RANK(T, k)
int OS-KEY-RANK(T, k){ if (k == key[root[T]]) return size[left[root[T]]] + 1; else if (k < key[root[T]]) return OS-KEY-RANK(left[root[T]], k); else return OS-KEY-RANK(right[root[T]], k) + size[left[root[T]]] + 1;}
14.1-5 确定元素x的第i个后继,时间为lg(n)
GET-SUCCESSOR(T, x, i){ r = OS-RANK(T, x); return OS-SELECT(root[T], r + i);}
14.1-6
在这题中,将每个结点的秩存于该结点自身之中,这个秩是相对于以该结点为根的子树而言的。
因而在插入结点x时,对于从root到x结点的路经上的所有结点y,如果插入路经经过y的左支,则rank[y]的值加1,若经过其右支,则rank[y]的值不变。
在删除结点x时,对于从root到x结点的路经上的所有结点y,如果删除路经经过y的左支,则rank[y]的值减1,若经过其右支,则rank[y]的值不变。
如图,在进行右旋转时,x的秩是不变的,node的秩变为rank[node]减去原来的rank[x]。左旋同理。
14.1-7 利用顺序统计树在O(nlgn)的时间内统计逆序对
在习题2-4中,其要求用归并排序来计算逆序对,见算法导论2-4习题解答(合并排序算法)
在这里,我们对于数组{2,3,8,6,1}这样分析,对于每个数,选取其前面的数与其比较,
对于6,与其对比的为2,3,8,逆序对有1对,记作inversion_count,
6在原数组的索引为3,记作j,
然后我们来分析子数组{2,3,8,6},6在其中的排名为3,记作rank_j;
再通过分析其他数,我们归纳如下:
inversion_count = j + 1 - rank_j
当把每个结点插入顺序统计树时,我们可以知道j的值,同时调用OS-RANK来得到rank_j,从而得到inversion_count,在这里,每插入一次,就计算一次。
由于插入和OS-RANK都是lg(n),故n个结点即为n*lg(n)。
14.1-8 现有一个圆上的n条弦,每条弦都按其端点来定义,请给出一个能在O(n*lgn)时间内确定圆内相交弦的对数的算法,假设任意两条弦都不会共享端点。
参考自http://topic.youkuaiyun.com/u/20081119/17/397cc718-f4dc-4570-9991-aee57dd73416.html如图,对于两条弦P1Q1和P3Q3来说,圆心与端点形成的向量有一个角度A如果A(P1)<A(P3)<A(Q1)<A(Q3)或者A(P3)<A(P1)<A(Q3)<A(Q1),这样角度区间“交叉”就意味着两条弦有交叉。
---
可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
算法导论14.1节习题解答
算法导论习题解答系列停了一年了,现在重新拾起,好多算法已经忘了,有的记得大概,但是真正的用代码实现却很难下手。
CLRS 14.1-3 写出OS-SELECT的非递归形式
一般递归形式改写为非递归形式要用到while,有时还要用到栈结构。
OS-SELECT(x, i) { r = size[left[x]] + 1; while (r != i) { if (i < r) { x = left[x]; r = size[left[x]] + 1; } else { x = right[x]; i = i -r; r = size[left[x]] + 1; } } return x; }
CLRS 14.1-4 写出一个递归过程OS-KEY-RANK(T, k)
int OS-KEY-RANK(T, k) { if (k == key[root[T]]) return size[left[root[T]]] + 1; else if (k < key[root[T]]) return OS-KEY-RANK(left[root[T]], k); else return OS-KEY-RANK(right[root[T]], k) + size[left[root[T]]] + 1; }
CLRS 14.1-5 确定元素x的第i个后继,时间为lg(n)
GET-SUCCESSOR(T, x, i) { r = OS-RANK(T, x); return OS-SELECT(root[T], r + i); }
CLRS 14.1-6
在这题中,将每个结点的秩存于该结点自身之中,这个秩是相对于以该结点为根的子树而言的。
因而在插入结点x时,对于从root到x结点的路经上的所有结点y,如果插入路经经过y的左支,则rank[y]的值加1,若经过其右支,则rank[y]的值不变。
在删除结点x时,对于从root到x结点的路经上的所有结点y,如果删除路经经过y的左支,则rank[y]的值减1,若经过其右支,则rank[y]的值不变。
如图,在进行右旋转时,x的秩是不变的,node的秩变为rank[node]减去原来的rank[x]。左旋同理。
CLRS 14.1-7 利用顺序统计树在O(nlgn)的时间内统计逆序对
在习题2-4中,其要求用归并排序来计算逆序对,见算法导论2-4习题解答(合并排序算法)
在这里,我们对于数组{2,3,8,6,1}这样分析,对于每个数,选取其前面的数与其比较,
对于6,与其对比的为2,3,8,逆序对有1对,记作inversion_count,
6在原数组的索引为3,记作j,
然后我们来分析子数组{2,3,8,6},6在其中的排名为3,记作rank_j;
再通过分析其他数,我们归纳如下:
inversion_count = j + 1 - rank_j
当把每个结点插入顺序统计树时,我们可以知道j的值,同时调用OS-RANK来得到rank_j,从而得到inversion_count,在这里,每插入一次,就计算一次。
由于插入和OS-RANK都是lg(n),故n个结点即为n*lg(n)。
CLRS 14.1-8 现有一个圆上的n条弦,每条弦都按其端点来定义,请给出一个能在O(n*lgn)时间内确定圆内相交弦的对数的算法,假设任意两条弦都不会共享端点。
可以转载, 但必须以超链接形式标明文章原始出处和作者信息及 版权声明