在C#中“就地”修改数组?

时间:2018-10-13 03:39:50

标签: c# arrays .net algorithm

有一个leetCode problem提示类似

  

给出一个数组“ nums”和一个值“ val”,删除该数组的所有实例   值就位并返回新长度。

     

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

     

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

     

示例1:

     

给出数字= [3,2,2,3],val = 3,

     

您的函数应返回length = 2,其中前两个元素为   nums是2。

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

3 个答案:

答案 0 :(得分:3)

此问题在C#中是否有意义? 是的。

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

提示:专注于这一部分...

  

可以更改元素的顺序。超出新长度后剩下的都无所谓。

答案 1 :(得分:1)

使用unsafefixed Pointers 完全荒谬的方法...为什么?好吧,为什么不呢。此外,任何 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 % |
-------------------------------------------------------------------------------

答案 2 :(得分:1)

看起来您正在寻找算法的解释,所以我将尽力解释它。

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

测试数据:

[12,34,56,34,78]

,我们要删除34,我们从两个光标都在位置0(即values[0])开始,其中“ newLength = 0”代表“新”数组的长度,因此“ write”光标当前位于:

[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

“ read”游标现在超过了数组的长度,所以我们完成了。由于我们将三个值“写入”数组,因此新的“长度”值现在为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