Thinking Different




[출처 : http://semicolon238.com/333]



 프로그래밍을 하다보면 여러가지 난관에 부딧친다. 이는 정적인 부분에서도 마찬가지인데, 그난관이란 대체적으로 논리문제 아니면, 내가 만든함정문제 아니면, 자료구조문제이다. 뭐 이 포스팅에서 그렇게까지 크게 이야기할 것은 없고, 오늘 해볼것은 바로 배열의 차원을 속이는 것이다. 뭐 속인다고해도 크게 속일 것은 없고, 1차원과 2차원을 속일 뿐이다.

 그렇다면, 왜 속여야하는가? 그냥 주어진대로 쓰고, 솔직하게 바꾸면 어디가 덧나나? 라고 생각할지 모르지만 천만의 말씀 만만의 콩떡이다. 때론 솔직히 돌아가는 것보다 거짓말을 하는것이 문제해결을 쉽게 만든다. 2차원 배열은 깔끔하고 직관적이지만 어떤 특정한 상황에선 1차원이 더 의미 있을 수도 있다. 물론 그반대경우가 될 수도 있다. 반드시 4x4타일맵을 작성하는데 있어 2차원배열을 종용하는 사람은 유도리가 없다는게 내 생각이다. 항상 특수한 경우를 생각해야되고 어떤게 더 옳을지 생각해보는것은 바로 프로그래머의 몫이다.

 그리고 조금은 다른이야기지만 2차원배열보다 1차원배열이 더 좋은 이유는 오버헤드가 없다는 것이다. 2차원배열은 아무래도 다차원이다 보니 세로의 원소를 읽을때는 메모리를 내부적인 포인터가 꽤 크게 건너뛰어야된다. 그러나 결국 메모리는 선형적이고 1차원배열과 그 쓰임이 비슷하다보니 2차원, 혹은 다차원배열보다는 1차원배열이 훨씬빠르다는 결론에이르게된다. 이는 배열의 크기가 크면클수록 더 커지게된다. 물론 이러한 기법을 이용한다고 해서 크게 성능이 향상되지는 않지만(어차피 2차원을 쓰는것처럼 1차원을 쓰므로) 그래도 순수하게 2차원배열을 쓰는것보다는 약간 빠르게된다. 물론 속도때문에 항상 이렇게 써야된다고 자신을 학대하는것 역시 옳지않은 생각이므로 적절한 상황에서 자신의 판단에 맞추어 제대로 쓰도록 하자. 그럼 지저분한 서론은 집어치우고 당장 본론으로 넘어가보겠다.


 여기에 이렇게 2차원배열 하나가 있다. 왜 쓰는지는 필요없고, 그래봤자 접근과 데이터변경의 용의성인 이유가 대부분이니 넘어가자.


 이런식으로 데이터를 원하는위치에 쉽게 넣을 수 있다. 뭐 여기까진 좋다. 


1
void array_print(int* arr[]);
 함수가 조금씩 더러워지기 시작한다.


1
2
3
4
5
6
void file_save()
{
    for(int y = 0; y < 5; ++y)
        for(int x = 0; x < 3; ++x)
            fprintf("%d", arr[y][x]);
}
 저장할때도 길어진다. 만일 일반자료형이 아닌, 내부적으로 계산해주어야 되는 경우나 데이터가 여러차원으로 분리되어있을떄는 저장위치 잡기도 애매해진다. 그럼 여기서 정리. 어떻게하면 1차원배열로 2차원처럼 보일 수있을까.


1
int arr[x * y];
 1차원과 2차원을 곱한것이 결국엔 해당 2차원배열의 원소수이므로, 이 원소수를 한계값으로 두는 1차원배열을 생성한다. 이거라면 여기 올릴만한 문제가 아니잖아! 라고 할텐데.. 조금 더 설명을 들어보시라.


1
2
for(int i = 0; i < (x * y); ++i)
    arr[i] = 3;
 값을 넣을때는 이렇게 넣는다. 그러나 그렇게되면 어렵잖아! 라고 말할것이 분명하다. 그렇다. 1차원배열을 2차원처럼 쓰는것은 쉬우나 값을 넣을때 일일히 계산해줘야되는 문제가 있었다. 그렇다면 이를 이렇게 해결해보자.


1
2
3
4
void set_array_element(int* pArr, int x, int y, int xcount)
{
    pArr[(y * xcount) + x] = 0;
}
 결국에는 현재 2차원배열의 가로사이즈(2차원에서의 x축)을 기반으로 하여 실제적인 1차원의 원소위치를 알아낸다. 획기적인건 아니고 많이 쓰는 방법이지만 아주 깔끔하고, add_data를 사용하는 사람 입장에서는 x,y축을 삽입하여 원하는 원소에 값을집어넣으므로 아주 좋은방법이다. 여기서 xcount를 모르는 사람은 없으리라고본다. 왜냐면 대부분 2차원배열은 맵등의 구조로서 쓰이는데 맵의 가로크기를 생각안하고 짜는 사람은 없기때문이다. 이는 값을 얻을때도 분명히 나타난다.


1
2
3
4
int get_array_element(int* pArr, int x, int y, int xcount)
{
    return pArr[(y * xcount) + x];
}
 이 함수만 쓴다면 값을 얻어오려는 사람은 2차원배열의 첨자를 기입하여 1차원배열의 값을 얻어온다. 조금 돌아가는 감이 있지만 생각해보면 꽤 깔끔한 방법이다. 그렇다면 이번엔 반대로 2차원을 1차원처럼 속여보자.


여기에 2차원 배열이 있다. 역시 왜 쓰는지는 필요없고 이를 1차원배열처럼 써보자. 


 보통은 이렇게 해서 넣어준다. 아까 1차원배열을 2차원처럼 속일때도 결국엔 원소의 x위치와 y위치를 이용하여 실제원소위치 i를 만들어내었다. 그렇다면 이번엔 반대로 하면 되는 것이다. 원소의 위치 i를 받아서 x와 y를 생성해보자.

1
2
3
4
5
6
void set_array_element(int* arr[], int i)
{
    int x = i % xcount;
    int y = i / xcount;
    arr[x][y] = 0;
}
 깔끔하다. 무엇보다 이 방법이 좋은점은 현재 위치를 입력하면 내부적으로 x와 y를 만들어버렸다는 것이다. 위의 방법과 병합하면, 2차원원소와 1차원원소의 복사가 엄청 쉬워진다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int arr1[50];
int arr2[10][5];
 
// 1차원기준으로 1차원과 2차원의 데이터를 서로 옮겨보자.
void swap_array_d1tod2(int* arr1, int* arr2[], int i, int xcount)
{
    int temp = arr1[i];
    arr1[i] = arr2[i/xcount][i%xcount];
    arr2[i/xcount][i%xcount] = temp;
}
 
// 2차원기준으로 1차원과 2차원의 데이터를 서로 옮겨보자.
void swap_array_d1tod2(int* arr1, int* arr2[], int x, int y, int xcount)
{
    int temp = arr1[(y * xcount) + x];
    arr1[(y * xcount) + x] = arr2[y][x];
    arr2[y][x] = temp;
} 
 매우깔끔하다. 실제로 1차원과 2차원 각각에 반대되는 형태의 원소위치를 넣어도 내부적으로 현재 원소의 위치를 구해내어 결국에는 서로의 값을 바꾸는데 아무런 지장이 없어지는 것이다. 만일 이러한 기법들을 사용하지 않는다면 소스는 훨씬 지저분해지며, 쓸때없이 변수들을 마구잡이로 생성하는 등의 안좋은 우회방법을 사용하기 일쑤일 것이다.

이 기법들은 사실 평소엔 거의 쓸일이 없다. 하지만 2차원배열을 사용하는 텍스쳐나, 게임의 배경맵 등을 짤때는 알아두면 알아둘수록 도움되지 않겠는가? 이런게 다 노하우인 것이다. 별것아닌 팁을 주의깊게 읽어주어 고맙다는 말로서 오늘의 이 포스팅을 마치도록 하겠다.