sequence-graph-data-structure
Version:
A graph data structure with topological sort.
466 lines (464 loc) • 16.1 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GraphDataStructure = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
;
Object.defineProperty(exports, "__esModule", { value: true });
class CycleError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, CycleError.prototype);
}
}
// A graph data structure with depth-first search and topological sort.
function Graph(serialized) {
// Returned graph instance
const graph = {
addNode,
removeNode,
nodes,
adjacent,
addEdge,
getEdges,
removeEdge,
hasEdge,
setEdgeWeight,
getEdgeWeight,
setEdgeId,
getEdgeId,
indegree,
outdegree,
depthFirstSearch,
hasCycle,
getCycles,
walk,
lowestCommonAncestors,
topologicalSort,
shortestPath,
serialize,
deserialize,
};
// The adjacency list of the graph.
// Keys are node ids.
// Values are adjacent node id arrays.
const edges = {};
// The weights of edges.
// Keys are string encodings of edges.
// Values are weights (numbers).
const edgeWeights = {};
// Creates a unique identifier for each each, separate from
// the EncodedEdge identifier.
const edgeIds = {};
// If a serialized graph was passed into the constructor, deserialize it.
if (serialized) {
deserialize(serialized);
}
// Adds a node to the graph.
// If node was already added, this function does nothing.
// If node was not already added, this function sets up an empty adjacency list.
function addNode(node) {
edges[node] = adjacent(node);
return graph;
}
// Removes a node from the graph.
// Also removes incoming and outgoing edges.
function removeNode(node) {
// Remove incoming edges.
Object.keys(edges).forEach(function (u) {
edges[u].forEach(function (v) {
if (v === node) {
removeEdge(u, v);
}
});
});
// Remove outgoing edges (and signal that the node no longer exists).
delete edges[node];
return graph;
}
// Gets the list of nodes that have been added to the graph.
function nodes() {
// TODO: Better implementation with set data structure
const nodeSet = {};
Object.keys(edges).forEach(function (u) {
nodeSet[u] = true;
edges[u].forEach(function (v) {
nodeSet[v] = true;
});
});
return Object.keys(nodeSet);
}
// Gets the adjacent node list for the given node.
// Returns an empty array for unknown nodes.
function adjacent(node) {
return edges[node] || [];
}
// Computes a string encoding of an edge,
// for use as a key in an object.
function encodeEdge(u, v) {
return u + "|" + v;
}
// Sets the weight of the given edge.
function setEdgeWeight(u, v, weight) {
edgeWeights[encodeEdge(u, v)] = weight;
return graph;
}
// Gets the weight of the given edge.
// Returns 1 if no weight was previously set.
function getEdgeWeight(u, v) {
const weight = edgeWeights[encodeEdge(u, v)];
return weight === undefined ? 1 : weight;
}
function setEdgeId(u, v, edgeId) {
edgeIds[encodeEdge(u, v)] = edgeId;
return graph;
}
function getEdgeId(u, v) {
return edgeIds[encodeEdge(u, v)];
}
// Adds an edge from node u to node v.
// Implicitly adds the nodes if they were not already added.
function addEdge(u, v, weight, edgeId) {
addNode(u);
addNode(v);
adjacent(u).push(v);
if (typeof weight !== "undefined") {
setEdgeWeight(u, v, weight);
}
if (typeof edgeId !== "undefined") {
setEdgeId(u, v, edgeId);
}
return graph;
}
// Removes the edge from node u to node v.
// Does not remove the nodes.
// Does nothing if the edge does not exist.
function removeEdge(u, v) {
if (edges[u]) {
edges[u] = adjacent(u).filter(function (_v) {
return _v !== v;
});
delete edgeIds[encodeEdge(u, v)];
}
return graph;
}
function getEdges() {
return edges;
}
// Returns true if there is an edge from node u to node v.
function hasEdge(u, v) {
return adjacent(u).includes(v);
}
// Computes the indegree for the given node.
// Not very efficient, costs O(E) where E = number of edges.
function indegree(node) {
let degree = 0;
function check(v) {
if (v === node) {
degree++;
}
}
Object.keys(edges).forEach(function (u) {
edges[u].forEach(check);
});
return degree;
}
// Computes the outdegree for the given node.
function outdegree(node) {
return node in edges ? edges[node].length : 0;
}
// Depth First Search algorithm, inspired by
// Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 604
// The additional option `includeSourceNodes` specifies whether to
// include or exclude the source nodes from the result (true by default).
// If `sourceNodes` is not specified, all nodes in the graph
// are used as source nodes.
function depthFirstSearchBase(type, sourceNodes, options = {
includeSourceNodes: true,
errorOnCycle: false,
collectCycles: false,
}) {
if (!sourceNodes) {
sourceNodes = nodes();
}
if (typeof options.includeSourceNodes !== "boolean") {
options.includeSourceNodes = true;
}
const visited = {};
const visiting = {};
const nodeList = [];
const cycles = {};
function DFSVisit(node, from) {
if (visiting[node] && options.errorOnCycle) {
throw new CycleError("Cycle found");
}
if (visiting[node] && options.collectCycles) {
cycles[from] = node;
return;
}
if (!visited[node]) {
visited[node] = true;
visiting[node] = true; // temporary flag while visiting
adjacent(node).forEach((adjacent) => DFSVisit(adjacent, node));
visiting[node] = false;
nodeList.push(node);
}
}
if (options.includeSourceNodes) {
sourceNodes.forEach((source) => DFSVisit(source));
}
else {
sourceNodes.forEach(function (node) {
visited[node] = true;
});
sourceNodes.forEach(function (node) {
adjacent(node).forEach((adjacent) => DFSVisit(adjacent, node));
});
}
if (type === "normal") {
return nodeList;
}
else {
return cycles;
}
}
function depthFirstSearch(sourceNodes, options = {
includeSourceNodes: true,
errorOnCycle: false,
}) {
return depthFirstSearchBase("normal", sourceNodes, Object.assign(Object.assign({}, options), { collectCycles: false }));
}
// Returns true if the graph has one or more cycles and false otherwise
function hasCycle() {
try {
depthFirstSearchBase("normal", undefined, {
errorOnCycle: true,
includeSourceNodes: true,
collectCycles: false,
});
// No error thrown -> no cycles
return false;
}
catch (error) {
if (error instanceof CycleError) {
return true;
}
else {
throw error;
}
}
}
function getCycles(sourceNodes) {
const cycles = depthFirstSearchBase("cycles", sourceNodes, {
collectCycles: true,
errorOnCycle: false,
includeSourceNodes: typeof sourceNodes !== "undefined",
});
const cycleList = [];
Object.keys(cycles).forEach((source) => {
if (source < cycles[source]) {
cycleList.push([source, cycles[source]]);
}
else {
cycleList.push([cycles[source], source]);
}
});
return cycleList;
}
// Least Common Ancestors
// Inspired by https://github.com/relaxedws/lca/blob/master/src/LowestCommonAncestor.php code
// but uses depth search instead of breadth. Also uses some optimizations
function lowestCommonAncestors(node1, node2) {
const node1Ancestors = [];
const lcas = [];
function CA1Visit(visited, node) {
if (!visited[node]) {
visited[node] = true;
node1Ancestors.push(node);
if (node == node2) {
lcas.push(node);
return false; // found - shortcut
}
return adjacent(node).every((node) => {
return CA1Visit(visited, node);
});
}
else {
return true;
}
}
function CA2Visit(visited, node) {
if (!visited[node]) {
visited[node] = true;
if (node1Ancestors.indexOf(node) >= 0) {
lcas.push(node);
}
else if (lcas.length == 0) {
adjacent(node).forEach((node) => {
CA2Visit(visited, node);
});
}
}
}
if (CA1Visit({}, node1)) {
// No shortcut worked
CA2Visit({}, node2);
}
return lcas;
}
// The topological sort algorithm yields a list of visited nodes
// such that for each visited edge (u, v), u comes before v in the list.
// Amazingly, this comes from just reversing the result from depth first search.
// Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 613
function topologicalSort(sourceNodes, includeSourceNodes = true) {
return depthFirstSearchBase("normal", sourceNodes, {
includeSourceNodes,
errorOnCycle: true,
}).reverse();
}
// Dijkstra's Shortest Path Algorithm.
// Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 658
// Variable and function names correspond to names in the book.
function shortestPath(source, destination) {
// Upper bounds for shortest path weights from source.
const d = {};
// Predecessors.
const p = {};
// Poor man's priority queue, keyed on d.
let q = {};
function initializeSingleSource() {
nodes().forEach(function (node) {
d[node] = Infinity;
});
if (d[source] !== Infinity) {
throw new Error("Source node is not in the graph");
}
if (d[destination] !== Infinity) {
throw new Error("Destination node is not in the graph");
}
d[source] = 0;
}
// Adds entries in q for all nodes.
function initializePriorityQueue() {
nodes().forEach(function (node) {
q[node] = true;
});
}
// Returns true if q is empty.
function priorityQueueEmpty() {
return Object.keys(q).length === 0;
}
// Linear search to extract (find and remove) min from q.
function extractMin() {
let min = Infinity;
let minNode;
Object.keys(q).forEach(function (node) {
if (d[node] < min) {
min = d[node];
minNode = node;
}
});
if (minNode === undefined) {
// If we reach here, there's a disconnected subgraph, and we're done.
q = {};
return null;
}
delete q[minNode];
return minNode;
}
function relax(u, v) {
const w = getEdgeWeight(u, v);
if (d[v] > d[u] + w) {
d[v] = d[u] + w;
p[v] = u;
}
}
function dijkstra() {
initializeSingleSource();
initializePriorityQueue();
while (!priorityQueueEmpty()) {
const u = extractMin();
if (u === null)
return;
adjacent(u).forEach(function (v) {
relax(u, v);
});
}
}
// Assembles the shortest path by traversing the
// predecessor subgraph from destination to source.
function path() {
const nodeList = [];
let weight = 0;
let node = destination;
while (p[node]) {
nodeList.push(node);
weight += getEdgeWeight(p[node], node);
node = p[node];
}
if (node !== source) {
throw new Error("No path found");
}
nodeList.push(node);
nodeList.reverse();
nodeList.weight = weight;
return nodeList;
}
dijkstra();
return path();
}
// Serializes the graph.
function serialize() {
const serialized = {
nodes: nodes().map(function (id) {
return { id: id };
}),
links: [],
};
serialized.nodes.forEach(function (node) {
const source = node.id;
adjacent(source).forEach(function (target) {
serialized.links.push({
source: source,
target: target,
weight: getEdgeWeight(source, target),
id: getEdgeId(source, target),
});
});
});
return serialized;
}
/**
* Walks through each node in the graph. Repeats elements
* if edges connect to a Node more than once.
*
* @param onElement Callback for node
*/
function walk(onElement) {
const visited = {};
const followNode = (following) => {
if (visited[following]) {
return;
}
visited[following] = true;
onElement(following);
const nextNodes = adjacent(following);
if (!nextNodes.length) {
return;
}
nextNodes.map(followNode);
};
nodes().map(followNode);
}
// Deserializes the given serialized graph.
function deserialize(serialized) {
serialized.nodes.forEach(function (node) {
addNode(node.id);
});
serialized.links.forEach(function (link) {
addEdge(link.source, link.target, link.weight);
});
return graph;
}
// The returned graph instance.
return graph;
}
exports.default = Graph;
},{}]},{},[1])(1)
});