Java 在无限列表中查找最大的N个数字
在最近的一次Java采访中,我被问到了这个问题Java 在无限列表中查找最大的N个数字,java,list,treeset,Java,List,Treeset,在最近的一次Java采访中,我被问到了这个问题 给定一个包含数百万项的列表,维护一个包含最多n项的列表。由于列表的大小,按降序对列表进行排序,然后取前n个项目肯定是没有效率的 下面是我所做的,如果有人能提供更高效、更优雅的解决方案,我将不胜感激,因为我相信这也可以通过使用PriorityQueue来解决: public TreeSet<Integer> findTopNNumbersInLargeList(final List<Integer> largeNumbersL
给定一个包含数百万项的列表,维护一个包含最多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 topN
中最小的元素只有在您将新元素插入到树集中时才能上升(一个O(Log N)
操作)。如果您同时从顶部N获得最小元素(秒final TopNBuffer<Integer> topN = new TopNBuffer<>(n);
largeNumbersList.foreach(topN::add);
final Set<Integer> highestN = topN.highest();