当前位置: 首页 > article >正文

常用数据结构 - 前缀树

应用场景

一般涉及到需要前缀信息来分析问题或者解决问题的时候,就应该考虑一下前缀树。
前缀树是对字符串每个位置上的字符进行编码,记录的是字符本身和字符串的信息,非常适合用于前缀匹配。

数据结构

前缀树的每个节点,是根据字符串的内容记录的路径信息,它有两种实现:

  • 根据字符串中包含的字符种类数 N N N,在每个节点上创建对应的 N 叉树。这样一来,每个节点的后继就可以通过树来轻松找到。另外还需要一个布尔型的变量 e n d end end,来标记是否有字符串在这里结束(或者用整型来统计次数)。这样的做法,可以参见 Leetcode 208.实现 Trie (前缀树)。
  • 字符串的路径信息,可以通过使用二维数组 t r e e tree tree 搭配计数变量 c o u n t count count 来维护。其中第一个下标根据某个字符信息来索引相应的类别信息,第二个下标则表示下一个节点的数据。此外,定义 p a s s pass pass e n d end end 数组来描述经过这个节点(包含到这里为止的前缀)的字符串以及以这个节点为终止位置(包含整个字符串)的字符串类别。

具体实现

树记录信息

class TreeNode {
    public int pass; // pass 表示经过当前节点(包含这个字符)的字符串数量
    public int end; // end 表示以当前节点为止(包含整个字符串)的字符串数量
    public TreeNode[] path; // 数组形式的树

    public TreeNode() {
        pass = 0;
        end = 0;
        path = new TreeNode[26];
    }
}

class Trie {
    private final TreeNode root;

    public Trie() {
        root = new TreeNode();
    }

    // 插入新字符串
    public void insert(String word) {
        TreeNode node = root;
        // 更新包含当前节点的字符串数量
        node.pass++;
        for (int i = 0, cur; i < word.length(); i++) {
            // cur 表示当前节点的下标
            cur = word.charAt(i) - 'a';
            if (node.path[cur] == null) {
                node.path[cur] = new TreeNode();
            }
            // 移动工作指针
            node = node.path[cur];
            node.pass++;
        }
        // 更新以当前节点为止的字符串数量
        node.end++;
    }

    // 统计某个字符串出现的次数
    public int countWords(String word) {
        TreeNode node = root;
        for (int i = 0, cur; i < word.length(); i++) {
            cur = word.charAt(i) - 'a';
            // 到某个节点时发现没有往下走的路径,则返回 0
            if (node.path[cur] == null) {
                return 0;
            }
            // 移动工作指针
            node = node.path[cur];
        }
        // 返回以当前节点为止的字符串数量
        return node.end;
    }

    // 统计以某个字符串为前缀的字符串的数量
    public int countPrefixes(String prefix) {
        TreeNode node = root;
        for (int i = 0, cur; i < prefix.length(); i++) {
            cur = prefix.charAt(i) - 'a';
            // 到某个节点时发现没有往下走的路径,则返回 0
            if (node.path[cur] == null) {
                return 0;
            }
            // 移动工作指针
            node = node.path[cur];
        }
        // 返回包含当前节点的字符串数量
        return node.pass;
    }

    // 删除字符串
    public void delete(String word) {
        // 确保该字符串存在,提高代码的健壮性
        if (countWords(word) > 0) {
            TreeNode node = root;
            // 更新包含该节点的字符串数量
            node.pass--;
            for (int i = 0, cur; i < word.length(); i++) {
                cur = word.charAt(i) - 'a';
                // 一旦更新过后经过下个节点的字符串数量为零,那么将走向它的路径制空
                if (--node.path[cur].pass == 0) {
                    node.path[cur] = null;
                    return;
                }
                // 移动工作指针
                node = node.path[cur];
            }
            // 更新以当前节点为止的字符串数量
            node.end--;
        }
    }
}

二维数组维护

import java.util.Arrays;

public class Trie {

    public static int MAX_N = 100;
    public static int[][] tree = new int[MAX_N][26]; // 二维数组维护路径信息
    public static int[] pass = new int[MAX_N]; // pass 表示经过某个节点(包含某个字符)的字符串数量
    public static int[] end = new int[MAX_N]; // end 表示以某个节点为止(包含某个字符串)的字符串数量
    public static int count; // count 维护存在的字符串种类数

    public Trie() {
        count = 1;
    }

