Skip to content

Commit f6b5dfe

Browse files
committed
feat(algorithms, trie): replace words algorithm
Replace words using different approaches to solving the problem
1 parent 78cea0b commit f6b5dfe

7 files changed

Lines changed: 289 additions & 127 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Replace Words
2+
3+
In this problem, we are considering the words that are composed of a prefix and a postfix. For example, if we append a
4+
postfix “happy” to a prefix “un”, it forms the word “unhappy”. Similarly, “disagree” is formed from a prefix, “dis”
5+
followed by a postfix, “agree”.
6+
7+
You’re given a dictionary, dictionary, consisting of prefixes, and a sentence, sentence, which has words separated by
8+
spaces only. Your task is to replace the postfix in sentence with their prefixes given in dictionary (if found) and
9+
return the modified sentence.
10+
11+
A couple of points to keep in mind:
12+
13+
- If a postfix in the sentence matches more than one prefix in the dictionary, replace it with the prefix that has the
14+
shortest length. For example, if we have the sentence “iphone is amazing”, and the dictionary {“i”, “ip”, “hone”},
15+
then the word “iphone” has two prefixes in the dictionary “i” and “ip”, but we will replace it with the one that is
16+
shorter among the two, that is, “i”.
17+
- If there is no root word against any word in the sentence, leave it unchanged.
18+
19+
## Constraints
20+
21+
- 1 <= dictionary.length <= 1000
22+
- 1 <= `dictionary[i].length` <= 100
23+
- `dictionary[i]` consists of only lower-case letters.
24+
- 1 <= `sentence.length` <= 10^6
25+
- `sentence` consists of only lower-case letters and spaces.
26+
- The number of words in `sentence` is in the range `[1, 1000]`
27+
- The length of each word in `sentence` is in the range `[1, 1000]`
28+
- Every two consecutive words in `sentence` will be separated by exactly one space.
29+
- `sentence` does not have leading or trailing spaces.
30+
31+
## Examples
32+
33+
Example 1:
34+
35+
```text
36+
Input: dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"
37+
Output: "the cat was rat by the bat"
38+
```
39+
40+
Example 2:
41+
42+
```text
43+
Input: dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"
44+
Output: "a a b c"
45+
```
46+
47+
Example 3:
48+
49+
```text
50+
Input: dictionary = ["a", "aa", "aaa", "aaaa"], sentence = "a aa a aaaa aaa aaa aaa aaaaaa bbb baba ababa"
51+
Output: "a a a a a a a a bbb baba a"
52+
```
53+
54+
Example 4:
55+
56+
```text
57+
Input: dictionary = ["catt","cat","bat","rat"], sentence = "the cattle was rattled by the battery"
58+
Output: "the cat was rat by the bat"
59+
```
60+
61+
Example 5:
62+
63+
```text
64+
Input: dictionary = ["ac","ab"], sentence = "it is abnormal that this solution is accepted"
65+
Output: "it is ab that this solution is ac"
66+
```
67+
68+
## Topics
69+
70+
- Array
71+
- Hash Table
72+
- String
73+
- Trie
74+
75+
## Solution(s)
76+
77+
1. [Naive Approach](#naive-approach)
78+
2. [Optimized approach using trie](#optimized-approach-using-trie)
79+
80+
### Naive approach
81+
82+
This problem requires us to observe the leading characters (prefixes) of words in a sentence that can be replaced with
83+
the shortest matching prefix from a dictionary.
84+
85+
In the naive approach, we traverse the given sentence, and for each word, we compare it with each prefix in the given
86+
dictionary to find the shortest matching prefix against it, if any. To make sure that we get to the right prefix, we
87+
store the respective lengths of prefixes with them. Additionally, we keep track of the shortest prefix as well. If, at
88+
any point, a match is found, we compare its length with the length of the shortest match found up to that point. If it
89+
is shorter than the current shortest prefix, we update the value of the shortest prefix.
90+
91+
In this approach, we iterate through each word in the sentence, which takes O(n) time, where n is the number of words in
92+
a sentence. For each word, we iterate over the prefixes in the dictionary, which takes O(d) time, where d is the number
93+
of prefixes in the dictionary. Therefore, the overall time complexity for this approach is O(n×d).
94+
95+
### Optimized approach using trie
96+
97+
Searching for the shortest prefixes for all the words in the given sentence requires repeated searches in the dictionary.
98+
While this would be an expensive proposition if we keep our dictionary words in data structures like arrays and linked
99+
lists, some more advanced data structures, such as tries, help reduce our search time complexity. In this approach, we
100+
first store all the dictionary prefixes in a trie and then find the shortest prefix against each word of the sentence so
101+
that we can perform the required replacement.
102+
103+
> A trie is visualized as a tree of characters that allows storing strings as graphs. A trie stores a word in a top-down
104+
> manner where all words having common leading characters (prefixes) have a common parent.
105+
106+
To store the dictionary prefixes in the trie, we start from its root node and perform the following steps repeatedly
107+
until we’ve stored all the words:
108+
109+
- We check if the current character of the dictionary prefix is listed among the immediate children of the current node.
110+
- If it doesn’t exist, we create a new trie node for this character as a child of the current node and move to this new
111+
trie node.
112+
- If it exists among the children, we simply move to that node.
113+
- If the current character is the last character of a given dictionary prefix, we use a flag to mark the trie node holding
114+
this last character. This flag represents the completion of a dictionary prefix on the trie. For example, if we have
115+
the prefix “dis” in the dictionary, then when we reach “s” and store it in the trie. We mark this node with the flag
116+
to represent that the word, “dis”, has been completed.
117+
118+
After we are done creating the trie from the dictionary prefixes, we start to locate the matching prefix for each word
119+
of the sentence. Once found, we replace the word in the sentence with it. For example, if the input sentence is
120+
“fantastic” and the given dictionary is [“fanta”, “tic”, “fan”], then both “fanta” and “fan” are matching prefixes for
121+
“fantastic”, but we replace it with “fan” since it is the shortest of all the found matches. Since we are using a trie,
122+
our search automatically picks up the shortest matching prefix, and we don’t need to do it later.
123+
124+
To traverse all the words in the sentence, we store them in a list. Then, for each word, wi, perform the following:
125+
126+
- Start with the first character, `wi[0]`, and look at whether the same character exists in any of the children of the
127+
root node of the trie.
128+
- If it exists, it implies that the prefix for `wi` exists, and we can continue as follows:
129+
- For remaining characters, keep looking for the matching character in the trie one by one.
130+
- If at any point while searching and matching, we reach a node that is flagged for being the last character, we
131+
stop and return this prefix as the shortest matching prefix for `wi`
132+
- Otherwise, the prefix against `wi` doesn’t exist, so just move to the next word of the sentence.
133+
134+
135+
Once found, replace the original word `wi` in the list with its matched prefix. Keep doing this until we process the
136+
last word in the sentence. In the end, change the list of words back to the string representing the modified sentence
137+
and return it as the output.
138+
139+
#### Summary
140+
141+
Let’s review the optimized approach we have discussed above.
142+
143+
- Create a trie and store all the dictionary prefixes in it.
144+
- Traverse the sentence and use the trie to find the shortest matching prefix for each word of the sentence.
145+
- Once found, replace the original word of the sentence with the matched prefix. We do this for all the words in the
146+
sentence.
147+
- If no matching prefix is found, leave the word as it is and move to the next word in the sentence.
148+
- Return the modified sentence as the output.
149+
150+
#### Time Complexity
151+
152+
It takes O(m) time to create a trie from the given dictionary prefixes, where m is the sum of the number of characters
153+
of all the prefixes in the dictionary.
154+
155+
Then, it takes O(n) time to iterate over each word in the sentence and find its matching prefix in the trie, where n is
156+
the number of words in the sentence.
157+
158+
Therefore, the overall time complexity for this approach is O(n+m).
159+
160+
#### Space Complexity
161+
162+
The space complexity is O(m), where m is the sum of the number of characters of all the prefixes in the dictionary.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import collections
22
from functools import reduce
33
from typing import List
4+
from datastructures import Trie, TrieNode
45

56

67
def replace_words_with_prefix_hash(dictionary: List[str], sentence: str) -> str:
@@ -74,3 +75,44 @@ def replace(word: str) -> str:
7475
return cur.get(end, word)
7576

7677
return " ".join(map(replace, sentence.split()))
78+
79+
80+
def replace_words_with_trie_2(sentence: str, dictionary: List[str]) -> str:
81+
trie = Trie()
82+
83+
# iterate over the prefixes in the dictionary and insert them into the trie
84+
for prefix in dictionary:
85+
trie.insert(prefix)
86+
87+
# Store each of the word in the sentence in a new list
88+
word_list = sentence.split()
89+
90+
# iterate over all the words in the list
91+
for idx in range(len(word_list)):
92+
93+
def replace(root: TrieNode, word: str) -> str:
94+
"""
95+
This replaces each word in the sentence with the smallest word from the dictionary
96+
"""
97+
curr = root
98+
# iterate over each dictionary word along with the index of that character
99+
for i, char in enumerate(word):
100+
# If the character does not belong to the current node's children, then return the word
101+
if char not in curr.children:
102+
return word
103+
104+
# Move to the child of the current node corresponding to the current character
105+
curr = curr.children[char]
106+
107+
# When the flag is_end becomes true, this means we have reached the end of the word in the trie. If this is
108+
# the case, then return this word
109+
if curr.is_end:
110+
return word[: i + 1]
111+
112+
return word
113+
114+
# replace each word in the word_list with the shortest prefix from the trie
115+
word_list[idx] = replace(trie.root, word_list[idx])
116+
117+
# After replacing each word with the shortest matching prefix, convert the list of words back to a single sentence.
118+
return " ".join(word_list)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from utils.test_utils import custom_test_name_func
5+
from algorithms.trie.replace_words import (
6+
replace_words_with_prefix_hash,
7+
replace_words_with_trie,
8+
replace_words_with_trie_2,
9+
)
10+
11+
REPLACE_WORDS_TEST_CASES = [
12+
(
13+
["cat", "bat", "rat"],
14+
"the cattle was rattled by the battery",
15+
"the cat was rat by the bat",
16+
),
17+
(
18+
["a", "aa", "aaa", "aaaa"],
19+
"a aa a aaaa aaa aaa aaa aaaaaa bbb baba ababa",
20+
"a a a a a a a a bbb baba a",
21+
),
22+
(["a", "b", "c"], "aadsfasf absbs bbab cadsfafs", "a a b c"),
23+
(
24+
["catt", "cat", "bat", "rat"],
25+
"the cattle was rattled by the battery",
26+
"the cat was rat by the bat",
27+
),
28+
(
29+
["ac", "ab"],
30+
"it is abnormal that this solution is accepted",
31+
"it is ab that this solution is ac",
32+
),
33+
]
34+
35+
36+
class ReplaceWordsHashTestCases(unittest.TestCase):
37+
@parameterized.expand(REPLACE_WORDS_TEST_CASES, name_func=custom_test_name_func)
38+
def test_replace_words_with_prefix_hash(
39+
self, dictionary: List[str], sentence: str, expected: str
40+
):
41+
actual = replace_words_with_prefix_hash(dictionary, sentence)
42+
self.assertEqual(expected, actual)
43+
44+
@parameterized.expand(REPLACE_WORDS_TEST_CASES, name_func=custom_test_name_func)
45+
def test_replace_words_with_trie(
46+
self, dictionary: List[str], sentence: str, expected: str
47+
):
48+
actual = replace_words_with_trie(dictionary, sentence)
49+
self.assertEqual(expected, actual)
50+
51+
@parameterized.expand(REPLACE_WORDS_TEST_CASES, name_func=custom_test_name_func)
52+
def test_replace_words_with_trie_2(
53+
self, dictionary: List[str], sentence: str, expected: str
54+
):
55+
actual = replace_words_with_trie_2(dictionary=dictionary, sentence=sentence)
56+
self.assertEqual(expected, actual)
57+
58+
59+
if __name__ == "__main__":
60+
unittest.main()

datastructures/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
11
from datastructures.sets import DisjointSetUnion, UnionFind
2+
from datastructures.trees.trie import (
3+
Trie,
4+
TrieNode,
5+
AlphabetTrieNode,
6+
AlphabetTrie,
7+
SuffixTreeNode,
8+
SuffixTree,
9+
)
210

3-
__all__ = ["DisjointSetUnion", "UnionFind"]
11+
__all__ = [
12+
"DisjointSetUnion",
13+
"UnionFind",
14+
"Trie",
15+
"TrieNode",
16+
"AlphabetTrieNode",
17+
"AlphabetTrie",
18+
"SuffixTreeNode",
19+
"SuffixTree",
20+
]

datastructures/trees/trie/trie.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,22 @@ def insert(self, word: str, index: Optional[int] = None) -> None:
4343
None
4444
"""
4545
curr = self.root
46+
# Strip the word of any leading or trailing white spaces
4647
stripped_word = word.strip()
4748

49+
# Iterate through each character in the word
4850
for char in stripped_word:
51+
# If the character does not belong to any child of the current trie node, then create a new trie node for
52+
# this character as a child of the current node
4953
if char not in curr.children:
5054
curr.children[char] = TrieNode()
55+
56+
# move to the child of the current node (either already present or just added)
5157
curr = curr.children[char]
5258
if index is not None:
5359
if curr.index is None or index < curr.index:
5460
curr.index = index
61+
# Set a flag as the end of the inserted word
5562
curr.is_end = True
5663

5764
def search_exact(self, word: str) -> bool:

puzzles/replace_words/README.md

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)