You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

寻求支持预处理的子字符串匹配单词检索最优算法

Great question! Your current linear scan works fine for small datasets, but as your word list grows, we can definitely optimize this with preprocessing. Let's break down the best options based on your needs:

Optimizing Substring Search for Word Lists

1. Inverted Index with N-grams (Best for Fast Inserts)

If frequent word additions are a priority, an n-gram-based inverted index is a strong choice. It balances speed for both inserts and queries without overly complex logic.

How it works:

  • Preprocessing: For each word, generate all contiguous character sequences (n-grams) of a fixed length (usually 2 or 3). For example, "Python" with n=2 produces n-grams like "Py", "yt", "th", "ho", "on".
  • Storage: Use a dictionary where each key is an n-gram, and the value is a set of words that contain that n-gram.
  • Query: Split your target substring into its own n-grams, find the intersection of all word sets linked to those n-grams, then verify exact substring matches to filter out false positives.

Example Implementation

from collections import defaultdict

class NGramSearch:
    def __init__(self, n=2):
        self.n = n
        self.index = defaultdict(set)
        self._all_words_cache = set()
    
    def add_word(self, word):
        if word in self._all_words_cache:
            return
        # Generate all unique n-grams for the word
        ngrams = {word[i:i+self.n] for i in range(len(word) - self.n + 1)}
        for gram in ngrams:
            self.index[gram].add(word)
        self._all_words_cache.add(word)
    
    def search(self, substring):
        sub_len = len(substring)
        if sub_len < self.n:
            # Fall back to linear scan for short substrings
            return [word for word in self._all_words_cache if substring in word]
        
        # Get n-grams from the query substring
        query_grams = {substring[i:i+self.n] for i in range(sub_len - self.n + 1)}
        if not query_grams:
            return []
        
        # Find intersection of candidate word sets
        candidates = None
        for gram in query_grams:
            if gram not in self.index:
                return []
            if candidates is None:
                candidates = self.index[gram].copy()
            else:
                candidates.intersection_update(self.index[gram])
        
        # Verify exact substring match to eliminate false positives
        return [word for word in candidates if substring in word]

# Usage
ngram_search = NGramSearch(n=2)
words = ["abc", "bcd", "thon", "Python"]
for word in words:
    ngram_search.add_word(word)

print(ngram_search.search("on"))  # Output: ['thon', 'Python']

Pros & Cons

  • Pros: Ultra-fast inserts (O(k) per word, where k is word length), simple to maintain, and query performance scales well with large datasets.
  • Cons: Uses more memory than linear scan; requires choosing an appropriate n-gram length (2-3 is standard for most use cases).

2. Aho-Corasick Automaton (Best for Fast Queries)

If query speed is your top concern (and you can tolerate more work upfront), the Aho-Corasick algorithm is unbeatable. It builds a trie-like automaton that lets you find all matching words in linear time relative to the substring length.

How it works:

  • Preprocessing: Construct an automaton where each node represents a character. Nodes have failure links (similar to the KMP algorithm) to handle partial matches. Each word is added to the automaton, and we track which words end at each node.
  • Query: Traverse the automaton with your substring. Any nodes you pass through (or their failure links) that are marked with words give you your matches.

Example Implementation

from collections import deque

class AhoCorasickNode:
    def __init__(self):
        self.children = {}
        self.failure = None
        self.matching_words = set()

