【问题标题】:Modifying an array "in-place" in C#?在 C# 中“就地”修改数组?
【发布时间】:2019-03-18 06:18:50
【问题描述】:

有一个leetCode problem 带有类似提示

给定一个数组“nums”和一个值“val”,删除它的所有实例 就地取值并返回新的长度。

不要为另一个数组分配额外的空间,你必须这样做 使用 O(1) 额外内存就地修改输入数组。

元素的顺序可以改变。你离开什么都没关系 超出新长度。

示例 1:

给定 nums = [3,2,2,3], val = 3,

您的函数应返回长度 = 2,前两个元素为 nums 为 2。

这个问题在 C#(网站上解决这个问题的支持语言之一)中有意义吗?数组的长度不能修改,只能分配一个新的数组。

【问题讨论】:

  • 在 C# 中我认为它需要一个临时数组或类似的
  • “就地”意味着你也不能分配一个新的数组。保留一个单独的 Length 变量并不少见,这在像 C 这样的语言中是必需的。这是最有效的方法。 List<>.RemoveAll() 为你做。

标签: c# arrays .net algorithm


【解决方案1】:

看起来你正在寻找算法的解释,所以我会尝试解释它。

您正在“重写”数组。考虑让游标遍历读取数据的数组,同时让游标在其后面写入值。在这种情况下,“读取”光标可以是 foreach 循环或带有索引器的 for 循环。

测试数据:

[12,34,56,34,78]

我们想删除34,我们从位置0(即values[0])的两个游标开始,“newLength = 0”代表“新”数组的长度,因此“写”游标的位置目前居住:

[12,34,56,34,78]
 ^r
 ^w
newLength: 0

