0

The problem may have been asked else where but i cannot find the solution to my problem. The problem is not language specific, the same problem can be asked in python. The task is algorithm to generate a list of strings like Enumerable.Range, but the chars are not only limited to 1, 2, 3... but can be any sequence of characters. The simplest sample is:

TestCase 1:
input:

baseChars: ['a','b'],
required string length: 2

output:

['aa','ab','ba','bb']

TestCase 2:

baseChars: ['a','b']
required string length: 1

output:

['a','b']

The function is working well:

    static IList<string> baseChars = new List<string>() { "0", "1", "2", "3" };
    static void CharsRange1(string prefix, int pos)
    {
        if (pos == 1)
        {
            foreach (string s in baseChars)
            {
                Console.WriteLine(prefix + s);
            }
        }
        else
        {
            foreach (string s in baseChars)
            {
                CharsRange1(prefix + s, pos - 1);
            }
        }
    }

Expected and actual output(newlines replaced with comma to save space):

000, 001, 002, 003, 010, 011, 012, 013, 020, 021, 022, 023, 030, 031, 032, 033, 100, 101, 102, 103, 110, 111, 112, 113, 120, 121, 122, 123, 130, 131, 132, 133, 200, 201, 202, 203, 210, 211, 212, 213, 220, 221, 222, 223, 230, 231, 232, 233, 300, 301, 302, 303, 310, 311, 312, 313, 320, 321, 322, 323, 330, 331, 332, 333

The problem is encapsule this function as a library, so return type should be IEnumerable<string>, so memory won't explode even for large input. but my code cannot return anything:

    static IEnumerable<string> CharsRange2(string prefix, int pos)
    {
        if (pos == 1)
        {
            foreach (string s in baseChars)
            {
                yield return prefix + s;
            }
        }
        else
        {
            foreach (string s in baseChars)
            {
                // here if i yield return then won't compile
                // i thought at the end of recursive loop it will return 
                CharsRange2(prefix + s, pos - 1);
            }
        }
    }

the main:

    static void Main(string[] args)
    {
        //CharsRange1("", 3);//working
        foreach (string s in CharsRange2("", 3))
        {
            Console.WriteLine(s);//nothing
        }
        Console.WriteLine("end");
        Console.ReadKey();
    }

Can anybody help? I've put my code in github. Also appreicated if you can change my implementation to non-recursive but keep the function return type.

Lei Yang
  • 3,970
  • 6
  • 38
  • 59
  • 5
    `CharsRange2(prefix + s, pos - 1);` So you are calling the function recursively and, umm, ignoring the results? I suspect you meant to `foreach` over that and use `yield return`. – mjwills Jun 16 '20 at 05:07
  • 1
    Your problem description isn't great either - since you haven't shown the expected result for a given set of inputs. I _think_ I understand what you are trying to do, but am not 100% sure... – mjwills Jun 16 '20 at 05:11
  • The code shown is very confusing compared even to first https://www.bing.com/search?q=c%23+recursive+yield+return result https://stackoverflow.com/questions/2055927/ienumerable-and-recursion-using-yield-return. Could you please review your code and make sure it makes sense... – Alexei Levenkov Jun 16 '20 at 05:15
  • Side note: counting in arbitrary base is indeed very common competition task... which seem to be what you are asking about. You may want to clarify that too with your edit (or clarify what type of sequence you want if it is not counting). – Alexei Levenkov Jun 16 '20 at 05:17
  • 1
    A (yield) return returns only to the immediate caller, not to whoever did the first call to the recursive chain – Hans Kesting Jun 16 '20 at 05:26
  • @AlexeiLevenkov thanks for comments. i updated the expected output. the question is **similiar** to 'counting in arbitrary base' but i don't think it is same. from searching results counting base seems to be converting base 10 to other bases. but my requirement don't input any base 10 number, i just need printing continous strings built up with any chars sequence(such as ['a', 'b', 'c']). – Lei Yang Jun 17 '20 at 02:18

