《自然语言处理入门》-2.4字典树 学习笔记
字典树的定义
参考博客:https://blog.youkuaiyun.com/weixin_39778570/article/details/81990417
这里还有一个映射的概念,图中能表示一个个字符串的集合,除了集合的功能之外,字典树也可以实现映射,只需将相应的值悬挂在键的终点节点上即可,后续的代码实现也是关于字典树的映射的。
字典树节点和树的Python代码实现
class Node(object):
def __init__(self, value):
# 该节点的子节点,是一个字典,键是单字char,而值是一个Node
self._children = {}
# 该节点对应的词,实现映射,如果不是一个词语的的结尾,那么value值始终为None,若是用字典树实现映射,那么词语结尾的value值就是映射值
self._value = value
def _add_child(self, char, value, overwrite=False):
# char是一个字符,首先检查该字符对应的节点是否存在
child = self._children.get(char)
print("Node方法中的char:", char)
print("Node方法中的value:", value)
print("Node方法中的child:", child)
print("------------------")
# 如果该节点不存在,那么插入该节点
if child is None:
# 这里的value仍然是None,但是用Node封装了一下作为子节点的值了,因此下次取值的时候就不会为None了
child = Node(value)
self._children[char] = child
print("生成的children:", self._children)
print("children中单字char对应的值(节点)中的value值:", self._children[char]._value)
print("=====================")
elif overwrite:
child._value = value
return child
class Trie(Node):
"""
Trie类继承了Node类,因此可以使用父类的方法
重载python的方法,使得可以像dict一样操作字典树
"""
def __init__(self):
super().__init__(Node)
def __contains__(self, key):
return self[key] is not None
def __getitem__(self, key):
state = self
for char in key:
state = state._children.get(char)
if state is None:
return None
return state._value
def __setitem__(self, key, value):
state = self
for i, char in enumerate(key):
# 如果没有到对应中文的边的结尾,那么_add_child方法中对应的value都是None值
if i < len(key) - 1:
state = state._add_child(char, None, False)
else:
state = state._add_child(char, value, True)
字典树的增加
测试代码:
if __name__ == '__main__':
trie = Trie()
# 增 对应于setitem方法
# 这里的中文中的每个字符都是节点中_add_child方法中的char,
trie['自然'] = 'nature'
trie['自然人'] = 'human'
测试结果如下:
Node方法中的char: 自
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'自': <__main__.Node object at 0x000002ED749F3668>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 然
Node方法中的value: nature
Node方法中的child: None
------------------
生成的children: {'然': <__main__.Node object at 0x000002ED749F36D8>}
children中单字char对应的值(节点)中的value值: nature
=====================
Node方法中的char: 自
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x000002ED749F3668>
------------------
Node方法中的char: 然
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x000002ED749F36D8>
------------------
Node方法中的char: 人
Node方法中的value: human
Node方法中的child: None
------------------
生成的children: {'人': <__main__.Node object at 0x000002ED749F3710>}
children中单字char对应的值(节点)中的value值: human
=====================
首先观察第一行代码:
trie['自然'] = 'nature'
代码使用了类似Python中dict的方式来增加节点,这行代码对应的是Trie类中的__setitem__方法
for i, char in enumerate(key): # 如果没有到对应中文的边的结尾,那么_add_child方法中对应的value都是None值 if i < len(key) - 1: state = state._add_child(char, None, False) else: state = state._add_child(char, value, True)
enumerate遍历了“自然”这个字符串的每个单字,在未到达字符串的结尾时,映射值为None,而当
i = len(key) - 1:
时,就将整个中文字符串对应的值作为value传入,调用了父类的_add_child方法来实现增加
对于Node类_add_child方法而言,新增一个节点的操作首先需要判断是否存在这个节点,不存在这个节点,分为两种情况:
1.要插入的节点不是字符串的末尾,也就没有了映射,那么新建节点的键是字符char,而值是一个value为None的节点,尽管value值为None,但是再次访问到这个节点时,child = self._children.get(char)获取到的不为空(这一点十分重要)
2.要插入的节点在字符串的末尾,此时有了一个value的映射值,那么新建节点的键是字符char,而值是一个value为映射值的节点
如果存在这个节点(且overwrite为False),那么直接返回该子节点即可。
字典树的查找
测试代码:
assert '自然' in trie
因为重写了__getitem__方法,所以只需采取in的写法就可以了
字典树的修改
测试代码:
# 改
trie['自然语言'] = 'language'
trie['自然语言'] = 'human language'
assert trie['自然语言'] == 'human language'
测试结果:
Node方法中的char: 自
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'自': <__main__.Node object at 0x000001D07D5935F8>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 然
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'然': <__main__.Node object at 0x000001D07D593668>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 语
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'语': <__main__.Node object at 0x000001D07D5936A0>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 言
Node方法中的value: language
Node方法中的child: None
------------------
生成的children: {'言': <__main__.Node object at 0x000001D07D5936D8>}
children中单字char对应的值(节点)中的value值: language
=====================
Node方法中的char: 自
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x000001D07D5935F8>
------------------
Node方法中的char: 然
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x000001D07D593668>
------------------
Node方法中的char: 语
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x000001D07D5936A0>
------------------
Node方法中的char: 言
Node方法中的value: human language
Node方法中的child: <__main__.Node object at 0x000001D07D5936D8>
------------------
同样用的是__setitem__方法,代码的逻辑跟增加的步骤大致相同,不过是进入了Node类_add_child方法的elif分支
输出 assert trie['自然语言'] == 'human language'并没有报错,说明修改成功
字典树的删除
测试代码:
# 删 对应于setitem方法,不过设置对应的值为None
trie['自然语言'] = 'language'
trie['自然语言'] = None
assert '自然语言' not in trie
测试结果:
Node方法中的char: 自
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'自': <__main__.Node object at 0x0000025C2C3D3630>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 然
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'然': <__main__.Node object at 0x0000025C2C3D36A0>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 语
Node方法中的value: None
Node方法中的child: None
------------------
生成的children: {'语': <__main__.Node object at 0x0000025C2C3D36D8>}
children中单字char对应的值(节点)中的value值: None
=====================
Node方法中的char: 言
Node方法中的value: language
Node方法中的child: None
------------------
生成的children: {'言': <__main__.Node object at 0x0000025C2C3D3710>}
children中单字char对应的值(节点)中的value值: language
=====================
Node方法中的char: 自
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x0000025C2C3D3630>
------------------
Node方法中的char: 然
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x0000025C2C3D36A0>
------------------
Node方法中的char: 语
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x0000025C2C3D36D8>
------------------
Node方法中的char: 言
Node方法中的value: None
Node方法中的child: <__main__.Node object at 0x0000025C2C3D3710>
------------------
输出只需要将最后的映射值置为None即可,复用了__setitem__方法。