字典树
在计算机科学中, 字典树(trie,中文又被称为”单词查找树“或 ”键树“), 也称为数字树,有时候也被称为基数树或前缀树(因为它们可以通过前缀搜索),它是一种搜索树--一种已排序的数据结构,通常用于存储动态集或键为字符串的关联数组。
与二叉搜索树不同, 树上没有节点存储与该节点关联的键; 相反,节点在树上的位置定义了与之关联的键。一个节点的全部后代节点都有一个与该节点关联的通用的字符串前缀, 与根节点关联的是空字符串。
值对于字典树中关联的节点来说,不是必需的,相反,值往往和相关的叶子相关,以及与一些键相关的内部节点相关。
有关字典树的空间优化示意,请参阅紧凑前缀树。
代码实现
TrieNode.js
import HashTable from '../hash-table/HashTable'; export default class TrieNode {//字典树节点类 /** * @param {string} character * @param {boolean} isCompleteWord */ constructor(character, isCompleteWord = false) { this.character = character;//当前字典树节点的字母 this.isCompleteWord = isCompleteWord;//是否是一个完整的单词 this.children = new HashTable();//children子节点,新的哈希表 } /** * @param {string} character * @return {TrieNode} */ getChild(character) {//从children哈希表中获取指定字母 return this.children.get(character); } /** * @param {string} character * @param {boolean} isCompleteWord * @return {TrieNode} */ addChild(character, isCompleteWord = false) {//给当前节点添加新的子节点 if (!this.children.has(character)) { //如果children哈希表属性里没有这个字母,就用哈希表的set方法添加 //key是字母,value是这个字母新建的字典树节点 this.children.set(character, new TrieNode(character, isCompleteWord)); } const childNode = this.children.get(character); //如果children哈希表中已经有这个字母了,获取到这个字典树节点 // In cases similar to adding "car" after "carpet" we need to mark "r" character as complete. childNode.isCompleteWord = childNode.isCompleteWord || isCompleteWord; //更新它的isCompleteWord属性 return childNode;//返回这个新字典树节点 } /** * @param {string} character * @return {TrieNode} */ removeChild(character) {//删除当前节点指定字母的子节点 const childNode = this.getChild(character);//从children哈希表中获取指定字母的子节点 // Delete childNode only if: // - childNode has NO children, // - childNode.isCompleteWord === false. if ( childNode && !childNode.isCompleteWord && !childNode.hasChildren() ) {//如果存在childNode,并且childNode不是完整单词,并且childNode没有子节点 this.children.delete(character);//满足上述条件就可以删除 } return this; } /** * @param {string} character * @return {boolean} */ hasChild(character) {//判断当前字典表节点有没有指定字母的子节点 return this.children.has(character); } /** * Check whether current TrieNode has children or not. * @return {boolean} */ hasChildren() {//判断当前字典表节点有没有子节点 return this.children.getKeys().length !== 0; } /** * @return {string[]} */ suggestChildren() {//返回当前字典表节点的所有子节点的字母组成的数组 return [...this.children.getKeys()]; } /** * @return {string} */ toString() { let childrenAsString = this.suggestChildren().toString();//当前字典表节点的所有子节点的字母的数组的字符串形式 childrenAsString = childrenAsString ? `:${childrenAsString}` : ''; const isCompleteString = this.isCompleteWord ? '*' : '';//当前字典表节点是否是完整单词属性的字符串值 return `${this.character}${isCompleteString}${childrenAsString}`; //返回当前字典表节点的字母,是否是完整单词字符串值,和所有子节点字母值连起来的字符串 } }
Trie.js
import TrieNode from './TrieNode'; // Character that we will use for trie tree root. const HEAD_CHARACTER = '*';//根节点不包含可用字符,存一个星号 export default class Trie {//字典树类 constructor() { this.head = new TrieNode(HEAD_CHARACTER);//头指针指向根节点,是一个不存可用字符的节点 } /** * @param {string} word * @return {Trie} */ addWord(word) {//往字典树里添加一个单词 const characters = Array.from(word);//将单词转变成一个数组,数组的每个元素都是字符串的一个字符 let currentNode = this.head;//当前节点是根节点 for (let charIndex = 0; charIndex < characters.length; charIndex += 1) { //循环单词的长度,如果循环到单词的最后一个字母,isComplete属性就是true,否则是false const isComplete = charIndex === characters.length - 1; currentNode = currentNode.addChild(characters[charIndex], isComplete); //为当前节点添加子节点,并将子节点赋值为当前节点然后继续循环添加下一个字母 } return this; } /** * @param {string} word * @return {Trie} */ deleteWord(word) {//删除单词 const depthFirstDelete = (currentNode, charIndex = 0) => { //currentNode删除的起始节点,charIndex当前节点字母作为一个单词的索引 if (charIndex >= word.length) {//如果索引已经超出单词长度了,返回undefined // Return if we're trying to delete the character that is out of word's scope. return; } const character = word[charIndex];//当前要删除的字母 const nextNode = currentNode.getChild(character);//获取起始节点的子节点,对应要删除的当前字母 if (nextNode == null) {//如果获取不到子节点,说明字典树中没有这个单词,返回undefined // Return if we're trying to delete a word that has not been added to the Trie. return; } // Go deeper. depthFirstDelete(nextNode, charIndex + 1);//递归调用,再继续寻找下一个字母对应的深层节点 // Since we're going to delete a word let's un-mark its last character isCompleteWord flag. if (charIndex === (word.length - 1)) { //如果找到了单词的最后一个字母,取消标记最后一个字母的isCompleteWord标记 nextNode.isCompleteWord = false; } // childNode is deleted only if: // - childNode has NO children // - childNode.isCompleteWord === false currentNode.removeChild(character);//从倒数第二个节点开始删除,递归删除 }; // Start depth-first deletion from the head node. depthFirstDelete(this.head);//从根节点开始深度删除程序 return this; } /** * @param {string} word * @return {string[]} */ suggestNextCharacters(word) { const lastCharacter = this.getLastCharacterNode(word);//获取单词对应的最后一个节点 if (!lastCharacter) {//如果没有单词对应的最后一个节点,返回null return null; } return lastCharacter.suggestChildren(); //获取当前单词最后一个字母对应节点的所有子节点的数组,也就是建议的下一个字母 } /** * Check if complete word exists in Trie. * * @param {string} word * @return {boolean} */ doesWordExist(word) {//判断字典树种是否存在某个单词 const lastCharacter = this.getLastCharacterNode(word);//获取到这个单词最后一个节点 return !!lastCharacter && lastCharacter.isCompleteWord; //如果有最后一个节点且isCompleteWord为true,说明此单词存在 } /** * @param {string} word * @return {TrieNode} */ getLastCharacterNode(word) {//获取一个单词的最后一个字母节点 const characters = Array.from(word);//将单词转变成一个数组,数组的每个元素都是字符串的一个字符 let currentNode = this.head;//当前节点从根节点开始 for (let charIndex = 0; charIndex < characters.length; charIndex += 1) { //循环单词数组,开始从根节点开始寻找 if (!currentNode.hasChild(characters[charIndex])) { //如果当前节点没有对应的单词的下一个字母的节点,说明字典树里没有这个单词,返回null return null; } currentNode = currentNode.getChild(characters[charIndex]); //如果有就继续往后查找,直到找到最后一个字母对应的节点 } return currentNode; } }