在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个回答

3
上述一些解决方案在集合大小很大时会出现问题,因为它们会创建大量需要收集的对象垃圾和需要复制数据。我们该如何避免这种情况呢?我们可以利用我们知道结果集大小的事实(2^n),预先分配一个相应大小的数组,然后只需将其附加到结尾,而不是复制。
随着n的增长,加速效果迅速增长。我将其与上面的João Silva解决方案进行了比较。在我的机器上(所有测量都是近似值),当n=13时快5倍,n=14时快7倍,n=15时快12倍,n=16时快25倍,n=17时快75倍,n=18时快140倍。因此,垃圾创建/收集和复制在否则看起来相似的大O解决方案中占据主导地位。
从一开始就预分配数组似乎比让其动态增长更优。当n=18时,动态增长总时间大约需要两倍。
public static <T> List<List<T>> powerSet(List<T> originalSet) {
    // result size will be 2^n, where n=size(originalset)
    // good to initialize the array size to avoid dynamic growing
    int resultSize = (int) Math.pow(2, originalSet.size());
    // resultPowerSet is what we will return
    List<List<T>> resultPowerSet = new ArrayList<List<T>>(resultSize);

    // Initialize result with the empty set, which powersets contain by definition
    resultPowerSet.add(new ArrayList<T>(0)); 

    // for every item in the original list
    for (T itemFromOriginalSet : originalSet) {

        // iterate through the existing powerset result
        // loop through subset and append to the resultPowerset as we go
        // must remember size at the beginning, before we append new elements
        int startingResultSize = resultPowerSet.size();
        for (int i=0; i<startingResultSize; i++) {
            // start with an existing element of the powerset
            List<T> oldSubset = resultPowerSet.get(i);

            // create a new element by adding a new item from the original list
            List<T> newSubset = new ArrayList<T>(oldSubset);
            newSubset.add(itemFromOriginalSet);

            // add this element to the result powerset (past startingResultSize)
            resultPowerSet.add(newSubset);
        }
    }
    return resultPowerSet;
}

3
以下解决方案摘自我的书 "Coding Interviews: Questions, Analysis & Solutions":
在数组中选择一些整数来组成一个组合。使用一组位,其中每个位代表数组中的一个整数。如果第i个字符被选中组成组合,则第i个位是1;否则为0。例如,在数组[1,2,3]的组合中使用了三个位。如果选择前两个整数1和2组成组合[1,2],相应的位是{1,1,0}。类似地,另一个组合[1,3]对应的位是{1,0,1}。如果我们可以得到所有可能的n位组合,我们就能获得长度为n的数组的所有组合。
一个数字由一组比特位组成。所有可能的 n 位组合对应于从 1 到 2^n-1 的数字。因此,在 1 和 2^n-1 之间的每个数字都对应于具有长度 n 的数组的组合。例如,数字 6 由比特位 {1, 1, 0} 组成,因此在数组 [1, 2, 3] 中选择第一和第二个字符以生成组合 [1, 2]。类似地,具有比特位 {1, 0, 1} 的数字 5 对应于组合 [1, 3]。
实现此解决方案的Java代码如下所示:
public static ArrayList<ArrayList<Integer>> powerSet(int[] numbers) {
    ArrayList<ArrayList<Integer>> combinations = new ArrayList<ArrayList<Integer>>(); 
    BitSet bits = new BitSet(numbers.length);
    do{
        combinations.add(getCombination(numbers, bits));
    }while(increment(bits, numbers.length));

    return combinations;
}

private static boolean increment(BitSet bits, int length) {
    int index = length - 1;

    while(index >= 0 && bits.get(index)) {
        bits.clear(index);
        --index;
    }

    if(index < 0)
        return false;

    bits.set(index);
    return true;
}

private static ArrayList<Integer> getCombination(int[] numbers, BitSet bits){
    ArrayList<Integer> combination = new ArrayList<Integer>();
    for(int i = 0; i < numbers.length; ++i) {
        if(bits.get(i))
            combination.add(numbers[i]);
    }

    return combination;
}

方法increment会增加一组位表示的数字。该算法会从最右边的位开始清除1位,直到找到0位。然后将最右边的0位设置为1。例如,为了使用位{1,0,1}增加数字5,它会从右侧清除1位,并将最右边的0位设置为1。数字6的位变为{1,1,0},这是将5增加1的结果。


我修改了两件事情:在getCombination中循环,而不是使用numbers.length(或bits.size()),可以迭代到bits.length(),这样可以稍微加快生成速度。最后,我按大小对子集进行了排序,因为我的问题需要这样做。 - BoLe

