边刷题边理解-前缀树

242 阅读2分钟

0.【LeetCode】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

前缀树的应用:

自动补全

拼写检查

词频统计

前缀树的实现

/**
 * @author SJ
 * @date 2021/4/2
 */
public class Trie {

    public TrieNode root;//树包含根节点

    //新建前缀树节点
    public static class TrieNode {

        public TrieNode[] children = new TrieNode[26]; //指向子节点
        public int freq = 1;//以该节点为end的单词出现的频次
        public boolean isEnd = false;//是否是终点

        //无参构造,根节点
        public TrieNode() {
        }

        //看子节点是否包含
        public boolean containsKey(char ch) {
            return this.children[ch - 'a'] != null;
        }

        //插入该字符
        public void putKey(TrieNode node, char ch) {
            this.children[ch - 'a'] = node;
        }

        //通过字符拿到该节点
        public TrieNode getNode(char ch) {
            return this.children[ch - 'a'];
        }

    }

    //初始化前缀树
    public Trie() {
        root = new TrieNode();
    }

    //向前缀树中插入一个单词
    public void insert(String word) {
        TrieNode curNode = root;//拿到当前树的根节点
        for (int i = 0; i < word.length(); i++) {
            char curChar = word.charAt(i);
            //如果当前树的子树里没有这个字符则插入
            if (!curNode.containsKey(curChar)) {

                curNode.putKey(new TrieNode(), curChar);
            }
            curNode = curNode.getNode(curChar);
        }
        curNode.isEnd = true;
        cueNode.freq++;

    }

    //返回该前缀的最终节点,若不存在该前缀则返回空
    public TrieNode searchPrefix(String word) {
        TrieNode curNode = root;
        for (int i = 0; i < word.length(); i++) {
            char curChar = word.charAt(i);
            if (curNode.containsKey(curChar)) {
                curNode = curNode.getNode(curChar);
            } else {
                return null;
            }
        }
        return curNode;
    }

    //判断一个单词是否在前缀树中
    public boolean search(String word) {
        TrieNode curNode = searchPrefix(word);

        return curNode != null && curNode.isEnd;
    }


    //返回是否存在该前缀
    public boolean startsWith(String prefix) {
        TrieNode curNode = searchPrefix(prefix);
        return curNode != null;
    }

    //单词出现的频率统计
    public int getfreq(String word) {
        TrieNode curNode = searchPrefix(word);
        if (curNode == null || !curNode.isEnd)
            return 0;
        else
            return curNode.freq;

    }

    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("apple");
        trie.insert("apple");
        System.out.println(trie.search("apple"));   // 返回 True
        System.out.println(trie.search("app"));    // 返回 False
        System.out.println(trie.startsWith("app")); // 返回 True
        trie.insert("app");
        System.out.println(trie.search("app"));     // 返回 True
        System.out.println(trie.getfreq("apple"));//返回2


    }
}

总结:

前缀树节点:

属性:(没有data属性,data属性用隐含的下标表示,例如:children[0]不为空,则包含一个值为'a'的子节点)

  • 子节点数组【大小为26,查找时采用下标索引】
  • 节点出现的频次
  • 是否终点

方法:

  • 子节点数组是否包含该值为该节点的节点
  • 向子节点数组中插入某一节点
  • 通过字符值拿到当前节点所在的子节点

前缀树:

属性:

  • 类型为前缀树节点的根节点

方法:

  • 构造一个前缀树
  • 向前缀树中插入一个单词
  • 判断该前缀树种是否存在某一前缀,若存在,则返回该前缀最后一个节点的位置
  • 判断该前缀树种是否存在某一单词
  • 统计某一单词出现的次数

前缀树的应用例题

1.【LeetCode】面试题 16.02. 单词频率

设计一个方法,找出任意指定单词在一本书中的出现频率。

