C#实现前向最大匹、字典树(分词、检索)的示例代码

2020-05-15 16:01:12王冬梅

  测试下:

  现在我们有了字典树,然后就不能以字典树来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() == 0)
      {
        throw new ArgumentException("字典树没有数据,请先调用 LoadTrieData 方法装载字典树");
      }
      //取词的最大长度
      int maxLength = trie.MaxLength();
      //取词的当前长度
      int wordLength = maxLength;
      //分词操作中,处于字符串中的当前位置
      int position = 0;
      //分词操作中,已经处理的字符串总长度
      int segLength = 0;
      //用于尝试分词的取词字符串
      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 == 1)
          {
            word = null;
            break;
          }

          //把截取的字符串,最边上的一个字去掉
          //从左到右 和 从右到左时,截掉的字符的位置不同
          if (leftToRight)
            word = word.Substring(0, word.Length - 1);
          else
            word = word.Substring(1);
        }

        //将分出匹配上的词,加入到分词字符串数组中,正向和逆向不同
        if (word != null)
        {
          if (leftToRight)
            segWords.Add(word);
          else
            segWordsReverse.Add(word);
          //已经完成分词的字符串长度,要相应增加
          segLength += word.Length;
        }
        else
        {
          //没匹配上的则+1,丢掉一个字(如果是分词 则不用判断word是否为空,单个字也返回)
          segLength += 1;
        }
      }

      //如果是逆向分词,对分词结果反转排序
      if (!leftToRight)
      {
        for (int i = segWordsReverse.Count - 1; i >= 0; i--)
        {
          //将反转的结果,保存在正向分词数组中 以便最后return 同一个变量segWords
          segWords.Add(segWordsReverse[i]);
        }
      }

      return segWords;
    }
  }