【问题标题】:Java Code Review: Merge sorted lists into a single sorted list [closed]Java代码审查:将排序列表合并到一个排序列表中[关闭]
【发布时间】:2017-09-27 10:14:10
【问题描述】:

我想将已排序的列表合并为一个列表。这个解决方案如何?我相信它在 O(n) 时间内运行。有任何明显的缺陷、效率低下或风格问题吗?

我不太喜欢为“这是第一次迭代”设置标志并使用它来确保“最低”具有默认值的习惯用法。有没有更好的解决方法?

public static <T extends Comparable<? super T>> List<T> merge(Set<List<T>> lists) {
    List<T> result = new ArrayList<T>();

    int totalSize = 0; // every element in the set
    for (List<T> l : lists) {
        totalSize += l.size();
    }

    boolean first; //awkward
    List<T> lowest = lists.iterator().next(); // the list with the lowest item to add

    while (result.size() < totalSize) { // while we still have something to add
        first = true;

        for (List<T> l : lists) {
            if (! l.isEmpty()) {
                if (first) {
                    lowest = l;
                    first = false;
                }
                else if (l.get(0).compareTo(lowest.get(0)) <= 0) {
                    lowest = l;
                }
            }
        }
        result.add(lowest.get(0));
        lowest.remove(0);
    }
    return result;
}

注意:这不是家庭作业,但也不适用于生产代码。

【问题讨论】:

  • 我认为您的实现很好,但请注意算法复杂性:假设输入列表数量恒定,则为 O(n)。但是由于您的方法可以处理任意数量的输入列表,因此运行时间为 O(Mn) - 您必须考虑可变数量的列表。如果 M > log2(n)+1(我认为),那么简单地连接所有列表并对其进行归并排序实际上会更快,这需要 O(nlog2(n))。这种情况不太可能经常发生,但值得注意。
  • 这是标准的合并排序代码。您可能可以在 google.com/codesearch#search/&q=merge%5C%20sort&type=cs 找到优化循环的方法的灵感。你不应该需要那个“第一个”布尔值。
  • @Rosarch:我建议不要删除任何东西——正如已经说过的那样,remove(0) 对ArrayList 来说是一场灾难,但对于任何List 来说,它都需要不必要的时间。维护Set&lt;Iterator&gt;(或一组索引以获得最大速度)应该会更好。既然 Java 有 TimSort,那么简单地连接列表并调用排序可能会更好。
  • 破坏性输入。
  • 这个问题似乎跑题了,因为它要求进行代码审查,应该在Code Review

标签: java list sorting


【解决方案1】:

如果lists 包含一个 ArrayList,效率会很差,因为lowest.remove(0) 将在列表长度上花费线性时间,使您的算法为 O(n^2)。

我愿意:

List<T> result = new ArrayList<T>();
for (List<T> list : lists) {
    result.addAll(list);
}
Collections.sort(result);

这是在 O(n log n) 中,并且留下了更少繁琐的代码来测试、调试和维护。

【讨论】:

  • 这个答案有一点问题。当创建一个新的ArrayList 而不指定它的容量时,它必须随着更多元素的添加而增长。每次增长时,都需要将其全部内容复制到一个新数组中,即 O(n)。对于 m 个数组,它可能会增加 O(m*n)(但这取决于ArrayList 的增长策略和初始容量)。为了轻松避免这个问题,我们可以先将所有lists 的大小相加,然后指定result 列表的初始容量。
  • 由于 Javadoc 写道“除了添加元素具有恒定的摊销时间成本这一事实之外,没有指定增长策略的细节。”,您可以通过一个小的常数因子来加快添加元素的速度,但这不是该算法的昂贵部分,因此整体效果可以忽略不计。具体来说,使用 oracle 实现的增长策略,运行时将复制大约 3n 个数组元素而不是 n,而第二部分将涉及大约 n log n 比较,每个比较都比复制一个数组元素要昂贵得多。
【解决方案2】:

扩展安东的评论:

通过将每个 List 的最新结果以及它是哪个列表的指示符放入堆中,然后不断地从堆中取出顶部,并从属于该项的列表中将一个新项放入堆中你刚刚起飞。

Java 的 PriorityQueue 可以提供堆实现。

