Map Sum Pairs
MedPrompt
Design a map that allows you to do the following:
- Maps a string key to a given value.
- Returns the sum of the values that have a key with a prefix equal to a given string.
Implement the MapSum class:
MapSum()Initializes theMapSumobject.void insert(String key, int val)Inserts thekey-valpair into the map. If thekeyalready existed, the originalkey-valuepair will be overridden to the new one.int sum(string prefix)Returns the sum of all the pairs' value whosekeystarts with theprefix.
Example 1:
Input
["MapSum", "insert", "sum", "insert", "sum"]
[[], ["apple", 3], ["ap"], ["app", 2], ["ap"]]
Output
[null, null, 3, null, 5]
Explanation
MapSum mapSum = new MapSum();
mapSum.insert("apple", 3);
mapSum.sum("ap"); // return 3 (apple = 3)
mapSum.insert("app", 2);
mapSum.sum("ap"); // return 5 (apple + app = 3 + 2 = 5)
Constraints:
1 <= key.length, prefix.length <= 50keyandprefixconsist of only lowercase English letters.1 <= val <= 1000- At most
50calls will be made toinsertandsum.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a standard HashMap to store the key-value pairs. The insert operation is straightforward, mapping directly to the put method of the hash map. To calculate the sum for a given prefix, we iterate through all the keys in the map, check if each key starts with the prefix, and accumulate the values of the matching keys.
Algorithm
- In the constructor, initialize a
HashMap<String, Integer>to store the key-value pairs. - For the
insert(String key, int val)method:- Use the map's
putmethod to insert or update the key-value pair:map.put(key, val).
- Use the map's
- For the
sum(String prefix)method:- Initialize a variable
totalSumto 0. - Iterate through every entry (
key,value) in the HashMap. - For each key, use the
startsWith()method to check if it begins with the givenprefix. - If
key.startsWith(prefix)is true, add the correspondingvaluetototalSum. - After checking all keys, return
totalSum.
- Initialize a variable
Walkthrough
The simplest way to solve this problem is by using a HashMap to store the mapping from a string key to an integer val.
insert(key, val) operation:
This operation is very simple. We just place the (key, val) pair into our hash map. If the key already exists, the put operation will automatically overwrite the old value with the new one, which is the desired behavior.
sum(prefix) operation:
For this operation, we need to find all keys that start with the given prefix. Since the hash map doesn't provide a direct way to query by prefix, we have to iterate through its entire set of keys. For each key, we check if it has the given prefix using the String.startsWith() method. If it does, we add the key's associated value to a running sum. Finally, we return this sum.
import java.util.HashMap;import java.util.Map; class MapSum { private Map<String, Integer> map; /** Initialize your data structure here. */ public MapSum() { map = new HashMap<>(); } public void insert(String key, int val) { map.put(key, val); } public int sum(String prefix) { int totalSum = 0; for (Map.Entry<String, Integer> entry : map.entrySet()) { if (entry.getKey().startsWith(prefix)) { totalSum += entry.getValue(); } } return totalSum; }}Complexity
Time
- `insert(key, val)`: O(L), where L is the length of the `key`. This is the time taken to compute the hash of the string. - `sum(prefix)`: O(N * P), where N is the number of keys in the map and P is the length of the `prefix`. We iterate through all N keys, and for each, `startsWith` takes O(P) time.
Space
O(K), where K is the total number of characters across all keys stored in the map. This is because the map needs to store all the key strings.
Trade-offs
Pros
Very simple to implement and understand.
Low memory overhead if the number of keys is small.
Cons
The
sumoperation is inefficient, especially for a large number of keys, as it requires a full scan of all stored keys.
A more optimized approach involves using a Trie (Prefix Tree). A Trie is a tree-like data structure perfect for handling prefix-based searches. Each node represents a character, and a path from the root to a node represents a prefix. We store the actual value of a key at the node where the key ends.
To calculate the sum, we first navigate to the node representing the given prefix. Then, we perform a traversal (like DFS or BFS) on the entire subtree rooted at this prefix node, summing up the values of all words we find.
Algorithm
- Define a
TrieNodeclass containing achildrenmap (or array) and an integervaluewhich stores the value of the key ending at this node. - In the
MapSumconstructor, initialize a rootTrieNode. - For
insert(key, val):- Traverse the trie from the root, character by character.
- Create new nodes for characters not yet in the trie.
- At the node corresponding to the last character of the key, set its
valuefield toval.
- For
sum(prefix):- Traverse the trie to find the node corresponding to the end of the
prefix. Let's call itprefixNode. - If
prefixNodedoes not exist, return 0. - Perform a traversal (e.g., Depth First Search) starting from
prefixNodeto visit all nodes in its subtree. - During the traversal, sum up the
valuefields of all visited nodes. This sum is the result.
- Traverse the trie to find the node corresponding to the end of the
Walkthrough
A Trie is a natural fit for this problem. We can build a Trie where each node has an array or map of children (one for each possible character) and a value field.
TrieNode Structure:
Each TrieNode will contain:
children: A map or an array ofTrieNodes (e.g.,TrieNode[26]).value: An integer that stores the value of the key that ends at this specific node. It's 0 if no key ends here.
insert(key, val) operation:
We traverse the Trie from the root, one character at a time. If a character's corresponding node doesn't exist, we create it. After processing all characters of the key, we arrive at the final node and set its value field to val. This handles both new insertions and updates.
sum(prefix) operation:
First, we traverse the Trie to locate the node that corresponds to the end of the prefix. If this path doesn't exist, no keys have this prefix, so we return 0. If the node is found, we then need to sum the values of all keys that pass through or end at this node. We can do this by performing a recursive Depth First Search (DFS) starting from this prefix node. The DFS will explore the entire subtree, and for every node it visits, it adds the node's value to a running total.
class MapSum { private static class TrieNode { TrieNode[] children = new TrieNode[26]; int value = 0; } private TrieNode root; public MapSum() { root = new TrieNode(); } public void insert(String key, int val) { TrieNode curr = root; for (char c : key.toCharArray()) { if (curr.children[c - 'a'] == null) { curr.children[c - 'a'] = new TrieNode(); } curr = curr.children[c - 'a']; } curr.value = val; } public int sum(String prefix) { TrieNode curr = root; for (char c : prefix.toCharArray()) { if (curr.children[c - 'a'] == null) { return 0; } curr = curr.children[c - 'a']; } return dfsSum(curr); } private int dfsSum(TrieNode node) { if (node == null) { return 0; } int currentSum = node.value; for (TrieNode child : node.children) { currentSum += dfsSum(child); } return currentSum; }}Complexity
Time
- `insert(key, val)`: O(L), where L is the length of the `key`. - `sum(prefix)`: O(P + M), where P is the length of the `prefix` and M is the number of nodes in the subtree rooted at the prefix node. In the worst case, M can be the total number of nodes in the Trie.
Space
O(K), where K is the total number of characters across all keys. In the worst case, if no keys share prefixes, the space is proportional to the sum of the lengths of all keys.
Trade-offs
Pros
Efficient
insertoperation.Generally faster
sumoperation than the brute-force approach, especially for longer prefixes that prune the search space significantly.
Cons
The
sumoperation can be slow if the prefix is short (e.g., an empty string) or if the subtree under the prefix is large, as it requires traversing all nodes in that subtree.
This is the most efficient approach, which optimizes the sum operation significantly. We modify the Trie so that each node stores the sum of all values in its entire subtree. This way, the sum for any prefix is pre-calculated and can be retrieved in time proportional to the prefix length.
To handle updates correctly (e.g., changing the value of an existing key), we also maintain a separate HashMap to keep track of the current value of each key. When a key is inserted or updated, we calculate the change in value (delta) and propagate this delta through the Trie, updating the sum at each node along the key's path.
Algorithm
- Define a
TrieNodeclass with achildrenmap/array and an integersumfield. - In the
MapSumclass, initialize a rootTrieNodeand aHashMap<String, Integer>to store the actual key-value pairs. - For
insert(key, val):- Check the auxiliary map to find the previous value of the key (0 if it's a new key).
- Calculate the difference:
delta = newValue - oldValue. - Traverse the trie for the given
key. For each node on the path, adddeltato itssumfield. - Finally, update the key's value in the auxiliary map:
map.put(key, val).
- For
sum(prefix):- Traverse the trie to find the node corresponding to the end of the
prefix. - If the path doesn't exist, return 0.
- Otherwise, return the
sumfield of the final node. This value is the precomputed sum for that prefix.
- Traverse the trie to find the node corresponding to the end of the
Walkthrough
This approach enhances the Trie by pre-calculating sums to make the sum query extremely fast.
TrieNode Structure:
Each TrieNode will contain:
children: A map or an array ofTrieNodes.sum: An integer that stores the sum of values of all keys that pass through or end in this node's subtree.
Data Structures: We use two main data structures:
- The modified Trie described above.
- A
HashMap<String, Integer>to store the actual value for each inserted key. This is crucial for handling updates.
insert(key, val) operation:
When inserting a (key, val) pair, we need to update the sums along the key's path in the Trie. If we are updating an existing key, the contribution to the prefix sums changes. We calculate this change, or delta, as newValue - oldValue. The oldValue is retrieved from our auxiliary HashMap (it's 0 for a new key). We then traverse the Trie for the key, and for every node on the path, we add this delta to its sum field. Finally, we update the key's value in the HashMap.
sum(prefix) operation:
This becomes very efficient. We simply traverse the Trie to the node corresponding to the prefix. The sum field of this node already holds the total sum of all keys starting with that prefix. We just return this value. If the prefix path does not exist in the Trie, the sum is 0.
import java.util.HashMap;import java.util.Map; class MapSum { private static class TrieNode { TrieNode[] children = new TrieNode[26]; int sum = 0; // Sum of all values in this subtree } private TrieNode root; private Map<String, Integer> map; public MapSum() { root = new TrieNode(); map = new HashMap<>(); } public void insert(String key, int val) { int delta = val - map.getOrDefault(key, 0); map.put(key, val); TrieNode curr = root; for (char c : key.toCharArray()) { if (curr.children[c - 'a'] == null) { curr.children[c - 'a'] = new TrieNode(); } curr = curr.children[c - 'a']; curr.sum += delta; } } public int sum(String prefix) { TrieNode curr = root; for (char c : prefix.toCharArray()) { if (curr.children[c - 'a'] == null) { return 0; } curr = curr.children[c - 'a']; } return curr.sum; }}Complexity
Time
- `insert(key, val)`: O(L), where L is the length of the `key`. - `sum(prefix)`: O(P), where P is the length of the `prefix`.
Space
O(K), where K is the total number of characters across all keys. The space is used by the Trie and the auxiliary HashMap, both of which depend on the keys' content and length.
Trade-offs
Pros
Extremely fast
sumoperation, making it ideal for scenarios with frequent sum queries.The
insertoperation remains efficient.
Cons
Higher space complexity due to the need for both a Trie and an auxiliary HashMap.
The
insertlogic is more complex than in other approaches.
Solutions
Solution
public class Trie { private Trie [] children = new Trie [ 26 ]; private int val ; public void Insert ( string w , int x ) { Trie node = this ; for ( int i = 0 ; i < w . Length ; ++ i ) { int idx = w [ i ] - 'a' ; if ( node . children [ idx ] == null ) { node . children [ idx ] = new Trie (); } node = node . children [ idx ]; node . val += x ; } } public int Search ( string w ) { Trie node = this ; for ( int i = 0 ; i < w . Length ; ++ i ) { int idx = w [ i ] - 'a' ; if ( node . children [ idx ] == null ) { return 0 ; } node = node . children [ idx ]; } return node . val ; } } public class MapSum { private Dictionary < string , int > d = new Dictionary < string , int >(); private Trie trie = new Trie (); public MapSum () { } public void Insert ( string key , int val ) { int x = val - ( d . ContainsKey ( key ) ? d [ key ] : 0 ); d [ key ] = val ; trie . Insert ( key , x ); } public int Sum ( string prefix ) { return trie . Search ( prefix ); } } /** * Your MapSum object will be instantiated and called as such: * MapSum obj = new MapSum(); * obj.Insert(key,val); * int param_2 = obj.Sum(prefix); */class Trie { private Trie [] children = new Trie [ 26 ]; private int val ; public void insert ( String w , int x ) { Trie node = this ; for ( int i = 0 ; i < w . length (); ++ i ) { int idx = w . charAt ( i ) - 'a' ; if ( node . children [ idx ] == null ) { node . children [ idx ] = new Trie (); } node = node . children [ idx ]; node . val += x ; } } public int search ( String w ) { Trie node = this ; for ( int i = 0 ; i < w . length (); ++ i ) { int idx = w . charAt ( i ) - 'a' ; if ( node . children [ idx ] == null ) { return 0 ; } node = node . children [ idx ]; } return node . val ; } } class MapSum { private Map < String , Integer > d = new HashMap <>(); private Trie trie = new Trie (); public MapSum () { } public void insert ( String key , int val ) { int x = val - d . getOrDefault ( key , 0 ); d . put ( key , val ); trie . insert ( key , x ); } public int sum ( String prefix ) { return trie . search ( prefix ); } } /** * Your MapSum object will be instantiated and called as such: * MapSum obj = new MapSum(); * obj.insert(key,val); * int param_2 = obj.sum(prefix); */class Trie { constructor () { this . children = new Array ( 26 ); this . val = 0 ; } insert ( w , x ) { let node = this ; for ( const c of w ) { const i = c . charCodeAt ( 0 ) - 97 ; if ( ! node . children [ i ]) { node . children [ i ] = new Trie (); } node = node . children [ i ]; node . val += x ; } } search ( w ) { let node = this ; for ( const c of w ) { const i = c . charCodeAt ( 0 ) - 97 ; if ( ! node . children [ i ]) { return 0 ; } node = node . children [ i ]; } return node . val ; } } var MapSum = function () { this . d = new Map (); this . t = new Trie (); }; /** * @param {string} key * @param {number} val * @return {void} */ MapSum . prototype . insert = function ( key , val ) { const x = val - ( this . d . get ( key ) ?? 0 ); this . d . set ( key , val ); this . t . insert ( key , x ); }; /** * @param {string} prefix * @return {number} */ MapSum . prototype . sum = function ( prefix ) { return this . t . search ( prefix ); }; /** * Your MapSum object will be instantiated and called as such: * var obj = new MapSum() * obj.insert(key,val) * var param_2 = obj.sum(prefix) */class Trie { public: Trie () : children ( 26 , nullptr ) { } void insert ( string & w , int x ) { Trie * node = this ; for ( char c : w ) { c -= 'a' ; if ( ! node -> children [ c ]) { node -> children [ c ] = new Trie (); } node = node -> children [ c ]; node -> val += x ; } } int search ( string & w ) { Trie * node = this ; for ( char c : w ) { c -= 'a' ; if ( ! node -> children [ c ]) { return 0 ; } node = node -> children [ c ]; } return node -> val ; } private: vector < Trie *> children ; int val = 0 ; }; class MapSum { public: MapSum () { } void insert ( string key , int val ) { int x = val - d [ key ]; d [ key ] = val ; trie -> insert ( key , x ); } int sum ( string prefix ) { return trie -> search ( prefix ); } private: unordered_map < string , int > d ; Trie * trie = new Trie (); }; /** * Your MapSum object will be instantiated and called as such: * MapSum* obj = new MapSum(); * obj->insert(key,val); * int param_2 = obj->sum(prefix); */class Trie : def __init__ ( self ): self . children : List [ Trie | None ] = [ None ] * 26 self . val : int = 0 def insert ( self , w : str , x : int ): node = self for c in w : idx = ord ( c ) - ord ( 'a' ) if node . children [ idx ] is None : node . children [ idx ] = Trie () node = node . children [ idx ] node . val += x def search ( self , w : str ) -> int : node = self for c in w : idx = ord ( c ) - ord ( 'a' ) if node . children [ idx ] is None : return 0 node = node . children [ idx ] return node . val class MapSum : def __init__ ( self ): self . d = defaultdict ( int ) self . tree = Trie () def insert ( self , key : str , val : int ) -> None : x = val - self . d [ key ] self . d [ key ] = val self . tree . insert ( key , x ) def sum ( self , prefix : str ) -> int : return self . tree . search ( prefix ) # Your MapSum object will be instantiated and called as such: # obj = MapSum() # obj.insert(key,val) # param_2 = obj.sum(prefix)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.