字典树 前缀树 Trie

介绍 Introduction

本章的重点是要回答以下两个问题:

  • 什么是字典树
  • 如何用代码来表示字典树

如果你对字典树不熟悉,那么本章将对你很有帮助。

1. 什么是字典树 What is Trie

字典树,又称前缀树,是一种特殊的多叉树,一般用来存储字符串。字典树的每个节点都代表一个字符串(或者前缀)。每个节点都有若干个子节点,并且从该节点通往每个子节点的路径代表着不同的字符。一个节点上的字符串是由父节点的字符串加上到达该节点的路径的字符组成的,比如下面的例子:

a
b
s
m
a
e
o
d
a
b
s
am
ba
be
so
bad

在例子中,每个节点中的值都是一个字符串。但是,由于没有路径到达根节点,所以根节点值为空。比如说,从根节点出发,选择一条路径b,然后选择一条路径a,然后选择一条路径d,我们可以到达节点bad。实际上,节点的值就是到达该节点的所有路径的值按顺序组合。

字典树有个很重要的属性,就是一个节点的所有子节点的值,都拥有相同的前缀。比如上例中的b节点下的两个子节点,都拥有相同的前缀b。反之,拥有相同前缀的节点必定是以该前缀所在节点为根。

字典树应用广泛,比如自动补全功能,拼写检查等等。在接下来的部分中,我们会介绍一些应用实例。

2. 如何表示一个字典树 How to represent a Trie

前面一部分中,我们介绍了字典树的基本概念,接下来我们会讨论下如何用编程语言将字典树这个数据结构表示出来。

在阅读下面的部分之前,请先复习一下多叉树的数据结构

字典树的特殊之处在于字符和其之后的子节点之间的关系,表示方式多种多样,下面我们提供两种。

方法一:数组

比如,如果我们保存的字符串中字符的范围是az的26个小写字符,那么我们可以在每一个节点中声明一个大小为26的数组,每个元素用来存储该字符下的子节点。对于指定的字符c,我们可以使用c - a作为下标在数组中找到相应的子节点。

// change this value to adapt to different cases
#define N 26

struct TrieNode {
    TrieNode* children[N];
    
    // you might need some extra values according to different cases
};

/** Usage:
 *  Initialization: TrieNode root = new TrieNode();
 *  Return a specific child node with char c: (root->children)[c - 'a']
 */

这种方法的好处显而易见,节点的增删改查都十分的简单,基本达到了O(1)。但是缺点也是显而易见的,如果用不到那么多节点,那么将会造成空间浪费。

方法二:哈希图

我们可以为每一个节点声明一个哈希图,值是字符,值是该字符下的子节点。

struct TrieNode {
    unordered_map<char, TrieNode*> children;
    
    // you might need some extra values according to different cases
};

/** Usage:
 *  Initialization: TrieNode root = new TrieNode();
 *  Return a specific child node with char c: (root->children)[c]
 */

这种方法,比上一种方法更为简单直接,甚至连下标转换都不需要做。但是,相对于数组来说,哈希图可能会慢上一丢丢。但是,由于其只存储用得到的节点,所以相对于数据,我们可以节省大量的空间。这种时空的交换,我觉得哈希图是略胜一筹的。

备注

上面我们说了如何来表示字典树。然而,在实际应用中,我们可能会需要除字符串以外的其他值。比如说,如果我们用字典树来表示字符串,但是显然并不是所有的字符串都是有意义的。那么如果我只想要字典树中的单词,我可能会在字典树的节点中加入一个bool型变量来表示这个节点的值是不是一个合法的单词。

基本操作 Basic Operations

1. 插入

我们在另一个模块已经讨论了如何向一个二叉搜索树中插入节点(二叉搜索树)。

Question:
你还记得怎么将一个新的节点插入二叉搜索树吗

当我们要将一个值插入到二叉搜索树时,对于每一个经过的节点,都要判断该节点的值要插入的值的关系,以确定该向哪个子节点走。同理,当我们要将一个值插入到字典树中时,一样需要根据要插入的值判断要插入的路径。

比如说,我们要插入一个字符串S到字典树中,我们将从根节点开始。先取S的首字符S[0],然后确定该走哪个子节点。确定以后,在第二个节点时,我们要取S的第二个字符S[1]来确定。以此类推,最终我们会遍历S的所有字符,而最后一个节点将正好表示了字符串S。如图:

a
b
s
m
a
e
o
d
bed
a
b
s
am
ba
be
so
bad
bed

如果我们要插入字符串bed,那么首先会获取bed的首字符b,于是,我们从根节点来到了节点b。在节点b处,我们选择bed的第二个字符e,于是我们来到了节点be。最后,我们选择bed的第三个字符d,而节点be没有子节点bed,于是我们创建了一个新的节点bed,及路径d。插入结束。

上述的伪代码可以如下表示:

1. Initialize: cur = root
2. for each char c in target string S:
3.      if cur does not have a child c:
4.          cur.children[c] = new Trie node
5.      cur = cur.children[c]
6. cur is the node which represents the string S

通常情况下,字典树需要你自行创建,而这个创建的过程其实就是调用若干次插入函数。不过要注意的是,在使用插入函数前,应初始化根节点。

2. 查找

查找分两种,查找前缀与单词。

查找前缀

如我们前面提到的那样,一个节点的所有子节点的值都拥有相同的前缀,且该前缀就是当前节点的值。

相似的,我们根据给定的前缀向下遍历字典树。一旦找不到我们想要的子节点,返回false。否则,继续向下寻找。伪代码如下:

1. Initialize: cur = root
2. for each char c in target string S:
3.      if cur does not have a child c:
4.          search fails
5.      cur = cur.children[c]
6. search successes
查找单词

要查询单词,我们可以按照与查找前缀同样的方法进行查询。

  1. 如果返回false,意味着没有单词以当前单词为前缀,所以该单词肯定不存在
  2. 如果成功,我们需要确认待查询的单词在字典树种是一个词的前缀还是确实就是一个单词。为了解决这个问题,我们需要略略地修改一下节点的数据结构。(添加一个bool型的变量)

原文

LeetCode Trie模块,网址为https://leetcode.com/explore/learn/card/trie/150/introduction-to-trie/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值