Implementing a Trie Data Structure for Prefix-Based String Operations

Core Structure

  • Root node: An empty node serving as the entry point; its children represent the first characters of stored strings.
  • Internal nodes: Represent intermediate characters in strings.
  • Leaf nodes: Mark the end of a valid word via a boolean flag, evenif they have children (e.g., "do" and "dog" can coexist).

Basic Implementation

We begin by defining a node that supports 26 lowercase English letters:

class TrieNode {
    private static final int ALPHABET_SIZE = 26;
    TrieNode[] branches = new TrieNode[ALPHABET_SIZE];
    boolean isTerminal;

    TrieNode() {
        isTerminal = false;
        for (int i = 0; i < ALPHABET_SIZE; i++) {
            branches[i] = null;
        }
    }
}

The trie class manages the root and provides core operations:

public class Trie {
    private TrieNode head;

    public Trie() {
        head = new TrieNode();
    }

    public void add(String word) {
        TrieNode current = head;
        for (char c : word.toCharArray()) {
            int position = c - 'a';
            if (current.branches[position] == null) {
                current.branches[position] = new TrieNode();
            }
            current = current.branches[position];
        }
        current.isTerminal = true;
    }

    public boolean contains(String word) {
        TrieNode current = head;
        for (char c : word.toCharArray()) {
            int position = c - 'a';
            if (current.branches[position] == null) {
                return false;
            }
            current = current.branches[position];
        }
        return current != null && current.isTerminal;
    }
}

Extending Functionality

To support deletion, we recursively traverse the path and remove nodes only if they are unused by other words:

public boolean remove(String word) {
    return remove(head, word, 0);
}

private boolean remove(TrieNode node, String word, int depth) {
    if (node == null) return false;

    if (depth == word.length()) {
        if (!node.isTerminal) return false;
        node.isTerminal = false;
        return isEmpty(node);
    }

    int idx = word.charAt(depth) - 'a';
    boolean shouldRemove = remove(node.branches[idx], word, depth + 1);

    if (shouldRemove) {
        node.branches[idx] = null;
        return isEmpty(node) && !node.isTerminal;
    }

    return false;
}

private boolean isEmpty(TrieNode node) {
    for (TrieNode child : node.branches) {
        if (child != null) return false;
    }
    return true;
}

To list all words with a given prefix, we first locate the prefix node, then perform a depth-first traversal:

public void listWordsWithPrefix(String prefix) {
    TrieNode node = findNode(prefix);
    if (node == null) {
        System.out.println("No matches found.");
        return;
    }
    listWords(node, prefix);
}

private void listWords(TrieNode node, String prefix) {
    if (node.isTerminal) {
        System.out.println(prefix);
    }

    for (int i = 0; i < 26; i++) {
        if (node.branches[i] != null) {
            char nextChar = (char)('a' + i);
            listWords(node.branches[i], prefix + nextChar);
        }
    }
}

private TrieNode findNode(String prefix) {
    TrieNode current = head;
    for (char c : prefix.toCharArray()) {
        int idx = c - 'a';
        if (current.branches[idx] == null) return null;
        current = current.branches[idx];
    }
    return current;
}

Application: Student Grade Manager

While tries efficiently store and search names, they are not inherently designed for value updates. To handle grades, we combine the trie with a hash map for O(1) access to associated data:

import java.util.HashMap;

public class StudentGradeSystem {
    private Trie nameIndex;
    private HashMap<String, Integer> gradeMap;

    public StudentGradeSystem() {
        nameIndex = new Trie();
        gradeMap = new HashMap<>();
    }

    public void register(String name, int grade) {
        nameIndex.add(name);
        gradeMap.put(name, grade);
    }

    public void updateGrade(String name, int newGrade) {
        if (!gradeMap.containsKey(name)) {
            throw new IllegalArgumentException("Student not registered.");
        }
        gradeMap.put(name, newGrade);
    }

    public void deregister(String name) {
        if (gradeMap.containsKey(name)) {
            gradeMap.remove(name);
            // Note: Trie node deletion omitted for simplicity
        } else {
            throw new IllegalArgumentException("Student not found.");
        }
    }

    public int getGrade(String name) {
        Integer grade = gradeMap.get(name);
        if (grade == null) {
            throw new IllegalArgumentException("Student not registered.");
        }
        return grade;
    }

    public boolean hasStudent(String name) {
        return nameIndex.contains(name);
    }
}

Usage example:

public static void main(String[] args) {
    StudentGradeSystem system = new StudentGradeSystem();
    system.register("alice", 90);
    system.register("bob", 85);
    system.register("charlie", 92);

    System.out.println(system.hasStudent("alice"));     // true
    System.out.println(system.getGrade("bob"));         // 85
    system.updateGrade("charlie", 98);
    System.out.println(system.getGrade("charlie"));     // 98
    system.deregister("bob");
    System.out.println(system.hasStudent("bob"));       // false
}

While this approach separates storage concerns for efficiency, it leaves orphaned trie nodes after deletions. For full consistency, the trie must be updated to prune unused branches — a trade-off between memory usage and operational complexity.

Tags: Trie prefix-tree string-search data-structure java

Posted on Mon, 22 Jun 2026 16:21:33 +0000 by Ruiser