You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					181 lines
				
				3.2 KiB
			
		
		
			
		
	
	
					181 lines
				
				3.2 KiB
			| 
											3 years ago
										 | /* eslint no-constant-condition: 0 */ | ||
|  | /** | ||
|  |  * Mnemonist BK Tree | ||
|  |  * ================== | ||
|  |  * | ||
|  |  * Implementation of a Burkhard-Keller tree, allowing fast lookups of words | ||
|  |  * that lie within a specified distance of the query word. | ||
|  |  * | ||
|  |  * [Reference]: | ||
|  |  * https://en.wikipedia.org/wiki/BK-tree
 | ||
|  |  * | ||
|  |  * [Article]: | ||
|  |  * W. Burkhard and R. Keller. Some approaches to best-match file searching, | ||
|  |  * CACM, 1973 | ||
|  |  */ | ||
|  | var forEach = require('obliterator/foreach'); | ||
|  | 
 | ||
|  | /** | ||
|  |  * BK Tree. | ||
|  |  * | ||
|  |  * @constructor | ||
|  |  * @param {function} distance - Distance function to use. | ||
|  |  */ | ||
|  | function BKTree(distance) { | ||
|  | 
 | ||
|  |   if (typeof distance !== 'function') | ||
|  |     throw new Error('mnemonist/BKTree.constructor: given `distance` should be a function.'); | ||
|  | 
 | ||
|  |   this.distance = distance; | ||
|  |   this.clear(); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to add an item to the tree. | ||
|  |  * | ||
|  |  * @param  {any} item - Item to add. | ||
|  |  * @return {BKTree} | ||
|  |  */ | ||
|  | BKTree.prototype.add = function(item) { | ||
|  | 
 | ||
|  |   // Initializing the tree with the first given word
 | ||
|  |   if (!this.root) { | ||
|  |     this.root = { | ||
|  |       item: item, | ||
|  |       children: {} | ||
|  |     }; | ||
|  | 
 | ||
|  |     this.size++; | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   var node = this.root, | ||
|  |       d; | ||
|  | 
 | ||
|  |   while (true) { | ||
|  |     d = this.distance(item, node.item); | ||
|  | 
 | ||
|  |     if (!node.children[d]) | ||
|  |       break; | ||
|  | 
 | ||
|  |     node = node.children[d]; | ||
|  |   } | ||
|  | 
 | ||
|  |   node.children[d] = { | ||
|  |     item: item, | ||
|  |     children: {} | ||
|  |   }; | ||
|  | 
 | ||
|  |   this.size++; | ||
|  |   return this; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to query the tree. | ||
|  |  * | ||
|  |  * @param  {number} n     - Maximum distance between query & item. | ||
|  |  * @param  {any}    query - Query | ||
|  |  * @return {BKTree} | ||
|  |  */ | ||
|  | BKTree.prototype.search = function(n, query) { | ||
|  |   if (!this.root) | ||
|  |     return []; | ||
|  | 
 | ||
|  |   var found = [], | ||
|  |       stack = [this.root], | ||
|  |       node, | ||
|  |       child, | ||
|  |       d, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   while (stack.length) { | ||
|  |     node = stack.pop(); | ||
|  |     d = this.distance(query, node.item); | ||
|  | 
 | ||
|  |     if (d <= n) | ||
|  |       found.push({item: node.item, distance: d}); | ||
|  | 
 | ||
|  |     for (i = d - n, l = d + n + 1; i < l; i++) { | ||
|  |       child = node.children[i]; | ||
|  | 
 | ||
|  |       if (child) | ||
|  |         stack.push(child); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return found; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to clear the tree. | ||
|  |  * | ||
|  |  * @return {undefined} | ||
|  |  */ | ||
|  | BKTree.prototype.clear = function() { | ||
|  | 
 | ||
|  |   // Properties
 | ||
|  |   this.size = 0; | ||
|  |   this.root = null; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Convenience known methods. | ||
|  |  */ | ||
|  | BKTree.prototype.toJSON = function() { | ||
|  |   return this.root; | ||
|  | }; | ||
|  | 
 | ||
|  | BKTree.prototype.inspect = function() { | ||
|  |   var array = [], | ||
|  |       stack = [this.root], | ||
|  |       node, | ||
|  |       d; | ||
|  | 
 | ||
|  |   while (stack.length) { | ||
|  |     node = stack.pop(); | ||
|  | 
 | ||
|  |     if (!node) | ||
|  |       continue; | ||
|  | 
 | ||
|  |     array.push(node.item); | ||
|  | 
 | ||
|  |     for (d in node.children) | ||
|  |       stack.push(node.children[d]); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Trick so that node displays the name of the constructor
 | ||
|  |   Object.defineProperty(array, 'constructor', { | ||
|  |     value: BKTree, | ||
|  |     enumerable: false | ||
|  |   }); | ||
|  | 
 | ||
|  |   return array; | ||
|  | }; | ||
|  | 
 | ||
|  | if (typeof Symbol !== 'undefined') | ||
|  |   BKTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = BKTree.prototype.inspect; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Static @.from function taking an arbitrary iterable & converting it into | ||
|  |  * a tree. | ||
|  |  * | ||
|  |  * @param  {Iterable} iterable - Target iterable. | ||
|  |  * @param  {function} distance - Distance function. | ||
|  |  * @return {Heap} | ||
|  |  */ | ||
|  | BKTree.from = function(iterable, distance) { | ||
|  |   var tree = new BKTree(distance); | ||
|  | 
 | ||
|  |   forEach(iterable, function(value) { | ||
|  |     tree.add(value); | ||
|  |   }); | ||
|  | 
 | ||
|  |   return tree; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Exporting. | ||
|  |  */ | ||
|  | module.exports = BKTree; |