实现 Trie
前缀树是一种数据结构,是一种多叉树,每个节点有26个子节点,分别代表26个字母,也是就对于任何一个字符串,可以分割成一个个字符表示在Trie中,每个节点的子节点表示其下一个字符,Trie主要用于判断字符串是否存在或者是否具有某种字符串前缀,所以又叫前缀树、字典树。
接下来直接来看前缀树的实现,分别实现了insert, search, 和 startsWith 这三个方法:
class Trie {
class Node {
boolean isWord;
Node[] next = new Node[26];//下一个指向有26个字母可选
}
Node root = new Node();
/** Initialize your data structure here. */
public Trie() {
}
/** Inserts a word into the trie. */
public void insert(String word) {
Node p = root;
for(char ch : word.toCharArray()) {
if(p.next[ch - 'a'] == null)//该字母为空,将其初始化
p.next[ch - 'a'] = new Node();
p = p.next[ch - 'a'];
}
p.isWord = true;//单词结束
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Node p = root;
for(char ch : word.toCharArray()) {
if(p.next[ch - 'a'] == null)
return false;
p = p.next[ch - 'a'];
}
return p.isWord;//直到单词结束并且所有字母都匹配才返回true
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
Node p = root;
for(char ch : prefix.toCharArray()) {
if(p.next[ch - 'a'] == null)
return false;
p = p.next[ch - 'a'];
}
return true;//只要匹配则返回true
}
}
接下来看一个例题:
677. 键值映射
思路:
实现一个 MapSum 类里的两个方法,insert 和 sum。对于方法 insert,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。对于方法 sum,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。
Input: insert("apple", 3), Output: Null
Input: sum("ap"), Output: 3
Input: insert("app", 2), Output: Null
Input: sum("ap"), Output: 5
首先比较容易想到的是用HashMap来解题,insert方法可以对应map的put方法,sum方法可以累加map.keySet()中满足条件的key的value即可,代码如下:
Map<String,Integer> map;
/** Initialize your data structure here. */
public MapSum() {
map = new HashMap<>();
}
public void insert(String key, int val) {
map.put(key,val);
}
public int sum(String prefix) {
int res = 0;
for(String key:map.keySet()){
if(prefix.length()<=key.length()&&prefix.equals(key.substring(0,prefix.length()))) res += map.get(key);
}
return res;
}
接下来用Trie实现这两个方法,由于当时我题目没看清,以为是遇到相同的键值不是覆盖而是相加(lll¬ω¬),先看一下我的错误代码:
class MapSum {
class Node{
public int value;
public Node[] next = new Node[26];
}
public Node root;
public MapSum() {
root = new Node();
}
public void insert(String key, int val) {
Node p = root;
for(char c : key.toCharArray()){
if(p.next[c - 'a'] == null){
p.next[c - 'a'] = new Node();
}
p.next[c - 'a'].value += val;
p = p.next[c - 'a'];
}
}
public int sum(String prefix) {
Node p = root;
for(char c : prefix.toCharArray()){
if(p.next[c - 'a'] == null){
return 0;
}
p = p.next[c - 'a'];
}
int sum = p.value;
//System.out.println(p.value);
// for(Node next : p.next){
// if(next != null) sum += next.value;
// }
return sum;
}
}
回归正题,Node的定义和上面是一样的,现在来写insert方法,insert方法可以写成递归的形式也可以写成for循环的形式,这里写成递归比较简洁:
public void insert(String key, int val) {
insert(key, root, val);
}
private void insert(String key, Node node, int val) {
if (key.length() == 0) {
node.value = val;
return;
}
int index = indexForChar(key.charAt(0));
if (node.child[index] == null) {
node.child[index] = new Node();
}
insert(key.substring(1), node.child[index], val);
}
private int indexForChar(char c) {
return c - 'a';
}
对于sum方法,先找到prefix的后一个节点,然后累加后面节点的value:
public int sum(String prefix) {
return sum(prefix, root);
}
private int sum(String prefix, Node node) {
if (node == null) return 0;
if (prefix.length() != 0) {
int index = indexForChar(prefix.charAt(0));
return sum(prefix.substring(1), node.child[index]);
}
int sum = node.value;
for (Node child : node.child) {
sum += sum(prefix, child);
}
return sum;
}
完整代码如下:
class MapSum {
private class Node {
Node[] child = new Node[26];
int value;
}
private Node root = new Node();
public MapSum() {
}
public void insert(String key, int val) {
insert(key, root, val);
}
private void insert(String key, Node node, int val) {
if (node == null) return;
if (key.length() == 0) {
node.value = val;
return;
}
int index = indexForChar(key.charAt(0));
if (node.child[index] == null) {
node.child[index] = new Node();
}
insert(key.substring(1), node.child[index], val);
}
public int sum(String prefix) {
return sum(prefix, root);
}
private int sum(String prefix, Node node) {
if (node == null) return 0;
if (prefix.length() != 0) {
int index = indexForChar(prefix.charAt(0));
return sum(prefix.substring(1), node.child[index]);
}
int sum = node.value;
for (Node child : node.child) {
sum += sum(prefix, child);
}
return sum;
}
private int indexForChar(char c) {
return c - 'a';
}
}
212. 单词搜索 II
思路:给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]
最直观的方式就是先遍历每个words中的单词,对其进行一次dfs,但是这样时间复杂度很大,那如何减少时间复杂度呢,首先words中的单词可能有一部分的前缀是一样的,如果我们直接使用dfs遍历每个单词那么前面的重复部分就会被搜索多次,浪费时间,而前缀树可以帮助我们减少这部分的搜索,具体是这样。
我们遍历字符数组的每个位置(将其最为起点),来判断该起点的位置对应的字符是否出现在前缀树中,如果有则继续向四个方向遍历直达找到有isEnd == true的节点,当然我们还需要一个visited数组来防止路径走重复了。
前缀树是这样构建的,不过我们多加了一个val成员变量以便于最后走到字符串终点时直接将val加入结果集:
class TrieNode{
String val;
TrieNode[] child = new TrieNode[26];
boolean isEnd;
}
class WordTrie{
TrieNode root = new TrieNode();
public void insert(String s){
TrieNode cur = root;
for(char c : s.toCharArray()){
if(cur.child[c - 'a'] == null){
cur.child[c - 'a'] = new TrieNode();
}
cur = cur.child[c - 'a'];
}
cur.isEnd = true;
cur.val = s;
}
}
递归部分如下,还有需要注意的一点是,遍历过的字符串我们将其isEnd设置为false防止最后又被加入到了结果集中(这里解释一下为什么暴力dfs不会加入相同的单词,因为暴力dfs遍历时对每一个单词dfs时都一心一意只找那一个单词,自然不会重复,这这里的前缀树的方法,每次dfs思想是“尽可能找到所有单词,而且是同时找”,因此这种方法无法预知自己通过遍历已经找到了哪几个单词,所以可能出现重复!!!):
public List<String> findWords(char[][] board, String[] words) {
WordTrie myTrie = new WordTrie();
TrieNode root = myTrie.root;
for(String word : words){
myTrie.insert(word);
}
List<String> res = new LinkedList<>();
int m = board.length;
int n = board[0].length;
boolean[][] visited = new boolean[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
find(board,visited,i,j,m,n,res,root);
}
}
return res;
}
public void find(char[][] board,boolean[][] visited,int i,int j,int m,int n,List<String> res,TrieNode cur){
//边界判断以及是否已经访问判断
if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j])
return;
//获取子节点状态,判断其是否有子节点
cur = cur.child[board[i][j] - 'a'];
if (cur == null) {
return;
}
//修改节点状态,防止重复访问
visited[i][j] = true;
//找到单词加入
if (cur.isEnd) {
res.add(cur.val);
//找到单词后,修改字典树内叶子节点状态为false,防止出现重复单词
cur.isEnd = false;
}
find(board, visited, i + 1, j, m, n, res, cur);
find(board, visited, i - 1, j, m, n, res, cur);
find(board, visited, i, j + 1, m, n, res, cur);
find(board, visited, i, j - 1, m, n, res, cur);
//最后修改节点状态为未访问状态
visited[i][j] = false;
}
面试题 17.17. 多次搜索
思路:
先定义前缀树:
class Node{
Node[] child = new Node[26];
String word;
boolean isWord;
}
class Trie{
Node root;
public Trie(){
root = new Node();
}
public void insert(String word){
Node node = root;
for(char c : word.toCharArray()){
if(node.child[c - 'a'] == null){
node.child[c - 'a'] = new Node();
}
node = node.child[c - 'a'];
}
node.isWord = true;
node.word = word;
}
public List<String> search(String word){
Node node = root;
List<String> record = new ArrayList<>();
int size = word.length();
for (int i = 0; i < size; i++){
char c = word.charAt(i);
if (node.child[c - 'a'] == null) break;
node = node.child[c - 'a'];
if (node.isWord) record.add(node.word);
}
return record;
}
}
详情见注释:
public int[][] multiSearch(String big, String[] smalls) {
Map<String,Integer> map = new HashMap<>();
List<List<Integer>> ans = new ArrayList();
int[][] res = new int[smalls.length][];//数组第二维长度可以不一样,所以先不定义
Trie trie = new Trie();
int index = 0;
for (String i : smalls){
trie.insert(i);
map.put(i, index++);
ans.add(new ArrayList());
}
int bigSize = big.length();
for (int i = 0; i < bigSize; i++){//从big的每个字符作为开头来搜寻
String words = big.substring(i, bigSize);
List<String> list = trie.search(words);//若搜到该单词表示i位置为该单词的起始位置
for (String matchs : list){
ans.get(map.get(matchs)).add(i);
}
}
for (int i = 0; i < smalls.length; i++)//链表转数组
{
int[] indexs = new int[ans.get(i).size()];
for (int j = 0; j < indexs.length; j++)
{
indexs[j] = ans.get(i).get(j);
}
res[i] = indexs;
}
return res;
}
421. 数组中两个数的最大异或值
思路:
枯了,我第一反应竟只有暴力法,看了答案才知道可以用前缀树,前缀树的思路就是,构造一个每个节点只有两个叶子的二叉树,右边代表1,左边代表0。所以我们先遍历所有的数,遍历完每个数的32位后将每个数的十进制值放在最后的左叶子上。
TreeNode root = new TreeNode(-1);
for(int n : nums) {
TreeNode node = root;
for(int i = 31; i>=0; i--) {
if ((n & (1 << i)) == 0) { // 0
if (node.left == null) {
node.left = new TreeNode(0);
}
node = node.left;
} else { // 1
if (node.right == null) {
node.right = new TreeNode(1);
}
node = node.right;
}
}
node.left = new TreeNode(n);//放置十进制的值
}
这里注意一定要从高位(31)开始构建树,因为我们要找最大的异或值,所以优先对比高位!!!并且根节点要用一个特殊值替代!!
构建好这棵后,就需要再次对每颗树遍历整棵树了,因为我们要找最大的异或值,所以遇我们应该尽量走于对应位的值相反的路径(不同值的位数越多,异或值越大),直到走完后于对应位直接异或,这样一来,每个数都能找到于其最大异或值的数,然后我们在这些异或值中去一个最大值即可:
int max = 0;
for(int n: nums) {//对于每一个数字(树分支)。这一步的目的是选出每个数与其他某个数的最大异或值
TreeNode node = root;
for(int i=31; i>=0; i--) {//从最高位32开始是因为最高位异或为1后会使整个数字增大更显著
if ((n & (1<<i)) == 0) {//或取对应位是0还是1
if (node.right != null) {//是0则往右边找
node = node.right;
} else {//右边不存在再找左边
node = node.left;
}
} else {
if (node.left != null) {//是1则往左边找
node = node.left;
} else {//左边不存在再找右边
node = node.right;
}
}
}
int nn = node.left.val;//值放在最后的左叶子上(前面有提到)
max = Math.max(max, n ^ nn);//每个数与其和自己二进制数异或值最大的值与其他数与其和自己二进制数异或值最大的值中选一个最大值
}
另外还需要构建前缀树:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
完整代码如下:
class Solution {
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public int findMaximumXOR(int[] nums) {
TreeNode root = new TreeNode(-1);
// build the tree
for(int n : nums) {
TreeNode node = root;
for(int i = 31; i>=0; i--) {
if ((n & (1 << i)) == 0) { // 0
if (node.left == null) {
node.left = new TreeNode(0);
}
node = node.left;
} else { // 1
if (node.right == null) {
node.right = new TreeNode(1);
}
node = node.right;
}
}
node.left = new TreeNode(n);//放置十进制的值
}
int max = 0;
for(int n: nums) {//对于每一个数字(树分支)。这一步的目的是选出每个数与其他某个数的最大异或值
TreeNode node = root;
for(int i=31; i>=0; i--) {//从最高位32开始是因为最高位异或为1后会使整个数字增大更显著
if ((n & (1<<i)) == 0) {//或取对应位是0还是1
if (node.right != null) {//是0则往右边找
node = node.right;
} else {//右边不存在再找左边
node = node.left;
}
} else {
if (node.left != null) {//是1则往左边找
node = node.left;
} else {//左边不存在再找右边
node = node.right;
}
}
}
int nn = node.left.val;//值放在最后的左叶子上(前面有提到)
max = Math.max(max, n ^ nn);//每个数与其和自己二进制数异或值最大的值与其他数与其和自己二进制数异或值最大的值中选一个最大值
}
return max;
}
}