All subsets of an array recursively only in one function JavaScript

Viewed 99

I've managed to do this in two recursive functions. But am really curious, is it possible to use both recursions in one function...

Here is a task of an exercise and a hint:

Write a function called subsets that will return all subsets of an array.

Examples below.

Hint: For subsets([1, 2, 3]), there are two kinds of subsets:

  1. Those that do not contain 3 (all of these are subsets of [1, 2]).
  2. For every subset that does not contain 3, there is also a corresponding subset that is the same, except it also does contain 3.

Here is my solution with 2 recursives, examples of the task are included:

let arr = []
console.log(JSON.stringify(subsets(arr))); // [[]]
arr = [1]
console.log(JSON.stringify(subsets(arr))); // [[], [1]]
arr = [1,2]
console.log(JSON.stringify(subsets(arr))); // [[], [1], [2], [1, 2]]
arr = [1,2,3]
console.log(JSON.stringify(subsets(arr))); // [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

// your code here
function subsets(arr) {
  if (arr.length === 0) return [[]]; // base

  let el = arr.slice(-1)
  let prev = subsets(arr.slice(0, -1))
  return prev.concat(addEl(prev,el))
  //can be instead iterative: return prev.concat(prev.map(x => x.concat([el])));  
}

function addEl(arrA, el) {
  let firstEl = arrA[0];
  let rest = arrA.slice(1)

  if (arrA.length === 1) return [firstEl.concat(el)]; // base

  return [firstEl.concat(el)].concat(addEl(rest, el)) 
}
.as-console-wrapper { min-height: 100%; top: 0; }

4 Answers

You were quite close already with the commented-out code, you just needed to use x.concat(el) instead of x.concat([el]). Or alternatively, el = arr.at(-1) instead of el = arr.slice(-1).

let arr = []
console.log(subsets(arr)); // [[]]
arr = [1]
console.log(subsets(arr)); // [[], [1]]
arr = [1,2]
console.log(subsets(arr)); // [[], [1], [2], [1, 2]]
arr = [1,2,3]
console.log(subsets(arr)); // [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

// your code here
function subsets(arr) {
  if (arr.length === 0) return [[]]; // base

  let el = arr.slice(-1)
  let prev = subsets(arr.slice(0, -1))
  return prev.concat(prev.map(x => x.concat(el)));  
}

A simple single-function recursion stores current working state in a second parameter, defaulting it to [], and then, on each call with remaining items, removes that items, and splits in two recursive calls, one in we don't add the item to our working state, one in which we do. When there are no remaining items, we simply return our working state, wrapped in one additional level of array. It might look like this:

const subsets = ([x, ...xs], ys = []) =>
  x == undefined
    ? [ys]
  : [...subsets (xs, ys), ...subsets (xs, [...ys, x])]

console .log (subsets ([1, 2, 3]))

We can visualize the tree of calls like this, with [<xs>]-[<ys>] at each node:

                              [1, 2, 3]-[]
                                   |
               ,-------------------+-------------------,
            [2, 3]-[]                              [2, 3]-[1]
               |                                        |
       ,-------+--------,                   ,-----------+------------,
    [3]-[]           [3]-[2]             [3]-[1]                [3]-[1, 2]
       |                |                   |                        |
  ,----+---,       ,----+----,         ,----+-----,           ,------+------,
[]-[]    []-[3]  []-[2]   []-[2, 3]  []-[1]   []-[1, 3]   []-[1, 2]   []-[1, 2, 3]
    \_____   \____   \_         \       /        __/        ___/       _____/  
          \____   \_   \_        |     |       _/      ____/    ______/
               \    \    \       |     |      /       /        /
               [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]

Similar approach to @Scott's answer, except this uses a rest parameter for ys, and it conditionally prepends the last element of xs to the rest parameter ys at each level of recursion, rather than conditionally appending the first element of xs:

const subsets = (xs, ...ys) => (
  xs.length === 0 ? [ys] : [
    subsets(xs.slice(0, -1), ...ys),
    subsets(xs.slice(0, -1), xs.at(-1), ...ys)
  ].flat()
);

console.log(subsets([1, 2, 3]));

Here is a visualization of the recursion tree for this example:

([1, 2, 3])
 |-------------------------------+
([1, 2])                        ([1, 2], 3)
 |------------+                  |------------------+
([1])        ([1], 2)           ([1], 3)           ([1], 2, 3)
 |----+       |-------+          |-------+          |----------+