你的实现应该支持如下操作:

  • WordsFrequency(book)构造函数,参数为字符串数组构成的一本书
  • get(word)查询指定单词在书中出现的频率

示例:

WordsFrequency wordsFrequency = new WordsFrequency({"i", "have", "an", "apple", "he", "have", "a", "pen"});
wordsFrequency.get("you"); //返回0,"you"没有出现过
wordsFrequency.get("have"); //返回2,"have"出现2次
wordsFrequency.get("an"); //返回1
wordsFrequency.get("apple"); //返回1
wordsFrequency.get("pen"); //返回1

提示:

  • book[i]中只包含小写字母
  • 1 <= book.length <= 100000
  • 1 <= book[i].length <= 10
  • get函数的调用次数不会超过100000

代码:直接套用上面已经实现的前缀树

/**
 * @author SJ
 * @date 2021/4/2
 */
public class WordsFrequency {
    public TrieNode root;//树包含根节点

    //新建前缀树节点
    public static class TrieNode {

        public TrieNode[] children = new TrieNode[26]; //指向子节点
        public int freq = 0;//节点的频次
        public boolean isEnd = false;//是否是终点

        //无参构造,根节点
        public TrieNode() {
        }

        //看子节点是否包含
        public boolean containsKey(char ch) {
            return this.children[ch - 'a'] != null;
        }

        //插入该字符
        public void putKey(TrieNode node, char ch) {
            this.children[ch - 'a'] = node;
        }

        //通过字符拿到该节点
        public TrieNode getNode(char ch) {
            return this.children[ch - 'a'];
        }

    }



    //向前缀树中插入一个单词
    public void insert(String word) {
        TrieNode curNode = root;//拿到当前树的根节点
        for (int i = 0; i < word.length(); i++) {
            char curChar = word.charAt(i);
            //如果当前树的子树里没有这个字符则插入
            //若存在且该节点为末尾节点。单词频次加1
            if (!curNode.containsKey(curChar)) {

                curNode.putKey(new TrieNode(), curChar);
            }
            curNode = curNode.getNode(curChar);
        }
        curNode.isEnd = true;
        curNode.freq++;

    }

    //返回该前缀的最终节点,若不存在该前缀则返回空
    public TrieNode searchPrefix(String word) {
        TrieNode curNode = root;
        for (int i = 0; i < word.length(); i++) {
            char curChar = word.charAt(i);
            if (curNode.containsKey(curChar)) {
                curNode = curNode.getNode(curChar);
            } else {
                return null;
            }
        }
        return curNode;
    }


    //单词出现的频率统计
    public int getfreq(String word) {
        TrieNode curNode = searchPrefix(word);
        if (curNode == null || !curNode.isEnd)
            return 0;
        else
            return curNode.freq;

    }
    public WordsFrequency(String[] book) {
        root=new TrieNode();
        for (int i = 0; i < book.length; i++) {
            insert(book[i]);
        }
    }
    public void insertBook(String[] book){

        for (int i = 0; i < book.length; i++) {
            insert(book[i]);
        }
    }

    public int get(String word) {
        return this.getfreq(word);
    }

    public static void main(String[] args) {
        String[] book={"o","op","o"};
        WordsFrequency wordsFrequency = new WordsFrequency(book);
        int o = wordsFrequency.getfreq("op");
        System.out.println(o);
    }
}

为了完成单词搜索2,我们先完成单词搜索1

2.【LeetCode】79. 单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false

思路很简单,找到单词首字母所在棋盘内的位置,以该首字母为起点进行深度优先搜索,一旦找到单词了单词的最后一个字母就在下一轮直接跳出。


/**
 * @author SJ
 * @date 2021/4/3
 */
