├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── jsdoc.json ├── package-lock.json ├── package.json ├── processors ├── graph.edge-weighted.directed.shortest-path-dijkstra.js ├── graph.edge-weighted.undirected.minimum-spanning-tree-prim.js ├── graph.unweighted.breadth-first-paths.js ├── graph.unweighted.depth-first-paths.js ├── graph.unweighted.directed.cycle.js ├── graph.unweighted.directed.topological-sort.js ├── graph.unweighted.undirected.connected-components.js └── graph.unweighted.undirected.cycle.js ├── structures ├── graph.edge-weighted.directed.js ├── graph.edge-weighted.undirected.js ├── graph.unweighted.directed.js ├── graph.unweighted.undirected.js ├── hash-table.js ├── linked-list.js ├── queue.circular.js ├── queue.js ├── queue.priority.max.js ├── queue.priority.min.js ├── stack.js ├── tree.binary-search.js ├── tree.red-black.js ├── tree.trie.prefix.js └── tree.trie.suffix.js └── test ├── processors ├── graph.edge-weighted.directed.shortest-path-dijkstra.js ├── graph.edge-weighted.undirected.minimum-spanning-tree-prim.js ├── graph.unweighted.directed.breadth-first-paths.js ├── graph.unweighted.directed.cycle.js ├── graph.unweighted.directed.depth-first-paths.js ├── graph.unweighted.directed.topological-sort.js ├── graph.unweighted.undirected.breadth-first-paths.js ├── graph.unweighted.undirected.connected-components.js ├── graph.unweighted.undirected.cycle.js └── graph.unweighted.undirected.depth-first-paths.js └── structures ├── graph.edge-weighted.directed.js ├── graph.edge-weighted.undirected.js ├── graph.unweighted.directed.js ├── graph.unweighted.undirected.js ├── hash-table.js ├── linked-list.js ├── queue.circular.js ├── queue.js ├── queue.priority.max.js ├── queue.priority.min.js ├── stack.js ├── tree.binary-search.js ├── tree.red-black.js ├── tree.trie.prefix.js └── tree.trie.suffix.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node Modules # 2 | ################### 3 | node_modules 4 | 5 | # Text Editor # 6 | ################### 7 | *.vscode 8 | 9 | # Compiled source # 10 | ################### 11 | documentation 12 | *.com 13 | *.class 14 | *.dll 15 | *.exe 16 | *.o 17 | *.so 18 | 19 | # Packages # 20 | ############ 21 | # it's better to unpack these files and commit the raw source 22 | # git has its own built in compression methods 23 | *.7z 24 | *.dmg 25 | *.gz 26 | *.iso 27 | *.jar 28 | *.rar 29 | *.tar 30 | *.zip 31 | 32 | # Logs and databases # 33 | ###################### 34 | *.log 35 | *.sql 36 | *.sqlite 37 | 38 | # OS generated files # 39 | ###################### 40 | .DS_Store 41 | .DS_Store? 42 | ._* 43 | .Spotlight-V100 44 | .Trashes 45 | ehthumbs.db 46 | Thumbs.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | notifications: 5 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 George Norberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ganorberg/data-structures-javascript.svg?branch=master)](https://travis-ci.org/ganorberg/data-structures-javascript) 2 | 3 | # JavaScript Data Structures 4 | 5 | Fast, fully tested and documented data structures built with JavaScript. 6 | 7 | ## Testing 8 | 9 | This library is 100% black box tested with 400+ unit tests. This means the test suite covers the public API and ignores implementation details (like private methods), thereby avoiding brittle tests and unnecessary re-work over time. 10 | 11 | Unit tests follow AAA testing pattern (Arrange -> Act -> Assert). 12 | 13 | If you would like to run the test suite after cloning the repo, run `npm install` then `npm test`. 14 | 15 | ## Generate documentation 16 | 17 | You can easily build JSDoc documentation by running `npm run doc`. 18 | 19 | ## Technical decisions: 20 | 21 | - prefer using space to improve time complexity 22 | - prefer multiple recursion if algorithm naturally branches 23 | - prefer iterative solutions over simple recursion 24 | - prefer throwing errors over silently failing on incorrect inputs and edge cases 25 | - prefer conditional statements that avoid code instead of wrapping it 26 | - give users access to class properties 27 | - prefer command-query separation -- setters do not return values 28 | - prefer single responsibility for all functions 29 | - prefer high cohesion over utility modules 30 | - prefer descriptive, verbose identifiers 31 | 32 | ## General notes 33 | 34 | ### Performance compared to other languages 35 | 36 | If you need to squeeze every ounce of performance out of your data structures, JavaScript is likely not your language of choice given its high level of abstraction. That said, I imagine many JavaScript developers still build data structures on a daily basis. This library provides those developers with clean, performant examples that take into account all of JavaScript's quirks. 37 | 38 | ### Object key iteration: for in loops vs Object.keys 39 | 40 | Although I generally avoid for in loops in my code and prefer the Airbnb style guide way of accessing object properties through Object.keys, this is highly inefficient for large data sets. All data structures in this library are built to scale with time efficiency as the primary goal, and the linear time operation of Object.keys is simply too costly to use at scale. 41 | 42 | ### Dynamic graphs 43 | 44 | The graphs in this library are dynamic and flexible -- vertices can be added or deleted at whim. This means that the traditional method of building graphs with vertices labeled 0 to n-1 in array-based adjacency lists is out of the question. In that case, extra values would need to be tracked -- for example, deleting values from the middle of the array would leave holes that need to be filled, and looping would be complicated. The index for the next addition would also need to be tracked. 45 | 46 | Object-oriented adjacency lists avoid these issues given that keys can be named anything and still accessed in constant time. 47 | 48 | ### Library structure: ES6 classes vs OLOO pattern 49 | 50 | I chose to use class syntax for its familiarity compared to classic object-oriented approaches to data structures. Users transitioning from object-oriented languages like Java or Python should understand the JavaScript implementations immediately. 51 | 52 | If I were to build these structures without "classes", I would use Kyle Simpson's OLOO pattern (objects linked to other objects). The performance would be nearly the same, yet the code would be cleaner and more flexible. Without extraneous constructors and the new keyword, we could directly and explicitly embrace the prototype chain. 53 | 54 | ### Space complexity vs auxiliary space 55 | 56 | When I describe space complexity in this library, I am only referring to the _extra_ space the algorithm uses with respect to the input size. I do not include the space used by the input. The formal definition for this is auxiliary space. For example, I would describe methods that modify every element of an array in-place as having O(1) space complexity because extra space is not required. 57 | 58 | ### Space complexities do not include call stack 59 | 60 | My space complexities refer to heap memory consumed rather than call stack space consumed. Call stack space is typically (logN) for depth-first search on trees and O(N) to traverse linear structures with recursive calls that aren't tail-call optimized. 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Data structures 2 | const EdgeWeightedDirectedGraph = require("./structures/graph.edge-weighted.directed"); 3 | const EdgeWeightedUndirectedGraph = require("./structures/graph.edge-weighted.undirected"); 4 | const DirectedGraph = require("./structures/graph.unweighted.directed"); 5 | const UndirectedGraph = require("./structures/graph.unweighted.undirected"); 6 | const HashTable = require("./structures/hash-table"); 7 | const LinkedList = require("./structures/linked-list"); 8 | const CircularQueue = require("./structures/queue.circular"); 9 | const Queue = require("./structures/queue"); 10 | const PriorityQueueMax = require("./structures/queue.priority.max"); 11 | const PriorityQueueMin = require("./structures/queue.priority.min"); 12 | const Stack = require("./structures/stack"); 13 | const BinarySearchTree = require("./structures/tree.binary-search"); 14 | const RedBlackTree = require("./structures/tree.red-black"); 15 | const PrefixTrie = require("./structures/tree.trie.prefix"); 16 | const SuffixTrie = require("./structures/tree.trie.suffix"); 17 | 18 | // Processors 19 | const ShortestPath = require("./processors/graph.edge-weighted.directed.shortest-path-dijkstra"); 20 | const MinimumSpanningTree = require("./processors/graph.edge-weighted.undirected.minimum-spanning-tree-prim"); 21 | const BreadthFirstPaths = require("./processors/graph.unweighted.breadth-first-paths"); 22 | const DepthFirstPaths = require("./processors/graph.unweighted.depth-first-paths"); 23 | const DirectedCycle = require("./processors/graph.unweighted.directed.cycle"); 24 | const TopologicalSort = require("./processors/graph.unweighted.directed.topological-sort"); 25 | const ConnectedComponents = require("./processors/graph.unweighted.undirected.connected-components"); 26 | const UndirectedCycle = require("./processors/graph.unweighted.undirected.cycle"); 27 | 28 | const dataStructures = { 29 | EdgeWeightedDirectedGraph, 30 | EdgeWeightedUndirectedGraph, 31 | DirectedGraph, 32 | UndirectedGraph, 33 | HashTable, 34 | LinkedList, 35 | CircularQueue, 36 | Queue, 37 | PriorityQueueMax, 38 | PriorityQueueMin, 39 | Stack, 40 | BinarySearchTree, 41 | RedBlackTree, 42 | PrefixTrie, 43 | SuffixTrie 44 | }; 45 | 46 | const processors = { 47 | ShortestPath, 48 | MinimumSpanningTree, 49 | BreadthFirstPaths, 50 | DepthFirstPaths, 51 | DirectedCycle, 52 | TopologicalSort, 53 | ConnectedComponents, 54 | UndirectedCycle 55 | }; 56 | 57 | module.exports = Object.assign({}, dataStructures, processors); 58 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "markdown": { 4 | "hardwrap": true 5 | }, 6 | "recurseDepth": 10, 7 | "source": { 8 | "includePattern": ".+\\.js(doc|x)?$" 9 | }, 10 | "sourceType": "module", 11 | "tags": { 12 | "allowUnknownTags": true, 13 | "dictionaries": ["jsdoc", "closure"] 14 | }, 15 | "templates": { 16 | "cleverLinks": false, 17 | "monospaceLinks": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ganorberg/data-structures-javascript", 3 | "version": "1.0.0", 4 | "description": "Data structure and graph processing library written in modern JavaScript", 5 | "scripts": { 6 | "doc": "jsdoc structures --readme README.md --recurse --configure jsdoc.json --destination documentation", 7 | "test": "mocha --recursive" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ganorberg/data-structures-javascript.git" 12 | }, 13 | "main": "index.js", 14 | "keywords": [ 15 | "data structures", 16 | "javascript", 17 | "graph", 18 | "undirected graph", 19 | "directed graph", 20 | "weighted graph", 21 | "breadth first search", 22 | "BFS", 23 | "depth first search", 24 | "DFS", 25 | "dijkstra", 26 | "shortest path", 27 | "prim", 28 | "minimum spanning tree", 29 | "red black tree", 30 | "RBT", 31 | "binary heap", 32 | "priority queue", 33 | "binary search tree", 34 | "BST", 35 | "suffix trie", 36 | "prefix trie", 37 | "trie", 38 | "circular queue", 39 | "circular buffer", 40 | "ring buffer", 41 | "queue", 42 | "stack", 43 | "hash table", 44 | "hash map", 45 | "linked list" 46 | ], 47 | "author": "George Norberg", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/ganorberg/data-structures-javascript/issues" 51 | }, 52 | "homepage": "https://github.com/ganorberg/data-structures-javascript#readme", 53 | "devDependencies": { 54 | "chai": "^4.2.0", 55 | "jsdoc": "^3.6.3", 56 | "mocha": "^7.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /processors/graph.edge-weighted.directed.shortest-path-dijkstra.js: -------------------------------------------------------------------------------- 1 | const MinimumPriorityQueue = require("../structures/queue.priority.min"); 2 | 3 | /** 4 | * @description Use Dijkstra's shortest path algorithm to find the shortest path 5 | * from a source vertex to all other vertices in a graph. This implementation 6 | * also delivers two vertex maps: the first tracks shortest distance from the 7 | * source, and the second tracks the final edge that leads to its shortest path. 8 | * 9 | * Strategy: Traverse all vertices using breadth-first search. Use a Set to 10 | * detect vertices already visited. Use a min heap to store vertices and their 11 | * shortest distance from the source known so far. Until the priority queue is 12 | * empty, greedily grab the minimum distance vertex. If unvisited, loop through 13 | * its edges to compare distances from the source. If a smaller distance is 14 | * found, update the distance, the parent, and add to the priority queue. 15 | * 16 | * Time complexity: O((E + V) * logV), where V is total vertices and E is total edges 17 | * Space complexity: O(V + E), where V is total vertices and E is total edges 18 | * 19 | * TIME COMPLEXITY EXPLAINED 20 | * Each vertex will be removed from the priority queue once, which is a logV 21 | * operation on V vertices yielding VlogV. Each edge has the potential to be 22 | * inserted into the priority queue, which is a logV operation on E edges 23 | * yielding ElogV. Adding these two gives VlogV + ElogV = logV * (V + E) 24 | * 25 | * SPACE COMPLEXITY EXPLAINED 26 | * Without decrease key, the priority queue has the potential to add all edges 27 | * from the graph. This requires O(E) space. The visited Set will always take 28 | * O(V) space. Therefore, the space complexity is O(V + E). 29 | * 30 | * @param {Object} distanceFromSource - track weight for each vertex from source 31 | * @param {Object} graph - graph being processed 32 | * @param {Object} parent - track how algorithm reaches each vertex 33 | * @param {String|Number} source - source vertex for all paths 34 | * @param {Set} visited - track which vertices have already been visited 35 | * 36 | * @private 37 | */ 38 | function initializeDijkstra( 39 | distanceFromSource, 40 | graph, 41 | parent, 42 | source, 43 | visited 44 | ) { 45 | if (!graph) { 46 | throw new Error("The graph is not loaded"); 47 | } 48 | if (!graph.adjacencyList.hasOwnProperty(source)) { 49 | throw new Error("This source vertex is not in the graph"); 50 | } 51 | 52 | // All vertices begin with infinity for distance comparisons later 53 | for (const vertex in graph.adjacencyList) { 54 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 55 | continue; 56 | } 57 | distanceFromSource[vertex] = Infinity; 58 | } 59 | 60 | // Source begins with 0 distance. Use constant to avoid magic numbers. 61 | const SOURCE_DISTANCE_FROM_ITSELF = 0; 62 | distanceFromSource[source] = SOURCE_DISTANCE_FROM_ITSELF; 63 | 64 | // Efficient for sparse graphs, as most real-world graphs tend to be 65 | const minPQ = new MinimumPriorityQueue(); 66 | minPQ.insert(SOURCE_DISTANCE_FROM_ITSELF, source); 67 | 68 | // Could check all vertices visited, but wouldn't work for unconnected graphs 69 | while (!minPQ.isEmpty()) { 70 | const minVertex = minPQ.deleteMin().value; 71 | 72 | // Allow dupes on priority queue but only visit one with smallest distance 73 | if (visited.has(minVertex)) { 74 | continue; 75 | } 76 | visited.add(minVertex); 77 | 78 | graph.adjacencyList[minVertex].forEach(edge => { 79 | const distance = distanceFromSource[minVertex] + edge.weight; 80 | const adjacentVertex = edge.to(); 81 | 82 | // Take action only if we find a shorter distance path 83 | if (distance >= distanceFromSource[adjacentVertex]) { 84 | return; 85 | } 86 | distanceFromSource[adjacentVertex] = distance; 87 | parent[adjacentVertex] = minVertex; 88 | minPQ.insert(distance, adjacentVertex); 89 | }); 90 | } 91 | } 92 | 93 | /** Class representing shortest path processor for weighted directed graphs */ 94 | class ShortestPath { 95 | /** 96 | * Modern Dijkstra's algorithm with source node and priority queue. Effective 97 | * for graphs with nonnegative weights and cycles. Performed without decrease key 98 | * to trade space for better time complexity as outlined in this research paper: 99 | * https://pdfs.semanticscholar.org/5c50/a1593b6cbb16b578dc57ebf38c26d479c317.pdf 100 | * 101 | * @constructor 102 | * 103 | * @param {Graph} graph - graph being processed 104 | * @param {String|Number} sourceVertex - source vertex for all paths 105 | * 106 | * @property {Object} distanceFromSource - track weight for each vertex from source 107 | * @property {Object} graph - graph being processed 108 | * @property {Object} parent - track how algorithm reaches each vertex 109 | * @property {String|Number} sourceVertex - source vertex for all paths 110 | * @property {Set} visited - track which vertices have already been visited 111 | */ 112 | constructor(graph, sourceVertex) { 113 | this.distanceFromSource = {}; 114 | this.graph = graph; 115 | this.parent = {}; 116 | this.sourceVertex = String(sourceVertex); 117 | this.visited = new Set(); 118 | 119 | initializeDijkstra( 120 | this.distanceFromSource, 121 | this.graph, 122 | this.parent, 123 | this.sourceVertex, 124 | this.visited 125 | ); 126 | } 127 | 128 | /** 129 | * @description Get the shortest distance from the source vertex to the input 130 | * vertex. 131 | * 132 | * Strategy: Access value in distanceFromSource object with vertex as key. 133 | * 134 | * Time complexity: O(1) 135 | * Space complexity: O(1) 136 | * 137 | * @param {String|Number} vertex - vertex whose distance from source is sought 138 | * 139 | * @returns {Number} - distance from source vertex to input vertex 140 | */ 141 | distanceTo(vertex) { 142 | if (!this.hasPathTo(vertex)) { 143 | return null; 144 | } 145 | return this.distanceFromSource[vertex]; 146 | } 147 | 148 | /** 149 | * @description Check if input vertex is connected to the source vertex. 150 | * 151 | * Strategy: Check if vertex is in the visited Set. 152 | * 153 | * Time complexity: O(1) 154 | * Space complexity: O(1) 155 | * 156 | * @param {String|Number} vertex - vertex that may be connected to source vertex 157 | * 158 | * @returns {Boolean} - true if vertex is connected to source vertex 159 | */ 160 | hasPathTo(vertex) { 161 | if (!this.graph.adjacencyList.hasOwnProperty(vertex)) { 162 | throw new Error("The input vertex is not in the graph"); 163 | } 164 | 165 | // Stringify to allow the user to call this method with numbers 166 | return this.visited.has(String(vertex)); 167 | } 168 | 169 | /** 170 | * @description Get the shortest path from the source vertex to the input 171 | * vertex. 172 | * 173 | * Strategy: Traverse parent object until source vertex is reached. 174 | * 175 | * Time complexity: O(P), where P is path length 176 | * Space complexity: O(P), where P is path length 177 | * 178 | * @param {String|Number} destinationVertex - vertex whose path is sought 179 | * 180 | * @returns {Array} - shortest path from destination to source 181 | */ 182 | shortestPathTo(destinationVertex) { 183 | if (!this.hasPathTo(destinationVertex)) { 184 | return null; 185 | } 186 | 187 | const path = []; 188 | for ( 189 | // Stringify to allow the user to call this method with numbers 190 | let vertex = String(destinationVertex); 191 | vertex !== this.sourceVertex; 192 | vertex = this.parent[vertex] 193 | ) { 194 | path.push(vertex); 195 | } 196 | 197 | path.push(this.sourceVertex); 198 | return path; 199 | } 200 | } 201 | 202 | module.exports = ShortestPath; 203 | -------------------------------------------------------------------------------- /processors/graph.edge-weighted.undirected.minimum-spanning-tree-prim.js: -------------------------------------------------------------------------------- 1 | const MinimumPriorityQueue = require("../structures/queue.priority.min"); 2 | 3 | /** 4 | * @description Private method that visits vertices placed in the minimum 5 | * spanning tree. Called in initializeMST. 6 | * 7 | * Strategy: Add input vertex to the Set of vertices in the MST. Loop through 8 | * its edges. If the other vertex is not in the MST, add the edge to the 9 | * priority queue. 10 | * 11 | * Time complexity: O(A), where A is adjacent vertices 12 | * Space complexity: O(1) 13 | * 14 | * @param {String|Number} vertex - current vertex visited 15 | * @param {Graph} graph - graph being processed 16 | * @param {Set} MSTvertices - vertices in the minimum spanning tree 17 | * @param {PriorityQueue} priorityQueue - minPQ tracking edge weights 18 | * 19 | * @private 20 | */ 21 | function visit(vertex, graph, MSTvertices, priorityQueue) { 22 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 23 | throw new Error("That vertex does not exist in the graph"); 24 | } 25 | 26 | MSTvertices.add(vertex); 27 | 28 | // Insert edges only if both vertices not in MST 29 | graph.adjacencyList[vertex].forEach(edge => { 30 | if (MSTvertices.has(edge.other(vertex))) { 31 | return; 32 | } 33 | priorityQueue.insert(edge.weight, edge); 34 | }); 35 | } 36 | 37 | /** 38 | * @description Private method that builds the minimum spanning tree. 39 | * 40 | * Strategy: Grab a starting vertex and visit it. Loop until all vertices are 41 | * included in MST. Greedily remove the min, add to MST, then visit unvisited 42 | * vertices. 43 | * 44 | * Time complexity: O(ElogV), where E is total edges and V is total vertices 45 | * Space complexity: O(E), where E is total edges 46 | * 47 | * @param {Graph} graph - graph being processed 48 | * @param {Set} MSTedges - edges in the minimum spanning tree 49 | * 50 | * @private 51 | */ 52 | function initializeMST(graph, MSTedges) { 53 | if (!graph) { 54 | throw new Error("The graph is not loaded"); 55 | } 56 | 57 | // Ugly but efficient way to grab a starting vertex from graph 58 | let sourceVertex; 59 | for (const vertex in graph.adjacencyList) { 60 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 61 | continue; 62 | } 63 | sourceVertex = vertex; 64 | break; 65 | } 66 | 67 | if (sourceVertex === undefined) { 68 | throw new Error("Your graph is empty"); 69 | } 70 | 71 | const MSTvertices = new Set(); 72 | const priorityQueue = new MinimumPriorityQueue(); 73 | 74 | // Populate priority queue with edges from source vertex and mark as visited 75 | visit(sourceVertex, graph, MSTvertices, priorityQueue); 76 | 77 | while (MSTvertices.size < graph.totalVertices) { 78 | const edge = priorityQueue.deleteMin().value; 79 | const { v1, v2 } = edge; 80 | if (MSTvertices.has(v1) && MSTvertices.has(v2)) { 81 | continue; 82 | } 83 | 84 | // Greedily add min edge to MST as long as only one vertex is in MST 85 | MSTedges.add(edge); 86 | 87 | // Populate priority queue and mark the vertex as visited 88 | if (!MSTvertices.has(v1)) { 89 | visit(v1, graph, MSTvertices, priorityQueue); 90 | } else if (!MSTvertices.has(v2)) { 91 | visit(v2, graph, MSTvertices, priorityQueue); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Class representing a minimum spanning tree processor for weighted undirected 98 | * connected graphs 99 | */ 100 | class MinimumSpanningTree { 101 | /** 102 | * Prim's algorithm (lazy). 103 | * 104 | * @param {Object} graph - graph being processed 105 | * 106 | * @property {Object} graph - graph being processed 107 | * @property {Set} minimumSpanningTree - edges of MST 108 | */ 109 | constructor(graph) { 110 | this.graph = graph; 111 | this.minimumSpanningTree = new Set(); 112 | 113 | initializeMST(this.graph, this.minimumSpanningTree); 114 | } 115 | 116 | /** 117 | * @description Get the edges that comprise the minimum spanning tree. 118 | * 119 | * Strategy: Access minimumSpanningTree property. 120 | * 121 | * Time complexity: O(1) 122 | * Space complexity: O(1) 123 | * 124 | * @returns {Set} - set of edges comprising MST 125 | */ 126 | getMinimumSpanningTree() { 127 | return this.minimumSpanningTree; 128 | } 129 | } 130 | 131 | module.exports = MinimumSpanningTree; 132 | -------------------------------------------------------------------------------- /processors/graph.unweighted.breadth-first-paths.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method that tracks path and distance information from 3 | * source vertex to other connected vertices using breadth-first search. 4 | * 5 | * Strategy: Create a queue to store vertices at each distance from the source 6 | * vertex. At distance 0, the queue will only contain the source. At distance 1, 7 | * the queue will contain all adjacent vertices to the source. At distance 2, 8 | * the queue will contain the adjacent adjacent vertices, and so on. Only 9 | * unvisited vertices will be added to the queue while traversing. End when 10 | * queue is empty, which means all connected vertices have been visited. 11 | * 12 | * Edge case: if user inserts a source vertex that does not exist in the graph 13 | * 14 | * Time complexity: O(V), where V is total vertices 15 | * Space complexity: O(V), where V is total vertices 16 | * 17 | * @param {Object} distanceFromSource - track how far each vertex is from source 18 | * @param {Object} graph - graph being processed 19 | * @param {Object} parent - track how algorithm reaches each vertex 20 | * @param {String} sourceVertex - vertex where BFS begins 21 | * @param {Set} visited - track which vertices have already been visited 22 | * 23 | * @private 24 | */ 25 | function initializeBFS( 26 | distanceFromSource, 27 | graph, 28 | parent, 29 | sourceVertex, 30 | visited 31 | ) { 32 | if (!graph) { 33 | throw new Error("The graph is not loaded"); 34 | } 35 | if (!graph.adjacencyList.hasOwnProperty(sourceVertex)) { 36 | throw new Error( 37 | "Please input a source vertex into the constructor that " + 38 | "exists in the graph." 39 | ); 40 | } 41 | 42 | // Begin search with source vertex at distance 0 43 | let queue = [sourceVertex]; 44 | visited.add(sourceVertex); 45 | let distance = 0; 46 | distanceFromSource[sourceVertex] = distance; 47 | 48 | while (true) { 49 | // Use additional queue to simplify distance tracking and avoid expensive shifts 50 | let nextQueue = []; 51 | distance++; 52 | 53 | // Key idea is to use vertices in queue to set up next queue 54 | queue.forEach(vertex => { 55 | graph.adjacencyList[vertex].forEach(adjacentVertex => { 56 | if (visited.has(adjacentVertex)) { 57 | return; 58 | } 59 | nextQueue.push(adjacentVertex); 60 | visited.add(adjacentVertex); 61 | 62 | // Track path and distance data for later use by public methods 63 | parent[adjacentVertex] = vertex; 64 | distanceFromSource[adjacentVertex] = distance; 65 | }); 66 | }); 67 | 68 | // Exit condition: nothing new to see 69 | if (nextQueue.length === 0) { 70 | return; 71 | } 72 | 73 | // Loop resumes with a new queue of vertices whose adjacent lists will be searched 74 | queue = nextQueue; 75 | } 76 | } 77 | 78 | /** Class representing breadth-first path processor for unweighted graphs */ 79 | class BreadthFirstPaths { 80 | /** 81 | * Breadth-first search that tracks distance and path information. 82 | * 83 | * @constructor 84 | * 85 | * @param {Graph} graph - graph being processed 86 | * @param {String|Number} sourceVertex - source vertex for processing... gotta start somewhere! 87 | * 88 | * @property {Object} distanceFromSource - track how far each vertex is from source 89 | * @property {Object} graph - graph being processed 90 | * @property {Object} parent - track how algorithm reaches each vertex 91 | * @property {String|Number} sourceVertex - vertex where BFS begins 92 | * @property {Set} visited - track which vertices have already been visited 93 | */ 94 | constructor(graph, sourceVertex) { 95 | this.distanceFromSource = {}; 96 | this.graph = graph; 97 | this.parent = {}; 98 | this.sourceVertex = String(sourceVertex); 99 | this.visited = new Set(); 100 | 101 | initializeBFS( 102 | this.distanceFromSource, 103 | this.graph, 104 | this.parent, 105 | this.sourceVertex, 106 | this.visited 107 | ); 108 | } 109 | 110 | /** 111 | * @description Get the distance from the source vertex to the input vertex. 112 | * 113 | * Strategy: Access value in distanceFromSource object with vertex as key. 114 | * 115 | * Time complexity: O(1) 116 | * Space complexity: O(1) 117 | * 118 | * @param {String|Number} vertex - vertex whose distance from source vertex is sought 119 | * 120 | * @returns {Number} - distance from source vertex to input vertex 121 | */ 122 | distanceTo(vertex) { 123 | if (!this.hasPathTo(vertex)) { 124 | return null; 125 | } 126 | return this.distanceFromSource[vertex]; 127 | } 128 | 129 | /** 130 | * @description Check if input vertex is connected to the source vertex. 131 | * 132 | * Strategy: Check if vertex is in the visited Set. 133 | * 134 | * Time complexity: O(1) 135 | * Space complexity: O(1) 136 | * 137 | * @param {String|Number} vertex - vertex that may be connected to source vertex 138 | * 139 | * @returns {Boolean} - true if vertex is connected to source vertex 140 | */ 141 | hasPathTo(vertex) { 142 | if (!this.graph.adjacencyList.hasOwnProperty(vertex)) { 143 | throw new Error("The input vertex is not in the graph"); 144 | } 145 | 146 | // Stringify to allow the user to call this method with numbers 147 | return this.visited.has(String(vertex)); 148 | } 149 | 150 | /** 151 | * @description Get the shortest path from the source vertex to the input 152 | * vertex. 153 | * 154 | * Strategy: Traverse parent object until source vertex is reached. 155 | * 156 | * Time complexity: O(P), where P is path length 157 | * Space complexity: O(P), where P is path length 158 | * 159 | * @param {String|Number} destinationVertex - vertex whose path is sought from source vertex 160 | * 161 | * @returns {Array} - shortest path from destination to source 162 | */ 163 | shortestPathTo(destinationVertex) { 164 | if (!this.hasPathTo(destinationVertex)) { 165 | return null; 166 | } 167 | 168 | const path = []; 169 | for ( 170 | // Stringify to allow the user to call this method with numbers 171 | let vertex = String(destinationVertex); 172 | vertex !== this.sourceVertex; 173 | vertex = this.parent[vertex] 174 | ) { 175 | path.push(vertex); 176 | } 177 | 178 | path.push(this.sourceVertex); 179 | return path; 180 | } 181 | } 182 | 183 | module.exports = BreadthFirstPaths; 184 | -------------------------------------------------------------------------------- /processors/graph.unweighted.depth-first-paths.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method that finds paths from source vertex to other 3 | * connected vertices using depth-first search. 4 | * 5 | * Strategy: Loop through adjacent vertices. Only visit unvisited vertices. 6 | * Upon visit, add to visited Set, store parent information, and continue DFS. 7 | * 8 | * Time complexity: O(V + E), where V is total vertices 9 | * Space complexity: O(V), where V is total vertices 10 | * 11 | * @param {Graph} graph - graph being processed 12 | * @param {Object} parent - stores path information 13 | * @param {String} vertex - current vertex being traversed 14 | * @param {Set} visited - track which vertices have already been visited 15 | * 16 | * @private 17 | */ 18 | function depthFirstSearch(graph, parent, vertex, visited) { 19 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 20 | throw new Error("The input vertex is not in the graph"); 21 | } 22 | 23 | graph.adjacencyList[vertex].forEach(adjacentVertex => { 24 | if (visited.has(adjacentVertex)) { 25 | return; 26 | } 27 | visited.add(adjacentVertex); 28 | 29 | // Store path information for public methods to use later 30 | parent[adjacentVertex] = vertex; 31 | 32 | depthFirstSearch(graph, parent, adjacentVertex, visited); 33 | }); 34 | } 35 | 36 | /** 37 | * @description Find paths from source vertex to all other vertices. Note this 38 | * function mutates objects passed from the constructor and will only touch 39 | * vertices connected to the source vertex. 40 | * 41 | * Strategy: Mark the source as visited then begin depth-first search. 42 | * 43 | * Time complexity: O(V + E), where V is total vertices and E is total edges 44 | * Space complexity: O(V), where V is total vertices 45 | * 46 | * @param {Graph} graph - graph being processed 47 | * @param {Object} parent - stores path information 48 | * @param {String} sourceVertex - starting point for DFS 49 | * @param {Set} visited - track which vertices have already been visited 50 | * 51 | * @private 52 | */ 53 | function findPaths(graph, parent, sourceVertex, visited) { 54 | if (!graph) { 55 | throw new Error("The graph is not loaded"); 56 | } 57 | if (!graph.adjacencyList.hasOwnProperty(sourceVertex)) { 58 | throw new Error("The source vertex is not in the graph"); 59 | } 60 | 61 | visited.add(sourceVertex); 62 | 63 | depthFirstSearch(graph, parent, sourceVertex, visited); 64 | } 65 | 66 | /** Class representing depth-first path processor for unweighted graphs */ 67 | class DepthFirstPaths { 68 | /** 69 | * Works for both directed and undirected graphs. 70 | * 71 | * @constructor 72 | * 73 | * @param {Graph} graph - graph being processed 74 | * @param {String|Number} sourceVertex - gotta start somewhere! 75 | * 76 | * @property {Graph} graph - graph being processed 77 | * @property {Object} parent - stores path information 78 | * @property {String|Number} sourceVertex - starting point for DFS 79 | * @property {Set} visited - track which vertices have already been visited 80 | */ 81 | constructor(graph, sourceVertex) { 82 | this.graph = graph; 83 | this.parent = {}; 84 | this.sourceVertex = String(sourceVertex); 85 | this.visited = new Set(); 86 | 87 | findPaths(this.graph, this.parent, this.sourceVertex, this.visited); 88 | } 89 | 90 | /** 91 | * @description Check if input vertex is connected to the source vertex. 92 | * 93 | * Strategy: Check if vertex is in the visited Set. 94 | * 95 | * Time complexity: O(1) 96 | * Space complexity: O(1) 97 | * 98 | * @param {String|Number} vertex - vertex that may be connected to source vertex 99 | * 100 | * @returns {Boolean} - true if vertex is connected to source vertex 101 | */ 102 | hasPathTo(vertex) { 103 | if (!this.graph.adjacencyList.hasOwnProperty(vertex)) { 104 | throw new Error("The input vertex is not in the graph"); 105 | } 106 | 107 | // Stringify to allow the user to call this method with numbers 108 | return this.visited.has(String(vertex)); 109 | } 110 | 111 | /** 112 | * @description Get the path from the source vertex to the input vertex. 113 | * 114 | * Strategy: Traverse parent object until source vertex is reached. 115 | * 116 | * Time complexity: O(P), where P is path length 117 | * Space complexity: O(P), where P is path length 118 | * 119 | * @param {String|Number} destinationVertex - vertex whose path is sought from source vertex 120 | * 121 | * @returns {Array} - path from destination to source 122 | */ 123 | pathTo(destinationVertex) { 124 | if (!this.hasPathTo(destinationVertex)) { 125 | return null; 126 | } 127 | 128 | const path = []; 129 | for ( 130 | // Stringify to allow the user to call this method with numbers 131 | let vertex = String(destinationVertex); 132 | vertex !== this.sourceVertex; 133 | vertex = this.parent[vertex] 134 | ) { 135 | path.push(vertex); 136 | } 137 | 138 | path.push(this.sourceVertex); 139 | return path; 140 | } 141 | } 142 | 143 | module.exports = DepthFirstPaths; 144 | -------------------------------------------------------------------------------- /processors/graph.unweighted.directed.cycle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method that detects cycles using depth-first search. 3 | * 4 | * Strategy: Loop through adjacent vertices. If we see the same vertex twice 5 | * since the root DFS call, then there is a cycle. If a vertex was visited from 6 | * a prior call, then no need to DFS through it again. Upon visit, add to 7 | * visited Set and continue DFS. 8 | * 9 | * Time complexity: O(V + E), where V is total vertices and E is total edges 10 | * Space complexity: O(V), where V is total vertices 11 | * 12 | * @param {Graph} graph - graph being processed 13 | * @param {String|Number} vertex - current vertex being traversed 14 | * @param {Set} visited - vertices already visited 15 | * @param {Object} visitedThisPass - vertices visited since root DFS call 16 | * 17 | * @private 18 | */ 19 | function depthFirstSearch(graph, vertex, visited, visitedThisPass) { 20 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 21 | throw new Error("The input vertex is not in the graph"); 22 | } 23 | 24 | // for of loop used instead of forEach to allow early return that breaks loop 25 | for (const adjacentVertex of graph.adjacencyList[vertex]) { 26 | if (visitedThisPass.has(adjacentVertex)) { 27 | return true; 28 | } 29 | if (visited.has(adjacentVertex)) { 30 | continue; 31 | } 32 | 33 | visitedThisPass.add(adjacentVertex); 34 | visited.add(adjacentVertex); 35 | 36 | const hasCycle = depthFirstSearch( 37 | graph, 38 | adjacentVertex, 39 | visited, 40 | visitedThisPass 41 | ); 42 | 43 | if (hasCycle) { 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | /** 52 | * @description Check whether a cycle exists in the graph. 53 | * 54 | * Strategy: Loop through every vertex in the graph. If the vertex has not 55 | * already been visited, then call depth-first search on it. Track vertices 56 | * visited on each pass of DFS to detect if there is a cycle in that pass. If 57 | * a cycle is detected, return immediately. Otherwise, update the visited set 58 | * with all the values that were just visited on that DFS pass and continue 59 | * looping. 60 | * 61 | * Time complexity: O(V + E), where V is total vertices and E is total edges 62 | * Space complexity: O(V), where V is total vertices 63 | * 64 | * @param {Object} graph - graph being processed 65 | * 66 | * @returns {Boolean} - true if cycle found, otherwise false 67 | * 68 | * @private 69 | */ 70 | function hasCycle(graph) { 71 | if (!graph) { 72 | throw new Error("The graph is not loaded"); 73 | } 74 | 75 | const visited = new Set(); 76 | 77 | for (const vertex in graph.adjacencyList) { 78 | // Ignore prototype chain 79 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 80 | continue; 81 | } 82 | 83 | // No need to repeat visits since DFS would have detected cycle on that pass 84 | if (visited.has(vertex)) { 85 | continue; 86 | } 87 | 88 | // If DFS visits a vertex twice since the initial call, then there is a cycle 89 | const visitedThisPass = new Set(); 90 | visitedThisPass.add(vertex); 91 | 92 | const hasCycle = depthFirstSearch(graph, vertex, visited, visitedThisPass); 93 | 94 | if (hasCycle) { 95 | return true; 96 | } 97 | } 98 | 99 | return false; 100 | } 101 | 102 | /** Class representing cycle processor for unweighted directed graphs */ 103 | class DirectedCycle { 104 | /** 105 | * This processor recognizes self-loops and back edges as cycles. 106 | * 107 | * @constructor 108 | * 109 | * @param {Object} graph - graph being processed 110 | * 111 | * @property {Object} graph - graph being processed 112 | * @property {Boolean} checkCycle - true if cycle found, otherwise false 113 | */ 114 | constructor(graph) { 115 | this.graph = graph; 116 | this.checkCycle = hasCycle(graph); 117 | } 118 | 119 | /** 120 | * @description Check whether graph has a cycle. 121 | * 122 | * Strategy: Read checkCycle property. 123 | * 124 | * Time complexity: O(1) 125 | * Space complexity: O(1) 126 | * 127 | * @returns {Boolean} - true if cycle present, otherwise false 128 | */ 129 | hasCycle() { 130 | return this.checkCycle; 131 | } 132 | } 133 | 134 | module.exports = DirectedCycle; 135 | -------------------------------------------------------------------------------- /processors/graph.unweighted.directed.topological-sort.js: -------------------------------------------------------------------------------- 1 | const DirectedCycle = require("./graph.unweighted.directed.cycle"); 2 | 3 | /** 4 | * @description Private method that sorts vertices topologically using 5 | * depth-first search. 6 | * 7 | * Strategy: Use Tarjan's algorithm where each vertex is sorted after they 8 | * complete their depth-first search calls. Track visits with a set. 9 | * 10 | * Time complexity: O(V + E), where V is total vertices and E is total edges 11 | * Space complexity: O(V), where V is total vertices 12 | * 13 | * @param {Graph} graph - graph being processed 14 | * @param {Array} sorted - vertices in topological sort order 15 | * @param {String|Number} vertex - current vertex being traversed 16 | * @param {Set} visited - vertices already visited 17 | * 18 | * @private 19 | */ 20 | function depthFirstSearch(graph, sorted, vertex, visited) { 21 | visited.add(vertex); 22 | 23 | graph.adjacencyList[vertex].forEach(adjacentVertex => { 24 | if (visited.has(adjacentVertex)) { 25 | return; 26 | } 27 | 28 | depthFirstSearch(graph, sorted, adjacentVertex, visited); 29 | }); 30 | 31 | // Here is the magic! At the end of DFS the vertex becomes sorted. 32 | sorted.push(vertex); 33 | } 34 | 35 | /** 36 | * @description Get the topological sort for the input graph. 37 | * 38 | * Strategy: If the graph does not contain any cycles, then loop through every 39 | * vertex in the graph. If the vertex has not already been visited, then call 40 | * the private depth-first search method that will build up the sorted list. 41 | * 42 | * Time complexity: O(V + E), where V is total vertices and E is total edges 43 | * Space complexity: O(V), where V is total vertices 44 | * 45 | * @param {Object} graph - graph being processed 46 | * @param {Array} sorted - list that will contain vertices sorted topologically 47 | * 48 | * @private 49 | */ 50 | function initializeSort(graph, sorted) { 51 | if (!graph) { 52 | throw new Error("The graph is not loaded"); 53 | } 54 | 55 | // Use another processor to check if there is a cycle 56 | const hasCycle = new DirectedCycle(graph).hasCycle(); 57 | if (hasCycle) { 58 | throw new Error( 59 | "Cycle found. Topological sort requires no cycles in the graph." 60 | ); 61 | } 62 | 63 | const visited = new Set(); 64 | for (const vertex in graph.adjacencyList) { 65 | // Ignore prototype chain 66 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 67 | continue; 68 | } 69 | if (visited.has(vertex)) { 70 | continue; 71 | } 72 | 73 | depthFirstSearch(graph, sorted, vertex, visited); 74 | } 75 | } 76 | 77 | /** Class representing topological sort processor for unweighted directed acyclic graphs */ 78 | class TopologicalSort { 79 | /** 80 | * Tarjan's algorithm for DAGs. 81 | * 82 | * @constructor 83 | * 84 | * @param {Graph} graph - graph being processed 85 | * 86 | * @property {Graph} graph - graph being processed 87 | * @property {Array} sorted - list that will contain vertices sorted topologically 88 | */ 89 | constructor(graph) { 90 | this.graph = graph; 91 | this.sorted = []; 92 | 93 | initializeSort(this.graph, this.sorted); 94 | } 95 | 96 | /** 97 | * @description Get vertices sorted in topological order. 98 | * 99 | * Strategy: Read sorted property. 100 | * 101 | * Time complexity: O(1) 102 | * Space complexity: O(1) 103 | * 104 | * @returns {Array} - list of vertices topologically sorted 105 | */ 106 | getTopologicalOrder() { 107 | return this.sorted; 108 | } 109 | } 110 | 111 | module.exports = TopologicalSort; 112 | -------------------------------------------------------------------------------- /processors/graph.unweighted.undirected.connected-components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method that traverses graph using DFS to give each 3 | * vertex their component id. 4 | * 5 | * Strategy: Upon reaching a vertex, record their component id and mark them so 6 | * that they won't be visited again. Then loop through their adjacent vertices 7 | * and recurse only unvisited vertices. 8 | * 9 | * Edge case(s): Input vertex does not exist in graph 10 | * 11 | * Time complexity: O(V + E), where V is total vertices and E is total edges 12 | * Space complexity: O(V), where V is total vertices 13 | * 14 | * @param {Number} componentCount - track how many components have been created 15 | * @param {Graph} graph - graph being processed 16 | * @param {Object} id - key is vertex and value is id representing its component 17 | * @param {String|Number} vertex - current vertex being traversed 18 | * @param {Set} visited - track which vertices have already been visited 19 | * 20 | * @private 21 | */ 22 | function depthFirstSearch(componentCount, graph, id, vertex, visited) { 23 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 24 | throw new Error("The input vertex is not in the graph"); 25 | } 26 | 27 | id[vertex] = componentCount; 28 | visited.add(vertex); 29 | 30 | graph.adjacencyList[vertex].forEach(adjacentVertex => { 31 | if (visited.has(adjacentVertex)) { 32 | return; 33 | } 34 | depthFirstSearch(componentCount, graph, id, adjacentVertex, visited); 35 | }); 36 | } 37 | 38 | /** 39 | * @description Count number of connected components in graph. 40 | * 41 | * Strategy: Loop through every vertex in the graph calling depth-first search 42 | * if the vertex has not been visited. After every search, increment the 43 | * total number of components to use as id. 44 | * 45 | * NOTE: I'm not a fan of this function both getting and setting simultaneously. 46 | * However, it keeps the ConnectedComponents constructor clean. 47 | * 48 | * Time complexity: O(V + E), where V is total vertices and E is total edges 49 | * Space complexity: O(V), where V is total vertices 50 | * 51 | * @param {Graph} graph - graph being processed 52 | * @param {Object} id - key is vertex and value is id representing its component 53 | * 54 | * @returns {Number} - total number of components 55 | * 56 | * @private 57 | */ 58 | function countComponents(graph, id) { 59 | if (!graph) { 60 | throw new Error("The graph is not loaded"); 61 | } 62 | 63 | const visited = new Set(); 64 | let componentCount = 0; 65 | 66 | for (const vertex in graph.adjacencyList) { 67 | // Ignore prototype chain 68 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 69 | continue; 70 | } 71 | 72 | if (visited.has(vertex)) { 73 | continue; 74 | } 75 | 76 | depthFirstSearch(componentCount, graph, id, vertex, visited); 77 | 78 | componentCount++; 79 | } 80 | 81 | return componentCount; 82 | } 83 | 84 | /** Class representing connection processor for unweighted undirected graphs */ 85 | class ConnectedComponents { 86 | /** 87 | * A connected component is a set of vertices whose edges connect them. If 88 | * you can traverse from one vertex to another by traveling along edges, 89 | * then they are part of the same component. 90 | * 91 | * @constructor 92 | * 93 | * @param {Graph} graph - graph being processed 94 | * 95 | * @property {Graph} graph - graph being processed 96 | * @property {Object} id - key is vertex and value is id representing its component 97 | * @property {Number} componentCount - number of components in graph 98 | */ 99 | constructor(graph) { 100 | this.graph = graph; 101 | this.id = {}; 102 | 103 | // NOTE: this also mutates the id object 104 | this.componentCount = countComponents(graph, this.id); 105 | } 106 | 107 | /** 108 | * @description Get number of components in graph. 109 | * 110 | * Strategy: Use component count property. 111 | * 112 | * Time complexity: O(1) 113 | * Space complexity: O(1) 114 | * 115 | * @returns {Number} - number of components in graph 116 | */ 117 | getComponentCount() { 118 | return this.componentCount; 119 | } 120 | 121 | /** 122 | * @description Get id of component that vertex belongs to. 123 | * 124 | * Strategy: Use id object. 125 | * 126 | * Time complexity: O(1) 127 | * Space complexity: O(1) 128 | * 129 | * @param {String|Number} vertex - vertex whose component id is sought 130 | * 131 | * @returns {Number} - id of component that vertex belongs to 132 | */ 133 | getComponentId(vertex) { 134 | if (!this.graph.adjacencyList.hasOwnProperty(vertex)) { 135 | throw new Error("The input vertex is not in the graph"); 136 | } 137 | 138 | return this.id[vertex]; 139 | } 140 | } 141 | 142 | module.exports = ConnectedComponents; 143 | -------------------------------------------------------------------------------- /processors/graph.unweighted.undirected.cycle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method that detects cycles using depth-first search. 3 | * 4 | * Strategy: Loop through adjacent vertices. If we see the same vertex twice 5 | * (not including parent of vertex in each DFS call), then there is a cycle. 6 | * Track visits using a set. 7 | * 8 | * Time complexity: O(V + E), where V is total vertices and E is total edges 9 | * Space complexity: O(V), where V is total vertices 10 | * 11 | * @param {Graph} graph - graph being processed 12 | * @param {String} parent - parent of the vertex whose adjacency list is looped 13 | * @param {String|Number} vertex - current vertex being traversed 14 | * @param {Set} visited - vertices already visited 15 | * 16 | * @private 17 | */ 18 | function depthFirstSearch(graph, parent, vertex, visited) { 19 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 20 | throw new Error("The input vertex is not in the graph"); 21 | } 22 | 23 | for (const adjacentVertex of graph.adjacencyList[vertex]) { 24 | if (visited.has(adjacentVertex)) { 25 | // Ignore backedges. Without this, a single edge is considered a cycle. 26 | if (adjacentVertex === parent) { 27 | continue; 28 | } 29 | 30 | // If already visited and NOT the parent, the graph has a cycle! 31 | return true; 32 | } 33 | 34 | visited.add(adjacentVertex); 35 | 36 | const hasCycle = depthFirstSearch(graph, vertex, adjacentVertex, visited); 37 | 38 | if (hasCycle) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | /** 47 | * @description Private method that detects parallel edges. 48 | * 49 | * Strategy: Loop through adjacency lists looking for duplicates. 50 | * 51 | * Time complexity: O(V + E), where V is total vertices and E is total edges 52 | * Space complexity: O(A), where A is longest adjacent list 53 | * 54 | * @param {Graph} graph - graph being processed 55 | * 56 | * @returns {Boolean} - true if parallel edges found, otherwise false 57 | * 58 | * @private 59 | */ 60 | function hasParallelEdges(graph) { 61 | for (const vertex in graph.adjacencyList) { 62 | // Ignore prototype chain 63 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 64 | continue; 65 | } 66 | 67 | const adjacentVertices = graph.adjacencyList[vertex]; 68 | const visited = new Set(); 69 | 70 | for (const adjacentVertex of adjacentVertices) { 71 | if (visited.has(adjacentVertex)) { 72 | return true; 73 | } 74 | visited.add(adjacentVertex); 75 | } 76 | } 77 | 78 | return false; 79 | } 80 | 81 | /** 82 | * @description Private method that detects self loops. 83 | * 84 | * Strategy: Loop through adjacency lists looking for values that match the 85 | * parent. 86 | * 87 | * Time complexity: O(V + E), where V is total vertices and E is total edges 88 | * Space complexity: O(1) 89 | * 90 | * @param {Graph} graph - graph being processed 91 | * 92 | * @returns {Boolean} - true if self loop found, otherwise false 93 | * 94 | * @private 95 | */ 96 | function hasSelfLoop(graph) { 97 | for (const vertex in graph.adjacencyList) { 98 | // Ignore prototype chain 99 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 100 | continue; 101 | } 102 | const adjacentVertices = graph.adjacencyList[vertex]; 103 | for (const adjacentVertex of adjacentVertices) { 104 | if (vertex === adjacentVertex) { 105 | return true; 106 | } 107 | } 108 | } 109 | 110 | return false; 111 | } 112 | 113 | /** 114 | * @description Check whether a cycle exists in the graph. 115 | * 116 | * Strategy: Check for self loops and parallel edges first, since those are 117 | * considered cycles. If none, loop through every vertex in the graph. Track 118 | * visits with a set. Call depth-first search on vertices that have not 119 | * already been visited. If DFS ever returns true, return true immediately. 120 | * 121 | * Time complexity: O(V + E), where V is total vertices and E is total edges 122 | * Space complexity: O(V) 123 | * 124 | * @param {Object} graph - graph being processed 125 | * 126 | * @returns {Boolean} - true if cycle found, otherwise false 127 | * 128 | * @private 129 | */ 130 | function hasCycle(graph) { 131 | if (!graph) { 132 | throw new Error("The graph is not loaded"); 133 | } 134 | if (hasSelfLoop(graph) || hasParallelEdges(graph)) { 135 | return true; 136 | } 137 | 138 | const visited = new Set(); 139 | for (const vertex in graph.adjacencyList) { 140 | // Ignore prototype chain 141 | if (!graph.adjacencyList.hasOwnProperty(vertex)) { 142 | continue; 143 | } 144 | 145 | if (visited.has(vertex)) { 146 | continue; 147 | } 148 | visited.add(vertex); 149 | 150 | const hasCycle = depthFirstSearch(graph, null, vertex, visited); 151 | 152 | if (hasCycle) { 153 | return true; 154 | } 155 | } 156 | 157 | return false; 158 | } 159 | 160 | /** Class representing cycle processor for unweighted undirected graphs */ 161 | class UndirectedCycle { 162 | /** 163 | * @constructor 164 | * 165 | * @param {Object} graph - graph being processed 166 | * 167 | * @property {Object} graph - graph being processed 168 | * @property {Boolean} checkCycle - true if cycle found, otherwise false 169 | */ 170 | constructor(graph) { 171 | this.graph = graph; 172 | this.checkCycle = hasCycle(graph); 173 | } 174 | 175 | /** 176 | * @description Check whether graph has a cycle. 177 | * 178 | * Strategy: Read checkCycle property. 179 | * 180 | * Time complexity: O(1) 181 | * Space complexity: O(1) 182 | * 183 | * @returns {Boolean} - true if cycle present, otherwise false 184 | */ 185 | hasCycle() { 186 | return this.checkCycle; 187 | } 188 | } 189 | 190 | module.exports = UndirectedCycle; 191 | -------------------------------------------------------------------------------- /structures/graph.edge-weighted.directed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class representing a weighted edge 3 | * @private 4 | */ 5 | class Edge { 6 | /** 7 | * Helper class to create weighted edges for Graph methods. 8 | * 9 | * Stringify vertices to avoid equality comparison issues against stringified 10 | * keys in adjacency list. 11 | * 12 | * @constructor 13 | * 14 | * @param {String|Number} v1 - vertex pointing to v2 15 | * @param {String|Number} v2 - vertex pointed at from v1 16 | * @param {Number} weight - weight of edge between v1 and v2 17 | * 18 | * @property {String} v1 - vertex pointing to v2 19 | * @property {String} v2 - vertex pointed at from v1 20 | * @property {Number} weight - weight of edge between v1 and v2 21 | */ 22 | constructor([v1, v2, weight]) { 23 | this.v1 = String(v1); 24 | this.v2 = String(v2); 25 | this.weight = weight; 26 | } 27 | 28 | /** 29 | * @description Return vertex where directed edge begins. 30 | * 31 | * @returns {String} - first vertex 32 | */ 33 | from() { 34 | return this.v1; 35 | } 36 | 37 | /** 38 | * @description Return vertex where directed edge ends. 39 | * 40 | * @returns {String} - other vertex 41 | */ 42 | to() { 43 | return this.v2; 44 | } 45 | } 46 | 47 | /** 48 | * @description Private method called in buildGraph function to add new edges to 49 | * the graph. Note this method mutates the graph. 50 | * 51 | * Strategy: Access first vertex in graph object and add edge to its adjacency 52 | * list. 53 | * 54 | * Time complexity: O(1) 55 | * Space complexity: O(1) 56 | * 57 | * @param {Object} adjacencyList - the graph object representation 58 | * @param {Object} edge - instance of Edge class with v1, v2 and weight 59 | * 60 | * @private 61 | */ 62 | function addEdge(adjacencyList, edge) { 63 | adjacencyList[edge.v1].push(edge); 64 | } 65 | 66 | /** 67 | * @description Private method called in buildGraph function to add new vertices 68 | * to the graph. Note this method mutates the graph. 69 | * 70 | * Strategy: In graph object, keys are vertices and values are arrays. 71 | * 72 | * Time complexity: O(1) 73 | * Space complexity: O(1) 74 | * 75 | * @param {Object} adjacencyList - the graph object representation 76 | * @param {String|Number} vertex - vertex to be added 77 | * 78 | * @private 79 | */ 80 | function addVertex(adjacencyList, vertex) { 81 | adjacencyList[vertex] = []; 82 | } 83 | 84 | /** 85 | * @description Private method called in constructor to deliver initial values 86 | * for graph. 87 | * 88 | * Strategy: Loop through edges array. At each iteration, add the edge to the 89 | * graph and any new vertices encountered. Update metrics when adding. 90 | * 91 | * Time complexity: O(N) 92 | * Space complexity: O(N) 93 | * 94 | * @param {Array=} edges - array of subarrays containing pairs of vertices [v1, v2] 95 | * 96 | * @returns {Object} - initial values for Graph instance 97 | * 98 | * @private 99 | */ 100 | function buildGraph(edges = []) { 101 | const adjacencyList = {}; 102 | let totalEdges = 0; 103 | let totalVertices = 0; 104 | 105 | edges.forEach(edge => { 106 | const [v1, v2] = edge; 107 | 108 | if (!adjacencyList.hasOwnProperty(v1)) { 109 | addVertex(adjacencyList, v1); 110 | totalVertices++; 111 | } 112 | if (!adjacencyList.hasOwnProperty(v2)) { 113 | addVertex(adjacencyList, v2); 114 | totalVertices++; 115 | } 116 | 117 | const edgeInstance = new Edge(edge); 118 | addEdge(adjacencyList, edgeInstance); 119 | totalEdges++; 120 | }); 121 | 122 | return { adjacencyList, totalEdges, totalVertices }; 123 | } 124 | 125 | /** Class representing a weighted graph with directed edges */ 126 | class EdgeWeightedDirectedGraph { 127 | /** 128 | * The graph is represented as an object-oriented adjacency list. Keys are 129 | * vertices and values are arrays of Edge objects. Each edge contains both 130 | * vertices and the weight of the connection. Vertex inputs are stringified in 131 | * all methods, so numbers (or any primitive data type) can be used as 132 | * vertices and will be represented as strings. 133 | * 134 | * Self-loops and parallel edges are allowed. 135 | * 136 | * To build the graph, instantiate with an array of edges or call addEdge 137 | * manually with all edge pairs. The addEdge method will dynamically add 138 | * vertices to the adjacency list if they did not exist prior. 139 | * 140 | * @constructor 141 | * 142 | * @param {Array=} edges - array of subarrays containing pairs of vertices [v1, v2] 143 | * 144 | * @property {Object} adjacencyList - the graph itself 145 | * @property {Number} totalVertices - incremented when a vertex is added 146 | * @property {Number} totalEdges - incremented when an edge is added 147 | */ 148 | constructor(edges = []) { 149 | const { adjacencyList, totalEdges, totalVertices } = buildGraph(edges); 150 | this.adjacencyList = adjacencyList; 151 | this.totalEdges = totalEdges; 152 | this.totalVertices = totalVertices; 153 | } 154 | 155 | /** 156 | * @description Add a new directed edge to the graph. Dynamically add vertices 157 | * if they did not exist prior. 158 | * 159 | * Strategy: Add vertices to the graph if they don't already exist. Then push 160 | * edge into first input vertex's adjacency list and increment edge count. 161 | * 162 | * Time complexity: O(1) 163 | * Space complexity: O(1) 164 | * 165 | * @param {Array} v1, v2, weight - directed edge created where v1 -> v2 166 | */ 167 | addEdge([v1, v2, weight]) { 168 | if (!this.adjacencyList.hasOwnProperty(v1)) { 169 | this.addVertex(v1); 170 | } 171 | if (!this.adjacencyList.hasOwnProperty(v2)) { 172 | this.addVertex(v2); 173 | } 174 | 175 | const edge = new Edge([v1, v2, weight]); 176 | 177 | this.adjacencyList[v1].push(edge); 178 | this.totalEdges++; 179 | } 180 | 181 | /** 182 | * @description Add a new vertex to the graph. 183 | * 184 | * Strategy: If vertex is not already in adjacency list, then store with 185 | * key as vertex and value as empty array. Increment total vertices. 186 | * 187 | * Edge case: throw error if vertex already exists 188 | * 189 | * Time complexity: O(1) 190 | * Space complexity: O(1) 191 | * 192 | * @param {String|Number} vertex - vertex added to graph 193 | */ 194 | addVertex(vertex) { 195 | if (this.adjacencyList.hasOwnProperty(vertex)) { 196 | throw new Error("That node already exists in the graph"); 197 | } 198 | 199 | this.adjacencyList[vertex] = []; 200 | this.totalVertices++; 201 | } 202 | 203 | /** 204 | * @description Get all vertices adjacent to input vertex. 205 | * 206 | * Strategy: Use adjacency list for instant lookup. 207 | * 208 | * Edge case: vertex does not exist in graph 209 | * 210 | * Time complexity: O(1) 211 | * Space complexity: O(1) 212 | * 213 | * @param {String|Number} vertex - vertex with potential adjacent vertices 214 | * 215 | * @returns {Array} - list of vertices adjacent to input vertex 216 | */ 217 | adjacentVertices(vertex) { 218 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 219 | throw new Error("That vertex does not exist in the graph"); 220 | } 221 | 222 | return this.adjacencyList[vertex]; 223 | } 224 | } 225 | 226 | module.exports = EdgeWeightedDirectedGraph; 227 | -------------------------------------------------------------------------------- /structures/graph.unweighted.undirected.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method called in buildGraph function to add new edges to 3 | * the graph. Note this method mutates the graph. 4 | * 5 | * Strategy: Add each vertex to the other's adjacency list as a string. 6 | * 7 | * Time complexity: O(1) 8 | * Space complexity: O(1) 9 | * 10 | * @param {Object} adjacencyList - the graph object representation 11 | * @param {Array} v1, v2 - vertices sharing new edge 12 | * 13 | * @private 14 | */ 15 | function addEdge(adjacencyList, [v1, v2]) { 16 | adjacencyList[v1].push(String(v2)); 17 | adjacencyList[v2].push(String(v1)); 18 | } 19 | 20 | /** 21 | * @description Private method called in buildGraph function to add new vertices 22 | * to the graph. Note this method mutates the graph. 23 | * 24 | * Strategy: In graph object, keys are vertices and values are arrays. 25 | * 26 | * Time complexity: O(1) 27 | * Space complexity: O(1) 28 | * 29 | * @param {Object} adjacencyList - the graph object representation 30 | * @param {String|Number} vertex - vertex to be added 31 | * 32 | * @private 33 | */ 34 | function addVertex(adjacencyList, vertex) { 35 | adjacencyList[vertex] = []; 36 | } 37 | 38 | /** 39 | * @description Private method called in constructor to deliver initial values 40 | * for graph. 41 | * 42 | * Strategy: Loop through edges array. At each iteration, add the edge to the 43 | * graph and any new vertices encountered. Update metrics when adding. 44 | * 45 | * Time complexity: O(N) 46 | * Space complexity: O(N) 47 | * 48 | * @param {Array=} edges - array of subarrays containing pairs of vertices [v1, v2] 49 | * 50 | * @returns {Object} - initial values for Graph instance 51 | * 52 | * @private 53 | */ 54 | function buildGraph(edges = []) { 55 | const adjacencyList = {}; 56 | let totalEdges = 0; 57 | let totalVertices = 0; 58 | 59 | edges.forEach(edge => { 60 | const [v1, v2] = edge; 61 | 62 | if (!adjacencyList.hasOwnProperty(v1)) { 63 | addVertex(adjacencyList, v1); 64 | totalVertices++; 65 | } 66 | if (!adjacencyList.hasOwnProperty(v2)) { 67 | addVertex(adjacencyList, v2); 68 | totalVertices++; 69 | } 70 | 71 | addEdge(adjacencyList, edge); 72 | totalEdges++; 73 | }); 74 | 75 | return { adjacencyList, totalEdges, totalVertices }; 76 | } 77 | 78 | /** Class representing a graph with undirected edges */ 79 | class UndirectedGraph { 80 | /** 81 | * Edges are represented in an object-oriented adjacency list where keys are 82 | * the vertices and values are arrays of adjacent vertices. Vertex inputs are 83 | * stringified in all methods, so numbers (or any primitive data type) can be 84 | * used as vertices and will be represented as strings. 85 | * 86 | * This graph allows self-loops and parallel edges. 87 | * 88 | * To build the graph, instantiate with an array of edges or call addEdge 89 | * manually with all edge pairs. The addEdge method will dynamically add 90 | * vertices to the adjacency list if they did not exist prior. 91 | * 92 | * @constructor 93 | * 94 | * @param {Array=} edges - array of subarrays containing pairs of vertices [v1, v2] 95 | * 96 | * @property {Object} adjacencyList - the graph itself 97 | * @property {Number} totalVertices - incremented when a vertex is added 98 | * @property {Number} totalEdges - incremented when an edge is added 99 | */ 100 | constructor(edges = []) { 101 | const { adjacencyList, totalEdges, totalVertices } = buildGraph(edges); 102 | this.adjacencyList = adjacencyList; 103 | this.totalEdges = totalEdges; 104 | this.totalVertices = totalVertices; 105 | } 106 | 107 | /** 108 | * @description Add a new edge to the graph. Also add vertices if they did not 109 | * exist prior. 110 | * 111 | * Strategy: Add vertices if they don't exist. Then push vertices to each 112 | * other's adjacency lists. Stringify second input vertex to avoid equality 113 | * comparison issues against stringified keys in other methods. 114 | * 115 | * Edge case: if vertices are the same (i.e. self-loop), add both to same 116 | * adjacency list. This maintains mathematical consistency whereby each edge 117 | * represents two values in adjacency list, providing convenient calculations 118 | * for methods like averageDegree. 119 | * 120 | * Time complexity: O(1) 121 | * Space complexity: O(1) 122 | * 123 | * @param {Array} v1, v2 - vertices sharing new edge 124 | */ 125 | addEdge([v1, v2]) { 126 | if (!this.adjacencyList.hasOwnProperty(v1)) { 127 | this.addVertex(v1); 128 | } 129 | if (!this.adjacencyList.hasOwnProperty(v2)) { 130 | this.addVertex(v2); 131 | } 132 | this.adjacencyList[v1].push(String(v2)); 133 | this.adjacencyList[v2].push(String(v1)); 134 | this.totalEdges++; 135 | } 136 | 137 | /** 138 | * @description Add a new vertex to the graph. 139 | * 140 | * Strategy: If vertex is not already in adjacency list, then store with 141 | * key as vertex and value as empty array. Array data type allows multiple 142 | * self-loops and parallel edges, unlike a Set. This trades functionality 143 | * for time complexity in methods seeking a specific edge. 144 | * 145 | * Edge case: throw error if vertex already exists 146 | * 147 | * Time complexity: O(1) 148 | * Space complexity: O(1) 149 | * 150 | * @param {String|Number} vertex - vertex added to graph 151 | */ 152 | addVertex(vertex) { 153 | if (this.adjacencyList.hasOwnProperty(vertex)) { 154 | throw new Error("That node already exists in the graph"); 155 | } 156 | 157 | this.adjacencyList[vertex] = []; 158 | this.totalVertices++; 159 | } 160 | 161 | /** 162 | * @description Get all vertices adjacent to input vertex. 163 | * 164 | * Strategy: Use adjacent list for instant lookup. 165 | * 166 | * Edge case: vertex does not exist in graph 167 | * 168 | * Time complexity: O(1) 169 | * Space complexity: O(1) 170 | * 171 | * @param {String|Number} vertex - vertex with potential adjacent vertices 172 | * @returns {Array} - list of vertices adjacent to input vertex 173 | */ 174 | adjacentVertices(vertex) { 175 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 176 | throw new Error("That vertex does not exist in the graph"); 177 | } 178 | 179 | return this.adjacencyList[vertex]; 180 | } 181 | 182 | /** 183 | * @description Get the average degree of the graph. 184 | * 185 | * Strategy: Since each edge adds 2 degrees in adjacency list (including 186 | * self-loops), doubling the number of edges represents total number of 187 | * degrees in the adjacency list. Divide this by the number of vertices to get 188 | * the average degree. 189 | * 190 | * Time complexity: O(1) 191 | * Space complexity: O(1) 192 | * 193 | * @returns {Number} - average degree of graph 194 | */ 195 | averageDegree() { 196 | // Cannot divide by 0 197 | return this.totalVertices === 0 198 | ? 0 199 | : (2 * this.totalEdges) / this.totalVertices; 200 | } 201 | 202 | /** 203 | * @description Get number of vertices adjacent to input vertex. 204 | * 205 | * Strategy: Adjacent vertices are stored in an array, so use length property. 206 | * 207 | * Edge case: vertex does not exist in graph 208 | * 209 | * Time complexity: O(1) 210 | * Space complexity: O(1) 211 | * 212 | * @param {String|Number} vertex - vertex whose degree is sought 213 | * @returns {Number} - degree of vertex 214 | */ 215 | degree(vertex) { 216 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 217 | throw new Error("That vertex does not exist in the graph"); 218 | } 219 | 220 | return this.adjacencyList[vertex].length; 221 | } 222 | 223 | /** 224 | * @description Get the highest degree in the graph. 225 | * 226 | * Strategy: Loop through each vertex with a for in loop. Calculate each 227 | * vertex's degree, then update max if it's the highest degree so far. 228 | * 229 | * Time complexity: O(V), where V is total vertices 230 | * Space complexity: O(1) 231 | * 232 | * @returns {Number} - largest degree in graph 233 | */ 234 | maxDegree() { 235 | let max = 0; 236 | for (const vertex in this.adjacencyList) { 237 | // Ignore prototype chain 238 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 239 | continue; 240 | } 241 | 242 | const degree = this.degree(vertex); 243 | if (degree > max) { 244 | max = degree; 245 | } 246 | } 247 | 248 | return max; 249 | } 250 | 251 | /** 252 | * @description Get the number of self loops in the graph. 253 | * 254 | * Strategy: Loop through each vertex's adjacent vertices and increment a 255 | * counter for all duplicates. Then divide that counter by 2 because the 256 | * addEdge implementation pushes 2 copies of the vertex to its own adjacency 257 | * list. 258 | * 259 | * Time complexity: O(V + E) where V is total vertices and E is total edges 260 | * Space complexity: O(1) 261 | * 262 | * @returns {Number} - number of self loops, as you might have guessed! 263 | */ 264 | numberOfSelfLoops() { 265 | let count = 0; 266 | for (const vertex in this.adjacencyList) { 267 | // Ignore prototype chain 268 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 269 | continue; 270 | } 271 | 272 | this.adjacencyList[vertex].forEach(adjacentVertex => { 273 | if (vertex === adjacentVertex) { 274 | count++; 275 | } 276 | }); 277 | } 278 | 279 | return count / 2; 280 | } 281 | } 282 | 283 | module.exports = UndirectedGraph; 284 | -------------------------------------------------------------------------------- /structures/hash-table.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Convert string to index between 0 and size (exclusive). Used in 3 | * all public HashTable methods. 4 | * 5 | * Time complexity: O(1) 6 | * Space complexity: O(1) 7 | * 8 | * @param {String} string - input that will be hashed 9 | * @param {Number} size - size of hash table 10 | * 11 | * @returns {Number} - index that will be used as the address in the hash table 12 | * 13 | * @private 14 | */ 15 | function hashCode(string, size) { 16 | let hash = 0; 17 | for (let i = 0; i < string.length; i++) { 18 | hash = ((hash << 5) - hash) + string.charCodeAt(i); 19 | 20 | // Convert to 32-bit integer 21 | hash &= hash; 22 | } 23 | 24 | return Math.abs(hash) % size ; 25 | } 26 | 27 | /** 28 | * @description Double or halve size of hash table. Load factor is 75%, which 29 | * trades minimal wasted space for minimal time dealing with collisions. 30 | * 31 | * Strategy: Loop through old storage looking for buckets. Then loop through 32 | * keys to insert key-value pairs into new storage using public insert method. 33 | * 34 | * Time complexity: O(M * N), where M is depth of bucket and N is size of array 35 | * Space complexity: O(M * N), where M is depth of bucket and N is size of array 36 | * NOTE: Average time/space with decent hash distributions is proportional to N 37 | * 38 | * @param {Array} storage - old storage array that is being resized 39 | * @param {Number} multiplier - 2 to double or 0.5 to cut in half 40 | * @param {Function} insertMethod - give access to table's insert method 41 | * 42 | * @returns {Array} - resized hash table 43 | * 44 | * @private 45 | */ 46 | function resize(storage, multiplier, insertMethod) { 47 | const newSize = storage.length * multiplier; 48 | const newStorage = new Array(newSize); 49 | 50 | storage.forEach(bucket => { 51 | if (bucket !== undefined) { 52 | Object.keys(bucket).forEach(key => { 53 | insertMethod(key, bucket[key], newSize, newStorage); 54 | }); 55 | } 56 | }); 57 | 58 | return newStorage; 59 | } 60 | 61 | /** Class representing a Hash Table */ 62 | class HashTable { 63 | /** 64 | * Collisions handled with chaining. Values are added to storage property. 65 | * Entries property represents number of items currently stored. 66 | * 67 | * @constructor 68 | * 69 | * @param {Number=} size - number of buckets available in array 70 | * 71 | * @property {Number} entries - how many entries have been added to hash table 72 | * @property {Number} size - current length of storage array 73 | * @property {Array} storage - the hash table itself 74 | */ 75 | constructor(size = 16) { 76 | this.entries = 0; 77 | this.size = size; 78 | this.storage = new Array(size); 79 | } 80 | 81 | /** 82 | * @description Insert key-value pair into hash table. Resize if insertion 83 | * crosses 75% entry threshold. If the same key is used multiple times, values 84 | * are overwritten so that only the latest insertion value will be held. 85 | * 86 | * Strategy: Hash key to deliver index where key-value pair will be inserted. 87 | * If that space is undefined, create object. Set key-value pair in 88 | * object at that address. 89 | * 90 | * Time complexity: O(1) amortized 91 | * Space complexity: O(1) amortized 92 | * 93 | * @param {String} key - key that will be hashed into an index 94 | * @param {*} value - value to be inserted with its key 95 | * @param {Number} size - size of storage where key-value pair is inserted 96 | * @param {Array} storage - storage where key-value pair is inserted 97 | */ 98 | insert(key, value, size = this.size, storage = this.storage) { 99 | /* Do not allow resize to reference 'this' keyword, which typically points 100 | at hash table. In production, may need to change 'this' to window or 101 | environment. Or could simply copy/paste insertion logic into resize. 102 | */ 103 | if (this !== undefined) { 104 | this.entries++; 105 | const CUT_OFF = 0.75 * size; 106 | const MULTIPLIER = 2; 107 | if (this.entries > CUT_OFF) { 108 | this.storage = resize(storage, MULTIPLIER, this.insert); 109 | this.size = size * MULTIPLIER; 110 | } 111 | } 112 | 113 | const address = hashCode(key, size); 114 | if (storage[address] === undefined) { storage[address] = {}; } 115 | storage[address][key] = value; 116 | } 117 | 118 | /** 119 | * @description Retrieve value associated with given key in hash table. 120 | * 121 | * Strategy: Hash key to deliver index for bucket with the key. Access the 122 | * value using object property access. 123 | * 124 | * Edge case(s): Check if bucket is undefined to avoid TypeError. 125 | * 126 | * Time complexity: O(1) 127 | * Space complexity: O(1) 128 | * 129 | * @param {String} key 130 | * 131 | * @returns {*} - value found with key, or undefined if key not found 132 | */ 133 | get(key) { 134 | const address = hashCode(key, this.size); 135 | if (this.storage[address] === undefined) { return undefined; } 136 | return this.storage[address][key]; 137 | } 138 | 139 | /** 140 | * @description Remove key-value pair from hash table. Resize if removal 141 | * crosses 25% entry threshold. 142 | * 143 | * Strategy: Hash key to deliver index where key-value pair will be removed. 144 | * Store value to return, then delete the key-value pair from the table. 145 | * 146 | * Edge case(s): Key does not exist in hash table 147 | * 148 | * Time complexity: O(1) amortized 149 | * Space complexity: O(1) amortized 150 | * 151 | * @param {String} key - key that will be hashed into an index and removed 152 | * 153 | * @returns {*} - value removed, or undefined if key not found 154 | */ 155 | remove(key) { 156 | this.entries--; 157 | const CUT_OFF = 0.25 * this.size; 158 | const MULTIPLIER = 0.5; 159 | if (this.entries < CUT_OFF) { 160 | this.storage = resize(this.storage, MULTIPLIER, this.insert); 161 | this.size *= MULTIPLIER; 162 | } 163 | 164 | const address = hashCode(key, this.size); 165 | if (this.storage[address] === undefined) { return undefined; } 166 | const removed = this.storage[address][key]; 167 | delete this.storage[address][key]; 168 | return removed; 169 | } 170 | } 171 | 172 | module.exports = HashTable; 173 | -------------------------------------------------------------------------------- /structures/linked-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class representing a node 3 | * @private 4 | */ 5 | class Node { 6 | /** 7 | * Quickly create nodes for linked lists 8 | * 9 | * @constructor 10 | * 11 | * @param {*} value - value held by node 12 | * 13 | * @property {*} value - value held by node 14 | * @property {Object|Null} next - point to next node in list 15 | */ 16 | constructor(value) { 17 | this.value = value; 18 | this.next = null; 19 | } 20 | } 21 | 22 | /** Class representing a linked list */ 23 | class LinkedList { 24 | /** 25 | * Track head, tail and key features of linked list. Provides access to tail 26 | * so tail operations can occur in constant time. Provides size property for 27 | * constant time access. 28 | * 29 | * @constructor 30 | * 31 | * @property {Object|Null} head - first node in linked list 32 | * @property {Object|Null} tail - last node in linked list 33 | * @property {Number} size - length of linked list 34 | */ 35 | constructor() { 36 | this.head = null; 37 | this.tail = null; 38 | this.size = 0; 39 | } 40 | 41 | /** 42 | * @description Add nodes with given value to end of list. 43 | * 44 | * Strategy: Since we have access to tail, append newly created Node to tail 45 | * and move tail pointer to new node. 46 | * 47 | * Edge case(s): empty list, inappropriate inputs 48 | * 49 | * Time complexity: O(1) 50 | * Space complexity: O(1) 51 | * 52 | * @param {*} value - value to be inserted into new node 53 | */ 54 | push(value) { 55 | if (value === null || value === undefined) { 56 | throw new Error( 57 | "This Linked List does not allow empty values like null and undefined" 58 | ); 59 | } 60 | 61 | const node = new Node(value); 62 | this.size++; 63 | 64 | // Edge case: empty list 65 | if (this.head === null) { 66 | this.head = this.tail = node; 67 | return; 68 | } 69 | 70 | this.tail.next = node; 71 | this.tail = node; 72 | } 73 | 74 | /** 75 | * @description Check if node with given value exists in list. 76 | * 77 | * Strategy: Loop through list checking if value of any node equals input 78 | * value. End loop when we reach the last node. 79 | * 80 | * Time complexity: O(N) 81 | * Space complexity: O(1) 82 | * 83 | * @param {*} value - checked if exists in list 84 | * 85 | * @returns {Boolean} - whether or not value exists in list 86 | */ 87 | 88 | contains(value) { 89 | let curr = this.head; 90 | while (curr !== null) { 91 | if (curr.value === value) { 92 | return true; 93 | } 94 | curr = curr.next; 95 | } 96 | 97 | return false; 98 | } 99 | 100 | /** 101 | * @description Remove first node with given value from list. 102 | * 103 | * Strategy: Loop through LL tracking previous and current nodes so you can 104 | * remove reference to target node by pointing prev's next at current's next. 105 | * 106 | * Edge case(s): empty list, one node, remove head or tail, value not in list 107 | * 108 | * Time complexity: O(N) 109 | * Space complexity: O(1) 110 | * 111 | * @param {*} value - value to be removed from list 112 | * 113 | * @returns {Object} - node removed 114 | */ 115 | remove(value) { 116 | // Edge case: empty list 117 | if (this.size === 0) { 118 | throw new Error("This Linked List is already empty"); 119 | } 120 | 121 | // Edge case: if head matches, need to update head 122 | if (this.head.value === value) { 123 | const node = this.head; 124 | this.head = this.head.next; 125 | 126 | // Edge case: if removing final node in list 127 | if (this.size === 1) { 128 | this.tail = null; 129 | } 130 | 131 | this.size--; 132 | return node; 133 | } 134 | 135 | let prev = this.head; 136 | let curr = this.head.next; 137 | 138 | while (curr !== null) { 139 | if (curr.value === value) { 140 | // Edge case: if tail matches, need to update tail 141 | if (curr === this.tail) { 142 | this.tail = prev; 143 | } 144 | 145 | const node = curr; 146 | prev.next = curr.next; 147 | this.size--; 148 | return node; 149 | } 150 | 151 | prev = curr; 152 | curr = curr.next; 153 | } 154 | 155 | throw new Error("That value does not exist in this Linked List"); 156 | } 157 | } 158 | 159 | module.exports = LinkedList; 160 | -------------------------------------------------------------------------------- /structures/queue.circular.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Helper function for enqueue and dequeue methods in CircularQueue. 3 | * 4 | * Strategy: Check if index is currently pointing at last element in ring. If 5 | * so, wrap around by setting index to 0. If not, increment index by 1. 6 | * 7 | * Time complexity: O(1) 8 | * Space complexity: O(1) 9 | * 10 | * @param {Number} index - pointer for oldest or newest element in queue 11 | * @param {Number} length - size of queue 12 | * 13 | * @returns {Number} - index increased by 1, or 0 if reached end of ring 14 | * 15 | * @private 16 | */ 17 | function incrementIndex(index, length) { 18 | return index === length - 1 ? 0 : index + 1; 19 | } 20 | 21 | /** Class representing a ring-buffered dynamic array */ 22 | class CircularQueue { 23 | /** 24 | * Given an array of fixed size, a queue can be implemented with two pointers 25 | * tracking the oldest and newest insertions. 26 | * 27 | * @constructor 28 | * 29 | * @param {Number} size - length of queue 30 | * 31 | * @property {Array} ring - circular queue itself 32 | * @property {Number} size - length of queue 33 | * @property {Number} oldest - index that points to location for deletion 34 | * @property {Number} newest - index that points to location for insertion 35 | */ 36 | constructor(size) { 37 | this.ring = []; 38 | this.size = size; 39 | 40 | // enqueue moves pointer before insertion, so oldest begins 1 after newest 41 | this.oldest = 1; 42 | this.newest = 0; 43 | } 44 | 45 | /** 46 | * @description Remove oldest item from queue. 47 | * 48 | * Strategy: Use oldest pointer to delete value. Then increment oldest pointer. 49 | * Do nothing if no values to dequeue. 50 | * 51 | * Time complexity: O(1) 52 | * Space complexity: O(1) 53 | * 54 | * @returns {*} - dequeued item 55 | */ 56 | dequeue() { 57 | if (this.ring[this.oldest] === undefined) { return; } 58 | 59 | const removed = this.ring[this.oldest]; 60 | delete this.ring[this.oldest]; 61 | this.oldest = incrementIndex(this.oldest, this.size); 62 | return removed; 63 | } 64 | 65 | /** 66 | * @description Insert item into queue. 67 | * 68 | * Strategy: Use newest pointer to insert value. Increment newest pointer to 69 | * point to next position. If value is present, move oldest pointer before 70 | * overwriting. Then set value. 71 | * 72 | * Time complexity: O(1) 73 | * Space complexity: O(1) 74 | * 75 | * @param {*} value - item inserted into queue 76 | */ 77 | enqueue(value) { 78 | this.newest = incrementIndex(this.newest, this.size); 79 | 80 | // If overwriting oldest value, move pointer to track next oldest value 81 | if (this.ring[this.newest] !== undefined) { 82 | this.oldest = incrementIndex(this.oldest, this.size); 83 | } 84 | 85 | this.ring[this.newest] = value; 86 | } 87 | } 88 | 89 | module.exports = CircularQueue; -------------------------------------------------------------------------------- /structures/queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class representing each node in queue 3 | * @private 4 | */ 5 | class Node { 6 | /** 7 | * Quickly create nodes for queue 8 | * 9 | * @constructor 10 | * 11 | * @param {*} value - value held by node 12 | * 13 | * @property {*} value - value held by node 14 | * @property {Object|Null} next - point to next node in list 15 | */ 16 | constructor(value) { 17 | this.value = value; 18 | this.next = null; 19 | } 20 | } 21 | 22 | /** Class representing a queue */ 23 | class Queue { 24 | /** 25 | * Use Linked List structure to trade space from extra pointers 26 | * for constant time complexity on all operations. 27 | * 28 | * @constructor 29 | * 30 | * @param {Number=} capacity - limits Queue size 31 | * 32 | * @property {Object|Null} front - start of queue 33 | * @property {Object|Null} rear - back of queue 34 | * @property {Number} size - length of queue 35 | * @property {Number} capacity - if size reaches capacity, queue stops enqueuing 36 | */ 37 | constructor(capacity = Infinity) { 38 | this.front = null; 39 | this.rear = null; 40 | this.size = 0; 41 | this.capacity = capacity; 42 | } 43 | 44 | /** 45 | * @description Remove node from front of queue 46 | * 47 | * Strategy: Reassign front to next node 48 | * 49 | * Time complexity: O(1) 50 | * Space complexity: O(1) 51 | * 52 | * @returns {*} - value removed from queue, or undefined if empty 53 | */ 54 | dequeue() { 55 | if (this.size === 0) { return; } 56 | 57 | const value = this.front.value; 58 | 59 | this.front = this.front.next; 60 | if (this.size === 1) { this.rear = null; } 61 | 62 | this.size--; 63 | return value; 64 | } 65 | 66 | /** 67 | * @description Add node with given input value to end of queue 68 | * 69 | * Strategy: Use native Array push method if under capacity 70 | * 71 | * Time complexity: O(1) 72 | * Space complexity: O(1) 73 | * 74 | * @param {*} val - value added to queue 75 | */ 76 | enqueue(val) { 77 | if (this.size >= this.capacity) { return; } 78 | 79 | const node = new Node(val); 80 | this.size++; 81 | 82 | // Empty queue check. Allows this.size++ to be in one place above. 83 | if (this.front === null) { 84 | this.front = this.rear = node; 85 | return; 86 | } 87 | 88 | this.rear.next = node; 89 | this.rear = this.rear.next; 90 | } 91 | 92 | /** 93 | * @description Check if queue is empty 94 | * 95 | * Strategy: Use size property 96 | * 97 | * Time complexity: O(1) 98 | * Space complexity: O(1) 99 | * 100 | * @returns {Boolean} - true if empty, or false otherwise 101 | */ 102 | isEmpty() { 103 | return this.size === 0; 104 | } 105 | 106 | /** 107 | * @description Check if queue has reached capacity 108 | * 109 | * Strategy: Check if size equals capacity 110 | * 111 | * Time complexity: O(1) 112 | * Space complexity: O(1) 113 | * 114 | * @returns {Boolean} - true if full, or false otherwise 115 | */ 116 | isFull() { 117 | return this.size === this.capacity; 118 | } 119 | 120 | /** 121 | * @description View value at front of queue 122 | * 123 | * Strategy: Look at front property 124 | * 125 | * Time complexity: O(1) 126 | * Space complexity: O(1) 127 | * 128 | * @returns {*} - value at front of queue 129 | */ 130 | peek() { 131 | if (this.front !== null) { return this.front.value; }; 132 | } 133 | 134 | /** 135 | * @description Check size of queue 136 | * 137 | * Strategy: Use size property 138 | * 139 | * Time complexity: O(1) 140 | * Space complexity: O(1) 141 | * 142 | * @returns {Number} - total amount of items in queue 143 | */ 144 | getSize() { 145 | return this.size; 146 | } 147 | } 148 | 149 | module.exports = Queue; 150 | -------------------------------------------------------------------------------- /structures/queue.priority.max.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description When root is replaced (such as upon deleteMax), new root value 3 | * is likely out of order. Sink checks children to properly place the swapped 4 | * element in the heap where it is larger than its children but not its parent. 5 | * 6 | * Strategy: Check parent against largest child repeatedly. If parent is 7 | * larger than largest child, it means it is larger than both of them, so we 8 | * can stop sinking. If parent is smaller than largest child, then regardless 9 | * of whether it is smaller than smaller child, it will swap with largest 10 | * child to maintain invariant. 11 | * 12 | * Time complexity: O(log N) 13 | * Space complexity: O(1) 14 | * 15 | * @param {Object} heap - the PriorityQueueMax instance's heap array 16 | * @param {Number} parentIndex - array index representing sinking element 17 | * 18 | * @private 19 | */ 20 | function sink(heap, parentIndex) { 21 | if ( 22 | !Array.isArray(heap) || 23 | !Number.isSafeInteger(parentIndex) || 24 | parentIndex < 0 || 25 | parentIndex > heap.length 26 | ) { 27 | throw new Error('Invalid input. See "sink" function for valid inputs.'); 28 | } 29 | 30 | // Repeat if any children exist 31 | while (2 * parentIndex < heap.length) { 32 | const parent = heap[parentIndex]; 33 | const childIndexA = parentIndex * 2; 34 | const childIndexB = parentIndex * 2 + 1; 35 | 36 | // Child B may be undefined, in which case select childA. Tie also prefers A. 37 | const indexOfLargerChild = 38 | heap[childIndexB] > heap[childIndexA] ? childIndexB : childIndexA; 39 | 40 | // Stop sinking when you're at least as large as your largest child 41 | if (parent >= heap[indexOfLargerChild]) { 42 | return; 43 | } 44 | 45 | // Child is larger, so swap 46 | swap(heap, parentIndex, indexOfLargerChild); 47 | 48 | // Reassign pointer for possibly another sink 49 | parentIndex = indexOfLargerChild; 50 | } 51 | } 52 | 53 | /** 54 | * @description Helper function to swap two array elements in-place. 55 | * 56 | * Strategy: Use array destructuring. 57 | * 58 | * Time complexity: O(1) 59 | * Space complexity: O(1) 60 | * 61 | * @param {Array} arr - array with elements to be swapped 62 | * @param {Number} indexA - index of first element to be swapped 63 | * @param {Number} indexB - index of second element to be swapped 64 | * 65 | * @private 66 | */ 67 | function swap(arr, indexA, indexB) { 68 | if ( 69 | !Array.isArray(arr) || 70 | !Number.isSafeInteger(indexA) || 71 | !Number.isSafeInteger(indexB) || 72 | indexA < 0 || 73 | indexB < 0 || 74 | indexA > arr.length || 75 | indexB > arr.length 76 | ) { 77 | throw new Error('Invalid input. See "swap" function for valid inputs.'); 78 | } 79 | 80 | [arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]]; 81 | } 82 | 83 | /** 84 | * @description When new values are added to the heap, they are likely not in 85 | * order. Swim checks parents to properly place these new elements in the heap. 86 | * 87 | * Strategy: Calculate parent index for size comparison. Swap with parent if 88 | * parent is smaller. Continue process until parent is greater. 89 | * 90 | * Time complexity: O(log N) 91 | * Space complexity: O(1) 92 | * 93 | * @param {Object} heap - the PriorityQueueMax instance's heap array 94 | * @param {Number} childsIndex - array index representing the element swimming up 95 | * 96 | * @private 97 | */ 98 | function swim(heap, childsIndex) { 99 | if ( 100 | !Array.isArray(heap) || 101 | !Number.isSafeInteger(childsIndex) || 102 | childsIndex < 0 || 103 | childsIndex > heap.length 104 | ) { 105 | throw new Error('Invalid input. See "swim" function for valid inputs.'); 106 | } 107 | 108 | // Avoid reassinging input parameters 109 | let childIndex = childsIndex; 110 | let parentIndex = Math.floor(childIndex / 2); 111 | 112 | // Child swims up until it's the root or not larger than its parent 113 | while (childIndex > 1 && heap[childIndex] > heap[parentIndex]) { 114 | swap(heap, childIndex, parentIndex); 115 | childIndex = parentIndex; 116 | parentIndex = Math.floor(childIndex / 2); 117 | } 118 | } 119 | 120 | /** Class representing our priority queue */ 121 | class PriorityQueueMax { 122 | /** 123 | * This priority queue is a max binary heap. 124 | * 125 | * Heap is tracked using a dynamic array. Element at index N has children at 126 | * indices 2N and 2N+1. Begin with null at index 0 for easier math. 127 | * 128 | * @constructor 129 | * 130 | * @property {Array} heap - priority queue represented as binary heap 131 | */ 132 | constructor() { 133 | this.heap = [null]; 134 | } 135 | 136 | /** 137 | * @description Remove largest value in heap and readjust heap to maintain 138 | * invariant that all parents are larger than (or equal to) their children. 139 | * 140 | * Strategy: Swap max (root of tree) with last element in heap. Pop max out 141 | * and store in variable to eventually return. Sink swapped element sitting 142 | * at the root to find its proper place. 143 | * 144 | * Edge case(s): empty heap 145 | * 146 | * Time complexity: O(log N) 147 | * Space complexity: O(1) 148 | * 149 | * @returns {Number|String} - max removed from heap 150 | */ 151 | deleteMax() { 152 | if (this.isEmpty()) { 153 | throw new Error("Cannot delete because the heap is empty."); 154 | } 155 | swap(this.heap, 1, this.heap.length - 1); 156 | const max = this.heap.pop(); 157 | sink(this.heap, 1); 158 | return max; 159 | } 160 | 161 | /** 162 | * @description Add key to proper place in heap. 163 | * 164 | * Strategy: Push to end of array, then swim up to proper location. 165 | * 166 | * Time complexity: O(log N) 167 | * Space complexity: O(1) 168 | * 169 | * @param {Number|String} key - value inserted into heap 170 | */ 171 | insert(key) { 172 | if (isNaN(key) || (typeof key !== "string" && typeof key !== "number")) { 173 | throw new Error( 174 | "Please insert valid number or string character into heap" 175 | ); 176 | } 177 | 178 | this.heap.push(key); 179 | swim(this.heap, this.heap.length - 1); 180 | } 181 | 182 | /** 183 | * @description Check if heap is empty. 184 | * 185 | * Strategy: See if we have a value at the root. 186 | * 187 | * Time complexity: O(1) 188 | * Space complexity: O(1) 189 | * 190 | * @returns {Boolean} - whether or not heap is empty 191 | */ 192 | isEmpty() { 193 | return this.heap[1] === undefined; 194 | } 195 | 196 | /** 197 | * @description Look at max without deleting it. 198 | * 199 | * Strategy: Read from array at root. 200 | * 201 | * Time complexity: O(1) 202 | * Space complexity: O(1) 203 | * 204 | * @returns {Number|String} - maximum value 205 | */ 206 | peekMax() { 207 | if (this.isEmpty()) { 208 | throw new Error( 209 | "The heap is empty. Avoid peeking until there are values in the heap." 210 | ); 211 | } 212 | 213 | return this.heap[1]; 214 | } 215 | } 216 | 217 | module.exports = PriorityQueueMax; 218 | -------------------------------------------------------------------------------- /structures/queue.priority.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description When root is replaced (such as upon deleteMin), new root value 3 | * is likely out of order. Sink checks children to properly place the swapped 4 | * element in the heap where it is smaller than its children but not its parent. 5 | * 6 | * Strategy: Check parent against smallest child repeatedly. If parent is 7 | * smaller than smallest child, it means it is smaller than both of them, so we 8 | * can stop sinking. If parent is larger than smallest child, then regardless 9 | * of whether it is larger than larger child, it will swap with smallest 10 | * child to maintain invariant. 11 | * 12 | * Time complexity: O(log N) 13 | * Space complexity: O(1) 14 | * 15 | * @param {Object} heap - the PriorityQueue instance's heap array 16 | * @param {Number} parentIndex - array index representing sinking element 17 | * 18 | * @private 19 | */ 20 | function sink(heap, parentIndex) { 21 | if ( 22 | !Array.isArray(heap) 23 | || !Number.isInteger(parentIndex) 24 | || parentIndex < 0 25 | || parentIndex > heap.length 26 | ) { 27 | throw new Error('Please input valid inputs, my friend!'); 28 | } 29 | 30 | const childIndexA = 2 * parentIndex; 31 | const childIndexB = 2 * parentIndex + 1; 32 | 33 | // Stop if no more children 34 | if (childIndexA >= heap.length) { return; } 35 | 36 | // Guard against second child possibly being undefined 37 | let smallestChildIndex; 38 | if (childIndexB >= heap.length) { smallestChildIndex = childIndexA; } 39 | else { 40 | smallestChildIndex = heap[childIndexA].key > heap[childIndexB].key 41 | ? childIndexB 42 | : childIndexA; 43 | } 44 | 45 | 46 | // Stop sinking when you're at least as small as your smallest child 47 | if (heap[smallestChildIndex].key > heap[parentIndex].key) { return; } 48 | 49 | // Child is smaller, so swap 50 | swap(heap, parentIndex, smallestChildIndex); 51 | 52 | // May need to keep sinking 53 | sink(heap, smallestChildIndex); 54 | } 55 | 56 | /** 57 | * @description Helper function to swap two array elements in-place. 58 | * 59 | * Strategy: Use array destructuring. 60 | * 61 | * Time complexity: O(1) 62 | * Space complexity: O(1) 63 | * 64 | * @param {Array} arr - array with elements to be swapped 65 | * @param {Number} indexA - index of first element to be swapped 66 | * @param {Number} indexB - index of second element to be swapped 67 | * 68 | * @private 69 | */ 70 | function swap(arr, indexA, indexB) { 71 | if ( 72 | !Array.isArray(arr) 73 | || !Number.isSafeInteger(indexA) 74 | || !Number.isSafeInteger(indexB) 75 | || indexA < 0 76 | || indexB < 0 77 | || indexA > arr.length 78 | || indexB > arr.length 79 | ) { throw new Error('Please insert valid inputs, my friend!'); } 80 | 81 | [arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]]; 82 | } 83 | 84 | /** 85 | * @description When new values are added to the heap, they are likely not in 86 | * order. Swim checks parents to properly place these new elements in the heap. 87 | * 88 | * Strategy: Calculate parent index for size comparison. Swap with parent if 89 | * parent is larger. Continue process until parent is smaller. 90 | * 91 | * Time complexity: O(log N) 92 | * Space complexity: O(1) 93 | * 94 | * @param {Object} heap - the PriorityQueue instance's heap array 95 | * @param {Number} childIndex - array index representing the element swimming up 96 | * 97 | * @private 98 | */ 99 | function swim(heap, childIndex) { 100 | if ( 101 | !Array.isArray(heap) 102 | || !Number.isSafeInteger(childIndex) 103 | || childIndex < 0 104 | || childIndex > heap.length 105 | ) { 106 | throw new Error('Please input valid inputs, my friend!'); 107 | } 108 | 109 | // Child swims up until it's the root or not smaller than its parent 110 | if (childIndex < 2) { return; } 111 | const parentIndex = Math.floor(childIndex / 2); 112 | if (heap[childIndex].key >= heap[parentIndex].key) { return; } 113 | 114 | swap(heap, childIndex, parentIndex); 115 | swim(heap, parentIndex); 116 | } 117 | 118 | /** Class representing a priority queue symbol table */ 119 | class PriorityQueueMin { 120 | /** 121 | * This priority queue is a min binary heap. 122 | * 123 | * Heap is tracked using a dynamic array. Element at index N has children at 124 | * indices 2N and 2N+1. Begin with null at index 0 for easier math. 125 | * 126 | * @constructor 127 | * 128 | * @property {Array} heap - priority queue represented as binary heap 129 | */ 130 | constructor() { 131 | this.heap = [null]; 132 | } 133 | 134 | /** 135 | * @description Remove smallest key in heap and readjust heap to maintain 136 | * invariant that all parents are smaller than (or equal to) their children. 137 | * 138 | * Strategy: Swap min (root of tree) with last element in heap. Pop min out 139 | * and store in variable to eventually return. Sink swapped element sitting 140 | * at the root to find its proper place. 141 | * 142 | * Edge case(s): empty heap 143 | * 144 | * Time complexity: O(log N) 145 | * Space complexity: O(1) 146 | * 147 | * @returns {Number|String} - min removed from heap 148 | */ 149 | deleteMin() { 150 | if (this.isEmpty()) { throw new Error('Heap is empty, my friend!'); } 151 | swap(this.heap, 1, this.heap.length - 1); 152 | const min = this.heap.pop(); 153 | sink(this.heap, 1); 154 | return min; 155 | } 156 | 157 | /** 158 | * @description Add key to proper place in heap. 159 | * 160 | * Strategy: Push to end of array, then swim up to proper location. 161 | * 162 | * Time complexity: O(log N) 163 | * Space complexity: O(1) 164 | * 165 | * @param {Number|String} key - key used for heap position comparisons 166 | * @param {*} value - value associated with input key 167 | */ 168 | insert(key, value) { 169 | if (isNaN(key) || (typeof key !== 'string' && typeof key !== 'number')) { 170 | throw new Error('Please insert valid number or string character for key'); 171 | } 172 | 173 | this.heap.push({ key, value }); 174 | swim(this.heap, this.heap.length - 1); 175 | } 176 | 177 | /** 178 | * @description Check if heap is empty. 179 | * 180 | * Strategy: See if we have a value at the root. 181 | * 182 | * Time complexity: O(1) 183 | * Space complexity: O(1) 184 | * 185 | * @returns {Boolean} - whether or not heap is empty 186 | */ 187 | isEmpty() { 188 | return this.heap[1] === undefined; 189 | } 190 | 191 | /** 192 | * @description Look at min without deleting it. 193 | * 194 | * Strategy: Read from array at root. 195 | * 196 | * Time complexity: O(1) 197 | * Space complexity: O(1) 198 | * 199 | * @returns {Number|String} - minimum value 200 | */ 201 | peekMin() { 202 | if (this.isEmpty()) { throw new Error('Heap is empty, my friend!'); } 203 | return this.heap[1]; 204 | } 205 | } 206 | 207 | module.exports = PriorityQueueMin; -------------------------------------------------------------------------------- /structures/stack.js: -------------------------------------------------------------------------------- 1 | /** Class representing stack */ 2 | class Stack { 3 | /** 4 | * Represents a specialized stack for numbers that instantly returns max value 5 | * 6 | * @constructor 7 | * 8 | * @param {Number=} capacity 9 | * 10 | * @property {Array} maxes - tracks largest value in stack for constant lookup 11 | * @property {Array} storage - the stack itself 12 | * @property {Number} capacity - stack stops push when at capacity 13 | */ 14 | constructor(capacity = Infinity) { 15 | this.maxes = [-Infinity]; 16 | this.storage = []; 17 | this.capacity = capacity; 18 | } 19 | 20 | /** 21 | * @description Check maximum in stack 22 | * 23 | * Strategy: Return last value added to maxes array 24 | * 25 | * Time complexity: O(1) 26 | * Space complexity: O(1) 27 | * 28 | * @returns {Number} - maximum value in stack 29 | */ 30 | getMax() { 31 | return this.maxes[this.maxes.length - 1]; 32 | } 33 | 34 | /** 35 | * @description Check if stack is empty 36 | * 37 | * Strategy: Use storage array length 38 | * 39 | * Time complexity: O(1) 40 | * Space complexity: O(1) 41 | * 42 | * @returns {Boolean} - true if empty, or false otherwise 43 | */ 44 | isEmpty() { 45 | return this.storage.length === 0; 46 | } 47 | 48 | /** 49 | * @description Check if stack has reached capacity 50 | * 51 | * Strategy: Check if storage array length matches capacity value 52 | * 53 | * Time complexity: O(1) 54 | * Space complexity: O(1) 55 | * 56 | * @returns {Boolean} - true if full, or false otherwise 57 | */ 58 | isFull() { 59 | return this.storage.length === this.capacity; 60 | } 61 | 62 | /** 63 | * @description View value at top of stack 64 | * 65 | * Strategy: Look at storage array's final element using index 66 | * 67 | * Time complexity: O(1) 68 | * Space complexity: O(1) 69 | * 70 | * @returns {*} - value at top of stack 71 | */ 72 | peek() { 73 | return this.storage[this.storage.length - 1]; 74 | } 75 | 76 | /** 77 | * @description Add input value to top of stack 78 | * 79 | * Strategy: Use native Array push method if under capacity. If value is 80 | * greater than previous max, make it the maximum. 81 | * 82 | * Time complexity: O(1) 83 | * Space complexity: O(1) 84 | * 85 | * @param {*} val - value added to stack 86 | */ 87 | push(val) { 88 | if (this.storage.length >= this.capacity) { 89 | return; 90 | } 91 | this.storage.push(val); 92 | if (val >= this.maxes[this.maxes.length - 1]) { 93 | this.maxes.push(val); 94 | } 95 | } 96 | 97 | /** 98 | * @description Remove value from top of stack 99 | * 100 | * Strategy: Use native Array pop method. Check if maximum for removal. 101 | * 102 | * Time complexity: O(1) 103 | * Space complexity: O(1) 104 | * 105 | * @returns {Number} - value removed from stack, or undefined if empty 106 | */ 107 | pop() { 108 | const popped = this.storage[this.storage.length - 1]; 109 | const max = this.maxes[this.maxes.length - 1]; 110 | if (popped === max) { 111 | this.maxes.pop(); 112 | } 113 | return this.storage.pop(); 114 | } 115 | 116 | /** 117 | * @description Check size of stack 118 | * 119 | * Strategy: Use storage length 120 | * 121 | * Time complexity: O(1) 122 | * Space complexity: O(1) 123 | * 124 | * @returns {Number} - total amount of items in stack 125 | */ 126 | size() { 127 | return this.storage.length; 128 | } 129 | } 130 | 131 | module.exports = Stack; 132 | -------------------------------------------------------------------------------- /structures/tree.trie.prefix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method used in public delete method. Remembers the last 3 | * node that should not be deleted, which occurs at a prefix word or fork. 4 | * 5 | * Strategy: Check if the current node ends a word or forks to other words. 6 | * 7 | * Time complexity: O(1) 8 | * Space complexity: O(1) 9 | * 10 | * @param {Object} currentNode - node visited 11 | * @param {Boolean} isFinalLetter - true if looping on final letter 12 | * 13 | * @returns {Boolean} - true if cutoff found, false otherwise 14 | * 15 | * @private 16 | */ 17 | function isCutOffNode(currentNode, isFinalLetter) { 18 | return ( 19 | // Has prefix that is a stored word 20 | (currentNode.hasOwnProperty("value") && !isFinalLetter) || 21 | // Forks to another word 22 | (!currentNode.hasOwnProperty("value") && 23 | Object.keys(currentNode).length > 1) 24 | ); 25 | } 26 | 27 | /** Class representing a prefix trie */ 28 | class PrefixTrie { 29 | /** 30 | * Track root of trie. 31 | * 32 | * Trie can store any non-empty string. Is case sensitive. 33 | * 34 | * @constructor 35 | * 36 | * @property {Object} root - top level node in trie 37 | */ 38 | constructor() { 39 | this.root = {}; 40 | } 41 | 42 | /** 43 | * @description Find all words with a given prefix. 44 | * 45 | * Strategy: Traverse to last letter in word, then call orderWords method and 46 | * prepend the prefix to all words found. 47 | * 48 | * Time complexity: O(s * N), where s is suffix length and N is number of matched words 49 | * Space complexity: O(N) 50 | * 51 | * @param {String} prefix - prefix to be matched 52 | * 53 | * @returns {Array} - all keys matching input prefix 54 | */ 55 | autoComplete(prefix) { 56 | if (typeof prefix !== "string") { 57 | throw new Error("This trie only stores strings"); 58 | } 59 | 60 | // Could also cache previous searches and return those here 61 | if (prefix === "") { 62 | return []; 63 | } 64 | 65 | let currentNode = this.root; 66 | 67 | for (let i = 0; i < prefix.length; i++) { 68 | const letter = prefix[i]; 69 | if (!currentNode.hasOwnProperty(letter)) { 70 | return []; 71 | } 72 | 73 | currentNode = currentNode[letter]; 74 | } 75 | 76 | return this.orderWords(currentNode).map(word => prefix + word); 77 | } 78 | 79 | /** 80 | * @description Delete a word from the trie, if it exists. 81 | * 82 | * Strategy: Track two nodes while traversing. One is the current node being 83 | * traversed, and the second remembers the latest cutoff point from which to 84 | * delete characters. Cutoff occurs when a word is a prefix of the input word 85 | * or there is a branch to other words. If no cutoff, simply remove first 86 | * letter at root, which removes whole word. 87 | * 88 | * Time complexity: O(c), where c is number of characters in word 89 | * Space complexity: O(1) 90 | * 91 | * @param {String} word - word to be removed 92 | */ 93 | delete(word) { 94 | if (!this.hasWord(word)) { 95 | throw new Error("That word is not in the trie"); 96 | } 97 | 98 | let currentNode = this.root; 99 | let cutOff; 100 | 101 | for (let i = 0; i < word.length; i++) { 102 | const letter = word[i]; 103 | const isFinalLetter = i === word.length - 1; 104 | if (isCutOffNode(currentNode, isFinalLetter)) { 105 | cutOff = { node: currentNode, letter }; 106 | } 107 | 108 | // Move to next letter in word 109 | currentNode = currentNode[letter]; 110 | } 111 | 112 | // Case 1: Input word is a prefix of another word, so leave letters alone 113 | if (Object.keys(currentNode).length > 1) { 114 | delete currentNode.value; 115 | return; 116 | } 117 | 118 | // Case 2: Prefix is needed (as its own word or a fork to other words), so leave prefix intact 119 | if (cutOff) { 120 | delete cutOff.node[cutOff.letter]; 121 | return; 122 | } 123 | 124 | // Case 3: Remove whole word since no part of it overlaps with other words 125 | delete this.root[word[0]]; 126 | } 127 | 128 | /** 129 | * @description Get a value tied to a given key. 130 | * 131 | * Strategy: Check each letter for existence down a branch. If letter does 132 | * not exist in trie, word does not exist. If word completes, return value. 133 | * 134 | * Time complexity: O(c), where c is number of characters in key 135 | * Space complexity: O(1) 136 | * 137 | * @param {String} word - key to access value 138 | * 139 | * @returns {*} - value if key found, or null otherwise 140 | */ 141 | get(word) { 142 | if (typeof word !== "string") { 143 | throw new Error("This trie only stores strings"); 144 | } 145 | 146 | let currentNode = this.root; 147 | 148 | for (let i = 0; i < word.length; i++) { 149 | const letter = word[i]; 150 | if (!currentNode.hasOwnProperty(letter)) { 151 | return null; 152 | } 153 | 154 | currentNode = currentNode[letter]; 155 | } 156 | 157 | return currentNode.hasOwnProperty("value") ? currentNode.value : null; 158 | } 159 | 160 | /** 161 | * @description Check if a prefix exists in the trie. 162 | * 163 | * Strategy: Traverse trie. Return false if letter is not found. Return true 164 | * if traversal ends successfully. 165 | * 166 | * Time complexity: O(c), where c is number of characters in prefix 167 | * Space complexity: O(1) 168 | * 169 | * @param {String} prefix - prefix to be searched 170 | * 171 | * @returns {Boolean} - true if prefix exists, or false otherwise 172 | */ 173 | hasPrefix(prefix) { 174 | if (typeof prefix !== "string" || prefix.length === 0) { 175 | throw new Error("This trie only stores non-empty strings"); 176 | } 177 | 178 | let currentNode = this.root; 179 | 180 | for (let i = 0; i < prefix.length; i++) { 181 | const letter = prefix[i]; 182 | if (!currentNode.hasOwnProperty(letter)) { 183 | return false; 184 | } 185 | 186 | currentNode = currentNode[letter]; 187 | } 188 | 189 | return true; 190 | } 191 | 192 | /** 193 | * @description Check if a word exists in the trie. 194 | * 195 | * Strategy: Use public get method. 196 | * 197 | * Time complexity: O(c), where c is number of characters in word 198 | * Space complexity: O(1) 199 | * 200 | * @param {String} word - key to be searched 201 | * 202 | * @returns {Boolean} - true if word exists, or false otherwise 203 | */ 204 | hasWord(word) { 205 | if (typeof word !== "string" || word.length === 0) { 206 | throw new Error("This trie only stores non-empty strings"); 207 | } 208 | 209 | return this.get(word) !== null; 210 | } 211 | 212 | /** 213 | * @description Get all words in alphabetical order. 214 | * 215 | * Strategy: Recursively traverse trie and send words to an array to be 216 | * returned. A word is found when a value property is seen. Sort letters to 217 | * ensure alphabetical order. 218 | * 219 | * Time complexity: O(N) 220 | * Space complexity: O(N) 221 | * 222 | * @param {Object=} node - node being traversed 223 | * @param {Array=} words - alphabetical list of words to be returned 224 | * @param {String=} word - word built up while traversing 225 | * 226 | * @returns {Array} - alphabetical list of all words in trie 227 | */ 228 | orderWords(node = this.root, words = [], word = "") { 229 | if (node.hasOwnProperty("value")) { 230 | words.push(word); 231 | } 232 | 233 | Object.keys(node) 234 | .sort() 235 | .forEach(key => { 236 | if (key === "value") { 237 | return; 238 | } 239 | this.orderWords(node[key], words, word + key); 240 | }); 241 | 242 | return words; 243 | } 244 | 245 | /** 246 | * @description Insert or update a key-value pair. 247 | * 248 | * Strategy: Check each letter for existence down a branch. If letter does 249 | * not exist in trie, create it. When word completes, add value to next node. 250 | * 251 | * Time complexity: O(c), where c is number of characters 252 | * Space complexity: O(1) 253 | * 254 | * Tradeoffs: A small amount of extra space is used to store value in a 255 | * separate object from the final character in exchange for instant lookup 256 | * time on characters. The popular alternative using arrays with length 257 | * proportional to character set requires far more space for the same time 258 | * complexity. 259 | * 260 | * @param {String} word - key for value 261 | * @param {*} value - inserted as value property in node after word's final character 262 | */ 263 | put(word, value) { 264 | if (typeof word !== "string" || word.length === 0) { 265 | throw new Error("This trie requires non-empty string keys"); 266 | } 267 | 268 | let currentNode = this.root; 269 | 270 | for (let i = 0; i < word.length; i++) { 271 | const letter = word[i]; 272 | if (!currentNode.hasOwnProperty(letter)) { 273 | currentNode[letter] = {}; 274 | } 275 | 276 | currentNode = currentNode[letter]; 277 | } 278 | 279 | currentNode.value = value; 280 | } 281 | } 282 | 283 | module.exports = PrefixTrie; 284 | -------------------------------------------------------------------------------- /structures/tree.trie.suffix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Private method that inserts suffixes into suffix trie. 3 | * 4 | * Strategy: Traverse trie letter by letter. If letter does not exist, create 5 | * it. At end, place terminating marker. Convert all letters to lowercase to 6 | * make trie case insensitive. Ignore whitespace with regex check. Notice that 7 | * the index is placed in the node after the final letter. 8 | * 9 | * Time complexity: O(c), where c is number of characters in suffix 10 | * Space complexity: O(c), where c is number of characters in suffix 11 | * 12 | * @param {String} suffix - suffix inserted into trie 13 | * @param {Object} trie - suffix trie where suffix is being inserted 14 | * @param {Number} index - index where suffix begins in original string 15 | * 16 | * @returns {Object} - root of trie 17 | * 18 | * @private 19 | */ 20 | function insert(suffix, trie, index) { 21 | let currentNode = trie; 22 | 23 | for (let i = 0; i < suffix.length; i++) { 24 | const letter = suffix[i].toLowerCase(); 25 | 26 | // Ignore whitespace 27 | if (/\s/.test(letter)) { 28 | continue; 29 | } 30 | 31 | if (!currentNode.hasOwnProperty(letter)) { 32 | currentNode[letter] = {}; 33 | } 34 | 35 | // Traverse to next node unless last letter 36 | currentNode = currentNode[letter]; 37 | } 38 | 39 | // Index also acts as terminating marker to represent a complete suffix 40 | currentNode.index = index; 41 | } 42 | 43 | /** 44 | * @description Private method that builds entire suffix trie upon class 45 | * instantiation. 46 | * 47 | * Strategy: Create root object. Insert every possible suffix of input string. 48 | * Build suffix string by prepending one character at a time starting at end of 49 | * word. For example, BANANA inserts A, NA, ANA, NANA, ANANA, BANANA. 50 | * 51 | * Time complexity: O(c^2), where c is number of characters in input string 52 | * Space complexity: O(c^2), where c is number of characters in input string 53 | * 54 | * @param {String} string - string that suffix trie represents 55 | * 56 | * @returns {Object} - root of trie 57 | * 58 | * @private 59 | */ 60 | function buildTrie(string) { 61 | if (string === "" || typeof string !== "string") { 62 | throw new Error("Insert a non-empty string upon instantiation"); 63 | } 64 | 65 | const trie = {}; 66 | let suffix = ""; 67 | 68 | for (let i = string.length - 1; i >= 0; i--) { 69 | const letter = string[i]; 70 | suffix = letter + suffix; 71 | insert(suffix, trie, i); 72 | } 73 | 74 | return trie; 75 | } 76 | 77 | /** Class representing a suffix trie */ 78 | class SuffixTrie { 79 | /** 80 | * Track root of trie. 81 | * 82 | * Trie can store any non-empty string. Case insensitive. Ignores whitespace. 83 | * 84 | * @constructor 85 | * 86 | * @param str - string used to build suffix trie 87 | * 88 | * @property {Object} root - top level node that points to rest of trie 89 | */ 90 | constructor(str) { 91 | this.root = buildTrie(str); 92 | } 93 | 94 | /** 95 | * @description Check if a suffix exists in the trie. 96 | * 97 | * Strategy: Traverse trie. Return false if letter is not found. Return true 98 | * if traversal ends successfully and the final node has a terminating marker. 99 | * 100 | * Time complexity: O(c), where c is number of characters in suffix 101 | * Space complexity: O(1) 102 | * 103 | * @param {String} suffix - suffix to be searched 104 | * 105 | * @returns {Boolean} - true if suffix exists, or false otherwise 106 | */ 107 | hasSuffix(suffix) { 108 | if (typeof suffix !== "string") { 109 | throw new Error("This trie only stores strings"); 110 | } 111 | 112 | let currentNode = this.root; 113 | 114 | for (let i = 0; i < suffix.length; i++) { 115 | const letter = suffix[i]; 116 | if (!currentNode.hasOwnProperty(letter)) { 117 | return false; 118 | } 119 | 120 | currentNode = currentNode[letter]; 121 | } 122 | 123 | return currentNode.hasOwnProperty("index"); 124 | } 125 | 126 | /** 127 | * @description Check if a pattern exists in the trie. 128 | * 129 | * Strategy: Traverse trie. Return false if letter is not found. Return true 130 | * if traversal ends successfully. 131 | * 132 | * Edge case: empty string returns true 133 | * 134 | * Time complexity: O(c), where c is number of characters in input pattern 135 | * Space complexity: O(1) 136 | * 137 | * @param {String} pattern - pattern to be searched 138 | * 139 | * @returns {Boolean} - true if pattern exists, or false otherwise 140 | */ 141 | matchesPattern(pattern) { 142 | if (typeof pattern !== "string") { 143 | throw new Error("This trie only stores strings"); 144 | } 145 | 146 | let currentNode = this.root; 147 | 148 | for (let i = 0; i < pattern.length; i++) { 149 | const letter = pattern[i]; 150 | if (!currentNode.hasOwnProperty(letter)) { 151 | return false; 152 | } 153 | 154 | currentNode = currentNode[letter]; 155 | } 156 | 157 | return true; 158 | } 159 | } 160 | 161 | module.exports = SuffixTrie; 162 | -------------------------------------------------------------------------------- /test/processors/graph.edge-weighted.directed.shortest-path-dijkstra.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let DirectedGraph; 4 | let graph; 5 | 6 | let ShortestPath; 7 | let path; 8 | 9 | const SOURCE_VERTEX = 0; 10 | const TEST_EDGES = [ 11 | [0, 1, 5], 12 | [0, 4, 9], 13 | [0, 7, 8], 14 | [1, 2, 12], 15 | [1, 3, 15], 16 | [1, 7, 4], 17 | [2, 3, 3], 18 | [2, 6, 11], 19 | [3, 6, 9], 20 | [4, 5, 4], 21 | [4, 6, 20], 22 | [4, 7, 5], 23 | [5, 2, 1], 24 | [5, 6, 13], 25 | [7, 5, 6], 26 | [7, 2, 7], 27 | [11, 12, 13], 28 | ]; 29 | 30 | try { 31 | DirectedGraph = require('../../structures/graph.edge-weighted.directed'); 32 | graph = new DirectedGraph(TEST_EDGES); 33 | 34 | ShortestPath = require('../../processors/graph.edge-weighted.directed.shortest-path-dijkstra'); 35 | path = new ShortestPath(graph, SOURCE_VERTEX); 36 | } catch (e) { 37 | throw new Error('ShortestPath could not be tested due to faulty import, ' + 38 | 'likely from an incorrect file path or exporting a non-constructor from ' + 39 | 'the processor or graph files.'); 40 | } 41 | 42 | describe('ShortestPath', () => { 43 | beforeEach(() => { 44 | graph = new DirectedGraph(TEST_EDGES); 45 | path = new ShortestPath(graph, SOURCE_VERTEX); 46 | }); 47 | 48 | it('should be extensible', () => { 49 | expect(path).to.be.extensible; 50 | }); 51 | 52 | it('should have properties granted from constructor call', () => { 53 | expect(path).to.have.all.keys( 54 | 'distanceFromSource', 55 | 'graph', 56 | 'parent', 57 | 'sourceVertex', 58 | 'visited', 59 | ); 60 | }); 61 | 62 | it('should set a string data type for source vertex', () => { 63 | expect(path.sourceVertex).to.equal(String(SOURCE_VERTEX)); 64 | }); 65 | 66 | describe('#distanceTo', () => { 67 | it('should return the weighted distance of a vertex adjacent to source vertex', () => { 68 | expect(path.distanceTo(4)).to.equal(9); 69 | }); 70 | 71 | it('should return the weighted distance of a vertex two edges away from source vertex', () => { 72 | expect(path.distanceTo(5)).to.equal(13); 73 | }); 74 | 75 | it('should return null if the input vertex is not connected to the source', () => { 76 | expect(path.distanceTo(11)).to.equal(null); 77 | }); 78 | }); 79 | 80 | describe('#hasPathTo', () => { 81 | it('should return true if the input vertex is connected to the source vertex', () => { 82 | expect(path.hasPathTo(6)).to.be.true; 83 | }); 84 | 85 | it('should return false if the input vertex is not connected to the source vertex', () => { 86 | expect(path.hasPathTo(11)).to.be.false; 87 | }); 88 | 89 | it('should throw an error if the input vertex is not in the graph', () => { 90 | expect(() => path.hasPathTo('not in graph')).to.throw(Error); 91 | }); 92 | }); 93 | 94 | describe('#shortestPathTo', () => { 95 | it('should return null if a path to the source vertex does not exist', () => { 96 | expect(path.shortestPathTo(11)).to.equal(null); 97 | }); 98 | 99 | it('should return the shortest path to the source vertex if a path exists', () => { 100 | expect(path.shortestPathTo(6)).to.deep.equal(['6', '2', '5', '4', '0']); 101 | }); 102 | }); 103 | }); 104 | 105 | 106 | -------------------------------------------------------------------------------- /test/processors/graph.edge-weighted.undirected.minimum-spanning-tree-prim.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let UndirectedGraph; 4 | let graph; 5 | 6 | let MinimumSpanningTree; 7 | let mst; 8 | 9 | const TEST_EDGES = [ 10 | [0, 7, 0.16], 11 | [2, 3, 0.17], 12 | [1, 7, 0.19], 13 | [0, 2, 0.26], 14 | [5, 7, 0.28], 15 | [1, 3, 0.29], 16 | [1, 5, 0.32], 17 | [2, 7, 0.34], 18 | [4, 5, 0.35], 19 | [1, 2, 0.36], 20 | [4, 7, 0.37], 21 | [0, 4, 0.38], 22 | [6, 2, 0.40], 23 | [3, 6, 0.52], 24 | [6, 0, 0.58], 25 | [6, 4, 0.93], 26 | ]; 27 | 28 | try { 29 | UndirectedGraph = require('../../structures/graph.edge-weighted.undirected'); 30 | graph = new UndirectedGraph(TEST_EDGES); 31 | 32 | MinimumSpanningTree = require('../../processors/graph.edge-weighted.undirected.minimum-spanning-tree-prim'); 33 | mst = new MinimumSpanningTree(graph); 34 | } catch (e) { 35 | throw new Error('MinimumSpanningTree could not be tested due to faulty import, ' + 36 | 'likely from an incorrect file path or exporting a non-constructor from ' + 37 | 'the processor or graph files.'); 38 | } 39 | 40 | describe('MinimumSpanningTree', () => { 41 | beforeEach(() => { 42 | graph = new UndirectedGraph(TEST_EDGES); 43 | mst = new MinimumSpanningTree(graph); 44 | }); 45 | 46 | it('should be extensible', () => { 47 | expect(mst).to.be.extensible; 48 | }); 49 | 50 | it('should have properties granted from constructor call', () => { 51 | expect(mst).to.have.all.keys( 52 | 'graph', 53 | 'minimumSpanningTree', 54 | ); 55 | }); 56 | 57 | describe('#getMinimumSpanningTree()', () => { 58 | it('should build the correct minimum spanning tree', () => { 59 | const correctEdges = [ 60 | { v1: '0', v2: '7', weight: 0.16 }, 61 | { v1: '1', v2: '7', weight: 0.19 }, 62 | { v1: '0', v2: '2', weight: 0.26 }, 63 | { v1: '2', v2: '3', weight: 0.17 }, 64 | { v1: '5', v2: '7', weight: 0.28 }, 65 | { v1: '4', v2: '5', weight: 0.35 }, 66 | { v1: '6', v2: '2', weight: 0.4 }, 67 | ]; 68 | 69 | const correctSet = new Set(correctEdges); 70 | 71 | expect(mst.getMinimumSpanningTree()).to.deep.equal(correctSet); 72 | }); 73 | }); 74 | }); 75 | 76 | 77 | -------------------------------------------------------------------------------- /test/processors/graph.unweighted.directed.breadth-first-paths.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let DirectedGraph; 4 | let graph; 5 | 6 | let BreadthFirstPaths; 7 | let paths; 8 | 9 | const SOURCE_VERTEX = 0; 10 | const TEST_EDGES = [ 11 | [0, 5], 12 | [4, 3], 13 | [0, 1], 14 | [9, 12], 15 | [6, 4], 16 | [5, 4], 17 | [0, 2], 18 | [11, 12], 19 | [9, 10], 20 | [0, 6], 21 | [7, 8], 22 | [9, 11], 23 | [5, 3], 24 | [5, 5], 25 | [14, 14], 26 | ]; 27 | 28 | try { 29 | DirectedGraph = require('../../structures/graph.unweighted.directed'); 30 | graph = new DirectedGraph(TEST_EDGES); 31 | 32 | BreadthFirstPaths = require('../../processors/graph.unweighted.breadth-first-paths'); 33 | paths = new BreadthFirstPaths(graph, SOURCE_VERTEX); 34 | } catch (e) { 35 | throw new Error('Undirected BreadthFirstPaths could not be tested due to ' + 36 | 'faulty import, likely from an incorrect file path or exporting a ' + 37 | 'non- constructor from the processor or graph files.'); 38 | } 39 | 40 | describe('BreadthFirstPaths', () => { 41 | beforeEach(() => { 42 | graph = new DirectedGraph(TEST_EDGES); 43 | paths = new BreadthFirstPaths(graph, SOURCE_VERTEX); 44 | }); 45 | 46 | it('should be extensible', () => { 47 | expect(paths).to.be.extensible; 48 | }); 49 | 50 | it('should have properties granted from constructor call', () => { 51 | expect(paths).to.have.all.keys( 52 | 'distanceFromSource', 53 | 'graph', 54 | 'parent', 55 | 'sourceVertex', 56 | 'visited', 57 | ); 58 | }); 59 | 60 | it('should set a string data type for source vertex', () => { 61 | expect(paths.sourceVertex).to.equal(String(SOURCE_VERTEX)); 62 | }); 63 | 64 | it('should process only vertices connected to the source vertex', () => { 65 | expect(paths.visited.has('0')).to.be.true; 66 | expect(paths.visited.has('5')).to.be.true; 67 | expect(paths.visited.has('4')).to.be.true; 68 | expect(paths.visited.has('3')).to.be.true; 69 | expect(paths.visited.has('6')).to.be.true; 70 | expect(paths.visited.has('1')).to.be.true; 71 | expect(paths.visited.has('2')).to.be.true; 72 | expect(paths.visited.has('7')).to.be.false; 73 | expect(paths.visited.has('8')).to.be.false; 74 | expect(paths.visited.has('9')).to.be.false; 75 | expect(paths.visited.has('11')).to.be.false; 76 | expect(paths.visited.has('12')).to.be.false; 77 | expect(paths.visited.has('14')).to.be.false; 78 | }); 79 | 80 | it('should throw an error if the source vertex is not in the graph', () => { 81 | graph = new DirectedGraph(); 82 | 83 | expect(() => new BreadthFirstPaths(graph, SOURCE_VERTEX)).to.throw(Error); 84 | }); 85 | 86 | it('should work for number and string data types', () => { 87 | // Arrange 88 | const edges = [ 89 | ['dog', 'woof'], 90 | ['dog', 'bark'], 91 | [0, 'meow'], 92 | ['meow', 'cat'], 93 | [14, 'dog'], 94 | ]; 95 | 96 | graph = new DirectedGraph(edges); 97 | 98 | // Act 99 | paths = new BreadthFirstPaths(graph, SOURCE_VERTEX); 100 | 101 | // Assert 102 | expect(paths.visited.has('0')).to.be.true; 103 | expect(paths.visited.has('meow')).to.be.true; 104 | expect(paths.visited.has('cat')).to.be.true; 105 | expect(paths.visited.has('14')).to.be.false; 106 | expect(paths.visited.has('dog')).to.be.false; 107 | expect(paths.visited.has('woof')).to.be.false; 108 | expect(paths.visited.has('bark')).to.be.false; 109 | }); 110 | 111 | describe('#distanceTo', () => { 112 | it('should return the distance if one level from the source vertex', () => { 113 | expect(paths.distanceTo(6)).to.equal(1); 114 | }); 115 | 116 | it('should return the distance if two levels from the source vertex', () => { 117 | expect(paths.distanceTo(4)).to.equal(2); 118 | }); 119 | 120 | it('should return null if the input vertex is not connected to the source', () => { 121 | expect(paths.distanceTo(14)).to.equal(null); 122 | }); 123 | }); 124 | 125 | describe('#hasPathTo', () => { 126 | it('should return true if the input vertex is connected to the source vertex', () => { 127 | expect(paths.hasPathTo('6')).to.be.true; 128 | }); 129 | 130 | it('should return false if the input vertex is not connected to the source vertex', () => { 131 | expect(paths.hasPathTo('11')).to.be.false; 132 | }); 133 | 134 | it('should throw an error if the input vertex is not in the graph', () => { 135 | expect(() => paths.hasPathTo('not in graph')).to.throw(Error); 136 | }); 137 | }); 138 | 139 | describe('#shortestPathTo', () => { 140 | it('should return null if a path to the source vertex does not exist', () => { 141 | expect(paths.shortestPathTo(11)).to.equal(null); 142 | }); 143 | 144 | it('should return the shortest path to the source vertex if a path exists', () => { 145 | expect(paths.shortestPathTo(6)).to.deep.equal(['6', '0']); 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /test/processors/graph.unweighted.directed.cycle.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let DirectedGraph; 4 | let graph; 5 | 6 | let DirectedCycle; 7 | let cycleProcessor; 8 | 9 | const TEST_EDGES = [ 10 | [0, 7], 11 | [0, 1], 12 | [1, 2], 13 | [2, 4], 14 | [1, 3], 15 | [3, 5], 16 | ]; 17 | 18 | try { 19 | DirectedGraph = require('../../structures/graph.unweighted.directed'); 20 | graph = new DirectedGraph(TEST_EDGES); 21 | 22 | DirectedCycle = require('../../processors/graph.unweighted.directed.cycle'); 23 | cycleProcessor = new DirectedCycle(graph); 24 | } catch (e) { 25 | throw new Error('DirectedCycle could not be tested due to faulty import, ' + 26 | 'likely from an incorrect file path or exporting a non-constructor from ' + 27 | 'the processor or graph files.'); 28 | } 29 | 30 | describe('DirectedCycle', () => { 31 | beforeEach(() => { 32 | graph = new DirectedGraph(TEST_EDGES); 33 | 34 | /* 35 | No cycles initially: 36 | 37 | 0 -> 7 38 | -> 1 -> 2 -> 4 39 | -> 3 -> 5 40 | */ 41 | 42 | cycleProcessor = new DirectedCycle(graph); 43 | }); 44 | 45 | it('should be extensible', () => { 46 | expect(cycleProcessor).to.be.extensible; 47 | }); 48 | 49 | it('should have properties granted from constructor call', () => { 50 | expect(cycleProcessor).to.have.all.keys( 51 | 'graph', 52 | 'checkCycle', 53 | ); 54 | }); 55 | 56 | it('should have a graph of type object', () => { 57 | expect(cycleProcessor.graph).to.be.an('object'); 58 | }); 59 | 60 | describe('#hasCycle()', () => { 61 | it('should return false if the graph does not contain a cycle', () => { 62 | expect(cycleProcessor.hasCycle()).to.be.false; 63 | }); 64 | 65 | it('should return true if the graph contains a cycle', () => { 66 | graph.addEdge([5, 0]); 67 | 68 | /* 69 | Now the graph has a cycle where 5 points to 0 70 | 71 | 0 -> 7 72 | -> 1 -> 2 -> 4 73 | -> 3 -> 5 -> cycle 0 74 | */ 75 | 76 | cycleProcessor = new DirectedCycle(graph); 77 | 78 | expect(cycleProcessor.hasCycle()).to.be.true; 79 | }); 80 | 81 | it('should return true if the graph contains a self loop', () => { 82 | graph.addEdge([5, 5]); 83 | 84 | /* 85 | Now the graph has a cycle where 5 points to 5 86 | 87 | 0 -> 7 88 | -> 1 -> 2 -> 4 89 | -> 3 -> 5 -> cycle 5 90 | */ 91 | 92 | cycleProcessor = new DirectedCycle(graph); 93 | 94 | expect(cycleProcessor.hasCycle()).to.be.true; 95 | }); 96 | 97 | it('should return true if the graph contains a back edge', () => { 98 | graph.addEdge([5, 8]); 99 | graph.addEdge([8, 5]); 100 | 101 | /* 102 | Now the graph has a cycle where 8 points to 5 103 | 104 | 0 -> 7 105 | -> 1 -> 2 -> 4 106 | -> 3 -> 5 -> 8 -> cycle 5 107 | */ 108 | 109 | cycleProcessor = new DirectedCycle(graph); 110 | 111 | expect(cycleProcessor.hasCycle()).to.be.true; 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/processors/graph.unweighted.directed.depth-first-paths.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let DirectedGraph; 4 | let graph; 5 | 6 | let DepthFirstPaths; 7 | let paths; 8 | 9 | const SOURCE_VERTEX = 0; 10 | const TEST_EDGES = [ 11 | [0, 5], 12 | [4, 3], 13 | [0, 1], 14 | [9, 12], 15 | [6, 4], 16 | [5, 4], 17 | [0, 2], 18 | [11, 12], 19 | [9, 10], 20 | [0, 6], 21 | [7, 8], 22 | [9, 11], 23 | [5, 3], 24 | [5, 5], 25 | [14, 14], 26 | ]; 27 | 28 | try { 29 | DirectedGraph = require('../../structures/graph.unweighted.directed'); 30 | graph = new DirectedGraph(TEST_EDGES); 31 | 32 | DepthFirstPaths = require('../../processors/graph.unweighted.depth-first-paths'); 33 | paths = new DepthFirstPaths(graph, SOURCE_VERTEX); 34 | } catch (e) { 35 | throw new Error('Directed DepthFirstPaths could not be tested due to ' + 36 | 'faulty import, likely from an incorrect file path or exporting a ' + 37 | 'non-constructor from the processor or graph files.'); 38 | } 39 | 40 | describe('Directed DepthFirstPaths', () => { 41 | beforeEach(() => { 42 | graph = new DirectedGraph(TEST_EDGES); 43 | paths = new DepthFirstPaths(graph, SOURCE_VERTEX); 44 | }); 45 | 46 | it('should be extensible', () => { 47 | expect(paths).to.be.extensible; 48 | }); 49 | 50 | it('should have properties granted from constructor call', () => { 51 | expect(paths).to.have.all.keys( 52 | 'graph', 53 | 'parent', 54 | 'sourceVertex', 55 | 'visited', 56 | ); 57 | }); 58 | 59 | it('should set a string data type for source vertex', () => { 60 | expect(paths.sourceVertex).to.equal(String(SOURCE_VERTEX)); 61 | }); 62 | 63 | it('should process only vertices connected to the source vertex', () => { 64 | expect(paths.visited.has('0')).to.be.true; 65 | expect(paths.visited.has('5')).to.be.true; 66 | expect(paths.visited.has('4')).to.be.true; 67 | expect(paths.visited.has('3')).to.be.true; 68 | expect(paths.visited.has('6')).to.be.true; 69 | expect(paths.visited.has('1')).to.be.true; 70 | expect(paths.visited.has('2')).to.be.true; 71 | expect(paths.visited.has('7')).to.be.false; 72 | expect(paths.visited.has('8')).to.be.false; 73 | expect(paths.visited.has('9')).to.be.false; 74 | expect(paths.visited.has('11')).to.be.false; 75 | expect(paths.visited.has('12')).to.be.false; 76 | expect(paths.visited.has('14')).to.be.false; 77 | }); 78 | 79 | it('should throw an error if the source vertex is not in the graph', () => { 80 | graph = new DirectedGraph(); 81 | 82 | expect(() => new DepthFirstPaths(graph, SOURCE_VERTEX)).to.throw(Error); 83 | }); 84 | 85 | it('should work for number and string data types', () => { 86 | // Arrange 87 | const edges = [ 88 | ['dog', 'woof'], 89 | ['dog', 'bark'], 90 | [0, 'meow'], 91 | ['meow', 'cat'], 92 | [14, 'dog'], 93 | ]; 94 | 95 | graph = new DirectedGraph(edges); 96 | 97 | // Act 98 | paths = new DepthFirstPaths(graph, SOURCE_VERTEX); 99 | 100 | // Assert 101 | expect(paths.visited.has('0')).to.be.true; 102 | expect(paths.visited.has('meow')).to.be.true; 103 | expect(paths.visited.has('cat')).to.be.true; 104 | expect(paths.visited.has('14')).to.be.false; 105 | expect(paths.visited.has('dog')).to.be.false; 106 | expect(paths.visited.has('woof')).to.be.false; 107 | expect(paths.visited.has('bark')).to.be.false; 108 | }); 109 | 110 | describe('#hasPathTo', () => { 111 | it('should return true if the input vertex is connected to the source vertex', () => { 112 | expect(paths.hasPathTo('6')).to.be.true; 113 | }); 114 | 115 | it('should return false if the input vertex is not connected to the source vertex', () => { 116 | expect(paths.hasPathTo('11')).to.be.false; 117 | }); 118 | 119 | it('should throw an error if the input vertex is not in the graph', () => { 120 | expect(() => paths.hasPathTo('not in graph')).to.throw(Error); 121 | }); 122 | }); 123 | 124 | describe('#pathTo', () => { 125 | it('should return null if a path to the source vertex does not exist', () => { 126 | expect(paths.pathTo(11)).to.equal(null); 127 | }); 128 | 129 | it('should return the path to the source vertex if it exists', () => { 130 | expect(paths.pathTo(6)).to.deep.equal(['6', '0']); 131 | }); 132 | }); 133 | }); -------------------------------------------------------------------------------- /test/processors/graph.unweighted.directed.topological-sort.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let DirectedGraph; 4 | let graph; 5 | 6 | let TopologicalSort; 7 | let topologicalProcessor; 8 | 9 | const TEST_EDGES = [ 10 | [0, 7], 11 | [0, 1], 12 | [1, 2], 13 | [2, 4], 14 | [1, 3], 15 | [3, 5], 16 | ]; 17 | 18 | try { 19 | DirectedGraph = require('../../structures/graph.unweighted.directed'); 20 | graph = new DirectedGraph(TEST_EDGES); 21 | 22 | TopologicalSort = require('../../processors/graph.unweighted.directed.topological-sort'); 23 | topologicalProcessor = new TopologicalSort(graph); 24 | } catch (e) { 25 | throw new Error('TopologicalSort could not be tested due to faulty import, ' + 26 | 'likely from an incorrect file path or exporting a non-constructor from ' + 27 | 'the processor or graph files.'); 28 | } 29 | 30 | describe('TopologicalSort', () => { 31 | beforeEach(() => { 32 | graph = new DirectedGraph(TEST_EDGES); 33 | 34 | /* 35 | 0 -> 7 36 | -> 1 -> 2 -> 4 37 | -> 3 -> 5 38 | */ 39 | 40 | topologicalProcessor = new TopologicalSort(graph); 41 | }); 42 | 43 | it('should be extensible', () => { 44 | expect(topologicalProcessor).to.be.extensible; 45 | }); 46 | 47 | it('should have properties granted from constructor call', () => { 48 | expect(topologicalProcessor).to.have.all.keys( 49 | 'graph', 50 | 'sorted', 51 | ); 52 | }); 53 | 54 | it('should have a graph of type object', () => { 55 | expect(topologicalProcessor.graph).to.be.an('object'); 56 | }); 57 | 58 | it('should topologically sort the graph', () => { 59 | expect(topologicalProcessor.sorted).to.deep.equal( 60 | ['7', '4', '2', '5', '3', '1', '0'] 61 | ); 62 | }); 63 | 64 | it('should throw an error if the graph contains a cycle', () => { 65 | graph.addEdge([5, 0]); 66 | 67 | /* 68 | Now the graph has a cycle where 5 points to 0 69 | 70 | 0 -> 7 71 | -> 1 -> 2 -> 4 72 | -> 3 -> 5 -> cycle 0 73 | */ 74 | 75 | expect(() => new TopologicalSort(graph)).to.throw(Error); 76 | }); 77 | 78 | it('should throw an error if the graph contains a self loop', () => { 79 | graph.addEdge([5, 5]); 80 | 81 | /* 82 | Now the graph has a cycle where 5 points to 5 83 | 84 | 0 -> 7 85 | -> 1 -> 2 -> 4 86 | -> 3 -> 5 -> cycle 5 87 | */ 88 | 89 | expect(() => new TopologicalSort(graph)).to.throw(Error); 90 | }); 91 | 92 | it('should throw an error if the graph contains a back edge', () => { 93 | graph.addEdge([5, 8]); 94 | graph.addEdge([8, 5]); 95 | 96 | /* 97 | Now the graph has a cycle where 8 points to 5 98 | 99 | 0 -> 7 100 | -> 1 -> 2 -> 4 101 | -> 3 -> 5 -> 8 -> cycle 5 102 | */ 103 | 104 | expect(() => new TopologicalSort(graph)).to.throw(Error); 105 | }); 106 | 107 | describe('#getTopologicalOrder()', () => { 108 | it('should return an array', () => { 109 | expect(topologicalProcessor.getTopologicalOrder()).to.be.instanceOf(Array); 110 | }); 111 | 112 | it('should return the vertices in topological order', () => { 113 | expect(topologicalProcessor.getTopologicalOrder()).to.deep.equal( 114 | ['7', '4', '2', '5', '3', '1', '0'] 115 | ); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/processors/graph.unweighted.undirected.breadth-first-paths.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let UndirectedGraph; 4 | let graph; 5 | 6 | let BreadthFirstPaths; 7 | let paths; 8 | 9 | const SOURCE_VERTEX = 0; 10 | const TEST_EDGES = [ 11 | [0, 5], 12 | [4, 3], 13 | [0, 1], 14 | [9, 12], 15 | [6, 4], 16 | [5, 4], 17 | [0, 2], 18 | [11, 12], 19 | [9, 10], 20 | [0, 6], 21 | [7, 8], 22 | [9, 11], 23 | [5, 3], 24 | [5, 5], 25 | [14, 14], 26 | ]; 27 | 28 | try { 29 | UndirectedGraph = require('../../structures/graph.unweighted.undirected'); 30 | graph = new UndirectedGraph(TEST_EDGES); 31 | 32 | BreadthFirstPaths = require('../../processors/graph.unweighted.breadth-first-paths'); 33 | paths = new BreadthFirstPaths(graph, SOURCE_VERTEX); 34 | } catch (e) { 35 | throw new Error('Undirected BreadthFirstPaths could not be tested due to ' + 36 | 'faulty import, likely from an incorrect file path or exporting a ' + 37 | 'non- constructor from the processor or graph files.'); 38 | } 39 | 40 | describe('BreadthFirstPaths', () => { 41 | beforeEach(() => { 42 | graph = new UndirectedGraph(TEST_EDGES); 43 | paths = new BreadthFirstPaths(graph, SOURCE_VERTEX); 44 | }); 45 | 46 | it('should be extensible', () => { 47 | expect(paths).to.be.extensible; 48 | }); 49 | 50 | it('should have properties granted from constructor call', () => { 51 | expect(paths).to.have.all.keys( 52 | 'distanceFromSource', 53 | 'graph', 54 | 'parent', 55 | 'sourceVertex', 56 | 'visited', 57 | ); 58 | }); 59 | 60 | it('should set a string data type for source vertex', () => { 61 | expect(paths.sourceVertex).to.equal(String(SOURCE_VERTEX)); 62 | }); 63 | 64 | it('should process only vertices connected to the source vertex', () => { 65 | expect(paths.visited.has('0')).to.be.true; 66 | expect(paths.visited.has('5')).to.be.true; 67 | expect(paths.visited.has('4')).to.be.true; 68 | expect(paths.visited.has('3')).to.be.true; 69 | expect(paths.visited.has('6')).to.be.true; 70 | expect(paths.visited.has('1')).to.be.true; 71 | expect(paths.visited.has('2')).to.be.true; 72 | expect(paths.visited.has('7')).to.be.false; 73 | expect(paths.visited.has('8')).to.be.false; 74 | expect(paths.visited.has('9')).to.be.false; 75 | expect(paths.visited.has('11')).to.be.false; 76 | expect(paths.visited.has('12')).to.be.false; 77 | expect(paths.visited.has('14')).to.be.false; 78 | }); 79 | 80 | it('should throw an error if the source vertex is not in the graph', () => { 81 | graph = new UndirectedGraph(); 82 | 83 | expect(() => new BreadthFirstPaths(graph, SOURCE_VERTEX)).to.throw(Error); 84 | }); 85 | 86 | it('should work for number and string data types', () => { 87 | // Arrange 88 | const edges = [ 89 | ['dog', 'woof'], 90 | ['dog', 'bark'], 91 | [0, 'meow'], 92 | ['meow', 'cat'], 93 | [14, 'dog'], 94 | ]; 95 | 96 | graph = new UndirectedGraph(edges); 97 | 98 | // Act 99 | paths = new BreadthFirstPaths(graph, SOURCE_VERTEX); 100 | 101 | // Assert 102 | expect(paths.visited.has('0')).to.be.true; 103 | expect(paths.visited.has('meow')).to.be.true; 104 | expect(paths.visited.has('cat')).to.be.true; 105 | expect(paths.visited.has('14')).to.be.false; 106 | expect(paths.visited.has('dog')).to.be.false; 107 | expect(paths.visited.has('woof')).to.be.false; 108 | expect(paths.visited.has('bark')).to.be.false; 109 | }); 110 | 111 | describe('#distanceTo', () => { 112 | it('should return the distance if one level from the source vertex', () => { 113 | expect(paths.distanceTo(6)).to.equal(1); 114 | }); 115 | 116 | it('should return the distance if two levels from the source vertex', () => { 117 | expect(paths.distanceTo(4)).to.equal(2); 118 | }); 119 | 120 | it('should return null if the input vertex is not connected to the source', () => { 121 | expect(paths.distanceTo(14)).to.equal(null); 122 | }); 123 | }); 124 | 125 | describe('#hasPathTo', () => { 126 | it('should return true if the input vertex is connected to the source vertex', () => { 127 | expect(paths.hasPathTo('6')).to.be.true; 128 | }); 129 | 130 | it('should return false if the input vertex is not connected to the source vertex', () => { 131 | expect(paths.hasPathTo('11')).to.be.false; 132 | }); 133 | 134 | it('should throw an error if the input vertex is not in the graph', () => { 135 | expect(() => paths.hasPathTo('not in graph')).to.throw(Error); 136 | }); 137 | }); 138 | 139 | describe('#shortestPathTo', () => { 140 | it('should return null if a path to the source vertex does not exist', () => { 141 | expect(paths.shortestPathTo(11)).to.equal(null); 142 | }); 143 | 144 | it('should return the shortest path to the source vertex if a path exists', () => { 145 | expect(paths.shortestPathTo(6)).to.deep.equal(['6', '0']); 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /test/processors/graph.unweighted.undirected.connected-components.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let UndirectedGraph; 4 | let graph; 5 | 6 | let ConnectedComponents; 7 | let connected; 8 | 9 | const TEST_EDGES = [ 10 | [0, 5], 11 | [4, 3], 12 | [0, 1], 13 | [9, 12], 14 | [6, 4], 15 | [5, 4], 16 | [0, 2], 17 | [11, 12], 18 | [9, 10], 19 | [0, 6], 20 | [7, 8], 21 | [9, 11], 22 | [5, 3], 23 | [5, 5], 24 | [14, 14], 25 | ]; 26 | 27 | try { 28 | UndirectedGraph = require('../../structures/graph.unweighted.undirected'); 29 | graph = new UndirectedGraph(TEST_EDGES); 30 | 31 | ConnectedComponents = require('../../processors/graph.unweighted.undirected.connected-components'); 32 | connected = new ConnectedComponents(graph); 33 | } catch (e) { 34 | throw new Error('ConnectedComponents could not be tested due to faulty import, ' + 35 | 'likely from an incorrect file path or exporting a non-constructor from ' + 36 | 'the processor or graph files.'); 37 | } 38 | 39 | describe('ConnectedComponents', () => { 40 | beforeEach(() => { 41 | graph = new UndirectedGraph(TEST_EDGES); 42 | connected = new ConnectedComponents(graph); 43 | }); 44 | 45 | it('should be extensible', () => { 46 | expect(connected).to.be.extensible; 47 | }); 48 | 49 | it('should have properties granted from constructor call', () => { 50 | expect(connected).to.have.all.keys( 51 | 'componentCount', 52 | 'graph', 53 | 'id', 54 | ); 55 | }); 56 | 57 | it('should process all vertices in the graph', () => { 58 | const graphVertices = Object.keys(graph.adjacencyList); 59 | const processedVertices = Object.keys(connected.id); 60 | 61 | expect(graphVertices).to.deep.equal(processedVertices); 62 | }); 63 | 64 | it('should work for number and string data types', () => { 65 | // Arrange 66 | const edges = [ 67 | ['dog', 'woof'], 68 | ['dog', 'bark'], 69 | ['cat', 'meow'], 70 | [0, 'meow'], 71 | [14, 'dog'], 72 | ]; 73 | 74 | graph = new UndirectedGraph(edges); 75 | 76 | // Act 77 | connected = new ConnectedComponents(graph); 78 | 79 | // Assert 80 | expect(connected.id.hasOwnProperty(0)).to.be.true; 81 | expect(connected.id.hasOwnProperty('meow')).to.be.true; 82 | expect(connected.id.hasOwnProperty('cat')).to.be.true; 83 | expect(connected.id.hasOwnProperty(14)).to.be.true; 84 | expect(connected.id.hasOwnProperty('dog')).to.be.true; 85 | expect(connected.id.hasOwnProperty('woof')).to.be.true; 86 | expect(connected.id.hasOwnProperty('bark')).to.be.true; 87 | }); 88 | 89 | describe('#getComponentCount', () => { 90 | it('should return the number of components in the graph', () => { 91 | expect(connected.getComponentCount()).to.equal(4); 92 | }); 93 | }); 94 | 95 | describe('#getComponentId', () => { 96 | it('should return the component id for a vertex in the first component', () => { 97 | expect(connected.getComponentId(6)).to.equal(0); 98 | }); 99 | 100 | it('should return the component id for a vertex in the last component', () => { 101 | expect(connected.getComponentId(14)).to.equal(3); 102 | }); 103 | 104 | it('should throw an error if the vertex is not in the graph', () => { 105 | expect(() => connected.getComponentId('not in graph')).to.throw(Error); 106 | }); 107 | }); 108 | }); -------------------------------------------------------------------------------- /test/processors/graph.unweighted.undirected.cycle.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let UndirectedGraph; 4 | let graph; 5 | 6 | let UndirectedCycle; 7 | let cycleProcessor; 8 | 9 | const TEST_EDGES = [ 10 | [0, 7], 11 | [0, 1], 12 | [1, 2], 13 | [2, 4], 14 | [1, 3], 15 | [3, 5], 16 | ]; 17 | 18 | try { 19 | UndirectedGraph = require('../../structures/graph.unweighted.undirected'); 20 | graph = new UndirectedGraph(TEST_EDGES); 21 | 22 | UndirectedCycle = require('../../processors/graph.unweighted.undirected.cycle'); 23 | cycleProcessor = new UndirectedCycle(graph); 24 | } catch (e) { 25 | throw new Error('UndirectedCycle could not be tested due to faulty import, ' + 26 | 'likely from an incorrect file path or exporting a non-constructor from ' + 27 | 'the processor or graph files.'); 28 | } 29 | 30 | describe('UndirectedCycle', () => { 31 | beforeEach(() => { 32 | graph = new UndirectedGraph(TEST_EDGES); 33 | 34 | /* 35 | No cycles initially: 36 | 37 | 0 <-> 7 38 | <-> 1 <-> 2 <-> 4 39 | <-> 3 <-> 5 40 | */ 41 | 42 | cycleProcessor = new UndirectedCycle(graph); 43 | }); 44 | 45 | it('should be extensible', () => { 46 | expect(cycleProcessor).to.be.extensible; 47 | }); 48 | 49 | it('should have properties granted from constructor call', () => { 50 | expect(cycleProcessor).to.have.all.keys( 51 | 'graph', 52 | 'checkCycle', 53 | ); 54 | }); 55 | 56 | it('should have a graph of type object', () => { 57 | expect(cycleProcessor.graph).to.be.an('object'); 58 | }); 59 | 60 | describe('#hasCycle()', () => { 61 | it('should return false if the graph does not contain a cycle', () => { 62 | expect(cycleProcessor.hasCycle()).to.be.false; 63 | }); 64 | 65 | it('should detect a self-loop cycle', () => { 66 | graph = new UndirectedGraph(); 67 | graph.addEdge([5, 5]); 68 | cycleProcessor = new UndirectedCycle(graph); 69 | 70 | expect(cycleProcessor.hasCycle()).to.be.true; 71 | }); 72 | 73 | it('should detect a parallel edge cycle', () => { 74 | graph = new UndirectedGraph(); 75 | graph.addEdge([0, 5]); 76 | graph.addEdge([0, 5]); 77 | cycleProcessor = new UndirectedCycle(graph); 78 | 79 | expect(cycleProcessor.hasCycle()).to.be.true; 80 | }); 81 | 82 | it('should detect a typical cycle', () => { 83 | graph.addEdge([5, 0]); 84 | 85 | /* 86 | Now the graph has a cycle at the edge shared by 5 and 0 87 | 88 | 0 <-> 7 89 | <-> 1 <-> 2 <-> 4 90 | <-> 3 <-> 5 <-> cycle 0 91 | */ 92 | 93 | cycleProcessor = new UndirectedCycle(graph); 94 | 95 | expect(cycleProcessor.hasCycle()).to.be.true; 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/processors/graph.unweighted.undirected.depth-first-paths.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let UndirectedGraph; 4 | let graph; 5 | 6 | let DepthFirstPaths; 7 | let paths; 8 | 9 | const SOURCE_VERTEX = 0; 10 | const TEST_EDGES = [ 11 | [0, 5], 12 | [4, 3], 13 | [0, 1], 14 | [9, 12], 15 | [6, 4], 16 | [5, 4], 17 | [0, 2], 18 | [11, 12], 19 | [9, 10], 20 | [0, 6], 21 | [7, 8], 22 | [9, 11], 23 | [5, 3], 24 | [5, 5], 25 | [14, 14], 26 | ]; 27 | 28 | try { 29 | UndirectedGraph = require('../../structures/graph.unweighted.undirected'); 30 | graph = new UndirectedGraph(TEST_EDGES); 31 | 32 | DepthFirstPaths = require('../../processors/graph.unweighted.depth-first-paths'); 33 | paths = new DepthFirstPaths(graph, SOURCE_VERTEX); 34 | } catch (e) { 35 | throw new Error('Undirected DepthFirstPaths could not be tested due to ' + 36 | 'faulty import, likely from an incorrect file path or exporting a ' + 37 | 'non-constructor from the processor or graph files.'); 38 | } 39 | 40 | describe('Undirected DepthFirstPaths', () => { 41 | beforeEach(() => { 42 | graph = new UndirectedGraph(TEST_EDGES); 43 | paths = new DepthFirstPaths(graph, SOURCE_VERTEX); 44 | }); 45 | 46 | it('should be extensible', () => { 47 | expect(paths).to.be.extensible; 48 | }); 49 | 50 | it('should have properties granted from constructor call', () => { 51 | expect(paths).to.have.all.keys( 52 | 'graph', 53 | 'parent', 54 | 'sourceVertex', 55 | 'visited', 56 | ); 57 | }); 58 | 59 | it('should set a string data type for source vertex', () => { 60 | expect(paths.sourceVertex).to.equal(String(SOURCE_VERTEX)); 61 | }); 62 | 63 | it('should process only vertices connected to the source vertex', () => { 64 | expect(paths.visited.has('0')).to.be.true; 65 | expect(paths.visited.has('5')).to.be.true; 66 | expect(paths.visited.has('4')).to.be.true; 67 | expect(paths.visited.has('3')).to.be.true; 68 | expect(paths.visited.has('6')).to.be.true; 69 | expect(paths.visited.has('1')).to.be.true; 70 | expect(paths.visited.has('2')).to.be.true; 71 | expect(paths.visited.has('7')).to.be.false; 72 | expect(paths.visited.has('8')).to.be.false; 73 | expect(paths.visited.has('9')).to.be.false; 74 | expect(paths.visited.has('11')).to.be.false; 75 | expect(paths.visited.has('12')).to.be.false; 76 | expect(paths.visited.has('14')).to.be.false; 77 | }); 78 | 79 | it('should throw an error if the source vertex is not in the graph', () => { 80 | graph = new UndirectedGraph(); 81 | 82 | expect(() => new DepthFirstPaths(graph, SOURCE_VERTEX)).to.throw(Error); 83 | }); 84 | 85 | it('should work for number and string data types', () => { 86 | // Arrange 87 | const edges = [ 88 | ['dog', 'woof'], 89 | ['dog', 'bark'], 90 | [0, 'meow'], 91 | ['meow', 'cat'], 92 | [14, 'dog'], 93 | ]; 94 | 95 | graph = new UndirectedGraph(edges); 96 | 97 | // Act 98 | paths = new DepthFirstPaths(graph, SOURCE_VERTEX); 99 | 100 | // Assert 101 | expect(paths.visited.has('0')).to.be.true; 102 | expect(paths.visited.has('meow')).to.be.true; 103 | expect(paths.visited.has('cat')).to.be.true; 104 | expect(paths.visited.has('14')).to.be.false; 105 | expect(paths.visited.has('dog')).to.be.false; 106 | expect(paths.visited.has('woof')).to.be.false; 107 | expect(paths.visited.has('bark')).to.be.false; 108 | }); 109 | 110 | describe('#hasPathTo', () => { 111 | it('should return true if the input vertex is connected to the source vertex', () => { 112 | expect(paths.hasPathTo('6')).to.be.true; 113 | }); 114 | 115 | it('should return false if the input vertex is not connected to the source vertex', () => { 116 | expect(paths.hasPathTo('11')).to.be.false; 117 | }); 118 | 119 | it('should throw an error if the input vertex is not in the graph', () => { 120 | expect(() => paths.hasPathTo('not in graph')).to.throw(Error); 121 | }); 122 | }); 123 | 124 | describe('#pathTo', () => { 125 | it('should return null if a path to the source vertex does not exist', () => { 126 | expect(paths.pathTo(11)).to.equal(null); 127 | }); 128 | 129 | it('should return the path to the source vertex if it exists', () => { 130 | expect(paths.pathTo(6)).to.deep.equal(['6', '4', '5', '0']); 131 | }); 132 | }); 133 | }); -------------------------------------------------------------------------------- /test/structures/graph.edge-weighted.directed.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let EdgeWeightedDirectedGraph; 4 | let graph; 5 | 6 | try { 7 | EdgeWeightedDirectedGraph = require('../../structures/graph.edge-weighted.directed'); 8 | graph = new EdgeWeightedDirectedGraph(); 9 | } catch (e) { 10 | throw new Error('EdgeWeightedDirectedGraph could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('EdgeWeightedDirectedGraph', () => { 15 | beforeEach(() => { 16 | graph = new EdgeWeightedDirectedGraph(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(graph).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(graph).to.have.all.keys('adjacencyList', 'totalEdges', 'totalVertices'); 25 | }); 26 | 27 | it('should initially have an adjacency list that is an empty object', () => { 28 | expect(graph.adjacencyList).to.deep.equal({}); 29 | }); 30 | 31 | it('should initially have 0 total vertices', () => { 32 | expect(graph.totalVertices).to.equal(0); 33 | }); 34 | 35 | it('should initially have 0 total edges', () => { 36 | expect(graph.totalEdges).to.equal(0); 37 | }); 38 | 39 | describe('#addEdge', () => { 40 | it('should add edges for new vertices', () => { 41 | graph.addEdge([0, 5, 0.1]); 42 | 43 | expect(graph.adjacencyList[0]).to.deep.equal([{ v1: '0', v2: '5', weight: 0.1 }]); 44 | }); 45 | 46 | it('should allow parallel edges', () => { 47 | graph.addEdge([0, 5, 0.1]); 48 | graph.addEdge([0, 5, 0.1]); 49 | 50 | expect(graph.adjacencyList[0]).to.deep.equal([ 51 | { v1: '0', v2: '5', weight: 0.1 }, 52 | { v1: '0', v2: '5', weight: 0.1 }, 53 | ]); 54 | }); 55 | 56 | it('should allow self-loops', () => { 57 | graph.addEdge([5, 5, 0.1]); 58 | 59 | expect(graph.adjacencyList[5]).to.deep.equal([ 60 | { v1: '5', v2: '5', weight: 0.1 } 61 | ]); 62 | }); 63 | 64 | it('should increment the total number of edges in the graph by 1', () => { 65 | graph.addEdge([0, 5, 0.1]); 66 | 67 | expect(graph.totalEdges).to.equal(1); 68 | }); 69 | }); 70 | 71 | describe('#addVertex', () => { 72 | it('should store the vertex as a key in the adjacency list with an empty array value', () => { 73 | graph.addVertex(0); 74 | 75 | expect(graph.adjacencyList[0]).to.deep.equal([]); 76 | }); 77 | 78 | it('should increment the total number of vertices in the graph by 1', () => { 79 | graph.addVertex(0); 80 | 81 | expect(graph.totalVertices).to.equal(1); 82 | }); 83 | 84 | it('should throw an error if the vertex already exists in the graph', () => { 85 | graph.addVertex(0); 86 | 87 | expect(() => graph.addVertex(0)).to.throw(Error); 88 | }); 89 | }); 90 | 91 | describe('#adjacentVertices', () => { 92 | it('should return an array of vertices adjacent to the input vertex', () => { 93 | graph.addEdge([0, 5, 0.1]); 94 | graph.addEdge([0, 1, 0.2]); 95 | graph.addEdge([6, 0, 0.3]); 96 | 97 | expect(graph.adjacentVertices(0)).to.deep.equal([ 98 | { v1: '0', v2: '5', weight: 0.1 }, 99 | { v1: '0', v2: '1', weight: 0.2 }, 100 | ]); 101 | }); 102 | 103 | it('should throw an error if the vertex does not exist in the graph', () => { 104 | expect(() => graph.adjacentVertices(0)).to.throw(Error); 105 | }); 106 | }); 107 | }); -------------------------------------------------------------------------------- /test/structures/graph.edge-weighted.undirected.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let EdgeWeightedUndirectedGraph; 4 | let graph; 5 | 6 | try { 7 | EdgeWeightedUndirectedGraph = require('../../structures/graph.edge-weighted.undirected'); 8 | graph = new EdgeWeightedUndirectedGraph(); 9 | } catch (e) { 10 | throw new Error('EdgeWeightedUndirectedGraph could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('EdgeWeightedUndirectedGraph', () => { 15 | beforeEach(() => { 16 | graph = new EdgeWeightedUndirectedGraph(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(graph).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(graph).to.have.all.keys('adjacencyList', 'totalEdges', 'totalVertices'); 25 | }); 26 | 27 | it('should initially have an adjacency list that is an empty object', () => { 28 | expect(graph.adjacencyList).to.deep.equal({}); 29 | }); 30 | 31 | it('should initially have 0 total vertices', () => { 32 | expect(graph.totalVertices).to.equal(0); 33 | }); 34 | 35 | it('should initially have 0 total edges', () => { 36 | expect(graph.totalEdges).to.equal(0); 37 | }); 38 | 39 | describe('#addEdge', () => { 40 | it('should add edges for new vertices', () => { 41 | graph.addEdge([0, 5, 0.1]); 42 | 43 | expect(graph.adjacencyList[0]).to.deep.equal([{ v1: '0', v2: '5', weight: 0.1 }]); 44 | expect(graph.adjacencyList[5]).to.deep.equal([{ v1: '0', v2: '5', weight: 0.1 }]); 45 | }); 46 | 47 | it('should allow parallel edges', () => { 48 | graph.addEdge([0, 5, 0.1]); 49 | graph.addEdge([0, 5, 0.1]); 50 | 51 | expect(graph.adjacencyList[0]).to.deep.equal([ 52 | { v1: '0', v2: '5', weight: 0.1 }, 53 | { v1: '0', v2: '5', weight: 0.1 }, 54 | ]); 55 | expect(graph.adjacencyList[5]).to.deep.equal([ 56 | { v1: '0', v2: '5', weight: 0.1 }, 57 | { v1: '0', v2: '5', weight: 0.1 }, 58 | ]); 59 | }); 60 | 61 | it('should allow self-loops and store value in adjacency list twice', () => { 62 | graph.addEdge([5, 5, 0.1]); 63 | 64 | expect(graph.adjacencyList[5]).to.deep.equal([ 65 | { v1: '5', v2: '5', weight: 0.1 }, 66 | { v1: '5', v2: '5', weight: 0.1 }, 67 | ]); 68 | }); 69 | 70 | it('should increment the total number of edges in the graph by 1', () => { 71 | graph.addEdge([0, 5, 0.1]); 72 | 73 | expect(graph.totalEdges).to.equal(1); 74 | }); 75 | }); 76 | 77 | describe('#addVertex', () => { 78 | it('should store the vertex as a key in the adjacency list with an empty array value', () => { 79 | graph.addVertex(0); 80 | 81 | expect(graph.adjacencyList[0]).to.deep.equal([]); 82 | }); 83 | 84 | it('should increment the total number of vertices in the graph by 1', () => { 85 | graph.addVertex(0); 86 | 87 | expect(graph.totalVertices).to.equal(1); 88 | }); 89 | 90 | it('should throw an error if the vertex already exists in the graph', () => { 91 | graph.addVertex(0); 92 | 93 | expect(() => graph.addVertex(0)).to.throw(Error); 94 | }); 95 | }); 96 | 97 | describe('#adjacentVertices', () => { 98 | it('should return an array of vertices adjacent to the input vertex', () => { 99 | graph.addEdge([0, 5, 0.1]); 100 | graph.addEdge([0, 1, 0.2]); 101 | graph.addEdge([6, 0, 0.3]); 102 | 103 | expect(graph.adjacentVertices(0)).to.deep.equal([ 104 | { v1: '0', v2: '5', weight: 0.1 }, 105 | { v1: '0', v2: '1', weight: 0.2 }, 106 | { v1: '6', v2: '0', weight: 0.3 }, 107 | ]); 108 | }); 109 | 110 | it('should throw an error if the vertex does not exist in the graph', () => { 111 | expect(() => graph.adjacentVertices(0)).to.throw(Error); 112 | }); 113 | }); 114 | 115 | describe('#averageDegree', () => { 116 | it('should calculate the average degree for all vertices in graph', () => { 117 | // Arrange 118 | const edges = [ 119 | [0, 5, 0.1], 120 | [4, 3, 0.1], 121 | [0, 1, 0.1], 122 | [9, 12, 0.1], 123 | [6, 4, 0.1], 124 | [5, 4, 0.1], 125 | [0, 2, 0.1], 126 | [11, 12, 0.1], 127 | [9, 10, 0.1], 128 | [0, 6, 0.1], 129 | [7, 8, 0.1], 130 | [9, 11, 0.1], 131 | [5, 3, 0.1], 132 | [5, 5, 0.1], 133 | [14, 14, 0.1], 134 | ]; 135 | 136 | edges.forEach(edge => graph.addEdge(edge)); 137 | 138 | // Raw calculations compare against method's mathematical solution 139 | let totalDegrees = 0; 140 | Object.keys(graph.adjacencyList).forEach(vertex => { 141 | graph.adjacencyList[vertex].forEach(v2 => { 142 | totalDegrees++; 143 | }); 144 | }); 145 | 146 | const averageDegree = totalDegrees / Object.keys(graph.adjacencyList).length; 147 | 148 | // Act, Assert 149 | expect(graph.averageDegree()).to.equal(averageDegree); 150 | }); 151 | 152 | it('should return 0 if the graph is empty', () => { 153 | expect(graph.averageDegree()).to.equal(0); 154 | }); 155 | }); 156 | 157 | describe('#degree', () => { 158 | it('should return the degree of the vertex', () => { 159 | graph.addEdge([0, 5, 0.1]); 160 | graph.addEdge([0, 1, 0.1]); 161 | 162 | expect(graph.degree(0)).to.equal(2); 163 | }); 164 | 165 | it('should throw an error if the vertex is not in the graph', () => { 166 | expect(() => graph.degree(0)).to.throw(Error); 167 | }); 168 | }); 169 | 170 | describe('#maxDegree', () => { 171 | it('should return the maximum degree in the graph', () => { 172 | const edges = [ 173 | [0, 5, 0.1], 174 | [4, 3, 0.1], 175 | [0, 1, 0.1], 176 | [9, 12, 0.1], 177 | [6, 4, 0.1], 178 | [5, 4, 0.1], 179 | [0, 2, 0.1], 180 | [11, 12, 0.1], 181 | [9, 10, 0.1], 182 | [0, 6, 0.1], 183 | [7, 8, 0.1], 184 | [9, 11, 0.1], 185 | [5, 3, 0.1], 186 | [5, 5, 0.1], 187 | [14, 14, 0.1], 188 | ]; 189 | 190 | edges.forEach(edge => graph.addEdge(edge)); 191 | 192 | expect(graph.maxDegree()).to.equal(5); 193 | }); 194 | 195 | it('should return 0 if the graph is empty', () => { 196 | expect(graph.maxDegree()).to.equal(0); 197 | }); 198 | }); 199 | 200 | describe('#numberOfSelfLoops', () => { 201 | it('should return the number of self loops in the graph', () => { 202 | graph.addEdge([0, 0, 0.1]); 203 | graph.addEdge([1, 1, 0.1]); 204 | graph.addEdge([2, 1, 0.1]); 205 | 206 | expect(graph.numberOfSelfLoops()).to.equal(2); 207 | }); 208 | 209 | it('should return 0 if the graph is empty', () => { 210 | expect(graph.numberOfSelfLoops()).to.equal(0); 211 | }); 212 | }); 213 | }); -------------------------------------------------------------------------------- /test/structures/graph.unweighted.directed.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let DirectedGraph; 4 | let graph; 5 | 6 | try { 7 | DirectedGraph = require('../../structures/graph.unweighted.directed'); 8 | graph = new DirectedGraph(); 9 | } catch (e) { 10 | throw new Error('DirectedGraph could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('DirectedGraph', () => { 15 | beforeEach(() => { 16 | graph = new DirectedGraph(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(graph).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(graph).to.have.all.keys('adjacencyList', 'totalEdges', 'totalVertices'); 25 | }); 26 | 27 | it('should initially have an adjacency list that is an empty object', () => { 28 | expect(graph.adjacencyList).to.deep.equal({}); 29 | }); 30 | 31 | it('should initially have 0 total vertices', () => { 32 | expect(graph.totalVertices).to.equal(0); 33 | }); 34 | 35 | it('should initially have 0 total edges', () => { 36 | expect(graph.totalEdges).to.equal(0); 37 | }); 38 | 39 | describe('#addEdge', () => { 40 | it('should add edges for new vertices in one direction', () => { 41 | graph.addEdge([0, 5]); 42 | 43 | expect(graph.adjacencyList[0]).to.deep.equal(['5']); 44 | expect(graph.adjacencyList[5]).to.deep.equal([]); 45 | }); 46 | 47 | it('should allow parallel edges', () => { 48 | graph.addEdge([0, 5]); 49 | graph.addEdge([0, 5]); 50 | 51 | expect(graph.adjacencyList[0]).to.deep.equal(['5', '5']); 52 | }); 53 | 54 | it('should allow self-loops', () => { 55 | graph.addEdge([5, 5]); 56 | 57 | expect(graph.adjacencyList[5]).to.deep.equal(['5']); 58 | }); 59 | 60 | it('should increment the total number of edges in the graph by 1', () => { 61 | graph.addEdge([0, 5]); 62 | 63 | expect(graph.totalEdges).to.equal(1); 64 | }); 65 | }); 66 | 67 | describe('#addVertex', () => { 68 | it('should store the vertex as a key in the adjacency list with an empty array value', () => { 69 | graph.addVertex(0); 70 | 71 | expect(graph.adjacencyList[0]).to.deep.equal([]); 72 | }); 73 | 74 | it('should increment the total number of vertices in the graph by 1', () => { 75 | graph.addVertex(0); 76 | 77 | expect(graph.totalVertices).to.equal(1); 78 | }); 79 | 80 | it('should throw an error if the vertex already exists in the graph', () => { 81 | graph.addVertex(0); 82 | 83 | expect(() => graph.addVertex(0)).to.throw(Error); 84 | }); 85 | }); 86 | 87 | describe('#adjacentVertices', () => { 88 | it('should return an array of vertices adjacent to the input vertex', () => { 89 | graph.addEdge([0, 5]); 90 | graph.addEdge([0, 1]); 91 | graph.addEdge([6, 0]); 92 | 93 | expect(graph.adjacentVertices(0)).to.deep.equal(['5', '1']); 94 | }); 95 | 96 | it('should throw an error if the vertex does not exist in the graph', () => { 97 | expect(() => graph.adjacentVertices(0)).to.throw(Error); 98 | }); 99 | }); 100 | 101 | describe('#averageDegree', () => { 102 | it('should calculate the average degree for all vertices in graph', () => { 103 | // Arrange 104 | const edges = [ 105 | [0, 5], 106 | [4, 3], 107 | [0, 1], 108 | [9, 12], 109 | [6, 4], 110 | [5, 4], 111 | [0, 2], 112 | [11, 12], 113 | [9, 10], 114 | [0, 6], 115 | [7, 8], 116 | [9, 11], 117 | [5, 3], 118 | [5, 5], 119 | [14, 14], 120 | ]; 121 | 122 | edges.forEach(edge => graph.addEdge(edge)); 123 | 124 | // Raw calculations compare against method's mathematical solution 125 | let totalDegrees = 0; 126 | Object.keys(graph.adjacencyList).forEach(vertex => { 127 | graph.adjacencyList[vertex].forEach(v2 => { 128 | totalDegrees++; 129 | }); 130 | }); 131 | 132 | const averageDegree = totalDegrees / Object.keys(graph.adjacencyList).length; 133 | 134 | // Act, Assert 135 | expect(graph.averageDegree()).to.equal(averageDegree); 136 | }); 137 | 138 | it('should return 0 if the graph is empty', () => { 139 | expect(graph.averageDegree()).to.equal(0); 140 | }); 141 | }); 142 | 143 | describe('#inDegree', () => { 144 | it('should return the indegree of the vertex', () => { 145 | graph.addEdge([5, 0]); 146 | graph.addEdge([2, 0]); 147 | graph.addEdge([0, 1]); 148 | 149 | expect(graph.inDegree(0)).to.equal(2); 150 | }); 151 | 152 | it('should throw an error if the vertex is not in the graph', () => { 153 | expect(() => graph.inDegree(0)).to.throw(Error); 154 | }); 155 | }); 156 | 157 | describe('#maxInDegree', () => { 158 | it('should return the maximum indegree in the graph', () => { 159 | const edges = [ 160 | [0, 5], 161 | [4, 3], 162 | [0, 1], 163 | [9, 12], 164 | [6, 4], 165 | [5, 4], 166 | [0, 2], 167 | [11, 12], 168 | [9, 10], 169 | [0, 6], 170 | [7, 8], 171 | [9, 11], 172 | [5, 3], 173 | [5, 5], 174 | [14, 14], 175 | ]; 176 | 177 | edges.forEach(edge => graph.addEdge(edge)); 178 | 179 | expect(graph.maxInDegree()).to.equal(2); 180 | }); 181 | 182 | it('should return 0 if the graph is empty', () => { 183 | expect(graph.maxInDegree()).to.equal(0); 184 | }); 185 | }); 186 | 187 | describe('#maxOutDegree', () => { 188 | it('should return the maximum outdegree in the graph', () => { 189 | const edges = [ 190 | [0, 5], 191 | [4, 3], 192 | [0, 1], 193 | [9, 12], 194 | [6, 4], 195 | [5, 4], 196 | [0, 2], 197 | [11, 12], 198 | [9, 10], 199 | [0, 6], 200 | [7, 8], 201 | [9, 11], 202 | [5, 3], 203 | [5, 5], 204 | [14, 14], 205 | ]; 206 | 207 | edges.forEach(edge => graph.addEdge(edge)); 208 | 209 | expect(graph.maxOutDegree()).to.equal(4); 210 | }); 211 | 212 | it('should return 0 if the graph is empty', () => { 213 | expect(graph.maxOutDegree()).to.equal(0); 214 | }); 215 | }); 216 | 217 | describe('#numberOfSelfLoops', () => { 218 | it('should return the number of self loops in the graph', () => { 219 | graph.addEdge([0, 0]); 220 | graph.addEdge([1, 1]); 221 | graph.addEdge([2, 1]); 222 | 223 | expect(graph.numberOfSelfLoops()).to.equal(2); 224 | }); 225 | 226 | it('should return 0 if the graph is empty', () => { 227 | expect(graph.numberOfSelfLoops()).to.equal(0); 228 | }); 229 | }); 230 | 231 | 232 | describe('#outDegree', () => { 233 | it('should return the outdegree of the vertex', () => { 234 | graph.addEdge([0, 5]); 235 | graph.addEdge([0, 1]); 236 | 237 | expect(graph.outDegree(0)).to.equal(2); 238 | }); 239 | 240 | it('should throw an error if the vertex is not in the graph', () => { 241 | expect(() => graph.outDegree(0)).to.throw(Error); 242 | }); 243 | }); 244 | 245 | describe('#reverse', () => { 246 | it('should add a new property to the graph', () => { 247 | graph.reverse(); 248 | 249 | expect(graph.reversedGraph).to.be.an('object'); 250 | }); 251 | 252 | it('should track all vertices from the original graph', () => { 253 | const originalVertices = Object.keys(graph.adjacencyList); 254 | 255 | graph.reverse(); 256 | const reversedVertices = Object.keys(graph.reversedGraph); 257 | 258 | expect(originalVertices).to.deep.equal(reversedVertices); 259 | }); 260 | 261 | it('should reverse the direction of the edges in the graph', () => { 262 | graph.addEdge([0, 1]); 263 | graph.addEdge([0, 2]); 264 | graph.addEdge([3, 0]); 265 | 266 | graph.reverse(); 267 | 268 | expect(graph.reversedGraph[0]).to.deep.equal(['3']); 269 | expect(graph.reversedGraph[1]).to.deep.equal(['0']); 270 | expect(graph.reversedGraph[2]).to.deep.equal(['0']); 271 | expect(graph.reversedGraph[3]).to.deep.equal([]); 272 | }); 273 | 274 | it('should return a new object that represents the reversed graph', () => { 275 | expect(graph.reverse()).to.be.an('object'); 276 | }); 277 | }); 278 | }); -------------------------------------------------------------------------------- /test/structures/graph.unweighted.undirected.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let UndirectedGraph; 4 | let graph; 5 | 6 | try { 7 | UndirectedGraph = require('../../structures/graph.unweighted.undirected'); 8 | graph = new UndirectedGraph(); 9 | } catch (e) { 10 | throw new Error('UndirectedGraph could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('UndirectedGraph', () => { 15 | beforeEach(() => { 16 | graph = new UndirectedGraph(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(graph).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(graph).to.have.all.keys('adjacencyList', 'totalEdges', 'totalVertices'); 25 | }); 26 | 27 | it('should initially have an adjacency list that is an empty object', () => { 28 | expect(graph.adjacencyList).to.deep.equal({}); 29 | }); 30 | 31 | it('should initially have 0 total vertices', () => { 32 | expect(graph.totalVertices).to.equal(0); 33 | }); 34 | 35 | it('should initially have 0 total edges', () => { 36 | expect(graph.totalEdges).to.equal(0); 37 | }); 38 | 39 | describe('#addEdge', () => { 40 | it('should add edges for new vertices', () => { 41 | graph.addEdge([0, 5]); 42 | 43 | expect(graph.adjacencyList[0]).to.deep.equal(['5']); 44 | expect(graph.adjacencyList[5]).to.deep.equal(['0']); 45 | }); 46 | 47 | it('should allow parallel edges', () => { 48 | graph.addEdge([0, 5]); 49 | graph.addEdge([0, 5]); 50 | 51 | expect(graph.adjacencyList[0]).to.deep.equal(['5', '5']); 52 | expect(graph.adjacencyList[5]).to.deep.equal(['0', '0']); 53 | }); 54 | 55 | it('should allow self-loops and store value in adjacency list twice', () => { 56 | graph.addEdge([5, 5]); 57 | 58 | expect(graph.adjacencyList[5]).to.deep.equal(['5', '5']); 59 | }); 60 | 61 | it('should increment the total number of edges in the graph by 1', () => { 62 | graph.addEdge([0, 5]); 63 | 64 | expect(graph.totalEdges).to.equal(1); 65 | }); 66 | }); 67 | 68 | describe('#addVertex', () => { 69 | it('should store the vertex as a key in the adjacency list with an empty array value', () => { 70 | graph.addVertex(0); 71 | 72 | expect(graph.adjacencyList[0]).to.deep.equal([]); 73 | }); 74 | 75 | it('should increment the total number of vertices in the graph by 1', () => { 76 | graph.addVertex(0); 77 | 78 | expect(graph.totalVertices).to.equal(1); 79 | }); 80 | 81 | it('should throw an error if the vertex already exists in the graph', () => { 82 | graph.addVertex(0); 83 | 84 | expect(() => graph.addVertex(0)).to.throw(Error); 85 | }); 86 | }); 87 | 88 | describe('#adjacentVertices', () => { 89 | it('should return an array of vertices adjacent to the input vertex', () => { 90 | graph.addEdge([0, 5]); 91 | graph.addEdge([0, 1]); 92 | graph.addEdge([6, 0]); 93 | 94 | expect(graph.adjacentVertices(0)).to.deep.equal(['5', '1', '6']); 95 | }); 96 | 97 | it('should throw an error if the vertex does not exist in the graph', () => { 98 | expect(() => graph.adjacentVertices(0)).to.throw(Error); 99 | }); 100 | }); 101 | 102 | describe('#averageDegree', () => { 103 | it('should calculate the average degree for all vertices in graph', () => { 104 | // Arrange 105 | const edges = [ 106 | [0, 5], 107 | [4, 3], 108 | [0, 1], 109 | [9, 12], 110 | [6, 4], 111 | [5, 4], 112 | [0, 2], 113 | [11, 12], 114 | [9, 10], 115 | [0, 6], 116 | [7, 8], 117 | [9, 11], 118 | [5, 3], 119 | [5, 5], 120 | [14, 14], 121 | ]; 122 | 123 | edges.forEach(edge => graph.addEdge(edge)); 124 | 125 | // Raw calculations compare against method's mathematical solution 126 | let totalDegrees = 0; 127 | Object.keys(graph.adjacencyList).forEach(vertex => { 128 | graph.adjacencyList[vertex].forEach(v2 => { 129 | totalDegrees++; 130 | }); 131 | }); 132 | 133 | const averageDegree = totalDegrees / Object.keys(graph.adjacencyList).length; 134 | 135 | // Act, Assert 136 | expect(graph.averageDegree()).to.equal(averageDegree); 137 | }); 138 | 139 | it('should return 0 if the graph is empty', () => { 140 | expect(graph.averageDegree()).to.equal(0); 141 | }); 142 | }); 143 | 144 | describe('#degree', () => { 145 | it('should return the degree of the vertex', () => { 146 | graph.addEdge([0, 5]); 147 | graph.addEdge([0, 1]); 148 | 149 | expect(graph.degree(0)).to.equal(2); 150 | }); 151 | 152 | it('should throw an error if the vertex is not in the graph', () => { 153 | expect(() => graph.degree(0)).to.throw(Error); 154 | }); 155 | }); 156 | 157 | describe('#maxDegree', () => { 158 | it('should return the maximum degree in the graph', () => { 159 | const edges = [ 160 | [0, 5], 161 | [4, 3], 162 | [0, 1], 163 | [9, 12], 164 | [6, 4], 165 | [5, 4], 166 | [0, 2], 167 | [11, 12], 168 | [9, 10], 169 | [0, 6], 170 | [7, 8], 171 | [9, 11], 172 | [5, 3], 173 | [5, 5], 174 | [14, 14], 175 | ]; 176 | 177 | edges.forEach(edge => graph.addEdge(edge)); 178 | 179 | expect(graph.maxDegree()).to.equal(5); 180 | }); 181 | 182 | it('should return 0 if the graph is empty', () => { 183 | expect(graph.maxDegree()).to.equal(0); 184 | }); 185 | }); 186 | 187 | describe('#numberOfSelfLoops', () => { 188 | it('should return the number of self loops in the graph', () => { 189 | graph.addEdge([0, 0]); 190 | graph.addEdge([1, 1]); 191 | graph.addEdge([2, 1]); 192 | 193 | expect(graph.numberOfSelfLoops()).to.equal(2); 194 | }); 195 | 196 | it('should return 0 if the graph is empty', () => { 197 | expect(graph.numberOfSelfLoops()).to.equal(0); 198 | }); 199 | }); 200 | }); -------------------------------------------------------------------------------- /test/structures/hash-table.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let HashTable; 4 | let table; 5 | 6 | try { 7 | HashTable = require('../../structures/hash-table'); 8 | table = new HashTable(); 9 | } catch (e) { 10 | throw new Error('Hash table could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('HashTable', () => { 15 | beforeEach(() => { 16 | table = new HashTable(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(table).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(table).to.have.all.keys('entries', 'size', 'storage'); 25 | }); 26 | 27 | describe('#get()', () => { 28 | it('should return the value associated with the key', () => { 29 | table.insert('dog', 'woof'); 30 | 31 | expect(table.get('dog')).to.equal('woof'); 32 | }); 33 | 34 | it('should return undefined if the key is not found in the table', () => { 35 | expect(table.get('dog')).to.equal(undefined); 36 | }); 37 | }); 38 | 39 | describe('#insert()', () => { 40 | it('should insert item into empty table', () => { 41 | table.insert('dog', 'woof'); 42 | 43 | expect(table.get('dog')).to.equal('woof'); 44 | }); 45 | 46 | it('should insert item into table containing one item', () => { 47 | table.insert('dog', 'woof'); 48 | table.insert('cat', 'meow'); 49 | 50 | expect(table.get('cat')).to.equal('meow'); 51 | }); 52 | 53 | it('should overwrite previous value if same key is used', () => { 54 | table.insert('dog', 'woof'); 55 | table.insert('dog', 'WOOOOOF'); 56 | 57 | expect(table.get('dog')).to.equal('WOOOOOF'); 58 | }); 59 | 60 | it('should double table size if number of entries exceeds 75% of size', () => { 61 | table.insert('dog', 'woof'); 62 | table.insert('cat', 'meow'); 63 | table.insert('cow', 'moo'); 64 | table.insert('fox', 'what does it say'); 65 | table.insert('cheetah', 'runs fast'); 66 | table.insert('dogs', 'woofs'); 67 | table.insert('cats', 'meows'); 68 | table.insert('cows', 'moos'); 69 | table.insert('foxes', 'what do they say'); 70 | table.insert('cheetahs', 'run fast'); 71 | table.insert('running', 'out of animals'); 72 | table.insert('have we resized yet', 'nope'); 73 | table.insert('now resized', 'yes'); 74 | 75 | expect(table.size).to.equal(32); 76 | }); 77 | 78 | it('should not change size of table', () => { 79 | table.insert('dog', 'woof'); 80 | 81 | expect(table.size).to.equal(16); 82 | }); 83 | 84 | it('should increment number of entries', () => { 85 | table.insert('dog', 'woof'); 86 | 87 | expect(table.entries).to.equal(1); 88 | }); 89 | }); 90 | 91 | describe('#remove()', () => { 92 | it('should remove the associated key-value pair from the table', () => { 93 | table.insert('dog', 'woof'); 94 | 95 | table.remove('dog'); 96 | 97 | expect(table.get('dog')).to.equal(undefined); 98 | }); 99 | 100 | it('should halve table size if number of entries drops below 25% of size', () => { 101 | table.insert('dog', 'woof'); 102 | table.insert('cat', 'meow'); 103 | table.insert('cow', 'moo'); 104 | table.insert('fox', 'what does it say'); 105 | 106 | table.remove('fox'); 107 | 108 | expect(table.size).to.equal(8); 109 | }); 110 | 111 | it('should halve table size if remove is used early in table lifecycle', () => { 112 | table.insert('dog', 'woof'); 113 | table.insert('cat', 'meow'); 114 | 115 | table.remove('cat'); 116 | 117 | expect(table.size).to.equal(8); 118 | }); 119 | 120 | it('should decrement number of entries', () => { 121 | table.insert('dog', 'woof'); 122 | table.insert('cat', 'meow'); 123 | 124 | table.remove('cat'); 125 | 126 | expect(table.entries).to.equal(1); 127 | }); 128 | 129 | it('should return removed value', () => { 130 | table.insert('dog', 'woof'); 131 | 132 | expect(table.remove('dog')).to.equal('woof'); 133 | }); 134 | 135 | it('should return undefined if key is not found in table', () => { 136 | expect(table.remove('dog')).to.equal(undefined); 137 | }); 138 | }); 139 | }); -------------------------------------------------------------------------------- /test/structures/linked-list.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let LinkedList; 4 | let list; 5 | 6 | try { 7 | LinkedList = require('../../structures/linked-list'); 8 | list = new LinkedList(); 9 | } catch (e) { 10 | throw new Error('LinkedList could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('LinkedList', () => { 15 | beforeEach(() => { 16 | list = new LinkedList(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(list).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(list).to.have.all.keys('head', 'tail', 'size'); 25 | }); 26 | 27 | describe('#push()', () => { 28 | it('should set head and tail to new node with given value', () => { 29 | list.push(0); 30 | 31 | expect(list.head).to.be.an('object'); 32 | expect(list.tail).to.be.an('object'); 33 | expect(list.head.value).to.equal(0); 34 | expect(list.tail.value).to.equal(0); 35 | }); 36 | 37 | it('should create a node with value and next properties', () => { 38 | list.push(0) 39 | 40 | expect(list.head).to.have.all.keys('value', 'next'); 41 | }); 42 | 43 | it('should append node with given value to end of list', () => { 44 | list.push(0); 45 | list.push(1); 46 | 47 | expect(list.tail.value).to.equal(1); 48 | }); 49 | 50 | it('should increase size of list by 1', () => { 51 | list.push(0); 52 | 53 | expect(list.size).to.equal(1); 54 | }); 55 | 56 | it('should throw an error for undefined input', () => { 57 | expect(() => list.push(undefined)).to.throw(Error); 58 | }); 59 | 60 | it('should throw an error for null input', () => { 61 | expect(() => list.push(null)).to.throw(Error); 62 | }); 63 | }); 64 | 65 | describe('#contains()', () => { 66 | it('should return true if value is in the middle of the list', () => { 67 | list.push(0); 68 | list.push(1); 69 | list.push(2); 70 | 71 | expect(list.contains(1)).to.equal(true); 72 | }); 73 | 74 | it('should return true if value is in the head', () => { 75 | list.push(0); 76 | 77 | expect(list.contains(0)).to.equal(true); 78 | }); 79 | 80 | it('should return true if value is in the tail', () => { 81 | list.push(0); 82 | list.push(1); 83 | list.push(2); 84 | 85 | expect(list.contains(2)).to.equal(true); 86 | }); 87 | 88 | it('should return false if value is not in the list', () => { 89 | list.push(0); 90 | list.push(2); 91 | 92 | expect(list.contains(1)).to.equal(false); 93 | }); 94 | 95 | it('should return false if list is empty', () => { 96 | expect(list.contains(1)).to.equal(false); 97 | }); 98 | }); 99 | 100 | describe('#remove()', () => { 101 | it('should remove node from middle of list', () => { 102 | list.push(0); 103 | list.push(1); 104 | list.push(2); 105 | 106 | list.remove(1); 107 | 108 | expect(list.head.next.value).to.equal(2); 109 | }); 110 | 111 | it('should decrement size upon removing node from middle of list', () => { 112 | list.push(0); 113 | list.push(1); 114 | list.push(2); 115 | 116 | list.remove(1); 117 | 118 | expect(list.size).to.equal(2); 119 | }); 120 | 121 | it('should return removed node from middle of list', () => { 122 | list.push(0); 123 | list.push(1); 124 | list.push(2); 125 | 126 | const removed = list.remove(1); 127 | 128 | expect(removed).to.be.an('object'); 129 | expect(removed.value).to.equal(1); 130 | }); 131 | 132 | it('should remove node from end of list and reassign tail pointer', () => { 133 | list.push(0); 134 | list.push(1); 135 | list.push(2); 136 | 137 | list.remove(2); 138 | 139 | expect(list.tail.value).to.equal(1); 140 | expect(list.tail.next).to.equal(null); 141 | }); 142 | 143 | it('should decrement size upon removing node from end of list', () => { 144 | list.push(0); 145 | list.push(1); 146 | list.push(2); 147 | 148 | list.remove(2); 149 | 150 | expect(list.size).to.equal(2); 151 | }); 152 | 153 | it('should return removed node from end of list', () => { 154 | list.push(0); 155 | list.push(1); 156 | list.push(2); 157 | 158 | const removed = list.remove(2); 159 | 160 | expect(removed).to.be.an('object'); 161 | expect(removed.value).to.equal(2); 162 | }); 163 | 164 | it('should remove matching head node and set head to next node', () => { 165 | list.push(0); 166 | list.push(1); 167 | 168 | list.remove(0); 169 | 170 | expect(list.head.value).to.equal(1); 171 | }); 172 | 173 | it('should decrement size upon removing head', () => { 174 | list.push(0); 175 | list.push(1); 176 | 177 | list.remove(0); 178 | 179 | expect(list.size).to.equal(1); 180 | }); 181 | 182 | it('should return removed head', () => { 183 | list.push(0); 184 | list.push(1); 185 | 186 | const removed = list.remove(0); 187 | 188 | expect(removed).to.be.an('object'); 189 | expect(removed.value).to.equal(0); 190 | }); 191 | 192 | it('should remove single node and set head and tail pointers to null', () => { 193 | list.push(0); 194 | 195 | list.remove(0); 196 | 197 | expect(list.head).to.equal(null); 198 | expect(list.tail).to.equal(null); 199 | }); 200 | 201 | it('should decrement size upon removing single node', () => { 202 | list.push(0); 203 | 204 | list.remove(0); 205 | 206 | expect(list.size).to.equal(0); 207 | }); 208 | 209 | it('should return removed single node', () => { 210 | list.push(0); 211 | 212 | const removed = list.remove(0); 213 | 214 | expect(removed).to.be.an('object'); 215 | expect(removed.value).to.equal(0); 216 | }); 217 | 218 | it('should throw an error if list is empty', () => { 219 | expect(() => list.remove(1)).to.throw(Error); 220 | }); 221 | 222 | it('should throw an error if attempting to remove value not in list', () => { 223 | list.push(0); 224 | 225 | expect(() => list.remove(1)).to.throw(Error); 226 | }); 227 | }); 228 | }); -------------------------------------------------------------------------------- /test/structures/queue.circular.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let CircularQueue; 4 | let queue; 5 | 6 | try { 7 | CircularQueue = require('../../structures/queue.circular'); 8 | queue = new CircularQueue(4); 9 | } catch (e) { 10 | throw new Error('Queue could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('CircularQueue', () => { 15 | beforeEach(() => { 16 | queue = new CircularQueue(4); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(queue).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(queue).to.have.all.keys('ring', 'size', 'oldest', 'newest'); 25 | }); 26 | 27 | describe('#enqueue()', () => { 28 | it('should insert item into empty queue', () => { 29 | queue.enqueue(10); 30 | 31 | expect(queue.ring[1]).to.equal(10); 32 | }); 33 | 34 | it('should insert item into queue containing one item', () => { 35 | queue.enqueue(10); 36 | queue.enqueue(9); 37 | 38 | expect(queue.ring[2]).to.equal(9); 39 | }); 40 | 41 | it('should overwrite oldest item if queue is full', () => { 42 | queue.enqueue(10); 43 | queue.enqueue(9); 44 | queue.enqueue(8); 45 | queue.enqueue(7); 46 | queue.enqueue(6); 47 | 48 | expect(queue.ring[1]).to.equal(6); 49 | }); 50 | 51 | it('should not change size of queue', () => { 52 | queue.enqueue(0); 53 | 54 | expect(queue.size).to.equal(4); 55 | }); 56 | }); 57 | 58 | describe('#dequeue()', () => { 59 | it('should remove oldest item from queue', () => { 60 | queue.enqueue(0); 61 | queue.enqueue(1); 62 | 63 | queue.dequeue(); 64 | 65 | expect(queue.ring[1]).to.equal(undefined); 66 | }); 67 | 68 | it('should remove last remaining item', () => { 69 | queue.enqueue(0); 70 | 71 | queue.dequeue(); 72 | 73 | expect(queue.ring[1]).to.equal(undefined); 74 | }); 75 | 76 | it('should return removed value', () => { 77 | queue.enqueue(0); 78 | 79 | expect(queue.dequeue()).to.equal(0); 80 | }); 81 | 82 | it('should return undefined for empty queue', () => { 83 | expect(queue.dequeue()).to.equal(undefined); 84 | }); 85 | }); 86 | }); -------------------------------------------------------------------------------- /test/structures/queue.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let Q; 4 | let queue; 5 | 6 | try { 7 | Q = require('../../structures/queue'); 8 | queue = new Q(); 9 | } catch (e) { 10 | throw new Error('Queue could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('Queue', () => { 15 | beforeEach(() => { 16 | queue = new Q(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(queue).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(queue).to.have.all.keys('front', 'rear', 'size', 'capacity'); 25 | }); 26 | 27 | describe('#isEmpty()', () => { 28 | it('should return true if queue is empty', () => { 29 | expect(queue.isEmpty()).to.equal(true); 30 | }); 31 | 32 | it('should return false if queue contains values', () => { 33 | queue.enqueue(0) 34 | 35 | expect(queue.isEmpty()).to.equal(false); 36 | }); 37 | }); 38 | 39 | describe('#dequeue()', () => { 40 | it('should decrease size of queue by 1', () => { 41 | queue.enqueue(0); 42 | queue.enqueue(1); 43 | 44 | queue.dequeue(); 45 | 46 | expect(queue.size).to.equal(1); 47 | }); 48 | 49 | it('should remove earliest item enqueued to queue (FIFO)', () => { 50 | queue.enqueue(0); 51 | queue.enqueue(1); 52 | 53 | queue.dequeue(); 54 | 55 | expect(queue.front.value).to.equal(1); 56 | }); 57 | 58 | it('should set front to null when dequeuing the last node', () => { 59 | queue.enqueue(1); 60 | 61 | queue.dequeue(); 62 | 63 | expect(queue.front).to.be.null; 64 | }); 65 | 66 | it('should set rear to null when dequeuing the last node', () => { 67 | queue.enqueue(1); 68 | 69 | queue.dequeue(); 70 | 71 | expect(queue.rear).to.be.null; 72 | }); 73 | 74 | it('should return removed value', () => { 75 | queue.enqueue(0); 76 | 77 | expect(queue.dequeue()).to.equal(0); 78 | }); 79 | }); 80 | 81 | 82 | describe('#enqueue()', () => { 83 | it('should increase size of queue by 1', () => { 84 | queue.enqueue(0); 85 | 86 | expect(queue.size).to.equal(1); 87 | }); 88 | 89 | it('should append input to end of queue', () => { 90 | queue.enqueue(0); 91 | queue.enqueue(1); 92 | 93 | expect(queue.rear.value).to.equal(1); 94 | }); 95 | 96 | it('should not insert item if queue is at capacity', () => { 97 | const cappedQueue = new Q(0); 98 | 99 | queue.enqueue('never enqueues'); 100 | 101 | expect(cappedQueue.size).to.equal(0); 102 | }); 103 | }); 104 | 105 | describe('#getSize()', () => { 106 | it('should return 0 if queue is empty', () => { 107 | expect(queue.getSize()).to.equal(0); 108 | }); 109 | 110 | it('should return 1 if queue contains one item', () => { 111 | queue.enqueue(0); 112 | 113 | expect(queue.getSize()).to.equal(1); 114 | }); 115 | }); 116 | 117 | describe('#isFull()', () => { 118 | it('should return false if queue has not reached capacity', () => { 119 | const cappedQueue = new Q(1); 120 | 121 | expect(cappedQueue.isFull()).to.equal(false); 122 | }); 123 | 124 | it('should return true if queue has reached capacity', () => { 125 | const cappedQueue = new Q(1); 126 | 127 | cappedQueue.enqueue(0); 128 | 129 | expect(cappedQueue.isFull()).to.equal(true); 130 | }); 131 | }); 132 | 133 | describe('#peek()', () => { 134 | it('should return value at top of queue', () => { 135 | queue.enqueue(0); 136 | 137 | expect(queue.peek()).to.equal(0); 138 | }); 139 | 140 | it('should return undefined for empty queue', () => { 141 | expect(queue.peek()).to.equal(undefined); 142 | }); 143 | }); 144 | }); -------------------------------------------------------------------------------- /test/structures/queue.priority.max.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let MaxPriorityQueue; 4 | let maxPQ; 5 | 6 | try { 7 | MaxPriorityQueue = require('../../structures/queue.priority.max'); 8 | maxPQ = new MaxPriorityQueue(); 9 | } catch (e) { 10 | throw new Error('MaxPriorityQueue could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('MaxPriorityQueue', () => { 15 | beforeEach(() => { 16 | maxPQ = new MaxPriorityQueue(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(maxPQ).to.be.extensible; 21 | }); 22 | 23 | it('should have a heap property pointing to an array with one null value', () => { 24 | expect(maxPQ.heap).to.deep.equal([null]); 25 | }); 26 | 27 | describe('#deleteMax()', () => { 28 | it('should remove the maximum value from the heap', () => { 29 | maxPQ.insert(6); 30 | maxPQ.insert(8); 31 | maxPQ.insert(9); 32 | maxPQ.insert(10); 33 | maxPQ.insert(12); 34 | maxPQ.insert(13); 35 | maxPQ.insert(14); 36 | 37 | maxPQ.deleteMax(); 38 | 39 | expect(maxPQ.heap[1]).to.equal(13); 40 | }); 41 | 42 | it('should return the deleted max value', () => { 43 | maxPQ.insert(0); 44 | 45 | expect(maxPQ.deleteMax()).to.equal(0); 46 | }); 47 | 48 | it('should throw an error for empty heap', () => { 49 | expect(() => maxPQ.deleteMax()).to.throw(Error); 50 | }); 51 | }); 52 | 53 | describe('#insert()', () => { 54 | it('should correctly insert keys when called in ascending order', () => { 55 | maxPQ.insert(6); 56 | maxPQ.insert(8); 57 | maxPQ.insert(9); 58 | maxPQ.insert(10); 59 | maxPQ.insert(12); 60 | maxPQ.insert(13); 61 | maxPQ.insert(14); 62 | 63 | /* 64 | 6 65 | 8 6 66 | 9 6 8 67 | 10 9 8 6 68 | 12 10 8 6 9 69 | 13 10 12 6 9 8 70 | 14 10 13 6 9 8 12 71 | */ 72 | 73 | expect(maxPQ.heap).to.deep.equal([null, 14, 10, 13, 6, 9, 8, 12]); 74 | }); 75 | 76 | it('should correctly insert keys when called in descending order', () => { 77 | maxPQ.insert(14); 78 | maxPQ.insert(13); 79 | maxPQ.insert(12); 80 | maxPQ.insert(10); 81 | maxPQ.insert(9); 82 | maxPQ.insert(8); 83 | maxPQ.insert(6); 84 | 85 | expect(maxPQ.heap).to.deep.equal([null, 14, 13, 12, 10, 9, 8, 6]); 86 | }); 87 | 88 | it('should throw an error for NaN input', () => { 89 | expect(() => maxPQ.insert(NaN)).to.throw(Error); 90 | }); 91 | 92 | it('should throw an error for non-string non-numeric input', () => { 93 | expect(() => maxPQ.insert({})).to.throw(Error); 94 | }); 95 | }); 96 | 97 | describe('#isEmpty()', () => { 98 | it('should return true if heap is empty', () => { 99 | expect(maxPQ.isEmpty()).to.equal(true); 100 | }); 101 | 102 | it('should return false if heap contains values', () => { 103 | maxPQ.insert(0); 104 | 105 | expect(maxPQ.isEmpty()).to.equal(false); 106 | }); 107 | }); 108 | 109 | describe('#peekMax', () => { 110 | it('should return the maximum value from the tree', () => { 111 | maxPQ.insert(6); 112 | maxPQ.insert(8); 113 | maxPQ.insert(9); 114 | maxPQ.insert(10); 115 | maxPQ.insert(12); 116 | maxPQ.insert(13); 117 | maxPQ.insert(14); 118 | 119 | expect(maxPQ.peekMax()).to.equal(14); 120 | }); 121 | }); 122 | }); -------------------------------------------------------------------------------- /test/structures/queue.priority.min.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let MinPQTable; 4 | let minPQ; 5 | 6 | try { 7 | MinPQTable = require('../../structures/queue.priority.min'); 8 | minPQ = new MinPQTable(); 9 | } catch (e) { 10 | throw new Error('MinPQTable could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('MinPQTable', () => { 15 | beforeEach(() => { 16 | minPQ = new MinPQTable(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(minPQ).to.be.extensible; 21 | }); 22 | 23 | it('should have a heap property pointing to an array with one null value', () => { 24 | expect(minPQ.heap).to.deep.equal([null]); 25 | }); 26 | 27 | describe('#deleteMin()', () => { 28 | it('should remove the minimum key from the heap', () => { 29 | minPQ.insert(14, 6); 30 | minPQ.insert(13, 8); 31 | minPQ.insert(12, 9); 32 | minPQ.insert(10, 10); 33 | minPQ.insert(9, 12); 34 | minPQ.insert(8, 13); 35 | minPQ.insert(6, 14); 36 | 37 | minPQ.deleteMin(); 38 | 39 | expect(minPQ.heap[1].key).to.equal(8); 40 | }); 41 | 42 | it('should return the deleted min value', () => { 43 | minPQ.insert(0, 0); 44 | 45 | expect(minPQ.deleteMin().key).to.equal(0); 46 | }); 47 | 48 | it('should throw an error for empty heap', () => { 49 | expect(() => minPQ.deleteMin()).to.throw(Error); 50 | }); 51 | }); 52 | 53 | describe('#insert()', () => { 54 | it('should correctly insert keys when called in descending order', () => { 55 | minPQ.insert(14, 'dog'); 56 | minPQ.insert(13, 'cat'); 57 | minPQ.insert(12, 'woof'); 58 | minPQ.insert(10, 'meow'); 59 | minPQ.insert(9, 'pig'); 60 | minPQ.insert(8, 'oink'); 61 | minPQ.insert(6, 'unicorn'); 62 | 63 | /* 64 | 14 65 | 13 14 66 | 12 14 13 67 | 10 12 13 14 68 | 9 10 13 14 12 69 | 8 10 9 14 12 13 70 | 6 10 8 14 12 13 9 71 | */ 72 | 73 | expect(minPQ.heap.slice(1).map(o => o.key)).to.deep.equal([6, 10, 8, 14, 12, 13, 9]); 74 | }); 75 | 76 | it('should correctly insert keys when called in ascending order', () => { 77 | minPQ.insert(6, 'unicorn'); 78 | minPQ.insert(8, 'oink'); 79 | minPQ.insert(9, 'pig'); 80 | minPQ.insert(10, 'meow'); 81 | minPQ.insert(12, 'woof'); 82 | minPQ.insert(13, 'cat'); 83 | minPQ.insert(14, 'dog'); 84 | 85 | expect(minPQ.heap.slice(1).map(o => o.key)).to.deep.equal([6, 8, 9, 10, 12, 13, 14]); 86 | }); 87 | 88 | it('should throw an error for NaN input', () => { 89 | expect(() => minPQ.insert(NaN)).to.throw(Error); 90 | }); 91 | 92 | it('should throw an error for non-string non-numeric input', () => { 93 | expect(() => minPQ.insert({})).to.throw(Error); 94 | }); 95 | }); 96 | 97 | describe('#isEmpty()', () => { 98 | it('should return true if heap is empty', () => { 99 | expect(minPQ.isEmpty()).to.equal(true); 100 | }); 101 | 102 | it('should return false if heap contains values', () => { 103 | minPQ.insert(0, 0); 104 | 105 | expect(minPQ.isEmpty()).to.equal(false); 106 | }); 107 | }); 108 | 109 | describe('#peekMin', () => { 110 | it('should return the minimum value from the tree', () => { 111 | minPQ.insert(6, 'dog'); 112 | minPQ.insert(8, 'woof'); 113 | 114 | expect(minPQ.peekMin().key).to.equal(6); 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /test/structures/stack.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let Stack; 4 | let stack; 5 | 6 | try { 7 | Stack = require('../../structures/stack'); 8 | stack = new Stack(); 9 | } catch (e) { 10 | throw new Error('Stack could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('Stack', () => { 15 | beforeEach(() => { 16 | stack = new Stack(); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(stack).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(stack).to.have.all.keys('maxes', 'storage', 'capacity'); 25 | }); 26 | 27 | describe('#getMax()', () => { 28 | it('should return single value in stack', () => { 29 | stack.push(2); 30 | 31 | expect(stack.getMax()).to.equal(2); 32 | }); 33 | 34 | it('should return max for ascending order pushes', () => { 35 | stack.push(2); 36 | stack.push(3); 37 | 38 | expect(stack.getMax()).to.equal(3); 39 | }); 40 | 41 | it('should return max for descending order pushes', () => { 42 | stack.push(3); 43 | stack.push(2); 44 | 45 | expect(stack.getMax()).to.equal(3); 46 | }); 47 | 48 | it('should return previous max after current max pops', () => { 49 | stack.push(2); 50 | stack.push(3); 51 | stack.pop(); 52 | 53 | expect(stack.getMax()).to.equal(2); 54 | }); 55 | 56 | it('should return max after multiple maxes pop', () => { 57 | stack.push(1); 58 | stack.push(3); 59 | stack.push(2); 60 | stack.push(10); 61 | stack.pop(); 62 | stack.pop() 63 | stack.pop() 64 | 65 | expect(stack.getMax()).to.equal(1); 66 | }); 67 | 68 | it('should return max after interweaved push and pop', () => { 69 | stack.push(3); 70 | stack.push(2); 71 | stack.pop(); 72 | stack.push(10); 73 | stack.push(5); 74 | stack.pop(); 75 | stack.push(6); 76 | stack.pop(); 77 | stack.pop(); 78 | 79 | expect(stack.getMax()).to.equal(3); 80 | }); 81 | 82 | it('should work with multiple sequential copies of maximum values', () => { 83 | stack.push(3); 84 | stack.push(3); 85 | stack.pop(); 86 | 87 | expect(stack.getMax()).to.equal(3); 88 | }); 89 | }); 90 | 91 | describe('#isEmpty()', () => { 92 | it('should return true if stack is empty', () => { 93 | expect(stack.isEmpty()).to.equal(true); 94 | }); 95 | 96 | it('should return false if stack contains values', () => { 97 | stack.push(0) 98 | 99 | expect(stack.isEmpty()).to.equal(false); 100 | }); 101 | }); 102 | 103 | describe('#isFull()', () => { 104 | it('should return false if stack has not reached capacity', () => { 105 | const cappedStack = new Stack(1); 106 | 107 | expect(cappedStack.isFull()).to.equal(false); 108 | }); 109 | 110 | it('should return true if stack has reached capacity', () => { 111 | const cappedStack = new Stack(1); 112 | 113 | cappedStack.push(0); 114 | 115 | expect(cappedStack.isFull()).to.equal(true); 116 | }); 117 | }); 118 | 119 | describe('#peek()', () => { 120 | it('should return value at top of stack', () => { 121 | stack.push(0); 122 | 123 | expect(stack.peek()).to.equal(0); 124 | }); 125 | 126 | it('should return undefined for empty stack', () => { 127 | expect(stack.peek()).to.equal(undefined); 128 | }); 129 | }); 130 | 131 | describe('#pop()', () => { 132 | it('should decrease size of stack by 1', () => { 133 | stack.push(0); 134 | stack.push(1); 135 | 136 | stack.pop(); 137 | 138 | expect(stack.storage.length).to.equal(1); 139 | }); 140 | 141 | it('should remove last item pushed to stack', () => { 142 | stack.push(0); 143 | stack.push(1); 144 | 145 | stack.pop(); 146 | 147 | expect(stack.storage[stack.storage.length - 1]).to.equal(0); 148 | }); 149 | 150 | it('should return removed value', () => { 151 | stack.push(0); 152 | 153 | expect(stack.pop()).to.equal(0); 154 | }); 155 | }); 156 | 157 | describe('#push()', () => { 158 | it('should increase size of stack by 1', () => { 159 | stack.push(0); 160 | 161 | expect(stack.storage.length).to.equal(1); 162 | }); 163 | 164 | it('should append input to top of stack', () => { 165 | stack.push(0); 166 | stack.push(1); 167 | 168 | expect(stack.storage[stack.storage.length - 1]).to.equal(1); 169 | }); 170 | 171 | it('should not insert item if stack is at capacity', () => { 172 | const cappedStack = new Stack(0); 173 | 174 | stack.push('never pushed'); 175 | 176 | expect(cappedStack.storage.length).to.equal(0); 177 | }); 178 | }); 179 | 180 | describe('#size()', () => { 181 | it('should return 0 if stack is empty', () => { 182 | expect(stack.size()).to.equal(0); 183 | }); 184 | 185 | it('should return 1 if stack contains one item', () => { 186 | stack.push(0); 187 | 188 | expect(stack.size()).to.equal(1); 189 | }); 190 | }); 191 | }); -------------------------------------------------------------------------------- /test/structures/tree.red-black.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let RedBlackTree; 4 | let RBT; 5 | 6 | try { 7 | RedBlackTree = require('../../structures/tree.red-black'); 8 | RBT = new RedBlackTree(); 9 | } catch (e) { 10 | throw new Error('RedBlackTree could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | // In tree drawings, routes with parentheses represent red links 15 | describe('RedBlackTree', () => { 16 | beforeEach(() => { 17 | RBT = new RedBlackTree(); 18 | }); 19 | 20 | it('should be extensible', () => { 21 | expect(RBT).to.be.extensible; 22 | }); 23 | 24 | it('should have properties granted from constructor call', () => { 25 | expect(RBT).to.have.all.keys('root'); 26 | }); 27 | 28 | describe('#insert()', () => { 29 | it('should set root as new node', () => { 30 | RBT.insert('D', 'woof'); 31 | 32 | expect(RBT.root).to.be.an('object'); 33 | }); 34 | 35 | it('should insert a node with color, key, value, left and right properties', () => { 36 | RBT.insert('D', 'woof'); 37 | 38 | expect(RBT.root).to.have.all.keys('color', 'key', 'value', 'left', 'right'); 39 | }); 40 | 41 | it('should set root with the given key-value pair', () => { 42 | RBT.insert('D', 'woof'); 43 | 44 | expect(RBT.root.key).to.equal('D'); 45 | expect(RBT.root.value).to.equal('woof'); 46 | }); 47 | 48 | it('should set root with black color', () => { 49 | RBT.insert('D', 'woof'); 50 | 51 | expect(RBT.root.color).to.equal(false); 52 | }); 53 | 54 | it('should insert nodes with self-balancing logic', () => { 55 | RBT.insert('S'); 56 | RBT.insert('E'); 57 | RBT.insert('A'); 58 | RBT.insert('R'); 59 | RBT.insert('C'); 60 | RBT.insert('H'); 61 | RBT.insert('X'); 62 | RBT.insert('M'); 63 | RBT.insert('P'); 64 | RBT.insert('L'); 65 | 66 | /* 67 | M 68 | / \ 69 | E R 70 | / \ / \ 71 | C L P X 72 | (/) (/) (/) 73 | A H S 74 | */ 75 | 76 | expect(RBT.root.key).to.equal('M'); 77 | expect(RBT.root.left.key).to.equal('E'); 78 | expect(RBT.root.right.key).to.equal('R'); 79 | expect(RBT.root.left.left.key).to.equal('C'); 80 | expect(RBT.root.left.right.key).to.equal('L'); 81 | expect(RBT.root.right.left.key).to.equal('P'); 82 | expect(RBT.root.right.right.key).to.equal('X'); 83 | expect(RBT.root.left.left.left.key).to.equal('A'); 84 | expect(RBT.root.left.right.left.key).to.equal('H'); 85 | expect(RBT.root.right.right.left.key).to.equal('S'); 86 | }); 87 | 88 | it('should overwrite value for duplicate key', () => { 89 | RBT.insert('D', 'woof'); 90 | RBT.insert('D', 'meow'); 91 | 92 | expect(RBT.root.value).to.equal('meow'); 93 | }); 94 | }); 95 | 96 | describe('#breadthFirstSearch()', () => { 97 | it('should apply callback to all nodes in level order', () => { 98 | RBT.insert('S'); 99 | RBT.insert('E'); 100 | RBT.insert('A'); 101 | RBT.insert('R'); 102 | RBT.insert('C'); 103 | RBT.insert('H'); 104 | RBT.insert('X'); 105 | RBT.insert('M'); 106 | RBT.insert('P'); 107 | RBT.insert('L'); 108 | const called = []; 109 | 110 | /* 111 | M 112 | / \ 113 | E R 114 | / \ / \ 115 | C L P X 116 | (/) (/) (/) 117 | A H S 118 | */ 119 | 120 | RBT.breadthFirstSearch(node => called.push(node.key)); 121 | 122 | expect(called).to.deep.equal(['M', 'E', 'R', 'C', 'L', 'P', 'X', 'A', 'H', 'S']); 123 | }); 124 | 125 | it('should throw an error for empty trees', () => { 126 | expect(() => RBT.breadthFirstSearch(console.log)).to.throw(Error); 127 | }); 128 | }); 129 | 130 | describe('#get()', () => { 131 | it('should return value if key is in the root', () => { 132 | RBT.insert('D', 'woof'); 133 | RBT.insert('C', 'meow'); 134 | RBT.insert('P', 'parrot'); 135 | RBT.insert('S', 'hiss'); 136 | RBT.insert('T', 'growl'); 137 | 138 | /* 139 | S 140 | (/) \ 141 | D T 142 | / \ 143 | C P 144 | */ 145 | 146 | expect(RBT.root.key).to.equal('S'); 147 | expect(RBT.root.value).to.equal('hiss'); 148 | expect(RBT.get('S')).to.equal('hiss'); 149 | }); 150 | 151 | it('should return value if key is in left leaf', () => { 152 | RBT.insert('D', 'woof'); 153 | RBT.insert('C', 'meow'); 154 | RBT.insert('P', 'parrot'); 155 | RBT.insert('S', 'hiss'); 156 | RBT.insert('T', 'growl'); 157 | 158 | /* 159 | S 160 | (/) \ 161 | D T 162 | / \ 163 | C P 164 | */ 165 | 166 | expect(RBT.root.left.key).to.equal('D'); 167 | expect(RBT.root.left.value).to.equal('woof'); 168 | expect(RBT.get('D')).to.equal('woof'); 169 | }); 170 | 171 | it('should return value if key is in right leaf', () => { 172 | RBT.insert('D', 'woof'); 173 | RBT.insert('C', 'meow'); 174 | RBT.insert('P', 'parrot'); 175 | RBT.insert('S', 'hiss'); 176 | RBT.insert('T', 'growl'); 177 | 178 | /* 179 | S 180 | (/) \ 181 | D T 182 | / \ 183 | C P 184 | */ 185 | 186 | expect(RBT.root.right.key).to.equal('T'); 187 | expect(RBT.root.right.value).to.equal('growl'); 188 | expect(RBT.get('T')).to.equal('growl'); 189 | }); 190 | 191 | it('should return value if key is in nested left leaf', () => { 192 | RBT.insert('D', 'woof'); 193 | RBT.insert('C', 'meow'); 194 | RBT.insert('P', 'parrot'); 195 | RBT.insert('S', 'hiss'); 196 | RBT.insert('T', 'growl'); 197 | 198 | /* 199 | S 200 | (/) \ 201 | D T 202 | / \ 203 | C P 204 | */ 205 | 206 | expect(RBT.root.left.left.key).to.equal('C'); 207 | expect(RBT.root.left.left.value).to.equal('meow'); 208 | expect(RBT.get('C')).to.equal('meow'); 209 | }); 210 | 211 | it('should return value if key is in nested right leaf', () => { 212 | RBT.insert('D', 'woof'); 213 | RBT.insert('C', 'meow'); 214 | RBT.insert('P', 'parrot'); 215 | RBT.insert('S', 'hiss'); 216 | RBT.insert('T', 'growl'); 217 | 218 | /* 219 | S 220 | (/) \ 221 | D T 222 | / \ 223 | C P 224 | */ 225 | 226 | expect(RBT.root.left.right.key).to.equal('P'); 227 | expect(RBT.root.left.right.value).to.equal('parrot'); 228 | expect(RBT.get('P')).to.equal('parrot'); 229 | }); 230 | 231 | it('should return null if the key is not found in the tree', () => { 232 | RBT.insert('C', 'meow'); 233 | 234 | expect(RBT.get('D')).to.equal(null); 235 | }); 236 | 237 | it('should return null if tree is empty', () => { 238 | expect(RBT.get('D')).to.equal(null); 239 | }); 240 | }); 241 | 242 | describe('#depthInOrder', () => { 243 | it('should apply callback to all nodes depth-first in-order', () => { 244 | RBT.insert('S'); 245 | RBT.insert('E'); 246 | RBT.insert('A'); 247 | RBT.insert('R'); 248 | RBT.insert('C'); 249 | RBT.insert('H'); 250 | RBT.insert('X'); 251 | RBT.insert('M'); 252 | RBT.insert('P'); 253 | RBT.insert('L'); 254 | const called = []; 255 | 256 | /* 257 | M 258 | / \ 259 | E R 260 | / \ / \ 261 | C L P X 262 | (/) (/) (/) 263 | A H S 264 | */ 265 | 266 | RBT.depthInOrder(node => called.push(node.key)); 267 | 268 | expect(called).to.deep.equal(['A', 'C', 'E', 'H', 'L', 'M', 'P', 'R', 'S', 'X']); 269 | }); 270 | 271 | it('should throw an error for empty trees', () => { 272 | expect(() => RBT.depthInOrder(console.log)).to.throw(Error); 273 | }); 274 | }); 275 | 276 | describe('#depthPostOrder', () => { 277 | it('should apply callback to all nodes depth-first post-order', () => { 278 | RBT.insert('S'); 279 | RBT.insert('E'); 280 | RBT.insert('A'); 281 | RBT.insert('R'); 282 | RBT.insert('C'); 283 | RBT.insert('H'); 284 | RBT.insert('X'); 285 | RBT.insert('M'); 286 | RBT.insert('P'); 287 | RBT.insert('L'); 288 | const called = []; 289 | 290 | /* 291 | M 292 | / \ 293 | E R 294 | / \ / \ 295 | C L P X 296 | (/) (/) (/) 297 | A H S 298 | */ 299 | 300 | RBT.depthPostOrder(node => called.push(node.key)); 301 | 302 | expect(called).to.deep.equal(['A', 'C', 'H', 'L', 'E', 'P', 'S', 'X', 'R', 'M']); 303 | }); 304 | 305 | it('should throw an error for empty trees', () => { 306 | expect(() => RBT.depthPostOrder(console.log)).to.throw(Error); 307 | }); 308 | }); 309 | 310 | describe('#depthPreOrder', () => { 311 | it('should apply callback to all nodes depth-first pre-order', () => { 312 | RBT.insert('S'); 313 | RBT.insert('E'); 314 | RBT.insert('A'); 315 | RBT.insert('R'); 316 | RBT.insert('C'); 317 | RBT.insert('H'); 318 | RBT.insert('X'); 319 | RBT.insert('M'); 320 | RBT.insert('P'); 321 | RBT.insert('L'); 322 | const called = []; 323 | 324 | /* 325 | M 326 | / \ 327 | E R 328 | / \ / \ 329 | C L P X 330 | (/) (/) (/) 331 | A H S 332 | */ 333 | 334 | RBT.depthPreOrder(node => called.push(node.key)); 335 | 336 | expect(called).to.deep.equal(['M', 'E', 'C', 'A', 'L', 'H', 'R', 'P', 'X', 'S']); 337 | }); 338 | 339 | it('should throw an error for empty trees', () => { 340 | expect(() => RBT.depthPreOrder(console.log)).to.throw(Error); 341 | }); 342 | }); 343 | }); -------------------------------------------------------------------------------- /test/structures/tree.trie.suffix.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | let SuffixTrie; 4 | let trie; 5 | 6 | try { 7 | SuffixTrie = require('../../structures/tree.trie.suffix'); 8 | trie = new SuffixTrie('BANANA'); 9 | } catch (e) { 10 | throw new Error('SuffixTrie could not be tested due to faulty import, likely ' + 11 | 'from an incorrect file path or exporting a non-constructor from the file.'); 12 | } 13 | 14 | describe('SuffixTrie', () => { 15 | beforeEach(() => { 16 | trie = new SuffixTrie('BANANA'); 17 | }); 18 | 19 | it('should be extensible', () => { 20 | expect(trie).to.be.extensible; 21 | }); 22 | 23 | it('should have properties granted from constructor call', () => { 24 | expect(trie).to.have.all.keys('root'); 25 | }); 26 | 27 | it('should throw an error for number input', () => { 28 | expect(() => new SuffixTrie(1)).to.throw(Error); 29 | }); 30 | 31 | it('should throw an error for undefined input', () => { 32 | expect(() => new SuffixTrie()).to.throw(Error); 33 | }); 34 | 35 | it('should throw an error for object input', () => { 36 | expect(() => new SuffixTrie({ objects: "not allowed" })).to.throw(Error); 37 | }); 38 | 39 | it('should throw an error for null input', () => { 40 | expect(() => new SuffixTrie(null)).to.throw(Error); 41 | }); 42 | 43 | it('should throw an error for NaN input', () => { 44 | expect(() => new SuffixTrie(NaN)).to.throw(Error); 45 | }); 46 | 47 | describe('#hasSuffix()', () => { 48 | it('should return true if the suffix is in the trie', () => { 49 | expect(trie.hasSuffix('ana')).to.be.true; 50 | }); 51 | 52 | it('should return true if suffix is a whole word', () => { 53 | expect(trie.hasSuffix('banana')).to.be.true; 54 | }); 55 | 56 | it('should return false if suffix is not in the trie', () => { 57 | expect(trie.hasSuffix('cat')).to.be.false; 58 | }); 59 | 60 | it('should throw an error for number input', () => { 61 | expect(() => trie.hasSuffix(1)).to.throw(Error); 62 | }); 63 | 64 | it('should throw an error for undefined input', () => { 65 | expect(() => trie.hasSuffix()).to.throw(Error); 66 | }); 67 | 68 | it('should throw an error for object input', () => { 69 | expect(() => trie.hasSuffix({ objects: "not allowed" })).to.throw(Error); 70 | }); 71 | 72 | it('should throw an error for null input', () => { 73 | expect(() => trie.hasSuffix(null)).to.throw(Error); 74 | }); 75 | 76 | it('should throw an error for NaN input', () => { 77 | expect(() => trie.hasSuffix(NaN)).to.throw(Error); 78 | }); 79 | }); 80 | 81 | describe('#matchesPattern()', () => { 82 | it('should return true if the pattern is a suffix', () => { 83 | expect(trie.matchesPattern('ana')).to.be.true; 84 | }); 85 | 86 | it('should return true if the pattern exists and is not a suffix', () => { 87 | expect(trie.matchesPattern('an')).to.be.true; 88 | }); 89 | 90 | it('should return true if pattern is a whole word', () => { 91 | expect(trie.matchesPattern('banana')).to.be.true; 92 | }); 93 | 94 | it('should return false if pattern is not in the trie', () => { 95 | expect(trie.matchesPattern('cat')).to.be.false; 96 | }); 97 | 98 | it('should return true if pattern is an empty string', () => { 99 | expect(trie.matchesPattern('')).to.be.true; 100 | }); 101 | 102 | it('should throw an error for number input', () => { 103 | expect(() => trie.matchesPattern(1)).to.throw(Error); 104 | }); 105 | 106 | it('should throw an error for undefined input', () => { 107 | expect(() => trie.matchesPattern()).to.throw(Error); 108 | }); 109 | 110 | it('should throw an error for object input', () => { 111 | expect(() => trie.matchesPattern({ objects: "not allowed" })).to.throw(Error); 112 | }); 113 | 114 | it('should throw an error for null input', () => { 115 | expect(() => trie.matchesPattern(null)).to.throw(Error); 116 | }); 117 | 118 | it('should throw an error for NaN input', () => { 119 | expect(() => trie.matchesPattern(NaN)).to.throw(Error); 120 | }); 121 | }); 122 | }); --------------------------------------------------------------------------------