在无限列表中找到最高的N个数字

时间:2014-12-30 16:42:18

标签: java list treeset

我在最近的一次Java采访中被问到这个问题。

  

如果列表包含数百万个项目,请维护最多n个项目的列表。由于列表大小,按降序对列表进行排序然后获取前n个项目肯定效率不高。

以下是我的所作所为,如果有人能提供更有效或更优雅的解决方案我会很感激,因为我相信这也可以通过PriorityQueue来解决:

public TreeSet<Integer> findTopNNumbersInLargeList(final List<Integer> largeNumbersList, 
final int highestValCount) {

    TreeSet<Integer> highestNNumbers = new TreeSet<Integer>();

    for (int number : largeNumbersList) {
        if (highestNNumbers.size() < highestValCount) {
            highestNNumbers.add(number);
        } else {
            for (int i : highestNNumbers) {
                if (i < number) {
                    highestNNumbers.remove(i);
                    highestNNumbers.add(number);
                    break;
                }
            }
        }
    }
    return highestNNumbers;
}

4 个答案:

答案 0 :(得分:6)

底部的for循环是不必要的,因为您可以立即告知是否应该保留number

TreeSet可让您找到O(log N) * 中的最小元素。将该最小元素与number进行比较。如果number更大,请将其添加到集合中,然后删除最小元素。否则,请继续前往largeNumbersList的下一个元素。

最糟糕的情况是原始列表按升序排序,因为您必须在每个步骤中替换TreeSet中的元素。在这种情况下,算法将采用O(K log N),其中K是原始列表中的项目数,对于排序数组的解决方案,log N K的改进。

注意:如果您的列表包含Integer,则可以使用不基于比较的线性排序算法来获得O(K)的整体渐近复杂度。这并不意味着对于任何固定数量的元素,线性解决方案必然比原始解决方案更快。

* 您可以保留最小元素的值O(1)

答案 1 :(得分:4)

可以支持分摊O(1)处理新元素和O(n)查询当前顶级元素,如下所示:

保持大小为2n的缓冲区,每当看到新元素时,将其添加到缓冲区。当缓冲区变满时,使用快速选择或其他线性中值查找算法来选择当前的前n个元素,并丢弃其余元素。这是一个O(n)操作,但您只需要每n个元素执行一次,这可以平衡O(1)的摊销时间。

这是Guava用于Ordering.leastOf的算法,它从Iterator或Iterable中提取前n个元素。在实践中,基于PriorityQueue的方法具有足够的竞争力,并且它对最坏情况的输入更具抵抗力。

答案 2 :(得分:3)

您不需要嵌套循环,只需保持插入并在集合过大时删除最小的数字:

public Set<Integer> findTopNNumbersInLargeList(final List<Integer> largeNumbersList, 
  final int highestValCount) {

  TreeSet<Integer> highestNNumbers = new TreeSet<Integer>();

  for (int number : largeNumbersList) {
    highestNNumbers.add(number);
    if (highestNNumbers.size() > highestValCount) {
      highestNNumbers.pollFirst();
    }
  }
  return highestNNumbers;
}

同样的代码也适用于PriorityQueue。无论如何,运行时应该是O(n log highestValCount)

P.S。正如在另一个答案中指出的那样,你可以通过跟踪最低数量来避免不必要的插入,从而进一步优化(以可读性为代价)。

答案 3 :(得分:0)

我首先要说的是,如上所述,你的问题是不可能的。如果没有完全遍历它,则无法在n中找到最高List项。并且没有办法完全遍历无限List

那就是说,你的问题的文字与标题不同。非常大和无限之间存在大量差异。请记住这一点。

为了回答可行的问题,我首先要实现一个缓冲类来封装保持顶部N的行为,让我们称之为TopNBuffer

class TopNBuffer<T extends Comparable<T>> {
    private final NavigableSet<T> backingSet = new TreeSet<>();

    private final int limit;

    public TopNBuffer(int limit) {
        this.limit = limit;
    }

    public void add(final T t) {
        if (backingSet.add(t) && backingSet.size() > limit) {
            backingSet.pollFirst();
        }
    }

    public SortedSet<T> highest() {
        return Collections.unmodifiableSortedSet(backingSet);
    }
}

我们在这里所做的就是,在add上,如果数字不是唯一的,并且添加数字会使Set超出其限制,那么我们只需删除{{Set中的最低元素1}}。

方法highest给出了当前最高元素的不可修改的视图。因此,在Java 8语法中,您需要做的就是:

final TopNBuffer<Integer> topN = new TopNBuffer<>(n);
largeNumbersList.foreach(topN::add);
final Set<Integer> highestN = topN.highest();

我认为在面试环境中,它还不足以简单地将大量代码打入方法中。展示对面向对象编程和关注点分离的理解也很重要。