场景:现在有一个错词库,维护的是错词和正确词对应关系。比如:错词“我门”对应的正确词“我们”。然后在用户输入的文字进行错词校验,需要判断输入的文字是否有错词,并找出错词以便提醒用户,并且可以显示出正确词以便用户确认,如果是错词就进行替换。

  首先想到的就是取出错词List放在内存中,当用户输入完成后用错词List来foreach每个错词,然后查找输入的字符串中是否包含错词。这是一种有效的方法,并且能够实现。问题是错词的数量比较多,目前有10多万条,将来也会不断更新扩展。所以pass了这种方案,为了让错词查找提高速度就用了字典树来存储错词。

字典树

  Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较。

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

通常字典树的查询时间复杂度是O(logL),L是字符串的长度。所以效率还是比较高的。而我们上面说的foreach循环则时间复杂度为O(n),根据时间复杂度来看,字典树效率应该是可行方案。

字典树原理

  根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。

  比如现在有错词:“我门”、“旱睡”、“旱起”。那么字典树如下图

   其中红色的点就表示词结束节点,也就是从根节点往下连接成我们的词。

  实现字典树:

 public class Trie
{
private class Node
{
/// <summary>
/// 是否单词根节点
/// </summary>
public bool isTail = false; public Dictionary<char, Node> nextNode; public Node(bool isTail)
{
this.isTail = isTail;
this.nextNode = new Dictionary<char, Node>();
}
public Node() : this(false)
{
}
} /// <summary>
/// 根节点
/// </summary>
private Node rootNode;
private int size;
private int maxLength; public Trie()
{
this.rootNode = new Node();
this.size = ;
this.maxLength = ;
} /// <summary>
/// 字典树中存储的单词的最大长度
/// </summary>
/// <returns></returns>
public int MaxLength()
{
return maxLength;
} /// <summary>
/// 字典树中存储的单词数量
/// </summary>
public int Size()
{
return size;
} /// <summary>
/// 获取字典树中所有的词
/// </summary>
public List<string> GetWordList()
{
return GetStrList(this.rootNode);
} private List<string> GetStrList(Node node)
{
List<string> wordList = new List<string>(); foreach (char nextChar in node.nextNode.Keys)
{
string firstWord = Convert.ToString(nextChar);
Node childNode = node.nextNode[nextChar]; if (childNode == null || childNode.nextNode.Count == )
{
wordList.Add(firstWord);
}
else
{ if (childNode.isTail)
{
wordList.Add(firstWord);
} List<string> subWordList = GetStrList(childNode);
foreach (string subWord in subWordList)
{
wordList.Add(firstWord + subWord);
}
}
} return wordList;
} /// <summary>
/// 向字典中添加新的单词
/// </summary>
/// <param name="word"></param>
public void Add(string word)
{
//从根节点开始
Node cur = this.rootNode;
//循环遍历单词
foreach (char c in word.ToCharArray())
{
//如果字典树节点中没有这个字母,则添加
if (!cur.nextNode.ContainsKey(c))
{
cur.nextNode.Add(c, new Node());
}
cur = cur.nextNode[c];
}
cur.isTail = true; if (word.Length > this.maxLength)
{
this.maxLength = word.Length;
}
size++;
} /// <summary>
/// 查询字典中某单词是否存在
/// </summary>
/// <param name="word"></param>
/// <returns></returns>
public bool Contains(string word)
{
return Match(rootNode, word);
} /// <summary>
/// 查找匹配
/// </summary>
/// <param name="node"></param>
/// <param name="word"></param>
/// <returns></returns>
private bool Match(Node node, string word)
{
if (word.Length == )
{
if (node.isTail)
{
return true;
}
else
{
return false;
}
}
else
{
char firstChar = word.ElementAt();
if (!node.nextNode.ContainsKey(firstChar))
{
return false;
}
else
{
Node childNode = node.nextNode[firstChar];
return Match(childNode, word.Substring(, word.Length - ));
}
}
}
}

  测试下:

  现在我们有了字典树,然后就不能以字典树来foreach,字典树用于检索。我们就以用户输入的字符串为数据源,去字典树种查找是否存在错词。因此需要对输入字符串进行取词检索。也就是分词,分词我们采用前向最大匹配。

