44

I need to get all possible subsets of an array.

Say I have this:

[1, 2, 3]

How do I get this?

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

I am interested in all subsets. For subsets of specific length, refer to the following questions:

  • Finding subsets of size n: 1, 2
  • Finding subsets of size > 1: 1
le_m
  • 15,910
  • 7
  • 55
  • 65
  • What would be the output of `[1, 1]` or `[1, 2, 1]` ...? – ibrahim mahrir Mar 13 '17 at 21:40
  • @ibrahimmahrir If we regard our input array as a set, each element is assumed to have a different identity. So the powerset of `[1, 1]` would probably be `[], [1], [1], [1, 1]` - but if you have a better idea, just post it. – le_m Mar 13 '17 at 21:42
  • 2
    what about `[3]`? – Nina Scholz Mar 13 '17 at 21:45
  • @NinaScholz All possible subsets of `[3]` are `[], [3]`. Whether or not the trivial solution - the empty set `[]` - is included in your specific implementation is up to you. – le_m Mar 13 '17 at 21:47
  • If you found any of the answers helpful, remember to accept the one that helped you the most in solving your problem. Have a nice day! – Angelos Chalaris Mar 17 '17 at 09:43
  • @AngelosChalaris You are right. But I will give the alternative solutions a few more days to collect upvotes as I hesitate accepting my own answer. – le_m Mar 17 '17 at 17:06
  • @le_m Fine by me, it's okay really to accept your own answer, provided it is well formulated and has enough upvotes plus it answers the question at hand. So, go ahead as soon as you feel ready! :) – Angelos Chalaris Mar 17 '17 at 17:16

11 Answers11

47

Here is one more very elegant solution with no loops or recursion, only using the map and reduce array native functions.

const getAllSubsets = 
      theArray => theArray.reduce(
        (subsets, value) => subsets.concat(
         subsets.map(set => [value,...set])
        ),
        [[]]
      );

console.log(getAllSubsets([1,2,3]));
MennyMez
  • 2,022
  • 15
  • 18
  • 15
    If you reverse [value,...set] to be [...set,value] then it also preserves order. – user3071643 May 18 '18 at 20:08
  • 3
    this approach isn't memory efficient – John Anisere Apr 10 '19 at 10:43
  • 3
    This method chokes a lot. Not memory friendly. – Cozzbie May 15 '19 at 19:06
  • Should be useful for smaller arrays. – Isaac C Way Jul 03 '19 at 22:56
  • 4
    A great example of anti-pattern. Very short !== very elegant. The problem of this approach is exactly what it sounds like - there are no loops, no hoops, no recursions to see. It is all done by the engine in the background so you can see the magic. And only magic. And not understand what is happening in the background. It would be great if everything was written out clearly, be readable, easy to understand. Brevity may be sister or talent but not a friend of understanding. – Yuri Predborski May 23 '20 at 14:38
  • 2
    This approach is using only native JavaScript methods. If you find that this is magic, you should learn the basics of JavaScript such as `reduce()` and `map()` and not blame a very much logic and understandle solution for your misunderstanding. – dmuensterer Oct 28 '20 at 14:31
24

We can solve this problem for a subset of the input array, starting from offset. Then we recurse back to get a complete solution.

Using a generator function allows us to iterate through subsets with constant memory usage:

// Generate all array subsets:
function* subsets(array, offset = 0) {
  while (offset < array.length) {
    let first = array[offset++];
    for (let subset of subsets(array, offset)) {
      subset.push(first);
      yield subset;
    }
  }
  yield [];
}

// Example:
for (let subset of subsets([1, 2, 3])) {
  console.log(subset); 
}

Runtime complexity is proportional to the number of solutions (2ⁿ) times the average length per solution (n/2) = O(n2ⁿ).

