diff --git a/src/data-structures/trie/README.md b/src/data-structures/trie/README.md new file mode 100644 index 0000000..917d883 --- /dev/null +++ b/src/data-structures/trie/README.md @@ -0,0 +1,75 @@ +# JavaScript Binary Heap Class + +by [Nicholas C. Zakas](https://humanwhocodes.com) + +If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate). + +## Overview + +A JavaScript implementation of a binary heap. This class uses the conventions of built-in JavaScript collection objects, such as: + +1. There is a `[Symbol.iterator]` method so each instance is iterable. +1. The `size` getter property instead of a `length` data property to indicate that the size of the list is dynamically counted rather than stored. +1. Defining a `values()` generator method. +1. Using `includes()` instead of `contains()`. + +## Usage + +Use CommonJS to get access to the `BinaryHeap` constructor: + +```js +const { BinaryHeap } = require("@humanwhocodes/binary-heap"); +``` + +Each instance of `BinaryHeap` has the following properties and methods: + +```js +const heap = new BinaryHeap(); + +// add an item to the end +heap.add("foo"); + +// get the minimum value without removing +let value = heap.peek(); + +// get the minimum value and remove +let value = heap.poll(); + +// get the number of items +let count = heap.size; + +// does the value exist in the heap? +let found = heap.includes(5); + +// convert to an array using iterators +let array1 = [...heap.values()]; +let array2 = [...heap]; + +// remove all items +heap.clear(); +``` + +By default, the `BinaryHeap` class is a min heap designed to work with numbers. You can change the comparator used to determine ordering by passing a function into the constructor, such as: + +```js +// create a max numeric heap +let heap = new BinaryHeap((a, b) => b - a); +``` + +The comparator function uses the same format as comparator functions for JavaScript arrays, two values are passed in and you must return: + +* A negative number if the first value should come before the second +* Zero if the ordering of the two values should remain unchanged +* A positive number if the first value should come after the second + +## Note on Code Style + +You may find the code style of this module to be overly verbose with a lot of comments. That is intentional, as the primary use of this module is intended to be for educational purposes. There are frequently more concise ways of implementing the details of this class, but the more concise ways are difficult for newcomers who are unfamiliar with linked lists as a concept or JavaScript as a whole. + +## Issues and Pull Requests + +As this is part of series of tutorials I'm writing, only bug fixes will be accepted. No new functionality will be added to this module. + +## License + +MIT \ No newline at end of file diff --git a/src/data-structures/trie/package.json b/src/data-structures/trie/package.json new file mode 100644 index 0000000..73016e3 --- /dev/null +++ b/src/data-structures/trie/package.json @@ -0,0 +1,27 @@ +{ + "name": "@humanwhocodes/binary-heap", + "version": "2.0.1", + "description": "A binary heap implementation in JavaScript", + "main": "binary-heap.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/binary-heap/binary-heap.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "heap", + "binary heap", + "data structure" + ], + "author": "Nicholas C. Zakas", + "license": "MIT", + "bugs": { + "url": "https://github.com/humanwhocodes/computer-science-in-javascript/issues" + }, + "homepage": "https://github.com/humanwhocodes/computer-science-in-javascript#readme", + "engines": { + "node": ">=8.0.0" + } +} diff --git a/src/data-structures/trie/trie.js b/src/data-structures/trie/trie.js new file mode 100644 index 0000000..0ad8a72 --- /dev/null +++ b/src/data-structures/trie/trie.js @@ -0,0 +1,212 @@ + +/** + * @fileoverview Trie implementation in JavaScript + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Private +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// TrieNode Class +//----------------------------------------------------------------------------- + +/** + * Represents a single node in a Trie. + * @class TrieNode + */ +class TrieNode { + + /** + * Creates a new instance of the TrieNode. + * @param {string} [character=null] A single character to store in this node. + * @param {boolean} [isWord=false] Indicates if this node terminates a + * valid word. + */ + constructor(character = null, isWord = false) { + + /** + * A single character that the node represents. + * @type string + * @property character + */ + this.character = character; + + /** + * Indicates if the node ancestors of this node combine to form + * a valid word. + * @type boolean + * @property isWord + */ + this.isWord = isWord; + + /** + * A map of child nodes where the key represents a character and + * the value is a TrieNode. You could also use the `HashMap` class + * from this repo but the implementation will be exactly the same. + * @type Map + * @property children + */ + this.children = new Map(); + } + + add(text, index = 0) { + + /* + * The `isLastCharacter` variable is used to determine if there is any + * further work to do once this method finishes. It is used both to + * set the `isWord` flag of any newly created node and to determine + * whether or not to make a recursive call to continue storing the text. + */ + const isLastCharacter = (index === text.length - 1); + + /** + * The `character` variable stores the character we are concerned with + * during the execution of this method. + */ + const character = text.charAt(index); + + // If there isn't already a node for this character, create one + if (!this.children.has(character)) { + this.children.set(character, new TrieNode(character, isLastCharacter)); + } + + // Get the node for this character from the map + let node = this.children.get(character); + + /* + * If `character` isn't the last character in the string, then + * recursively call `add()` on the new node to continue. The + * second argument is `index + 1` to ensure we are continuing to add + * characters moving towards the end of the string. + */ + if (!isLastCharacter) { + node.add(text, index + 1); + } + + } +} + +//----------------------------------------------------------------------------- +// Trie Class +//----------------------------------------------------------------------------- + +/* + * These symbols are used to represent properties that should not be part of + * the public interface. You could also use ES2019 private fields, but those + * are not yet widely available as of the time of my writing. + */ +const root = Symbol("root"); + +/** + * A trie implementation in JavaScript. + * @class Trie + */ +class Trie { + + /** + * Creates a new instance of Trie. + */ + constructor() { + + /** + * The root node of the Trie. While this uses a `TrieNode`, + * the node doesn't represent any character (`character` is + * `null`) because all words are formed off of this node. We could + * just use a `Map` here, but in the traditional implementation + * of a trie the root is always another node, so that's what this + * implementation uses. + * @property root + * @type TrieNode + * @private + */ + this[root] = new TrieNode(); + } + + /** + * Adds a word to the trie.. + * @param {string} word The word to add into the trie. + * @returns {void} + */ + add(word) { + this[root].add(word.toLowerCase()); + } + + isPrefix(prefix) { + let found = false; + let node = this[root]; + + for (let i = 0, length = prefix.length; i < length && !found; i++) { + let character = prefix.charAt(i); + if (node.children.has(character)) { + node = node.children.get(character); + } else { + break; + } + } + + return found; + + } + + /** + * Returns the number of values in the heap. + * @returns {int} The number of values in the heap. + */ + get size() { + // return this[array].length; + } + + /** + * Determines if the given word exists in the trie. + * @param {string} word The word to search for. + * @returns {boolean} True if the word exists in the heap, false if not. + */ + includes(word) { + } + + /** + * Removes all values from the trie. + * @returns {void} + */ + clear() { + this[root] = new TrieNode(); + } + + /** + * The default iterator for the class. + * @returns {Iterator} An iterator for the class. + */ + [Symbol.iterator]() { + return this.values(); + } + + /** + * Create an iterator that returns each node in the list. + * @returns {Iterator} An iterator on the list. + */ + *values() { + + function *traverse(node) { + for (const [key, value] of node.children) { + yield key; + yield* traverse(value); + } + } + + yield* traverse(this[root]); + + } + + /** + * Converts the heap into a string representation. + * @returns {String} A string representation of the heap. + */ + toString(){ + return [...this].toString(); + } +} + +exports.Trie = Trie; \ No newline at end of file diff --git a/tests/data-structures/trie/trie.js b/tests/data-structures/trie/trie.js new file mode 100644 index 0000000..ca5b526 --- /dev/null +++ b/tests/data-structures/trie/trie.js @@ -0,0 +1,338 @@ +/** + * @fileoverview Trie tests + */ +/* global it, describe, beforeEach */ +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { Trie } = require("../../../src/data-structures/trie/trie"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the heap match the values of the array. + * @param {Trie} heap The list to check + * @param {Array} values An array of values that should match. + * @throws {AssertionError} If the values in the list don't match the + * values in the array. + */ +function assertTrieValues(heap, values) { + const heapValues = [...heap.values()]; + assert.deepStrictEqual(heapValues, values); + +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("Trie", () => { + + let trie; + + beforeEach(() => { + trie = new Trie(); + }); + + describe("add()", () => { + + it("should store a word when one word is added", () => { + trie.add("apple"); + assertTrieValues(trie, ["a", "p", "p", "l", "e"]); + }); + + it("should store two words when multiple words are added", () => { + trie.add("apple"); + trie.add("ape"); + + assertTrieValues(trie, ["a", "p", "p", "l", "e", "e"]); + }); + + it("should store two items when multiple items are added", () => { + trie.add(2); + trie.add(3); + assertTrieValues(trie, [2,3]); + }); + + it("should store three items when multiple items are added", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assertTrieValues(trie, [1,3,2]); + }); + + it("should store four items when multiple items are added", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assertTrieValues(trie, [0,1,2,3]); + }); + }); + + describe("size", () => { + + it("should return the correct size when the heap has no items", () => { + assert.strictEqual(trie.size, 0); + }); + + it("should return the correct size when the heap has one item", () => { + trie.add(1); + assert.strictEqual(trie.size, 1); + }); + + it("should return the correct size when the heap has two items", () => { + trie.add(2); + trie.add(1); + assert.strictEqual(trie.size, 2); + }); + + it("should return the correct size when the heap has three items", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assert.strictEqual(trie.size, 3); + }); + + it("should return the correct size when the heap has four items", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assert.strictEqual(trie.size, 4); + }); + }); + + describe("isEmpty()", () => { + + it("should return true when the heap is empty", () => { + assert.isTrue(trie.isEmpty()); + }); + + it("should return false when the heap has one item", () => { + trie.add(1); + assert.isFalse(trie.isEmpty()); + }); + + it("should return false when the heap has two items", () => { + trie.add(2); + trie.add(1); + assert.isFalse(trie.isEmpty()); + }); + + }); + + describe("includes()", () => { + + it("should return false when the heap is empty", () => { + assert.isFalse(trie.includes(5)); + }); + + it("should return true when the item is found", () => { + trie.add(1); + assert.isTrue(trie.includes(1)); + }); + + it("should return false when the item is not found", () => { + trie.add(1); + assert.isFalse(trie.includes(10)); + }); + + it("should return true when the heap has two items and the item is found", () => { + trie.add(2); + trie.add(1); + assert.isTrue(trie.includes(2)); + }); + + }); + + describe("peek()", () => { + + it("should return the only item from a one-item heap", () => { + trie.add(1); + assert.strictEqual(trie.peek(), 1); + assert.strictEqual(trie.size, 1); + }); + + it("should return the lowest value from a two-item heap", () => { + trie.add(2); + trie.add(1); + assert.strictEqual(trie.peek(), 1); + assert.strictEqual(trie.size, 2); + }); + + it("should return the lowest value from a three-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assert.strictEqual(trie.peek(), 1); + assert.strictEqual(trie.size, 3); + }); + + it("should return the lowest value from a four-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assert.strictEqual(trie.peek(), 0); + assert.strictEqual(trie.size, 4); + }); + }); + + describe("poll()", () => { + + it("should return the only item from a one-item heap", () => { + trie.add(1); + assert.strictEqual(trie.poll(), 1); + assert.strictEqual(trie.size, 0); + assertTrieValues(trie, []); + }); + + it("should return the lowest value from a two-item heap", () => { + trie.add(2); + trie.add(1); + assert.strictEqual(trie.poll(), 1); + assert.strictEqual(trie.size, 1); + assertTrieValues(trie, [2]); + }); + + it("should return the lowest value from a three-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assert.strictEqual(trie.poll(), 1); + assert.strictEqual(trie.size, 2); + assertTrieValues(trie, [2,3]); + }); + + it("should return the lowest value from a four-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assert.strictEqual(trie.poll(), 0); + assert.strictEqual(trie.size, 3); + assertTrieValues(trie, [1,3,2]); + }); + }); + + describe("Custom Comparator", () => { + + beforeEach(() => { + trie = new Trie((a, b) => b - a); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + trie.add(1); + assertTrieValues(trie, [1]); + }); + + it("should store two items when multiple items are added", () => { + trie.add(2); + trie.add(1); + + assertTrieValues(trie, [2, 1]); + }); + + it("should store two items when multiple items are added", () => { + trie.add(2); + trie.add(3); + assertTrieValues(trie, [3, 2]); + }); + + it("should store three items when multiple items are added", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assertTrieValues(trie, [3, 2, 1]); + }); + + it("should store four items when multiple items are added", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assertTrieValues(trie, [3, 2, 1, 0]); + }); + }); + + describe("peek()", () => { + + it("should return the only item from a one-item heap", () => { + trie.add(1); + assert.strictEqual(trie.peek(), 1); + assert.strictEqual(trie.size, 1); + }); + + it("should return the highest value from a two-item heap", () => { + trie.add(2); + trie.add(1); + assert.strictEqual(trie.peek(), 2); + assert.strictEqual(trie.size, 2); + }); + + it("should return the highest value from a three-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assert.strictEqual(trie.peek(), 3); + assert.strictEqual(trie.size, 3); + }); + + it("should return the highest value from a four-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assert.strictEqual(trie.peek(), 3); + assert.strictEqual(trie.size, 4); + }); + }); + + describe("poll()", () => { + + it("should return the only item from a one-item heap", () => { + trie.add(1); + assert.strictEqual(trie.poll(), 1); + assert.strictEqual(trie.size, 0); + assertTrieValues(trie, []); + }); + + it("should return the highest value from a two-item heap", () => { + trie.add(2); + trie.add(1); + assert.strictEqual(trie.poll(), 2); + assert.strictEqual(trie.size, 1); + assertTrieValues(trie, [1]); + }); + + it("should return the highest value from a three-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + assert.strictEqual(trie.poll(), 3); + assert.strictEqual(trie.size, 2); + assertTrieValues(trie, [2, 1]); + }); + + it("should return the highest value from a four-item heap", () => { + trie.add(2); + trie.add(3); + trie.add(1); + trie.add(0); + assert.strictEqual(trie.poll(), 3); + assert.strictEqual(trie.size, 3); + assertTrieValues(trie, [2, 0, 1]); + }); + }); + }); + +});