public class SearchWords {
    public static boolean exist(char[][] board, String word) {
        flag=false;
        int X=board[0].length;//棋盘宽度
        int Y=board.length;//棋盘高度
        boolean[][] visited=new boolean[Y][X];

        for (int i = 0; i <Y ; i++) {
            for (int j = 0; j < X; j++) {
                if (board[i][j]==word.charAt(0)){
                    //visited每一遍都要更新
                    for (int m = 0; m < Y; m++) {
                        for (int n = 0; n < X; n++) {
                            visited[m][n]=false;
                        }
                    }
                    visited[i][j]=true;
                    if (word.length()==1)
                        return true;
                   else if (search(board,i,j,word,1,visited))
                       return true;
                }
            }

        }
        return false;

    }

    static boolean flag;

    //从坐标为(i,j)的位置开始搜索board,当前搜索到了work中下标为k的位置
    public static boolean search(char[][] board,int i,int j,String word,int k,boolean[][] visited){

        if (k==word.length()){
            flag=true;
            return true;
        }

        //向左找
        if (j-1>=0&&!visited[i][j-1]&&word.charAt(k)==board[i][j-1]){
            visited[i][j-1]=true;
            search(board,i,j-1,word,k+1,visited);
            if (flag)
                return true;
            else
              visited[i][j-1]=false;
        }
        //向上找
        if (i-1>=0&&!visited[i-1][j]&&word.charAt(k)==board[i-1][j]){
            visited[i-1][j]=true;
            search(board,i-1,j,word,k+1,visited);
            if (flag)
                return true;
            else
                visited[i-1][j]=false;
        }
        //向下找
        if (i+1<board.length&&!visited[i+1][j]&&word.charAt(k)==board[i+1][j]){
            visited[i+1][j]=true;
            search(board,i+1,j,word,k+1,visited);
            if (flag)
                return true;
            else
                visited[i+1][j]=false;
        }
        //向右找
        if (j+1<board[0].length&&!visited[i][j+1]&&word.charAt(k)==board[i][j+1]){
            visited[i][j+1]=true;
            search(board,i,j+1,word,k+1,visited);
            if (flag)
                return true;
            else
                visited[i][j+1]=false;
        }

        return false;
    }

    public static void main(String[] args) {
        char[][] board={{'C','A','A'},{'A','A','A'},{'B','C','D'}};
        String word="AAB";
        boolean exist = exist(board, word);
        System.out.println(exist);
    }

}

3.【LeetCode】212. 单词搜索 II

给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。

单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

示例 1:

img

输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]

思路:

  • 根据给定字典构造前缀树,先构造前缀树节点
    • 记录子节点数组,便于快速定位
    • 记录子节点数量,便于确定叶子节点,过程中进行剪枝
    • 是否单词终点,这个不用多说
    • 父节点,便于回溯到根打印单词
    • 节点的值,便于回溯打印单词
  • 遍历网格,如果当前网格字符存在于前缀树的第一层节点中(默认第0层位根),则将该子树拿出,对其进行深度优先搜索。
  • 网格搜索与树的遍历同时进行。假设当前搜索到树的第3层。网格有四个方向,当且仅当该方向的字符存在于前缀树第三层,网格才继续向这个方向进行。
  • 对于前缀树来说,每一层搜索都要判断当前节点是不是end节点,若是,则证明搜索到一个单词,将该单词放入结果集中,并对前缀树进行清理。

注:前缀树是你自己定义的,你想要什么属性就加什么属性,想要什么功能就自己写,我们只是参考这个数据结构的思路而已,不要被网上的条条框框框住了。

import java.util.ArrayList;
import java.util.List;

/**
 * @author SJ
 * @date 2021/4/3
 */
public class SearchWords2 {
    //前缀树节点
    public static class TrieNode {
        //子节点
        TrieNode[] children = new TrieNode[26];
        int childrenNum = 0;
        //是否终点
        boolean isEnd = false;
        //parent
        TrieNode fatherNode;
        //数据
        Character data;

        //子节点是否包含某一字母
        public boolean contain(char ch) {
            return children[ch - 'a'] != null;
        }

