From 6fed950fa9eda38c5278f32ff42a92aa1e0b64af Mon Sep 17 00:00:00 2001 From: Huangzj Date: Tue, 15 Dec 2020 09:49:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(Go-Tool)=EF=BC=9A2020/12/15=EF=BC=9A?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=89=8D=E7=BC=80=E6=A0=91=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=89=8D=E7=BC=80=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=92=8C=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=90=9C=E7=B4=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 2 + util/set/Set.go | 15 ++++ util/trie/Trie.go | 156 +++++++++++++++++++++++++++++++++++++++++ util/trie/Trie_test.go | 115 ++++++++++++++++++++++++++++++ util/trie/readme.md | 19 +++++ 5 files changed, 307 insertions(+) create mode 100644 util/trie/Trie.go create mode 100644 util/trie/Trie_test.go create mode 100644 util/trie/readme.md diff --git a/readme.md b/readme.md index c655803..64b0c5e 100644 --- a/readme.md +++ b/readme.md @@ -68,6 +68,8 @@ 2020/12/14:添加validator.v8源码解析和使用示例 +2020/12/15:添加前缀树工具,实现前缀搜索和字符串搜索功能 + # mod vendor模式加载包 通过go mod的方式加载的github上面的包会有报红的问题,但是包本身是可以运行的,这样就是会有一个问题,如果你想要点击去看方法的内容,没办法做到 diff --git a/util/set/Set.go b/util/set/Set.go index ad3cc43..0eee248 100644 --- a/util/set/Set.go +++ b/util/set/Set.go @@ -8,6 +8,15 @@ package set import "sync" +type ISet interface { + Add(i interface{}) + AddAll(list ...interface{}) + Remove(i interface{}) + Contains(i interface{}) bool + IsEmpty() bool + GetAllSet() []interface{} +} + type Set struct { setMap sync.Map //用来存储元素的map } @@ -16,6 +25,12 @@ func (set *Set) Add(i interface{}) { set.setMap.Store(i, nil) } +func (set *Set) AddAll(list ...interface{}) { + for _, item := range list { + set.Add(item) + } +} + func (set *Set) Remove(i interface{}) { set.setMap.Delete(i) } diff --git a/util/trie/Trie.go b/util/trie/Trie.go new file mode 100644 index 0000000..8e7d08b --- /dev/null +++ b/util/trie/Trie.go @@ -0,0 +1,156 @@ +/* + * @Author : huangzj + * @Time : 2020/12/14 16:15 + * @Description:前缀树\字典树 + */ + +package trie + +import "github.com/ahmetb/go-linq" + +type Trie struct { + Child map[rune]*Trie //存储当前字符(Rune)下面对应的Rune的集合,使用Map可以提高查找效率 + Word string //存储字典树上有的字符串信息,如果Word是空字符串(""),则代表没有在该节点找到对应的字符串信息 +} + +const ( + emptyTag = "" +) + +type WordFunc func(sentence []rune, start int, end int) string + +func New() *Trie { + return initTrie() +} + +//初始化Trie +func initTrie() *Trie { + return &Trie{ + Child: make(map[rune]*Trie, 0), + Word: emptyTag, + } +} + +//往前缀树中添加对应的字符串(build模式) +func (trie *Trie) insert(word string) *Trie { + traverse := trie + for _, v := range []rune(word) { + if _, ok := traverse.Child[v]; !ok { + newTrie := initTrie() + traverse.Child[v] = newTrie + } + traverse = traverse.Child[v] + } + traverse.Word = word + return trie +} + +//删除对应的字符串,如果查找不到则直接返回(build模式) +func (trie *Trie) Delete(word string) *Trie { + traverse := trie + for _, v := range []rune(word) { + if _, ok := traverse.Child[v]; !ok { + return trie + } + traverse = traverse.Child[v] + } + traverse.Word = emptyTag + return trie +} + +//查找对应的字符串,找到则返回true,没找到则返回false +func (trie *Trie) Search(word string) bool { + traverse := trie + for _, v := range []rune(word) { + if _, ok := traverse.Child[v]; !ok { + return false + } + traverse = traverse.Child[v] + } + if traverse.Word != emptyTag { + return true + } + return false +} + +//遍历前缀树存储的所有字符串 +func (trie *Trie) Traverse() []string { + result := make([]string, 0) + result = traverse(trie, result) + return result +} + +func traverse(trie *Trie, result []string) []string { + for _, value := range trie.Child { + if value.Word != emptyTag { + result = append(result, value.Word) + } + //进行子的路径遍历 + if len(value.Child) != 0 { + result = traverse(value, result) + } + } + + return result +} + +//查找字符串中匹配的子字符串,进行自定义处理 +func (trie *Trie) DealFunc(sentence string, wordFunc WordFunc) string { + traverse := trie + for index, v := range []rune(sentence) { + //从句子的字符开始往下查找,找到最后一个对应的字符(如果uuck和uucku都是敏感词汇的话,会把uucku设置为*,而不只是uuck.) + if _, ok := traverse.Child[v]; ok { + traverse = traverse.Child[v] + continue + } + if traverse.Word != emptyTag { + //传入到wordFunc里面的是字符串的rune数组、查询到的词汇在字符串中的开始位置、查询到的词汇在字符串中的结束位置 + sentence = wordFunc([]rune(sentence), index-len([]rune(traverse.Word)), index) + } + //trie从头开始 + traverse = trie + } + + return sentence +} + +//搜索前缀匹配的字符串集合(精准匹配) +func (trie *Trie) SearchTrie(word string) ([]string, bool) { + traverse := trie + for _, value := range []rune(word) { + if _, ok := traverse.Child[value]; !ok { + return []string{}, false + } + traverse = traverse.Child[value] + } + return traverse.Traverse(), true +} + +//搜索前缀匹配的字符串集合(模糊匹配) +func (trie *Trie) SearchTrieVague(word string) ([]string, bool) { + var tries []*Trie + traverse := trie + for _, value := range []rune(word) { + if _, ok := traverse.Child[value]; !ok { + break + } + traverse = traverse.Child[value] + tries = append(tries, traverse) + } + result := make([]string, 0) + for _, item := range tries { + result = append(result, item.Traverse()...) + } + + if len(result) == 0 { + return []string{}, false + } + realResult := make([]string, 0) + linq.From(result).Distinct().Sort(func(i, j interface{}) bool { + if i.(string) > j.(string) { + return true + } + return false + }).ToSlice(&realResult) //去重 + return realResult, true +} diff --git a/util/trie/Trie_test.go b/util/trie/Trie_test.go new file mode 100644 index 0000000..2f34d4b --- /dev/null +++ b/util/trie/Trie_test.go @@ -0,0 +1,115 @@ +/* + * @Author : huangzj + * @Time : 2020/12/14 16:15 + * @Description: + */ + +package trie + +import ( + "fmt" + "testing" +) + +func TestTrie(t *testing.T) { + commonTest() //常规功能测试 + sensitiveWordsTest() //敏感词汇屏蔽测试 + searchTrie() //查找前缀匹配的所有字符串 + searchTrieVague() //模糊查询前缀匹配的所有字符串 +} + +func searchTrieVague() { + fmt.Println() + fmt.Println("模糊查询 查找前缀匹配的所有字符串") + trie := New() + trie.insert("abcuuid").insert("abcuued").insert("abcMxns").insert("abssde").insert("avddfa") + trie.insert("我爱中国").insert("我爱祖国").insert("我爱学习").insert("我是谁").insert("我在哪里") + trie.insert("我是你").insert("我").insert("我喜欢学习").insert("我在厦门").insert("我是一个程序员") + + fmt.Println() + fmt.Println() + result3, _ := trie.SearchTrieVague("我爱她") + for _, r := range result3 { + fmt.Println(r) + } +} + +func searchTrie() { + fmt.Println() + fmt.Println("查找前缀匹配的所有字符串") + trie := New() + trie.insert("abcuuid").insert("abcuued").insert("abcMxns").insert("abssde").insert("avddfa") + trie.insert("我爱中国").insert("我爱祖国").insert("我爱学习").insert("我是谁").insert("我在哪里") + + fmt.Println() + fmt.Println() + result, _ := trie.SearchTrie("abc") + for _, r := range result { + fmt.Println(r) + } + + fmt.Println() + fmt.Println() + result1, _ := trie.SearchTrie("我爱") + for _, r := range result1 { + fmt.Println(r) + } + + fmt.Println() + fmt.Println() + result2, _ := trie.SearchTrie("我爱她") + for _, r := range result2 { + fmt.Println(r) + } + +} + +func sensitiveWordsTest() { + trie := New() + trie.insert("粗话").insert("fuck").insert("fuckk").insert("脏话") + s := "我是脏话,我是粗话,fuckk,fuck,fffuccck" + word := trie.DealFunc(s, func(sentence []rune, start int, end int) string { + var replace string + for i := 0; i < end-start; i++ { + replace = replace + "*" + } + runes := append(sentence[0:start], []rune(replace)...) + runes = append(runes, sentence[end:]...) + return string(runes) + }) + fmt.Println(word) +} + +func commonTest() { + trie := New() + trie.insert("ABC").insert("AB").insert("ABE").insert("ABEX").insert("XYZ").insert("你").insert("你好").insert("你是谁") + result := trie.Traverse() + for _, r := range result { + fmt.Println(r) + } + + fmt.Println() + fmt.Println("进行查找测试") + fmt.Println(trie.Search("Abc")) + fmt.Println(trie.Search("ABC")) + fmt.Println(trie.Search("ABEX")) + fmt.Println(trie.Search("ABEx")) + fmt.Println(trie.Search("你好")) + fmt.Println(trie.Search("你好吗")) + + fmt.Println("进行删除测试") + trie.Delete("你").Delete("ABC") + fmt.Println(trie.Search("你")) + fmt.Println(trie.Search("你好")) + fmt.Println(trie.Search("你是谁")) + fmt.Println(trie.Search("AB")) + fmt.Println(trie.Search("ABE")) + fmt.Println(trie.Search("ABC")) + + fmt.Println() + fmt.Println("再次遍历所有的字典") + result = trie.Traverse() + for _, r := range result { + fmt.Println(r) + } +} diff --git a/util/trie/readme.md b/util/trie/readme.md new file mode 100644 index 0000000..52ec60b --- /dev/null +++ b/util/trie/readme.md @@ -0,0 +1,19 @@ +## 1、什么是前缀树 +参考知乎的解答:[前缀树](https://www.zhihu.com/question/318375802/answer/663596639) + +前缀树在字符串查找起到很重要的作用,它的key都为字符串,能做到高效查询和插入,时间复杂度为O(k),k为字符串长度,缺点是如果大量字符串没有共同前缀时很耗内存。 + + +## 2、前缀树的作用 + 1. 前缀匹配 + 2. 字符串检索 + + +## 3、提供方法 + 1、往前缀树中添加对应的字符串(build模式) + 2、删除对应的字符串,如果查找不到则直接返回(build模式) + 3、查找对应的字符串,找到则返回true,没找到则返回false + 4、遍历前缀树存储的所有字符串 + 5、查找字符串中匹配的子字符串,进行自定义处理 + 6、搜索前缀匹配的字符串集合(精准匹配) + 7、搜索前缀匹配的字符串集合(模糊匹配)