简体   繁体   中英

Accessing multi-dimensional array with one-dimensional index

See the piece of code below. What is the way to access a multi-dimensional array with a one-dimensional index. Foreach can do it. Yeah I know, IEnumerable with yield isn't the same as an index. Should I use a foreach and create a new array? Or can I do it without creating a new array?

    int[,] myArray = new int[2, 2];
    myArray[0,0] = 1;
    myArray[1,1] = 2;
    myArray[0,0] = 3;
    myArray[1,1] = 4;
    foreach (var value in myArray)
    {
        Console.Write(value);
    }

    var valueAtIndex = myArray[2]; //Won't compile, error: Wrong number of indices inside []; expected 2

If by one-dimensional index you mean an index that wraps around the multidimensional array's rows in order from top to bottom from left to right, then you can calculate x and y from it using this formula:

x = index % width
y = index / width

If you want not just to read the values but also set them in the same order as foreach traverses the array, you can use the following general indexer class:

public class ArrayIndexer
{
    readonly int totalLength;
    readonly int lastIndexLength;
    readonly int[] lengths;
    readonly int[] lowerBounds;
    int current;
    readonly int[] currentZeroBased;

    public ArrayIndexer(int[] lengths, int[] lowerBounds)
    {
        lastIndexLength = lengths[lengths.Length - 1];
        totalLength = lengths[0];
        for (int i = 1; i < lengths.Length; i++)
        {
            totalLength *= lengths[i];
        }
        this.lengths = lengths;
        this.lowerBounds = lowerBounds;
        currentZeroBased = new int[lengths.Length];
        current = -1;
    }

    public bool MoveNext()
    {
        current++;
        if (current != 0)
        {
            int currLastIndex = current % lastIndexLength;
            currentZeroBased[currentZeroBased.Length - 1] = currLastIndex;
            if (currLastIndex == 0)
            {
                for (int i = currentZeroBased.Length - 2; i >= 0; i--)
                {
                    currentZeroBased[i]++;
                    if (currentZeroBased[i] != lengths[i])
                        break;
                    currentZeroBased[i] = 0;
                }
            }
        }
        return current < totalLength;
    }

    public int[] Current
    {
        get
        {
            int[] result = new int[currentZeroBased.Length];
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = currentZeroBased[i] + lowerBounds[i];
            }
            return result;
        }
    }
}

And you can use it like this for setting the whole array:

int[,] myArray = new int[2, 2];
ArrayIndexer arrayIndexer = new ArrayIndexer(new[] {2, 2}, new[] {0, 0});
int i = 0;
while (arrayIndexer.MoveNext())
{
    myArray.SetValue(++i, arrayIndexer.Current);
}

How about aproaching it from the other end?

int rows=2, cols=2;
int[] myArray = new int[rows*cols];

public void setMyArray(int[] myArray, int x, int y, int value)
{
    myArray[(y)*cols + x] = value;
}

setMyArray(0,0,1);
setMyArray(0,1,2);
setMyArray(1,0,3);
setMyArray(1,1,4);

As for the enumerator, it's really simple - it only allows sequential access. Internally, it holds an array of the indices and increments them one by one. It doesn't use any hack to make the array one-dimensional or anything - that would be an ugly violation of the framework guarantees. And there's no yield either, this was written long before yield was implemented :)

You could write such a hack yourself, of course, using unsafe code. However, it's a really bad idea. You don't really know how such arrays are organized in memory - that's an implementation detail of the runtime. Just don't.

Instead, figure out a reasonable addressing schema. If it makes sense to have a single index, why is the array multi-dimensional in the first place? If it makes sense to have the array multi-dimensional, why are you accessing it in a single-dimensional manner?

If it does make sense, for some reason, you probably want to make your own wrapper class around some internal array, rather than using just arrays. It's as simple as

public class DualAccessArray<T>
{
  private readonly T[,] _internalArray;
  private readonly int _width;
  private readonly int _height;

  public DualAccessArray(int width, int height)
  {
    _width = width;
    _height = height;
    _internalArray = new T[width, height];
  }

  public T this[long index]
  {
    get { return _internalArray[index / _width, index % _width]; }
    set { _internalArray[index / _width, index % _width] = value; }
  }

  public T this[int x, int y]
  {
    get { return _internalArray[x, y]; }
    set { _internalArray[x, y] = value; }
  }
}

Which you can then use like

var ar = new DualAccessArray<string>(10, 10);
ar[15] = "Hi!";
Console.WriteLine(ar[1, 5]);

You can fit this to your needs. For example, it might make more sense to have a different kind of addressing and memory layout, it might make sense to have the array single-dimensional, you might want to wrap an existing array... it all depends on what you're actually trying to do.

Foreach can do it

No it can't. There's really no analogy.

Since a multi-dimensional array is ipso facto an array with more than one dimension, you've got three options.

  1. Stop using a multi-dimensional array. If this sort of operation predominates your use then an array of arrays (a "jagged array") may be a better match to what you want.

  2. Ignore the details of the array and use pointers into memory. Best avoided if not vitally necessary.

  3. Just use both indices. You can find the range of the other index easily enough, so do that.

The following will operate on a two-dimensional array, and when given a value for the first index iterate through all the values of that index.

public static IEnumerable<T> GetRow<T>(this T[,] array, int index)
{
  int last = array.GetUpperBound(1);
  for(int idx = array.GetLowerBound(1); idx <= last; ++idx)
    yield return array[index, idx];
}

Answering the title here. Let's say we need to access an arbitrary-dimensional array with one-dimensional index.

var test = new [,,,]
{
    {
        {
            { 00, 01, 02 }, { 03, 04, 05 }, { 06, 07, 08 }
        },
        {
            { 09, 10, 11 }, { 12, 13, 14 }, { 15, 16, 17 }
        },
        {
            { 18, 19, 20 }, { 21, 22, 23 }, { 24, 25, 26 }
        }
    },
    {
        {
            { 27, 28, 29 }, { 30, 31, 32 }, { 33, 34, 35 }
        },
        {
            { 36, 37, 38 }, { 39, 40, 41 }, { 42, 43, 44 }
        },
        {
            { 45, 46, 47 }, { 48, 49, 50 }, { 51, 52, 53 }
        }
    }
};

Func<int, IEnumerable<int>, IEnumerable<int>> unwrapLinearIndex = (linidx, bounds) =>
    bounds.Select((b, i) => bounds.Take(i).Aggregate(linidx, (acc, bnd) => acc / bnd) % b);

// Reverse() to enumerate innermost dimensions first
var testBounds = new int[test.Rank].Select((_, d) => test.GetUpperBound(d) + 1).Reverse().ToArray();

for (int i = 00; i < test.Length; i++)
{
    var indexes = unwrapLinearIndex(i, testBounds).Reverse().ToArray();
    Console.Write($"{test.GetValue(indexes)} ");
}

The output is 0 1 2 3 4... 53 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM