• 前缀树及AC自动机


    前缀树

    前缀树也就是字典树,Trie树

    力扣上就有这么一题让你实现前缀树,咱直接看这题:208. 实现 Trie (前缀树)

    Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

    请你实现 Trie 类:

    Trie() 初始化前缀树对象。
    void insert(String word) 向前缀树中插入字符串 word 。
    boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
    boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。

    1. class Trie {
    2. public Trie() {
    3. }
    4. public void insert(String word) {
    5. }
    6. public boolean search(String word) {
    7. }
    8. public boolean startsWith(String prefix) {
    9. }
    10. }

    我们先来看看前缀树的结构

     这里每个节点下其实是有26个分支的(当然如果不止英文字母的话可以根据自己的需求设计分支的数量,本文中的是只有英文的,也就26个分支),至于 'a'、'b'、'c' 这些字符是不需要存进节点中的,只要在节点中搞一个节点数组nexts,大小为26,那么 nexts[0]~nexts[25] 就分别对应 a~z

    节点:

    1. static class Node {
    2. private boolean isEnd; //用于记录当前节点是否表示插入字符串的尾节点
    3. private Node[] nexts = new Node[26];
    4. }

    代码很好理解,我们直接看代码

    1. class Trie {
    2. static class Node {
    3. private boolean isEnd;
    4. private Node[] nexts = new Node[26];
    5. }
    6. Node root = new Node();
    7. public Trie() {}
    8. // 对于这道题来说,题目已经明确说了 1 <= word.length, prefix.length <= 2000 了,
    9. // 我们就不需要 校验传入参数 word 和 prefix 的合法性了
    10. public void insert(String word) {
    11. char[] str = word.toCharArray();
    12. Node cur = root;
    13. for (char c : str) {
    14. int index = c - 'a';
    15. if (cur.nexts[index] == null) {
    16. cur.nexts[index] = new Node();
    17. }
    18. cur = cur.nexts[index];
    19. }
    20. cur.isEnd = true;
    21. }
    22. public boolean search(String word) {
    23. Node cur = goToLast(word);
    24. //return cur == null ? false : cur.isEnd; 可以下面这样简单点
    25. return cur != null && cur.isEnd;
    26. }
    27. public boolean startsWith(String prefix) {
    28. return goToLast(prefix) != null; // 能沿着prefix走完就必然存在该前缀
    29. }
    30. // 沿着s顺着前缀树往下走
    31. // 返回代表s最后一个字符的节点
    32. public Node goToLast(String s) {
    33. char[] str = s.toCharArray();
    34. Node cur = root;
    35. for (char c : str) {
    36. int index = c - 'a';
    37. // 如果沿着s还没走完就为null了,那直接返回null
    38. if (cur.nexts[index] == null) {
    39. return null;
    40. }
    41. cur = cur.nexts[index];
    42. }
    43. return cur;
    44. }
    45. }

     

    上面那种实现是一个基本的结构,当我们要实现其它一些功能(返回插入过的字符串中出现了几次 word,出现了几次 prefix,删除字符串)的时候就可以用下面这种结构了

    节点:

    1. class Node {
    2. int pass; //用于记录有多少个字符串经过了该节点
    3. int end; //用于记录有多少个字符串以当前节点结尾
    4. Node[] nexts = new Node[26];
    5. }

    代码实现:

    1. class Trie {
    2. static class Node {
    3. int pass; //用于记录有多少个字符串经过了该节点
    4. int end; //用于记录有多少个字符串以当前节点结尾
    5. Node[] nexts = new Node[26];
    6. }
    7. Node root = new Node();
    8. public Trie() {}
    9. public void insert(String word) {
    10. if (word == null || word.length() == 0) {
    11. return;
    12. }
    13. char[] str = word.toCharArray();
    14. Node cur = root;
    15. for (char c : str) {
    16. int index = c - 'a';
    17. if (cur.nexts[index] == null) {
    18. cur.nexts[index] = new Node();
    19. }
    20. cur = cur.nexts[index];
    21. cur.pass++;
    22. }
    23. cur.end++;
    24. }
    25. // 返回以前插入的word里面出现多少次当前要查的word
    26. public int searchPro(String word) {
    27. Node cur = goToLast(word);
    28. return cur == null ? 0 : cur.end;
    29. }
    30. // 返回以前插入的word里面出现多少次prefix前缀
    31. public int startsWithPro(String prefix) {
    32. Node cur = goToLast(prefix);
    33. return cur == null ? 0 : cur.pass;
    34. }
    35. // 沿着前缀树往下走
    36. public Node goToLast(String s) {
    37. if (s == null || s.length() == 0) {
    38. return null;
    39. }
    40. char[] str = s.toCharArray();
    41. Node cur = root;
    42. for (char c : str) {
    43. int index = c - 'a';
    44. if (cur.nexts[index] == null) {
    45. return null;
    46. }
    47. cur = cur.nexts[index];
    48. }
    49. return cur;
    50. }
    51. // 实现删除功能
    52. public void delete(String word) {
    53. if (searchPro(word) == 0) { // 如果都没有 word 的话删个毛啊
    54. return;
    55. }
    56. Node cur = root;
    57. char[] str = word.toCharArray();
    58. for (char c : str) {
    59. int index = c - 'a';
    60. // 如果要去的节点pass减完后都等于0了那就没有必要再走了
    61. if (--cur.nexts[index].pass == 0) {
    62. cur.nexts[index] = null;
    63. return;
    64. }
    65. cur = cur.nexts[index];
    66. }
    67. cur.end--;
    68. }
    69. }

    AC自动机

    作用:一篇大文章 str,一个包含若干敏感词的词典 str[],收集文章中所有出现的敏感词

    建立一个 AC 自动机有两个步骤:

    1. 基础的 Trie 结构:将所有的模式串构成一棵 Trie。
    2. KMP 的思想:对 Trie 树上所有的结点构造失配指针。

    而铅笔部分就是失配指针fail,每个节点都有适配指针,没画的fail指针的节点都是指向root的,通过它就实现了KMP的效果(匹配到后面匹配不上了也能尽量利用前面匹配上的部分)

    接下来我们看代码:

    1. class ACAutomation {
    2. static class Node {
    3. // end 用于一个字符串的末尾节点存放当前字符串
    4. // 路上经过的节点的 end 都是为 null 的
    5. private String end;
    6. private boolean endUse; // 用于记录是否已经收集过该字符串了
    7. private Node fail; // 失配指针
    8. private Node[] nexts = new Node[26];
    9. }
    10. Node root = new Node();
    11. // 插入敏感词
    12. // 在不调整fail指针的情况下先构造好Trie树
    13. public void insert(String s) {
    14. char[] str = s.toCharArray();
    15. Node cur = root;
    16. for (char c : str) {
    17. int index = c - 'a';
    18. if (cur.nexts[index] == null) {
    19. cur.nexts[index] = new Node();
    20. }
    21. cur = cur.nexts[index];
    22. }
    23. cur.end = s;
    24. }
    25. // 设置好fail指针
    26. // 如果对这里不是很理解,可以网上找个动态图看看就明白了,
    27. // 光口头表达对于这个来说确实不好表达清楚,
    28. // 虽然我理解这过程但我现在还不会画动图,感觉对不起大家呀,有空一定学学咋画
    29. public void build() {
    30. LinkedList list = new LinkedList<>(); // 用于层序遍历
    31. list.add(root);
    32. while (!list.isEmpty()) { // 经典的层序遍历
    33. Node cur = list.poll();
    34. // 在当前节点时去设置nexts中节点的fail指针
    35. for (int i = 0; i < 26; i++) {
    36. if (cur.nexts[i] != null) {
    37. // 先让它指向root节点
    38. cur.nexts[i].fail = root;
    39. // 去看 cFail 节点的子节点能否成为 cur.nexts[i]失配指针指向的节点
    40. Node cFail = cur.fail;
    41. while (cFail != null) {
    42. if (cFail.nexts[i] != null) {
    43. cur.nexts[i].fail = cFail.nexts[i];
    44. break;
    45. }
    46. cFail = cFail.fail;
    47. }
    48. list.add(cur.nexts[i]);
    49. }
    50. }
    51. }
    52. }
    53. // 收集content中出现过的敏感词
    54. public List containWords(String content) {
    55. char[] str = content.toCharArray();
    56. ArrayList res = new ArrayList<>();
    57. Node cur = root;
    58. for (char c : str) {
    59. int index = c - 'a';
    60. while (cur.nexts[index] == null && cur != root) {
    61. cur = cur.fail;
    62. }
    63. cur = cur.nexts[index] != null ? cur.nexts[index] : root;
    64. Node follow = cur;
    65. while (follow != root) {
    66. if (follow.endUse) { // 如果已经收集过了就不再收集了
    67. break;
    68. }
    69. if (follow.end != null) {
    70. res.add(follow.end);
    71. follow.endUse = true;
    72. }
    73. follow = follow.fail;
    74. }
    75. }
    76. return res;
    77. }
    78. }

    好了今天的算法就到这了,希望能帮大家复习一下这块知识! 

     

  • 相关阅读:
    window10环境构建和运行skia源码
    【考研数学】概率论与数理统计 —— 第三章 | 二维随机变量及其分布(1,二维连续型和离散型随机变量基本概念与性质)
    RabbitMQ系列【3】安装RabbitMQ
    domino web开发 使用原生的视图
    电容笔哪种好?ipad第三方电容笔推荐
    (WRF/Chem)在大气环境领域实践技术应用
    MySQL——数据库基础
    开发必备的常用 Linux 命令整理
    【算法leetcode】2325. 解密消息(rust和go重拳出击)
    【阿里云】ssl证书到期更新
  • 原文地址:https://blog.csdn.net/qq_61557294/article/details/126769778