mirror of
https://github.com/zrwusa/data-structure-typed.git
synced 2024-11-23 12:54:04 +00:00
Merge pull request #15 from zrwusa/heap
[heap] fibonacci heap implemented. [test] big O estimate. [project] n…
This commit is contained in:
commit
ef05fdee75
|
@ -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",
|
||||
|
|
|
@ -10,7 +10,7 @@ import {DFSOrderPattern} from '../../types';
|
|||
|
||||
export class Heap<E> {
|
||||
protected nodes: E[] = [];
|
||||
private readonly comparator: Comparator<E>;
|
||||
protected readonly comparator: Comparator<E>;
|
||||
|
||||
constructor(comparator: Comparator<E>) {
|
||||
this.comparator = comparator;
|
||||
|
@ -18,21 +18,29 @@ export class Heap<E> {
|
|||
|
||||
/**
|
||||
* 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<E> {
|
||||
this.nodes.push(value);
|
||||
add(element: E): Heap<E> {
|
||||
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<E> {
|
||||
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<E> {
|
|||
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<E> {
|
|||
|
||||
/**
|
||||
* 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<E> {
|
|||
|
||||
/**
|
||||
* 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<E> {
|
|||
|
||||
/**
|
||||
* 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<E> {
|
|||
return binaryHeap;
|
||||
}
|
||||
}
|
||||
|
||||
export class FibonacciHeapNode<E> {
|
||||
element: E;
|
||||
degree: number;
|
||||
left?: FibonacciHeapNode<E>;
|
||||
right?: FibonacciHeapNode<E>;
|
||||
child?: FibonacciHeapNode<E>;
|
||||
parent?: FibonacciHeapNode<E>;
|
||||
marked: boolean;
|
||||
constructor(element: E, degree = 0) {
|
||||
this.element = element;
|
||||
this.degree = degree;
|
||||
this.marked = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class FibonacciHeap<E> {
|
||||
root?: FibonacciHeapNode<E>;
|
||||
protected min?: FibonacciHeapNode<E>;
|
||||
size: number = 0;
|
||||
protected readonly comparator: Comparator<E>;
|
||||
|
||||
constructor(comparator?: Comparator<E>) {
|
||||
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<E> {
|
||||
return new FibonacciHeapNode<E>(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given node with the root list.
|
||||
* @param node - The node to be merged.
|
||||
*/
|
||||
protected mergeWithRoot(node: FibonacciHeapNode<E>): 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<E>} FibonacciHeap<E> - The heap itself.
|
||||
*/
|
||||
add(element: E): FibonacciHeap<E> {
|
||||
return this.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* O(1) time operation.
|
||||
* Insert an element into the heap and maintain the heap properties.
|
||||
* @param element
|
||||
* @returns {FibonacciHeap<E>} FibonacciHeap<E> - The heap itself.
|
||||
*/
|
||||
push(element: E): FibonacciHeap<E> {
|
||||
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<E>} head - The head of the linked list.
|
||||
* @protected
|
||||
* @returns FibonacciHeapNode<E>[] - An array containing the nodes of the linked list.
|
||||
*/
|
||||
consumeLinkedList(head?: FibonacciHeapNode<E>): FibonacciHeapNode<E>[] {
|
||||
const nodes: FibonacciHeapNode<E>[] = [];
|
||||
if (!head) return nodes;
|
||||
|
||||
let node: FibonacciHeapNode<E> | 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<E>): 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<E>, node: FibonacciHeapNode<E>): 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<E>, x: FibonacciHeapNode<E>): 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<E> | undefined)[] = new Array(this.size);
|
||||
const nodes = this.consumeLinkedList(this.root);
|
||||
let x: FibonacciHeapNode<E> | undefined, y: FibonacciHeapNode<E> | undefined, d: number, t: FibonacciHeapNode<E> | undefined;
|
||||
|
||||
for (const node of nodes) {
|
||||
x = node;
|
||||
d = x.degree;
|
||||
|
||||
while (A[d]) {
|
||||
y = A[d] as FibonacciHeapNode<E>;
|
||||
|
||||
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<E>): 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();
|
||||
}
|
||||
}
|
||||
|
|
1
test/types/index.ts
Normal file
1
test/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './utils';
|
1
test/types/utils/big-o.ts
Normal file
1
test/types/utils/big-o.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type AnyFunction = (...args: any[]) => any;
|
1
test/types/utils/index.ts
Normal file
1
test/types/utils/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './big-o';
|
|
@ -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<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
heap = new FibonacciHeap<number>();
|
||||
});
|
||||
|
||||
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<number>((a, b) => b - a);
|
||||
maxHeap.push(10);
|
||||
maxHeap.push(5);
|
||||
expect(maxHeap.peek()).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FibonacciHeap', () => {
|
||||
let heap: FibonacciHeap<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
heap = new FibonacciHeap<number>();
|
||||
});
|
||||
|
||||
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<number>();
|
||||
const heap2 = new FibonacciHeap<number>();
|
||||
|
||||
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<number>(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<number>();
|
||||
heap1.push(5).push(10);
|
||||
|
||||
const heap2 = new FibonacciHeap<number>();
|
||||
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<number>();
|
||||
|
||||
// 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<typeof testByMagnitude>(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'));
|
||||
|
||||
});
|
||||
});
|
||||
|
|
193
test/utils/big-o.ts
Normal file
193
test/utils/big-o.ts
Normal file
|
@ -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<string, [number, number][] > = 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<F extends AnyFunction>(fn: F, args: Parameters<F>, 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;
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
export * from './number';
|
||||
export * from './magnitude';
|
||||
export * from './big-o';
|
||||
|
|
|
@ -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
|
||||
};
|
Loading…
Reference in a new issue