The problem. Given two integers n and k, return all possible combinations of k numbers chosen from 1 to n, in any order.
n = 4, k = 2 -> [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
n = 1, k = 1 -> [[1]]A combination is a set, so [1, 2] and [2, 1] are the same. Building each combination in increasing order makes every set appear exactly once: after choosing x, only try numbers bigger than x. That's the start parameter. And there's a cheap but powerful prune: if the path still needs k − path.length more numbers, the next choice can be at most n − (k − path.length) + 1 — anything bigger leaves too few numbers to finish.
function combine(n, k) { const result = []; const path = []; const build = (start) => { if (path.length === k) { result.push([...path]); return; } for (let x = start; x <= n; x++) { // only numbers bigger than the last one path.push(x); build(x + 1); path.pop(); } }; build(1); return result;}Time O(C(n, k) · k) plus dead branches that run out of numbers, space O(k).
A node never tries a number that leaves too few numbers to finish.
function combine(n, k) { const result = []; const path = []; const build = (start) => { if (path.length === k) { result.push([...path]); return; } const last = n - (k - path.length) + 1; // beyond this, too few numbers are left for (let x = start; x <= last; x++) { path.push(x); build(x + 1); path.pop(); } }; build(1); return result;}Time O(C(n, k) · k), space O(k). Every branch now reaches a real combination, so no work is wasted — a big saving when k is close to n.