class AhoCorasickSearch:
    def __init__(self):
        self.root = AhoCorasickNode()
    
    def add_word(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = AhoCorasickNode()
            node = node.children[char]
        node.matching_words.add(word)
    
    def build_failure_links(self):
        queue = deque()
        # Initialize failure links for root's direct children
        for child in self.root.children.values():
            child.failure = self.root
            queue.append(child)
        
        # Process remaining nodes to build failure links
        while queue:
            current_node = queue.popleft()
            for char, child in current_node.children.items():
                # Traverse failure links to find the correct fallback node
                failure_node = current_node.failure
                while failure_node is not None and char not in failure_node.children:
                    failure_node = failure_node.failure
                
                child.failure = failure_node.children[char] if failure_node else self.root
                # Merge matches from the failure node
                child.matching_words.update(child.failure.matching_words)
                queue.append(child)
    
    def search(self, substring):
        node = self.root
        all_matches = set()
        
        for char in substring:
            # Follow failure links until we find a matching child or reach root
            while node is not None and char not in node.children:
                node = node.failure
            if node is None:
                node = self.root
                continue
            
            node = node.children[char]
            all_matches.update(node.matching_words)
        
        return list(all_matches)

# Usage
ac_search = AhoCorasickSearch()
words = ["abc", "bcd", "thon", "Python"]
for word in words:
    ac_search.add_word(word)
ac_search.build_failure_links()

print(ac_search.search("on"))  # Output: ['thon', 'Python']

Pros & Cons

  • Pros: Blazing-fast queries (O(m + z), where m is substring length and z is number of matches); efficient for multiple consecutive queries.
  • Cons: Adding new words requires rebuilding failure links (incremental updates are possible but complex); higher upfront preprocessing time.

3. Rolling Hash (Rabin-Karp) Approach

This method uses hashing to compare substrings efficiently, striking a balance between insert and query performance without storing large amounts of n-gram data.

How it works:

  • Preprocessing: Compute rolling hashes for all prefixes of each word. This lets us calculate the hash of any substring in constant time.
  • Query: Compute the hash of your target substring, then check if this hash exists in any word's substring hashes. We double-check matches to avoid hash collisions.

Example Implementation

class RollingHashSearch:
    def __init__(self, base=911382629, mod=10**18 + 3):
        self.base = base
        self.mod = mod
        self.word_prefix_hashes = {}
        self.powers = [1]  # Precomputed powers of base for hash calculations
    
    def _compute_prefix_hashes(self, word):
        n = len(word)
        prefix_hashes = [0] * (n + 1)
        for i in range(n):
            prefix_hashes[i+1] = (prefix_hashes[i] * self.base + ord(word[i])) % self.mod
        
        # Extend precomputed powers if needed
        while len(self.powers) <= n:
            self.powers.append((self.powers[-1] * self.base) % self.mod)
        
        return prefix_hashes
    
    def add_word(self, word):
        if word not in self.word_prefix_hashes:
            self.word_prefix_hashes[word] = self._compute_prefix_hashes(word)
    
    def _get_substring_hash(self, prefix_hashes, start_idx, sub_length):
        end_idx = start_idx + sub_length
        return (prefix_hashes[end_idx] - prefix_hashes[start_idx] * self.powers[sub_length]) % self.mod
    
    def search(self, substring):
        sub_len = len(substring)
        if sub_len == 0:
            return list(self.word_prefix_hashes.keys())
        
        # Compute hash of the query substring
        sub_hash = 0
        for char in substring:
            sub_hash = (sub_hash * self.base + ord(char)) % self.mod
        
        matches = []
        for word, prefix_hashes in self.word_prefix_hashes.items():
            word_len = len(word)
            if word_len < sub_len:
                continue
            
            # Check all possible substrings of the word for matching hash
            for i in range(word_len - sub_len + 1):
                current_hash = self._get_substring_hash(prefix_hashes, i, sub_len)
                if current_hash == sub_hash:
                    # Double-check to avoid hash collisions
                    if substring == word[i:i+sub_len]:
                        matches.append(word)
                        break
        
        return matches

# Usage
rh_search = RollingHashSearch()
words = ["abc", "bcd", "thon", "Python"]
for word in words:
    rh_search.add_word(word)

print(rh_search.search("on"))  # Output: ['thon', 'Python']

Pros & Cons

  • Pros: Balanced insert and query performance; avoids storing large n-gram datasets.
  • Cons: Small risk of hash collisions (mitigated by manual verification); precomputing base powers adds minor overhead.

Which One Should You Pick?

  • Fast inserts first: Go with the n-gram inverted index—it's simple, maintainable, and handles dynamic word lists seamlessly.
  • Fast queries first: Use the Aho-Corasick automaton for lightning-fast lookups, just note that adding new words requires rebuilding failure links.
  • Balanced needs: The rolling hash approach strikes a middle ground between insert and query speed without excessive complexity.

内容的提问来源于stack exchange,提问作者Brambor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:44:09