1. 程式人生 > 程式設計 >C#實現前向最大匹、字典樹(分詞、檢索)的示例程式碼

C#實現前向最大匹、字典樹(分詞、檢索)的示例程式碼

  場景:現在有一個錯詞庫,維護的是錯詞和正確詞對應關係。比如:錯詞“我門”對應的正確詞“我們”。然後在使用者輸入的文字進行錯詞校驗,需要判斷輸入的文字是否有錯詞,並找出錯詞以便提醒使用者,並且可以顯示出正確詞以便使用者確認,如果是錯詞就進行替換。

  首先想到的就是取出錯詞List放在記憶體中,當用戶輸入完成後用錯詞List來foreach每個錯詞,然後查詢輸入的字串中是否包含錯詞。這是一種有效的方法,並且能夠實現。問題是錯詞的數量比較多,目前有10多萬條,將來也會不斷更新擴充套件。所以pass了這種方案,為了讓錯詞查詢提高速度就用了字典樹來儲存錯詞。

字典樹

  Trie樹,即字典樹,又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較。

Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。

通常字典樹的查詢時間複雜度是O(logL),L是字串的長度。所以效率還是比較高的。而我們上面說的foreach迴圈則時間複雜度為O(n),根據時間複雜度來看,字典樹效率應該是可行方案。

C#實現前向最大匹、字典樹(分詞、檢索)的示例程式碼

字典樹原理

  根節點不包含字元,除根節點外每一個節點都只包含一個字元; 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串; 每個節點的所有子節點包含的字元都不相同。

  比如現在有錯詞:“我門”、“旱睡”、“旱起”。那麼字典樹如下圖

C#實現前向最大匹、字典樹(分詞、檢索)的示例程式碼

  其中紅色的點就表示詞結束節點,也就是從根節點往下連線成我們的詞。

  實現字典樹:

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。

  這種匹配方式還有後向最大匹配以及雙向匹配,這個大家可以去了解下。

  實現前向最大匹配,這裡後向最大匹配也可以一起實現。

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# 前向最大匹、字典樹內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!