Home/Blog/Java/Algorithms for AI: Trie, Graph Sorting, and K-Way Merge for LLM Systems
JavaAdvancedโ€ข13 min readโ€ข

Algorithms for AI: Trie, Graph Sorting, and K-Way Merge for LLM Systems

Master the core algorithms that power LLM token filtering, agent dependency planning, and hybrid search retrieval.

Abstract Algorithms

Abstract Algorithms

Helping engineers master software engineering topics.

TLDR: Generative AI relies heavily on classic computer science algorithms under the hood. In this guide, we explore how to implement Trie prefix filtering for constrained model decoding, Topological Sort for multi-agent scheduling, and K-Way Merge for merging dense and lexical search rankings.


๐Ÿ“– Part 1: Real-World Motivation for Algorithms in AI

Many software engineers think that AI engineering is only about calling Python APIs or writing prompt wrappers. But in production, when LLM systems need to interface with deterministic code, classic algorithmic bottlenecks appear:

  • Unconstrained Token Excursions: You prompt an LLM to return only "YES" or "NO", but under low temperature it still occasionally generates "I agree" or "I cannot answer." This happens because language models operate probabilistically, calculating logits (log-odds) for every token in their vocabulary on each generation step. Without deterministic interceptors, the model can choose low-probability alternative tokens that break JSON schemas or API inputs.
  • Cyclic Dependency Deadlocks: A multi-step planner agent designs a sequence of code compilation and package installation tasks, but inserts circular dependencies (e.g. Task A requires Task B to compile, but Task B requires Task A's output classes). Without cycle checks, execution threads spin, triggering timeouts, database lock contentions, and resource exhaustion.
  • Hybrid Search Score Merging: When combining lexical search (BM25) and dense vector search, you receive multiple sorted lists of matches. Merging them inefficiently (like dumping all hits into a single array and re-sorting) scales quadratically under high query-per-second (QPS) loads, causing database response latency to spike.

To build production-grade AI platforms, you must master the fundamental data structures and algorithms that solve these challenges at scale.


๐Ÿ” Core Intuition and Analogies

Let's break down the intuition behind the three algorithms we will build:

  • Trie (Prefix Tree) for Guided Decoding: Think of a Trie as a decision tree on a keyboard. If you only allow the agent to type the commands "ADD", "DELETE", and "EDIT", once they type "A", the only allowed next character on the keyboard is "D". By traversing a Trie step-by-step alongside the model's token selection loop, we can zero out the probabilities of invalid next characters, forcing the model to generate only valid words.
  • Topological Sort for Agent Task Dependencies: Imagine cooking a recipe where some tasks must happen before others (e.g., you must preheat the oven before baking the cake, and you must mix the batter before baking). This is equivalent to scheduling college courses with prerequisites. Topological sort arranges these tasks linearly, ensuring that dependencies are processed before target tasks and flagging circular prerequisite traps.
  • K-Way Merge for Hybrid Search: Imagine $K$ conveyor belts, each delivering sorted boxes of documents from different search algorithms (dense vectors, keyword index, semantic ranks). Instead of dumping all boxes into one big pile and sorting them from scratch, we look only at the front box of each belt and use a Min-Heap (Priority Queue) to pull the highest-scored document. This keeps our merge process fast and lightweight.

โš™๏ธ Core Algorithm Mechanics

The following flowchart details the search traversal path inside a Trie node structure to validate character paths:

flowchart TD
    Start([Start Trie Search]) --> Root{Is Root null?}
    Root -- Yes --> Empty[Return empty valid tokens list]
    Root -- No --> Traverse[Loop through characters of prefix]
    Traverse --> Match{Does child node exist?}
    Match -- No --> Empty
    Match -- Yes --> NextNode[Move to child node]
    NextNode --> Traverse
    Traverse --> EndOfPrefix{All prefix characters traversed?}
    EndOfPrefix -- Yes --> Gather[Traverse subtree to collect all children words]
    Gather --> Return[Return matching words]

This traversal graph demonstrates how prefix queries are validated. The algorithm matches characters sequentially. If any character path is missing, the search short-circuits to an empty list, saving execution cycles compared to regular expression scanning over massive vocabularies.

During topological sort, the mechanics depend on tracking in-degrees (the count of incoming dependency edges pointing to a node). Nodes with an in-degree of zero are placed in a queue. As we pop a node, we decrement the in-degree of its neighbors. If a neighbor's in-degree drops to zero, it enters the queue. This sequence continues until the queue is empty.


๐Ÿง  Deep Dive: Complexity and Code Skeletons

To build clean algorithmic implementations, we must understand their internal layouts, Big-O characteristics, and baseline patterns.

Internals

  • Trie Nodes: Structured as a nested tree where each node has a character key, a map of children pointers, and a boolean flag indicating whether the node marks the end of a valid word. For small alphabets, children maps can be replaced with fixed-size arrays to optimize memory lookup.
  • Graph Adjacency Lists: For dependency sorting, tasks are represented as graph vertices. Directed edges model execution flows. A cycle occurs if a back-edge points to an ancestor node in the active traversal stack. Adjacency lists provide optimal storage efficiency for sparse task dependency graphs.
  • Heap Arrays: K-way merging uses a priority queue built as a binary heap array. The heap maintains a structural invariant: the parent node's value is always greater than or equal to (or less than or equal to) its children. The element at index $i$ has children at indices $2i + 1$ and $2i + 2$, enabling rapid heapify-up and heapify-down bubble operations.

Performance Analysis

  • Trie Prefix Search:
    • Time Complexity: $O(L)$ where $L$ is the length of the query string. This is independent of the total number of words stored in the Trie, which makes it faster than checking lists.
    • Space Complexity: $O(\Sigma \times N)$ where $\Sigma$ is the alphabet/token size and $N$ is the number of total characters.
  • Topological Sort (Kahn's):
    • Time Complexity: $O(V + E)$ where $V$ is the number of tasks and $E$ is the number of dependency edges. Each node and edge is processed exactly once.
    • Space Complexity: $O(V)$ to maintain in-degree counters and recursion queues.
  • K-Way Merge:
    • Time Complexity: $O(N \log K)$ where $N$ is the total number of documents across all lists and $K$ is the number of sorted lists. The heap size never exceeds $K$.
    • Space Complexity: $O(K)$ to hold the active elements in the binary heap structure.

๐Ÿ“Š Visualizing the Algorithm State Transitions

Below is a state transition visualization of Kahn's Algorithm for Topological Sort on a set of dependent agent tasks.

flowchart TD
    S1[Task Graph: A -> B, A -> C, B -> D, C -> D] --> S2[Calculate In-Degrees: A=0, B=1, C=1, D=2]
    S2 --> S3[Enqueue In-Degree = 0 nodes: Queue = A]
    S3 --> S4[Pop A: Add to Sorted List. Decrement neighbors: B=0, C=0]
    S4 --> S5[Enqueue new In-Degree = 0 nodes: Queue = B, C]
    S5 --> S6[Pop B: Add to Sorted List. Decrement neighbors: D=1]
    S6 --> S7[Pop C: Add to Sorted List. Decrement neighbors: D=0]
    S7 --> S8[Enqueue D: Queue = D]
    S8 --> S9[Pop D: Add to Sorted List. Queue empty]
    S9 --> S10[Final Order: A, B, C, D]

This visualization traces how Kahn's algorithm resolves graph nodes step-by-step. By maintaining in-degree counters, we isolate nodes with zero dependencies, process them, and decrement their neighbors' counters. If any nodes remain unprocessed, it indicates a circular dependency loop.


๐Ÿงช Practical Java Implementations: 3 Classic Problems

Every coding example section below has an introductory paragraph: what is being demonstrated, why this scenario, what to look for. Never start with a code block.

Problem 1: Trie Prefix Tree for LLM Token Filtering

This implementation demonstrates a complete Trie data structure designed to filter generated text tokens. In generative AI pipelines, this prevents hallucination by restricting token options to a closed dictionary vocabulary. Pay close attention to how getValidNextCharacters returns child map keys dynamically to validate character pathways.

package dev.abstractalgorithms.dsa;

import java.util.*;

public class TokenFilterTrie {
    private static class TrieNode {
        private final Map<Character, TrieNode> children = new HashMap<>();
        private boolean isWord = false;
    }

    private final TrieNode root = new TrieNode();

    // Inserts a new word into the prefix tree
    public void insert(String word) {
        if (word == null || word.isEmpty()) return;
        TrieNode current = root;
        for (char ch : word.toCharArray()) {
            current = current.children.computeIfAbsent(ch, k -> new TrieNode());
        }
        current.isWord = true;
    }

    // Fetches valid next character routes from prefix state
    public Set<Character> getValidNextCharacters(String prefix) {
        if (prefix == null) return Collections.emptySet();
        TrieNode current = root;
        for (char ch : prefix.toCharArray()) {
            current = current.children.get(ch);
            if (current == null) {
                return Collections.emptySet(); // Prefix path does not exist
            }
        }
        return current.children.keySet();
    }

    // Autocompletes word routes starting with the specified prefix
    public List<String> autocomplete(String prefix) {
        List<String> results = new ArrayList<>();
        TrieNode current = root;
        for (char ch : prefix.toCharArray()) {
            current = current.children.get(ch);
            if (current == null) {
                return results;
            }
        }
        searchHelper(current, new StringBuilder(prefix), results);
        return results;
    }

    private void searchHelper(TrieNode node, StringBuilder currentWord, List<String> results) {
        if (node.isWord) {
            results.add(currentWord.toString());
        }
        for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
            currentWord.append(entry.getKey());
            searchHelper(entry.getValue(), currentWord, results);
            currentWord.deleteCharAt(currentWord.length() - 1);
        }
    }
}

Problem 2: Topological Sort for Multi-Agent Task Dependency Resolution

This code shows Kahn's Algorithm for Topological Sort, implemented in Java. It resolves execution sequences for dependencies in AI agent pipelines, ensuring that files are cloned before compilation, and compilation finishes before test execution. The code also runs a cycle check to flag deadlocks from circular plans.

package dev.abstractalgorithms.dsa;

import java.util.*;

public class AgentTaskScheduler {
    public static class Task {
        private final String id;
        private final List<String> dependencies = new ArrayList<>();

        public Task(String id) {
            this.id = id;
        }

        public void dependsOn(String dependencyTaskId) {
            dependencies.add(dependencyTaskId);
        }

        public String getId() { return id; }
        public List<String> getDependencies() { return dependencies; }
    }

    // Schedules tasks linearly using Kahn's algorithm
    public List<String> schedule(List<Task> tasks) {
        Map<String, List<String>> adjList = new HashMap<>();
        Map<String, Integer> inDegree = new HashMap<>();

        // Initialize graphs
        for (Task task : tasks) {
            adjList.put(task.getId(), new ArrayList<>());
            inDegree.put(task.getId(), 0);
        }

        // Build edges (reversing to model: dependency -> task)
        for (Task task : tasks) {
            String target = task.getId();
            for (String dep : task.getDependencies()) {
                if (adjList.containsKey(dep)) {
                    adjList.get(dep).add(target);
                    inDegree.put(target, inDegree.get(target) + 1);
                }
            }
        }

        Queue<String> queue = new LinkedList<>();
        for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
            if (entry.getValue() == 0) {
                queue.add(entry.getKey());
            }
        }

        List<String> order = new ArrayList<>();
        while (!queue.isEmpty()) {
            String current = queue.poll();
            order.add(current);

            for (String neighbor : adjList.get(current)) {
                inDegree.put(neighbor, inDegree.get(neighbor) - 1);
                if (inDegree.get(neighbor) == 0) {
                    queue.add(neighbor);
                }
            }
        }

        // If sorted count does not match total nodes, graph contains a cycle
        if (order.size() != tasks.size()) {
            throw new IllegalArgumentException("Circular dependency detected! Cycle prevents scheduling.");
        }

        return order;
    }
}

