【问题标题】:How to use LINQ with a 2 dimensional array如何将 LINQ 与二维数组一起使用
【发布时间】:2015-03-24 12:08:15
【问题描述】:

我有一个看起来像这样的二维字节数组:

0 0 0 0 1

1 1 1 1 0

0 0 1 1 1

1 0 1 0 1

数组中的每个值只能是 0 或 1。上面的简化示例显示了 4 行,每行有 5 列。我试图弄清楚如何使用 LINQ 将索引返回到设置了最多 1 的行,在上面的示例中应该返回 1。

下面的非 LINQ C# 代码解决了这个问题:

static int GetMaxIndex(byte[,] TwoDArray)
{
   // This method finds the row with the greatest number of 1s set.
   //
   int NumRows = TwoDArray.GetLength(0);
   int NumCols = TwoDArray.GetLength(1);
   int RowCount, MaxRowCount = 0, MaxRowIndex = 0;
   //
   for (int LoopR = 0; LoopR < NumRows; LoopR++)
   {
      RowCount = 0;
      for (int LoopC = 0; LoopC < NumCols; LoopC++)
      {
         if (TwoDArray[LoopR, LoopC] != 0)
            RowCount++;
      }
      if (RowCount > MaxRowCount)
      {
         MaxRowCount = RowCount;
         MaxRowIndex = LoopR;
      }
   }
   return MaxRowIndex;
}

static void Main()
{
   byte[,] Array2D = new byte[4, 5] { { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 0 }, { 0, 0, 1, 1, 1 }, { 1, 0, 1, 0, 1 } };
   int MaxInd = GetMaxIndex(Array2D);
   Console.WriteLine("MaxInd = {0}", MaxInd);
}

所以,我的问题是:

  1. 如何使用 LINQ 来解决这个问题,在这里使用 LINQ 会比使用上面的非 LINQ 代码效率低吗?
  2. 是否可以使用 PLINQ 解决此问题?或者,假设每行至少有 1,000 列,直接使用任务并行库 (TPL) 并将每行中 1 的数量拆分到单独的线程中会更有效吗?

【问题讨论】:

  • 必须是byte[,] 还是IEnumerable&lt;IEnumerable&lt;byte&gt;&gt;byte[][]
  • @Los Firjoles 我可以让它成为 IEnumerable> 或 byte[][] 的锯齿状数组。
  • 我假设您已经看过像 stackoverflow.com/questions/3150678/…stackoverflow.com/questions/18673822/… 这样的答案...哪些建议不起作用/为什么?
  • @Alexei Levenkov 我确实看到了第一个链接,但不确定我是否看到了第二个。其他问题/解决方案没有任何分组代码,而且我还不是 LINQ 专家。在发布之前花了一些时间进行研究并试图弄清楚它如何与 LINQ 一起工作。非常感谢以下解决方案。我现在要做的是在上面的循环代码中添加一些并行代码,并将其与下面的每个解决方案进行比较,看看哪个最快。我会在几天后有更多时间时用结果更新 OP。

标签: c# arrays linq plinq


【解决方案1】:

使用 LINQ 处理多维数组很困难,但您可以这样做:

var arr = new [,] { { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 0 }, { 0, 0, 1, 1, 1 }, { 1, 0, 1, 0, 1 } };

var data =
    Enumerable.Range(0, 4)
        .Select(
            row =>
                new
                {
                    index = row,
                    count = Enumerable.Range(0, 5).Select(col => arr[row, col]).Count(x => x == 1)
                })
        .OrderByDescending(x => x.count)
        .Select(x => x.index)
        .First();

