【问题标题】:Generate all combinations with unknown number of slots生成所有槽数未知的组合
【发布时间】:2018-06-06 02:22:13
【问题描述】:

我有一个充满字符串的文本文件,每行一个。其中一些字符串将包含未知数量的“@”字符。每个“@”可以代表数字 1、2、3 或 4。我想为每个“@”生成所有可能的字符串组合(排列?)。如果每个字符串有一定数量的“@”,我只会使用嵌套的 for 循环(快速而肮脏)。我需要帮助找到一种更优雅的方式来处理未知数量的“@”。

示例1:输入字符串为a@bc

输出字符串为:

a1bc
a2bc
a3bc
a4bc

示例2:输入字符串为a@bc@d

输出字符串为:

a1bc1d
a1bc2d
a1bc3d
a1bc4d
a2bc1d
a2bc2d
a2bc3d
...
a4bc3d
a4bc4d

有人可以帮忙吗?我正在使用 C#。

【问题讨论】:

    标签: c# combinations


    【解决方案1】:

    这实际上是一个递归函数的好地方。我不会写 C#,但我会创建一个函数 List<String> expand(String str),它接受一个字符串并返回一个包含扩展字符串的数组。

    expand 然后可以搜索字符串以找到第一个 @ 并创建一个包含字符串的第一部分 + 扩展的列表。然后,它可以在字符串的最后一部分调用expand,并将其扩展中的每个元素添加到最后一部分扩展中的每个元素中。

    使用 Java ArrayLists 的示例实现:

     ArrayList<String> expand(String str) {
    
        /* Find the first "@" */
        int i = str.indexOf("@");
    
        ArrayList<String> expansion = new ArrayList<String>(4);
        /* If the string doesn't have any "@" */
        if(i < 0) {
            expansion.add(str);
            return expansion;
        }
    
        /* New list to hold the result */
        ArrayList<String> result = new ArrayList<String>();
    
        /* Expand the "@" */
        for(int j = 1; j <= 4; j++)
            expansion.add(str.substring(0,i-1) + j);
    
        /* Combine every expansion with every suffix expansion */
        for(String a : expand(str.substring(i+1)))
            for(String b : expansion)
                result.add(b + a);
        return result;
    }
    

    【讨论】:

      【解决方案2】:

      我在这里为您提供解决手头问题的极简方法。 是的,就像其他人所说的那样,递归是要走的路。

      递归非常适合这里,因为我们可以通过为输入的一小部分提供解决方案来解决这个问题,然后从另一部分重新开始,直到我们完成并合并结果。

      每个递归都必须有一个停止条件——这意味着不再需要递归。

      这里我的停止条件是字符串中不再有"@"。 我使用字符串作为我的一组值 (1234),因为它是一个 IEnumerable&lt;char&gt;

      这里的所有其他解决方案都很棒,只是想向您展示一个简短的方法。

      internal static IEnumerable<string> GetStrings(string input)
      {
          var values = "1234";
          var permutations = new List<string>();
      
           var index = input.IndexOf('@');
           if (index == -1) return new []{ input };
      
           for (int i = 0; i < values.Length; i++)
           {
               var newInput = input.Substring(0, index) + values[i] + input.Substring(index + 1);
               permutations.AddRange(GetStrings(newInput));
           }
      
           return permutations;
      }
      

      使用 LINQ 的更短、更简洁的方法:

      internal static IEnumerable<string> GetStrings(string input)
      {
        var values = "1234";
      
        var index = input.IndexOf('@');
        if (index == -1) return new []{ input };
      
        return 
            values
            .Select(ReplaceFirstWildCardWithValue)
            .SelectMany(GetStrings);
      
        string ReplaceFirstWildCardWithValue(char value) => input.Substring(0, index) + value + input.Substring(index + 1);
      }
      

      【讨论】:

        【解决方案3】:

        这是在大声呼喊递归解决方案。

        首先,让我们创建一个方法,从给定的一组值生成一定长度的所有组合。因为我们只对生成字符串感兴趣,所以让我们利用string 是不可变的这一事实(参见 P.D.2);这使得递归函数更容易实现和推理:

        static IEnumerable<string> GetAllCombinations<T>(
            ISet<T> set, int length)
        {
            IEnumerable<string> getCombinations(string current)
            {
                if (current.Length == length)
                {
                    yield return current;
                }
                else
                {
                    foreach (var s in set)
                    {
                        foreach (var c in getCombinations(current + s))
                        {
                            yield return c;
                        }
                    }
                }
            }
        
            return getCombinations(string.Empty);
        }
        

        仔细研究这种方法的工作原理。用手算出一些小例子来理解它。

        现在,一旦我们知道如何生成所有可能的组合,构建字符串就很容易了:

        1. 找出指定字符串中通配符的数量:这将是我们的组合长度。
        2. 对于每个组合,将遇到通配符的每个字符按顺序插入到字符串中。

        好的,让我们这样做:

        public static IEnumerable<string> GenerateCombinations<T>(
            this string s,
            IEnumerable<T> set,
            char wildcard)
        {
            var length = s.Count(c => c == wildcard);
            var combinations = GetAllCombinations(set, length);
            var builder = new StringBuilder();
        
            foreach (var combination in combinations)
            {
                var index = 0;
        
                foreach (var c in s)
                {
                    if (c == wildcard)
                    {
                        builder.Append(combination[index]);
                        index += 1;
                    }
                    else
                    {
                        builder.Append(c);
                    }
                }
        
                yield return builder.ToString();
                builder.Clear();
            }
        }
        

        我们完成了。用法是:

        var set = new HashSet<int>(new[] { 1, 2, 3, 4 });
        Console.WriteLine(
            string.Join("; ", "a@bc@d".GenerateCombinations(set, '@')));
        

        果然,输出是:

        a1bc1d; a1bc2d; a1bc3d; a1bc4d; a2bc1d; a2bc2d; a2bc3d; 
        a2bc4d; a3bc1d; a3bc2d; a3bc3d; a3bc4d; a4bc1d; a4bc2d;
        a4bc3d; a4bc4d
        

        这是最高效的实现吗?可能不是,但它的可读性和可维护性。除非您有未达到的特定性能目标,否则请编写有效且易于理解的代码。

        P.D.我省略了所有错误处理和参数验证。

        P.D.2:如果组合的长度很大,在GetAllCombinations 中连接字符串可能不是一个好主意。在这种情况下,我会让GetAllCombinations 返回一个IEnumerable&lt;IEnumerable&lt;T&gt;&gt;,实现一个简单的ImmutableStack&lt;T&gt;,并将其用作组合缓冲区而不是string

        【讨论】:

          猜你喜欢
          • 2017-02-02
          • 2020-05-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多