From eae90b0db587c7fd7fcbd3cc8be4139e7d84a36c Mon Sep 17 00:00:00 2001 From: ForAeons Date: Sun, 17 Mar 2024 16:23:10 +0800 Subject: [PATCH 1/3] Add trie data structure --- .../java/seedu/address/commons/util/Trie.java | 126 ++++++++++++++++++ .../seedu/address/commons/util/TrieNode.java | 93 +++++++++++++ .../address/commons/util/TrieNodeTest.java | 51 +++++++ .../seedu/address/commons/util/TrieTest.java | 112 ++++++++++++++++ 4 files changed, 382 insertions(+) create mode 100644 src/main/java/seedu/address/commons/util/Trie.java create mode 100644 src/main/java/seedu/address/commons/util/TrieNode.java create mode 100644 src/test/java/seedu/address/commons/util/TrieNodeTest.java create mode 100644 src/test/java/seedu/address/commons/util/TrieTest.java diff --git a/src/main/java/seedu/address/commons/util/Trie.java b/src/main/java/seedu/address/commons/util/Trie.java new file mode 100644 index 00000000000..4ff89a9f7d4 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/Trie.java @@ -0,0 +1,126 @@ +package seedu.address.commons.util; + +/** + * A class that represents a Trie data structure. + */ +public class Trie { + private final TrieNode root; + + /** + * Constructor for Trie. Accepts a list of words to be inserted into the Trie. + * @param words the words to be inserted into the Trie + */ + public Trie(String... words) { + this.root = new TrieNode(null); + for (String word : words) { + insert(word); + } + } + + //@@author eugenp + //Reused from https://www.baeldung.com/trie-java with minor modifications + /** + * Insert a word into the Trie. + * @author eugenp + * @param word the word to be inserted + */ + public void insert(String word) { + TrieNode current = root; + for (char c : word.toCharArray()) { + if (current.getChild(c) == null) { + current.setChild(c); + } + current = current.getChild(c); + } + current.setEndOfWord(true); + } + //@@author + + //@@author eugenp + //Reused from https://www.baeldung.com/trie-java with minor modifications + /** + * Delete a word from the Trie. + * @author eugenp + * @param word the word to be deleted + */ + public void delete(String word) { + delete(root, word, 0); + } + //@@author + + //@@author eugenp + //Reused from https://www.baeldung.com/trie-java with minor modifications + private boolean delete(TrieNode current, String word, int index) { + if (index == word.length()) { + if (!current.isEndOfWord()) { + return false; + } + current.setEndOfWord(false); + return current.getChildren().isEmpty(); + } + + char c = word.charAt(index); + TrieNode node = current.getChild(c); + if (node == null) { + return false; + } + + boolean shouldDeleteCurrentNode = delete(node, word, index + 1) && !node.isEndOfWord(); + if (shouldDeleteCurrentNode) { + current.deleteChild(c); + return current.getChildren().isEmpty(); + } + + return false; + } + //@@author + + //@@author eugenp + //Reused from https://www.baeldung.com/trie-java with minor modifications + /** + * Search for a word in the Trie. + * @author eugenp + * @param word the word to be searched + * @return true if the word is found, false otherwise + */ + public boolean search(String word) { + TrieNode current = root; + for (char c : word.toCharArray()) { + if (current.getChild(c) == null) { + return false; + } + current = current.getChild(c); + } + return current.isEndOfWord(); + } + //@@author + + /** + * Find the first word in the Trie that starts with the given prefix. + * @param prefix the prefix to be searched + * @return the first word that starts with the given prefix. If no such word exists, return null. + */ + public String findFirstWordWithPrefix(String prefix) { + TrieNode current = root; + StringBuilder sb = new StringBuilder(); + for (char c : prefix.toCharArray()) { + if (current.getChild(c) == null) { + return null; + } + sb.append(c); + current = current.getChild(c); + } + return findFirstWordWithPrefixHelper(current, sb); + } + + private String findFirstWordWithPrefixHelper(TrieNode current, StringBuilder sb) { + if (current.isEndOfWord()) { + return sb.toString(); + } + for (char c : current.getChildren().keySet()) { + sb.append(c); + return findFirstWordWithPrefixHelper(current.getChild(c), sb); + } + return null; + } +} diff --git a/src/main/java/seedu/address/commons/util/TrieNode.java b/src/main/java/seedu/address/commons/util/TrieNode.java new file mode 100644 index 00000000000..53bfb81914f --- /dev/null +++ b/src/main/java/seedu/address/commons/util/TrieNode.java @@ -0,0 +1,93 @@ +package seedu.address.commons.util; + +import java.util.HashMap; + +/** + * A class that represents a TrieNode data structure. + */ +public class TrieNode { + private final HashMap children; + private final Character character; + private boolean isEndOfWord; + + /** + * Constructor for TrieNode. + */ + public TrieNode(Character c) { + this.children = new HashMap<>(); + this.character = c; + this.isEndOfWord = false; + } + + /** + * Retrieve the child node of the given character. + * @param c the character of the child TrieNode to be retrieved + * @return the child TrieNode of the given character + */ + public TrieNode getChild(char c) { + return children.get(c); + } + + /** + * Retrieve all children + * @return the children of the current TrieNode + */ + public HashMap getChildren() { + return children; + } + + /** + * Set a TrieNode as a child of the current TrieNode. + * @param c the character of the child TrieNode + */ + public void setChild(char c) { + this.children.put(c, new TrieNode(c)); + } + + /** + * Delete a child TrieNode. + * @param c the character of the child TrieNode + */ + public void deleteChild(char c) { + this.children.remove(c); + } + + /** + * Set end of word. + */ + public void setEndOfWord(boolean isEndOfWord) { + this.isEndOfWord = isEndOfWord; + } + + /** + * Retrieve the end of word status. + */ + public boolean isEndOfWord() { + return isEndOfWord; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof TrieNode)) { + return false; + } + + TrieNode otherTrieNode = (TrieNode) other; + return otherTrieNode.character.equals(this.character) + && otherTrieNode.isEndOfWord == this.isEndOfWord; + } + + @Override + public int hashCode() { + return character.hashCode(); + } + + @Override + public String toString() { + return String.format("TrieNode: %s", this.character); + } +} diff --git a/src/test/java/seedu/address/commons/util/TrieNodeTest.java b/src/test/java/seedu/address/commons/util/TrieNodeTest.java new file mode 100644 index 00000000000..27bc1325205 --- /dev/null +++ b/src/test/java/seedu/address/commons/util/TrieNodeTest.java @@ -0,0 +1,51 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class TrieNodeTest { + + @Test + void getChild() { + TrieNode trieNode = new TrieNode('a'); + assertNull(trieNode.getChild('b')); + + trieNode.setChild('b'); + assertNotNull(trieNode.getChild('b')); + + trieNode.setChild('c'); + assertNotNull(trieNode.getChild('c')); + + trieNode.deleteChild('b'); + assertNull(trieNode.getChild('b')); + } + + @Test + void getChildren() { + TrieNode trieNode = new TrieNode('a'); + assertNotNull(trieNode.getChildren()); + + assert(trieNode.getChildren().isEmpty()); + + trieNode.setChild('b'); + trieNode.setChild('c'); + assertEquals(2, trieNode.getChildren().size()); + + trieNode.deleteChild('b'); + assertEquals(1, trieNode.getChildren().size()); + } + @Test + void testEquals() { + TrieNode trieNode = new TrieNode('a'); + TrieNode trieNode2 = new TrieNode('a'); + + assertEquals(trieNode, trieNode2); + + trieNode.setEndOfWord(true); + assertNotEquals(trieNode, trieNode2); + } +} diff --git a/src/test/java/seedu/address/commons/util/TrieTest.java b/src/test/java/seedu/address/commons/util/TrieTest.java new file mode 100644 index 00000000000..f6f04b6c4d1 --- /dev/null +++ b/src/test/java/seedu/address/commons/util/TrieTest.java @@ -0,0 +1,112 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class TrieTest { + + @Test + void search() { + Trie trie = new Trie(); + trie.insert("hello"); + trie.insert("world"); + trie.insert("hell"); + trie.insert("word"); + trie.insert("work"); + trie.insert("wording"); + + assertTrue(trie.search("hello")); + assertTrue(trie.search("world")); + assertTrue(trie.search("hell")); + assertTrue(trie.search("word")); + assertTrue(trie.search("work")); + assertTrue(trie.search("wording")); + assertFalse(trie.search("he")); + assertFalse(trie.search("wor")); + assertFalse(trie.search("wordings")); + } + + @Test + void delete() { + Trie trie = new Trie( + "hello", + "world", + "hell", + "word", + "work", + "wording" + ); + + trie.delete("hello"); + assertFalse(trie.search("hello")); + assertTrue(trie.search("world")); + assertTrue(trie.search("hell")); + assertTrue(trie.search("word")); + assertTrue(trie.search("work")); + assertTrue(trie.search("wording")); + + trie.delete("world"); + assertFalse(trie.search("hello")); + assertFalse(trie.search("world")); + assertTrue(trie.search("hell")); + assertTrue(trie.search("word")); + assertTrue(trie.search("work")); + assertTrue(trie.search("wording")); + + trie.delete("hell"); + assertFalse(trie.search("hello")); + assertFalse(trie.search("world")); + assertFalse(trie.search("hell")); + assertTrue(trie.search("word")); + assertTrue(trie.search("work")); + assertTrue(trie.search("wording")); + + trie.delete("word"); + assertFalse(trie.search("hello")); + assertFalse(trie.search("world")); + assertFalse(trie.search("hell")); + assertFalse(trie.search("word")); + assertTrue(trie.search("work")); + assertTrue(trie.search("wording")); + + trie.delete("work"); + assertFalse(trie.search("hello")); + assertFalse(trie.search("world")); + assertFalse(trie.search("hell")); + assertFalse(trie.search("word")); + assertFalse(trie.search("work")); + assertTrue(trie.search("wording")); + + trie.delete("wording"); + assertFalse(trie.search("hello")); + assertFalse(trie.search("world")); + assertFalse(trie.search("hell")); + assertFalse(trie.search("word")); + assertFalse(trie.search("work")); + assertFalse(trie.search("wording")); + } + + @Test + void findFirstWordWithPrefix() { + Trie trie = new Trie( + "hello", + "world", + "hell", + "word", + "work", + "wording" + ); + + assertEquals("hell", trie.findFirstWordWithPrefix("he")); + assertEquals("hello", trie.findFirstWordWithPrefix("hello")); + assertEquals("word", trie.findFirstWordWithPrefix("wor")); + assertEquals("world", trie.findFirstWordWithPrefix("world")); + assertEquals("wording", trie.findFirstWordWithPrefix("wording")); + assertNull(trie.findFirstWordWithPrefix("a")); + assertNull(trie.findFirstWordWithPrefix("ab")); + } +} From cca24d9446fdd36c05285e81165ba5a18ca82c4d Mon Sep 17 00:00:00 2001 From: ForAeons Date: Sun, 17 Mar 2024 16:25:33 +0800 Subject: [PATCH 2/3] Acknowledge code referenced in devguide md file --- docs/DeveloperGuide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index b2e479ccd99..eeafa24f0a2 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -13,6 +13,7 @@ ## **Acknowledgements** +* Trie implementation is reused from [eugenp's tutorials](https://github.com/eugenp/tutorials) with minor modifications. _{ list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well }_ -------------------------------------------------------------------------------------------------------------------- From fccdf65963d38412855e6bacddda794140a8b1e6 Mon Sep 17 00:00:00 2001 From: ForAeons Date: Tue, 19 Mar 2024 09:47:52 +0800 Subject: [PATCH 3/3] Add test cases for TriNode --- .../seedu/address/commons/util/TrieNode.java | 5 ++++- .../address/commons/util/TrieNodeTest.java | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/address/commons/util/TrieNode.java b/src/main/java/seedu/address/commons/util/TrieNode.java index 53bfb81914f..37796fd1f6b 100644 --- a/src/main/java/seedu/address/commons/util/TrieNode.java +++ b/src/main/java/seedu/address/commons/util/TrieNode.java @@ -88,6 +88,9 @@ public int hashCode() { @Override public String toString() { - return String.format("TrieNode: %s", this.character); + return String.format("TrieNode: %s%s", + this.character, + this.isEndOfWord ? "\0" : "" + ); } } diff --git a/src/test/java/seedu/address/commons/util/TrieNodeTest.java b/src/test/java/seedu/address/commons/util/TrieNodeTest.java index 27bc1325205..8a8379c14c8 100644 --- a/src/test/java/seedu/address/commons/util/TrieNodeTest.java +++ b/src/test/java/seedu/address/commons/util/TrieNodeTest.java @@ -41,6 +41,11 @@ void getChildren() { @Test void testEquals() { TrieNode trieNode = new TrieNode('a'); + + assertEquals(trieNode, trieNode); + assertNotEquals(trieNode, null); + assertNotEquals(trieNode, "a"); + TrieNode trieNode2 = new TrieNode('a'); assertEquals(trieNode, trieNode2); @@ -48,4 +53,21 @@ void testEquals() { trieNode.setEndOfWord(true); assertNotEquals(trieNode, trieNode2); } + + @Test + void testHashCode() { + TrieNode trieNode = new TrieNode('a'); + TrieNode trieNode2 = new TrieNode('a'); + + assertEquals(trieNode.hashCode(), trieNode2.hashCode()); + } + + @Test + void testToString() { + TrieNode trieNode = new TrieNode('a'); + assertEquals("TrieNode: a", trieNode.toString()); + + trieNode.setEndOfWord(true); + assertEquals("TrieNode: a\0", trieNode.toString()); + } }