将排序集合合并到新排序集合的最有效方法?

时间:2014-03-13 16:31:19

标签: javascript algorithm scala sorting merge

我必须将已经排序的各种集合合并到一个已排序的集合中。这个操作需要针对非常大的集合(大约5个元素)和不多的源(可能大约10个)进行,但输出应该快速计算(非阻塞服务器)。

对于两个源集合,它非常简单,但是当源集合的数量增加时,需要考虑不同的策略(n源集合,每个集合都有m元素:

  • 连接,然后排序。它具有O(n*m*log(n*m))复杂度(快速排序n*m元素)。
  • 扫描所有头部,选择最低,将其推送到目标集合。我猜它有O(n*n*m)复杂度(扫描nn*m次,即元素总数。)
  • 分割并征服,成对合并集合,直到您只有一个集合。我猜它有O(log(n)*n*m)复杂度(做log(n)合并阶段,每个阶段都包含所有n*m元素。)
  • 将集合保存在最小堆中(使用头部作为键),删除min并在每个步骤中使用下一个头重新添加集合。我猜这也有O(log(n)*n*m)(堆删除和插入操作是log(n),它们完成了n*m次。

因此,进行后续二进制合并的计算复杂度(如果我没有犯任何错误)和每次更复杂的基于堆的min元素选择似乎同样好。在内存方面,堆可能更加垃圾收集器友好,因为它不需要那么多的中间临时集合。

我是否错过了一些错误(错误计算复杂性或错过任何替代方法)?我的实现最有可能在Javascript或Scala中完成,因此欢迎任何可能适用于这些执行环境的运行时问题!

BTW这是相关:Most efficient way to merge collections preserving order?

2 个答案:

答案 0 :(得分:3)

由于您的数据集非常小,您的渐近考虑因素几乎无关紧要(尽管正确),因为更重要的因素是微优化。我建议你至少比较以下几个选项,以增加实现它们所需的工作量:

  • 连接并使用内置排序算法。这很好,因为短数组可能非常快,因为在Javascript的情况下,它是用宿主语言编写的,并且可能比任何给定的纯JS解决方案快得多(即使使用JIT编译)
  • 连接并使用插入排序。下降次数为O(k)。对于5个大小为10的列表,k的最坏情况是1000.但实际上它会少得多。你甚至可以防止最坏的情况,例如通过按照增加第一个元素的顺序预先排序列表或者只是随机化顺序。
  • 将源分成两组,连接每个组并使用插入排序对它们进行排序(对于25个元素,插入排序可能是总体上最快的比较排序),然后合并结果。
  • 对于10个源,以二叉树方式合并它们(将2,3与4等合并1,然后重复生成大小为20的结果列表)。基本上这只是合并排序的第二阶段

这些是一般性建议。根据您的数据集,可能会有一个更好的定制选项。例如,如果输入的数字足够小,则使用计数排序。也许使用基数排序来获得更大的数字,但这可能不会比插入排序提供更多的速度。如果您在输入中有任何模式,请利用它。

答案 1 :(得分:2)

<强> 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, ...)

所以,你去吧。对于您指定的大小的数据,以及使用列表(非常干净地合并),一个好的赌注确实是分而治之的合并。 (堆可能不会更好,可能会更糟,因为维护堆的额外复杂性;出于同样的原因,heapsort往往比mergesort慢。)

(注意: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,