([]) ([], 1) ([], 2) ([], 1, 2) ([], 3) ([], 1, 3) ([], 2, 3) ([], 1, 2, 3)
 |    |       |       |          |       |          |          |
[[],  [1],    [2],    [1, 2],    [3],    [1, 3],    [2, 3]     [1, 2, 3]]

recursive generator

To get a lazy iterable of the subsets, you can use a recursive generator. A utility tee function is used to "branch" the iterable -

function *subsets(arr) {
  if (arr.length === 0) return yield []
  const last = arr.slice(-1)
  const [exclude, include] = tee(subsets(arr.slice(0, -1))) // recursive
  yield *exclude
  for (const s of include) yield s.concat(last)
}

function *tee(g, n = 2) {
  const memo = []
  function* iter(i) {
    while (true) {
      if (i >= memo.length) {
        const w = g.next()
        if (w.done) return
        memo.push(w.value)
      }
      else yield memo[i++]
    }
  }
  while (n-- >= 0) yield iter(0)
}

console.log(JSON.stringify(Array.from(subsets([]))))
console.log(JSON.stringify(Array.from(subsets([1]))))
console.log(JSON.stringify(Array.from(subsets([1,2]))))
console.log(JSON.stringify(Array.from(subsets([1,2,3]))))
.as-console-wrapper { min-height: 100%; top: 0; }

[[]]
[[],[1]]
[[],[1],[2],[1,2]]
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

map for free

Array.from has optional parameters, mapFn and thisArg. We might naively write Array.from(...).map(mapFn), but Array.from(..., mapFn) does the same thing in half the iterations.

Imagine we write withStat to provide some statistic about the subsets as they are generated -

function withStat(subset) {
  return {
    length: subset.length,
    sum: subset.reduce((a,b) => a + b, 0),
    subset,
  }
}

Array.from(subsets([1,2,3]), withStat)
[
  {"length":0,"sum":0,"subset":[]},
  {"length":1,"sum":1,"subset":[1]},
  {"length":1,"sum":2,"subset":[2]},
  {"length":2,"sum":3,"subset":[1,2]},
  {"length":1,"sum":3,"subset":[3]},
  {"length":2,"sum":4,"subset":[1,3]},
  {"length":2,"sum":5,"subset":[2,3]},
  {"length":3,"sum":6,"subset":[1,2,3]},
]

persistent iterators

Above we use tee to read an iterable's values multiple times. Below we show iter that wraps a generator in a persistent iterator interface, allowing us to use the iterator multiple times.

function *subsets(arr) {
  if (arr.length === 0) return yield []
  const last = arr.slice(-1)
  const r = iter(subsets(arr.slice(0, -1))) // assign r
  yield *r // read from r
  for (const s of r) yield s.concat(last) // read from r again
}

For that we write an iter abstraction -

function iter(g) {
  const computed = thunk(_ => g.next())
  return {
    get done() {
      return computed().done
    },
    get value() {
      return computed().value
    },
    next: thunk(_ =>
      computed() && iter(g)
    ),
    *[Symbol.iterator]() {
      let t = this
      while(!t.done) yield t.value, t = t.next()
    }
  }
}

Which depends on a thunk abstraction -

function thunk(f) {
  const nil = Symbol()
  let computed = nil
  return _ => {
    if (computed === nil) computed = f()
    return computed
  }
}

Hopefully you can begin to see how data abstraction allows us to write higher-level programs and keep complexity under control.

function *subsets(arr) {
  if (arr.length === 0) return yield []
  const last = arr.slice(-1)
  const r = iter(subsets(arr.slice(0, -1)))
  yield *r
  for (const s of r) yield s.concat(last)
}

function iter(g) {
  let computed = thunk(_ => g.next())
  return {
    get done() {
      return computed().done
    },
    get value() {
      return computed().value
    },
    next: thunk(_ =>
      computed() && iter(g)
    ),
    *[Symbol.iterator]() {
      let t = this
      while(!t.done) yield t.value, t = t.next()
    }
  }
}

function thunk(f) {
  const nil = Symbol()
  let computed = nil
  return _ => {
    if (computed === nil) computed = f()
    return computed
  }
}

console.log(JSON.stringify(Array.from(subsets([]))))
console.log(JSON.stringify(Array.from(subsets([1]))))
console.log(JSON.stringify(Array.from(subsets([1,2]))))
console.log(JSON.stringify(Array.from(subsets([1,2,3]))))
.as-console-wrapper { min-height: 100%; top: 0; }

[[]]
[[],[1]]
[[],[1],[2],[1,2]]
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Related