How to find all subsets of a set in JavaScript? (Powerset of array)
Asked Answered
T

16

65

I need to get all possible subsets of an array.

Say I have this:

[1, 2, 3]

How do I get this?

[], [1], [2], [3], [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
Titanothere answered 13/3, 2017 at 21:35 Comment(2)
What would be the output of [1, 1] or [1, 2, 1] ...?Enervate
@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.Titanothere
B
72

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]));
Burkett answered 6/11, 2017 at 23:31 Comment(7)
If you reverse [value,...set] to be [...set,value] then it also preserves order.District
this approach isn't memory efficientSomnambulate
This method chokes a lot. Not memory friendly.Buttons
Should be useful for smaller arrays.Naughton
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.Dextrogyrate
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.Belsen
@YuriPredborski It is short, elegant, easy to understand and straightforward to verify. The 3 looping constructs may not be very efficient, but they are clear to see: reduce, concat and map. If you think there's magic happening you should study a bit of functional programming.Gangway
T
28

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ⁿ).

Titanothere answered 13/3, 2017 at 21:35 Comment(5)
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?Kelt
"Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances." - 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.Titanothere
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: medium.com/javascript-scene/…)Kelt
@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?Titanothere
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: benignbemine.github.io/2015/07/19/es6-tail-callsKelt
J
26

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]

Jarmon answered 15/11, 2020 at 13:19 Comment(1)
Great solution, thanks for this! By the way if you want combinations without the first element just return subsets.slice(1).Synclastic
S
16

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; }
Shem answered 13/3, 2017 at 21:54 Comment(3)
Just curious: what is the purpose of .as-console-wrapper?Titanothere
it makes a greater console output.Shem
Studying this code is what helped me to visualize the strategy of this solution and how it works. Great answer!Pompon
S
13

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.

Suspension answered 13/3, 2017 at 21:55 Comment(4)
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 << jTitanothere
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).Piston
The smart use of mathematics and binary here is what I love. A real computer scientist.Mingle
Can anyone explain what does bitwise affect in this code please? Thanks in advanced!Blague
F
7

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]))
Feaze answered 6/7, 2020 at 22:17 Comment(0)
H
3

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 ] ]
Humphreys answered 10/8, 2020 at 0:19 Comment(0)
H
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;
}
Humphreys answered 15/2, 2019 at 4:12 Comment(0)
B
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])]
Beside answered 8/9, 2020 at 13:41 Comment(2)
Seems to me that your second and third versions also don't return subsets in increasing order of size as OP requested.Swellhead
@thund: you're right. These each have logical orders, but none of them matches the actual request. I misread that. Perhaps when I have some time, I will look for an elegant solution to that problem.Beside
G
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]]
Gastroscope answered 3/4, 2021 at 13:56 Comment(0)
B
1

For loop:

function powerSet(numbers) {
    const subsets = [[]]
    for (const number of numbers) {
        subsets.forEach(subset => subsets.push([...subset, number]))
    }
    return subsets
}

Recursion:

function powerSet(numbers) {
    const subsets = [[]]
    if (numbers.length === 0) return subsets
    for (let i = 0; i < numbers.length; i++) {
        subsets.push(...powerSet(numbers.slice(i + 1)).map(subset => [numbers[i], ...subset]))
        // Or step by step:
        // const number = numbers[i]
        // const otherNumbers = numbers.slice(i + 1)
        // const otherNumbersSubsets = powerSet(otherNumbers)
        // const otherNumbersSubsetsWithNumber = otherNumbersSubsets.map(subset => [number, ...subset])
        // subsets.push(...otherNumbersSubsetsWithNumber)
    }
    return subsets
}
Bellina answered 10/10, 2021 at 5:12 Comment(0)
P
1

Using reduceRight:

const subsets = array =>
  array.reduceRight(
    (accumulator, a) => [...accumulator, ...accumulator.map(b => [a, ...b])],
    [[]]
  );
