1481. Least Number of Unique Integers after K Removals
Problem Description
Given an array arr
of integers and an integer k
, the task is to determine the minimum number of unique integers that remain in the array after precisely k
elements have been removed. The key to solving this problem lies in effectively selecting which elements to remove to achieve the least number of unique integers possible.
Intuition
To solve this problem, we should consider removing elements that appear more frequently first since this will not reduce the unique count as quickly. Ideally, we want to remove the numbers that occur the least amount of times last. We can apply a strategy that consists of the following steps:
- Count the frequency of each unique integer in the array using a hash table (or
Counter
in Python). - Sort these unique integers by their frequency in ascending order. This way, the elements that appear less frequently are at the beginning of the sorted list.
- Traverse the sorted list, and with each step, decrease
k
by the frequency of the current element. This simulates removing that element's occurrences from the array. - If
k
becomes negative, it means we can't remove all occurrences of the current element without exceeding the allowedk
removals. Therefore, the current count of unique integers minus the number of elements we've been able to fully remove by this point gives us the answer. - If we go through the entire list without
k
becoming negative, it means we managed to remove all occurrences of certain elements, and we end up with 0 unique integers.
Having this strategy in place allows us to reach the solution in an efficient manner.
Solution Approach
The implementation of the solution follows a straightforward approach outlined in previous steps, which uses common data structures and algorithms:
-
Hash Table (Counter): We employ a hash table which is native to Python called
Counter
from thecollections
module. This structure automatically counts the frequency of each unique integer, which is essential to our strategy. It simplifies the process of determining how many times each integer appears in the array.cnt = Counter(arr)
-
Sorting: After counting the occurrences of each integer, we sort these counts. This sorting is ascending, meaning that unique integers with fewer occurrences are considered first when we start removing elements.
sorted(cnt.values())
-
Traversal and Subtraction: With the sorted frequencies, we traverse the list. For each frequency value
v
, we subtractk
byv
, simulating the removal ofv
occurrences from the array.for i, v in enumerate(sorted(cnt.values())): k -= v ...
-
If
k
becomes less than zero during the iteration, it indicates that we cannot remove all occurrences of the current element as it would exceedk
. Hence, the minimal number of unique integers is obtained by subtracting the current indexi
from the total number of unique integers (the length ofcnt
):if k < 0: return len(cnt) - i
-
-
Return Result: If we are able to traverse the entire sorted list of frequencies without
k
becoming negative, we have managed to remove all occurrences of certain elements leading to zero unique integers left.return 0
The overall complexity of the solution is determined mainly by the sorting operation and the counting operation, with the rest being linear traversals and basic arithmetic operations.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's use a small example to illustrate the solution approach:
Suppose arr = [4, 3, 1, 1, 2, 3, 3]
and k = 3
. Our goal is to remove k
elements from arr
in such a way that the number of unique integers remaining is minimal.
-
Count Frequency: We first count how many times each integer appears in the array:
- 1 appears 2 times
- 2 appears 1 time
- 3 appears 3 times
- 4 appears 1 time
Using a hash table:
cnt = {1: 2, 2: 1, 3: 3, 4: 1}
-
Sort by Frequency: We sort these counts in ascending order based on frequency:
Sorted counts:
[1, 1, 2, 3]
The number of unique integers is initially 4.
-
Traverse and Remove: We then traverse the sorted list and remove
k
elements.- We start with the first element (frequency of
1
). We remove one instance of the integer with a count of1
, which decreasesk
to2
. - We move to the next element (another frequency of
1
). We remove one instance of another integer with a count of1
, which decreasesk
to1
. - With the third element (frequency of
2
), we can remove both instances of the integer1
, which would decreasek
to-1
.
However, since we can't go negative, it means we can't remove all instances of
1
. At this point, we have removed two unique integers (those with the initial frequency of1
), resulting in a remaining unique integer count of2
(those with frequencies of2
and3
are still present).Here's the traversal in action:
cnt = {1: 2, 2: 1, 3: 3, 4: 1} sorted_counts = [1, 1, 2, 3] k = 3 for i, v in enumerate(sorted_counts): k -= v if k < 0: return 4 - i # number of unique integers initially minus the index
- We start with the first element (frequency of
-
Return Result: With the loop broken, we know we've removed instances of unique integers only until the array's
k
becomes negative. This means we return4 - 2
, which equals2
.
Therefore, the minimum number of unique integers remaining in the array arr
after removing exactly k
elements is 2
.
Solution Implementation
1from collections import Counter
2from typing import List
3
4class Solution:
5 def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
6 # Create a counter for all elements in the array
7 counter = Counter(arr)
8
9 # Sort the counts of each unique integer
10 sorted_counts = sorted(counter.values())
11
12 # Go through the counts starting from the smallest
13 for index, value in enumerate(sorted_counts):
14 # Reduce the count of deletable elements by the current count value
15 k -= value
16
17 # If k becomes negative, we can't delete anymore unique integers
18 if k < 0:
19 # Return the count of remaining unique integers
20 return len(counter) - index
21
22 # If k is not negative after trying to remove all, return 0
23 # because all elements can be removed to achieve k deletion
24 return 0
25
1class Solution {
2 public int findLeastNumOfUniqueInts(int[] arr, int k) {
3 // Create a hashmap to store the frequency of each integer in the array
4 Map<Integer, Integer> frequencyMap = new HashMap<>();
5 // Populate the frequency map
6 for (int num : arr) {
7 frequencyMap.merge(num, 1, Integer::sum); // Increment the count for each occurrence of a number
8 }
9 // Create a list to store the frequencies only
10 List<Integer> frequencies = new ArrayList<>(frequencyMap.values());
11 // Sort the frequencies in ascending order
12 Collections.sort(frequencies);
13 // Iterate over the list of frequencies
14 for (int i = 0, totalUniqueNumbers = frequencies.size(); i < totalUniqueNumbers; ++i) {
15 k -= frequencies.get(i); // Subtract the frequency from 'k'
16 if (k < 0) {
17 // If 'k' becomes negative, the current frequency can't be fully removed
18 // so return the number of remaining unique integers
19 return totalUniqueNumbers - i;
20 }
21 }
22 // If all frequencies have been removed with 'k' operations, return 0 as there are no unique integers left
23 return 0;
24 }
25}
26
1#include <vector>
2#include <unordered_map>
3#include <algorithm>
4
5class Solution {
6public:
7 int findLeastNumOfUniqueInts(vector<int>& arr, int k) {
8 // Create a hashmap to count the occurrence of each integer in the array
9 unordered_map<int, int> frequencyMap;
10 for (int number : arr) {
11 ++frequencyMap[number];
12 }
13
14 // Extract the frequencies and sort them in ascending order
15 vector<int> frequencies;
16 for (auto& [number, count] : frequencyMap) {
17 frequencies.push_back(count);
18 }
19 sort(frequencies.begin(), frequencies.end());
20
21 // Determine the least number of unique integers by removing k occurrences
22 int uniqueIntegers = frequencies.size(); // start with all unique integers
23 for (int i = 0; i < frequencies.size(); ++i) {
24 // Subtract the frequency of the current number from k
25 k -= frequencies[i];
26
27 // If k becomes negative, we can't remove any more numbers
28 if (k < 0) {
29 return uniqueIntegers - i; // Return the remaining number of unique integers
30 }
31 }
32
33 // If k is non-negative after all removals, we've removed all duplicates
34 return 0;
35 }
36};
37
1function findLeastNumOfUniqueInts(arr: number[], k: number): number {
2 // Create a map to hold the frequency of each integer in the array
3 const frequencyMap: Map<number, number> = new Map();
4 // Iterate over the array and populate the frequency map
5 for (const number of arr) {
6 frequencyMap.set(number, (frequencyMap.get(number) || 0) + 1);
7 }
8
9 // Extract the frequency values from the map and store them in an array
10 const frequencies: number[] = [];
11 for (const frequency of frequencyMap.values()) {
12 frequencies.push(frequency);
13 }
14
15 // Sort the frequencies array in ascending order
16 frequencies.sort((a, b) => a - b);
17
18 // Iterate over the sorted frequencies
19 for (let i = 0; i < frequencies.length; ++i) {
20 // Decrement k by the current frequency
21 k -= frequencies[i];
22 // If k becomes negative, we've used up k removals, so we return
23 // the number of unique integers left, which is the length of the
24 // frequencies array minus the current index
25 if (k < 0) {
26 return frequencies.length - i;
27 }
28 }
29
30 // If we've processed all frequencies and haven't used up k removals,
31 // all integers have been removed and 0 unique integers are left
32 return 0;
33}
34
Time and Space Complexity
The time complexity of the given code is O(n log n)
. This complexity arises from the sorting operation sorted(cnt.values())
where cnt.values()
represents the counts of unique integers in the array arr
; sorting these counts requires O(n log n)
time since sorting is typically done using comparison-based algorithms like quicksort or mergesort which have O(n log n)
complexity in the average and worst case.
The space complexity of the code is O(n)
because we are storing counts of the elements in the array in a dictionary cnt
. In the worst case, if all elements are unique, it will contain n
key-value pairs which relate directly to the size of the input array arr
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Want a Structured Path to Master System Design Too? Don’t Miss This!