UNPKG

doubly-linked-list-typed

Version:
594 lines (593 loc) 23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Trie = exports.TrieNode = void 0; const base_1 = require("../base"); class TrieNode { constructor(key) { this._key = key; this._isEnd = false; this._children = new Map(); } /** * The function returns the value of the protected variable _key. * @returns The value of the `_key` property, which is a string. */ get key() { return this._key; } /** * The above function sets the value of a protected variable called "key". * @param {string} value - The value parameter is a string that represents the value to be assigned * to the key. */ set key(value) { this._key = value; } /** * The function returns the children of a TrieNode as a Map. * @returns The `children` property of the TrieNode object, which is a Map containing string keys and * TrieNode values. */ get children() { return this._children; } /** * The function sets the value of the `_children` property of a TrieNode object. * @param value - The value parameter is a Map object that represents the children of a TrieNode. The * keys of the map are strings, which represent the characters that are associated with each child * TrieNode. The values of the map are TrieNode objects, which represent the child nodes of the * current TrieNode. */ set children(value) { this._children = value; } /** * The function returns a boolean value indicating whether a certain condition is met. * @returns The method is returning a boolean value, specifically the value of the variable `_isEnd`. */ get isEnd() { return this._isEnd; } /** * The function sets the value of the "_isEnd" property. * @param {boolean} value - The value parameter is a boolean value that indicates whether the current * state is the end state or not. */ set isEnd(value) { this._isEnd = value; } } exports.TrieNode = TrieNode; /** * 1. Node Structure: Each node in a Trie represents a string (or a part of a string). The root node typically represents an empty string. * 2. Child Node Relationship: Each node's children represent the strings that can be formed by adding one character to the string at the current node. For example, if a node represents the string 'ca', one of its children might represent 'cat'. * 3. Fast Retrieval: Trie allows retrieval in O(m) time complexity, where m is the length of the string to be searched. * 4. Space Efficiency: Trie can store a large number of strings very space-efficiently, especially when these strings share common prefixes. * 5. Autocomplete and Prediction: Trie can be used for implementing autocomplete and word prediction features, as it can quickly find all strings with a common prefix. * 6. Sorting: Trie can be used to sort a set of strings in alphabetical order. * 7. String Retrieval: For example, searching for a specific string in a large set of strings. * 8. Autocomplete: Providing recommended words or phrases as a user types. * 9. Spell Check: Checking the spelling of words. * 10. IP Routing: Used in certain types of IP routing algorithms. * 11. Text Word Frequency Count: Counting and storing the frequency of words in a large amount of text data. * @example * // Autocomplete: Prefix validation and checking * const autocomplete = new Trie<string>(['gmail.com', 'gmail.co.nz', 'gmail.co.jp', 'yahoo.com', 'outlook.com']); * * // Get all completions for a prefix * const gmailCompletions = autocomplete.getWords('gmail'); * console.log(gmailCompletions); // ['gmail.com', 'gmail.co.nz', 'gmail.co.jp'] * @example * // File System Path Operations * const fileSystem = new Trie<string>([ * '/home/user/documents/file1.txt', * '/home/user/documents/file2.txt', * '/home/user/pictures/photo.jpg', * '/home/user/pictures/vacation/', * '/home/user/downloads' * ]); * * // Find common directory prefix * console.log(fileSystem.getLongestCommonPrefix()); // '/home/user/' * * // List all files in a directory * const documentsFiles = fileSystem.getWords('/home/user/documents/'); * console.log(documentsFiles); // ['/home/user/documents/file1.txt', '/home/user/documents/file2.txt'] * @example * // Autocomplete: Basic word suggestions * // Create a trie for autocomplete * const autocomplete = new Trie<string>([ * 'function', * 'functional', * 'functions', * 'class', * 'classes', * 'classical', * 'closure', * 'const', * 'constructor' * ]); * * // Test autocomplete with different prefixes * console.log(autocomplete.getWords('fun')); // ['functional', 'functions', 'function'] * console.log(autocomplete.getWords('cla')); // ['classes', 'classical', 'class'] * console.log(autocomplete.getWords('con')); // ['constructor', 'const'] * * // Test with non-matching prefix * console.log(autocomplete.getWords('xyz')); // [] * @example * // Dictionary: Case-insensitive word lookup * // Create a case-insensitive dictionary * const dictionary = new Trie<string>([], { caseSensitive: false }); * * // Add words with mixed casing * dictionary.add('Hello'); * dictionary.add('WORLD'); * dictionary.add('JavaScript'); * * // Test lookups with different casings * console.log(dictionary.has('hello')); // true * console.log(dictionary.has('HELLO')); // true * console.log(dictionary.has('Hello')); // true * console.log(dictionary.has('javascript')); // true * console.log(dictionary.has('JAVASCRIPT')); // true * @example * // IP Address Routing Table * // Add IP address prefixes and their corresponding routes * const routes = { * '192.168.1': 'LAN_SUBNET_1', * '192.168.2': 'LAN_SUBNET_2', * '10.0.0': 'PRIVATE_NETWORK_1', * '10.0.1': 'PRIVATE_NETWORK_2' * }; * * const ipRoutingTable = new Trie<string>(Object.keys(routes)); * * // Check IP address prefix matching * console.log(ipRoutingTable.hasPrefix('192.168.1')); // true * console.log(ipRoutingTable.hasPrefix('192.168.2')); // true * * // Validate IP address belongs to subnet * const ip = '192.168.1.100'; * const subnet = ip.split('.').slice(0, 3).join('.'); * console.log(ipRoutingTable.hasPrefix(subnet)); // true */ class Trie extends base_1.IterableElementBase { /** * The constructor initializes a Trie data structure with optional options and words provided as * input. * @param {Iterable<string> | Iterable<R>} words - The `words` parameter in the constructor is an * iterable containing either strings or elements of type `R`. It is used to initialize the Trie with * a list of words or elements. If no `words` are provided, an empty iterable is used as the default * value. * @param [options] - The `options` parameter in the constructor is an optional object that can * contain configuration options for the Trie data structure. One of the options it can have is * `caseSensitive`, which is a boolean value indicating whether the Trie should be case-sensitive or * not. If `caseSensitive` is set to ` */ constructor(words = [], options) { super(options); this._size = 0; this._caseSensitive = true; this._root = new TrieNode(''); if (options) { const { caseSensitive } = options; if (caseSensitive !== undefined) this._caseSensitive = caseSensitive; } if (words) { this.addMany(words); } } /** * The size function returns the size of the stack. * @return The number of elements in the list */ get size() { return this._size; } /** * The caseSensitive function is a getter that returns the value of the protected _caseSensitive property. * @return The value of the _caseSensitive protected variable */ get caseSensitive() { return this._caseSensitive; } /** * The root function returns the root node of the tree. * @return The root node */ get root() { return this._root; } /** * Time Complexity: O(l), where l is the length of the word being added. * Space Complexity: O(l) - Each character in the word adds a TrieNode. * * Add a word to the Trie structure. * @param {string} word - The word to add. * @returns {boolean} True if the word was successfully added. */ add(word) { word = this._caseProcess(word); let cur = this.root; let isNewWord = false; for (const c of word) { let nodeC = cur.children.get(c); if (!nodeC) { nodeC = new TrieNode(c); cur.children.set(c, nodeC); } cur = nodeC; } if (!cur.isEnd) { isNewWord = true; cur.isEnd = true; this._size++; } return isNewWord; } /** * Time Complexity: O(n * l) * Space Complexity: O(1) * * The `addMany` function in TypeScript takes an iterable of strings or elements of type R, converts * them using a provided function if available, and adds them to a data structure while returning an * array of boolean values indicating success. * @param {Iterable<string> | Iterable<R>} words - The `words` parameter in the `addMany` function is * an iterable that contains either strings or elements of type `R`. * @returns The `addMany` method returns an array of boolean values indicating whether each word in * the input iterable was successfully added to the data structure. */ addMany(words) { const ans = []; for (const word of words) { if (this.toElementFn) { ans.push(this.add(this.toElementFn(word))); } else { ans.push(this.add(word)); } } return ans; } /** * Time Complexity: O(l), where l is the length of the input word. * Space Complexity: O(1) - Constant space. * * Check if the Trie contains a given word. * @param {string} word - The word to check for. * @returns {boolean} True if the word is present in the Trie. */ has(word) { word = this._caseProcess(word); let cur = this.root; for (const c of word) { const nodeC = cur.children.get(c); if (!nodeC) return false; cur = nodeC; } return cur.isEnd; } /** * Time Complexity: O(1) * Space Complexity: O(1) * * The isEmpty function checks if the size of the queue is 0. * @return True if the size of the queue is 0 */ isEmpty() { return this._size === 0; } /** * Time Complexity: O(1) * Space Complexity: O(1) * * The clear function resets the size of the Trie to 0 and creates a new root TrieNode. */ clear() { this._size = 0; this._root = new TrieNode(''); } /** * Time Complexity: O(l), where l is the length of the word being deleted. * Space Complexity: O(n) - Due to the recursive DFS approach. * * Remove a word from the Trie structure. * @param{string} word - The word to delete. * @returns {boolean} True if the word was successfully removed. */ delete(word) { word = this._caseProcess(word); let isDeleted = false; const dfs = (cur, i) => { const char = word[i]; const child = cur.children.get(char); if (child) { if (i === word.length - 1) { if (child.isEnd) { if (child.children.size > 0) { child.isEnd = false; } else { cur.children.delete(char); } isDeleted = true; return true; } return false; } const res = dfs(child, i + 1); if (res && !cur.isEnd && child.children.size === 0) { cur.children.delete(char); return true; } return false; } return false; }; dfs(this.root, 0); if (isDeleted) { this._size--; } return isDeleted; } /** * Time Complexity: O(n) * Space Complexity: O(1) * * The function `getHeight` calculates the height of a trie data structure starting from the root * node. * @returns The `getHeight` method returns the maximum depth or height of the trie tree starting from * the root node. It calculates the depth using a breadth-first search (BFS) traversal of the trie * tree and returns the maximum depth found. */ getHeight() { const startNode = this.root; let maxDepth = 0; if (startNode) { const bfs = (node, level) => { if (level > maxDepth) { maxDepth = level; } const { children } = node; if (children) { for (const child of children.entries()) { bfs(child[1], level + 1); } } }; bfs(startNode, 0); } return maxDepth; } /** * Time Complexity: O(l), where l is the length of the input prefix. * Space Complexity: O(1) - Constant space. * * Check if a given input string has an absolute prefix in the Trie, meaning it's not a complete word. * @param {string} input - The input string to check. * @returns {boolean} True if it's an absolute prefix in the Trie. */ hasPurePrefix(input) { input = this._caseProcess(input); let cur = this.root; for (const c of input) { const nodeC = cur.children.get(c); if (!nodeC) return false; cur = nodeC; } return !cur.isEnd; } /** * Time Complexity: O(l), where l is the length of the input prefix. * Space Complexity: O(1) - Constant space. * * Check if a given input string is a prefix of any existing word in the Trie, whether as an absolute prefix or a complete word. * @param {string} input - The input string representing the prefix to check. * @returns {boolean} True if it's a prefix in the Trie. */ hasPrefix(input) { input = this._caseProcess(input); let cur = this.root; for (const c of input) { const nodeC = cur.children.get(c); if (!nodeC) return false; cur = nodeC; } return true; } /** * Time Complexity: O(n), where n is the total number of nodes in the trie. * Space Complexity: O(l), where l is the length of the input prefix. * * Check if the input string is a common prefix in the Trie, meaning it's a prefix shared by all words in the Trie. * @param {string} input - The input string representing the common prefix to check for. * @returns {boolean} True if it's a common prefix in the Trie. */ hasCommonPrefix(input) { input = this._caseProcess(input); let commonPre = ''; const dfs = (cur) => { commonPre += cur.key; if (commonPre === input) return; if (cur.isEnd) return; if (cur && cur.children && cur.children.size === 1) dfs(Array.from(cur.children.values())[0]); else return; }; dfs(this.root); return commonPre === input; } /** * Time Complexity: O(n), where n is the total number of nodes in the trie. * Space Complexity: O(l), where l is the length of the longest common prefix. * * Get the longest common prefix among all the words stored in the Trie. * @returns {string} The longest common prefix found in the Trie. */ getLongestCommonPrefix() { let commonPre = ''; const dfs = (cur) => { commonPre += cur.key; if (cur.isEnd) return; if (cur && cur.children && cur.children.size === 1) dfs(Array.from(cur.children.values())[0]); else return; }; dfs(this.root); return commonPre; } /** * Time Complexity: O(w * l), where w is the number of words retrieved, and l is the average length of the words. * Space Complexity: O(w * l) - The space required for the output array. * * The `getAll` function returns an array of all words in a Trie data structure that start with a given prefix. * @param {string} prefix - The `prefix` parameter is a string that represents the prefix that we want to search for in the * trie. It is an optional parameter, so if no prefix is provided, it will default to an empty string. * @param {number} max - The max count of words will be found * @param isAllWhenEmptyPrefix - If true, when the prefix provided as '', returns all the words in the trie. * @returns {string[]} an array of strings. */ getWords(prefix = '', max = Number.MAX_SAFE_INTEGER, isAllWhenEmptyPrefix = false) { prefix = this._caseProcess(prefix); const words = []; let found = 0; function dfs(node, word) { for (const char of node.children.keys()) { const charNode = node.children.get(char); if (charNode !== undefined) { dfs(charNode, word.concat(char)); } } if (node.isEnd) { if (found > max - 1) return; words.push(word); found++; } } let startNode = this.root; if (prefix) { for (const c of prefix) { const nodeC = startNode.children.get(c); if (nodeC) { startNode = nodeC; } else { // Early return if the whole prefix is not found return []; } } } if (isAllWhenEmptyPrefix || startNode !== this.root) dfs(startNode, prefix); return words; } /** * Time Complexity: O(n) * Space Complexity: O(n) * * The `clone` function returns a new instance of the Trie class with the same values and case * sensitivity as the original Trie. * @returns A new instance of the Trie class is being returned. */ clone() { return new Trie(this, { caseSensitive: this.caseSensitive, toElementFn: this.toElementFn }); } /** * Time Complexity: O(n) * Space Complexity: O(n) * * The `filter` function takes a predicate function and returns a new array containing all the * elements for which the predicate function returns true. * @param predicate - The `predicate` parameter is a callback function that takes three arguments: * `word`, `index`, and `this`. It should return a boolean value indicating whether the current * element should be included in the filtered results or not. * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that allows you to * specify the value of `this` within the `predicate` function. It is used when you want to bind a * specific object as the context for the `predicate` function. If `thisArg` is provided, it will be * @returns The `filter` method is returning an array of strings (`string[]`). */ filter(predicate, thisArg) { const results = new Trie([], { toElementFn: this.toElementFn, caseSensitive: this.caseSensitive }); let index = 0; for (const word of this) { if (predicate.call(thisArg, word, index, this)) { results.add(word); } index++; } return results; } /** * Time Complexity: O(n) * Space Complexity: O(n) * * The `map` function creates a new Trie by applying a callback function to each element in the * current Trie. * @param callback - The callback parameter is a function that will be called for each element in the * Trie. It takes four arguments: * @param [toElementFn] - The `toElementFn` parameter is an optional function that can be used to * convert the raw element (`RM`) into a string representation. This can be useful if the raw element * is not already a string or if you want to customize how the element is converted into a string. If * this parameter is * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that allows you to * specify the value of `this` within the callback function. It is used to set the context or scope * in which the callback function will be executed. If `thisArg` is provided, it will be used as the * value of * @returns a new Trie object. */ map(callback, toElementFn, thisArg) { const newTrie = new Trie([], { toElementFn, caseSensitive: this.caseSensitive }); let index = 0; for (const word of this) { newTrie.add(callback.call(thisArg, word, index, this)); index++; } return newTrie; } /** * Time Complexity: O(n) * Space Complexity: O(n) * * The function `_getIterator` returns an iterable iterator that performs a depth-first search on a * trie data structure and yields all the paths to the end nodes. */ *_getIterator() { function* _dfs(node, path) { if (node.isEnd) { yield path; } for (const [char, childNode] of node.children) { yield* _dfs(childNode, path + char); } } yield* _dfs(this.root, ''); } get _total() { return this._size; } /** * Time Complexity: O(l), where l is the length of the input string. * Space Complexity: O(1) - Constant space. * * @param str * @protected */ _caseProcess(str) { if (!this._caseSensitive) { str = str.toLowerCase(); // Convert str to lowercase if case-insensitive } return str; } } exports.Trie = Trie;