leetcode
leetcode 551 ~ 600
设计搜索自动补全系统

设计搜索自动补全系统

难度:

标签:

题目描述

代码结果

运行时间: 232 ms, 内存: 18.9 MB


// Java Stream solution for LeetCode problem 642: Design Search Autocomplete System
// 
// Problem Statement:
// Implement an autocomplete system using Java Streams for more concise code.
// This solution will still use a Trie data structure for efficiently storing
// and querying prefixes, but we'll leverage Streams for operations like sorting.
 
import java.util.*;
import java.util.stream.*;
 
class AutocompleteSystemStream {
    private class TrieNode {
        Map<Character, TrieNode> children = new HashMap<>();
        Map<String, Integer> counts = new HashMap<>();
        boolean isWord = false;
    }
    private TrieNode root = new TrieNode();
    private String currentSentence = "";
 
    public AutocompleteSystemStream(String[] sentences, int[] times) {
        for (int i = 0; i < sentences.length; i++) {
            add(sentences[i], times[i]);
        }
    }
 
    private void add(String sentence, int time) {
        TrieNode node = root;
        for (char c : sentence.toCharArray()) {
            node = node.children.computeIfAbsent(c, k -> new TrieNode());
            node.counts.put(sentence, node.counts.getOrDefault(sentence, 0) + time);
        }
        node.isWord = true;
    }
 
    public List<String> input(char c) {
        if (c == '#') {
            add(currentSentence, 1);
            currentSentence = "";
            return Collections.emptyList();
        }
        currentSentence += c;
        TrieNode node = root;
        for (char cc : currentSentence.toCharArray()) {
            if (!node.children.containsKey(cc)) {
                return Collections.emptyList();
            }
            node = node.children.get(cc);
        }
        return node.counts.entrySet().stream()
                .sorted((a, b) -> b.getValue().equals(a.getValue()) ? a.getKey().compareTo(b.getKey()) : b.getValue() - a.getValue())
                .limit(3)
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
    }
}

解释

方法:

这个题解使用了一个排序列表来维护输入的句子和它们的频率。每次输入字符时,它都会筛选出以该字符开头的所有句子,并返回最频繁的前三个。当输入'#'时,它会增加当前输入句子的频率,并重新排序列表。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
在`AutocompleteSystem`构造函数中,为什么选择按频率降序和字典序升序对句子进行排序?这种排序方式对性能有什么影响?
按频率降序排列确保了最常用的句子能够被更快地访问和返回,这是因为用户更可能搜索高频句子。字典序升序排列则是为了在频率相同的情况下提供一致且可预测的排序顺序。这种排序方式对性能的主要影响是初始化时的排序操作需要O(n log n)的时间复杂度,但它简化了后续的检索操作,因为可以直接获取列表的前三个元素来快速返回最常见的搜索结果。
🦆
方法`input`中,当输入字符为'#'时,为什么要先调用`pop`再调用`insert`方法?这样处理的目的是什么?
当输入字符为'#'时,意味着用户完成了一次完整的句子输入。先调用`pop`方法是为了获取并删除当前输入句子的旧频率(如果存在),这样可以确保句子列表中不会有重复项。随后调用`insert`方法增加该句子的频率(旧频率基础上加1),然后重新插入到排序列表中。这样处理可以确保句子列表始终保持正确的按频率排序,且句子频率更新是高效且准确的。
🦆
在`pop`方法中,如果没有找到指定的句子,返回0有什么特殊意义?这对整个系统有何影响?
在`pop`方法中,返回0表示指定的句子在列表中不存在,即该句子之前未被输入过。返回0的特殊意义在于它可以被`insert`方法使用来作为该新句子的初始频率(0+1=1)。这样的设计保证了系统能够处理新句子的加入,且在没有先前记录的情况下也能正确地更新其频率。
🦆
在`input`方法中,为什么要在每次输入非'#'字符后更新`self.sentences`和`self.idx`?这样的设计有什么优点或缺点?
在每次输入非'#'字符后,更新`self.sentences`和`self.idx`的目的是为了缩小搜索范围并正确地跟踪用户的当前输入状态。这样设计的优点是可以减少每次查询的计算量,因为只需在缩小的句子集合中进行搜索,这提高了搜索效率。缺点是,每次字符输入都需要进行数组操作(如过滤和索引更新),这可能在输入较长或句子库较大时影响性能。

相关问题

实现 Trie (前缀树)

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false

 

示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True

 

提示:

  • 1 <= word.length, prefix.length <= 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 3 * 104