数组结构——Trie树

一、什么是 Trie树?

Trie树,也叫字典树,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。

Trie树的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起。如下图。



Trie树的根节点不存数据,每个节点表示一个字符串的字符,从根节点到红色节点一条路径代表一个字符串。

二、如何实现一棵 Trie树。

1、存储 Trie树

我们假设存储的字符都是a~z,我们可以用一个数组大小26来存储字符,每一层每个字符都有26个子节点,数组的下标为存入下标的字符ASCII码值-a的ASCII码值,这样我们可以通过下标就可以知道该节点存储的是哪个字符。代码表示如下:

class TrieNode{
  char data;
  TrieNode children[26];
}
2、将字符串集合构造成 Trie 树

构造 Trie树,相当于把每个字符串插入到Trie树中。代码实现如下:

public class Trie {
    private TrieNode root;

    public Trie() {
        // 树顶存储无意义字符
        this.root = new TrieNode('/');
    }

    /**
     * 插入字符串
     *
     * @param text 字符串
     */
    public void insert(char[] text) {
        TrieNode p = root;
        int n = text.length;
        for (int i = 0; i < n; i++) {
            // 字符应该存储的数组下标
            int idx = text[i] - 'a';
            // 该字符在Trie树中不存在,将该字符插入到Trie树中
            if (p.children[idx] == null) {
                TrieNode newNode = new TrieNode(text[i]);
                p.children[idx] = newNode;
            }
            // 继续存储下一个字符
            p = p.children[idx];
        }
        // 存储完成后,修改结尾字符标识
        p.isEndingChar = true;
    }
}

class TrieNode {
    public char data;
    public TrieNode[] children;
    public boolean isEndingChar;

    public TrieNode(char data) {
        this.data = data;
        this.children = new TrieNode[26];
        // 是否是字符串结尾字符
        this.isEndingChar = false;
    }
}

构建Trie树的过程,需要扫描所有的字符串,时间复杂度是O(n),n表示所有字符串的长度和。

3、Tire树匹配字符串

在 Trie 树中,查找某个字符串,用代码实现如下:

    /**
     * 在 Trie 树中查找一个字符串
     * @param pattern 模式串
     * @return 是否存在
     */
    public boolean find(char[] pattern) {
        TrieNode p = root;
        for (int i = 0; i < pattern.length; i++) {
            int index = pattern[i] - 'a';
            // 不存在 pattern
            if (p.children[index] == null) {
                return false;
            }
            // 存在继续往下找
            p = p.children[index];
        }
        // ture 代表完全匹配,false 代表不能完全匹配,只是前缀
        return p.isEndingChar;
    }

从代码中可以看出Tire树的查找字符串,的时间复杂度为O(n),n代表要查找的字符串的长度。如果不考虑构建tire树的操作,查询字符串,还是比较高效的。

三、Tire 树的优缺点和应场景

1、Tire树是非常耗内存的,拿空间换时间;如果字符串前缀重合不多,会导致空间消耗变大。
2、字符串包含的字符集不能太大。如果字符集太大,那存储空间可能会浪费很多。即便可以优化,但也要付出牺牲查询、插入的代价。
3、如果要用Trie树解决问题,要从零开始实现一个Trie树,还要保证没有bug,这个工程上是将简单问题复杂化,除非必须,一般不建议这样做。
4、Trie树中用到了指针,因为指针串起来的数据块是不连续的,所以对缓存并不友好,性能上会打个折扣。

综合以上几点,针对在一组字符串中查找字符串的问题,用散列表或者红黑树比较好。因为这两种数据结构,编程语言中现成类库都有,不需要自己实现。

Trie 树不适合精确匹配查找,它比较适合查找前缀匹配的字符串;比如搜索关键词的提示功能,我们用代码实现如下:

/**
     * 查找匹配前缀所有字符串
     *
     * @param pattern 前缀
     * @return 字符串集合
     */
    public List<String> findPrefix(char[] pattern) {
        TrieNode p = root;
        // 找出前缀的结尾字符所在节点
        for (int i = 0; i < pattern.length; i++) {
            int idx = pattern[i] - 'a';
            // 不存在,说明没有这个前缀的字符串
            if (p.children[idx] == null) {
                return null;
            }
            p = p.children[idx];
        }
        // 遍历节点p,把前缀是pattern都找出来
        List<String> res = new ArrayList<String>();
        iterateTrie(p, String.valueOf(pattern), res);
        return res;
    }

    // 遍历
    private void iterateTrie(TrieNode node, String str, List<String> res) {
        TrieNode[] children = node.children;
        // 遍历子节点
        for (TrieNode child : children) {
            // 如果当前子节点为空,直接跳到下一个子节点
            if (child == null) continue;
            // 拼接字符串
            String temp = str + child.data;
            // 如果当前子节点是结尾字符,把temp添加到res中
            if (child.isEndingChar) {
                res.add(temp);
            }
            // 继续往下递归
            iterateTrie(child, temp, res);
        }
    }

四、扩展—经典多模式串匹配算法:AC自动机

