【问题标题】:Algorithm to merge multiple sorted sequences into one sorted sequence in javascript在javascript中将多个排序序列合并为一个排序序列的算法
【发布时间】:2017-05-30 01:38:09
【问题描述】:

我正在寻找一种算法来合并多个排序序列,比如 X 排序序列与 n 个元素,在 javascript 中合并为一个排序序列,你能提供一些例子吗?

注意:我不想使用任何库。 正在努力解决https://icpc.kattis.com/problems/stacking

在条件下合并排序数组所需的最少操作数是多少:

拆分一个堆栈可以拆分为两个堆栈,方法是抬起堆栈的任何顶部并将其放在一边以形成一个新堆栈。

加入可以通过将一个放在另一个之上来加入两个堆栈。仅当顶部堆栈的底板不大于底部堆栈的顶板时才允许这样做,也就是说,连接的堆栈必须正确排序。

【问题讨论】:

  • 请提供一些数据,想要的结果和一些代码,你试过了。也请看这里:minimal reproducible example
  • 嗨 Nina,输入 1:1 2 4 和 3 5 输出:5 输入 2:1 1 1 1、1 1 1 1 和 1 1 1 1 输出:2 我正在尝试解决 @987654324 @
  • 我已经添加了图片和条件,请提供一些输入。

标签: javascript arrays algorithm sorting


【解决方案1】:

历史

这个问题已经解决了一个多世纪,可以追溯到 Hermann Hollerith 和打孔卡。大量的穿孔卡片,例如来自人口普查的穿孔卡片,通过将它们分成批次进行排序,对每个批次进行排序,然后将排序后的批次合并 - 所谓 "merge sort"。您在 1950 年代的科幻电影中看到的那些磁带驱动器很可能将多个分类的磁带合并到一个磁带上。

算法

您需要的所有算法都可以在https://en.wikipedia.org/wiki/Merge_algorithm 找到。用 JS 写这个很简单。更多信息可在问题Algorithm for N-way merge 中找到。另请参阅this question,这是一个几乎完全相同的副本,尽管我不确定任何答案都非常好。

天真的 concat-and-resort 方法甚至不能作为问题的答案。有点幼稚的从任何输入中获取下一个最小值的方法要好得多,但不是最优的,因为它需要更多的时间来找到下一个从中获取值的输入。这就是为什么使用称为“最小堆”或“优先队列”的最佳解决方案的原因。

简单的JS解决方案

这是一个真正简单的版本,除了能够看到它在做什么之外,我并没有声称对其进行了优化:

const data = [[1, 3, 5], [2, 4]];    

// Merge an array or pre-sorted arrays, based on the given sort criteria.
function merge(arrays, sortFunc) {
  let result = [], next;
   
  // Add an 'index' property to each array to keep track of where we are in it.
  arrays.forEach(array => array.index = 0);
 
  // Find the next array to pull from.
  // Just sort the list of arrays by their current value and take the first one.     
  function findNext() {
    return arrays.filter(array => array.index < array.length)
      .sort((a, b) => sortFunc(a[a.index], b[b.index]))[0];
  }

  // This is the heart of the algorithm.
  while (next = findNext()) result.push(next[next.index++]);

  return result;
}

function arithAscending(a, b) { return a - b; }

console.log(merge(data, arithAscending));

上面的代码在每个输入数组上维护一个index 属性来记住我们在哪里。最简单的替代方法是 shift 每个数组前面的元素在轮到它被合并时,但这将是相当低效的。

优化查找下一个要从中提取的数组

findNext 的这种幼稚实现,要查找要从中提取下一个值的数组,只需按第一个元素对输入列表进行排序,然后获取结果中的第一个数组。您可以通过使用"min-heap" 按排序顺序管理数组来优化这一点,这样就无需每次都使用它们。最小堆是一棵树,由节点组成,其中每个节点包含一个值,该值是下面所有值中的最小值,左右节点提供附加(更大)值,依此类推。您可以找到有关最小堆 here 的 JS 实现的信息。

生成器解决方案

把它写成一个生成器可能会更简洁一些,它接受一个可迭代列表作为输入,其中包括数组。

// Test data.
const data = [[1, 3, 5], [2, 4]];

// Merge an array or pre-sorted arrays, based on the given sort criteria.
function* merge(iterables, sortFunc) {
  let next;

  // Create iterators, with "result" property to hold most recent result.
  const iterators = iterables.map(iterable => {
    const iterator = iterable[Symbol.iterator]();
    iterator.result = iterator.next();
    return iterator;
  });

  // Find the next iterator whose value to use.
  function findNext() {
    return iterators
      .filter(iterator => !iterator.result.done)
      .reduce((ret, cur) => !ret || cur.result.value < ret.result.value ? cur : ret, 
         null);
  }

  // This is the heart of the algorithm.
  while (next = findNext()) {
    yield next.result.value;
    next.result = next.next();
  }
}

