顺序统计树

来源:互联网 发布:商情数据 编辑:程序博客网 时间:2024/06/07 16:40
 

顺序统计树

分类: 算法导论习题 878人阅读 评论(3) 收藏 举报
数据结构算法

      在包含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) 整个过程如下:

[cpp] view plaincopy
  1. OS-SELECT(x, i)  
  2. r ← size[left[x]]+1  
  3. if i=r  
  4.    then return x  
  5. elseif i<r  
  6.    then return OS-SELECT(left[x], i)  
  7. else return OS-SELECT(left[x], i-r)  
  8.    
 

每次调用,必定会下降一层,故OS-SELECT的时间复杂度为O(lgn)

 

确定一个元素的秩

      这里的秩指的是节点x在线性序中的位置。根据排序树的性质,也就是节点x在中序遍历中的位置。利用中序遍历的特性就可以得到。

      OS-RANK(T, x) 整个过程如下:

[cpp] view plaincopy
  1. OS-RANK(T, x)  
  2. r ← size[left[x]]+1  
  3. y ← x  
  4. while y ≠ root[T]  
  5.    do if y = right[p[y]]  
  6.       then r ← r + size[left[y]]+1  
  7.    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)的整个过程如下: 

[c-sharp] view plaincopy
  1. OS-RANK(T, x, i)  
  2. r ← OS-RANK(T, x)  
  3. 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

  如此我们便可以迭代的求取。

[cpp] view plaincopy
  1. INVERSION(A)  
  2. T ← Θ  
  3. v ← 0  
  4. for i ← 1 to n  
  5.    do OS-INSERT(T, A[i])  
  6.       r ← OS-RANK(T, A[i])  
  7.       v ← v + (i-r)  
  8. 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个位置的元素,并对剩余集合的元素个数取模。整个过程如下

 

[cpp] view plaincopy
  1. JOSEPHUS(n , m)  
  2. T ← Θ   
  3. for i ← 1 to n  
  4.    do create a node x with key[x]=i  
  5.       OS-INSERT(T,x)  
  6. k ← n  
  7. j ← m  
  8. while k > 2  
  9.    do x ← OS-SELECT(root[T], j)  
  10.       print key[x]  
  11.       OS-DELETE(T, x)  
  12.       k ← k − 1  
  13.       j ← (( j + m − 2) mod k) + 1  



2012-05-08 19:34:13cnblogs.com-Microgoogle-点击数:39
更多0

算法导论习题解答系列停了一年了,现在重新拾起,好多算法已经忘了,有的记得大概,但是真正的用代码实现却很难下手。

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.csdn.net/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),这样角度区间“交叉”就意味着两条弦有交叉。 

由于有n条弦,故有2n个端点,每个端点的取值范围为[0, 2*pi),对这2n个端点按角度值进行从小到大排序,排序的时间复杂度为O(n*lgn),所得数组为A[1...2n]然后建立一个顺序统计树,起先为空,先插入A[1],它为一条弦的起始端点,然后遇到其他弦的起始端点就插入,当遇到一条弦的终端点时,就统计在该树中大于该弦的起始端点角度值的端点个数,之后就从树中删除该弦的两个端点。如图,先插入P1,再插入P2,再插入P3,再插入P4,再插入Q4,Q4为弦P4Q4的终端店,故停止插入,开始统计树中大于P4角度值的端点个数,为0,然后删除P4与Q4。再插入Q2,为终端点,统计此时树中大于P2端点值的个数,为1个,然后删除P2与Q2。再插入Q1,为终端点,统计此时树中大于P1端点值的个数,为1个,然后删除P1与Q1。再插入Q3,为终端点,统计此时树中大于P3端点值的个数,为0个,然后删除P3与Q3。结束,统计结果一共为2,与图中相符。在这里,要用到顺序统计树的插入、删除操作,以及OS-RANK函数。每个操作都为O(lgn)的时间复杂度,有2n个结点,故时间复杂度仍是O(n*lgn)。

 

---
可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明

算法导论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)时间内确定圆内相交弦的对数的算法,假设任意两条弦都不会共享端点。

参考自http://topic.csdn.net/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),这样角度区间“交叉”就意味着两条弦有交叉。
 

由于有n条弦,故有2n个端点,每个端点的取值范围为[0, 2*π),对这2n个端点按角度值进行从小到大排序,排序的时间复杂度为O(n*lgn),所得数组为A[1...2n]
然后建立一个顺序统计树,起先为空,先插入A[1],它为一条弦的起始端点,然后遇到其他弦的起始端点就插入,当遇到一条弦的终端点时,就统计在该树中大于该弦的起始端点角度值的端点个数,之后就从树中删除该弦的两个端点。
如图,先插入P1,再插入P2,再插入P3,再插入P4,再插入Q4,Q4为弦P4Q4的终端店,故停止插入,开始统计树中大于P4角度值的端点个数,为0,然后删除P4与Q4。
再插入Q2,为终端点,统计此时树中大于P2端点值的个数,为1个,然后删除P2与Q2。
再插入Q1,为终端点,统计此时树中大于P1端点值的个数,为1个,然后删除P1与Q1。
再插入Q3,为终端点,统计此时树中大于P3端点值的个数,为0个,然后删除P3与Q3。
结束,统计结果一共为2,与图中相符。
在这里,要用到顺序统计树的插入、删除操作,以及OS-RANK函数。每个操作都为O(lgn)的时间复杂度,有2n个结点,故时间复杂度仍是O(n*lgn)。

 

---
可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
分类: 算法导论习题解答