前向最大匹配

  我们分词的目的是将输入字符串分成若干个词语,前向最大匹配就是从前向后寻找在词典中存在的词。

  例子:我们假设maxLength= 3,即假设单词的最大长度为3。实际上我们应该以字典树中的最大单词长度,作为最大长度来分词(上面我们的字典最大长度应该是2)。这样效率更高,为了演示匹配过程就假设maxLength为3,这样演示的更清楚。

  用前向最大匹配来划分“我们应该早睡早起” 这句话。因为我是错词匹配,所以这句话我改成“我门应该旱睡旱起”。

  第一次:取子串 “我门应”,正向取词,如果匹配失败,每次去掉匹配字段最后面的一个字。

  “我门应”,扫描词典中单词,没有匹配,子串长度减 1 变为“我门”。

  “我门”,扫描词典中的单词,匹配成功,得到“我门”错词,输入变为“应该旱”。

  第二次:取子串“应该旱”

  “应该旱”,扫描词典中单词,没有匹配,子串长度减 1 变为“应该”。

  “应该”,扫描词典中的单词,没有匹配,输入变为“应”。

  “应”,扫描词典中的单词,没有匹配,输入变为“该旱睡”。

  第三次:取子串“该旱睡”

  “该旱睡”,扫描词典中单词,没有匹配,子串长度减 1 变为“该旱”。

  “该旱”,扫描词典中的单词,没有匹配,输入变为“该”。

  “该”,扫描词典中的单词,没有匹配,输入变为“旱睡旱”。

  第四次:取子串“旱睡旱”

  “旱睡旱”,扫描词典中单词,没有匹配,子串长度减 1 变为“旱睡”。

  “旱睡”,扫描词典中的单词,匹配成功,得到“旱睡”错词,输入变为“早起”。

  以此类推,我们得到错词 我们/旱睡/旱起。

  因为我是结合字典树匹配错词所以一个字也可能是错字,则匹配到单个字,如果只是分词则上面的到一个字的时候就应该停止分词了,直接字符串长度减1。

  这种匹配方式还有后向最大匹配以及双向匹配,这个大家可以去了解下。

  实现前向最大匹配,这里后向最大匹配也可以一起实现。

  public class ErrorWordMatch
{
private static ErrorWordMatch singleton = new ErrorWordMatch();
private static Trie trie = new Trie();
private ErrorWordMatch()
{ } public static ErrorWordMatch Singleton()
{
return singleton;
} public void LoadTrieData(List<string> errorWords)
{
foreach (var errorWord in errorWords)
{
trie.Add(errorWord);
}
} /// <summary>
/// 最大 正向/逆向 匹配错词
/// </summary>
/// <param name="inputStr">需要匹配错词的字符串</param>
/// <param name="leftToRight">true为从左到右分词,false为从右到左分词</param>
/// <returns>匹配到的错词</returns>
public List<string> MatchErrorWord(string inputStr, bool leftToRight)
{
if (string.IsNullOrWhiteSpace(inputStr))
return null;
if (trie.Size() == )
{
throw new ArgumentException("字典树没有数据,请先调用 LoadTrieData 方法装载字典树");
}
//取词的最大长度
int maxLength = trie.MaxLength();
//取词的当前长度
int wordLength = maxLength;
//分词操作中,处于字符串中的当前位置
int position = ;
//分词操作中,已经处理的字符串总长度
int segLength = ;
//用于尝试分词的取词字符串
string word = ""; //用于储存正向分词的字符串数组
List<string> segWords = new List<string>();
//用于储存逆向分词的字符串数组
List<string> segWordsReverse = new List<string>(); //开始分词,循环以下操作,直到全部完成
while (segLength < inputStr.Length)
{
//如果剩余没分词的字符串长度<取词的最大长度,则取词长度等于剩余未分词长度
if ((inputStr.Length - segLength) < maxLength)
wordLength = inputStr.Length - segLength;
//否则,按最大长度处理
else
wordLength = maxLength; //从左到右 和 从右到左截取时,起始位置不同
//刚开始,截取位置是字符串两头,随着不断循环分词,截取位置会不断推进
if (leftToRight)
position = segLength;
else
position = inputStr.Length - segLength - wordLength; //按照指定长度,从字符串截取一个词
word = inputStr.Substring(position, wordLength); //在字典中查找,是否存在这样一个词
//如果不包含,就减少一个字符,再次在字典中查找
//如此循环,直到只剩下一个字为止
while (!trie.Contains(word))
{
//如果最后一个字都没有匹配,则把word设置为空,用来表示没有匹配项(如果是分词直接break)
if (word.Length == )
{
word = null;
break;
} //把截取的字符串,最边上的一个字去掉
//从左到右 和 从右到左时,截掉的字符的位置不同
if (leftToRight)
word = word.Substring(, word.Length - );
else
word = word.Substring();
} //将分出匹配上的词,加入到分词字符串数组中,正向和逆向不同
if (word != null)
{
if (leftToRight)
segWords.Add(word);
else
segWordsReverse.Add(word);
//已经完成分词的字符串长度,要相应增加
segLength += word.Length;
}
else
{
//没匹配上的则+1,丢掉一个字(如果是分词 则不用判断word是否为空,单个字也返回)
segLength += ;
}
} //如果是逆向分词,对分词结果反转排序
if (!leftToRight)
{
for (int i = segWordsReverse.Count - ; i >= ; i--)
{
//将反转的结果,保存在正向分词数组中 以便最后return 同一个变量segWords
segWords.Add(segWordsReverse[i]);
}
} return segWords;
}
}

  这里使用了单例模式用来在项目中共用,在第一次装入了字典树后就可以在其他地方匹配错词使用了。

  这个是结合我具体使用,简化了些代码,如果只是分词的话就是分词那个实现方法就行了。最后分享就到这里吧,如有不对之处,请加以指正。

