9-3-라.룩업 테이블

임의의 날짜가 주어졌을 때 이 날짜 다음의 날짜, 즉 내일을 구하는 예제를 만들어 보자. 내일이란 굉장히 쉽게 구할 수 있을 것 같지만 날짜라는 정보의 구조가 간단하지 않기 때문에 생각보다 훨씬 더 어렵다. 오늘 날짜가 m월 d일이라고 했을 때 대부분의 경우는 d만 1증가시키면 된다. 그러나 오늘이 월말일 경우는 d는 1이 되고 m은 1증가해야 하는데 예를 들어 3월 31일의 내일은 3월 32일이 아니라 4월 1일이다.

월이 바뀔 때도 m이 무조건 1증가되기만 하는 것은 아니다. 13월이라는 것은 없으므로 12월 다음은 1월이 되어야 한다. 오늘이 월말이라는 것을 알아 내려면 매 달마다 몇일까지 있는지를 조사해야 한다. 3월은 31일까지 있지만 4월은 30일까지밖에 없다. 이런 정보를 조사할 때도 배열을 사용할 수 있다. 다음 예제는 배열로 이 문제를 풀어본 것이다.

 

: PrintTomorrow

#include <Turboc.h>

 

void PrintTomorrow(int m, int d)

{

     static int days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

 

     if (d > days[m] || m < 1 || m > 12) {

          printf("입력한 날짜가 틀렸습니다.\n");

     } else {

          d++;

          if (d > days[m]) {

              d=1;

              m++;

              if (m == 13) {

                   m=1;

              }

          }

          printf("내일은 %d월 %d일입니다.\n",m,d);

     }

}

 

void main()

{

     int mon,day;

 

     printf("오늘 날짜(월 일)을 공백으로 구분하여 입력해 주세요 : ");

     scanf("%d%d",&mon,&day);

 

     PrintTomorrow(mon,day);

}

 

PrintTomorrow 함수는 날짜를 전달하면 이 날짜의 내일을 조사하여 출력한다. 매월의 마지막 날에 대한 정보를 days라는 배열로 작성해 놓고 days의 첨자로 m을 사용하면 m월의 날 수를 쉽게 조사할 수 있다. 이런 식으로 값의 참조를 위해 사용하는 배열을 룩업 테이블(Lookup Table)이라고 한다. 만약 배열을 사용하지 않는다면 switch case문이나 여러 개의 조건문을 가지는 if문을 구성해야 할 것이다.

 

