UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

341 lines 12.2 kB
/** * 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