data-structures
Version:
JavaScript data structures written in CoffeeScript.
252 lines (214 loc) • 6.29 kB
JavaScript
/*
Good for fast insertion/removal/lookup of strings.
## Overview example:
```js
var trie = new Trie(['bear', 'beer']);
trie.add('hello'); // => 'hello'
trie.add('helloha!'); // => 'helloha!'
trie.has('bears'); // => false
trie.longestPrefixOf('beatrice'); // => 'bea'
trie.wordsWithPrefix('hel'); // => ['hello', 'helloha!']
trie.remove('beers'); // => undefined. 'beer' still exists
trie.remove('Beer') // => undefined. Case-sensitive
trie.remove('beer') // => 'beer'. Removed
```
## Properties:
- size: The total number of words.
*/
(function() {
var Queue, Trie, WORD_END, _hasAtLeastNChildren;
Queue = require('./Queue');
WORD_END = 'end';
Trie = (function() {
function Trie(words) {
var word, _i, _len;
if (words == null) {
words = [];
}
/*
Pass an optional array of strings to be inserted initially.
*/
this._root = {};
this.size = 0;
for (_i = 0, _len = words.length; _i < _len; _i++) {
word = words[_i];
this.add(word);
}
}
Trie.prototype.add = function(word) {
/*
Add a whole string to the trie.
_Returns:_ the word added. Will return undefined (without adding the value)
if the word passed is null or undefined.
*/
var currentNode, letter, _i, _len;
if (word == null) {
return;
}
this.size++;
currentNode = this._root;
for (_i = 0, _len = word.length; _i < _len; _i++) {
letter = word[_i];
if (currentNode[letter] == null) {
currentNode[letter] = {};
}
currentNode = currentNode[letter];
}
currentNode[WORD_END] = true;
return word;
};
Trie.prototype.has = function(word) {
/*
__Returns:_ true or false.
*/
var currentNode, letter, _i, _len;
if (word == null) {
return false;
}
currentNode = this._root;
for (_i = 0, _len = word.length; _i < _len; _i++) {
letter = word[_i];
if (currentNode[letter] == null) {
return false;
}
currentNode = currentNode[letter];
}
if (currentNode[WORD_END]) {
return true;
} else {
return false;
}
};
Trie.prototype.longestPrefixOf = function(word) {
/*
Find all words containing the prefix. The word itself counts as a prefix.
```js
var trie = new Trie;
trie.add('hello');
trie.longestPrefixOf('he'); // 'he'
trie.longestPrefixOf('hello'); // 'hello'
trie.longestPrefixOf('helloha!'); // 'hello'
```
_Returns:_ the prefix string, or empty string if no prefix found.
*/
var currentNode, letter, prefix, _i, _len;
if (word == null) {
return '';
}
currentNode = this._root;
prefix = '';
for (_i = 0, _len = word.length; _i < _len; _i++) {
letter = word[_i];
if (currentNode[letter] == null) {
break;
}
prefix += letter;
currentNode = currentNode[letter];
}
return prefix;
};
Trie.prototype.wordsWithPrefix = function(prefix) {
/*
Find all words containing the prefix. The word itself counts as a prefix.
**Watch out for edge cases.**
```js
var trie = new Trie;
trie.wordsWithPrefix(''); // []. Check later case below.
trie.add('');
trie.wordsWithPrefix(''); // ['']
trie.add('he');
trie.add('hello');
trie.add('hell');
trie.add('bear');
trie.add('z');
trie.add('zebra');
trie.wordsWithPrefix('hel'); // ['hell', 'hello']
```
_Returns:_ an array of strings, or empty array if no word found.
*/
var accumulatedLetters, currentNode, letter, node, queue, subNode, words, _i, _len, _ref;
if (prefix == null) {
return [];
}
(prefix != null) || (prefix = '');
words = [];
currentNode = this._root;
for (_i = 0, _len = prefix.length; _i < _len; _i++) {
letter = prefix[_i];
currentNode = currentNode[letter];
if (currentNode == null) {
return [];
}
}
queue = new Queue();
queue.enqueue([currentNode, '']);
while (queue.size !== 0) {
_ref = queue.dequeue(), node = _ref[0], accumulatedLetters = _ref[1];
if (node[WORD_END]) {
words.push(prefix + accumulatedLetters);
}
for (letter in node) {
subNode = node[letter];
queue.enqueue([subNode, accumulatedLetters + letter]);
}
}
return words;
};
Trie.prototype.remove = function(word) {
/*
_Returns:_ the string removed, or undefined if the word in its whole doesn't
exist. **Note:** this means removing `beers` when only `beer` exists will
return undefined and conserve `beer`.
*/
var currentNode, i, letter, prefix, _i, _j, _len, _ref;
if (word == null) {
return;
}
currentNode = this._root;
prefix = [];
for (_i = 0, _len = word.length; _i < _len; _i++) {
letter = word[_i];
if (currentNode[letter] == null) {
return;
}
currentNode = currentNode[letter];
prefix.push([letter, currentNode]);
}
if (!currentNode[WORD_END]) {
return;
}
this.size--;
delete currentNode[WORD_END];
if (_hasAtLeastNChildren(currentNode, 1)) {
return word;
}
for (i = _j = _ref = prefix.length - 1; _ref <= 1 ? _j <= 1 : _j >= 1; i = _ref <= 1 ? ++_j : --_j) {
if (!_hasAtLeastNChildren(prefix[i][1], 1)) {
delete prefix[i - 1][1][prefix[i][0]];
} else {
break;
}
}
if (!_hasAtLeastNChildren(this._root[prefix[0][0]], 1)) {
delete this._root[prefix[0][0]];
}
return word;
};
return Trie;
})();
_hasAtLeastNChildren = function(node, n) {
var child, childCount;
if (n === 0) {
return true;
}
childCount = 0;
for (child in node) {
childCount++;
if (childCount >= n) {
return true;
}
}
return false;
};
module.exports = Trie;
}).call(this);