将二维数组快速转换为列表(一维)的方法

35

我有一个二维数组,需要将其转换为列表(同一对象)。我不想使用 forforeach 循环来遍历每个元素并添加到列表中。有没有其他方法可以做到这一点?


4
你的二维数组(T[,])是矩形的还是交错的(T[][])? - CodesInChaos
2
你为什么想要避免循环? - CodesInChaos
2
你想要一个短的还是快的解决方案? - CodesInChaos
2
Yanshof:您能回应一下您所接受答案中的评论吗?您声称想要一个“快速”的解决方案(鉴于标题),但您已经接受了三个答案中最慢的一个。 - Jon Skeet
1
@naveen:不,我坚持我的评论。这是最慢的方法,所以任何寻求高效解决方案的人都不应该使用它。 - Jon Skeet
显示剩余4条评论
3个回答

63

好吧,你可以使用“blit”式的拷贝方式来实现,尽管这意味着要再做一份额外的拷贝 :(

double[] tmp = new double[array.GetLength(0) * array.GetLength(1)];    
Buffer.BlockCopy(array, 0, tmp, 0, tmp.Length * sizeof(double));
List<double> list = new List<double>(tmp);

如果你只需要一个一维数组,那么可以忽略最后一行 :)

Buffer.BlockCopy 是作为本地方法实现的,我期望它在验证后使用极高效的复制。接受 IEnumerable<T>List<T> 构造函数 在实现了 IList<T> 的情况下进行了优化,像 double[] 一样。它将创建正确大小的后备数组,并要求它将自身复制到该数组中。希望这也会使用 Buffer.BlockCopy 或类似的东西。

以下是三种方法(for循环,Cast<double>().ToList() 和 Buffer.BlockCopy)的快速基准测试:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        double[,] source = new double[1000, 1000];
        int iterations = 1000;

        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            UsingCast(source);
        }
        sw.Stop();
        Console.WriteLine("LINQ: {0}", sw.ElapsedMilliseconds);

        GC.Collect();
        GC.WaitForPendingFinalizers();

        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            UsingForLoop(source);
        }
        sw.Stop();
        Console.WriteLine("For loop: {0}", sw.ElapsedMilliseconds);

        GC.Collect();
        GC.WaitForPendingFinalizers();

        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            UsingBlockCopy(source);
        }
        sw.Stop();
        Console.WriteLine("Block copy: {0}", sw.ElapsedMilliseconds);
    }


    static List<double> UsingCast(double[,] array)
    {
        return array.Cast<double>().ToList();
    }

    static List<double> UsingForLoop(double[,] array)
    {
        int width = array.GetLength(0);
        int height = array.GetLength(1);
        List<double> ret = new List<double>(width * height);
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                ret.Add(array[i, j]);
            }
        }
        return ret;
    }

    static List<double> UsingBlockCopy(double[,] array)
    {
        double[] tmp = new double[array.GetLength(0) * array.GetLength(1)];    
        Buffer.BlockCopy(array, 0, tmp, 0, tmp.Length * sizeof(double));
        List<double> list = new List<double>(tmp);
        return list;
    }
}

结果(以毫秒为单位):

LINQ: 253463
For loop: 9563
Block copy: 8697

编辑:将 for 循环改为在每次迭代中调用 array.GetLength() 后,for 循环和块复制的时间大致相同。


