Facebook Pixel

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:

  1. Constructor FirstUnique(int[] nums): Initialize the data structure with an initial array of integers. These integers form the starting queue.

  2. 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.

  3. 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 is 2 (appears once and is first among unique values)
  • After adding 2 to make it [2, 3, 5, 3, 2], the first unique integer becomes 5 (since 2 now appears twice)
  • If all integers appear more than once, showFirstUnique() returns -1
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. A frequency counter to know how many times each integer appears
  2. 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:

  1. Counter (self.cnt): A hash map that stores the frequency count of each integer in the queue
  2. 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 where self.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 array
  • showFirstUnique(): 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 Evaluator

Example Walkthrough

Let's trace through an example to see how the solution works:

Initial Setup: FirstUnique([7, 7, 7, 7, 7, 3, 3, 17])

  1. 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})
  2. Call showFirstUnique():

    • unique is not empty
    • Return first key: 17
  3. Call add(17):

    • Update counter: cnt[17] = 2
    • Since count changed from 1 to 2, and 17 is in unique:
      • Remove 17 from unique
    • Result: unique = OrderedDict({}) (empty)
  4. Call showFirstUnique():

    • unique is empty
    • Return -1
  5. Call add(2):

    • Update counter: cnt[2] = 1
    • Since count is 1 (new unique value):
      • Add 2 to unique
    • Result: unique = OrderedDict({2: 1})
  6. Call add(9):

    • Update counter: cnt[9] = 1
    • Since count is 1 (new unique value):
      • Add 9 to unique
    • Result: unique = OrderedDict({2: 1, 9: 1})
  7. Call showFirstUnique():

    • unique is not empty
    • Return first key: 2 (maintains insertion order)
  8. Call add(2):

    • Update counter: cnt[2] = 2
    • Since count changed from 1 to 2, and 2 is in unique:
      • Remove 2 from unique
    • Result: unique = OrderedDict({9: 1})
  9. 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) where n is the length of the input array. This involves iterating through nums 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. The next() 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 also O(1).

  • add(value): O(1) on average. Incrementing the counter is O(1), checking if count equals 1 is O(1), adding to OrderedDict is O(1), checking membership in OrderedDict is O(1), and popping from OrderedDict is O(1) on average.

Space Complexity:

  • O(n) where n 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, taking O(m) space where m is the number of distinct elements.
  • The unique OrderedDict stores at most all distinct elements that appear exactly once, which in the worst case is O(m) space.
  • Overall space complexity is O(m) where m ≤ n, simplified to O(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)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More