【问题标题】:Iterate through an array of arbitrary dimension遍历任意维度的数组
【发布时间】:2012-04-12 10:36:21
【问题描述】:

使用c#,如何遍历unknown维度的多维数组?

例如考虑将数组的每个元素设置为指定值,数组的所有条目都需要迭代。该方法应处理以下所有情况并使用值 4 填充所有条目,而不管传递的数组的维度如何。

ClearArray(new int[3], 4);
ClearArray(new int[3,3], 4);
ClearArray(new int[3, 3, 3, 3], 4);

方法签名显然看起来像

static void ClearArray(Array a, int val) { ... }

我知道如何遍历一个维度:

for (int i=0; i<a.GetLength(dimension); i++) 
{
    ...
}

注意:这个问题不是关于 2D 数组、3D 数组,也不是 4D 数组。它应该处理Array 对象上的Rank 属性所说的任何维度。

【问题讨论】:

    标签: c# arrays performance algorithm


    【解决方案1】:

    使用 Array.Rank 确定维数,然后使用 Array.GetLowerBound(int dimension) 和 Array.GetUpperBound(int dimension) 了解每个给定排名的范围。

    没有指定您的迭代器应该如何工作(例如,迭代顺序是否有任何语义)。但是,要实现 ClearArray(),迭代的顺序并不重要。

    使用 a.SetValue(object value, params int[] indices) 设置为您的 ClearArray 方法指定的值。

    【讨论】:

    • 是的。我也是这么想的。我们甚至可以使用 0-Array.GetLength,对吧?但是如何将这一切缝合在一起呢?递归?可以看到。
    • 迭代的顺序无关紧要,任何顺序都可以。但是每个元素只能访问一次。以及传递给 SetValue 的魔法索引 [] 参数?我们如何计算它?这是数组的实际迭代。
    【解决方案2】:

    使用Lexicographical order:
    索引是“数字”的序列。在每次迭代中,最后一个“数字”在边界内递增,然后下一个“数字”递增,等等

    Func<Array, int[]> firstIndex = 
      array => Enumerable.Range(0, array.Rank)
             .Select(_i => array.GetLowerBound(_i))
             .ToArray();
    
    Func<Array, int[], int[]> nextIndex = (array, index) => 
      {
        for (int i = index.Length-1; i >= 0; --i)
        {
           index[i]++;
           if (index[i] <= array.GetUpperBound(i))
             return index;
           index[i] = array.GetLowerBound(i);
        }
        return null;
      };
    
    for (var index = firstIndex(array); index != null; index = nextIndex(array, index))
    {
       var v = array.GetValue(index);
       ...
       array.SetValue(newValue, index);
    }
    

    【讨论】:

    • 聪明!但是如何遍历字典顺序呢?如果我从 [0,0,...,0] 开始,下一个索引是什么?我如何知道所有索引何时被迭代?
    • 谢谢!奇迹般有效。那里的一些怪癖使它跳过每个维度中的最后一个索引。
    【解决方案3】:

    简单的两步解决方案,未尝试优化:

        public static void ClearArray(Array a, int val)
        {
            int[] indices = new int[a.Rank];
            ClearArray(a, 0, indices, val);
        }
    
        private static void ClearArray(Array a, int r, int[] indices, int v)
        {
            for (int i = 0; i < a.GetLength(r); i++)
            {
                indices[r] = i;
    
                if (r + 1 < a.Rank)
                    ClearArray(a, r + 1, indices, v);
                else
                    a.SetValue(v, indices);
            }
        }
    

    【讨论】:

    • 不错!短而干净。像魅力一样工作。
    【解决方案4】:

    您可以从一堆零件中构建解决方案。解决方案的草图是:制作一堆从零到长度为1的序列,数组的每个维度都有一个序列。然后取这些序列的笛卡尔积。这会为您提供一系列索引。

    让我们从产品开始:

    static IEnumerable<IEnumerable<T>> CartesianProduct<T>(
        this IEnumerable<IEnumerable<T>> sequences) 
    { 
      IEnumerable<IEnumerable<T>> emptyProduct = 
        new[] { Enumerable.Empty<T>() }; 
      return sequences.Aggregate( 
        emptyProduct, 
        (accumulator, sequence) => 
          from accseq in accumulator 
          from item in sequence 
          select accseq.Concat(new[] {item})); 
    }
    

    我在这里讨论这段代码的工作原理:

    http://blogs.msdn.com/b/ericlippert/archive/2010/06/28/computing-a-cartesian-product-with-linq.aspx

    我们需要一个序列序列。什么序列?

    var sequences = from dimension in Enumerable.Range(0, array.Rank)
            select Enumerable.Range(array.GetLowerBound(dimension), array.GetLength(dimension));
    

    所以我们有序列,比如说:

    {
       { 0, 1, 2 },
       { 0, 1, 2, 3 }
    }
    

    现在计算产品:

    var product = sequences.CartesianProduct();
    

    所以产品是

    {
       { 0, 0 },
       { 0, 1 },
       { 0, 2 },
       { 0, 3 },
       { 1, 0 },
       { 1, 1 },
       { 1, 2 },
       { 1, 3 },
       { 2, 0 },
       { 2, 1 },
       { 2, 2 },
       { 2, 3 }
    }
    

    现在你可以说

    foreach(IEnumerable<int> indices in product)
        array.SetValue(value, indices.ToArray());
    

    这一切都有意义吗?

    【讨论】:

    • 是的,这确实很有意义!非常有教育意义,写得很清楚。谢谢。
    • @EricLippert select Enumerable.Range(0, array.GetUpperBound(dimension)); 应该是 select Enumerable.Range(0, array.GetLength(dimension)); 我相信。考虑大小为 1 的一维数组的情况。UpperBound 将为 0,range 将返回 0 值,当它应该返回 1。Range 需要一个计数,而不是一个要转到的值。
    • @DarkGray 好吧,如果您有很多尺寸和/或大长度,那么操作会很慢。没有真正的解决方法......
    • @Servy 在这个解决方案中大量的内存分配。在每个迭代上分配的索引数组。
    • @DarkGray:我同意你的看法。但是,看看更大的图景。原始海报是通过调用辅助方法动态填充未知维度的数组。这已经比填充一维数组慢了。如果这段代码对性能非常敏感,以至于每一纳秒都很重要,那么游戏就已经失败了;如果你有非常严格的性能要求,你不应该首先做这种事情。此外:这是程序中最慢的东西吗?如果不是,那么有一个更大的问题需要先解决。
    【解决方案5】:

    最快的解决方案是 Buffer.BlockCopy:

    static void ClearArray(Array array, int val)
    {
      var helper = Enumerable.Repeat(val, Math.Min(array.Length, 1024)).ToArray();
      var itemSize = 4;
    
    
      Buffer.BlockCopy(helper, 0, array, 0, helper.Length * itemSize);
      for (var len = helper.Length; len < array.Length; len *= 2)
      {
        Buffer.BlockCopy(array, 0, array, len * itemSize, Math.Min(len, array.Length - len) * itemSize);
      }
    }
    
    static int Count(Array array, Func<int, bool> predicate)
    {
      var helper = new int[Math.Min(array.Length, 4096)];
      var itemSize = 4;
    
      var count = 0;
      for (var offset = 0; offset < array.Length; offset += helper.Length)
      {
        var len = Math.Min(helper.Length, array.Length - offset);
        Buffer.BlockCopy(array, offset * itemSize, helper, 0, len * itemSize);
        for (var i = 0; i < len; ++i)
          if (predicate(helper[i]))
            count++;
      } 
      return count;
    }
    

    统计:

    time: 00:00:00.0449501, method: Buffer.BlockCopy
    time: 00:00:01.4371424, method: Lexicographical order
    time: 00:00:01.3588629, method: Recursed
    time: 00:00:06.2005057, method: Cartesian product with index array reusing
    time: 00:00:08.2433531, method: Cartesian product w/o index array reusing
    

    统计(计数函数):

    time: 00:00:00.0812866, method: Buffer.BlockCopy
    time: 00:00:02.7617093, method: Lexicographical order
    

    代码:

      Array array = Array.CreateInstance(typeof(int), new[] { 100, 200, 400 }, new[] { -10, -20, 167 });
      foreach (var info in new [] 
        { 
          new {Name = "Buffer.BlockCopy", Method = (Action<Array, int>)ClearArray_BufferCopy},
          new {Name = "Lexicographical order", Method = (Action<Array, int>)ClearArray_LexicographicalOrder}, 
          new {Name = "Recursed", Method = (Action<Array, int>)ClearArray_Recursed}, 
          new {Name = "Cartesian product with index array reusing", Method = (Action<Array, int>)ClearArray_Cartesian_ReuseArray}, 
          new {Name = "Cartesian product w/o index array reusing", Method = (Action<Array, int>)ClearArray_Cartesian}, 
        }
       )
      {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var count = 10;
        for (var i = 0; i < count; ++i)
          info.Method(array, i);
        stopwatch.Stop();
        Console.WriteLine("time: {0}, method: {1}", TimeSpan.FromTicks(stopwatch.Elapsed.Ticks / count), info.Name);
      }
    

    【讨论】:

    • +1 用于快速解决方案,甚至是 cmoparision!仅适用于填充数组,而不是遍历它。例如计算非零条目的数量或不适合这张图片。
    • @vidstige 使用 Buffer.BlockCopy 的计数方法是最快的(参见上面的示例)。 Eric 不喜欢多维数组,所以他们在 .net 中工作很慢 :)
    猜你喜欢
    • 2010-12-08
    • 1970-01-01
    • 1970-01-01
    • 2020-09-25
    • 1970-01-01
    • 2012-04-16
    • 2016-09-04
    相关资源
    最近更新 更多