Facebook Pixel

1481. Least Number of Unique Integers after K Removals

MediumGreedyArrayHash TableCountingSorting
Leetcode Link

Problem Description

You are given an array of integers arr and an integer k. Your task is to remove exactly k elements from the array such that the number of unique integers remaining in the array is minimized. Return the least number of unique integers after removing exactly k elements.

For example, if arr = [5, 5, 4] and k = 1, you can remove one element. If you remove one 5, you'll have [5, 4] with 2 unique integers. If you remove the 4, you'll have [5, 5] with only 1 unique integer. So the answer would be 1.

The strategy is to remove integers that appear less frequently first, as this will reduce the number of unique integers more efficiently. By removing all occurrences of integers with smaller frequencies first, you can minimize the total number of unique integers remaining.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To minimize the number of unique integers after removing k elements, we need to think about which elements to remove first. The key insight is that we should prioritize removing integers that appear less frequently in the array.

Why? Consider this: if an integer appears only once, removing that single element eliminates one unique integer entirely. If an integer appears 5 times, we'd need to remove all 5 occurrences to eliminate that unique integer. So with limited removals (k), it's more efficient to target integers with lower frequencies.

The greedy approach works here: sort the integers by their frequency counts in ascending order, then remove them starting from the least frequent ones. For each unique integer, we remove all its occurrences. We keep removing until we've used up our k removals.

For example, if we have frequencies [1, 1, 2, 3, 4] and k = 4, we can remove the integers with frequencies 1 and 1 (using 2 removals), then the integer with frequency 2 (using 2 more removals, totaling 4). This eliminates 3 unique integers, leaving us with 2 unique integers remaining.

By counting frequencies first, sorting them, and then greedily removing from smallest to largest frequency, we ensure we're eliminating the maximum number of unique integers possible with our k removals.

Learn more about Greedy and Sorting patterns.

Solution Approach

The implementation follows these steps:

  1. Count frequencies: Use a hash table (Counter in Python) to count how many times each integer appears in the array. This gives us a mapping from each unique integer to its frequency.

  2. Sort frequencies: Extract just the frequency values from the hash table and sort them in ascending order. We don't need to track which integer has which frequency - we only care about the frequency values themselves.

  3. Greedy removal: Iterate through the sorted frequencies from smallest to largest. For each frequency value v:

    • Subtract v from k (representing the removal of all occurrences of that integer)
    • If k becomes negative, it means we couldn't fully remove this integer with our remaining removals. The number of unique integers left is the total number of unique integers minus the number we've successfully removed (which is the current index i)
    • If k is still non-negative, continue to the next frequency
  4. Return result: If we successfully process all frequencies without k going negative, it means we've removed all unique integers, so return 0.

The algorithm works because:

  • By processing frequencies in ascending order, we maximize the number of unique integers we can completely remove
  • We keep track of our progress using the index i in the sorted frequency list
  • When k goes negative, we know exactly how many unique integers we've eliminated (i) and can calculate how many remain (len(cnt) - i)

Time complexity: O(n log n) for sorting the frequencies, where n is the number of unique integers. Space complexity: O(n) for the hash table storing the frequencies.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with arr = [4, 3, 1, 1, 3, 3, 2] and k = 3.

Step 1: Count frequencies

  • Integer 1 appears 2 times
  • Integer 2 appears 1 time
  • Integer 3 appears 3 times
  • Integer 4 appears 1 time

So our frequency map is: {1: 2, 2: 1, 3: 3, 4: 1}

Step 2: Sort frequencies Extract the frequency values: [2, 1, 3, 1] Sort them in ascending order: [1, 1, 2, 3]

