6

我试图编写一种方法,该方法尽可能简单地返回给定枚举的排列。编码:

using System.Collections.Generic;

public static partial class Permutable {
    static IEnumerable<IEnumerable<T>> PermuteIterator<T>(
        IEnumerable<T> source, int offset) {
        var count=0;

        foreach(var dummy in source)
            if(++count>offset)
                foreach(
                    var sequence in
                        Permutable.PermuteIterator(
                            source.Exchange(offset, count-1), 1+offset)
                    )
                    yield return sequence;

        if(offset==count-1)
            yield return source;
    }

    public static IEnumerable<IEnumerable<T>> AsPermutable<T>(
        this IEnumerable<T> source) {
        return Permutable.PermuteIterator(source, 0);
    }

    public static IEnumerable<T> Exchange<T>(
        this IEnumerable<T> source, int index1, int index2) {
        // exchange elements at index1 and index2
    }
}

由于迭代器块中的代码已经简化,我试图使它成为一个简单的 LINQ 查询表达式。

在嵌套的代码中有一个递归,甚至在;foreach之外可能还有另一个可能 yield foreach这是我用查询语法重写它的困难部分。

我读过这个答案:

C# 字符串排列

但我想这不是我的解决方案..

我尝试了各种方法,并认为这并不容易做到。我怎样才能完成它?

Exchange方法是另一个问题,我问了一个问题:

如何通过只交互一次来交换枚举项?

但我想这不是问题..)

4

3 回答 3

4

由于您正在寻找利用现有 LINQ 查询运算符而不是使用迭代器块的答案,因此这里有一个。请注意,它不会像其他一些解决方案那样有效;这些查询运算符的使用不如 Eric Lippert 的解决方案那么有效。(虽然它要短得多。)

另请注意,由于此解决方案使用SelectManyWhere接受索引的重载,因此需要使用方法语法而不是查询语法调用这些运算符,并且其他几个运算符没有查询语法等效项。我可以将其更改Select为查询语法,但为了保持一致性,我没有。

public static IEnumerable<IEnumerable<T>> Permuatations<T>(
    this IEnumerable<T> source)
{
    var list = source.ToList();//becase we iterate it multiple times
    return list.SelectMany((item, i) => list.Where((_, index) => index != i)
            .Permuatations()
            .Select(subsequence => new[] { item }.Concat(subsequence)))
        .DefaultIfEmpty(Enumerable.Empty<T>());
}

所以,来谈谈这是在做什么。

首先它通过源序列;对于该序列中的每个项目,它都会创建一个与源序列一样的序列,但会取出“当前项目”。(这是list.Where方法)。

接下来它(递归地)获取该子序列的所有排列。

之后,它将“已删除”项目添加到每个子序列的开头。

所有这些子序列都被展平在一起,因为它们都在SelectMany.

DefaultIfEmpty用于确保外部序列永远不会为空。置换一个空序列会产生一个内部有一个空序列的序列。这是递归操作的“基本情况”。

于 2013-06-07T16:29:12.707 回答
3

编辑1:

没有递归的单行解决方案

我已经重新创建了核心方法(来自此答案下面的上一个解决方案),因此现在它不再是递归的。现在很容易从中制作单行解决方案。

我不得不使用Enumerable方法和扩展方法来做到这一点。没有这些,我认为不可能做到这一点。

class Permutator
{
    private static IEnumerable<IEnumerable<int>> CreateIndices(int length)
    {
        var factorial = Enumerable.Range(2, length - 1)
            .Aggregate((a, b) => a * b);

        return (from p in Enumerable.Range(0, factorial)
                // creating module values from 2 up to length
                // e.g. length = 3: mods = [ p%2, p%3 ]
                // e.g. length = 4: mods = [ p%2, p%3, p%4 ]
                let mods = Enumerable.Range(2, length - 1)
                    .Select(m => p % m).ToArray()
                select (
                    // creating indices for each permutation
                    mods.Aggregate(
                        new[] { 0 },
                        (s, i) =>
                            s.Take(i)
                            .Concat(new[] { s.Length })
                            .Concat(s.Skip(i)).ToArray())
                    ));
    }

    public static IEnumerable<IEnumerable<T>> Get<T>(IEnumerable<T> items)
    {
        var array = items.ToArray();
        return from indices in CreateIndices(array.Length)
                select (from i in indices select array[i]);
    }
}

现在最终解决方案

结果就是这个怪物:

class Permutator
{
    public static IEnumerable<IEnumerable<T>> Get<T>(IEnumerable<T> items)
    {
        return
            from p in Enumerable.Range(0,
                Enumerable.Range(2, items.Count() - 1)
                    .Aggregate((a, b) => a * b))
            let mods = Enumerable.Range(2, items.Count() - 1)
                .Select(m => p % m).ToArray()
            select mods.Aggregate(
                items.Take(1).ToArray(),
                (s, i) =>
                    s.Take(i)
                    .Concat(items.Skip(s.Length).Take(1))
                    .Concat(s.Skip(i)).ToArray());
    }
}

以前的解决方案

我创建了一些可能是您正在寻找的东西:

