diff --git a/Readme.md b/Readme.md index b587b32..974eadf 100644 --- a/Readme.md +++ b/Readme.md @@ -255,5 +255,17 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy) - [Improving the `Router` API](/chapters/ch06.3-improving-the-router-api.md) - [The need for a `trie`](/chapters/ch06.4-the-need-for-a-trie.md) - [What is a `Trie` anyway?](/chapters/ch06.4-the-need-for-a-trie.md#what-is-a-trie-anyway) +- [Exercise - Implementing a `Trie`](/chapters/ch07.0-ex-implementing-a-trie.md#exercise---implementing-a-trie) + - [Root Node](/chapters/ch07.0-ex-implementing-a-trie.md#root-node) + - [End of the word](/chapters/ch07.0-ex-implementing-a-trie.md#end-of-the-word) + - [Challenge 1: Basic Trie with `insert` Method](/chapters/ch07.0-ex-implementing-a-trie.md#challenge-1-basic-trie-with-insert-method) + - [Requirements](/chapters/ch07.0-ex-implementing-a-trie.md#requirements) + - [More details](/chapters/ch07.0-ex-implementing-a-trie.md#more-details) + - [Solution](/chapters/ch07.0-ex-implementing-a-trie.md#solution) + - [Challenge 2: Implement `search` method](/chapters/ch07.0-ex-implementing-a-trie.md#challenge-2-implement-search-method) + - [Requirements](/chapters/ch07.0-ex-implementing-a-trie.md#requirements-1) + - [More details](/chapters/ch07.0-ex-implementing-a-trie.md#more-details-1) + - [Hints](/chapters/ch07.0-ex-implementing-a-trie.md#hints) + - [Solution](#solution-1) ![](https://uddrapi.com/api/img?page=readme) diff --git a/assets/imgs/Untitled-2023-07-28-1312.png b/assets/imgs/Untitled-2023-07-28-1312.png new file mode 100644 index 0000000..ecfe579 Binary files /dev/null and b/assets/imgs/Untitled-2023-07-28-1312.png differ diff --git a/assets/imgs/compressed/call-stack.png b/assets/imgs/compressed/call-stack.png new file mode 100644 index 0000000..c35a88e Binary files /dev/null and b/assets/imgs/compressed/call-stack.png differ diff --git a/assets/imgs/compressed/client-server.png b/assets/imgs/compressed/client-server.png new file mode 100644 index 0000000..db5d486 Binary files /dev/null and b/assets/imgs/compressed/client-server.png differ diff --git a/assets/imgs/compressed/cover.jpg b/assets/imgs/compressed/cover.jpg new file mode 100644 index 0000000..46a3958 Binary files /dev/null and b/assets/imgs/compressed/cover.jpg differ diff --git a/assets/imgs/compressed/elysia-claim.png b/assets/imgs/compressed/elysia-claim.png new file mode 100644 index 0000000..99da362 Binary files /dev/null and b/assets/imgs/compressed/elysia-claim.png differ diff --git a/assets/imgs/compressed/hello-world.png b/assets/imgs/compressed/hello-world.png new file mode 100644 index 0000000..a47916a Binary files /dev/null and b/assets/imgs/compressed/hello-world.png differ diff --git a/assets/imgs/compressed/idle_memory.png b/assets/imgs/compressed/idle_memory.png new file mode 100644 index 0000000..c228942 Binary files /dev/null and b/assets/imgs/compressed/idle_memory.png differ diff --git a/assets/imgs/compressed/intellisense.png b/assets/imgs/compressed/intellisense.png new file mode 100644 index 0000000..4756704 Binary files /dev/null and b/assets/imgs/compressed/intellisense.png differ diff --git a/assets/imgs/compressed/latency_2.png b/assets/imgs/compressed/latency_2.png new file mode 100644 index 0000000..bd8f8e7 Binary files /dev/null and b/assets/imgs/compressed/latency_2.png differ diff --git a/assets/imgs/compressed/latency_without_max.png b/assets/imgs/compressed/latency_without_max.png new file mode 100644 index 0000000..d06cfd1 Binary files /dev/null and b/assets/imgs/compressed/latency_without_max.png differ diff --git a/assets/imgs/compressed/max_latency.png b/assets/imgs/compressed/max_latency.png new file mode 100644 index 0000000..5b5ecdf Binary files /dev/null and b/assets/imgs/compressed/max_latency.png differ diff --git a/assets/imgs/compressed/mem_const_load.png b/assets/imgs/compressed/mem_const_load.png new file mode 100644 index 0000000..8a929a9 Binary files /dev/null and b/assets/imgs/compressed/mem_const_load.png differ diff --git a/assets/imgs/compressed/mem_const_load_2.png b/assets/imgs/compressed/mem_const_load_2.png new file mode 100644 index 0000000..bd2a7cc Binary files /dev/null and b/assets/imgs/compressed/mem_const_load_2.png differ diff --git a/assets/imgs/compressed/mem_idle_2.png b/assets/imgs/compressed/mem_idle_2.png new file mode 100644 index 0000000..164f4d2 Binary files /dev/null and b/assets/imgs/compressed/mem_idle_2.png differ diff --git a/assets/imgs/compressed/modular-function-ch6.png b/assets/imgs/compressed/modular-function-ch6.png new file mode 100644 index 0000000..4e9bd7c Binary files /dev/null and b/assets/imgs/compressed/modular-function-ch6.png differ diff --git a/assets/imgs/compressed/next.png b/assets/imgs/compressed/next.png new file mode 100644 index 0000000..11ced17 Binary files /dev/null and b/assets/imgs/compressed/next.png differ diff --git a/assets/imgs/compressed/prev.png b/assets/imgs/compressed/prev.png new file mode 100644 index 0000000..9a2396e Binary files /dev/null and b/assets/imgs/compressed/prev.png differ diff --git a/assets/imgs/compressed/referer.png b/assets/imgs/compressed/referer.png new file mode 100644 index 0000000..be35886 Binary files /dev/null and b/assets/imgs/compressed/referer.png differ diff --git a/assets/imgs/compressed/rps.png b/assets/imgs/compressed/rps.png new file mode 100644 index 0000000..5730b19 Binary files /dev/null and b/assets/imgs/compressed/rps.png differ diff --git a/assets/imgs/compressed/rps_2.png b/assets/imgs/compressed/rps_2.png new file mode 100644 index 0000000..ed8deb6 Binary files /dev/null and b/assets/imgs/compressed/rps_2.png differ diff --git a/assets/imgs/compressed/user-agent.png b/assets/imgs/compressed/user-agent.png new file mode 100644 index 0000000..fab5ecd Binary files /dev/null and b/assets/imgs/compressed/user-agent.png differ diff --git a/assets/imgs/trie-eow.png b/assets/imgs/trie-eow.png new file mode 100644 index 0000000..da52666 Binary files /dev/null and b/assets/imgs/trie-eow.png differ diff --git a/assets/imgs/trie-overview.png b/assets/imgs/trie-overview.png new file mode 100644 index 0000000..f602ff3 Binary files /dev/null and b/assets/imgs/trie-overview.png differ diff --git a/chapters/ch06.4-the-need-for-a-trie.md b/chapters/ch06.4-the-need-for-a-trie.md index 544aabd..67a949d 100644 --- a/chapters/ch06.4-the-need-for-a-trie.md +++ b/chapters/ch06.4-the-need-for-a-trie.md @@ -104,3 +104,5 @@ Every node, including `root` will be an object that contain some necessary infor 4. `children`: Any children nodes. (We'll get more deep into this in the upcoming chapters) Enough with the theory. In the next chapter, we'll dive into our very first exercise for this book: **implementing a Trie**. + +[![Read Next](/assets/imgs/next.png)](/chapters/ch07.0-ex-implementing-a-trie.md) diff --git a/chapters/ch07.0-ex-implementing-a-trie.md b/chapters/ch07.0-ex-implementing-a-trie.md new file mode 100644 index 0000000..6d6586c --- /dev/null +++ b/chapters/ch07.0-ex-implementing-a-trie.md @@ -0,0 +1,305 @@ +[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.4-the-need-for-a-trie.md) + +# Exercise - Implementing a `Trie` + +> 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. +> +> 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. + +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. + +Here's an illustration that shows how does a `Trie` look like in theory: + +![](/assets/imgs/trie-overview.png) + +Here's how you can visualize the Trie above based on the words "OK", "TO", "CAR", "CAT", and "CUP": + +## Root Node + +The Trie starts with a root node that doesn't hold any character. It serves as the starting point of the Trie. + +```bash + Root + / | \ + T O C +``` + +- **Level 1**: You have the characters "O", "T", and "C" branching from the root node. + +- **Level 2 and Beyond**: These nodes further branch out to form the words. + + - "O" branches to "K", completing the word "OK". + - "T" branches to "O", completing the word "TO". + - "C" branches to "A" and "U": + - "A" further branches to "R" for "CAR" and "T" for "CAT". + - "U" further branches to "P", completing the word "CUP". + +## End of the word + +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. + +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" + +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. + +Here's an another illustration to explain the "end-of-word" (EOW): + +![](/assets/imgs/trie-eow.png) + +## Challenge 1: Basic Trie with `insert` Method + +In this first challenge, your task is to implement a Trie data structure with only one functionality: inserting a word into the Trie. + +### Requirements + +1. Create a class called `Trie`. + +2. Implement an `insert(word)` method that takes a string `word` and inserts it into the Trie. + +### More details + +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. + +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. + +3. **Node Creation**: If a character in the word doesn't match any child node of the current node: + + - Create a new node for that character. + - Link this new node to the current one. + - Move down to this new node and continue with the next character in the word. + +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. + +Here's the boilerplate to get you started. + +> 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. + +```js +class TrieNode { + constructor() { + this.children = {}; // To store TrieNode children with char keys + // this.children = new Map(); You may also use a Map instead. + this.isEndOfWord = false; // To mark the end of a word + } +} + +class Trie { + constructor() { + this.root = new TrieNode(); + } + + insert(word) { + // Your code here + } +} +``` + +Once implemented, your code should allow operations like: + +```js +const trie = new Trie(); +trie.insert("hello"); +``` + +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. + +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". + +> 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. + +### Solution + +```js +class Trie { + constructor() { + this.root = new TrieNode(); + } + + insert(wordToInsert, node = this.root) { + let length = wordToInsert.length; + if (length == 0) return; + + const letters = wordToInsert.split(""); + + const foundNode = node.children.get(wordToInsert[0]); + + if (foundNode) { + this.insert(letters.slice(1).join(""), foundNode); + } else { + let insertedNode = node.add(letters[0], length == 1); + this.insert(letters.slice(1).join(""), insertedNode); + } + } +} + +class TrieNode { + constructor() { + /** + * Children will be Map + */ + this.isEndOfWord = false; + this.children = new Map(); + } + + add(letter, _isLastCharacter) { + let newNode = new TrieNode(); + this.children.set(letter, newNode); + + if (_isLastCharacter) newNode.isEndOfWord = true; + return newNode; + } +} + +const trie = new Trie(); +trie.insert("node"); +trie.insert("note"); +trie.insert("not"); +``` + +Let's take a look at the code: + +```js +class TrieNode { + constructor() { + this.isEndOfWord = false; + this.children = new Map(); + } +} +``` + +Initializes an instance of the `TrieNode` class. A TrieNode has two properties: + +- `isEndOfWord`: A boolean flag that denotes whether the node is the last character of a word in the Trie. Initially set to `false`. +- `children`: A Map to store the children nodes. The keys are letters, and the values are TrieNode objects. + +```js +add(letter, _isLastCharacter) { +        let newNode = new TrieNode(); + this.children.set(letter, newNode); + + if (_isLastCharacter) newNode.isEndOfWord = true; + return newNode; +} +``` + +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. + +```js +class Trie { + insert(wordToInsert, node = this.root) { + let length = wordToInsert.length; + + // Exit condition: If the word to insert is empty, terminate the recursion. + if (length == 0) return; + + // Convert the string into an array of its individual characters. + const letters = wordToInsert.split(""); + + // Attempt to retrieve the TrieNode corresponding to the first letter + // of the word from the children of the current node. + const foundNode = node.children.get(wordToInsert[0]); + + if (foundNode) { + // The first letter already exists as a child of the current node. + // Continue inserting the remaining substring (sans the first letter) + // starting from this found node. + this.insert(letters.slice(1).join(""), foundNode); + } else { + // The first letter doesn't exist in the children of the current node. + // Create a new TrieNode for this letter and insert it as a child of the current node. + // Also, set the node's 'isEndOfWord' flag if this is the last character of the word. + let insertedNode = node.add(letters[0], length == 1); + + // Continue inserting the remaining substring (without the first letter) + // starting from this new node. + this.insert(letters.slice(1).join(""), insertedNode); + } + } +} +``` + +## Challenge 2: Implement `search` method + +Now that we have a Trie with insertion capabilities, let's add a `search` method. + +### Requirements + +1. Add a `search(word)` method to the `Trie` class. +2. The method should return `true` if the word exists in the Trie and `false` otherwise. + +### More details + +1. **Start at the Root**: Begin your search at the root node. +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. +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. + +Once implemented, your code should allow: + +```js +const trie = new Trie(); +trie.insert("code"); +trie.insert("coding"); + +let found = trie.search("code"); +console.log(found); // true + +found = trie.search("cod"); +console.log(found); // false +``` + +Go ahead and implement the `Trie.search` method. Don't read anything below before implementing it yourself. + +If you are having trouble or are stuck, here are some hints to help you with the implementation - + +### Hints + +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. + +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. + + - **If Yes**: Move to that child node. + - **If No**: Return `false`, as the word can't possibly exist in the Trie. + +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. + +4. **Recursion or Loop**: You can choose to implement this method either recursively or iteratively. + + - **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. + - **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. + +5. **Return Value**: Don't forget to return `true` or `false` to indicate whether the word exists in the Trie. + +Good luck! + +### Solution + +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. + +```js +search(word) { + // Initialize 'currentNode' to the root node of the Trie. + let currentNode = this.root; + + // Loop through each character in the input word. + for (let index = 0; index < word.length; index++) { + + // Check if the current character exists as a child node + // of the 'currentNode'. + if (currentNode.children.has(word[index])) { + + // If it does, update 'currentNode' to this child node. + currentNode = currentNode.children.get(word[index]); + } else { + + // If it doesn't, the word is not in the Trie. Return false. + return false; + } + } + + // After looping through all the characters, check if the 'currentNode' + // marks the end of a word in the Trie. + return currentNode.isEndOfWord; +} +``` + +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.