Skip to content

Commit 201367f

Browse files
committed
feat(datastructures, mapsum): map sum pairs
1 parent 1791c05 commit 201367f

File tree

8 files changed

+306
-2
lines changed

8 files changed

+306
-2
lines changed

datastructures/map_sum/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Map Sum Pairs
2+
3+
Design a data structure that supports the following operations:
4+
5+
1. Insert a key-value pair:
6+
- Each key is a string, and each value is an integer.
7+
- If the key already exists, update its value to (overriding the previous value).
8+
9+
2. Return the prefix sum:
10+
- Given a string, `prefix`, return the total sum of all values associated with keys that start with this prefix.
11+
12+
To accomplish this, implement a class MapSum:
13+
14+
- Constructor: Initializes the object.
15+
- `void insert (String key, int val)`: Inserts the key-value pair into the data structure. If the key already exists,
16+
its value is updated to the new one.
17+
- `int sum (String prefix)`: Returns the total sum of values for all keys that begin with the specified prefix.
18+
19+
## Constraints
20+
21+
- 1 ≤ `key.length`, `prefix.length` ≤ 50
22+
- Both `key` and `prefix` consist of only lowercase English letters.
23+
- 1 ≤ `val` ≤ 1000
24+
- At most 50 calls will be made to insert and sum.
25+
26+
## Examples
27+
28+
![Example 1](./images/examples/map_sum_pairs_example_1.png)
29+
![Example 2](./images/examples/map_sum_pairs_example_2.png)
30+
![Example 3](./images/examples/map_sum_pairs_example_3.png)
31+
32+
## Topics
33+
34+
- Hash Table
35+
- String
36+
- Design
37+
- Trie

