手写前缀树

前言:

        还记得之前遇到过一些问题,比如一篇文章单词出现过多少次?一篇文章 每个单词前缀为**的出现几次? 一篇文章  ** 单词段出现过几次,看完前缀树都有了答案。

思路

        1 构建一棵树 需要记录每个节点的  pass 经过数量  end 单词末尾是这个字母的数量

        2 node节点上 需要有个一 数组  26 大小 用于存放这个节点的叶子节点

        3 添加单词的时候 需要对经过的节点pass +1 end +1 如果没有节点需要新建节点

        4 删除的时候 pass -1 end -1 如果pass = 0 则要删除节点

方法设计

 1)void insert(String str)           添加某个字符串,可以重复添加,每次算1个

3)  void delete(String str)           删掉某个字符串,可以重复删除,每次算1个

2)int search(String str)             查询单词 某个字符串在结构中还有几个

4)int prefixNumber(String str)  查询字符串前缀,是以str做前缀的有多少个

4)int prefixNumber(String str)  查询有多少个字符串片段 str  例如  hello 包含 ll 计数1

时间复杂度

       1 添加 时间复杂度为 O(n*k) 这里 n 是单词的个数  k 是单词的长度 其实因为单词不是很长也可以忽略成O(n)

       2 删除 时间复杂度为 O(n*k) 这里 n 是单词的个数  k 是单词的长度 其实因为单词不是很长也可以忽略成O(n) 删除一个就可以当做O(1)

       3 查询单词 O(k) 因为只要循环字符串每个字符就能找到

         4 同3

        5 这个需要遍历 前缀树的每个节点 O(n) 这里的n是前缀树的节点数 还要O(p)p代表需要字符串首字母在单词不同位置出现的次数, 判断以这个字符串开头的节点是否后面都能满足这个字符串,把满足的数量相加。时间复杂度 n * p * k  因为p 最多等于k 而 k又是单词长度 相对不会超过10一般  所以时间复杂度 O(n)

代码

package 算法;

import com.sun.tools.javac.util.StringUtils;

import java.util.*;

public class 前缀树 {

    /**
     * 前缀树节点
     */
    public static class Node{
        //记录经过这个节点的数量
        public int pass;
        //记录这个节点是文字结束的结束的数量
        public int end;
        //走向26个字母的路
        public Node[] nodes;
        public Node() {
            nodes = new Node[26];
        }
    }
    /**
     * 前缀树
     */
    public static class tree{
        private Node nodeTree;//前缀树根节点
        //添加某个字符串,可以重复添加,每次算1个

        public tree() {
            this.nodeTree = new Node();
        }

        public void insert(String str){
            if(str.isEmpty()){
                return;
            }
            //拆分字符串
            char[] chars = str.toCharArray();
            //拿到树的引用
            Node node = nodeTree;
            //循环每一个字符放入到前缀树中
            for (int i = 0; i < chars.length; i++) {
               //大小写范围 a=97z=122A=65Z=90
                //这里默认所有字母小写 计算出 对应数组的下标
                int subscript = chars[i]-'a';
                //如果对应字母为空就添加一个
                if(node.nodes[subscript]==null){
                    node.nodes[subscript] = new Node();
                };
                //跳到对应node节点
                node = node.nodes[subscript];
                //经过的++
                node.pass++;
            }
            node.end++;
        }
        //删掉某个字符串,可以重复删除,每次算1个
        public void delete(String str){
            if(search(str)==0){
                return;
            }
            if(str.isEmpty()){
                return;
            }
            //拆分成char
            char[] chars = str.toCharArray();
            //拿到node的引用
            Node node = nodeTree;
            for (int i = 0; i < chars.length; i++) {
                //大小写范围 a=97z=122A=65Z=90
                //这里默认所有字母小写 计算出 对应数组的下标
                int subscript = chars[i]-'a';
                //减去经过的值  如果减去完了等于0 直接 删掉

                if(--node.nodes[subscript].pass==0){
                    node.nodes[subscript] = null;
                    return;
                }
                node = node.nodes[subscript];
            }
            node.end--;
        }
        //查询某个字符串在结构中还有几个 就是某个单词出现过几次
        public int search(String str) {
            Node node = nodeTree;
            char[] chars = str.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                int subscript = chars[i]-'a';
                node = node.nodes[subscript];
                if (node == null) {
                    return 0;
                }
            }
            return node.end;
        }