Problem 3: K-Way Merge for Merging Hybrid Retrieval Lists

This class demonstrates merging $K$ pre-sorted document relevance lists using a Min-Heap. In search infrastructures, dense vector search and keyword matchers run in parallel, returning sorted ranks. This algorithm unites these outputs into a single top-$M$ list without sorting the whole set.

package dev.abstractalgorithms.dsa;

import java.util.*;

public class HybridSearchMerger {
    public static class DocumentMatch {
        private final String documentId;
        private final double relevanceScore;

        public DocumentMatch(String documentId, double relevanceScore) {
            this.documentId = documentId;
            this.relevanceScore = relevanceScore;
        }

        public String getDocumentId() { return documentId; }
        public double getRelevanceScore() { return relevanceScore; }
    }

    private static class HeapNode {
        private final DocumentMatch match;
        private final int listIndex;
        private final int elementIndex;

        public HeapNode(DocumentMatch match, int listIndex, int elementIndex) {
            this.match = match;
            this.listIndex = listIndex;
            this.elementIndex = elementIndex;
        }
    }

    // Merges sorted lists using a Min-Heap algorithm representation
    public List<DocumentMatch> merge(List<List<DocumentMatch>> sortedLists, int limit) {
        List<DocumentMatch> mergedResult = new ArrayList<>();
        if (sortedLists == null || sortedLists.isEmpty()) {
            return mergedResult;
        }

        // Heap based on score (descending order search requires highest score first)
        PriorityQueue<HeapNode> maxHeap = new PriorityQueue<>(
            (a, b) -> Double.compare(b.match.getRelevanceScore(), a.match.getRelevanceScore())
        );

        // Populate initial element from each list
        for (int i = 0; i < sortedLists.size(); i++) {
            List<DocumentMatch> list = sortedLists.get(i);
            if (list != null && !list.isEmpty()) {
                maxHeap.add(new HeapNode(list.get(0), i, 0));
            }
        }

        while (!maxHeap.isEmpty() && mergedResult.size() < limit) {
            HeapNode node = maxHeap.poll();
            mergedResult.add(node.match);

            int nextElementIndex = node.elementIndex + 1;
            List<DocumentMatch> originalList = sortedLists.get(node.listIndex);

            if (nextElementIndex < originalList.size()) {
                maxHeap.add(new HeapNode(originalList.get(nextElementIndex), node.listIndex, nextElementIndex));
            }
        }

        return mergedResult;
    }
}

