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..26a60b5865d6
--- /dev/null
+++ b/src/main/java/com/thealgorithms/datastructures/tries/PatriciaTrie.java
@@ -0,0 +1,301 @@
+package com.thealgorithms.datastructures.tries;
+
+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 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).
+ *
+ *
+ */
+public final class PatriciaTrie {
+
+ /** Node with compressed outgoing edges (label -> child). */
+ private static final class Node {
+ 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() {
+ }
+ /**
+ * Inserts or updates the value associated with {@code key}.
+ *
+ * @param key non-null key (empty allowed)
+ * @param value non-null value
+ */
+ 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 non-null key
+ * @return stored value or 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 non-null key
+ * @return true if the key is present
+ */
+ 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.
+ *
+ * Also compacts pass-through nodes (no value + single child) by concatenating
+ * edge labels to keep the trie compressed.
+ *
+ * @param key non-null key
+ * @return true if the key existed and was removed
+ */
+ public boolean remove(String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ if (key.isEmpty()) {
+ if (root.hasValue) {
+ root.hasValue = false;
+ root.value = null;
+ size--;
+ return true;
+ }
+ return false;
+ }
+ return removeRecursive(root, key);
+ }
+
+ /**
+ * 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) {
+ throw new IllegalArgumentException("prefix must not be null");
+ }
+ if (prefix.isEmpty()) {
+ return size > 0;
+ }
+ Node n = findPrefixNode(root, prefix);
+ return n != null;
+ }
+
+ /** Returns the 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) {
+ if (key.isEmpty()) {
+ if (!node.hasValue) {
+ size++;
+ }
+ node.hasValue = true;
+ node.value = value;
+ return;
+ }
+
+ for (Map.Entry> e : node.children.entrySet()) {
+ String edge = e.getKey();
+ int cpl = commonPrefixLen(edge, key);
+
+ if (cpl == 0) {
+ continue; // No common prefix, try next edge.
+ }
+
+ // Case 1: 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 2: edge is prefix of key
+ if (cpl == edge.length() && cpl < key.length()) {
+ Node child = e.getValue();
+ String rest = key.substring(cpl);
+ insert(child, rest, value);
+ return;
+ }
+
+ // Case 3: partial overlap → split
+ String prefix = edge.substring(0, cpl);
+ String edgeSuffix = edge.substring(cpl);
+ String keySuffix = key.substring(cpl);
+
+ Node mid = new Node<>();
+ node.children.remove(edge);
+ node.children.put(prefix, mid);
+
+ // Reattach old child under the split node
+ Node oldChild = e.getValue();
+ mid.children.put(edgeSuffix, oldChild);
+
+ if (keySuffix.isEmpty()) {
+ // New key ends at split
+ if (!mid.hasValue) {
+ size++;
+ }
+ mid.hasValue = true;
+ mid.value = value;
+ } else {
+ // New branch after split
+ Node leaf = new Node<>();
+ leaf.hasValue = true;
+ leaf.value = value;
+ mid.children.put(keySuffix, leaf);
+ size++; // important: new key added
+ }
+ return;
+ }
+
+ // Case 4: no shared prefix; create a new edge
+ 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();
+ if (key.startsWith(edge)) {
+ String rest = key.substring(edge.length());
+ return findNode(e.getValue(), rest);
+ }
+ }
+ 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();
+ if (prefix.startsWith(edge)) {
+ String rest = prefix.substring(edge.length());
+ return findPrefixNode(e.getValue(), rest);
+ }
+ if (edge.startsWith(prefix)) {
+ return e.getValue();
+ }
+ }
+ return null;
+ }
+
+ /** 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
+ for (Map.Entry> entry : new HashMap<>(parent.children).entrySet()) {
+ String edge = entry.getKey();
+ Node child = entry.getValue();
+
+ if (!key.startsWith(edge)) {
+ continue;
+ }
+
+ String rest = key.substring(edge.length());
+ boolean removed;
+ if (rest.isEmpty()) {
+ if (!child.hasValue) {
+ return false; // key not present
+ }
+ child.hasValue = false;
+ child.value = null;
+ size--;
+ removed = true;
+ } else {
+ removed = removeRecursive(child, rest);
+ }
+
+ if (!removed) {
+ return false;
+ }
+
+ // Cleanup/compaction
+ if (!child.hasValue) {
+ int degree = child.children.size();
+ if (degree == 0) {
+ parent.children.remove(edge);
+ } 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; // processed on this path
+ }
+ return false;
+ }
+
+ /** 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());
+ for (int i = 0; i < n; i++) {
+ if (a.charAt(i) != b.charAt(i)) {
+ return i;
+ }
+ }
+ return n;
+ }
+}
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..5f4f566d25d8
--- /dev/null
+++ b/src/test/java/com/thealgorithms/datastructures/tries/PatriciaTrieTest.java
@@ -0,0 +1,76 @@
+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);
+ assertEquals(1, t.size());
+
+ // 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");
+ assertTrue(t.contains("romane"));
+ assertTrue(t.contains("romanus"));
+ 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));
+ }
+}