在C#中如何向数组前面添加元素

39
在C#中,给定一个已填充的byte[] values,我想在数组前面添加值(byte)0x00。 我认为这将需要创建一个新数组并添加旧数组的内容。 速度是我的应用程序的重要方面。最佳方法是什么?
- 编辑 - byte [] 用于存储DSA(数字签名算法)参数。 操作每个数组只需要执行一次,但速度很重要,因为我可能在许多不同的byte [] 上执行此操作。

抱歉:您的意思是通过在位置0处添加并将其他所有内容移动到“index + 1”来执行prepend操作吗? - Andre Calil
2
@AndreCalil - 是的。Prepend的意思是添加到开头,与添加到末尾的Append相对。 - Oded
2
C# 数组是静态的,因此您需要创建一个副本才能在前面添加值。但是,您在问题中没有提供足够的细节以便进行明智的评论。您想要实现什么?数组有多大?您将如何频繁地更新它? - Peter Ruderman
如果你需要快速插入,为什么要使用数组呢?你应该使用类似链表的数据结构。 - Ed S.
该数组存储在DSA(数字签名算法)公钥中使用的参数,包括参数G、P、Q和Y。 - cytinus
显示剩余7条评论
8个回答

50
如果您只打算执行此操作一次,那么选择就不是很多。由Monroe的答案提供的代码应该可以胜任。
byte[] newValues = new byte[values.Length + 1];
newValues[0] = 0x00;                                // set the prepended value
Array.Copy(values, 0, newValues, 1, values.Length); // copy the old values
如果您需要执行此操作多次,则有更多选择。然而,将数据添加到数组的开头并不是一种高效的操作,因此您可以选择使用替代数据结构。 LinkedList 可以有效地在开头添加数据,但对于大多数任务来说它的效率较低,因为它涉及很多内存分配/释放并且还会失去内存局部性,因此这可能不是一个好的选择。
双端队列(也称为 deque)是非常适合您的一种数据结构。您可以有效地添加到开头或结尾,并且可以有效地访问结构中的任何位置的数据(但是您不能有效地在其他位置插入)。主要问题在于 .NET 没有提供 deque 的实现。您需要查找具有实现的第三方库。
通过跟踪“我需要添加的数据”(使用 List/Queue 等),等待尽可能长的时间来实际添加数据,以便尽量减少创建新数组的次数,并限制现有元素的复制次数,从而节省大量拷贝开销。
您还可以考虑调整结构,使其添加到末尾而不是开头(即使您知道以后需要将其反转)。如果您在短时间内经常添加,则将数据存储在 List 中(可以有效地添加到 end),并添加到末尾可能是值得的。根据您的需求,甚至可以制作一个类作为 List 的包装器,并隐藏它被反转的事实。您可以创建一个索引器将 i 映射到 Count-i 等,以便从外部看起来,尽管内部的 List 实际上将数据反向存储,但似乎正常存储数据。

