我必须将已排序的各种集合合并到一个已排序的集合中。此操作需要针对不是很大的集合(大约 5 个元素)且来源不多(可能大约 10 个)来完成,但输出应该快速计算(非阻塞服务器)。
在两个源集合的情况下,这非常简单,但是当源集合数量增加时,需要考虑不同的策略(
n
源集合,每个源集合都有m
元素):
O(n*m*log(n*m))
复杂性(快速排序 n*m
元素)。O(n*n*m)
复杂度(扫描n
头n*m
次,即元素总数)。Merge(c3,Merge(c2,Merge(c1,c0)))
。我猜它也有 O(n*n*m)
的复杂性(进行 n-1
合并阶段,每个阶段最多有 n*m
个元素)。Merge(Merge(c3,c2),Merge(c1,c0))
。在这种情况下,我们有 log2(n)
级合并,在每个级别中,我们将集合数量减半,但元素数量加倍,因此应该是 O(log(n)*n*m)
。O(log(n)*n*m)
(堆删除和插入操作是log(n)
,并且完成了n*m
次)。因此,每次基于堆的复杂选择最小元素的计算复杂度(如果我没有犯任何错误)似乎是最好的。从内存角度来看,堆可能对垃圾收集器更友好,因为它不需要那么多中间临时收集。
我是否错过了什么(计算复杂性时出错或错过任何替代方法)?我的实现很可能是用 Javascript 或 Scala 完成的,因此欢迎任何可能适用于这些执行环境的运行时问题!
顺便说一句,这是不相关:合并集合保持顺序的最有效方法?
由于您的数据集很小,因此您的渐近考虑因素几乎无关紧要(尽管是正确的),因为更重要的因素将是微优化。我建议您至少比较以下选项,按照实施它们所需的努力的顺序递增:
O(k)
。对于 5 个大小均为 10 的列表,k
最坏的情况是 1000。但实际上它会少得多。您甚至可以防止最坏的情况,例如按照增加第一个元素的顺序对列表进行预排序,或者只是随机化顺序。这些是一般性建议。根据您的数据集,可能有更好的定制选项。例如,如果输入的数字足够小,请使用计数排序。也许对较大的数字使用基数排序,但这可能不会比插入排序提供更快的速度。如果输入中有任何模式,请利用它。
tl;博士
合并。
完整版
没有什么比测试更好的了。
假设您的基础集合是
List
。我们将它们存储在 Array
.
val test = Array(
List("alpha", "beta", "gamma", "delta", "epsilon"),
List("one", "two", "three", "four", "five", "six"),
List("baby", "child", "adult", "senior"),
List("red", "yellow", "green"),
List("red", "orange", "yellow", "green", "blue", "indigo", "violet"),
List("tabby", "siamese", "manx", "persian"),
List("collie", "boxer", "bulldog", "rottweiler", "poodle", "terrier"),
List("budgie", "cockatiel", "macaw", "galah", "cockatoo"),
List("Alabama", "California", "Georgia", "Maine", "Texas", "Vermont", "Wyoming"),
List("I", "have", "to", "merge", "into")
).map(_.sorted)
那么您最初的想法可能就是压平和排序。
scala> val ans = th.pbench{ test.flatten.sorted.toList }
Benchmark (20460 calls in 183.2 ms)
Time: 8.246 us 95% CI 8.141 us - 8.351 us (n=18)
Garbage: 390.6 ns (n=1 sweeps measured)
ans: List[String] = List(Alabama, California, Georgia, I, Maine, ...)
或者您可以实现自定义的展平和排序:
def flat(ss: Array[List[String]], i0: Int, i1: Int): Array[String] = {
var n = 0
var i = i0
while (i < i1) {
n += ss(i).length
i += 1
}
val a = new Array[String](n)
var j = 0
i = i0
while (i < i1) {
var s = ss(i)
while (s ne Nil) {
a(j) = s.head
j += 1
s = s.tail
}
i += 1
}
a
}
def mrg(ss: Array[List[String]]): List[String] = {
val a = flat(ss, 0, ss.length)
java.util.Arrays.sort(a, new java.util.Comparator[String]{
def compare(x: String, y: String) = x.compare(y)
})
a.toList
}
scala> val ans = th.pbench{ mrg(test) }
Benchmark (20460 calls in 151.7 ms)
Time: 6.883 us 95% CI 6.850 us - 6.915 us (n=18)
Garbage: 293.0 ns (n=1 sweeps measured)
ans: List[String] = List(Alabama, California, Georgia, I, Maine, ...)
或者自定义成对合并
def mer(s1: List[String], s2: List[String]): List[String] = {
var s3 = List.newBuilder[String]
var i1 = s1
var i2 = s2
while (true) {
if (i2.head < i1.head) {
s3 += i2.head
i2 = i2.tail
if (i2 eq Nil) {
do {
s3 += i1.head
i1 = i1.tail
} while (i1 ne Nil)
return s3.result
}
}
else {
s3 += i1.head
i1 = i1.tail
if (i1 eq Nil) {
do {
s3 += i2.head
i2 = i2.tail
} while (i2 ne Nil)
return s3.result
}
}
}
Nil // Should never get here
}
接下来是分而治之的策略
def mge(ss: Array[List[String]]): List[String] = {
var n = ss.length
val a = java.util.Arrays.copyOf(ss, ss.length)
while (n > 1) {
var i,j = 0
while (i < n) {
a(j) = if (i+1 < n) mer(a(i), a(i+1)) else a(i)
i += 2
j += 1
}
n = j
}
a(0)
}
然后你就看到了
scala> val ans = th.pbench{ mge(test) }
Benchmark (40940 calls in 141.1 ms)
Time: 2.806 us 95% CI 2.731 us - 2.882 us (n=19)
Garbage: 146.5 ns (n=1 sweeps measured)
ans: List[String] = List(Alabama, California, Georgia, I, Maine, ...)
那么,就这样吧。对于您指定的大小的数据,以及使用列表(非常干净地合并),一个好的选择确实是分治合并。 (由于维护堆的额外复杂性,堆可能不会更好,甚至可能更糟;出于同样的原因,堆排序往往比合并排序慢。)
(注意:
th.pbench
是对我的微基准测试实用程序的调用,Thyme
。)
一些额外的建议涉及插入排序:
def inst(xs: Array[String]): Array[String] = {
var i = 1
while (i < xs.length) {
val x = xs(i)
var j = i
while (j > 0 && xs(j-1) > x) {
xs(j) = xs(j-1)
j -= 1
}
xs(j) = x
i += 1
}
xs
}
但是这些与合并排序相比,无论是一次扫描都没有竞争力:
scala> val ans = th.pbench( inst(flat(test, 0, test.length)).toList )
Benchmark (20460 calls in 139.2 ms)
Time: 6.601 us 95% CI 6.414 us - 6.788 us (n=19)
Garbage: 293.0 ns (n=1 sweeps measured)
ans: List[String] = List(Alabama, California, Georgia, I, Maine, ...)
或两个:
scala> th.pbench( mer(inst(flat(test, 0, test.length/2)).toList,
inst(flat(test, test.length/2,test.length)).toList) )
Benchmark (20460 calls in 119.3 ms)
Time: 5.407 us 95% CI 5.244 us - 5.570 us (n=20)
Garbage: 390.6 ns (n=1 sweeps measured)
res25: List[String] = List(Alabama, California, Georgia, I, Maine,