中文分词基础中trie树的实现方式研究

来源:互联网 发布:第一台量子网络 编辑:程序博客网 时间:2024/05/22 08:15
在分词器中涉及到一种数据结构,trie树。

trie树的作用

又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。



trie树有几种实现方式,其中最简单的是数组结构。树中每个节点用一个固定大小的数组来表示,如果节点具有子节点,就在数组的相应位置插入子节点的指针。如下图



数组方式查询的速度很快,O(1)的时间复杂度,插入的速度也快,但是空间占用很大。如果是汉语这种“字母”较多的话,会有巨大的空间浪费。


如果用哈希表的方式来实现,就能规避掉这种空间浪费。

树的每个节点都用一个哈希表来表示,如果存在子节点,就会在相应的键对应的值上存储子节点的指针。

哈希表的方式,查询的速度接近O(1),插入也一样,但是空间浪费较小。


附上我写的哈希表方式实现trie树的go代码。



package Collectionsimport ("fmt")//树节点 用Hash表存储<Character, Node>type TrieNode struct {Num       int                //经过这个节点的单词数量Sons      map[rune]*TrieNode //所有子节点IsEnd     bool               //是否是某个单词的终点Character rune               //存储的值}func newTrieNode() *TrieNode {node := &TrieNode{}node.Num = 1node.Sons = make(map[rune]*TrieNode)node.IsEnd = falsereturn node}//map实现的trie树,查询速度快,占用空间有浪费type MapTrie struct {Root *TrieNode}func NewMapTrie() *MapTrie {newtrie := &MapTrie{}newtrie.Root = newTrieNode()return newtrie}//加载词典func (this *MapTrie) LoadDict(dictpath string) {}//插入一个单词func (this *MapTrie) Insert(word string) {if len(word) == 0 {return}//if this.Has(word.py) {//return//}tmpNode := this.Rootletters := []rune(word)for _, letter := range letters {//如果没有这个字母if _, ok := tmpNode.Sons[letter]; !ok {tmpNode.Sons[letter] = newTrieNode()tmpNode.Sons[letter].Character = letter} else {tmpNode.Sons[letter].Num++}tmpNode = tmpNode.Sons[letter]}tmpNode.IsEnd = true}//计算包含某个前缀的单词数量func (this *MapTrie) CountPrefix(prefix string) int {if len(prefix) == 0 {return -1}tmpNode := this.Rootletters := []rune(prefix)for _, letter := range letters {//如果没有这个字母if _, ok := tmpNode.Sons[letter]; !ok {return 0} else {tmpNode = tmpNode.Sons[letter]}}return tmpNode.Num}//前序遍历节点func PreTraverse(node *TrieNode) {if node != nil {fmt.Println(string(node.Character))for _, son := range node.Sons {PreTraverse(son)}}}func (this *MapTrie) PreTraverseByPrefix(prefix string) {node := this.FindPrefix(prefix)if node == nil {return}PreTraverse(node)}//是否包含某个单词func (this *MapTrie) Has(word string) bool {if len(word) == 0 {return false}tmpNode := this.Rootletters := []rune(word)for _, letter := range letters {//如果没有这个字母if _, ok := tmpNode.Sons[letter]; !ok {return false} else {tmpNode = tmpNode.Sons[letter]}}if tmpNode.IsEnd == true {return true}return false}//找到某个前缀的最终节点func (this *MapTrie) FindPrefix(prefix string) *TrieNode {if len(prefix) == 0 {return nil}tmpNode := this.Rootletters := []rune(prefix)for _, letter := range letters {//如果没有这个字母if _, ok := tmpNode.Sons[letter]; !ok {return nil} else {tmpNode = tmpNode.Sons[letter]}}return tmpNode}//分词func (this *MapTrie) Segment(setence string) []string {chars := []rune(setence)results := make([]string, 0)//tmpChar := ""tmpHead := 0         //扫描全部字符过程中位于单词头部的指针位置tmpNode := this.Root //在词典里顺延的节点指针length := len(chars)for index := 0; index < length; index++ {//如果有这个字符,就继续顺下去(要求扫描指针未到尾部)// 防止尾部的单词是个完整词,同时也是一个另一个词的前缀if _, ok := tmpNode.Sons[chars[index]]; ok {tmpNode = tmpNode.Sons[chars[index]]if index != length-1 {continue} else {// 这个地方后移是为了接下来截取单词能正确,因// 为如果发现了匹配的单词,用到列表切片// 用了slice[start:end]。如果不是因为是最后一个,// index所在的位置都是单词之后index++}}//匹配到了单词(词典里面没有的字,或者是词典里面的词匹配完成了)//第一个字词典里面就没有if tmpHead == index {word := string(chars[tmpHead])results = append(results, word)//将头指针移到下一个字符tmpHead = index + 1// 将指针节点移到树根tmpNode = this.Root} else if tmpNode.IsEnd { //词典里面的词结束了word := string(chars[tmpHead:index])results = append(results, word)//将头指针移到现在的位置tmpHead = indexindex-- // index往回退一个,因为这一次的字符和之前的不连,不退就错过了一个字符// 将节点指针移到树根tmpNode = this.Root} else {//匹配到词典某个词的非词前缀(此前缀本身不是词)。//这一段只是词典里某个词的前缀 割出第一个字,扫描指针重新到刚刚的字后面word := string(chars[tmpHead])results = append(results, word)tmpHead++           //单词头指针移动一格index = tmpHead - 1 //扫描指针回到前面 这个地方减一是因为这次循环结束会加1tmpNode = this.Root}}return results}


原创粉丝点击