if (m==1 || m==3 || m==5 || m==7 || m==8 || m==10 || m==12) {

     // 31일까지 있음

} else if (m==4 || m==6 || m==9 || m==11 {

     // 30일까지 있음

} else {

     // 28일까지 있음

}

 

조사 대상이 12개 정도이고 비슷한 값들을 가진다면 이 정도 조건문으로도 문제를 일단 해결할 수 있겠지만 만약 100개 정도의 불규칙한 값을 조사해야 한다면 이런 방법은 한계가 있다. 아무리 값이 많아도 또 값이 불규칙해도 룩업 테이블을 작성하면 아주 빠른 속도로 원하는 값을 찾을 수 있을 뿐만 아니라 이후 값을 수정하기도 편리하다. 값을 선택하는 기준이 정수라면 이 정수값을 첨자로 하는 룩업 테이블을 작성하고 정수를 첨자로 넣기만 하면 배열을 통해 원하는 값을 바로 찾을 수 있다.

룩업 테이블은 보통 한 함수만 사용하는데다 읽기 전용인 경우가 많으므로 static으로 선언하는 것이 좋다. 만약 static으로 선언하지 않고 단순 지역 배열로 선언한다면 함수가 호출될 때마다 이 배열을 매번 초기화해야 하는데 이는 엄청난 실행 속도의 저하를 가져온다. 배열의 크기가 크면 클수록 초기화 속도는 느려지는데 이럴 때 static 기억 부류를 사용하면 딱 한 번만 초기화하므로 속도에 유리하다.

다음 예제는 룩업 테이블의 또 다른 사용예인데 전혀 규칙성이 없는 난수중 하나를 선택한다. 발생 가능한 난수 목록을 배열에 미리 작성해 놓고 난수로 배열 첨자를 고르는 방식이다. 실행할 때마다 룩업 테이블에 있는 정수중 하나가 선택되어 출력될 것이다.

 

: RandTable

#include <Turboc.h>

 

void main()

{

     int arRand[]={2,9,14,19,27};

     int Num;

    

     randomize();

     Num=arRand[random(sizeof(arRand)/sizeof(arRand[0]))];

     printf("생성한 난수 = %d\n",Num);

}

 

이런 방법을 쓰는 대신 원하는 난수를 포괄하는 범위에서 수를 하나 생성하고 조건문으로 원하는 난수 중 하나가 맞는지 점검하는 방법을 쓸 수도 있다. while문으로 조건에 맞는 난수가 나올 때까지 루프를 돌리면 언젠가는 원하는 값이 나오기는 하겠지만 루프를 여러 번 돌아야 하므로 비효율적이고 난수를 고르는 시간이 일정하지 않다. 룩업 테이블은 첨자만 난수로 선택하므로 단 한 번에 원하는 값을 고를 수 있고 난수의 목록을 편집하기도 편리하다.

다음 예제의 Congratulation 함수는 게임의 시도 회수에 따라 축하 메시지를 출력하는데 시도 회수가 적을수록 높은 점수를 부여한다. 회수별로 메시지를 다르게 출력하기 위해 switch case문을 사용했다.

 

: GameMessage1

#include <Turboc.h>

 

void Congratulation(int count)

{

     switch (count) {

     case 1:

          puts("축하합니다. 최고 성적입니다.");

          break;

     case 2:

          puts("대단한 성적입니다.");

          break;

     case 3:

          puts("참 잘 하셨습니다");

          break;

     case 4:

          puts("보통이 아니군요.");

          break;

     case 5:

          puts("보통입니다.");

          break;

     case 6:

          puts("조금 더 노력하셔야겠습니다.");

          break;

     case 7:

          puts("정말 못하시는군요.");

          break;

     case 8:

          puts("수준 이하입니다.");

          break;

     default:

          puts("다음부터 절대로 이 게임을 하지 마세요.");

          break;

     }

}

 

void main()

{

     Congratulation(3);

}

 

이 예제는 기능상의 문제는 전혀 없지만 길다란 switch case문이 왠지 비효율적으로 보인다. 시도 회수인 count로부터 메시지만을 구하는 룩업 테이블을 작성하면 출력문을 훨씬 간단하게 구성할 수 있으며 또한 메시지를 편집하거나 추가하기도 편리하다. 다음 예제는 똑같은 동작을 하되 룩업 테이블로 바꿔 본 것이다.

 

: GameMessage2

#include <Turboc.h>

 

void Congratulation(int count)

{

     static char *Message[]={"",

          "축하합니다. 최고 성적입니다.",

          "대단한 성적입니다.",

          "참 잘 하셨습니다",

          "보통이 아니군요.",

          "보통입니다.",

          "조금 더 노력하셔야겠습니다.",

          "정말 못하시는군요.",

          "수준 이하입니다.",

          "다음부터 절대로 이 게임을 하지 마세요.",

     };

 

     if (count >= 9) count=9;

     puts(Message[count]);

}

 

void main()

{

     Congratulation(3);

}

 

count를 첨자로 하는 문자열의 배열을 작성했는데 이 룩업 테이블도 물론 static으로 선언해야 한다. count가 0인 경우는 없으므로 빈 문자열로 초기화해 미사용으로 남겨 두었다. 메시지 문자열들이 한 곳에 모여 있으므로 소스가 훨씬 더 짧아지고 보기에도 좋다. 만약 count와 메시지가 일대일로 대응되지 않고 일정 범위별로 대응된다면 같은 메시지를 배열에 계속 나열할 필요없이 2차 룩업 테이블을 만든다. 예를 들어 1~3회는 상, 4~8까지는 중, 9~12까지는 하로 범위를 나누어 메시지를 다르게 출력하고 싶다면 다음과 같이 수정한다.

 

: GameMessage3

#include <Turboc.h>

 

void Congratulation(int count)

{

     static char *Message[]={

          "잘 하셨습니다.",

          "보통입니다.",

          "못 하는군요.",

     };

     static int arMes[]={0,0,0,0,1,1,1,1,1,2,2,2,2};

 

     if (count >= 12) count=12;

     puts(Message[arMes[count]]);

}

 

void main()

{

     Congratulation(8);

}

 

count를 arMes 룩업 테이블에 넣어 몇 번째 메시지인가를 먼저 선택하고 이 값을 Message 배열의 첨자로 넣어 실제 메시지를 고르는 것이다. count로부터 최종 메시지가 선택되는 과정은 다음과 같다.

이렇게 하면 메시지를 출력하는데 필요한 최소한의 정보만 유지할 수 있으며 편집하기도 쉽다. count에 따른 메시지를 변경하려면 arMes배열을 편집하고 최종 메시지 문자열을 변경하려면 Message 배열을 편집하기만 하면 된다.

 

 PrintTomorrow2

PrintTomorrow 예제는 년도에 대한 고려는 하지 않는데 년도까지 계산에 포함하면 윤년을 고려해야 한다. 윤년이란 4로 나누어 떨어지되 100으로는 나누어 떨어지지 않는 년도인데 1904년은 윤년이지만 1900년은 윤년이 아니다. 또한 100으로 나누어 떨어지는 년도라도 400으로 나누어 떨어지면 윤년이 되는데 2000년은 윤년이다. 윤년에는 2월달이 29일까지 있으므로 룩업 테이블에서 읽은 값 중 2월의 값을 수정한 후 사용해야 한다. 년, 월, 일 정보를 모두 입력받아 윤년까지 고려해서 내일 날짜를 출력하는 예제를 작성하라.