在 C# 中对二维数组进行零填充

Zero padding a 2D array in C#

我目前在使用零填充二维数组时遇到问题。我想将我的数组中的当前数据传输到一个新数组,它是完全相同的数组,但周围有一个 0 的边框。 示例:

|1 2 3|

|4 5 6|

|7 8 9|

应该变成

|0 0 0 0 0|

|0 1 2 3 0|

|0 4 5 6 0|

|0 7 8 9 0|

|0 0 0 0 0|

 int[,] Array = new int[,] { { 1, 2, 3 }, { 3, 4, 5 }, { 6, 7, 8 } };
        
        int[,] ArrayZeroPad = new int[Array.GetLength(0) + 2, Array.GetLength(1) + 2];
        for (int y = 0; y < Array.GetLength(1); y++)
        {

            for (int x = 0; x < ArrayZeroPad.GetLength(0); x++)
            {
                if (y == 0)
                { ArrayZeroPad[y, x] = 0; }
                else if (y == ArrayZeroPad.GetLength(1))
                { ArrayZeroPad[y, x] = 0; }
                else if (x == 0)
                {
                    ArrayZeroPad[y, x] = 0;

                }
                else if (x == ArrayZeroPad.GetLength(0))
                { ArrayZeroPad[y, x] = 0; }
                else ArrayZeroPad[y, x] = Array[y, x];
            }
        }
        for (int y = 0; y < ArrayZeroPad.GetLength(1); y++)
        {
            Console.WriteLine();
            for (int x = 0; x < ArrayZeroPad.GetLength(0); x++)
            { Console.Write(ArrayZeroPad[y, x]); }
            Console.ReadLine();
        }
    }

这就是我到目前为止所遇到的问题,但我一直卡在越界错误中,有没有人可以通过一些解释为我解决这个问题?

亲切的问候, D.

您似乎混淆了尺寸 - Array.GetLength(0) 是访问中的第一个 Array[i, j]Array.GetLength(1) 是第二个。您还可以通过扫描 Array 元素并将目标索引调整一个来简化复制,您不需要将其他元素显式设置为 0 因为它会为您完成(除非您使用 stackallocskipping local init 但我非常怀疑情况是否如此):

var length0 = Array.GetLength(0);
var length1 = Array.GetLength(1);
for (int i = 0; i < length0; i++)
{
    for (int j = 0; j < length1; j++)
    {
        ArrayZeroPad[i + 1, j + 1] = Array[i, j];
    }
}

并且在“打印”方法中 - y 应该是第一个维度,x - 第二个维度:

var length = ArrayZeroPad.GetLength(0);
for (int y = 0; y < length; y++)
{
    Console.WriteLine();
    var i = ArrayZeroPad.GetLength(1);
    for (int x = 0; x < i; x++)
    {
        Console.Write(ArrayZeroPad[y, x]);
    }
    Console.ReadLine();
}
        int[,] Array = new int[,] { { 1, 2, 3 }, { 3, 4, 5 }, { 6, 7, 8 } };
        int[,] ArrayZeroPad = new int[Array.GetLength(0) + 2, Array.GetLength(1) + 2];

        for (int x = 0; x < ArrayZeroPad.GetLength(0); x++)
        {
            for (int y = 0; y < ArrayZeroPad.GetLength(0); y++)
            {
                //First row and last row
                if (x == 0 || x == ArrayZeroPad.GetLength(0) - 1)
                    ArrayZeroPad[x, y] = 0;
                else
                {
                    //Fist column and last column
                    if (y == 0 || y == ArrayZeroPad.GetLength(0) - 1)
                        ArrayZeroPad[x, y] = 0;
                    else
                    {
                        //Content
                        ArrayZeroPad[x, y] = Array[x-1, y-1];
                    }
                }
            }
        }

您也可以使用 Array.Copy() 解决此问题。如果您需要最高性能并且数组足够大,那么这可能比显式复制每个元素更快:

public static int[,] Pad(int[,] input)
{
    int h = input.GetLength(0);
    int w = input.GetLength(1);
    var output = new int[h+2, w+2];

    for (int r = 0; r < h; ++r)
    {
        Array.Copy(input, r*w, output, (r+1)*(w+2)+1, w);
    }

    return output;
}

(r+1)*(w+2)+1 需要一些解释。 Array.Copy() 将二维数组视为线性一维数组,您必须将副本的目标偏移量指定为距一维数组开头的偏移量(按行优先顺序)。

由于 w 是输入数组的宽度,而 r 是输入数组的当前行,因此当前输入行的副本目标将是输出行号, (r+1) 乘以输出行宽 (w+2),加上 1 以占输出数组中 0 的左侧列。

使用 Buffer.BlockCopy()(对字节进行操作)可能会更快:

public static int[,] Pad(int[,] input)
{
    int h = input.GetLength(0);
    int w = input.GetLength(1);
    var output = new int[h+2, w+2];

    for (int r = 0; r < h; ++r)
    {
        Buffer.BlockCopy(input, r*w*sizeof(int), output, ((r+1)*(w+2)+1)*sizeof(int), w*sizeof(int));
    }

    return output;
}

一如既往,只有在性能至关重要的情况下才值得担心,即便如此,也只有在您对代码进行基准测试以验证它实际上 更快之后才值得担心。

这不是您要问的(我认为完全不同的替代方案会很有趣)。

这是一个 No-Copy 版本,适用于任何类型、任何大小的数组。如果原始数组非常大(因为它不需要副本),这是合适的。

它使用二维索引器,returns 边缘项目的默认值 T(零或空),并使用原始数组(具有索引偏移量)用于非边缘值:

public class ZeroPadArray <T>
{
    private readonly T[,] _initArray;

    public ZeroPadArray(T[,] arrayToPad)
    {
        _initArray = arrayToPad;
    }

    public T this[int i, int j]
    {
        get
        {
            if (i < 0 || i > _initArray.GetLength(0) + 1)
            {
                throw new ArgumentOutOfRangeException(nameof(i),
                    $@"Index {nameof(i)} must be between 0 and the width of the padded array");
            }
            if (j < 0 || j > _initArray.GetLength(1) + 1)
            {
                throw new ArgumentOutOfRangeException(nameof(j),
                    $@"Index {nameof(j)} must be between 0 and the width of the padded array");
            }

            if (i == 0 || j == 0)
            {
                return default(T);
            }

            if (i == _initArray.GetLength(0) + 1)
            {
                return default(T);
            }

            if (j == _initArray.GetLength(1) + 1)
            {
                return default(T);
            }
            //otherwise, just offset into the original array
            return _initArray[i - 1, j - 1];
        }
    }
}

我刚刚通过一些 Debug.Assert 调用对其进行了测试。测试覆盖率很低,但足以说“这可能有效”:

int[,] array = new int[,] { { 1, 2, 3 }, { 11, 12, 13 }, { 21, 22, 23 } };
var paddedArray = new ZeroPadArray<int>(array);
Debug.Assert(paddedArray[0, 0] == 0);
Debug.Assert(paddedArray[4,4] == 0);
Debug.Assert(paddedArray[2,3] == 13);

最后,为了好玩,我添加了一个不错的小 hack 来创建这些东西需要更少的输入。当你调用一个方法时,编译器通常能够从方法参数中推断出对象的泛型类型。这不适用于构造函数。这就是为什么你需要指定 new ZeroPadArray<int>(array) 即使 array 显然是 int.

的数组

解决此问题的方法是创建第二个非泛型 class,用作创建事物的静态工厂。类似于:

public static class ZeroPadArray
{
    public static ZeroPadArray<T> Create<T>(T[,] arrayToPad)
    {
        return new ZeroPadArray<T>(arrayToPad);
    }
}

现在,不用输入:

var paddedArray = new ZeroPadArray<int>(array);

您可以输入:

var paddedArray = ZeroPadArray.Create(array);

为您节省了两个字符的输入(但是,您需要承认输入 <int> 令人沮丧)。