1
这是我的递归解决方案,可以使用Java泛型获取任何集合的幂集。其主要思想是将输入数组的头部与剩余数组的所有可能解决方案组合起来,具体如下。
import java.util.LinkedHashSet;
import java.util.Set;

public class SetUtil {
    private static<T>  Set<Set<T>> combine(T head, Set<Set<T>> set) {
        Set<Set<T>> all = new LinkedHashSet<>();

        for (Set<T> currentSet : set) {
            Set<T> outputSet = new LinkedHashSet<>();

            outputSet.add(head);
            outputSet.addAll(currentSet);

            all.add(outputSet);
        }

        all.addAll(set);        

        return all;
    }

    //Assuming that T[] is an array with no repeated elements ...
    public static<T> Set<Set<T>> powerSet(T[] input) {
        if (input.length == 0) {
            Set <Set<T>>emptySet = new LinkedHashSet<>();

            emptySet.add(new LinkedHashSet<T>());

            return emptySet;
        }

        T head = input[0];
        T[] newInputSet = (T[]) new Object[input.length - 1];

        for (int i = 1; i < input.length; ++i) {
            newInputSet[i - 1] = input[i];
        }

        Set<Set<T>> all = combine(head, powerSet(newInputSet));

        return all;
    }

    public static void main(String[] args) {            
        Set<Set<Integer>> set = SetUtil.powerSet(new Integer[] {1, 2, 3, 4, 5, 6});

        System.out.println(set);
    }
}

这将输出:
[[1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5], [1, 2, 3, 4, 6], [1, 2, 3, 4], [1, 2, 3, 5, 6], [1, 2, 3, 5], [1, 2, 3, 6], [1, 2, 3], [1, 2, 4, 5, 6], [1, 2, 4, 5], [1, 2, 4, 6], [1, 2, 4], [1, 2, 5, 6], [1, 2, 5], [1, 2, 6], [1, 2], [1, 3, 4, 5, 6], [1, 3, 4, 5], [1, 3, 4, 6], [1, 3, 4], [1, 3, 5, 6], [1, 3, 5], [1, 3, 6], [1, 3], [1, 4, 5, 6], [1, 4, 5], [1, 4, 6], [1, 4], [1, 5, 6], [1, 5], [1, 6], [1], [2, 3, 4, 5, 6], [2, 3, 4, 5], [2, 3, 4, 6], [2, 3, 4], [2, 3, 5, 6], [2, 3, 5], [2, 3, 6], [2, 3], [2, 4, 5, 6], [2, 4, 5], [2, 4, 6], [2, 4], [2, 5, 6], [2, 5], [2, 6], [2], [3, 4, 5, 6], [3, 4, 5], [3, 4, 6], [3, 4], [3, 5, 6], [3, 5], [3, 6], [3], [4, 5, 6], [4, 5], [4, 6], [4], [5, 6], [5], [6], []]

1
另一个样例实现:

 public static void main(String args[])
    {
        int[] arr = new int[]{1,2,3,4};
        // Assuming that number of sets are in integer range
        int totalSets = (int)Math.pow(2,arr.length);
        for(int i=0;i<totalSets;i++)
        {
            String binaryRep = Integer.toBinaryString(i);      
            for(int j=0;j<binaryRep.length();j++)
            {
                int index=binaryRep.length()-1-j;
                if(binaryRep.charAt(index)=='1')
                System.out.print(arr[j] +" ");       
            }
            System.out.println();
        }
    }

1

这是我的lambda方法。

public static <T> Set<Set<T>> powerSet(T[] set) {
      return IntStream
            .range(0, (int) Math.pow(2, set.length))
            .parallel() //performance improvement
            .mapToObj(e -> IntStream.range(0, set.length).filter(i -> (e & (0b1 << i)) != 0).mapToObj(i -> set[i]).collect(Collectors.toSet()))
            .map(Function.identity())
            .collect(Collectors.toSet());
        }

或者并行执行(参见parallel()注释):

输入集大小:18

逻辑处理器:8个,主频3.4GHz

性能提升:30%


1
一种不使用递归的方法是:使用二进制掩码并生成所有可能的组合。
public HashSet<HashSet> createPowerSet(Object[] array)
{
    HashSet<HashSet> powerSet=new HashSet();
    boolean[] mask= new boolean[array.length];

    for(int i=0;i<Math.pow(2, array.length);i++)
    {
        HashSet set=new HashSet();
        for(int j=0;j<mask.length;j++)
        {
            if(mask[i])
                set.add(array[j]);
        }
        powerSet.add(set);      

        increaseMask(mask);
    }

    return powerSet;
}

