三、字典树
1. 实现 Trie (前缀树)
要求:实现前缀树数据结构的相关类 Trie
类。
Trie
类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回True
(即,在检索之前已经插入);否则,返回False
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回True
;否则,返回False
。
说明:
1 <= word.length, prefix.length <= 2000
word
和prefix
仅由小写英文字母组成insert
、search
和startsWith
调用次数 总计 不超过3 * 10e4
次
# 定义节点类
class Node:
# 定义节点属性(初始化无参数实例对象)
def __init__(self):
# 节点实质上是一个哈希表
self.ch = dict()
# 是否为结束节点
self.end = False
# 定义多叉树
class Trie:
# 定义多叉树属性
def __init__(self):
# 定义根节点
self.root = Node()
# 定义插入字符串的方法
def insert(self, word: str) -> None:
# 当前节点指向的字典
cur = self.root
# 将字母作为键,建立一个空字典作为它的值,即下级节点
for ch in word:
# 新键就存入,旧键就跳过
if ch not in cur.ch:
cur.ch[ch] = Node()
# 当前节点转向下一节点
cur = cur.ch[ch]
# 传入完毕,最后一个节点的属性修改为结束节点
cur.end = True
# 定义查找字符串的方法
def search(self, word: str) -> bool:
cur = self.root
for ch in word:
# 失配就跳出
if ch not in cur.ch:
return False
cur = cur.ch[ch]
# 顺利结束遍历,则判断下一节点是否 存在且为结束节点
return cur is not None and cur.end
# 定义查找前缀的方法
def startsWith(self, prefix: str) -> bool:
cur = self.root
for ch in prefix:
if ch not in cur.ch:
return False
cur = cur.ch[ch]
# 下一节点是否 存在
return cur is not None
为 object 类的 dict 对象赋予更多的属性,并构建多级哈希表存储数据,合并相同数据单元,用尽量少的空间换取更多时间。
class Trie:
def __init__(self):
self.node = dict()
def insert(self, word: str) -> None:
now = self.node
for i in word:
if i not in now:
now[i] = dict()
now = now[i]
now['end'] = 1
def search(self, word: str) -> bool:
now = self.node
for i in word:
if i not in now:
return False
now = now[i]
return True if 'end' in now else False
def startsWith(self, prefix: str) -> bool:
now = self.node
for i in prefix:
if i not in now:
return False
now = now[i]
return True
也可以选择不用 Node 类给 dict 附加属性,在字典中添加特征键 'end' 实现。
2. 键值映射
要求:实现一个 MapSum 类,支持两个方法,insert
和 sum
:
MapSum()
初始化 MapSum 对象。void insert(String key, int val)
插入key-val
键值对,字符串表示键key
,整数表示值val
。如果键key
已经存在,那么原来的键值对将被替代成新的键值对。int sum(string prefix)
返回所有以该前缀prefix
开头的键key
的值的总和。
说明:
1 <= key.length, prefix.length <= 50
key
和prefix
仅由小写英文字母组成1 <= val <= 1000
- 最多调用
50
次insert
和sum
class MapSum:
def __init__(self):
self.node = dict()
def insert(self, key: str, val: int) -> None:
# 生成字典树:将单词的val存放于每个字母下的子节点
now = self.node
for i in key:
if i not in now:
now[i] = dict()
now = now[i]
now['val'] = val
# 若节点已开辟,则将val加入节点
else:
now = now[i]
now['val'] += val
'''检查树中是否存在重复单词'''
# 不存在:用'end'键标记结束节点,将对应val存入该键
if 'end' not in now:
now['end'] = val
# 存在
else:
# 交换val
val, now['end'] = now['end'], val
# 减去路径节点的原val,实现替换
now = self.node
for i in key:
now = now[i]
now['val'] -= val
def sum(self, prefix: str) -> int:
now = self.node
# 失配则非前缀
for i in prefix:
if i not in now:
return 0
now = now[i]
# 匹配成功则返回val的和
return now['val']
解法关键在于将结束节点作为替换键值对的标志和实现方式。
3. 驼峰式匹配
描述:给定待查询列表 queries
,和模式串 pattern
。如果我们可以将小写字母(0 个或多个)插入模式串 pattern
中间(任意位置)得到待查询项 queries[i]
,那么待查询项与给定模式串匹配。如果匹配,则对应答案为 True
,否则为 False
。
要求:将匹配结果存入由布尔值组成的答案列表中,并返回。
说明:
1 <= pattern.length, queries.length <= 100
1 <= queries[i].length <= 100
queries[i]
和pattern
由英文字母组成
class Solution:
def camelMatch(self, queries: List[str], pattern: str) -> List[bool]:
# 正则化:取模式串大写字母
s = re.sub('[^A-Z]','',pattern)
for i in range(len(queries)):
# 取列表单词
ss = queries[i]
# 先决条件:两串大写字母排列相同
if s == re.sub('[^A-Z]','',ss):
# 初始化:单词指针,过界跳出标志
j, f = 0, 1
# 遍历模式串字母
for k in pattern:
# 失配
while k != ss[j]:
# 切换到单词的下一个字母
j+=1
# 单词索引过界
if j == len(ss):
# 原地修改为判定结果
queries[i] = False
# 过界跳出标志
f=0
break
j+=1 # leedcode的bug判定机制:即使还未运行某行代码,也不能索引过界
if not(f):break
if f:queries[i] = True
else:
queries[i] = False
return queries
核心规则是:两串大写字母排列相同,patter 的字母依次在出现在单词中出现。
class Solution:
def camelMatch(self, queries: List[str], pattern: str) -> List[bool]:
def helper(a, b):
# 两串指针初始化
i, j = 0, 0
while i < len(a) and j < len(b):
# 匹配成功,移动模式串指针
if a[i] == b[j]:
i += 1
# 失配,确定单词中的大写字母是否先于模式串出现,或者是否是大写字母失配
elif b[j].isupper():
return False
# 单词指针移动
j += 1
# 当模式串匹配全部成功,单词的未配段没有大写字母,返回True,反之为False
return i == len(a) and all(x.islower() for x in b[j:])
# 利用函数和生成式实现列表类型结果的输出
return [helper(pattern, x) for x in queries]
利用双指针完成的匹配,其中大写字母的排列相等的判定可转化为:pattern 的首字母在单词中出现前和尾字母在单词中出现后,单词中是否有大写字母,中段的判定则与小写字母一并进行。
总而言之,大写字母不能失配,小写字母按序配完即可。
4. 添加与搜索单词 - 数据结构设计
要求:设计一个数据结构,支持「添加新单词」和「查找字符串是否与任何先前添加的字符串匹配」。
实现词典类 WordDictionary:
WordDictionary()
初始化词典对象。void addWord(word)
将word
添加到数据结构中,之后可以对它进行匹配bool search(word)
如果数据结构中存在字符串与word
匹配,则返回True
;否则,返回False
。word
中可能包含一些.
,每个.
都可以表示任何一个字母。
说明:
1 <= word.length <= 25
addWord
中的word
由小写英文字母组成search
中的word
由 '.' 或小写英文字母组成- 最多调用
10e4
次addWord
和search
class WordDictionary:
# 初始化:词长属性,检索记录
def __init__(self):
self.s, self.c = {}, {} # 集合:防止重复插入
# 加词方法:将相同长度的单词放入同一节点
def addWord(self, word):
# 键为词长
n = len(word)
# 值为单词集合
if n not in self.s: self.s[n] = set()
self.s[n].add(word)
# 清空记录
self.c.clear()
def search(self, word):
# 当前检索单词无记录:添加记录,默认为False
if word not in self.c:
self.c[word] = False
# 以词长为关键字查找
n = len(word)
if n in self.s:
# 遍历当前词长的子节点,即单词集合,逐个取出单词
for s in self.s[n]:
# 匹配标志
ok = True
# 失配:出现'.'则跳过
for i in range(n):
if word[i] != '.' and s[i] != word[i]: ok = False; break
# 可配:修改记录
if ok:
self.c[word] = True
break
# 返回结果
'''
若为连续检索操作,可直接调用记录返回
若全部失配,返回默认False
有可配词,返回True
'''
return self.c[word]
由于搜索词中存在缺失字母,所以以字母为结点建立多叉树已经不再合适,因为多个节点的缺失意味着多枝检索,即多层遍历,字典树的单一搜索路径优势,即单次遍历优势,已经失效了。
此解法中,将树的深度设计为两层,在节点中直接存储单词,将词长作为哈希值,用集合结构排重,用词典的检索记录减少重复检索的运算量。
缩进错误详见:http://t.csdnimg.cn/ZcQZ5
5. 单词替换
描述:给定一个由许多词根组成的字典列表 dictionary
,以及一个句子字符串 sentence
。
要求:将句子中有词根的单词用词根替换掉。如果单词有很多词根,则用最短的词根替换掉他。最后输出替换之后的句子。
说明:
1 <= dictionary.length <= 1000
1 <= dictionary[i].length <= 100
dictionary[i]
仅由小写字母组成。1 <= sentence.length <= 10e6
sentence
仅由小写字母和空格组成。sentence
中单词的总量在范围[1, 1000]
内。sentence
中每个单词的长度在范围[1, 1000]
内。sentence
中单词之间由一个空格隔开。sentence
没有前导或尾随空格。