Design a number container system that can do the following:
Implement the NumberContainers
class:
NumberContainers()
Initializes the number container system.void change(int index, int number)
Fills the container at index
with the number
. If there is already a number at that index
, replace it.int find(int number)
Returns the smallest index for the given number
, or -1
if there is no index that is filled by number
in the system.
Example 1:
Input ["NumberContainers", "find", "change", "change", "change", "change", "find", "change", "find"] [[], [10], [2, 10], [1, 10], [3, 10], [5, 10], [10], [1, 20], [10]] Output [null, -1, null, null, null, null, 1, null, 2] Explanation NumberContainers nc = new NumberContainers(); nc.find(10); // There is no index that is filled with number 10. Therefore, we return -1. nc.change(2, 10); // Your container at index 2 will be filled with number 10. nc.change(1, 10); // Your container at index 1 will be filled with number 10. nc.change(3, 10); // Your container at index 3 will be filled with number 10. nc.change(5, 10); // Your container at index 5 will be filled with number 10. nc.find(10); // Number 10 is at the indices 1, 2, 3, and 5. Since the smallest index that is filled with 10 is 1, we return 1. nc.change(1, 20); // Your container at index 1 will be filled with number 20. Note that index 1 was filled with 10 and then replaced with 20. nc.find(10); // Number 10 is at the indices 2, 3, and 5. The smallest index that is filled with 10 is 2. Therefore, we return 2.
Constraints:
1 <= index, number <= 109
105
calls will be made in total to change
and find
.This problem requires designing a data structure that efficiently handles insertion/replacement and searching for the smallest index of a given number. A combination of hash tables and ordered sets (or equivalent structures) provides an optimal solution.
Approach:
We use two data structures:
index_map
(Hash Table): This maps an index to the number stored at that index. This allows O(1) lookup of the number at a given index.
number_map
(Hash Table with Ordered Sets): This maps a number to a set of indices where that number is stored. The crucial part is that we use an ordered set (like SortedSet
in Python or TreeSet
in Java). This allows us to efficiently find the smallest index for a given number in O(1) time.
Algorithms:
change(index, number)
:
index
exists in index_map
. If it does, remove the old number from its corresponding set in number_map
.index_map
with the new number
.index
to the set associated with the number
in number_map
.find(number)
:
number
exists as a key in number_map
.Time Complexity:
change(index, number)
: O(log n), where n is the number of indices associated with the old number (due to the set operations). In the worst case, this could be O(n), but it's amortized O(log n).find(number)
: O(1). Accessing and retrieving the first element from an ordered set is constant time.Space Complexity: O(m + n), where m is the number of unique numbers and n is the total number of index-number pairs.
Python:
from collections import defaultdict
from sortedcontainers import SortedSet
class NumberContainers:
def __init__(self):
self.index_map = {} # index -> number
self.number_map = defaultdict(SortedSet) # number -> SortedSet(indices)
def change(self, index: int, number: int) -> None:
if index in self.index_map:
old_number = self.index_map[index]
self.number_map[old_number].remove(index)
self.index_map[index] = number
self.number_map[number].add(index)
def find(self, number: int) -> int:
if number in self.number_map and self.number_map[number]:
return self.number_map[number][0] #get the smallest index
return -1
Java:
import java.util.*;
class NumberContainers {
private Map<Integer, Integer> indexMap;
private Map<Integer, TreeSet<Integer>> numberMap;
public NumberContainers() {
indexMap = new HashMap<>();
numberMap = new HashMap<>();
}
public void change(int index, int number) {
if (indexMap.containsKey(index)) {
int oldNumber = indexMap.get(index);
numberMap.get(oldNumber).remove(index);
}
indexMap.put(index, number);
numberMap.computeIfAbsent(number, k -> new TreeSet<>()).add(index);
}
public int find(int number) {
TreeSet<Integer> indices = numberMap.get(number);
return indices == null || indices.isEmpty() ? -1 : indices.first();
}
}
C++:
#include <iostream>
#include <unordered_map>
#include <set>
class NumberContainers {
public:
NumberContainers() {}
void change(int index, int number) {
if (indexMap.count(index)) {
int oldNumber = indexMap[index];
numberMap[oldNumber].erase(index);
}
indexMap[index] = number;
numberMap[number].insert(index);
}
int find(int number) {
if (numberMap.count(number) && !numberMap[number].empty()) {
return *numberMap[number].begin();
}
return -1;
}
private:
std::unordered_map<int, int> indexMap;
std::unordered_map<int, std::set<int>> numberMap;
};
Other Languages (Go, TypeScript): The core logic remains the same; you would adapt the choice of ordered set data structures appropriately for your chosen language. For example, Go might use a redblacktree
library, while TypeScript might need to implement its own or use a library like typescript-collections
. The implementation detail of the ordered set will change, but the overall algorithm stays consistent.