        //拿到该节点
        public TrieNode getKey(char ch) {
            return children[ch - 'a'];
        }

        //插入该节点
        public void putKey(TrieNode node, char ch) {
            node.fatherNode = this;
            node.data = ch;
            this.children[ch - 'a'] = node;
            this.childrenNum++;
        }

        //设为终点
        public void setEnd() {
            this.isEnd = true;
        }

        //删除该节点
        public void deleteNode() {
            if (this.fatherNode != null) {
                TrieNode node = this.fatherNode;
                node.children[this.data - 'a'] = null;
            }

        }
    }

    //树根
    public TrieNode root;

    //插入一个单词
    public void insert(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            if (!node.contain(word.charAt(i)))
                node.putKey(new TrieNode(), word.charAt(i));
            node = node.getKey(word.charAt(i));
        }
        node.setEnd();

    }

    //插入一组单词
    public void insertDic(String[] words) {
        for (String word : words) {
            insert(word);
        }
    }

    //返回当前节点到根节点之间的单词,并对当前的前缀树进行请理
    public String outPutWord(TrieNode node) {
        StringBuilder stringBuilder = new StringBuilder();

        node.isEnd = false;
        while (node.data != null) {
            stringBuilder.insert(0, node.data);
            TrieNode temp = node;

            node = node.fatherNode;

            //在这里对前缀树进行请理,如果是叶子节点,就直接删除,如果非叶子节点,就将isEnd置为false
            if (temp.childrenNum == 0 && !temp.isEnd) {
                temp.deleteNode();
                node.childrenNum--;
            }

        }
        return stringBuilder.toString();
    }


    static List<String> ans;

    public List<String> findWords(char[][] board, String[] words) {
        //构造前缀树
        root = new TrieNode();
        insertDic(words);

        //返回数组
        ans = new ArrayList<>();

        int X = board[0].length;//棋盘宽度
        int Y = board.length;//棋盘高度
        boolean[][] visited = new boolean[Y][X];

        for (int i = 0; i < Y; i++) {
            for (int j = 0; j < X; j++) {

                if (root.contain(board[i][j])) {
                    //每次都要初始化visited数组
                    for (int m = 0; m < Y; m++) {
                        for (int n = 0; n < X; n++) {
                            visited[m][n] = false;
                        }
                    }
                    visited[i][j] = true;
                    TrieNode node = root.getKey(board[i][j]);
                    search(node, board, i, j, visited);
                }
            }

        }
        return ans;


    }

    //棋盘到了某一节点(i,j),先看当前节点的children里有无该节点,如果有则继续往下搜
    public void search(TrieNode node, char[][] board, int i, int j, boolean[][] visited) {
        //当搜索到end节点时,说明搜索完了一个单词,则输出
        if (node.isEnd) {

            ans.add(outPutWord(node));

        }
        //当搜索到叶子节点时,进行回溯
        if (node.childrenNum == 0)
            return;

        //上下左右四个方向寻找
        int[][] state = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        for (int[] ints : state) {
            int cur_i = ints[0] + i;
            int cur_j = ints[1] + j;
            
            if (cur_i >= 0 && cur_i < board.length && cur_j >= 0 && cur_j < board[0].length && node.contain(board[cur_i][cur_j])) {
                char c = board[cur_i][cur_j];
                visited[cur_i][cur_j] = true;
                search(node.getKey(c), board, cur_i, cur_j, visited);
                visited[cur_i][cur_j] = false;
            }
        }


    }

    public static void main(String[] args) {
        char[][] board = {{'o', 'a', 'a', 'n'}, {'e', 't', 'a', 'e'}, {'i', 'h', 'k', 'r'}, {'i', 'f', 'l', 'v'}};
        String[] words = {"oath", "pea", "eat", "rain"};
        SearchWords2 searchWords2 = new SearchWords2();
        searchWords2.findWords(board, words);
        for (String an : ans) {
            System.out.println(an);
        }
    }

}