指出LinkedList的O(1)插入属性,特别是与List在前面插入时的O(n)插入行为相比较,这一点值得赞赏。 - Monroe Thomas
@MonroeThomas 虽然这是正确的,但出于我提到的原因,这可能不是最佳选择。总的来说,LinkLists 对于大型集合的扩展性并不好,即使您只执行对该数据结构“有效”的操作。实际上,即使像遍历其他任何数据结构一样,遍历链接列表也非常低效。 - Servy
总体而言,我同意。在过去的五年中,我只使用过链表几次。关于他的用例最佳存储优化方案,发帖者并没有发布足够的信息来确定。 :) - Monroe Thomas
@MonroeThomas 如果你从我的回答中无法确定,我会说deque是最适合他需求的。现在如果基础库框架中已经存在一个deque就好了(尽管搜索C# Deque可以提供很多实现)。 - Servy

23

大家好,让我们来看一下这个问题的性能问题。

这不是一个回答,只是一个微小的基准测试,以查看哪个选项更有效。

那么,让我们设定场景:

  • 一个包含1,000,000个随机填充项的字节数组
  • 我们需要在第0项之前添加0x00

我们有3个选项:

  1. 手动创建和填充新数组
  2. 手动创建新数组并使用Array.Copy(@Monroe)
  3. 创建一个列表,加载数组,插入该项,然后将列表转换为数组

以下是代码:

    byte[] byteArray = new byte[1000000];

    for (int i = 0; i < byteArray.Length; i++)
    {
        byteArray[i] = Convert.ToByte(DateTime.Now.Second);
    }

    Stopwatch stopWatch = new Stopwatch();

    //#1 Manually creating and populating a new array;

    stopWatch.Start();

    byte[] extendedByteArray1 = new byte[byteArray.Length + 1];

    extendedByteArray1[0] = 0x00;

    for (int i = 0; i < byteArray.Length; i++)
    {
        extendedByteArray1[i + 1] = byteArray[i];
    }

    stopWatch.Stop();
    Console.WriteLine(string.Format("#1: {0} ms", stopWatch.ElapsedMilliseconds));
    stopWatch.Reset();

    //#2 Using a new array and Array.Copy

    stopWatch.Start();

    byte[] extendedByteArray2 = new byte[byteArray.Length + 1];
    extendedByteArray2[0] = 0x00;                                
    Array.Copy(byteArray, 0, extendedByteArray2, 1, byteArray.Length);

    stopWatch.Stop();
    Console.WriteLine(string.Format("#2: {0} ms", stopWatch.ElapsedMilliseconds));
    stopWatch.Reset();

    //#3 Using a List

    stopWatch.Start();

    List<byte> byteList = new List<byte>();
    byteList.AddRange(byteArray);
    byteList.Insert(0, 0x00);

    byte[] extendedByteArray3 = byteList.ToArray();

    stopWatch.Stop();
    Console.WriteLine(string.Format("#3: {0} ms", stopWatch.ElapsedMilliseconds));
    stopWatch.Reset();

    Console.ReadLine();

结果如下:

#1: 9 ms
#2: 1 ms
#3: 6 ms

我已经运行了多次,每次得到的数字都不同,但比例总是相同的:#2 始终是最有效的选择

我的结论:数组比列表更高效(尽管它们提供的功能较少),而且一些Array.Copy确实进行了优化(虽然我想要理解它)。

欢迎任何反馈意见。

最好的问候。

附:这不是一篇争论文章,我们在一个问答网站上来学习和教授知识。并且是学习


5
+1代表“这不是一篇关于剑术的帖子,我们在问答网站上是为了学习和教育。并且互相学习。” - Tom
谢谢。我觉得这里的人(包括我自己!)变得太情绪化了 =) - Andre Calil
2
Array.Copy通常会更快,因为它可以一次性复制更大的内存块(32位、64位或128位,取决于处理器类型),而不是像测试#1中的for循环。甚至可能有部分复制是并行完成的。如果使用int[]或long[]而不是byte[],我敢打赌测试#1和#2之间的差距会缩小。 - Monroe Thomas
2
内存复制是CPU指令(movl movsl)。由于数组是连续排列的,因此在数组之间移动内存非常快,因为CPU可以在单个命令中完成。 - PRMan

19

对于 .NET 4.7.1 及以上版本,最简单、最干净的方法是使用无副作用的 Prepend()

将一个值添加到序列的开头。

示例:

// Creating an array of numbers
var numbers = new[] { 1, 2, 3 };

// Trying to prepend any value of the same type
var results = numbers.Prepend(0);

// output is 0, 1, 2, 3
Console.WriteLine(string.Join(", ", results ));

非常简洁,非常适合我的用例!然而,OP确实指出“速度很重要”,而这种方法会导致复制 - 所以在性能方面可能不是最好的选择,因此被接受的答案可能更胜一筹。 - mindplay.dk
1
事实上,我只是感到遗憾,对于一个标题为“在C#数组中添加元素”的问题,竟然没有人提到Prepend - aloisdg
我喜欢这个答案,但建议更明确地说明正在发生的事情,例如类型、原地修改与新副本以及返回值。 - undefined

