1429. First Unique Number 🔒
Problem Description
You need to design a data structure that maintains a queue of integers and can efficiently identify the first unique (non-duplicate) integer in the queue.
The FirstUnique
class should support three operations:
-
Constructor
FirstUnique(int[] nums)
: Initialize the data structure with an initial array of integers. These integers form the starting queue. -
Method
showFirstUnique()
: Return the value of the first unique integer in the queue. A unique integer is one that appears exactly once in the entire queue. If no unique integer exists, return-1
. The "first" unique integer refers to the one that appears earliest in the queue order. -
Method
add(int value)
: Add a new integer to the end of the queue. This may affect which integers are considered unique.
Key Points:
- The queue maintains insertion order - elements added first appear before elements added later
- An integer is unique if it appears exactly once in the entire queue
- When
showFirstUnique()
is called, you need to find the earliest-positioned integer that appears only once - Adding a duplicate of a previously unique integer makes it non-unique
- The data structure needs to efficiently track both the count of each integer and maintain the order of unique integers
Example Scenario:
- If the queue contains
[2, 3, 5, 3]
, the first unique integer is2
(appears once and is first among unique values) - After adding
2
to make it[2, 3, 5, 3, 2]
, the first unique integer becomes5
(since2
now appears twice) - If all integers appear more than once,
showFirstUnique()
returns-1
Intuition
To find the first unique integer efficiently, we need to track two things: how many times each integer appears, and the order of unique integers.
The naive approach would be to iterate through the entire queue each time showFirstUnique()
is called, counting occurrences and finding the first unique value. However, this would be inefficient with O(n)
time for each query.
The key insight is to separate the tracking of counts from the tracking of unique values. We can maintain:
- A frequency counter to know how many times each integer appears
- A separate collection that only stores unique integers in their order of appearance
When we think about maintaining order for unique integers, we need a data structure that preserves insertion order and allows efficient removal when an integer becomes non-unique. An OrderedDict
is perfect for this - it maintains insertion order and supports O(1)
removal.
Here's the clever part: when an integer's count changes, we only need to update the unique collection in two cases:
- When count goes from 0 to 1: The integer becomes unique, so add it to our ordered unique collection
- When count goes from 1 to 2: The integer is no longer unique, so remove it from our unique collection
By maintaining these two data structures, showFirstUnique()
becomes an O(1)
operation - we just return the first key in our ordered unique collection. The add()
operation is also O(1)
since we're only updating a counter and potentially adding/removing from the OrderedDict.
This approach trades space for time efficiency - we store additional data structures but gain constant-time operations for our queries.
Learn more about Queue and Data Stream patterns.
Solution Approach
The solution uses two main data structures to efficiently track unique integers:
- Counter (
self.cnt
): A hash map that stores the frequency count of each integer in the queue - OrderedDict (
self.unique
): An ordered dictionary that maintains only the unique integers in their order of appearance
Let's walk through the implementation:
Constructor: __init__(self, nums)
def __init__(self, nums: List[int]):
self.cnt = Counter(nums)
self.unique = OrderedDict({v: 1 for v in nums if self.cnt[v] == 1})
- First, create a
Counter
from the input array to get the frequency of each integer - Then, build an
OrderedDict
containing only the integers that appear exactly once - The OrderedDict comprehension iterates through
nums
(preserving original order) and includes only values whereself.cnt[v] == 1
- The value
1
in the OrderedDict is just a placeholder - we only care about the keys and their order
Query Method: showFirstUnique()
def showFirstUnique(self) -> int:
return -1 if not self.unique else next(v for v in self.unique.keys())
- If the
unique
OrderedDict is empty, return-1
- Otherwise, return the first key in the OrderedDict using
next()
on the keys iterator - This is
O(1)
since we're just accessing the first element
Update Method: add(value)
def add(self, value: int) -> None:
self.cnt[value] += 1
if self.cnt[value] == 1:
self.unique[value] = 1
elif value in self.unique:
self.unique.pop(value)
- First, increment the count of the value in our counter
- If the new count is 1, this value just became unique, so add it to
self.unique
- If the value already exists in
self.unique
(which means its previous count was 1 and now it's 2), remove it since it's no longer unique - We don't need to handle cases where count goes from 2+ to 3+ since those values aren't in
self.unique
anyway
Time Complexity:
__init__
:O(n)
where n is the length of the initial arrayshowFirstUnique()
:O(1)
add()
:O(1)
average case for hash map operations
Space Complexity: O(n)
where n is the total number of unique values seen, as we store each value in the counter and potentially in the OrderedDict.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through an example to see how the solution works:
Initial Setup: FirstUnique([7, 7, 7, 7, 7, 3, 3, 17])
-
Constructor execution:
Counter
creates:{7: 5, 3: 2, 17: 1}
- Build
unique
OrderedDict by iterating through[7, 7, 7, 7, 7, 3, 3, 17]
:- 7 appears 5 times → skip
- 3 appears 2 times → skip
- 17 appears 1 time → add to unique
- Result:
unique = OrderedDict({17: 1})
-
Call
showFirstUnique()
:unique
is not empty- Return first key:
17
-
Call
add(17)
:- Update counter:
cnt[17] = 2
- Since count changed from 1 to 2, and 17 is in
unique
:- Remove 17 from
unique
- Remove 17 from
- Result:
unique = OrderedDict({})
(empty)
- Update counter:
-
Call
showFirstUnique()
:unique
is empty- Return
-1
-
Call
add(2)
:- Update counter:
cnt[2] = 1
- Since count is 1 (new unique value):
- Add 2 to
unique
- Add 2 to
- Result:
unique = OrderedDict({2: 1})
- Update counter:
-
Call
add(9)
:- Update counter:
cnt[9] = 1
- Since count is 1 (new unique value):
- Add 9 to
unique
- Add 9 to
- Result:
unique = OrderedDict({2: 1, 9: 1})
- Update counter:
-
Call
showFirstUnique()
:unique
is not empty- Return first key:
2
(maintains insertion order)
-
Call
add(2)
:- Update counter:
cnt[2] = 2
- Since count changed from 1 to 2, and 2 is in
unique
:- Remove 2 from
unique
- Remove 2 from
- Result:
unique = OrderedDict({9: 1})
- Update counter:
-
Call
showFirstUnique()
:unique
is not empty- Return first key:
9
This walkthrough demonstrates how the OrderedDict maintains insertion order of unique values and efficiently updates when values transition between unique and non-unique states.
Solution Implementation
1from typing import List
2from collections import Counter, OrderedDict
3
4
5class FirstUnique:
6 def __init__(self, nums: List[int]):
7 """
8 Initialize the data structure with an initial list of numbers.
9
10 Args:
11 nums: Initial list of integers
12 """
13 # Count frequency of all numbers in the initial list
14 self.frequency_counter = Counter(nums)
15
16 # Maintain ordered dictionary of unique numbers (frequency == 1)
17 # OrderedDict preserves insertion order for first unique lookup
18 self.unique_numbers = OrderedDict(
19 {num: 1 for num in nums if self.frequency_counter[num] == 1}
20 )
21
22 def showFirstUnique(self) -> int:
23 """
24 Return the first unique number in the data structure.
25
26 Returns:
27 The first unique number, or -1 if no unique number exists
28 """
29 if not self.unique_numbers:
30 return -1
31
32 # Get the first key from the ordered dictionary
33 return next(iter(self.unique_numbers.keys()))
34
35 def add(self, value: int) -> None:
36 """
37 Add a number to the data structure.
38
39 Args:
40 value: Integer to add to the data structure
41 """
42 # Update frequency count
43 self.frequency_counter[value] += 1
44
45 # If this is the first occurrence, add to unique numbers
46 if self.frequency_counter[value] == 1:
47 self.unique_numbers[value] = 1
48 # If it was unique but now has duplicates, remove from unique numbers
49 elif value in self.unique_numbers:
50 self.unique_numbers.pop(value)
51
52
53# Your FirstUnique object will be instantiated and called as such:
54# obj = FirstUnique(nums)
55# param_1 = obj.showFirstUnique()
56# obj.add(value)
57
1class FirstUnique {
2 // Map to store the frequency count of each number
3 private Map<Integer, Integer> frequencyMap = new HashMap<>();
4 // LinkedHashSet to maintain unique numbers in insertion order
5 private Set<Integer> uniqueNumbers = new LinkedHashSet<>();
6
7 /**
8 * Constructor initializes the data structure with an array of numbers.
9 * First pass: Count frequency of each number
10 * Second pass: Add numbers with frequency 1 to the unique set
11 *
12 * @param nums Initial array of integers
13 */
14 public FirstUnique(int[] nums) {
15 // Count frequency of each number in the array
16 for (int num : nums) {
17 frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
18 }
19
20 // Add all unique numbers (frequency = 1) to the unique set
21 for (int num : nums) {
22 if (frequencyMap.get(num) == 1) {
23 uniqueNumbers.add(num);
24 }
25 }
26 }
27
28 /**
29 * Returns the first unique number from the data structure.
30 * If no unique number exists, returns -1.
31 *
32 * @return The first unique number or -1 if none exists
33 */
34 public int showFirstUnique() {
35 // Return -1 if no unique numbers exist, otherwise return the first element
36 return uniqueNumbers.isEmpty() ? -1 : uniqueNumbers.iterator().next();
37 }
38
39 /**
40 * Adds a new number to the data structure and updates uniqueness.
41 * If the number becomes unique (frequency = 1), add it to unique set.
42 * If the number is no longer unique (frequency > 1), remove it from unique set.
43 *
44 * @param value The integer value to add
45 */
46 public void add(int value) {
47 // Update frequency count for the value
48 frequencyMap.put(value, frequencyMap.getOrDefault(value, 0) + 1);
49
50 // Check if this is the first occurrence (now unique)
51 if (frequencyMap.get(value) == 1) {
52 uniqueNumbers.add(value);
53 } else {
54 // Value is no longer unique, remove from unique set
55 uniqueNumbers.remove(value);
56 }
57 }
58}
59
60/**
61 * Your FirstUnique object will be instantiated and called as such:
62 * FirstUnique obj = new FirstUnique(nums);
63 * int param_1 = obj.showFirstUnique();
64 * obj.add(value);
65 */
66
1class FirstUnique {
2public:
3 /**
4 * Constructor: Initialize the data structure with an array of integers
5 * @param nums: Initial array of integers to process
6 */
7 FirstUnique(vector<int>& nums) {
8 // Process each number in the initial array
9 for (int& num : nums) {
10 // Increment the frequency count for this number
11 frequency_map[num]++;
12 // Add the number to the queue to maintain insertion order
13 number_queue.push_back(num);
14 }
15 }
16
17 /**
18 * Return the first unique number in the current data structure
19 * @return: The first unique number, or -1 if no unique number exists
20 */
21 int showFirstUnique() {
22 // Remove all non-unique numbers from the front of the queue
23 while (!number_queue.empty() && frequency_map[number_queue.front()] != 1) {
24 number_queue.pop_front();
25 }
26
27 // Return the first unique number if it exists, otherwise return -1
28 return number_queue.empty() ? -1 : number_queue.front();
29 }
30
31 /**
32 * Add a new number to the data structure
33 * @param value: The integer value to add
34 */
35 void add(int value) {
36 // Increment the frequency count for this value
37 frequency_map[value]++;
38 // Add the value to the queue to maintain insertion order
39 number_queue.push_back(value);
40 }
41
42private:
43 // Hash map to store the frequency count of each number
44 unordered_map<int, int> frequency_map;
45
46 // Deque to maintain the order of numbers and efficiently remove from front
47 deque<int> number_queue;
48};
49
50/**
51 * Your FirstUnique object will be instantiated and called as such:
52 * FirstUnique* obj = new FirstUnique(nums);
53 * int param_1 = obj->showFirstUnique();
54 * obj->add(value);
55 */
56
1// Hash map to store the frequency count of each number
2let frequencyMap: Map<number, number>;
3
4// Deque to maintain the order of numbers and efficiently remove from front
5let numberQueue: number[];
6
7/**
8 * Constructor: Initialize the data structure with an array of integers
9 * @param nums - Initial array of integers to process
10 */
11function FirstUnique(nums: number[]): void {
12 // Initialize the data structures
13 frequencyMap = new Map<number, number>();
14 numberQueue = [];
15
16 // Process each number in the initial array
17 for (const num of nums) {
18 // Increment the frequency count for this number
19 frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
20 // Add the number to the queue to maintain insertion order
21 numberQueue.push(num);
22 }
23}
24
25/**
26 * Return the first unique number in the current data structure
27 * @returns The first unique number, or -1 if no unique number exists
28 */
29function showFirstUnique(): number {
30 // Remove all non-unique numbers from the front of the queue
31 while (numberQueue.length > 0 && frequencyMap.get(numberQueue[0]) !== 1) {
32 numberQueue.shift();
33 }
34
35 // Return the first unique number if it exists, otherwise return -1
36 return numberQueue.length === 0 ? -1 : numberQueue[0];
37}
38
39/**
40 * Add a new number to the data structure
41 * @param value - The integer value to add
42 */
43function add(value: number): void {
44 // Increment the frequency count for this value
45 frequencyMap.set(value, (frequencyMap.get(value) || 0) + 1);
46 // Add the value to the queue to maintain insertion order
47 numberQueue.push(value);
48}
49
50/**
51 * Your FirstUnique object will be instantiated and called as such:
52 * FirstUnique(nums);
53 * let param1 = showFirstUnique();
54 * add(value);
55 */
56
Time and Space Complexity
Time Complexity:
-
__init__(nums)
:O(n)
wheren
is the length of the input array. This involves iterating throughnums
twice - once to build the Counter (O(n)
) and once to build the OrderedDict comprehension (O(n)
). -
showFirstUnique()
:O(1)
in the average case when there are unique elements. Thenext()
function retrieves the first key from the OrderedDict which is a constant time operation. In the worst case where the OrderedDict is empty, checking emptiness is alsoO(1)
. -
add(value)
:O(1)
on average. Incrementing the counter isO(1)
, checking if count equals 1 isO(1)
, adding to OrderedDict isO(1)
, checking membership in OrderedDict isO(1)
, and popping from OrderedDict isO(1)
on average.
Space Complexity:
O(n)
wheren
is the total number of distinct elements that have been added to the data structure (including initialization).- The
cnt
Counter stores all distinct values and their frequencies, takingO(m)
space wherem
is the number of distinct elements. - The
unique
OrderedDict stores at most all distinct elements that appear exactly once, which in the worst case isO(m)
space. - Overall space complexity is
O(m)
wherem ≤ n
, simplified toO(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Using a Regular Dictionary Instead of OrderedDict
The Problem:
A common mistake is using a regular dict
instead of OrderedDict
for tracking unique numbers. While Python 3.7+ guarantees insertion order for regular dictionaries, this isn't explicit and can lead to confusion or issues in older Python versions.
# Problematic approach
class FirstUnique:
def __init__(self, nums: List[int]):
self.cnt = Counter(nums)
# Regular dict might not preserve order in older Python versions
self.unique = {v: 1 for v in nums if self.cnt[v] == 1}
The Solution:
Always use OrderedDict
to make the ordering guarantee explicit and ensure compatibility:
from collections import OrderedDict self.unique = OrderedDict({v: 1 for v in nums if self.cnt[v] == 1})
Pitfall 2: Incorrect Order Preservation in Constructor
The Problem: When building the unique dictionary in the constructor, iterating over the Counter's keys instead of the original array loses the original order:
# Wrong - iterates over counter keys which don't preserve original order self.unique = OrderedDict() for num, count in self.cnt.items(): if count == 1: self.unique[num] = 1
This would give incorrect results. For example, with nums = [7, 7, 7, 7, 7, 7, 7, 7, 3, 3, 3, 1]
, the counter's iteration order might place 1
before 3
, even though 3
appears first in the original array.
The Solution:
Always iterate through the original nums
array to preserve insertion order:
self.unique = OrderedDict({v: 1 for v in nums if self.cnt[v] == 1})
Pitfall 3: Inefficient showFirstUnique()
Implementation
The Problem: Converting the entire OrderedDict to a list just to get the first element is wasteful:
def showFirstUnique(self) -> int:
# Inefficient - creates entire list just for first element
if not self.unique:
return -1
return list(self.unique.keys())[0]
The Solution: Use an iterator to get just the first element without creating a full list:
def showFirstUnique(self) -> int:
if not self.unique:
return -1
return next(iter(self.unique.keys()))
Pitfall 4: Forgetting to Handle the Transition from Unique to Non-Unique
The Problem: A critical bug occurs if you forget to remove a number from the unique dictionary when it becomes non-unique:
def add(self, value: int) -> None:
self.cnt[value] += 1
if self.cnt[value] == 1:
self.unique[value] = 1
# Bug: Forgot to remove value when it's no longer unique!
This would incorrectly keep numbers in the unique dictionary even after they've been added multiple times.
The Solution: Always check if a value needs to be removed from the unique dictionary:
def add(self, value: int) -> None:
self.cnt[value] += 1
if self.cnt[value] == 1:
self.unique[value] = 1
elif value in self.unique: # Critical: remove when no longer unique
self.unique.pop(value)
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
Coding Interview 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
Want a Structured Path to Master System Design Too? Don’t Miss This!