Smallest Number in Infinite Set
MedPrompt
You have a set which contains all positive integers [1, 2, 3, 4, 5, ...].
Implement the SmallestInfiniteSet class:
SmallestInfiniteSet()Initializes the SmallestInfiniteSet object to contain all positive integers.int popSmallest()Removes and returns the smallest integer contained in the infinite set.void addBack(int num)Adds a positive integernumback into the infinite set, if it is not already in the infinite set.
Example 1:
Input
["SmallestInfiniteSet", "addBack", "popSmallest", "popSmallest", "popSmallest", "addBack", "popSmallest", "popSmallest", "popSmallest"]
[[], [2], [], [], [], [1], [], [], []]
Output
[null, null, 1, 2, 3, null, 1, 4, 5]
Explanation
SmallestInfiniteSet smallestInfiniteSet = new SmallestInfiniteSet();
smallestInfiniteSet.addBack(2); // 2 is already in the set, so no change is made.
smallestInfiniteSet.popSmallest(); // return 1, since 1 is the smallest number, and remove it from the set.
smallestInfiniteSet.popSmallest(); // return 2, and remove it from the set.
smallestInfiniteSet.popSmallest(); // return 3, and remove it from the set.
smallestInfiniteSet.addBack(1); // 1 is added back to the set.
smallestInfiniteSet.popSmallest(); // return 1, since 1 was added back to the set and
// is the smallest number, and remove it from the set.
smallestInfiniteSet.popSmallest(); // return 4, and remove it from the set.
smallestInfiniteSet.popSmallest(); // return 5, and remove it from the set.
Constraints:
1 <= num <= 1000- At most
1000calls will be made in total topopSmallestandaddBack.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach models the problem by managing two groups of numbers: a contiguous block of integers starting from a point that have never been touched, and a separate collection of smaller integers that were popped and then added back. We use an integer variable, currentInteger, to track the start of the infinite sequence (initially 1). A Min-Heap is used to store the numbers added back, allowing efficient retrieval of the smallest among them. When popSmallest is called, we return the minimum of currentInteger and the smallest element in the heap. To efficiently prevent adding duplicate numbers back into the heap, an auxiliary HashSet is used.
Algorithm
- Initialize an integer
currentIntegerto1to track the smallest integer in the conceptual infinite sequence. - Use a Min-Heap (like
PriorityQueuein Java) to store any numbers that are added back after being popped. - To handle duplicates efficiently, use a
HashSetto keep track of the numbers currently present in the Min-Heap. - For
popSmallest():- Compare the smallest element in the heap (
minHeap.peek()) withcurrentInteger. - If the heap is not empty and its top element is smaller than
currentInteger, it is the smallest overall. Pop it from the heap and remove it from the HashSet. - Otherwise,
currentIntegeris the smallest. Return it and increment it for the next call.
- Compare the smallest element in the heap (
- For
addBack(num):- A number can only be added back if it was previously popped. This means
nummust be smaller thancurrentInteger. - Also, it must not already be in the heap (check using the HashSet).
- If both conditions are met, add
numto both the heap and the HashSet.
- A number can only be added back if it was previously popped. This means
Walkthrough
This implementation uses a counter currentInteger for the main sequence of numbers, a PriorityQueue as a min-heap for numbers added back, and a HashSet for quick lookups to avoid duplicates in the heap.
import java.util.PriorityQueue;import java.util.HashSet; class SmallestInfiniteSet { private int currentInteger; private PriorityQueue<Integer> minHeap; private HashSet<Integer> isAddedBack; public SmallestInfiniteSet() { currentInteger = 1; minHeap = new PriorityQueue<>(); isAddedBack = new HashSet<>(); } public int popSmallest() { int result; // If the heap has a smaller number than the next in the sequence, pop from the heap. if (!minHeap.isEmpty() && minHeap.peek() < currentInteger) { result = minHeap.poll(); isAddedBack.remove(result); } else { // Otherwise, take the next integer from the main sequence. result = currentInteger; currentInteger++; } return result; } public void addBack(int num) { // Only add back if the number is smaller than our current sequence pointer // and is not already in the heap. if (num < currentInteger && !isAddedBack.contains(num)) { minHeap.add(num); isAddedBack.add(num); } }}Complexity
Time
`popSmallest()`: O(log K), where K is the number of elements in the heap. `addBack()`: O(log K). The `contains` check on the HashSet is O(1) on average, and adding to the heap is O(log K).
Space
O(K), where K is the maximum number of items that are added back. This space is used by the Min-Heap and the HashSet.
Trade-offs
Pros
It's a general solution that works even if the numbers or the number of calls were much larger, as its space complexity depends on the number of
addBackcalls, not the magnitude of the numbers.It correctly models the "infinite" nature of the set without relying on a fixed-size array.
The logarithmic time complexity is very efficient for general cases.
Cons
Slightly more complex to implement than a simple array-based approach.
For the given constraints, it might be slightly slower than an optimized array-based solution due to the overhead of heap operations (O(log K) vs O(1) amortized).
Given the problem's constraints (num <= 1000, at most 1000 calls), the numbers involved will stay within a limited range. This allows for a highly optimized approach using a simple boolean array to track the presence of each number. We can pre-allocate an array (e.g., of size 2002) where isPresent[i] tells us if number i is in the set. A pointer, smallestPtr, is maintained to keep track of the smallest known available integer, which makes the search in popSmallest very efficient on average.
Algorithm
- Initialize a boolean array,
isPresent, of a size large enough to cover all possible numbers (e.g., 2002, based on problem constraints). Mark all entries from 1 upwards astrue. - Initialize an integer pointer,
smallestPtr, to1. - For
popSmallest():- Use a
whileloop to advancesmallestPtruntil it points to an indexiwhereisPresent[i]istrue. - This
smallestPtris the smallest number. MarkisPresent[smallestPtr]asfalse. - Return the value of
smallestPtr.
- Use a
- For
addBack(num):- Mark
isPresent[num]astrue. - As an optimization, if
numis smaller thansmallestPtr, updatesmallestPtr = num.
- Mark
Walkthrough
This approach leverages the problem's constraints to use a fixed-size boolean array for O(1) lookups and updates. A pointer smallestPtr is used to efficiently find the next smallest element.
class SmallestInfiniteSet { private boolean[] isPresent; private int smallestPtr; public SmallestInfiniteSet() { // Constraints: num <= 1000, 1000 calls. // Max number popped could be 1000. Max num added back is 1000. // A size of 2002 is safe to handle all possible numbers. isPresent = new boolean[2002]; for (int i = 1; i < isPresent.length; i++) { isPresent[i] = true; } smallestPtr = 1; } public int popSmallest() { // Find the first available number starting from smallestPtr. // This loop's total work is amortized across all calls. while (!isPresent[smallestPtr]) { smallestPtr++; } int result = smallestPtr; isPresent[result] = false; return result; } public void addBack(int num) { if (!isPresent[num]) { isPresent[num] = true; // Optimization: if we add back a number smaller than our current pointer, // it becomes the new candidate for the smallest. if (num < smallestPtr) { smallestPtr = num; } } }}Complexity
Time
`popSmallest()`: Amortized O(1). The `smallestPtr` only moves forward. Across all calls, the total work for the search loop is bounded by the maximum number reached, making the amortized cost constant. `addBack()`: O(1).
Space
O(M), where M is the maximum possible value of a number we need to track (e.g., 2002 in this implementation). The space is constant with respect to the number of calls.
Trade-offs
Pros
Extremely fast with O(1) amortized time for
popSmallestand O(1) foraddBack.Very simple to understand and implement.
Cons
The solution is not general. It relies on the constraints on the input values (
num) and the number of calls to pre-allocate a fixed-size array.If
numcould be very large (e.g., 10^9), this approach would be infeasible due to memory limitations.The space complexity is proportional to the maximum value of
num, not the number of operations.
Solutions
Solution
class SmallestInfiniteSet { private TreeSet < Integer > s = new TreeSet <>(); public SmallestInfiniteSet () { for ( int i = 1 ; i <= 1000 ; ++ i ) { s . add ( i ); } } public int popSmallest () { return s . pollFirst (); } public void addBack ( int num ) { s . add ( num ); } } /** * Your SmallestInfiniteSet object will be instantiated and called as such: * SmallestInfiniteSet obj = new SmallestInfiniteSet(); * int param_1 = obj.popSmallest(); * obj.addBack(num); */class SmallestInfiniteSet { public: SmallestInfiniteSet () { for ( int i = 1 ; i <= 1000 ; ++ i ) { s . insert ( i ); } } int popSmallest () { int x = * s . begin (); s . erase ( s . begin ()); return x ; } void addBack ( int num ) { s . insert ( num ); } private: set < int > s ; }; /** * Your SmallestInfiniteSet object will be instantiated and called as such: * SmallestInfiniteSet* obj = new SmallestInfiniteSet(); * int param_1 = obj->popSmallest(); * obj->addBack(num); */from sortedcontainers import SortedSet class SmallestInfiniteSet : def __init__ ( self ): self . s = SortedSet ( range ( 1 , 1001 )) def popSmallest ( self ) -> int : x = self . s [ 0 ] self . s . remove ( x ) return x def addBack ( self , num : int ) -> None : self . s . add ( num ) # Your SmallestInfiniteSet object will be instantiated and called as such: # obj = SmallestInfiniteSet() # param_1 = obj.popSmallest() # obj.addBack(num)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.