diff --git a/__tests__/unit/bfs.spec.ts b/__tests__/unit/bfs.spec.ts new file mode 100644 index 0000000..baf0a8f --- /dev/null +++ b/__tests__/unit/bfs.spec.ts @@ -0,0 +1,200 @@ +import breadthFirstSearch from "../../packages/graph/src/bfs"; +import { Graph } from "@antv/graphlib"; + +const data = { + nodes: [ + { + id: 'A', + data: {} + }, + { + id: 'B', + data: {} + }, + { + id: 'C', + data: {} + }, + { + id: 'D', + data: {} + }, + { + id: 'E', + data: {} + }, + { + id: 'F', + data: {} + }, + { + id: 'G', + data: {} + }, + ], + edges: [ + { + id: 'e1', + source: 'A', + target: 'B', + data: {} + }, + { + id: 'e2', + source: 'B', + target: 'C', + data: {} + }, + { + id: 'e3', + source: 'C', + target: 'G', + data: {} + }, + { + id: 'e4', + source: 'A', + target: 'D', + data: {} + }, + { + id: 'e5', + source: 'A', + target: 'E', + data: {} + }, + { + id: 'e6', + source: 'E', + target: 'F', + data: {} + }, + { + id: 'e7', + source: 'F', + target: 'D', + data: {} + }, + { + id: 'e8', + source: 'D', + target: 'E', + data: {} + }, + ], +}; +const graph = new Graph(data); + +describe('breadthFirstSearch', () => { + it('should perform BFS operation on graph', () => { + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse graphs without callbacks first. + breadthFirstSearch(graph, 'A'); + + // Traverse graph with enterNode and leaveNode callbacks. + breadthFirstSearch(graph, 'A', { + enter: enterNodeCallback, + leave: leaveNodeCallback, + }); + expect(enterNodeCallback).toHaveBeenCalledTimes(graph.getAllNodes().length); + expect(leaveNodeCallback).toHaveBeenCalledTimes(graph.getAllNodes().length); + + const nodeA = 'A'; + const nodeB = 'B'; + const nodeC = 'C'; + const nodeD = 'D'; + const nodeE = 'E'; + const nodeF = 'F'; + const nodeG = 'G'; + + const enterNodeParamsMap = [ + { currentNode: nodeA, previousNode: '' }, + { currentNode: nodeB, previousNode: nodeA }, + { currentNode: nodeD, previousNode: nodeB }, + { currentNode: nodeE, previousNode: nodeD }, + { currentNode: nodeC, previousNode: nodeE }, + { currentNode: nodeF, previousNode: nodeC }, + { currentNode: nodeG, previousNode: nodeF }, + ]; + + for (let callIndex = 0; callIndex < 6; callIndex += 1) { + const params = enterNodeCallback.mock.calls[callIndex][0]; + expect(params.current).toEqual(enterNodeParamsMap[callIndex].currentNode); + expect(params.previous).toEqual( + enterNodeParamsMap[callIndex].previousNode && + enterNodeParamsMap[callIndex].previousNode, + ); + } + + const leaveNodeParamsMap = [ + { currentNode: nodeA, previousNode: '' }, + { currentNode: nodeB, previousNode: nodeA }, + { currentNode: nodeD, previousNode: nodeB }, + { currentNode: nodeE, previousNode: nodeD }, + { currentNode: nodeC, previousNode: nodeE }, + { currentNode: nodeF, previousNode: nodeC }, + { currentNode: nodeG, previousNode: nodeF }, + ]; + + for (let callIndex = 0; callIndex < 6; callIndex += 1) { + const params = leaveNodeCallback.mock.calls[callIndex][0]; + expect(params.current).toEqual(leaveNodeParamsMap[callIndex].currentNode); + expect(params.previous).toEqual( + leaveNodeParamsMap[callIndex].previousNode && + leaveNodeParamsMap[callIndex].previousNode, + ); + } + }); + + it('should allow to create custom node visiting logic', () => { + + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse graph with enterNode and leaveNode callbacks. + breadthFirstSearch(graph, 'A', { + enter: enterNodeCallback, + leave: leaveNodeCallback, + allowTraversal: ({ current, next }) => { + return !(current === 'A' && next === 'B'); + }, + }); + + expect(enterNodeCallback).toHaveBeenCalledTimes(4); + expect(leaveNodeCallback).toHaveBeenCalledTimes(4); + + const enterNodeParamsMap = [ + { currentNode: 'A', previousNode: '' }, + { currentNode: 'D', previousNode: 'A' }, + { currentNode: 'E', previousNode: 'D' }, + { currentNode: 'F', previousNode: 'E' }, + { currentNode: 'D', previousNode: 'F' }, + ]; + + for (let callIndex = 0; callIndex < 4; callIndex += 1) { + const params = enterNodeCallback.mock.calls[callIndex][0]; + expect(params.current).toEqual(enterNodeParamsMap[callIndex].currentNode); + expect(params.previous).toEqual( + enterNodeParamsMap[callIndex].previousNode, + ); + } + + const leaveNodeParamsMap = [ + { currentNode: 'A', previousNode: '' }, + { currentNode: 'D', previousNode: 'A' }, + { currentNode: 'E', previousNode: 'D' }, + { currentNode: 'F', previousNode: 'E' }, + { currentNode: 'D', previousNode: 'F' }, + ]; + + for (let callIndex = 0; callIndex < 4; callIndex += 1) { + const params = leaveNodeCallback.mock.calls[callIndex][0]; + expect(params.current).toEqual(leaveNodeParamsMap[callIndex].currentNode); + expect(params.previous).toEqual( + leaveNodeParamsMap[callIndex].previousNode, + ); + } + }); +}); diff --git a/__tests__/unit/dfs.spec.ts b/__tests__/unit/dfs.spec.ts new file mode 100644 index 0000000..aca783c --- /dev/null +++ b/__tests__/unit/dfs.spec.ts @@ -0,0 +1,183 @@ +import depthFirstSearch from "../../packages/graph/src/dfs"; +import { Graph } from "@antv/graphlib"; + + +const data = { + nodes: [ + { + id: 'A', + data: {} + }, + { + id: 'B', + data: {} + }, + { + id: 'C', + data: {} + }, + { + id: 'D', + data: {} + }, + { + id: 'E', + data: {} + }, + { + id: 'F', + data: {} + }, + { + id: 'G', + data: {} + }, + ], + edges: [ + { + id: 'e1', + source: 'A', + target: 'B', + data: {} + }, + { + id: 'e2', + source: 'B', + target: 'C', + data: {} + }, + { + id: 'e3', + source: 'C', + target: 'G', + data: {} + }, + { + id: 'e4', + source: 'A', + target: 'D', + data: {} + }, + { + id: 'e5', + source: 'A', + target: 'E', + data: {} + }, + { + id: 'e6', + source: 'E', + target: 'F', + data: {} + }, + { + id: 'e7', + source: 'F', + target: 'D', + data: {} + }, + { + id: 'e8', + source: 'D', + target: 'E', + data: {} + }, + ], +}; +const graph = new Graph(data); +describe('depthFirstSearch', () => { + it('should perform DFS operation on graph', () => { + + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse graphs without callbacks first to check default ones. + depthFirstSearch(graph, 'A'); + + // Traverse graph with enterNode and leaveNode callbacks. + depthFirstSearch(graph, 'A', { + enter: enterNodeCallback, + leave: leaveNodeCallback, + }); + expect(enterNodeCallback).toHaveBeenCalledTimes(graph.getAllNodes().length); + expect(leaveNodeCallback).toHaveBeenCalledTimes(graph.getAllNodes().length); + + const enterNodeParamsMap = [ + { currentNode: 'A', previousNode: '' }, + { currentNode: 'B', previousNode: 'A' }, + { currentNode: 'C', previousNode: 'B' }, + { currentNode: 'G', previousNode: 'C' }, + { currentNode: 'D', previousNode: 'A' }, + { currentNode: 'F', previousNode: 'D' }, + { currentNode: 'E', previousNode: 'F' }, + ]; + for (let callIndex = 0; callIndex < data.nodes.length; callIndex += 1) { + const params = enterNodeCallback.mock.calls[callIndex][0]; + expect(params.previous).toEqual( + enterNodeParamsMap[callIndex].previousNode, + ); + } + + depthFirstSearch(graph, 'B', { + enter: enterNodeCallback, + leave: leaveNodeCallback, + }); + const leaveNodeParamsMap = [ + { currentNode: 'G', previousNode: 'C' }, + { currentNode: 'C', previousNode: 'B' }, + { currentNode: 'B', previousNode: 'A' }, + { currentNode: 'E', previousNode: 'F' }, + { currentNode: 'F', previousNode: 'D' }, + { currentNode: 'D', previousNode: 'A' }, + { currentNode: 'A', previousNode: '' }, + ]; + + for (let callIndex = 0; callIndex < data.nodes.length; callIndex += 1) { + const params = leaveNodeCallback.mock.calls[callIndex][0]; + expect(params.previous).toEqual( + leaveNodeParamsMap[callIndex].previousNode, + ); + } + }); + + it('allow users to redefine node visiting logic', () => { + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + depthFirstSearch(graph, 'A', { + enter: enterNodeCallback, + leave: leaveNodeCallback, + allowTraversal: ({ current: currentNode, next: nextNode }) => { + return !(currentNode === 'A' && nextNode === 'B'); + }, + }); + expect(enterNodeCallback).toHaveBeenCalledTimes(4); + expect(leaveNodeCallback).toHaveBeenCalledTimes(4); + + const enterNodeParamsMap = [ + { currentNode: 'A', previousNode: '' }, + { currentNode: 'D', previousNode: 'A' }, + { currentNode: 'F', previousNode: 'D' }, + { currentNode: 'E', previousNode: 'F' }, + ]; + + for (let callIndex = 0; callIndex < 4; callIndex += 1) { + const params = enterNodeCallback.mock.calls[callIndex][0]; + expect(params.previous && params.previous).toEqual( + enterNodeParamsMap[callIndex].previousNode, + ); + } + const leaveNodeParamsMap = [ + { currentNode: 'E', previousNode: 'F' }, + { currentNode: 'F', previousNode: 'D' }, + { currentNode: 'D', previousNode: 'A' }, + { currentNode: 'A', previousNode: '' }, + ]; + for (let callIndex = 0; callIndex < 4; callIndex += 1) { + const params = leaveNodeCallback.mock.calls[callIndex][0]; + expect(params.current).toEqual(leaveNodeParamsMap[callIndex].currentNode); + expect(params.previous).toEqual( + leaveNodeParamsMap[callIndex].previousNode, + ); + } + }); +}); diff --git a/package.json b/package.json index c6cac2c..42043cd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build:ci": "pnpm -r run build:ci", "prepare": "husky install", "test": "jest", - "test_one": "jest ./__tests__/unit/kCore.spec.ts", + "test_one": "jest ./__tests__/unit/dfs.spec.ts", "coverage": "jest --coverage", "build:site": "vite build", "deploy": "gh-pages -d site/dist", diff --git a/packages/graph/src/bfs.ts b/packages/graph/src/bfs.ts new file mode 100644 index 0000000..c8f73f6 --- /dev/null +++ b/packages/graph/src/bfs.ts @@ -0,0 +1,69 @@ +import Queue from './structs/queue'; +import { Graph, IAlgorithmCallbacks, NodeID } from './types'; + +/** +* @param startNodeId The ID of the bfs traverse starting node. +* @param callbacks Optional object containing callback functions. + - allowTraversal: Determines if BFS should traverse from the vertex along its edges to its neighbors. By default, a node can only be traversed once. + - enterNode: Called when BFS visits a node. + - leaveNode: Called after BFS visits the node. +*/ +function initCallbacks(callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) { + const initiatedCallback = callbacks; + const stubCallback = () => { }; + const allowTraversalCallback = () => true; + initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback; + initiatedCallback.enter = callbacks.enter || stubCallback; + initiatedCallback.leave = callbacks.leave || stubCallback; + return initiatedCallback; +} + +/** +Performs breadth-first search (BFS) traversal on a graph. +@param graph - The graph to perform BFS on. +@param startNodeId - The ID of the starting node for BFS. +@param originalCallbacks - Optional object containing callback functions for BFS. +*/ +const breadthFirstSearch = ( + graph: Graph, + startNodeId: NodeID, + originalCallbacks?: IAlgorithmCallbacks, +) => { + const visit = new Set(); + const callbacks = initCallbacks(originalCallbacks); + const nodeQueue = new Queue(); + // init Queue. Enqueue node ID. + nodeQueue.enqueue(startNodeId); + visit.add(startNodeId); + let previousNodeId: NodeID = ''; + // 遍历队列中的所有顶点 + while (!nodeQueue.isEmpty()) { + const currentNodeId: NodeID = nodeQueue.dequeue(); + callbacks.enter({ + current: currentNodeId, + previous: previousNodeId, + }); + + // Enqueue all neighbors of currentNode + graph.getNeighbors(currentNodeId).forEach((nextNode) => { + const nextNodeId = nextNode.id; + if ( + callbacks.allowTraversal({ + previous: previousNodeId, + current: currentNodeId, + next: nextNodeId, + }) && !visit.has(nextNodeId) + ) { + visit.add(nextNodeId); + nodeQueue.enqueue(nextNodeId); + } + }); + callbacks.leave({ + current: currentNodeId, + previous: previousNodeId, + }); + previousNodeId = currentNodeId; + } +}; + +export default breadthFirstSearch; diff --git a/packages/graph/src/dfs.ts b/packages/graph/src/dfs.ts new file mode 100644 index 0000000..83a131f --- /dev/null +++ b/packages/graph/src/dfs.ts @@ -0,0 +1,52 @@ +import { Graph, IAlgorithmCallbacks, NodeID } from './types'; + +function initCallbacks(callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) { + const initiatedCallback = callbacks; + const stubCallback = () => { }; + const allowTraversalCallback = () => true; + initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback; + initiatedCallback.enter = callbacks.enter || stubCallback; + initiatedCallback.leave = callbacks.leave || stubCallback; + + return initiatedCallback; +} + +function depthFirstSearchRecursive( + graph: Graph, + currentNodeId: NodeID, + previousNodeId: NodeID, + callbacks: IAlgorithmCallbacks, + visit: Set +) { + callbacks.enter({ + current: currentNodeId, + previous: previousNodeId, + }); + graph.getNeighbors(currentNodeId).forEach((nextNode) => { + const nextNodeId = nextNode.id; + if ( + callbacks.allowTraversal({ + previous: previousNodeId, + current: currentNodeId, + next: nextNodeId, + }) && !visit.has(nextNodeId) + ) { + visit.add(nextNodeId); + depthFirstSearchRecursive(graph, nextNodeId, currentNodeId, callbacks, visit); + } + }); + callbacks.leave({ + current: currentNodeId, + previous: previousNodeId, + }); +} + +export default function depthFirstSearch( + graph: Graph, + startNodeId: NodeID, + originalCallbacks?: IAlgorithmCallbacks, +) { + const visit = new Set(); + visit.add(startNodeId); + depthFirstSearchRecursive(graph, startNodeId, '', initCallbacks(originalCallbacks), visit); +} diff --git a/packages/graph/src/index.ts b/packages/graph/src/index.ts index 6a22580..772f5dc 100644 --- a/packages/graph/src/index.ts +++ b/packages/graph/src/index.ts @@ -4,3 +4,5 @@ export * from "./louvain"; export * from "./iLouvain"; export * from "./k-core"; export * from "./floydWarshall"; +export * from "./bfs"; +export * from "./dfs"; \ No newline at end of file diff --git a/packages/graph/src/k-core.ts b/packages/graph/src/k-core.ts index b2a2dd6..9d8f8de 100644 --- a/packages/graph/src/k-core.ts +++ b/packages/graph/src/k-core.ts @@ -12,12 +12,12 @@ export function kCore( const nodes = graph.getAllNodes(); let edges = graph.getAllEdges(); nodes.sort((a, b) => graph.getDegree(a.id, 'both') - graph.getDegree(b.id, 'both')); - let i = 0; + const i = 0; while (true) { const curNode = nodes[i]; if (graph.getDegree(curNode.id, 'both') >= k) break; - nodes.splice(i, 1);//remove node - edges = edges.filter(e => !(e.source === i || e.target === i)); + nodes.splice(i, 1);// remove node + edges = edges.filter((e) => !(e.source === i || e.target === i)); } return { nodes, edges }; } \ No newline at end of file diff --git a/packages/graph/src/structs/linked-list.ts b/packages/graph/src/structs/linked-list.ts new file mode 100644 index 0000000..89abeee --- /dev/null +++ b/packages/graph/src/structs/linked-list.ts @@ -0,0 +1,242 @@ + +/** + * ListNode in LinkedList + */ +export class LinkedListNode { + public value: T; + + public next: LinkedListNode; + + constructor(value: T, next: LinkedListNode = null) { + this.value = value; + this.next = next; + } + + toString(callback?: Function) { + return callback ? callback(this.value) : `${this.value}`; + } +} + +export default class LinkedList { + public head: LinkedListNode; + + public tail: LinkedListNode; + + public compare: Function; + defaultComparator = (a: T, b: T) => { + if (a === b) { + return true; + } + return false; + }; + + constructor(comparator?: Function) { + this.head = null; + this.tail = null; + this.compare = comparator || this.defaultComparator; + } + + /** + * Adds the specified element to the header of the linked list + * @param value The element + */ + prepend(value: T) { + // 在头部添加一个节点 + const newNode = new LinkedListNode(value, this.head); + this.head = newNode; + + if (!this.tail) { + this.tail = newNode; + } + + return this; + } + + /** + * Adds the specified element to the linked list + * @param value The element + */ + append(value: T) { + const newNode = new LinkedListNode(value); + + // 如果不存在头节点,则将创建的新节点作为头节点 + if (!this.head) { + this.head = newNode; + this.tail = newNode; + + return this; + } + + // 将新节点附加到链表末尾 + this.tail.next = newNode; + this.tail = newNode; + + return this; + } + + /** + * Delete the specified element + * @param value The element + */ + delete(value: T): LinkedListNode { + if (!this.head) { + return null; + } + + let deleteNode = null; + + // 如果删除的是头部元素,则将next作为头元素 + while (this.head && this.compare(this.head.value, value)) { + deleteNode = this.head; + this.head = this.head.next; + } + + let currentNode = this.head; + + if (currentNode !== null) { + // 如果删除了节点以后,将next节点前移 + while (currentNode.next) { + if (this.compare(currentNode.next.value, value)) { + deleteNode = currentNode.next; + currentNode.next = currentNode.next.next; + } else { + currentNode = currentNode.next; + } + } + } + + // 检查尾部节点是否被删除 + if (this.compare(this.tail.value, value)) { + this.tail = currentNode; + } + + return deleteNode; + } + + /** + * Finds the first occurrence of a node in the linked list that matches the specified value or satisfies the callback function. + @param value - The value to search for in the linked list. + @param callback - An optional callback function to determine if a node matches the search criteria. + Copy.The callback should accept a value from a node as its argument and return a boolean indicating a match. + @returns The first LinkedListNode that matches the search criteria, or null if no match is found. + */ + find({ value = undefined, callback = undefined }: { value: T, callback: Function }): LinkedListNode { + if (!this.head) { + return null; + } + let currentNode = this.head; + while (currentNode) { + //find by callback first + if (callback && callback(currentNode.value)) { + return currentNode; + } + if (value !== undefined && this.compare(currentNode.value, value)) { + return currentNode; + } + currentNode = currentNode.next; + } + + return null; + } + + /** + * Delete tail node + */ + deleteTail() { + const deletedTail = this.tail; + + if (this.head === this.tail) { + // 链表中只有一个元素 + this.head = null; + this.tail = null; + return deletedTail; + } + + let currentNode = this.head; + while (currentNode.next) { + if (!currentNode.next.next) { + currentNode.next = null; + } else { + currentNode = currentNode.next; + } + } + + this.tail = currentNode; + + return deletedTail; + } + + /** + * Delete head node + */ + deleteHead() { + if (!this.head) { + return null; + } + + const deletedHead = this.head; + + if (this.head.next) { + this.head = this.head.next; + } else { + this.head = null; + this.tail = null; + } + + return deletedHead; + } + + /** + * Convert a set of elements to nodes in a linked list + * @param values element in linkedlist + */ + fromArray(values: T[]) { + values.forEach((value) => this.append(value)); + return this; + } + + /** + * Convert nodes in a linked list into array elements + */ + toArray() { + const nodes = []; + + let currentNode = this.head; + + while (currentNode) { + nodes.push(currentNode); + currentNode = currentNode.next; + } + + return nodes; + } + + /** + * Invert element nodes in a linked list + */ + reverse() { + let currentNode = this.head; + let prevNode = null; + let nextNode = null; + while (currentNode) { + // 存储下一个元素节点 + nextNode = currentNode.next; + + // 更改当前节点的下一个节点,以便将它连接到上一个节点上 + currentNode.next = prevNode; + + // 将 prevNode 和 currentNode 向前移动一步 + prevNode = currentNode; + currentNode = nextNode; + } + + this.tail = this.head; + this.head = prevNode; + } + + toString(callback: Function = undefined) { + return this.toArray() + .map((node) => node.toString(callback)) + .toString(); + } +} diff --git a/packages/graph/src/structs/queue.ts b/packages/graph/src/structs/queue.ts new file mode 100644 index 0000000..3c5b8c5 --- /dev/null +++ b/packages/graph/src/structs/queue.ts @@ -0,0 +1,43 @@ +import LinkedList from './linked-list'; + +export default class Queue { + public linkedList: LinkedList; + + constructor() { + this.linkedList = new LinkedList(); + } + + public isEmpty() { + return !this.linkedList.head; + } + + /** + * get the first element without dequeue + */ + public peek() { + if (!this.linkedList.head) { + return null; + } + return this.linkedList.head.value; + } + + /** + * enqueue an element at the tail + * @param value + */ + public enqueue(value: T) { + this.linkedList.append(value); + } + + /** + * Dequeue the first element. If the queue is empty, return null. + */ + public dequeue() { + const removeHead = this.linkedList.deleteHead(); + return removeHead ? removeHead.value : null; + } + + public toString(callback?: any) { + return this.linkedList.toString(callback); + } +} diff --git a/packages/graph/src/structs/stack.ts b/packages/graph/src/structs/stack.ts new file mode 100644 index 0000000..37336d1 --- /dev/null +++ b/packages/graph/src/structs/stack.ts @@ -0,0 +1,62 @@ +import LinkedList from './linked-list'; +export default class Stack { + + private linkedList: LinkedList; + + private maxStep: number; + + constructor(maxStep: number = 10) { + this.linkedList = new LinkedList(); + this.maxStep = maxStep; + } + + get length() { + return this.linkedList.toArray().length; + } + + /** + * Determine whether the stack is empty, if there is no header element in the linked list, the stack is empty + */ + isEmpty() { + return !this.linkedList.head; + } + + /** + * Whether to the maximum length of the defined stack, if the maximum length is reached, the stack is no longer allowed to enter the stack + */ + isMaxStack() { + return this.toArray().length >= this.maxStep; + } + + /** + * Access the top element + */ + peek() { + if (this.isEmpty()) { + return null; + } + return this.linkedList.head.value; + } + + push(value: T) { + this.linkedList.prepend(value); + if (this.length > this.maxStep) { + this.linkedList.deleteTail(); + } + } + + pop() { + const removeHead = this.linkedList.deleteHead(); + return removeHead ? removeHead.value : null; + } + + toArray() { + return this.linkedList.toArray().map((node) => node.value); + } + + clear() { + while (!this.isEmpty()) { + this.pop(); + } + } +} diff --git a/packages/graph/src/types.ts b/packages/graph/src/types.ts index 8fe9dce..fb9038f 100644 --- a/packages/graph/src/types.ts +++ b/packages/graph/src/types.ts @@ -2,7 +2,7 @@ import { Edge, Graph as IGraph, Node, PlainObject } from "@antv/graphlib"; // 数据集中属性/特征值分布的map export interface KeyValueMap { - [key:string]: any[]; + [key: string]: any[]; } export interface NodeData extends PlainObject { @@ -30,4 +30,11 @@ export interface ClusterMap { export type Graph = IGraph; -export type Matrix = number[]; \ No newline at end of file +export type Matrix = number[]; +export interface IAlgorithmCallbacks { + enter?: (param: { current: NodeID; previous: NodeID }) => void; + leave?: (param: { current: NodeID; previous?: NodeID }) => void; + allowTraversal?: (param: { previous?: NodeID; current?: NodeID; next: NodeID }) => boolean; +} + +export type NodeID = string | number; \ No newline at end of file