力扣 212. 单词搜索 II + 力扣 208. 实现 Trie (前缀树)

题目来源:208. 实现 Trie (前缀树)212. 单词搜索 II

大致题意:
给定一个 m×n 的二维字符数组,和一个字符串数组。输出字符串数组中能由字符数组的连续字符组成的字符串。连续字符是指上下左右相邻的字符元素,在一个匹配中,一个字符元素只允许出现一次。

思路

本来我想的就是用 dfs,直接对字符串数组的每个元素,都对字符数组每个位置都进行 dfs,然后找到一个字符串就加进去答案。
理论上可行,但是写出来不对,而且很明显复杂度太高,就去看题解。
然后发现是用前缀树解题,我又不知道前缀树是什么,只好先去把前缀树那道题做了。

前缀树

前缀树就是一种树形数据结构,可以高效的存储和检索字符数据集的键。
这里实现它的初始化、插入和查询方法

class Trie {

    private Trie[] children;
    private boolean isEnd;

    public Trie() {
        // 数组长度 26 对应小写字母个数,即每个节点最多可能有 26 个子节点
        this.children = new Trie[26];
        this.isEnd = false;
    }
    // 插入一个新词
    public void insert(String word) {
        // 用 node 指代 this,方便之后字典树的构建
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            int ch = word.charAt(i) - 'a';
            // 若当前节点还未有 ch 子节点,则 new 一个
            if (node.children[ch] == null) {
                node.children[ch] = new Trie();
            }
            // 迭代更新 node
            node = node.children[ch];
        }
        // 表示当前节点是一个词的末尾
        node.isEnd = true;
    }

    // 查找单词
    public boolean search(String word) {
        Trie node = searchPrefix(word);
        return node != null && node.isEnd; 
    }

    // 查找一个前缀
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null;
    }

    // 在前缀树中查找是否有该前缀,有则返回前缀的末尾节点
    private Trie searchPrefix(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            int ch = prefix.charAt(i) - 'a';
            // 查找是否有当前字符的子节点
            if (node.children[ch] == null) {
                return null;
            }
            node = node.children[ch];
        }
        return node;
    }
}
前缀树 + dfs
  1. 先遍历字符串数组,将所有的字符串插入前缀树
  2. 遍历字符数组的所有元素,对每一个元素进行 dfs 遍历
  3. 在 dfs 遍历时,若当前元素在当前的前缀树子节点中,则继续遍历它的未经过的相邻节点。为了防止经过已遍历的节点,在对当前节点的邻点进行 dfs 前,先将当前元素做标记,比如将当前字符替换为 ‘#’ ;若当前节点不再前缀树子节点,直接返回。遍历过程中,若当前字符对应的前缀树节点表示当前位置是之前放入的某个字符串末尾,则将该字符串放入 Set
  4. 最后返回 Set 转换后的 List 即可

代码:

public class FindWords {
    public List<String> findWords(char[][] board, String[] words) {
        Set<String> ans = new HashSet<String>();
        // 表示相邻的四个方向
        int[][] direction = new int[][]{{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
        Trie trie = new Trie();
        // 将字符串都放入前缀树
        for (String word : words) {
            trie.insert(word);
        }
        // 对每个字符都进行 dfs
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                dfs(board, trie, i, j, ans, direction);
            }
        }
        // Set 转 List
        return new ArrayList<String>(ans);
    }
    // dfs 遍历字符数组,查看是否有连续的元素构成前缀树中插入的某个字符串
    public void dfs(char[][] board, Trie node, int x, int y, Set<String> ans, int[][] direction) {
        // 当前字符不在前缀树子结点中,也就是不再前缀树中,直接返回
        if (!node.children.containsKey(board[x][y])) {
            return;
        }
        char ch = board[x][y];
        // 将当前字符位置做标记,防止重复遍历和出现多次
        board[x][y] = '#';
        // 获取字符对应的子节点
        node = node.children.get(ch);
        // 若该子节点是一个字符串的末尾,放入 Set
        if (!node.word.equals("")) {
            ans.add(node.word);
        }
        // 继续 dfs 四个相邻点
        for (int[] dir : direction) {
            int newX = x + dir[0];
            int newY = y + dir[1];
            // 防止越界
            if (newX >= 0 && newX < board.length && newY >= 0 && newY < board[0].length) {
                dfs(board, node, newX, newY, ans, direction);
            }
        }
        // 遍历结束,标记去除
        board[x][y] = ch;
    }

}
// 前缀树定义
class Trie {
    // 存节点对应的字符串
    String word;
    // 字典树,一种字符对应一个子节点
    Map<Character, Trie> children;
    // 初始化,构造函数
    public Trie() {
        this.word = "";
        this.children = new HashMap<Character, Trie>();
    }

    public void insert(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            // 若当前节点还未有该子节点,则新建一个
            if (!node.children.containsKey(ch)) {
                node.children.put(ch, new Trie());
            }
            node = node.children.get(ch);
        }
        // 子节点为字符串末尾时,在节点中存下该字符串
        node.word = word;
    }
}
剪枝优化

在上个方法中,可能会重复遍历到同一个字符串多次。有大量的冗余计算。
优化:

  • 每次遍历到前缀树的叶节点,也就是遍历完一个字符串时,就将该节点对应的字符串置空,同时再将该节点从它的父节点中去掉,也就是在 dfs 函数的末尾,将该节点与父节点的链接去除。那么同理,若父节点没有其它的子节点,或者说其子节点都被去除后,它也变成了新的叶节点,会在遍历后去除。

