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>
2120 */
2221public 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