网上有很多发表内容的网站,对于用户输入一些淫秽、反动、谩骂等敏感内容,会对进行过滤。
我们现在来学习一下,如何实现一个高性能的敏感词过滤系统呢?这里就需要用到多模式串匹配算法。

1、多模式串与单模式串匹配算法的区别

单模式匹配算法,是在一个模式串和主串之间进行匹配,也就是说,在一个主串中查找一个模式串。用单模式串算法来实现多模式串的匹配,要让每个模式串跟主串进行匹配,这会导致主串会被扫描多次;当主串特别长、模式串也很多时,这种处理就比较低效。

多模式串匹配算法,就是在多个模式串和一个主串之间做匹配,也就是上说,在一个主串中查找多个模式串。实现多模式串的匹配,它只需要扫描一遍主串,就能在主串中一次性查找多个模式串是否存在,大大提高了匹配效率。

2、AC自动机

AC自动机算法,全称是Aho-Corasick算法;AC自动机借鉴了单模式串的优化改进方法,对多模式串Trie树进行改进,进一步提高Trie树的效率;AC自动机实际上就是在Trie树之上,加了类似KMP的next数组,只不过此处的next数组是构建在树上而已。 用代码表示如下:

public class AcNode{
  public char data;
  public AcNode[] children;
  // 失败指针
  public AcNode fail;
  // 当isEndingChar = true,记录模式串长度
  public int length;
  // 是否是结尾字符
  public boolean isEndingChar;
  public AcNode(char data){
    this.data = data;
    // 字符集只包含a~z 这26个字符
    this.children = new AcNode[26];
    this.length = -1;
    this.isEndingChar = false;
  }
}

AC自动机的构建包括两个操作:
1、将多个模式串构建成Trie树
代码实现如下:

public class AcTire{
  private AcNode root;
  public Tire(){
    // 树顶存储无意义
    this.root = new AcNode('/');
  }
  // 字符串插入
  public void insert(char[] text){
    AcNode p = root;
    int n = text.length;
    for(int i = 0; i < n; i++){
      // 当前字符的下标
      int idx = text[i] - 'a';
      // 在Trie树中不存在,新增节点添加到树中
      if(p.children[idx] == null){
        AcNode newNode = new AcNode(text[i]);
        p.children[idx] = newNode;
      }
      // 继续下一个字符匹配
      p = p.children[idx] 
    }
    // 插入完成,记录结尾字符下标和字符串长度
    p.isEndingChar = true;
    p.length = n;
  } 
}

2、在Trie树上构建失败指针(相当于KMP中的失效函数next数组)

这个失败指针的作用是什么?比如一个主串为:abce; 模式串为:abcd,bcd,bc,c;我们把主串字符匹配,发现匹配到第一个模式串abcd的最后一个字符匹配失败了,此时我们知道abcd模式串已经匹配不上了;为了提高效率,我们把已经能匹配上的字符串,跟其它其它模式串能不能匹配上;这时我知道abc是已经匹配上的字符串,我们把abc的后缀(bc、c)去跟其它模式串匹配,可以匹配到模式串bc和c;这时我们可以取最长可匹配后缀子串 bc 就可以了(因为c已经包含在bc中了,如果bc没有匹配上,那么就取c),那么字符串abc中的节点c的失败指针指向模式串 bc。如下图

通过上面的分析,主串匹配模式串,关键就是找到模式串每个节点的失败指针;

现在我们来构建Trie树的失败指针,这个过程跟KMP算法中next数组的构建过程很相似,我们假设节点p的失败指针指向节点 q,我们看节点p的子节点pc对应的字符,是否也可以在节点q的子节点找到。如果找到了节点 q 的子节点 qc,对应字符跟节点 pc 对应的字符相同 ,则将节点pc的失败指针指向节点qc;如果没有找到,我们再找q的失败指针子节点的字符跟节点 pc 对应的字符相同;就这样继续下去,如果都没有找到,我们就把pc节点的失败节点指向根节点;根节点的失败指针对应为 null; 如下图

整个实现代码如下:

 // 构建失败指针
    public void getFail() {
        // 使用队列来遍历每个节点
        Queue<AcNode> queue = new LinkedList<>();
        root.fail = null;
        queue.add(root);
        while (!queue.isEmpty()) {
            AcNode p = queue.remove();
            for (AcNode child : p.children) {
                // 当前子节点为空,直接跳到下一个子节点;
                if (child == null) continue;
                // 父节点是根节点
                if (p == root) {
                    child.fail = root;
                } else {
                    // 比较child字符
                    int idx = child.data - 'a';
                    AcNode pFail = p.fail;
                    while (pFail != null) {
                        // 如果相等
                        if (pFail.children[idx] != null) {
                            child.fail = pFail.children[idx];
                            break;
                        }
                        pFail = pFail.fail;
                    }
                    // 如果没找到
                    if (pFail == null) {
                        child.fail = root;
                    }
                }
                // 将子节点加入到队列中
                queue.add(child);
            }
        }
    }

// 匹配字符串
public void match(char[] text){
  
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容