27
Data Structures in TypeScript - Graph
The graph is a data structure that consists of vertices (or nodes) that can be connected to other vertices by edges.
The degree is the number of edges that are connected to a vertex, for example, the vertex A has a degree of 1 and the vertex C has a degree of 2.
Graphs can be either directed or undirected, directed graphs are like a one-way street, undirected is like a two-way street.
Graphs can also have cycles.
Graphs might be a disconnected one, that means it consists of isolated subgraphs, or a connected one, in which all every pair of nodes are connected by an edge.
Graphs can be used to represent networks, websites structure, also used in path optimization algorithms, there are applications in other fields, such as linguistics, physics, chemistry, biology, mathematics, etc.
Graphs can be represented with
Adjacency list - Every node stores a list of adjacent vertices, for example, an array or that contains all vertices and each vertex contains another array with adjacent vertices, other data structures can be used instead of an array, like a hash table and a linked list.
Adjacency matrix - An NxN boolean matrix (where N is the number of vertices), if the matrix[i][j] stores the value true, there is a connection between the vertices i and j. In an undirected graph matrix[j][i] also will store the value true. You can use other types instead of boolean, for example, numbers to represent weight.
Depth-first search is a way to navigate a graph, it starts from a given vertex and visits each branch completely before moving to another branch. DFS is often used when we need to visit every node in the graph.
Using DPS on the graph above the nodes will be visited in the following order: A, B, D, C, E, F.
This is another way to navigate a graph, it starts from a given vertex and visits all adjacent vertices before go to any of their children. BFS is useful to find a path between two nodes.
Using DPS on the graph above the nodes will be visited in the following order: A, B, E, F, D, C.
Consists of running two breadth-first searches simultaneously, each one starts from a different vertex and runs until they collide. This is useful to find the shortest path between two vertices.
Heres an implementation of a directed graph using a adjacency list, because it will perform better in almost all operations:
export class Node<T> {
data: T;
adjacent: Node<T>[];
comparator: (a: T, b: T) => number;
constructor(data: T, comparator: (a: T, b: T) => number) {
this.data = data;
this.adjacent = [];
this.comparator = comparator;
}
addAdjacent(node: Node<T>): void {
this.adjacent.push(node);
}
removeAdjacent(data: T): Node<T> | null {
const index = this.adjacent.findIndex(
(node) => this.comparator(node.data, data) === 0
);
if (index > -1) {
return this.adjacent.splice(index, 1)[0];
}
return null;
}
}
class Graph<T> {
nodes: Map<T, Node<T>> = new Map();
comparator: (a: T, b: T) => number;
constructor(comparator: (a: T, b: T) => number) {
this.comparator = comparator;
}
/**
* Add a new node if it was not added before
*
* @param {T} data
* @returns {Node<T>}
*/
addNode(data: T): Node<T> {
let node = this.nodes.get(data);
if (node) return node;
node = new Node(data, this.comparator);
this.nodes.set(data, node);
return node;
}
/**
* Remove a node, also remove it from other nodes adjacency list
*
* @param {T} data
* @returns {Node<T> | null}
*/
removeNode(data: T): Node<T> | null {
const nodeToRemove = this.nodes.get(data);
if (!nodeToRemove) return null;
this.nodes.forEach((node) => {
node.removeAdjacent(nodeToRemove.data);
});
this.nodes.delete(data);
return nodeToRemove;
}
/**
* Create an edge between two nodes
*
* @param {T} source
* @param {T} destination
*/
addEdge(source: T, destination: T): void {
const sourceNode = this.addNode(source);
const destinationNode = this.addNode(destination);
sourceNode.addAdjacent(destinationNode);
}
/**
* Remove an edge between two nodes
*
* @param {T} source
* @param {T} destination
*/
removeEdge(source: T, destination: T): void {
const sourceNode = this.nodes.get(source);
const destinationNode = this.nodes.get(destination);
if (sourceNode && destinationNode) {
sourceNode.removeAdjacent(destination);
}
}
/**
* Depth-first search
*
* @param {T} data
* @param {Map<T, boolean>} visited
* @returns
*/
private depthFirstSearchAux(node: Node<T>, visited: Map<T, boolean>): void {
if (!node) return;
visited.set(node.data, true);
console.log(node.data);
node.adjacent.forEach((item) => {
if (!visited.has(item.data)) {
this.depthFirstSearchAux(item, visited);
}
});
}
depthFirstSearch() {
const visited: Map<T, boolean> = new Map();
this.nodes.forEach((node) => {
if (!visited.has(node.data)) {
this.depthFirstSearchAux(node, visited);
}
});
}
/**
* Breadth-first search
*
* @param {T} data
* @returns
*/
private breadthFirstSearchAux(node: Node<T>, visited: Map<T, boolean>): void {
const queue: Queue<Node<T>> = new Queue();
if (!node) return;
queue.add(node);
visited.set(node.data, true);
while (!queue.isEmpty()) {
node = queue.remove();
if (!node) continue;
console.log(node.data);
node.adjacent.forEach((item) => {
if (!visited.has(item.data)) {
visited.set(item.data, true);
queue.add(item);
}
});
}
}
breadthFirstSearch() {
const visited: Map<T, boolean> = new Map();
this.nodes.forEach((node) => {
if (!visited.has(node.data)) {
this.breadthFirstSearchAux(node, visited);
}
});
}
}
function comparator(a: number, b: number) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
const graph = new Graph(comparator);
27