Skip to content

Commit f2d4b0e

Browse files
dankogaiclaude
andcommitted
Make Combination iteration O(size) per element
Fixes #75. The common iterator inherited from _CBase calls .at() for every index, which goes through combinadic() and costs O(size**3) BigInt operations per element. Combination now overrides [Symbol.iterator] with the lexicographic successor algorithm, yielding the same sequence with O(size) number operations per element. Iterating all C(200,3) combinations drops from ~58s to ~40ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 10efb64 commit f2d4b0e

6 files changed

Lines changed: 113 additions & 0 deletions

File tree

combinatorics.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ export declare class Permutation<T> extends _CBase<T, T> {
151151
export declare class Combination<T> extends _CBase<T, T> {
152152
comb: (anyint: any) => number[];
153153
constructor(seed: Iterable<T>, size?: number);
154+
/**
155+
* Overrides the common iterator, which is `O(size**3)` per element
156+
* because it calls `.at()` for each index. This one yields each
157+
* combination from its lexicographic successor in `O(size)`,
158+
* in the same order as the common iterator.
159+
*/
160+
[Symbol.iterator](): Generator<T[], void, unknown>;
154161
/**
155162
* returns an iterator which is more efficient
156163
* than the default iterator that uses .nth

combinatorics.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,31 @@ class Combination extends _CBase {
275275
this.comb = combinadic(this.seed.length, this.size);
276276
Object.freeze(this);
277277
}
278+
/**
279+
* Overrides the common iterator, which is `O(size**3)` per element
280+
* because it calls `.at()` for each index. This one yields each
281+
* combination from its lexicographic successor in `O(size)`,
282+
* in the same order as the common iterator.
283+
*/
284+
[Symbol.iterator]() {
285+
return function* (it) {
286+
if (it.length === 0n)
287+
return;
288+
const [n, k] = [it.seed.length, it.size];
289+
const indices = Array.from({ length: k }, (_, i) => i);
290+
while (true) {
291+
yield indices.map(i => it.seed[i]);
292+
let i = k - 1;
293+
while (0 <= i && indices[i] === i + n - k)
294+
i--;
295+
if (i < 0)
296+
return;
297+
indices[i]++;
298+
for (let j = i + 1; j < k; j++)
299+
indices[j] = indices[j - 1] + 1;
300+
}
301+
}(this);
302+
}
278303
/**
279304
* returns an iterator which is more efficient
280305
* than the default iterator that uses .nth

combinatorics.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,27 @@ export class Combination<T> extends _CBase<T, T> {
282282
this.comb = combinadic(this.seed.length, this.size);
283283
Object.freeze(this);
284284
}
285+
/**
286+
* Overrides the common iterator, which is `O(size**3)` per element
287+
* because it calls `.at()` for each index. This one yields each
288+
* combination from its lexicographic successor in `O(size)`,
289+
* in the same order as the common iterator.
290+
*/
291+
[Symbol.iterator]() {
292+
return function* (it: Combination<T>) {
293+
if (it.length === 0n) return;
294+
const [n, k] = [it.seed.length, it.size];
295+
const indices = Array.from({ length: k }, (_, i) => i);
296+
while (true) {
297+
yield indices.map(i => it.seed[i]);
298+
let i = k - 1;
299+
while (0 <= i && indices[i] === i + n - k) i--;
300+
if (i < 0) return;
301+
indices[i]++;
302+
for (let j = i + 1; j < k; j++) indices[j] = indices[j - 1] + 1;
303+
}
304+
}(this);
305+
}
285306
/**
286307
* returns an iterator which is more efficient
287308
* than the default iterator that uses .nth

commonjs/combinatorics.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,31 @@ class Combination extends _CBase {
277277
this.comb = combinadic(this.seed.length, this.size);
278278
Object.freeze(this);
279279
}
280+
/**
281+
* Overrides the common iterator, which is `O(size**3)` per element
282+
* because it calls `.at()` for each index. This one yields each
283+
* combination from its lexicographic successor in `O(size)`,
284+
* in the same order as the common iterator.
285+
*/
286+
[Symbol.iterator]() {
287+
return function* (it) {
288+
if (it.length === 0n)
289+
return;
290+
const [n, k] = [it.seed.length, it.size];
291+
const indices = Array.from({ length: k }, (_, i) => i);
292+
while (true) {
293+
yield indices.map(i => it.seed[i]);
294+
let i = k - 1;
295+
while (0 <= i && indices[i] === i + n - k)
296+
i--;
297+
if (i < 0)
298+
return;
299+
indices[i]++;
300+
for (let j = i + 1; j < k; j++)
301+
indices[j] = indices[j - 1] + 1;
302+
}
303+
}(this);
304+
}
280305
/**
281306
* returns an iterator which is more efficient
282307
* than the default iterator that uses .nth

test/03-combination.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,14 @@ describe('class Combination', () => {
3737
[[1, 2], [5, 6]],
3838
[[3, 4], [5, 6]]
3939
]));
40+
// https://github.com/dankogai/js-combinatorics/issues/75
41+
const abc = 'abcdefgh';
42+
for (let len = 0; len <= abc.length + 1; len++) {
43+
it(`[...new Combination('${abc}', ${len})] === .at(0)...at(.length-1)`, () => {
44+
let c = new Combination(abc, len);
45+
let byAt = [];
46+
for (let i = 0n; i < c.length; i++) byAt.push(c.at(i));
47+
$$([...c]).to.deep.equal(byAt);
48+
});
49+
}
4050
});

umd/combinatorics.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,31 @@
281281
this.comb = combinadic(this.seed.length, this.size);
282282
Object.freeze(this);
283283
}
284+
/**
285+
* Overrides the common iterator, which is `O(size**3)` per element
286+
* because it calls `.at()` for each index. This one yields each
287+
* combination from its lexicographic successor in `O(size)`,
288+
* in the same order as the common iterator.
289+
*/
290+
[Symbol.iterator]() {
291+
return function* (it) {
292+
if (it.length === 0n)
293+
return;
294+
const [n, k] = [it.seed.length, it.size];
295+
const indices = Array.from({ length: k }, (_, i) => i);
296+
while (true) {
297+
yield indices.map(i => it.seed[i]);
298+
let i = k - 1;
299+
while (0 <= i && indices[i] === i + n - k)
300+
i--;
301+
if (i < 0)
302+
return;
303+
indices[i]++;
304+
for (let j = i + 1; j < k; j++)
305+
indices[j] = indices[j - 1] + 1;
306+
}
307+
}(this);
308+
}
284309
/**
285310
* returns an iterator which is more efficient
286311
* than the default iterator that uses .nth

0 commit comments

Comments
 (0)