Skip to content

Commit 4e87904

Browse files
committed
feat(tries): add Patricia (radix) trie with tests
Signed-off-by: shimmer12 <[email protected]>
1 parent edae757 commit 4e87904

File tree

2 files changed

+473
-0
lines changed

2 files changed

+473
-0
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
package com.thealgorithms.datastructures.tries;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.Objects;
6+
7+
/**
8+
* Patricia (radix) trie for String keys and generic values.
9+
*
10+
* <p>Edges are compressed: each child edge stores a non-empty String label.
11+
* Operations run in O(L) where L is the key length, with small constant factors
12+
* from edge-label comparisons.</p>
13+
*
14+
* <p>Notes:
15+
* <ul>
16+
* <li>Null keys are not allowed (IllegalArgumentException).</li>
17+
* <li>Empty-string key ("") is allowed as a valid key.</li>
18+
* <li>Null values are not allowed (IllegalArgumentException).</li>
19+
* </ul>
20+
* </p>
21+
*/
22+
public final class PatriciaTrie<V> {
23+
24+
/** A trie node with compressed outgoing edges (label -> child). */
25+
private static final class Node<V> {
26+
Map<String, Node<V>> children = new HashMap<>();
27+
boolean hasValue;
28+
V value;
29+
}
30+
31+
private final Node<V> root = new Node<>();
32+
private int size; // number of stored keys
33+
34+
/** Creates an empty Patricia trie. */
35+
public PatriciaTrie() {}
36+
37+
/**
38+
* Inserts or updates the value associated with {@code key}.
39+
*
40+
* @param key the key (non-null; empty string allowed)
41+
* @param value the value (non-null)
42+
* @throws IllegalArgumentException if key or value is null
43+
*/
44+
public void put(String key, V value) {
45+
if (key == null) {
46+
throw new IllegalArgumentException("key must not be null");
47+
}
48+
if (value == null) {
49+
throw new IllegalArgumentException("value must not be null");
50+
}
51+
insert(root, key, value);
52+
}
53+
54+
/**
55+
* Returns the value associated with {@code key}, or {@code null} if absent.
56+
*
57+
* @param key the key (non-null)
58+
* @return the stored value or {@code null} if key not present
59+
* @throws IllegalArgumentException if key is null
60+
*/
61+
public V get(String key) {
62+
if (key == null) {
63+
throw new IllegalArgumentException("key must not be null");
64+
}
65+
Node<V> n = findNode(root, key);
66+
return (n != null && n.hasValue) ? n.value : null;
67+
}
68+
69+
/**
70+
* Returns true if the trie contains {@code key}.
71+
*
72+
* @param key the key (non-null)
73+
* @return true if key is present
74+
* @throws IllegalArgumentException if key is null
75+
*/
76+
public boolean contains(String key) {
77+
if (key == null) {
78+
throw new IllegalArgumentException("key must not be null");
79+
}
80+
Node<V> n = findNode(root, key);
81+
return n != null && n.hasValue;
82+
}
83+
84+
/**
85+
* Removes {@code key} if present.
86+
*
87+
* @param key the key (non-null)
88+
* @return true if the key existed and was removed
89+
* @throws IllegalArgumentException if key is null
90+
*/
91+
public boolean remove(String key) {
92+
if (key == null) {
93+
throw new IllegalArgumentException("key must not be null");
94+
}
95+
return delete(root, key);
96+
}
97+
98+
/**
99+
* Returns true if there exists any key with the given {@code prefix}.
100+
*
101+
* @param prefix non-null prefix (empty prefix matches if trie non-empty)
102+
* @return true if any key starts with {@code prefix}
103+
* @throws IllegalArgumentException if prefix is null
104+
*/
105+
public boolean startsWith(String prefix) {
106+
if (prefix == null) {
107+
throw new IllegalArgumentException("prefix must not be null");
108+
}
109+
if (prefix.isEmpty()) {
110+
return size > 0;
111+
}
112+
Node<V> n = findPrefixNode(root, prefix);
113+
return n != null;
114+
}
115+
116+
/** Number of stored keys. */
117+
public int size() {
118+
return size;
119+
}
120+
121+
/** Returns true if no keys are stored. */
122+
public boolean isEmpty() {
123+
return size == 0;
124+
}
125+
126+
// ---------------- internal helpers ----------------
127+
128+
private void insert(Node<V> node, String key, V value) {
129+
// Special case: empty remaining key => store at node
130+
if (key.isEmpty()) {
131+
if (!node.hasValue) {
132+
size++;
133+
}
134+
node.hasValue = true;
135+
node.value = value;
136+
return;
137+
}
138+
139+
// Find a child edge with a non-zero common prefix with 'key'
140+
for (Map.Entry<String, Node<V>> e : node.children.entrySet()) {
141+
String edge = e.getKey();
142+
int cpl = commonPrefixLen(edge, key);
143+
if (cpl == 0) {
144+
continue;
145+
}
146+
147+
// Case A: Edge fully matches the remaining key (edge == key)
148+
if (cpl == edge.length() && cpl == key.length()) {
149+
Node<V> child = e.getValue();
150+
if (!child.hasValue) {
151+
size++;
152+
}
153+
child.hasValue = true;
154+
child.value = value;
155+
return;
156+
}
157+
158+
// Case B: Key is longer (edge is full prefix of key) => descend
159+
if (cpl == edge.length() && cpl < key.length()) {
160+
Node<V> child = e.getValue();
161+
String rest = key.substring(cpl);
162+
insert(child, rest, value);
163+
// After recursion, maybe compact child (not required here)
164+
return;
165+
}
166+
167+
// Case C: Edge longer (key is full prefix of edge) OR partial split
168+
// Need to split the existing edge.
169+
// Split into 'prefix' (common), and two suffix edges.
170+
String prefix = edge.substring(0, cpl);
171+
String edgeSuffix = edge.substring(cpl); // might be non-empty
172+
String keySuffix = key.substring(cpl); // might be empty or non-empty
173+
174+
// Create an intermediate node for 'prefix'
175+
Node<V> mid = new Node<>();
176+
node.children.remove(edge);
177+
node.children.put(prefix, mid);
178+
179+
// Old child moves under 'edgeSuffix'
180+
Node<V> oldChild = e.getValue();
181+
if (!edgeSuffix.isEmpty()) {
182+
mid.children.put(edgeSuffix, oldChild);
183+
} else {
184+
// edgeSuffix empty means 'edge' == 'prefix'; just link child
185+
// (handled by not adding anything)
186+
mid.children.put("", oldChild); // should not happen since cpl < edge.length()
187+
}
188+
189+
// If keySuffix empty => store value at mid
190+
if (keySuffix.isEmpty()) {
191+
if (!mid.hasValue) {
192+
size++;
193+
}
194+
mid.hasValue = true;
195+
mid.value = value;
196+
} else {
197+
// Add a new leaf under keySuffix
198+
Node<V> leaf = new Node<>();
199+
leaf.hasValue = true;
200+
leaf.value = value;
201+
mid.children.put(keySuffix, leaf);
202+
}
203+
return;
204+
}
205+
206+
// No common prefix with any child => add new edge directly
207+
Node<V> leaf = new Node<>();
208+
leaf.hasValue = true;
209+
leaf.value = value;
210+
node.children.put(key, leaf);
211+
size++;
212+
}
213+
214+
private Node<V> findNode(Node<V> node, String key) {
215+
if (key.isEmpty()) {
216+
return node;
217+
}
218+
for (Map.Entry<String, Node<V>> e : node.children.entrySet()) {
219+
String edge = e.getKey();
220+
int cpl = commonPrefixLen(edge, key);
221+
if (cpl == 0) {
222+
continue;
223+
}
224+
if (cpl == edge.length()) {
225+
// Edge fully matches a prefix of key
226+
String rest = key.substring(cpl);
227+
return findNode(e.getValue(), rest);
228+
} else {
229+
// Partial match but edge not fully consumed => key absent
230+
return null;
231+
}
232+
}
233+
return null;
234+
}
235+
236+
private Node<V> findPrefixNode(Node<V> node, String prefix) {
237+
if (prefix.isEmpty()) {
238+
return node;
239+
}
240+
for (Map.Entry<String, Node<V>> e : node.children.entrySet()) {
241+
String edge = e.getKey();
242+
int cpl = commonPrefixLen(edge, prefix);
243+
if (cpl == 0) {
244+
continue;
245+
}
246+
if (cpl == prefix.length()) {
247+
// consumed the whole prefix: prefix exists in this subtree
248+
return e.getValue();
249+
}
250+
if (cpl == edge.length()) {
251+
// consume edge, continue with remaining prefix
252+
String rest = prefix.substring(cpl);
253+
return findPrefixNode(e.getValue(), rest);
254+
}
255+
// partial split where neither fully consumed => no such prefix path
256+
return null;
257+
}
258+
return null;
259+
}
260+
261+
private boolean delete(Node<V> node, String key) {
262+
if (key.isEmpty()) {
263+
if (!node.hasValue) {
264+
return false;
265+
}
266+
node.hasValue = false;
267+
node.value = null;
268+
size--;
269+
// After removing value at this node, maybe merge if only one child
270+
// (merging handled by caller via cleanup step)
271+
return true;
272+
}
273+
274+
// Find matching child by common prefix
275+
for (Map.Entry<String, Node<V>> e : node.children.entrySet()) {
276+
String edge = e.getKey();
277+
int cpl = commonPrefixLen(edge, key);
278+
if (cpl == 0) {
279+
continue;
280+
}
281+
if (cpl < edge.length()) {
282+
// Partial overlap (edge not fully matched) -> key not present
283+
return false;
284+
}
285+
// Edge fully matched; go deeper
286+
String rest = key.substring(cpl);
287+
Node<V> child = e.getValue();
288+
boolean removed = delete(child, rest);
289+
if (!removed) {
290+
return false;
291+
}
292+
// Cleanup/merge after successful deletion
293+
mergeIfNeeded(node, edge, child);
294+
return true;
295+
}
296+
return false;
297+
}
298+
299+
/**
300+
* If the child at {@code parent.children[edge]} can be merged up (no value and
301+
* a single child), compress the two edges into one. Also, if the child has no
302+
* value and no children, remove it.
303+
*/
304+
private void mergeIfNeeded(Node<V> parent, String edge, Node<V> child) {
305+
if (child.hasValue) {
306+
// Can't merge if child holds a value
307+
return;
308+
}
309+
int deg = child.children.size();
310+
if (deg == 0) {
311+
// Remove empty child
312+
parent.children.remove(edge);
313+
return;
314+
}
315+
if (deg == 1) {
316+
// Merge child's only edge into parent edge: edge + subEdge
317+
Map.Entry<String, Node<V>> only = child.children.entrySet().iterator().next();
318+
String subEdge = only.getKey();
319+
Node<V> grand = only.getValue();
320+
321+
parent.children.remove(edge);
322+
parent.children.put(edge + subEdge, grand);
323+
}
324+
}
325+
326+
/** Returns length of common prefix of a and b (0..min(a.length,b.length)). */
327+
private static int commonPrefixLen(String a, String b) {
328+
int n = Math.min(a.length(), b.length());
329+
int i = 0;
330+
while (i < n && a.charAt(i) == b.charAt(i)) {
331+
i++;
332+
}
333+
return i;
334+
}
335+
336+
@Override
337+
public int hashCode() {
338+
// not used by algorithms; keep minimal but deterministic with size
339+
return Objects.hash(size);
340+
}
341+
342+
@Override
343+
public boolean equals(Object obj) {
344+
// Structural equality is not required; keep reference equality
345+
return this == obj;
346+
}
347+
}

0 commit comments

Comments
 (0)