2
这个问题的主要问题是它可能会在大对象堆上留下一个大的临时数组。 - CodesInChaos
1
@CodeInChaos:绝对没错。我们无法告诉List<T>只使用给定的数组,这真是一件痛苦的事情:(不过我认为这仍然比循环要快。 - Jon Skeet
1
告诉 List<T> 使用特定数组的问题在于,我们可能会告诉多个列表使用同一个数组。不确定这在实践中会成为多大的问题。 - CodesInChaos
关于循环解决方案的一个有趣观察是,如果交换内部和外部循环,它会慢两倍。这很可能是由于CPU缓存在顺序读/写时工作得更好。 - CodesInChaos
@CodeInChaos:是的,这是一个相当知名的现象。我认为原因与CPU缓存没有太大关系,更多地与内存中数据的物理位置有关。如果你有一个以行主序列索引的二维数组,通过顺序迭代数组比跳来跳去要快得多。在此处阅读更多内容(https://dev59.com/NEbRa4cB1Zd3GeqPxR94)和(https://dev59.com/pXNA5IYBdhLWcg3wWsYA)。 - Cody Gray
显示剩余5条评论

42

如果你想要一个一行代码的方式将 double[,] 转化为 List<double>,可以使用以下方法:

double[,] d = new double[,]
{
    {1.0, 2.0},
    {11.0, 22.0},
    {111.0, 222.0},
    {1111.0, 2222.0},
    {11111.0, 22222.0}
};
List<double> lst = d.Cast<double>().ToList();


不过,如果你正在寻找高效的方法,我宁愿说你不要使用这段代码。
请按照下面提到的两个答案之一进行操作。两种方法都采用了更好的技术实现。


5
除了其他事情之外,这将导致将数组中的每个“double”限制在一定范围内…它的性能会很差。 - Jon Skeet
1
@Danny:我真的不确定这种方法比for循环更清晰或更易理解,而OP明确希望避免使用for循环。更不用说标题中还有“快速”两个字。 - Cody Gray
10
在我的快速基准测试中,对于一个1000 x 1000的数组,这个方法的执行速度比for循环或Buffer.BlockCopy解决方案慢了30倍以上。考虑到标题中的“快速”一词,我感到非常惊讶它竟然被接受了。 - Jon Skeet
2
@downvoters:感谢你们让我知道我比JonSkeet少了多少。 :) 请理解我不会删除答案,因为OP在某个地方使用了这段代码并且很满意。告诉我,你们中有多少人在企业级别工作?有趣。 - naveen
2
@naveen:我没有看到真正的证据,而且考虑到原帖作者可以通过复制粘贴任何答案并调整变量名来使用它们,按照这个定义,它们都是同样“快”的。即使你认为这是最可能的意图,你的答案也没有提供适当的低效指示,以便为所有读者服务,而不仅仅是原始帖子的作者。 - Jon Skeet
显示剩余6条评论

11

for循环是最快的方法。

你可能可以使用LINQ,但那会更慢。而且尽管你不必亲自编写循环,但在幕后仍然有一个循环。

  • 对于嵌套数组,您可以尝试使用arr.SelectMany(x=>x).ToList()
  • 对于T[,],您只需使用arr.ToList()即可,因为T[,]IEnumerable<T>返回2D数组中的所有元素。看起来2D数组只实现了IEnumerable但没有IEnumerable<T>所以您需要像yetanothercoder建议的那样插入一个Cast<double>。这将由于装箱而使其变慢。

唯一能使代码比朴素的循环更快的方法是计算元素数量并使用正确的容量构造List,使其不需要增长。
如果您的数组是矩形的,您可以将大小计算为width*height;对于嵌套数组,这可能较难。

int width=1000;
int height=3000;
double[,] arr=new double[width,height];
List<double> list=new List<double>(width*height);
int size1=arr.GetLength(1);
int size0=arr.GetLength(0);
for(int i=0;i<size0;i++)
{  
  for(int j=0;j<size1;j++)
    list.Add(arr[i,j]);
}

理论上可能使用私有反射和不安全的代码来进行原始内存复制,以使其更快。但我强烈建议不要这样做。


1
你能给出一个在for循环中的样例,这样我就可以将其与我的Buffer.BlockCopy方法进行基准测试了吗?我期望我的方法更快,但我想确保我正在测试正确的东西... - Jon Skeet
我认为应该是 arr.Cast<T>().ToList() 吧? - Cheng Chen
看起来你的代码大约快了两倍,@Jon - CodesInChaos
@Danny 这就是为什么在你写评论的时候我编辑了它。 - CodesInChaos
1
@CodeInChaos:你可以通过在每次迭代时不调用“GetLength”来优化自己的代码...但在我的测试中,Buffer.BlockCopy仍然稍微快一些。 - Jon Skeet
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接