๐ŸŒ Real-World Applications: Search Engines and LLMs

  • Guided Grammar Parsing: Frameworks like Outlines or JSON-Mode use Tries containing terminal tokens to constrain next-token generation on LLM serving hosts, blocking syntax errors at generation time.
  • Vector DB Hybrid Search: Vector databases compile lexical matches from Lucene and dense similarity index lists from HNSW. They merge these paths using K-Way merge heaps. For details on how vector databases manage indices under the hood, see Vector Databases Explained.
  • LangGraph Agent Compilation: Multi-agent tools like LangGraph compile execution dependency charts. They check for execution validity and detect scheduling loops using Topological Sort. To learn about assembling RAG pipelines using such workflows, see LangChain RAG Walkthrough.

โš–๏ธ Trade-offs and Failure Modes

  • Trie Memory vs. Speed: A Trie provides lookup in time proportional to string length, but maps eat memory. You trade off memory for search speed.
  • Topological Sort Stack Limits: While Kahn's algorithm handles graphs using a queue, DFS-based alternatives risk stack overflow crashes on deep dependency chains.
  • K-Way Merge Heap Sizing: Merging hundreds of lists expands the heap footprint. If $K$ is extremely large, the overhead of bubble-down operations inside the heap array degrades performance.

๐Ÿงญ Decision Guide: Choosing the Right Data Structure

