Algorithmic Challenges for Large Datasets in Java: Solving TopK Unique Integers

Use a min-heap (PriorityQueue) combined with a HashSet to extract the top K unique integers from massive datasets in O(N log K) time and O(K) space, allowing you to process billions of rows without loading them entirely into memory.

The doocs/advanced-java repository provides battle-tested patterns for handling algorithmic challenges for large datasets in Java. When you need to find the top K unique integers from a stream that exceeds available memory, the min-heap approach documented in docs/big-data/topk-problems-and-solutions.md offers the most reliable balance of speed and memory efficiency.

Why Min-Heap is the Standard Solution for TopK Problems

When facing algorithmic challenges for large datasets in Java, the min-heap (implemented as java.util.PriorityQueue) is the canonical choice for streaming TopK problems. The repository emphasizes this approach because it requires only a single pass through the data and bounds memory usage regardless of input size.

Core algorithm:

  1. Initialize a min-heap of size K.
  2. For each incoming integer, insert it into the heap.
  3. If the heap size exceeds K, remove the smallest element (poll()).
  4. After processing all N items, the heap contains the K largest values.

Complexity characteristics:

  • Time: O(N log K) — each insertion and removal costs log K.
  • Space: O(K) — only K elements are stored, making this suitable for terabyte-scale inputs.

As noted in docs/big-data/topk-problems-and-solutions.md (lines 46-48), Java's PriorityQueue provides exactly the building block needed for this pattern, contrasting with the C++ implementation shown elsewhere in the file.

Java Implementation: Extracting Top K Unique Integers

To solve the specific variant of finding unique integers, you must combine the min-heap with a HashSet to filter duplicates. The following implementation mirrors the logic described in the repository while providing production-ready Java code.

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class TopKUniqueIntegers {

    /**
     * Returns the K largest **unique** integers from the given stream.
     *
     * @param source   a {@link IntStream} that may be huge (e.g., read from a file)
     * @param k        how many top values to keep (k > 0)
     * @return a list containing the top K values in descending order
     */
    public static List<Integer> topKUnique(IntStream source, int k) {
        // Use a min‑heap of size K to retain the largest values.
        PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);

        // To guarantee uniqueness we keep a HashSet of the values already seen.
        Set<Integer> seen = new HashSet<>();

        source.forEach(value -> {
            if (seen.add(value)) {                 // first time we see this integer
                minHeap.offer(value);
                if (minHeap.size() > k) {
                    int removed = minHeap.poll(); // discard the smallest
                    // also remove it from the seen set – it’s no longer in the top‑K
                    seen.remove(removed);
                }
            }
        });

        // Extract heap content into a list sorted descending.
        List<Integer> result = new ArrayList<>(minHeap);
        result.sort(Comparator.reverseOrder());
        return result;
    }

    // --------------------------------------------------------------------
    // Demo driver that reads ints from a text file (one per line) and prints
    // the top 10 unique numbers.
    // --------------------------------------------------------------------
    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.err.println("Usage: java TopKUniqueIntegers <file> <K>");
            System.exit(1);
        }
        Path path = Path.of(args[0]);
        int k = Integer.parseInt(args[1]);

        try (IntStream lines = Files.lines(path)
                .mapToInt(Integer::parseInt)) {
            List<Integer> topK = topKUnique(lines, k);
            System.out.println("Top " + k + " unique integers:");
            topK.forEach(System.out::println);
        }
    }
}

Key implementation details:

  • PriorityQueue<Integer> creates the min-heap that automatically orders elements ascending, allowing O(log K) insertion and removal.
  • HashSet<Integer> enforces uniqueness; the seen.add(value) call returns true only for first occurrences, preventing duplicate entries from inflating the heap.
  • Streaming support via IntStream allows the method to process files, database cursors, or Kafka streams without materializing the entire dataset in RAM.
  • Cleanup on eviction removes evicted elements from seen to prevent memory leaks when the same integer reappears later in the stream.

Alternative Algorithms for Large Dataset Challenges

While the min-heap is the general-purpose winner for algorithmic challenges for large datasets in Java, the doocs/advanced-java repository documents several alternatives optimized for specific constraints.

Quick-Select (Partial Quicksort)

Best for: When data fits in memory and you need average-case linear time.

Approach: Use the partition scheme from quicksort to recursively narrow down the K-th largest element. Once found, the elements to its left are your top K.

