diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..4934076 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + "env": { + "commonjs": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ] + } +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index d390fdf..64c74cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +node_modules/ holding/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1e53f24 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: node_js +node_js: + - "8" + - "9" + - "10" + - "11" +sudo: false +branches: + only: + - master + +# Run npm test always +script: + - "npm test" diff --git a/README b/README deleted file mode 100644 index 5611c31..0000000 --- a/README +++ /dev/null @@ -1,30 +0,0 @@ -Collection of classic computer science paradigms, algorithms, and approaches written in JavaScript. All of the code is available under an MIT License. - -Each example has an associated blog post: - -Base64 Encoding -http://www.nczonline.net/blog/2009/12/08/computer-science-in-javascript-base64-encoding/ - -Binary Search -http://www.nczonline.net/blog/2009/09/01/computer-science-in-javascript-binary-search/ - -Binary Search Tree -http://www.nczonline.net/blog/2009/06/09/computer-science-in-javascript-binary-search-tree-part-1/ -http://www.nczonline.net/blog/2009/06/16/computer-science-in-javascript-binary-search-tree-part-2/ - -Bubble Sort -http://www.nczonline.net/blog/2009/05/26/computer-science-in-javascript-bubble-sort/ - -Credit Card Number Validation -http://www.nczonline.net/blog/2009/08/04/computer-science-in-javascript-credit-card-number-validation/ - -Doubly Linked List -http://www.nczonline.net/blog/2009/04/21/computer-science-in-javascript-doubly-linked-lists/ - -Linked List -http://www.nczonline.net/blog/2009/04/13/computer-science-in-javascript-linked-list/ - -Selection Sort -http://www.nczonline.net/blog/2009/09/08/computer-science-in-javascript-selection-sort/ - -Please note: Since this is the repository that goes along with my blog post series, only pull requests for bugs are accepted. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d36769 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# Computer Science in JavaScript + +by [Nicholas C. Zakas](https://humanwhocodes.com) + +If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate). + +## Description + +Collection of classic computer science paradigms, algorithms, and approaches written in JavaScript. This is the source code for the series of blog posts on my website. + +## Folder Structure + +The most recent packages are found in these directories: + +* `src` - the implementation source code +* `tests` - tests for the implementation source code + +These directories contain **old** implementations that will be replaced eventually, they are just here to avoid confusing people who find this repo through the old blog posts: + +* `data-structures` - data structure implementations that have not been updated yet +* `encodings` - encoding implementations that have not been updated yet +* `algorithms` - miscellanous algorithm implementations that have not been updated yet + +As I update these, implementations will move from these folders into `src`. + +## Branches + +* **2009** - the branch containing all of the original implementations as reflected in my 2009 blog post series. +* **master** - the branch where I'm updating the original implementations to use ECMAScript 2018 and later features. + +## Installing + +You must be using Node.js v8 or later. + +First, clone the repo: + +``` +$ git clone git://github.com/humanwhocodes/computer-science-in-javascript.git +$ cd computer-science-in-javascript +``` + +Then install the dependencies: + +``` +$ npm install +``` + +You can then run tests like this: + +``` +$ npm test +``` + +## Updated Blog Posts (2019) + +These are the most recent blog posts covering the most recent version of the code. + +### Data Structures + +* [Linked List](https://humanwhocodes.com/blog/2019/01/computer-science-in-javascript-linked-list/) +* [Doubly-Linked List](https://humanwhocodes.com/blog/2019/02/computer-science-in-javascript-doubly-linked-lists/) +* [Circular Doubly-Linked List](https://humanwhocodes.com/blog/2019/03/computer-science-in-javascript-circular-doubly-linked-lists/) + +## Original Blog Posts + +At some point I will update these blog posts for the new implementations. For now, they still refer only to the 2009 version of this code. + +### Data Structures + +* Binary Search Tree: [Part 1](https://humanwhocodes.com/blog/2009/06/09/computer-science-in-javascript-binary-search-tree-part-1/), [Part 2](https://humanwhocodes.com/blog/2009/06/16/computer-science-in-javascript-binary-search-tree-part-2/) +* [Doubly Linked List](https://humanwhocodes.com/blog/2009/04/21/computer-science-in-javascript-doubly-linked-lists/) +* [Linked List](https://humanwhocodes.com/blog/2009/04/13/computer-science-in-javascript-linked-list/) + +### Sorting Algorithms + +* [Bubble Sort](https://humanwhocodes.com/blog/2009/05/26/computer-science-in-javascript-bubble-sort/) +* [Selection Sort](https://humanwhocodes.com/blog/2009/09/08/computer-science-in-javascript-selection-sort/) + +### Other Algorithms + +* [Base64 Encoding](https://humanwhocodes.com/blog/2009/12/08/computer-science-in-javascript-base64-encoding/) +* [Binary Search](https://humanwhocodes.com/blog/2009/09/01/computer-science-in-javascript-binary-search/) +* [Credit Card Number Validation](https://humanwhocodes.com/blog/2009/08/04/computer-science-in-javascript-credit-card-number-validation/) + +## 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/algorithms/sorting/bubble-sort/bubble-sort-2.js b/algorithms/sorting/bubble-sort/bubble-sort-2.js deleted file mode 100644 index e3ceb96..0000000 --- a/algorithms/sorting/bubble-sort/bubble-sort-2.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Bubble sort implementation in JavaScript - * Copyright (c) 2009 Nicholas C. Zakas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - - -/** - * Swaps two values in an array. - * @param {Array} items The array containing the items. - * @param {int} firstIndex Index of first item to swap. - * @param {int} secondIndex Index of second item to swap. - * @return {void} - */ -function swap(items, firstIndex, secondIndex){ - var temp = items[firstIndex]; - items[firstIndex] = items[secondIndex]; - items[secondIndex] = temp; -} - -/** - * A bubble sort implementation in JavaScript. The array - * is sorted in-place. This uses two reversed loops that - * count down instead of counting up. - * @param {Array} items An array of items to sort. - * @return {Array} The sorted array. - */ -function bubbleSort(items){ - var len = items.length, - i, j; - - for (i=len-1; i >= 0; i--){ - for (j=len-i; j >= 0; j--){ - if (items[j] < items[j-1]){ - swap(items, j, j-1); - } - } - } - - return items; -} diff --git a/algorithms/sorting/insertion-sort/insertion-sort.js b/algorithms/sorting/insertion-sort/insertion-sort.js new file mode 100644 index 0000000..923c89c --- /dev/null +++ b/algorithms/sorting/insertion-sort/insertion-sort.js @@ -0,0 +1,55 @@ +/* + * Insertion sort implementation in JavaScript + * Copyright (c) 2012 Nicholas C. Zakas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of items software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and items permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * An insertion sort implementation in JavaScript. The array + * is sorted in-place. + * @param {Array} items An array of items to sort. + * @return {Array} The sorted array. + */ +function insertionSort(items) { + + var len = items.length, // number of items in the array + value, // the value currently being compared + i, // index into unsorted section + j; // index into sorted section + + for (i=0; i < len; i++) { + + // store the current value because it may shift later + value = items[i]; + + /* + * Whenever the value in the sorted section is greater than the value + * in the unsorted section, shift all items in the sorted section over + * by one. This creates space in which to insert the value. + */ + for (j=i-1; j > -1 && items[j] > value; j--) { + items[j+1] = items[j]; + } + + items[j+1] = value; + } + + return items; +} \ No newline at end of file diff --git a/algorithms/sorting/merge-sort-iterative/merge-sort-iterative.js b/algorithms/sorting/merge-sort-iterative/merge-sort-iterative.js index c906b62..6287a33 100644 --- a/algorithms/sorting/merge-sort-iterative/merge-sort-iterative.js +++ b/algorithms/sorting/merge-sort-iterative/merge-sort-iterative.js @@ -55,12 +55,18 @@ function merge(left, right){ * @return {Array} The sorted array. */ function mergeSort(items){ - if (items.length == 1) { + + // Terminal condition - don't need to do anything for arrays with 0 or 1 items + if (items.length < 2) { return items; } - var work = []; - for (var i=0, len=items.length; i < len; i++){ + var work = [], + i, + len; + + + for (i=0, len=items.length; i < len; i++){ work.push([items[i]]); } work.push([]); //in case of odd number of items diff --git a/algorithms/sorting/bubble-sort/bubble-sort.js b/algorithms/sorting/merge-sort-recursive/merge-sort-inplace.js similarity index 50% rename from algorithms/sorting/bubble-sort/bubble-sort.js rename to algorithms/sorting/merge-sort-recursive/merge-sort-inplace.js index 5722f8d..4c945d3 100644 --- a/algorithms/sorting/bubble-sort/bubble-sort.js +++ b/algorithms/sorting/merge-sort-recursive/merge-sort-inplace.js @@ -1,6 +1,6 @@ /* - * Bubble sort implementation in JavaScript - * Copyright (c) 2009 Nicholas C. Zakas + * Recursive merge sort implementation in JavaScript + * Copyright (c) 2012 Nicholas C. Zakas * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,39 +20,49 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ - /** - * Swaps two values in an array. - * @param {Array} items The array containing the items. - * @param {int} firstIndex Index of first item to swap. - * @param {int} secondIndex Index of second item to swap. - * @return {void} + * Merges to arrays in order based on their natural + * relationship. + * @param {Array} left The first array to merge. + * @param {Array} right The second array to merge. + * @return {Array} The merged array. */ -function swap(items, firstIndex, secondIndex){ - var temp = items[firstIndex]; - items[firstIndex] = items[secondIndex]; - items[secondIndex] = temp; +function merge(left, right){ + var result = [], + il = 0, + ir = 0; + + while (il < left.length && ir < right.length){ + if (left[il] < right[ir]){ + result.push(left[il++]); + } else { + result.push(right[ir++]); + } + } + + return result.concat(left.slice(il)).concat(right.slice(ir)); } - + /** - * A bubble sort implementation in JavaScript. The array - * is sorted in-place. - * @param {Array} items An array of items to sort. + * Sorts an array in ascending natural order using + * merge sort. + * @param {Array} items The array to sort. * @return {Array} The sorted array. */ -function bubbleSort(items){ - - var len = items.length, - i, j, stop; +function mergeSort(items){ - for (i=0; i < len; i++){ - for (j=0, stop=len-i; j < stop; j++){ - if (items[j] > items[j+1]){ - swap(items, j, j+1); - } - } + if (items.length < 2) { + return items; } + + var middle = Math.floor(items.length / 2), + left = items.slice(0, middle), + right = items.slice(middle), + params = merge(mergeSort(left), mergeSort(right)); + // Add the arguments to replace everything between 0 and last item in the array + params.unshift(0, items.length); + items.splice.apply(items, params); return items; } \ No newline at end of file diff --git a/algorithms/sorting/merge-sort-recursive/merge-sort-recursive.js b/algorithms/sorting/merge-sort-recursive/merge-sort-recursive.js index b21f9a0..abfff01 100644 --- a/algorithms/sorting/merge-sort-recursive/merge-sort-recursive.js +++ b/algorithms/sorting/merge-sort-recursive/merge-sort-recursive.js @@ -22,24 +22,26 @@ */ /** - * Merges to arrays in order based on their natural + * Merges two arrays in order based on their natural * relationship. * @param {Array} left The first array to merge. * @param {Array} right The second array to merge. * @return {Array} The merged array. */ function merge(left, right){ - var result = []; + var result = [], + il = 0, + ir = 0; - while (left.length > 0 && right.length > 0){ - if (left[0] < right[0]){ - result.push(left.shift()); + while (il < left.length && ir < right.length){ + if (left[il] < right[ir]){ + result.push(left[il++]); } else { - result.push(right.shift()); + result.push(right[ir++]); } } - return result.concat(left).concat(right); + return result.concat(left.slice(il)).concat(right.slice(ir)); } /** @@ -50,7 +52,7 @@ function merge(left, right){ */ function mergeSort(items){ - if (items.length == 1) { + if (items.length < 2) { return items; } @@ -59,4 +61,4 @@ function mergeSort(items){ right = items.slice(middle); return merge(mergeSort(left), mergeSort(right)); -} \ No newline at end of file +} diff --git a/algorithms/sorting/quicksort/quicksort.js b/algorithms/sorting/quicksort/quicksort.js new file mode 100644 index 0000000..0a0e4bb --- /dev/null +++ b/algorithms/sorting/quicksort/quicksort.js @@ -0,0 +1,104 @@ +/* + * Quick sort implementation in JavaScript + * Copyright (c) 2012 Nicholas C. Zakas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of items software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and items permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Swaps two values in an array. + * @param {Array} items The array containing the items. + * @param {int} firstIndex Index of first item to swap. + * @param {int} secondIndex Index of second item to swap. + * @return {void} + */ +function swap(items, firstIndex, secondIndex){ + var temp = items[firstIndex]; + items[firstIndex] = items[secondIndex]; + items[secondIndex] = temp; +} + +function partition(items, left, right) { + + var pivot = items[Math.floor((right + left) / 2)], // pivot value is middle item + i = left, // starts from left and goes right to pivot index + j = right; // starts from right and goes left to pivot index + + + // while the two indices don't match + while (i <= j) { + + // if the item on the left is less than the pivot, continue right + while (items[i] < pivot) { + i++; + } + + // if the item on the right is greater than the pivot, continue left + while (items[j] > pivot) { + j--; + } + + // if the two indices still don't match, swap the values + if (i <= j) { + swap(items, i, j); + + // change indices to continue loop + i++; + j--; + } + } + + // this value is necessary for recursion + return i; +} + +/** + * A quicksort implementation in JavaScript. The array + * is sorted in place. + * @param {Array} items An array of items to sort. + * @return {Array} The sorted array. + */ +function quickSort(items, left, right) { + + var index; + + // performance - don't sort an array with zero or one items + if (items.length > 1) { + + // fix left and right values - might not be provided + left = typeof left != "number" ? 0 : left; + right = typeof right != "number" ? items.length - 1 : right; + + // split up the entire array + index = partition(items, left, right); + + // if the returned index + if (left < index - 1) { + quickSort(items, left, index - 1); + } + + if (index < right) { + quickSort(items, index, right); + } + + } + + return items; +} + diff --git a/algorithms/sorting/selection-sort/selection-sort.js b/algorithms/sorting/selection-sort/selection-sort.js index 12f2545..7cab6c2 100644 --- a/algorithms/sorting/selection-sort/selection-sort.js +++ b/algorithms/sorting/selection-sort/selection-sort.js @@ -44,21 +44,21 @@ function swap(items, firstIndex, secondIndex){ function selectionSort(items){ var len = items.length, - min; + min, i, j; for (i=0; i < len; i++){ - //set minimum to this position + // set minimum to this position min = i; - //check the rest of the array to see if anything is smaller + // check the rest of the array to see if anything is smaller for (j=i+1; j < len; j++){ if (items[j] < items[min]){ min = j; } } - //if the minimum isn't in the position, swap it + // if the minimum isn't in the position, swap it if (i != min){ swap(items, i, min); } diff --git a/data-structures/binary-search-tree/binary-search-tree-tests.htm b/data-structures/binary-search-tree/binary-search-tree-tests.htm deleted file mode 100644 index 326becd..0000000 --- a/data-structures/binary-search-tree/binary-search-tree-tests.htm +++ /dev/null @@ -1,245 +0,0 @@ - - -Binary Search Tree Tests - - - - - - - - - -

Binary Search Tree Tests

