diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd0eb14..e48741f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.18.0](https://github.com/amejiarosario/dsa.js/compare/1.17.0...1.18.0) (2020-09-07) + + +### Features + +* **book/graph:** add schedule exercise and solution ([6a83cf8](https://github.com/amejiarosario/dsa.js/commit/6a83cf8a5d454b26e3048aa4ca73e44eafca0ed3)) + # [1.17.0](https://github.com/amejiarosario/dsa.js/compare/1.16.0...1.17.0) (2020-09-04) diff --git a/book/D-interview-questions-solutions.asc b/book/D-interview-questions-solutions.asc index 180a6222..d7228268 100644 --- a/book/D-interview-questions-solutions.asc +++ b/book/D-interview-questions-solutions.asc @@ -655,3 +655,194 @@ We could also have used a Map and keep track of the indexes, but that's not nece - Time: `O(n)`. We visit each letter once. - Space: `O(W)`, where `W` is the max length of non-repeating characters. The maximum size of the Set gives the space complexity. In the worst-case scenario, all letters are unique (`W = n`), so our space complexity would be `O(n)`. In the avg. case where there are one or more duplicates, it uses less space than `n`, because `W < n`. + + + + + + + +:leveloffset: +1 + +=== Solutions for Graph Questions +(((Interview Questions Solutions, Graph))) + +:leveloffset: -1 + + +[#graph-q-course-schedule] +include::content/part03/graph-search.asc[tag=graph-q-course-schedule] + +Basically, we have to detect if the graph has a cycle or not. +There are multiple ways to detect cycles on a graph using BFS and DFS. + +One of the most straightforward ways to do it is using DFS one each course (node) and traverse their prerequisites (neighbors). If we start in a node, and then we see that node again, we found a cycle! (maybe) + +A critical part of solving this exercise is coming up with good test cases. Let's examine these two: + +[graphviz, course-schedule-examples, png] +.... +digraph G { + subgraph cluster_1 { + a0 -> a1 -> a2 + a0 -> a2 [color=gray] + label = "Example A" + } + + subgraph cluster_2 { + b0 -> b1 -> b2 -> b3 + b3 -> b1 [color=red] + label = "Example B"; + } +} +.... + +Let's say we are using a regular DFS, where we visit the nodes and keep track of visited nodes. If we test the example A, we can get to the course 2 (a2) in two ways. So, we can't blindly assume that "seen" nodes are because of a cycle. To solve this issue, we can keep track of the parent. + +For example B, if we start in course 0 (b0), we can find a cycle. However, the cycle does not involve course 0 (parent). When we visit course 1 (b1) and mark it as the parent, we will see that reach to course 1 (b1) again. Then, we found a cycle! + +[source, javascript] +---- +include::interview-questions/course-schedule.js[tags=brute1] +---- + +We built the graph on the fly as an adjacency list (Map + Arrays). +Then we visited each node, checking if there it has cycles. If none has cyles, then we return true. + +The cycle check uses DFS. We keep track of seen nodes and also who the parent is. If we get to the parent more than once, we have a cycle like examples A and B. + +What's the time complexity? + +We visite every node/vertex: `O(|V|)` and then for every node, we visite all it's edges, so we have `O(|V|*|E|)`. + +Can we do better? + +There's no need to visit nodes more than once. Instead of having a local `seen` variable for each node, we can move it outside the loop. However, it won't be a boolean anymore (seen or not seen). We could see nodes more than once, without being in a cycle (example A). One idea is to have 3 states: `unvisited` (0), `visiting` (1) and `visited` (2). Let's devise the algorithm: + +*Algorithm*: + +* Build a graph as an adjacency list (map + arrays). +* Fill in every prerequisite as an edge on the graph. +* Visit every node and if there's a cycle, return false. +** When we start visiting a node, we mark it as 1 (visiting) +** Visit all its adjacent nodes +** Mark current node as 2 (visited) when we finish visiting neighbors. +** If we see a node in visiting state more than once, it's a cycle! +** If we see a node in a visited state, skip it. + +*Implementation*: + +[source, javascript] +---- +include::interview-questions/course-schedule.js[tags=description;solution] +---- + +In the first line, we initialize the map with the course index and an empty array. +This time the `seen` array is outside the recursion. + +*Complexity Analysis*: + +- Time: `O(|V| + |E|)`. We go through each node and edge only once. +- Space: `O(|V| + |E|)`. The size of the adjacency list. + + + + +// +[#graph-q-critical-connections-in-a-network] +include::content/part03/graph-search.asc[tag=graph-q-critical-connections-in-a-network] + +On idea to find if a path is critical is to remove it. If we visit the graph and see that some nodes are not reachable, then, oops, It was critical! + +We can code precisely that. We can remove one link at a time and check if all other nodes are reachable. It's not very efficient, but it's a start. + +[source, javascript] +---- +include::interview-questions/critical-connections-in-a-network.js[tags=criticalConnectionsBrute1] +---- + +We are using a function `areAllNodesReachable`, which implements a BFS for visiting the graph, but DFS would have worked too. The runtime is `O(|E| + |V|)`, where `E` is the number of edges and `V` the number of nodes/servers. In `criticalConnectionsBrute1`, We are looping through all `connections` (`E`) to remove one connection at a time and then checking if all servers are still reachable with `areAllNodesReachable`. + +The time complexity is `O(|E|^2 * |V|)`. Can we do it on one pass? Sure we can! + +*Tarjan's Strongly Connected Components Algorithms* + +A connection is critical only if it's not part of the cycle. + +In other words, a critical path is like a bridge that connects islands; if you remove it you won't cross from one island to the other. + +Connections that are part of the cycle (blue) have redundancy. If you eliminate one, you can still reach other nodes. Check out the examples below. + +[graphviz, critical-connections-sol-examples, png] +.... +graph G { + subgraph cluster_0 { + a0 -- a1 [color=blue] + a1 -- a2 [color=blue] + a2 -- a0 [color=blue] + a1 -- a3 [color=blue] + a3 -- a2 [color=blue] + label = "Example A"; + } + + subgraph cluster_3 { + b0 -- b1 [color=blue] + b1 -- b2 [color=blue] + b2 -- b0 [color=blue] + b1 -- b3 [color=red] + b3 -- b2 [color=transparent] // removed + label = "Example B"; + } + + subgraph cluster_1 { + c0 -- c1 -- c2 -- c3 [color=red] + label = "Example C"; + } +} +.... + +The red connections are critical; if we remove any, some servers won't be reachable. + +We can solve this problem in one pass using DFS. But for that, we keep track of the nodes that are part of a loop (strongly connected components). To do that, we use the time of visit (or depth in the recursion) each node. + +For example C, if we start on `c0`, it belongs to group 0, then we move c1, c2, and c3, increasing the depth counter. Each one will be on its own group since there's no loop. + +For example B, we can start at `b0`, and then we move to `b1` and `b2`. However, `b2` circles back to `b0`, which is on group 0. We can update the group of `b1` and `b2` to be 0 since they are all connected in a loop. + +For an *undirected graph*, If we found a node on our dfs, that we have previously visited, we found a loop! We can mark all of them with the lowest group number. We know we have a critical path when it's a connection that links two different groups. For example A, they all will belong to group 0, since they are all in a loop. For Example B, we will have `b0`, `b1`, and `b2` on the same group while `b3` will be on a different group. + +*Algorithm*: + +* Build the graph as an adjacency list (map + array) +* Run dfs on any node. E.g. `0`. +** Keep track of the nodes that you have seen using `group` array. But instead of marking them as seen or not. Let's mark it with the `depth`. +** Visit all the adjacent nodes that are NOT the parent. +** If we see a node that we have visited yet, do a dfs on it and increase the depth. +** If the adjacent node has a lower grouping number, update the current node with it. +** If the adjacent node has a higher grouping number, then we found a critical path. + +*Implementation*: + +[source, javascript] +---- +include::interview-questions/critical-connections-in-a-network.js[tags=description;solution] +---- + +This algorithm only works with DFS. + +*Complexity Analysis*: + +- Time: `O(|E| + |V|)`. We visit each node and edge only once. +- Space: `O(|E| + |V|)`. The graph has all the edges and nodes. Additionally, we use the `group` variable with a size of `|V|`. + + + + + + + +// + + + + diff --git a/book/content/part03/graph-search.asc b/book/content/part03/graph-search.asc index 48bf5308..08356768 100644 --- a/book/content/part03/graph-search.asc +++ b/book/content/part03/graph-search.asc @@ -7,9 +7,9 @@ endif::[] Graph search allows you to visit search elements. -WARNING: Graph search is very similar to <>. So, if you read that sections some of the concepts here will be familiar to you. +WARNING: Graph search is very similar to <>. So, if you read that section, some of the concepts here will be familiar to you. -There are two ways to navigate the graph, one is using Depth-First Search (DFS) and the other one is Breadth-First Search (BFS). Let's see the difference using the following graph. +There are two ways to navigate the graph, one is using Depth-First Search (DFS), and the other one is Breadth-First Search (BFS). Let's see the difference using the following graph. image::directed-graph.png[directed graph] @@ -44,10 +44,10 @@ image::directed-graph.png[directed graph] ==== Depth-First Search for Graphs -With Depth-First Search (DFS) we go deep before going wide. +With Depth-First Search (DFS), we go deep before going wide. Let's say that we use DFS on the graph shown above, starting with node `0`. -A DFS, will probably visit 5, then visit `1` and continue going down `3` and `2`. As you can see, we need to keep track of visited nodes, since in graphs we can have cycles like `1-3-2`. +A DFS will probably visit 5, then visit `1` and continue going down `3` and `2`. As you can see, we need to keep track of visited nodes, since in graphs, we can have cycles like `1-3-2`. Finally, we back up to the remaining node `0` children: node `4`. So, DFS would visit the graph: `[0, 5, 1, 3, 2, 4]`. @@ -56,13 +56,13 @@ So, DFS would visit the graph: `[0, 5, 1, 3, 2, 4]`. ==== Breadth-First Search for Graphs -With Breadth-First Search (BFS) we go wide before going deep. +With Breadth-First Search (BFS), we go wide before going deep. // TODO: BFS traversal Let's say that we use BFS on the graph shown above, starting with the same node `0`. -A BFS, will visit 5 as well, then visit `1` and will not go down to it's children. +A BFS will visit 5 as well, then visit `1` and not go down to its children. It will first finish all the children of node `0`, so it will visit node `4`. -After all the children of node `0` are visited it continue with all the children of node `5`, `1` and `4`. +After all the children of node `0` are visited, it will continue with all the children of node `5`, `1`, and `4`. In summary, BFS would visit the graph: `[0, 5, 1, 4, 3, 2]` @@ -86,4 +86,131 @@ You might wonder what the difference between search algorithms in a tree and a g The difference between searching a tree and a graph is that the tree always has a starting point (root node). However, in a graph, you can start searching anywhere. There's no root. -NOTE: Every tree is a graph, but not every graph is a tree. +NOTE: Every tree is a graph, but not every graph is a tree. Only acyclic directed graphs (DAG) are trees. + + +==== Practice Questions +(((Interview Questions, graph))) + + + + +// tag::graph-q-course-schedule[] +===== Course Schedule + +*gr-1*) _Check if it's possible to take a number of courses while satisfying their prerequisites._ + +// end::graph-q-course-schedule[] + +// _Seen in interviews at: Amazon, Facebook, Bytedance (TikTok)._ + + +*Starter code*: + +[source, javascript] +---- +include::../../interview-questions/course-schedule.js[tags=description;placeholder] +---- + + +*Examples*: + +[source, javascript] +---- +canFinish(2, [[1, 0]]); // true +// 2 courses: 0 and 1. One prerequisite: 0 -> 1 +// To take course 1 you need to take course 0. +// Course 0 has no prerequisite, so you can take 0 and then 1. + +canFinish(2, [[1, 0], [0, 1]]); // false +// 2 courses: 0 and 1. Two prerequisites: 0 -> 1 and 1 -> 0. +// To take course 1, you need to take course 0. +// To Course 0, you need course 1, so you can't any take them! + +canFinish(3, [[2, 0], [1, 0], [2, 1]]); // true +// 3 courses: 0, 1, 2. Three prerequisites: 0 -> 2 and 0 -> 1 -> 2 +// To take course 2 you need course 0, course 0 has no prerequisite. +// So you can take course 0 first, then course 1, and finally course 2. + +canFinish(4, [[1, 0], [2, 1], [3, 2], [1, 3]]); // false +// 4 courses: 0, 1, 2, 3. Prerequisites: 0 -> 1 -> 2 -> 3 and 3 -> 1. +// You can take course 0 first since it has no prerequisite. +// For taking course 1, you need course 3. However, for taking course 3 +// you need 2 and 1. You can't finish then! +---- + + +_Solution: <>_ + + + + + + + +// tag::graph-q-critical-connections-in-a-network[] +===== Critical Network Paths + +*gr-2*) _Given `n` servers and the connections between them, return the critical paths._ + +// end::graph-q-critical-connections-in-a-network[] + +// _Seen in interviews at: Amazon, Google._ + +Examples: + +[graphviz, critical-path-examples, png] +.... +graph G { + subgraph cluster_1 { + a0 -- a1 -- a2 [color=firebrick1] + label = "Example A"; + } + + subgraph cluster_0 { + b0 -- b1 [color=blue] + b1 -- b2 [color=blue] + b2 -- b0 [color=blue] + b1 -- b3 [color=blue] + b3 -- b2 [color=blue] + label = "Example B"; + b0, b1, b2, b3 [color=midnightblue] + } + + subgraph cluster_3 { + c0 -- c1 [color=blue] + c1 -- c2 [color=blue] + c2 -- c0 [color=blue] + c1 -- c3 [color=firebrick1] + c3 -- c2 [color=transparent] // removed + label = "Example C"; + c0, c1, c2 [color=midnightblue] + // c3 [color=red] + } +} +.... + +[source, javascript] +---- +// Example A +criticalConnections(3, [[0, 1], [1, 2]]);// [[0, 1], [1, 2]] +// if you remove any link, there will be stranded servers. + +// Example B +criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 2]]);// [] +// you can remove any connection and all servers will be reachable. + +// Example C +criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3]]); // [[1, 3]] +// if you remove [1, 3], then server 3 won't be reachable. +// If you remove any other link. It will be fine. +---- + +Starter code: + +[source, javascript] +---- +include::../../interview-questions/critical-connections-in-a-network.js[tags=description;placeholder] +---- + +_Solution: <>_ diff --git a/book/images/course-schedule-examples.png b/book/images/course-schedule-examples.png new file mode 100644 index 00000000..348fe3de Binary files /dev/null and b/book/images/course-schedule-examples.png differ diff --git a/book/images/critical-connections-sol-examples.png b/book/images/critical-connections-sol-examples.png new file mode 100644 index 00000000..eb3e568f Binary files /dev/null and b/book/images/critical-connections-sol-examples.png differ diff --git a/book/images/critical-path-examples.png b/book/images/critical-path-examples.png new file mode 100644 index 00000000..63b0e330 Binary files /dev/null and b/book/images/critical-path-examples.png differ diff --git a/book/interview-questions/course-schedule.js b/book/interview-questions/course-schedule.js new file mode 100644 index 00000000..c3e8b354 --- /dev/null +++ b/book/interview-questions/course-schedule.js @@ -0,0 +1,61 @@ +// tag::description[] +/** + * Check if you can finish all courses with their prerequisites. + * @param {number} n - The number of courses + * @param {[number, number][]} prerequisites - Array of courses pairs. + * E.g. [[200, 101]], to take course 202 you need course 101 first. + * @returns {boolean} - True = can finish all courses, False otherwise + */ +function canFinish(n, prerequisites) { + // end::description[] + // tag::placeholder[] + // write your code here... + // end::placeholder[] + // tag::solution[] + const graph = new Map(Array(n).fill().map((_, i) => ([i, []]))); + prerequisites.forEach(([u, v]) => graph.get(v).push(u)); + + const seen = []; + const hasCycle = (node) => { + if (seen[node] === 1) return true; // if visiting, it's a cycle! + if (seen[node] === 2) return false; // if visited, skip it. + + seen[node] = 1; // mark as visiting. + for (const adj of graph.get(node)) if (hasCycle(adj)) return true; + seen[node] = 2; // mark as visited. + return false; + }; + + for (let i = 0; i < n; i++) if (hasCycle(i)) return false; + return true; + // end::solution[] + // tag::description[] +} +// end::description[] + + +// tag::brute1[] +function canFinishBrute1(n, prerequisites) { + const graph = new Map(); // inialize adjacency list as map of arrays + for (let i = 0; i < n; i++) graph.set(i, []); // build nodes + prerequisites.forEach(([u, v]) => graph.get(v).push(u)); // edges + + const hasCycles = (node, parent = node, seen = []) => { + for (const next of graph.get(node)) { + if (next === parent) return true; + if (seen[next]) continue; + seen[next] = true; + if (hasCycles(next, parent, seen)) return true; + } + return false; + }; + + for (let i = 0; i < n; i++) { + if (hasCycles(i)) return false; + } + + return true; +} +// end::brute1[] + +module.exports = { canFinish, canFinishBrute1 }; diff --git a/book/interview-questions/course-schedule.spec.js b/book/interview-questions/course-schedule.spec.js new file mode 100644 index 00000000..61bb9a5f --- /dev/null +++ b/book/interview-questions/course-schedule.spec.js @@ -0,0 +1,54 @@ +const { canFinish, canFinishBrute1 } = require('./course-schedule'); +// const { } = require('../../src/index'); + +[canFinish, canFinishBrute1].forEach((fn) => { + describe(`TOPIC: ${fn.name}`, () => { + it('should work with null/empty', () => { + const actual = []; + const expected = true; + expect(fn(0, actual)).toEqual(expected); + }); + + it('should work basic case', () => { + const actual = [[1, 0]]; + const courses = 2; + const expected = true; + expect(fn(courses, actual)).toEqual(expected); + }); + + it('should detect cycle', () => { + const actual = [[0, 1], [1, 0]]; + const courses = 2; + const expected = false; + expect(fn(courses, actual)).toEqual(expected); + }); + + it('multiple links to a node without cycle', () => { + const actual = [[2, 1], [1, 0], [2, 0]]; + const courses = 3; + const expected = true; + expect(fn(courses, actual)).toEqual(expected); + }); + + it('multiple links to a node without cycle (different order)', () => { + const actual = [[2, 0], [1, 0], [2, 1]]; + const courses = 3; + const expected = true; + expect(fn(courses, actual)).toEqual(expected); + }); + + it('indirect cycle', () => { + const actual = [[1, 0], [2, 1], [0, 2]]; + const courses = 3; + const expected = false; + expect(fn(courses, actual)).toEqual(expected); + }); + + it('indirect cycle with nodes without indegrees', () => { + const actual = [[1, 0], [2, 1], [3, 2], [1, 3]]; + const courses = 4; + const expected = false; + expect(fn(courses, actual)).toEqual(expected); + }); + }); +}); diff --git a/book/interview-questions/critical-connections-in-a-network.js b/book/interview-questions/critical-connections-in-a-network.js new file mode 100644 index 00000000..bd7bfe37 --- /dev/null +++ b/book/interview-questions/critical-connections-in-a-network.js @@ -0,0 +1,76 @@ +const { Queue } = require('../../src/index'); + +// tag::description[] +function criticalConnections(n, connections) { + // end::description[] + // tag::placeholder[] + // write your code here... + // end::placeholder[] + // tag::solution[] + const critical = []; + const graph = new Map(Array(n).fill(0).map((_, i) => [i, []])); + connections.forEach(([u, v]) => { + graph.get(u).push(v); + graph.get(v).push(u); + }); + + const dfs = (node, parent = null, depth = 0, group = []) => { + group[node] = depth; + for (const adj of (graph.get(node) || [])) { + if (adj === parent) continue; // skip parent node + if (group[adj] === undefined) dfs(adj, node, depth + 1, group); + group[node] = Math.min(group[node], group[adj]); // update group. + if (group[adj] >= depth + 1) critical.push([node, adj]); + } + }; + + dfs(0); + return critical; + // end::solution[] + // tag::description[] +} +// end::description[] + +// tag::criticalConnectionsBrute1[] +function areAllNodesReachable(n, graph) { + const seen = Array(n).fill(false); + const queue = new Queue([0]); + + while (queue.size) { + const node = queue.dequeue(); + if (seen[node]) continue; + seen[node] = true; + + for (const adj of (graph.get(node) || [])) { + queue.enqueue(adj); + } + } + + return !seen.some((s) => !s); +} + +function criticalConnectionsBrute1(n, connections) { + const critical = []; + const graph = new Map(Array(n).fill(0).map((_, i) => [i, []])); + connections.forEach(([u, v]) => { + graph.get(u).push(v); + graph.get(v).push(u); + }); + + for (const [u, v] of connections) { + // remove edge + graph.set(u, (graph.get(u) || []).filter((e) => e !== v)); + graph.set(v, (graph.get(v) || []).filter((e) => e !== u)); + + if (!areAllNodesReachable(n, graph)) critical.push([u, v]); + + // add it back + graph.get(u).push(v); + graph.get(v).push(u); + } + + return critical; +} +// end::criticalConnectionsBrute1[] + +module.exports = { criticalConnections, criticalConnectionsBrute1 }; diff --git a/book/interview-questions/critical-connections-in-a-network.spec.js b/book/interview-questions/critical-connections-in-a-network.spec.js new file mode 100644 index 00000000..003374d7 --- /dev/null +++ b/book/interview-questions/critical-connections-in-a-network.spec.js @@ -0,0 +1,37 @@ +const { criticalConnections, criticalConnectionsBrute1 } = require('./critical-connections-in-a-network'); +// const { } = require('../../src/index'); + +[criticalConnections, criticalConnectionsBrute1].forEach((fn) => { + describe(`Graph: ${fn.name}`, () => { + it('should work with null/empty', () => { + const actual = fn(0, []); + const expected = []; + expect(actual).toEqual(expected); + }); + + it('should work with critical path', () => { + const actual = fn(4, [[0, 1], [1, 2], [2, 0], [1, 3]]); + const expected = [[1, 3]]; + expect(actual).toEqual(expected); + }); + + it('should work without critical path', () => { + const actual = fn(4, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 2]]); + const expected = []; + expect(actual).toEqual(expected); + }); + + it('should work with other case', () => { + const actual = fn(3, [[0, 1], [1, 2]]); + const expected = [[0, 1], [1, 2]]; + expect(actual).toEqual(expect.arrayContaining(expected)); + }); + + + it('should work with 2 SCC', () => { + const actual = fn(6, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 4], [4, 5], [5, 3]]); + const expected = [[1, 3]]; + expect(actual).toEqual(expected); + }); + }); +}); diff --git a/book/interview-questions/network-delay-time.js b/book/interview-questions/network-delay-time.js new file mode 100644 index 00000000..99f8c108 --- /dev/null +++ b/book/interview-questions/network-delay-time.js @@ -0,0 +1,28 @@ +// https://leetcode.com/problems/network-delay-time/solution/ +function networkDelayTime(times: number[][], N: number, K: number): number { + const graph = new Map(Array(N).fill(0).map((_, i) => [i + 1, []])); + times.forEach(([u, v, w]) => graph.get(u)?.push([v, w])); + + const queue = new Queue([[K, 0]]); + const seen = Array(N + 1).fill(Infinity); + + while (queue.size()) { + const [node, dist] = queue.dequeue(); + seen[node] = Math.min(seen[node], dist); + + for (const [adj, w] of graph.get(node) || []) { + if (seen[adj] > dist + w) queue.enqueue([adj, dist + w]); + } + } + + const max = Math.max(...seen.slice(1)); + return max === Infinity ? -1 : max; +}; + +/* +[[2,1,1],[2,3,1],[3,4,1]] +4 +2 + + +*/ diff --git a/book/interview-questions/network-delay-time.spec.js b/book/interview-questions/network-delay-time.spec.js new file mode 100644 index 00000000..c56ff203 --- /dev/null +++ b/book/interview-questions/network-delay-time.spec.js @@ -0,0 +1,5 @@ +describe('', () => { + it('', () => { + + }); +}); diff --git a/package-lock.json b/package-lock.json index 7d5d9509..bf7a41de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dsa.js", - "version": "1.17.0", + "version": "1.18.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index feaa51db..5e130a84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsa.js", - "version": "1.17.0", + "version": "1.18.0", "description": "Data Structures & Algorithms in JS", "author": "Adrian Mejia (https://adrianmejia.com)", "homepage": "https://github.com/amejiarosario/dsa.js",