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

Posted

技术标签:

【中文标题】如何合并、拆分和查询第 k 个排序列表?【英文标题】:How can I merge, split and query k-th of sorted lists? 【发布时间】:2016-10-27 12:53:51 【问题描述】:

最初我有 n 个元素,它们位于 n 个图块中。

我需要支持3种查询:

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

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

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

仍然假设有 n 个查询。我可以达到什么最坏情况的时间复杂度?

【问题讨论】:

1. O(n1 + n2), 2. 取决于数据结构中的任何一个 O(k1 + k2), O(min(k1, k2)), O(1), 3. O(1) 具有适当的数据结构。你的问题到底是什么?这些都是很常见的问题,解决起来并不难。 :) 显然我要求的是复杂性,例如 O(logn) 或 O(sqrt(n))。而且这些查询应该一并解决……它们不是单独的问题。 不,你问的不是很清楚。您是否正在寻找任意数量的合并和拆分操作的摊销时间复杂度?一次性解决是什么意思? 合并的怎么样了。我的意思是,如果你有一个瓷砖列表,那么你应该只合并 2 个连续的瓷砖?? 不,我可以合并任意两个图块。 @Shasha99 【参考方案1】:

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

我的想法是基于skip list。

让每个图块都是一个可索引的排序跳过列表。

拆分那么就很简单了:找到k-th元素,打断i > k1-thj <= k1-th元素之间的每一个链接(最多有O(log n)这样的链接)。

合并比较棘手。

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

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

比较T1 中的第一个元素t1T2 中的t2。让我们 说t1 < t2

然后,在T1中找到最后一个t1'仍小于t2

我们必须在t1' 之后插入t2。但首先,我们正在查看T1t1' 之后的元素t1*

现在在T2 中搜索最后一个t2' 仍小于t1*

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

所以,我们在t1't2'进行拆分,获取新列表T1aT1bT2aT2b

我们连接T1aT2aT1b,得到新的列表T1*

我们正在为T1*T2b 重复整个过程。

在一些伪python代码中:

#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) 这样的迭代,其中pT1T2 中交错运行的次数。每次迭代都需要O(log n) 操作才能完成。

正如@newbie 所指出的,有一个例子,ps 的总和等于n log n。 此 python 脚本为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)
    )
)

这是一棵二叉树,我们在其中合并高度为j2^j 大小的图块数量2^(k-j)。瓦片的构造方式使其元素始终交错,因此对于大小为q 的瓦片,我们正在执行O(q) 拆分连接。

但是,对于这种特定情况,它仍然不会恶化 O(n log n) 的整体复杂性,因为(非常非正式地说)“小”列表的每个拆分连接的成本低于 O(log n),而且还有更多“小”列表比“大”列表。

我不确定是否有更糟糕的反例,但目前我认为n 查询的总体最坏情况复杂度介于n log^2 nn log n 之间。

【讨论】:

我相信 p 是 O(nlogn)。令 n=2^k,考虑 a1...an=1...n,令 a 的前 n/2 个元素为 1,3,5,...,n-1,最后n/2 为 2,4,6,8,...,n 并递归地构造每一半。然后让我们像在段树中一样从下到上合并。那么 p 将是 O(nlogn)。 @newbie,如果我理解正确,那可能是对的。尽管如此,O(n log^2 n) 的整体复杂性还不错。虽然我们正在合并大小为2 ^ j2 ^ (k - j) 列表,而不是大小为n 的列表,但对于每个j &lt; k,它仍然是O(n log n)。我再给它一些想法,然后编辑我的答案。【参考方案2】:

寻找:

    std::merge 或 std::set_union std::partition std::find(或 std::find_if)

1 和 2 的线性复杂度。 取决于您的容器 3,最坏的情况是线性的。

但不清楚你到底在问什么。你有一些我们可以看的代码吗?

【讨论】:

我对操作的线性复杂度不感兴趣。 对不起,我以为这是一个 C++ 问题。更多的是关于算法。【参考方案3】:

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

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

让我们为每个排序列表构建一个段树,段树是基于值 (1~n) 构建的。在段树的每个节点中存储了这个范围内有多少个数字,我们称之为节点的值。

似乎需要 O(nlogn) 空间来存储每个段树,但我们可以简单地删除 value=0 的节点,只有当它们的值>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)。

让我们考虑合并,除了'nothing to merge',每次我们调用merge时,分配的segment tree节点的总数都会减少1(t2不再有用了)。显然 'nothing to merge' 只有在它的父亲真正被合并时才会被调用,所以它们与复杂性无关。

分配的segment tree节点总数为O(nlogn),每次有用的合并都会减1,所以所有合并的总复杂度为O(nlogn)。

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

查询第k个也很简单,我们已经做到了:)

//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);

【讨论】:

以上是关于如何合并、拆分和查询第 k 个排序列表?的主要内容,如果未能解决你的问题,请参考以下文章

java 23.合并k个排序列表(#)。java

java 23.合并k个排序列表(#)。java

java 23.合并k个排序列表(#)。java

java 23.合并k个排序列表(#)。java

java 23.合并k个排序列表(#)。java

java 23.合并k个排序列表(#)。java