15

正如你所猜测的那样,最快的方法是创建一个新数组,长度为原来的长度加一,并将所有旧值复制到新数组中。

如果您要多次执行此操作,则建议使用 List<byte> 而不是 byte[],因为在增加底层存储时重新分配和复制的成本更有效地摊销;通常情况下,当向 List 添加或插入元素时,它的基础向量会按照两倍的因子进行增长,直到超过其当前容量。

...

byte[] newValues = new byte[values.Length + 1];
newValues[0] = 0x00;                                // set the prepended value
Array.Copy(values, 0, newValues, 1, values.Length); // copy the old values

3
当我需要频繁添加数据但同时又希望以O(1)的时间复杂度随机访问单个元素时,我会使用一个数组进行过度分配,以快速添加新值。这意味着您需要在另一个变量中存储实际内容长度,因为array.length将指示长度+填充。通过使用填充的一个插槽来追加新值,直到用完填充,才需要进行分配和复制。实际上,分配是分摊到几个追加操作中的。有速度空间权衡,如果您有许多这些数据结构,则程序中任何时候都可能使用相当多的填充。
同样的技巧也可以用于预处理。与追加一样,您可以在用户和实现之间引入接口或抽象:您可以有几个填充插槽,以便仅偶尔需要新的内存分配。正如一些人建议的那样,您还可以使用反转索引的追加数据结构实现前置界面。
我会将数据结构打包为某些通用集合接口的实现,以使接口对用户显得相当正常(例如,ArrayList之类)。
(此外,如果支持删除,则立即清除元素可能有助于减少gc负载。)
主要的一点是将实现和接口分开考虑,因为解耦它们可以让您灵活选择不同的实现方式或使用最小接口隐藏实现细节。
根据其适用性,您可以使用许多其他数据结构。Ropes或Gap Buffer; 另外,Trie也有一些有用的功能。请参见What is best data structure suitable to implement editor like notepad?

3

我知道这是一个非常古老的帖子,但我实际上喜欢使用lambda表达式。虽然我的代码可能不是最有效率的方式,但它易于阅读并且只需一行。我使用.Concat和ArraySegment的组合。

 string[] originalStringArray = new string[] { "1", "2", "3", "5", "6" };
 int firstElementZero = 0;
 int insertAtPositionZeroBased = 3;
 string stringToPrepend = "0";
 string stringToInsert = "FOUR"; // Deliberate !!!

 originalStringArray = new string[] { stringToPrepend }
            .Concat(originalStringArray).ToArray();

 insertAtPositionZeroBased += 1; // BECAUSE we prepended !!
 originalStringArray = new ArraySegment<string>(originalStringArray, firstElementZero, insertAtPositionZeroBased)       
                .Concat(new string[] { stringToInsert })
                .Concat(new ArraySegment<string>(originalStringArray, insertAtPositionZeroBased, originalStringArray.Length - insertAtPositionZeroBased)).ToArray();

2

最佳选择取决于您以后会如何使用此集合。 如果这是唯一可能发生的长度更改,那么最好的方法是创建一个具有一个额外插槽的新数组,并使用Array.Copy()完成其他操作。 无需初始化第一个值,因为新的C#数组总是归零:

byte[] PrependWithZero(byte[] input)
{
    var newArray = new byte[input.Length + 1];
    Array.Copy(input, 0, newArray, 1, input.Length);
    return newArray;
}

如果还会有其他可能改变长度的编辑,那么在性能方面最好的选择可能是一直使用一个List<byte>,只要添加不总是在开头进行。(如果是这种情况,甚至连链表也可能不是你可以轻易忽略的选项。)
var list = new List<byte>(input);
list.Insert(0, 0);

0

我知道这篇帖子已经超过4年了,但对于那些可能相关的人来说,Buffer.BlockCopy会更快。


这真的是这样吗?对于这个问题的答案并不完全一致。 - derHugo
是的。你链接中的答案说,在性能关键的情况下最好使用Buffer.BlockCopy。 - SubqueryCrunch

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