The top N elements in an array in Java come either from sorting a copy of the array in descending order and keeping the first N elements, or from one scan in which a min-heap of size N holds the largest values seen so far. Sorting takes O(n log n) time, and the heap takes O(n log N), which is faster when N is much smaller than the array.
Top N queries drive leaderboards, the best-selling products of the week, the slowest API calls in a log, and the highest bids in an auction. In each case, we want a few winners from many values, and we show them in descending order.
The following example picks the three highest scores with a stream, and with the heap-based topN() method from section 4.
int[] scores = {72, 95, 60, 88, 95, 41, 79};
List<Integer> top3 = Arrays.stream(scores).boxed().sorted(Comparator.reverseOrder()).limit(3).toList(); // [95, 95, 88]
int[] best = topN(scores, 3); // [95, 95, 88]
Both versions keep the duplicate 95, and both return the result in descending order. The sections that follow settle what top N means, compare sorting, streams and heaps, extend them to objects, and show the Guava collector that does the same job.
1. What Counts as the Top N
The phrase top N leaves a few decisions open before we write code. The answer changes the result, so we fix them first and pick the method second.
- The top N scores are the largest values, but the top N lap times are the smallest ones, so we flip the comparator and change nothing else.
- In {95, 95, 88}, two players share first place. A leaderboard keeps both, whereas a list of distinct prices keeps one, which we get with distinct().
- Asking for the top 10 of 7 scores should return all 7 values, not throw an exception.
- Most callers want the winners in descending order, and some want them in their original array order.
All the methods in this article keep duplicates, accept any N, and return descending order unless we say otherwise.
2. Sorting a Copy and Taking the First N
The simplest approach sorts the values and reads N of them from the high end. The method Arrays.sort() sorts in place, so we sort a clone of the array and leave the caller’s data unchanged. For an int[], the sort is ascending only, so we read the copy from its last index backward.
static int[] topNBySort(int[] values, int n) {
int[] copy = values.clone();
Arrays.sort(copy);
int count = Math.min(n, copy.length);
int[] top = new int[count];
for (int i = 0; i < count; i++) {
top[i] = copy[copy.length - 1 - i];
}
return top;
}
int[] scores = {72, 95, 60, 88, 95, 41, 79};
int[] top3 = topNBySort(scores, 3); // [95, 95, 88]
int[] all = topNBySort(scores, 10); // [95, 95, 88, 79, 72, 60, 41]
int first = scores[0]; // 72, input unchanged
An Integer[] can be sorted in descending order with Collections.reverseOrder(), after which Arrays.copyOfRange() cuts out the first N elements. The array sorting tutorial lists the other Arrays.sort() overloads, such as sorting only a range.
Integer[] bids = {120, 340, 95, 410, 260};
Integer[] sorted = bids.clone();
Arrays.sort(sorted, Collections.reverseOrder());
Integer[] top2 = Arrays.copyOfRange(sorted, 0, Math.min(2, sorted.length)); // [410, 340]
3. Streams with sorted() and limit()
A stream expresses the same sort in one line, and it never touches the source array. For an int[], we call boxed() first, because IntStream.sorted() has no comparator parameter and sorts only in ascending order. The limit() method keeps the first N elements and returns fewer when the stream is shorter.
int[] scores = {72, 95, 60, 88, 95, 41, 79};
List<Integer> top3 = Arrays.stream(scores).boxed().sorted(Comparator.reverseOrder()).limit(3).toList(); // [95, 95, 88]
List<Integer> distinct3 = Arrays.stream(scores).boxed().distinct().sorted(Comparator.reverseOrder()).limit(3).toList(); // [95, 88, 79]
int[] lowest2 = Arrays.stream(scores).sorted().limit(2).toArray(); // [41, 60]
The stream still sorts every element, so it costs O(n log n) time and O(n) memory, the same as section 2. For arrays of a few thousand elements, the difference from a heap does not matter, and the stream is the most readable choice.
4. Keeping a Min-Heap of Size N
A PriorityQueue keeps its smallest element at the head, so poll() removes the smallest value in O(log N) time. When we add every value and poll whenever the queue grows beyond N elements, the queue ends up holding the N largest values. It never stores more than N + 1 values, whatever the array size.

