【问题标题】:Java - Finding all subsets of a String (powerset) recursivelyJava - 递归查找字符串(powerset)的所有子集
【发布时间】:2026-01-17 07:30:02
【问题描述】:

所以,我需要递归查找给定字符串的所有子集。到目前为止我所拥有的是:

static ArrayList<String> powerSet(String s){
        ArrayList<String> ps = new ArrayList<String>();
        ps.add(s);


        for(int i=0; i<s.length(); i++){
            String temp = s.replace(Character.toString(s.charAt(i)), "");
            ArrayList<String> ps2 = powerSet(temp);
            for(int j = 0; j < ps2.size(); j++){
                ps.add(ps2.get(j));
            }
        }   

        return ps;

我想我现在知道问题出在哪里,但我不知道如何解决它。目前,我找到了所有 temp 的幂集,它们是“bcd”、“acd”、“abd”、“abc”,它们会导致重复。有想法该怎么解决这个吗?

通过powerset,我的意思是如果字符串是abc,它将返回“”,“a”,“b”,“c”,“ab”,“ac”,“bc”,“abc”。

【问题讨论】:

  • 代码没有正确处理空字符串的边缘情况。所以,如果你用空字符串调用它,它会返回空字符串。
  • 我想我现在知道问题出在哪里,但我不知道如何解决它。目前,我找到了所有 temp 的幂集,它们是“bcd”、“acd”、“abd”、“abc”,它们会导致重复。有关如何解决此问题的任何想法?
  • 如果您想到算法遍历的树,那么有不止一种方法可以删除两个或多个字符(每个递归级别都会删除一个字符)。因此,当第一个 a 然后 b 是,当第一个 b 然后 a 被删除时,你会得到 c。
  • 我看到了问题,我只是不知道如何做不同的事情。是否有快速解决此问题的方法,还是我应该从头开始?

标签: java algorithm


【解决方案1】:

具有 n 个元素的集合的子集数为 2n。例如,如果我们有字符串“abc”,我们将有 2n = 23 = 8 个子集。

n位可以表示的状态数也是2n。我们可以证明,枚举 n 位的所有可能状态与具有 n 个元素的集合的所有可能子集之间存在对应关系:

   2 1 0   2 1 0
   c b a    bits
0          0 0 0
1      a   0 0 1
2    b     0 1 0
3    b a   0 1 1
4  c       1 0 0
5  c   a   1 0 1
6  c b     1 1 0 
7  c b a   1 1 1

如果我们考虑第 5 行,例如,第 2 位和第 0 位是活动的。如果我们做abc.charAt(0) + abc.charAt(2),我们会得到子集ac

要枚举 n 位的所有可能状态,我们从 0 开始,然后求和 1,直到达到 2n - 1。在这个解决方案中,我们将从 2n 开始- 1并递减直到0,所以我们不需要另一个参数来保持子集的数量,但效果是一样的:

static List<String> powerSet(String s) {
    // the number of subsets is 2^n
    long numSubsets = 1L << s.length();
    return powerSet(s, numSubsets - 1);
}

static List<String> powerSet(String s, long active) {
    if (active < 0) {
        // Recursion base case
        // All 2^n subsets were visited, stop here and return a new list
        return new ArrayList<>();
    }

    StringBuilder subset = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        // For each bit
        if (isSet(active, i)) {
            // If the bit is set, add the correspondent char to this subset
            subset.append(s.charAt(i));
        }
    }
    // Make the recursive call, decrementing active to the next state,
    // and get the returning list
    List<String> subsets = powerSet(s, active - 1);
    // Add this subset to the list of subsets
    subsets.add(subset.toString());
    return subsets;
}

static boolean isSet(long bits, int i) {
    // return true if the ith bit is set
    return (bits & (1L << i)) != 0;
}

那么你只需要调用它:

System.out.println(powerSet("abc"));

并得到所有 8 个子集:

[, a, b, ab, c, ac, bc, abc]

