在Java中获取一个集合的幂集

86

{1, 2, 3}的幂集是:

{{}, {2}, {3}, {2, 3}, {1, 2}, {1, 3}, {1, 2, 3}, {1}}

假设我有一个Java中的Set

Set<Integer> mySet = new HashSet<Integer>();
mySet.add(1);
mySet.add(2);
mySet.add(3);
Set<Set<Integer>> powerSet = getPowerset(mySet);

如何编写复杂度最佳的getPowerset函数?(我认为它可能是O(2^n))


7
假设您有一组配置,比如说“A”、“B”和“C”,可以用来参数化一个模型,并且您想知道哪个子集能够产生最佳结果,例如只有“A”。可能的解决方法是测试幂集中的每个成员。 - João Silva
7
这是谷歌面试中给软件开发人员的一个问题。这是一个刻意设计的问题,旨在测试您思维的敏捷性。 - Eric Leschinski
这是一个合理的问题。例如,要实现cribbage的评分函数,您必须测试幂集中的任何元素是否相加为15。 - John Henckel
28个回答

103

是的,它确实是 O(2^n),因为您需要生成 2^n 种可能的组合。这里有一个使用泛型和集合的工作实现:

public static <T> Set<Set<T>> powerSet(Set<T> originalSet) {
    Set<Set<T>> sets = new HashSet<Set<T>>();
    if (originalSet.isEmpty()) {
        sets.add(new HashSet<T>());
        return sets;
    }
    List<T> list = new ArrayList<T>(originalSet);
    T head = list.get(0);
    Set<T> rest = new HashSet<T>(list.subList(1, list.size())); 
    for (Set<T> set : powerSet(rest)) {
        Set<T> newSet = new HashSet<T>();
        newSet.add(head);
        newSet.addAll(set);
        sets.add(newSet);
        sets.add(set);
    }       
    return sets;
}  

根据您的示例输入,进行一次测试:

 Set<Integer> mySet = new HashSet<Integer>();
 mySet.add(1);
 mySet.add(2);
 mySet.add(3);
 for (Set<Integer> s : SetUtils.powerSet(mySet)) {
     System.out.println(s);
 }

1
@CosminVacaroiu ...它还能做什么呢? - user253751
4
你确定它是O(2^n)吗?这是幂集中集合的数量,但每个集合都必须在内存中创建,这需要至少与集合大小成比例的时间。根据Wolfram Alpha,它是O(n * 2^n)Wolfram Alpha查询 - fabian
1
如果集合的大小达到10^5,这个方法仍然有效吗? - bane19
1
@GauravShankar 2^100=2^(10^2)已经大于10^30。无论您使用哪个图灵机计算,您都不会见证计算的完成。 - Kalle Richter
1
这是非常优美的Java。感谢您的回复! - Manuel Araoz
显示剩余6条评论

29

实际上,我已经编写了 O(1) 的代码来完成你所要求的操作。问题是你接下来打算对 Set 做什么操作。如果你只是在调用 size(),那就是 O(1),但如果你在迭代它,那显然是 O(2^n)

contains()O(n),等等。

你真的需要这个吗?

编辑:

这个代码现在可以通过 Guava 中的方法 Sets.powerSet(set) 进行调用,具体的代码可参考 Guava 的 GitHub 仓库


12

这里有一个解决方案,我使用了生成器,优点是整个幂集不会一次性存储...因此,您可以逐个迭代它,而无需将其存储在内存中。我认为这是更好的选择...请注意,复杂度相同,为O(2^n),但内存要求降低了(假设垃圾收集器表现良好! ;))

/**
 *
 */
package org.mechaevil.util.Algorithms;

import java.util.BitSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author st0le
 *
 */
public class PowerSet<E> implements Iterator<Set<E>>,Iterable<Set<E>>{
    private E[] arr = null;
    private BitSet bset = null;

    @SuppressWarnings("unchecked")
    public PowerSet(Set<E> set)
    {
        arr = (E[])set.toArray();
        bset = new BitSet(arr.length + 1);
    }

    @Override
    public boolean hasNext() {
        return !bset.get(arr.length);
    }

