Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -2926,6 +2929,20 @@ export function groupBy<T>(
key: keyof T | ((item: T) => string)
): Record<string, T[]>;

/**
* 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<T = number> {
constructor(elements?: Iterable<T>);
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
// ============================================================================
Expand Down
60 changes: 60 additions & 0 deletions src/data/unionFind.ts
Original file line number Diff line number Diff line change
@@ -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<T = number> {
private parent = new Map<T, T>();
private compSize = new Map<T, number>();

constructor(elements?: Iterable<T>) {
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;
}
}
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('package entry point', () => {
| 'paginate'
| 'diffTree'
| 'applyTreeDiff'
| 'UnionFind'
>();

expectTypeOf<ExampleName<'search'>>().toEqualTypeOf<
Expand Down
26 changes: 26 additions & 0 deletions tests/unionFind.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>(['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);
});
});