datastructures/map_sum/__init__.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from typing import Dict
2+
from collections import Counter
3+
4+
from datastructures.trees.trie import TrieNode
5+
6+
7+
class MapSumBruteForce(object):
8+
"""
9+
This solution to creating a map sum data structure that finds the sum of keys with a matching prefix uses a
10+
Hash Table combined with Brute-Force Search and String Matching.
11+
12+
Time Complexity: Every insert operation is O(1). Every sum operation is O(N*P) where N is the number of items in the
13+
map, and P is the length of the input prefix.
14+
15+
Space Complexity: The space used by map is linear in the size of all input key and val values combined.
16+
"""
17+
18+
def __init__(self):
19+
self.mapping: Dict[str, int] = {}
20+
21+
def insert(self, key: str, val: int) -> None:
22+
"""
23+
Inserts the key with the given value into the hash table
24+
Args:
25+
key (str): key to insert
26+
val (int): value to insert
27+
"""
28+
self.mapping[key] = val
29+
30+
def sum(self, prefix: str) -> int:
31+
"""
32+
Finds the sum of all keys with the prefix `prefix`.
33+
Args:
34+
prefix (str): prefix to search for
35+
Returns:
36+
int: sum of all keys with the prefix `prefix`
37+
"""
38+
running_sum = 0
39+
for k, v in self.mapping.items():
40+
if k.startswith(prefix):
41+
running_sum += v
42+
43+
return running_sum
44+
45+
46+
class MapSumPrefix(object):
47+
"""
48+
We can remember the answer for all possible prefixes in a HashMap score. When we get a new (key, val) pair, we
49+
update every prefix of key appropriately: each prefix will be changed by delta = val - map[key], where map is the
50+
previously associated value of key (zero if undefined.)
51+
52+
Time Complexity: Every insert operation is O(K^2), where K is the length of the key, as K strings are made of an
53+
average length of K. Every sum operation is O(1).
54+
55+
Space Complexity: The space used by map is linear in the size of all input key and val values combined.
56+
"""
57+
58+
def __init__(self):
59+
self.mapping: Dict[str, int] = {}
60+
self.score = Counter()
61+
62+
def insert(self, key: str, val: int) -> None:
63+
"""
64+
Inserts the key with the given value into the hash table
65+
Args:
66+
key (str): key to insert
67+
val (int): value to insert
68+
"""
69+
delta = val - self.mapping.get(key, 0)
70+
self.mapping[key] = val
71+
for i in range(len(key) + 1):
72+
prefix = key[:i]
73+
self.score[prefix] += delta
74+
75+
def sum(self, prefix: str) -> int:
76+
"""
77+
Finds the sum of all keys with the prefix `prefix`.
78+
Args:
79+
prefix (str): prefix to search for
80+
Returns:
81+
int: sum of all keys with the prefix `prefix`
82+
"""
83+
return self.score[prefix]
84+
85+
86+
class MapSumTrie(object):
87+
"""
88+
Since we are dealing with prefixes, a Trie (prefix tree) is a natural data structure to approach this problem. For
89+
every node of the trie corresponding to some prefix, we will remember the desired answer (score) and store it at
90+
this node. As in the approach of using a prefix has map, this involves modifying each node by delta = val - map[key].
91+
92+
Time Complexity: Every insert operation is O(K), where K is the length of the key. Every sum operation is O(K).
93+
Space Complexity: The space used is linear in the size of the total input.
94+
"""
95+
96+
def __init__(self):
97+
self.mapping: Dict[str, int] = {}
98+
self.score = Counter()
99+
self.root = TrieNode()
100+
101+
def insert(self, key: str, val: int) -> None:
102+
"""
103+
Inserts the key with the given value into the hash table
104+
Args:
105+
key (str): key to insert
106+
val (int): value to insert
107+
"""
108+
delta = val - self.mapping.get(key, 0)
109+
self.mapping[key] = val
110+
current = self.root
111+
current.score += delta
112+
for char in key:
113+
current = current.children[char]
114+
current.score += delta
115+
116+
def sum(self, prefix: str) -> int:
117+
"""
118+
Finds the sum of all keys with the prefix `prefix`.
119+
Args:
120+
prefix (str): prefix to search for
121+
Returns:
122+
int: sum of all keys with the prefix `prefix`
123+
"""
124+
current = self.root
125+
for char in prefix:
126+
if char not in current.children:
127+
return 0
128+
current = current.children[char]
129+
return current.score
81.3 KB
Loading
68.7 KB
Loading
92.2 KB
Loading
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import unittest
2+
from typing import Tuple, List
3+
from parameterized import parameterized
4+
from datastructures.map_sum import MapSumBruteForce, MapSumPrefix, MapSumTrie
5+
6+
7+
MAP_SUM_TEST_CASES = [
8+
(
9+
[
10+
("insert", ("apple", 3)),
11+
("sum", ("ap", 3)),
12+
("insert", ("apple", 2)),
13+
("sum", ("ap", 2)),
14+
],
15+
),
16+
(
17+
[
18+
("insert", ("apple", 3)),
19+
("sum", ("ap", 3)),
20+
("insert", ("ap", 2)),
21+
("sum", ("ap", 5)),
22+
],
23+
),
24+
(
25+
[
26+
("insert", ("apple", 3)),
27+
("insert", ("apple", 5)),
28+
("sum", ("ap", 5)),
29+
("insert", ("apricot", 2)),
30+
("sum", ("ap", 7)),
31+
],
32+
),
33+
(
34+
[
35+
("insert", ("car", 3)),
36+
("insert", ("cat", 2)),
37+
("insert", ("cart", 4)),
38+
("sum", ("ca", 9)),
39+
("sum", ("car", 7)),
40+
],
41+
),
42+
(
43+
[
44+
("insert", ("dog", 5)),
45+
("insert", ("cat", 7)),
46+
("sum", ("z", 0)),
47+
],
48+
),
49+
(
50+
[
51+
("insert", ("a", 3)),
52+
("insert", ("apple", 2)),
53+
("sum", ("a", 5)),
54+
("sum", ("app", 2)),
55+
],
56+
),
57+
]
58+
59+
60+
class MapSumPairsTestCase(unittest.TestCase):
61+
@parameterized.expand(MAP_SUM_TEST_CASES)
62+
def test_map_sum_pairs_brute_force(
63+
self, operations: List[Tuple[str, Tuple[str, int]]]
64+
):
65+
map_sum = MapSumBruteForce()
66+
for operation in operations:
67+
cmd = operation[0]
68+
params = operation[1]
69+
if cmd == "insert":
70+
key, value = params
71+
map_sum.insert(key, value)
72+
73+
if cmd == "sum":
74+
prefix, expected = params
75+
actual = map_sum.sum(prefix)
76+
self.assertEqual(expected, actual)
77+
78+
@parameterized.expand(MAP_SUM_TEST_CASES)
79+
def test_map_sum_pairs_prefix(self, operations: List[Tuple[str, Tuple[str, int]]]):
80+
map_sum = MapSumPrefix()
81+
for operation in operations:
82+
cmd = operation[0]
83+
params = operation[1]
84+
if cmd == "insert":
85+
key, value = params
86+
map_sum.insert(key, value)
87+
88+
if cmd == "sum":
89+
prefix, expected = params
90+
actual = map_sum.sum(prefix)
91+
self.assertEqual(expected, actual)
92+
93+
@parameterized.expand(MAP_SUM_TEST_CASES)
94+
def test_map_sum_pairs_trie(self, operations: List[Tuple[str, Tuple[str, int]]]):
95+
map_sum = MapSumTrie()
96+
for operation in operations:
97+
cmd = operation[0]
98+
params = operation[1]
99+
if cmd == "insert":
100+
key, value = params
101+
map_sum.insert(key, value)
102+
103+
if cmd == "sum":
104+
prefix, expected = params
105+
actual = map_sum.sum(prefix)
106+
self.assertEqual(expected, actual)
107+
108+
109+
if __name__ == "__main__":
110+
unittest.main()