    @Override
    public Set<E> next() {
        Set<E> returnSet = new TreeSet<E>();
        for(int i = 0; i < arr.length; i++)
        {
            if(bset.get(i))
                returnSet.add(arr[i]);
        }
        //increment bset
        for(int i = 0; i < bset.size(); i++)
        {
            if(!bset.get(i))
            {
                bset.set(i);
                break;
            }else
                bset.clear(i);
        }

        return returnSet;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Not Supported!");
    }

    @Override
    public Iterator<Set<E>> iterator() {
        return this;
    }

}

调用它时,请使用以下模式:

        Set<Character> set = new TreeSet<Character> ();
        for(int i = 0; i < 5; i++)
            set.add((char) (i + 'A'));

        PowerSet<Character> pset = new PowerSet<Character>(set);
        for(Set<Character> s:pset)
        {
            System.out.println(s);
        }

这是从我的Project Euler库中获取的... :)


Guava的工作方式与这个很相似,但限制为32个元素。这并不是不合理的,因为2 ** 32可能是太多的迭代次数。它使用的内存比你的更少,因为它只在需要时生成AbstractSet。尝试将您的代码与Guava进行比较,其中您仅在10,000个元素中打印1个,并创建一个大示例。我敢打赌Guava会更快。 - Eyal
@Eyal,我相信它确实如此,我从未声称过其他。这是我自己编写的,不适用于生产代码。这只是算法练习。 - st0le
1
小提醒:你的 'returnSet' 是一个 TreeSet,它要求其项是可比较的。这可能不是情况。考虑将其替换为 HashSet 或 LinkedHashSet。 - Joris Kinable

10

假设 n < 63,这是一个合理的假设,因为你试图构建幂集(除非使用迭代器实现),否则会用尽内存。这是一种更简洁的方法。二进制操作比使用Math.pow()和数组来掩码更快,但Java用户似乎害怕使用它们...

List<T> list = new ArrayList<T>(originalSet);
int n = list.size();

Set<Set<T>> powerSet = new HashSet<Set<T>>();

for( long i = 0; i < (1 << n); i++) {
    Set<T> element = new HashSet<T>();
    for( int j = 0; j < n; j++ )
        if( (i >> j) % 2 == 1 ) element.add(list.get(j));
    powerSet.add(element); 
}

return powerSet;

在for循环中的终止条件应该是i < (2 << n - 1),而不是i < (1 << n - 1)。 - bazeusz
谢谢@bazeusz,我把它改成了i < (1 << n),这是等价的。 - Andrew Mao
由于使用了位运算,我认为可以使用((i >> j) &1) == 1而不是(i >> j) % 2 == 1。另外,long是有符号的,所以你认为检查溢出是否有意义? - Ravi Tiwari

9
这里有一个教程,描述了你想要的东西,包括代码。你是正确的,复杂度是O(2^n)。

点击这里查看教程。


3
复杂度不是(n*2^n)吗?因为二进制字符串的长度为n,在主循环的每次迭代中,我们都要遍历整个二进制字符串。 - Maggie
1
教程很好,但我在解决HackerRank问题时使用了这种技术:它只通过了一半的测试用例,另一半由于超时或运行时错误而失败。 - Eugenia Ozirna

7
我想到了另一种基于@Harry He的想法的解决方案。可能不是最优雅的,但我理解如下:
我们以经典的简单示例PowerSet of S P(S) = {{1},{2},{3}}为例。我们知道获取子集数量的公式是2^n (7 + 空子集)。对于这个例子,2^3 = 8个子集。
为了找到每个子集,我们需要将0-7十进制转换为二进制表示,如下面的转换表所示:
如果我们按行遍历表格,每行将产生一个子集,并且每个子集的值将来自已启用位的值。
Bin Value部分中的每列对应于原始输入集中的索引位置。
以下是我的代码:
public class PowerSet {

/**
 * @param args
 */
public static void main(String[] args) {
    PowerSet ps = new PowerSet();
    Set<Integer> set = new HashSet<Integer>();
    set.add(1);
    set.add(2);
    set.add(3);
    for (Set<Integer> s : ps.powerSet(set)) {
        System.out.println(s);
    }
}

public Set<Set<Integer>> powerSet(Set<Integer> originalSet) {
    // Original set size e.g. 3
    int size = originalSet.size();
    // Number of subsets 2^n, e.g 2^3 = 8
    int numberOfSubSets = (int) Math.pow(2, size);
    Set<Set<Integer>> sets = new HashSet<Set<Integer>>();
    ArrayList<Integer> originalList = new ArrayList<Integer>(originalSet);
    for (int i = 0; i < numberOfSubSets; i++) {
        // Get binary representation of this index e.g. 010 = 2 for n = 3
        String bin = getPaddedBinString(i, size);
        //Get sub-set
        Set<Integer> set = getSet(bin, originalList));
        sets.add(set);
    }
    return sets;
}

