字典树的简介
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。
Tree 树 是利用空间复杂度作为代价换区时间复杂度的应用,查找复杂度 为 o(Len),空间复杂度 是比较大的。
下面这个图是一个由 RA RC RZ RAC RAD RACE 等等组成 红色的节点 被标记成 了一个单词的结尾。
字典树的应用场景
- 求最近公共祖先,需要利用tire树作为数据结构
- 排序:前序遍历tire树,即可以得到一个按字母顺序的排序表
- 字符串检索:在搜索引擎中会用的(搜索时会弹出可能的单词结果),主要是已知一个字符串集合,对这个集合进行预处理生成一个tire树,以后就维护这个tire树即可。
Tire用于统计:
题目:给你100000个长度不超过10的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置。
解法 :从第一个单词开始构造Tire树,Tire树包含两字段,字符与位置,对于非结尾字符,其位置标0,结尾字符,标注在100000个单词词表中的位置。对词表建造Tire树,对每个词检索到词尾,若词尾的数字字段>0,表示单词已经在之前出现过,否则建立结尾字符标志,下次出现即可得知其首次出现位置,便利词表即可依次计算出每个单词首次出现位置复杂度为O(N×L)L为最长单词长度,N为词表大小
Tire用于排序
题目:对10000个英文名按字典顺序排序
解法:建造Tire树,先序便利即可得到结果。
基本数据结构
type trieNode struct {
isWord bool//是否单词结尾
next map[byte][]*trieNode //子节点
}
type Trie struct{
size int //大小
root *trieNode //根节点
}
首先 我们定义了 trie 节点 的基本数据结构,每个节点记录了 所有子节点的指针。 Trie 结构 定义了 大小 和 根节点。
首先 我们构造了一颗 Trie树,初始化Root节点。
//初始化 Trie树
func NewTrie() *Trie{
node := trieNode{
isWord: false,
next: make(map[byte][]*trieNode,0),
}
a := Trie{
size: 0,
root: &node,
}
return &a
}
结构很简单 就像 一个链表一样 只不过 有点区别 就是 每个节点 都有多个 子节点。
增删改查 我们先实现 增加:
这里面使用了非递归的方式,首先得到 root 下面的 所有节点,然后 遍历 节点
找不到 就新建 找到了 把 当前节点指针cur 改成 下一个节点指针,然后 如果是插入到最后一个字符了 ,就把 isWord 修改成True
//往 节点内 添加内容
func(trie *Trie) Insert(content string){
node := trieNode{
isWord: false,
next: make(map[byte]*trieNode,0),
}
if content == ""{
panic("error!")
}
if trie.root == nil{
trie.root = &node
}
rootnode := trie.root.next
var cur *trieNode
for i,v := range []byte(content){
var tmpnode *trieNode
//如果cur为 nil
if cur == nil{
if rootnode[v] == nil{
tmpnode = &trieNode{
isWord: false,
next: make(map[byte]*trieNode,0),
}
//如果是字符串 结尾
if len(content) -1 == i{
tmpnode.isWord = true
}
cur = tmpnode
rootnode[v] = cur
}else{
cur = rootnode[v]
}
//如果 cur 已经被初始化
}else {
if cur.next[v] == nil{
tmpnode = &trieNode{
isWord: false,
next: make(map[byte]*trieNode,0),
}
//如果是字符串 结尾
if len(content) -1 == i{
tmpnode.isWord = true
}
cur.next[v] = tmpnode
cur = tmpnode
}else{
//如果是字符串 结尾
if len(content) -1 == i{
cur.isWord = true
}
cur = cur.next[v]
}
}
}
}
有了 添加指定字符串,接下来我们再来写一个查找是否包含的方法:
让我们整理下思路,要查找 是否包含指定字符串
先定义一个 指针 记录当前节点
var cur *node
然后遍历 要查找的字符串
for i,key range := str
第一次 当前节点 就直接从根节点拿,
if cur == nil
cur == root.next
否则的话 从 子节点列表 取得 然后 根新为当前节点
else
cur = cur.next[key]
如果 cur 为 空 的话 表明 没找到
然后 如果 找到 字符串最后一个节点,但是字符串没有被 设置为 单词 就认为 没有找到。
上面的步骤其实很简单,比起插入来说。当然 如果用递归 可以跟简介一些。但其实 也差不多。下面给出代码。
func (trie *Trie) Contains(content string) bool{
if trie.root == nil{
panic("error nil")
}
var cur *trieNode //当前节点指针
for i,v := range []byte(content){
if cur == nil{
//第一次 从root的next子节点获取
cur = trie.root.next[v]
}else{
//当前节点 的next子节点里获取
cur = cur.next[v]
}
//如果 没有 获取到 认为 没有
if cur == nil{
return false
}
//如果已经 到达了最后一个节点 但是这个节点没有被标记成 单词
if len(content) -1 == i && cur.isWord == false{
return false
}
}
//如果 上面条件都不满足 则认为 找到了
return true
}
下面再来写个 遍历所有 Trie树里内容吧
这里我们用 递归 比较方便。 其实就是递归搜索,一直搜索 遍历完 所有 节点 然后 把每个节点记录的 字符 传递到下一个节点,如果是 单词就打印。
func deepSearchx(root *trieNode,deep int,res []byte) {
if root.isWord{
//如果 是一个单词 打印
fmt.Println(string(res))
}
for k,v := range root.next{
//搜索深度
if deep <= 0{
return
}
tmp := make([]byte,0,0)
tmp = append(tmp,res...)
tmp =append(tmp,k)
//不停递归传递 切片
deepSearchx(v, deep -1, tmp)
}
}
下面 是 我测试了下 代码:
下面顺便实现 下 删除的代码吧
有了上面的代码 要实现这个 是不是跟 玩一样。。。。
分解步骤 只需要两步:
第一步 找到 那个单词的根节点
第二部 判断 是不是单词,如果不是 我们不需要修改返回false 如果 是 单词 修改返回true
这里 我们 参考 ,Contains的代码做一点小修改就好了。
其实 如果 简单的话加上一行删除就好了。但是 如果删除了树 是不是 得回收原来的空间 。所以 我们需要在添加一个方法 递归 把 没用的节点删除了 这个 可以参考Search 的代码 .
这里用递归的思路
思路是这样的,如果 每个节点 判断 他下面么有挂载单词的节点 了 就认为 下面的 都没用了 那么就设置为 nil。
只要递归玩得好,代码实现 也是很简单的
//将被删除的节点清除掉
func adjust(root *trieNode,ptr *trieNode,key byte,deep int){
//如果 没有next了 并且 这个节点 没有 记录单词
if len(root.next) == 0 && root.isWord == false{
deepSearchforDelete(ptr,nil)
delete(ptr.next,key)
return
}else if len(root.next) == 0{
return
}
//如果 是 单词记录节点
for k,v := range root.next{
if deep == 0{
key = k
}
//记录下这个节点
if root.isWord {
ptr = root //记录父节点
key = k//记录子节点 key
}
adjust(v,ptr,key,deep + 1)
}
}
这里每次遇到单词节点 就记录下,如果 到达最后一个节点且最后一个节点 没有 isword 那么 就从 上一个 isword 处的next删除这一段。这里 我们 递归 有 3个参数 一个 不用多说 就是递归子节点,另 2 各参数 是用来保存 上一个 单词节点的 子节点,然后是要删除的key。
自动索引功能: 你打代码时 不是IDEA 经常 有自动提示吗,还有搜索引擎。
这里我们也实现下吧,其实 还是很简单的
首先是查找节点,然后把节点下所有的 单词打出来 这两个功能在 前面 已经分别实现过了。只要 把它们组合在一起就好啦。
/**
* Created by @CaomaoBoy on 2020/3/17.
* email:<115882934@qq.com>
*/
type trieNode struct {
isWord bool//是否单词结尾
next map[byte]*trieNode //子节点
}
type Trie struct{
size int //大小
root *trieNode //根节点
}
//初始化 Trie树
func NewTrie() *Trie{
node := trieNode{
isWord: false,
next: make(map[byte]*trieNode,26),
}
a := Trie{
size: 0,
root: &node,
}
return &a
}
//如果存在插入
func(trie *Trie) InsertIfContains(content string) bool{
if !trie.Contains(content){
trie.Insert(content)
return true
}
return false
}
//往 节点内 添加内容
func(trie *Trie) Insert(content string){
if content == ""{
panic("error!")
}
if trie.root == nil{
node := trieNode{
isWord: false,
next: make(map[byte]*trieNode,0),
}
trie.root = &node
}
rootnode := trie.root.next
var cur *trieNode
for i,v := range []byte(content){
var tmpnode *trieNode
//如果cur为 nil
if cur == nil{
if rootnode[v] == nil{
tmpnode = &trieNode{
isWord: false,
next: make(map[byte]*trieNode,0),
}
//如果是字符串 结尾
if len(content) -1 == i{
tmpnode.isWord = true
trie.size ++
}
cur = tmpnode
rootnode[v] = cur
}else{
cur = rootnode[v]
}
//如果 cur 已经被初始化
}else {
if cur.next[v] == nil{
tmpnode = &trieNode{
isWord: false,
next: make(map[byte]*trieNode,0),
}
//如果是字符串 结尾
if len(content) -1 == i{
tmpnode.isWord = true
trie.size ++
}
cur.next[v] = tmpnode
cur = tmpnode
}else{
cur = cur.next[v]
//如果是字符串 结尾
if len(content) -1 == i{
cur.isWord = true
trie.size ++
return
}
}
}
}
}
//是否包含 str
func (trie *Trie) Contains(content string) bool{
if trie.root == nil{
panic("error nil")
}
var cur *trieNode
for i,v := range []byte(content){
if cur == nil{
//第一次 从root的next子节点获取
cur = trie.root.next[v]
}else{
//当前节点 的next子节点里获取
cur = cur.next[v]
}
//如果 没有 获取到 认为 没有
if cur == nil{
return false
}
//如果已经 到达了最后一个节点 但是这个节点没有被标记成 单词
if len(content) -1 == i && cur.isWord == false{
return false
}
}
return true
}
//遍历查找所有节点内容
func(trie *Trie) Search(deep int){
if trie.root == nil{
panic("root nil")
}
//res := make([][]byte,0)
//deepSearch(trie.root,0,&res)
//for _,v := range res{
// fmt.Println(string(v))
//}
deepSearchx(trie.root,deep,nil)
}
深度优先遍历
//func deepSearch(root *trieNode,deep int,res *[][]byte) []byte{
// if root.isWord{
// return nil
// }
// for k,v := range root.next{
// tmp := make([]byte,0,0)
// tmp= append(tmp, k)
// tmp = append(tmp, deepSearch(v, deep+1, res)...)
// if deep == 0 {
// *res = append(*res, tmp)
// continue
// }else{
// fmt.Println(string(tmp))
// return tmp
// }
//
//
// }
// return nil
//}
//深度优先遍历
func deepSearchx(root *trieNode,deep int,res []byte) {
if root.isWord{
fmt.Println(string(res))
}
for k,v := range root.next{
//搜索深度
if deep == 0 && deep != -1{
return
}
tmp := make([]byte,0,0)
tmp = append(tmp,res...)
tmp =append(tmp,k)
deepSearchx(v, deep -1, tmp)
}
}
//用来输出显示
func deepSearchforDelete(root *trieNode,res []byte) {
if len(root.next) == 0{
fmt.Println("clear:",string(res))
}
for k,v := range root.next{
tmp := make([]byte,0,0)
tmp = append(tmp,res...)
tmp =append(tmp,k)
deepSearchforDelete(v, tmp)
}
}
//是否包含 str
func (trie *Trie) Delete(content string) bool{
if trie.root == nil{
panic("error nil")
}
var cur *trieNode
for i,v := range []byte(content){
if cur == nil{
//第一次 从root的next子节点获取
cur = trie.root.next[v]
}else{
//当前节点 的next子节点里获取
cur = cur.next[v]
}
//如果 没有 获取到 认为 没有
if cur == nil{
return false
}
//如果已经 到达了最后一个节点 但是这个节点没有被标记成 单词
if len(content) -1 == i && cur.isWord == false{
return false
}
}
cur.isWord = false//找到了 就删除
trie.Adjust()//删除没用节点
return true
}
//将被删除的节点清除掉
func(trie *Trie) Adjust(){
adjust(trie.root,trie.root,0,0)
}
//将被删除的节点清除掉
func adjust(root *trieNode,ptr *trieNode,key byte,deep int){
//如果 没有next了 并且 这个节点 没有 记录单词
if len(root.next) == 0 && root.isWord == false{
deepSearchforDelete(ptr,nil)
delete(ptr.next,key)
return
}else if len(root.next) == 0{
return
}
//如果 是 单词记录节点
for k,v := range root.next{
if deep == 0{
key = k
}
//记录下这个节点
if root.isWord {
ptr = root //记录父节点
key = k//记录子节点 key
}
adjust(v,ptr,key,deep + 1)
}
}
//自动提示
func(trie *Trie) Suggess(content string){
if trie.root == nil{
panic("error nil")
}
var cur *trieNode
for i,v := range []byte(content){
if cur == nil{
//第一次 从root的next子节点获取
cur = trie.root.next[v]
}else{
//当前节点 的next子节点里获取
cur = cur.next[v]
}
//如果 没有 获取到 认为 没有
if cur == nil{
fmt.Println("no srarch")
}
//如果已经 到达了最后一个节点 但是这个节点没有被标记成 单词
if len(content) -1 == i {
deepSearchx(cur,-1,nil)
}
}
}
func main(){
t := NewTrie()
t.Insert("caomao")
t.Insert("triemap")
t.Insert("tEST00001")
t.Insert("tEST0")
t.Insert("SADASDASDSADASD")
t.Search(20)
t.Insert("apple");
t.Insert("app");
fmt.Println(t.Contains("app"))
fmt.Println(t.Contains("tEST0"))
fmt.Println(t.Contains("apple"))
t.Delete("app")
fmt.Println(t.Contains("apple"))
t.Suggess("tE")
}
初步测试了下,代码应该没什么大问题,由于 随便写的 逻辑应该没问题,代码比较粗糙,随便写的 也没借鉴别人的代码。