diff --git a/.eslintrc.js b/.eslintrc.js index 3b46646..05afdb4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { "ignorePatterns": ["lib/", "dist/", "umd/", "coverage/", "docs/"], "rules": { "import/no-anonymous-default-export": "off", - "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", diff --git a/src/data-structures/heap/heap.ts b/src/data-structures/heap/heap.ts index 4205333..8134703 100644 --- a/src/data-structures/heap/heap.ts +++ b/src/data-structures/heap/heap.ts @@ -10,7 +10,7 @@ import {DFSOrderPattern} from '../../types'; export class Heap { protected nodes: E[] = []; - private readonly comparator: Comparator; + protected readonly comparator: Comparator; constructor(comparator: Comparator) { this.comparator = comparator; @@ -18,21 +18,29 @@ export class Heap { /** * Insert an element into the heap and maintain the heap properties. - * @param value - The element to be inserted. + * @param element - The element to be inserted. */ - add(value: E): Heap { - this.nodes.push(value); + add(element: E): Heap { + return this.push(element); + } + + /** + * Insert an element into the heap and maintain the heap properties. + * @param element - The element to be inserted. + */ + push(element: E): Heap { + this.nodes.push(element); this.bubbleUp(this.nodes.length - 1); return this; } /** * Remove and return the top element (smallest or largest element) from the heap. - * @returns The top element or null if the heap is empty. + * @returns The top element or undefined if the heap is empty. */ - poll(): E | null { + poll(): E | undefined { if (this.nodes.length === 0) { - return null; + return undefined; } if (this.nodes.length === 1) { return this.nodes.pop() as E; @@ -44,6 +52,14 @@ export class Heap { return topValue; } + /** + * Remove and return the top element (smallest or largest element) from the heap. + * @returns The top element or undefined if the heap is empty. + */ + pop(): E | undefined { + return this.poll(); + } + /** * Float operation to maintain heap properties after adding an element. * @param index - The index of the newly added element. @@ -97,11 +113,11 @@ export class Heap { /** * Peek at the top element of the heap without removing it. - * @returns The top element or null if the heap is empty. + * @returns The top element or undefined if the heap is empty. */ - peek(): E | null { + peek(): E | undefined { if (this.nodes.length === 0) { - return null; + return undefined; } return this.nodes[0]; } @@ -115,10 +131,10 @@ export class Heap { /** * Get the last element in the heap, which is not necessarily a leaf node. - * @returns The last element or null if the heap is empty. + * @returns The last element or undefined if the heap is empty. */ - get leaf(): E | null { - return this.nodes[this.size - 1] ?? null; + get leaf(): E | undefined { + return this.nodes[this.size - 1] ?? undefined; } /** @@ -147,11 +163,11 @@ export class Heap { /** * Use a comparison function to check whether a binary heap contains a specific element. - * @param value - the element to check. + * @param element - the element to check. * @returns Returns true if the specified element is contained; otherwise, returns false. */ - has(value: E): boolean { - return this.nodes.includes(value); + has(element: E): boolean { + return this.nodes.includes(element); } /** @@ -235,3 +251,308 @@ export class Heap { return binaryHeap; } } + +export class FibonacciHeapNode { + element: E; + degree: number; + left?: FibonacciHeapNode; + right?: FibonacciHeapNode; + child?: FibonacciHeapNode; + parent?: FibonacciHeapNode; + marked: boolean; + constructor(element: E, degree = 0) { + this.element = element; + this.degree = degree; + this.marked = false; + } +} + +export class FibonacciHeap { + root?: FibonacciHeapNode; + protected min?: FibonacciHeapNode; + size: number = 0; + protected readonly comparator: Comparator; + + constructor(comparator?: Comparator) { + this.clear(); + this.comparator = comparator || this.defaultComparator; + + if (typeof this.comparator !== 'function') { + throw new Error('FibonacciHeap constructor: given comparator should be a function.'); + } + } + + /** + * Default comparator function used by the heap. + * @param {E} a + * @param {E} b + * @protected + */ + protected defaultComparator(a: E, b: E): number { + if (a < b) return -1; + if (a > b) return 1; + return 0; + } + + /** + * Get the size (number of elements) of the heap. + * @returns {number} The size of the heap. Returns 0 if the heap is empty. Returns -1 if the heap is invalid. + */ + clear(): void { + this.root = undefined; + this.min = undefined; + this.size = 0; + } + + /** + * Create a new node. + * @param element + * @protected + */ + protected createNode(element: E): FibonacciHeapNode { + return new FibonacciHeapNode(element); + } + + /** + * Merge the given node with the root list. + * @param node - The node to be merged. + */ + protected mergeWithRoot(node: FibonacciHeapNode): void { + if (!this.root) { + this.root = node; + } else { + node.right = this.root.right; + node.left = this.root; + this.root.right!.left = node; + this.root.right = node; + } + } + + /** + * O(1) time operation. + * Insert an element into the heap and maintain the heap properties. + * @param element + * @returns {FibonacciHeap} FibonacciHeap - The heap itself. + */ + add(element: E): FibonacciHeap { + return this.push(element); + } + + /** + * O(1) time operation. + * Insert an element into the heap and maintain the heap properties. + * @param element + * @returns {FibonacciHeap} FibonacciHeap - The heap itself. + */ + push(element: E): FibonacciHeap { + const node = this.createNode(element); + node.left = node; + node.right = node; + this.mergeWithRoot(node); + + if (!this.min || this.comparator(node.element, this.min.element) <= 0) { + this.min = node; + } + + this.size++; + return this; + } + + /** + * O(1) time operation. + * Peek at the top element of the heap without removing it. + * @returns The top element or undefined if the heap is empty. + * @protected + */ + peek(): E | undefined { + return this.min ? this.min.element : undefined; + } + + /** + * O(1) time operation. + * Get the size (number of elements) of the heap. + * @param {FibonacciHeapNode} head - The head of the linked list. + * @protected + * @returns FibonacciHeapNode[] - An array containing the nodes of the linked list. + */ + consumeLinkedList(head?: FibonacciHeapNode): FibonacciHeapNode[] { + const nodes: FibonacciHeapNode[] = []; + if (!head) return nodes; + + let node: FibonacciHeapNode | undefined = head; + let flag = false; + + while (true) { + if (node === head && flag) break; + else if (node === head) flag = true; + + if (node) { + nodes.push(node); + node = node.right; + } + } + + return nodes; + } + + /** + * O(log n) time operation. + * Remove and return the top element (smallest or largest element) from the heap. + * @param node - The node to be removed. + * @protected + */ + protected removeFromRoot(node: FibonacciHeapNode): void { + if (this.root === node) this.root = node.right; + if (node.left) node.left.right = node.right; + if (node.right) node.right.left = node.left; + } + + /** + * O(log n) time operation. + * Remove and return the top element (smallest or largest element) from the heap. + * @param parent + * @param node + */ + mergeWithChild(parent: FibonacciHeapNode, node: FibonacciHeapNode): void { + if (!parent.child) { + parent.child = node; + } else { + node.right = parent.child.right; + node.left = parent.child; + parent.child.right!.left = node; + parent.child.right = node; + } + } + + /** + * O(log n) time operation. + * Remove and return the top element (smallest or largest element) from the heap. + * @param y + * @param x + * @protected + */ + protected link(y: FibonacciHeapNode, x: FibonacciHeapNode): void { + this.removeFromRoot(y); + y.left = y; + y.right = y; + this.mergeWithChild(x, y); + x.degree++; + y.parent = x; + } + + /** + * O(log n) time operation. + * Remove and return the top element (smallest or largest element) from the heap. + * @protected + */ + protected consolidate(): void { + const A: (FibonacciHeapNode | undefined)[] = new Array(this.size); + const nodes = this.consumeLinkedList(this.root); + let x: FibonacciHeapNode | undefined, y: FibonacciHeapNode | undefined, d: number, t: FibonacciHeapNode | undefined; + + for (const node of nodes) { + x = node; + d = x.degree; + + while (A[d]) { + y = A[d] as FibonacciHeapNode; + + if (this.comparator(x.element, y.element) > 0) { + t = x; + x = y; + y = t; + } + + this.link(y, x); + A[d] = undefined; + d++; + } + + A[d] = x; + } + + for (let i = 0; i < this.size; i++) { + if (A[i] && this.comparator(A[i]!.element, this.min!.element) <= 0) { + this.min = A[i]!; + } + } + } + + /** + * O(log n) time operation. + * Remove and return the top element (smallest or largest element) from the heap. + * @returns The top element or undefined if the heap is empty. + */ + poll(): E | undefined { + return this.pop(); + } + + /** + * O(log n) time operation. + * Remove and return the top element (smallest or largest element) from the heap. + * @returns The top element or undefined if the heap is empty. + */ + pop(): E | undefined { + if (this.size === 0) return undefined; + + const z = this.min!; + if (z.child) { + const nodes = this.consumeLinkedList(z.child); + for (const node of nodes) { + this.mergeWithRoot(node); + node.parent = undefined; + } + } + + this.removeFromRoot(z); + + if (z === z.right) { + this.min = undefined; + this.root = undefined; + } else { + this.min = z.right; + this.consolidate(); + } + + this.size--; + + return z.element; + } + + /** + * O(log n) time operation. + * merge two heaps. The heap that is merged will be cleared. The heap that is merged into will remain. + * @param heapToMerge + */ + merge(heapToMerge: FibonacciHeap): void { + if (heapToMerge.size === 0) { + return; // Nothing to merge + } + + // Merge the root lists of the two heaps + if (this.root && heapToMerge.root) { + const thisRoot = this.root; + const otherRoot = heapToMerge.root; + + const thisRootRight = thisRoot.right!; + const otherRootLeft = otherRoot.left!; + + thisRoot.right = otherRoot; + otherRoot.left = thisRoot; + + thisRootRight.left = otherRootLeft; + otherRootLeft.right = thisRootRight; + } + + // Update the minimum node + if (!this.min || (heapToMerge.min && this.comparator(heapToMerge.min.element, this.min.element) < 0)) { + this.min = heapToMerge.min; + } + + // Update the size + this.size += heapToMerge.size; + + // Clear the heap that was merged + heapToMerge.clear(); + } +} diff --git a/test/types/index.ts b/test/types/index.ts new file mode 100644 index 0000000..04bca77 --- /dev/null +++ b/test/types/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/test/types/utils/big-o.ts b/test/types/utils/big-o.ts new file mode 100644 index 0000000..7c015ba --- /dev/null +++ b/test/types/utils/big-o.ts @@ -0,0 +1 @@ +export type AnyFunction = (...args: any[]) => any; diff --git a/test/types/utils/index.ts b/test/types/utils/index.ts new file mode 100644 index 0000000..1a1fd5d --- /dev/null +++ b/test/types/utils/index.ts @@ -0,0 +1 @@ +export * from './big-o'; diff --git a/test/unit/data-structures/heap/heap.test.ts b/test/unit/data-structures/heap/heap.test.ts index d987c02..5c3f3c3 100644 --- a/test/unit/data-structures/heap/heap.test.ts +++ b/test/unit/data-structures/heap/heap.test.ts @@ -1,4 +1,5 @@ -import {MaxHeap, MinHeap} from '../../../../src'; +import {FibonacciHeap, MaxHeap, MinHeap} from '../../../../src'; +import {logBigOMetricsWrap} from "../../../utils"; describe('Heap Operation Test', () => { it('should numeric heap work well', function () { @@ -60,3 +61,190 @@ describe('Heap Operation Test', () => { } }); }); + +describe('FibonacciHeap', () => { + let heap: FibonacciHeap; + + beforeEach(() => { + heap = new FibonacciHeap(); + }); + + test('push & peek', () => { + heap.push(10); + heap.push(5); + expect(heap.peek()).toBe(5); + }); + + test('pop', () => { + heap.push(10); + heap.push(5); + heap.push(15); + expect(heap.pop()).toBe(5); + expect(heap.pop()).toBe(10); + expect(heap.pop()).toBe(15); + }); + + test('pop on an empty heap', () => { + expect(heap.pop()).toBeUndefined(); + }); + + test('size', () => { + expect(heap.size).toBe(0); + heap.push(10); + expect(heap.size).toBe(1); + heap.pop(); + expect(heap.size).toBe(0); + }); + + test('clear', () => { + heap.push(10); + heap.push(5); + heap.clear(); + expect(heap.size).toBe(0); + expect(heap.peek()).toBeUndefined(); + }); + + test('custom comparator', () => { + const maxHeap = new FibonacciHeap((a, b) => b - a); + maxHeap.push(10); + maxHeap.push(5); + expect(maxHeap.peek()).toBe(10); + }); +}); + +describe('FibonacciHeap', () => { + let heap: FibonacciHeap; + + beforeEach(() => { + heap = new FibonacciHeap(); + }); + + it('should initialize an empty heap', () => { + expect(heap.size).toBe(0); + expect(heap.peek()).toBeUndefined(); + }); + + it('should push items into the heap and update size', () => { + heap.push(10); + heap.push(5); + + expect(heap.size).toBe(2); + }); + + it('should peek the minimum item', () => { + heap.push(10); + heap.push(5); + heap.push(15); + + expect(heap.peek()).toBe(5); + }); + + it('should pop the minimum item and update size', () => { + heap.push(10); + heap.push(5); + heap.push(15); + + const minItem = heap.pop(); + + expect(minItem).toBe(5); + expect(heap.size).toBe(2); + }); + + it('should correctly merge two heaps', () => { + const heap1 = new FibonacciHeap(); + const heap2 = new FibonacciHeap(); + + heap1.push(10); + heap2.push(5); + + heap1.merge(heap2); + + expect(heap1.size).toBe(2); + expect(heap1.peek()).toBe(5); + }); + + it('should clear the heap', () => { + heap.push(10); + heap.push(5); + + heap.clear(); + + expect(heap.size).toBe(0); + expect(heap.peek()).toBeUndefined(); + }); + + it('should handle custom comparators', () => { + const customComparator = (a: number, b: number) => b - a; + const customHeap = new FibonacciHeap(customComparator); + + customHeap.push(10); + customHeap.push(5); + customHeap.push(15); + + expect(customHeap.peek()).toBe(15); + }); + + describe('FibonacciHeap Merge', () => { + it('should merge two Fibonacci heaps correctly', () => { + const heap1 = new FibonacciHeap(); + heap1.push(5).push(10); + + const heap2 = new FibonacciHeap(); + heap2.push(3).push(7); + + heap1.merge(heap2); + + expect(heap1.size).toBe(4); // Combined size of both heaps + expect(heap2.size).toBe(0); // Merged heap should be empty + expect(heap1.peek()).toBe(3); // Minimum element should be 3 + }); + }); +}); + + +describe('FibonacciHeap Stress Test', () => { + it('should handle a large number of elements efficiently', () => { + + const testByMagnitude = (magnitude: number) => { + const heap = new FibonacciHeap(); + + // Add 1000 elements to the heap + for (let i = 1; i <= magnitude; i++) { + heap.push(i); + } + + // Verify that the minimum element is 1 (smallest element) + expect(heap.peek()).toBe(1); + + // Remove all 1000 elements from the heap + const elements = []; + while (heap.size > 0) { + elements.push(heap.pop()); + } + + // Verify that all elements were removed in ascending order + for (let i = 1; i <= magnitude; i++) { + expect(elements[i - 1]).toBe(i); + } + + // Verify that the heap is now empty + expect(heap.size).toBe(0); + } + + testByMagnitude(1000); + + // [ + // 10, 100, 1000, 5000, 10000, 20000, 50000, 75000, 100000, + // 150000, 200000, 250000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000 + // ].forEach(m => logBigOMetricsWrap(testByMagnitude, [m])); + [ + 10, 100, 1000, 5000, 10000, 20000, 50000, 75000, 100000, + 150000, 200000, 250000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000 + ].forEach(m => logBigOMetricsWrap((c: number) => { + const result: number[] = []; + for (let i = 0; i < c; i++) result.push(i); + return result; + } , [m], 'loopPush')); + + }); +}); diff --git a/test/utils/big-o.ts b/test/utils/big-o.ts new file mode 100644 index 0000000..2f18aba --- /dev/null +++ b/test/utils/big-o.ts @@ -0,0 +1,193 @@ +import {AnyFunction} from "../types"; + +const orderReducedBy = 2; // reduction of bigO's order compared to the baseline bigO + +export const magnitude = { + CONSTANT: Math.floor(Number.MAX_SAFE_INTEGER / Math.pow(10, orderReducedBy)), + LOG_N: Math.pow(10, 9 - orderReducedBy), + LINEAR: Math.pow(10, 6 - orderReducedBy), + N_LOG_N: Math.pow(10, 5 - orderReducedBy), + SQUARED: Math.pow(10, 4 - orderReducedBy), + CUBED: Math.pow(10, 3 - orderReducedBy), + FACTORIAL: 20 - orderReducedBy +}; + +export const bigO = { + CONSTANT: magnitude.CONSTANT / 100000, + LOG_N: Math.log2(magnitude.LOG_N) / 1000, + LINEAR: magnitude.LINEAR / 1000, + N_LOG_N: (magnitude.N_LOG_N * Math.log2(magnitude.LOG_N)) / 1000, + SQUARED: Math.pow(magnitude.SQUARED, 2) / 1000, + CUBED: Math.pow(magnitude.SQUARED, 3) / 1000, + FACTORIAL: 10000 +}; + +function findPotentialN(input: any): number { + let longestArray: any[] = []; + let mostProperties: { [key: string]: any } = {}; + + function recurse(obj: any) { + if (Array.isArray(obj)) { + if (obj.length > longestArray.length) { + longestArray = obj; + } + } else if (typeof obj === 'object' && obj !== null) { + const keys = Object.keys(obj); + if (keys.length > Object.keys(mostProperties).length) { + mostProperties = obj; + } + keys.forEach((key) => { + recurse(obj[key]); + }); + } + } + + if (Array.isArray(input)) { + input.forEach((item) => { + recurse(item); + }); + } else { + recurse(input); + } + + // return [longestArray, mostProperties] : [any[], { [key: string]: any }]; + return Math.max(longestArray.length, Object.keys(mostProperties).length); +} + +function linearRegression(x: number[], y: number[]) { + const n = x.length; + + const sumX = x.reduce((acc, val) => acc + val, 0); + const sumY = y.reduce((acc, val) => acc + val, 0); + + const sumXSquared = x.reduce((acc, val) => acc + val ** 2, 0); + const sumXY = x.reduce((acc, val, i) => acc + val * y[i], 0); + + const slope = (n * sumXY - sumX * sumY) / (n * sumXSquared - sumX ** 2); + const intercept = (sumY - slope * sumX) / n; + + const yHat = x.map((val) => slope * val + intercept); + + const totalVariation = y.map((val, i) => (val - yHat[i]) ** 2).reduce((acc, val) => acc + val, 0); + const explainedVariation = y.map((val) => (val - (sumY / n)) ** 2).reduce((acc, val) => acc + val, 0); + + const rSquared = 1 - totalVariation / explainedVariation; + + return { slope, intercept, rSquared }; +} + +function estimateBigO(runtimes: number[], dataSizes: number[]): string { + // Make sure the input runtimes and data sizes have the same length + if (runtimes.length !== dataSizes.length) { + return "输入数组的长度不匹配"; + } + + // Create an array to store the computational complexity of each data point + const complexities: string[] = []; + + // Traverse different possible complexities + const complexitiesToCheck: string[] = [ + "O(1)", // constant time complexity + "O(log n)", // Logarithmic time complexity + "O(n)", // linear time complexity + "O(n log n)", // linear logarithmic time complexity + "O(n^2)", // squared time complexity + ]; + + for (const complexity of complexitiesToCheck) { + // Calculate data points for fitting + const fittedData: number[] = dataSizes.map((size) => { + if (complexity === "O(1)") { + return 1; // constant time complexity + } else if (complexity === "O(log n)") { + return Math.log(size); + } else if (complexity === "O(n)") { + return size; + } else if (complexity === "O(n log n)") { + return size * Math.log(size); + } else if (complexity === "O(n^2)") { + return size ** 2; + } else { + return size ** 10 + } + }); + + // Fit the data points using linear regression analysis + const regressionResult = linearRegression(fittedData, runtimes); + + // Check the R-squared value of the fit. It is usually considered a valid fit if it is greater than 0.9. + if (regressionResult.rSquared >= 0.9) { + complexities.push(complexity); + } + } + + // If there is no valid fitting result, return "cannot estimate", otherwise return the estimated time complexity + if (complexities.length === 0) { + return "Unable to estimate"; + } else { + return complexities.join(" or "); + } +} + +const methodLogs: Map = new Map(); + +export function logBigOMetrics(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: any[]) { + const startTime = performance.now(); + const result = originalMethod.apply(this, args); + const endTime = performance.now(); + const runTime = endTime - startTime; + + const methodName = `${target.constructor.name}.${propertyKey}`; + if (!methodLogs.has(methodName)) { + methodLogs.set(methodName, []); + } + + const methodLog = methodLogs.get(methodName); + + const maxDataSize = args.length === 1 && typeof args[0] === "number" ? args[0] : findPotentialN(args); + if (methodLog) { + methodLog.push([runTime, maxDataSize]); + + if (methodLog.length >= 20) { + console.log('triggered', methodName, methodLog); + const bigO = estimateBigO(methodLog.map(([runTime,]) => runTime), methodLog.map(([runTime,]) => runTime)); + console.log(`Estimated Big O: ${bigO}`); + methodLogs.delete(methodName) + } + } + + return result; + }; + + return descriptor; +} + +export function logBigOMetricsWrap(fn: F, args: Parameters, fnName: string) { + const startTime = performance.now(); + const result = fn(args); + const endTime = performance.now(); + const runTime = endTime - startTime; + const methodName = `${fnName}`; + if (!methodLogs.has(methodName)) { + methodLogs.set(methodName, []); + } + + const methodLog = methodLogs.get(methodName); + + const maxDataSize = args.length === 1 && typeof args[0] === "number" ? args[0] : findPotentialN(args); + if (methodLog) { + methodLog.push([runTime, maxDataSize]); + + if (methodLog.length >= 20) { + console.log('triggered', methodName, methodLog); + const bigO = estimateBigO(methodLog.map(([runTime,]) => runTime), methodLog.map(([runTime,]) => runTime)); + console.log(`Estimated Big O: ${bigO}`); + methodLogs.delete(methodName) + } + } + + return result; +} diff --git a/test/utils/index.ts b/test/utils/index.ts index f605d40..8ac34d1 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,2 +1,2 @@ export * from './number'; -export * from './magnitude'; +export * from './big-o'; diff --git a/test/utils/magnitude.ts b/test/utils/magnitude.ts deleted file mode 100644 index b32f1cd..0000000 --- a/test/utils/magnitude.ts +++ /dev/null @@ -1,21 +0,0 @@ -const orderReducedBy = 2; // reduction of magnitude's order compared to the baseline magnitude - -export const magnitude = { - CONSTANT: Math.floor(Number.MAX_SAFE_INTEGER / Math.pow(10, orderReducedBy)), - LOG_N: Math.pow(10, 9 - orderReducedBy), - LINEAR: Math.pow(10, 6 - orderReducedBy), - N_LOG_N: Math.pow(10, 5 - orderReducedBy), - SQUARED: Math.pow(10, 4 - orderReducedBy), - CUBED: Math.pow(10, 3 - orderReducedBy), - FACTORIAL: 20 - orderReducedBy -}; - -export const bigO = { - CONSTANT: magnitude.CONSTANT / 100000, - LOG_N: Math.log2(magnitude.LOG_N) / 1000, - LINEAR: magnitude.LINEAR / 1000, - N_LOG_N: (magnitude.N_LOG_N * Math.log2(magnitude.LOG_N)) / 1000, - SQUARED: Math.pow(magnitude.SQUARED, 2) / 1000, - CUBED: Math.pow(magnitude.SQUARED, 3) / 1000, - FACTORIAL: 10000 -};