嵌套循环的替代方法更快吗?


85

我需要创建一个数字组合列表。数字很​​小,所以我可以用byte代替int。但是,它需要许多嵌套循环才能获得所有可能的组合。我想知道是否有一种更有效的方式来执行我要执行的操作。到目前为止的代码是:

var data = new List<byte[]>();
for (byte a = 0; a < 2; a++)
for (byte b = 0; b < 3; b++)
for (byte c = 0; c < 4; c++)
for (byte d = 0; d < 3; d++)
for (byte e = 0; e < 4; e++)
for (byte f = 0; f < 3; f++)
for (byte g = 0; g < 3; g++)
for (byte h = 0; h < 4; h++)
for (byte i = 0; i < 2; i++)
for (byte j = 0; j < 4; j++)
for (byte k = 0; k < 4; k++)
for (byte l = 0; l < 3; l++)
for (byte m = 0; m < 4; m++)
{
    data.Add(new [] {a, b, c, d, e, f, g, h, i, j, k, l, m});
}

我当时正在考虑使用类似a的东西,BitArray但是我不确定如何合并它。

任何建议将不胜感激。或者,也许这是我想要做的最快的方法?

编辑 几个要点(抱歉,我没有在原始帖子中提到):

  • 数字和它们的顺序(2、3、4、3、4、3、3等)非常重要,因此使用诸如使用LINQ生成置换的解决方案将无济于事,因为每个“列”中的最大值不同
  • 我不是数学家,所以如果我没有正确使用“排列”和“组合”等技术术语,我深表歉意:)
  • 确实需要一次填充所有这些组合-我不能仅仅基于索引来获取一个或多个
  • 保证使用byte比使用更快。拥有67m +字节数组而不是整数在内存使用方面也要好得多int
  • 我的最终目标是寻找一种更快的嵌套循环替代方法。
  • 我考虑过使用并行编程,但是由于要实现的迭代性质,我找不到成功的方法(即使使用ConcurrentBag),但是很高兴被证明是错误的:)

结论

Caramiriel提供了很好的微观优化,可节省一些时间,因此,我将该答案标记为正确。Eric还提到预分配列表更快。但是,在这个阶段,嵌套循环似乎实际上是最快的方法(令人沮丧,我知道!)。

如果您想尝试使用的基准测试StopWatch,请使用13个循环,每个循环最多计数4个循环-列表中约有67m +行。在我的机器(i5-3320M 2.6GHz)上,大约需要2.2s来完成优化版本。


1
尝试使用linq,如果您使用的是多核处理器,则Parrallel.for
Jalpesh Vadgama,2015年

1
基于我所看到的这些不是排列,而是几个非常小的(2-4个元素)集合的组合是对的,还是您确实想要一个集合的所有/某些排列?
卡斯滕

我假设已经搜索过bing.com/search?q=c%23+permutation+enumerable,并且由于某种原因(在帖子中未提及)而决定拒绝现有的答案,例如stackoverflow.com/questions/4319049/… ...考虑列出您查看并决定拒绝的选项,以使这个问题更好。
Alexei Levenkov,2015年

3
如果这与性能有关:您可以预分配列表(构造函数)并展开一些循环,但是我认为就这样……除了预先计算和存储这些数字。循环(开销)可能是所有循环中最昂贵的,因为体内的操作量很少。
Caramiriel 2015年

5
@benpage:为什么需要预先生成所有组合?为什么在需要时不从其索引生成组合?
Pieter Witvoet 2015年

Answers:


61

您可以使用结构的属性并预先分配结构。我在下面的示例中降低了一些级别,但是我敢肯定您将能够弄清具体细节。比原始速度(释放模式)快5-6倍。

块:

struct ByteBlock
{
    public byte A;
    public byte B;
    public byte C;
    public byte D;
    public byte E;
}

循环:

var data = new ByteBlock[2*3*4*3*4];
var counter = 0;

var bytes = new ByteBlock();

for (byte a = 0; a < 2; a++)
{
    bytes.A = a;
    for (byte b = 0; b < 3; b++)
    {
        bytes.B = b;
        for (byte c = 0; c < 4; c++)
        {
            bytes.C = c;
            for (byte d = 0; d < 3; d++)
            {
                bytes.D = d;
                for (byte e = 0; e < 4; e++)
                {
                    bytes.E = e;
                    data[counter++] = bytes;
                }
            }
        }
    }
}

