From b392188bddfe5cbfc42e30dc360974343b57486d Mon Sep 17 00:00:00 2001 From: zqqcee Date: Fri, 8 Sep 2023 16:28:17 +0800 Subject: [PATCH 1/7] feat: v5 algorithm bfs --- packages/graph/src/bfs.ts | 69 ++++++ packages/graph/src/index.ts | 2 + packages/graph/src/structs/binary-heap.ts | 90 ++++++++ packages/graph/src/structs/linked-list.ts | 245 ++++++++++++++++++++++ packages/graph/src/structs/queue.ts | 46 ++++ packages/graph/src/structs/stack.ts | 65 ++++++ packages/graph/src/structs/union-find.ts | 45 ++++ packages/graph/src/types.ts | 11 +- 8 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 packages/graph/src/bfs.ts create mode 100644 packages/graph/src/structs/binary-heap.ts create mode 100644 packages/graph/src/structs/linked-list.ts create mode 100644 packages/graph/src/structs/queue.ts create mode 100644 packages/graph/src/structs/stack.ts create mode 100644 packages/graph/src/structs/union-find.ts diff --git a/packages/graph/src/bfs.ts b/packages/graph/src/bfs.ts new file mode 100644 index 0000000..6269620 --- /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(startNodeId: NodeID, 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(startNodeId, 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/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/structs/binary-heap.ts b/packages/graph/src/structs/binary-heap.ts new file mode 100644 index 0000000..bf3eb70 --- /dev/null +++ b/packages/graph/src/structs/binary-heap.ts @@ -0,0 +1,90 @@ +const defaultCompare = (a, b) => { + return a - b; +}; + +export default class MinBinaryHeap { + list: any[]; + + compareFn: (a: any, b: any) => number; + + constructor(compareFn = defaultCompare) { + this.compareFn = compareFn; + this.list = []; + } + + getLeft(index) { + return 2 * index + 1; + } + + getRight(index) { + return 2 * index + 2; + } + + getParent(index) { + if (index === 0) { + return null; + } + return Math.floor((index - 1) / 2); + } + + isEmpty() { + return this.list.length <= 0; + } + + top() { + return this.isEmpty() ? undefined : this.list[0]; + } + + delMin() { + const top = this.top(); + const bottom = this.list.pop(); + if (this.list.length > 0) { + this.list[0] = bottom; + this.moveDown(0); + } + return top; + } + + insert(value) { + if (value !== null) { + this.list.push(value); + const index = this.list.length - 1; + this.moveUp(index); + return true; + } + return false; + } + + moveUp(index) { + let parent = this.getParent(index); + while (index && index > 0 && this.compareFn(this.list[parent], this.list[index]) > 0) { + // swap + const tmp = this.list[parent]; + this.list[parent] = this.list[index]; + this.list[index] = tmp; + // [this.list[index], this.list[parent]] = [this.list[parent], this.list[index]] + index = parent; + parent = this.getParent(index); + } + } + + moveDown(index) { + let element = index; + const left = this.getLeft(index); + const right = this.getRight(index); + const size = this.list.length; + if (left !== null && left < size && this.compareFn(this.list[element], this.list[left]) > 0) { + element = left; + } else if ( + right !== null && + right < size && + this.compareFn(this.list[element], this.list[right]) > 0 + ) { + element = right; + } + if (index !== element) { + [this.list[index], this.list[element]] = [this.list[element], this.list[index]]; + this.moveDown(element); + } + } +} diff --git a/packages/graph/src/structs/linked-list.ts b/packages/graph/src/structs/linked-list.ts new file mode 100644 index 0000000..b4ebee6 --- /dev/null +++ b/packages/graph/src/structs/linked-list.ts @@ -0,0 +1,245 @@ +const defaultComparator = (a, b) => { + if (a === b) { + return true; + } + + return false; +} + +/** + * 链表中单个元素节点 + */ +export class LinkedListNode { + public value; + + public next: LinkedListNode; + + constructor(value, next: LinkedListNode = null) { + this.value = value; + this.next = next; + } + + toString(callback?: any) { + return callback ? callback(this.value) : `${this.value}`; + } +} + +export default class LinkedList { + public head: LinkedListNode; + + public tail: LinkedListNode; + + public compare: Function; + + constructor(comparator = defaultComparator) { + this.head = null; + this.tail = null; + this.compare = comparator; + } + + /** + * 将指定元素添加到链表头部 + * @param value + */ + prepend(value) { + // 在头部添加一个节点 + const newNode = new LinkedListNode(value, this.head); + this.head = newNode; + + if (!this.tail) { + this.tail = newNode; + } + + return this; + } + + /** + * 将指定元素添加到链表中 + * @param value + */ + append(value) { + 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; + } + + /** + * 删除指定元素 + * @param value 要删除的元素 + */ + delete(value): 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; + } + + /** + * 查找指定的元素 + * @param param0 + */ + find({ value = undefined, callback = undefined }): LinkedListNode { + if (!this.head) { + return null; + } + + let currentNode = this.head; + + while (currentNode) { + // 如果指定了 callback,则按指定的 callback 查找 + if (callback && callback(currentNode.value)) { + return currentNode; + } + + // 如果指定了 value,则按 value 查找 + if (value !== undefined && this.compare(currentNode.value, value)) { + return currentNode; + } + + currentNode = currentNode.next; + } + + return null; + } + + /** + * 删除尾部节点 + */ + 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; + } + + /** + * 删除头部节点 + */ + 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; + } + + /** + * 将一组元素转成链表中的节点 + * @param values 链表中的元素 + */ + fromArray(values) { + values.forEach((value) => this.append(value)); + return this; + } + + /** + * 将链表中的节点转成数组元素 + */ + toArray() { + const nodes = []; + + let currentNode = this.head; + + while (currentNode) { + nodes.push(currentNode); + currentNode = currentNode.next; + } + + return nodes; + } + + /** + * 反转链表中的元素节点 + */ + 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 = 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..f265e0c --- /dev/null +++ b/packages/graph/src/structs/queue.ts @@ -0,0 +1,46 @@ +import LinkedList from './linked-list'; + +export default class Queue { + public linkedList: LinkedList; + + constructor() { + this.linkedList = new LinkedList(); + } + + /** + * 队列是否为空 + */ + public isEmpty() { + return !this.linkedList.head; + } + + /** + * 读取队列头部的元素, 不删除队列中的元素 + */ + public peek() { + if (!this.linkedList.head) { + return null; + } + return this.linkedList.head.value; + } + + /** + * 在队列的尾部新增一个元素 + * @param value + */ + public enqueue(value) { + this.linkedList.append(value); + } + + /** + * 删除队列中的头部元素,如果队列为空,则返回 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..af60cb3 --- /dev/null +++ b/packages/graph/src/structs/stack.ts @@ -0,0 +1,65 @@ +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; + } + + /** + * 判断栈是否为空,如果链表中没有头部元素,则栈为空 + */ + isEmpty() { + return !this.linkedList.head; + } + + /** + * 是否到定义的栈的最大长度,如果达到最大长度后,不再允许入栈 + */ + isMaxStack() { + return this.toArray().length >= this.maxStep; + } + + /** + * 访问顶端元素 + */ + peek() { + if (this.isEmpty()) { + return null; + } + + // 返回头部元素,不删除元素 + return this.linkedList.head.value; + } + + push(value) { + 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/structs/union-find.ts b/packages/graph/src/structs/union-find.ts new file mode 100644 index 0000000..940ed6d --- /dev/null +++ b/packages/graph/src/structs/union-find.ts @@ -0,0 +1,45 @@ +/** + * 并查集 Disjoint set to support quick union + */ +export default class UnionFind { + count: number; + + parent: {}; + + constructor(items: (number | string)[]) { + this.count = items.length; + this.parent = {}; + for (const i of items) { + this.parent[i] = i; + } + } + + // find the root of the item + find(item) { + while (this.parent[item] !== item) { + item = this.parent[item]; + } + return item; + } + + union(a, b) { + const rootA = this.find(a); + const rootB = this.find(b); + + if (rootA === rootB) return; + + // make the element with smaller root the parent + if (rootA < rootB) { + if (this.parent[b] !== b) this.union(this.parent[b], a); + this.parent[b] = this.parent[a]; + } else { + if (this.parent[a] !== a) this.union(this.parent[a], b); + this.parent[a] = this.parent[b]; + } + } + + // whether a and b are connected, i.e. a and b have the same root + connected(a, b) { + return this.find(a) === this.find(b); + } +} 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 From 9b7caf49c826552e38300e725d27993a94117f43 Mon Sep 17 00:00:00 2001 From: zqqcee Date: Fri, 8 Sep 2023 16:28:54 +0800 Subject: [PATCH 2/7] test: bfs unit test --- __tests__/unit/bfs.spec.ts | 200 +++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 __tests__/unit/bfs.spec.ts 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, + ); + } + }); +}); From ebb967c3242f0dfafe76424c53c3ba59776c4e8a Mon Sep 17 00:00:00 2001 From: zqqcee Date: Fri, 8 Sep 2023 16:29:56 +0800 Subject: [PATCH 3/7] fix: fix initCallbacks --- packages/graph/src/bfs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graph/src/bfs.ts b/packages/graph/src/bfs.ts index 6269620..64d971e 100644 --- a/packages/graph/src/bfs.ts +++ b/packages/graph/src/bfs.ts @@ -8,7 +8,7 @@ import { Graph, IAlgorithmCallbacks, NodeID } from './types'; - enterNode: Called when BFS visits a node. - leaveNode: Called after BFS visits the node. */ -function initCallbacks(startNodeId: NodeID, callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) { +function initCallbacks(callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) { const initiatedCallback = callbacks; const stubCallback = () => { }; const allowTraversalCallback = () => true; @@ -30,7 +30,7 @@ const breadthFirstSearch = ( originalCallbacks?: IAlgorithmCallbacks, ) => { const visit = new Set(); - const callbacks = initCallbacks(startNodeId, originalCallbacks); + const callbacks = initCallbacks(originalCallbacks); const nodeQueue = new Queue(); // init Queue. Enqueue node ID. nodeQueue.enqueue(startNodeId); From 759aad38fd47e80fb2b22c15716581a493974f6d Mon Sep 17 00:00:00 2001 From: zqqcee Date: Fri, 8 Sep 2023 17:24:31 +0800 Subject: [PATCH 4/7] feat: v5 algorithm dfs --- packages/graph/src/dfs.ts | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/graph/src/dfs.ts 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); +} From d33fb12ac408c6d12ab8457059a0ec0e3c441335 Mon Sep 17 00:00:00 2001 From: zqqcee Date: Fri, 8 Sep 2023 17:24:47 +0800 Subject: [PATCH 5/7] test: dfs unit test --- __tests__/unit/dfs.spec.ts | 183 +++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/dfs.spec.ts 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", From 64bf4ae1fff438ccef2951b5a2f74895295b0c9c Mon Sep 17 00:00:00 2001 From: zqqcee Date: Sat, 9 Sep 2023 15:18:27 +0800 Subject: [PATCH 6/7] fix: fix lint --- packages/graph/src/bfs.ts | 2 +- packages/graph/src/k-core.ts | 6 +- packages/graph/src/structs/binary-heap.ts | 90 ----------------------- packages/graph/src/structs/linked-list.ts | 2 +- packages/graph/src/structs/union-find.ts | 45 ------------ 5 files changed, 5 insertions(+), 140 deletions(-) delete mode 100644 packages/graph/src/structs/binary-heap.ts delete mode 100644 packages/graph/src/structs/union-find.ts diff --git a/packages/graph/src/bfs.ts b/packages/graph/src/bfs.ts index 64d971e..7eb62a7 100644 --- a/packages/graph/src/bfs.ts +++ b/packages/graph/src/bfs.ts @@ -1,4 +1,4 @@ -import Queue from './structs/queue' +import Queue from './structs/queue'; import { Graph, IAlgorithmCallbacks, NodeID } from './types'; /** 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/binary-heap.ts b/packages/graph/src/structs/binary-heap.ts deleted file mode 100644 index bf3eb70..0000000 --- a/packages/graph/src/structs/binary-heap.ts +++ /dev/null @@ -1,90 +0,0 @@ -const defaultCompare = (a, b) => { - return a - b; -}; - -export default class MinBinaryHeap { - list: any[]; - - compareFn: (a: any, b: any) => number; - - constructor(compareFn = defaultCompare) { - this.compareFn = compareFn; - this.list = []; - } - - getLeft(index) { - return 2 * index + 1; - } - - getRight(index) { - return 2 * index + 2; - } - - getParent(index) { - if (index === 0) { - return null; - } - return Math.floor((index - 1) / 2); - } - - isEmpty() { - return this.list.length <= 0; - } - - top() { - return this.isEmpty() ? undefined : this.list[0]; - } - - delMin() { - const top = this.top(); - const bottom = this.list.pop(); - if (this.list.length > 0) { - this.list[0] = bottom; - this.moveDown(0); - } - return top; - } - - insert(value) { - if (value !== null) { - this.list.push(value); - const index = this.list.length - 1; - this.moveUp(index); - return true; - } - return false; - } - - moveUp(index) { - let parent = this.getParent(index); - while (index && index > 0 && this.compareFn(this.list[parent], this.list[index]) > 0) { - // swap - const tmp = this.list[parent]; - this.list[parent] = this.list[index]; - this.list[index] = tmp; - // [this.list[index], this.list[parent]] = [this.list[parent], this.list[index]] - index = parent; - parent = this.getParent(index); - } - } - - moveDown(index) { - let element = index; - const left = this.getLeft(index); - const right = this.getRight(index); - const size = this.list.length; - if (left !== null && left < size && this.compareFn(this.list[element], this.list[left]) > 0) { - element = left; - } else if ( - right !== null && - right < size && - this.compareFn(this.list[element], this.list[right]) > 0 - ) { - element = right; - } - if (index !== element) { - [this.list[index], this.list[element]] = [this.list[element], this.list[index]]; - this.moveDown(element); - } - } -} diff --git a/packages/graph/src/structs/linked-list.ts b/packages/graph/src/structs/linked-list.ts index b4ebee6..9f7c7f8 100644 --- a/packages/graph/src/structs/linked-list.ts +++ b/packages/graph/src/structs/linked-list.ts @@ -4,7 +4,7 @@ const defaultComparator = (a, b) => { } return false; -} +}; /** * 链表中单个元素节点 diff --git a/packages/graph/src/structs/union-find.ts b/packages/graph/src/structs/union-find.ts deleted file mode 100644 index 940ed6d..0000000 --- a/packages/graph/src/structs/union-find.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 并查集 Disjoint set to support quick union - */ -export default class UnionFind { - count: number; - - parent: {}; - - constructor(items: (number | string)[]) { - this.count = items.length; - this.parent = {}; - for (const i of items) { - this.parent[i] = i; - } - } - - // find the root of the item - find(item) { - while (this.parent[item] !== item) { - item = this.parent[item]; - } - return item; - } - - union(a, b) { - const rootA = this.find(a); - const rootB = this.find(b); - - if (rootA === rootB) return; - - // make the element with smaller root the parent - if (rootA < rootB) { - if (this.parent[b] !== b) this.union(this.parent[b], a); - this.parent[b] = this.parent[a]; - } else { - if (this.parent[a] !== a) this.union(this.parent[a], b); - this.parent[a] = this.parent[b]; - } - } - - // whether a and b are connected, i.e. a and b have the same root - connected(a, b) { - return this.find(a) === this.find(b); - } -} From d0947e12cdc96d8a48dd5240828ce03d3746fc72 Mon Sep 17 00:00:00 2001 From: zqqcee Date: Sat, 9 Sep 2023 23:10:44 +0800 Subject: [PATCH 7/7] feat: add type checking for structs --- packages/graph/src/bfs.ts | 2 +- packages/graph/src/structs/linked-list.ts | 87 +++++++++++------------ packages/graph/src/structs/queue.ts | 17 ++--- packages/graph/src/structs/stack.ts | 15 ++-- 4 files changed, 56 insertions(+), 65 deletions(-) diff --git a/packages/graph/src/bfs.ts b/packages/graph/src/bfs.ts index 7eb62a7..c8f73f6 100644 --- a/packages/graph/src/bfs.ts +++ b/packages/graph/src/bfs.ts @@ -31,7 +31,7 @@ const breadthFirstSearch = ( ) => { const visit = new Set(); const callbacks = initCallbacks(originalCallbacks); - const nodeQueue = new Queue(); + const nodeQueue = new Queue(); // init Queue. Enqueue node ID. nodeQueue.enqueue(startNodeId); visit.add(startNodeId); diff --git a/packages/graph/src/structs/linked-list.ts b/packages/graph/src/structs/linked-list.ts index 9f7c7f8..89abeee 100644 --- a/packages/graph/src/structs/linked-list.ts +++ b/packages/graph/src/structs/linked-list.ts @@ -1,47 +1,46 @@ -const defaultComparator = (a, b) => { - if (a === b) { - return true; - } - - return false; -}; /** - * 链表中单个元素节点 + * ListNode in LinkedList */ -export class LinkedListNode { - public value; +export class LinkedListNode { + public value: T; - public next: LinkedListNode; + public next: LinkedListNode; - constructor(value, next: LinkedListNode = null) { + constructor(value: T, next: LinkedListNode = null) { this.value = value; this.next = next; } - toString(callback?: any) { + toString(callback?: Function) { return callback ? callback(this.value) : `${this.value}`; } } -export default class LinkedList { - public head: LinkedListNode; +export default class LinkedList { + public head: LinkedListNode; - public tail: LinkedListNode; + public tail: LinkedListNode; public compare: Function; + defaultComparator = (a: T, b: T) => { + if (a === b) { + return true; + } + return false; + }; - constructor(comparator = defaultComparator) { + constructor(comparator?: Function) { this.head = null; this.tail = null; - this.compare = comparator; + this.compare = comparator || this.defaultComparator; } /** - * 将指定元素添加到链表头部 - * @param value + * Adds the specified element to the header of the linked list + * @param value The element */ - prepend(value) { + prepend(value: T) { // 在头部添加一个节点 const newNode = new LinkedListNode(value, this.head); this.head = newNode; @@ -54,10 +53,10 @@ export default class LinkedList { } /** - * 将指定元素添加到链表中 - * @param value + * Adds the specified element to the linked list + * @param value The element */ - append(value) { + append(value: T) { const newNode = new LinkedListNode(value); // 如果不存在头节点,则将创建的新节点作为头节点 @@ -76,10 +75,10 @@ export default class LinkedList { } /** - * 删除指定元素 - * @param value 要删除的元素 + * Delete the specified element + * @param value The element */ - delete(value): LinkedListNode { + delete(value: T): LinkedListNode { if (!this.head) { return null; } @@ -115,27 +114,25 @@ export default class LinkedList { } /** - * 查找指定的元素 - * @param param0 - */ - find({ value = undefined, callback = undefined }): LinkedListNode { + * 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) { - // 如果指定了 callback,则按指定的 callback 查找 + //find by callback first if (callback && callback(currentNode.value)) { return currentNode; } - - // 如果指定了 value,则按 value 查找 if (value !== undefined && this.compare(currentNode.value, value)) { return currentNode; } - currentNode = currentNode.next; } @@ -143,7 +140,7 @@ export default class LinkedList { } /** - * 删除尾部节点 + * Delete tail node */ deleteTail() { const deletedTail = this.tail; @@ -170,7 +167,7 @@ export default class LinkedList { } /** - * 删除头部节点 + * Delete head node */ deleteHead() { if (!this.head) { @@ -190,16 +187,16 @@ export default class LinkedList { } /** - * 将一组元素转成链表中的节点 - * @param values 链表中的元素 + * Convert a set of elements to nodes in a linked list + * @param values element in linkedlist */ - fromArray(values) { + fromArray(values: T[]) { values.forEach((value) => this.append(value)); return this; } /** - * 将链表中的节点转成数组元素 + * Convert nodes in a linked list into array elements */ toArray() { const nodes = []; @@ -215,7 +212,7 @@ export default class LinkedList { } /** - * 反转链表中的元素节点 + * Invert element nodes in a linked list */ reverse() { let currentNode = this.head; @@ -237,7 +234,7 @@ export default class LinkedList { this.head = prevNode; } - toString(callback = undefined) { + 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 index f265e0c..3c5b8c5 100644 --- a/packages/graph/src/structs/queue.ts +++ b/packages/graph/src/structs/queue.ts @@ -1,21 +1,18 @@ import LinkedList from './linked-list'; -export default class Queue { - public linkedList: LinkedList; +export default class Queue { + public linkedList: LinkedList; constructor() { - this.linkedList = new LinkedList(); + this.linkedList = new LinkedList(); } - /** - * 队列是否为空 - */ public isEmpty() { return !this.linkedList.head; } /** - * 读取队列头部的元素, 不删除队列中的元素 + * get the first element without dequeue */ public peek() { if (!this.linkedList.head) { @@ -25,15 +22,15 @@ export default class Queue { } /** - * 在队列的尾部新增一个元素 + * enqueue an element at the tail * @param value */ - public enqueue(value) { + public enqueue(value: T) { this.linkedList.append(value); } /** - * 删除队列中的头部元素,如果队列为空,则返回 null + * Dequeue the first element. If the queue is empty, return null. */ public dequeue() { const removeHead = this.linkedList.deleteHead(); diff --git a/packages/graph/src/structs/stack.ts b/packages/graph/src/structs/stack.ts index af60cb3..37336d1 100644 --- a/packages/graph/src/structs/stack.ts +++ b/packages/graph/src/structs/stack.ts @@ -1,8 +1,7 @@ import LinkedList from './linked-list'; +export default class Stack { -export default class Stack { - - private linkedList: LinkedList; + private linkedList: LinkedList; private maxStep: number; @@ -16,32 +15,30 @@ export default class Stack { } /** - * 判断栈是否为空,如果链表中没有头部元素,则栈为空 + * 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) { + push(value: T) { this.linkedList.prepend(value); if (this.length > this.maxStep) { this.linkedList.deleteTail();