//Gets a sub-set based on the binary representation. E.g. for 010 where n = 3 it will bring a new Set with value 2
private Set<Integer> getSet(String bin, List<Integer> origValues){
    Set<Integer> result = new HashSet<Integer>();
    for(int i = bin.length()-1; i >= 0; i--){
        //Only get sub-sets where bool flag is on
        if(bin.charAt(i) == '1'){
            int val = origValues.get(i);
            result.add(val);
        }
    }
    return result;
}

//Converts an int to Bin and adds left padding to zero's based on size
private String getPaddedBinString(int i, int size) {
    String bin = Integer.toBinaryString(i);
    bin = String.format("%0" + size + "d", Integer.parseInt(bin));
    return bin;
}

}

5
如果您正在使用Eclipse Collections(前身为GS Collections),则可以在所有SetIterables上使用powerSet()方法。
MutableSet<Integer> set = UnifiedSet.newSetWith(1, 2, 3);
System.out.println("powerSet = " + set.powerSet());
// prints: powerSet = [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

注意: 我是 Eclipse Collections 的提交者。


你能分享并解释一下你的解决方案的代码吗? - Konrad Höffner
3
您可以在这里查看代码:https://github.com/goldmansachs/gs-collections/blob/2765876efc37cf0f6b450f17d3284398eb013a40/collections/src/main/java/com/gs/collections/impl/utility/internal/SetIterables.java#L136 - Craig P. Motlin

4

我希望你能提供一个比这里发布的解决方案更小巧的解决方法。此方案针对Java 7,因此需要为版本5和6粘贴一些代码。

Set<Set<Object>> powerSetofNodes(Set<Object> orig) {
    Set<Set<Object>> powerSet = new HashSet<>(),
        runSet = new HashSet<>(),
        thisSet = new HashSet<>();

    while (powerSet.size() < (Math.pow(2, orig.size())-1)) {
        if (powerSet.isEmpty()) {
            for (Object o : orig) {
                Set<Object> s = new TreeSet<>();
                s.add(o);
                runSet.add(s);
                powerSet.add(s);
            }
            continue;
        }
        for (Object o : orig) {
            for (Set<Object> s : runSet) {
                Set<Object> s2 = new TreeSet<>();
                s2.addAll(s);
                s2.add(o);
                powerSet.add(s2);
                thisSet.add(s2);
            }
        }
        runSet.clear();
        runSet.addAll(thisSet);
        thisSet.clear();
    }
    powerSet.add(new TreeSet());
    return powerSet;

这里有一些示例代码可供测试:
Set<Object> hs = new HashSet<>();
hs.add(1);
hs.add(2);
hs.add(3);
hs.add(4);
for(Set<Object> s : powerSetofNodes(hs)) {
    System.out.println(Arrays.toString(s.toArray()));
}

powerSetofNodes()函数末尾不是缺少一个“}”符号吗? - Peter Mortensen

4

这里有一个简单的迭代 O(2^n) 解决方案:

public static Set<Set<Integer>> powerSet(List<Integer> intList){

    Set<Set<Integer>> result = new HashSet();
    result.add(new HashSet());

    for (Integer i : intList){

        Set<Set<Integer>> temp = new HashSet();

        for(Set<Integer> intSet : result){

            intSet = new HashSet(intSet);
            intSet.add(i);                
            temp.add(intSet);
        }
        result.addAll(temp);
    }
    return result;
}

1
这个解决方案还使用了O(2^n)的空间,对于大型输入集来说太多了。最好遵循递归定义,在递归的位置使用堆栈或队列。 - rossb83

3
import java.util.Set;
import com.google.common.collect.*;

Set<Set<Integer>> sets = Sets.powerSet(ImmutableSet.of(1, 2, 3));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接