function arithAscending(a, b) { return a - b; }

console.log(Array.from(merge(data, arithAscending)));

【讨论】:

  • torazaburo,请提供给定条件的算法。
  • @torazaburo 我已经理解了这个问题,但它似乎没有你建议的那么有效。我已经做了一些测试,合并 1000 个随机排序的数组,每个随机长度在 5-10 个项目之间花费了大约 1700 毫秒,而在我通过一个一个合并的算法减少相同数量的数据时,只花费了 110 毫秒。可能是我做错了什么。请检查min-heapdynamical
【解决方案2】:

简单的方法是连接所有k 序列,并对结果进行排序。但如果每个序列都有n 元素,则成本将为O(k*n*log(k*n))。太多了!

相反,您可以使用优先级队列或堆。像这样:

var sorted = [];
var pq = new MinPriorityQueue(function(a, b) {
  return a.number < b.number;
});
var indices = new Array(k).fill(0);
for (var i=0; i<k; ++i) if (sequences[i].length > 0) {
  pq.insert({number: sequences[i][0], sequence: i});
}
while (!pq.empty()) {
  var min = pq.findAndDeleteMin();
  sorted.push(min.number);
  ++indices[min.sequence];
  if (indices[min.sequence] < sequences[i].length) pq.insert({
    number: sequences[i][indices[min.sequence]],
    sequence: min.sequence
  });
}

优先级队列最多同时包含k 个元素,每个序列一个。您不断提取最小的元素,并在该序列中插入以下元素。

有了这个,成本将是:

  • k*n 插入k 元素堆:O(k*n)
  • k*n 删除一堆 k 元素:O(k*n*log(k))
  • 每个数字的各种常量操作:O(k*n)

所以只有O(k*n*log(k))

