diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..713f733097 Binary files /dev/null and b/.gitattributes differ diff --git a/.husky/.gitignore b/.husky/.gitignore index 31354ec138..82cec543b4 100644 --- a/.husky/.gitignore +++ b/.husky/.gitignore @@ -1 +1,7 @@ -_ +# Enforce LF for all text files +* text=auto eol=lf + +# Ignore all files in .husky except .sh scripts +* +!.gitignore +!*.sh diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 598a5dceaa..0000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npm run lint -# npm run test diff --git a/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js b/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js index c3f9903102..b242ed0690 100644 --- a/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js +++ b/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js @@ -27,15 +27,40 @@ describe('detectUndirectedCycle', () => { .addEdge(edgeBC) .addEdge(edgeCD); + // no cycle yet expect(detectUndirectedCycle(graph)).toBeNull(); + // add the final edge that closes cycle B-C-D-E-B graph.addEdge(edgeDE); - expect(detectUndirectedCycle(graph)).toEqual({ - B: vertexC, - C: vertexD, - D: vertexE, - E: vertexB, + const cycle = detectUndirectedCycle(graph); + + // should return ordered array of vertices representing cycle (first === last) + expect(Array.isArray(cycle)).toBe(true); + expect(cycle.length).toBeGreaterThanOrEqual(3); + expect(cycle[0].getKey()).toBe(cycle[cycle.length - 1].getKey()); + + // Extract keys for easier assertions + const keys = cycle.map((v) => v.getKey()); + + // The expected cycle is B -> C -> D -> E -> B (but the returned cycle may be a rotation), + // so accept any rotation of that sequence. + const allowedRotations = [ + ['B', 'C', 'D', 'E', 'B'], + ['C', 'D', 'E', 'B', 'C'], + ['D', 'E', 'B', 'C', 'D'], + ['E', 'B', 'C', 'D', 'E'], + ]; + + // Check that keys match one of the allowed rotations + const matchesRotation = allowedRotations.some((rot) => { + if (rot.length !== keys.length) return false; + for (let i = 0; i < rot.length; i += 1) { + if (rot[i] !== keys[i]) return false; + } + return true; }); + + expect(matchesRotation).toBe(true); }); }); diff --git a/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js b/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js index 5bcc9bb699..b6f1d49bdd 100644 --- a/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js +++ b/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js @@ -4,56 +4,57 @@ import depthFirstSearch from '../depth-first-search/depthFirstSearch'; * Detect cycle in undirected graph using Depth First Search. * * @param {Graph} graph + * @returns {Vertex[] | null} ordered array of vertices forming the cycle (first === last), or null */ export default function detectUndirectedCycle(graph) { - let cycle = null; + let cycle = null; // will hold ordered array once found - // List of vertices that we have visited. - const visitedVertices = {}; + const visitedVertices = {}; // visited vertices + const parents = {}; // parent for every visited vertex - // List of parents vertices for every visited vertex. - const parents = {}; - - // Callbacks for DFS traversing. const callbacks = { allowTraversal: ({ currentVertex, nextVertex }) => { - // Don't allow further traversal in case if cycle has been detected. - if (cycle) { - return false; - } + if (cycle) return false; // stop traversal once cycle is found - // Don't allow traversal from child back to its parent. const currentVertexParent = parents[currentVertex.getKey()]; - const currentVertexParentKey = currentVertexParent ? currentVertexParent.getKey() : null; + const currentVertexParentKey = currentVertexParent + ? currentVertexParent.getKey() + : null; return currentVertexParentKey !== nextVertex.getKey(); }, + enterVertex: ({ currentVertex, previousVertex }) => { if (visitedVertices[currentVertex.getKey()]) { - // Compile cycle path based on parents of previous vertices. - cycle = {}; - - let currentCycleVertex = currentVertex; - let previousCycleVertex = previousVertex; - - while (previousCycleVertex.getKey() !== currentVertex.getKey()) { - cycle[currentCycleVertex.getKey()] = previousCycleVertex; - currentCycleVertex = previousCycleVertex; - previousCycleVertex = parents[previousCycleVertex.getKey()]; + // Build ordered cycle array + const startKey = currentVertex.getKey(); + const cycleArr = [currentVertex]; + + let walker = previousVertex; + while (walker && walker.getKey() !== startKey) { + cycleArr.push(walker); + walker = parents[walker.getKey()]; } - cycle[currentCycleVertex.getKey()] = previousCycleVertex; + cycleArr.push(currentVertex); // close the cycle + cycle = cycleArr; } else { - // Add next vertex to visited set. visitedVertices[currentVertex.getKey()] = currentVertex; parents[currentVertex.getKey()] = previousVertex; } }, }; - // Start DFS traversing. - const startVertex = graph.getAllVertices()[0]; - depthFirstSearch(graph, startVertex, callbacks); + const allVertices = graph.getAllVertices(); + for (let i = 0; i < allVertices.length; i += 1) { + const startVertex = allVertices[i]; + + if (!visitedVertices[startVertex.getKey()]) { + depthFirstSearch(graph, startVertex, callbacks); + + if (cycle) break; // early exit once cycle is found + } + } return cycle; }