public void increaseMask(boolean[] mask)
{
    boolean carry=false;

    if(mask[0])
        {
            mask[0]=false;
            carry=true;
        }
    else
        mask[0]=true;

    for(int i=1;i<mask.length;i++)
    {
        if(mask[i]==true && carry==true)
        mask[i]=false;
        else if (mask[i]==false && carry==true)
        {
            mask[i]=true;
            carry=false;
        }
        else 
            break;

    }

}

1
如果S是一个有N个元素的有限集合,那么S的幂集包含2^N个元素。简单枚举幂集元素的时间是2^N,因此O(2^N)是急切地构建幂集的时间复杂度的下限。
简而言之,任何涉及创建幂集的计算都不会在大量N值的情况下扩展。除了避免创建幂集的需要外,没有聪明的算法能帮助你...

1

算法:

输入:Set[],set_size 1. 获取幂集的大小 powet_set_size = pow(2, set_size) 2. 循环计数器从0到pow_set_size (a) 循环i从0到set_size (i) 如果计数器中第i位被设置 打印该子集中的第i个元素 (b) 打印子集分隔符,即换行

#include <stdio.h>
#include <math.h>
 
void printPowerSet(char *set, int set_size)
{
    /*set_size of power set of a set with set_size
      n is (2**n -1)*/
    unsigned int pow_set_size = pow(2, set_size);
    int counter, j;
 
    /*Run from counter 000..0 to 111..1*/
    for(counter = 0; counter < pow_set_size; counter++)
    {
      for(j = 0; j < set_size; j++)
       {
          /* Check if jth bit in the counter is set
             If set then pront jth element from set */
          if(counter & (1<<j))
            printf("%c", set[j]);
       }
       printf("\n");
    }
}
 
/*Driver program to test printPowerSet*/
int main()
{
    char set[] = {'a','b','c'};
    printPowerSet(set, 3);
 
    getchar();
    return 0;
}


0

这里是生成幂集的方法。首先将S[0]作为第一个元素,将剩下的元素组成较小的集合S[1,...n]

计算较小集合的所有子集并将它们放入allsubsets中。

对于allsubsets中的每个子集,克隆它并将first添加到子集中。

ArrayList<ArrayList<Integer>> getSubsets(ArrayList<Integer> set, int index){
    ArrayList<ArrayList<Integer>> allsubsets;
    if(set.size() == index){
        allsubsets = new ArrayList<ArrayList<Integer>>();
        allsubsets.add(new ArrayList<Integer>()); // the empty set 
    }else{
        allsubsets = getSubsets(set, index+1);
        int item = set.get(index);

        ArrayList<ArrayList<Integer>> moresubsets = new ArrayList<ArrayList<Integer>>();

        for(ArrayList<Integer> subset: allsubsets){
            ArrayList<Integer> newsubset = new ArrayList<Integer>();

            newsubset.addAll(subset);
            newsubset.add(item);
            moresubsets.add(newsubset);

        }

        moresubsets.addAll(moresubsets);

    }

    return allsubsets;
}

0

t 的子集是指通过删除 t 中零个或多个元素而制成的任何集合。missingFirst 子集添加了那些缺少第一个元素的 t 子集,for 循环将处理添加带有第一个元素的子集。例如,如果 t 包含元素 ["1", "2", "3"],missingFirst 将添加 [[""],["2"],["3"],["2","3"]],for 循环将把 "1" 放在这些元素的前面并添加到 newSet 中。因此,我们最终会得到 [[""],["1"],["2"],["3"],["1","2"],["1","3"],["2","3"],["1","2","3"]]。

public static Set<Set<String>> allSubsets(Set<String> t) {
        Set<Set<String>> powerSet = new TreeSet<>();
        if(t.isEmpty()) {
            powerSet.add(new TreeSet<>());
            return powerSet;
        }
        String first = t.get(0);
        Set<Set<String>> withoutFirst = allSubsets(t.subSet(1, t.size()));
        for (List<String> 1st : withoutFirst) {
            Set<String> newSet = new TreeSet<>();
            newSet.add(first);
            newSet.addAll(lst);
            powerSet.add(newSet);
        }
        powerSet.addAll(withoutFirst);
        return powerSet;
    }

请考虑在您提供的代码旁边添加一个简短的解释。 - Mirza Sisic
这甚至无法编译,似乎是用某种幻想的Java版本编写的。Set没有带有索引的get方法,也没有subSet方法;1st不是有效的标识符(我想应该是指lst)。将所有的集合更改为列表,它就可以几乎编译了... - john16384

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