Prerequisites

Before attempting this problem, you should be comfortable with:

  • Queues - FIFO data structure for maintaining insertion order of elements
  • Hash Maps - Tracking element counts and uniqueness status in O(1) time
  • LinkedHashSet / OrderedDict - Combining hash map O(1) lookups with insertion order preservation

1. Brute Force

Intuition

The straightforward approach stores all numbers in a queue. When asked for the first unique number, we scan through the queue and count occurrences of each element. The first element with count 1 is our answer.

Algorithm

  1. Constructor: Store all initial numbers in a queue.
  2. add(value): Append the value to the queue.
  3. showFirstUnique(): Iterate through the queue. For each element, count how many times it appears in the entire queue. Return the first element with count equal to 1, or -1 if none exists.
class FirstUnique:
    def __init__(self, nums: List[int]):
        self._queue = deque(nums)

    def showFirstUnique(self):
        for item in self._queue:
            if self._queue.count(item) == 1:
                return item
        return -1

    def add(self, value):
        self._queue.append(value)

Time & Space Complexity

  • Time complexity:

    • constructor: O(K)O(K)

    • add(): O(1)O(1)

    • showFirstUnique(): O(N2)O(N^2)

  • Space complexity: O(N)O(N)

Where KK is the length of the initial array passed into the constructor and NN is the total number of items added into the queue so far (including those from the constructor).


2. Queue and HashMap of Unique-Status

Intuition

We can speed up uniqueness checks by maintaining a hash map that tracks whether each number is unique. The queue preserves insertion order. When showing the first unique, we pop non-unique elements from the front of the queue until we find a unique one or the queue is empty.

Algorithm

  1. Constructor: For each number in the initial array, call add().
  2. add(value):
    • If the value is new, mark it as unique in the hash map and add to the queue.
    • If it already exists, mark it as non-unique in the hash map.
  3. showFirstUnique():
    • Remove elements from the front of the queue while they are marked as non-unique.
    • Return the front element if the queue is non-empty, otherwise return -1.
class FirstUnique:
    
    def __init__(self, nums: List[int]):
        self._queue = deque(nums)
        self._is_unique = {}
        
        for num in nums:
            # Notice that we're calling the "add" method of FirstUnique; not of the queue. 
            self.add(num)
    

    def showFirstUnique(self) -> int:
        # We need to start by "cleaning" the queue of any non-uniques at the start.
        # Note that we know that if a value is in the queue, then it is also in
        # is_unique, as the implementation of add() guarantees this.
        while self._queue and not self._is_unique[self._queue[0]]:
            self._queue.popleft()
        
        # Check if there is still a value left in the queue. There might be no uniques.
        if self._queue:
            return self._queue[0]  # We don't want to actually *remove* the value.
        
        return -1
    

    def add(self, value: int) -> None:
        # Case 1: We need to add the number to the queue and mark it as unique. 
        if value not in self._is_unique:
            self._is_unique[value] = True
            self._queue.append(value)
        
        # Case 2 and 3: We need to mark the number as no longer unique.
        else:
            self._is_unique[value] = False

Time & Space Complexity

  • Time complexity:

    • constructor: O(K)O(K)

    • add(): O(1)O(1)

    • showFirstUnique(): O(1)O(1) (amortized)

  • Space complexity: O(N)O(N)

Where KK is the length of the initial array passed into the constructor and NN is the total number of items added into the queue so far (including those from the constructor).


3. LinkedHashSet for Queue, and HashMap of Unique-Statuses

Intuition

Instead of lazily removing non-unique elements during showFirstUnique(), we can eagerly remove them when they become non-unique. Using a LinkedHashSet (or OrderedDict) allows O(1) removal by value while preserving insertion order. This makes showFirstUnique() a true O(1) operation.

Algorithm

  1. Constructor: For each number in the initial array, call add().
  2. add(value):
    • If the value is new, mark it as unique in the hash map and add it to the set.
    • If seen once before (currently unique), mark it as non-unique and remove it from the set.
    • If already non-unique, do nothing.
  3. showFirstUnique(): Return the first element from the set if non-empty, otherwise return -1.
# In Python, we have to make do with the OrderedDict class. We can use it as a Set by setting
# the values to None.

class FirstUnique:

    def __init__(self, nums: List[int]):
        self._queue = OrderedDict()
        self._is_unique = {}

        for num in nums:
            # Notice that we're calling the "add" method of FirstUnique; not of the queue. 
            self.add(num)
        
    def showFirstUnique(self) -> int:
        # Check if there is still a value left in the queue. There might be no uniques.
        if self._queue:
            # We don't want to actually *remove* the value.
            # Seeing as OrderedDict has no "get first" method, the way that we can get
            # the first value is to create an iterator, and then get the "next" value
            # from that. Note that this is O(1).
            return next(iter(self._queue))
        
        return -1
        
    def add(self, value: int) -> None:
        # Case 1: We need to add the number to the queue and mark it as unique. 
        if value not in self._is_unique:
            self._is_unique[value] = True
            self._queue[value] = None

        # Case 2: We need to mark the value as no longer unique and then 
        # remove it from the queue.
        elif self._is_unique[value]:
            self._is_unique[value] = False
            self._queue.pop(value)

        # Case 3: We don't need to do anything; the number was removed from the queue
        # the second time it occurred.

Time & Space Complexity

  • Time complexity:

    • constructor: O(K)O(K)

    • add(): O(1)O(1)

    • showFirstUnique(): O(1)O(1)

  • Space complexity: O(N)O(N)

Where KK is the length of the initial array passed into the constructor and NN is the total number of items added into the queue so far (including those from the constructor).


Common Pitfalls

Adding Initial Numbers Twice to the Queue

In approaches that use both a queue and a hash map, calling add() for initial numbers while also directly pushing them to the queue results in duplicates. Either iterate through the initial array and call add() for each element (which handles both structures), or manually handle both structures without calling add(). Mixing both approaches corrupts the data structure.

Not Cleaning Stale Non-Unique Elements from the Queue

When lazily removing non-unique elements, the queue may contain stale entries that are no longer unique. Failing to pop these entries before returning the front element causes showFirstUnique() to return a non-unique number. Always check the hash map status and remove invalidated entries from the front before returning.

Modifying Uniqueness Status Incorrectly on Duplicate Adds

When a number is added for the third or subsequent time, its uniqueness status should remain false. A common bug is toggling the status back to true or re-adding it to the queue. The hash map should transition from "not seen" to "unique" to "non-unique" and never go back.