diff --git a/docs/index.d.ts b/docs/index.d.ts index 7dc08f3..c71c69e 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -94,6 +94,9 @@ export const examples: { readonly flatten: 'examples/jsonDiff.ts'; readonly unflatten: 'examples/jsonDiff.ts'; readonly paginate: 'examples/pagination.ts'; + readonly diffTree: 'examples/treeDiff.ts'; + readonly applyTreeDiff: 'examples/treeDiff.ts'; + readonly UnionFind: 'examples/graph.ts'; }; readonly performance: { readonly debounce: 'examples/requestDedup.ts'; @@ -2926,6 +2929,20 @@ export function groupBy( key: keyof T | ((item: T) => string) ): Record; +/** + * Disjoint Set Union (Union-Find) with path compression and union by size. + * Use for: connectivity queries, Kruskal MST, clustering. + * Import: data/unionFind.ts + */ +export class UnionFind { + constructor(elements?: Iterable); + makeSet(x: T): void; + find(x: T): T; + union(a: T, b: T): boolean; + connected(a: T, b: T): boolean; + size(x: T): number; +} + // ============================================================================ // 📈 GRAPH ALGORITHMS // ============================================================================ diff --git a/src/data/unionFind.ts b/src/data/unionFind.ts new file mode 100644 index 0000000..f1136c6 --- /dev/null +++ b/src/data/unionFind.ts @@ -0,0 +1,60 @@ +/** + * Disjoint Set Union (Union-Find) with path compression and union by size. + * Useful for: connectivity queries, Kruskal MST, clustering. + */ +export class UnionFind { + private parent = new Map(); + private compSize = new Map(); + + constructor(elements?: Iterable) { + if (elements) { + for (const el of elements) { + this.makeSet(el); + } + } + } + + makeSet(x: T): void { + if (!this.parent.has(x)) { + this.parent.set(x, x); + this.compSize.set(x, 1); + } + } + + find(x: T): T { + if (!this.parent.has(x)) this.makeSet(x); + const direct = this.parent.get(x); + let p: T = direct === undefined ? x : (direct as T); + if (p !== x) { + p = this.find(p); + this.parent.set(x, p); + } + return p; + } + + union(a: T, b: T): boolean { + let ra = this.find(a); + let rb = this.find(b); + if (ra === rb) return false; + const sa = this.compSize.get(ra)!; + const sb = this.compSize.get(rb)!; + if (sa < sb) { + const tmp = ra; + ra = rb; + rb = tmp; + } + // attach rb under ra + this.parent.set(rb, ra); + this.compSize.set(ra, sa + sb); + this.compSize.delete(rb); + return true; + } + + connected(a: T, b: T): boolean { + return this.find(a) === this.find(b); + } + + size(x: T): number { + return this.compSize.get(this.find(x)) ?? 1; + } +} diff --git a/src/index.ts b/src/index.ts index 2d44e46..ab88224 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,6 +94,7 @@ export const examples = { paginate: 'examples/pagination.ts', diffTree: 'examples/treeDiff.ts', applyTreeDiff: 'examples/treeDiff.ts', + UnionFind: 'examples/graph.ts', }, performance: { debounce: 'examples/requestDedup.ts', @@ -412,7 +413,6 @@ export { satCollision } from './spatial/sat.js'; */ export { circleRayIntersection } from './spatial/circleRay.js'; /** -<<<<<<< HEAD * Fast circle-circle overlap test. * * Example file: examples/circle.ts @@ -430,7 +430,7 @@ export { circleAabbCollision } from './spatial/circleCollision.js'; * Example file: examples/circle.ts */ export { circleSegmentIntersection } from './spatial/circleCollision.js'; -======= +/** * Ray vs. segment intersection test returning closest hit. * * Example file: examples/raycast.ts @@ -442,7 +442,6 @@ export { raycastSegment } from './spatial/raycast.js'; * Example file: examples/raycast.ts */ export { raycastAabb } from './spatial/raycast.js'; ->>>>>>> 83f962b (feat(spatial): add raycasting utilities (raycastSegment, raycastAabb); docs + tests + example) /** * Continuous swept AABB collision detection for moving boxes. @@ -1021,6 +1020,12 @@ export type { * Tree diff helpers for hierarchical data. */ export { diffTree, applyTreeDiff } from './data/treeDiff.js'; +/** + * Disjoint Set Union (Union-Find) with path compression and union by size. + * + * Example file: examples/graph.ts + */ +export { UnionFind } from './data/unionFind.js'; export type { TreeNode, diff --git a/tests/index.test.ts b/tests/index.test.ts index dcf120e..cb04ce9 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -123,6 +123,7 @@ describe('package entry point', () => { | 'paginate' | 'diffTree' | 'applyTreeDiff' + | 'UnionFind' >(); expectTypeOf>().toEqualTypeOf< diff --git a/tests/unionFind.test.ts b/tests/unionFind.test.ts new file mode 100644 index 0000000..73953c5 --- /dev/null +++ b/tests/unionFind.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { UnionFind } from '../src/index.js'; + +describe('UnionFind', () => { + it('unions and finds components for numbers', () => { + const uf = new UnionFind([0, 1, 2, 3, 4]); + uf.union(0, 1); + uf.union(2, 3); + expect(uf.connected(0, 1)).toBe(true); + expect(uf.connected(0, 2)).toBe(false); + uf.union(1, 2); + expect(uf.connected(0, 3)).toBe(true); + expect(uf.size(0)).toBe(4); + expect(uf.size(4)).toBe(1); + }); + + it('supports string keys', () => { + const uf = new UnionFind(['a', 'b', 'c']); + uf.union('a', 'b'); + expect(uf.connected('a', 'b')).toBe(true); + expect(uf.connected('a', 'c')).toBe(false); + uf.makeSet('d'); + expect(uf.size('d')).toBe(1); + }); +}); +