Repository: humanwhocodes/computer-science-in-javascript
Branch: master
Commit: 7a14efde3751
Files: 49
Total size: 261.4 KB
Directory structure:
gitextract_sd20h0oq/
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── algorithms/
│ ├── checksums/
│ │ └── luhn-algorithm/
│ │ └── luhn-algorithm.js
│ ├── searching/
│ │ └── binary-search/
│ │ └── binary-search.js
│ └── sorting/
│ ├── insertion-sort/
│ │ └── insertion-sort.js
│ ├── merge-sort-iterative/
│ │ └── merge-sort-iterative.js
│ ├── merge-sort-recursive/
│ │ ├── merge-sort-inplace.js
│ │ └── merge-sort-recursive.js
│ ├── quicksort/
│ │ └── quicksort.js
│ └── selection-sort/
│ └── selection-sort.js
├── encodings/
│ └── base64/
│ ├── base64.htm
│ └── base64.js
├── package.json
├── src/
│ ├── algorithms/
│ │ └── sorting/
│ │ └── bubble-sort/
│ │ ├── README.md
│ │ ├── bubble-sort.js
│ │ └── package.json
│ └── data-structures/
│ ├── binary-heap/
│ │ ├── README.md
│ │ ├── binary-heap.js
│ │ └── package.json
│ ├── binary-search-tree/
│ │ ├── README.md
│ │ ├── binary-search-tree.js
│ │ └── package.json
│ ├── circular-doubly-linked-list/
│ │ ├── README.md
│ │ ├── circular-doubly-linked-list.js
│ │ └── package.json
│ ├── circular-linked-list/
│ │ ├── README.md
│ │ ├── circular-linked-list.js
│ │ └── package.json
│ ├── doubly-linked-list/
│ │ ├── README.md
│ │ ├── doubly-linked-list.js
│ │ └── package.json
│ ├── hash-map/
│ │ ├── README.md
│ │ ├── hash-map.js
│ │ └── package.json
│ └── linked-list/
│ ├── README.md
│ ├── linked-list.js
│ └── package.json
└── tests/
├── algorithms/
│ └── sorting/
│ ├── bubble-sort/
│ │ └── bubble-sort.js
│ └── quicksort/
│ └── quicksort.js
└── data-structures/
├── binary-heap/
│ └── binary-heap.js
├── binary-search-tree/
│ └── binary-search-tree.js
├── circular-doubly-linked-list/
│ └── circular-doubly-linked-list.js
├── circular-linked-list/
│ └── circular-linked-list.js
├── doubly-linked-list/
│ └── doubly-linked-list.js
├── hash-map/
│ └── hash-map.js
└── linked-list/
└── linked-list.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
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"
]
}
};
================================================
FILE: .gitignore
================================================
node_modules/
holding/
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "8"
- "9"
- "10"
- "11"
sudo: false
branches:
only:
- master
# Run npm test always
script:
- "npm test"
================================================
FILE: LICENSE
================================================
Copyright (c) 2009 Nicholas C. Zakas. All rights reserved.
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.
================================================
FILE: README.md
================================================
# 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
================================================
FILE: algorithms/checksums/luhn-algorithm/luhn-algorithm.js
================================================
/*
* Luhn algorithm 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.
*/
/**
* Uses Luhn algorithm to validate a numeric identifier.
* @param {String} identifier The identifier to validate.
* @return {Boolean} True if the identifier is valid, false if not.
*/
function isValidIdentifier(identifier) {
var sum = 0,
alt = false,
i = identifier.length-1,
num;
while (i >= 0){
//get the next digit
num = parseInt(identifier.charAt(i), 10);
//if it's not a valid number, abort
if (isNaN(num)){
return false;
}
//if it's an alternate number...
if (alt) {
num *= 2;
if (num > 9){
num = (num % 10) + 1;
}
}
//flip the alternate bit
alt = !alt;
//add to the rest of the sum
sum += num;
//go to next digit
i--;
}
//determine if it's valid
return (sum % 10 == 0);
}
================================================
FILE: algorithms/searching/binary-search/binary-search.js
================================================
/*
* Binary search 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.
*/
/**
* Uses a binary search algorithm to locate a value in the specified array.
* @param {Array} items The array containing the item.
* @param {variant} value The value to search for.
* @return {int} The zero-based index of the value in the array or -1 if not found.
*/
function binarySearch(items, value){
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
//adjust search area
if (value < items[middle]){
stopIndex = middle - 1;
} else if (value > items[middle]){
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
================================================
FILE: algorithms/sorting/insertion-sort/insertion-sort.js
================================================
/*
* 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;
}
================================================
FILE: algorithms/sorting/merge-sort-iterative/merge-sort-iterative.js
================================================
/*
* Iterative merge sort implementation in JavaScript
* Copyright (c) 2009-2011 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.
*/
/**
* 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 merge(left, right){
var result = [];
while (left.length > 0 && right.length > 0){
if (left[0] < right[0]){
result.push(left.shift());
} else {
result.push(right.shift());
}
}
result = result.concat(left).concat(right);
//make sure remaining arrays are empty
left.splice(0, left.length);
right.splice(0, right.length);
return result;
}
/**
* Sorts an array in ascending natural order using
* merge sort.
* @param {Array} items The array to sort.
* @return {Array} The sorted array.
*/
function mergeSort(items){
// Terminal condition - don't need to do anything for arrays with 0 or 1 items
if (items.length < 2) {
return items;
}
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
for (var lim=len; lim > 1; lim = Math.floor((lim+1)/2)){
for (var j=0,k=0; k < lim; j++, k+=2){
work[j] = merge(work[k], work[k+1]);
}
work[j] = []; //in case of odd number of items
}
return work[0];
}
================================================
FILE: algorithms/sorting/merge-sort-recursive/merge-sort-inplace.js
================================================
/*
* 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
* 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.
*/
/**
* 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 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));
}
/**
* Sorts an array in ascending natural order using
* merge sort.
* @param {Array} items The array to sort.
* @return {Array} The sorted array.
*/
function mergeSort(items){
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;
}
================================================
FILE: algorithms/sorting/merge-sort-recursive/merge-sort-recursive.js
================================================
/*
* Recursive merge 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.
*/
/**
* 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 = [],
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));
}
/**
* Sorts an array in ascending natural order using
* merge sort.
* @param {Array} items The array to sort.
* @return {Array} The sorted array.
*/
function mergeSort(items){
if (items.length < 2) {
return items;
}
var middle = Math.floor(items.length / 2),
left = items.slice(0, middle),
right = items.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
================================================
FILE: algorithms/sorting/quicksort/quicksort.js
================================================
/*
* 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 (i <= right && items[i] < pivot) {
i++;
}
// if the item on the right is greater than the pivot, continue left
while (j >= left && 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;
}
================================================
FILE: algorithms/sorting/selection-sort/selection-sort.js
================================================
/*
* Selection 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 selection 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 selectionSort(items){
var len = items.length,
min, i, j;
for (i=0; i < len; i++){
// set minimum to this position
min = i;
// 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 (i != min){
swap(items, i, min);
}
}
return items;
}
================================================
FILE: encodings/base64/base64.htm
================================================
Base64 Tests
Base64 Encoding/Decoding Tests
================================================
FILE: encodings/base64/base64.js
================================================
/*
* Base 64 implementation in JavaScript
* Copyright (c) 2009 Nicholas C. Zakas. All rights reserved.
*
* 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.
*/
/**
* Base64-encodes a string of text.
* @param {String} text The text to encode.
* @return {String} The base64-encoded string.
*/
function base64Encode(text){
if (/([^\u0000-\u00ff])/.test(text)){
throw new Error("Can't base64 encode non-ASCII characters.");
}
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
i = 0,
cur, prev, byteNum,
result=[];
while(i < text.length){
cur = text.charCodeAt(i);
byteNum = i % 3;
switch(byteNum){
case 0: //first byte
result.push(digits.charAt(cur >> 2));
break;
case 1: //second byte
result.push(digits.charAt((prev & 3) << 4 | (cur >> 4)));
break;
case 2: //third byte
result.push(digits.charAt((prev & 0x0f) << 2 | (cur >> 6)));
result.push(digits.charAt(cur & 0x3f));
break;
}
prev = cur;
i++;
}
if (byteNum == 0){
result.push(digits.charAt((prev & 3) << 4));
result.push("==");
} else if (byteNum == 1){
result.push(digits.charAt((prev & 0x0f) << 2));
result.push("=");
}
return result.join("");
}
/**
* Base64-decodes a string of text.
* @param {String} text The text to decode.
* @return {String} The base64-decoded string.
*/
function base64Decode(text){
//ignore white space
text = text.replace(/\s/g,"");
//first check for any unexpected input
if(!(/^[a-z0-9\+\/\s]+\={0,2}$/i.test(text)) || text.length % 4 > 0){
throw new Error("Not a base64-encoded string.");
}
//local variables
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
cur, prev, digitNum,
i=0,
result = [];
//remove any equals signs
text = text.replace(/=/g, "");
//loop over each character
while(i < text.length){
cur = digits.indexOf(text.charAt(i));
digitNum = i % 4;
switch(digitNum){
//case 0: first digit - do nothing, not enough info to work with
case 1: //second digit
result.push(String.fromCharCode(prev << 2 | cur >> 4));
break;
case 2: //third digit
result.push(String.fromCharCode((prev & 0x0f) << 4 | cur >> 2));
break;
case 3: //fourth digit
result.push(String.fromCharCode((prev & 3) << 6 | cur));
break;
}
prev = cur;
i++;
}
//return a string
return result.join("");
}
================================================
FILE: package.json
================================================
{
"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"
}
}
================================================
FILE: src/algorithms/sorting/bubble-sort/README.md
================================================
# 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
================================================
FILE: src/algorithms/sorting/bubble-sort/bubble-sort.js
================================================
/**
* @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;
};
================================================
FILE: src/algorithms/sorting/bubble-sort/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/binary-heap/README.md
================================================
# 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
================================================
FILE: src/data-structures/binary-heap/binary-heap.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/binary-heap/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/binary-search-tree/README.md
================================================
# 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
================================================
FILE: src/data-structures/binary-search-tree/binary-search-tree.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/binary-search-tree/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/circular-doubly-linked-list/README.md
================================================
# 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
================================================
FILE: src/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/circular-doubly-linked-list/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/circular-linked-list/README.md
================================================
# 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
================================================
FILE: src/data-structures/circular-linked-list/circular-linked-list.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/circular-linked-list/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/doubly-linked-list/README.md
================================================
# 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
================================================
FILE: src/data-structures/doubly-linked-list/doubly-linked-list.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/doubly-linked-list/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/hash-map/README.md
================================================
# 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
================================================
FILE: src/data-structures/hash-map/hash-map.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/hash-map/package.json
================================================
{
"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"
}
}
================================================
FILE: src/data-structures/linked-list/README.md
================================================
# 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
================================================
FILE: src/data-structures/linked-list/linked-list.js
================================================
/**
* @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;
================================================
FILE: src/data-structures/linked-list/package.json
================================================
{
"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"
}
}
================================================
FILE: tests/algorithms/sorting/bubble-sort/bubble-sort.js
================================================
/**
* @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);
});
});
});
================================================
FILE: tests/algorithms/sorting/quicksort/quicksort.js
================================================
/**
* @fileoverview Quick Sort tests
*/
/* global it, describe */
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const assert = require("chai").assert;
const fs = require("fs");
const path = require("path");
// Load and evaluate the quickSort code in global scope
const quickSortPath = path.join(__dirname, "../../../../algorithms/sorting/quicksort/quicksort.js");
const quickSortCode = fs.readFileSync(quickSortPath, "utf8");
// Use Function constructor to create functions in the parent scope
const createFunctions = new Function(quickSortCode + "\nreturn { swap: swap, partition: partition, quickSort: quickSort };");
const { quickSort } = createFunctions();
//-----------------------------------------------------------------------------
// Tests
//-----------------------------------------------------------------------------
describe("quickSort()", () => {
[
[],
[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);
quickSort(items);
assert.deepStrictEqual(items, result);
});
});
// Test cases specifically for pivot edge cases
it("should sort when pivot is minimum value", () => {
const items = [5, 5, 5, 1, 5];
const expected = [1, 5, 5, 5, 5];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
it("should sort when pivot is maximum value", () => {
const items = [1, 1, 1, 9, 1];
const expected = [1, 1, 1, 1, 9];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
it("should sort an array with all same values", () => {
const items = [3, 3, 3, 3, 3];
const expected = [3, 3, 3, 3, 3];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
it("should sort an already sorted array", () => {
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
it("should sort a reverse sorted array", () => {
const items = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
const expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
it("should sort an array with negative numbers", () => {
const items = [-5, 3, -1, 7, -10, 15];
const expected = [-10, -5, -1, 3, 7, 15];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
it("should sort an array with duplicates", () => {
const items = [5, 2, 8, 2, 9, 1, 5, 5];
const expected = [1, 2, 2, 5, 5, 5, 8, 9];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
// Edge case: array where subarray partitions could access out-of-bounds elements
it("should handle edge case with specific value distribution", () => {
const items = [100, 1, 2, 3, 4, 5];
const expected = [1, 2, 3, 4, 5, 100];
quickSort(items);
assert.deepStrictEqual(items, expected);
});
});
================================================
FILE: tests/data-structures/binary-heap/binary-heap.js
================================================
/**
* @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]);
});
});
});
});
================================================
FILE: tests/data-structures/binary-search-tree/binary-search-tree.js
================================================
/**
* @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]);
});
});
});
});
================================================
FILE: tests/data-structures/circular-doubly-linked-list/circular-doubly-linked-list.js
================================================
/**
* @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]);
});
});
});
================================================
FILE: tests/data-structures/circular-linked-list/circular-linked-list.js
================================================
/**
* @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]);
});
});
});
================================================
FILE: tests/data-structures/doubly-linked-list/doubly-linked-list.js
================================================
/**
* @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]);
});
});
});
================================================
FILE: tests/data-structures/hash-map/hash-map.js
================================================
/**
* @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]);
});
});
});
================================================
FILE: tests/data-structures/linked-list/linked-list.js
================================================
/**
* @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]);
});
});
});
});