Java 在无限列表中查找最大的N个数字

Java 在无限列表中查找最大的N个数字,java,list,treeset,Java,List,Treeset,在最近的一次Java采访中,我被问到了这个问题 给定一个包含数百万项的列表,维护一个包含最多n项的列表。由于列表的大小,按降序对列表进行排序,然后取前n个项目肯定是没有效率的 下面是我所做的,如果有人能提供更高效、更优雅的解决方案,我将不胜感激,因为我相信这也可以通过使用PriorityQueue来解决: public TreeSet<Integer> findTopNNumbersInLargeList(final List<Integer> largeNumbersL

在最近的一次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;
}
public TreeSet findTopNNumbersInLargeList(最终列表largeNumbersList,
最终整数(最高值计数){
TreeSet highestNNumbers=新树集();
对于(整数编号:largeNumbersList){
if(最高数目的.size()
您不需要嵌套循环,只要在集合太大时继续插入并删除最小的数字即可:

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;
}
public Set findTopNNumbersInLargeList(最终列表largeNumbersList,
最终整数(最高值计数){
TreeSet highestNNumbers=新树集();
对于(整数编号:largeNumbersList){
最高编号。添加(编号);
if(最高位nnnumbers.size()>highestValCount){
最高编号。pollFirst();
}
}
返回最高编号;
}
同样的代码也应该适用于
优先级队列
。在任何情况下,运行时都应该是
O(n log highestValCount)


另外,正如另一个答案中所指出的,您可以通过跟踪最低数量,避免不必要的插入来进一步优化此操作(以可读性为代价)。

底部的
for
循环是不必要的,因为您可以立即判断是否应该保留
编号

TreeSet
允许您在
O(log N)
*中查找最小的元素。将最小的元素与编号进行比较。如果
编号
较大,则将其添加到集合中,并删除最小的元素。否则,继续走到
largeNumbersList
的下一个元素

最坏的情况是原始列表按升序排序,因为在每一步中都必须替换
树集合
中的一个元素。在这种情况下,算法将采用
O(K logn)
,其中
K
是原始列表中的项目数,这是logNK对数组排序解决方案的改进

注意:如果列表由
整数组成,则可以使用不基于比较的线性排序算法来获得
O(K)
。这并不意味着对于任何固定数量的元素,线性解必然比原始解快


*你可以保留最小元素的值,使之成为
O(1)

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

也就是说,你的问题的文本与标题不同。超大和无限之间存在着巨大的差异。请记住这一点

为了回答这个可行的问题,我将首先实现一个缓冲区类来封装保持顶部
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);
    }
}

我认为在面试环境中,仅仅在一个方法中插入大量代码是不够的。展示对OO编程和关注点分离的理解也很重要。

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

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


这是Guava使用的算法,它从迭代器或Iterable中提取前n个元素。实际上,它的速度足够快,可以与基于优先级队列的方法相媲美,而且它更能抵抗最坏情况的输入。

实现一个简单、有界、有序、循环的缓冲区会更有效-可能基于
树集。如果你有重复的元素呢?首先我会问列表是如何包含的,是随机的还是什么,如果它有一些特定的顺序,你可以用它来分割列表,什么不适合他们想要的。因为嵌套的循环没有效率,我想既然你可能会有
1000000^3
那么它不是应该是
pollLast()
吗?默认顺序是升序,所以pollFirst将删除最小的数字,这就是我们想要的。谢谢Stefan,忘记使用pollFirst()方法了。重新设计代码毫无意义。是的,这是我认为任何人都能给出的最好答案,老实说,在这种情况下,我认为听你答案的人应该更关心逻辑和算法,以及时间复杂性,然后是语法等等。你有没有机会详细说明你的O(1)解决方案?对于每个元素O(1),不需要基数排序和友元;请看我的答案。@gsdev top
N
中最小的元素只有在您将新元素插入到树集中时才能上升(一个
O(Log N)
操作)。如果您同时从顶部N获得最小元素(秒final TopNBuffer<Integer> topN = new TopNBuffer<>(n); largeNumbersList.foreach(topN::add); final Set<Integer> highestN = topN.highest();