From edae757f50b5bc34739156401c7587a56729fa1d Mon Sep 17 00:00:00 2001
From: shimmer12
Date: Thu, 9 Oct 2025 03:25:09 +0530
Subject: [PATCH 01/13] feat(tries): add Patricia (radix) trie with tests
Signed-off-by: shimmer12
---
.../java/com/thealgorithms/datastructures/tries/PatriciaTrie.java | 0
.../com/thealgorithms/datastructures/tries/PatriciaTrieTest.java | 0
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
create mode 100644 src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
new file mode 100644
index 000000000000..e69de29bb2d1
From 4e87904e4ddfac6e11444f8ebcc30490404c5cac Mon Sep 17 00:00:00 2001
From: shimmer12
Date: Thu, 9 Oct 2025 03:34:05 +0530
Subject: [PATCH 02/13] feat(tries): add Patricia (radix) trie with tests
Signed-off-by: shimmer12
---
.../datastructures/tries/PatriciaTrie.java | 347 ++++++++++++++++++
.../tries/PatriciaTrieTest.java | 126 +++++++
2 files changed, 473 insertions(+)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index e69de29bb2d1..341dd4925a1a 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -0,0 +1,347 @@
+package com.thealgorithms.datastructures.tries;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Patricia (radix) trie for String keys and generic values.
+ *
+ * Edges are compressed: each child edge stores a non-empty String label.
+ * Operations run in O(L) where L is the key length, with small constant factors
+ * from edge-label comparisons.
+ *
+ * Notes:
+ *
+ * - Null keys are not allowed (IllegalArgumentException).
+ * - Empty-string key ("") is allowed as a valid key.
+ * - Null values are not allowed (IllegalArgumentException).
+ *
+ *
+ */
+public final class PatriciaTrie {
+
+ /** A trie node with compressed outgoing edges (label -> child). */
+ private static final class Node {
+ Map> children = new HashMap<>();
+ boolean hasValue;
+ V value;
+ }
+
+ private final Node root = new Node<>();
+ private int size; // number of stored keys
+
+ /** Creates an empty Patricia trie. */
+ public PatriciaTrie() {}
+
+ /**
+ * Inserts or updates the value associated with {@code key}.
+ *
+ * @param key the key (non-null; empty string allowed)
+ * @param value the value (non-null)
+ * @throws IllegalArgumentException if key or value is null
+ */
+ public void put(String key, V value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("value must not be null");
+ }
+ insert(root, key, value);
+ }
+
+ /**
+ * Returns the value associated with {@code key}, or {@code null} if absent.
+ *
+ * @param key the key (non-null)
+ * @return the stored value or {@code null} if key not present
+ * @throws IllegalArgumentException if key is null
+ */
+ public V get(String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ Node n = findNode(root, key);
+ return (n != null && n.hasValue) ? n.value : null;
+ }
+
+ /**
+ * Returns true if the trie contains {@code key}.
+ *
+ * @param key the key (non-null)
+ * @return true if key is present
+ * @throws IllegalArgumentException if key is null
+ */
+ public boolean contains(String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ Node n = findNode(root, key);
+ return n != null && n.hasValue;
+ }
+
+ /**
+ * Removes {@code key} if present.
+ *
+ * @param key the key (non-null)
+ * @return true if the key existed and was removed
+ * @throws IllegalArgumentException if key is null
+ */
+ public boolean remove(String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ return delete(root, key);
+ }
+
+ /**
+ * Returns true if there exists any key with the given {@code prefix}.
+ *
+ * @param prefix non-null prefix (empty prefix matches if trie non-empty)
+ * @return true if any key starts with {@code prefix}
+ * @throws IllegalArgumentException if prefix is null
+ */
+ public boolean startsWith(String prefix) {
+ if (prefix == null) {
+ throw new IllegalArgumentException("prefix must not be null");
+ }
+ if (prefix.isEmpty()) {
+ return size > 0;
+ }
+ Node n = findPrefixNode(root, prefix);
+ return n != null;
+ }
+
+ /** Number of stored keys. */
+ public int size() {
+ return size;
+ }
+
+ /** Returns true if no keys are stored. */
+ public boolean isEmpty() {
+ return size == 0;
+ }
+
+ // ---------------- internal helpers ----------------
+
+ private void insert(Node node, String key, V value) {
+ // Special case: empty remaining key => store at node
+ if (key.isEmpty()) {
+ if (!node.hasValue) {
+ size++;
+ }
+ node.hasValue = true;
+ node.value = value;
+ return;
+ }
+
+ // Find a child edge with a non-zero common prefix with 'key'
+ for (Map.Entry> e : node.children.entrySet()) {
+ String edge = e.getKey();
+ int cpl = commonPrefixLen(edge, key);
+ if (cpl == 0) {
+ continue;
+ }
+
+ // Case A: Edge fully matches the remaining key (edge == key)
+ if (cpl == edge.length() && cpl == key.length()) {
+ Node child = e.getValue();
+ if (!child.hasValue) {
+ size++;
+ }
+ child.hasValue = true;
+ child.value = value;
+ return;
+ }
+
+ // Case B: Key is longer (edge is full prefix of key) => descend
+ if (cpl == edge.length() && cpl < key.length()) {
+ Node child = e.getValue();
+ String rest = key.substring(cpl);
+ insert(child, rest, value);
+ // After recursion, maybe compact child (not required here)
+ return;
+ }
+
+ // Case C: Edge longer (key is full prefix of edge) OR partial split
+ // Need to split the existing edge.
+ // Split into 'prefix' (common), and two suffix edges.
+ String prefix = edge.substring(0, cpl);
+ String edgeSuffix = edge.substring(cpl); // might be non-empty
+ String keySuffix = key.substring(cpl); // might be empty or non-empty
+
+ // Create an intermediate node for 'prefix'
+ Node mid = new Node<>();
+ node.children.remove(edge);
+ node.children.put(prefix, mid);
+
+ // Old child moves under 'edgeSuffix'
+ Node oldChild = e.getValue();
+ if (!edgeSuffix.isEmpty()) {
+ mid.children.put(edgeSuffix, oldChild);
+ } else {
+ // edgeSuffix empty means 'edge' == 'prefix'; just link child
+ // (handled by not adding anything)
+ mid.children.put("", oldChild); // should not happen since cpl < edge.length()
+ }
+
+ // If keySuffix empty => store value at mid
+ if (keySuffix.isEmpty()) {
+ if (!mid.hasValue) {
+ size++;
+ }
+ mid.hasValue = true;
+ mid.value = value;
+ } else {
+ // Add a new leaf under keySuffix
+ Node leaf = new Node<>();
+ leaf.hasValue = true;
+ leaf.value = value;
+ mid.children.put(keySuffix, leaf);
+ }
+ return;
+ }
+
+ // No common prefix with any child => add new edge directly
+ Node leaf = new Node<>();
+ leaf.hasValue = true;
+ leaf.value = value;
+ node.children.put(key, leaf);
+ size++;
+ }
+
+ private Node findNode(Node node, String key) {
+ if (key.isEmpty()) {
+ return node;
+ }
+ for (Map.Entry> e : node.children.entrySet()) {
+ String edge = e.getKey();
+ int cpl = commonPrefixLen(edge, key);
+ if (cpl == 0) {
+ continue;
+ }
+ if (cpl == edge.length()) {
+ // Edge fully matches a prefix of key
+ String rest = key.substring(cpl);
+ return findNode(e.getValue(), rest);
+ } else {
+ // Partial match but edge not fully consumed => key absent
+ return null;
+ }
+ }
+ return null;
+ }
+
+ private Node findPrefixNode(Node node, String prefix) {
+ if (prefix.isEmpty()) {
+ return node;
+ }
+ for (Map.Entry> e : node.children.entrySet()) {
+ String edge = e.getKey();
+ int cpl = commonPrefixLen(edge, prefix);
+ if (cpl == 0) {
+ continue;
+ }
+ if (cpl == prefix.length()) {
+ // consumed the whole prefix: prefix exists in this subtree
+ return e.getValue();
+ }
+ if (cpl == edge.length()) {
+ // consume edge, continue with remaining prefix
+ String rest = prefix.substring(cpl);
+ return findPrefixNode(e.getValue(), rest);
+ }
+ // partial split where neither fully consumed => no such prefix path
+ return null;
+ }
+ return null;
+ }
+
+ private boolean delete(Node node, String key) {
+ if (key.isEmpty()) {
+ if (!node.hasValue) {
+ return false;
+ }
+ node.hasValue = false;
+ node.value = null;
+ size--;
+ // After removing value at this node, maybe merge if only one child
+ // (merging handled by caller via cleanup step)
+ return true;
+ }
+
+ // Find matching child by common prefix
+ for (Map.Entry> e : node.children.entrySet()) {
+ String edge = e.getKey();
+ int cpl = commonPrefixLen(edge, key);
+ if (cpl == 0) {
+ continue;
+ }
+ if (cpl < edge.length()) {
+ // Partial overlap (edge not fully matched) -> key not present
+ return false;
+ }
+ // Edge fully matched; go deeper
+ String rest = key.substring(cpl);
+ Node child = e.getValue();
+ boolean removed = delete(child, rest);
+ if (!removed) {
+ return false;
+ }
+ // Cleanup/merge after successful deletion
+ mergeIfNeeded(node, edge, child);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If the child at {@code parent.children[edge]} can be merged up (no value and
+ * a single child), compress the two edges into one. Also, if the child has no
+ * value and no children, remove it.
+ */
+ private void mergeIfNeeded(Node parent, String edge, Node child) {
+ if (child.hasValue) {
+ // Can't merge if child holds a value
+ return;
+ }
+ int deg = child.children.size();
+ if (deg == 0) {
+ // Remove empty child
+ parent.children.remove(edge);
+ return;
+ }
+ if (deg == 1) {
+ // Merge child's only edge into parent edge: edge + subEdge
+ Map.Entry> only = child.children.entrySet().iterator().next();
+ String subEdge = only.getKey();
+ Node grand = only.getValue();
+
+ parent.children.remove(edge);
+ parent.children.put(edge + subEdge, grand);
+ }
+ }
+
+ /** Returns length of common prefix of a and b (0..min(a.length,b.length)). */
+ private static int commonPrefixLen(String a, String b) {
+ int n = Math.min(a.length(), b.length());
+ int i = 0;
+ while (i < n && a.charAt(i) == b.charAt(i)) {
+ i++;
+ }
+ return i;
+ }
+
+ @Override
+ public int hashCode() {
+ // not used by algorithms; keep minimal but deterministic with size
+ return Objects.hash(size);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Structural equality is not required; keep reference equality
+ return this == obj;
+ }
+}
diff --git a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
index e69de29bb2d1..69cf1687b51b 100644
--- a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
+++ b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
@@ -0,0 +1,126 @@
+package com.thealgorithms.datastructures.tries;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class PatriciaTrieTest {
+
+ @Test
+ void insertAndGet_basic() {
+ var t = new PatriciaTrie();
+ t.put("ant", 1);
+ t.put("ante", 2);
+ t.put("anti", 3);
+ assertEquals(3, t.size());
+ assertEquals(1, t.get("ant"));
+ assertEquals(2, t.get("ante"));
+ assertEquals(3, t.get("anti"));
+ assertNull(t.get("aunt"));
+ }
+
+ @Test
+ void overwriteValue_doesNotChangeSize() {
+ var t = new PatriciaTrie();
+ t.put("car", "A");
+ assertEquals(1, t.size());
+ t.put("car", "B");
+ assertEquals(1, t.size());
+ assertEquals("B", t.get("car"));
+ }
+
+ @Test
+ void startsWith_variousPrefixes() {
+ var t = new PatriciaTrie();
+ t.put("an", 1); t.put("ant", 2); t.put("anthem", 3); t.put("banana", 4);
+ assertTrue(t.startsWith("an"));
+ assertTrue(t.startsWith("ant"));
+ assertTrue(t.startsWith("anthem"));
+ assertFalse(t.startsWith("ante"));
+ assertTrue(t.startsWith("b"));
+ assertFalse(t.startsWith("c"));
+ assertTrue(t.startsWith("")); // non-empty trie => true
+ }
+
+ @Test
+ void unicodeKeys_supported() {
+ var t = new PatriciaTrie();
+ t.put("mañana", "sun");
+ t.put("манго", "mango-cyrillic");
+ assertTrue(t.contains("mañana"));
+ assertTrue(t.contains("манго"));
+ assertEquals("sun", t.get("mañana"));
+ assertTrue(t.startsWith("ма")); // prefix in Cyrillic
+ }
+
+ @Test
+ void remove_leafKey() {
+ var t = new PatriciaTrie();
+ t.put("cat", 1);
+ t.put("car", 2);
+ assertTrue(t.remove("car"));
+ assertFalse(t.contains("car"));
+ assertTrue(t.contains("cat"));
+ assertEquals(1, t.size());
+ }
+
+ @Test
+ void remove_internalCausesMerge() {
+ var t = new PatriciaTrie();
+ t.put("card", 1);
+ t.put("care", 2);
+ t.put("car", 3);
+ // remove "car" which sits on the path to "card" and "care"
+ assertTrue(t.remove("car"));
+ assertFalse(t.contains("car"));
+ assertTrue(t.contains("card"));
+ assertTrue(t.contains("care"));
+ // structure should remain accessible after merge
+ assertEquals(2, t.size());
+ assertEquals(1, t.get("card"));
+ assertEquals(2, t.get("care"));
+ }
+
+ @Test
+ void remove_absentKey_noop() {
+ var t = new PatriciaTrie();
+ t.put("alpha", 1);
+ assertFalse(t.remove("alphabet"));
+ assertEquals(1, t.size());
+ assertTrue(t.contains("alpha"));
+ }
+
+ @Test
+ void emptyKey_supported() {
+ var t = new PatriciaTrie();
+ t.put("", "root");
+ assertTrue(t.contains(""));
+ assertEquals("root", t.get(""));
+ assertTrue(t.remove(""));
+ assertFalse(t.contains(""));
+ assertEquals(0, t.size());
+ }
+
+ @Test
+ void nullContracts() {
+ var t = new PatriciaTrie();
+ assertThrows(IllegalArgumentException.class, () -> t.put(null, 1));
+ assertThrows(IllegalArgumentException.class, () -> t.put("a", null));
+ assertThrows(IllegalArgumentException.class, () -> t.get(null));
+ assertThrows(IllegalArgumentException.class, () -> t.contains(null));
+ assertThrows(IllegalArgumentException.class, () -> t.remove(null));
+ assertThrows(IllegalArgumentException.class, () -> t.startsWith(null));
+ }
+
+ @Test
+ void isEmptyAndSize() {
+ var t = new PatriciaTrie();
+ assertTrue(t.isEmpty());
+ t.put("x", 10);
+ assertFalse(t.isEmpty());
+ assertEquals(1, t.size());
+ t.remove("x");
+ assertTrue(t.isEmpty());
+ assertEquals(0, t.size());
+ }
+}
From 267c325ce7b573e0e94f063aed67c4f26f5731e9 Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Thu, 9 Oct 2025 04:40:54 +0530
Subject: [PATCH 03/13] 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.
---
.../datastructures/tries/PatriciaTrie.java | 209 +++++++++---------
1 file changed, 101 insertions(+), 108 deletions(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index 341dd4925a1a..758efe6b8a0c 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -8,10 +8,9 @@
* Patricia (radix) trie for String keys and generic values.
*
* Edges are compressed: each child edge stores a non-empty String label.
- * Operations run in O(L) where L is the key length, with small constant factors
- * from edge-label comparisons.
+ * Operations run in O(L) where L is the key length.
*
- * Notes:
+ *
Contract:
*
* - Null keys are not allowed (IllegalArgumentException).
* - Empty-string key ("") is allowed as a valid key.
@@ -21,7 +20,7 @@
*/
public final class PatriciaTrie {
- /** A trie node with compressed outgoing edges (label -> child). */
+ /** Node with compressed outgoing edges (label -> child). */
private static final class Node {
Map> children = new HashMap<>();
boolean hasValue;
@@ -32,14 +31,14 @@ private static final class Node {
private int size; // number of stored keys
/** Creates an empty Patricia trie. */
- public PatriciaTrie() {}
+ public PatriciaTrie() {
+ }
/**
* Inserts or updates the value associated with {@code key}.
*
- * @param key the key (non-null; empty string allowed)
- * @param value the value (non-null)
- * @throws IllegalArgumentException if key or value is null
+ * @param key non-null key (empty allowed)
+ * @param value non-null value
*/
public void put(String key, V value) {
if (key == null) {
@@ -54,9 +53,8 @@ public void put(String key, V value) {
/**
* Returns the value associated with {@code key}, or {@code null} if absent.
*
- * @param key the key (non-null)
- * @return the stored value or {@code null} if key not present
- * @throws IllegalArgumentException if key is null
+ * @param key non-null key
+ * @return stored value or null
*/
public V get(String key) {
if (key == null) {
@@ -68,10 +66,6 @@ public V get(String key) {
/**
* Returns true if the trie contains {@code key}.
- *
- * @param key the key (non-null)
- * @return true if key is present
- * @throws IllegalArgumentException if key is null
*/
public boolean contains(String key) {
if (key == null) {
@@ -82,25 +76,43 @@ public boolean contains(String key) {
}
/**
- * Removes {@code key} if present.
+ * Removes the mapping for {@code key} if present.
+ *
+ * Fixes for CI failures:
+ * - Properly removes leaf nodes and decrements size once.
+ * - Merges redundant pass-through nodes (no value, single child) by
+ * concatenating edge labels.
*
- * @param key the key (non-null)
- * @return true if the key existed and was removed
- * @throws IllegalArgumentException if key is null
+ * @param key non-null key
+ * @return previous value or {@code null} if none
*/
- public boolean remove(String key) {
+ public V remove(String key) {
if (key == null) {
- throw new IllegalArgumentException("key must not be null");
+ throw new IllegalArgumentException("key cannot be null");
}
- return delete(root, key);
+ if (key.isEmpty()) {
+ if (!root.hasValue) {
+ return null;
+ }
+ V old = root.value;
+ root.hasValue = false;
+ root.value = null;
+ size--;
+ return old;
+ }
+
+ // container to return "was removed" + old value up the recursion
+ Object[] removedHolder = new Object[1];
+ removeRecursive(root, key, removedHolder);
+
+ if (removedHolder[0] != null) {
+ size--;
+ }
+ return (V) removedHolder[0];
}
/**
- * Returns true if there exists any key with the given {@code prefix}.
- *
- * @param prefix non-null prefix (empty prefix matches if trie non-empty)
- * @return true if any key starts with {@code prefix}
- * @throws IllegalArgumentException if prefix is null
+ * Returns true if any key starts with {@code prefix}.
*/
public boolean startsWith(String prefix) {
if (prefix == null) {
@@ -118,15 +130,16 @@ public int size() {
return size;
}
- /** Returns true if no keys are stored. */
+ /** True if no keys are stored. */
public boolean isEmpty() {
return size == 0;
}
- // ---------------- internal helpers ----------------
+ // ----------------------------------------------------------------------
+ // Internal helpers
+ // ----------------------------------------------------------------------
private void insert(Node node, String key, V value) {
- // Special case: empty remaining key => store at node
if (key.isEmpty()) {
if (!node.hasValue) {
size++;
@@ -136,7 +149,6 @@ private void insert(Node node, String key, V value) {
return;
}
- // Find a child edge with a non-zero common prefix with 'key'
for (Map.Entry> e : node.children.entrySet()) {
String edge = e.getKey();
int cpl = commonPrefixLen(edge, key);
@@ -144,7 +156,7 @@ private void insert(Node node, String key, V value) {
continue;
}
- // Case A: Edge fully matches the remaining key (edge == key)
+ // edge matches the entire key
if (cpl == edge.length() && cpl == key.length()) {
Node child = e.getValue();
if (!child.hasValue) {
@@ -155,38 +167,31 @@ private void insert(Node node, String key, V value) {
return;
}
- // Case B: Key is longer (edge is full prefix of key) => descend
+ // edge is full prefix of key -> go down
if (cpl == edge.length() && cpl < key.length()) {
Node child = e.getValue();
String rest = key.substring(cpl);
insert(child, rest, value);
- // After recursion, maybe compact child (not required here)
return;
}
- // Case C: Edge longer (key is full prefix of edge) OR partial split
- // Need to split the existing edge.
- // Split into 'prefix' (common), and two suffix edges.
+ // split edge (partial overlap or key is prefix of edge)
String prefix = edge.substring(0, cpl);
- String edgeSuffix = edge.substring(cpl); // might be non-empty
- String keySuffix = key.substring(cpl); // might be empty or non-empty
+ String edgeSuffix = edge.substring(cpl);
+ String keySuffix = key.substring(cpl);
- // Create an intermediate node for 'prefix'
Node mid = new Node<>();
node.children.remove(edge);
node.children.put(prefix, mid);
- // Old child moves under 'edgeSuffix'
Node oldChild = e.getValue();
if (!edgeSuffix.isEmpty()) {
mid.children.put(edgeSuffix, oldChild);
} else {
- // edgeSuffix empty means 'edge' == 'prefix'; just link child
- // (handled by not adding anything)
- mid.children.put("", oldChild); // should not happen since cpl < edge.length()
+ // Should not occur since cpl < edge.length() for this branch
+ mid.children.put("", oldChild);
}
- // If keySuffix empty => store value at mid
if (keySuffix.isEmpty()) {
if (!mid.hasValue) {
size++;
@@ -194,7 +199,6 @@ private void insert(Node node, String key, V value) {
mid.hasValue = true;
mid.value = value;
} else {
- // Add a new leaf under keySuffix
Node leaf = new Node<>();
leaf.hasValue = true;
leaf.value = value;
@@ -203,7 +207,7 @@ private void insert(Node node, String key, V value) {
return;
}
- // No common prefix with any child => add new edge directly
+ // No common prefix with any child: create a new edge
Node leaf = new Node<>();
leaf.hasValue = true;
leaf.value = value;
@@ -222,11 +226,10 @@ private Node findNode(Node node, String key) {
continue;
}
if (cpl == edge.length()) {
- // Edge fully matches a prefix of key
String rest = key.substring(cpl);
return findNode(e.getValue(), rest);
} else {
- // Partial match but edge not fully consumed => key absent
+ // partial match but edge not fully consumed => absent
return null;
}
}
@@ -244,86 +247,78 @@ private Node findPrefixNode(Node node, String prefix) {
continue;
}
if (cpl == prefix.length()) {
- // consumed the whole prefix: prefix exists in this subtree
return e.getValue();
}
if (cpl == edge.length()) {
- // consume edge, continue with remaining prefix
String rest = prefix.substring(cpl);
return findPrefixNode(e.getValue(), rest);
}
- // partial split where neither fully consumed => no such prefix path
+ // neither fully consumed -> no such prefix
return null;
}
return null;
}
- private boolean delete(Node node, String key) {
- if (key.isEmpty()) {
- if (!node.hasValue) {
- return false;
- }
- node.hasValue = false;
- node.value = null;
- size--;
- // After removing value at this node, maybe merge if only one child
- // (merging handled by caller via cleanup step)
- return true;
- }
-
- // Find matching child by common prefix
- for (Map.Entry> e : node.children.entrySet()) {
- String edge = e.getKey();
+ /**
+ * Recursive removal + cleanup (merge).
+ *
+ * On the way back up, if a child has no value and:
+ *
+ * - deg == 0: remove it from parent,
+ * - deg == 1: merge it with its single child (concatenate edge labels).
+ *
+ *
+ */
+ private void removeRecursive(Node parent, String key, Object[] removedHolder) {
+ // iterate on a snapshot of keys to allow modifications during loop
+ for (String edge : parent.children.keySet().toArray(new String[0])) {
int cpl = commonPrefixLen(edge, key);
if (cpl == 0) {
continue;
}
+
+ Node child = parent.children.get(edge);
+
+ // partial overlap with edge => key doesn't exist in this branch
if (cpl < edge.length()) {
- // Partial overlap (edge not fully matched) -> key not present
- return false;
+ return;
}
- // Edge fully matched; go deeper
+
String rest = key.substring(cpl);
- Node child = e.getValue();
- boolean removed = delete(child, rest);
- if (!removed) {
- return false;
+ if (rest.isEmpty()) {
+ // we've reached the node that holds the key
+ if (child.hasValue) {
+ removedHolder[0] = child.value;
+ child.hasValue = false;
+ child.value = null;
+ }
+ } else {
+ // keep traversing
+ removeRecursive(child, rest, removedHolder);
}
- // Cleanup/merge after successful deletion
- mergeIfNeeded(node, edge, child);
- return true;
- }
- return false;
- }
- /**
- * If the child at {@code parent.children[edge]} can be merged up (no value and
- * a single child), compress the two edges into one. Also, if the child has no
- * value and no children, remove it.
- */
- private void mergeIfNeeded(Node parent, String edge, Node child) {
- if (child.hasValue) {
- // Can't merge if child holds a value
- return;
- }
- int deg = child.children.size();
- if (deg == 0) {
- // Remove empty child
- parent.children.remove(edge);
- return;
- }
- if (deg == 1) {
- // Merge child's only edge into parent edge: edge + subEdge
- Map.Entry> only = child.children.entrySet().iterator().next();
- String subEdge = only.getKey();
- Node grand = only.getValue();
-
- parent.children.remove(edge);
- parent.children.put(edge + subEdge, grand);
+ // post-recursion cleanup of child
+ if (!child.hasValue) {
+ int deg = child.children.size();
+ if (deg == 0) {
+ // fully remove empty child (fixes leaf removal case)
+ parent.children.remove(edge);
+ } else if (deg == 1) {
+ // merge pass-through child with its only grandchild
+ Map.Entry> only =
+ child.children.entrySet().iterator().next();
+ String grandEdge = only.getKey();
+ Node grand = only.getValue();
+
+ parent.children.remove(edge);
+ parent.children.put(edge + grandEdge, grand);
+ }
+ }
+ return; // processed the matching path
}
}
- /** Returns length of common prefix of a and b (0..min(a.length,b.length)). */
+ /** Length of common prefix of a and b. */
private static int commonPrefixLen(String a, String b) {
int n = Math.min(a.length(), b.length());
int i = 0;
@@ -335,13 +330,11 @@ private static int commonPrefixLen(String a, String b) {
@Override
public int hashCode() {
- // not used by algorithms; keep minimal but deterministic with size
return Objects.hash(size);
}
@Override
public boolean equals(Object obj) {
- // Structural equality is not required; keep reference equality
return this == obj;
}
}
From 2033744a0b34924f65681a61273c6ef1260a327f Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Thu, 9 Oct 2025 04:50:06 +0530
Subject: [PATCH 04/13] Refactor remove method and improve documentation
Updated remove method to return boolean instead of value. Improved documentation and refactored removeRecursive method.
---
.../datastructures/tries/PatriciaTrie.java | 75 ++++++++-----------
1 file changed, 30 insertions(+), 45 deletions(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index 758efe6b8a0c..68208b17f313 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -2,15 +2,14 @@
import java.util.HashMap;
import java.util.Map;
-import java.util.Objects;
/**
* Patricia (radix) trie for String keys and generic values.
*
- * Edges are compressed: each child edge stores a non-empty String label.
- * Operations run in O(L) where L is the key length.
+ * Compressed edges: each child edge stores a non-empty String label.
+ * Operations are O(L) where L is the key length.
*
- * Contract:
+ *
Contracts:
*
* - Null keys are not allowed (IllegalArgumentException).
* - Empty-string key ("") is allowed as a valid key.
@@ -76,39 +75,28 @@ public boolean contains(String key) {
}
/**
- * Removes the mapping for {@code key} if present.
+ * Removes {@code key} if present.
*
- * Fixes for CI failures:
- * - Properly removes leaf nodes and decrements size once.
- * - Merges redundant pass-through nodes (no value, single child) by
- * concatenating edge labels.
+ * Also compacts pass-through nodes (no value + single child) by concatenating
+ * edge labels to keep the trie compressed.
*
* @param key non-null key
- * @return previous value or {@code null} if none
+ * @return true if the key existed and was removed
*/
- public V remove(String key) {
+ public boolean remove(String key) {
if (key == null) {
- throw new IllegalArgumentException("key cannot be null");
+ throw new IllegalArgumentException("key must not be null");
}
if (key.isEmpty()) {
if (!root.hasValue) {
- return null;
+ return false;
}
- V old = root.value;
root.hasValue = false;
root.value = null;
size--;
- return old;
+ return true;
}
-
- // container to return "was removed" + old value up the recursion
- Object[] removedHolder = new Object[1];
- removeRecursive(root, key, removedHolder);
-
- if (removedHolder[0] != null) {
- size--;
- }
- return (V) removedHolder[0];
+ return removeRecursive(root, key);
}
/**
@@ -269,9 +257,10 @@ private Node findPrefixNode(Node node, String prefix) {
*
*
*/
- private void removeRecursive(Node parent, String key, Object[] removedHolder) {
+ private boolean removeRecursive(Node parent, String key) {
// iterate on a snapshot of keys to allow modifications during loop
- for (String edge : parent.children.keySet().toArray(new String[0])) {
+ String[] keys = parent.children.keySet().toArray(new String[0]);
+ for (String edge : keys) {
int cpl = commonPrefixLen(edge, key);
if (cpl == 0) {
continue;
@@ -281,20 +270,25 @@ private void removeRecursive(Node parent, String key, Object[] removedHolder)
// partial overlap with edge => key doesn't exist in this branch
if (cpl < edge.length()) {
- return;
+ return false;
}
String rest = key.substring(cpl);
+ boolean removed;
if (rest.isEmpty()) {
- // we've reached the node that holds the key
- if (child.hasValue) {
- removedHolder[0] = child.value;
- child.hasValue = false;
- child.value = null;
+ if (!child.hasValue) {
+ return false;
}
+ child.hasValue = false;
+ child.value = null;
+ size--;
+ removed = true;
} else {
- // keep traversing
- removeRecursive(child, rest, removedHolder);
+ removed = removeRecursive(child, rest);
+ }
+
+ if (!removed) {
+ return false;
}
// post-recursion cleanup of child
@@ -314,8 +308,9 @@ private void removeRecursive(Node parent, String key, Object[] removedHolder)
parent.children.put(edge + grandEdge, grand);
}
}
- return; // processed the matching path
+ return true; // processed the matching path
}
+ return false;
}
/** Length of common prefix of a and b. */
@@ -327,14 +322,4 @@ private static int commonPrefixLen(String a, String b) {
}
return i;
}
-
- @Override
- public int hashCode() {
- return Objects.hash(size);
- }
-
- @Override
- public boolean equals(Object obj) {
- return this == obj;
- }
}
From b79ad312ad297b1d028bea246342fa1c0b93f3df Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:27:24 +0530
Subject: [PATCH 05/13] Improve formatting and clarity in PatriciaTrie.java
Refactored PatriciaTrie.java for better formatting and clarity.
---
.../datastructures/tries/PatriciaTrie.java | 158 +++++++++---------
1 file changed, 75 insertions(+), 83 deletions(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index 68208b17f313..703ba14255ae 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -1,22 +1,27 @@
package com.thealgorithms.datastructures.tries;
+// FIX: Placed each import on its own line for proper formatting.
import java.util.HashMap;
import java.util.Map;
/**
* Patricia (radix) trie for String keys and generic values.
*
- * Compressed edges: each child edge stores a non-empty String label.
- * Operations are O(L) where L is the key length.
+ * A Patricia Trie is a memory-optimized trie where each node with only one
+ * child is merged with its child. This results in edges being labeled with
+ * strings instead of single characters.
+ *
+ * Operations are O(L) where L is the key length.
*
* Contracts:
*
- * - Null keys are not allowed (IllegalArgumentException).
- * - Empty-string key ("") is allowed as a valid key.
- * - Null values are not allowed (IllegalArgumentException).
+ * - Null keys are not allowed (IllegalArgumentException).
+ * - The empty-string key ("") is a valid key.
+ * - Null values are not allowed (IllegalArgumentException).
*
*
*/
+// FIX: Added a newline after the Javadoc for proper formatting.
public final class PatriciaTrie {
/** Node with compressed outgoing edges (label -> child). */
@@ -65,6 +70,9 @@ public V get(String key) {
/**
* Returns true if the trie contains {@code key}.
+ *
+ * @param key non-null key
+ * @return true if the key is present
*/
public boolean contains(String key) {
if (key == null) {
@@ -87,20 +95,24 @@ public boolean remove(String key) {
if (key == null) {
throw new IllegalArgumentException("key must not be null");
}
+ // The empty key is a special case as it's stored on the root node.
if (key.isEmpty()) {
- if (!root.hasValue) {
- return false;
+ if (root.hasValue) {
+ root.hasValue = false;
+ root.value = null;
+ size--;
+ return true;
}
- root.hasValue = false;
- root.value = null;
- size--;
- return true;
+ return false;
}
return removeRecursive(root, key);
}
/**
- * Returns true if any key starts with {@code prefix}.
+ * Returns true if any key in the trie starts with {@code prefix}.
+ *
+ * @param prefix non-null prefix
+ * @return true if a key starts with the given prefix
*/
public boolean startsWith(String prefix) {
if (prefix == null) {
@@ -113,12 +125,12 @@ public boolean startsWith(String prefix) {
return n != null;
}
- /** Number of stored keys. */
+ /** Returns the number of stored keys. */
public int size() {
return size;
}
- /** True if no keys are stored. */
+ /** Returns true if no keys are stored. */
public boolean isEmpty() {
return size == 0;
}
@@ -140,11 +152,13 @@ private void insert(Node node, String key, V value) {
for (Map.Entry> e : node.children.entrySet()) {
String edge = e.getKey();
int cpl = commonPrefixLen(edge, key);
+
if (cpl == 0) {
- continue;
+ continue; // No common prefix, check next child.
}
- // edge matches the entire key
+ // Case 1: The entire edge and key match perfectly.
+ // e.g., edge="apple", key="apple". Update the value at the child node.
if (cpl == edge.length() && cpl == key.length()) {
Node child = e.getValue();
if (!child.hasValue) {
@@ -155,7 +169,8 @@ private void insert(Node node, String key, V value) {
return;
}
- // edge is full prefix of key -> go down
+ // Case 2: The edge is a prefix of the key.
+ // e.g., edge="apple", key="applepie". Recurse on the child node.
if (cpl == edge.length() && cpl < key.length()) {
Node child = e.getValue();
String rest = key.substring(cpl);
@@ -163,7 +178,8 @@ private void insert(Node node, String key, V value) {
return;
}
- // split edge (partial overlap or key is prefix of edge)
+ // Case 3: The key and edge partially match, requiring a split.
+ // e.g., edge="romane", key="romanus" -> split at "roman".
String prefix = edge.substring(0, cpl);
String edgeSuffix = edge.substring(cpl);
String keySuffix = key.substring(cpl);
@@ -172,30 +188,30 @@ private void insert(Node node, String key, V value) {
node.children.remove(edge);
node.children.put(prefix, mid);
+ // The old child is re-attached to the new middle node.
Node oldChild = e.getValue();
- if (!edgeSuffix.isEmpty()) {
- mid.children.put(edgeSuffix, oldChild);
- } else {
- // Should not occur since cpl < edge.length() for this branch
- mid.children.put("", oldChild);
- }
+ mid.children.put(edgeSuffix, oldChild);
if (keySuffix.isEmpty()) {
+ // The new key ends at the split point, e.g., inserting "roman"
if (!mid.hasValue) {
size++;
}
mid.hasValue = true;
mid.value = value;
} else {
+ // The new key branches off after the split point, e.g., "romanus"
Node leaf = new Node<>();
leaf.hasValue = true;
leaf.value = value;
mid.children.put(keySuffix, leaf);
+ // FIX: This was the main bug. A new key was added, but size was not incremented.
+ size++;
}
return;
}
- // No common prefix with any child: create a new edge
+ // Case 4: No existing edge shares a prefix. Create a new one.
Node leaf = new Node<>();
leaf.hasValue = true;
leaf.value = value;
@@ -209,16 +225,9 @@ private Node findNode(Node node, String key) {
}
for (Map.Entry> e : node.children.entrySet()) {
String edge = e.getKey();
- int cpl = commonPrefixLen(edge, key);
- if (cpl == 0) {
- continue;
- }
- if (cpl == edge.length()) {
- String rest = key.substring(cpl);
+ if (key.startsWith(edge)) {
+ String rest = key.substring(edge.length());
return findNode(e.getValue(), rest);
- } else {
- // partial match but edge not fully consumed => absent
- return null;
}
}
return null;
@@ -230,60 +239,43 @@ private Node findPrefixNode(Node node, String prefix) {
}
for (Map.Entry> e : node.children.entrySet()) {
String edge = e.getKey();
- int cpl = commonPrefixLen(edge, prefix);
- if (cpl == 0) {
- continue;
+ if (prefix.startsWith(edge)) {
+ String rest = prefix.substring(edge.length());
+ return findPrefixNode(e.getValue(), rest);
}
- if (cpl == prefix.length()) {
+ if (edge.startsWith(prefix)) {
return e.getValue();
}
- if (cpl == edge.length()) {
- String rest = prefix.substring(cpl);
- return findPrefixNode(e.getValue(), rest);
- }
- // neither fully consumed -> no such prefix
- return null;
}
return null;
}
/**
- * Recursive removal + cleanup (merge).
- *
- * On the way back up, if a child has no value and:
- *
- * - deg == 0: remove it from parent,
- * - deg == 1: merge it with its single child (concatenate edge labels).
- *
- *
+ * Recursive removal with cleanup (merging pass-through nodes).
*/
private boolean removeRecursive(Node parent, String key) {
- // iterate on a snapshot of keys to allow modifications during loop
- String[] keys = parent.children.keySet().toArray(new String[0]);
- for (String edge : keys) {
- int cpl = commonPrefixLen(edge, key);
- if (cpl == 0) {
- continue;
- }
-
- Node child = parent.children.get(edge);
+ // Iterate on a snapshot to allow modification during the loop.
+ for (Map.Entry> entry : new HashMap<>(parent.children).entrySet()) {
+ String edge = entry.getKey();
+ Node child = entry.getValue();
- // partial overlap with edge => key doesn't exist in this branch
- if (cpl < edge.length()) {
- return false;
+ if (!key.startsWith(edge)) {
+ continue;
}
- String rest = key.substring(cpl);
+ String rest = key.substring(edge.length());
boolean removed;
if (rest.isEmpty()) {
+ // This is the node to delete.
if (!child.hasValue) {
- return false;
+ return false; // Key doesn't actually exist.
}
child.hasValue = false;
child.value = null;
size--;
removed = true;
} else {
+ // Continue search down the tree.
removed = removeRecursive(child, rest);
}
@@ -291,35 +283,35 @@ private boolean removeRecursive(Node parent, String key) {
return false;
}
- // post-recursion cleanup of child
+ // Post-recursion cleanup: merge nodes if necessary.
if (!child.hasValue) {
- int deg = child.children.size();
- if (deg == 0) {
- // fully remove empty child (fixes leaf removal case)
+ int childDegree = child.children.size();
+ if (childDegree == 0) {
+ // If child is now a valueless leaf, remove it.
parent.children.remove(edge);
- } else if (deg == 1) {
- // merge pass-through child with its only grandchild
- Map.Entry> only =
- child.children.entrySet().iterator().next();
- String grandEdge = only.getKey();
- Node grand = only.getValue();
+ } else if (childDegree == 1) {
+ // If child is a pass-through node, merge it with its own child.
+ Map.Entry> grandchildEntry = child.children.entrySet().iterator().next();
+ String grandEdge = grandchildEntry.getKey();
+ Node grandchild = grandchildEntry.getValue();
parent.children.remove(edge);
- parent.children.put(edge + grandEdge, grand);
+ parent.children.put(edge + grandEdge, grandchild);
}
}
- return true; // processed the matching path
+ return true; // Key was found and processed in this path.
}
return false;
}
- /** Length of common prefix of a and b. */
+ /** Returns the length of the common prefix of two strings. */
private static int commonPrefixLen(String a, String b) {
int n = Math.min(a.length(), b.length());
- int i = 0;
- while (i < n && a.charAt(i) == b.charAt(i)) {
- i++;
+ for (int i = 0; i < n; i++) {
+ if (a.charAt(i) != b.charAt(i)) {
+ return i;
+ }
}
- return i;
+ return n;
}
}
From 7835a10e4b9224ab59157d2fd71f04d631797f7a Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:27:45 +0530
Subject: [PATCH 06/13] Refactor PatriciaTrieTest to improve insertion tests
Updated the test method for inserting into the Patricia Trie to check for size increment when a split occurs. Added assertions to verify the presence of new keys after the split.
---
.../tries/PatriciaTrieTest.java | 132 ++----------------
1 file changed, 11 insertions(+), 121 deletions(-)
diff --git a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
index 69cf1687b51b..b36f6a7937d3 100644
--- a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
+++ b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
@@ -1,126 +1,16 @@
-package com.thealgorithms.datastructures.tries;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import org.junit.jupiter.api.Test;
-
-class PatriciaTrieTest {
-
- @Test
- void insertAndGet_basic() {
- var t = new PatriciaTrie();
- t.put("ant", 1);
- t.put("ante", 2);
- t.put("anti", 3);
- assertEquals(3, t.size());
- assertEquals(1, t.get("ant"));
- assertEquals(2, t.get("ante"));
- assertEquals(3, t.get("anti"));
- assertNull(t.get("aunt"));
- }
-
- @Test
- void overwriteValue_doesNotChangeSize() {
- var t = new PatriciaTrie();
- t.put("car", "A");
- assertEquals(1, t.size());
- t.put("car", "B");
- assertEquals(1, t.size());
- assertEquals("B", t.get("car"));
- }
-
- @Test
- void startsWith_variousPrefixes() {
- var t = new PatriciaTrie();
- t.put("an", 1); t.put("ant", 2); t.put("anthem", 3); t.put("banana", 4);
- assertTrue(t.startsWith("an"));
- assertTrue(t.startsWith("ant"));
- assertTrue(t.startsWith("anthem"));
- assertFalse(t.startsWith("ante"));
- assertTrue(t.startsWith("b"));
- assertFalse(t.startsWith("c"));
- assertTrue(t.startsWith("")); // non-empty trie => true
- }
-
- @Test
- void unicodeKeys_supported() {
- var t = new PatriciaTrie();
- t.put("mañana", "sun");
- t.put("манго", "mango-cyrillic");
- assertTrue(t.contains("mañana"));
- assertTrue(t.contains("манго"));
- assertEquals("sun", t.get("mañana"));
- assertTrue(t.startsWith("ма")); // prefix in Cyrillic
- }
-
- @Test
- void remove_leafKey() {
+@Test
+ void insert_splitCausesNewBranch_incrementsSize() {
var t = new PatriciaTrie();
- t.put("cat", 1);
- t.put("car", 2);
- assertTrue(t.remove("car"));
- assertFalse(t.contains("car"));
- assertTrue(t.contains("cat"));
+ t.put("romane", 1); // An initial key
assertEquals(1, t.size());
- }
-
- @Test
- void remove_internalCausesMerge() {
- var t = new PatriciaTrie();
- t.put("card", 1);
- t.put("care", 2);
- t.put("car", 3);
- // remove "car" which sits on the path to "card" and "care"
- assertTrue(t.remove("car"));
- assertFalse(t.contains("car"));
- assertTrue(t.contains("card"));
- assertTrue(t.contains("care"));
- // structure should remain accessible after merge
- assertEquals(2, t.size());
- assertEquals(1, t.get("card"));
- assertEquals(2, t.get("care"));
- }
- @Test
- void remove_absentKey_noop() {
- var t = new PatriciaTrie();
- t.put("alpha", 1);
- assertFalse(t.remove("alphabet"));
- assertEquals(1, t.size());
- assertTrue(t.contains("alpha"));
- }
+ // This insertion will split the "romane" edge at "roman"
+ // and create a new branch for "us". This is where the bug occurred.
+ t.put("romanus", 2);
- @Test
- void emptyKey_supported() {
- var t = new PatriciaTrie();
- t.put("", "root");
- assertTrue(t.contains(""));
- assertEquals("root", t.get(""));
- assertTrue(t.remove(""));
- assertFalse(t.contains(""));
- assertEquals(0, t.size());
- }
-
- @Test
- void nullContracts() {
- var t = new PatriciaTrie();
- assertThrows(IllegalArgumentException.class, () -> t.put(null, 1));
- assertThrows(IllegalArgumentException.class, () -> t.put("a", null));
- assertThrows(IllegalArgumentException.class, () -> t.get(null));
- assertThrows(IllegalArgumentException.class, () -> t.contains(null));
- assertThrows(IllegalArgumentException.class, () -> t.remove(null));
- assertThrows(IllegalArgumentException.class, () -> t.startsWith(null));
- }
-
- @Test
- void isEmptyAndSize() {
- var t = new PatriciaTrie();
- assertTrue(t.isEmpty());
- t.put("x", 10);
- assertFalse(t.isEmpty());
- assertEquals(1, t.size());
- t.remove("x");
- assertTrue(t.isEmpty());
- assertEquals(0, t.size());
+ assertEquals(2, t.size(), "Size should increment when a split creates a new key branch");
+ assertTrue(t.contains("romane"));
+ assertTrue(t.contains("romanus"));
+ assertEquals(1, t.get("romane"));
+ assertEquals(2, t.get("romanus"));
}
-}
From 428d04b7871858eaf462412c6097ba662006ff6b Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:35:59 +0530
Subject: [PATCH 07/13] Refactor PatriciaTrie for improved readability
---
.../datastructures/tries/PatriciaTrie.java | 74 ++++++++-----------
1 file changed, 29 insertions(+), 45 deletions(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index 703ba14255ae..b46be97356cb 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -1,42 +1,38 @@
package com.thealgorithms.datastructures.tries;
-// FIX: Placed each import on its own line for proper formatting.
import java.util.HashMap;
import java.util.Map;
/**
* Patricia (radix) trie for String keys and generic values.
*
- * A Patricia Trie is a memory-optimized trie where each node with only one
- * child is merged with its child. This results in edges being labeled with
- * strings instead of single characters.
+ * A Patricia Trie is a memory-optimized trie where nodes with a single child
+ * are merged, so edges are labeled with strings instead of single characters.
*
* Operations are O(L) where L is the key length.
*
* Contracts:
*
- * - Null keys are not allowed (IllegalArgumentException).
- * - The empty-string key ("") is a valid key.
- * - Null values are not allowed (IllegalArgumentException).
+ * - Null keys are not allowed (IllegalArgumentException).
+ * - The empty-string key ("") is a valid key.
+ * - Null values are not allowed (IllegalArgumentException).
*
*
*/
-// FIX: Added a newline after the Javadoc for proper formatting.
public final class PatriciaTrie {
/** Node with compressed outgoing edges (label -> child). */
private static final class Node {
- Map> children = new HashMap<>();
- boolean hasValue;
- V value;
+ private final Map> children = new HashMap<>();
+ private boolean hasValue;
+ private V value;
}
private final Node root = new Node<>();
private int size; // number of stored keys
/** Creates an empty Patricia trie. */
- public PatriciaTrie() {
- }
+ public PatriciaTrie() {}
/**
* Inserts or updates the value associated with {@code key}.
@@ -95,7 +91,6 @@ public boolean remove(String key) {
if (key == null) {
throw new IllegalArgumentException("key must not be null");
}
- // The empty key is a special case as it's stored on the root node.
if (key.isEmpty()) {
if (root.hasValue) {
root.hasValue = false;
@@ -154,11 +149,10 @@ private void insert(Node node, String key, V value) {
int cpl = commonPrefixLen(edge, key);
if (cpl == 0) {
- continue; // No common prefix, check next child.
+ continue; // No common prefix, try next edge.
}
- // Case 1: The entire edge and key match perfectly.
- // e.g., edge="apple", key="apple". Update the value at the child node.
+ // Case 1: edge == key
if (cpl == edge.length() && cpl == key.length()) {
Node child = e.getValue();
if (!child.hasValue) {
@@ -169,8 +163,7 @@ private void insert(Node node, String key, V value) {
return;
}
- // Case 2: The edge is a prefix of the key.
- // e.g., edge="apple", key="applepie". Recurse on the child node.
+ // Case 2: edge is prefix of key
if (cpl == edge.length() && cpl < key.length()) {
Node child = e.getValue();
String rest = key.substring(cpl);
@@ -178,8 +171,7 @@ private void insert(Node node, String key, V value) {
return;
}
- // Case 3: The key and edge partially match, requiring a split.
- // e.g., edge="romane", key="romanus" -> split at "roman".
+ // Case 3: partial overlap → split
String prefix = edge.substring(0, cpl);
String edgeSuffix = edge.substring(cpl);
String keySuffix = key.substring(cpl);
@@ -188,30 +180,29 @@ private void insert(Node node, String key, V value) {
node.children.remove(edge);
node.children.put(prefix, mid);
- // The old child is re-attached to the new middle node.
+ // Reattach old child under the split node
Node oldChild = e.getValue();
mid.children.put(edgeSuffix, oldChild);
if (keySuffix.isEmpty()) {
- // The new key ends at the split point, e.g., inserting "roman"
+ // New key ends at split
if (!mid.hasValue) {
size++;
}
mid.hasValue = true;
mid.value = value;
} else {
- // The new key branches off after the split point, e.g., "romanus"
+ // New branch after split
Node leaf = new Node<>();
leaf.hasValue = true;
leaf.value = value;
mid.children.put(keySuffix, leaf);
- // FIX: This was the main bug. A new key was added, but size was not incremented.
- size++;
+ size++; // important: new key added
}
return;
}
- // Case 4: No existing edge shares a prefix. Create a new one.
+ // Case 4: no shared prefix; create a new edge
Node leaf = new Node<>();
leaf.hasValue = true;
leaf.value = value;
@@ -250,11 +241,9 @@ private Node findPrefixNode(Node node, String prefix) {
return null;
}
- /**
- * Recursive removal with cleanup (merging pass-through nodes).
- */
+ /** Recursive removal with cleanup (merging pass-through nodes). */
private boolean removeRecursive(Node parent, String key) {
- // Iterate on a snapshot to allow modification during the loop.
+ // Iterate on a snapshot to allow modification during the loop
for (Map.Entry> entry : new HashMap<>(parent.children).entrySet()) {
String edge = entry.getKey();
Node child = entry.getValue();
@@ -266,16 +255,14 @@ private boolean removeRecursive(Node parent, String key) {
String rest = key.substring(edge.length());
boolean removed;
if (rest.isEmpty()) {
- // This is the node to delete.
if (!child.hasValue) {
- return false; // Key doesn't actually exist.
+ return false; // key not present
}
child.hasValue = false;
child.value = null;
size--;
removed = true;
} else {
- // Continue search down the tree.
removed = removeRecursive(child, rest);
}
@@ -283,23 +270,20 @@ private boolean removeRecursive(Node parent, String key) {
return false;
}
- // Post-recursion cleanup: merge nodes if necessary.
+ // Cleanup/compaction
if (!child.hasValue) {
- int childDegree = child.children.size();
- if (childDegree == 0) {
- // If child is now a valueless leaf, remove it.
+ int degree = child.children.size();
+ if (degree == 0) {
parent.children.remove(edge);
- } else if (childDegree == 1) {
- // If child is a pass-through node, merge it with its own child.
- Map.Entry> grandchildEntry = child.children.entrySet().iterator().next();
- String grandEdge = grandchildEntry.getKey();
- Node grandchild = grandchildEntry.getValue();
-
+ } else if (degree == 1) {
+ Map.Entry> gc = child.children.entrySet().iterator().next();
+ String grandEdge = gc.getKey();
+ Node grandchild = gc.getValue();
parent.children.remove(edge);
parent.children.put(edge + grandEdge, grandchild);
}
}
- return true; // Key was found and processed in this path.
+ return true; // processed on this path
}
return false;
}
From 1e5faa8efe7b9551a6e8e84abebfed8a1e92202a Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:36:25 +0530
Subject: [PATCH 08/13] Refactor PatriciaTrieTest with additional test cases
---
.../tries/PatriciaTrieTest.java | 68 +++++++++++++++++--
1 file changed, 64 insertions(+), 4 deletions(-)
diff --git a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
index b36f6a7937d3..0d2a3438709f 100644
--- a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
+++ b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
@@ -1,11 +1,18 @@
-@Test
+package com.thealgorithms.datastructures.tries;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+public class PatriciaTrieTest {
+
+ @Test
void insert_splitCausesNewBranch_incrementsSize() {
var t = new PatriciaTrie();
- t.put("romane", 1); // An initial key
+ t.put("romane", 1);
assertEquals(1, t.size());
- // This insertion will split the "romane" edge at "roman"
- // and create a new branch for "us". This is where the bug occurred.
+ // Split "romane" at "roman", create branch "us"
t.put("romanus", 2);
assertEquals(2, t.size(), "Size should increment when a split creates a new key branch");
@@ -14,3 +21,56 @@ void insert_splitCausesNewBranch_incrementsSize() {
assertEquals(1, t.get("romane"));
assertEquals(2, t.get("romanus"));
}
+
+ @Test
+ void basicPutGetContainsAndRemove() {
+ var t = new PatriciaTrie();
+ assertTrue(t.isEmpty());
+
+ t.put("", "root"); // empty key
+ t.put("a", "x");
+ t.put("ab", "y");
+ t.put("abc", "z");
+
+ assertEquals(4, t.size());
+ assertEquals("root", t.get(""));
+ assertEquals("x", t.get("a"));
+ assertEquals("y", t.get("ab"));
+ assertEquals("z", t.get("abc"));
+
+ assertTrue(t.contains("ab"));
+ assertFalse(t.contains("abcd"));
+
+ assertTrue(t.startsWith("ab"));
+ assertTrue(t.startsWith("abc"));
+ assertFalse(t.startsWith("zzz"));
+
+ assertTrue(t.remove("ab"));
+ assertFalse(t.contains("ab"));
+ assertEquals(3, t.size());
+
+ // removing non-existent
+ assertFalse(t.remove("ab"));
+ assertEquals(3, t.size());
+ }
+
+ @Test
+ void updatesDoNotIncreaseSize() {
+ var t = new PatriciaTrie();
+ t.put("apple", 1);
+ t.put("apple", 2);
+ assertEquals(1, t.size());
+ assertEquals(2, t.get("apple"));
+ }
+
+ @Test
+ void nullContracts() {
+ var t = new PatriciaTrie();
+ assertThrows(IllegalArgumentException.class, () -> t.put(null, 1));
+ assertThrows(IllegalArgumentException.class, () -> t.put("x", null));
+ assertThrows(IllegalArgumentException.class, () -> t.get(null));
+ assertThrows(IllegalArgumentException.class, () -> t.contains(null));
+ assertThrows(IllegalArgumentException.class, () -> t.remove(null));
+ assertThrows(IllegalArgumentException.class, () -> t.startsWith(null));
+ }
+}
From bf8a751efeb6900112b448fd4133a94de824038e Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:41:39 +0530
Subject: [PATCH 09/13] Update PatriciaTrieTest.java
---
.../thealgorithms/datastructures/tries/PatriciaTrieTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
index 0d2a3438709f..5f4f566d25d8 100644
--- a/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
+++ b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
@@ -27,7 +27,7 @@ void basicPutGetContainsAndRemove() {
var t = new PatriciaTrie();
assertTrue(t.isEmpty());
- t.put("", "root"); // empty key
+ t.put("", "root"); // empty key
t.put("a", "x");
t.put("ab", "y");
t.put("abc", "z");
From c1a45ae4cd60df07e1ea3473e1378a6c24f3b2fd Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:42:42 +0530
Subject: [PATCH 10/13] Refactor PatriciaTrie constructor for clarity
---
.../com/thealgorithms/datastructures/tries/PatriciaTrie.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index b46be97356cb..fcf5e7ab7cfa 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -32,8 +32,8 @@ private static final class Node {
private int size; // number of stored keys
/** Creates an empty Patricia trie. */
- public PatriciaTrie() {}
-
+ public PatriciaTrie() {
++ }
/**
* Inserts or updates the value associated with {@code key}.
*
From 0cdf91603d6df7a8e3396f30d91cc05e06bd0284 Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:44:51 +0530
Subject: [PATCH 11/13] Update PatriciaTrie.java
---
.../com/thealgorithms/datastructures/tries/PatriciaTrie.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index fcf5e7ab7cfa..b5cc913ef3fe 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -33,7 +33,7 @@ private static final class Node {
/** Creates an empty Patricia trie. */
public PatriciaTrie() {
-+ }
++ }
/**
* Inserts or updates the value associated with {@code key}.
*
From ae81b3457dd7af13d383e4ef571ad2c461947cf0 Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:46:14 +0530
Subject: [PATCH 12/13] Add empty constructor to PatriciaTrie
---
.../com/thealgorithms/datastructures/tries/PatriciaTrie.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index b5cc913ef3fe..ad7dcb2d94f0 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -33,6 +33,7 @@ private static final class Node {
/** Creates an empty Patricia trie. */
public PatriciaTrie() {
+
+ }
/**
* Inserts or updates the value associated with {@code key}.
From 87a42a9acca975a01334343ab10a906c8af12fc0 Mon Sep 17 00:00:00 2001
From: Srishti Soni <92056170+shimmer12@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:49:26 +0530
Subject: [PATCH 13/13] Update PatriciaTrie.java
---
.../com/thealgorithms/datastructures/tries/PatriciaTrie.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
index ad7dcb2d94f0..26a60b5865d6 100644
--- a/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -33,8 +33,7 @@ private static final class Node {
/** Creates an empty Patricia trie. */
public PatriciaTrie() {
-
-+ }
+ }
/**
* Inserts or updates the value associated with {@code key}.
*