如何合并、拆分和查询第 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-th
和j <= k1-th
元素之间的每一个链接(最多有O(log n)
这样的链接)。
合并比较棘手。
首先,假设我们可以在O(log n)
中连接两个跳过列表。
假设我们正在合并两个图块 T1
和 T2
。
T1
中的第一个元素t1
和T2
中的t2
。让我们
说t1 < t2
然后,在T1
中找到最后一个t1'
仍小于t2
。
我们必须在t1'
之后插入t2
。但首先,我们正在查看T1
中t1'
之后的元素t1*
。
现在在T2
中搜索最后一个t2'
仍小于t1*
。
必须在t1'
和t1*
之间插入从t2
开始到t2'
结束的整个T2
元素序列。
所以,我们在t1'
和t2'
进行拆分,获取新列表T1a
、T1b
、T2a
、T2b
。
我们连接T1a
、T2a
和T1b
,得到新的列表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)
这样的迭代,其中p
是T1
和T2
中交错运行的次数。每次迭代都需要O(log n)
操作才能完成。
正如@newbie 所指出的,有一个例子,p
s 的总和等于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)
)
)
这是一棵二叉树,我们在其中合并高度为j
的2^j
大小的图块数量2^(k-j)
。瓦片的构造方式使其元素始终交错,因此对于大小为q
的瓦片,我们正在执行O(q)
拆分连接。
但是,对于这种特定情况,它仍然不会恶化 O(n log n)
的整体复杂性,因为(非常非正式地说)“小”列表的每个拆分连接的成本低于 O(log n)
,而且还有更多“小”列表比“大”列表。
我不确定是否有更糟糕的反例,但目前我认为n
查询的总体最坏情况复杂度介于n log^2 n
和n 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 ^ j
的2 ^ (k - j)
列表,而不是大小为n
的列表,但对于每个j < 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 个排序列表?的主要内容,如果未能解决你的问题,请参考以下文章