Using a min-heap for the largest values may look odd. The head of the heap is the smallest of the current winners, which is the one value a newcomer must beat, so the comparison happens against the head only.
static int[] topN(int[] values, int n) {
if (values == null || n <= 0) {
return new int[0];
}
PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap
for (int v : values) {
if (heap.size() < n) {
heap.offer(v);
} else if (v > heap.peek()) {
heap.poll(); // drop the smallest winner
heap.offer(v);
}
}
int[] result = new int[heap.size()];
for (int i = result.length - 1; i >= 0; i--) {
result[i] = heap.poll(); // smallest goes last
}
return result;
}
int[] scores = {72, 95, 60, 88, 95, 41, 79};
int[] top3 = topN(scores, 3); // [95, 95, 88]
int[] top10 = topN(scores, 10); // [95, 95, 88, 79, 72, 60, 41]
int[] none = topN(new int[0], 3); // []
Printing the queue itself does not show sorted values, because toString() and the iterator of a PriorityQueue follow the internal heap order. We get descending order only by polling, which is why the method fills the result array from its end.
PriorityQueue<Integer> heap = new PriorityQueue<>(List.of(100, 76, 90));
String inside = heap.toString(); // "[76, 100, 90]"
5. Top N Objects by a Field
A game leaderboard shows the three players with the most points, not three numbers. The same approaches work with objects once we give them a Comparator that extracts the field.
record Player(String name, int points) {}
Player[] players = {new Player("ana", 310), new Player("raj", 450), new Player("li", 280), new Player("tom", 450)};
List<String> podium = Arrays.stream(players).sorted(Comparator.comparingInt(Player::points).reversed()).limit(3).map(Player::name).toList(); // [raj, tom, ana]
The stream sort is stable, so raj stays ahead of tom because raj comes first in the array. For large arrays, the heap version from section 4 becomes a generic method with the comparator as a parameter.
static <T> List<T> topN(T[] items, int n, Comparator<? super T> order) {
PriorityQueue<T> heap = new PriorityQueue<>(order); // smallest winner at the head
for (T item : items) {
heap.offer(item);
if (heap.size() > n) {
heap.poll();
}
}
List<T> result = new ArrayList<>(heap);
result.sort(order.reversed());
return result;
}
Player[] players = {new Player("ana", 310), new Player("raj", 450), new Player("li", 280), new Player("tom", 400)};
List<Player> top2 = topN(players, 2, Comparator.comparingInt(Player::points)); // [Player[name=raj, points=450], Player[name=tom, points=400]]
List<Player> slowest = topN(players, 1, Comparator.comparingInt(Player::points).reversed()); // [Player[name=li, points=280]]
Passing the reversed comparator turns the same method into a bottom N search, as the slowest line shows. The heap does not keep the array order of equal values, so two players with the same points may come back in either order.
6. Guava Comparators.greatest()
Projects that use Guava can collect the top N with Comparators.greatest(), available since Guava 22.0. It keeps only N elements, runs in expected O(n) time, and returns an unmodifiable list in descending order. Its mirror Comparators.least() returns the smallest N values in ascending order.
int[] scores = {72, 95, 60, 88, 95, 41, 79};
List<Integer> top3 = Arrays.stream(scores).boxed().collect(Comparators.greatest(3, Comparator.<Integer>naturalOrder())); // [95, 95, 88]
List<Integer> low2 = Arrays.stream(scores).boxed().collect(Comparators.least(2, Comparator.<Integer>naturalOrder())); // [41, 60]
Older code uses Ordering.natural().greatestOf(list, n), or the bounded MinMaxPriorityQueue that evicts its greatest element when it is full. The Comparators collectors are the shorter choice when we already work with streams.
7. Choosing a Method for the Top N Elements in an Array
For small arrays, the stream with sorted().limit() is the clearest code; for large arrays and a small N, the min-heap or Guava’s collector avoids sorting values that can never make the list. A quickselect, the partition step of quicksort, finds the top N in O(n) on average, but it reorders the array and degrades to O(n2) on bad pivots, so it rarely pays off in application code.
| Method | Time | Extra memory | Changes the input |
|---|---|---|---|
| Sort a copy, take N | O(n log n) | O(n) | No |
| sorted().limit(n) | O(n log n) | O(n) | No |
| Min-heap of size N | O(n log N) | O(N) | No |
| Comparators.greatest() | Expected O(n), worst O(n log N) | O(N) | No |
| Sort the array in place | O(n log n) | O(log n) | Yes |
8. Top N Elements FAQs
Leaderboards and reports raise the same follow-up questions about top N searches.
8.1. How do we find the kth largest element of an array?
We take the last element of the top k. With the heap method, topN(scores, k)[k – 1] returns it, so the second largest value is topN(scores, 2)[1]. When duplicates should count once, we use the stream version with distinct().
int[] scores = {72, 95, 60, 88, 95, 41, 79};
int second = topN(scores, 2)[1]; // 95
int secondDistinct = Arrays.stream(scores).boxed().distinct().sorted(Comparator.reverseOrder()).skip(1).findFirst().orElseThrow(); // 88
8.2. How do we get the N smallest values instead?
We reverse the order. On an IntStream, sorted().limit(n) already returns the smallest values, and the heap version needs a max-heap, new PriorityQueue<>(Comparator.reverseOrder()), that drops its largest value. The max and min of an array tutorial covers the case N = 1.
8.3. How do we find the N most frequent elements?
We count first and take the top N of the counts. The collector groupingBy() with counting() builds a map from value to count, and we sort its entries by count.
int[] ratings = {4, 5, 3, 5, 4, 5, 2};
List<Integer> mostFrequent2 = Arrays.stream(ratings).boxed().collect(Collectors.groupingBy(r -> r, Collectors.counting())).entrySet().stream().sorted(Map.Entry.<Integer, Long>comparingByValue().reversed()).limit(2).map(Map.Entry::getKey).toList(); // [5, 4]
8.4. How do we keep the top N in their original array order?
We pick the top N indexes instead of the values, sort those indexes in ascending order, and read the values back. The stable sort keeps the first of two equal scores.
int[] scores = {72, 95, 60, 88, 95, 41, 79};
int[] idx = IntStream.range(0, scores.length).boxed().sorted(Comparator.comparingInt((Integer i) -> scores[i]).reversed()).limit(3).mapToInt(Integer::intValue).sorted().toArray(); // [1, 3, 4]
int[] inOrder = Arrays.stream(idx).map(i -> scores[i]).toArray(); // [95, 88, 95]
8.5. Is a PriorityQueue faster than sorting for top N?
Yes, when N is small compared to the array. The heap makes about n log N comparisons, whereas a sort makes about n log n. For N close to n, both cost the same, and the sort is simpler code.
9. Conclusion
Finding the top N items starts with deciding what top means, largest or smallest, with or without duplicates. After that, sorting a copy or a stream with sorted().limit(n) gives readable code in O(n log n) time.
For large arrays and a small N, a min-heap of size N keeps only the current winners and runs in O(n log N), and Guava’s Comparators.greatest() packs the same idea into a collector. The PriorityQueue returns its elements in sorted order only through poll(), never through toString() or iteration.
10. References
- PriorityQueue (Java SE 25 API)
- Stream.limit() (Java SE 25 API)
- Arrays.sort() (Java SE 25 API)
- Comparators (Guava API)
Happy Learning !!