leetcode刷题记录——字典树

208 实现 Tire(前缀树) Medium

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

请你实现 Trie 类:

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
输入
["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

思路

首先需要解释下什么是Tire结构,该结构的每个几点存储了对其子节点的链接,每个节点代表一个字符。根节点不包含字符,除根节点外的每个节点都与一个字符相关联。除此之外,节点通常还包含一个标志,表示该结点是否是某个字符串的结束。

初始化

使用字典去存储,并用一个bool型变量去标记是否是单词的结尾

插入字符

从根节点开始遍历,如果不存在就创建一个新的子节点。遍历完成后标记最后一个节点为字符串的结束。

搜索字符串

这里和前面一样,只不过存在返回一个True不存在返回False,但需要保证最后一个节点被标记为字符串的结束

前缀搜索

能够完整遍历,但不需要检查最后一个节点是否被标记为结束

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Trie:

def __init__(self):
self.children = {}
self.is_end_of_word = False

def insert(self, word: str) -> None:
current = self
for char in word:
if char not in current.children:
current.children[char] = Trie()
current = current.children[char]
current.is_end_of_word = True
def search(self, word: str) -> bool:
current = self
for char in word:
if char not in current.children:
return False
current = current.children[char]
return current.is_end_of_word

def startsWith(self, prefix: str) -> bool:
current = self
for char in prefix:
if char not in current.children:
return False
current = current.children[char]
return True

时间复杂度

时间复杂度均为O(n),空间复杂度为O(m * n),执行时间133ms,消耗内存31.3MB

211 添加与搜索单词 - 数据结构设计 Medium

请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。

实现词典类 WordDictionary

  • WordDictionary() 初始化词典对象
  • void addWord(word)word 添加到数据结构中,之后可以对它进行匹配
  • bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 falseword 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]

解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True

提示:

  • 1 <= word.length <= 25
  • addWord 中的 word 由小写英文字母组成
  • search 中的 word 由 ‘.’ 或小写英文字母组成
  • 最多调用 104addWordsearch

思路

和前一题的思路基本上一致,但由于在 search 中我们需要分辨含有 . 的特殊节点,所以我们采用dfs去做,如果我们能够匹配到就返回,不能满足就下一个,所有都不满足就False

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Node:
def __init__(self):
self.children = [None] * 26
self.isEnd = False
class WordDictionary:

def __init__(self):
self.root = Node()

def addWord(self, word: str) -> None:
node = self.root
for char in word:
index = ord(char) - ord('a')
if not node.children[index]:
node.children[index] = Node()
node = node.children[index]
node.isEnd = True

def search(self, word: str) -> bool:
def dfs(word, index, root):
if not root: return False
if index == len(word): return root.isEnd
if word[index] != '.':
i = ord(word[index]) - ord('a')
return dfs(word, index + 1, root.children[i])
else:
for child in root.children:
if dfs(word, index + 1, child):
return True
return False
return dfs(word, 0, self.root)

时间复杂度

时间复杂度 addWord 为O(n),search 最坏是O(n * 26),最好是O(n),执行时间为1916ms,消耗内存75.7MB

212 单词搜索 II Hard

给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words返回所有二维网格上的单词

单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

示例 1:

212.1

1
2
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]

示例 2:

212.2

1
2
输入:board = [["a","b"],["c","d"]], words = ["abcb"]
输出:[]

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 12
  • board[i][j] 是一个小写英文字母
  • 1 <= words.length <= 3 * 104
  • 1 <= words[i].length <= 10
  • words[i] 由小写英文字母组成
  • words 中的所有字符串互不相同

思路

首先建立一个 TrieNode,再构建一个 Trie 树

再构建一个DFS,遍历当前节点的四个方位,并存储到结果集中,最终转换为列表返回

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class TireNode:
def __init__(self):
self.children = {} # 存储 TireNode 的子节点
self.is_end_of_word = False # 是否是结尾
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
# 创建 Tire
trie = TireNode()
for word in words:
node = trie
for char in word:
if char not in node.children:
node.children[char] = TireNode()
node = node.children[char]
node.is_end_of_word = True

result = set() # 存储搜索到的单词
rows, cols = len(board), len(board[0])

def dfs(row, col, node, path):
# 检查边界条件和当前字符是否已被访问
if not (0 <= row < rows and 0 <= col < cols) or board[row][col] == '#':
return
char = board[row][col]
# 如果当前字符不在子节点中,直接返回
if char not in node.children:
return
# 获取当前字符对应的字 TireNode
child = node.children[char]
# 是结尾就加入结果集
if child.is_end_of_word:
result.add(path + char)
# 如果没有子节点,直接返回
if not child.children:
return
# 标记当前字符为已访问字符
board[row][col] = '#'
# 上下左右依次DFS
dfs(row + 1, col, child, path + char)
dfs(row - 1, col, child, path + char)
dfs(row, col + 1, child, path + char)
dfs(row, col - 1, child, path + char)
# 恢复原字符
board[row][col] = char

for row in range(rows):
for col in range(cols):
dfs(row, col, trie, '')
return list(result)

时间复杂度

时间复杂度O(N * L + M * N * 4^L),空间复杂度O(N * L + L + K),N 是单词数量,L 是单词的平均长度,MN 是字符网格的行数和列数,K 是找到的单词数量。执行时间2852ms,消耗内存18.6MB

总结

这三道题我是推荐以第三题为主,因为他的整个流程更加全面,能够很好的对此类题构建出一个模板来,所以我这里也更推荐采用第三题作为主力研究对象。

我自己大致总结了一下做题的模板和方法:

操作实现方法

初始化

  • 使用一个字典来存储子节点。
  • 用一个布尔变量 is_end_of_word 标记当前节点是否是单词的结束节点。

插入操作

  • 从根节点开始,逐字符检查字典树中是否存在对应的子节点。
  • 如果不存在,则创建一个新节点;遍历完所有字符后,将最后一个节点标记为单词结束。

搜索操作

  • 从根节点逐字符遍历字典树。如果找到对应节点且最终节点被标记为单词结束,则返回 True;否则返回 False

前缀搜索操作

  • 从根节点逐字符遍历字典树,只要能成功遍历完整个前缀即可返回 True,无需检查最终节点是否为单词结束。

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Trie:
def __init__(self):
self.children = {}
self.is_end_of_word = False
def insert(self, word: str) -> None:
current = self
for char in word:
if char not in current.children:
current.children[char] = Trie()
current = current.children[char]
current.is_end_of_word = True

def search(self, word: str) -> bool:
current = self
for char in word:
if char not in current.children:
return False
current = current.children[char]
return current.is_end_of_word

def startsWith(self, prefix: str) -> bool:
current = self
for char in prefix:
if char not in current.children:
return False
current = current.children[char]
return True

整体上来看,都是基于字典树的基本结构,操作上主要是通过遍历和检查节点是否存在来完成插入、查找和前缀匹配的功能。

同时可以看到核心代码几乎完全复用,体现了字典树操作的模块化与通用性。

在这里还需要补充一个点,ord函数。该函数的作用是将单个字符转换为其对应的Unicode整数表示。这个函数的功能可以概括如下:

  1. 字符到整数的转换:ord()函数接受一个单个字符作为输入,并返回该字符的Unicode码点(整数值)
  2. Unicode支持:ord()函数可以处理任何Unicode字符,不仅限于ASCII字符
  3. 单字符输入:ord()函数只接受单个字符作为参数。如果尝试传入多个字符,会导致错误

leetcode刷题记录——字典树
https://www.lx02918.ltd/2024/08/28/leetcode-字典树/
作者
Seth
发布于
2024年8月28日
许可协议