Skip to content

Commit 267c325

Browse files
authored
Refactor comments and improve remove method
Updated comments for clarity and corrected parameter descriptions in the PatriciaTrie class. Enhanced the remove method to properly handle key removal and cleanup.
1 parent 4e87904 commit 267c325

File tree

1 file changed

+101
-108
lines changed

1 file changed

+101
-108
lines changed

src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java

Lines changed: 101 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
* Patricia (radix) trie for String keys and generic values.
99
*
1010
* <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>
11+
* Operations run in O(L) where L is the key length.</p>
1312
*
14-
* <p>Notes:
13+
* <p>Contract:
1514
* <ul>
1615
* <li>Null keys are not allowed (IllegalArgumentException).</li>
1716
* <li>Empty-string key ("") is allowed as a valid key.</li>
@@ -21,7 +20,7 @@
2120
*/
2221
public final class PatriciaTrie<V> {
2322

24-
/** A trie node with compressed outgoing edges (label -> child). */
23+
/** Node with compressed outgoing edges (label -> child). */
2524
private static final class Node<V> {
2625
Map<String, Node<V>> children = new HashMap<>();
2726
boolean hasValue;
@@ -32,14 +31,14 @@ private static final class Node<V> {
3231
private int size; // number of stored keys
3332

3433
/** Creates an empty Patricia trie. */
35-
public PatriciaTrie() {}
34+
public PatriciaTrie() {
35+
}
3636

3737
/**
3838
* Inserts or updates the value associated with {@code key}.
3939
*
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
40+
* @param key non-null key (empty allowed)
41+
* @param value non-null value
4342
*/
4443
public void put(String key, V value) {
4544
if (key == null) {
@@ -54,9 +53,8 @@ public void put(String key, V value) {
5453
/**
5554
* Returns the value associated with {@code key}, or {@code null} if absent.
5655
*
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
56+
* @param key non-null key
57+
* @return stored value or null
6058
*/
6159
public V get(String key) {
6260
if (key == null) {
@@ -68,10 +66,6 @@ public V get(String key) {
6866

6967
/**
7068
* 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
7569
*/
7670
public boolean contains(String key) {
7771
if (key == null) {
@@ -82,25 +76,43 @@ public boolean contains(String key) {
8276
}
8377

8478
/**
85-
* Removes {@code key} if present.
79+
* Removes the mapping for {@code key} if present.
80+
*
81+
* <p>Fixes for CI failures:
82+
* - Properly removes leaf nodes and decrements size once.
83+
* - Merges redundant pass-through nodes (no value, single child) by
84+
* concatenating edge labels.</p>
8685
*
87-
* @param key the key (non-null)
88-
* @return true if the key existed and was removed
89-
* @throws IllegalArgumentException if key is null
86+
* @param key non-null key
87+
* @return previous value or {@code null} if none
9088
*/
91-
public boolean remove(String key) {
89+
public V remove(String key) {
9290
if (key == null) {
93-
throw new IllegalArgumentException("key must not be null");
91+
throw new IllegalArgumentException("key cannot be null");
9492
}
95-
return delete(root, key);
93+
if (key.isEmpty()) {
94+
if (!root.hasValue) {
95+
return null;
96+
}
97+
V old = root.value;
98+
root.hasValue = false;
99+
root.value = null;
100+
size--;
101+
return old;
102+
}
103+
104+
// container to return "was removed" + old value up the recursion
105+
Object[] removedHolder = new Object[1];
106+
removeRecursive(root, key, removedHolder);
107+
108+
if (removedHolder[0] != null) {
109+
size--;
110+
}
111+
return (V) removedHolder[0];
96112
}
97113

98114
/**
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
115+
* Returns true if any key starts with {@code prefix}.
104116
*/
105117
public boolean startsWith(String prefix) {
106118
if (prefix == null) {
@@ -118,15 +130,16 @@ public int size() {
118130
return size;
119131
}
120132

121-
/** Returns true if no keys are stored. */
133+
/** True if no keys are stored. */
122134
public boolean isEmpty() {
123135
return size == 0;
124136
}
125137

126-
// ---------------- internal helpers ----------------
138+
// ----------------------------------------------------------------------
139+
// Internal helpers
140+
// ----------------------------------------------------------------------
127141

128142
private void insert(Node<V> node, String key, V value) {
129-
// Special case: empty remaining key => store at node
130143
if (key.isEmpty()) {
131144
if (!node.hasValue) {
132145
size++;
@@ -136,15 +149,14 @@ private void insert(Node<V> node, String key, V value) {
136149
return;
137150
}
138151

139-
// Find a child edge with a non-zero common prefix with 'key'
140152
for (Map.Entry<String, Node<V>> e : node.children.entrySet()) {
141153
String edge = e.getKey();
142154
int cpl = commonPrefixLen(edge, key);
143155
if (cpl == 0) {
144156
continue;
145157
}
146158

147-
// Case A: Edge fully matches the remaining key (edge == key)
159+
// edge matches the entire key
148160
if (cpl == edge.length() && cpl == key.length()) {
149161
Node<V> child = e.getValue();
150162
if (!child.hasValue) {
@@ -155,46 +167,38 @@ private void insert(Node<V> node, String key, V value) {
155167
return;
156168
}
157169

158-
// Case B: Key is longer (edge is full prefix of key) => descend
170+
// edge is full prefix of key -> go down
159171
if (cpl == edge.length() && cpl < key.length()) {
160172
Node<V> child = e.getValue();
161173
String rest = key.substring(cpl);
162174
insert(child, rest, value);
163-
// After recursion, maybe compact child (not required here)
164175
return;
165176
}
166177

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.
178+
// split edge (partial overlap or key is prefix of edge)
170179
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
180+
String edgeSuffix = edge.substring(cpl);
181+
String keySuffix = key.substring(cpl);
173182

174-
// Create an intermediate node for 'prefix'
175183
Node<V> mid = new Node<>();
176184
node.children.remove(edge);
177185
node.children.put(prefix, mid);
178186

179-
// Old child moves under 'edgeSuffix'
180187
Node<V> oldChild = e.getValue();
181188
if (!edgeSuffix.isEmpty()) {
182189
mid.children.put(edgeSuffix, oldChild);
183190
} 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()
191+
// Should not occur since cpl < edge.length() for this branch
192+
mid.children.put("", oldChild);
187193
}
188194

189-
// If keySuffix empty => store value at mid
190195
if (keySuffix.isEmpty()) {
191196
if (!mid.hasValue) {
192197
size++;
193198
}
194199
mid.hasValue = true;
195200
mid.value = value;
196201
} else {
197-
// Add a new leaf under keySuffix
198202
Node<V> leaf = new Node<>();
199203
leaf.hasValue = true;
200204
leaf.value = value;
@@ -203,7 +207,7 @@ private void insert(Node<V> node, String key, V value) {
203207
return;
204208
}
205209

206-
// No common prefix with any child => add new edge directly
210+
// No common prefix with any child: create a new edge
207211
Node<V> leaf = new Node<>();
208212
leaf.hasValue = true;
209213
leaf.value = value;
@@ -222,11 +226,10 @@ private Node<V> findNode(Node<V> node, String key) {
222226
continue;
223227
}
224228
if (cpl == edge.length()) {
225-
// Edge fully matches a prefix of key
226229
String rest = key.substring(cpl);
227230
return findNode(e.getValue(), rest);
228231
} else {
229-
// Partial match but edge not fully consumed => key absent
232+
// partial match but edge not fully consumed => absent
230233
return null;
231234
}
232235
}
@@ -244,86 +247,78 @@ private Node<V> findPrefixNode(Node<V> node, String prefix) {
244247
continue;
245248
}
246249
if (cpl == prefix.length()) {
247-
// consumed the whole prefix: prefix exists in this subtree
248250
return e.getValue();
249251
}
250252
if (cpl == edge.length()) {
251-
// consume edge, continue with remaining prefix
252253
String rest = prefix.substring(cpl);
253254
return findPrefixNode(e.getValue(), rest);
254255
}
255-
// partial split where neither fully consumed => no such prefix path
256+
// neither fully consumed -> no such prefix
256257
return null;
257258
}
258259
return null;
259260
}
260261

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();
262+
/**
263+
* Recursive removal + cleanup (merge).
264+
*
265+
* <p>On the way back up, if a child has no value and:
266+
* <ul>
267+
* <li>deg == 0: remove it from parent,</li>
268+
* <li>deg == 1: merge it with its single child (concatenate edge labels).</li>
269+
* </ul>
270+
* </p>
271+
*/
272+
private void removeRecursive(Node<V> parent, String key, Object[] removedHolder) {
273+
// iterate on a snapshot of keys to allow modifications during loop
274+
for (String edge : parent.children.keySet().toArray(new String[0])) {
277275
int cpl = commonPrefixLen(edge, key);
278276
if (cpl == 0) {
279277
continue;
280278
}
279+
280+
Node<V> child = parent.children.get(edge);
281+
282+
// partial overlap with edge => key doesn't exist in this branch
281283
if (cpl < edge.length()) {
282-
// Partial overlap (edge not fully matched) -> key not present
283-
return false;
284+
return;
284285
}
285-
// Edge fully matched; go deeper
286+
286287
String rest = key.substring(cpl);
287-
Node<V> child = e.getValue();
288-
boolean removed = delete(child, rest);
289-
if (!removed) {
290-
return false;
288+
if (rest.isEmpty()) {
289+
// we've reached the node that holds the key
290+
if (child.hasValue) {
291+
removedHolder[0] = child.value;
292+
child.hasValue = false;
293+
child.value = null;
294+
}
295+
} else {
296+
// keep traversing
297+
removeRecursive(child, rest, removedHolder);
291298
}
292-
// Cleanup/merge after successful deletion
293-
mergeIfNeeded(node, edge, child);
294-
return true;
295-
}
296-
return false;
297-
}
298299

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);
300+
// post-recursion cleanup of child
301+
if (!child.hasValue) {
302+
int deg = child.children.size();
303+
if (deg == 0) {
304+
// fully remove empty child (fixes leaf removal case)
305+
parent.children.remove(edge);
306+
} else if (deg == 1) {
307+
// merge pass-through child with its only grandchild
308+
Map.Entry<String, Node<V>> only =
309+
child.children.entrySet().iterator().next();
310+
String grandEdge = only.getKey();
311+
Node<V> grand = only.getValue();
312+
313+
parent.children.remove(edge);
314+
parent.children.put(edge + grandEdge, grand);
315+
}
316+
}
317+
return; // processed the matching path
323318
}
324319
}
325320

326-
/** Returns length of common prefix of a and b (0..min(a.length,b.length)). */
321+
/** Length of common prefix of a and b. */
327322
private static int commonPrefixLen(String a, String b) {
328323
int n = Math.min(a.length(), b.length());
329324
int i = 0;
@@ -335,13 +330,11 @@ private static int commonPrefixLen(String a, String b) {
335330

336331
@Override
337332
public int hashCode() {
338-
// not used by algorithms; keep minimal but deterministic with size
339333
return Objects.hash(size);
340334
}
341335

342336
@Override
343337
public boolean equals(Object obj) {
344-
// Structural equality is not required; keep reference equality
345338
return this == obj;
346339
}
347340
}

0 commit comments

Comments
 (0)