C#实现前向最大匹、字典树(分词、检索)的更多相关文章

  1. HDU - 6096 处理后缀的字典树

    题意:给定n个字符串,m次询问,每次询问多少个字符串前缀是pre且后缀是suf,前后缀不可相交 字典树同时存储前后缀,假设字符串长为len则更新2*len个节点,依次按s[0],s[len-1],s[ ...

  2. UVA - 12333 Revenge of Fibonacci 高精度加法 + 字典树

    题目:给定一个长度为40的数字,问其是否在前100000项fibonacci数的前缀 因为是前缀,容易想到字典树,同时因为数字的长度只有40,所以我们只要把fib数的前40位加入字典树即可.这里主要讨 ...

  3. Trie树(字典树) 最热门的前N个搜索关键词

    方法介绍 1.1.什么是Trie树 Trie树,即字典树,又称单词查找树或键树,是一种树形结构.典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计.它的优 ...

  4. 萌新笔记——用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)

    前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽. 基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成"* ...

  5. 用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)

    前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽. 基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成“***”就可 ...

  6. [LeetCode] Implement Trie (Prefix Tree) 实现字典树(前缀树)

    Implement a trie with insert, search, and startsWith methods. Note:You may assume that all inputs ar ...

  7. HDU1671 字典树

    Phone List Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  8. 字典树(Trie Tree)

    在图示中,键标注在节点中,值标注在节点之下.每一个完整的英文单词对应一个特定的整数.Trie 可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的.键不需要被显式地保存在节点中. ...

  9. 字典树(Trie树)的实现及应用

    >>字典树的概念 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树.与二叉查找树不同,Trie树的 ...

随机推荐

  1. swoole--服务平滑重启

    参考来源:https://wiki.swoole.com/wiki/page/p-server/reload.html shell代码: echo "loading..." pid ...

  2. 2019-2020-1 20199308《Linux内核原理与分析》第三周作业

    <Linux内核分析> 第二章 操作系统是如何工作的 2.1 函数调用堆栈 3个关键性的方法机制(3个法宝) 存储程序计算机 函数调用堆栈机制 中断 堆栈相关的寄存器 ESP:堆栈指针(s ...

  3. 设计数据库 ER 图太麻烦?不妨试试这两款工具,自动生成数据库 ER 图!!!

    忙,真忙 点赞再看,养成习惯,微信搜索『程序通事』,关注就完事了! 点击查看更多精彩的文章 这两个星期真是巨忙,年前有个项目因为各种莫名原因,一直拖到这个月才开始真正测试.然后上周又接到新需求,马不停 ...

  4. 怎样实现App安装来源追踪

    众所周知,国内的应用商店存在一定的限制,开发者很难有效监测到App安装来源的精准数据.但在实际推广中,广告效果.用户行为.付费统计.邀请关系等不同渠道的指标却是衡量渠道价值的关键,对App的运营推广和 ...

  5. 项目Alpha冲刺 Day12

    1)站立式会议: 2)今日安排: 项目演示. 3)项目情况 项目进展:系统已实现预期的所有的功能.问题困难:系统测试不够全面,主要做功能测试,对于非功能测试,如压力测试.效能测试.安全性等并未测试.心 ...

  6. postman的使用概览

    本文主要描述postman的功能与使用方法Postman是404大厂的基于javascript语言完成的一款超级强大的插件,名字也很亲近(邮递员).可以用于做API请求测试.前端后台测试使用Postm ...

  7. XmlSerializer .NET 序列化、反序列化

    序列化对象   要序列化对象,首先创建要序列化的对象并设置其公共属性和字段.为此,您必须确定要将XML流存储的传输格式,作为流或文件. 例如,如果XML流必须以永久形式保存,则创建一个FileStre ...

  8. Docker容器利用weave实现跨主机互联

    Docker容器利用weave实现跨主机互联 环境: 实现目的:实现主机A中容器1与主机B中容器1的网络互联 主机A步骤: ①下载复制weave二进制执行文件(需要internet)[root@192 ...

  9. jQuery简单竖排手风琴折叠菜单代码

    项目需求1.刚开始只显示,每个标题, 2.让每个 li列表隔行换色 3.当我点击某个标题时,下面的列表会缓慢的展开,其他列表展开的内容会收起 <!DOCTYPE html> <htm ...

  10. P3983 赛斯石(双背包)

    这题不算难的,但是脑子真的特别乱.....传送门 \(Ⅰ.物品可以拆开来但船不能拆开来,所以1-10载重船的最大收益完全可以用背包求出来.\) \(Ⅱ.最后一定是选一些船走,而船的收益已经固定.所以用 ...