双数组trie树详解

双数组trie树的构建

NLP中trie树常用于做快速查询,但普通的trie树由于要保存大量的节点信息,当储存的词量非常大时,不仅所占空间巨大,而且查询的速度也不够快。而双数组trie树就可以比较好的解决这两个问题。

之所以叫双数组trie树,是因为它只用base[]check[]两个数组就将整个trie的信息储存了起来,这两个数组的构建规则是:
b a s e [ i ] + c o d e ( x ) = j c h e c k [ j ] = i \begin{aligned} &base[i] + code(x) = j\\ &check[j] = i \end{aligned} base[i]+code(x)=jcheck[j]=i
其中i,j都是base array的index,base array的长度是trie树中节点的个数,每个节点在base array中都有一个对应的index。

  • base[]数组用于记录跳转结构, base array中index为i的那个节点,如果按照字符x转移,会转移到index为j的节点
  • check[]数组用于 标识出 base array 中每个状态的前一个状态,其主要作用是检验按base做转移的转移正确性
  • code(x)是字符x的编码,现实中为了方便通常直接用char code

构建base array

下面我们看一个例子来学习base array的构建方法:

现在有“清华”、“清华大学”、“清新”、“中华”、“华人”五个词,构成的trie树如下图所示:
在这里插入图片描述
假设例树的字符编码表为:

char
code1234567

初始化root的base index为0,base值记为1。首先看root的所有子节点"清,中,华":

  • base[0] + code(清) = 2,此时位置2空闲,因此“清”放入位置2
  • base[0] + code(中) = 7,此时位置7空闲,因此“中”放入位置7
  • base[0] + code(华) = 3,此时位置3空闲,因此“华”放入位置3

目前base array的情况如下:

position012345678910
charroot
base1
  • 每次遍历完一个节点的所有子节点,只可以确认当前节点的base值,以及它的子节点的index位置
  • 子节点的base值此时会默认继承当前节点的base值,但在遍历子节点的子节点时,一旦有冲突,子节点的base值就会做相应修改

接下来遍历第二层的节点"华,新,华,人":

  • base[2] + code(华) = 3,冲突!因此 base[2] 修改为2;base[2] + code(华) = 4,可用,因此 “清华” 放入位置4
  • base[2] + code(新) = 7,冲突!因此 base[2] 修改为3;base[2] + code(新) = 8,可用,因此 “清新” 放入位置8
  • 因为base[2] 再次北被修改,所以“清华”的位置要重新计算:base[2] + code(华) = 5,可用,因此 “清华” 放入位置5
  • base[7] + code(华) = 3,冲突!因此 base[7] 修改为2;base[7] + code(华) = 4,可用,因此 “中华” 放入位置4
  • base[3] + code(人) = 8,冲突!因此 base[3] 修改为2;base[3] + code(人) = 9,可用,因此 “华人” 放入位置9

目前base array的情况如下:

position012345678910
charroot中华清华清新华人
base1322

接下来遍历第三层的节点"大":

  • base[5] + code(大) = 6,可用;因此 “清华大” 放入位置6

目前base array的情况如下:

position012345678910
charroot中华清华清华大清新华人
base13223

接下来遍历第四层的节点"学":

  • base[6] + code(学) = 7,冲突!base[6] 修改为4;base[6] + code(学) = 8,还是冲突!base[6] 修改为5;base[6] + code(学) = 9,还是冲突!base[6] 修改为6;base[6] + code(学) = 10,可用,因此 “清华大学” 放入位置10

目前base array的情况如下:

position012345678910
charroot中华清华清华大清新华人清华大学
base132623

此时节点已经遍历完,剩余base值未确定的都是尾节点了,因为它们都没用子节点了,所以不存在位置冲突,因此可以直接继承父节点的base值

position012345678910
charroot中华清华清华大清新华人清华大学
base1322362326

构建check array

check array的构建比较简单,只需要将子节点index的check值设为父节点的position值即可,所以有:

position012345678910
charroot中华清华清华大清新华人清华大学
base1322362326
check-2007250236

双数组trie树的查询

此时如果有一个词“清中”,我们要查询它是否在双数组trie里面,那么首先从root出发,由于“清”的code是1,因此会走到 base[0]+code(清) = 2,这里要用check数组检查位置2上的节点其父节点是否是位置0上的节点:check[2] = 0,等式成立!

然后继续看“中”:base[2]+code(中) = 9,再检查位置9上的节点的父节点是否是位置2上的节点:check[9] = 3 !=2 ,检查发现不满足,因此"清中"不在该trie里面。


Reference:
小白详解 Trie 树
double-array-trie双数组trie树原理解析和数据构建过程

  • 10
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
好的,以下是一个简单的基于数组Trie的敏感词过滤算法的Java实现: ```java import java.util.*; public class DoubleArrayTrie { private int[] base; // base数组 private int[] check; // check数组 private int[] fail; // fail指针数组 private boolean[] isWord; // 是否为单词结尾 private int size; // trie中节点数 // 将敏感词列表构建为trie public void buildTrie(List<String> words) { // 计算trie的节点数 size = 1; Map<Character, Integer> charToIndex = new HashMap<>(); charToIndex.put('\0', 0); // 根节点 for (String word : words) { for (char c : word.toCharArray()) { if (!charToIndex.containsKey(c)) { charToIndex.put(c, size++); } } } // 初始化base、check、fail和isWord数组 base = new int[size]; check = new int[size]; fail = new int[size]; isWord = new boolean[size]; // 构建trie Arrays.fill(base, -1); Arrays.fill(check, -1); Arrays.fill(fail, -1); for (String word : words) { int cur = 0; for (char c : word.toCharArray()) { int next = charToIndex.get(c); if (base[cur] == -1) { base[cur] = next; } else { int t = base[cur] + next; int k = -1; while (true) { if (check[t + k] == -1) { check[t + k] = cur; base[next] = t + k; break; } else { k--; } } } cur = base[cur] + next; } isWord[cur] = true; } } // 计算fail指针 public void buildFail() { Queue<Integer> queue = new LinkedList<>(); for (int i = 0; i < 256; i++) { if (base[i] != -1) { fail[base[i]] = 0; queue.offer(base[i]); } } while (!queue.isEmpty()) { int cur = queue.poll(); for (int i = 0; i < 256; i++) { int next = base[cur] + i; if (check[next] == cur) { fail[next] = base[fail[cur]] + i; queue.offer(next); } else if (check[next] == -1) { check[next] = fail[next] = base[fail[cur]] + i; } } isWord[cur] |= isWord[fail[cur]]; // 更新是否为单词结尾 } } // 匹配文本串中的敏感词 public List<String> match(String text) { int cur = 0; List<String> result = new ArrayList<>(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); while (base[cur] == -1 && cur != 0) { // 回溯到可跳转的状态 cur = fail[cur]; } cur = base[cur] + charToIndex.getOrDefault(c, 0); if (isWord[cur]) { // 匹配到了一个敏感词 int start = i - word.length() + 1; result.add(text.substring(start, i + 1)); } } return result; } } ``` 使用示例: ```java List<String> words = Arrays.asList("敏感词1", "敏感词2", "敏感词3"); DoubleArrayTrie trie = new DoubleArrayTrie(); trie.buildTrie(words); trie.buildFail(); List<String> result = trie.match("这是一个包含敏感词1和敏感词2的文本"); System.out.println(result); // ["敏感词1", "敏感词2"] ``` 希望对您有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值