Rust中的Trie数据结构(附实例)

847 阅读5分钟

让我们回到数据结构中去看看。理论够了,让我们自己来写点代码吧 :D

目录

  1. 什么是三联体?
  2. 实施的初步想法
  3. 实际的实现

要修改Trie数据结构中的一般操作,请阅读这篇文章。之后,你可以继续阅读这篇文章,我们将在Rust编程语言中实现Trie数据结构。

什么是Trie?

Trie,也叫数字树或前缀树,是一种数据结构,类似于我们在另一篇文章中已经介绍过的二进制树。这个结构的突出之处在于,它的设计是为了让人们更容易找到一个集合中的特定键。这些集合通常是字符串,树上的每个节点都是这些字符串中的一个字符,当你从深度第一的角度移动时,它们被连接起来形成单词。

这里有一个例子:

Trie-Example

在Crates.io上快速搜索,发现了一大堆trie的实现方式。但为了实践,我们将自己实现它。

Trie-in-Crates-IO

实施的初步想法

至少,我们的 trie 应该支持搜索、插入和删除字符串,并且最好是O(m) 的复杂度,其中 'm' 是我们使用的字符串的长度。

所以,让我们试着弄清楚这个问题。trie是一棵树。节点,以及它们之间的链接。所以我们应该从这里开始。首先,我们将有一个节点结构,它将持有一个字符,一个布尔值来识别它是否是字符串中的最后一个字符,以及一个指向它的子节点的引用列表。然后是我们适当的Trie结构,它将保存根节点,因为它是我们所有搜索和插入的起点。

考虑到这一点,这将是我们的基本程序结构:

pub struct TrieNode {
    value: char,
    is_final: bool,
    child_nodes: Vec<Box<TrieNode>>,
}

impl TrieNode {
    // Create new node
    pub fn create(c: char, is_final: bool) -> TrieNode {}
    // Check if a node has that value
    pub fn check_value(self, c: char) -> bool {
        return true;
    }
}

struct TrieStruct {
    root_node: TrieNode,
}

impl TrieStruct {
    // Insert a string
    pub fn insert(string_val: String) {}
    // Find a string
    pub fn find(string_val: String) -> bool {
        return true;
    }
}

fn main() {
    // Create Trie
    // Insert Stuff
    // Find Stuff
}

让我们使用这个基本结构,开始逐一实现各项功能。首先让我们检查一下我们的基本结构。我们有一些可以做的改进:

use std::collections::HashMap;

pub struct TrieNode {
    value: Option<char>,
    is_final: bool,
    child_nodes: HashMap<char, TrieNode>,
}

我们要把我们的Value替换成一个选项,以便有机会出现空值(这表示我们的根节点。)然后我们把我们的子节点从一个Vec变成一个HashMap。为什么?好吧,用一个键来访问一个地图会更快,而不是在一个向量中一个一个字符地访问,那样会有问题。

接下来,让我们为我们的TrieNodes实现一些基本功能

我想它们是不言自明的,所以:

impl TrieNode {
    // Create new node
    pub fn new(c: char, is_final: bool) -> TrieNode {
        TrieNode {
            value: Option::Some(c),
            is_final: is_final,
            child_nodes: HashMap::new(),
        }
    }

    pub fn new_root() -> TrieNode {
        TrieNode {
            value: Option::None,
            is_final: false,
            child_nodes: HashMap::new(),
        }
    }

    // Check if a node has that value
    pub fn check_value(self, c: char) -> bool {
        self.value == Some(c)
    }

    pub fn insert_value(&mut self, c: char, is_final: bool) {
        self.child_nodes.insert(c, TrieNode::new(c, is_final));
    }
}

然后我们有我们的Trie结构本身,以及它的实现(这不是最好的,这只是我的方法,你可以把它作为 "家庭作业 "寻找改进的方法:D)

#[derive(Debug)]
struct TrieStruct {
    root_node: TrieNode,
}

impl TrieStruct {
    // Create a TrieStruct
    pub fn create() -> TrieStruct {
        TrieStruct {
            root_node: TrieNode::new_root(),
        }
    }

    // Insert a string
    pub fn insert(&mut self, string_val: String) {
        let mut current_node = &mut self.root_node;
        let char_list: Vec<char> = string_val.chars().collect();
        let mut last_match = 0;

        for letter_counter in 0..char_list.len() {
            if current_node
                .child_nodes
                .contains_key(&char_list[letter_counter])
            {
                current_node = current_node
                    .child_nodes
                    .get_mut(&char_list[letter_counter])
                    .unwrap();
            } else {
                last_match = letter_counter;
                break;
            }
            last_match = letter_counter + 1;
        }

        if last_match == char_list.len() {
            current_node.is_final = true;
        } else {
            for new_counter in last_match..char_list.len() {
                println!(
                    "Inserting {} into {}",
                    char_list[new_counter],
                    current_node.value.unwrap_or_default()
                );
                current_node.insert_value(char_list[new_counter], false);
                current_node = current_node
                    .child_nodes
                    .get_mut(&char_list[new_counter])
                    .unwrap();
            }
            current_node.is_final = true;
        }
    }

    // Find a string
    pub fn find(&mut self, string_val: String) -> bool {
        let mut current_node = &mut self.root_node;
        let char_list: Vec<char> = string_val.chars().collect();

        for counter in 0..char_list.len() {
            if !current_node.child_nodes.contains_key(&char_list[counter]) {
                return false;
            } else {
                current_node = current_node
                    .child_nodes
                    .get_mut(&char_list[counter])
                    .unwrap();
            }
        }
        return true;
    }
}

插入的工作是这样的。我们抓取我们想要插入的字符串,并将其分割成一个字符向量。之后,我们穿过树,检查是否有可能被保存的现有字符。一旦我们发现一个节点没有我们列表中的下一个字符,我们就把这个索引保存为last_match。在这之后,我们只需从最后一个匹配开始迭代,直到列表结束,并将我们的字符插入适当的子节点中。

最后但并非最不重要的是,我们的主函数,以测试我们的结构

fn main() {
    // Create Trie
    let mut trie_test = TrieStruct::create();

    // Insert Stuff
    trie_test.insert("Test".to_string());
    trie_test.insert("Tea".to_string());
    trie_test.insert("Background".to_string());
    trie_test.insert("Back".to_string());
    trie_test.insert("Brown".to_string());

    // Find Stuff
    println!(
        "Is Testing in the trie? {}",
        trie_test.find("Testing".to_string())
    );
    println!(
        "Is Brown in the trie? {}",
        trie_test.find("Brown".to_string())
    );
}

运行的结果,如下。
Trie-Result

这个演示有点擦伤,但它显示了正在发生的事情。每个字母都被插入到相应的节点中。

免责声明:你可能注意到我们缺少一个删除函数。的确,我们是这样。Rust的工作方式使得它很难建立在某些类型的数据结构上进行迭代的函数,同时对节点本身进行突变。我鼓励你使用这段代码并尝试添加一个删除函数。你将与所有权模型和借贷检查器斗争数小时(在我的例子中是4天)。

像这样的某些小众数据结构其实不应该被使用,或者重新制作(别人已经做了,就用那个。还记得我在另一篇文章中所说的吗?不要重新发明轮子!)

今天就到这里吧,朋友们。对不起,这篇文章花了这么长时间来写。我太固执了,而且太晚才寻求帮助。