From 336ca10dcc64100bb53dfa4b40827e4f3abeb0bd Mon Sep 17 00:00:00 2001 From: Jeevan Yewale Date: Wed, 21 Jan 2026 14:59:09 +0530 Subject: [PATCH] Add Ternary Search Tree implementation with comprehensive tests Signed-off-by: Jeevan Yewale --- .../trees/TernarySearchTree.java | 191 ++++++++++++++++++ .../trees/TernarySearchTreeTest.java | 157 ++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 src/main/java/com/thealgorithms/datastructures/trees/TernarySearchTree.java create mode 100644 src/test/java/com/thealgorithms/datastructures/trees/TernarySearchTreeTest.java diff --git a/src/main/java/com/thealgorithms/datastructures/trees/TernarySearchTree.java b/src/main/java/com/thealgorithms/datastructures/trees/TernarySearchTree.java new file mode 100644 index 000000000000..0c9b65f9759d --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/TernarySearchTree.java @@ -0,0 +1,191 @@ +package com.thealgorithms.datastructures.trees; + +/** + * Ternary Search Tree implementation for efficient string storage and retrieval. + * + * A Ternary Search Tree (TST) is a data structure that combines the time efficiency + * of digital tries with the space efficiency of binary search trees. + * + * Time Complexity: + * - Insert: O(log n) average, O(n) worst case + * - Search: O(log n) average, O(n) worst case + * - Delete: O(log n) average, O(n) worst case + * + * Space Complexity: O(n) where n is the number of characters + * + * @see Ternary Search Tree + * @author JeevanYewale + */ +public final class TernarySearchTree { + + private Node root; + + private static class Node { + char data; + boolean isEnd; + Node left, middle, right; + + Node(char data) { + this.data = data; + this.isEnd = false; + } + } + + /** + * Inserts a word into the ternary search tree. + * + * @param word the word to insert + * @throws IllegalArgumentException if word is null or empty + */ + public void insert(String word) { + if (word == null || word.isEmpty()) { + throw new IllegalArgumentException("Word cannot be null or empty"); + } + root = insert(root, word, 0); + } + + private Node insert(Node node, String word, int index) { + char c = word.charAt(index); + + if (node == null) { + node = new Node(c); + } + + if (c < node.data) { + node.left = insert(node.left, word, index); + } else if (c > node.data) { + node.right = insert(node.right, word, index); + } else { + if (index < word.length() - 1) { + node.middle = insert(node.middle, word, index + 1); + } else { + node.isEnd = true; + } + } + + return node; + } + + /** + * Searches for a word in the ternary search tree. + * + * @param word the word to search for + * @return true if the word exists, false otherwise + * @throws IllegalArgumentException if word is null or empty + */ + public boolean search(String word) { + if (word == null || word.isEmpty()) { + throw new IllegalArgumentException("Word cannot be null or empty"); + } + return search(root, word, 0); + } + + private boolean search(Node node, String word, int index) { + if (node == null) { + return false; + } + + char c = word.charAt(index); + + if (c < node.data) { + return search(node.left, word, index); + } else if (c > node.data) { + return search(node.right, word, index); + } else { + if (index == word.length() - 1) { + return node.isEnd; + } + return search(node.middle, word, index + 1); + } + } + + /** + * Checks if any word in the tree starts with the given prefix. + * + * @param prefix the prefix to search for + * @return true if any word starts with the prefix, false otherwise + * @throws IllegalArgumentException if prefix is null or empty + */ + public boolean startsWith(String prefix) { + if (prefix == null || prefix.isEmpty()) { + throw new IllegalArgumentException("Prefix cannot be null or empty"); + } + return startsWith(root, prefix, 0); + } + + private boolean startsWith(Node node, String prefix, int index) { + if (node == null) { + return false; + } + + if (index == prefix.length()) { + return true; + } + + char c = prefix.charAt(index); + + if (c < node.data) { + return startsWith(node.left, prefix, index); + } else if (c > node.data) { + return startsWith(node.right, prefix, index); + } else { + return startsWith(node.middle, prefix, index + 1); + } + } + + /** + * Deletes a word from the ternary search tree. + * + * @param word the word to delete + * @return true if the word was deleted, false if it didn't exist + * @throws IllegalArgumentException if word is null or empty + */ + public boolean delete(String word) { + if (word == null || word.isEmpty()) { + throw new IllegalArgumentException("Word cannot be null or empty"); + } + + if (!search(word)) { + return false; + } + + root = delete(root, word, 0); + return true; + } + + private Node delete(Node node, String word, int index) { + if (node == null) { + return null; + } + + char c = word.charAt(index); + + if (c < node.data) { + node.left = delete(node.left, word, index); + } else if (c > node.data) { + node.right = delete(node.right, word, index); + } else { + if (index == word.length() - 1) { + node.isEnd = false; + } else { + node.middle = delete(node.middle, word, index + 1); + } + } + + // Remove node if it's not needed + if (!node.isEnd && node.left == null && node.middle == null && node.right == null) { + return null; + } + + return node; + } + + /** + * Checks if the tree is empty. + * + * @return true if the tree is empty, false otherwise + */ + public boolean isEmpty() { + return root == null; + } +} \ No newline at end of file diff --git a/src/test/java/com/thealgorithms/datastructures/trees/TernarySearchTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/TernarySearchTreeTest.java new file mode 100644 index 000000000000..a3afc57d96bf --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/TernarySearchTreeTest.java @@ -0,0 +1,157 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test cases for TernarySearchTree implementation. + * + * @author JeevanYewale + */ +class TernarySearchTreeTest { + + private TernarySearchTree tst; + + @BeforeEach + void setUp() { + tst = new TernarySearchTree(); + } + + @Test + void testInsertAndSearch() { + tst.insert("cat"); + tst.insert("cats"); + tst.insert("up"); + tst.insert("bug"); + + assertTrue(tst.search("cat")); + assertTrue(tst.search("cats")); + assertTrue(tst.search("up")); + assertTrue(tst.search("bug")); + assertFalse(tst.search("ca")); + assertFalse(tst.search("dog")); + } + + @Test + void testInsertNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> tst.insert(null)); + assertThrows(IllegalArgumentException.class, () -> tst.insert("")); + } + + @Test + void testSearchNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> tst.search(null)); + assertThrows(IllegalArgumentException.class, () -> tst.search("")); + } + + @Test + void testStartsWith() { + tst.insert("cat"); + tst.insert("cats"); + tst.insert("car"); + tst.insert("card"); + + assertTrue(tst.startsWith("ca")); + assertTrue(tst.startsWith("cat")); + assertTrue(tst.startsWith("car")); + assertFalse(tst.startsWith("dog")); + assertFalse(tst.startsWith("cb")); + } + + @Test + void testStartsWithNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> tst.startsWith(null)); + assertThrows(IllegalArgumentException.class, () -> tst.startsWith("")); + } + + @Test + void testDelete() { + tst.insert("cat"); + tst.insert("cats"); + tst.insert("car"); + + assertTrue(tst.delete("cat")); + assertFalse(tst.search("cat")); + assertTrue(tst.search("cats")); + assertTrue(tst.search("car")); + + assertFalse(tst.delete("dog")); + assertFalse(tst.delete("cat")); // Already deleted + } + + @Test + void testDeleteNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> tst.delete(null)); + assertThrows(IllegalArgumentException.class, () -> tst.delete("")); + } + + @Test + void testIsEmpty() { + assertTrue(tst.isEmpty()); + + tst.insert("test"); + assertFalse(tst.isEmpty()); + + tst.delete("test"); + assertTrue(tst.isEmpty()); + } + + @Test + void testSingleCharacterWords() { + tst.insert("a"); + tst.insert("b"); + tst.insert("z"); + + assertTrue(tst.search("a")); + assertTrue(tst.search("b")); + assertTrue(tst.search("z")); + assertFalse(tst.search("c")); + } + + @Test + void testOverlappingWords() { + tst.insert("he"); + tst.insert("she"); + tst.insert("his"); + tst.insert("hers"); + + assertTrue(tst.search("he")); + assertTrue(tst.search("she")); + assertTrue(tst.search("his")); + assertTrue(tst.search("hers")); + + assertTrue(tst.startsWith("he")); + assertTrue(tst.startsWith("h")); + assertFalse(tst.search("her")); + } + + @Test + void testCaseSensitivity() { + tst.insert("Cat"); + tst.insert("cat"); + + assertTrue(tst.search("Cat")); + assertTrue(tst.search("cat")); + assertFalse(tst.search("CAT")); + } + + @Test + void testLargeDataset() { + String[] words = {"apple", "application", "apply", "appreciate", "approach", + "appropriate", "approve", "approximate", "april", "area"}; + + for (String word : words) { + tst.insert(word); + } + + for (String word : words) { + assertTrue(tst.search(word)); + } + + assertTrue(tst.startsWith("app")); + assertTrue(tst.startsWith("appr")); + assertFalse(tst.startsWith("orange")); + } +} \ No newline at end of file