速度更快,因为它不会在您每次将新列表添加到列表时分配新列表。另外,由于它正在创建此列表,因此需要引用其他每个值(a,b,c,d,e)。您可以假设每个值在循环内仅被修改一次,因此我们可以对其进行优化(数据局部性)。

另请阅读注释的副作用。

编辑答案,使用T[]而不是List<T>


1
它是一个结构,所以您应该没问题=)它们都是唯一的。调用List<T>.Add方法时将其复制。
Caramiriel 2015年

4
它甚至更快,如果你有能力分配给列表()
埃里克·

5
当心计算器在堆栈上分配过多的对象时例外。
AndreiTătar2015年

7
@安德鲁我不明白你的意思。此代码不是递归的,并且堆栈使用最少。
CodesInChaos

3
@Andrew:那没内存,不是stackoverflow。这是因为该List<T>.Add()方法超出了它可以存储的范围。这将使其调整大小(大小增加一倍),从而超过2GB的内存。尝试使用新的List <ByteBlock>(maxPerLevel.Aggregate(1,(x,y)=> x * y))进行预分配,尽管它已经是“随机的”,您需要在内存中存储完整的2GB数据块。还要注意data.ToArray(); 之所以昂贵,是因为它在那时将项目两次保存在内存中。[改写]
Caramiriel 2015年

33

您正在执行的操作是计数(基数可变,但仍在计数)。

由于您使用的是C#,因此我假设您不想使用有用的内存布局和数据结构来真正优化代码。

因此,我在这里发布了一些不同的内容,这可能不适合您的情况,但值得注意的是:如果您实际上以稀疏方式访问列表,则该类可让您在线性时间内计算第i个元素(而不是指数答案)

class Counter
{
    public int[] Radices;

    public int[] this[int n]
    {
        get 
        { 
            int[] v = new int[Radices.Length];
            int i = Radices.Length - 1;

            while (n != 0 && i >= 0)
            {
                //Hope C# has an IL-opcode for div-and-reminder like x86 do
                v[i] = n % Radices[i];
                n /= Radices[i--];
            }
            return v;
        }
    }
}

您可以通过这种方式使用此类

Counter c = new Counter();
c.Radices = new int[] { 2,3,4,3,4,3,3,4,2,4,4,3,4};

现在c[i]是一样的列表,将其命名ll[i]

如您所见,即使您预先计算了所有列表,也可以轻松避免所有这些循环:),因为您可以简单地实现Carry-Ripple计数器。

计数器是一门非常研究的学科,我强烈建议您去找一些文献。


4
我喜欢您的答案,但是说所有其他答案都是指数的则是不正确的。
饼干2015年

1
与Caramiriel的回答相比,此速度如何?
约翰·奥多姆

17
“ C-kiddy-#”,真的吗?这似乎完全没有必要。
KChaloux 2015年

2
它的确是:Math.DivRem
Caramiriel 2015年

1
我认为在某种程度上,优化是一个使用问题。例如,如果每个数组仅使用一次,则可以避免密集的内存分配,这是我认为的关键瓶颈。此外,如果要计算所有值,则应利用这样的事实,即进行单次增量(即+1增量)以避免除法。这更像是“开箱即用”的answear或原型,我并没有真正尝试加快速度,我只是喜欢这样:)

14

方法1

一种使速度更快的方法是,如果您打算继续使用List<byte[]>,请指定容量,这样。

var data = new List<byte[]>(2 * 3 * 4 * 3 * 4 * 3 * 3 * 4 * 2 * 4 * 4 * 3 * 4);

方法2

此外,您可以System.Array直接使用以获得更快的访问权限。如果您的问题坚持要求每个元素都预先存储在内存中,则我建议使用此方法。

var data = new byte[2 * 3 * 4 * 3 * 4 * 3 * 3 * 4 * 2 * 4 * 4 * 3 * 4][];
int counter = 0;

for (byte a = 0; a < 2; a++)
    for (byte b = 0; b < 3; b++)
        for (byte c = 0; c < 4; c++)
            for (byte d = 0; d < 3; d++)
                for (byte e = 0; e < 4; e++)
                    for (byte f = 0; f < 3; f++)
                        for (byte g = 0; g < 3; g++)
                            for (byte h = 0; h < 4; h++)
                                for (byte i = 0; i < 2; i++)
                                    for (byte j = 0; j < 4; j++)
                                        for (byte k = 0; k < 4; k++)
                                            for (byte l = 0; l < 3; l++)
                                                for (byte m = 0; m < 4; m++)
                                                    data[counter++] = new[] { a, b, c, d, e, f, g, h, i, j, k, l, m };

