UNPKG

deque-typed

Version:
627 lines (578 loc) 19.6 kB
/** * data-structure-typed * * @author Pablo Zeng * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com> * @license MIT License */ import type { GraphOptions, VertexKey } from '../../types'; import { IGraph } from '../../interfaces'; import { AbstractEdge, AbstractGraph, AbstractVertex } from './abstract-graph'; import { arrayRemove } from '../../utils'; export class UndirectedVertex<V = any> extends AbstractVertex<V> { constructor(key: VertexKey, value?: V) { super(key, value); } } export class UndirectedEdge<E = number> extends AbstractEdge<E> { endpoints: [VertexKey, VertexKey]; constructor(v1: VertexKey, v2: VertexKey, weight?: number, value?: E) { super(weight, value); this.endpoints = [v1, v2]; } } /** * Undirected graph implementation. * @template V - Vertex value type. * @template E - Edge value type. * @template VO - Concrete vertex class (extends AbstractVertex<V>). * @template EO - Concrete edge class (extends AbstractEdge<E>). * @remarks Time O(1), Space O(1) * @example * // basic UndirectedGraph vertex and edge creation * // Create a simple undirected graph * const graph = new UndirectedGraph<string>(); * * // Add vertices * graph.addVertex('A'); * graph.addVertex('B'); * graph.addVertex('C'); * graph.addVertex('D'); * * // Verify vertices exist * console.log(graph.hasVertex('A')); // true; * console.log(graph.hasVertex('B')); // true; * console.log(graph.hasVertex('E')); // false; * * // Check vertex count * console.log(graph.size); // 4; * @example * // UndirectedGraph edge operations (bidirectional) * const graph = new UndirectedGraph<string>(); * * // Add vertices * graph.addVertex('A'); * graph.addVertex('B'); * graph.addVertex('C'); * * // Add undirected edges (both directions automatically) * graph.addEdge('A', 'B', 1); * graph.addEdge('B', 'C', 2); * graph.addEdge('A', 'C', 3); * * // Verify edges exist in both directions * console.log(graph.hasEdge('A', 'B')); // true; * console.log(graph.hasEdge('B', 'A')); // true; // Bidirectional! * * console.log(graph.hasEdge('C', 'B')); // true; * console.log(graph.hasEdge('B', 'C')); // true; // Bidirectional! * * // Get neighbors of A * const neighborsA = graph.getNeighbors('A'); * console.log(neighborsA[0].key); // 'B'; * console.log(neighborsA[1].key); // 'C'; * @example * // UndirectedGraph deleteEdge and vertex operations * const graph = new UndirectedGraph<string>(); * * // Build a simple undirected graph * graph.addVertex('X'); * graph.addVertex('Y'); * graph.addVertex('Z'); * graph.addEdge('X', 'Y', 1); * graph.addEdge('Y', 'Z', 2); * graph.addEdge('X', 'Z', 3); * * // Delete an edge * graph.deleteEdge('X', 'Y'); * console.log(graph.hasEdge('X', 'Y')); // false; * * // Bidirectional deletion confirmed * console.log(graph.hasEdge('Y', 'X')); // false; * * // Other edges should remain * console.log(graph.hasEdge('Y', 'Z')); // true; * console.log(graph.hasEdge('Z', 'Y')); // true; * * // Delete a vertex * graph.deleteVertex('Y'); * console.log(graph.hasVertex('Y')); // false; * console.log(graph.size); // 2; * @example * // UndirectedGraph connectivity and neighbors * const graph = new UndirectedGraph<string>(); * * // Build a friendship network * const people = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve']; * for (const person of people) { * graph.addVertex(person); * } * * // Add friendships (undirected edges) * graph.addEdge('Alice', 'Bob', 1); * graph.addEdge('Alice', 'Charlie', 1); * graph.addEdge('Bob', 'Diana', 1); * graph.addEdge('Charlie', 'Eve', 1); * graph.addEdge('Diana', 'Eve', 1); * * // Get friends of each person * const aliceFriends = graph.getNeighbors('Alice'); * console.log(aliceFriends[0].key); // 'Bob'; * console.log(aliceFriends[1].key); // 'Charlie'; * console.log(aliceFriends.length); // 2; * * const dianaFriends = graph.getNeighbors('Diana'); * console.log(dianaFriends[0].key); // 'Bob'; * console.log(dianaFriends[1].key); // 'Eve'; * console.log(dianaFriends.length); // 2; * * // Verify bidirectional friendship * const bobFriends = graph.getNeighbors('Bob'); * console.log(bobFriends[0].key); // 'Alice'; // Alice -> Bob -> Alice ✓ * console.log(bobFriends[1].key); // 'Diana'; * @example * // UndirectedGraph for social network connectivity analysis * interface Person { * id: number; * name: string; * location: string; * } * * // UndirectedGraph is perfect for modeling symmetric relationships * // (friendships, collaborations, partnerships) * const socialNetwork = new UndirectedGraph<number, Person>(); * * // Add people as vertices * const people: [number, Person][] = [ * [1, { id: 1, name: 'Alice', location: 'New York' }], * [2, { id: 2, name: 'Bob', location: 'San Francisco' }], * [3, { id: 3, name: 'Charlie', location: 'Boston' }], * [4, { id: 4, name: 'Diana', location: 'New York' }], * [5, { id: 5, name: 'Eve', location: 'Seattle' }] * ]; * * for (const [id] of people) { * socialNetwork.addVertex(id); * } * * // Add friendships (automatically bidirectional) * socialNetwork.addEdge(1, 2, 1); // Alice <-> Bob * socialNetwork.addEdge(1, 3, 1); // Alice <-> Charlie * socialNetwork.addEdge(2, 4, 1); // Bob <-> Diana * socialNetwork.addEdge(3, 5, 1); // Charlie <-> Eve * socialNetwork.addEdge(4, 5, 1); // Diana <-> Eve * * console.log(socialNetwork.size); // 5; * * // Find direct connections for Alice * const aliceConnections = socialNetwork.getNeighbors(1); * console.log(aliceConnections[0].key); // 2; * console.log(aliceConnections[1].key); // 3; * console.log(aliceConnections.length); // 2; * * // Verify bidirectional connections * console.log(socialNetwork.hasEdge(1, 2)); // true; * console.log(socialNetwork.hasEdge(2, 1)); // true; // Friendship works both ways! * * // Remove a person from network * socialNetwork.deleteVertex(2); // Bob leaves * console.log(socialNetwork.hasVertex(2)); // false; * console.log(socialNetwork.size); // 4; * * // Alice loses Bob as a friend * const updatedAliceConnections = socialNetwork.getNeighbors(1); * console.log(updatedAliceConnections[0].key); // 3; * console.log(updatedAliceConnections[1]); // undefined; * * // Diana loses Bob as a friend * const dianaConnections = socialNetwork.getNeighbors(4); * console.log(dianaConnections[0].key); // 5; * console.log(dianaConnections[1]); // undefined; */ export class UndirectedGraph< V = any, E = any, VO extends UndirectedVertex<V> = UndirectedVertex<V>, EO extends UndirectedEdge<E> = UndirectedEdge<E> > extends AbstractGraph<V, E, VO, EO> implements IGraph<V, E, VO, EO> { /** * Construct an undirected graph with runtime defaults. * @param options - `GraphOptions<V>` (e.g. `vertexValueInitializer`, `defaultEdgeWeight`). * @remarks Time O(1), Space O(1) */ constructor(options?: Partial<GraphOptions<V>>) { super(options); this._edgeMap = new Map<VO, EO[]>(); } protected _edgeMap: Map<VO, EO[]>; get edgeMap(): Map<VO, EO[]> { return this._edgeMap; } set edgeMap(v: Map<VO, EO[]>) { this._edgeMap = v; } /** * Construct an undirected graph from keys with value initializer `v => v`. * @template K - Vertex key type. * @param keys - Iterable of vertex keys. * @returns UndirectedGraph with all keys added. * @remarks Time O(V), Space O(V) */ static fromKeys<K extends VertexKey>( keys: Iterable<K> ): UndirectedGraph<K, any, UndirectedVertex<K>, UndirectedEdge<any>> { const g: UndirectedGraph<K, any, UndirectedVertex<K>, UndirectedEdge<any>> = new UndirectedGraph<K, any>({ vertexValueInitializer: (k: VertexKey) => k as K }); for (const k of keys) g.addVertex(k); return g; } /** * Construct an undirected graph from `[key, value]` entries. * @template V - Vertex value type. * @param entries - Iterable of `[key, value]` pairs. * @returns UndirectedGraph with all vertices added. * @remarks Time O(V), Space O(V) */ static fromEntries<V>( entries: Iterable<[VertexKey, V]> ): UndirectedGraph<V, any, UndirectedVertex<V>, UndirectedEdge<any>> { const g: UndirectedGraph<V, any, UndirectedVertex<V>, UndirectedEdge<any>> = new UndirectedGraph<V, any>(); for (const [k, v] of entries) g.addVertex(k, v); return g; } /** * Create an undirected vertex instance. Does not insert into the graph. * @param key - Vertex identifier. * @param value - Optional payload. * @returns Concrete vertex instance. * @remarks Time O(1), Space O(1) */ createVertex(key: VertexKey, value?: VO['value']): VO { return new UndirectedVertex(key, value) as VO; } /** * Create an undirected edge instance. Does not insert into the graph. * @param v1 - One endpoint key. * @param v2 - The other endpoint key. * @param weight - Edge weight; defaults to `defaultEdgeWeight`. * @param value - Edge payload. * @returns Concrete edge instance. * @remarks Time O(1), Space O(1) */ override createEdge(v1: VertexKey, v2: VertexKey, weight?: number, value?: EO['value']): EO { return new UndirectedEdge(v1, v2, weight ?? this.options.defaultEdgeWeight ?? 1, value) as EO; } /** * Get an undirected edge between two vertices, if present. * @param v1 - One vertex or key. * @param v2 - The other vertex or key. * @returns Edge instance or `undefined`. * @remarks Time O(1) avg, Space O(1) */ getEdge(v1: VO | VertexKey | undefined, v2: VO | VertexKey | undefined): EO | undefined { let edgeMap: EO[] | undefined = []; if (v1 !== undefined && v2 !== undefined) { const vertex1: VO | undefined = this._getVertex(v1); const vertex2: VO | undefined = this._getVertex(v2); if (vertex1 && vertex2) { edgeMap = this._edgeMap.get(vertex1)?.filter(e => e.endpoints.includes(vertex2.key)); } } return edgeMap ? edgeMap[0] || undefined : undefined; } /** * Delete a single undirected edge between two vertices. * @param v1 - One vertex or key. * @param v2 - The other vertex or key. * @returns Removed edge or `undefined`. * @remarks Time O(1) avg, Space O(1) */ deleteEdgeBetween(v1: VO | VertexKey, v2: VO | VertexKey): EO | undefined { const vertex1: VO | undefined = this._getVertex(v1); const vertex2: VO | undefined = this._getVertex(v2); if (!vertex1 || !vertex2) { return undefined; } const v1Edges = this._edgeMap.get(vertex1); let removed: EO | undefined = undefined; if (v1Edges) { removed = arrayRemove<EO>(v1Edges, (e: EO) => e.endpoints.includes(vertex2.key))[0] || undefined; } const v2Edges = this._edgeMap.get(vertex2); if (v2Edges) { arrayRemove<EO>(v2Edges, (e: EO) => e.endpoints.includes(vertex1.key)); } return removed; } /** * Delete an edge by instance or by a pair of keys. * @param edgeOrOneSideVertexKey - Edge instance or one endpoint vertex/key. * @param otherSideVertexKey - Required second endpoint when deleting by pair. * @returns Removed edge or `undefined`. * @remarks Time O(1) avg, Space O(1) */ deleteEdge(edgeOrOneSideVertexKey: EO | VertexKey, otherSideVertexKey?: VertexKey): EO | undefined { let oneSide: VO | undefined, otherSide: VO | undefined; if (this.isVertexKey(edgeOrOneSideVertexKey)) { if (this.isVertexKey(otherSideVertexKey)) { oneSide = this._getVertex(edgeOrOneSideVertexKey); otherSide = this._getVertex(otherSideVertexKey); } else { return; } } else { oneSide = this._getVertex(edgeOrOneSideVertexKey.endpoints[0]); otherSide = this._getVertex(edgeOrOneSideVertexKey.endpoints[1]); } if (oneSide && otherSide) { return this.deleteEdgeBetween(oneSide, otherSide); } else { return; } } /** * Delete a vertex and remove it from all neighbor lists. * @param vertexOrKey - Vertex or key. * @returns `true` if removed; otherwise `false`. * @remarks Time O(deg), Space O(1) */ deleteVertex(vertexOrKey: VO | VertexKey): boolean { let vertexKey: VertexKey; let vertex: VO | undefined; if (this.isVertexKey(vertexOrKey)) { vertex = this.getVertex(vertexOrKey); vertexKey = vertexOrKey; } else { vertex = vertexOrKey; vertexKey = this._getVertexKey(vertexOrKey); } /** * All neighbors connected via undirected edges. * @param vertexOrKey - Vertex or key. * @returns Array of neighbor vertices. * @remarks Time O(deg), Space O(deg) */ const neighbors = this.getNeighbors(vertexOrKey); if (vertex) { neighbors.forEach(neighbor => { const neighborEdges = this._edgeMap.get(neighbor); if (neighborEdges) { const restEdges = neighborEdges.filter(edge => { return !edge.endpoints.includes(vertexKey); }); this._edgeMap.set(neighbor, restEdges); } }); this._edgeMap.delete(vertex); } return this._vertexMap.delete(vertexKey); } /** * Degree of a vertex (# of incident undirected edges). * @param vertexOrKey - Vertex or key. * @returns Non-negative integer. * @remarks Time O(1) avg, Space O(1) */ degreeOf(vertexOrKey: VertexKey | VO): number { const vertex = this._getVertex(vertexOrKey); if (vertex) { return this._edgeMap.get(vertex)?.length || 0; } else { return 0; } } /** * Incident undirected edges of a vertex. * @param vertexOrKey - Vertex or key. * @returns Array of incident edges. * @remarks Time O(deg), Space O(deg) */ edgesOf(vertexOrKey: VertexKey | VO): EO[] { const vertex = this._getVertex(vertexOrKey); if (vertex) { return this._edgeMap.get(vertex) || []; } else { return []; } } /** * Unique set of undirected edges across endpoints. * @returns Array of edges. * @remarks Time O(E), Space O(E) */ edgeSet(): EO[] { const edgeSet: Set<EO> = new Set(); this._edgeMap.forEach(edgeMap => { edgeMap.forEach(edge => { edgeSet.add(edge); }); }); return [...edgeSet]; } getNeighbors(vertexOrKey: VO | VertexKey): VO[] { const neighbors: VO[] = []; const vertex = this._getVertex(vertexOrKey); if (vertex) { const neighborEdges = this.edgesOf(vertex); for (const edge of neighborEdges) { const neighbor = this._getVertex(edge.endpoints.filter(e => e !== vertex.key)[0]); if (neighbor) { neighbors.push(neighbor); } } } return neighbors; } /** * Resolve an edge's two endpoints to vertex instances. * @param edge - Edge instance. * @returns `[v1, v2]` or `undefined` if either endpoint is missing. * @remarks Time O(1), Space O(1) */ getEndsOfEdge(edge: EO): [VO, VO] | undefined { if (!this.hasEdge(edge.endpoints[0], edge.endpoints[1])) { return undefined; } const v1 = this._getVertex(edge.endpoints[0]); const v2 = this._getVertex(edge.endpoints[1]); if (v1 && v2) { return [v1, v2]; } else { return undefined; } } /** * Whether the graph has no vertices and no edges. * @remarks Time O(1), Space O(1) */ isEmpty(): boolean { return this.vertexMap.size === 0 && this.edgeMap.size === 0; } /** * Remove all vertices and edges. * @remarks Time O(V + E), Space O(1) */ clear() { this._vertexMap = new Map<VertexKey, VO>(); this._edgeMap = new Map<VO, EO[]>(); } /** * Deep clone as the same concrete class. * @returns A new graph of the same concrete class (`this` type). * @remarks Time O(V + E), Space O(V + E) */ override clone(): this { return super.clone(); } /** * Tarjan-based bridge and articulation point detection. * @returns `{ dfnMap, lowMap, bridges, cutVertices }`. * @remarks Time O(V + E), Space O(V + E) */ tarjan(): { dfnMap: Map<VO, number>; lowMap: Map<VO, number>; bridges: EO[]; cutVertices: VO[] } { const dfnMap = new Map<VO, number>(); const lowMap = new Map<VO, number>(); const bridges: EO[] = []; const cutVertices: VO[] = []; let time = 0; const dfs = (vertex: VO, parent: VO | undefined) => { dfnMap.set(vertex, time); lowMap.set(vertex, time); time++; const neighbors = this.getNeighbors(vertex); let childCount = 0; for (const neighbor of neighbors) { if (!dfnMap.has(neighbor)) { childCount++; dfs(neighbor, vertex); lowMap.set(vertex, Math.min(lowMap.get(vertex)!, lowMap.get(neighbor)!)); if (lowMap.get(neighbor)! > dfnMap.get(vertex)!) { // Found a bridge const edge = this.getEdge(vertex, neighbor); if (edge) { bridges.push(edge); } } if (parent !== undefined && lowMap.get(neighbor)! >= dfnMap.get(vertex)!) { // Found an articulation point cutVertices.push(vertex); } } else if (neighbor !== parent) { lowMap.set(vertex, Math.min(lowMap.get(vertex)!, dfnMap.get(neighbor)!)); } } if (parent === undefined && childCount > 1) { // Special case for root in DFS tree cutVertices.push(vertex); } }; for (const vertex of this.vertexMap.values()) { if (!dfnMap.has(vertex)) { dfs(vertex, undefined); } } return { dfnMap, lowMap, bridges, cutVertices }; } /** * Get bridges discovered by `tarjan()`. * @returns Array of edges that are bridges. * @remarks Time O(B), Space O(1) */ getBridges() { return this.tarjan().bridges; } /** * Get articulation points discovered by `tarjan()`. * @returns Array of cut vertices. * @remarks Time O(C), Space O(1) */ getCutVertices() { return this.tarjan().cutVertices; } /** * DFN index map computed by `tarjan()`. * @returns Map from vertex to DFN index. * @remarks Time O(V), Space O(V) */ getDFNMap() { return this.tarjan().dfnMap; } /** * LOW link map computed by `tarjan()`. * @returns Map from vertex to LOW value. * @remarks Time O(V), Space O(V) */ getLowMap() { return this.tarjan().lowMap; } /** * Internal hook to attach an undirected edge into adjacency maps. * @param edge - Edge instance. * @returns `true` if both endpoints exist; otherwise `false`. * @remarks Time O(1) avg, Space O(1) */ protected _addEdge(edge: EO): boolean { for (const end of edge.endpoints) { const endVertex = this._getVertex(end); if (endVertex === undefined) return false; if (endVertex) { const edgeMap = this._edgeMap.get(endVertex); if (edgeMap) { edgeMap.push(edge); } else { this._edgeMap.set(endVertex, [edge]); } } } return true; } }