前缀表达式变成树_15个示例帮你解决常见前缀树(Trie)问题 part-1

前缀树概念

计算机科学中的「前缀树(Trie)」是什么,优缺点是哪些,有什么应用场景?​www.zhihu.com
e18453ca6fbf17f583f828f1f54760f3.png

示例1(Implement Trie (Prefix Tree))

实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。

Trie trie = new Trie();

trie.insert("apple");
trie.search("apple");   // 返回 true
trie.search("app");     // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");   
trie.search("app");     // 返回 true

思路

使用 HashMap 存储各个字符。

插入操作

for 循环遍历需要插入的字符,如果当前节点不包含字符中的某个字节,则进行插入操作。

插入操作后/本来就包含对应字符,则往下走一层,直到走到最后一层,标记一个 '$',表示结束。

8b3f97214f58eb5efcc480b63f596b05.png
插入 ab

查询操作

for 循环遍历需要查询的字符串,从 hashTable 顶部开始往下查找。

  • 若未找到对应的字符,则表示该前缀树中未包含对应的字符串。
  • 若包含对应的字符,则继续往下查找,如果查找到的最后一个字符为 "$",则表示前缀树中包含对应的字符串。

f60dc0404e8e293a69027e29b6538dc3.png
查询 ab,最后一个字符为 $ ,表示前缀树中包含对应的字符串

ed248f46a6fc50adda0f98b168f603c2.png
查询 a,最后一个字符为 b,表示前缀树中不包含对应的字符串

JavaScript 实现

/**
 * Initialize your data structure here.
 */
var Trie = function() {
    this.root = {}
};

/**
 * Inserts a word into the trie. 
 * @param {string} word
 * @return {void}
 */
Trie.prototype.insert = function(word) {
    let cur = this.root
    
    for (const n of word) {
      if (!cur[n]) {
        cur[n] = {}
      }
      cur = cur[n]
    }
  
    cur['$'] = true
};

/**
 * Returns if the word is in the trie. 
 * @param {string} word
 * @return {boolean}
 */
Trie.prototype.search = function(word) {
    let cur = this.root
    
    for (const n of word) {
      if (cur[n]) {
        cur = cur[n]
      } else {
        return false
      }
    }
  
  return cur['$'] === true
};

/**
 * Returns if there is any word in the trie that starts with the given prefix. 
 * @param {string} prefix
 * @return {boolean}
 */
Trie.prototype.startsWith = function(prefix) {
    let cur = this.root
    
    for (const n of prefix) {
      if (cur[n]) {
        cur = cur[n]
      } else {
        return false
      }
    }
  
  return true
};

/** 
 * Your Trie object will be instantiated and called as such:
 * var obj = new Trie()
 * obj.insert(word)
 * var param_2 = obj.search(word)
 * var param_3 = obj.startsWith(prefix)
 */

示例2(Add and Search Word)

设计一个支持以下两种操作的数据结构:

void addWord(word)
bool search(word)

search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 .a-z. 可以表示任何一个字母。

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

思路

与示例1的差别是,查询操作多了一个可以匹配任意字符的 .

插入操作

与示例1保持一致

查询操作

因为多了一个可以匹配任意字符的 .,所以需要考虑一些特殊情况。

for 循环遍历需要查询的字符串,如果循环到的字符为 .,则转换为子前缀树问题。

10abf3bc773068162c68a7e33cf9c3d0.png
在前缀树种查询 a.d 转变成了两个前缀树查询子问题

我们可以通过递归来实现子问题的查询。

JavaScript 实现

/**
 * Initialize your data structure here.
 */
var WordDictionary = function() {
  this.hashTable = {};
};

/**
 * Adds a word into the data structure.
 * @param {string} word
 * @return {void}
 */
WordDictionary.prototype.addWord = function(word) {
  let map = this.hashTable;

  for (let i = 0; i < word.length; i++) {
    if (!map[word[i]]) {
      map[word[i]] = {};
    }

    map = map[word[i]];
  }

  map["$"] = true;
};

/**
 * Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter.
 * @param {string} word
 * @return {boolean}
 */
WordDictionary.prototype.search = function(word) {
  return searchHelper(word, 0, this.hashTable);
};

const searchHelper = (word, index, node) => {
  if (index === word.length) {
    return Boolean(node["$"]);
  }

  const char = word[index];
  if (node[char]) {
    return searchHelper(word, index + 1, node[char]);
  } else if (char === ".") {
    for (let letter in node) {
      if (searchHelper(word, index + 1, node[letter])) {
        return true;
      }
    }
  }

  return false;
};

/**
 * Your WordDictionary object will be instantiated and called as such:
 * var obj = new WordDictionary()
 * obj.addWord(word)
 * var param_2 = obj.search(word)
 */

示例3(Word Search II)

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

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

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

输出: ["eat","oath"]

说明: 你可以假设所有输入都由小写字母a-z 组成。

思路

给定的 words 是一个数组,如果我们通过遍历 words 数组把它转化「给定一个二维网格和一个单词,找出该单词是否存在于网格中」的问题的话,时间复杂度会很高。我们可以利用前缀树来减少时间复杂度。

构建前缀树

["oath","eea","eat","rain"] 为例,我们构建对应的前缀树。

4767045496463b14674fd8c7cfa9574e.png
前缀树

dfs 查找

遍历 board,从第一个字符开始进行上/下/左/右的查找。

如果形成的单词不在前缀树中,则直接停止查找,因为我们不可能找到对应的单词。

191f33f5f3cf7ab9d887ce3f0dbc6f83.png
a 不在前缀树中,停止查找

如果上/下/左/右是边界,则停止查找该方向。

01f21353cd7c92e8a68a9d4d1e657885.png
停止往左边查找

走过的路进行标记,防止重复查找。

50149b0af658a979320f77c742f83000.png

找到对应的值时保存该路径的字符。

b92e5e289a9b4f801857dd2663250137.png
保存 oath

JavaScript 代码示例

/**
 * @param {character[][]} board
 * @param {string[]} words
 * @return {string[]}
 */
var findWords = function(board, words) {
  if (!words || words.length === 0 || board.length === 0) {
    return [];
  }

  const m = board.length;
  const n = board[0].length;

  const trie = new Trie();

  for (let i = 0; i < words.length; i++) {
    trie.insert(words[i]);
  }

  const res = [];

  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      searchWord(trie.hashMap, i, j, board, res);
    }
  }

  return res;
};

function searchWord(trie, i, j, board, res) {
  if (trie["$"]) {
    res.push(trie["$"]);
    trie["$"] = null;
  }

  if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
  if (board[i][j] === "#" || !trie[board[i][j]]) return;

  const c = board[i][j];

  board[i][j] = "#";

  // down
  searchWord(trie[c], i + 1, j, board, res);
  // up
  searchWord(trie[c], i - 1, j, board, res);
  // right
  searchWord(trie[c], i, j + 1, board, res);
  //left
  searchWord(trie[c], i, j - 1, board, res);

  board[i][j] = c;
}

class Trie {
  constructor() {
    this.hashMap = {};
  }

  insert(word) {
    let map = this.hashMap;

    for (let i = 0; i < word.length; i++) {
      if (!map[word[i]]) {
        map[word[i]] = {};
      }

      map = map[word[i]];
    }

    map["$"] = word;
  }
}

王天笑:15个示例帮你解决常见前缀树(Trie)问题 part-2​zhuanlan.zhihu.com
王天笑:15个示例帮你解决常见前缀树(Trie)问题 part-3​zhuanlan.zhihu.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值