前言
【从蛋壳到满天飞】JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)
源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)
全部源代码已上传 github,点击我吧,光看文章能够掌握两成,动手敲代码、动脑思考、画图才可以掌握八成。
本文章适合 对数据结构想了解并且感兴趣的人群,文章风格一如既往如此,就觉得手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,希望对想学习数据结构的人或者正在学习数据结构的人群有帮助。
Trie 字典树 前缀树
- Trie 这种树结构和之前的二分搜索树、堆、线段树不一样,
- 之前的树结构本质都是一棵二叉树,
- 而 Trie 是一种奇特的 n 叉树,这个 n 是大于 2 的,
- 这种 n 叉树可以非常快速的处理和字符串相关的问题,
- 也就是字典树。
- 在有些语言中将映射 Map 当作是字典
- 字典其实表示的是
- 一个词条与一段释意相对应 的这样的一种结构。
- Trie 是专门的真正的为字典设计的数据结构
- 因为通常 Trie 只会用来处理字符串,
- 而映射 Map 来说,从一个对象映射到另外一个对象,
- 这个对象不一定是字符串,而对于 Trie 这种数据结构,
- 它是专门为处理字符串设计的。
- 相关场景
- 一个字典中有 n 个条目的话,
- 如果使用映射 Map 这种数据结构,它的底层是一个二分搜索树之类的树结构,
- 查询操作的时间复杂度是
O(logn)
这个级别, - 二分搜索树可能还会退化成一个链表,但是使用平衡二叉树就不会这样,
- 其实
O(logn)
这个级别已经非常高效了,但是如果字典中有 100 万个条目, - 大概就是(2^20),那么 logn 大约是 20。
- 但是如果使用 Trie 这种数据结构,
- 那么就可以做到查询每个条目的时间复杂度和字典中具体有多少个条目毫无相关,
- 会和你查询的这个字符串的长度相关,那么时间复杂度是
O(w)
这个级别, - w 为查询的那个单词的长度,如果你查询的字符串有 10 个字符,
- 那么使用 trie 查询的时间复杂度就是 10,如果你查询的字符串有 3 个字符的话,
- 那么使用 trie 查询的时间复杂度就是 3,这样一来对于大多数英语单词来说就有优势了,
- 因为绝大多数单词的长度小于 10。
- trie 的初级原理
- 之前的映射 Map 存储单词的时候是将整个字符串看作是一个整体,
- 但是 trie 打破了这个思路,它将整个字符串以字母为单位一个一个拆开,
- 从根节点开始,一直到叶子节点去遍历,每遍历到一个叶子节点就形成了一个单词,
- 查询任何一个单词,从根节点出发,
- 只需要经过这个单词有多少个字母就相应的经过了多少个节点最终到达叶子节点,
- 这样就成功的查找了单词,这样的一种数据结构就叫做 Trie,非常的好理解。
- trie 的节点定义
- 每一个节点有 26 个指向下一个节点的指针,这是因为在英文的字母表中一共有 26 个字母,
- 从根节点开始出发,相应的他有 26 棵子树,
- 每一棵子树代表的是从一个不同的字母开始的这样的一个新的子树,
- 在 trie 中节点的定义大概是这个样子。
class TrieNode { c; // char next; // Array(26) } 复制代码
- 这样的节点的定义会遇到这样的问题
- 语言的不同和情景的不同,有可能 26 个指针是富于的,也有可能 26 个指针是不够的,
- 但是每一个节点下面跟 26 个孩子,在这里就没有考虑大小写的问题,
- 不过你要设计的 trie 要考虑大小写的问题,相应的就需要有 52 个指针,
- 但是你的 trie 你装载的内容更加复杂的话,比如你装载的内容是网址或者是邮件地址,
- 这一类的字符串,那么相应的有一些字符也应该计算在内,比如
@:/\_-
等等, - 正因为这个原因,如果你想设计一个更灵活的 trie,
- 通常不会固定每一个节点只有 26 个指向下一个节点的指针,
- 除非你非常肯定这个 trie 所处理的内容只包含所有小写的英文字母,
- 通常会让每一个节点有若干个指向下一个节点的指针,将 26 改成若干,
- 也就是将静态的节点数改成了动态的节点数,这是一种动态的思想,
- 要实现这种动态的思想,那么那个 next 就可以使用一个映射 Map 来实现,
- 实际上这个 next 就是指 一个 char 和一个 Node 之间的映射,
- 这个 Map 中存多少个映射其实是不知道的,
- 但是每一个映射 Map 中一定都是一个字符到一个新的节点这样的一个映射,
- 自己实现的映射这种数据结构,在这里又使用上了,
- 也可以使用 系统内置的 Map,因为自己实现的映射 Map 的底层是二分搜索树,
- 二分搜索树在最坏的情况下会退化为一个链表,而 Map 是使用改良后的平衡的二叉树,
- 而且 系统内置的 Map 的底层是红黑树,所以性能相对来说会好一些。
class TrieNode { c; // char next; // Map } 复制代码
- trie 的中级原理
- 其实从根节点找到下一个节点的过程中,其实你就已经知道这个字母是谁了,
- 是因为你在根节点就知道了你下一个节点要到指定的那个节点,
- 所以更准确的来说,其实在你来到这个节点之前就已经知道这个字母具体是什么,
- 才可能通过这个映射 Map 来找到下一个节点,所以在这个节点的实现中,
- 你不存储这个
char c
是没有问题的,在 trie 中添加或者查询某一个单词的时候, - 不存储这个
char c
是没有问题的, - 因为在这个映射 Map 中已经有了从某一个 char 到某一个节点这样相应一个条目,
- 直接通过这个条目来到下一个节点,
- 那么你自然就知道了这个节点对应的就是映射里相应的那个字符,
- 有可能在有一些情境下在节点中存这样一个
char c
, - 可以帮你更快的组织这个逻辑,这一点了解即可。
class TrieNode { next; // Map } 复制代码
- 另外一个很关键的问题是
- trie 查询一个单词都是从根节点出发一直到叶子节点,
- 到了叶子节点的时候,就到了一个单词的地方,
- 不过在英语单词世界中很多单词可能是另外一个单词的前缀,
- 例如平底锅 pan 和熊猫 panda,你即要存 pan 又要存 panda,
- 此时对于 pan 这个单词结尾的这个 n 并不是一个叶子节点,
- 不然就没有办法存 panda 这个单词了,正因为如此,
- 对于每一个 node 就需要一个标识,
- 用这个标识来标明当前这个节点是否是某一个单词的结尾,
- 某一个单词的结尾只靠叶子节点是不能区分出来的,
- 那么 pan 这个单词相当于是 panda 这个单词的一个前缀,
- 这样一来就需要多添加一个 bool 值
boolean isWord
, - 表示的是 当前这个节点是否代表了一个单词的结尾,
- 也就是访问到了当前这个节点之后,是否访问到了一个单词了,
- 这样就将 Trie 中每一个节点进行了相应的一个定义,
- 节点的定义最复杂的部分在于这个 next,
- 因为这个 next 又是一个映射,相当于每一个节点里面其实
- 都蕴含一个相对比较复杂的一个数据结构来支撑这个节点的运行,
- 其实这也是数据结构本身的魅力,就是这样一点一点的从最底层
- 开始像搭积木一样逐渐的搭出更加复杂的数据结构,
- 而已经搭建好的这些结构封装好了之后,就可以非常简单的复用,
- 对于上层用户来说完全屏蔽了底层的实现细节。
class TrieNode { isWord; // Boolean next; // Map } 复制代码
Trie 字典树 简单实现及添加操作
- 中日韩这些语言体系中对于什么是单词这样的定义是模糊的,
- 它不像在英文的语句中单词和单词之间直接由空格分开,
- 可以非常清晰的界定什么是一个单词,
- 所以很多时候对于这种其它的语言体系
- 它不是由一个一个字母组成的单词这样的语言体系,
- 就会有特殊的语言处理的方法,所以了解即可。
- 自己实现的 Trie 不是泛型的
- 主要用于英语,其它中日韩不管它。
- 对于 Trie 来说它的本质只是一个多叉树而已
- 和二叉树是没有区别的,对于二叉树来说,
- 它有两个指针,分别是 left 和 right,
- 而对于 Trie 来说是有多个,所以才用一个映射来存储,
- 区别只在这里而已,所以整体添加元素的逻辑和二叉树是非常像的。
- Trie 的添加操作 非递归
- 其实非常的容易,添加的是一个字符串并不是一个字符,
- 这是 trie 和二叉树的一个区别,添加一个字符串,
- 是因为要把这个字符串拆成一个一个的字符,
- 然后把这一个一个的字符做成一个一个的节点,
- 最后再添加进这个树结构中,就是这样的一个逻辑。
- 原理
- 根据单词中的每一个字符创建新的节点,
- 每一个节点中都有一个 next(映射),
- 这个映射中存储了 指定字符对应的指定节点的信息,
- 根节点是不存字符的,从根节点开始,从它的 next 中开始存储,
- 将新添加的单词进行字符的拆分,这个添加顺序是从左到右,
- 存储的顺序是从外到内的,映射中有字符对应一个节点,
- 这个节点也有映射,所以这是一个嵌套的关系,就像一颗树,
- 存储完一个单词后,这个单词的最后一个字符会被设置一个标记,即表示单词的结尾。
代码示例
-
MyTrie
// 自定义字典树节点 TrieNode class MyTrieNode { constructor(letterChar, isWord = false) { this.letterChar = letterChar; this.isWord = isWord; // 是否是单词 this.next = new Map(); // 存储 字符所对应的节点的 字典映射 } } // 自定义字典树 Trie class MyTrie { constructor() { this.root = new MyTrieNode(); this.size = 0; } // 向Trie中添加一个新的单词word add(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; this.size++; } } // 向Trie中添加一个新的单词word 递归算法 recursiveAdd(word) { this.recursiveAddFn(this.root, word, 0); } // 向Trie中添加一个新的单词word 递归辅助函数 recursiveAddFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) { if (!node.isWord) { node.isWord = true; this.size++; } return; } const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空就添加 if (!map.has(letterChar)) map.set(letterChar, new MyTrieNode(letterChar)); recursiveAddFn(map.get(letterChar), word, index + 1); } } 复制代码
Trie 字典树 查询操作
- 字典树中不会去添加重复的单词,
- 这和之前自己实现的集合 Set 很像,
- 只不过对于 Trie 来说,
- 它是一个只能够
存储字符串
这样的元素的相应的集合
, - 而之前基于二分搜索树实现的集合,可以存储任意元素,
- 更准确的来说,是可比较大小的这样的元素,
- 相应的集合 Set 都可以进行存储。
- 集合 Set 和映射 Map 之间相应的是有联系的
- 如果是在 TrieNode 中再设置一个属性,
- 这个属性就是该字符串的特殊意义,如存放词频数等等,
- 那样 Trie 就被
改造成
了一个映射
了。
让目前这个 Trie 与集合 Set 进行对比
- 使用 Trie 实现一个 TrieSet
- 与链表 Set 和二分搜索树 Set 对比后,
- TrieSet 性能相对来说比较好,
- 当你添加到 set 中的数据越多,
- 那么 TrieSet 的性能相对来说就越好。
- 在 trie 中添加字符串和查询字符串
- 与 trie 中有多少个元素有多少个字符串是没有关系的,
- 只和你添加的那个字符串和你查找的那个字符串的长度有关,
- 如果你添加的字符串整体都比较短的话,
- 那么在一个大的集合中使用 Trie 就会有非常高的性能优势。
代码示例
-
(class: MyTrie, class: MyTrieSet, class: Main)
-
MyTrie
// 自定义字典树节点 TrieNode class MyTrieNode { constructor(letterChar, isWord = false) { this.letterChar = letterChar; this.isWord = isWord; // 是否是单词 this.next = new Map(); // 存储 字符所对应的节点的 字典映射 } } // 自定义字典树 Trie class MyTrie { constructor() { this.root = new MyTrieNode(); this.size = 0; } // 向Trie中添加一个新的单词word add(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; this.size++; } } // 向Trie中添加一个新的单词word 递归算法 recursiveAdd(word) { this.recursiveAddFn(this.root, word, 0); } // 向Trie中添加一个新的单词word 递归辅助函数 - recursiveAddFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) { if (!node.isWord) { node.isWord = true; this.size++; } return; } const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空就添加 if (!map.has(letterChar)) map.set(letterChar, new MyTrieNode(letterChar)); this.recursiveAddFn(map.get(letterChar), word, index + 1); } // 查询单词word是否在Trie中 contains(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (node === null) return false; // 游标切换到这个节点 cur = node; } // 单词遍历完毕 // 返回最后一个字符是否是一个单词的结尾 return cur.isWord; } // 查询单词word是否在Trie中 递归算法 recursiveContains(word) { return this.recursiveContainsFn(this.root, word, 0); } // 查询单词word是否在Trie中 递归赋值函数 - recursiveContainsFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空那么就说明这个单词没有进行存储 if (!map.has(letterChar)) return false; return this.recursiveContainsFn(map.get(letterChar), word, index + 1); } // 获取字典树中存储的单词数量 getSize() { return this.size; } // 获取字典树中是否为空 isEmpty() { return this.size === 0; } } 复制代码
-
MyTrieSet
// 自定义字典集合 TrieSet class MyTrieSet { constructor() { this.trie = new MyTrie(); } // 添加操作 add(word) { this.trie.add(word); } // 删除操作 待实现 remove(word) { return false; } // 查单词是否存在 contains(word) { return this.trie.contains(word); } // 获取实际元素个数 getSize() { return this.trie.getSize(); } // 获取当前集合是否为空 isEmpty() { return this.trie.isEmpty(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('Set Comparison Area'); const n = 2000000; const myBSTSet = new MyBinarySearchTreeSet(); const myTrieSet = new MyTrieSet(); let performanceTest1 = new PerformanceTest(); const random = Math.random; let arr = []; // 循环添加随机数的值 for (let i = 0; i < n; i++) { arr.push(i.toString()); } this.alterLine('MyBSTSet Comparison Area'); const myBSTSetInfo = performanceTest1.testCustomFn(function() { for (const word of arr) myBSTSet.add(word); }); // 总毫秒数:3173 console.log(myBSTSetInfo); this.show(myBSTSetInfo); this.alterLine('MyTrieSet Comparison Area'); const myTrieSetInfo = performanceTest1.testCustomFn(function() { for (const word of arr) myTrieSet.add(word); }); // 总毫秒数:2457 console.log(myTrieSetInfo); this.show(myTrieSetInfo); } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
Trie 字典树 前缀查询
- 查看 Trie 中是否有包含某个前缀相关的单词
- 很像是模糊搜索,
- 例如 panda 这个单词,p 是 panda 的前缀,pa 也是 panda 的前缀,
- 一个单词的本身也算是这个单词的前缀。
- 因为 Trie 就是这样的一种数据结构,所以可以通过前缀来进行查询,
- 查当前所存储的所有的单词中,
- 是否有某一个前缀对应的这样一个单词。
- 对于 Trie 这样的一个数据结构来说
- 可以以非常高效的性能,
- 也就是以这个前缀的长度的时间复杂度,
- 在一个集合中是否能够直接找到以这个字符串为前缀的单词,
- 所以 Trie 又叫做前缀树。
- 对于前缀查询这样的一个操作
- 如果使用基于二分搜索树实现的集合来完成的话,
- 相应的就会复杂很多,要先查询这个前缀是不是一个单词,
- 然后遍历每个二分搜索树 Set 中的单词,
- 最后以前缀字符串的长度进行逐个对比,时间复杂度是 n 方的级别。
leetcode 上前缀树的题目
-
208.实现 Trie (前缀树)
https://leetcode-cn.com/problems/implement-trie-prefix-tree/
- 这个就是自己实现的前缀树,
- 第一个版本是映射 Map 版,是动态的
- 第二个版本是数组版,是静态的
-
Trie
// 答题 class Solution { // leetcode 208.实现 Trie (前缀树) Trie() { // 数组版的Trie 静态Trie function ArrayTrie() { // TrieNode var TrieNode = function(isWord = false) { this.isWord = isWord; this.next = new Array(26); }; /** * Initialize your data structure here. */ var Trie = function() { this.root = new TrieNode(); }; /** * Inserts a word into the trie. * @param {string} word * @return {void} */ Trie.prototype.insert = function(word) { // 指定游标 let cur = this.root; for (const c of word) { const index = c.charCodeAt(0) - 97; const array = cur.next; if (array[index] === null || array[index] === undefined) array[index] = new TrieNode(); cur = array[index]; } if (!cur.isWord) cur.isWord = 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 c of word) { const index = c.charCodeAt(0) - 97; const array = cur.next; if (array[index] === null || array[index] === undefined) return false; cur = array[index]; } return cur.isWord; }; /** * 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 c of prefix) { const index = c.charCodeAt(0) - 97; const array = cur.next; if (array[index] === null || array[index] === undefined) return false; cur = array[index]; } return true; }; /** * Your Trie object will be instantiated and called as such: * var obj = Object.create(Trie).createNew() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */ return new Trie(); } // 映射版的Trie 动态Trie function MapTrie() { // TrieNode var TrieNode = function(isWord = false) { this.isWord = isWord; this.next = new Map(); }; /** * Initialize your data structure here. */ var Trie = function() { this.root = new TrieNode(); }; /** * Inserts a word into the trie. * @param {string} word * @return {void} */ Trie.prototype.insert = function(word) { // 指定游标 let cur = this.root; for (const c of word) { const map = cur.next; if (!map.has(c)) map.set(c, new TrieNode()); cur = map.get(c); } if (!cur.isWord) cur.isWord = 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 c of word) { const map = cur.next; if (!map.has(c)) return false; cur = map.get(c); } return cur.isWord; }; /** * 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 c of prefix) { const map = cur.next; if (!map.has(c)) return false; cur = map.get(c); } return true; }; /** * Your Trie object will be instantiated and called as such: * var obj = Object.create(Trie).createNew() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */ return new Trie(); } // return new ArrayTrie(); return new MapTrie(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('leetcode 208.实现 Trie (前缀树)'); let s = new Solution(); let trie = s.Trie(); this.show(trie.insert('apple') + ''); this.show(trie.search('apple') + ' // 返回 true'); // 返回 true this.show(trie.search('app') + '// 返回 false'); // 返回 false this.show(trie.startsWith('app') + '// 返回 true'); // 返回 true this.show(trie.insert('app') + ''); this.show(trie.search('app') + '// 返回 true'); // 返回 true } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
代码示例
// 自定义字典树 Trie
class MyTrie {
constructor() {
this.root = new MyTrieNode();
this.size = 0;
}
// 向Trie中添加一个新的单词word
add(word) {
// 指定游标
let cur = this.root;
// 遍历出当前单词的每一个字符
for (const c of word) {
// 下一个字符所对应的映射是否为空
if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c));
// 切换到下一个节点
cur = cur.next.get(c);
}
// 如果当前这个单词是一个新的单词
if (!cur.isWord) {
// 当前这个字符是这个单词的结尾
cur.isWord = true;
this.size++;
}
}
// 向Trie中添加一个新的单词word 递归算法
recursiveAdd(word) {
this.recursiveAddFn(this.root, word, 0);
}
// 向Trie中添加一个新的单词word 递归辅助函数 -
recursiveAddFn(node, word, index) {
// 解决基本的问题,因为已经到底了
if (index === word.length) {
if (!node.isWord) {
node.isWord = true;
this.size++;
}
return;
}
const map = node.next; // 获取节点的next 也就是字符对应的映射
const letterChar = word[index]; // 获取当前位置对应的单词中的字符
// 下一个字符所对应的映射是否为空 为空就添加
if (!map.has(letterChar)) map.set(letterChar, new MyTrieNode(letterChar));
this.recursiveAddFn(map.get(letterChar), word, index + 1);
}
// 查询单词word是否在Trie中
contains(word) {
// 指定游标
let cur = this.root;
// 遍历出当前单词的每一个字符
for (const c of word) {
// 获取当前这个字符所对应的节点
const node = cur.next.get(c);
// 这个节点不存在,那么就说明就没有存储这个字符
if (node === null) return false;
// 游标切换到这个节点
cur = node;
}
// 单词遍历完毕
// 返回最后一个字符是否是一个单词的结尾
return cur.isWord;
}
// 查询单词word是否在Trie中 递归算法
recursiveContains(word) {
return this.recursiveContainsFn(this.root, word, 0);
}
// 查询单词word是否在Trie中 递归赋值函数 -
recursiveContainsFn(node, word, index) {
// 解决基本的问题,因为已经到底了
if (index === word.length) return node.isWord;
const map = node.next; // 获取节点的next 也就是字符对应的映射
const letterChar = word[index]; // 获取当前位置对应的单词中的字符
// 下一个字符所对应的映射是否为空 为空那么就说明这个单词没有进行存储
if (!map.has(letterChar)) return false;
return this.recursiveContainsFn(map.get(letterChar), word, index + 1);
}
// 查询在Trie中是否有单词以 prefix 为前缀
isPrefix(prefix) {
// 指定游标
let cur = this.root;
// 遍历出当前单词的每一个字符
for (const c of prefix) {
// 获取当前这个字符所对应的节点
const node = cur.next.get(c);
// 这个节点不存在,那么就说明就没有存储这个字符
if (node === null) return false;
// 游标切换到这个节点
cur = node;
}
// 前缀遍历完毕 说明这个前缀有单词与之匹配
return true;
}
// 获取字典树中存储的单词数量
getSize() {
return this.size;
}
// 获取字典树中是否为空
isEmpty() {
return this.size === 0;
}
}
复制代码
Trie 字典树 简单的模式匹配
- match 方法,是一个递归函数
- 有三个参数,
- 第一个参数是当前的节点,
- 第二个参数是字符串,
- 第三个参数是 当前匹配的字符的索引
- match 方法逻辑
- 先分成两种情况,一种是递归到底的情况,
- 一种是没有递归到底就去调用这个递归相应的逻辑。
- 在调用递归的这个逻辑中,对当前考虑的这个字符进行判断,
- 一种是当前这个字符等于正则表达式通配符号的
.
, - 另一种是当前的这个字符不等于正则表达式通配符号的
.
。 - 如果不等于
.
,那么就很简单, - 直接查看这个字符对应的 TrieNode 是否为 null,
- 如果为 null 的话说明匹配失败,直接返回 false,
- 否则就继续以 return 的方式调用 match 函数,
- 传入的参数进行一下变更,下一个节点、字符串、下一个要匹配的字符的索引。
- 如果等于
.
,那么就相对来说复杂一点, - 需要对当前节点下一个字符的所有可能都去进行一下匹配,
- 也就是遍历当前节点的下一个映射 Map 中的所有 key,
- 也就是当前节点的`next.keys(),每遍历到每一个字符的时候都要做一下判断,
- 判断的方式是调用 match 方法,传入的参数也是一样进行变更,
- 下一个节点、字符串、下一个要匹配的字符的索引,
- 目的是为了看看当前字符所对应的下一个节点是否能够匹配成功,
- 如果匹配成功就直接返回 true,否则所有遍历的字符的下一个节点都匹配失败的话,
- 那么就返回 false,只要遍历的字符中有一个下一个节点匹配成功就算匹配成功。
- 递归到底的条件是 当前匹配的字符的索引等于这个字符串的长度。
leetcode 上的题目
-
211.添加与搜索单词 - 数据结构设计
https://leetcode-cn.com/problems/add-and-search-word-data-structure-design/
- 和 实现前缀树那道题差不多,
- 第一个版本是使用映射 Map 来实现的
- 第二个版本是使用数组来实现的
-
WordDictionary
// 答题 class Solution { // leetcode 211.添加与搜索单词 - 数据结构设计 WordDictionary() { // 数组版 function ArrayWordDictionary() { // TrieNode var TrieNode = function() { this.isWord = false; this.next = new Array(26); }; /** * Initialize your data structure here. */ var WordDictionary = function() { this.root = new TrieNode(); }; /** * Adds a word into the data structure. * @param {string} word * @return {void} */ WordDictionary.prototype.addWord = function(word) { // 指定游标 let cur = this.root; for (const c of word) { const index = c.charCodeAt(0) - 97; const array = cur.next; if (!array[index]) array[index] = new TrieNode(); cur = array[index]; } if (!cur.isWord) cur.isWord = 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 this.recursiveMatch(this.root, word, 0); }; // 递归搜索 WordDictionary.prototype.recursiveMatch = function( node, word, index ) { if (index === word.length) return node.isWord; const letterChar = word[index]; if (letterChar !== '.') { const i = letterChar.charCodeAt(0) - 97; if (!node.next[i]) return false; return this.recursiveMatch(node.next[i], word, index + 1); } else { for (const next of node.next) { if (next === undefined) continue; if (this.recursiveMatch(next, word, index + 1)) return true; } return false; } }; /** * Your WordDictionary object will be instantiated and called as such: * var obj = Object.create(WordDictionary).createNew() * obj.addWord(word) * var param_2 = obj.search(word) */ return new WordDictionary(); } // 映射版 function MapWordDictionary() { // TrieNode var TrieNode = function(isWord = false) { this.isWord = isWord; this.next = new Map(); }; /** * Initialize your data structure here. */ var WordDictionary = function() { this.root = new TrieNode(); }; /** * Adds a word into the data structure. * @param {string} word * @return {void} */ WordDictionary.prototype.addWord = function(word) { let cur = this.root; for (const c of word) { if (!cur.next.has(c)) cur.next.set(c, new TrieNode()); cur = cur.next.get(c); } if (!cur.isWord) cur.isWord = 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 this.recursiveMatch(this.root, word, 0); }; WordDictionary.prototype.recursiveMatch = function( node, word, index ) { if (index === word.length) return node.isWord; const letterChar = word[index]; if (letterChar !== '.') { const map = node.next; if (!map.has(letterChar)) return false; return this.recursiveMatch( map.get(letterChar), word, index + 1 ); } else { const map = node.next; const keys = map.keys(); for (const key of keys) if (this.recursiveMatch(map.get(key), word, index + 1)) return true; return false; } }; /** * Your WordDictionary object will be instantiated and called as such: * var obj = Object.create(WordDictionary).createNew() * obj.addWord(word) * var param_2 = obj.search(word) */ return new WordDictionary(); } // return new ArrayWordDictionary(); return new MapWordDictionary(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('leetcode 208. 实现 Trie (前缀树)'); let trie = new MyTrie(); this.show(trie.add('apple') + ''); this.show(trie.contains('apple') + ' // 返回 true'); // 返回 true this.show(trie.contains('app') + '// 返回 false'); // 返回 false this.show(trie.isPrefix('app') + '// 返回 true'); // 返回 true this.show(trie.add('app') + ''); this.show(trie.contains('app') + '// 返回 true'); // 返回 true this.alterLine('leetcode 211. 添加与搜索单词 - 数据结构设计'); trie = new MyTrie(); this.show(trie.add('bad') + ''); this.show(trie.add('dad') + ''); this.show(trie.add('mad') + ''); this.show(trie.regexpSearch('pad') + '-> false'); //-> false this.show(trie.regexpSearch('bad') + '-> true'); //-> true this.show(trie.regexpSearch('.ad') + '-> true'); //-> true this.show(trie.regexpSearch('b..') + '-> true'); //-> true this.show(trie.regexpSearch('b....') + '-> false'); //-> false } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
代码示例
-
MyTrie
// 自定义字典树 Trie class MyTrie { constructor() { this.root = new MyTrieNode(); this.size = 0; } // 向Trie中添加一个新的单词word add(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; this.size++; } } // 向Trie中添加一个新的单词word 递归算法 recursiveAdd(word) { this.recursiveAddFn(this.root, word, 0); } // 向Trie中添加一个新的单词word 递归辅助函数 - recursiveAddFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) { if (!node.isWord) { node.isWord = true; this.size++; } return; } const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空就添加 if (!map.has(letterChar)) map.set(letterChar, new MyTrieNode(letterChar)); this.recursiveAddFn(map.get(letterChar), word, index + 1); } // 查询单词word是否在Trie中 contains(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (node === null) return false; // 游标切换到这个节点 cur = node; } // 单词遍历完毕 // 返回最后一个字符是否是一个单词的结尾 return cur.isWord; } // 查询单词word是否在Trie中 递归算法 recursiveContains(word) { return this.recursiveContainsFn(this.root, word, 0); } // 查询单词word是否在Trie中 递归赋值函数 - recursiveContainsFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空那么就说明这个单词没有进行存储 if (!map.has(letterChar)) return false; return this.recursiveContainsFn(map.get(letterChar), word, index + 1); } // 查询在Trie中是否有单词以 prefix 为前缀 isPrefix(prefix) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of prefix) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (node === null) return false; // 游标切换到这个节点 cur = node; } // 前缀遍历完毕 说明这个前缀有单词与之匹配 return true; } // 正则表达式 查询单词word是否在Trie中,目前只支持 统配符 "." regexpSearch(regexpWord) { return this.match(this.root, regexpWord, 0); } // 正则表达式 匹配单词 递归算法 - match(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 判断这个字符是否是通配符 if (letterChar !== '.') { // 如果映射中不包含这个字符 if (!map.has(letterChar)) return false; // 如果映射中包含这个字符,那么就去找个字符对应的节点中继续匹配 return this.match(map.get(letterChar), word, index + 1); } else { // 遍历 下一个字符的集合 // 如果 从下一个字符继续匹配,只要匹配成功就返回 true for (const key of map.keys()) if (this.match(map.get(key), word, index + 1)) return true; // 遍历一遍之后还是没有匹配成功 那么就算匹配失败 return false; } } // 获取字典树中存储的单词数量 getSize() { return this.size; } // 获取字典树中是否为空 isEmpty() { return this.size === 0; } } 复制代码
Trie 字典树 字符串映射 Map
- leetcode 上的 677 号题目与 211 号题目类似,
- 也是需要进行类似模式匹配的模糊查询,
- 如果模糊匹配成功就会返回最终存的 value 值,
- 也就是就是说先匹配已明确指示的字符,
- 然后再遍历,如果最后遍历到底证明那是一个单词,
- 那么就直接返回该单词对应的 value 即可。
- 添加操作的话直接是覆盖的操作,
- 相同的单词所定义的映射的 value 直接覆盖掉即可。
- 使用 Trie 实现映射 Map 并不是很难,
- 其实就是在使用 Trie 实现集合 Set 的基础上加一个属性而已,
- 每添加一个字符串就在那个字符串最后一个字符的相应节点设置这个属性的值即可。
leetcode 上的题目
-
677. 键值映射
https://leetcode-cn.com/problems/map-sum-pairs/
- 和 211 号题目类似
- 第一个版本是使用映射 Map 来实现的
- 第二个版本是使用数组来实现的
-
MapSum
// 答题 class Solution { // leetcode 677. 键值映射 MapSum() { // 数组版 function ArrayVersion() { var TrieNode = function(value) { this.value = value; this.next = new Array(26); }; /** * Initialize your data structure here. */ var MapSum = function() { this.root = new TrieNode(0); }; /** * @param {string} key * @param {number} val * @return {void} */ MapSum.prototype.insert = function(key, val) { this.__insert(this.root, key, val, 0); }; MapSum.prototype.__insert = function(node, word, value, index) { if (index === word.length) { node.value = value; return; } const array = node.next; const i = word[index].charCodeAt(0) - 97; if (!array[i]) array[i] = new TrieNode(0); this.__insert(array[i], word, value, index + 1); }; /** * @param {string} prefix * @return {number} */ MapSum.prototype.sum = function(prefix) { // 先进行前缀匹配 let cur = this.root; for (const c of prefix) { const index = c.charCodeAt(0) - 97; if (!cur.next[index]) return 0; cur = cur.next[index]; } // 前缀匹配成功之后 进行剩余单词的匹配 求和 return this.__sum(cur); }; MapSum.prototype.__sum = function(node) { let result = node.value || 0; for (const next of node.next) { if (!next) continue; result += this.__sum(next); } return result; }; /** * Your MapSum object will be instantiated and called as such: * var obj = Object.create(MapSum).createNew() * obj.insert(key,val) * var param_2 = obj.sum(prefix) */ return new MapSum(); } // 映射版 function MapVersion() { var TrieNode = function(value) { this.value = value; this.next = new Map(); }; /** * Initialize your data structure here. */ var MapSum = function() { this.root = new TrieNode(); }; /** * @param {string} key * @param {number} val * @return {void} */ MapSum.prototype.insert = function(key, val) { let cur = this.root; for (const c of key) { const map = cur.next; if (!map.has(c)) map.set(c, new TrieNode()); cur = map.get(c); } cur.value = val; }; /** * @param {string} prefix * @return {number} */ MapSum.prototype.sum = function(prefix) { // 先处理前缀部分 let cur = this.root; for (const c of prefix) { const map = cur.next; if (!map.has(c)) return 0; cur = map.get(c); } return this.__sum(cur); }; MapSum.prototype.__sum = function(node) { let result = node.value || 0; const map = node.next; const keys = map.keys(); for (const key of keys) result += this.__sum(map.get(key)); return result; }; /** * Your MapSum object will be instantiated and called as such: * var obj = Object.create(MapSum).createNew() * obj.insert(key,val) * var param_2 = obj.sum(prefix) */ return new MapSum(); } // return new ArrayVersion(); return new MapVersion(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('leetcode 677. 键值映射'); let s = new Solution(); let trie = s.MapSum(); this.show(trie.insert('apple', 3) + ' 输出: Null'); this.show(trie.sum('ap') + ' 输出: 3'); this.show(trie.insert('app', 2) + ' 输出: Null'); this.show(trie.sum('ap') + ' 输出: 5'); } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
代码示例
-
(class: MyTrieUpgrade, class: MyTrieMap, class: Main)
-
MyTrie
// 自定义字典树节点升级版 TrieNodeUpgrade class MyTrieNodeUpgrade { constructor(letterChar, element, isWord = false) { this.letterChar = letterChar; this.element = element; // 升级后可以存储特殊数据 this.isWord = isWord; // 是否是单词 this.next = new Map(); // 存储 字符所对应的节点的 字典映射 } } // 自定义字典树升级版 TrieUpgrade class MyTrieUpgrade { constructor() { this.root = new MyTrieNodeUpgrade(); this.size = 0; } add(word, element) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; // 存储 额外信息 cur.element = element; this.size++; } } // 向Trie中添加一个新的单词word 并且在word中存储额外的信息,如果额外信息存在就覆盖 put(word, element) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; this.size++; } // 设置或者覆盖 额外信息 cur.element = element; } // 向Trie中添加一个新的单词word 递归算法 // 并且在word中存储额外的信息,如果额外信息存在就覆盖 recursivePut(word, element) { this.recursiveAddFn(this.root, word, element, 0); } // 向Trie中添加一个新的单词word 递归辅助函数 - // 并且在word中存储额外的信息,如果额外信息存在就覆盖 recursivePutFn(node, word, element, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) { if (!node.isWord) { node.isWord = true; this.size++; } // 设置或者覆盖 额外信息 node.element = element; return; } const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空就添加 if (!map.has(letterChar)) map.set(letterChar, new MyTrieNodeUpgrade(letterChar)); this.recursiveAddFn(map.get(letterChar), word, element, index + 1); } // 根据这个单词来获取额外信息 get(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return false; // 游标切换到这个节点 cur = node; } // 单词遍历完毕 if (cur.isWord) return cur.element; return null; } // 获取与这个单词前缀相关的 所有额外信息 getPrefixAll(prefix) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of prefix) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return null; // 游标切换到这个节点 cur = node; } // 前缀遍历完毕 说明这个前缀有单词与之匹配 // 开始进行获取与这个前缀相关的所有单词及其额外信息 // 将这些单词和额外信息以 {word1 : elemnt1, word2 : element2} 形式存储并返回 return this.recursiveGetPrefixAllInfo(cur, prefix, {}); } // 获取与这个单词前缀相关的 所有额外信息 递归算法 - recursiveGetPrefixAllInfo(node, word, result) { if (node.isWord) result[word] = node.element; const map = node.next; const keys = map.keys(); for (const key of keys) { this.recursiveGetPrefixAllInfo( map.get(key), word.concat(key), result ); } return result; } // 获取与这个单词前缀相关的 带有层次结构的所有额外信息 递归算法 - recursiveGetPrefixAllTreeInfo(node, word) { const result = []; if (node.isWord) result.push({ word: node.element }); const map = node.next; const keys = map.keys(); for (const key of keys) result.push( this.recursiveGetPrefixAll( map.get(key), word.concat(node.letterChar) ) ); return result; } // 查询单词word是否在Trie中 contains(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return false; // 游标切换到这个节点 cur = node; } // 单词遍历完毕 // 返回最后一个字符是否是一个单词的结尾 return cur.isWord; } // 查询单词word是否在Trie中 递归算法 recursiveContains(word) { return this.recursiveContainsFn(this.root, word, 0); } // 查询单词word是否在Trie中 递归赋值函数 - recursiveContainsFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空那么就说明这个单词没有进行存储 if (!map.has(letterChar)) return false; return this.recursiveContainsFn(map.get(letterChar), word, index + 1); } // 查询在Trie中是否有单词以 prefix 为前缀 isPrefix(prefix) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of prefix) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return false; // 游标切换到这个节点 cur = node; } // 前缀遍历完毕 说明这个前缀有单词与之匹配 return true; } // 正则表达式 查询单词word是否在Trie中,目前只支持 统配符 "." regexpSearch(regexpWord) { return this.match(this.root, regexpWord, 0); } // 正则表达式 匹配单词 递归算法 - match(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 判断这个字符是否是通配符 if (letterChar !== '.') { // 如果映射中不包含这个字符 if (!map.has(letterChar)) return false; // 如果映射中包含这个字符,那么就去找个字符对应的节点中继续匹配 return this.match(map.get(letterChar), word, index + 1); } else { // 遍历 下一个字符的集合 // 如果 从下一个字符继续匹配,只要匹配成功就返回 true for (const key of map.keys()) if (this.match(map.get(key), word, index + 1)) return true; // 遍历一遍之后还是没有匹配成功 那么就算匹配失败 return false; } } // 获取字典树中存储的单词数量 getSize() { return this.size; } // 获取字典树中是否为空 isEmpty() { return this.size === 0; } } 复制代码
-
MyTrieMap
// 自定义字典映射 TrieMap class MyTrieMap { constructor() { this.trie = new MyTrieUpgrade(); } // 添加操作 add(key, value) { this.trie.add(key, value); } // 查询操作 get(key) { return this.trie.get(key); } // 删除操作 remove(key) { return null; } // 查看key是否存在 contains(key) { return this.trie.contains(key); } // 更新操作 set(key, value) { this.trie.set(key, value); } // 获取映射Map中所有的key getKeys() { let items = this.trie.getPrefixAll(''); return Object.keys(items); } // 获取映射Map中所有的value getValues() { let items = this.trie.getPrefixAll(''); return Object.values(items); } // 获取映射Map中实际元素个数 getSize() { return this.trie.getSize(); } // 查看映射Map中是否为空 isEmpty() { return this.trie.isEmpty(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('Map Comparison Area'); const n = 2000000; const myBSTMap = new MyBinarySearchTreeMap(); const myTrieMap = new MyTrieMap(); let performanceTest1 = new PerformanceTest(); const random = Math.random; let arr = []; // 循环添加随机数的值 for (let i = 0; i < n; i++) { arr.push(i.toString()); } this.alterLine('MyBSTMap Comparison Area'); const myBSTMapInfo = performanceTest1.testCustomFn(function() { for (const word of arr) myBSTMap.add(word, String.fromCharCode(word)); }); // 总毫秒数:3692 console.log(myBSTMapInfo); this.show(myBSTMapInfo); this.alterLine('MyTrieMap Comparison Area'); const myTrieMapInfo = performanceTest1.testCustomFn(function() { for (const word of arr) myTrieMap.add(word, String.fromCharCode(word)); }); // 总毫秒数:2805 console.log(myTrieMapInfo); this.show(myTrieMapInfo); console.log(myTrieMap.getKeys()); // 有效 console.log(myTrieMap.getValues()); // 有效 } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
更多与 Trie 相关的话题
- 其实 Trie 还有一些话题值得去深入理解
- Trie 的删除操作
- 在大多数情况下,尤其是在竞赛中使用 Trie 的时候,
- 基本上都不会涉及到删除操作,
- 但是在实际的应用中去使用 Trie,很多时候需要涉及删除操作的。
- 例如实现一个通讯录,使用 Trie 创建这个通讯录的话,
- 其实就是实现一个 TrieMap,人名作为单词,
- 在这个单词的最后一个字母的位置存储相应的那个人的信息,
- 如 电话号码、邮编、家庭地址等等都是可以的。
- 如果使用 Trie 实现这样一个通讯录,相应就需要支持删除操作,
- 删除就是查询到某一个单词的最后一个字符的时候,
- 先将它的 isWord 设置为 false,表示这个单词要被删除了,
- 然后再自底向上的进行删除操作,
- 但是对于每一个节点来说,如果它的 next 为空,
- 那么就证明了它没有涉及到其它的单词,那么可以直接删除了,
- 否则就不能删除这个节点,从而这个节点以上的节点都不能删除,
- 那么只需要将那个单词的最后一个节点的 isWord 设置为 false 即可,
- 这个操作在你查询到这个单词最后一个字符的时候就是做了。
- Trie 总体都是在处理和字符串相关的问题
- 字符串本身是计算机科学领域研究的一个非常非常重要的一种数据的形式,
- 这是因为在具体使用计算机的时候,在大多数时候,其实都是在和字符串打交道,
- 无路是在编程的时候还是在网上文字聊天、搜索引擎搜索关键字、网页显示页面、
- 网页显示的源码等等这些都是字符串,所以字符串是在计算机科学中无处不在,
- 所以他也是一个被研究的非常深入的问题,在字符串领域有很多经典的问题,
- 其中最为经典的就是子串查询,验证某一个字符串是不是另外一个字符串的子串,
- 这个场景很常见,
- 例如浏览一个网页搜索网页中的关键词或者在 word 中搜索关键词等等,
- 其实都是在做子串查询,无论是网页还是 word 中的字符串,
- 查询的对象都是非常大的,无论是一整本网页还是一整本电子书,
- 它们非常的长,包含的字符非常多,所以高效的子串查询是非常有意义的,
- 经典的子串查询方法比如 KMP、Boyer-Moore、Rabin-Karp 等等算法,
- 它们是非常重要的。
- 更多字符串问题
- 字符串领域另一个很重要的问题,就是文件压缩,
- 其实不管你这个文件是什么文件,它背后都是 01 这种数字码,
- 如果你是以文本的形式打开他们,都是可以打开的,
- 也就是说每一个文件其实就是一个字符串,正因如此,
- 文件压缩算法的本质其实就是对一个超级长超级大的字符串进行压缩,
- 文件压缩相应的也有非常多的算法,最为基础的是哈夫曼算法,
- 哈夫曼算法本身也是建立了一棵树,在文件压缩领域还有更多现代的算法。
- 模式匹配
- 模式匹配本身就是也是字符串领域研究的一个非常重要的问题,
- 例如最常用的正则表达式,
- 如何实现一个高效的正则表达式这样的一个引擎,
- 其实就是在使用模式匹配这个领域相应的问题,
- 一个正则表达式的引擎其中所包含的和字符串相关的算法是非常多的,
- 是一个很综合的问题,可以尝试查找更多的资料了解模式匹配背后的很多算法,
- 甚至可能可以实现一个属于自己的小型的正则表达式这样的一个引擎。
- 编译原理
- 其实可以将正则表达式理解成一种程序,所有的程序如 java、c++、python,
- 都是一个字符串,编译器就做了一件非常伟大的事情,
- 它将你所写的程序这样的一个代码字符串进行了解析,
- 进而在计算机中进行了运行,那么这个过程本身是非常复杂的一个过程,
- 所以有一个专门的学科,
- 专门来研究这个过程以及这个过程中所涉及算法及相应的优化,
- 这个学科就叫做编译原理,其中就会有很多解决和字符串相关的问题,
- 编译原理本身又是一个更大的学科,有专门的教科书来专门讲解。
- DNA
- 对于字符串来说不仅仅是在计算机科学领域发挥着重大的作用,
- 在很多其它的领域也发挥着重大的作用,
- 最典型的例子就是在生活科学领域,它里面所研究的 DNA,
- 他本身就是一个超长的字符串,
- 对于 DNA 来说就是由四种嘌呤所组成的一个巨大的字符串,
- 很多生物问题甚至是医学医药方面的问题,
- 都是在这个巨大的字符串中去寻找模式及特殊的目标,
- 他们的本质都是字符串问题,
- 字符串相关的问题是一个非常重要的领域,
- 其中所包含算法非常非常的多,
- Trie 这种数据结构只是为某一些情况下存储字符串时,
- 高效的查询提供了一些方便而已。
Trie 的删除操作
- 自底向上就是模拟递归函数的回溯过程,在回溯的时候进行处理。
Trie 的局限性
- 如果有一种数据结构能够有那么大的优势有那么高的性能进行字符串的访问,
- 那么它就会有相应的劣势,可能这个世界通常都是很公平的,
- 这个劣势就是它相应要付出的代价,也就是空间,对于 Trie 来说,
- 它每一个 node 实际上只承载了一个字符的信息,而对于从这个字符到下一个字符,
- 需要使用一个 TreeMap 这样的映射 Map 来映射到下一个字符,
- 即使只涉及到 26 个字母表这样的一个字符空间,那么最多也需要存储 26 条记录,
- 那么需要的存储空间整体可以认为是原来的字符串所占的空间的 27 倍那么大,
- 那么空间的消耗其实是非常多的。
- 为了解决这个问题,相应的就有一些变种
- 最为典型的一种变种就叫做压缩字典树(Compressed Trie),
- 在使用 Trie 的过程种很多时候对于一个节点,他只有一个后续的字符,
- 在这种情况下这两个后续的字符完全可以合并起来(将多个字符合并成一个字符串),
- 如果单链上有多个字符,那么可以完全把它们合并在一起,
- 因为通过这个字符节点无法访问到其它的字符,只能访问到下一个字符,
- 如果有 pan 和 panda 这样的单词,那么就把这条单链合并成 pan 和 da,
- 这样合并的树就叫压缩字典树,对于压缩字典树来说,
- 显然空间进行了一定的节省,但是它的缺点是维护成本更加的高了,
- 因为你在压缩字典树中在添加一个单词,比如添加的这个单词是 park,
- 但是 pan 合在了一起,那么就需要对 pan 进行拆分的操作,拆成 pa 和 n 两部分,
- 只有这样才能把 park 这个单词加入进去,所以整体会更加复杂一些,
- 所以有得就有失,为了节省一定的空间,所以整体的操作就会更复杂一些,
- 相应的也会更加费时一些,这是一个平衡。
- 另外一个变种:
Ternary Search Trie
- Ternary 和 Binary 相对应,表示三叉也叫三分,
- 这是一个三分搜索树,
- 这三叉分别代表小于、等于、大于根节点的字符,
- 小于根节点字符的就放到左边去,
- 等于根节点字符的就放到中间来,
- 大于跟节点字符的就放到右侧去,
- 这样就创建了一个三分搜索树的字典树,
- 在这样的三分搜索树中,每一个节点都会有三条叉,
- 例如你在下图中,搜索 dog 这个词,首先搜索字母 d,
- 先从根节点开始找,根节点是 d,那么就找到了 d,
- 然后要继续搜索 o,之前找到了 d,那么就在 d 中间这个叉下去找,
- k 不是 o,那么就需要从 k 的三条叉中找,o 比 k 要大,
- 那么就到 k 的右子树中去找,找到了 o,
- 最后继续搜索 g,之前找到了 o,那么就在 o 中间这个叉下去找,
- i 不是 g,那么就需要在 i 的三条叉中去找,g 比 i 小,
- 那么就到 i 的左子树中去找,找到了 g,
- 至此就找到了 dog 这个单词了。
- 使用这种三分搜索树搜索的时间要比这个字母的总数要多的,因为你走到一个节点,
- 而这个节点并不是你所要找的那个单词的字符,
- 要通过判断当前这个节点的字母与要找的字母之间的大小关系,
- 从而才能进行一个转向,
- 不过这种三分搜索树它的
优点
就是每一个节点只有左中右三个孩子, - 而不像 Trie 那样用一个映射 Map 来表示它的孩子,如果你考虑 26 个字母,
- 那么它就有 26 个孩子,如果考虑大写字母那就有 52 个孩子,
- 如果再考虑一些特殊的符号,那么有可能一个节点有几十个孩子,
- 但是对于三分搜索树的 Trie 来说它只有可能有三个孩子,所以大大节省了空间,
- 但是代价就是相应的吸收了一定的时间,不过虽然吸收一定的时间,
- 三分搜索树上查找一个单词所用的时间依然是和这个单词中的字母数量成正比。
// d // / | \ // / | \ // / | \ // a k z // / | \ // / | \ // o // / | \ // / | \ // i // / | \ // / | \ // g 复制代码
字符串识别模式
- Trie 实际上是一种前缀树,相应还有一种树叫做后缀树,
- 后缀树在解决很多模式匹配的问题时候都有非常大的优势,
- 这个后缀树听起来叫做后缀树,
- 但是它并不是简单的把一个字符串给它倒序过来然后创建要给 Trie 就好了,
- 他有他自己的一种建构的方式,本身还是一种很巧妙的一种树结构。
代码示例
-
(class: MyTrieUpgrade, class: MyTrieMap, class: PerformanceTest, class: Main)
-
MyTrie
// 自定义字典树节点升级版 TrieNodeUpgrade class MyTrieNodeUpgrade { constructor(letterChar, element, isWord = false) { this.letterChar = letterChar; this.element = element; // 升级后可以存储特殊数据 this.isWord = isWord; // 是否是单词 this.next = new Map(); // 存储 字符所对应的节点的 字典映射 } } // 自定义字典树升级版 TrieUpgrade class MyTrieUpgrade { constructor() { this.root = new MyTrieNodeUpgrade(); this.size = 0; } add(word, element) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; // 存储 额外信息 cur.element = element; this.size++; } } // 向Trie中添加一个新的单词word 并且在word中存储额外的信息,如果额外信息存在就覆盖 put(word, element) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 下一个字符所对应的映射是否为空 if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c)); // 切换到下一个节点 cur = cur.next.get(c); } // 如果当前这个单词是一个新的单词 if (!cur.isWord) { // 当前这个字符是这个单词的结尾 cur.isWord = true; this.size++; } // 设置或者覆盖 额外信息 cur.element = element; } // 向Trie中添加一个新的单词word 递归算法 // 并且在word中存储额外的信息,如果额外信息存在就覆盖 recursivePut(word, element) { this.recursiveAddFn(this.root, word, element, 0); } // 向Trie中添加一个新的单词word 递归辅助函数 - // 并且在word中存储额外的信息,如果额外信息存在就覆盖 recursivePutFn(node, word, element, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) { if (!node.isWord) { node.isWord = true; this.size++; } // 设置或者覆盖 额外信息 node.element = element; return; } const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空就添加 if (!map.has(letterChar)) map.set(letterChar, new MyTrieNodeUpgrade(letterChar)); this.recursiveAddFn(map.get(letterChar), word, element, index + 1); } // 从Trie中删除一个单词word remove(word) { return this.recursiveRemove(this.root, word, 0); } // 从Trie中删除一个单词word 递归算法 - recursiveRemove(node, word, index) { let element = null; // 递归到底了 if (index === word.length) { // 如果不是一个单词,那么直接返回 为null的element if (!node.isWord) return element; element = node.element; node.isWord = false; this.size--; return element; } const map = node.next; const letterChar = word[index]; const nextNode = map.get(letterChar); if (map.has(letterChar)) element = this.recursiveRemove(nextNode, word, index + 1); if (element !== null) { if (!nextNode.isWord && nextNode.next.size === 0) map.delete(letterChar); } return element; } // 根据这个单词来获取额外信息 get(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return false; // 游标切换到这个节点 cur = node; } // 单词遍历完毕 if (cur.isWord) return cur.element; return null; } // 获取与这个单词前缀相关的 所有额外信息 getPrefixAll(prefix) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of prefix) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return null; // 游标切换到这个节点 cur = node; } // 前缀遍历完毕 说明这个前缀有单词与之匹配 // 开始进行获取与这个前缀相关的所有单词及其额外信息 // 将这些单词和额外信息以 {word1 : elemnt1, word2 : element2} 形式存储并返回 return this.recursiveGetPrefixAllInfo(cur, prefix, {}); } // 获取与这个单词前缀相关的 所有额外信息 递归算法 - recursiveGetPrefixAllInfo(node, word, result) { if (node.isWord) result[word] = node.element; const map = node.next; const keys = map.keys(); for (const key of keys) { this.recursiveGetPrefixAllInfo( map.get(key), word.concat(key), result ); } return result; } // 获取与这个单词前缀相关的 带有层次结构的所有额外信息 递归算法 - recursiveGetPrefixAllTreeInfo(node, word) { const result = []; if (node.isWord) result.push({ word: node.element }); const map = node.next; const keys = map.keys(); for (const key of keys) result.push( this.recursiveGetPrefixAll( map.get(key), word.concat(node.letterChar) ) ); return result; } // 查询单词word是否在Trie中 contains(word) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of word) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return false; // 游标切换到这个节点 cur = node; } // 单词遍历完毕 // 返回最后一个字符是否是一个单词的结尾 return cur.isWord; } // 查询单词word是否在Trie中 递归算法 recursiveContains(word) { return this.recursiveContainsFn(this.root, word, 0); } // 查询单词word是否在Trie中 递归赋值函数 - recursiveContainsFn(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 下一个字符所对应的映射是否为空 为空那么就说明这个单词没有进行存储 if (!map.has(letterChar)) return false; return this.recursiveContainsFn(map.get(letterChar), word, index + 1); } // 查询在Trie中是否有单词以 prefix 为前缀 isPrefix(prefix) { // 指定游标 let cur = this.root; // 遍历出当前单词的每一个字符 for (const c of prefix) { // 获取当前这个字符所对应的节点 const node = cur.next.get(c); // 这个节点不存在,那么就说明就没有存储这个字符 if (!node) return false; // 游标切换到这个节点 cur = node; } // 前缀遍历完毕 说明这个前缀有单词与之匹配 return true; } // 正则表达式 查询单词word是否在Trie中,目前只支持 统配符 "." regexpSearch(regexpWord) { return this.match(this.root, regexpWord, 0); } // 正则表达式 匹配单词 递归算法 - match(node, word, index) { // 解决基本的问题,因为已经到底了 if (index === word.length) return node.isWord; const map = node.next; // 获取节点的next 也就是字符对应的映射 const letterChar = word[index]; // 获取当前位置对应的单词中的字符 // 判断这个字符是否是通配符 if (letterChar !== '.') { // 如果映射中不包含这个字符 if (!map.has(letterChar)) return false; // 如果映射中包含这个字符,那么就去找个字符对应的节点中继续匹配 return this.match(map.get(letterChar), word, index + 1); } else { // 遍历 下一个字符的集合 // 如果 从下一个字符继续匹配,只要匹配成功就返回 true for (const key of map.keys()) if (this.match(map.get(key), word, index + 1)) return true; // 遍历一遍之后还是没有匹配成功 那么就算匹配失败 return false; } } // 获取字典树中存储的单词数量 getSize() { return this.size; } // 获取字典树中是否为空 isEmpty() { return this.size === 0; } } 复制代码
-
MyTrieMap
// 自定义字典映射 TrieMap class MyTrieMap { constructor() { this.trie = new MyTrieUpgrade(); } // 添加操作 add(key, value) { this.trie.add(key, value); } // 查询操作 get(key) { return this.trie.get(key); } // 删除操作 remove(key) { return this.trie.remove(key); } // 查看key是否存在 contains(key) { return this.trie.contains(key); } // 更新操作 set(key, value) { this.trie.set(key, value); } // 获取映射Map中所有的key getKeys() { let items = this.trie.getPrefixAll(''); return Object.keys(items); } // 获取映射Map中所有的value getValues() { let items = this.trie.getPrefixAll(''); return Object.values(items); } // 获取映射Map中实际元素个数 getSize() { return this.trie.getSize(); } // 查看映射Map中是否为空 isEmpty() { return this.trie.isEmpty(); } } 复制代码
-
PerformanceTest
// 性能测试 class PerformanceTest { constructor() {} // 对比队列 testQueue(queue, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { queue.enqueue(random() * openCount); } while (!queue.isEmpty()) { queue.dequeue(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比栈 testStack(stack, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { stack.push(random() * openCount); } while (!stack.isEmpty()) { stack.pop(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比集合 testSet(set, openCount) { let startTime = Date.now(); let random = Math.random; let arr = []; let temp = null; // 第一遍测试 for (var i = 0; i < openCount; i++) { temp = random(); // 添加重复元素,从而测试集合去重的能力 set.add(temp * openCount); set.add(temp * openCount); arr.push(temp * openCount); } for (var i = 0; i < openCount; i++) { set.remove(arr[i]); } // 第二遍测试 for (var i = 0; i < openCount; i++) { set.add(arr[i]); set.add(arr[i]); } while (!set.isEmpty()) { set.remove(arr[set.getSize() - 1]); } let endTime = Date.now(); // 求出两次测试的平均时间 let avgTime = Math.ceil((endTime - startTime) / 2); return this.calcTime(avgTime); } // 对比映射 testMap(map, openCount) { let startTime = Date.now(); let array = new MyArray(); let random = Math.random; let temp = null; let result = null; for (var i = 0; i < openCount; i++) { temp = random(); result = openCount * temp; array.add(result); array.add(result); array.add(result); array.add(result); } for (var i = 0; i < array.getSize(); i++) { result = array.get(i); if (map.contains(result)) map.add(result, map.get(result) + 1); else map.add(result, 1); } for (var i = 0; i < array.getSize(); i++) { result = array.get(i); map.remove(result); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比堆 主要对比 使用heapify 与 不使用heapify时的性能 testHeap(heap, array, isHeapify) { const startTime = Date.now(); // 是否支持 heapify if (isHeapify) heap.heapify(array); else { for (const element of array) heap.add(element); } console.log('heap size:' + heap.size() + '\r\n'); document.body.innerHTML += 'heap size:' + heap.size() + '
'; // 使用数组取值 let arr = new Array(heap.size()); for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax(); console.log( 'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n' ); document.body.innerHTML += 'Array size:' + arr.length + ',heap size:' + heap.size() + '
'; // 检验一下是否符合要求 for (let i = 1; i < arr.length; i++) if (arr[i - 1] < arr[i]) throw new Error('error.'); console.log('test heap completed.' + '\r\n'); document.body.innerHTML += 'test heap completed.' + '
'; const endTime = Date.now(); return this.calcTime(endTime - startTime); } // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒 calcTime(result) { //获取距离的天数 var day = Math.floor(result / (24 * 60 * 60 * 1000)); //获取距离的小时数 var hours = Math.floor((result / (60 * 60 * 1000)) % 24); //获取距离的分钟数 var minutes = Math.floor((result / (60 * 1000)) % 60); //获取距离的秒数 var seconds = Math.floor((result / 1000) % 60); //获取距离的毫秒数 var milliSeconds = Math.floor(result % 1000); // 计算时间 day = day < 10 ? '0' + day : day; hours = hours < 10 ? '0' + hours : hours; minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; milliSeconds = milliSeconds < 100 ? milliSeconds < 10 ? '00' + milliSeconds : '0' + milliSeconds : milliSeconds; // 输出耗时字符串 result = day + '天' + hours + '小时' + minutes + '分' + seconds + '秒' + milliSeconds + '毫秒' + ' <<<<============>>>> 总毫秒数:' + result; return result; } // 自定义对比 testCustomFn(fn) { let startTime = Date.now(); fn(); let endTime = Date.now(); return this.calcTime(endTime - startTime); } } 复制代码 -
Main
// main 函数 class Main { constructor() { this.alterLine('Map Comparison Area'); const n = 2000000; const myBSTMap = new MyBinarySearchTreeMap(); const myTrieMap = new MyTrieMap(); let performanceTest1 = new PerformanceTest(); const random = Math.random; let arr = []; // 循环添加随机数的值 for (let i = 0; i < n; i++) { arr.push(Math.floor(n * random()).toString()); } this.alterLine('MyBSTMap Comparison Area'); const myBSTMapInfo = performanceTest1.testCustomFn(function() { // 添加 for (const word of arr) myBSTMap.add(word, String.fromCharCode(word)); // 删除 for (const word of arr) myBSTMap.remove(word); // 查找 for (const word of arr) if (myBSTMap.contains(word)) throw new Error("doesn't remove ok."); }); // 总毫秒数:18703 console.log(myBSTMapInfo); this.show(myBSTMapInfo); this.alterLine('MyTrieMap Comparison Area'); const myTrieMapInfo = performanceTest1.testCustomFn(function() { for (const word of arr) myTrieMap.add(word, String.fromCharCode(word)); // 删除 for (const word of arr) myTrieMap.remove(word); // // 查找 for (const word of arr) if (myTrieMap.contains(word)) throw new Error("doesn't remove ok."); }); // 总毫秒数:8306 console.log(myTrieMapInfo); this.show(myTrieMapInfo); console.log(myTrieMap.getKeys()); // 有效 console.log(myTrieMap.getValues()); // 有效 } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码