Post

Tries in Kotlin: Building an Autocomplete System

A deep dive into the Trie (Prefix Tree) data structure for efficient string searching, prefix matching, and word suggestions.

Tries in Kotlin: Building an Autocomplete System

A Trie, also known as a Prefix Tree, is a specialized tree-based data structure used to store a set of strings. Unlike a Hash Map, where keys are stored as a whole, a Trie stores keys character by character.

This structure is incredibly efficient for features we use every day, such as search engine autocomplete and spell checkers.


1. The Trie Node

In a Trie, each node represents a character. It contains a map of its children (the characters that can follow it) and a boolean flag to indicate if it marks the completion of a word.

1
2
3
4
data class TrieNode(
    val children: MutableMap<Char, TrieNode> = sortedMapOf<Char, TrieNode>(),
    var isEndOfWord: Boolean = false
)

Insertion

To insert a word, we iterate through its characters. If a character doesn’t exist as a child of the current node, we create a new node. Finally, we mark the last node as isEndOfWord.

1
2
3
4
5
6
7
fun insert(word: String, root: TrieNode) {
    var current = root
    for (char in word) {
        current = current.children.getOrPut(char) { TrieNode() }
    }
    current.isEndOfWord = true
}

Searching follows the same path. If we successfully navigate through all characters and the final node reached is marked as an end of a word, the word exists in our Trie.

1
2
3
4
5
6
7
8
9
10
11
12
fun search(word: String, root: TrieNode): Boolean {
    var currNode = root

    for(i in word) {
        if(currNode.children[i] != null) {
            currNode = currNode.children[i]!!
        } else {
            return false
        }
    }
    return currNode.isEndOfWord
}

3. Autocomplete and Prefix Suggestions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fun getSuggestions(prefix: String, root: TrieNode): List<String> {
    var current = root

    // 1. Move to the node representing the end of the prefix
    for (char in prefix) {
        val node = current.children[char] ?: return emptyList()
        current = node
    }

    // 2. Collect all words from this node downward
    val results = mutableListOf<String>()
    collectWords(current, StringBuilder(prefix), results)
    return results
}

private fun collectWords(node: TrieNode, currentWord: StringBuilder, results: MutableList<String>) {
    if (node.isEndOfWord) {
        results.add(currentWord.toString())
    }

    for ((char, childNode) in node.children) {
        currentWord.append(char)
        collectWords(childNode, currentWord, results)
        currentWord.deleteCharAt(currentWord.length - 1) // Backtrack
    }
}

Complexity Analysis

OperationTime ComplexitySpace Complexity
InsertO(L)O(L)$$O(N \times L)
SearchO(L)O(1)
Prefix SearchO(L)O(1)

Where L is the length of the word and $N$ is the number of words.

Source Code

🚀 Code: All the code for this tutorial is available in my DSA-Kotlin Repository.

This post is licensed under CC BY 4.0 by the author.