读取的第一个元素 12 不匹配,因此我们将该元素包含在“新”数组中,方法是将其写入数组中由“新”数组的当前长度表示的位置(开始, 长度为 0,所以是 values[0]。在这种情况下,我们正在向该元素写入相同的值,所以没有任何变化,我们通过增加“新”数组的长度将写入光标移动到下一个位置

[12,34,56,34,78]
    ^r
    ^w
newLength: 1

现在下一个元素确实匹配要删除的值,所以我们不想将它包含在新数组中。我们通过不增加“新”数组的长度并在读取光标移动到下一个元素时将写入光标留在原处来做到这一点:

[12,34,56,34,78]
       ^r
    ^w
newLength: 1

如果这个数组只有两个元素,我们就完成了,数组中的值没有改变,但返回的长度只有1。随着我们有更多的元素,让我们继续看看会发生什么。我们读取56,它与要删除的值不匹配,因此我们将其“写入”到新“长度”指定的位置,然后增加长度:

[12,56,56,34,78]
          ^r
       ^w
newLength: 2

现在我们读取了一个匹配的值,所以我们跳过写入它:

[12,56,56,34,78]
             ^r
       ^w
newLength: 2

而且最终的值不匹配,所以我们写在“length”指定的位置,并增加长度:

[12,56,78,34,78]
                ^r
          ^w
newLength: 3

“读取”光标现在超出了数组的长度,所以我们完成了。新的“长度”值现在是3,因为我们向数组“写入”了三个值。将数组与newLength 值结合使用,在功能上会产生“新”数组[12,56,78]

这是@TheGeneral 使用指针的功能正确但不安全的安全实现:

public static int DoStuffSafely( int[] values, int valueToRemove, int length )
{
    var newLength = 0;

    // ~13.5% slower than @TheGeneral's unsafe implementation
    foreach( var v in values )
    {
        // if the value matches the value to remove, skip it
        if( valueToRemove != v )
        {
            // newLength tracks the length of the "new" array
            // we'll set the value at that index
            values[ newLength ] = v;
            // then increase the length of the "new" array
            ++newLength;
            // I never use post-increment/decrement operators
            // it requires a temp memory alloc that we toss immediately
            // which is expensive for this simple loop, adds about 11% in execution time
        }
    }

    return newLength;
}

编辑:为@Richardissimo

                values[ newLength ] = v;
00007FFBC1AC8993  cmp         eax,r10d  
00007FFBC1AC8996  jae         00007FFBC1AC89B0  
00007FFBC1AC8998  movsxd      rsi,eax  
00007FFBC1AC899B  mov         dword ptr [r8+rsi*4+10h],r11d  
                ++newLength;
00007FFBC1AC89A0  inc         eax

                values[ newLength++ ] = v;
00007FFBC1AD8993  lea         esi,[rax+1]
00007FFBC1AD8996  cmp         eax,r10d  
00007FFBC1AD8999  jae         00007FFBC1AD89B3  
00007FFBC1AD899B  movsxd      rax,eax  
00007FFBC1AD899E  mov         dword ptr [r8+rax*4+10h],r11d  
00007FFBC1AD89A3  mov         eax,esi

【讨论】:

  • 不错的答案,现在我最好回到我应该做的事情
  • 你我都
  • “我从不使用后增量/减量...它需要临时内存分配” - 我记得旧 C/C++ 编译器中的这一点,但它是 doesn't apply to C#
  • @Richardissimo - 正如我在另一个答案的评论中所说,当不使用预先递增/递减的值(即编译器对其进行优化)时,使用它似乎并不重要,但它肯定使用时会影响性能(即values[ newLength++ ] 确实比values[ newLength ]; newLength++; 慢)。我在两种情况下都针对边缘情况(很少匹配)进行了上述测试,平均性能差异约为 11%。自己试试吧!因此,我建议使用++i 而不是i++ 来养成从不使用后自增/自减运算符的习惯
  • 我见过的地方,如我链接到的问题的接受答案所示:它们编译为相同的 IL,所以我对你所描述的内容感兴趣,因为这意味着情况并非如此。也许您应该考虑发布您的代码及其生成的 IL 作为该问题的答案。
【解决方案2】:

一种完全荒谬的方法,使用unsafefixed指针...为什么?那么为什么不呢。此外,任何 leet 代码问题都应该在其求解中使用 leet 指针。

给定

public static unsafe int Filter(this int[] array, int value, int length)
{
   fixed (int* pArray = array)
   {
      var pI = pArray;
      var pLen = pArray + length;
      for (var p = pArray; p < pLen; p++)
         if (*p != value)
         {
            *pI = *p;
            pI++;
         }
      return (int)(pI - pArray);
   }
}

用法

var input = new[] {23,4,4,3,32};
var length = input.Filter(4, input.GetLength(0));

for (var i = 0; i < length; i++)
   Console.WriteLine(input[i]);

输出

23
3
32

仅供参考,正常的安全解决方案如下所示

var result = 0;
for (var i = 0; i < length; i++)
   if (array[i] != value) array[result++] = array[i];
return result;

基准测试

因为我打开了我的基准测试器并且非常好奇,所以测试优化总是好的,因为你永远不知道。每个测试在每个规模上运行 10000 次,每次运行都会收集垃圾。

----------------------------------------------------------------------------
Mode             : Release (64Bit)
Test Framework   : .NET Framework 4.7.1 (CLR 4.0.30319.42000)
----------------------------------------------------------------------------
Operating System : Microsoft Windows 10 Pro
Version          : 10.0.17134
----------------------------------------------------------------------------
CPU Name         : Intel(R) Core(TM) i7-2600 CPU @ 3.40GHz
Description      : Intel64 Family 6 Model 42 Stepping 7
Cores (Threads)  : 4 (8)      : Architecture  : x64
Clock Speed      : 3401 MHz   : Bus Speed     : 100 MHz
L2Cache          : 1 MB       : L3Cache       : 8 MB
----------------------------------------------------------------------------

测试 1

--- Standard input ------------------------------------------------------------
| Value      |    Average |    Fastest |    Cycles | Garbage | Test |    Gain |
--- Scale 100 -------------------------------------------------- Time 9.902 ---
| Array      | 434.091 ns | 300.000 ns |   3.731 K | 0.000 B | Base |  0.00 % |
| UnsafeOpti | 445.116 ns | 300.000 ns |   3.662 K | 0.000 B | N/A  | -2.54 % |
| Unsafe     | 448.286 ns | 300.000 ns |   3.755 K | 0.000 B | N/A  | -3.27 % |
--- Scale 1,000 ----------------------------------------------- Time 10.161 ---
| UnsafeOpti |   1.421 µs | 900.000 ns |   7.221 K | 0.000 B | N/A  | 17.70 % |
| Array      |   1.727 µs |   1.200 µs |   8.230 K | 0.000 B | Base |  0.00 % |
| Unsafe     |   1.740 µs |   1.200 µs |   8.302 K | 0.000 B | N/A  | -0.78 % |
--- Scale 10,000 ---------------------------------------------- Time 10.571 ---
| UnsafeOpti |  10.910 µs |   9.306 µs |  39.518 K | 0.000 B | N/A  | 20.03 % |
| Array      |  13.643 µs |  12.007 µs |  48.849 K | 0.000 B | Base |  0.00 % |
| Unsafe     |  13.657 µs |  12.007 µs |  48.907 K | 0.000 B | N/A  | -0.10 % |
--- Scale 100,000 --------------------------------------------- Time 15.443 ---
| UnsafeOpti | 105.386 µs |  84.954 µs | 362.969 K | 0.000 B | N/A  | 19.93 % |
| Unsafe     | 130.150 µs | 110.771 µs | 447.383 K | 0.000 B | N/A  |  1.12 % |
| Array      | 131.621 µs | 113.773 µs | 452.262 K | 0.000 B | Base |  0.00 % |
--- Scale 1,000,000 ------------------------------------------- Time 22.183 ---
| UnsafeOpti |   1.556 ms |   1.029 ms |   5.209 M | 0.000 B | N/A  | 23.13 % |
| Unsafe     |   2.007 ms |   1.303 ms |   6.780 M | 0.000 B | N/A  |  0.85 % |
| Array      |   2.024 ms |   1.354 ms |   6.841 M | 0.000 B | Base |  0.00 % |
-------------------------------------------------------------------------------

【讨论】:

  • 在这种情况下使用带指针的不安全方法有什么好处?你对花括号有什么看法?
  • 性能*,因为指针让您可以直接访问传染性内存
  • 没问题,您可能还想在答案中将“aught”编辑为“ought”。我们应该争取将“表演者”放入字典reference
  • 顺便说一下,如果可能的话,你想在安全版本的循环中使用array.Length - 它允许你跳过三个溢出检查中的两个,这应该使解决方案更接近一点到优化的不安全版本。
  • @thebenman 这是我写的一个专门的工具,用来做我需要的各种事情。虽然 BenchmarkDotNet 可能是你想去的地方
【解决方案3】:

这个问题在 C# 中有意义吗? 是的。

[C#中]不能修改数组的长度,只能分配一个新的数组。 绝对正确。所以重新阅读这个问题,看看你可以做什么。我不想给出答案,因为很明显你会从自己解决这个问题中学到东西。所以在这里停止阅读;如果您需要提示,请继续阅读。

提示:关注这部分...

元素的顺序可以改变。在新长度之外留下什么并不重要。

【讨论】:

  • 我知道如何做一个不稳定的分区来把“删除”的值放在后面。但我看不出这是如何解决问题的,它专门要求删除值(即不在同一个数组中)。
  • @iterator 我认为你误解了这个问题,它想要相同的数组,它不关心最后是什么,它只关心从开始到删除给定值新长度
猜你喜欢
  • 2019-02-20
  • 2014-10-29
  • 1970-01-01
  • 2018-07-25
  • 1970-01-01
  • 2022-10-12
  • 2019-11-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多