【讨论】:

    【解决方案2】:

    我会这样做。它或多或少与其他人相同,但没有任何Enumerable.Range(并不是那些有什么问题(我一直在使用它们)......它只是让代码在这种情况下更加缩进)。

    这个还包括 PLINQ 的东西。 TPL (async/await) 不适合这种情况,因为它是计算绑定的,而 TPL 更适合 I/O 绑定操作。如果您使用 async/await 而不是 PLINQ,您的代码最终会按顺序执行。这是因为 async/await 在线程被释放之前不会并行(并且它可以启动下一个任务......然后可以并行)并且纯同步函数(例如 CPU 的东西)不会真正等待。 ..他们会一路跑过去。基本上,它会在开始下一件事之前完成列表中的第一件事,使其按顺序执行。 PLINQ 显式启动并行任务并且没有这个问题。

    //arry is your 2d byte array (byte[,] arry)
    var maxIndex = arry
        .Cast<byte>() //cast the entire array into bytes
        .AsParallel() //make the transition to PLINQ (remove this to not use it)
        .Select((b, i) => new // create indexes
            {
                value = b,
                index = i
            })
        .GroupBy(g => g.index / arry.GetLength(1)) // group it by rows
        .Select((g, i) => new
            {
                sum = g.Select(g2 => (int)g2.value).Sum(), //sum each row
                index = i
            })
        .OrderByDescending(g => g.sum) //max by sum
        .Select(g => g.index) //grab the index
        .First(); //this should be the highest index
    

    就效率而言,使用 for 循环可能会获得更好的结果。我要问的问题是,哪个更具可读性和清晰性?

    【讨论】:

      【解决方案3】:

      1) 您可以通过这种方式使用 LINQ...

      private static int GetMaxIndex(byte[,] TwoDArray) {
          return Enumerable.Range(0, TwoDArray.GetLength(0))
                           .Select(
                               x => new {
                                   Index = x,
                                   Count = Enumerable.Range(0, TwoDArray.GetLength(1)).Count(y => TwoDArray[x, y] == 1)
                               })
                           .OrderByDescending(x => x.Count)
                           .First()
                           .Index;
      }
      

      ...您必须对其进行测试以查看 LINQ 是更快还是更慢。

      2) 可以使用 PLINQ。只需将ParallelEnumerable.Range 用于行索引生成器

      private static int GetMaxIndex2(byte[,] TwoDArray) {
          return ParallelEnumerable.Range(0, TwoDArray.GetLength(0))
                                   .Select(
                                       x => new {
                                           Index = x,
                                           Count = Enumerable.Range(0, TwoDArray.GetLength(1)).Count(y => TwoDArray[x, y] == 1)
                                       })
                                   .OrderByDescending(x => x.Count)
                                   .First()
                                   .Index;
      }
      

      【讨论】:

        【解决方案4】:
        // This code is extracted from
        // http://www.codeproject.com/Articles/170662/Using-LINQ-and-Extension-Methods-in-C-to-Sort-Vect
        private static IEnumerable<T[]> ConvertToSingleDimension<T>(T[,] source)
        {
            T[] arRow;
            for (int row = 0; row < source.GetLength(0); ++row)
            {
                arRow = new T[source.GetLength(1)];
                for (int col = 0; col < source.GetLength(1); ++col)
                    arRow[col] = source[row, col];
                yield return arRow;
            }
        }
        
        
        // Convert byte[,] to anonymous type {int index, IEnumerable<byte[]>} for linq operation
        var result = (from item in ConvertToSingleDimension(Array2D).Select((i, index) => new {Values = i, Index = index})
                     orderby item.Values.Sum(i => i) descending, item.Index
                     select item.Index).FirstOrDefault();
        

        【讨论】:

          【解决方案5】:

          从这个问题来看,对于您的代码“更有效”的内容,这实际上是一个两部分的答案。呈现的循环已经非常依赖资源,但意图可能更清楚。

          根据被移动的数据大小,即使是 10 倍,PLINQ 也会更加占用资源,这仅仅是因为启动线程需要做多少工作。

          1.) 使用 LINQ 可以使该方法更具可读性

          我遇到的大多数二维数组 LINQ 查询在搜索之前会将其转换为锯齿状数组(或数组数组)。这是一个帮助我们进行转换的辅助方法,并帮助使这个人看起来更干净:

          public static T[][] GetJagged<T>(this T[,] raw)
              {
                  int lenX = raw.GetLength(0);
                  int lenY = raw.GetLength(1);
          
                  T[][] jagged = new T[lenX][];
          
                  for (int x = 0; x < lenX; x++)
                  {
                      jagged[x] = new T[lenY];
                      for (int y = 0; y < lenY; y++)
                      {
                          jagged[x][y] = raw[x, y];
                      }
                  }
          
                  return jagged;
              }
          

          现在,我们剩下的就是为每个成员查询现在的一维数组,并返回每个成员的总和。在这里,我使用选择器(b =&gt; b),本质上是说如果有一个字节,则为Sum方法选择if。

          static int GetMaxIndexLINQ(byte[,] TwoDArray)
              {
                  byte[][] jagged = TwoDArray.GetJagged();
          
                  IEnumerable<int> rowSums = from bitRows in jagged
                                             select bitRows.Sum((b) => b);
          
                  int maxIndex = rowSums.Max();
                  int MaxRowIndex = Array.IndexOf(rowSums.ToArray(), maxIndex);
                  return MaxRowIndex;
              }
          

          这种方式非常清晰易读,即使读者不熟悉编码,也很容易了解这里发生的事情。

          我想指出,使您的代码更具可读性使其更高效。团队合作让梦想成真,团队成员越快清楚地了解代码中发生的事情,对每个人都越好。

          2.) 性能优化

          正如我之前所说,这里发生的事情并没有什么可以变得更精简的,任何方法调用或不必要的检查都会减慢这个过程。

          话虽如此,为了便于优化,需要进行一些小改动。因为在这种情况下我们只处理 1 和 0,所以我们可以使用编译器进行的内部优化,这对我们有好处。与其检查一个值是否为 0,不如将其添加到我们的运行总和中实际上要快得多!

          static int GetMaxIndex_EvenBetter(byte[,] TwoDArray)
              {
                  int NumRows = TwoDArray.GetLength(0);
                  int NumCols = TwoDArray.GetLength(1);
                  int RowCount, MaxRowCount = 0, MaxRowIndex = 0;
          
                  for (int row = 0; row < NumRows; row++)
                  {
                      RowCount = 0;
          
                      for (int col = 0; col < NumCols; col++)
                      {
                          RowCount += TwoDArray[row, col]; //See my change here
                      }
                      if (RowCount > MaxRowCount)
                      {
                          MaxRowCount = RowCount;
                          MaxRowIndex = row;
                      }
                  }
          
                  return MaxRowIndex;
              }
          

          在大多数其他情况下,您不会只使用 1 和 0,因此您确实希望在添加之前检查这些值,但是在这里没有必要。

          【讨论】:

            猜你喜欢
            • 2012-02-21
            • 1970-01-01
            • 2016-04-11
            • 1970-01-01
            • 2015-07-30
            • 2020-10-24
            • 2011-05-06
            • 1970-01-01
            • 2014-03-06
            相关资源
            最近更新 更多