【讨论】:

    【解决方案3】:

    您的解决方案可能是最快的解决方案。 SortedLists 的插入成本为 log(n),因此您最终会得到 M log (M)(其中 M 是列表的总大小)。

    将它们添加到一个列表并排序,虽然更易于阅读,但仍然是 M log(M)。

    你的解决方案只是 M。

    您可以通过调整结果列表的大小以及使用对最低列表的引用而不是布尔值来稍微清理一下您的代码。

    public static <T extends Comparable<? super T>> List<T> merge(Set<List<T>> lists) {
        int totalSize = 0; // every element in the set
        for (List<T> l : lists) {
            totalSize += l.size();
        }
    
        List<T> result = new ArrayList<T>(totalSize);
    
        List<T> lowest;
    
        while (result.size() < totalSize) { // while we still have something to add
            lowest = null;
    
            for (List<T> l : lists) {
                if (! l.isEmpty()) {
                    if (lowest == null) {
                        lowest = l;
                    } else if (l.get(0).compareTo(lowest.get(0)) <= 0) {
                        lowest = l;
                    }
                }
            }
    
            result.add(lowest.get(0));
            lowest.remove(0);
        }
    
        return result;
    }
    

    如果你真的很特别,使用 List 对象作为输入,最低可以初始化为 lists.get(0),你可以跳过 null 检查。

    【讨论】:

      【解决方案4】:

      这是一个非常老的问题,但我不喜欢任何提交的答案,所以这就是我最终要做的。

      由于日志线性复杂性(O(m n log(m n))),将它们全部添加到一个列表中并排序的解决方案很糟糕。如果这对您不重要,那么这绝对是最简单和最直接的答案。您最初的解决方案还不错,但有点混乱,@Dathan 指出,对于 m 个列表和 n 个总元素,复杂性是 O(m n)。您可以通过使用堆减少每个元素的比较次数来将其减少到O(n log(m))。我使用一个帮助类来比较可迭代对象。这样我就不会破坏初始列表,并且无论输入什么类型的列表,它都应该以合理的复杂性运行。我在下面的实现中看到的唯一缺陷是它不支持带有null 元素的列表,但是如果需要,可以使用标记来修复。

      public static <E extends Comparable<? super E>> List<E> merge(Collection<? extends List<? extends E>> lists) {
          PriorityQueue<CompIterator<E>> queue = new PriorityQueue<CompIterator<E>>();
          for (List<? extends E> list : lists)
              if (!list.isEmpty())
                  queue.add(new CompIterator<E>(list.iterator()));
      
          List<E> merged = new ArrayList<E>();
          while (!queue.isEmpty()) {
              CompIterator<E> next = queue.remove();
              merged.add(next.next());
              if (next.hasNext())
                  queue.add(next);
          }
          return merged;
      }
      
      private static class CompIterator<E extends Comparable<? super E>> implements Iterator<E>, Comparable<CompIterator<E>> {
          E peekElem;
          Iterator<? extends E> it;
      
          public CompIterator(Iterator<? extends E> it) {
              this.it = it;
              if (it.hasNext()) peekElem = it.next();
              else peekElem = null;
          }
      
          @Override
          public boolean hasNext() {
              return peekElem != null;
          }
      
          @Override
          public E next() {
              E ret = peekElem;
              if (it.hasNext()) peekElem = it.next();
              else peekElem = null;
              return ret;
          }
      
          @Override
          public void remove() {
              throw new UnsupportedOperationException();
          }
      
          @Override
          public int compareTo(CompIterator<E> o) {
              if (peekElem == null) return 1;
              else return peekElem.compareTo(o.peekElem);
          }
      
      }
      

      返回列表的每个元素都涉及两个 O(log(m)) 堆操作,还有对所有列表的初始迭代。因此,对于 n 个总元素和 m 个列表,总体复杂度为 O(n log(m) + m)。使这总是比连接和排序更快。

      【讨论】:

      • 是的,基于堆的 k-way 合并绝对是性能热点的必经之路。但是开发、调试和维护也比较困难,所以我只是在衡量简单算法不够快后才使用它。
      【解决方案5】:

      由于 Balus 和 Meriton 一起对您关于算法的问题做出了很好的回应,我将与您讨论“第一”成语。

      肯定还有其他方法(例如将最低设置为“魔术”值),但我碰巧觉得“第一”(我可能会给它一个更长的名字,但那是迂腐的)是最好的,因为它非常清楚。像“first”这样的布尔值的存在是一个明确的信号,表明你的循环第一次会做一些特别的事情。它可以帮助读者。

      如果您采用 Balus/meriton 方法,当然不需要它,但这种情况会突然出现。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-27
        • 2014-03-30
        • 2017-03-01
        • 2013-11-10
        相关资源
        最近更新 更多