Choose the data structure that best matches your latency and memory footprint constraints.

Algorithm / PatternTime ComplexitySpace ComplexityBest Used For
Trie Prefix Tree$O(L)$ where $L$ is length$O(\Sigma \times N)$guided LLM token generation, autocomplete
Topological Sort$O(V + E)$$O(V)$task dependency scheduling, build pipelines
K-Way Merge Heap$O(N \log K)$$O(K)$hybrid search result merging, external sorting

๐Ÿ› ๏ธ Library / OSS Implementations: What is Used in Production

  • outlines (Python): Under the hood, this guided-generation library parses regexes and JSON schemas into Tries to match token vocabularies during LLM output sampling.
  • Apache Lucene: Uses finite state transducers (FSTs), a memory-efficient variant of Tries, to store term dictionaries for reverse-index lookups.
  • Java java.util.PriorityQueue: The standard library class used to implement K-Way mergers and Dijkstra pathfinders.

๐Ÿ“š Lessons Learned: Common Algorithmic Bugs

  1. Cycle Detection Ignorance: When writing graph schedulers, forgetting to handle cycle detection throws the execution thread into an infinite stall. Always verify output list size equals the task vertex size.
  2. Off-by-One Array Indexing: When stepping through list indices during K-Way merges, failing to check bounds on empty child lists throws IndexOutOfBoundsException.
  3. Memory Footprint from Child Maps: Implementing tries using simple array maps new TrieNode[256] wastes space for sparse subtrees. Favor hash-map instantiation or double-array tries for sparse vocabularies.

๐Ÿ“Œ Key Takeaways

  • Guided Sampling: Tries constrain model output to valid vocabularies, ensuring system integration contracts remain intact.
  • Deterministic Plans: Use Topological Sort to schedule agent task execution dependency chains, preventing circular execution deadlocks.
  • Fast List Merges: K-Way Merge Heaps combine independent ranked search results without scanning whole lists.

Article tools

Reader feedback

Was this article useful?

Rate it if it helped, then continue with the next deep dive when you are ready.