如何合并,拆分和查询排序列表的第k个?

时间:2016-10-27 12:53:52

标签: c++ algorithm data-structures time-complexity binary-search-tree

最初我有n个元素,它们在n个tile中。

我需要支持3种查询:

  1. 将两个图块合并为一个图块。

  2. 将一个瓷砖分成两个瓷砖。 (正式对于大小为k的图块,将其拆分为两个大小为k1和k2,k = k1 + k2的图块,第一个图块包含最小的k1元素,第二个图块包含其余的图块)

  3. 在一个图块中找到第k个最小元素。

  4. 仍假设有n个查询。我可以实现最糟糕的时间复杂度?

3 个答案:

答案 0 :(得分:1)

那不是一个完整的答案,而是对可以做些什么的一些想法。

我的想法基于skip list

让每个磁贴成为可索引的排序跳过列表。

拆分然后相当简单:找到k-th元素并中断i > k1-thj <= k1-th元素之间的每个链接(最多O(log n)个链接)。

合并比较棘手。

首先,假设我们可以连接 O(log n)中的两个跳过列表。

假设我们正在合并两个图块T1T2

  • 比较来自t1的{​​{1}}和来自T1的{​​{1}}的第一个元素。让&#39; S 说t2
  • 然后,在T2中找到仍然低于t1 < t2的最后t1'

  • 我们必须在t2之后立即插入T1。但首先,我们正在t2t1'后面查看元素t1*

  • 现在在[{1}}中搜索仍然少于t1'的最后T1

  • 必须在t2't1*之间插入T2的从T2开始到t2结尾的整个元素序列。

  • 因此,我们正在t2't1'进行拆分,获取新列表t1*t1',{{1 },t2'

  • 我们联接T1aT1bT2a,获取新列表T2b

  • 我们正在重复T1aT2a的整个过程。

    Algorithm illustration

在一些伪python代码中:

T1b

最多T1*次此类迭代,其中T1*T2b#skiplist interface: # split(list, k) - splits list after the k-th element, returns two lists # concat(list1, list2) - concatenates two lists, returns the new one # index(list, k) - returns k-th element from the list # upper_bound(list, val) - returns the index of the last element less that val # empty(list) - check if list is empty def Query(tile, k) return index(tile, k) def Split(tile, k) return split(tile, k) def Merge(tile1, tile2): if empty(tile1): return tile2 if empty(tile2): return tile1 t1 = index(tile1, 0) t2 = index(tile2, 0) if t1 < t2: #(1) i1 = upper_bound(tile1, t2) t1s = index(tile1, i1 + 1) i2 = upper_bound(tile2, t1s) t1_head, t1_tail = split(tile1, i1) t2_head, t2_tail = split(tile2, i2) head = concat(t1_head, t2_head) tail = Merge(t1_tail, t2_tail) return concat(head, tail) else: #swap tile1, tile2, do (1) 中的交错次数。每次迭代都需要O(p)次操作才能完成。

正如@newbie所指出的,有一个例子,其中p s的总和等于T1。 这个python脚本为T2生成了这样一个例子(输出中的加号代表合并):

O(log n)

p

n log n

给我们以下疑问:

k = log_2 n

这是一个二叉树,我们正在合并def f(l): if len(l) == 2: return "%s+%s" % (l[0], l[1]) if len(l) == 1: return str(l[0]) l1 = [l[i] for i in xrange(0, len(l), 2)] l2 = [l[i + 1] for i in xrange(0, len(l), 2)] l_str = f(l1) r_str = f(l2) return "(%s)+(%s)" % (l_str, r_str) def example(k): print f(list(range(0, 2 ** k))) n = 16个大小的瓷砖,高度为example(4) 。瓷砖的构造方式使它们的元素始终是交错的,因此对于大小为( ( (0+8)+(4+12) ) + ( (2+10)+(6+14) ) ) + ( ( (1+9)+(5+13) ) + ( (3+11)+(7+15) ) ) 的瓷砖,我们正在进行2^(k-j)分割 - 连接。

