@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
341 lines • 12.2 kB
JavaScript
/**
* Compressed Sparse Row (CSR) graph representation
*
* Provides cache-efficient graph storage with sequential memory access patterns.
* Optimized for traversal operations and sparse graphs.
*/
export class CSRGraph {
constructor(adjacencyList, weights, buildReverse = true) {
this.data = this.buildCSR(adjacencyList, weights, buildReverse);
}
/**
* Build CSR structure from adjacency list
*/
buildCSR(adjacencyList, weights, buildReverse = true) {
// Collect all unique nodes (both sources and targets)
const allNodes = new Set();
for (const [source, neighbors] of adjacencyList) {
allNodes.add(source);
for (const neighbor of neighbors) {
allNodes.add(neighbor);
}
}
// Sort nodes for consistent ordering. Numeric IDs sort numerically,
// string IDs sort lexicographically for predictable iteration order.
const nodes = Array.from(allNodes).sort((a, b) => {
if (typeof a === "number" && typeof b === "number") {
return a - b;
}
return String(a).localeCompare(String(b));
});
const nodeCount = nodes.length;
// Build node mappings
const nodeIdToIndex = new Map();
const indexToNodeId = [];
nodes.forEach((nodeId, index) => {
nodeIdToIndex.set(nodeId, index);
indexToNodeId[index] = nodeId;
});
// Count total edges
let edgeCount = 0;
for (const neighbors of adjacencyList.values()) {
edgeCount += neighbors.length;
}
// Allocate arrays
const rowPointers = new Uint32Array(nodeCount + 1);
const columnIndices = new Uint32Array(edgeCount);
const edgeWeights = weights ? new Float32Array(edgeCount) : undefined;
// Build CSR structure
let currentEdge = 0;
for (let i = 0; i < nodeCount; i++) {
rowPointers[i] = currentEdge;
const nodeId = indexToNodeId[i];
if (nodeId === undefined) {
throw new Error(`Invalid node index ${String(i)}`);
}
const neighbors = adjacencyList.get(nodeId) ?? [];
// Sort neighbors for better cache locality and binary search
const sortedNeighbors = neighbors
.map((n) => {
const index = nodeIdToIndex.get(n);
if (index === undefined) {
throw new Error(`Node ${String(n)} not found in nodeIdToIndex map`);
}
return { id: n, index };
})
.sort((a, b) => a.index - b.index);
for (const neighbor of sortedNeighbors) {
columnIndices[currentEdge] = neighbor.index;
if (edgeWeights && weights) {
const edgeKey = `${String(nodeId)}-${String(neighbor.id)}`;
const weight = weights.get(edgeKey);
if (weight !== undefined) {
edgeWeights[currentEdge] = weight;
}
else {
edgeWeights[currentEdge] = 1;
}
}
currentEdge++;
}
}
rowPointers[nodeCount] = currentEdge;
const result = {
rowPointers,
columnIndices,
nodeIdToIndex,
indexToNodeId,
};
if (edgeWeights) {
result.edgeWeights = edgeWeights;
}
// Build reverse edges for bottom-up BFS
if (buildReverse) {
const reverseAdjacency = new Map();
for (let i = 0; i < nodeCount; i++) {
reverseAdjacency.set(i, []);
}
// Build reverse adjacency from forward edges
for (let i = 0; i < nodeCount; i++) {
const start = rowPointers[i];
const end = rowPointers[i + 1];
if (start !== undefined && end !== undefined) {
for (let j = start; j < end; j++) {
const target = columnIndices[j];
if (target !== undefined) {
reverseAdjacency.get(target)?.push(i);
}
}
}
}
// Build reverse CSR
const reverseRowPointers = new Uint32Array(nodeCount + 1);
const reverseColumnIndices = new Uint32Array(edgeCount);
let reverseEdge = 0;
for (let i = 0; i < nodeCount; i++) {
reverseRowPointers[i] = reverseEdge;
const incoming = reverseAdjacency.get(i) ?? [];
incoming.sort((a, b) => a - b);
for (const source of incoming) {
reverseColumnIndices[reverseEdge++] = source;
}
}
reverseRowPointers[nodeCount] = reverseEdge;
result.reverseRowPointers = reverseRowPointers;
result.reverseColumnIndices = reverseColumnIndices;
}
return result;
}
// Core API methods
nodeCount() {
return this.data.indexToNodeId.length;
}
edgeCount() {
return this.data.columnIndices.length;
}
hasNode(nodeId) {
return this.data.nodeIdToIndex.has(nodeId);
}
hasEdge(source, target) {
const sourceIndex = this.data.nodeIdToIndex.get(source);
const targetIndex = this.data.nodeIdToIndex.get(target);
if (sourceIndex === undefined || targetIndex === undefined) {
return false;
}
const start = this.data.rowPointers[sourceIndex];
const end = this.data.rowPointers[sourceIndex + 1];
// Binary search for target in sorted neighbors
if (start === undefined || end === undefined) {
return false;
}
return this.binarySearch(this.data.columnIndices, targetIndex, start, end) !== -1;
}
/**
* Get neighbors as node IDs
*/
neighbors(nodeId) {
const nodeIndex = this.data.nodeIdToIndex.get(nodeId);
if (nodeIndex === undefined) {
return new Set().values();
}
const start = this.data.rowPointers[nodeIndex];
const end = this.data.rowPointers[nodeIndex + 1];
const { columnIndices, indexToNodeId } = this.data;
function* generateNeighbors() {
if (start !== undefined && end !== undefined) {
for (let i = start; i < end; i++) {
const idx = columnIndices[i];
if (idx !== undefined) {
const nodeId = indexToNodeId[idx];
if (nodeId !== undefined) {
yield nodeId;
}
}
}
}
}
return generateNeighbors();
}
/**
* Get all nodes
*/
nodes() {
return this.data.indexToNodeId.values();
}
/**
* Get neighbors as indices (internal use)
*/
getNeighborIndices(nodeIndex) {
if (nodeIndex < 0 || nodeIndex >= this.data.indexToNodeId.length) {
return [];
}
const start = this.data.rowPointers[nodeIndex];
const end = this.data.rowPointers[nodeIndex + 1];
if (start === undefined || end === undefined) {
return [];
}
return Array.from(this.data.columnIndices.subarray(start, end));
}
outDegree(nodeId) {
const nodeIndex = this.data.nodeIdToIndex.get(nodeId);
if (nodeIndex === undefined) {
return 0;
}
return this.outDegreeByIndex(nodeIndex);
}
/**
* Get out-degree by index (internal use)
*/
outDegreeByIndex(nodeIndex) {
const start = this.data.rowPointers[nodeIndex];
const end = this.data.rowPointers[nodeIndex + 1];
if (start !== undefined && end !== undefined) {
return end - start;
}
return 0;
}
/**
* Iterator support for neighbor indices
*/
*iterateNeighborIndices(nodeIndex) {
const start = this.data.rowPointers[nodeIndex];
const end = this.data.rowPointers[nodeIndex + 1];
if (start !== undefined && end !== undefined) {
for (let i = start; i < end; i++) {
const value = this.data.columnIndices[i];
if (value !== undefined) {
yield value;
}
}
}
}
/**
* Iterator support for incoming neighbor indices (for bottom-up BFS)
*/
*iterateIncomingNeighborIndices(nodeIndex) {
if (!this.data.reverseRowPointers || !this.data.reverseColumnIndices) {
return;
}
const start = this.data.reverseRowPointers[nodeIndex];
const end = this.data.reverseRowPointers[nodeIndex + 1];
if (start !== undefined && end !== undefined) {
for (let i = start; i < end; i++) {
const value = this.data.reverseColumnIndices[i];
if (value !== undefined) {
yield value;
}
}
}
}
/**
* Convert node ID to index
*/
nodeToIndex(nodeId) {
const index = this.data.nodeIdToIndex.get(nodeId);
if (index === undefined) {
throw new Error(`Node ${String(nodeId)} not found in graph`);
}
return index;
}
/**
* Convert index to node ID
*/
indexToNodeId(index) {
const nodeId = this.data.indexToNodeId[index];
if (nodeId === undefined) {
throw new Error(`Index ${String(index)} out of bounds`);
}
return nodeId;
}
/**
* Get edge weight
*/
getEdgeWeight(source, target) {
if (!this.data.edgeWeights) {
return undefined;
}
const sourceIndex = this.data.nodeIdToIndex.get(source);
const targetIndex = this.data.nodeIdToIndex.get(target);
if (sourceIndex === undefined || targetIndex === undefined) {
return undefined;
}
const start = this.data.rowPointers[sourceIndex];
const end = this.data.rowPointers[sourceIndex + 1];
if (start === undefined || end === undefined) {
return undefined;
}
const edgeIndex = this.binarySearch(this.data.columnIndices, targetIndex, start, end);
if (edgeIndex === -1) {
return undefined;
}
return this.data.edgeWeights[edgeIndex];
}
/**
* Binary search for target in sorted array
*/
binarySearch(arr, target, start, end) {
let left = start;
let right = end - 1;
while (left <= right) {
const mid = (left + right) >>> 1;
const value = arr[mid];
if (value === undefined) {
return -1;
}
if (value === target) {
return mid;
}
if (value < target) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return -1;
}
/**
* Create CSR graph from standard Graph
*/
static fromGraph(graph) {
const adjacencyList = new Map();
const weights = new Map();
// Build adjacency list
for (const node of graph.nodes()) {
const neighbors = [];
for (const neighbor of graph.neighbors(node.id)) {
neighbors.push(neighbor);
// Get edge weight if available
if (graph.getEdge) {
const edge = graph.getEdge(node.id, neighbor);
if (edge?.weight !== undefined) {
weights.set(`${String(node.id)}-${String(neighbor)}`, edge.weight);
}
}
}
adjacencyList.set(node.id, neighbors);
}
return new CSRGraph(adjacencyList, weights.size > 0 ? weights : undefined);
}
}
//# sourceMappingURL=csr-graph.js.map