2차원 배열은 행과 열로 찾는다
Esc

    2차원 배열은 행과 열로 찾는다

    –C#자료구조

    앞 글의 배열은 칸이 한 줄이었습니다. 그런데 다루려는 것이 원래 격자인 경우가 있습니다. 바둑판, 지도, 표 같은 것들입니다.

    한 줄짜리 배열로도 할 수는 있습니다. 3 × 3 판이면 9칸짜리 배열을 만들고 직접 계산해서 쓰면 됩니다.

    int[] board = new int[9];
    
    board[1 * 3 + 2] = 1;      // 1행 2열

    돌아가긴 합니다. 다만 1 * 3 + 2 를 매번 손으로 적어야 하고, 판 크기가 바뀌면 곱하는 숫자를 전부 고쳐야 합니다.

    번호 두 개로 칸을 가리키게 해 주는 것이 2차원 배열입니다.

    2차원 배열은 이렇게 생겼다

    int[,] board = new int[3, 3];
    
    board[1, 2] = 1;
    Console.WriteLine(board[1, 2]);    // 1

    대괄호 안에 쉼표가 하나 들어갔습니다. 이게 “번호를 두 개 받는다” 는 표시입니다. new int[3, 3] 은 3행 3열, 그러니까 아홉 칸입니다.

    값을 미리 적을 때는 중괄호를 겹쳐 씁니다.

    int[,] board =
    {
        { 1, 0, 0 },
        { 0, 1, 0 },
        { 0, 0, 1 },
    };

    적은 모양이 그대로 판 모양입니다. 첫 번호가 행(줄), 둘째 번호가 열(칸)입니다.

    격자로 보이지만 실제로는 한 줄이다

    여기가 이 글에서 짚고 갈 부분입니다.

    메모리는 격자가 아닙니다. 앞에서 본 대로 주소가 0번부터 쭉 이어지는 한 줄입니다. 2차원 배열도 거기에 놓입니다.

    코드에서 보는 모양 0,00,10,2 1,01,11,2 2,02,12,2 메모리에 놓인 모양 0,00,10,2 1,01,11,2 2,02,12,2 0행 1행 2행 한 행을 다 놓고 나서 다음 행을 이어 놓는다 board[1, 2] → 앞에 놓인 칸 수 = 1 × 3 + 2 = 5 → 시작 주소에서 5칸 떨어진 자리
    행 단위로 이어 붙인다. 번호 두 개는 결국 한 번호로 계산된다.

    한 행을 다 놓고 나서 다음 행을 이어 놓습니다. 그래서 board[행, 열] 은 결국 행 × 열개수 + 열 번째 칸입니다. 맨 앞에서 우리가 손으로 계산했던 1 * 3 + 2 를 컴파일러가 대신 해 주는 것입니다.

    문법이 바뀐 것이지 메모리가 격자가 된 것은 아닙니다.

    이중 반복문으로 전부 훑는다

    앞에서 바깥이 행, 안쪽이 열이라고 했습니다. 여기가 그 자리입니다.

    for (int row = 0; row < 3; row++)
    {
        for (int col = 0; col < 3; col++)
        {
            Console.Write(board[row, col] + " ");
        }
        Console.WriteLine();
    }

    별찍기에서 Write 로 이어 찍고 WriteLine() 으로 줄을 바꿨던 것과 같습니다. 안쪽이 한 줄을 다 찍고, 바깥이 줄을 넘깁니다.

    행과 열의 개수는 GetLength 로 받습니다.

    board.GetLength(0)      // 행 개수
    board.GetLength(1)      // 열 개수

    Length 는 전체 칸 수(9)를 줍니다. 반복문 조건에 그냥 쓰면 안 됩니다.

    순서를 바꾸면 느려질 수 있다

    두 반복문의 순서를 바꿔도 결과는 같습니다. 아홉 칸을 전부 방문하는 것은 똑같으니까요.

    for (int col = 0; col < 3; col++)
    {
        for (int row = 0; row < 3; row++)
        {
            Console.Write(board[row, col] + " ");   // 열 방향으로 훑습니다
        }
    }

    3 × 3 이면 차이가 없습니다. 그런데 판이 커지면 이야기가 달라집니다.

    메모리에는 행 순서로 놓여 있습니다. 행 방향으로 훑으면 바로 옆 칸으로 가고, 열 방향으로 훑으면 매번 한 행 크기만큼 건너뜁니다. CPU 는 값 하나를 읽을 때 그 주변을 한 덩어리로 미리 가져다 두는데, 멀리 뛰면 그 덩어리가 매번 헛것이 됩니다.

    방문한 칸 수는 같은데 건너뛴 횟수가 다릅니다. 행 우선은 열한 번 전부 바로 옆 칸이고, 열 우선은 열한 번 전부 건너뜁니다. 격자에서는 똑같이 한 칸씩 옮겨 다니는데 메모리에서는 그렇지 않습니다.

    지금 단계에서 외울 것은 아닙니다. 다만 격자로 보여도 아래는 한 줄이라는 사실이 성능에 드러나는 자리가 있다는 것 정도는 알아두면 됩니다.

    칸마다 길이가 다른 격자

    행마다 칸 수가 다른 경우가 있습니다. 이때는 모양이 다릅니다.

    int[][] jagged = new int[3][];
    
    jagged[0] = new int[2];
    jagged[1] = new int[5];
    jagged[2] = new int[1];
    
    jagged[1][3] = 7;      // 대괄호가 두 벌입니다

    int[,] 는 대괄호 한 벌에 쉼표, int[][] 는 대괄호 두 벌입니다. 생김새가 비슷해서 헷갈리는데 안이 완전히 다릅니다.

    int[,] 는 통짜 한 줄이고, int[][] 는 배열을 담는 배열입니다. 바깥 배열의 칸마다 다른 배열의 주소가 들어 있습니다. 그래서 행마다 길이를 다르게 만들 수 있고, 대신 칸들이 한 줄로 붙어 있지 않습니다.

    대부분은 int[,] 로 충분합니다.

    정리

    • int[,] 는 번호 두 개로 칸을 가리키는 배열입니다. 첫 번호가 행, 둘째가 열입니다.
    • 격자로 보이지만 메모리에는 한 행씩 이어 붙여 한 줄로 놓입니다.
    • board[행, 열] 은 행 × 열개수 + 열 번째 칸입니다.
    • 전부 훑을 때는 이중 반복문을 씁니다. 바깥이 행, 안쪽이 열입니다.
    • 행 개수는 GetLength(0), 열 개수는 GetLength(1) 입니다. Length 는 전체 칸 수입니다.
    • 행 방향으로 훑는 편이 메모리에 놓인 순서와 맞습니다.
    • 행마다 길이가 다르면 int[][] 입니다. 배열을 담는 배열이라 안이 다릅니다.