然而,对于这个特定情况,它仍然不会使2^j的总体复杂性恶化,因为(非常非正式地说)每个小&#39;列出的成本低于j,而且还有更多“小”的成本。列出的不是&#39; big&#39;。

我不确定是否有更糟糕的反例,但目前我认为q查询的总体最差案例复杂性介于O(q)O(n log n)之间。

答案 1 :(得分:0)

寻找:

  1. std :: merge或std :: set_union
  2. std :: partition
  3. std :: find(或std :: find_if)
  4. 1和2的线性复杂度。
    取决于您的容器3,线性最差。

    但目前尚不清楚你究竟在问什么。你有一些我们可以看的代码吗?

答案 2 :(得分:0)

当我问这个问题时,我不知道如何解决它,因为似乎可以回答我自己的问题,我会自己回答这个问题:/ < / em>的

首先,假设排序列表中的值是1~n之间的整数。如果没有,您可以对它们进行排序和映射。

让我们为每个排序列表构建一个分段树,根据值(1~n)构建分段树。在段树的每个节点中,存储该范围内的数量,让我们将其称为节点的值。

它似乎需要O(nlogn)空间来存储每个段树,但我们可以简单地删除值为0的节点,并且只有当它们的值变为&gt; 0时才真正分配这些节点。

因此,对于只包含一个元素的排序列表,我们只需构建此值的链,因此只需要O(logn)内存。

int s[SZ]/*value of a node*/,
ch[SZ][2]/*a node's two children*/;
//make a seg with only node p, return in the first argument
//call with sth. like build(root,1,n,value);
void build(int& x,int l,int r,int p)
{
    x=/*a new node*/; s[x]=1;
    if(l==r) return;
    int m=(l+r)>>1;
    if(p<=m) build(ch[x][0],l,m,p);
    else build(ch[x][1],m+1,r,p);
}

当我们拆分段树(排序列表)时,只需递归地拆分两个子项:

//make a new node t2, split t1 to t1 and t2 so that s[t1]=k
void split(int t1,int& t2,int k)
{
    t2=/*a new node*/;
    int ls=s[ch[t1][0]]; //size of t1's left child
    if(k>ls) split(ch[t1][1],ch[t2][1],k-ls); //split the right child of t1
    else swap(ch[t1][1],ch[t2][1]); //all right child belong to t2
    if(k<ls) split(ch[t1][0],ch[t2][0],k); //split the left child of t1
    s[t2]=s[t1]-k; s[t1]=k;
}

当我们合并两个已排序的列表时,请强制合并它们:

//merge trees t1&t2, return merged segment tree
int merge(int t1,int t2)
{
    if(t1&&t2);else return t1^t2; //nothing to merge
    ch[t1][0]=merge(ch[t1][0],ch[t2][0]);
    ch[t1][1]=merge(ch[t1][1],ch[t2][1]);
    s[t1]+=s[t2]; /*erase t2, it's useless now*/ return t1;
}

看起来很简单,不是吗?但它的总复杂性实际上是O(nlogn)。

  

证明:

     

让我们研究分配的分段树节点的总数。

     

最初我们将分配O(nlogn)这样的节点(每个节点O(logn))。

     

对于每次分裂尝试,我们将最多分配O(logn),因此它总共也是O(nlogn)。原因显然是我们将递归地分割节点的左子节点或右子节点。

     

因此,分配的分段树节点的总数最多只有O(nlogn)。

     

考虑合并,除了没有合并之外,每次调用merge时,分配的分段树节点的总数将减少1(t2不是很有用)了)。显然没有什么可以合并的#39;只有在父亲真正合并时才会被调用,因此他们与复杂性无关。

     

分配的分段树节点总数为O(nlogn),对于每个有用的合并,它将减少1,因此所有合并的总复杂度为O(nlogn)。

     

总结一下,我们得到了结果。

查询k-th也非常简单,我们已经完成了:)

//query k-th of segment tree x[l,r]
int ask(int x,int l,int r,int k)
{
    if(l==r) return l;
    int ls=s[ch[x][0]]; //how many nodes in left child
    int m=(l+r)>>1;
    if(k>ls) return ask(ch[x][1],m+1,r,k-ls);
    return ask(ch[x][0],l,m,k);
}