2 Answers2

4

Option 1, yield each value from the recursive call.

    foreach (string s in baseChars)
        foreach (var r in CharsRange2(prefix + s, pos - 1))
            yield return r;

Option 2, reuse existing IEnumerable types built into the framework to avoid yield return completely;

    if (pos == 1)
        return baseChars.Select(s => prefix + s);
    else
        return baseChars.SelectMany(s => CharsRange2(prefix + s, pos - 1));

Option 3, use nested loops instead of a recursive method, left as an exercise for the reader.

Jeremy Lakeman
  • 9,515
  • 25
  • 29
2

As pointed out the call CharsRange2(prefix + s, pos - 1); isn't being used. You need to nest the foreach and yield each result.

Here's an alternative that's more based on the idea of Enumerable.Range.

Start with a general purpose base changer:

public static IEnumerable<int> ToBase(this int x, int b)
{
    IEnumerable<int> ToBaseReverse()
    {
        if (x == 0)
        {
            yield return 0;
            yield break;
        }
        int z = x;
        while (z > 0)
        {
            yield return z % b;
            z = z / b;
        }
    }

    return ToBaseReverse().Reverse();
}

Now add a method to convert this to a specific set of digits:

public static string ToBase(this int number, string digits) =>
    String.Concat(number.ToBase(digits.Length).Select(x => digits[x]));

This can be used like this:

string result = 45.ToBase("0X2Y");
Console.WriteLine(result);

Which gives:

2YX

Now it's trivial to write Enumerable.Range(0, 10).Select(n => n.ToBase("0X2Y")).

That gives:

0, X, 2, Y, X0, XX, X2, XY, 20, 2X

This counts properly for all non-zero numbers, without displaying the leading zeros except for zero itself.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • i'm sorry to mention that your answer seems to be converting input dec number to other base? my question is only about generating continous sequence(just like 000, 001, ... 002, 010... 111). so if the steps contain some calculations(devide and mod) the performance might be bad, i guess. – Lei Yang Jun 16 '20 at 07:59
  • @LeiYang - Probably not. Divide and mod are exceedingly fast. Those operations get very close to the CPU core. Also string appending operations like `prefix + s` are incredibly slow. The only way to tell is to run both sets of code and compare the performance. – Enigmativity Jun 16 '20 at 10:06
  • @LeiYang - Which I did. It turns out that mine is about 4x slower. I suspect, from my experience, that's nothing to do with the divide and mod, but more the `IEnumerable` and the `Reverse()`. So, time to try to make a faster version. :-) – Enigmativity Jun 16 '20 at 10:08
  • thanks for clarifying this. i ended up with @Jeremy Lakeman's solution but changed the function like this `public IEnumerable Permutate(char[] strchars, int pos)` where init `strchars[pos]` is updated in each loop. and returning `new string(strchars)`. do you think this is faster than old version of string `+` ? – Lei Yang Jun 17 '20 at 01:15
  • @LeiYang - I'd need to see the code, but the only real way to know is to time it. – Enigmativity Jun 17 '20 at 03:14
  • i've updated github https://github.com/LeiYangGH/csharp-yield-recursive, please compare CharsRange2, 3 if you have interest, thanks! – Lei Yang Jun 17 '20 at 04:48
  • Enigmativity `.ToList().Reverse()` *should* solve the perf issue. Padding with "zero" character should give output @LeiYangq wanted (which they can trivially do). – Alexei Levenkov Jun 17 '20 at 17:55
  • @AlexeiLevenkov - You're suggesting that I should have `ToBaseReverse().ToList().Reverse()`? – Enigmativity Jun 17 '20 at 22:08
  • @Enigmativity yes, but I'm not actually sure why I thought adding `ToList()` would make it faster - `Reverse()` is essentially ToList + iterate it backward... coffee first, answering later :) – Alexei Levenkov Jun 17 '20 at 22:36