le_m
  • 15,910
  • 7
  • 55
  • 65
  • 1
    Nice use of the generator function! Could you point to an article that goes into further detail about the generator function using `constant memory usage`? – Shaheen Ghiassy Jun 14 '17 at 18:37
  • 2
    ["Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances."](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/function*) - the recursion depth is limited by the array length and each recursion pushes one element on the current subset until it is complete - so memory usage for each iteration of `for (let subset of subsets(array))` is bound by the length of `array`, let's say **n** and not **2^n** which would be the case if we first build all subsets and then iterate over them. – le_m Jun 14 '17 at 21:05
  • Per my understanding, it seems that this type of implementation using generators is increasing at `O(n)` memory growth. Per my perf testing, on the first iteration of the generator (i.e: `.next()`) we are instantiating all generator functions from `0...n`. Unlike tail recursion which grows at `O(1)` memory growth, this seems to grow at `O(n)`... btw - I hope I'm wrong in this analysis because the generator approach seems like an elegant approach. (P.S: A quality article I found: https://medium.com/javascript-scene/7-surprising-things-i-learned-writing-a-fibonacci-generator-4886a5c87710) – Shaheen Ghiassy Jun 16 '17 at 04:23
  • @ShaheenGhiassy I don't understand "grows at `O(1)` memory growth" - memory complexity can't be `O(1)` as each subset already occupies `O(n)` space. What do you mean with "increasing at `O(n)` memory growth` - the total space complexity is in O(n) where n is the array length? – le_m Jun 16 '17 at 16:25
  • Yup, I see your point @le_m. I guess I was just wondering if it was possible to use generators recursively and still achieve constant memory usage? It might not be possible for this particular question, but I do know that ES6 introduced tail-call optimizations. With that optimization, problems such as generating Fibonacci sequences can achieve constant memory usage regardless of the number of recursions (which I referred to as `O(1) memory growth`). Seems generators won't be optimized in the same way as a tail-call function. See: http://benignbemine.github.io/2015/07/19/es6-tail-calls – Shaheen Ghiassy Jun 17 '17 at 07:09
14

Another simple solution.

function getCombinations(array) {

    function fork(i, t) {
        if (i === array.length) {
            result.push(t);
            return;
        }
        fork(i + 1, t.concat([array[i]]));
        fork(i + 1, t);
    }

    var result = [];
    fork(0, []);
    return result;
}

var data = [1, 2, 3],
    result = getCombinations(data);
 
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 323,592
  • 20
  • 270
  • 324
9

You can easily generate the powerset from an array, using something like the following:

var arr = [1, 2, 3];

function generatePowerSet(array) {
  var result = [];
  result.push([]);

  for (var i = 1; i < (1 << array.length); i++) {
    var subset = [];
    for (var j = 0; j < array.length; j++)
      if (i & (1 << j))
        subset.push(array[j]);

    result.push(subset);
  }

  return result;
}

console.log(generatePowerSet(arr));

Throughout the main loop of the function, subsets are created and then pushed into the result array.

kevinji
  • 9,983
  • 4
  • 35
  • 55
Angelos Chalaris
  • 5,882
  • 7
  • 44
  • 65
  • 3
    A solid non-recursive solution. Lifting `result.push(subset)` out of the loop increment to the loop body would probably improve readability. And if you are already resorting to bitwise operations: `Math.pow(2, j) == 1 << j` – le_m Mar 13 '17 at 21:59
  • I am trying to really grok the intuition of this answer. I get the concept that 1s in a binary string map to whether an element is included in the subset. For example, ['a','b','c'], 101 => ['a', 'c']. I also get that a powerset has 2^n elements. What I am *not* yet intuitively getting is how it elegantly ties together. FWIW, on jsperf, 1 << j is 10x faster than Math.pow(2,j). – Zack Burt Apr 03 '20 at 22:34
6

Simple solution without recursion:

function getAllSubsets(array) {
    const subsets = [[]];
    
    for (const el of array) {
        const last = subsets.length-1;
        for (let i = 0; i <= last; i++) {
            subsets.push( [...subsets[i], el] );
        }
    }
    
    return subsets;
}


How does it work?

If we have some subsets generated from input numbers and we want to add one more number to our input array, it means that we can take all already existing subsets and generate new ones by appending the new number to each of the existing.


Here is an example for [1, 2, 3]

  • Start with an empty subset: []

  • Create new subsets by adding "1" to each existing subset. It will be:[] [1]

  • Create new subsets by adding "2" to each existing subset. It will be:[], [1] [2], [1, 2]

  • Create new subsets by adding "3" to each existing subset. It will be: [], [1], [2], [1, 2] [3], [1, 3], [2, 3], [1, 2, 3]

koorchik
  • 1,451
  • 2
  • 12
  • 9
3

