From 25dde41c09f7729cce94fa50676985e9aa518e80 Mon Sep 17 00:00:00 2001
From: shimmer12
Date: Thu, 9 Oct 2025 03:07:26 +0530
Subject: [PATCH 1/2] added PATRICIA Trie
---
.../datastructures/tries/PatriciaTrie.java | 0
.../datastructures/tries/PatriciaTrie.java | 126 ++++++++++++++++++
2 files changed, 126 insertions(+)
create mode 100644 src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
create mode 100644 src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrie.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/PatriciaTrie.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
new file mode 100644
index 000000000000..69cf1687b51b
--- /dev/null
+++ b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrie.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 72d77b93b37e526e11853d8a15f154629b1f95f1 Mon Sep 17 00:00:00 2001
From: shimmer12
Date: Thu, 9 Oct 2025 03:09:36 +0530
Subject: [PATCH 2/2] added PATRICIA Trie
---
.../datastructures/tries/PatriciaTrie.java | 347 ++++++++++++++++++
...atriciaTrie.java => PatriciaTrieTest.java} | 0
2 files changed, 347 insertions(+)
rename src/test/java/com/thealgorithms/datastructures/tries/{PatriciaTrie.java => PatriciaTrieTest.java} (100%)
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/PatriciaTrie.java b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
similarity index 100%
rename from src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
rename to src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java