【讨论】:

    【解决方案2】:

    有一种方法可以在不使用递归的情况下做到这一点,它依赖于位串和子集之间的简单对应。

    因此,假设您有一个三个字符串“abc”,那么,正如您所指出的,子集将是“”、“c”、“b”、“bc”、“a”、“ac”、“ ab", "abc"

    如果您制作一个字符表并为子集中的每个字符写一个 1,为不在子集中的每个字符写一个 0,您可以看到一个模式:

        a b c   bits    decimal
                0 0 0   0
            c   0 0 1   1
          b     0 1 0   2
          b c   0 1 1   3
        a       1 0 0   4
        a   c   1 0 1   5
        a b     1 1 0   6
        a b c   1 1 1   7
    

    对于每个长度为 n 的唯一字符字符串,您将有 2n 个子集,您可以通过简单地从 i=0 到 i=2 进行一个 for 循环来生成它们n-1,并且只包括那些与i 中为 1 的位相对应的字符。

    我写了一个Java示例here和一个C示例here

    【讨论】:

    • 使用此方法,您还可以轻松地遍历子集。有史以来最好的解决方案!谢谢你拯救了我的一天!
    • 最优雅且易于理解的方法。非常感谢
    【解决方案3】:

    我发现在设计递归算法时首先考虑简单的极端情况很有帮助,即空字符串和一个字符的字符串。然后你通常会拆分问题并对字符串的其余/尾部进行递归调用。有点像这样:

        static List<String> nuPowerSet(String s) {
    
        if (s.length() == 0) { // trivial, subset of empty string is empty
            return emptyList();
        }
    
        String head = s.substring(0, 1);
        if (s.length() ==1) // the subset of a one character string is exactly that character
            return asList(head);
    
        String tail = s.substring(1);
    
        ArrayList<String> ps = new ArrayList<String>();
    
        ps.add(head); // one of the subsets is the current first character
    
    
        List<String> tailSubsets = nuPowerSet(tail); // all the subsets of the remainder.
    
        List<String> tailSubsetsWithCurrentHeadPrepended = tailSubsets
                .stream()
                .map(element -> head + element) 
                .collect(Collectors.toList());
    
        ps.addAll(tailSubsets);
        ps.addAll(tailSubsetsWithCurrentHeadPrepended);
    
        return ps;
    }
    

    【讨论】:

      【解决方案4】:

      要消除重复,您只需将它们全部添加到Set 中,这可以通过某种助手轻松完成:

      static ArrayList<String> powerSet(String s) {
          return new ArrayList<>(_powerSet(s));
      }
      
      static HashSet<String> _powerSet(String s) {
          HashSet<String> set = new HashSet<>();
          set.add(s);
      
          for(int i = 0; i < s.length(); i++) {
              String tmp = s.substring(0, i) + s.substring(i+1, s.length());
              set.addAll(_powerSet(tmp));
          }
      
          return set;
      }
      

      顺便说一句,您的代码本质上已经处理了边缘情况。你不必担心这个。

      【讨论】:

      • 在字符串 "abc" 上运行代码我得到 *Error...
      • @alfasin 因为它是递归的。它不适用于太大的数据集。您可以尝试另一种无堆栈的语言,也可以使用技术实现它。
      • 调试你的代码,set.addAll(_powerSet(tmp); 正在创建一个无限递归调用!
      • @alfasin 忘记了这个。它循环是因为s.substring(i, s.length()) 所以问题的大小没有减少。我把它改成了s.substring(i+1, s.length())。这应该可以解决它。
      【解决方案5】:

      你是对的,你确实有重复,因为你正在创建 temp 多次(每次都没有另一个字符)所以当你递归调用时,会有不同的子集共享相同的字符并创建重复。例如,“abc”将创建一个temp,其中包含:[“ab”、“ac”、“bc”],它们中的每一个都将递归调用只有一个字符,所以你会得到每个“a” , "b" 和 "c" 两次。

      避免这种情况的一种方法(只需很少的更改)是使用 Set 而不是列表 - 这将省略所有重复:

      static Set<String> powerSet(String s) {
          Set<String> ps = new HashSet<>();
          ps.add(s);
      
          for (int i = 0; i < s.length(); i++) {
              String temp = s.replace(Character.toString(s.charAt(i)), "");
              Set<String> ps2 = powerSet(temp);
              for (String x : ps2) {
                  ps.add(x);
              }
          }
          return ps;
      }
      

      现在输出将是:

      bc
      a
      ab
      b
      ac
      abc
      c
      

      另一种解决方案:

      public static List<String> powerset(String s) {
          List<String> ans = new LinkedList<>();
          if (null == s) {
              return ans;
          }
          return powerset(s, ans);
      }
      
      private static List<String> powerset(String s, List<String> ans) {
          if ("".equals(s)) {
              return ans;
          }
          String first = s.substring(0, 1);
          String rest = s.substring(1);
          ans.add(first);
          List<String> pAns = new LinkedList<>(ans);
          for (String partial : ans.subList(0, ans.size()-1)) {
              pAns.add(partial + first);
          }
          return powerset(rest, pAns);
      }
      

      输出

      [a, b, ab, c, ac, bc, abc]
      

      【讨论】:

        【解决方案6】:
        import java.util.ArrayList;
            import java.util.Arrays;
            import java.util.List;
        
            public class MainClass {
        
                static List<List<char[]>> list = new ArrayList<List<char[]>>();
        
                // static List<int[]> list1 = new ArrayList<int[]>();
                public static void main(String[] args) {
                    List<char[]> list1 = new ArrayList<char[]>();
                    String string = "abcd";
                    char[] a = string.toCharArray();
                    generate(a, 0, 0, list1);
        
                    for (List<char[]> l : list) {
                        for (char[] b : l) {
                            for (char c : b) {
                                System.out.print(c + ",");
                            }
                            System.out.println();
                        }
                    }
        
                }
        
                public static void generate(char[] array, int offset, int index, List<char[]> list1) {
                    if (offset >= array.length)
                        return;
                    char[] newArray = Arrays.copyOfRange(array, offset, index);
                    list1.add(newArray);
                    if (index >= array.length) {
                        list.add(list1);
                        offset++;
                        index = offset;
                        generate(array, offset, index, new ArrayList<char[]>());
                    } else {
                        index++;
                        generate(array, offset, index, list1);
                    }
                }
            }
        

        【讨论】: