Merge pull request #15 from zrwusa/heap

[heap] fibonacci heap implemented. [test] big O estimate. [project] n…
This commit is contained in:
zrwusa 2023-10-21 02:00:33 +08:00 committed by GitHub
commit ef05fdee75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 724 additions and 40 deletions

View file

@ -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",

View file

@ -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
View file

@ -0,0 +1 @@
export * from './utils';

View file

@ -0,0 +1 @@
export type AnyFunction = (...args: any[]) => any;

View file

@ -0,0 +1 @@
export * from './big-o';

View file

@ -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
View 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;
}

View file

@ -1,2 +1,2 @@
export * from './number';
export * from './magnitude';
export * from './big-o';

View file

@ -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
};