在我的计算机上,这需要596毫秒才能完成,比相关代码(需658毫秒)10.4%

方法3

或者,您可以使用以下技术进行低成本初始化,以稀疏方式进行访问。当可能仅需要某些元素并且不需要预先确定所有元素时,这特别有利。此外,当内存不足时,当使用更多的大型元素时,此类技术可能成为唯一可行的选择。

在该实施方式中,每个元素在访问时被懒惰地即时确定。当然,这要付出访问期间额外的CPU的代价。

class HypotheticalBytes
{
    private readonly int _c1, _c2, _c3, _c4, _c5, _c6, _c7, _c8, _c9, _c10, _c11, _c12;
    private readonly int _t0, _t1, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9, _t10, _t11;

    public int Count
    {
        get { return _t0; }
    }

    public HypotheticalBytes(
        int c0, int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8, int c9, int c10, int c11, int c12)
    {
        _c1 = c1;
        _c2 = c2;
        _c3 = c3;
        _c4 = c4;
        _c5 = c5;
        _c6 = c6;
        _c7 = c7;
        _c8 = c8;
        _c9 = c9;
        _c10 = c10;
        _c11 = c11;
        _c12 = c12;
        _t11 = _c12 * c11;
        _t10 = _t11 * c10;
        _t9 = _t10 * c9;
        _t8 = _t9 * c8;
        _t7 = _t8 * c7;
        _t6 = _t7 * c6;
        _t5 = _t6 * c5;
        _t4 = _t5 * c4;
        _t3 = _t4 * c3;
        _t2 = _t3 * c2;
        _t1 = _t2 * c1;
        _t0 = _t1 * c0;
    }

    public byte[] this[int index]
    {
        get
        {
            return new[]
            {
                (byte)(index / _t1),
                (byte)((index / _t2) % _c1),
                (byte)((index / _t3) % _c2),
                (byte)((index / _t4) % _c3),
                (byte)((index / _t5) % _c4),
                (byte)((index / _t6) % _c5),
                (byte)((index / _t7) % _c6),
                (byte)((index / _t8) % _c7),
                (byte)((index / _t9) % _c8),
                (byte)((index / _t10) % _c9),
                (byte)((index / _t11) % _c10),
                (byte)((index / _c12) % _c11),
                (byte)(index % _c12)
            };
        }
    }
}

这需要897毫秒才能在我的计算机上完成(也需要Array方法2创建并添加到),这比所讨论的代码(需要658毫秒)要慢36.3%


1
您的第二个建议也是在内存消耗方面的大量节省。(但我要指出的是,它假设列表不会更改)
Taemyr

我需要一次创建整个列表-我无法引用列表中的索引。
benpage

@Taemyr谢谢。我将进行更新以相应地进行说明。如果实现确实坚持要求您预先填充整个列表,则此第三个选项显然对您不起作用。
饼干2015年

3
@benpage为什么需要填充列表?
塔伊米尔2015年

14

在我的机器上,这将在222 ms和760 ms(13个for循环)中生成组合:

private static byte[,] GenerateCombinations(byte[] maxNumberPerLevel)
{
    var levels = maxNumberPerLevel.Length;

    var periodsPerLevel = new int[levels];
    var totalItems = 1;
    for (var i = 0; i < levels; i++)
    {
        periodsPerLevel[i] = totalItems;
        totalItems *= maxNumberPerLevel[i];
    }

    var results = new byte[totalItems, levels];

    Parallel.For(0, levels, level =>
    {
        var periodPerLevel = periodsPerLevel[level];
        var maxPerLevel = maxNumberPerLevel[level];
        for (var i = 0; i < totalItems; i++)
            results[i, level] = (byte)(i / periodPerLevel % maxPerLevel);
    });

    return results;
}

这是一个很好的答案!不幸的是,它的运行速度比嵌套循环慢。您是否有可能使用TPL进行编辑?
2015年

不幸的是,它仍然慢很多。
benpage

1
@benpage有一种简单的方法可以使其速度至少快2倍。您只需将结果类型更改为int [,]。这将在一次调用中分配整个阵列内存。我不确定如何满足您的需求(更改返回类型)。
安德烈·塔塔尔(AndreiTătar)2015年

8
var numbers = new[] { 2, 3, 4, 3, 4, 3, 3, 4, 2, 4, 4, 3, 4 };
var result = (numbers.Select(i => Enumerable.Range(0, i))).CartesianProduct();

使用http://ericlippert.com/2010/06/28/computing-a-cartesian-product-with-linq/上的扩展方法