Trade-offs: Requires O(N) average time but O(N^2) worst-case. Needs random access to the full dataset, making it unsuitable for true streaming scenarios.

Bitmap (Bit Vector)

Best for: Bounded integer domains (e.g., 0 to 10^9) with no duplicates or when duplicates are handled separately.

Approach: Allocate a bit array where each bit represents the presence of an integer. Scan the bitmap from the highest index downward to collect K set bits.

Trade-offs: Consumes memory proportional to the integer range, not the data size. Excellent for dense data but wasteful for sparse 64-bit integers.

Hash-Bucket Counting

Best for: Frequency-based TopK (e.g., finding the K most frequent items rather than largest values).

Approach: Use a HashMap to count occurrences, then apply a min-heap or quick-select on the frequency buckets.

Trade-offs: Requires storing all unique keys and counts. The doocs/advanced-java repository notes this is ideal when the cardinality is manageable but the stream volume is massive.

Trie (Prefix Tree)

Best for: String data or numbers with shared prefixes (e.g., IP addresses, URLs).

Approach: Insert data into a trie and traverse to find the lexicographically or numerically largest K entries.

Trade-offs: Higher memory overhead per node but supports efficient prefix-based queries and incremental updates.

Hybrid Distributed Approach

Best for: Multi-terabyte datasets spread across a cluster.

Approach: Compute local TopK on each shard using min-heaps, then merge the partial results with a final heap or sort.

Trade-offs: Adds network latency and coordination complexity but scales horizontally without bound.

Key Files in the doocs/advanced-java Repository

The repository organizes big-data algorithmic solutions under docs/big-data/. These files provide the theoretical foundation for the Java implementations discussed above.

File Purpose Direct Link
docs/big-data/topk-problems-and-solutions.md Comprehensive guide to TopK algorithms including heap, quick-select, and bitmap methods. topk-problems-and-solutions.md
docs/big-data/find-no-repeat-number.md Techniques for identifying non-repeating numbers, useful for uniqueness validation in TopK. find-no-repeat-number.md
docs/big-data/find-rank-top-500-numbers.md Specific implementation for ranking large sets, demonstrating scaling TopK to larger K values. find-rank-top-500-numbers.md
docs/big-data/find-mid-value-in-500-millions.md Median-finding algorithms for massive data, closely related to selection problems. find-mid-value-in-500-millions.md

Summary

  • Min-heap with HashSet is the most robust solution for algorithmic challenges for large datasets in Java, providing O(N log K) time and O(K) space complexity.
  • The doocs/advanced-java repository documents this pattern in docs/big-data/topk-problems-and-solutions.md, emphasizing streaming compatibility.
  • For bounded integer ranges, consider bitmap approaches to reduce memory overhead.
  • Quick-select offers better average-case performance when the entire dataset fits in memory and random access is available.
  • In distributed environments, compute local TopK on each shard using heaps, then merge partial results.

Frequently Asked Questions

What is the time complexity of finding TopK unique integers using a min-heap?

The algorithm runs in O(N log K) time, where N is the total number of elements in the stream. Each insertion into the PriorityQueue costs O(log K), and we perform at most N insertions and N removals. This is significantly more efficient than sorting the entire dataset, which would require O(N log N) time.

How do I handle duplicate values when computing TopK in Java?

To ensure uniqueness, maintain a HashSet<Integer> alongside your min-heap. Before inserting a value into the PriorityQueue, check seen.add(value). If it returns true (indicating the value is new), proceed with heap insertion. When the heap exceeds size K and you remove the smallest element, also remove that element from the HashSet to prevent memory leaks and allow the value to be reprocessed if it appears again later in the stream.

When should I use a bitmap instead of a heap for large dataset problems?

Use a bitmap when your integer domain is bounded and dense (e.g., values range from 0 to 10⁹ with few gaps) and you need to check existence or rank rather than maintain a dynamic top-K. Bitmaps consume memory proportional to the range of values (one bit per possible integer), making them unsuitable for sparse 64-bit integers or unbounded streams. For TopK problems with arbitrary integer ranges, the heap approach remains superior.

Can the TopK algorithm be distributed across multiple machines?

Yes. In distributed environments, implement a hybrid approach: compute local TopK results on each shard or node using min-heaps (O(N/K log K) per shard), then transmit these partial results to a central reducer. The reducer merges the incoming streams by maintaining a final min-heap of size K. This approach scales horizontally because each node processes only its local partition, and the network transfer is limited to K elements per shard rather than the full dataset.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →