如何在 JavaScript 中查找集合的所有子集?(数组的幂集)

IT技术 javascript subset powerset
2021-01-17 10:40:18

我需要获取数组的所有可能子集。

说我有这个:

[1, 2, 3]

我怎么得到这个?

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

我对所有子集都感兴趣。具体长度的子集,参考以下问题:

  • 查找大小为 n: 1 , 2 的子集
  • 查找大小 > 1: 1 的子集
6个回答

这是另一种非常优雅的解决方案,没有循环或递归,仅使用 map 和 reduce 数组本机函数。

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

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

如果将 [value,...set] 反转为 [...set,value],那么它也会保留顺序。
2021-03-13 10:40:18
这种方法很哽咽。不记忆友好。
2021-03-31 10:40:18
这种方法仅使用原生 JavaScript 方法。如果您发现这很神奇,您应该学习 JavaScript 的基础知识,例如reduce()和,map()而不是将您的误解归咎于非常逻辑和理解的解决方案。
2021-04-01 10:40:18
这种方法内存效率不高
2021-04-03 10:40:18
反模式的一个很好的例子。很短!== 很优雅。这种方法的问题正是它听起来的样子 - 没有循环,没有箍,没有递归可看。这一切都由引擎在后台完成,因此您可以看到魔术。而且只有魔法。并且不明白在后台发生了什么。如果一切都写得清楚,可读,易于理解,那就太好了。简洁可能是姐妹或才能,但不是理解的朋友。
2021-04-05 10:40:18

我们可以为输入数组的一个子集解决这个问题,从 开始offset然后我们递归回来得到一个完整的解决方案。

使用生成器函数允许我们迭代具有恒定内存使用量的子集:

// 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); 
}

运行时复杂度与解决方案的数量 (2ⁿ) 乘以每个解决方案的平均长度 (n/2) = O(n2ⁿ) 成正比

是的,我明白你的观点@le_m。我想我只是想知道是否可以递归使用生成器并仍然实现恒定的内存使用?对于这个特定问题可能不可能,但我知道 ES6 引入了尾调用优化。通过这种优化,诸如生成斐波那契数列之类的问题可以实现恒定的内存使用,而不管递归的次数(我称之为O(1) memory growth)。似乎生成器不会以与尾调用函数相同的方式进行优化。参见:benignbemine.github.io/2015/07/19/es6-tail-calls
2021-03-26 10:40:18
“生成器是可以退出并稍后重新进入的函数。它们的上下文(变量绑定)将在重新进入时保存。” -递归深度由阵列长度的限制,并且每个递归推动上的当前子集的一个元素,直到它完成-因此对于每次迭代存储器使用for (let subset of subsets(array))通过的长度的约束array,假设Ñ而不是2 ^ N这将是如果我们首先构建所有子集然后迭代它们的情况。
2021-03-28 10:40:18
根据我的理解,似乎这种使用生成器的实现随着O(n)内存增长而增加。根据我的性能测试,在生成器(即:)的第一次迭代中,.next()我们从0...n. O(1)内存增长而增长的尾递归不同,这似乎增长O(n)......顺便说一句 - 我希望我在这个分析中是错误的,因为生成器方法似乎是一种优雅的方法。(PS:我找到的一篇优质文章:medium.com/javascript-scene/...
2021-04-06 10:40:18
@ShaheenGhiassy 我不明白“随着O(1)内存增长而增长”-内存复杂性不可能是O(1)因为每个子集都已经占用了O(n)空间。O(n)内存增长增加”是什么意思- 总空间复杂度在 O(n) 中,其中 n 是数组长度?
2021-04-06 10:40:18
很好用的生成器功能!您能否指出一篇文章,该文章更详细地介绍了使用constant memory usage?
2021-04-08 10:40:18

另一个简单的解决方案。

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; }

研究此代码帮助我可视化此解决方案的策略及其工作原理。很好的答案!
2021-03-23 10:40:18
只是好奇:目的是.as-console-wrapper什么?
2021-03-30 10:40:18
它产生更大的控制台输出。
2021-03-31 10:40:18

您可以使用如下所示的方法轻松地从数组生成 powerset:

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));

在函数的整个主循环中,子集被创建,然后被推送到result数组中。

我试图真正理解这个答案的直觉。我得到一个概念,即二进制字符串中的 1 映射到一个元素是否包含在子集中。例如,['a','b','c'], 101 => ['a', 'c']。我还知道一个 powerset 有 2^n 个元素。没有直观地了解到它是如何优雅地联系在一起的。FWIW,在 jsperf 上,1 << j 比 Math.pow(2,j) 快 10 倍。
2021-03-15 10:40:18
一个可靠的非递归解决方案。result.push(subset)将循环增量提升到循环体可能会提高可读性。如果您已经在使用按位运算:Math.pow(2, j) == 1 << j
2021-03-16 10:40:18

没有递归的简单解决方案:

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;
}


它是如何工作的?

如果我们有一些从输入数字生成的子集,并且我们想在我们的输入数组中再添加一个数字,这意味着我们可以获取所有已经存在的子集,并通过将新数字附加到每个现有子集来生成新的子集。


这是一个例子 [1, 2, 3]

  • 从一个空子集开始: []

  • 通过向每个现有子集添加“1”来创建新子集。这将是:[] [1]

  • 通过向每个现有子集添加“2”来创建新子集。这将是:[], [1] [2], [1, 2]

  • 通过向每个现有子集添加“3”来创建新子集。这将是:[], [1], [2], [1, 2] [3], [1, 3], [2, 3], [1, 2, 3]

很好的解决方案,谢谢!顺便说一句,如果您想要没有第一个元素的组合,只需 return subsets.slice(1)
2021-04-09 10:40:18