class Permutator
{
    private static IEnumerable<IEnumerable<int>> CreateIndices(int length)
    {
        return (from p in Enumerable.Range(0, length)
                select (
                    from s in Permutator.CreateIndices(length - 1)
                              .DefaultIfEmpty(Enumerable.Empty<int>())
                    select s.Take(p)
                           .Concat(new[] { length - 1 })
                           .Concat(s.Skip(p))
                    ))
                    .SelectMany(i => i);
    }

    public static IEnumerable<IEnumerable<T>> Get<T>(IEnumerable<T> items)
    {
        var array = items.ToArray();
        return from indices in CreateIndices(array.Length)
                select (from i in indices select array[i]);
    }
}

如何使用它的示例:

var items = new[] { "0", "1", "2" };
var p = Permutator.Get(items);
var result = p.Select(a=>a.ToArray()).ToArray();

这个怎么运作

核心是CreateIndices方法。它为每个排列创建一个包含源元素索引的序列。

最好用一个例子来解释:

CreateIndices(0);
// returns no permutations

CreateIndices(1);
// returns 1 permutation
// [ 0 ]

CreateIndices(2);
// returns 2 permutations
// [ 1, 0 ]
// [ 0, 1 ]

CreateIndices(3);
// returns 6 permutations
// [ 2, 1, 0 ]
// [ 2, 0, 1 ]
// [ 1, 2, 0 ]
// [ 0, 2, 1 ]
// [ 1, 0, 2 ]
// [ 0, 1, 2 ]

它是一种递归方法,仅基于可枚举扩展和 LINQ 语法查询。

递归的想法是每个级别都基于前一个级别构建。

CreateIndices(n)在所有可用位置将元素添加n-1到由 , 返回的排列中。CreateIndices(n-1)

递归的根是CreateIndices(0)返回一个空的排列集。

一步一步解释:CreateIndices(3)


1. 让我们从创建结果开始CreateIndices(0)

  • 空的


2. 那么结果CreateIndices(1)

  • 在位置 0 [ 0 ]将元素0(n-1) 添加到每个先前的排列


3. 那么结果CreateIndices(2)

  • 1在位置 0
    [ 1, 0 ]将元素(n-1) 添加到每个先前的排列
  • 1在位置 1
    [ 0, 1 ]将元素(n-1) 添加到每个先前的排列


4. 那么结果CreateIndices(3)

  • 2在位置 0
    [ 2, 1, 0 ]
    [ 2, 0, 1 ]将元素(n-1) 添加到每个先前的排列
  • 2在位置 1
    [ 1, 2, 0 ]
    [ 0, 2, 1 ]将元素(n-1) 添加到每个先前的排列
  • 2在位置 2
    [ 1, 0, 2 ]
    [ 0, 1, 2 ]将元素(n-1) 添加到每个先前的排列

接下来发生什么

现在我们有了每个排列的索引,我们可以使用它们来构建值的真实排列。这就是泛型Get方法的作用。

另请注意,该Get方法是将源序列具体化为数组的唯一方法。只是一个枚举器CreateIndices,没有任何真实对象......所以您只需在枚举序列时支付成本,在调用时Get您需要支付创建源序列数组的费用。

这解释了为什么在调用Get示例之后,我必须具体化结果,以便我们可以使用它:

var items = new[] { "0", "1", "2" };
var p = Permutator.Get(items); // pay to create array from source elements
var result = p.Select(a => a.ToArray() // pay to create arrays for each of the permutations
    ).ToArray(); // pay to get the permutations

如果我们只列举一半的排列,这允许我们只支付一半的成本。

于 2013-06-13T00:05:09.140 回答
2

虽然我年纪大了,但我从不喜欢不必要的递归。

由于无论如何您都需要迭代源,所以我只是将它存储在一个数组中。我跟踪一个“交换”数组,它告诉程序用什么交换什么;索引是交换的源位置,索引是目标位置。

如果您愿意,也可以进行子排列。

如果您真的对很多痛苦感到满意,我想您也可以将交换更改为复杂的 Exchange IEnumerable;我想这是增加很多复杂性同时让一切变慢的好方法。:-)

我还重用了数组;每次都重新迭代所有内容似乎很愚蠢,当“src”很大时,这可以为您节省大量复制数据和执行递归的时间。这是相当有效的。

public static IEnumerable<IEnumerable<T>> Permutations<T>(this IEnumerable<T> src)
{
    T[] array = src.ToArray();

    // Initialize
    int[] swappings = new int[array.Length];
    for (int j = 0; j < array.Length; ++j)
    {
        swappings[j] = j;
    }

    yield return array;

    int i = swappings.Length - 2;
    while (i >= 0)
    {
        int prev = swappings[i];
        Swap(array, i, prev);
        int next = prev + 1;
        swappings[i] = next;
        Swap(array, i, next);

        yield return array;

        // Prepare next
        i = swappings.Length - 1;
        while (i >= 0 && swappings[i] == array.Length - 1)
        {
            // Undo the swap represented by permSwappings[i]
            Swap(array, i, swappings[i]);
            swappings[i] = i;
            i--;
        }
    }
}

private static void Swap<T>(T[] array, int i, int next)
{
    var tmp = array[i];
    array[i] = array[next];
    array[next] = tmp;
}
于 2013-06-12T10:28:42.533 回答