题目

大家好!今天给大家带来的是一道前缀树的题目。
原帖地址:http://leanote.com/blog/post/60768fe7ab64414216007075

题目

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

请你实现 Trie 类:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/implement-trie-prefix-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路分析

这个题目需要我们实现一个数据结构,能够快速查找放过的单词和他们的前缀
如果单纯的只是需要查找单词,我们可以使用Set或者是Map来实现。但是这里还需要查找前缀,如果我们还按照集合来实现,查找前缀就会特别麻烦,假设输入的字符串长度为M,共有N个,查找前缀字符串至少是O(M)的级别,对每个字符串遍历又是O(N)的级别,加起来就是O(MN)的级别,过于冗长。那么我们用了什么结构来实现呢?
题目里也给了我们提示,用树来实现,我们经常使用的是二叉树,在这里我们不使用二叉树,而是使用26叉树。因为有26个字母,我们对一个字符串进行拆分,对每一个字母建一棵树,这样在插入前缀相同字符串的时候,就不会重复占用空间,而且还能重复使用前缀,查找前缀也变成O(N)级别的操作了,所以这个题目的关键点就是如何去构建和维护我们的前缀树(也称字典树)
当然,还有一个点我们需要考虑的。就是如何表示一个字符串的结尾,假设我们的这棵树是这样的

'a'->'b'->'c'->空指针

那么在我们搜索前缀字符串"ab"的时候,它会返回true,可是我们搜索search的时候,它返回也为true,因为这个结构不知道我们放进去的是"abc"还是"ab"和"abc",这样就会出现问题。那么如何解决这个问题呢,也非常简单,对每一个树都加入一个标识符

'a'(非结尾)->'b'(非结尾)->'c'(结尾)->空指针

这样就一目了然了,如果我们还需向字典树中加入"ab"字符串,也不会引起冲突。

'a'(非结尾)->'b'(结尾)->'c'(结尾)->空指针

这样这个树在搜索"ab"和"abc"的时候都会返回true,而在其他答案的时候都会返回false。
细节都讲完了,接下来看看代码
C++代码:

class Trie {
private:
    vector<Trie*> children;
    bool isEnd;

    Trie* searchPrefix(string prefix) {
        Trie* node = this;
        for (char ch : prefix) {
            ch -= 'a';
            if (node->children[ch] == nullptr) {
                return nullptr;
            }
            node = node->children[ch];
        }
        return node;
    }

public:
    Trie() : children(26), isEnd(false) {}

    void insert(string word) {
        Trie* node = this;
        for (char ch : word) {
            ch -= 'a';
            if (node->children[ch] == nullptr) {
                node->children[ch] = new Trie();
            }
            node = node->children[ch];
        }
        node->isEnd = true;
    }

    bool search(string word) {
        Trie* node = this->searchPrefix(word);
        return node != nullptr && node->isEnd;
    }

    bool startsWith(string prefix) {
        return this->searchPrefix(prefix) != nullptr;
    }
};

Rust代码

#[derive(Default)]
pub struct Trie {
    root: Node,
}

#[derive(Default)]
struct Node {
    children: [Option<Box<Node>>; 26],
    is_word: bool,
}

impl Trie {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn insert(&mut self, word: String) {
        let mut node = &mut self.root;
        for c in word.as_bytes() {
            let idx = (c - b'a') as usize;
            let next = &mut node.children[idx];
            node = next.get_or_insert_with(Box::<Node>::default);
        }
        node.is_word = true;
    }

    pub fn search(&self, word: String) -> bool {
        self.get_node(&word).map_or(false, |w| w.is_word)
    }

    pub fn starts_with(&self, prefix: String) -> bool {
        self.get_node(&prefix).is_some()
    }
    fn get_node(&self, s: &str) -> Option<&Node> {
        let mut node = &self.root;
        for c in s.as_bytes() {
            let idx = (c - b'a') as usize;
            match &node.children[idx] {
                Some(next) => node = next.as_ref(),
                None => return None,
            }
        }
        Some(node)
    }
}

Rust代码就比较复杂了。因为还是我们上一期讲到的所有权的问题,还有一个内存安全的问题。
如果我们直接写child=[Trie; 26]这个struct就会报错,错误是这个结构体有无限大小,所以我们必须给它外面包上一层Box<>,因为Box是一个智能指针,有着确定的大小。
而且为了确保空指针不会出现,我们必须在其外再加上一层Option,这是一个枚举类型,包含了Some(T)和None,主要是为了替代空指针而被创造的,它是一个内存安全的结构,常常被用于模拟空指针。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值