服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C#实现前向最大匹、字典树(分词、检索)的示例代码

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

2022-09-07 15:02Spring2Sun C#

这篇文章主要介绍了C#实现前向最大匹、字典树的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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

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

字典树

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

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

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

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

字典树原理

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

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

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

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

  实现字典树:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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 = 0;
    this.maxlength = 0;
  }
 
  /// <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 == 0)
      {
        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 == 0)
    {
      if (node.istail)
      {
        return true;
      }
      else
      {
        return false;
      }
    }
    else
    {
      char firstchar = word.elementat(0);
      if (!node.nextnode.containskey(firstchar))
      {
        return false;
      }
      else
      {
        node childnode = node.nextnode[firstchar];
        return match(childnode, word.substring(1, word.length - 1));
      }
    }
  }
}

  测试下:

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

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

前向最大匹配

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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;
    }
  }

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

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

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

到此这篇关于c#实现前向最大匹、字典树(分词、检索)的示例代码的文章就介绍到这了,更多相关c# 前向最大匹、字典树内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://www.cnblogs.com/SunSpring/p/12891504.html

延伸 · 阅读

精彩推荐
  • C#c#图片上传和显示的实现方法

    c#图片上传和显示的实现方法

    这篇文章主要介绍了c#实现图片上传和显示的实现方法,可实现图片上传效果预览功能,需要的朋友可以参考下。...

    叶超Luka8842021-12-08
  • C#Windows系统中C#调用WinRAR来压缩和解压缩文件的方法

    Windows系统中C#调用WinRAR来压缩和解压缩文件的方法

    这篇文章主要介绍了Windows系统中C#调用WinRAR来压缩和解压缩文件的方法,个人感觉在Windows中WinRAR相对7-zip更加稳定一些,需要的朋友可以参考下...

    hzy37743832021-11-18
  • C#C#中图片、二进制与字符串的相互转换方法

    C#中图片、二进制与字符串的相互转换方法

    这篇文章主要介绍了C#中图片、二进制与字符串的相互转换方法,涉及C#针对不同数据类型的解析与转换操作技巧,需要的朋友可以参考下...

    smartsmile20124602021-11-25
  • C#基于mvc5+ef6+Bootstrap框架实现身份验证和权限管理

    基于mvc5+ef6+Bootstrap框架实现身份验证和权限管理

    最近刚做完一个项目,项目架构师使用mvc5+ef6+Bootstrap,用的是vs2015,数据库是sql server2014。下面小编把mvc5+ef6+Bootstrap项目心得之身份验证和权限管理模块的...

    丰叔叔11652021-11-29
  • C#C#.Net ArrayList的使用方法

    C#.Net ArrayList的使用方法

    这篇文章主要介绍了C#.Net ArrayList的使用方法,使用动态数组的优点是可以根据用户需要,有效利用存储空间,需要的朋友可以参考下...

    weiling8012021-11-01
  • C#聊一聊C# 8.0中的await foreach使用

    聊一聊C# 8.0中的await foreach使用

    这篇文章主要介绍了聊一聊C# 8.0中的await foreach使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    码农阿宇10162022-07-24
  • C#C# WebApi 接口传参详解

    C# WebApi 接口传参详解

    这篇文章主要介绍了C# WebApi 接口传参详解,本篇打算通过get、post、put、delete四种请求方式分别谈谈基础类型(包括int/string/datetime等)、实体、数组等类型...

    懒得安分9872022-02-25
  • C#C#编写的艺术字类实例代码

    C#编写的艺术字类实例代码

    本文给大家分享使用纯C#编写的艺术字类实例代码,代码简单易懂,需要的朋友参考下本教程...

    C#教程网8072021-11-16