Skip to content

Commit dff4c32

Browse files
committed
adds exercise 1 - ch 7.0
1 parent 00f50b5 commit dff4c32

26 files changed

+319
-0
lines changed

Readme.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,5 +255,17 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy)
255255
- [Improving the `Router` API](/chapters/ch06.3-improving-the-router-api.md)
256256
- [The need for a `trie`](/chapters/ch06.4-the-need-for-a-trie.md)
257257
- [What is a `Trie` anyway?](/chapters/ch06.4-the-need-for-a-trie.md#what-is-a-trie-anyway)
258+
- [Exercise - Implementing a `Trie`](/chapters/ch07.0-ex-implementing-a-trie.md#exercise---implementing-a-trie)
259+
- [Root Node](/chapters/ch07.0-ex-implementing-a-trie.md#root-node)
260+
- [End of the word](/chapters/ch07.0-ex-implementing-a-trie.md#end-of-the-word)
261+
- [Challenge 1: Basic Trie with `insert` Method](/chapters/ch07.0-ex-implementing-a-trie.md#challenge-1-basic-trie-with-insert-method)
262+
- [Requirements](/chapters/ch07.0-ex-implementing-a-trie.md#requirements)
263+
- [More details](/chapters/ch07.0-ex-implementing-a-trie.md#more-details)
264+
- [Solution](/chapters/ch07.0-ex-implementing-a-trie.md#solution)
265+
- [Challenge 2: Implement `search` method](/chapters/ch07.0-ex-implementing-a-trie.md#challenge-2-implement-search-method)
266+
- [Requirements](/chapters/ch07.0-ex-implementing-a-trie.md#requirements-1)
267+
- [More details](/chapters/ch07.0-ex-implementing-a-trie.md#more-details-1)
268+
- [Hints](/chapters/ch07.0-ex-implementing-a-trie.md#hints)
269+
- [Solution](#solution-1)
258270

259271
![](https://uddrapi.com/api/img?page=readme)
108 KB
Loading

assets/imgs/compressed/call-stack.png

46.1 KB
Loading
14.6 KB
Loading

assets/imgs/compressed/cover.jpg

189 KB
Loading
25.8 KB
Loading
13 KB
Loading
18.6 KB
Loading
24 KB
Loading

assets/imgs/compressed/latency_2.png

11.3 KB
Loading
58.6 KB
Loading
12.7 KB
Loading
20.9 KB
Loading
16.2 KB
Loading

assets/imgs/compressed/mem_idle_2.png

14.2 KB
Loading
28.1 KB
Loading

assets/imgs/compressed/next.png

561 Bytes
Loading

assets/imgs/compressed/prev.png

673 Bytes
Loading

assets/imgs/compressed/referer.png

13.8 KB
Loading

assets/imgs/compressed/rps.png

18.8 KB
Loading

assets/imgs/compressed/rps_2.png

24.9 KB
Loading

assets/imgs/compressed/user-agent.png

32.5 KB
Loading

assets/imgs/trie-eow.png

155 KB
Loading

assets/imgs/trie-overview.png

125 KB
Loading

chapters/ch06.4-the-need-for-a-trie.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ Every node, including `root` will be an object that contain some necessary infor
104104
4. `children`: Any children nodes. (We'll get more deep into this in the upcoming chapters)
105105

106106
Enough with the theory. In the next chapter, we'll dive into our very first exercise for this book: **implementing a Trie**.
107+
108+
[![Read Next](/assets/imgs/next.png)](/chapters/ch07.0-ex-implementing-a-trie.md)
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.4-the-need-for-a-trie.md)
2+
3+
# Exercise - Implementing a `Trie`
4+
5+
> This exercise will motivate you to work on implementing your solution independently. Once you have completed the exercise, you can move on to the next challenge or read the solution to find a different approach.
6+
>
7+
> In these exercises, we are not focusing on performance, so it's important to focus on making your solution work correctly the first time you attempt to solve a problem.
8+
9+
To re-iterate, Trie (pronounced "try") is a tree-like data structure that stores a dynamic set of strings, typically used to facilitate operations like searching, insertion, and deletion. Tries are particularly useful for tasks that require quick lookups of strings with a common prefix, such as in text autocomplete or in a Router implementation to find the matching paths.
10+
11+
Here's an illustration that shows how does a `Trie` look like in theory:
12+
13+
![](/assets/imgs/trie-overview.png)
14+
15+
Here's how you can visualize the Trie above based on the words "OK", "TO", "CAR", "CAT", and "CUP":
16+
17+
## Root Node
18+
19+
The Trie starts with a root node that doesn't hold any character. It serves as the starting point of the Trie.
20+
21+
```bash
22+
Root
23+
/ | \
24+
T O C
25+
```
26+
27+
- **Level 1**: You have the characters "O", "T", and "C" branching from the root node.
28+
29+
- **Level 2 and Beyond**: These nodes further branch out to form the words.
30+
31+
- "O" branches to "K", completing the word "OK".
32+
- "T" branches to "O", completing the word "TO".
33+
- "C" branches to "A" and "U":
34+
- "A" further branches to "R" for "CAR" and "T" for "CAT".
35+
- "U" further branches to "P", completing the word "CUP".
36+
37+
## End of the word
38+
39+
The "end of the word" is often represented by a boolean flag at a node to signify that the path from the root of the Trie to that node corresponds to a complete word. This flag helps distinguish between a string that is merely a prefix and one that is a full word in the Trie.
40+
41+
For example, consider a Trie that stores the words "car", "cat", and "cup". The node corresponding to the last 't' in "cat" and the last 'p' in "cup" would have the end-of-word marker, indicating that they are complete words, as opposed to just prefixes. Same for 'k' in "ok" and 'o' in "to"
42+
43+
By doing so, if someone searches for "ca" it should not return true, since we only stored "cat" and "car" where as "ca" is just a prefix.
44+
45+
Here's an another illustration to explain the "end-of-word" (EOW):
46+
47+
![](/assets/imgs/trie-eow.png)
48+
49+
## Challenge 1: Basic Trie with `insert` Method
50+
51+
In this first challenge, your task is to implement a Trie data structure with only one functionality: inserting a word into the Trie.
52+
53+
### Requirements
54+
55+
1. Create a class called `Trie`.
56+
57+
2. Implement an `insert(word)` method that takes a string `word` and inserts it into the Trie.
58+
59+
### More details
60+
61+
1. **Initialization**: You'll begin with a root node. This node will be the starting point for all word insertions, and it won't store any character itself.
62+
63+
2. **Traversal**: For each character in the word you want to insert, you'll traverse the Trie from the root node, going as far down as the current character sequence allows.
64+
65+
3. **Node Creation**: If a character in the word doesn't match any child node of the current node:
66+
67+
- Create a new node for that character.
68+
- Link this new node to the current one.
69+
- Move down to this new node and continue with the next character in the word.
70+
71+
4. **End-of-Word**: When you've inserted all the characters for a particular word, mark the last node in some way to indicate that it's the end of a valid word. This could be a boolean property in the node object, for example.
72+
73+
Here's the boilerplate to get you started.
74+
75+
> Note: If you wish, you may code everything from scratch, without using the boilerplate below. I recommend doing it that way if you're comfortable.
76+
77+
```js
78+
class TrieNode {
79+
constructor() {
80+
this.children = {}; // To store TrieNode children with char keys
81+
// this.children = new Map(); You may also use a Map instead.
82+
this.isEndOfWord = false; // To mark the end of a word
83+
}
84+
}
85+
86+
class Trie {
87+
constructor() {
88+
this.root = new TrieNode();
89+
}
90+
91+
insert(word) {
92+
// Your code here
93+
}
94+
}
95+
```
96+
97+
Once implemented, your code should allow operations like:
98+
99+
```js
100+
const trie = new Trie();
101+
trie.insert("hello");
102+
```
103+
104+
Go ahead and implement the `insert` method, and then share your code to help others or to receive feedback in the [Github discussions](https://github.com/ishtms/learn-nodejs-hard-way/discussions) section. I'll try to review all the code submissions and provide feedback if required.
105+
106+
Great. You just implemented a `Trie` which is a Tree data structure. You've also wrote code to traverse a tree which is generally called "tree traversal".
107+
108+
> In case you were not able to figure out what to do, I would still like you to scrap the code you've written and start again from scratch. Get a pen and paper, and visualize it. That way you can convert hard problems into easier ones.
109+
110+
### Solution
111+
112+
```js
113+
class Trie {
114+
constructor() {
115+
this.root = new TrieNode();
116+
}
117+
118+
insert(wordToInsert, node = this.root) {
119+
let length = wordToInsert.length;
120+
if (length == 0) return;
121+
122+
const letters = wordToInsert.split("");
123+
124+
const foundNode = node.children.get(wordToInsert[0]);
125+
126+
if (foundNode) {
127+
this.insert(letters.slice(1).join(""), foundNode);
128+
} else {
129+
let insertedNode = node.add(letters[0], length == 1);
130+
this.insert(letters.slice(1).join(""), insertedNode);
131+
}
132+
}
133+
}
134+
135+
class TrieNode {
136+
constructor() {
137+
/**
138+
* Children will be Map<key(String), node(TrieNode)>
139+
*/
140+
this.isEndOfWord = false;
141+
this.children = new Map();
142+
}
143+
144+
add(letter, _isLastCharacter) {
145+
let newNode = new TrieNode();
146+
this.children.set(letter, newNode);
147+
148+
if (_isLastCharacter) newNode.isEndOfWord = true;
149+
return newNode;
150+
}
151+
}
152+
153+
const trie = new Trie();
154+
trie.insert("node");
155+
trie.insert("note");
156+
trie.insert("not");
157+
```
158+
159+
Let's take a look at the code:
160+
161+
```js
162+
class TrieNode {
163+
constructor() {
164+
this.isEndOfWord = false;
165+
this.children = new Map();
166+
}
167+
}
168+
```
169+
170+
Initializes an instance of the `TrieNode` class. A TrieNode has two properties:
171+
172+
- `isEndOfWord`: A boolean flag that denotes whether the node is the last character of a word in the Trie. Initially set to `false`.
173+
- `children`: A Map to store the children nodes. The keys are letters, and the values are TrieNode objects.
174+
175+
```js
176+
add(letter, _isLastCharacter) {
177+
        let newNode = new TrieNode();
178+
this.children.set(letter, newNode);
179+
180+
if (_isLastCharacter) newNode.isEndOfWord = true;
181+
return newNode;
182+
}
183+
```
184+
185+
I've created a utility method on `TrieNode` to extract some logic from the `Trie.insert` method. This adds a new `TrieNode` as a child of the current node, corresponding to the given letter.
186+
187+
```js
188+
class Trie {
189+
insert(wordToInsert, node = this.root) {
190+
let length = wordToInsert.length;
191+
192+
// Exit condition: If the word to insert is empty, terminate the recursion.
193+
if (length == 0) return;
194+
195+
// Convert the string into an array of its individual characters.
196+
const letters = wordToInsert.split("");
197+
198+
// Attempt to retrieve the TrieNode corresponding to the first letter
199+
// of the word from the children of the current node.
200+
const foundNode = node.children.get(wordToInsert[0]);
201+
202+
if (foundNode) {
203+
// The first letter already exists as a child of the current node.
204+
// Continue inserting the remaining substring (sans the first letter)
205+
// starting from this found node.
206+
this.insert(letters.slice(1).join(""), foundNode);
207+
} else {
208+
// The first letter doesn't exist in the children of the current node.
209+
// Create a new TrieNode for this letter and insert it as a child of the current node.
210+
// Also, set the node's 'isEndOfWord' flag if this is the last character of the word.
211+
let insertedNode = node.add(letters[0], length == 1);
212+
213+
// Continue inserting the remaining substring (without the first letter)
214+
// starting from this new node.
215+
this.insert(letters.slice(1).join(""), insertedNode);
216+
}
217+
}
218+
}
219+
```
220+
221+
## Challenge 2: Implement `search` method
222+
223+
Now that we have a Trie with insertion capabilities, let's add a `search` method.
224+
225+
### Requirements
226+
227+
1. Add a `search(word)` method to the `Trie` class.
228+
2. The method should return `true` if the word exists in the Trie and `false` otherwise.
229+
230+
### More details
231+
232+
1. **Start at the Root**: Begin your search at the root node.
233+
2. **Traversal**: For each character in the word, traverse down the Trie, going from one node to its child that corresponds to the next character.
234+
3. **Word Existence**: If you reach a node that is marked as the end of a word (`isEndOfWord = true`), and you've exhausted all the characters in the word you're searching for, then the word exists in the Trie.
235+
236+
Once implemented, your code should allow:
237+
238+
```js
239+
const trie = new Trie();
240+
trie.insert("code");
241+
trie.insert("coding");
242+
243+
let found = trie.search("code");
244+
console.log(found); // true
245+
246+
found = trie.search("cod");
247+
console.log(found); // false
248+
```
249+
250+
Go ahead and implement the `Trie.search` method. Don't read anything below before implementing it yourself.
251+
252+
If you are having trouble or are stuck, here are some hints to help you with the implementation -
253+
254+
### Hints
255+
256+
1. **Starting Point**: Similar to the `insert` method, you'll start at the root node and traverse the Trie based on the characters in the word you're searching for.
257+
258+
2. **Character Check**: For each character in the word, check if there's a child node for that character from the current node you're at.
259+
260+
- **If Yes**: Move to that child node.
261+
- **If No**: Return `false`, as the word can't possibly exist in the Trie.
262+
263+
3. **End-of-Word Check**: If you've reached the last character of the word, check the `isEndOfWord` property of the current node. If it's `true`, the word exists in the Trie; otherwise, it doesn't.
264+
265+
4. **Recursion or Loop**: You can choose to implement this method either recursively or iteratively.
266+
267+
- **Recursion**: If you opt for recursion, you might want to include an additional parameter in the `search` method for the current node, similar to how you did it for the `insert` method.
268+
- **Loop**: If you prefer loops, you can use a `for` loop to go through each character in the word, updating your current node as you go along.
269+
270+
5. **Return Value**: Don't forget to return `true` or `false` to indicate whether the word exists in the Trie.
271+
272+
Good luck!
273+
274+
### Solution
275+
276+
I chose to implement tree traversal using a for loop this time, to showcase different ways of doing things. I usually prefer for-loops over recursion most of the time, due to the over head of function calls.
277+
278+
```js
279+
search(word) {
280+
// Initialize 'currentNode' to the root node of the Trie.
281+
let currentNode = this.root;
282+
283+
// Loop through each character in the input word.
284+
for (let index = 0; index < word.length; index++) {
285+
286+
// Check if the current character exists as a child node
287+
// of the 'currentNode'.
288+
if (currentNode.children.has(word[index])) {
289+
290+
// If it does, update 'currentNode' to this child node.
291+
currentNode = currentNode.children.get(word[index]);
292+
} else {
293+
294+
// If it doesn't, the word is not in the Trie. Return false.
295+
return false;
296+
}
297+
}
298+
299+
// After looping through all the characters, check if the 'currentNode'
300+
// marks the end of a word in the Trie.
301+
return currentNode.isEndOfWord;
302+
}
303+
```
304+
305+
Awesome work. Now you know the basics of the `Trie` data structure and how to implement it. In the next exercise, we'll implement our `Router` from scratch! The next exercise will be more challenging and exhaustive.

0 commit comments

Comments
 (0)