Requirements:- Functional: insert/update sentences with frequencies; query top-k hot sentences for a prefix.- Non-functional: low query latency (sub-100ms), support large corpus (millions of sentences), memory vs latency tradeoff configurable.High-level design:- Trie where each node stores: - children map (char → node) - optional terminal flag and frequency - top_k list: sorted list of up to k (sentence, freq) for fast lookup- On insert/update: walk trie, update node.top_k at each node (maintain sorted by freq, tie-break lexicographically).- Query: follow prefix to node and return node.top_k in O(len(prefix) + k).Python implementation (simple, single-threaded, in-memory):python
from bisect import bisect_left
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
self.freq = 0
self.top_k = [] # list of (freq, sentence), sorted descending freq, then lex
class Autocomplete:
def __init__(self, k=3):
self.root = TrieNode()
self.k = k
self.freq_map = {} # maps sentence -> freq
def _insert_topk(self, node, sentence, freq):
entry = (-freq, sentence) # negative freq for ascending sort via bisect
lst = node.top_k
# remove old if present
for i, e in enumerate(lst):
if e[1] == sentence:
lst.pop(i)
break
# insert keeping ascending by (-freq, sentence)
idx = bisect_left(lst, entry)
lst.insert(idx, entry)
if len(lst) > self.k:
lst.pop() # remove worst
def insert(self, sentence, count=1):
new_freq = self.freq_map.get(sentence, 0) + count
self.freq_map[sentence] = new_freq
node = self.root
self._insert_topk(node, sentence, new_freq)
for ch in sentence:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
self._insert_topk(node, sentence, new_freq)
node.is_end = True
node.freq = new_freq
def query(self, prefix):
node = self.root
for ch in prefix:
if ch not in node.children:
return []
node = node.children[ch]
# convert back to (sentence, freq) sorted by freq desc
return [(s, -f) for f, s in node.top_k]
Key points and reasoning:- Storing top-k per node gives O(len(prefix)) query time to return suggestions (constant k).- Insert/update costs O(L * k) where L is sentence length (because top_k update per node uses O(k) ops). Using heap or balanced tree can alter constants.- Memory: storing top-k at every node multiplies memory by O(nodes * k * avg_sentence_length_ref). For large corpora this can be heavy.- Trade-offs: - Faster queries: store top-k at nodes (as above). - Lower memory: store only pointers to sentences in a central table, or compute suggestions on-the-fly by traversing subtree (slow: O(#subtree) per query). - Sharding and persistence: for scale, shard Trie by prefix ranges, store in-memory caches for hot prefixes, back with persistent key-value store (Redis, RocksDB). - Updates: for high write rates, batch updates or use approximate top-k (T-digest, lossy counters) to reduce update cost.Scalability & production considerations:- Compress Trie (radix/compact trie) to reduce nodes.- Persist sentences and frequencies externally; keep node.top_k as references/IDs to reduce duplication.- Use background jobs to recompute heavy node.top_k for expensive updates; use eventual consistency.- Monitor memory and QPS; tune k and caching policies accordingly.Complexity:- Query: O(len(prefix) + k)- Insert: O(L * k) time, O(L) new nodes worst-case- Memory: O(total_nodes * (children pointers + k entries)) — tunable via compression and indirection.Edge cases:- Unicode handling, very long sentences, tie-breaking rules, deletes (need to remove from top_k and maybe prune nodes).