Skip to content

Commit 336ca10

Browse files
author
Jeevan Yewale
committed
Add Ternary Search Tree implementation with comprehensive tests
Signed-off-by: Jeevan Yewale <jeevanyewale4@gmial.com>
1 parent 1b014a2 commit 336ca10

File tree

2 files changed

+348
-0
lines changed

2 files changed

+348
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.thealgorithms.datastructures.trees;
2+
3+
/**
4+
* Ternary Search Tree implementation for efficient string storage and retrieval.
5+
*
6+
* A Ternary Search Tree (TST) is a data structure that combines the time efficiency
7+
* of digital tries with the space efficiency of binary search trees.
8+
*
9+
* Time Complexity:
10+
* - Insert: O(log n) average, O(n) worst case
11+
* - Search: O(log n) average, O(n) worst case
12+
* - Delete: O(log n) average, O(n) worst case
13+
*
14+
* Space Complexity: O(n) where n is the number of characters
15+
*
16+
* @see <a href="https://en.wikipedia.org/wiki/Ternary_search_tree">Ternary Search Tree</a>
17+
* @author JeevanYewale
18+
*/
19+
public final class TernarySearchTree {
20+
21+
private Node root;
22+
23+
private static class Node {
24+
char data;
25+
boolean isEnd;
26+
Node left, middle, right;
27+
28+
Node(char data) {
29+
this.data = data;
30+
this.isEnd = false;
31+
}
32+
}
33+
34+
/**
35+
* Inserts a word into the ternary search tree.
36+
*
37+
* @param word the word to insert
38+
* @throws IllegalArgumentException if word is null or empty
39+
*/
40+
public void insert(String word) {
41+
if (word == null || word.isEmpty()) {
42+
throw new IllegalArgumentException("Word cannot be null or empty");
43+
}
44+
root = insert(root, word, 0);
45+
}
46+
47+
private Node insert(Node node, String word, int index) {
48+
char c = word.charAt(index);
49+
50+
if (node == null) {
51+
node = new Node(c);
52+
}
53+
54+
if (c < node.data) {
55+
node.left = insert(node.left, word, index);
56+
} else if (c > node.data) {
57+
node.right = insert(node.right, word, index);
58+
} else {
59+
if (index < word.length() - 1) {
60+
node.middle = insert(node.middle, word, index + 1);
61+
} else {
62+
node.isEnd = true;
63+
}
64+
}
65+
66+
return node;
67+
}
68+
69+
/**
70+
* Searches for a word in the ternary search tree.
71+
*
72+
* @param word the word to search for
73+
* @return true if the word exists, false otherwise
74+
* @throws IllegalArgumentException if word is null or empty
75+
*/
76+
public boolean search(String word) {
77+
if (word == null || word.isEmpty()) {
78+
throw new IllegalArgumentException("Word cannot be null or empty");
79+
}
80+
return search(root, word, 0);
81+
}
82+
83+
private boolean search(Node node, String word, int index) {
84+
if (node == null) {
85+
return false;
86+
}
87+
88+
char c = word.charAt(index);
89+
90+
if (c < node.data) {
91+
return search(node.left, word, index);
92+
} else if (c > node.data) {
93+
return search(node.right, word, index);
94+
} else {
95+
if (index == word.length() - 1) {
96+
return node.isEnd;
97+
}
98+
return search(node.middle, word, index + 1);
99+
}
100+
}
101+
102+
/**
103+
* Checks if any word in the tree starts with the given prefix.
104+
*
105+
* @param prefix the prefix to search for
106+
* @return true if any word starts with the prefix, false otherwise
107+
* @throws IllegalArgumentException if prefix is null or empty
108+
*/
109+
public boolean startsWith(String prefix) {
110+
if (prefix == null || prefix.isEmpty()) {
111+
throw new IllegalArgumentException("Prefix cannot be null or empty");
112+
}
113+
return startsWith(root, prefix, 0);
114+
}
115+
116+
private boolean startsWith(Node node, String prefix, int index) {
117+
if (node == null) {
118+
return false;
119+
}
120+
121+
if (index == prefix.length()) {
122+
return true;
123+
}
124+
125+
char c = prefix.charAt(index);
126+
127+
if (c < node.data) {
128+
return startsWith(node.left, prefix, index);
129+
} else if (c > node.data) {
130+
return startsWith(node.right, prefix, index);
131+
} else {
132+
return startsWith(node.middle, prefix, index + 1);
133+
}
134+
}
135+
136+
/**
137+
* Deletes a word from the ternary search tree.
138+
*
139+
* @param word the word to delete
140+
* @return true if the word was deleted, false if it didn't exist
141+
* @throws IllegalArgumentException if word is null or empty
142+
*/
143+
public boolean delete(String word) {
144+
if (word == null || word.isEmpty()) {
145+
throw new IllegalArgumentException("Word cannot be null or empty");
146+
}
147+
148+
if (!search(word)) {
149+
return false;
150+
}
151+
152+
root = delete(root, word, 0);
153+
return true;
154+
}
155+
156+
private Node delete(Node node, String word, int index) {
157+
if (node == null) {
158+
return null;
159+
}
160+
161+
char c = word.charAt(index);
162+
163+
if (c < node.data) {
164+
node.left = delete(node.left, word, index);
165+
} else if (c > node.data) {
166+
node.right = delete(node.right, word, index);
167+
} else {
168+
if (index == word.length() - 1) {
169+
node.isEnd = false;
170+
} else {
171+
node.middle = delete(node.middle, word, index + 1);
172+
}
173+
}
174+
175+
// Remove node if it's not needed
176+
if (!node.isEnd && node.left == null && node.middle == null && node.right == null) {
177+
return null;
178+
}
179+
180+
return node;
181+
}
182+
183+
/**
184+
* Checks if the tree is empty.
185+
*
186+
* @return true if the tree is empty, false otherwise
187+
*/
188+
public boolean isEmpty() {
189+
return root == null;
190+
}
191+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.thealgorithms.datastructures.trees;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
8+
/**
9+
* Test cases for TernarySearchTree implementation.
10+
*
11+
* @author JeevanYewale
12+
*/
13+
class TernarySearchTreeTest {
14+
15+
private TernarySearchTree tst;
16+
17+
@BeforeEach
18+
void setUp() {
19+
tst = new TernarySearchTree();
20+
}
21+
22+
@Test
23+
void testInsertAndSearch() {
24+
tst.insert("cat");
25+
tst.insert("cats");
26+
tst.insert("up");
27+
tst.insert("bug");
28+
29+
assertTrue(tst.search("cat"));
30+
assertTrue(tst.search("cats"));
31+
assertTrue(tst.search("up"));
32+
assertTrue(tst.search("bug"));
33+
assertFalse(tst.search("ca"));
34+
assertFalse(tst.search("dog"));
35+
}
36+
37+
@Test
38+
void testInsertNullOrEmpty() {
39+
assertThrows(IllegalArgumentException.class, () -> tst.insert(null));
40+
assertThrows(IllegalArgumentException.class, () -> tst.insert(""));
41+
}
42+
43+
@Test
44+
void testSearchNullOrEmpty() {
45+
assertThrows(IllegalArgumentException.class, () -> tst.search(null));
46+
assertThrows(IllegalArgumentException.class, () -> tst.search(""));
47+
}
48+
49+
@Test
50+
void testStartsWith() {
51+
tst.insert("cat");
52+
tst.insert("cats");
53+
tst.insert("car");
54+
tst.insert("card");
55+
56+
assertTrue(tst.startsWith("ca"));
57+
assertTrue(tst.startsWith("cat"));
58+
assertTrue(tst.startsWith("car"));
59+
assertFalse(tst.startsWith("dog"));
60+
assertFalse(tst.startsWith("cb"));
61+
}
62+
63+
@Test
64+
void testStartsWithNullOrEmpty() {
65+
assertThrows(IllegalArgumentException.class, () -> tst.startsWith(null));
66+
assertThrows(IllegalArgumentException.class, () -> tst.startsWith(""));
67+
}
68+
69+
@Test
70+
void testDelete() {
71+
tst.insert("cat");
72+
tst.insert("cats");
73+
tst.insert("car");
74+
75+
assertTrue(tst.delete("cat"));
76+
assertFalse(tst.search("cat"));
77+
assertTrue(tst.search("cats"));
78+
assertTrue(tst.search("car"));
79+
80+
assertFalse(tst.delete("dog"));
81+
assertFalse(tst.delete("cat")); // Already deleted
82+
}
83+
84+
@Test
85+
void testDeleteNullOrEmpty() {
86+
assertThrows(IllegalArgumentException.class, () -> tst.delete(null));
87+
assertThrows(IllegalArgumentException.class, () -> tst.delete(""));
88+
}
89+
90+
@Test
91+
void testIsEmpty() {
92+
assertTrue(tst.isEmpty());
93+
94+
tst.insert("test");
95+
assertFalse(tst.isEmpty());
96+
97+
tst.delete("test");
98+
assertTrue(tst.isEmpty());
99+
}
100+
101+
@Test
102+
void testSingleCharacterWords() {
103+
tst.insert("a");
104+
tst.insert("b");
105+
tst.insert("z");
106+
107+
assertTrue(tst.search("a"));
108+
assertTrue(tst.search("b"));
109+
assertTrue(tst.search("z"));
110+
assertFalse(tst.search("c"));
111+
}
112+
113+
@Test
114+
void testOverlappingWords() {
115+
tst.insert("he");
116+
tst.insert("she");
117+
tst.insert("his");
118+
tst.insert("hers");
119+
120+
assertTrue(tst.search("he"));
121+
assertTrue(tst.search("she"));
122+
assertTrue(tst.search("his"));
123+
assertTrue(tst.search("hers"));
124+
125+
assertTrue(tst.startsWith("he"));
126+
assertTrue(tst.startsWith("h"));
127+
assertFalse(tst.search("her"));
128+
}
129+
130+
@Test
131+
void testCaseSensitivity() {
132+
tst.insert("Cat");
133+
tst.insert("cat");
134+
135+
assertTrue(tst.search("Cat"));
136+
assertTrue(tst.search("cat"));
137+
assertFalse(tst.search("CAT"));
138+
}
139+
140+
@Test
141+
void testLargeDataset() {
142+
String[] words = {"apple", "application", "apply", "appreciate", "approach",
143+
"appropriate", "approve", "approximate", "april", "area"};
144+
145+
for (String word : words) {
146+
tst.insert(word);
147+
}
148+
149+
for (String word : words) {
150+
assertTrue(tst.search(word));
151+
}
152+
153+
assertTrue(tst.startsWith("app"));
154+
assertTrue(tst.startsWith("appr"));
155+
assertFalse(tst.startsWith("orange"));
156+
}
157+
}

0 commit comments

Comments
 (0)