        // 查询有多少个字符串,是以str做前缀的 就是前缀在之前出现过几次
        public int prefixNumber(String str){
            Node node = nodeTree;
            char[] chars = str.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                int subscript = chars[i]-'a';
                node = node.nodes[subscript];
                if (node == null) {
                    return 0;
                }
            }
            return node.pass;
        }


        /**
         * 查询某个字符串段落 在之前 字符串中包含的有几个  例如  hello  hi  name  family  amy  这几个 包含 am的  单词有3个
         * 思路
         *  1 遍历每一层的树找有 字母a开头的节点 放到队列
         *  2 遍历 队列中的a开头的看是否符合am 然后当到m的点 累计 pass 的数量
         * @param str
         * @return
         */
        public int searchStr(String str) {
            Node node = nodeTree;
            char[] chars = str.toCharArray();

            //1 先遍历整个树找到a开头的 放到队列里
            Queue<Node> nodeQueue = new LinkedList<>();
            Queue<Node> nodeStrQueue = new LinkedList<>();
            nodeQueue.add(node);
            int headWord = chars[0]-'a';

            while (nodeQueue.peek()!=null){
                Node node1 = nodeQueue.poll();
                for (int i = 0; i < 26; i++) {
                    //将不为空的node加入队列 继续遍历寻找 等于第一个字母的
                    if (node1.nodes[i]!=null) {
                        //判断是否等于第一个字母
                        if (i == headWord) {
                            //放到是第一个字母的队列,等待遍历查找有多少符合的
                            nodeStrQueue.add(node1.nodes[i]);
                        }
                        //放到查找队列里面继续找 第一个字母开头的node
                        nodeQueue.add(node1.nodes[i]);
                    }
                }
            }
            //2 看第一个字母开头的是否都符合下面的字母
            int num = 0;//累加的字母
            //从第二个字母开始找
            while (nodeStrQueue.isEmpty()){
                Node node1 = nodeStrQueue.poll();
                if(node1==null){

                }
                for (int i = 1; i < chars.length; i++) {
                    int subscript = chars[i] - 'a';
                    //找是否有下个字符
                    node1 = node1.nodes[subscript];
                    //没有就停止查找 退出这个node 对char数组的字符段查找
                    if (node1 == null) {
                       break;
                    }
                    //如果当前找的是最后一个字母节点并且找到了
                    if (i == chars.length-1) {
                        //累计当前节点出现的次数
                        num+=node1.pass;
                    }
                }
            }
            return num;
        }

    }
    // =========================用于前缀树测试代码
    // for test
    public static String generateRandomString(int strLen) {
        char[] ans = new char[(int) (Math.random() * strLen) + 1];
        for (int i = 0; i < ans.length; i++) {
            int value = (int) (Math.random() * 6);
            ans[i] = (char) (97 + value);
        }
        return String.valueOf(ans);
    }

    // for test
    public static String[] generateRandomStringArray(int arrLen, int strLen) {
        String[] ans = new String[(int) (Math.random() * arrLen) + 1];
        for (int i = 0; i < ans.length; i++) {
            ans[i] = generateRandomString(strLen);
        }
        return ans;
    }
    public static class Right {

        private HashMap<String, Integer> box;

        public Right() {
            box = new HashMap<>();
        }

        public void insert(String word) {
            if (!box.containsKey(word)) {
                box.put(word, 1);
            } else {
                box.put(word, box.get(word) + 1);
            }
        }

        public void delete(String word) {
            if (box.containsKey(word)) {
                if (box.get(word) == 1) {
                    box.remove(word);
                } else {
                    box.put(word, box.get(word) - 1);
                }
            }
        }

        public int search(String word) {
            if (!box.containsKey(word)) {
                return 0;
            } else {
                return box.get(word);
            }
        }

        public int prefixNumber(String pre) {
            int count = 0;
            for (String cur : box.keySet()) {
                if (cur.startsWith(pre)) {
                    count += box.get(cur);
                }
            }
            return count;
        }
    }
    // =========================用于前缀树测试代码
    public static void main(String[] args) {

        //自己测试 添加删除
        tree trie = new tree();
        trie.insert("aaa");
        trie.insert("bbb");
        trie.insert("cc");

        trie.delete("aaa");
        trie.delete("bbb");
        trie.delete("cc");

        //查找 整个单次
        trie.insert("hi");
        trie.insert("hi");
        trie.insert("hello");
        trie.insert("mama");
        System.out.println("hi出现过:"+trie.search("hi"));
        System.out.println("hello出现过:"+trie.search("hello"));
        System.out.println("ma:"+trie.search("ma"));

        //查找 前缀
        System.out.println("ll出现过:"+trie.search("ll"));
        System.out.println("h出现过:"+trie.search("h"));
        System.out.println("hi出现过:"+trie.search("hi"));
        System.out.println("ma出现过:"+trie.search("ma"));

        //查找段落
        trie.insert("myhi");
        trie.insert("ahid");
        System.out.println("hi出现过:"+trie.searchStr("hi"));
        System.out.println("ll出现过:"+trie.searchStr("ll"));








        /***
         * 视频中的对数器
         */
        int arrLen = 100;
        int strLen = 20;
        int testTimes = 100000;
        for (int i = 0; i < testTimes; i++) {
            String[] arr = generateRandomStringArray(arrLen, strLen);
            tree trie1 = new tree();
            Right right = new Right();
            for (int j = 0; j < arr.length; j++) {
                double decide = Math.random();
                if (decide < 0.25) {
                    trie1.insert(arr[j]);
                    right.insert(arr[j]);
                } else if (decide < 0.5) {
                    trie1.delete(arr[j]);
                    right.delete(arr[j]);
                } else if (decide < 0.75) {
                    int ans1 = trie1.search(arr[j]);
                    int ans3 = right.search(arr[j]);
                    if (ans1 !=  ans3) {
                        System.out.println("Oops!");
                    }
                } else {
                    int ans1 = trie1.prefixNumber(arr[j]);
                    int ans3 = right.prefixNumber(arr[j]);
                    if (ans1 != ans3) {
                        System.out.println("Oops!");
                    }
                }
            }
        }
        System.out.println("finish!");

    }
}

おすすめ

転載: blog.csdn.net/u010191034/article/details/121038980
おすすめ