console.log(subsets([1, 2, 3]));  // [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]
Professionalize answered 21/2, 2022 at 22:44 Comment(0)
R
1

Update ES2020

With ES2020 BigInts have become available.

Bigints don’t have a fixed storage size in bits; their sizes adapt to the integers they represent.

- Dr. Axel Rauschmayer; JavaScript for impatient programmers - Chapter 18.2 BigInts

See source.

Using BitInts we can use a binary counter to calculate the power set and are no longer limited by the maximum integer size.

Using a generator we can additionally loop over a power set with constant memory requirement which is important if you want to generate a huge power set.

Here an example using you original array [1, 2, 3].

/**
 * Generate power set from a given array
 * @param {Array<any>} array array to create power set from
 */
function* powerSet(array){
  // use BigInt to be no longer limited by maximum length of 53-bits
  const size = 2n ** BigInt(array.length); 
  for (let i = 0; i < size; i++) {
    const cur = [];
    for(let j = 0; j < array.length; j++){
      // check if i-th bit is set to 1
      if((i & (1 << j)) > 0){
        // push array value (represented by that 1-bit) to result
        cur.push(array[j]);
      }
    }
    // generate next result
    yield cur;
  } 
}

// generate power set for [1, 2, 3] and print results
console.log([...powerSet([1, 2, 3])]);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Here how you could loop over a very large power set with constant memory and no upper bound (theoretically, there will be an upper bound in terms of compute time) for the array length.

/**
 * Generate power set from a given array
 * @param {Array<any>} array array to create power set from
 */
function* powerSet(array){
  // use BigInt to no longer limited by maximum length of 53-bits
  const size = 2n ** BigInt(array.length); 
  for (let i = 0; i < size; i++) {
    const cur = [];
    for(let j = 0; j < array.length; j++){
      // check if i-th bit is set to 1
      if((i & (1 << j)) > 0){
        // push array value (represented by that 1-bit) to result
        cur.push(array[j]);
      }
    }
    // generate next result
    yield cur;
  } 
}

/**
 * Helper function to generate an array containing more than 53 elements
 * @param {number} start 
 * @param {number} end 
 */
function* range(start, end){
  for (let i = start; i < end; i++) {
    yield i;  
  }
}

// create an array containing elments 1 through 60 ([1, 2, 3, ..., 60])
const oneToSixty = [...range(1, 61)];
let i = 0;
const max = 1000; 
// loop over whole powerSet with constant memory requirement
// abort after 1000 subsets, otherwise this will take a very long time to complete
for(const subset of powerSet(oneToSixty)){
  console.log(subset);
  if(i++ === max) break;
}
.as-console-wrapper { max-height: 100% !important; top: 0; }
Radiochemical answered 25/4, 2022 at 11:32 Comment(0)
G
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
Go answered 7/6, 2019 at 7:35 Comment(0)
B
0
 function getAllSubsets(arr) {
    let subset = [[]];
    function generateSubsets(indx, currentSubset) {
        if (indx == arr.length) {
            return;
        }

        for (let i = indx; i < arr.length; i++) {
            currentSubset.push(arr[i])
            subset.push([...currentSubset])
            generateSubsets(i+1,currentSubset)
            currentSubset.pop()
        }

    }
    generateSubsets(0, [])
    return subset
}

// Example usage:
const inputArray = [1, 2, 3];
const subsets = getAllSubsets(inputArray);

console.log(subsets);
Backslide answered 27/3 at 14:12 Comment(0)
P
-2
const numArray = [1,2,3]

//const subArray =[[1],[1,2],[1,2,3],[2],[2,3],[3]]


let result = []
for(let i=0; i<=numArray.length; i++){

  for( let j = i+1; j<=numArray.length; j++){
  
   result.push(numArray.slice(i,j))
  
  }
  
}

console.log(result)`enter code here`
Polled answered 6/4 at 5:28 Comment(1)
This is missing [] and [1,3]Gangway

© 2022 - 2024 — McMap. All rights reserved.