data-structures
Version:
JavaScript data structures written in CoffeeScript.
184 lines (165 loc) • 5.3 kB
text/coffeescript
###
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.
###
# Needed for breath-first traversal for finding all words with a certain prefix.
Queue = require './Queue'
# Special value for marking end of word. No conflict possible, since every
# node's one letter.
WORD_END = 'end'
class Trie
constructor: (words = []) ->
###
Pass an optional array of strings to be inserted initially.
###
# Example structure for 'he', 'hello', 'za'
# h:
# e:
# end: true,
# l:
# l:
# o:
# end: true,
# z:
# a:
# end: true
@_root = {}
@size = 0
@add word for word in words
add: (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.
###
if not word? then return
@size++
currentNode = @_root
for letter in word
if not currentNode[letter]? then currentNode[letter] = {}
currentNode = currentNode[letter]
# Add the terminator. The value should be something defined, as to not cause
# search problems and so that we can validate using `if
# currentNode[WORD_END]` rather than `if currentNode[WORD_END]?`
currentNode[WORD_END] = yes
return word
has: (word) ->
###
__Returns:_ true or false.
###
if not word? then return no
currentNode = @_root
for letter in word
if not currentNode[letter]? then return no
currentNode = currentNode[letter]
# Check for null terminator
if currentNode[WORD_END] then yes
else no
longestPrefixOf: (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.
###
if not word? then return ''
currentNode = @_root
prefix = ''
for letter in word
if not currentNode[letter]? then break
prefix += letter
currentNode = currentNode[letter]
return prefix
wordsWithPrefix: (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.
###
if not prefix? then return []
# Can't set default value to parameter. It turns undefined and null into ''.
prefix? or prefix = ''
words = []
currentNode = @_root
for letter in prefix
currentNode = currentNode[letter]
if not currentNode? then return []
# Reached the end of prefix.
# Enqueue like this: [node, accumulatedLetters]
queue = new Queue()
queue.enqueue [currentNode, '']
while queue.size isnt 0
[node, accumulatedLetters] = queue.dequeue()
if node[WORD_END]
words.push prefix + accumulatedLetters
for letter, subNode of node
queue.enqueue [subNode, accumulatedLetters + letter]
return words
remove: (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`.
###
if not word? then return
currentNode = @_root
prefix = []
for letter in word
if not currentNode[letter]? then return
currentNode = currentNode[letter]
prefix.push [letter, currentNode]
# Check for null terminator
if not currentNode[WORD_END] then return
# Traverse back upward to delete nodes. Last node in prefix is the before-
# last letter right now.
@size--
delete currentNode[WORD_END]
if _hasAtLeastNChildren currentNode, 1 then return word
for i in [prefix.length - 1..1]
if not _hasAtLeastNChildren prefix[i][1], 1
# previousNode[currentLetter]
delete prefix[i - 1][1][prefix[i][0]]
else break
# Clean the ndoe at the root itself.
if not _hasAtLeastNChildren @_root[prefix[0][0]], 1
delete @_root[prefix[0][0]]
return word
_hasAtLeastNChildren = (node, n) ->
return yes if n is 0
childCount = 0
for child of node
childCount++
return yes if childCount >= n
return no
module.exports = Trie