我在最近的一次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;
}
答案 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();
我认为在面试环境中,它还不足以简单地将大量代码打入方法中。展示对面向对象编程和关注点分离的理解也很重要。