- - - diff --git a/data-structures/binary-search-tree/binary-search-tree.js b/data-structures/binary-search-tree/binary-search-tree.js deleted file mode 100644 index 89d57a9..0000000 --- a/data-structures/binary-search-tree/binary-search-tree.js +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Binary Search Tree implementation in JavaScript - * Copyright (c) 2009 Nicholas C. Zakas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * A binary search tree implementation in JavaScript. This implementation - * does not allow duplicate values to be inserted into the tree, ensuring - * that there is just one instance of each value. - * @class BinarySearchTree - * @constructor - */ -function BinarySearchTree() { - - /** - * Pointer to root node in the tree. - * @property _root - * @type Object - * @private - */ - this._root = null; -} - -BinarySearchTree.prototype = { - - //restore constructor - constructor: BinarySearchTree, - - //------------------------------------------------------------------------- - // Private members - //------------------------------------------------------------------------- - - /** - * Appends some data to the appropriate point in the tree. If there are no - * nodes in the tree, the data becomes the root. If there are other nodes - * in the tree, then the tree must be traversed to find the correct spot - * for insertion. - * @param {variant} value The data to add to the list. - * @return {Void} - * @method add - */ - add: function (value){ - - //create a new item object, place data in - var node = { - value: value, - left: null, - right: null - }, - - //used to traverse the structure - current; - - //special case: no items in the tree yet - if (this._root === null){ - this._root = node; - } else { - current = this._root; - - while(true){ - - //if the new value is less than this node's value, go left - if (value < current.value){ - - //if there's no left, then the new node belongs there - if (current.left === null){ - current.left = node; - break; - } else { - current = current.left; - } - - //if the new value is greater than this node's value, go right - } else if (value > current.value){ - - //if there's no right, then the new node belongs there - if (current.right === null){ - current.right = node; - break; - } else { - current = current.right; - } - - //if the new value is equal to the current one, just ignore - } else { - break; - } - } - } - }, - - /** - * Determines if the given value is present in the tree. - * @param {variant} value The value to find. - * @return {Boolean} True if the value is found, false if not. - * @method contains - */ - contains: function(value){ - - var found = false, - current = this._root - - //make sure there's a node to search - while(!found && current){ - - //if the value is less than the current node's, go left - if (value < current.value){ - current = current.left; - - //if the value is greater than the current node's, go right - } else if (value > current.value){ - current = current.right; - - //values are equal, found it! - } else { - found = true; - } - } - - //only proceed if the node was found - return found; - - }, - - /** - * Removes the node with the given value from the tree. This may require - * moving around some nodes so that the binary search tree remains - * properly balanced. - * @param {variant} value The value to remove. - * @return {void} - * @method remove - */ - remove: function(value){ - - var found = false, - parent = null, - current = this._root, - childCount, - replacement, - replacementParent; - - //make sure there's a node to search - while(!found && current){ - - //if the value is less than the current node's, go left - if (value < current.value){ - parent = current; - current = current.left; - - //if the value is greater than the current node's, go right - } else if (value > current.value){ - parent = current; - current = current.right; - - //values are equal, found it! - } else { - found = true; - } - } - - //only proceed if the node was found - if (found){ - - //figure out how many children - childCount = (current.left !== null ? 1 : 0) + (current.right !== null ? 1 : 0); - - //special case: the value is at the root - if (current === this._root){ - switch(childCount){ - - //no children, just erase the root - case 0: - this._root = null; - break; - - //one child, use one as the root - case 1: - this._root = (current.right === null ? current.left : current.right); - break; - - //two children, little work to do - case 2: - - //new root will be the old root's left child...maybe - replacement = this._root.left; - - //find the right-most leaf node to be the real new root - while (replacement.right !== null){ - replacementParent = replacement; - replacement = replacement.right; - } - - //it's not the first node on the left - if (replacementParent !== null){ - - //remove the new root from it's previous position - replacementParent.right = replacement.left; - - //give the new root all of the old root's children - replacement.right = this._root.right; - replacement.left = this._root.left; - } else { - - //just assign the children - replacement.right = this._root.right; - } - - //officially assign new root - this._root = replacement; - - //no default - - } - - //non-root values - } else { - - switch (childCount){ - - //no children, just remove it from the parent - case 0: - //if the current value is less than its parent's, null out the left pointer - if (current.value < parent.value){ - parent.left = null; - - //if the current value is greater than its parent's, null out the right pointer - } else { - parent.right = null; - } - break; - - //one child, just reassign to parent - case 1: - //if the current value is less than its parent's, reset the left pointer - if (current.value < parent.value){ - parent.left = (current.left === null ? current.right : current.left); - - //if the current value is greater than its parent's, reset the right pointer - } else { - parent.right = (current.left === null ? current.right : current.left); - } - break; - - //two children, a bit more complicated - case 2: - - //reset pointers for new traversal - replacement = current.left; - replacementParent = current; - - //find the right-most node - while(replacement.right !== null){ - replacementParent = replacement; - replacement = replacement.right; - } - - replacementParent.right = replacement.left; - - //assign children to the replacement - replacement.right = current.right; - replacement.left = current.left; - - //place the replacement in the right spot - if (current.value < parent.value){ - parent.left = replacement; - } else { - parent.right = replacement; - } - - //no default - - - } - - } - - } - - }, - - /** - * Returns the number of items in the tree. To do this, a traversal - * must be executed. - * @return {int} The number of items in the tree. - * @method size - */ - size: function(){ - var length = 0; - - this.traverse(function(node){ - length++; - }); - - return length; - }, - - /** - * Converts the tree into an array. - * @return {Array} An array containing all of the data in the tree. - * @method toArray - */ - toArray: function(){ - var result = []; - - this.traverse(function(node){ - result.push(node.value); - }); - - return result; - }, - - /** - * Converts the list into a string representation. - * @return {String} A string representation of the list. - * @method toString - */ - toString: function(){ - return this.toArray().toString(); - }, - - /** - * Traverses the tree and runs the given method on each node it comes - * across while doing an in-order traversal. - * @param {Function} process The function to run on each node. - * @return {void} - * @method traverse - */ - traverse: function(process){ - - //helper function - function inOrder(node){ - if (node){ - - //traverse the left subtree - if (node.left !== null){ - inOrder(node.left); - } - - //call the process method on this node - process.call(this, node); - - //traverse the right subtree - if (node.right !== null){ - inOrder(node.right); - } - } - } - - //start with the root - inOrder(this._root); - } -}; diff --git a/data-structures/doubly-linked-list/doubly-linked-list-tests.htm b/data-structures/doubly-linked-list/doubly-linked-list-tests.htm deleted file mode 100644 index 1d21912..0000000 --- a/data-structures/doubly-linked-list/doubly-linked-list-tests.htm +++ /dev/null @@ -1,228 +0,0 @@ - - -Doubly Linked List Tests - - - - - - - - - -

Doubly Linked List Tests

- - - diff --git a/data-structures/doubly-linked-list/doubly-linked-list.js b/data-structures/doubly-linked-list/doubly-linked-list.js deleted file mode 100644 index 70ba32c..0000000 --- a/data-structures/doubly-linked-list/doubly-linked-list.js +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Doubly Linked List implementation in JavaScript - * Copyright (c) 2009 Nicholas C. Zakas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * A linked list implementation in JavaScript. - * @class DoublyLinkedList - * @constructor - */ -function DoublyLinkedList() { - - /** - * Pointer to first item in the list. - * @property _head - * @type Object - * @private - */ - this._head = null; - - /** - * Pointer to last item in the list. - * @property _tail - * @type Object - * @private - */ - this._tail = null; - - /** - * The number of items in the list. - * @property _length - * @type int - * @private - */ - this._length = 0; -} - -DoublyLinkedList.prototype = { - - //restore constructor - constructor: DoublyLinkedList, - - /** - * Appends some data to the end of the list. This method traverses - * the existing list and places the value at the end in a new item. - * @param {variant} data The data to add to the list. - * @return {Void} - * @method add - */ - add: function (data){ - - //create a new item object, place data in - var node = { - data: data, - next: null, - prev: null - }; - - //special case: no items in the list yet - if (this._length == 0) { - this._head = node; - this._tail = node; - } else { - - //attach to the tail node - this._tail.next = node; - node.prev = this._tail; - this._tail = node; - } - - //don't forget to update the count - this._length++; - - }, - - /** - * Retrieves the data in the given position in the list. - * @param {int} index The zero-based index of the item whose value - * should be returned. - * @return {variant} The value in the "data" portion of the given item - * or null if the item doesn't exist. - * @method item - */ - item: function(index){ - - //check for out-of-bounds values - if (index > -1 && index < this._length){ - var current = this._head, - i = 0; - - while(i++ < index){ - current = current.next; - } - - return current.data; - } else { - return null; - } - }, - - /** - * Removes the item from the given location in the list. - * @param {int} index The zero-based index of the item to remove. - * @return {variant} The data in the given position in the list or null if - * the item doesn't exist. - * @method remove - */ - remove: function(index){ - - //check for out-of-bounds values - if (index > -1 && index < this._length){ - - var current = this._head, - i = 0; - - //special case: removing first item - if (index === 0){ - this._head = current.next; - - /* - * If there's only one item in the list and you remove it, - * then this._head will be null. In that case, you should - * also set this._tail to be null to effectively destroy - * the list. Otherwise, set the previous pointer on the new - * this._head to be null. - */ - if (!this._head){ - this._tail = null; - } else { - this._head.prev = null; - } - - //special case: removing last item - } else if (index === this._length -1){ - current = this._tail; - this._tail = current.prev; - this._tail.next = null; - } else { - - //find the right location - while(i++ < index){ - current = current.next; - } - - //skip over the item to remove - current.prev.next = current.next; - } - - //decrement the length - this._length--; - - //return the value - return current.data; - - } else { - return null; - } - - - }, - - /** - * Returns the number of items in the list. - * @return {int} The number of items in the list. - * @method size - */ - size: function(){ - return this._length; - }, - - /** - * Converts the list into an array. - * @return {Array} An array containing all of the data in the list. - * @method toArray - */ - toArray: function(){ - var result = [], - current = this._head; - - while(current){ - result.push(current.data); - current = current.next; - } - - return result; - }, - - /** - * Converts the list into a string representation. - * @return {String} A string representation of the list. - * @method toString - */ - toString: function(){ - return this.toArray().toString(); - } -}; - - - diff --git a/data-structures/linked-list-classic/linked-list-classic-tests.htm b/data-structures/linked-list-classic/linked-list-classic-tests.htm deleted file mode 100644 index 69a4277..0000000 --- a/data-structures/linked-list-classic/linked-list-classic-tests.htm +++ /dev/null @@ -1,228 +0,0 @@ - - -Linked List Tests - - - - - - - - - -

Linked List Tests

- - - diff --git a/data-structures/linked-list-classic/linked-list-classic.js b/data-structures/linked-list-classic/linked-list-classic.js deleted file mode 100644 index 7822995..0000000 --- a/data-structures/linked-list-classic/linked-list-classic.js +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Linked List implementation in JavaScript - * Copyright (c) 2009 Nicholas C. Zakas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * A linked list implementation in JavaScript. This is a more "classic" - * approach that doesn't keep track of the number of items in the list - * and instead calculates the size when necessary. - * @class LinkedList - * @constructor - */ -function LinkedList() { - - /** - * Pointer to first item in the list. - * @property _head - * @type Object - * @private - */ - this._head = null; -} - -LinkedList.prototype = { - - //restore constructor - constructor: LinkedList, - - /** - * Appends some data to the end of the list. This method traverses - * the existing list and places the value at the end in a new item. - * @param {variant} data The data to add to the list. - * @return {Void} - * @method add - */ - add: function (data){ - - //create a new item object, place data in - var node = { - data: data, - next: null - }, - - //used to traverse the structure - current; - - //special case: no items in the list yet - if (this._head === null){ - this._head = node; - } else { - current = this._head; - - while(current.next){ - current = current.next; - } - - current.next = node; - } - - }, - - /** - * Retrieves the data in the given position in the list. - * @param {int} index The zero-based index of the item whose value - * should be returned. - * @return {variant} The value in the "data" portion of the given item - * or null if the item doesn't exist. - * @method item - */ - item: function(index){ - - //check for out-of-bounds values - if (index > -1){ - var current = this._head, - i = 0; - - while(i++ < index && current){ - current = current.next; - } - - return (current ? current.data : null); - } else { - return null; - } - }, - - /** - * Removes the item from the given location in the list. - * @param {int} index The zero-based index of the item to remove. - * @return {variant} The data in the given position in the list or null if - * the item doesn't exist. - * @method remove - */ - remove: function(index){ - - //check for out-of-bounds values - if (index > -1){ - - var current = this._head, - previous, - i = 0; - - //special case: removing first item - if (index === 0){ - this._head = current.next; - } else { - - //find the right location - while(i++ < index){ - previous = current; - current = current.next; - } - - //skip over the item to remove - previous.next = current.next; - } - - //return the value - return (current ? current.data : null); - - } else { - return null; - } - - }, - - /** - * Returns the number of items in the list. - * @return {int} The number of items in the list. - * @method size - */ - size: function(){ - var current = this._head, - count = 0; - - while(current){ - count++; - current = current.next; - } - - return count; - }, - - /** - * Converts the list into an array. - * @return {Array} An array containing all of the data in the list. - * @method toArray - */ - toArray: function(){ - var result = [], - current = this._head; - - while(current){ - result.push(current.data); - current = current.next; - } - - return result; - }, - - /** - * Converts the list into a string representation. - * @return {String} A string representation of the list. - * @method toString - */ - toString: function(){ - return this.toArray().toString(); - } -}; diff --git a/data-structures/linked-list/linked-list-tests.htm b/data-structures/linked-list/linked-list-tests.htm deleted file mode 100644 index ff5cb4c..0000000 --- a/data-structures/linked-list/linked-list-tests.htm +++ /dev/null @@ -1,228 +0,0 @@ - - -Linked List Tests - - - - - - - - - -

Linked List Tests

- - - diff --git a/data-structures/linked-list/linked-list.js b/data-structures/linked-list/linked-list.js deleted file mode 100644 index b7115ac..0000000 --- a/data-structures/linked-list/linked-list.js +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Linked List implementation in JavaScript - * Copyright (c) 2009 Nicholas C. Zakas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * A linked list implementation in JavaScript. - * @class LinkedList - * @constructor - */ -function LinkedList() { - - /** - * The number of items in the list. - * @property _length - * @type int - * @private - */ - this._length = 0; - - /** - * Pointer to first item in the list. - * @property _head - * @type Object - * @private - */ - this._head = null; -} - -LinkedList.prototype = { - - //restore constructor - constructor: LinkedList, - - /** - * Appends some data to the end of the list. This method traverses - * the existing list and places the value at the end in a new item. - * @param {variant} data The data to add to the list. - * @return {Void} - * @method add - */ - add: function (data){ - - //create a new item object, place data in - var node = { - data: data, - next: null - }, - - //used to traverse the structure - current; - - //special case: no items in the list yet - if (this._head === null){ - this._head = node; - } else { - current = this._head; - - while(current.next){ - current = current.next; - } - - current.next = node; - } - - //don't forget to update the count - this._length++; - - }, - - /** - * Retrieves the data in the given position in the list. - * @param {int} index The zero-based index of the item whose value - * should be returned. - * @return {variant} The value in the "data" portion of the given item - * or null if the item doesn't exist. - * @method item - */ - item: function(index){ - - //check for out-of-bounds values - if (index > -1 && index < this._length){ - var current = this._head, - i = 0; - - while(i++ < index){ - current = current.next; - } - - return current.data; - } else { - return null; - } - }, - - /** - * Removes the item from the given location in the list. - * @param {int} index The zero-based index of the item to remove. - * @return {variant} The data in the given position in the list or null if - * the item doesn't exist. - * @method remove - */ - remove: function(index){ - - //check for out-of-bounds values - if (index > -1 && index < this._length){ - - var current = this._head, - previous, - i = 0; - - //special case: removing first item - if (index === 0){ - this._head = current.next; - } else { - - //find the right location - while(i++ < index){ - previous = current; - current = current.next; - } - - //skip over the item to remove - previous.next = current.next; - } - - //decrement the length - this._length--; - - //return the value - return current.data; - - } else { - return null; - } - - }, - - /** - * Returns the number of items in the list. - * @return {int} The number of items in the list. - * @method size - */ - size: function(){ - return this._length; - }, - - /** - * Converts the list into an array. - * @return {Array} An array containing all of the data in the list. - * @method toArray - */ - toArray: function(){ - var result = [], - current = this._head; - - while(current){ - result.push(current.data); - current = current.next; - } - - return result; - }, - - /** - * Converts the list into a string representation. - * @return {String} A string representation of the list. - * @method toString - */ - toString: function(){ - return this.toArray().toString(); - } -}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a8a78a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1065 @@ +{ + "name": "@humanwhocodes/computer-science-in-javascript", + "version": "2.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "acorn": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true + }, + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.10.0.tgz", + "integrity": "sha512-HpqzC+BHULKlnPwWae9MaVZ5AXJKpkxCVXQHrFaRw3hbDj26V/9ArYM4Rr/SQ8pi6qUPLXSSXC4RBJlyq2Z2OQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.5.3", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "imurmurhash": "^0.1.4", + "inquirer": "^6.1.0", + "js-yaml": "^3.12.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.0.2", + "text-table": "^0.2.0" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", + "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz", + "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", + "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inquirer": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", + "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.10", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slice-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", + "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", + "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", + "dev": true, + "requires": { + "ajv": "^6.6.1", + "lodash": "^4.17.11", + "slice-ansi": "2.0.0", + "string-width": "^2.1.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c1b46c7 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@humanwhocodes/computer-science-in-javascript", + "version": "2.0.0", + "private": true, + "description": "Collection of classic computer science paradigms, algorithms, and approaches written in JavaScript.", + "main": "README.md", + "scripts": { + "lint": "eslint src/ tests/", + "test": "npm run lint && mocha tests/**/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nzakas/computer-science-in-javascript.git" + }, + "keywords": [ + "cs", + "algorithms", + "data", + "structures" + ], + "author": "nzakas", + "license": "MIT", + "bugs": { + "url": "https://github.com/nzakas/computer-science-in-javascript/issues" + }, + "homepage": "https://github.com/nzakas/computer-science-in-javascript#readme", + "directories": { + "test": "tests" + }, + "devDependencies": { + "chai": "^4.2.0", + "eslint": "^5.10.0", + "mocha": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } +} diff --git a/src/algorithms/sorting/bubble-sort/README.md b/src/algorithms/sorting/bubble-sort/README.md new file mode 100644 index 0000000..0905bea --- /dev/null +++ b/src/algorithms/sorting/bubble-sort/README.md @@ -0,0 +1,36 @@ +# Bubble Sort in JavaScript + +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 + +This is an implementation of the bubble sort algorithm in JavaScript. The function sorts an array in place. + +**Note:** You should always use the builtin `sort()` method on arrays in your code because it is already optimized for production use. This implementation should be used for learning purposes only. + +## Usage + +Use CommonJS to get access to the `bubbleSort()` function: + +```js +const { bubbleSort } = require("@humanwhocodes/bubble-sort"); + +const items = [1, 5, 2]; +const result = bubbleSort(items); + +console.log(result); // [1, 2, 5] +``` + +## 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/algorithms/sorting/bubble-sort/bubble-sort.js b/src/algorithms/sorting/bubble-sort/bubble-sort.js new file mode 100644 index 0000000..49d4855 --- /dev/null +++ b/src/algorithms/sorting/bubble-sort/bubble-sort.js @@ -0,0 +1,54 @@ +/** + * @fileoverview Bubble sort implementation in JavaScript + */ + +/** + * Swaps two values in an array. + * @param {Array} items The array containing the items. + * @param {int} firstIndex Index of first item to swap. + * @param {int} secondIndex Index of second item to swap. + * @returns {void} + */ +function swap(items, firstIndex, secondIndex){ + var temp = items[firstIndex]; + items[firstIndex] = items[secondIndex]; + items[secondIndex] = temp; +} + +/** + * A bubble sort implementation in JavaScript. The array + * is sorted in-place. + * @param {Array} items An array of items to sort. + * @return {Array} The sorted array. + */ +exports.bubbleSort = (items) => { + + /* + * The outer loop moves from the first item in the array to the last item + * in the array. + */ + for (let i = 0; i < items.length; i++){ + + /* + * The inner loop also moves from the first item in the array towards + * a stopping point. The `stop` value is the length of the array + * minus the position of the outer loop minus one. The stop position + * is used because items start by being sorted at the back of the + * array and increasing towards the front of the array. The minus one + * is necessary because we are comparing each item to the next + * item, and the last item doesn't have a next item to compare to. + */ + for (let j = 0, stop = items.length - i - 1; j < stop; j++){ + + /* + * If the item at index `j` is greater than the item at index + * `j + 1`, then swap the values. + */ + if (items[j] > items[j + 1]){ + swap(items, j, j + 1); + } + } + } + + return items; +}; \ No newline at end of file diff --git a/src/algorithms/sorting/bubble-sort/package.json b/src/algorithms/sorting/bubble-sort/package.json new file mode 100644 index 0000000..1abe138 --- /dev/null +++ b/src/algorithms/sorting/bubble-sort/package.json @@ -0,0 +1,27 @@ +{ + "name": "@humanwhocodes/bubble-sort", + "version": "2.0.0", + "description": "A bubble sort implementation in JavaScript", + "main": "bubble-sort.js", + "scripts": { + "test": "npx mocha ../../../../tests/algorithms/sorting/bubble-sort/bubble-sort.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "sorting", + "algorithm", + "bubble sort" + ], + "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/binary-heap/README.md b/src/data-structures/binary-heap/README.md new file mode 100644 index 0000000..917d883 --- /dev/null +++ b/src/data-structures/binary-heap/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/binary-heap/binary-heap.js b/src/data-structures/binary-heap/binary-heap.js new file mode 100644 index 0000000..44fb1ec --- /dev/null +++ b/src/data-structures/binary-heap/binary-heap.js @@ -0,0 +1,365 @@ + +/** + * @fileoverview Binary Heap implementation in JavaScript + */ + +//----------------------------------------------------------------------------- +// Private +//----------------------------------------------------------------------------- + +/** + * Determines the index in an array that is the parent of the given index. + * @param {int} index The index to find the parent of. + * @returns {int} The index of the parent value. + * @private + */ +function getParentIndex(index) { + return Math.floor((index - 1) / 2); +} + +/** + * Determines the index in an array that is the left child of the given index. + * @param {int} index The index to find the left child of. + * @returns {int} The index of the left child value. + * @private + */ +function getLeftChildIndex(index) { + return (index * 2) + 1; +} + +/** + * Determines the index in an array that is the right child of the given index. + * @param {int} index The index to find the right child of. + * @returns {int} The index of the right child value. + * @private + */ +function getRightChildIndex(index) { + return (index * 2) + 2; +} + +/** + * Determines if a left child exists for the given index in the array. + * @param {Array} array The array to check. + * @param {int} index The index to check. + * @returns {boolean} True if the index has a left child, false if not. + * @private + */ +function hasLeftChild(array, index) { + return getLeftChildIndex(index) < array.length; +} + +/** + * Determines if a right child exists for the given index in the array. + * @param {Array} array The array to check. + * @param {int} index The index to check. + * @returns {boolean} True if the index has a right child, false if not. + * @private + */ +function hasRightChild(array, index) { + return getRightChildIndex(index) < array.length; +} + +/** + * Swaps the positions of two values in an array. + * @param {Array} array The array to swap values in. + * @param {int} index1 The first index to swap. + * @param {int} index2 The second index to swap. + * @returns {void} + * @private + */ +function swap(array, index1, index2) { + const value = array[index1]; + array[index1] = array[index2]; + array[index2] = value; +} + +/** + * Normalizes the heap by starting with the last inserted item and ensuring + * the entire path up to the heap root is in the correct order. + * @param {Array} array The array to adjust. + * @param {Function} compare The comparator to use on values in the array. + * @returns {void} + * @private + */ +function heapifyUp(array, compare) { + + /* + * `currentIndex` is used to traverse the array. It starts at the last item + * in the array and moves to the first item (the heap root). + */ + let currentIndex = array.length - 1; + + /* + * This loop continues so long as `currentIndex` is not the heap root (in + * position 0). When `currentIndex` is 0, it means the path from the last + * inserted value to the heap root is correct. + */ + while (currentIndex > 0) { + + // get the index of this value's parent so we can get the value + let parentIndex = getParentIndex(currentIndex); + + /* + * If the value of the parent should come after the current value + * (pointed to by `currentIndex`), then swap the two values so they + * are in the correct order. Note that any value returned from the + * comparator that is greater than zero means that the parent value + * should come after the current value. + */ + if (compare(array[parentIndex], array[currentIndex]) > 0) { + swap(array, parentIndex, currentIndex); + + // move the current index to the parent so the loop can continue + currentIndex = parentIndex; + } else { + + /* + * If we've reached here then the parent and current values are + * already in the correct order. We can infer that this means + * the rest of the path up to the root is also in the correct order + * and so we can safely exit the loop. + */ + break; + } + } +} + +/** + * Normalizes the heap by starting with the root item and ensuring + * the entire heap is in the correct order. This is run after a node is + * removed from the heap and the root is replaced with a value, so + * the root is most likely incorrect. + * @param {Array} array The array to adjust. + * @param {Function} compare The comparator to use on values in the array. + * @returns {void} + * @private + */ +function heapifyDown(array, compare) { + + /* + * `currentIndex` is used to traverse the array. It starts at the first item + * in the array and moves towards the last. + */ + let currentIndex = 0; + + /* + * This loop continues so long as the current item has at least one child. + * Because the heap is filled in starting with the left child and then + * moving to the right, simply checking if a left child is present is enough + * to continue because we know there is at least one child. + * + * When the current item has no children, we know the entire path there is + * in the correct state and so we can exit. + */ + while (hasLeftChild(array, currentIndex)) { + + /* + * This variable is called `smallerChildIndex` because we want to + * identify which of the two children contain the smaller value. + * We can start by assuming this will be the left child and then + * change it if we discover the right child is actually smaller. + */ + let smallerChildIndex = getLeftChildIndex(currentIndex); + + // if there is a right child, check that + if (hasRightChild(array, currentIndex)) { + let rightChildIndex = getRightChildIndex(currentIndex); + + /* + * If the right child value should come after the left child value + * (meaning the `compare()` function returns a value greater than + * zero), then note that the smaller value is actually in the right + * child by storing the right child index in `smallerChildIndex`. + */ + if (compare(array[smallerChildIndex], array[rightChildIndex]) > 0) { + smallerChildIndex = rightChildIndex; + } + } + + /* + * If the current value should come after the smaller child value, then + * the two values need to be swapped to be in the correct order. + */ + if (compare(array[currentIndex], array[smallerChildIndex]) > 0) { + swap(array, currentIndex, smallerChildIndex); + + // move down the tree to the previous location of the smaller child + currentIndex = smallerChildIndex; + } else { + + /* + * If we've reached here then the current and child values are + * already in the correct order. We can infer that this means + * the rest of the path down is also in the correct order + * and so we can safely exit the loop. + */ + break; + } + } + +} + +//----------------------------------------------------------------------------- +// BinaryHeap 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 array = Symbol("array"); +const compare = Symbol("compare"); + +/** + * A binary heap implementation in JavaScript. + * @class BinaryHeap + */ +class BinaryHeap { + + /** + * Creates a new instance of BinaryHeap + * @param {Function} [comparator] A comparator function. + */ + constructor(comparator = (a, b) => a - b) { + + /** + * Array used to manage the heap. + * @property array + * @type Array + * @private + */ + this[array] = []; + + /** + * Comparator to compare values. + * @property comparator + * @type Function + * @private + */ + this[compare] = comparator; + } + + /** + * Appends some data to the heap. + * @param {*} data The data to add to the heap. + * @returns {void} + */ + add(data) { + this[array].push(data); + heapifyUp(this[array], this[compare]); + } + + /** + * Determines if the heap is empty. + * @returns {boolean} True if the heap is empty, false if not. + */ + isEmpty() { + return this[array].length === 0; + } + + /** + * Returns the value at the top of the heap but does not remove it from + * the heap. + * @returns {*} The value at the top of the heap. + */ + peek() { + if (this.isEmpty()) { + throw new Error("Heap is empty."); + } + + return this[array][0]; + } + + /** + * Returns and removes the value at the top of the heap. + * @returns {*} The value at the top of the heap. + */ + poll() { + if (this.isEmpty()) { + throw new Error("Heap is empty."); + } + + /* + * If there are at least two items in the array, then we need to do a + * remove and rebalance operation to ensure the heap remains consistent. + */ + if (this[array].length > 1) { + + // first remove the top value for safe keeping + const topValue = this[array][0]; + + /* + * Next, take the last item from the array and move it into the top + * slot. This will likely not be the correct value but maintains the + * tree hierarchy. Then, reorganize the heap so it remains properly + * ordered. + */ + const replacementValue = this[array].pop(); + this[array][0] = replacementValue; + heapifyDown(this[array], this[compare]); + + // finally, return the value + return topValue; + } else { + + /* + * In this case, the array has only one item, so it's simpler to just + * pop the value off of the array and return it. + */ + return this[array].pop(); + } + + } + + /** + * 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 value exists in the heap. + * @param {*} value The value to search for. + * @returns {boolean} True if the value exists in the heap, false if not. + */ + includes(value) { + return this[array].includes(value); + } + + /** + * Removes all values from the heap. + * @returns {void} + */ + clear() { + this[array] = []; + } + + /** + * 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() { + return this[array].values(); + } + + /** + * Converts the heap into a string representation. + * @returns {String} A string representation of the heap. + */ + toString(){ + return [...this[array]].toString(); + } +} + +exports.BinaryHeap = BinaryHeap; \ No newline at end of file diff --git a/src/data-structures/binary-heap/package.json b/src/data-structures/binary-heap/package.json new file mode 100644 index 0000000..73016e3 --- /dev/null +++ b/src/data-structures/binary-heap/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/binary-search-tree/README.md b/src/data-structures/binary-search-tree/README.md new file mode 100644 index 0000000..0f951ca --- /dev/null +++ b/src/data-structures/binary-search-tree/README.md @@ -0,0 +1,64 @@ +# JavaScript Binary Search Tree 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 search tree. 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 tree is dynamically counted rather than stored. +1. Defining a `values()` generator method. + +Additionally, this implementation follows the JavaScript `Set` interface for adding, detecting, and removing values: + +* `add(value)` to add a value into the tree +* `has(value)` to detect if a value is in the tree +* `delete(value)` to remove a value from the tree + +## Usage + +Use CommonJS to get access to the `BinarySearchTree` constructor: + +```js +const { BinarySearchTree } = require("@humanwhocodes/binary-search-tree"); +``` + +Each instance of `BinarySearchTree` has the following properties and methods: + +```js +const tree = new BinarySearchTree(); + +// add an item to the tree +tree.add(2); + +// determine if a value is in the tree +let found = tree.has(2); + +// get the number of nodes in the tree +let count = tree.size; + +// convert to an array using iterators +let array1 = [...tree.values()]; +let array2 = [...tree]; + +// remove a node with the given value +let value = tree.delete(2); + +// remove all nodes +tree.clear(); +``` + +## 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 trees 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/binary-search-tree/binary-search-tree.js b/src/data-structures/binary-search-tree/binary-search-tree.js new file mode 100644 index 0000000..70cd8ba --- /dev/null +++ b/src/data-structures/binary-search-tree/binary-search-tree.js @@ -0,0 +1,464 @@ +/** + * @fileoverview Binary Search Tree implementation in JavaScript + */ + +/* + * 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"); + +/** + * Represents a single node in a BinarySearchTree. + * @class BinarySearchTree + */ +class BinarySearchTreeNode { + + /** + * Creates a new instance of BinarySearchTreeNode. + * @param {*} value The value to store in the node. + */ + constructor(value) { + + /** + * The value that this node stores. + * @property value + * @type * + */ + this.value = value; + + /** + * A pointer to the left node in the BinarySearchTree. + * @property left + * @type ?BinarySearchTreeNode + */ + this.left = null; + + /** + * A pointer to the right node in the BinarySearchTree. + * @property right + * @type ?BinarySearchTreeNode + */ + this.right = null; + + } +} + +/** + * A linked tree implementation in JavaScript. + * @class BinarySearchTree + */ +class BinarySearchTree { + + /** + * Creates a new instance of BinarySearchTree + */ + constructor() { + + /** + * Pointer to the root node in the tree. + * @property root + * @type ?BinarySearchTreeNode + * @private + */ + this[root] = null; + } + + /** + * Adds some value into the tree. This method traverses the tree to find + * the correct location to insert the value. Duplicate values are discarded. + * @param {*} value The value to add to the tree. + * @returns {void} + */ + add(value) { + + /* + * Create a new node to insert into the tree and store the value in it. + * This node will be added into the tree. + */ + const newNode = new BinarySearchTreeNode(value); + + // special case: no nodes in the tree yet + if (this[root] === null) { + this[root] = newNode; + } else { + + /* + * The `current` variable is used to track the item that is being + * used inside of the loop below. It starts out pointing to + * `this[root]` and is overwritten inside of the loop. + */ + let current = this[root]; + + /* + * This is loop is broken only when the node has been inserted + * in the correct spot or a duplicate value is found. + */ + while (current !== null) { + + // if the new value is less than this node's value, go left + if (value < current.value) { + + //if there's no left, then the new node belongs there + if (current.left === null) { + current.left = newNode; + break; + } else { + current = current.left; + } + + // if the new value is greater than this node's value, go right + } else if (value > current.value) { + + //if there's no right, then the new node belongs there + if (current.right === null) { + current.right = newNode; + break; + } else { + current = current.right; + } + + // if the new value is equal to the nodeToRemove one, just ignore + } else { + break; + } + } + } + } + + + /** + * Determines if a given value exists in the tree. + * @param {*} value The value to find. + * @returns {boolean} True if the value is found in the tree, false if not. + */ + has(value) { + + + /* + * `found` keeps track of whether or not the value in question was + * found in the tree. This variable is used for both the control + * condition of the loop below and is the return value of this function. + */ + let found = false; + + /* + * The `current` variable is used to track the item that is being + * used inside of the loop below. It starts out pointing to + * `this[root]` and is overwritten inside of the loop. + */ + let current = this[root]; + + /* + * The loop continues until either the value is found (so `found` + * is `true`) or the tree has been completely searched + * (`current` is `null`). + */ + while (!found && current !== null) { + + // if the value is less than the current node's, go left + if (value < current.value) { + current = current.left; + + // if the value is greater than the current node's, go right + } else if (value > current.value) { + current = current.right; + + //values are equal, found it! + } else { + found = true; + } + } + + /* + * Execution reaches here either because `found` is `true`, or because + * the tree was completely searched and the value was not found. Either + * way, the value of `found` is now the result of the search, so just + * return it. + */ + return found; + } + + + /** + * Deletes the value from the tree. + * @param {int} index The zero-based index of the item to remove. + * @returns {*} The value in the given position in the tree. + * @throws {RangeError} If index is out of range. + */ + delete(value) { + + // special case: the tree is empty, just exit + if (this[root] === null) { + return; + } + + /* + * The `found` variable keeps track of whether or not the value has + * been found in the tree. + */ + let found = false; + + /* + * These two variables keep track of the location during traversal. + * The `current` variable is the node we have traversed to while the + * `parent` variable is the parent node of `current`. Unlike with other + * traversals, we need to keep track of the parent so we can remove + * the child node. + */ + let current = this[root], + parent = null; + + /* + * The first step is to do a search for the value to remove. This is + * the same algorithm as the `has()` method, with the difference being + * that the parent is also tracked. + */ + while (!found && current !== null) { + + // if the value is less than the current node's, go left + if (value < current.value) { + parent = current; + current = current.left; + + // if the value is greater than the current node's, go right + } else if (value > current.value) { + parent = current; + current = current.right; + + // values are equal, found it! + } else { + found = true; + } + } + + // if the value wasn't found, just exit + if (!found) { + return; + } + + /* + * If we make it to here, the `nodeToRemove` variable continues the node + * to remove. This assignment isn't necessary but makes it easier to + * figure out what's going on in the code below. + */ + const nodeToRemove = current; + + /* + * The `replacement` variable is filled with what should replace + * `nodeToRemove`. It starts out set to `null` but can change based + * on what we find later. + */ + let replacement = null; + + /* + * The most complicated case is when the `nodeToRemove` node has both a left + * and a right child. In that case, we need to move things around to + * ensure the tree remains properly structured. + */ + if ((nodeToRemove.left !== null) && (nodeToRemove.right !== null)) { + + /* + * We need to find the best replacement for the removed node by + * traversing the subtrees. To start, we assume that the best + * replacement is `nodeToRemove.left`. + */ + replacement = nodeToRemove.left; + + /* + * We need to keep track of the replacement's parent to modify + * the subtree as we go. + */ + let replacementParent = nodeToRemove; + + /* + * The best replacement is found by traversing the right subtree + * of `replacement`. The rightmost node in this subtree is the + * largest value in `nodeToRemove`'s left subtree and so is the + * easiest one to use as a replacement to minimize the number of + * modifications we need to do. + */ + while (replacement.right !== null) { + replacementParent = replacement; + replacement = replacement.right; + } + + + /* + * Because `replacement` has no right subtree, we can copy over the + * `nodeToRemove`'s right subtree directly. + */ + replacement.right = nodeToRemove.right; + + /* + * Special case: if `nodeToRemove.left` doesn't have a right subtree, + * then `replacementParent` will be equal to `nodeToRemove`. In that + * case, we should not make any further changes. Otherwise, we need + * to rearrange some more nodes. + */ + if (replacementParent !== nodeToRemove) { + + /* + * Remove `replacement` from its current location and replace it + * with `replacement.left` (we know `replacement.right` is `null`). + * It's possible that `replacement.left` is `null`, but that doesn't + * matter. Both `null` and non-`null` values keep the tree intact. + */ + replacementParent.right = replacement.left; + + /* + * Assign the complete left subtree of `nodeToRemove` to + * `replacement` to maintain the proper structure. + */ + replacement.left = nodeToRemove.left; + } + + } else if (nodeToRemove.left !== null) { + replacement = nodeToRemove.left; + } else if (nodeToRemove.right !== null) { + replacement = nodeToRemove.right; + } + + /* + * If the `nodeToRemove` node has no children, then the default value of + * `null` is used for `replacement`. + */ + + // special case: the `nodeToRemove` node is the root + if (nodeToRemove === this[root]) { + this[root] = replacement; + } else { + + if (nodeToRemove.value < parent.value) { + parent.left = replacement; + } else { + parent.right = replacement; + } + + } + + + + } + + /** + * Removes all nodes from the tree. + * @returns {void} + */ + clear() { + this[root] = null; + } + + /** + * Returns the number of items in the tree. + * @returns {int} The number of items in the tree. + */ + get size() { + + // special case: the tree is empty + if (this[root] === null) { + return 0; + } + + /* + * The `count` variable is used to keep track of how many items have + * been visited inside the loop below. This is important because this + * is the value to return from this method. + */ + let count = 0; + + /* + * Traversal is easiest when using a recursive function, so define + * a helper function here. This function does an in-order traversal + * of the tree, meaning it yields values in sorted order from + * lowest value to highest. It does this by traversing to the leftmost + * node first, then working its way back up the tree, visiting right nodes + * along the way. + */ + const traverse = (node) => { + + // special case: there is no node + if (node) { + + //traverse the left subtree + if (node.left !== null) { + traverse(node.left); + } + + // increment the counter + count++; + + //traverse the right subtree + if (node.right !== null) { + traverse(node.right); + } + } + }; + + // start traversing from the root + traverse(this[root]); + + // return the final count, which was updated inside traverse() + return count; + + } + + /** + * 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 tree. + * @returns {Iterator} An iterator on the tree. + */ + *values(){ + + /* + * Traversal is easiest when using a recursive function, so define + * a helper function here. This function does an in-order traversal + * of the tree, meaning it yields values in sorted order from + * lowest value to highest. It does this by traversing to the leftmost + * node first, then working its way back up the tree, visiting right nodes + * along the way. + * + * This function cannot be an arrow function because arrow functions + * cannot be generators. + */ + function *traverse(node) { + + // special case: there is no node + if (node) { + + //traverse the left subtree + if (node.left !== null) { + yield* traverse(node.left); + } + + // emit the value + yield node.value; + + //traverse the right subtree + if (node.right !== null) { + yield* traverse(node.right); + } + } + } + + yield* traverse(this[root]); + } + + /** + * Converts the tree into a string representation. + * @returns {String} A string representation of the tree. + */ + toString(){ + return [...this].toString(); + } +} + +exports.BinarySearchTree = BinarySearchTree; \ No newline at end of file diff --git a/src/data-structures/binary-search-tree/package.json b/src/data-structures/binary-search-tree/package.json new file mode 100644 index 0000000..2414f8f --- /dev/null +++ b/src/data-structures/binary-search-tree/package.json @@ -0,0 +1,28 @@ +{ + "name": "@humanwhocodes/binary-search-tree", + "version": "2.0.0", + "description": "A binary search tree implementation in JavaScript", + "main": "binary-search-tree.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/binary-search-tree/binary-search-tree.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "binary search tree", + "linked list", + "data structure", + "iterable" + ], + "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/circular-doubly-linked-list/README.md b/src/data-structures/circular-doubly-linked-list/README.md new file mode 100644 index 0000000..284eb9a --- /dev/null +++ b/src/data-structures/circular-doubly-linked-list/README.md @@ -0,0 +1,74 @@ +# JavaScript Circular Doubly Linked List 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 circular doubly linked list. 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. Returning `undefined` from `get()` when no such index exists. + +Read the [blog post](https://humanwhocodes.com/blog/2019/03/computer-science-in-javascript-circular-doubly-linked-lists/) about the design of this class. + +## Usage + +Use CommonJS to get access to the `CircularDoublyLinkedList` constructor: + +```js +const { CircularDoublyLinkedList } = require("@humanwhocodes/circular-doubly-linked-list"); +``` + +Each instance of `CircularDoublyLinkedList` has the following properties and methods: + +```js +const list = new CircularDoublyLinkedList(); + +// add an item to the end +list.add("foo"); + +// insert an item +list.insertBefore("bar", 0); +list.insertAfter("baz", 1); + +// get the value at an index +let value = list.get(0); + +// get the number of items +let count = list.size; + +// get the index of a value +let index = list.indexOf("foo"); + +// convert to an array using iterators +let array1 = [...list.values()]; +let array2 = [...list]; + +// create a circular iterator to keep iterating over values +const iterator = list.circularValues(); + +// convert to an array in reverse order using an iterator +let array3 = [...list.reverse()]; + +// remove an item at the given index and return the data that was removed +let data = list.remove(0); + +// remove all items +list.clear(); +``` + +## 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/circular-doubly-linked-list/circular-doubly-linked-list.js b/src/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js new file mode 100644 index 0000000..54bc937 --- /dev/null +++ b/src/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js @@ -0,0 +1,669 @@ +/** + * @fileoverview Circular Doubly linked list implementation in JavaScript + */ + +/* + * 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 head = Symbol("head"); + +/** + * Represents a single item in a CircularDoublyLinkedList. + * @class CircularDoublyLinkedListNode + */ +class CircularDoublyLinkedListNode { + + /** + * Creates a new instance of CircularDoublyLinkedListNode. + * @param {*} data The data to store in the node. + */ + constructor(data) { + + /** + * The data that this node stores. + * @property data + * @type * + */ + this.data = data; + + /** + * A pointer to the next item in the CircularDoublyLinkedList. + * @property next + * @type ?CircularDoublyLinkedListNode + */ + this.next = null; + + /** + * A pointer to the previous item in the CircularDoublyLinkedList. + * @property previous + * @type ?CircularDoublyLinkedListNode + */ + this.previous = null; + } +} + +/** + * A doubly linked list implementation in JavaScript. + * @class CircularDoublyLinkedList + */ +class CircularDoublyLinkedList { + + /** + * Creates a new instance of CircularDoublyLinkedList + */ + constructor() { + + /** + * Pointer to first item in the list. + * @property head + * @type ?CircularDoublyLinkedListNode + * @private + */ + this[head] = null; + } + + /** + * Appends some data to the end of the list. This method traverses + * the existing list and places the data at the end in a new item. + * @param {*} data The data to add to the list. + * @returns {void} + */ + add(data) { + + /* + * Create a new list item object and store the data in it. + * This item will be added to the end of the existing list. + */ + const newNode = new CircularDoublyLinkedListNode(data); + + // special case: no items in the list yet + if (this[head] === null) { + + /* + * Because there are no items in the list, just set the + * `this[head]` pointer to the new item. + */ + this[head] = newNode; + + /* + * Setup the new node to point to itself in both directions + * to create the circular link. + */ + newNode.next = newNode; + newNode.previous = newNode; + } else { + + // get a reference to the last item in the list + const tail = this[head].previous; + + /* + * Setup the tail and `newNode` to point to each other, effectively + * adding `newNode` to the end of the list. + */ + tail.next = newNode; + newNode.previous = tail; + + /* + * Because `newNode` is now the last item, `newNode.next` must point + * to the head and vice versa. + */ + newNode.next = this[head]; + this[head].previous = newNode; + } + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new item at a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index at which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertBefore(data, index) { + + /* + * Create a new list item object and store the data in it. + * This item will be inserted into the existing list. + */ + const newNode = new CircularDoublyLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * Special case: if `index` is `0`, then no traversal is needed + * and we need to update `this[head]` to point to `newNode`. + */ + if (index === 0) { + + // get the last item in the list to make things a bit clearer + const tail = this[head].previous; + + // first make `tail` and `newNode` point to each other + tail.next = newNode; + newNode.previous = tail; + + // then make `this[head]` and `newNode` point to each other + newNode.next = this[head]; + this[head].previous = newNode; + + // now it's safe to update `this[head]` to be `newNode` + this[head] = newNode; + } else { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + * + * The `previous` variable tracks one step behind `current`, which + * is necessary because we need to adjust the node at `index`-1's + * `next` pointer to point to the new node. + */ + let current = this[head], + previous = null; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse and make sure to keep track of how many nodes have + * been visited and update the `previous` pointer in addition to + * `current`. When `i` is the same as `index`, it means we've + * found the location to insert the new data. + */ + while ((current.next !== this[head]) && (i < index)) { + previous = current; + current = current.next; + i++; + } + + /* + * At this point, `current` is either the item to insert the new data + * before, or the last item in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data before and `previous` is the node to insert + * new data after. So `previous.next` must point to `newNode` and + * `newNode.next` must point to `current`. + */ + previous.next = newNode; + newNode.previous = previous; + + newNode.next = current; + current.previous = newNode; + } + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new item after a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index after which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertAfter(data, index) { + + /* + * Create a new list item object and store the data in it. + * This item will be inserted into the existing list. + */ + const newNode = new CircularDoublyLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` (the first node) and is overwritten inside of the loop. + */ + let current = this[head]; + + // special case: insert after index 0 doesn't require a traversal + if (index > 0) { + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse and keep track of how many nodes have been visited and + * update the `current` pointer. When `i` is the same as `index`, + * it means we've found the location to insert the new data. + */ + do { + current = current.next; + i++; + } while ((current !== this[head]) && (i < index)); + + /* + * At this point, `current` is either the node to insert the new data + * before, or the last node in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data after. So `current.next` must point to + * `newNode` for the data to be in the correct spot, but before that, + * `newNode.next` must point to `current.next` to ensure the list + * remains intact. + */ + newNode.next = current.next; + current.next.previous = newNode; + + current.next = newNode; + newNode.previous = current; + } + + /** + * Retrieves the data in the given position in the list. + * @param {int} index The zero-based index of the item whose data + * should be returned. + * @returns {*} The data in the "data" portion of the given item + * or undefined if the item doesn't exist. + */ + get(index) { + + // ensure `index` is a positive value and the list isn't empty + if ((index > -1) && (this[head] !== null)) { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` (the first node) and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the nodes in the list and keep track of how deep we are + * into the list. If we've reached the first node in the list + * (`this[head]`) or we've gone past the end of the list + * (`i > index`), then exit the loop. + */ + do { + + /* + * If the current position matches, the index being requested, + * then return the data in the current node and exit immediately. + */ + if (i === index) { + return current.data; + } + + // otherwise, go on to the next node + current = current.next; + + // and increment the index + i++; + + } while ((current !== this[head]) && (i <= index)); + + /* + * If we've made it here, it means that that the index is past the + * end of the list. Execution now falls through the last `return` + * statement in this method, returning `undefined` to indicate no + * data was found. + */ + } + + return undefined; + } + + /** + * Retrieves the index of the data in the list. + * @param {*} data The data to search for. + * @returns {int} The index of the first instance of the data in the list + * or -1 if not found. + */ + indexOf(data) { + + // special case: the list is empty so there's nothing to search + if (this[head] === null) { + return -1; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `index` variable is used to track how deep into the list we've + * gone. This is important because this is the value that is returned + * from this method. + */ + let index = 0; + + /* + * This loop checks each node in the list to see if it matches `data`. + * If a match is found, it returns `index` immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search because `current` + * is once again pointing the first node in the list. + */ + do { + + /* + * If the data in the current node matches the data we are looking + * for, then return `index`. This exits the loop immediately. + */ + if (current.data === data) { + return index; + } + + // otherwise, go on to the next node + current = current.next; + + // and increment the index + index++; + + } while (current !== this[head]); + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return -1 as the "not found" value. + */ + return -1; + } + + /** + * Removes the item from the given location in the list. + * @param {int} index The zero-based index of the item to remove. + * @returns {*} The data in the given position in the list. + * @throws {RangeError} If index is out of range. + */ + remove(index) { + + // special cases: no nodes in the list or `index` is an invalid value + if ((this[head] === null) || (index < 0)) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + // special case: removing the first node + if (index === 0) { + + // if there's only one node, null out `this[head]` + if (current.next === this[head]) { + this[head] = null; + } else { + + // get the last item in the list + const tail = this[head].previous; + + /* + * Set the tail to point to the second item in the list. + * Then make sure that item also points back to the tail. + */ + tail.next = current.next; + current.next.previous = tail; + + // now it's safe to update the head + this[head] = tail.next; + } + + // return the data at the previous head of the list + return current.data; + } + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to remove. + */ + let i = 0; + + /* + * Traverse the list and exit the loop when either the start of the + * list is encountered or `i` is no longer less than `index` (meaning + * we have found the node to remove). + */ + do { + + // traverse to the next node + current = current.next; + + // increment the count + i++; + + } while ((current !== this[head]) && (i < index)); + + /* + * If `current` isn't `this[head]`, then that means we've found the node + * to remove. + */ + if (current !== this[head]) { + + // skip over the node to remove + current.previous.next = current.next; + current.next.previous = current.previous; + + // return the value that was just removed from the list + return current.data; + } + + /* + * If we've made it this far, it means `index` is a value that + * doesn't exist in the list, so throw an error. + */ + throw new RangeError(`Index ${index} does not exist in the list.`); + + } + + /** + * Removes all items from the list. + * @returns {void} + */ + clear() { + + // just reset the head pointer to null + this[head] = null; + } + + /** + * Returns the number of items in the list. + * @returns {int} The number of items in the list. + */ + get size() { + + // special case: the list is empty + if (this[head] === null) { + return 0; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. `this[head]` points to the first node + * in the list because the last node always points to the first node. + */ + let current = this[head]; + + /* + * The `count` variable is used to keep track of how many nodes have + * been visited inside the loop below. This is important because this + * is the value to return from this method. + */ + let count = 0; + + /* + * Because the list is circular, we need to stop when `current` is + * equal to `this[head]`, otherwise this will be an infinite loop. + */ + do { + count++; + current = current.next; + } while (current !== this[head]); + + /* + * The loop is exited and the value of `count` + * is the number of nodes that were counted in the loop. + */ + return count; + + } + + /** + * 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() { + + // special case: list is empty + if (this[head] !== null) { + + // special case: only one node + if (this[head].next === this[head]) { + yield this[head].data; + } else { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * Because the list is circular, we need to stop when `current` is + * equal to the first node, otherwise this will be an infinite loop. + * And because `current` starts out equal to the first node, we need + * to use a post-test loop because we know the loop should execute + * at least once. + */ + do { + yield current.data; + current = current.next; + } while (current !== this[head]); + } + + } + } + + /** + * Create an iterator that returns each node in the list and repeats + * each node if it continues to be called. This is designed to be used + * to manually iterate through the list, outside of using syntax such + * as `for-of` (which will result in an infinite loop with this iterator). + * @returns {Iterator} A circular iterator on the list. + */ + *circularValues() { + + // special case: list is empty + if (this[head] !== null) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * This is an infinite loop if you remove the `yield` call. The `yield` + * allows execution to stop and not pick up again until the iterator's + * `next()` method is called again. + * + * It's not possible for this loop to exit. Even removing all nodes + * from the list using `remove()` or `clear()` will not cause the + * loop to stop yield values. That's because `current.next` always + * has a value, even if it just points back to `current`. + */ + do { + yield current.data; + current = current.next; + } while (true); + } + + } + + /** + * Create an iterator that returns each item in the list in reverse order. + * @returns {Iterator} An iterator on the list. + */ + *reverse(){ + + // special case: list is empty + if (this[head] !== null) { + + /* + * The `current` variable is used to iterate over the list items. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head].previous; + + /* + * As long as `current` is not `head`, there is a piece of data + * to yield. + */ + do { + yield current.data; + current = current.previous; + } while (current !== this[head].previous); + } + } + + /** + * Converts the list into a string representation. + * @returns {String} A string representation of the list. + */ + toString(){ + return [...this].toString(); + } +} + +exports.CircularDoublyLinkedList = CircularDoublyLinkedList; \ No newline at end of file diff --git a/src/data-structures/circular-doubly-linked-list/package.json b/src/data-structures/circular-doubly-linked-list/package.json new file mode 100644 index 0000000..840ee80 --- /dev/null +++ b/src/data-structures/circular-doubly-linked-list/package.json @@ -0,0 +1,30 @@ +{ + "name": "@humanwhocodes/circular-doubly-linked-list", + "version": "2.0.2", + "description": "A circular doubly linked list implementation in JavaScript", + "main": "circular-doubly-linked-list.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "circular linked list", + "cicular doubly linked list", + "linked list", + "doubly linked list", + "data structure", + "iterable" + ], + "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/circular-linked-list/README.md b/src/data-structures/circular-linked-list/README.md new file mode 100644 index 0000000..84aaad7 --- /dev/null +++ b/src/data-structures/circular-linked-list/README.md @@ -0,0 +1,69 @@ +# JavaScript Circular Linked List 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 linked list. 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. Returning `undefined` from `get()` when no such index exists. + +## Usage + +Use CommonJS to get access to the `CircularLinkedList` constructor: + +```js +const { CircularLinkedList } = require("@humanwhocodes/circular-linked-list"); +``` + +Each instance of `CircularLinkedList` has the following properties and methods: + +```js +const list = new CircularLinkedList(); + +// add an item to the end +list.add("foo"); + +// insert an item +list.insertBefore("bar", 0); +list.insertAfter("baz", 1); + +// get the value at an index +let value = list.get(0); + +// get the number of items +let count = list.size; + +// get the index of a value +let index = list.indexOf("foo"); + +// convert to an array using iterators +let array1 = [...list.values()]; +let array2 = [...list]; + +// create a circular iterator to keep iterating over values +const iterator = list.circularValues(); + +// remove an item at the given index and return the data that was removed +let data = list.remove(0); + +// remove all items +list.clear(); +``` + +## 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/circular-linked-list/circular-linked-list.js b/src/data-structures/circular-linked-list/circular-linked-list.js new file mode 100644 index 0000000..c4c3137 --- /dev/null +++ b/src/data-structures/circular-linked-list/circular-linked-list.js @@ -0,0 +1,616 @@ +/** + * @fileoverview Circular Linked List implementation in JavaScript + */ + +/* + * 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 tail = Symbol("tail"); + +/** + * Represents a single node in a LinkedList. + * @class CircularLinkedListNode + */ +class CircularLinkedListNode { + + /** + * Creates a new instance of CircularLinkedListNode. + * @param {*} data The data to store in the node. + */ + constructor(data) { + + /** + * The data that this node stores. + * @property data + * @type * + */ + this.data = data; + + /** + * A pointer to the next node in the LinkedList. + * @property next + * @type ?CircularLinkedListNode + */ + this.next = null; + + } +} + +/** + * A linked list implementation in JavaScript. + * @class CircularLinkedList + */ +class CircularLinkedList { + + /** + * Creates a new instance of LinkedList + */ + constructor() { + + /** + * Pointer to last node added to the list. + * @property tail + * @type ?CircularLinkedListNode + * @private + */ + this[tail] = null; + } + + /** + * Appends some data to the end of the list. This method traverses + * the existing list and places the data at the end in a new node. + * @param {*} data The data to add to the list. + * @returns {void} + */ + add(data) { + + /* + * Create a new list node object and store the data in it. + * This node will be added to the end of the existing list. + */ + const newNode = new CircularLinkedListNode(data); + + //special case: no nodes in the list yet + if (this[tail] === null) { + + /* + * Because there are no nodes in the list, set `node.next` + * equal to itself to complete the cycle. + */ + newNode.next = newNode; + } else { + + /* + * We are inserting `newNode` in between `this[tail]` and + * `this[tail].next`, so update pointers. + */ + newNode.next = this[tail].next; + this[tail].next = newNode; + } + + // update the pointer to the last inserted node + this[tail] = newNode; + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new node at a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index at which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertBefore(data, index) { + + /* + * Create a new list node object and store the data in it. + * This node will be inserted into the existing list. + */ + const newNode = new CircularLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[tail] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * Special case: if `index` is `0`, then no traversal is needed + * and we need to update `this[tail]` to point to `newNode`. First, + * set `node.next` to the current `this[tail]` so the previous + * head of the list is now the second node in the list. Then it's + * safe to update `this[tail]` to point to `newNode`. + */ + if (index === 0) { + newNode.next = this[tail].next; + this[tail].next = newNode; + } else { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[tail].next` and is overwritten inside of the loop. + * + * The `previous` variable tracks one step behind `current`, which + * is necessary because we need to adjust the node at `index`-1's + * `next` pointer to point to the new node. + */ + let current = this[tail].next, + previous = null; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse and make sure to keep track of how many nodes have + * been visited and update the `previous` pointer in addition to + * `current`. When `i` is the same as `index`, it means we've + * found the location to insert the new data. + */ + while ((current.next !== this[tail].next) && (i < index)) { + previous = current; + current = current.next; + i++; + } + + /* + * At this point, `current` is either the item to insert the new data + * before, or the last item in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data before and `previous` is the node to insert + * new data after. So `previous.next` must point to `newNode` and + * `node.next` must point to `current`. + */ + previous.next = newNode; + newNode.next = current; + } + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new node after a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index after which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertAfter(data, index) { + + /* + * Create a new list node object and store the data in it. + * This node will be inserted into the existing list. + */ + const newNode = new CircularLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[tail] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[tail].next` (the first node) and is overwritten inside of the loop. + */ + let current = this[tail].next; + + // special case: insert after index 0 doesn't require a traversal + if (index > 0) { + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse and keep track of how many nodes have been visited and + * update the `current` pointer. When `i` is the same as `index`, + * it means we've found the location to insert the new data. + */ + do { + current = current.next; + i++; + } while ((current !== this[tail]) && (i < index)); + + /* + * At this point, `current` is either the node to insert the new data + * before, or the last node in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data after. So `current.next` must point to + * `newNode` for the data to be in the correct spot, but before that, + * `newNode.next` must point to `current.next` to ensure the list + * remains intact. + */ + newNode.next = current.next; + current.next = newNode; + + if (current === this[tail]) { + this[tail] = newNode; + } + } + + /** + * Retrieves the data in the given position in the list. + * @param {int} index The zero-based index of the node whose data + * should be returned. + * @returns {*} The data in the "data" portion of the given node + * or undefined if the node doesn't exist. + */ + get(index) { + + // ensure `index` is a positive value and the list isn't empty + if ((index > -1) && (this[tail] !== null)) { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[tail].next` (the first node) and is overwritten inside of the loop. + */ + let current = this[tail].next; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the nodes in the list and keep track of how deep we are + * into the list. If we've reached the first node in the list + * (`this[head].next`) or we've gone past the end of the list + * (`i > index`), then exit the loop. + */ + do { + + /* + * If the current position matches, the index being requested, + * then return the data in the current node and exit immediately. + */ + if (i === index) { + return current.data; + } + + // otherwise, go on to the next node + current = current.next; + + // and increment the index + i++; + + } while ((current !== this[tail].next) && (i <= index)); + + /* + * If we've made it here, it means that that the index is past the + * end of the list. Execution now falls through the last `return` + * statement in this method, returning `undefined` to indicate no + * data was found. + */ + } + + return undefined; + } + + /** + * Retrieves the index of the data in the list. + * @param {*} data The data to search for. + * @returns {int} The index of the first instance of the data in the list + * or -1 if not found. + */ + indexOf(data) { + + // special case: the list is empty so there's nothing to search + if (this[tail] === null) { + return -1; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[tail].next; + + /* + * The `index` variable is used to track how deep into the list we've + * gone. This is important because this is the value that is returned + * from this method. + */ + let index = 0; + + /* + * This loop checks each node in the list to see if it matches `data`. + * If a match is found, it returns `index` immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search because `current` + * is once again pointing the first node in the list. + */ + do { + + /* + * If the data in the current node matches the data we are looking + * for, then return `index`. This exits the loop immediately. + */ + if (current.data === data) { + return index; + } + + // otherwise, go on to the next node + current = current.next; + + // and increment the index + index++; + + } while (current !== this[tail].next); + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return -1 as the "not found" value. + */ + return -1; + } + + /** + * Removes the node from the given location in the list. + * @param {int} index The zero-based index of the node to remove. + * @returns {*} The data in the given position in the list. + * @throws {RangeError} If index is out of range. + */ + remove(index) { + + // special case: no nodes in the list + if (this[tail] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + // special case: `index` is an invalid value + if (index < 0) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[tail].next; + + // special case: removing the first node + if (index === 0) { + + // if there's only one node, null out `this[tail]` + if (current.next === this[tail]) { + this[tail] = null; + } else { + /* + * The tail doesn't change when there is more than one item + * in the list. Just skip over `current` by setting + * `this[tail].next` to `current.next`. + */ + this[tail].next = current.next; + } + + // return the data at the previous head of the list + return current.data; + } + + /* + * The `previous` variable keeps track of the node just before + * `current` in the loop below. This is necessary because removing + * an node means updating the previous node's `next` pointer. + */ + let previous = null; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to remove. + */ + let i = 0; + + /* + * Traverse the list, keeping track of the previous position so + * that we can remove the node once it's found. The loop is exited + * when either the start of the list is encountered or `i` is no + * longer less than `index` (meaning we have found the node to remove). + */ + do { + + // save the value of current + previous = current; + + // traverse to the next node + current = current.next; + + // increment the count + i++; + + } while ((current !== this[tail].next) && (i < index)); + + /* + * If `current` isn't `this[tail].next`, then that means we've found the node + * to remove. + */ + if (current !== this[tail].next) { + + // skip over the node to remove + previous.next = current.next; + + // return the value that was just removed from the list + return current.data; + } + + /* + * If we've made it this far, it means `index` is a value that + * doesn't exist in the list, so throw an error. + */ + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /** + * Removes all nodes from the list. + * @returns {void} + */ + clear() { + this[tail] = null; + } + + /** + * Returns the number of nodes in the list. + * @returns {int} The number of nodes in the list. + */ + get size() { + + // special case: the list is empty + if (this[tail] === null) { + return 0; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. `this[tail].next` points to the first node + * in the list because the last node always points to the first node. + */ + let current = this[tail].next; + + /* + * The `count` variable is used to keep track of how many nodes have + * been visited inside the loop below. This is important because this + * is the value to return from this method. + */ + let count = 0; + + /* + * Because the list is circular, we need to stop when `current` is + * equal to `this[tail].next`, otherwise this will be an infinite loop. + */ + do { + count++; + current = current.next; + } while (current !== this[tail].next); + + /* + * The loop is exited and the value of `count` + * is the number of nodes that were counted in the loop. + */ + return count; + } + + /** + * 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(){ + + // special case: list is empty + if (this[tail] !== null) { + + // special case: only one node + if (this[tail].next === this[tail]) { + yield this[tail].data; + } else { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. `this[tail].next] points to the first node + * in the list because the last node always points to the first node. + */ + let current = this[tail].next; + + /* + * Because the list is circular, we need to stop when `current` is + * equal to the first node, otherwise this will be an infinite loop. + * And because `current` starts out equal to the first node, we need + * to use a post-test loop because we know the loop should execute + * at least once. + */ + do { + yield current.data; + current = current.next; + } while (current !== this[tail].next); + } + + } + } + + /** + * Create an iterator that returns each node in the list and repeats + * each node if it continues to be called. This is designed to be used + * to manually iterate through the list, outside of using syntax such + * as `for-of` (which will result in an infinite loop with this iterator). + * @returns {Iterator} A circular iterator on the list. + */ + *circularValues(){ + + // special case: list is empty + if (this[tail] !== null) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. `this[tail].next] points to the first node + * in the list because the last node always points to the first node. + */ + let current = this[tail].next; + + /* + * This is an infinite loop if you remove the `yield` call. The `yield` + * allows execution to stop and not pick up again until the iterator's + * `next()` method is called again. + * + * It's possible for this loop to exit if the list is emptied + * in between calls to the iterator's `next()` method. That will + * cause `current` to be `null` and the iterator will close. + */ + do { + yield current.data; + current = current.next; + } while (current !== null); + } + + } + + /** + * Converts the list into a string representation. + * @returns {String} A string representation of the list. + */ + toString(){ + return [...this].toString(); + } +} + +exports.CircularLinkedList = CircularLinkedList; \ No newline at end of file diff --git a/src/data-structures/circular-linked-list/package.json b/src/data-structures/circular-linked-list/package.json new file mode 100644 index 0000000..ec46d61 --- /dev/null +++ b/src/data-structures/circular-linked-list/package.json @@ -0,0 +1,28 @@ +{ + "name": "@humanwhocodes/circular-linked-list", + "version": "2.0.0", + "description": "A circular linked list implementation in JavaScript", + "main": "circular-linked-list.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/circular-linked-list/circular-linked-list.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "circular linked list", + "linked list", + "data structure", + "iterable" + ], + "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/doubly-linked-list/README.md b/src/data-structures/doubly-linked-list/README.md new file mode 100644 index 0000000..da6a042 --- /dev/null +++ b/src/data-structures/doubly-linked-list/README.md @@ -0,0 +1,77 @@ +# JavaScript Doubly Linked List 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 doubly linked list. 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. Defining a `find()` method for searching the list to return data. +1. Defining a `findIndex()` method for searching the list to return an index. +1. Returning `undefined` from `get()` when no such index exists. + +Read the [blog post](https://humanwhocodes.com/blog/2019/02/computer-science-in-javascript-doubly-linked-lists/) about the design of this class. + +## Usage + +Use CommonJS to get access to the `DoublyLinkedList` constructor: + +```js +const { DoublyLinkedList } = require("@humanwhocodes/doubly-linked-list"); +``` + +Each instance of `DoublyLinkedList` has the following properties and methods: + +```js +const list = new DoublyLinkedList(); + +// add an item to the end +list.add("foo"); + +// insert an item +list.insertBefore("bar", 0); +list.insertAfter("baz", 1); + +// get the value at an index +let value = list.get(0); + +// get the number of items +let count = list.size; + +// get the index of a value +let index = list.indexOf("foo"); + +// search for a value +let result = list.find(value => value.length > 3); +let foundIndex = list.findIndex(value => value.length > 3); + +// convert to an array using iterators +let array1 = [...list.values()]; +let array2 = [...list]; + +// convert to an array in reverse order using an iterator +let array3 = [...list.reverse()]; + +// remove an item at the given index and return the data that was removed +let data = list.remove(0); + +// remove all items +list.clear(); +``` + +## 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/doubly-linked-list/doubly-linked-list.js b/src/data-structures/doubly-linked-list/doubly-linked-list.js new file mode 100644 index 0000000..cad7fca --- /dev/null +++ b/src/data-structures/doubly-linked-list/doubly-linked-list.js @@ -0,0 +1,694 @@ +/** + * @fileoverview Doubly linked list implementation in JavaScript + */ + +/* + * 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 head = Symbol("head"); +const tail = Symbol("tail"); + +/** + * Represents a single node in a DoublyLinkedList. + * @class DoublyLinkedListNode + */ +class DoublyLinkedListNode { + + /** + * Creates a new instance of DoublyLinkedListNode. + * @param {*} data The data to store in the node. + */ + constructor(data) { + + /** + * The data that this node stores. + * @property data + * @type * + */ + this.data = data; + + /** + * A pointer to the next node in the DoublyLinkedList. + * @property next + * @type ?DoublyLinkedListNode + */ + this.next = null; + + /** + * A pointer to the previous node in the DoublyLinkedList. + * @property previous + * @type ?DoublyLinkedListNode + */ + this.previous = null; + } +} + +/** + * A doubly linked list implementation in JavaScript. + * @class DoublyLinkedList + */ +class DoublyLinkedList { + + /** + * Creates a new instance of DoublyLinkedList + */ + constructor() { + + /** + * Pointer to first node in the list. + * @property head + * @type ?DoublyLinkedListNode + * @private + */ + this[head] = null; + + /** + * Pointer to last node in the list. + * @property tail + * @type ?DoublyLinkedListNode + * @private + */ + this[tail] = null; + } + + /** + * Appends some data to the end of the list. + * @param {*} data The data to add to the list. + * @returns {void} + */ + add(data) { + + /* + * Create a new list node object and store the data in it. + * This node will be added to the end of the existing list. + */ + const newNode = new DoublyLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + + /* + * Because there are no nodes in the list, just set the + * `this[head]` pointer to the new node. + */ + this[head] = newNode; + } else { + + /* + * Unlike in a singly linked list, we have a direct reference to + * the last node in the list. Set the `next` pointer of the + * current last node to `newNode` in order to append the new data + * to the end of the list. Then, set `newNode.previous` to the current + * tail to ensure backwards tracking work. + */ + this[tail].next = newNode; + newNode.previous = this[tail]; + } + + /* + * Last, reset `this[tail]` to `newNode` to ensure we are still + * tracking the last node correctly. + */ + this[tail] = newNode; + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new node at a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index at which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertBefore(data, index) { + + /* + * Create a new list node object and store the data in it. + * This node will be inserted into the existing list. + */ + const newNode = new DoublyLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * Special case: if `index` is `0`, then no traversal is needed + * and we need to update `this[head]` to point to `newNode`. + */ + if (index === 0) { + + /* + * Ensure the new node's `next` property is pointed to the current + * head. + */ + newNode.next = this[head]; + + /* + * The current head's `previous` property needs to point to the new + * node to ensure the list is traversable backwards. + */ + this[head].previous = newNode; + + /* + * Now it's safe to set `this[head]` to the new node, effectively + * making the new node the first node in the list. + */ + this[head] = newNode; + } else { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the list nodes using `next` pointers, and make + * sure to keep track of how many nodes have been visited. When + * `i` is the same as `index`, it means we've found the location to + * insert the new data. + */ + while ((current.next !== null) && (i < index)) { + current = current.next; + i++; + } + + /* + * At this point, `current` is either the node to insert the new data + * before, or the last node in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data before. + * + * First, insert `newNode` after `current.previous` by updating + * `current.previous.next` and `newNode.previous`. + */ + current.previous.next = newNode; + newNode.previous = current.previous; + + /* + * Next, insert `current` after `newNode` by updating `newNode.next` and + * `current.previous`. + */ + newNode.next = current; + current.previous = newNode; + } + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new node after a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index after which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertAfter(data, index) { + + /* + * Create a new list node object and store the data in it. + * This node will be inserted into the existing list. + */ + const newNode = new DoublyLinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the list nodes similar to the `add()` method, but make + * sure to keep track of how many nodes have been visited and update + * the `previous` pointer in addition to `current`. When + * `i` is the same as `index`, it means we've found the location to + * insert the new data. + */ + while ((current !== null) && (i < index)) { + current = current.next; + i++; + } + + /* + * At this point, `current` is either the node to insert the new data + * before, or the last node in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data after. + */ + + // special case: `current` is the tail, so reset `this[tail]` + if (this[tail] === current) { + this[tail] = newNode; + } else { + + /* + * Otherwise, insert `newNode` before `current.next` by updating + * `current.next.previous` and `newNode.node`. + */ + current.next.previous = newNode; + newNode.next = current.next; + } + + /* + * Next, insert `newNode` after `current` by updating `newNode.previous` and + * `current.next`. + */ + newNode.previous = current; + current.next = newNode; + } + + /** + * Retrieves the data in the given position in the list. + * @param {int} index The zero-based index of the node whose data + * should be returned. + * @returns {*} The data in the "data" portion of the given node + * or undefined if the node doesn't exist. + */ + get(index) { + + // ensure `index` is a positive value + if (index > -1) { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the list nodes, but make sure to keep track of how many + * nodes have been visited and update the `previous` pointer in + * addition to `current`. When `i` is the same as `index`, it means + * we've found the location to insert the new data. + */ + while ((current !== null) && (i < index)) { + current = current.next; + i++; + } + + /* + * At this point, `current` might be null if we've gone past the + * end of the list. In that case, we return `undefined` to indicate + * that the node at `index` was not found. If `current` is not + * `null`, then it's safe to return `current.data`. + */ + return current !== null ? current.data : undefined; + } else { + return undefined; + } + } + + /** + * Retrieves the index of the data in the list. + * @param {*} data The data to search for. + * @returns {int} The index of the first instance of the data in the list + * or -1 if not found. + */ + indexOf(data) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `index` variable is used to track how deep into the list we've + * gone. This is important because this is the value that is returned + * from this method. + */ + let index = 0; + + /* + * This loop checks each node in the list to see if it matches `data`. + * If a match is found, it returns `index` immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search (when `current` is `null`). + */ + while (current !== null) { + if (current.data === data) { + return index; + } + + // traverse to the next node in the list + current = current.next; + + // keep track of where we are + index++; + } + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return -1 as the "not found" value. + */ + return -1; + } + + /** + * Returns the first item that matches a given function. + * @param {Function} matcher A function returning true when an item matches + * and false when an item doesn't match. + * @returns {*} The first item that returns true from the matcher, undefined + * if no items match. + */ + find(matcher) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * This loop checks each node in the list to see if it matches. + * If a match is found, it returns the data immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search (when `current` is `null`). + */ + while (current !== null) { + if (matcher(current.data)) { + return current.data; + } + + // traverse to the next node in the list + current = current.next; + } + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return `undefined` as the + * "not found" value. + */ + return undefined; + } + + /** + * Returns the index of the first item that matches a given function. + * @param {Function} matcher A function returning true when an item matches + * and false when an item doesn't match. + * @returns {int} The index of the first item that matches a given function + * or -1 if there are no matching items. + */ + findIndex(matcher) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `index` variable is used to track how deep into the list we've + * gone. This is important because this is the value that is returned + * from this method. + */ + let index = 0; + + /* + * This loop checks each node in the list to see if it matches. + * If a match is found, it returns the index immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search (when `current` is `null`). + */ + while (current !== null) { + if (matcher(current.data)) { + return index; + } + + // traverse to the next node in the list + current = current.next; + + // keep track of where we are + index++; + } + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return -1 as the + * "not found" value. + */ + return -1; + } + + /** + * Removes the node from the given location in the list. + * @param {int} index The zero-based index of the node to remove. + * @returns {*} The data in the given position in the list. + * @throws {RangeError} If index is out of range. + */ + remove(index) { + + // special cases: no nodes in the list or `index` is negative + if ((this[head] === null) || (index < 0)) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + // special case: removing the first node + if (index === 0) { + + // store the data from the current head + const data = this[head].data; + + // just replace the head with the next node in the list + this[head] = this[head].next; + + // special case: there was only one node, so also reset `this[tail]` + if (this[head] === null) { + this[tail] = null; + } else { + this[head].previous = null; + } + + // return the data at the previous head of the list + return data; + } + + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to remove. + */ + let i = 0; + + /* + * Traverse the list nodes similar to the `get()` method, but make + * sure to keep track of how many nodes have been visited. When + * `i` is the same as `index`, it means we've found the location to + * remove. + */ + while ((current !== null) && (i < index)) { + + // traverse to the next node + current = current.next; + + // increment the count + i++; + } + + /* + * If `current` isn't `null`, then that means we've found the node + * to remove. + */ + if (current !== null) { + + // skip over the node to remove + current.previous.next = current.next; + + /* + * If we are at the end of the list, then update `this[tail]`. + * + * If we are not at the end of the list, then update the backwards + * pointer for `current.next` to preserve reverse traversal. + */ + if (this[tail] === current) { + this[tail] = current.previous; + } else { + current.next.previous = current.previous; + } + + // return the value that was just removed from the list + return current.data; + } + + /* + * If we've made it this far, it means `index` is a value that + * doesn't exist in the list, so throw an error. + */ + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /** + * Removes all nodes from the list. + * @returns {void} + */ + clear() { + + // just reset both the head and tail pointer to null + this[head] = null; + this[tail] = null; + } + + /** + * Returns the number of nodes in the list. + * @returns {int} The number of nodes in the list. + */ + get size() { + + // special case: the list is empty + if (this[head] === null) { + return 0; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `count` variable is used to keep track of how many nodes have + * been visited inside the loop below. This is important because this + * is the value to return from this method. + */ + let count = 0; + + /* + * As long as `current` is not `null`, that means we're not yet at the + * end of the list, so adding 1 to `count` and traverse to the next node. + */ + while (current !== null) { + count++; + current = current.next; + } + + /* + * When `current` is `null`, the loop is exited at the value of `count` + * is the number of nodes that were counted in the loop. + */ + return count; + } + + /** + * 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(){ + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * As long as `current` is not `null`, there is a piece of data + * to yield. + */ + while (current !== null) { + yield current.data; + current = current.next; + } + } + + /** + * Create an iterator that returns each node in the list in reverse order. + * @returns {Iterator} An iterator on the list. + */ + *reverse(){ + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the tail and is overwritten inside + * of the loop below. + */ + let current = this[tail]; + + /* + * As long as `current` is not `null`, there is a piece of data + * to yield. + */ + while (current !== null) { + yield current.data; + current = current.previous; + } + } + + /** + * Converts the list into a string representation. + * @returns {String} A string representation of the list. + */ + toString(){ + return [...this].toString(); + } +} + +exports.DoublyLinkedList = DoublyLinkedList; \ No newline at end of file diff --git a/src/data-structures/doubly-linked-list/package.json b/src/data-structures/doubly-linked-list/package.json new file mode 100644 index 0000000..ab7ddb9 --- /dev/null +++ b/src/data-structures/doubly-linked-list/package.json @@ -0,0 +1,28 @@ +{ + "name": "@humanwhocodes/doubly-linked-list", + "version": "2.2.0", + "description": "A doubly linked list implementation in JavaScript", + "main": "doubly-linked-list.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/doubly-linked-list/doubly-linked-list.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "linked list", + "doubly linked list", + "data structure", + "iterable" + ], + "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/hash-map/README.md b/src/data-structures/hash-map/README.md new file mode 100644 index 0000000..e250514 --- /dev/null +++ b/src/data-structures/hash-map/README.md @@ -0,0 +1,71 @@ +# JavaScript Hash Map 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 hash map where all keys must be strings. 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 `entries()`, `keys()`, and `values()` generator methods. + +## Usage + +Use CommonJS to get access to the `HashMap` constructor: + +```js +const { HashMap } = require("@humanwhocodes/hash-map"); +``` + +Each instance of `HashMap` has the following properties and methods: + +```js +const map = new HashMap(); + +// add an item +map.set("foo", 1); + +// get the value of an item +let value = map.get("foo"); + +// get the number of items +let count = map.size; + +// does the key exist in the map? +let found = map.has("foo"); + +// remove a key +map.delete("foo"); + +// get all key-value pairs +let entries1 = [...map.entries()]; +let entries2 = [...map]; + +// get all keys +let keys = [...map.keys()]; + +// get all values +let values = [...map.values()]; + +// remove all items +map.clear(); +``` + +## 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. + +## Note on Usage + +This module is intended for educational purposes. For production purposes, you should use the native JavaScript `Map` class. + +## 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/hash-map/hash-map.js b/src/data-structures/hash-map/hash-map.js new file mode 100644 index 0000000..dbdd2f4 --- /dev/null +++ b/src/data-structures/hash-map/hash-map.js @@ -0,0 +1,473 @@ + +/** + * @fileoverview Hash Map implementation in JavaScript + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const { DoublyLinkedList } = require("@humanwhocodes/doubly-linked-list"); + +//----------------------------------------------------------------------------- +// Private +//----------------------------------------------------------------------------- + +const ARRAY_SIZE = 16; + +/** + * Adds up all of the code points in a string to create a numeric hash. + * @param {string} key The text key to hash. + * @returns {int} A numeric hash of text. + * @private + */ +function hashCodePoints(key) { + + let result = 0; + + // Iterate over each character (not each byte) in the string + for (const character of key) { + + /* + * The `codePointAt()` method has support for multi-byte characters, + * so that's better than `charCodeAt()` for this purpose. + * + * `character` is a single-character string, so we only want the code + * point at position 0. + */ + result += character.codePointAt(0); + } + + return result; +} + +/** + * Determines the correct array index for the given hash code. + * @param {int} hashCode The hash code to compute an index for. + * @returns {int} An index between 0 and `ARRAY_SIZE - 1`. + */ +function getArrayIndexFromHashCode(hashCode) { + return hashCode % ARRAY_SIZE; +} + +/** + * Creates an array to use as the basis for a hash map. + * @returns {void} + * @private + */ +function createArray() { + + /* + * FYI: It's not necessary to use an instance of `Array` for the + * purpose of creating a hash map. You could just as easily use a + * regular object. This implementation uses an array strictly because + * it's the most direct equivalent to how other languages implement + * hash maps. + */ + + // Object.seal() ensures we don't accidentally add more items + return Object.seal( + + /* + * Creates a new array with a predefined length of 16. This doesn't + * actually create each of the properties in the array, so the + * `fill()` method is used to do so. This is necessary because a + * sealed array cannot have new properties defined, and since the + * array starts out without any numeric properties defined, it + * would prevent us from even assigning a value to array[0]. + */ + new Array(ARRAY_SIZE).fill(undefined) + ); +} + +/** + * Checks that a key is a non-empty string. + * @param {string} key The key to validate. + * @returns {void} + * @throws {TypeError} When the key is either not a string or is an empty + * string. + */ +function assertNonEmptyString(key) { + if (typeof key !== "string" || key.length === 0) { + throw new TypeError("Key must be a non-empty string."); + } +} + +//----------------------------------------------------------------------------- +// HashMap 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 array = Symbol("array"); + +/** + * A binary heap implementation in JavaScript. + * @class HashMap + */ +class HashMap { + + /** + * Creates a new instance of HashMap + */ + constructor() { + + /** + * Array used to manage the hash map. The array is sealed to ensure + * no new items accidentally get added. + * @property array + * @type Array + * @private + */ + this[array] = createArray(); + } + + /** + * Adds a key-value pair into the hash map. + * @param {string} key The key to add. + * @param {*} value The value to add. + * @returns {void} + * @throws {TypeError} If the key isn't a non-empty string. + */ + set(key, value) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then create a new linked list in that + * location before continuing. + * + * Note: It's not necessary to use a linked list. Because JavaScript + * has an excellent `Array` class, you could also just use an array + * here. This implementation sticks with a linked list because that's + * the most common implementation in other languages. + */ + if (this[array][index] === undefined) { + this[array][index] = new DoublyLinkedList(); + } + + /* + * Check to see if the exact key is already present + * in the hash map. If so, then just update the value. + */ + const result = this[array][index].find((value) => { + return value.key === key; + }); + + // If the key doesn't already exist, then add it + if (result === undefined) { + + /* + * Add everything we know about this key-value pair, including the key, + * the value, and the hash code. We will need all of these later. + */ + this[array][index].add({ + key, + value + }); + } else { + + // The key already exists in the hash map, so just update the value. + result.value = value; + } + + } + + /** + * Retrieves a key-value pair from the hash map. + * @param {string} key The key whose value should be retrieved. + * @returns {*} The value associated with the key or `undefined` if the + * key doesn't exist in the hash map. + * @throws {TypeError} If the key isn't a non-empty string. + */ + get(key) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then the key doesn't exist. Return + * `undefined` to indicate the key doesn't exist. + */ + if (this[array][index] === undefined) { + return undefined; + } + + /* + * If we've made it to here, then there is a linked list in the + * array at this location, so try to find the key that matches. + */ + const result = this[array][index].find((value) => { + return value.key === key; + }); + + /* + * If an item with the given hash code and key was not found, then + * there is no value to return. Just return undefined. + */ + if (result === undefined) { + return undefined; + } + + /* + * If we've made it to here, it means that the hash code and key were + * found, so return the value. + */ + return result.value; + } + + /** + * Determines if a given key is present in the hash map. + * @param {string} key The key to check. + * @returns {boolean} True if the key exists in the hash map, false if not. + * @throws {TypeError} If the key isn't a non-empty string. + */ + has(key) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then the key doesn't exist. Return + * `false` to indicate the key doesn't exist. + */ + if (this[array][index] === undefined) { + return false; + } + + /* + * If we've made it to here, then there is a linked list in the + * array at this location, so try to find the key that matches. + */ + const resultIndex = this[array][index].findIndex((value) => { + return value.key === key; + }); + + /* + * Any value greater than -1 indicates that the key was found in the + * hash map and therefore this method should return `true`. + */ + return resultIndex > -1; + } + + /** + * Deletes the given key from the hash map. + * @param {string} key The key to delete. + * @returns {boolean} True if the key exists and was deleted, false if the + * key didn't exist. + * @throws {TypeError} If the key isn't a non-empty string. + */ + delete(key) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then the key doesn't exist. Return + * `false` to indicate the key doesn't exist. + */ + if (this[array][index] === undefined) { + return false; + } + + /* + * If we've made it to here, then there is a linked list in the + * array at this location, so try to find the key that matches. + */ + const resultIndex = this[array][index].findIndex((value) => { + return value.key === key; + }); + + /* + * Special case: If `resultIndex` is -1, meaning the value wasn't + * found, just return -1. + */ + if (resultIndex === -1) { + return -1; + } + + /* + * If we've made it to here, then `resultIndex` is greater than -1 + * and we need to remove the given key from the hash map. + */ + this[array][index].remove(resultIndex); + + /* + * Because we actually performed the removal, we need to return `true` + * to give feedback that this happened. + */ + return true; + + } + + /** + * Returns the number of key-value pairs in the hash map. + * @returns {int} The number of key-value pairs in the hash map. + */ + get size() { + + /* + * Because the `entries()` generator already implements the correct + * traversal algorithm, use that here instead of duplicating the + * algorithm. + * + * The first step is to create a new iterator by calling `entries()`. + */ + const iterator = this.entries(); + + /* + * The `count` variable stores the number of entries we see and is + * incremented inside of a loop later on. + */ + let count = 0; + + /* + * Get the first entry from the iterator. Each entry has two properties: + * `value`, containing the value from the hash map, and `done`, a + * a Boolean value that is `true` when there are no more entries. + */ + let entry = iterator.next(); + + // Continue the loop as long as there are more entries + while (!entry.done) { + + // Increment the count to reflect the last entry + count++; + + // Get the entry from the iterator and repeat the loop + entry = iterator.next(); + } + + /* + * Once the loop exits, the `count` variable contains the number of + * entries in the iterator so return that value. + */ + return count; + } + + /** + * Removes all values from the heap. + * @returns {void} + */ + clear() { + + /* + * The simplest way to clear all values is just to overwrite the array + * we started with. + */ + this[array] = createArray(); + } + + /** + * The default iterator for the class. + * @returns {Iterator} An iterator for the class. + */ + [Symbol.iterator]() { + return this.entries(); + } + + /** + * Create an iterator that returns each entry in the hash map. + * @returns {Iterator} An iterator on the hash map. + */ + *entries() { + + // For each item in the array + for (const list of this[array]) { + + // If there is no linked list then no need to go any further + if (list !== undefined) { + + // If there is a linked list then yield each key-value pair + for (const item of list) { + yield [item.key, item.value]; + } + } + } + } + + /** + * Create an iterator that returns each key in the hash map. + * @returns {Iterator} An iterator on the hash map keys. + */ + *keys() { + + /* + * Because this iterator just returns a subset of the information + * returned by the `entries()` iterator, we just use `entries()` + * and take the information we care about. + */ + for (const [key] of this.entries()) { + yield key; + } + } + + /** + * Create an iterator that returns each value in the hash map. + * @returns {Iterator} An iterator on the hash map value. + */ + *values() { + + /* + * Because this iterator just returns a subset of the information + * returned by the `entries()` iterator, we just use `entries()` + * and take the information we care about. + */ + for (const [,value] of this.entries()) { + yield value; + } + } + + /** + * Converts the heap into a string representation. + * @returns {String} A string representation of the heap. + */ + toString(){ + // TODO + return [...this[array]].toString(); + } +} + +exports.HashMap = HashMap; \ No newline at end of file diff --git a/src/data-structures/hash-map/package.json b/src/data-structures/hash-map/package.json new file mode 100644 index 0000000..557a5bd --- /dev/null +++ b/src/data-structures/hash-map/package.json @@ -0,0 +1,32 @@ +{ + "name": "@humanwhocodes/hash-map", + "version": "2.0.0", + "description": "A hash map implementation in JavaScript", + "main": "hash-map.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/hash-map/hash-map.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "hash", + "hash table", + "hash map", + "data structure", + "iterable" + ], + "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" + }, + "dependencies": { + "@humanwhocodes/doubly-linked-list": "^2.2.0" + } +} diff --git a/src/data-structures/linked-list/README.md b/src/data-structures/linked-list/README.md new file mode 100644 index 0000000..9b17e5d --- /dev/null +++ b/src/data-structures/linked-list/README.md @@ -0,0 +1,68 @@ +# JavaScript Linked List 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 linked list. 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. Returning `undefined` from `get()` when no such index exists. + +Read the [blog post](https://humanwhocodes.com/blog/2019/01/computer-science-in-javascript-linked-list/) about the design of this class. + +## Usage + +Use CommonJS to get access to the `LinkedList` constructor: + +```js +const { LinkedList } = require("@humanwhocodes/linked-list"); +``` + +Each instance of `LinkedList` has the following properties and methods: + +```js +const list = new LinkedList(); + +// add an item to the end +list.add("foo"); + +// insert an item +list.insertBefore("bar", 0); +list.insertAfter("baz", 1); + +// get the value at an index +let value = list.get(0); + +// get the number of items +let count = list.size; + +// get the index of a value +let index = list.indexOf("foo"); + +// convert to an array using iterators +let array1 = [...list.values()]; +let array2 = [...list]; + +// remove an item at the given index and return the data that was removed +let data = list.remove(0); + +// remove all items +list.clear(); +``` + +## 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/linked-list/linked-list.js b/src/data-structures/linked-list/linked-list.js new file mode 100644 index 0000000..2d15298 --- /dev/null +++ b/src/data-structures/linked-list/linked-list.js @@ -0,0 +1,536 @@ +/** + * @fileoverview Linked List implementation in JavaScript + */ + +/* + * 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 head = Symbol("head"); + +/** + * Represents a single node in a LinkedList. + * @class LinkedListNode + */ +class LinkedListNode { + + /** + * Creates a new instance of LinkedListNode. + * @param {*} data The data to store in the node. + */ + constructor(data) { + + /** + * The data that this node stores. + * @property data + * @type * + */ + this.data = data; + + /** + * A pointer to the next node in the LinkedList. + * @property next + * @type ?LinkedListNode + */ + this.next = null; + + } +} + +/** + * A linked list implementation in JavaScript. + * @class LinkedList + */ +class LinkedList { + + /** + * Creates a new instance of LinkedList + */ + constructor() { + + /** + * Pointer to first node in the list. + * @property head + * @type ?LinkedListNode + * @private + */ + this[head] = null; + } + + /** + * Appends some data to the end of the list. This method traverses + * the existing list and places the data at the end in a new node. + * @param {*} data The data to add to the list. + * @returns {void} + */ + add(data) { + + /* + * Create a new list node object and store the data in it. + * This node will be added to the end of the existing list. + */ + const newNode = new LinkedListNode(data); + + //special case: no nodes in the list yet + if (this[head] === null) { + + /* + * Because there are no nodes in the list, just set the + * `this[head]` pointer to the new node. + */ + this[head] = newNode; + } else { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * Follow each `next` pointer until the end. The last node in the + * list has `next` equal to `null`, so when we reach that node, + * we know we're at the end. + */ + while (current.next !== null) { + current = current.next; + } + + /* + * At this point, `current` is equal to the last node in the list. + * Setting its `current.next` equal to node means adding a new node + * at the end of the list. + */ + current.next = newNode; + } + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new node at a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index at which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertBefore(data, index) { + + /* + * Create a new list node object and store the data in it. + * This node will be inserted into the existing list. + */ + const newNode = new LinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * Special case: if `index` is `0`, then no traversal is needed + * and we need to update `this[head]` to point to `node`. First, + * set `node.next` to the current `this[head]` so the previous + * head of the list is now the second node in the list. Then it's + * safe to update `this[head]` to point to `node`. + */ + if (index === 0) { + newNode.next = this[head]; + this[head] = newNode; + } else { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + * + * The `previous` variable tracks one step behind `current`, which + * is necessary because we need to adjust the node at `index`-1's + * `next` pointer to point to the new node. + */ + let current = this[head], + previous = null; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the list nodes similar to the `add()` method, but make + * sure to keep track of how many nodes have been visited and update + * the `previous` pointer in addition to `current`. When + * `i` is the same as `index`, it means we've found the location to + * insert the new data. + */ + while ((current.next !== null) && (i < index)) { + previous = current; + current = current.next; + i++; + } + + /* + * At this point, `current` is either the node to insert the new data + * before, or the last node in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data before and `previous` is the node to insert + * new data after. So `previous.next` must point to `node` and + * `node.next` must point to `current`. + */ + previous.next = newNode; + newNode.next = current; + } + } + + /** + * Inserts some data into the middle of the list. This method traverses + * the existing list and places the data in a new node after a specific index. + * @param {*} data The data to add to the list. + * @param {int} index The zero-based index after which to insert the data. + * @returns {void} + * @throws {RangeError} If the index doesn't exist in the list. + */ + insertAfter(data, index) { + + /* + * Create a new list node object and store the data in it. + * This node will be inserted into the existing list. + */ + const newNode = new LinkedListNode(data); + + // special case: no nodes in the list yet + if (this[head] === null) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the list nodes similar to the `add()` method, but make + * sure to keep track of how many nodes have been visited and update + * the `previous` pointer in addition to `current`. When + * `i` is the same as `index`, it means we've found the location to + * insert the new data. + */ + while ((current !== null) && (i < index)) { + current = current.next; + i++; + } + + /* + * At this point, `current` is either the node to insert the new data + * before, or the last node in the list. The only way to tell is if + * `i` is still less than `index`, that means the index is out of range + * and an error should be thrown. + */ + if (i < index) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /* + * If code continues to execute here, it means `current` is the node + * to insert new data after. So `current.next` must point to + * `node` for the data to be in the correct spot, but before that, + * `node.next` must point to `current.next` to ensure the list + * remains intact. + */ + newNode.next = current.next; + current.next = newNode; + } + + /** + * Retrieves the data in the given position in the list. + * @param {int} index The zero-based index of the node whose data + * should be returned. + * @returns {*} The data in the "data" portion of the given node + * or undefined if the node doesn't exist. + */ + get(index) { + + // ensure `index` is a positive value + if (index > -1) { + + /* + * The `current` variable is used to track the node that is being + * used inside of the loop below. It starts out pointing to + * `this[head]` and is overwritten inside of the loop. + */ + let current = this[head]; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to insert into. + */ + let i = 0; + + /* + * Traverse the list nodes similar to the `add()` method, but make + * sure to keep track of how many nodes have been visited and update + * the `previous` pointer in addition to `current`. When + * `i` is the same as `index`, it means we've found the location to + * insert the new data. + */ + while ((current !== null) && (i < index)) { + current = current.next; + i++; + } + + /* + * At this point, `current` might be null if we've gone past the + * end of the list. In that case, we return `undefined` to indicate + * that the node at `index` was not found. If `current` is not + * `null`, then it's safe to return `current.data`. + */ + return current !== null ? current.data : undefined; + } else { + return undefined; + } + } + + /** + * Retrieves the index of the data in the list. + * @param {*} data The data to search for. + * @returns {int} The index of the first instance of the data in the list + * or -1 if not found. + */ + indexOf(data) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `index` variable is used to track how deep into the list we've + * gone. This is important because this is the value that is returned + * from this method. + */ + let index = 0; + + /* + * This loop checks each node in the list to see if it matches `data`. + * If a match is found, it returns `index` immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search (when `current` is `null`). + */ + while (current !== null) { + if (current.data === data) { + return index; + } + + // traverse to the next node in the list + current = current.next; + + // keep track of where we are + index++; + } + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return -1 as the "not found" value. + */ + return -1; + } + + /** + * Removes the node from the given location in the list. + * @param {int} index The zero-based index of the node to remove. + * @returns {*} The data in the given position in the list. + * @throws {RangeError} If index is out of range. + */ + remove(index) { + + // special cases: empty list or invalid `index` + if ((this[head] === null) || (index < 0)) { + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + // special case: removing the first node + if (index === 0) { + + // temporarily store the data from the node + const data = this[head].data; + + // just replace the head with the next node in the list + this[head] = this[head].next; + + // return the data at the previous head of the list + return data; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `previous` variable keeps track of the node just before + * `current` in the loop below. This is necessary because removing + * an node means updating the previous node's `next` pointer. + */ + let previous = null; + + /* + * The `i` variable is used to track how deep into the list we've + * gone. This is important because it's the only way to know when + * we've hit the `index` to remove. + */ + let i = 0; + + /* + * Traverse the list nodes similar to the `add()` method, but make + * sure to keep track of how many nodes have been visited and update + * the `previous` pointer in addition to `current`. When + * `i` is the same as `index`, it means we've found the location to + * remove. + */ + while ((current !== null) && (i < index)) { + + // save the value of current + previous = current; + + // traverse to the next node + current = current.next; + + // increment the count + i++; + } + + /* + * If `current` isn't `null`, then that means we've found the node + * to remove. + */ + if (current !== null) { + + // skip over the node to remove + previous.next = current.next; + + // return the value that was just removed from the list + return current.data; + } + + /* + * If we've made it this far, it means `index` is a value that + * doesn't exist in the list, so throw an error. + */ + throw new RangeError(`Index ${index} does not exist in the list.`); + } + + /** + * Removes all nodes from the list. + * @returns {void} + */ + clear() { + this[head] = null; + } + + /** + * Returns the number of nodes in the list. + * @returns {int} The number of nodes in the list. + */ + get size() { + + // special case: the list is empty + if (this[head] === null) { + return 0; + } + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `count` variable is used to keep track of how many nodes have + * been visited inside the loop below. This is important because this + * is the value to return from this method. + */ + let count = 0; + + /* + * As long as `current` is not `null`, that means we're not yet at the + * end of the list, so adding 1 to `count` and traverse to the next node. + */ + while (current !== null) { + count++; + current = current.next; + } + + /* + * When `current` is `null`, the loop is exited at the value of `count` + * is the number of nodes that were counted in the loop. + */ + return count; + } + + /** + * 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(){ + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * As long as `current` is not `null`, there is a piece of data + * to yield. + */ + while (current !== null) { + yield current.data; + current = current.next; + } + } + + /** + * Converts the list into a string representation. + * @returns {String} A string representation of the list. + */ + toString(){ + return [...this].toString(); + } +} + +exports.LinkedList = LinkedList; \ No newline at end of file diff --git a/src/data-structures/linked-list/package.json b/src/data-structures/linked-list/package.json new file mode 100644 index 0000000..663ba68 --- /dev/null +++ b/src/data-structures/linked-list/package.json @@ -0,0 +1,27 @@ +{ + "name": "@humanwhocodes/linked-list", + "version": "2.0.2", + "description": "A LinkedList implementation in JavaScript", + "main": "linked-list.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/linked-list/linked-list.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "linked list", + "data structure", + "iterable" + ], + "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/tests/algorithms/sorting/bubble-sort/bubble-sort.js b/tests/algorithms/sorting/bubble-sort/bubble-sort.js new file mode 100644 index 0000000..ea27bb1 --- /dev/null +++ b/tests/algorithms/sorting/bubble-sort/bubble-sort.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Bubble Sort tests + */ +/* global it, describe */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { bubbleSort } = require("../../../../src/algorithms/sorting/bubble-sort"); + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("bubbleSort()", () => { + + [ + [], + [1], + [2, 1], + [2, 1, 3], + [32,4,5,7,9,4,1], + [3,1,1,5,9,4,2, 5, 12, 45] + ].forEach(items => { + + it("should sort an array when the array has " + items.length + " item(s)", () => { + const result = [...items].sort((a, b) => a - b); + + bubbleSort(items); + assert.deepStrictEqual(items, result); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/data-structures/binary-heap/binary-heap.js b/tests/data-structures/binary-heap/binary-heap.js new file mode 100644 index 0000000..8d5fd91 --- /dev/null +++ b/tests/data-structures/binary-heap/binary-heap.js @@ -0,0 +1,338 @@ +/** + * @fileoverview Doubly Linked List tests + */ +/* global it, describe, beforeEach */ +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { BinaryHeap } = require("../../../src/data-structures/binary-heap/binary-heap"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the heap match the values of the array. + * @param {BinaryHeap} 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 assertHeapValues(heap, values) { + const heapValues = [...heap.values()]; + assert.deepStrictEqual(heapValues, values); + +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("BinaryHeap", () => { + + let heap; + + beforeEach(() => { + heap = new BinaryHeap(); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + heap.add(1); + assertHeapValues(heap, [1]); + }); + + it("should store two items when multiple items are added", () => { + heap.add(2); + heap.add(1); + + assertHeapValues(heap, [1,2]); + }); + + it("should store two items when multiple items are added", () => { + heap.add(2); + heap.add(3); + assertHeapValues(heap, [2,3]); + }); + + it("should store three items when multiple items are added", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assertHeapValues(heap, [1,3,2]); + }); + + it("should store four items when multiple items are added", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assertHeapValues(heap, [0,1,2,3]); + }); + }); + + describe("size", () => { + + it("should return the correct size when the heap has no items", () => { + assert.strictEqual(heap.size, 0); + }); + + it("should return the correct size when the heap has one item", () => { + heap.add(1); + assert.strictEqual(heap.size, 1); + }); + + it("should return the correct size when the heap has two items", () => { + heap.add(2); + heap.add(1); + assert.strictEqual(heap.size, 2); + }); + + it("should return the correct size when the heap has three items", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assert.strictEqual(heap.size, 3); + }); + + it("should return the correct size when the heap has four items", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assert.strictEqual(heap.size, 4); + }); + }); + + describe("isEmpty()", () => { + + it("should return true when the heap is empty", () => { + assert.isTrue(heap.isEmpty()); + }); + + it("should return false when the heap has one item", () => { + heap.add(1); + assert.isFalse(heap.isEmpty()); + }); + + it("should return false when the heap has two items", () => { + heap.add(2); + heap.add(1); + assert.isFalse(heap.isEmpty()); + }); + + }); + + describe("includes()", () => { + + it("should return false when the heap is empty", () => { + assert.isFalse(heap.includes(5)); + }); + + it("should return true when the item is found", () => { + heap.add(1); + assert.isTrue(heap.includes(1)); + }); + + it("should return false when the item is not found", () => { + heap.add(1); + assert.isFalse(heap.includes(10)); + }); + + it("should return true when the heap has two items and the item is found", () => { + heap.add(2); + heap.add(1); + assert.isTrue(heap.includes(2)); + }); + + }); + + describe("peek()", () => { + + it("should return the only item from a one-item heap", () => { + heap.add(1); + assert.strictEqual(heap.peek(), 1); + assert.strictEqual(heap.size, 1); + }); + + it("should return the lowest value from a two-item heap", () => { + heap.add(2); + heap.add(1); + assert.strictEqual(heap.peek(), 1); + assert.strictEqual(heap.size, 2); + }); + + it("should return the lowest value from a three-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assert.strictEqual(heap.peek(), 1); + assert.strictEqual(heap.size, 3); + }); + + it("should return the lowest value from a four-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assert.strictEqual(heap.peek(), 0); + assert.strictEqual(heap.size, 4); + }); + }); + + describe("poll()", () => { + + it("should return the only item from a one-item heap", () => { + heap.add(1); + assert.strictEqual(heap.poll(), 1); + assert.strictEqual(heap.size, 0); + assertHeapValues(heap, []); + }); + + it("should return the lowest value from a two-item heap", () => { + heap.add(2); + heap.add(1); + assert.strictEqual(heap.poll(), 1); + assert.strictEqual(heap.size, 1); + assertHeapValues(heap, [2]); + }); + + it("should return the lowest value from a three-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assert.strictEqual(heap.poll(), 1); + assert.strictEqual(heap.size, 2); + assertHeapValues(heap, [2,3]); + }); + + it("should return the lowest value from a four-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assert.strictEqual(heap.poll(), 0); + assert.strictEqual(heap.size, 3); + assertHeapValues(heap, [1,3,2]); + }); + }); + + describe("Custom Comparator", () => { + + beforeEach(() => { + heap = new BinaryHeap((a, b) => b - a); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + heap.add(1); + assertHeapValues(heap, [1]); + }); + + it("should store two items when multiple items are added", () => { + heap.add(2); + heap.add(1); + + assertHeapValues(heap, [2, 1]); + }); + + it("should store two items when multiple items are added", () => { + heap.add(2); + heap.add(3); + assertHeapValues(heap, [3, 2]); + }); + + it("should store three items when multiple items are added", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assertHeapValues(heap, [3, 2, 1]); + }); + + it("should store four items when multiple items are added", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assertHeapValues(heap, [3, 2, 1, 0]); + }); + }); + + describe("peek()", () => { + + it("should return the only item from a one-item heap", () => { + heap.add(1); + assert.strictEqual(heap.peek(), 1); + assert.strictEqual(heap.size, 1); + }); + + it("should return the highest value from a two-item heap", () => { + heap.add(2); + heap.add(1); + assert.strictEqual(heap.peek(), 2); + assert.strictEqual(heap.size, 2); + }); + + it("should return the highest value from a three-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assert.strictEqual(heap.peek(), 3); + assert.strictEqual(heap.size, 3); + }); + + it("should return the highest value from a four-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assert.strictEqual(heap.peek(), 3); + assert.strictEqual(heap.size, 4); + }); + }); + + describe("poll()", () => { + + it("should return the only item from a one-item heap", () => { + heap.add(1); + assert.strictEqual(heap.poll(), 1); + assert.strictEqual(heap.size, 0); + assertHeapValues(heap, []); + }); + + it("should return the highest value from a two-item heap", () => { + heap.add(2); + heap.add(1); + assert.strictEqual(heap.poll(), 2); + assert.strictEqual(heap.size, 1); + assertHeapValues(heap, [1]); + }); + + it("should return the highest value from a three-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + assert.strictEqual(heap.poll(), 3); + assert.strictEqual(heap.size, 2); + assertHeapValues(heap, [2, 1]); + }); + + it("should return the highest value from a four-item heap", () => { + heap.add(2); + heap.add(3); + heap.add(1); + heap.add(0); + assert.strictEqual(heap.poll(), 3); + assert.strictEqual(heap.size, 3); + assertHeapValues(heap, [2, 0, 1]); + }); + }); + }); + +}); diff --git a/tests/data-structures/binary-search-tree/binary-search-tree.js b/tests/data-structures/binary-search-tree/binary-search-tree.js new file mode 100644 index 0000000..b1c6c5f --- /dev/null +++ b/tests/data-structures/binary-search-tree/binary-search-tree.js @@ -0,0 +1,318 @@ +/** + * @fileoverview BinarySearchTree tests + */ +/* global it, describe, beforeEach */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const BinarySearchTree = require("../../../src/data-structures/binary-search-tree").BinarySearchTree; + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the tree match the values of the array. + * @param {BinarySearchTree} tree The tree to check + * @param {Array} values An array of values that should match. + * @throws {AssertionError} If the values in the tree don't match the + * values in the array. + */ +function assertTreeValues(tree, values) { + const treeValues = [...tree.values()]; + assert.deepStrictEqual(treeValues, values); +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("BinarySearchTree", () => { + + let tree; + + beforeEach(() => { + tree = new BinarySearchTree(); + }); + + describe("add()", () => { + + it("should store an node when one node is added", () => { + tree.add(1); + assertTreeValues(tree, [1]); + }); + + it("should store two nodes when two nodes are added", () => { + tree.add(1); + tree.add(2); + assertTreeValues(tree, [1, 2]); + }); + + it("should store multiple nodes when multiple nodes are added", () => { + tree.add(2); + tree.add(1); + tree.add(3); + assertTreeValues(tree, [1, 2, 3]); + }); + + it("should not store duplicate nodes when multiple nodes are added", () => { + tree.add(2); + tree.add(1); + tree.add(3); + tree.add(1); + assertTreeValues(tree, [1, 2, 3]); + }); + }); + + describe("has()", () => { + + it("should return true when there is one node in the tree and the value exists", () => { + tree.add(1); + assert.isTrue(tree.has(1)); + }); + + it("should return false when there is one node in the tree and the value doesn't exist", () => { + tree.add(1); + assert.isFalse(tree.has(2)); + }); + + it("should return false when there is an empty tree and the value doesn't exist", () => { + assert.isFalse(tree.has(2)); + }); + + it("should return true when there are two nodes in the tree and the value exists", () => { + tree.add(2); + tree.add(1); + assert.isTrue(tree.has(1)); + }); + + it("should return false when there is one node in the tree and the value doesn't exist", () => { + tree.add(1); + tree.add(2); + assert.isFalse(tree.has(3)); + }); + + it("should return true when there are three nodes in the tree and the value exists", () => { + tree.add(2); + tree.add(1); + tree.add(3); + assert.isTrue(tree.has(1)); + }); + + it("should return false when there is one node in the tree and the value doesn't exist", () => { + tree.add(2); + tree.add(1); + tree.add(3); + assert.isFalse(tree.has(4)); + }); + }); + + describe("delete()", () => { + + it("should delete node when there is only one node in the tree.", () => { + tree.add(10); + tree.delete(10); + assertTreeValues(tree, []); + }); + + it("should delete node when tree has two nodes.", () => { + tree.add(10); + tree.add(5); + + tree.delete(10); + assertTreeValues(tree, [5]); + }); + + it("should delete node when tree has left subtree nodes only.", () => { + tree.add(10); + tree.add(5); + tree.add(2); + + tree.delete(5); + assertTreeValues(tree, [2, 10]); + }); + + it("should delete node when tree has left subtree nodes only and root is removed.", () => { + tree.add(10); + tree.add(5); + tree.add(2); + + tree.delete(10); + assertTreeValues(tree, [2, 5]); + }); + + it("should delete node when tree has right subtree nodes only and root is removed.", () => { + tree.add(10); + tree.add(15); + tree.add(20); + + tree.delete(15); + assertTreeValues(tree, [10, 20]); + }); + + it("should delete node when tree has right subtree nodes only and root is removed.", () => { + tree.add(10); + tree.add(15); + tree.add(20); + + tree.delete(15); + assertTreeValues(tree, [10, 20]); + }); + + it("should remove node when there are two children in shallow tree", () => { + tree.add(7); + tree.add(11); + tree.add(8); + tree.add(13); + + tree.delete(11); + assertTreeValues(tree, [7, 8, 13]); + }); + + it("should remove node when there are three children in shallow tree", () => { + tree.add(7); + tree.add(11); + tree.add(8); + tree.add(13); + tree.add(9); + + tree.delete(11); + assertTreeValues(tree, [7, 8, 9, 13]); + }); + + it("should remove node when there are two children in deep tree", () => { + + tree.add(8); + tree.add(3); + tree.add(1); + tree.add(10); + tree.add(6); + tree.add(4); + tree.add(7); + tree.add(14); + tree.add(13); + + tree.delete(3); + assertTreeValues(tree, [1, 4, 6, 7, 8, 10, 13, 14]); + }); + + it("should remove an node when there is only one node", () => { + tree.add(1); + assertTreeValues(tree, [1]); + + tree.delete(1); + assertTreeValues(tree, []); + }); + + it("should remove an node when multiple nodes are in the tree and the middle node is removed", () => { + tree.add(1); + tree.add(2); + tree.add(3); + assertTreeValues(tree, [1, 2, 3]); + + // remove middle node + tree.delete(2); + assertTreeValues(tree, [1, 3]); + }); + + }); + + describe("clear()", () => { + + it("should not throw an error when the tree has no nodes", () => { + assertTreeValues(tree, []); + + tree.clear(); + assertTreeValues(tree, []); + }); + + it("should remove all nodes when the tree has one node", () => { + tree.add(1); + assertTreeValues(tree, [1]); + + tree.clear(); + assertTreeValues(tree, []); + }); + + it("should remove all nodes when the tree has multiple nodes", () => { + tree.add(1); + tree.add(2); + assertTreeValues(tree, [1, 2]); + + tree.clear(); + assertTreeValues(tree, []); + }); + + }); + + + describe("size", () => { + + it("should return 0 when the tree is empty", () => { + assert.strictEqual(tree.size, 0); + }); + + it("should return 1 when the tree has one node", () => { + tree.add(1); + assert.strictEqual(tree.size, 1); + }); + + it("should return 2 when the tree has two nodes", () => { + tree.add(1); + tree.add(2); + assert.strictEqual(tree.size, 2); + }); + + it("should return 3 when the tree has three nodes", () => { + tree.add(2); + tree.add(1); + tree.add(4); + assert.strictEqual(tree.size, 3); + }); + + it("should return 5 when the tree has five nodes", () => { + tree.add(2); + tree.add(1); + tree.add(4); + tree.add(9); + tree.add(12); + assert.strictEqual(tree.size, 5); + }); + + }); + + + ["values", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no nodes", () => { + assert.deepStrictEqual([...tree[method]()], []); + }); + + it("should iterate over tree when there is one node", () => { + tree.add(1); + + assert.deepStrictEqual([...tree[method]()], [1]); + }); + + it("should iterate over tree when there are multiple nodes", () => { + tree.add(1); + tree.add(2); + tree.add(3); + + assert.deepStrictEqual([...tree[method]()], [1, 2, 3]); + }); + + }); + + }); + + + +}); diff --git a/tests/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js b/tests/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js new file mode 100644 index 0000000..55fcd83 --- /dev/null +++ b/tests/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js @@ -0,0 +1,412 @@ +/** + * @fileoverview Circular Doubly Linked List tests + */ +/* global it, describe, beforeEach */ +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { CircularDoublyLinkedList } = require("../../../src/data-structures/circular-doubly-linked-list/circular-doubly-linked-list"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the list match the values of the array. + * @param {CircularDoublyLinkedList} list 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 assertListValues(list, values) { + const listValues = [...list.values()]; + assert.deepStrictEqual(listValues, values); + + const reverseValues = [...list.reverse()]; + assert.deepStrictEqual(reverseValues, values.reverse()); + +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("CircularDoublyLinkedList", () => { + + let list; + + beforeEach(() => { + list = new CircularDoublyLinkedList(); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + list.add(1); + assertListValues(list, [1]); + }); + + it("should store multiple items when multiple items are added", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + }); + }); + + describe("insertBefore()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(0, 0); + assertListValues(list, [0, 1, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(1.5, 1); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(2.5, 2); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertBefore(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertBefore(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("insertAfter()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(1.5, 0); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(2.5, 1); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(3.5, 2); + assertListValues(list, [1, 2, 3, 3.5]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertAfter(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertAfter(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("get()", () => { + + it("should return the first item when get(0) is called", () => { + list.add(1); + assert.strictEqual(list.get(0), 1); + }); + + it("should return the correct value when get() is called multiple times", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(0), 1); + assert.strictEqual(list.get(1), 2); + }); + + it("should return undefined when get() is called with -1", () => { + assert.strictEqual(list.get(-1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in an empty list", () => { + assert.strictEqual(list.get(1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in a non-empty list", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(5), undefined); + }); + + }); + + describe("remove()", () => { + + it("should remove an item when there is only one item", () => { + list.add(1); + assertListValues(list, [1]); + + assert.strictEqual(list.remove(0), 1); + assertListValues(list, []); + }); + + it("should remove an item when multiple items are in the list and the middle item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove middle item + assert.strictEqual(list.remove(1), 2); + assertListValues(list, [1, 3]); + }); + + it("should remove an item when multiple items are in the list and the last item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove last item + assert.strictEqual(list.remove(2), 3); + assertListValues(list, [1, 2]); + }); + + it("should remove an item when multiple items are in the list and the first item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove first item + assert.strictEqual(list.remove(0), 1); + assertListValues(list, [2, 3]); + }); + + it("should throw an error when multiple items are in the list and an out-of-bounds index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(5); + }, "Index 5 does not exist in the list."); + }); + + it("should throw an error when multiple items are in the list and a negative index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(-1); + }, "Index -1 does not exist in the list."); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.remove(0); + }, "Index 0 does not exist in the list."); + }); + }); + + describe("clear()", () => { + + it("should not throw an error when the list has no items", () => { + assertListValues(list, []); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has one item", () => { + list.add(1); + assertListValues(list, [1]); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has multiple items", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + + list.clear(); + assertListValues(list, []); + }); + + }); + + + describe("size", () => { + + it("should return 0 when the list is empty", () => { + assert.strictEqual(list.size, 0); + }); + + it("should return 1 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.size, 1); + }); + + it("should return 2 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.size, 2); + }); + + }); + + describe("indexOf()", () => { + + it("should return -1 when the list is empty", () => { + assert.strictEqual(list.indexOf(1), -1); + }); + + it("should return 0 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.indexOf(1), 0); + }); + + it("should return 1 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(2), 1); + }); + + it("should return -1 when the list doesn't contain the value", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(3), -1); + }); + + }); + + ["values", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list[method]()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + assert.deepStrictEqual([...list[method]()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + assert.deepStrictEqual([...list[method]()], [1, 2, 3]); + }); + + }); + + }); + + describe("reverse()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list.reverse()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + assert.deepStrictEqual([...list.reverse()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + assert.deepStrictEqual([...list.reverse()], [3, 2, 1]); + }); + + }); + + describe("circularValues()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list.circularValues()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + const [first, second] = list.circularValues(); + assert.deepStrictEqual([first, second], [1, 1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + const [first, second, third, fourth, fifth] = list.circularValues(); + assert.deepStrictEqual([first, second, third, fourth, fifth], [1, 2, 3, 1, 2]); + }); + + }); + + +}); diff --git a/tests/data-structures/circular-linked-list/circular-linked-list.js b/tests/data-structures/circular-linked-list/circular-linked-list.js new file mode 100644 index 0000000..f107058 --- /dev/null +++ b/tests/data-structures/circular-linked-list/circular-linked-list.js @@ -0,0 +1,393 @@ +/** + * @fileoverview Linked List tests + */ +/* global it, describe, beforeEach */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { CircularLinkedList } = require("../../../src/data-structures/circular-linked-list"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the list match the values of the array. + * @param {LinkedList} list 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 assertListValues(list, values) { + const listValues = [...list.values()]; + assert.deepStrictEqual(listValues, values); +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("CircularLinkedList", () => { + + let list; + + beforeEach(() => { + list = new CircularLinkedList(); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + list.add(1); + assertListValues(list, [1]); + }); + + it("should store two items when two items are added", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + }); + + it("should store three items when three items are added", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + }); + }); + + describe("insertBefore()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(0, 0); + assertListValues(list, [0, 1, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(1.5, 1); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(2.5, 2); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertBefore(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertBefore(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("insertAfter()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(1.5, 0); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(2.5, 1); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(3.5, 2); + assertListValues(list, [1, 2, 3, 3.5]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertAfter(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertAfter(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("get()", () => { + + it("should return the first item when get(0) is called", () => { + list.add(1); + assert.strictEqual(list.get(0), 1); + }); + + it("should return the correct value when get() is called multiple times", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(0), 1); + assert.strictEqual(list.get(1), 2); + }); + + it("should return undefined when get() is called with -1", () => { + assert.strictEqual(list.get(-1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in an empty list", () => { + assert.strictEqual(list.get(1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in a non-empty list", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(5), undefined); + }); + + }); + + describe("remove()", () => { + + it("should remove an item when there is only one item", () => { + list.add(1); + assertListValues(list, [1]); + + assert.strictEqual(list.remove(0), 1); + assertListValues(list, []); + }); + + it("should remove an item when multiple items are in the list and the middle item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove middle item + assert.strictEqual(list.remove(1), 2); + assertListValues(list, [1, 3]); + }); + + it("should remove an item when multiple items are in the list and the last item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove last item + assert.strictEqual(list.remove(2), 3); + assertListValues(list, [1, 2]); + }); + + it("should remove an item when multiple items are in the list and the first item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove first item + assert.strictEqual(list.remove(0), 1); + assertListValues(list, [2, 3]); + }); + + it("should throw an error when multiple items are in the list and an out-of-bounds index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(5); + }, "Index 5 does not exist in the list."); + }); + + it("should throw an error when multiple items are in the list and a negative index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(-1); + }, "Index -1 does not exist in the list."); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.remove(0); + }, "Index 0 does not exist in the list."); + }); + }); + + describe("clear()", () => { + + it("should not throw an error when the list has no items", () => { + assertListValues(list, []); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has one item", () => { + list.add(1); + assertListValues(list, [1]); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has multiple items", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + + list.clear(); + assertListValues(list, []); + }); + + }); + + describe("size", () => { + + it("should return 0 when the list is empty", () => { + assert.strictEqual(list.size, 0); + }); + + it("should return 1 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.size, 1); + }); + + it("should return 2 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.size, 2); + }); + + }); + + describe("indexOf()", () => { + + it("should return -1 when the list is empty", () => { + assert.strictEqual(list.indexOf(1), -1); + }); + + it("should return 0 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.indexOf(1), 0); + }); + + it("should return 1 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(2), 1); + }); + + it("should return -1 when the list doesn't contain the value", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(3), -1); + }); + + }); + + ["values", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list[method]()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + assert.deepStrictEqual([...list[method]()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + assert.deepStrictEqual([...list[method]()], [1, 2, 3]); + }); + + }); + + }); + + describe("circularValues()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list.circularValues()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + const [first, second] = list.circularValues(); + assert.deepStrictEqual([first, second], [1, 1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + const [first, second, third, fourth, fifth] = list.circularValues(); + assert.deepStrictEqual([first, second, third, fourth, fifth], [1, 2, 3, 1, 2]); + }); + + }); + + +}); diff --git a/tests/data-structures/doubly-linked-list/doubly-linked-list.js b/tests/data-structures/doubly-linked-list/doubly-linked-list.js new file mode 100644 index 0000000..41ff746 --- /dev/null +++ b/tests/data-structures/doubly-linked-list/doubly-linked-list.js @@ -0,0 +1,437 @@ +/** + * @fileoverview Doubly Linked List tests + */ +/* global it, describe, beforeEach */ +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { DoublyLinkedList } = require("../../../src/data-structures/doubly-linked-list/doubly-linked-list"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the list match the values of the array. + * @param {DoublyLinkedList} list 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 assertListValues(list, values) { + const listValues = [...list.values()]; + assert.deepStrictEqual(listValues, values); + + const reverseValues = [...list.reverse()]; + assert.deepStrictEqual(reverseValues, values.reverse()); + +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("DoublyLinkedList", () => { + + let list; + + beforeEach(() => { + list = new DoublyLinkedList(); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + list.add(1); + assertListValues(list, [1]); + }); + + it("should store multiple items when multiple items are added", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + }); + }); + + describe("insertBefore()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(0, 0); + assertListValues(list, [0, 1, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(1.5, 1); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(2.5, 2); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertBefore(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertBefore(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("insertAfter()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(1.5, 0); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(2.5, 1); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(3.5, 2); + assertListValues(list, [1, 2, 3, 3.5]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertAfter(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertAfter(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("get()", () => { + + it("should return the first item when get(0) is called", () => { + list.add(1); + assert.strictEqual(list.get(0), 1); + }); + + it("should return the correct value when get() is called multiple times", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(0), 1); + assert.strictEqual(list.get(1), 2); + }); + + it("should return undefined when get() is called with -1", () => { + assert.strictEqual(list.get(-1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in an empty list", () => { + assert.strictEqual(list.get(1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in a non-empty list", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(5), undefined); + }); + + }); + + describe("remove()", () => { + + it("should remove an item when there is only one item", () => { + list.add(1); + assertListValues(list, [1]); + + assert.strictEqual(list.remove(0), 1); + assertListValues(list, []); + }); + + it("should remove an item when multiple items are in the list and the middle item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove middle item + assert.strictEqual(list.remove(1), 2); + assertListValues(list, [1, 3]); + }); + + it("should remove an item when multiple items are in the list and the last item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove last item + assert.strictEqual(list.remove(2), 3); + assertListValues(list, [1, 2]); + }); + + it("should remove an item when multiple items are in the list and the first item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove first item + assert.strictEqual(list.remove(0), 1); + assertListValues(list, [2, 3]); + }); + + it("should throw an error when multiple items are in the list and an out-of-bounds index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(5); + }, "Index 5 does not exist in the list."); + }); + + it("should throw an error when multiple items are in the list and a negative index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(-1); + }, "Index -1 does not exist in the list."); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.remove(0); + }, "Index 0 does not exist in the list."); + }); + }); + + describe("clear()", () => { + + it("should not throw an error when the list has no items", () => { + assertListValues(list, []); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has one item", () => { + list.add(1); + assertListValues(list, [1]); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has multiple items", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + + list.clear(); + assertListValues(list, []); + }); + + }); + + + describe("size", () => { + + it("should return 0 when the list is empty", () => { + assert.strictEqual(list.size, 0); + }); + + it("should return 1 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.size, 1); + }); + + it("should return 2 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.size, 2); + }); + + }); + + describe("indexOf()", () => { + + it("should return -1 when the list is empty", () => { + assert.strictEqual(list.indexOf(1), -1); + }); + + it("should return 0 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.indexOf(1), 0); + }); + + it("should return 1 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(2), 1); + }); + + it("should return -1 when the list doesn't contain the value", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(3), -1); + }); + + }); + + describe("find()", () => { + + it("should return undefined when the list is empty", () => { + assert.isUndefined(list.find(() => true)); + }); + + it("should return 1 when the matching value is found", () => { + list.add(1); + assert.strictEqual(list.find(value => (value > 0)), 1); + }); + + it("should return 2 when the matching value is second", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.find(value => (value > 1)), 2); + }); + + it("should return undefined when the list doesn't contain a match", () => { + list.add(1); + list.add(2); + assert.isUndefined(list.find((value) => value > 2)); + }); + + }); + + describe("findIndex()", () => { + + it("should return -1 when the list is empty", () => { + assert.strictEqual(list.findIndex(() => true), -1); + }); + + it("should return 0 when the matching value is found in the first item", () => { + list.add(1); + assert.strictEqual(list.findIndex(value => (value > 0)), 0); + }); + + it("should return 1 when the matching value is second", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.findIndex(value => (value > 1)), 1); + }); + + it("should return -1 when the list doesn't contain a match", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.findIndex((value) => value > 2), -1); + }); + + }); + + ["values", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list[method]()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + assert.deepStrictEqual([...list[method]()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + assert.deepStrictEqual([...list[method]()], [1, 2, 3]); + }); + + }); + + }); + + describe("reverse()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list.reverse()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + assert.deepStrictEqual([...list.reverse()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + assert.deepStrictEqual([...list.reverse()], [3, 2, 1]); + }); + + }); + +}); diff --git a/tests/data-structures/hash-map/hash-map.js b/tests/data-structures/hash-map/hash-map.js new file mode 100644 index 0000000..8645295 --- /dev/null +++ b/tests/data-structures/hash-map/hash-map.js @@ -0,0 +1,261 @@ +/** + * @fileoverview Hash Map tests + */ +/* global it, describe, beforeEach */ +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { HashMap } = require("../../../src/data-structures/hash-map/hash-map"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the hash map match the values of the array. + * @param {HashMap} map 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 assertHashMapValues(map, values) { + const hashMapValues = [...map]; + assert.deepStrictEqual(hashMapValues, values); + +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("HashMap", () => { + + let map; + + beforeEach(() => { + map = new HashMap(); + }); + + describe("set()", () => { + + it("should store a key in the hash map", () => { + map.set("foo", 1); + assertHashMapValues(map, [["foo", 1]]); + }); + + it("should store two keys in the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + assertHashMapValues(map, [["foo", 1], ["bar", 2]]); + }); + + it("should store two keys with the same hash codes in the hash map", () => { + map.set("foo", 1); + map.set("oof", 2); + assertHashMapValues(map, [["foo", 1], ["oof", 2]]); + }); + + it("should overwrite a key in the hash map when called twice with the same key", () => { + map.set("foo", 1); + map.set("bar", 2); + map.set("foo", 3); + assertHashMapValues(map, [["foo", 3], ["bar", 2]]); + }); + + }); + + describe("get()", () => { + + it("should retrieve a key from the hash map", () => { + map.set("foo", 1); + assert.strictEqual(map.get("foo"), 1); + }); + + it("should retrieve two keys from the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.strictEqual(map.get("foo"), 1); + assert.strictEqual(map.get("bar"), 2); + }); + + it("should retrieve two keys from the hash map when one key is overwritten", () => { + map.set("foo", 1); + map.set("bar", 2); + map.set("foo", 3); + assert.strictEqual(map.get("foo"), 3); + assert.strictEqual(map.get("bar"), 2); + }); + + }); + + describe("has()", () => { + + it("should return true when a key exists in the hash map", () => { + map.set("foo", 1); + assert.isTrue(map.has("foo")); + }); + + it("should false when a key doesn't exist in the hash map", () => { + map.set("foo", 1); + assert.isFalse(map.has("foox")); + }); + + it("should return true when multiple keys exist in the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.isTrue(map.has("foo")); + assert.isTrue(map.has("bar")); + }); + + it("should return true when one key is overwritten", () => { + map.set("foo", 1); + map.set("bar", 2); + map.set("foo", 3); + assert.isTrue(map.has("foo")); + assert.isTrue(map.has("bar")); + }); + + }); + + describe("delete()", () => { + + it("should delete a key in the hash map", () => { + map.set("foo", 1); + map.delete("foo"); + assertHashMapValues(map, []); + }); + + it("should return true when a key is deleted from the hash map", () => { + map.set("foo", 1); + assert.isTrue(map.delete("foo")); + }); + + it("should return false when a key is not deleted from the hash map", () => { + map.set("foo", 1); + assert.isFalse(map.delete("f")); + }); + + it("should return false when the hash map is empty", () => { + assert.isFalse(map.delete("f")); + }); + + it("should delete two keys in the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + map.delete("foo"); + map.delete("bar"); + assertHashMapValues(map, []); + }); + + it("should delete one key when the hash map has two keys", () => { + map.set("foo", 1); + map.set("oof", 2); + map.delete("foo"); + assertHashMapValues(map, [["oof", 2]]); + }); + + }); + + describe("size", () => { + + it("should return the correct size when the hash map has no items", () => { + assert.strictEqual(map.size, 0); + }); + + it("should return the correct size when the hash map has one item", () => { + map.set("foo", 1); + assert.strictEqual(map.size, 1); + }); + + it("should return the correct size when the hash map has two items", () => { + map.set("bar", 2); + map.set("foo", 1); + assert.strictEqual(map.size, 2); + }); + + it("should return the correct size when the hash map has three items", () => { + map.set("bar", 2); + map.set("baz", 3); + map.set("foo", 1); + assert.strictEqual(map.size, 3); + }); + + it("should return the correct size when the hash map has four items", () => { + map.set("bar", 2); + map.set("baz", 3); + map.set("foo", 1); + map.set("bazoo", 0); + assert.strictEqual(map.size, 4); + }); + }); + + ["entries", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...map[method]()], []); + }); + + it("should iterate over list when there is one item", () => { + map.set("foo", 1); + + assert.deepStrictEqual([...map[method]()], [["foo", 1]]); + }); + + it("should iterate over list when there are multiple items", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.deepStrictEqual([...map[method]()], [["foo", 1], ["bar", 2]]); + }); + + }); + + }); + + describe("keys()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...map.keys()], []); + }); + + it("should iterate over list when there is one item", () => { + map.set("foo", 1); + + assert.deepStrictEqual([...map.keys()], ["foo"]); + }); + + it("should iterate over list when there are multiple items", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.deepStrictEqual([...map.keys()], ["foo", "bar"]); + }); + + }); + + describe("values()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...map.values()], []); + }); + + it("should iterate over list when there is one item", () => { + map.set("foo", 1); + + assert.deepStrictEqual([...map.values()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.deepStrictEqual([...map.values()], [1, 2]); + }); + + }); + + +}); diff --git a/tests/data-structures/linked-list/linked-list.js b/tests/data-structures/linked-list/linked-list.js new file mode 100644 index 0000000..c07a487 --- /dev/null +++ b/tests/data-structures/linked-list/linked-list.js @@ -0,0 +1,364 @@ +/** + * @fileoverview Linked List tests + */ +/* global it, describe, beforeEach */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const LinkedList = require("../../../src/data-structures/linked-list").LinkedList; + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the list match the values of the array. + * @param {LinkedList} list 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 assertListValues(list, values) { + const listValues = [...list.values()]; + assert.deepStrictEqual(listValues, values); +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("LinkedList", () => { + + let list; + + beforeEach(() => { + list = new LinkedList(); + }); + + describe("add()", () => { + + it("should store an item when one item is added", () => { + list.add(1); + assertListValues(list, [1]); + }); + + it("should store multiple items when multiple items are added", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + }); + }); + + describe("insertBefore()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(0, 0); + assertListValues(list, [0, 1, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(1.5, 1); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertBefore(2.5, 2); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertBefore(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertBefore(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("insertAfter()", () => { + + it("should store an item when one item is inserted at the start", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(1.5, 0); + assertListValues(list, [1, 1.5, 2, 3]); + }); + + it("should store an item when one item is inserted in the middle", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(2.5, 1); + assertListValues(list, [1, 2, 2.5, 3]); + }); + + it("should store an item when one item is inserted at the end", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + list.insertAfter(3.5, 2); + assertListValues(list, [1, 2, 3, 3.5]); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.insertAfter(1, 0); + }, "Index 0 does not exist in the list."); + }); + + it("should throw an error when the index is out of range", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + assert.throws(() => { + list.insertAfter(1, 5); + }, "Index 5 does not exist in the list."); + }); + + }); + + describe("get()", () => { + + it("should return the first item when get(0) is called", () => { + list.add(1); + assert.strictEqual(list.get(0), 1); + }); + + it("should return the correct value when get() is called multiple times", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(0), 1); + assert.strictEqual(list.get(1), 2); + }); + + it("should return undefined when get() is called with -1", () => { + assert.strictEqual(list.get(-1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in an empty list", () => { + assert.strictEqual(list.get(1), undefined); + }); + + it("should return undefined when get() is called with an out-of-range index in a non-empty list", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.get(5), undefined); + }); + + }); + + describe("remove()", () => { + + it("should remove an item when there is only one item", () => { + list.add(1); + assertListValues(list, [1]); + + assert.strictEqual(list.remove(0), 1); + assertListValues(list, []); + }); + + it("should remove an item when multiple items are in the list and the middle item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove middle item + assert.strictEqual(list.remove(1), 2); + assertListValues(list, [1, 3]); + }); + + it("should remove an item when multiple items are in the list and the last item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove last item + assert.strictEqual(list.remove(2), 3); + assertListValues(list, [1, 2]); + }); + + it("should remove an item when multiple items are in the list and the first item is removed", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove first item + assert.strictEqual(list.remove(0), 1); + assertListValues(list, [2, 3]); + }); + + it("should throw an error when multiple items are in the list and an out-of-bounds index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(5); + }, "Index 5 does not exist in the list."); + }); + + it("should throw an error when multiple items are in the list and a negative index is used", () => { + list.add(1); + list.add(2); + list.add(3); + assertListValues(list, [1, 2, 3]); + + // remove unknown item + assert.throws(() => { + list.remove(-1); + }, "Index -1 does not exist in the list."); + }); + + it("should throw an error when the list is empty", () => { + assert.throws(() => { + list.remove(0); + }, "Index 0 does not exist in the list."); + }); + }); + + describe("clear()", () => { + + it("should not throw an error when the list has no items", () => { + assertListValues(list, []); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has one item", () => { + list.add(1); + assertListValues(list, [1]); + + list.clear(); + assertListValues(list, []); + }); + + it("should remove all items when the list has multiple items", () => { + list.add(1); + list.add(2); + assertListValues(list, [1, 2]); + + list.clear(); + assertListValues(list, []); + }); + + }); + + + describe("size", () => { + + it("should return 0 when the list is empty", () => { + assert.strictEqual(list.size, 0); + }); + + it("should return 1 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.size, 1); + }); + + it("should return 2 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.size, 2); + }); + + }); + + describe("indexOf()", () => { + + it("should return -1 when the list is empty", () => { + assert.strictEqual(list.indexOf(1), -1); + }); + + it("should return 0 when the list has one item", () => { + list.add(1); + assert.strictEqual(list.indexOf(1), 0); + }); + + it("should return 1 when the list has two items", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(2), 1); + }); + + it("should return -1 when the list doesn't contain the value", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.indexOf(3), -1); + }); + + }); + + ["values", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...list[method]()], []); + }); + + it("should iterate over list when there is one item", () => { + list.add(1); + + assert.deepStrictEqual([...list[method]()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + list.add(1); + list.add(2); + list.add(3); + + assert.deepStrictEqual([...list[method]()], [1, 2, 3]); + }); + + }); + + }); + + + +});