public static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences)
{
    // base case: 
    IEnumerable<IEnumerable<T>> result =
        new[] { Enumerable.Empty<T>() };
    foreach (var sequence in sequences)
    {
        // don't close over the loop variable (fixed in C# 5 BTW)
        var s = sequence;
        // recursive case: use SelectMany to build 
        // the new product out of the old one 
        result =
            from seq in result
            from item in s
            select seq.Concat(new[] { item });
    }
    return result;
}

1
这运行慢得多:(
benpage

8

List内部有一个数组,用于存储其值,并且具有固定的length。当您调用List.Add时,它将检查是否有足够的空间。如果无法添加新元素,它将创建一个更大的新数组,将所有先前的元素复制过来,然后添加然后再添加一个。这需要花费很多时间。

由于您已经知道元素的数量,因此可以创建正确大小的列表,这应该已经快得多了。

另外,不确定如何访问这些值,但是可以创建该对象并将其保存在代码中(从磁盘加载图像可能会比您现在执行的要慢。您对此进行了多少次读取/写入操作事情?


我实际上已经尝试过预分配一个常规数组,不管是否相信,它都比较慢。就像我在上面说的那样,这需要即时创建,我无法一次计算并保留。
benpage

真?哇-您正在启用优化功能,对吗?(只是问问)
卡斯滕

嗯,这是另一个问题,常规数组[x,y]很好用,但是数组数组会更快。stackoverflow.com/questions/597720/…因为它们是如何在IL中实现的
gjvdkamp 2015年

5

这是仅需要2个循环的另一种方式。想法是增加第一个元素,如果该数字超过,则增加下一个元素。

除了显示数据,您还可以使用currentValues.Clone并将克隆的版本添加到列表中。对我来说,运行速度比您的版本快。

byte[] maxValues = {2, 3, 4};
byte[] currentValues = {0, 0, 0};

do {
    Console.WriteLine("{0}, {1}, {2}", currentValues[0], currentValues[1], currentValues[2]);

    currentValues[0] += 1;

    for (int i = 0; i <= maxValues.Count - 2; i++) {
        if (currentValues[i] < maxValues[i]) {
            break;
        }

        currentValues[i] = 0;
        currentValues[i + 1] += 1;
    }

// Stop the whole thing if the last number is over
// } while (currentValues[currentValues.Length-1] < maxValues[maxValues.Length-1]);
} while (currentValues.Last() < maxValues.Last());
  • 希望这段代码有效,我从vb转换了它

3

您所有的数字都是编译时间常数。

如何将所有循环展开到列表中(使用程序编写代码):

data.Add(new [] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
data.Add(new [] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
etc.

这至少应该消除for循环的开销(如果有的话)。

我对C#不太熟悉,但是似乎有一些序列化对象的方法。如果您只是生成该列表并以某种形式对其进行序列化怎么办?我不确定反序列化是否更快,然后创建列表并添加元素。


序列化是开箱即用的一种非常好的思维方式!
乔尔B

不幸的是,列表中的最大值是动态的,我无法静态地键入此值。好主意!
benpage

2

您是否需要将结果设为数组数组?在当前设置下,内部数组的长度是固定的,可以用结构代替。这将允许将整个对象保留为一个连续的内存块,并提供对元素的更轻松访问(不确定以后如何使用该对象)。

下面的方法要快得多(我的盒子上的原始方法是41ms对1071ms):

struct element {
    public byte a;
    public byte b;
    public byte c;
    public byte d;
    public byte e;
    public byte f;
    public byte g;
    public byte h;
    public byte i;
    public byte j;
    public byte k;
    public byte l;
    public byte m;
}

element[] WithStruct() {
    var t = new element[3981312];
    int z = 0;
    for (byte a = 0; a < 2; a++)
    for (byte b = 0; b < 3; b++)
    for (byte c = 0; c < 4; c++)
    for (byte d = 0; d < 3; d++)
    for (byte e = 0; e < 4; e++)
    for (byte f = 0; f < 3; f++)
    for (byte g = 0; g < 3; g++)
    for (byte h = 0; h < 4; h++)
    for (byte i = 0; i < 2; i++)
    for (byte j = 0; j < 4; j++)
    for (byte k = 0; k < 4; k++)
    for (byte l = 0; l < 3; l++)
    for (byte m = 0; m < 4; m++)
    {
        t[z].a = a;
        t[z].b = b;
        t[z].c = c;
        t[z].d = d;
        t[z].e = e;
        t[z].f = f;
        t[z].g = g;
        t[z].h = h;
        t[z].i = i;
        t[z].j = j;
        t[z].k = k;
        t[z].l = l;
        t[z].m = m;
        z++;
    }
    return t;
}

好主意-实际上,这实际上是我在实际项目中所做的-因为简单,我只是没有将其放在原始解决方案中。我主要是在寻找嵌套循环的更好的选择。
benpage

1

使用Parallel.For()运行该怎么办?(@Caramiriel的结构优化荣誉)。我稍微修改了值(a是5而不是2),所以我对结果更有信心。

    var data = new ConcurrentStack<List<Bytes>>();
    var sw = new Stopwatch();

    sw.Start();

    Parallel.For(0, 5, () => new List<Bytes>(3*4*3*4*3*3*4*2*4*4*3*4),
      (a, loop, localList) => {
        var bytes = new Bytes();
        bytes.A = (byte) a;
        for (byte b = 0; b < 3; b++) {
          bytes.B = b;
          for (byte c = 0; c < 4; c++) {
            bytes.C = c; 
            for (byte d = 0; d < 3; d++) {
              bytes.D = d; 
              for (byte e = 0; e < 4; e++) {
                bytes.E = e; 
                for (byte f = 0; f < 3; f++) {
                  bytes.F = f; 
                  for (byte g = 0; g < 3; g++) {
                    bytes.G = g; 
                    for (byte h = 0; h < 4; h++) {
                      bytes.H = h; 
                      for (byte i = 0; i < 2; i++) {
                        bytes.I = i; 
                        for (byte j = 0; j < 4; j++) {
                          bytes.J = j; 
                          for (byte k = 0; k < 4; k++) {
                            bytes.K = k; 
                            for (byte l = 0; l < 3; l++) {
                              bytes.L = l;
                              for (byte m = 0; m < 4; m++) {
                                bytes.M = m;
                                localList.Add(bytes);
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }


        return localList;
      }, x => {
        data.Push(x);
    });

    var joinedData = _join(data);

_join() 是一个私有方法,定义为:

private static IList<Bytes> _join(IEnumerable<IList<Bytes>> data) {
  var value = new List<Bytes>();
  foreach (var d in data) {
    value.AddRange(d);
  }
  return value;
}

在我的系统上,此版本的运行速度大约提高了6倍(1.718秒对0.266秒)。


1
这几乎可以保证为您提供错误的共享,并且可能会慢很多倍。
gjvdkamp 2015年

不错-不幸的是,它的运行速度比for循环。FWIW我尝试了ALLParallel.For和VS使其崩溃!
benpage

@gjvdkamp我已经用并行版本更新了我的答案,我相信它消除了错误的共享问题。
jdphenix

0

您的某些数字完全适合整数整数位数,因此您可以将它们与上位数字“打包”在一起:

for (byte lm = 0; lm < 12; lm++)
{
    ...
    t[z].l = (lm&12)>>2;
    t[z].m = lm&3;
    ...
}

当然,这会使代码的可读性降低,但是您节省了一个循环。每当数字之一是2的幂时,就可以这样做,在您的情况下,是7倍。


我想了解更多有关此答案的信息-您能对此进行扩展吗?
benpage

对不起,我迟到了。m从0到3,以二进制形式从00到11,从0到2,从00到10,因此,如果单独打印它们,则为:00 00 00 01 00 10 00 11 01 00 .. 。10 11您可以将它们合并成一个4位的整数(从0000到1011),并使用掩码lm和3进行双向选择,并在lm和(11)b之间进行双向选择; lm&12与lm相同(1100)b然后我们将两位移位以得到“实数”。顺便说一句,只是意识到在这种情况下做lm >> 2是足够的。
法比恩·杜邦

0

这是另一种解决方案。在VS之外,它的运行速度高达437.5毫秒,比原始代码(我的计算机上的593.7)快26%:

static List<byte[]> Combinations(byte[] maxs)
{
  int length = maxs.Length;
  int count = 1; // 3981312;
  Array.ForEach(maxs, m => count *= m);
  byte[][] data = new byte[count][];
  byte[] counters = new byte[length];

  for (int r = 0; r < count; r++)
  {
    byte[] row = new byte[length];
    for (int c = 0; c < length; c++)
      row[c] = counters[c];
    data[r] = row;

    for (int i = length - 1; i >= 0; i--)
    {
      counters[i]++;
      if (counters[i] == maxs[i])
        counters[i] = 0;
      else
        break;
    }
  }

  return data.ToList();
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.