Step 3: Greedy removal We have k = 3 removals available. Process frequencies from smallest to largest:

  • Index 0: frequency = 1

    • Remove this integer completely (uses 1 removal)
    • k = 3 - 1 = 2 (still positive, continue)
  • Index 1: frequency = 1

    • Remove this integer completely (uses 1 removal)
    • k = 2 - 1 = 1 (still positive, continue)
  • Index 2: frequency = 2

    • Try to remove this integer completely (would use 2 removals)
    • k = 1 - 2 = -1 (negative! Can't fully remove this integer)
    • Stop here

Step 4: Calculate result

  • Total unique integers originally: 4
  • Successfully removed integers: 2 (at indices 0 and 1)
  • Remaining unique integers: 4 - 2 = 2

The answer is 2. We removed the two integers that appeared once (integers 2 and 4), leaving us with integers 1 and 3 in the array.

Solution Implementation

1from typing import List
2from collections import Counter
3
4class Solution:
5    def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
6        # Count frequency of each element in the array
7        frequency_map = Counter(arr)
8      
9        # Sort frequencies in ascending order to remove least frequent elements first
10        sorted_frequencies = sorted(frequency_map.values())
11      
12        # Iterate through sorted frequencies
13        for index, frequency in enumerate(sorted_frequencies):
14            # Subtract current frequency from k (simulate removing all occurrences)
15            k -= frequency
16          
17            # If k becomes negative, we can't remove this element completely
18            if k < 0:
19                # Return remaining unique integers
20                # (total unique - number of completely removed elements)
21                return len(frequency_map) - index
22      
23        # If we removed all elements (k >= sum of all frequencies)
24        return 0
25
1class Solution {
2    public int findLeastNumOfUniqueInts(int[] arr, int k) {
3        // Create a frequency map to count occurrences of each element
4        Map<Integer, Integer> frequencyMap = new HashMap<>();
5      
6        // Count the frequency of each element in the array
7        for (int element : arr) {
8            frequencyMap.merge(element, 1, Integer::sum);
9        }
10      
11        // Extract all frequency values into a list
12        List<Integer> frequencies = new ArrayList<>(frequencyMap.values());
13      
14        // Sort frequencies in ascending order to remove elements with lowest frequencies first
15        Collections.sort(frequencies);
16      
17        // Iterate through sorted frequencies and remove elements
18        int totalUniqueElements = frequencies.size();
19        for (int i = 0; i < totalUniqueElements; i++) {
20            // Subtract current frequency from k (remove all occurrences of current element)
21            k -= frequencies.get(i);
22          
23            // If k becomes negative, we can't remove the current element completely
24            // Return the number of remaining unique elements
25            if (k < 0) {
26                return totalUniqueElements - i;
27            }
28        }
29      
30        // All unique elements were removed, return 0
31        return 0;
32    }
33}
34
1class Solution {
2public:
3    int findLeastNumOfUniqueInts(vector<int>& arr, int k) {
4        // Count frequency of each element in the array
5        unordered_map<int, int> frequencyMap;
6        for (int& element : arr) {
7            ++frequencyMap[element];
8        }
9      
10        // Extract all frequencies into a vector
11        vector<int> frequencies;
12        for (auto& [value, frequency] : frequencyMap) {
13            frequencies.push_back(frequency);
14        }
15      
16        // Sort frequencies in ascending order
17        // Strategy: Remove elements with lowest frequency first to maximize unique elements removed
18        sort(frequencies.begin(), frequencies.end());
19      
20        // Greedily remove elements starting from lowest frequency
21        int uniqueCount = frequencies.size();
22        for (int i = 0; i < uniqueCount; ++i) {
23            k -= frequencies[i];
24          
25            // If we can't remove all occurrences of current element
26            // Return remaining unique elements count
27            if (k < 0) {
28                return uniqueCount - i;
29            }
30        }
31      
32        // All unique elements were removed
33        return 0;
34    }
35};
36
1/**
2 * Finds the least number of unique integers after removing exactly k elements
3 * @param arr - The input array of integers
4 * @param k - The number of elements to remove
5 * @returns The minimum number of unique integers remaining
6 */
7function findLeastNumOfUniqueInts(arr: number[], k: number): number {
8    // Create a frequency map to count occurrences of each unique integer
9    const frequencyMap: Map<number, number> = new Map<number, number>();
10  
11    // Count the frequency of each integer in the array
12    for (const num of arr) {
13        frequencyMap.set(num, (frequencyMap.get(num) ?? 0) + 1);
14    }
15
16    // Extract all frequencies and sort them in ascending order
17    // Strategy: Remove integers with lowest frequencies first to minimize unique count
18    const frequencies: number[] = [...frequencyMap.values()].sort((a, b) => a - b);
19
20    // Iterate through sorted frequencies and remove integers starting from lowest frequency
21    for (let i = 0; i < frequencies.length; i++) {
22        // Subtract current frequency from remaining removals
23        k -= frequencies[i];
24      
25        // If we can't completely remove this integer, return remaining unique count
26        if (k < 0) {
27            return frequencies.length - i;
28        }
29    }
30  
31    // All unique integers were removed
32    return 0;
33}
34

Time and Space Complexity

The time complexity is O(n log n), where n is the length of the array arr. This is because:

  • Creating the Counter takes O(n) time to count all elements
  • Getting the values from Counter takes O(m) time where m is the number of unique elements (at most n)
  • Sorting the frequency values takes O(m log m) time, which is at most O(n log n)
  • The enumeration loop iterates through at most m values, taking O(m) time
  • Overall: O(n) + O(m log m) + O(m) = O(n log n) since m โ‰ค n

The space complexity is O(n), where n is the length of the array arr. This is because:

  • The Counter dictionary stores at most n key-value pairs (when all elements are unique)
  • The sorted list of values creates a new list of size at most n
  • Overall: O(n) + O(n) = O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Attempting to Track Which Elements to Remove

A common mistake is trying to maintain which specific elements are being removed from the array, rather than just working with frequencies. This leads to unnecessary complexity.

Incorrect approach:

def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
    frequency_map = Counter(arr)
    # Trying to sort by value AND track the keys
    sorted_items = sorted(frequency_map.items(), key=lambda x: x[1])
  
    removed_elements = set()
    for value, freq in sorted_items:
        if k >= freq:
            k -= freq
            removed_elements.add(value)  # Unnecessary tracking
        else:
            break
  
    return len(frequency_map) - len(removed_elements)

Why it's problematic: We don't need to know which specific integers we're removing - we only need to count how many unique integers remain.

Pitfall 2: Off-by-One Error in Counting Remaining Elements

Another frequent error occurs when k becomes negative, leading to incorrect calculation of remaining unique integers.

Incorrect approach:

def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
    frequency_map = Counter(arr)
    sorted_frequencies = sorted(frequency_map.values())
  
    for index, frequency in enumerate(sorted_frequencies):
        k -= frequency
        if k < 0:
            # Wrong! This counts the current element as removed
            return len(frequency_map) - index - 1
  
    return 0

Why it's problematic: When k goes negative, it means we couldn't fully remove the element at the current index. The number of completely removed elements is exactly index, not index + 1.

Pitfall 3: Not Handling Edge Cases Properly

Failing to consider when k equals 0 or when k is greater than or equal to the array length.

Incorrect approach:

def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
    if k == 0:  # Unnecessary special case
        return len(set(arr))
  
    frequency_map = Counter(arr)
    sorted_frequencies = sorted(frequency_map.values())
  
    # Missing check for k >= len(arr)
    for index, frequency in enumerate(sorted_frequencies):
        k -= frequency
        if k < 0:
            return len(frequency_map) - index
  
    # May not reach here if logic is wrong

Solution: The original code handles these cases naturally without special checks. When k = 0, the first frequency subtraction will make k negative, returning the full count. When k >= len(arr), all frequencies will be processed and the function returns 0.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More