这样就保证了所有字符串的前缀树只会被遍历出一次,大大减少了冗余计算

代码:

class Solution {
   public List<String> findWords(char[][] board, String[] words) {
        List<String> ans = new ArrayList<String>();
        // 表示相邻的四个方向
        int[][] direction = new int[][]{{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
        Trie trie = new Trie();
        // 将字符串都放入前缀树
        for (String word : words) {
            trie.insert(word);
        }
        // 对每个字符都进行 dfs
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                dfs(board, trie, i, j, ans, direction);
            }
        }
        // Set 转 List
        return ans;
    }
    // dfs 遍历字符数组,查看是否有连续的元素构成前缀树中插入的某个字符串
    public void dfs(char[][] board, Trie node, int x, int y, List<String> ans, int[][] direction) {
        // 当前字符不在前缀树子结点中,也就是不再前缀树中,直接返回
        if (!node.children.containsKey(board[x][y])) {
            return;
        }
        char ch = board[x][y];
        // 获取字符对应的子节点
        Trie next = node.children.get(ch);
        // 若该子节点是一个字符串的末尾,放入 Set
        if (!next.word.equals("")) {
            ans.add(next.word);
            // 删除匹配过的单词
            next.word = "";
        }
        // 当节点有子节点时才进行遍历
        if (!next.children.isEmpty()) {
            // 将当前字符位置做标记,防止重复遍历和出现多次
            board[x][y] = '#';
            // 继续 dfs 四个相邻点
            for (int[] dir : direction) {
                int newX = x + dir[0];
                int newY = y + dir[1];
                // 防止越界
                if (newX >= 0 && newX < board.length && newY >= 0 && newY < board[0].length) {
                    dfs(board, next, newX, newY, ans, direction);
                }
            }
            // 遍历结束,标记去除
            board[x][y] = ch;
        }
        // 当节点的子节点为空,去除该节点(因为该节点也遍历过了,若有对应的字符串也已经放入Set)
        if (next.children.isEmpty()) {
            node.children.remove(ch);
        }
    }

}
// 前缀树定义
class Trie {
    // 存节点对应的字符串
    String word;
    // 字典树,一种字符对应一个子节点
    Map<Character, Trie> children;
    // 初始化,构造函数
    public Trie() {
        this.word = "";
        this.children = new HashMap<Character, Trie>();
    }

    public void insert(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            // 若当前节点还未有该子节点,则新建一个
            if (!node.children.containsKey(ch)) {
                node.children.put(ch, new Trie());
            }
            node = node.children.get(ch);
        }
        // 子节点为字符串末尾时,在节点中存下该字符串
        node.word = word;
    }
}
### C++ 实现前缀Trie前缀Trie),也称为字典或多叉,是一种用于高效存储和检索字符串数据集的数据结构。它通过共享公共前缀来减少内存消耗并加速查询操作。 以下是基于力扣(LeetCode)题目 `208. Implement Trie (Prefix Tree)` 的解法实现: #### 定义 Trie 节点类 每个节点包含两个部分:指向子节点的指针数组以及标记当前节点是否为单词结尾的布尔变量。 ```cpp class TrieNode { public: bool isEnd; vector<TrieNode*> children; TrieNode() : isEnd(false), children(26, nullptr) {} }; ``` #### 定义 Trie 类及其方法 `Trie` 类封装了插入、搜索前缀匹配的功能。 ```cpp class Trie { private: TrieNode* root; public: /** Initialize your data structure here. */ Trie() { root = new TrieNode(); } /** Inserts a word into the trie. */ void insert(string word) { TrieNode* node = root; for (char c : word) { int index = c - 'a'; if (!node->children[index]) { node->children[index] = new TrieNode(); } node = node->children[index]; } node->isEnd = true; // Mark end of word } /** Returns if the word is in the trie. */ bool search(string word) { TrieNode* node = startsWithHelper(word); return node && node->isEnd; } /** Returns if there is any word in the trie that starts with the given prefix. */ bool startsWith(string prefix) { return startsWithHelper(prefix) != nullptr; } private: TrieNode* startsWithHelper(const string& s) { TrieNode* node = root; for (char c : s) { int index = c - 'a'; if (!node->children[index]) { return nullptr; } node = node->children[index]; } return node; } }; ``` --- ### 关键点解析 1. **时间复杂度** 插入、搜索前缀匹配的时间复杂度均为 \(O(m)\),其中 \(m\) 是输入字符串的长度[^5]。 2. **空间复杂度** 空间复杂度取决于字符集大小和插入的字符串数量。对于本例中的小写字母集合,最坏情况下需要的空间为 \(O(NM)\),其中 \(N\) 表示字符串的数量,\(M\) 表示最长字符串的长度。 3. **动态分配与释放内存** 在实际应用中需要注意手动管理内存,避免内存泄漏。可以通过析构函数回收资源。 --- ### 示例代码运行逻辑 假设我们需要执行以下操作序列: - 插入 `"apple"`; - 查询是否存在 `"apple"`; - 查询是否存在 `"app"` 作为完整单词; - 判断是否有任何单词前缀 `"app"` 开头。 ```cpp int main() { Trie trie; trie.insert("apple"); cout << (trie.search("apple") ? "true" : "false") << endl; // 返回 true cout << (trie.search("app") ? "true" : "false") << endl; // 返回 false cout << (trie.startsWith("app") ? "true" : "false") << endl; // 返回 true trie.insert("app"); cout << (trie.search("app") ? "true" : "false") << endl; // 返回 true } ``` 上述程序会依次打印 `true`, `false`, `true`, `true`。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三更鬼

谢谢老板!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值