    // 插入新字符串
    public static void insert(String word) {
        // 从类别为 1 的字符串开始访问
        int index = 1;
        // 更新包含该节点的字符串数量
        pass[index]++;
        for (int i = 0, cur; i < word.length(); i++) {
            cur = word.charAt(i) - 'a';
            // 如果这种字符串先前不存在,那么 count 先自增
            if (tree[index][cur] == 0) {
                tree[index][cur] = ++count;
            }
            // 移动工作指针
            index = tree[index][cur];
            pass[index]++;
        }
        // 更新以当前节点为止的字符串数量
        end[index]++;
    }

    // 统计某个字符串出现的次数
    public static int countWords(String word) {
        // 从类别为 1 的字符串开始访问
        int index = 1;
        for (int i = 0, cur; i < word.length(); i++) {
            cur = word.charAt(i) - 'a';
            // 如果这种字符串先前不存在,则返回 0
            if (tree[index][cur] == 0) {
                return 0;
            }
            // 移动工作指针
            index = tree[index][cur];
        }
        // 返回以当前节点为止的字符串数量
        return end[index];
    }

    // 统计以某个字符串为前缀的字符串的数量
    public static int countPrefixes(String pre) {
        // 从类别为 1 的字符串开始访问
        int index = 1;
        for (int i = 0, cur; i < pre.length(); i++) {
            cur = pre.charAt(i) - 'a';
            // 到某个节点时发现没有往下走的路径,则返回 0
            if (tree[index][cur] == 0) {
                return 0;
            }
            // 移动工作指针
            index = tree[index][cur];
        }
        // 返回包含当前节点的字符串数量
        return pass[index];
    }

    // 删除字符串
    public static void delete(String word) {
        // 确保该字符串存在,提高代码的健壮性
        if (countWords(word) > 0) {
            // 从类别为 1 的字符串开始访问
            int index = 1;
            for (int i = 0, cur; i < word.length(); i++) {
                cur = word.charAt(i) - 'a';
                // 一旦更新过后经过下个节点的字符串数量为零,那么将走向它的路径制空
                if (--pass[tree[index][cur]] == 0) {
                    tree[index][cur] = 0;
                    return;
                }
                // 移动工作指针
                index = tree[index][cur];
            }
            // 更新以当前节点为止的字符串数量
            end[index]--;
        }
    }

    // 由于定义的是静态变量,在不同测试用例之间需要清理空间防止出现脏数据
    // 清空二维数组
    public static void clear() {
        for (int i = 1; i <= count; i++) {
            Arrays.fill(tree[i], 0);
            end[i] = 0;
            pass[i] = 0;
        }
    }
}

总结梳理

实际上类定义的前缀树是更常用的选择,用数组来实现看起来很美好,其实需要一些时间来开大数组,更适合竞赛或者手动处理输入的场景。

后记

用 Leetcode 208.实现 Trie (前缀树) 进行测试,类实现能很好地完成目标;而二维数组的实现需要将数组的第一个维度增加到 35000 35000 35000 以上,并且在对象初始化时需要先调用 clear 方法,在这个简单场景下表现不尽如人意。


http://www.kler.cn/a/455938.html

相关文章:

  • 七、队列————相关概念详解
  • “图书馆服务自动化”:基于SSM框架的图书借阅系统开发
  • WebSocket实现直播弹幕滚动推送效果
  • 【环境配置】Jupyter Notebook切换虚拟环境
  • Html——10 关键字和描述
  • CSS基础入门【2】
  • Python爬虫(一)- Requests 安装与基本使用教程
  • [Android]init中添加新的command
  • 高中数学刷题版:函数奇偶性[干货]
  • GaussDB典型SQL调优点之自诊断和语句下推调优
  • 五模型对比!Transformer-GRU、Transformer、CNN-GRU、GRU、CNN五模型多变量时间序列预测
  • Kafka_2.13-3.6.0 常用命令快速指南
  • .net core 的软件开发流程
  • Springboot关于格式化记录
  • java常用类(下)
  • uniapp使用live-pusher实现模拟人脸识别效果
  • 窗口函数-详细讲解分析
  • vue 集成 webrtc-streamer 播放视频流 - 解决阿里云内外网访问视频流问题
  • 面试知识点汇总_02
  • 【STM32】RTT-Studio中HAL库开发教程十:EC800M-4G模块使用