【讨论】:

    【解决方案3】:

    只需将它们添加到一个大数组中并对其进行排序。

    您可以使用堆,将每个序列的第一个元素添加到其中,弹出最低的元素(这是您的第一个合并元素),从弹出元素的序列中添加下一个元素并继续直到所有序列结束。

    不过,将它们添加到一个大数组中并对其进行排序要容易得多。

    【讨论】:

    • 这忽略了重点,即利用输入已预先排序的事实。
    • 其实不然。我给了你合并算法。然而,实现它可能会浪费您的时间,因为它需要的不仅仅是几行代码。你确定那里有性能问题吗?从简单的解决方案开始。如果太慢,请实现更复杂的东西。
    【解决方案4】:

    这是我想出的一个简单的 javascript 算法。希望能帮助到你。它将采用任意数量的排序数组并进行合并。我正在维护一个用于数组位置索引的数组。它基本上遍历每个数组的索引位置并检查哪个是最小值。基于此,它获取最小值并插入到合并的数组中。此后,它增加该特定数组的位置索引。我觉得时间复杂度可以提高。如果我想出更好的算法(可能使用最小堆),我会回复。

    function merge() {
       var mergedArr = [],pos = [], finished = 0;
       for(var i=0; i<arguments.length; i++) {
           pos[i] = 0;
       }
       while(finished != arguments.length) {
           var min = null, selected;
           for(var i=0; i<arguments.length; i++) {
              if(pos[i] != arguments[i].length) {
                  if(min == null || min > arguments[i][pos[i]]) {
                      min = arguments[i][pos[i]];
                      selected = i;
                  }
              }
          }
          mergedArr.push(arguments[selected][pos[selected]]);
          pos[selected]++;
          if(pos[selected] == arguments[selected].length) {
             finished++;
          }
       }
       return mergedArr;
    }
    

    【讨论】:

    • 如果有k 序列,每个都有n 元素,这将花费O(k^2 * n),因为对于每个k*n 元素,您检查它是k 序列中的最小值.
    • @Oriol - 是的,我同意这不是最有效的算法。正如我提到的,最好使用最小堆来实现。感谢您计算时间复杂度。
    • @poushy 你确定这是在合并数组吗?
    • 是的,请在控制台上试用。输入应该是排序的数组。示例合并([1,2,3,4],[3,4,5,6])
    【解决方案5】:

    这是一个美丽的问题。与连接数组并应用.sort() 不同;使用.reduce() 的简单动态规划方法将产生 O(m.n) 时间复杂度的结果。其中 m 是数组的数量,n 是它们的平均长度。

    我们将一一处理数组。首先,我们将合并前两个数组,然后将结果与第三个数组合并,依此类推。

    function mergeSortedArrays(a){
      return a.reduce(function(p,c){
                        var pc = 0,
                            cc = 0,
                           len = p.length < c.length ? p.length : c.length,
                           res = [];
                        while (p[pc] !== undefined && c[cc] !== undefined) p[pc] < c[cc] ? res.push(p[pc++])
                                                                                         : res.push(c[cc++]);
                        return p[pc] === undefined ? res.concat(c.slice(cc))
                                                   : res.concat(p.slice(pc));
                      });
    }
    
    
    var sortedArrays = Array(5).fill().map(_ => Array(~~(Math.random()*5)+5).fill().map(_ => ~~(Math.random()*20)).sort((a,b) => a-b));
     sortedComposite = mergeSortedArrays(sortedArrays);
    
    sortedArrays.forEach(a => console.log(JSON.stringify(a)));
    console.log(JSON.stringify(sortedComposite));

    好的,根据@Mirko Vukušić 对该算法与.concat().sort() 的比较,该算法仍然是FF 中最快的解决方案,但不是Chrome。 Chrome .sort() 实际上非常快,我无法确定它的时间复杂度。我只需要在不触及算法本质的情况下稍微调整一下 JS 性能。所以现在看来​​比FF的concat和sort要快。

    function mergeSortedArrays(a){
      return a.reduce(function(p,c){
                        var pc = 0,
                            pl =p.length,
                            cc = 0,
                            cl = c.length,
                           res = [];
                        while (pc < pl && cc < cl) p[pc] < c[cc] ? res.push(p[pc++])
                                                                 : res.push(c[cc++]);
                        if (cc < cl) while (cc < cl) res.push(c[cc++]);
                        else while (pc < pl) res.push(p[pc++]);
                        return res;
                      });
    }
    
    function concatAndSort(a){
      return a.reduce((p,c) => p.concat(c))
              .sort((a,b) => a-b);
    }
    
    
    var sortedArrays = Array(5000).fill().map(_ => Array(~~(Math.random()*5)+5).fill().map(_ => ~~(Math.random()*20)).sort((a,b) => a-b));
    console.time("merge");
     mergeSorted = mergeSortedArrays(sortedArrays);
    console.timeEnd("merge");
    console.time("concat");
    concatSorted = concatAndSort(sortedArrays);
    console.timeEnd("concat");

    5000 个随机排序的数组,随机长度在 5-10 之间。

    【讨论】:

    • 看来您只是在进行m 合并,每次都添加一些额外的n 元素。这将花费O(2n + 3n + ... + m*n) = O(m^2 * n)
    • 嗨 redu,在给定条件下,合并排序数组所需的最少操作数是多少。如果我添加一个计数器并在 while 循环中增加它,我不会得到预期的输出。
    • 如果在我看来,您正在进行 n-1 合并,这似乎不太可能是最佳的。
    • 如果您可以在O(m*n) 中执行此操作,那么您可以使用此算法对O(m) 中的m 随机数进行排序,只需将每个随机数与n=1 放在不同的序列中即可。机器人排序m元素成本O(m*log(m))
    • 这无论如何都不是最优的。您需要编写的代码量、代码的可读性以及最终的性能比一个简单的 concat.sort() 慢 30-40 倍。这是要比较的 jsperf:jsperf.com/concat-sort 和 @Redu,您只提供了这个快 40 倍和短 40 倍的答案? :)
    【解决方案6】:

    es6 语法:

    function mergeAndSort(arrays) {
        return [].concat(...arrays).sort()
    }
    

    函数接收要合并和排序的数组数组。

    *编辑:正如@Redu 所说,上面的代码不正确。如果没有提供排序功能,默认sort() 是字符串Unicode。固定(和较慢)的代码是:

    function mergeAndSort(arrays) {
        return [].concat(...arrays).sort((a,b)=>a-b)
    }
    

    【讨论】:

    • 嗨 Mirko,谢谢,但我需要算法,我正在尝试解决 icpc.kattis.com/problems/stacking
    • 这种方法不会利用已经排序的数组的宝贵特性。
    • @Redu,当您必须对两个数组的结果合并进行排序时,您将如何使用两个数组已经排序的事实?不过,此时它与原始问题无关,因为在此答案之后它已更改。在最初的问题中,没有提到 kattis.com 的问题,在编辑之前,和其他人一样,我认为他需要合并/排序两个或更多数组。
    • @Redu,哎呀,真是个错误。是的,默认排序确实是按字符串。
    猜你喜欢
    • 2014-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-10
    • 2013-10-18
    • 1970-01-01
    • 2018-07-05
    • 2011-10-16
    相关资源
    最近更新 更多