datastructures/trees/trie/trie_node.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
class TrieNode:
66
def __init__(self):
7-
# self.char = char
87
"""
98
Initializes a TrieNode instance.
109
11-
A TrieNode contains a character and a dictionary of its children. It also contains a boolean indicating whether the node is the end of a word in the Trie.
10+
A TrieNode contains a character and a dictionary of its children. It also contains a boolean indicating whether
11+
the node is the end of a word in the Trie.
1212
1313
Parameters:
1414
None
@@ -19,6 +19,7 @@ def __init__(self):
1919
self.children: DefaultDict[str, TrieNode] = defaultdict(TrieNode)
2020
self.is_end = False
2121
self.index: Optional[int] = None
22+
self.score: int = 0
2223

2324
def __repr__(self):
2425
return f"TrieNode(index={self.index}, is_end={self.is_end})"

datastructures/trees/trie/word_dictionary/test_word_dictionary.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ def test_case_1(self):
2929
actual_get_words_two = word_dictionary.get_words()
3030
self.assertEqual(expected_words, actual_get_words_two)
3131

32+
def test_case_2(self):
33+
word_dictionary = WordDictionary()
34+
actual_words_1 = word_dictionary.get_words()
35+
self.assertEqual([], actual_words_1)
36+
37+
word_dictionary.add_word("apple")
38+
word_dictionary.add_word("grape")
39+
actual_words_2 = word_dictionary.get_words()
40+
expected_words_1 = ["apple", "grape"]
41+
self.assertEqual(expected_words_1, actual_words_2)
42+
43+
actual_search_word_1 = word_dictionary.search_word("strawberry")
44+
self.assertFalse(actual_search_word_1)
45+
46+
word_dictionary.add_word("banana")
47+
word_dictionary.add_word("banan")
48+
49+
actual_search_word_2 = word_dictionary.search_word("bana..")
50+
self.assertTrue(actual_search_word_2)
51+
52+
actual_search_word_3 = word_dictionary.search_word("ba...a")
53+
self.assertTrue(actual_search_word_3)
54+
55+
actual_get_words_3 = word_dictionary.get_words()
56+
expected_words_2 = ["apple", "banan", "banana", "grape"]
57+
self.assertEqual(sorted(expected_words_2), sorted(actual_get_words_3))
58+
3259

3360
if __name__ == "__main__":
3461
unittest.main()

0 commit comments

Comments
 (0)