I set out to understand what is happening with the examples in this post. While the function generator example, bit-wise operator example, and the example use of the array map and reduce functions are very elegant and impressive, I found it tough to mentally visual what precisely was happening. I have 2 examples below that I believe are easy to visualize both a non-recursive and a recursive solution. I hope this helps others attempting to wrap their heads around the process of finding all subsets.

NON-RECURSIVE: For each value of the array clone all existing subsets (including the empty set) and add the new value to each of the clones, pushing the clones back to the results.

const PowerSet = array => {
  const result = [[]] // Starting with empty set
  
  for (let value of array) { // For each value of the array
     const length = result.length // Can't use result.length in loop since 
                                  // results length is increased in loop
      for (let i = 0; i < length; i++){
        let temp = result[i].slice(0) // Make a clone of the value at index i  
        temp.push(value) // Add current value to clone
        result.push(temp) // Add clone back to results array
      }
  }
  
  return result;
  }

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

RECURSIVELY: Build the powerset by recursively pushing a combination of the current index value concatenated with an ever increasing prefix array of values.

const powerSetRecursive = (arr, prefix=[], set=[[]]) => {
  if(arr.length === 0) return// Base case, end recursion
  
  for (let i = 0; i < arr.length; i++) {
      set.push(prefix.concat(arr[i]))// If a prefix comes through, concatenate value
      powerSetRecursive(arr.slice(i + 1), prefix.concat(arr[i]), set)
      // Call function recursively removing values at or before i and adding  
      // value at i to prefix
  }
  return set
}

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

function subSets(num){


/*
example given number : [1,3]
         []
 1:  copy   push 1
    []        [1]
  
 3: copy      push 3
    [] [1] [3] [1,3]


*/
  let result = [];

  result.push([]);

  for(let i=0; i < num.length;i++){

    let currentNum = num[i];
    let len = result.length;

    for(let j=0; j < len; j++){
      let cloneData = result[j].slice();
      cloneData.push(currentNum);
      result.push(cloneData)
    }
  }

  return result;
}

let test = [1,3];
console.log(subSets(test))//[ [], [ 1 ], [ 3 ], [ 1, 3 ] ]
ASHISH RANJAN
  • 3,171
  • 1
  • 17
  • 11
1
let subsets = (n) => {

let result = [];
result.push([]);

n.forEach(a => {

  //array length
   let length = result.length;
    let i =0;

    while(i < length){

      let temp = result[i].slice(0);
      temp.push(a);

      result.push(temp);
      i++;

    }
})

return result;
}
ASHISH RANJAN
  • 3,171
  • 1
  • 17
  • 11
1

Using flatMap and rest/spread, this can be fairly elegant:

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

console .log (subsets ([1, 2, 3]))
.as-console-wrapper {max-height: 100% !important; top: 0}

This version does not return them in the requested order. Doing that seems slightly less elegant, and there's probably a better version:

const subset = (xs = []) => {
  if (xs.length == 0) {return [[]]}
  const ys = subset (xs .slice (0, -1))
  const x = xs .slice (-1) [0]
  return [... ys, ... ys .map (y => [... y, x])]
}

Or, the same algorithm in a different style,

const subsets = (
  xs = [], 
  x = xs .slice (-1) [0], 
  ys = xs.length && subsets (xs .slice (0, -1))
) =>
  xs .length == 0
    ? [[]]
    : [... ys, ... ys .map (y => [... y, x])]
Scott Sauyet
  • 37,179
  • 4
  • 36
  • 82
1

A shorter version of @koorchik's answer.

var getAllSubsets = (nums) => {
  const subsets = [[]];
  for (n of nums) {
    subsets.map((el) => {
      subsets.push([...el, n]);
    });
  }
  return subsets;
};

console.log(getAllSubsets([1, 2, 3])); 
// [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Penny Liu
  • 7,720
  • 5
  • 40
  • 66
0

This one is with recursion

var subsets = function(s){
  if(s.length === 0) {
    return [[]]
  }
  var h,t,ss_excl_h;
  var ss_incl_h = [];
  [h,...t] = s;
  ss_excl_h = subsets(t)
  for(ss of ss_excl_h) {
    let hArr = [];
    hArr.push(h);
    let temp = hArr.concat(ss)
    ss_incl_h.push(temp);
  }
  return ss_incl_h.concat(ss_excl_h)
}

console.log(subsets([1,2,3])) // returns distinct subsets
pride
  • 39
  • 1
  • 6