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}. *