반응형

Space Complexity = 필요한 메모리

stability = 정렬되기 이전의 동일한 데이터끼리의 순서를 정렬 후에도 유지하고 있는가?


선택정렬 : 최소값을 찾아서 제일 앞으로

삽입정렬 : 앞쪽에 데이터를 넣으면서 정렬을 시킨다 그래서 정렬된 =>이미 정렬된 부분에 이 값이 어디에 들어갈 것인가

버블정렬 : 인접한 값을 비교 교환

쉘 : 자료가 역순으로 정렬되어있어 속도가 많이 떨어지는 삽입정렬의 단점을 보완한 정렬 기법

추가 메모리 공간 소요가 없으면서도 정렬중에서도 속도가 빠르다

퀵소트 : 의 평균성능은 NlogN 이지만 최악의 경우 N^2 로 간다 => 추가 메모리를 비교적 필요로 하는 상황이 벌어진다

힙소트 : 최대값이 루트에 있다 , 추가 메모리를 필요하지 않는다 , => 힙소트 자체로 정렬 할 경우 NlogN 중에서는 비교적 조금 느린 성능을 보인다

특성을 잘 활용하는 것이 중요

병합정렬 : 순차적인 접근방식이라 순차적 접근방식에서 사용될만 한 방식=> stability 가 유지 되면서 추가 메모리가 필요 없다

Radix(기수) 들과 Distrubtion(분포수세기) 은 양의정수만을 또는 이진수를 정렬 할 수 있는 정렬 => 속도가 빠르다

반응형
반응형

http://blog.naver.com/kareons777/70075825158


// 렌더링 프레임수 계산
static DWORD FPS_Frames=0;
static float FPS_Num, FPS_LastTime=0;

if(FPS_Frames == 0) FPS_LastTime = (float)timeGetTime()/1000.0f;

float FPS_Time = (float)timeGetTime()/1000.0f;

if(FPS_Time - FPS_LastTime < 1.0f)
{ 
FPS_Frames++;
}
else
{
FPS_Num = FPS_Frames/((FPS_Time-FPS_LastTime));
FPS_Frames = 0;
FPS_LastTime = FPS_Time;
//FPS_Num 출력
}
timeGetTime()함수는 winmm.lib를 링크해야 합니다. 또한 #include <mmsystem.h> 해줘야 하고요.
이걸 함수로 뽑던지 그냥 랜더 함수에 넣어 주던지 하면 됩니다.

[출처] fps 계산법|작성자 쑤기


반응형
반응형

http://blog.naver.com/pluulove84/100058770623



다차원검색트리

 

KD-트리

- 이진검색트리를 확장한 것으로 k개(k≥2)의 필드로 이루어지는 키를 사용

- 동일한 레벨에 있는 노드는 모두 동일한 하나의 필드만 이용해서 분기한다

 

 

KD-트리 삽입

 

 

A(50, 50) → B(10, 70) → C(80, 85) → D(25, 20) → E(40, 85) → F(70, 85) → G(10, 60)

 

1. A(50, 50)이 루트 자리를 차지

2. B(10, 70)는 x값 10이 루트의 x값 50보다 작으므로 왼쪽 자식이 된다

3. C(80, 85)는 x값 80이 루트의 x값 50보다 크므로 오른쪽 자식이 된다

4. D(25, 20)는 먼저 x값 25가 루트의 x값 50보다 작으므로 루트의 왼쪽으로 가서 B(10, 70)를 만난다

    y값 20이 B의 y값 70보다 작으므로 B의 왼쪽 자식이 된다

5. E(40, 85)는 x값 40의 루트의 x값 50보다 작고, y보다 85가 B의 y값 70보다 크므로 B의 오른쪽 자식이 된다

6. F(70, 85)는 x값 70이 루트의 x값 50보다 크고, y값 85가 C의 y값 85와 같으므로 B의 오른쪽 자식이 되었다.

7. G(10, 60)는 x값 10이 루트의 x값 50보다 작고, y값 60이 B의 y값 70보다 작고,

    x값 10이 D의 x값 25보다 작으므로 D의 왼쪽 자식이 된다.

 

 

KD-트리 삭제

 

 

KD-트리 삭제

1. 자식이 없는 경우

 : 노드만 제거

2. 자식이 하나뿐인 경우

 : 자식이 둘인 경우와 같이 해결

 : 왼쪽 자식만 있는경우, 왼쪽 서브트리 중 노드 r에서의 분기에 사용한 필드 값이 가장 큰 노드를 찾아서 이동

3. 자식이 둘인 경우

 : 오른족 서브트리 중 노드 r에서 분기에 사용한 필드의 값이 가장 작은 노드를 찾아서 이동

 

 

KDB-트리

 

 

KDB-트리의 노드의 종류

1. 영역 노드 : 복수 개의(영역, 페이지 번호) 쌍으로 구성된다. 모든 내부 노드는 영역 노드이다.

2. 키 노드 : 복수개의(키, 페이지 번호) 쌍으로 구성된다. 모든 리프 노드는 키 노드이다.

 

각 차원에서 범위가 주어지고 영역은 이들을 모두 만족하는 공간을 말한다.

 

KDB-트리의 키 검색

 : 루트 노드부터 시작해서 해당 키가 포함되는 영역을 따라 리프 노드까지 내려가면 된다.

 

KDB-트리의 키 삽입

1. 삽입할 키가 속하는 리프 노드를 찾는다.

2. 해당 리프 노드가 키를 더 수용할 수 있는 공간이 있으면 (키, 페이지 번호) 쌍을 삽입하고 끝난다

3. 리프 노드가 키를 더이상 수용할 수 없을 경우, 형제 노드와 재분배할 수 있으면 재분배하고 작업은 끝난다.

4. 재분배가 불가능하면 리프 노드를 분할하여 두개로 만든다.

   → 이에 따라 부모 노드에 있던 한 영역이 두개로 나누어지므로(영역, 페이지 번호) 쌍이 하나 늘어난다.

   → 부모 노드가(영역, 페이지 번호)를 하나 더 수용할 공간이 있으면 작업은 끝이다.

   → 만일 부모 노드가 이것을 수용할 수 없으면 부모 노드를 분할 한다.

반응형
반응형

http://yeols.com/80075364360



기수 정렬(Radix Sort)

 

  • 특수 정렬 알고리즘(기수 정렬, 계수 정렬) 중 하나.

  • 특수 정렬 알고리즘
    • 비교 정렬
      • 두 원소를 비교하는 정렬의 하한선은 Ω(nlogn)

        최악의 경우 정렬 시간이 θ(nlogn)보다 더 빠를 수는 없는가? "

    • 그러나 원소들이 특수한 성질을 만족하면 O(n)정렬도 가능하다.
      • 기수 정렬
        - 원소들이 모두 k 이하의 자리 수를 가졌을 때 (k: 상수)

  • 기수 정렬의 개념
    • 입력이 모두 K 이하의 자리 수를 가진 특수한 경우에(자연수가 아닌 제한된 종류를 가진 알파벳 등도 해당) 사용할 수 있는 방법


    • θ(n)시간이 소요되는 알고리즘

  • 기수 정렬 수행 과정 #1
    • 원소들의 일의 자리부터 비교 후 정렬, 다음은 십의 자리를 비교 후 정렬 ...

 

  • 기수 정렬의 수행 과정 #2
    • 정렬되지 않은[69, 10, 30, 2, 16, 8, 31, 22]의 자료들을 기수 정렬 방법으로 정렬하는 과정을 살펴보자.
      • 키 값이 두 자리 십진수 이므로, 10개의 버킷을 사용하여 기수 정렬을 두 번 반복한다.

1. 초기 상태 : 큐(여러 개의 원소들이 일정한 순서로 나열된 자료 구조이며, 스택과는 달리 한쪽 끝에서는 삽입만 할 수 있고, 삭제는 반대쪽 끝에서만 할 수 있도록 되어 있음)를 사용하여 0부터 9까지 10개의 버킷을 만든다.

 



2. 키 값의 일의 자리에 대해서 기수 정렬 수행 [1단계]

- 정렬할 원소의 일의 자리 값에 따라서 순서대로 버킷에 분배 -

 

 

2. 키 값의 일의 자리에 대해서 기수 정렬 수행 [2단계]

- 버킷에 분배된 원소들을 순서대로 꺼내서 저장 -

 

 

3. 키 값의 십의 자리에 대해서 기수 정렬 수행 [1단계]

 - 정렬할 원소의 십의 자리 값에 따라서 순서대로 버킷에 분배 -

 

 

3. 키 값의 십의 자리에 대해서 기수 정렬 수행 [2단계]

 - 버킷에 분배된 원소들을 순서대로 꺼내서 저장 -

 

  • 기수 정렬 알고리즘 분석
    • 메모리 사용공간
      • 원소 n개에 대해서 n개의 메모리 공간 사용
      • 기수 r 에 따라 버킷 공간이 추가로 필요

    • 연산 시간
      • 연산 시간은 정렬할 원소의 수 n 과 키 값의 자릿수 d 와 버킷의 수를 결정하는 기수 r 에 따라 달라진다.
        - 정렬할 원소 n 개를 r 개의 버킷에 분배하는 작업 : (n+r)
        - 이 작업을 자릿수 d 만큼 반복

      • 수행할 전체 작업 : d(n+r)
      • 시간 복잡도 : O(d(n+r))

반응형
반응형

분포수세기 알고리즘

기수정렬 알고리즘

직접기수정렬 알고리리즘(분포수 세기가 활용) 등이 있다

이것은 이준수 정렬 알고리즘에 사용 될 수 도 있다

C++ 알고리즘 강의에 있음 참고

-기수 정렬은 비교 구문이 없이 정렬 된다

일반적으로 직접기수 정렬이 좋다


반응형
반응형

이진트리(바이너리트리)를 이용하여 검색하는 방법 O(logN) 의 성능 : 성능이 괜챃다

이진검색트리는 이진트리(바이너리트리)를 이용한 검색 방법

조건 Heap 은 부모노드가 두개의 자식노드보다 커야 하지만

1.이진검색트리는 부모노드는 왼쪽 sub-tree 의 모든 노드보다 크고

2.부모노드는 오른쪽 sub-tree 의 모든 노드보다 작아야 한다

재귀적으로 모든 조건을 만족해야 한다

그랫 In-Order Traversal 하면 정렬된 순서가 된다 (왼쪽 부모, 오른쪽 노드순으로 방문)

트리는 부모가 양 자식노드의 포인터를 가지고 있는 상태

검색

1.. 루트부터 검색 시작

2. 각 노드와 찾을 값을 비교해가면서 노드에서 좌측은 작고 우측은 큰것이기 때문에 키값이 같을때까지 비교를 반복해간다

=> 조건에 따라 자식 포인터를 타고 계속 내려간다 키를 찾을때까지

마지막 까리 노드까지 내려가서 찾지 못하면 fail

삽입

루트부터 비교를 하면서 새로운 노드는 항상 leaf 에 배치한다

(이때 트리 구조상 부모가 자식을 가르키고 있어야 함으로

포인터를 두개를 두어 이전 포인터 값을 유지하고 있어야 한다

하나는 현재 노드 하나는 이전노드를 가르키는 포인터)

또한 마지막 노드의 왼쪽 오른쪽에 들어가야 하므로

부모노드를 저장하고 있는 포인터가 P 였고 P 의 오른쪽에 노드가 추가 되어야 한다면

P 의 Right 에 노드 하나가 추가 되면서 추가된 노드 또한 아무것도 가르키고 있지 않은 포인터 두개(자식2개) 를 갖는 노드가 되어

추가 되어야 한다

삭제

삭제를 하고도 이진트리의 조건에 맞아야 한다

마지막 노드는 그냥 삭제 하면 되지만 중간에 잇는 노드를 삭제할때는

자신보다 작은 노드중 최대노드 또는

자신보다 큰 노드중 최소 노드를 선택

선택 방법은 부모 노드에서 왼쪽은 모두 부모모다 작은 것임으로

자신보다 작은 노드중 최대노드는 부모에서 왼쪽으로 한번갔다가 오른쪽으로 쭉 가는 순서

=>부모의 왼쪽 트리중에서(부모제외한) 중위순회를 해서 제일 마지막에 나오는 노드가 작은것중에 큰것

자신보다 큰 노드중 최소 노드는 부모를 제외한 오른쪽 노드트리에서 중위순회를 하여 가장 첫번째 나오는 것이

부모를 제외한 오른쪽 노드트리에서 가장 작은 값이다

...자식의 자식에 대한 처리 이하 생략..


이진 트리는 좌우로 균형이 맞을때 성능이 가장 좋다

입력 순서가 중요하다,

만약 정렬된 순서(순차 or 역순)로 입력이 될 경우 링크드리스트 처럼 입력이 될 수 있다 => 최악의 경우로 가게 된다

find, insert 최악의 경우 O(N), 균형이 맞으면 O(logN)

remove O(logN) => 삭제할때 insert 시 O(N)의 성능을 가져도 노드끼리의 포인터 연결이 간단해 짐으로 결론적으로 O(logN) 의 성능을 가짐

insert, find, remove 평균적으로는 O(logN) 의 성능을 갖는다

결국 트리의 균형을 맞추는 것이 중요하다 그래서 Balanced Tree 가 많이 연구됨

그래서 이진트리의 삽입을 개선한다


이진트리를 중위순회 하면 정렬된 순서가 나온다

자료가 균형에 맞게 대강 들어가는데 O(NlogN)의 성능을 갖는다

하나가 insert 되는데 logN 이 걸림으로 N 개의 자료에 대해 걸쳐 수행하는 것이니 N*logN

그런데 정렬된 순서나 역순이면 최악의 성능 O(N^2) 의 성능을 가진다


[이진트리 균형 맞추기]

정렬된 데이터를 배열로 가지고 있고 이것을 이진트리에 저장하려고 할때

트리를 중위순회 방식으로 순회하면서 트리를 생성할때 중위순회시 데이터를 노드에 저장하면 한쪽으로 치우친 형태가 아닌

균형이 맞는 balance 형태의 트리로구성할 수 있다

- 성능이 않좋을때 balance 로 균형을 한번 맞춰준다


중복키 문제

이진트리는 중복키에 대해서 제대로 처리 하지는 못하는데 이걸 해결 하기 위해 링크드 리스트와 바이너리 리스트를 결합해 해결한다

중복되는 키를 가지고 있는 노드에 대해서 링크드 리스트를 두어 링크드 리스트 뒤에 넣어주는 식으로 해놓고

키에 대해서 next 를 호출하면 중복된 키들의 첫번째에서 두번째로 이동하여 해당 데이터를 리턴


이진트리 특징

all nodes in left subtree < parent < all nodes in right subtree

성능이 트리의 균형에 좌우된다

반응형
반응형


n >= 1

F_n = F_{n-1} + F_{n-2}

F_1 = F_2 = 1

1 1 2 3 5 ....

F_1 F_2 F_3 F_n

n 은 피보나치 수열을 만드는 번째..

int fibonacci(int n){

if( n == 1 || n == 2)

return 1;

return fibonacci( n - 1 ) + fibonacci( n - 2 ) ; // 후위 순회와 유사하다 값을 나중에 리턴 함으로..

}








피보나치수열 참고


참고 : http://blog.naver.com/kwanskim?Redirect=Log&logNo=20110843012




자연계의 피보나치 수열  게시판 

2010/08/02 05:30

복사http://blog.naver.com/kwanskim/20110843012

전용뷰어

피보나치 수열 (Fibonacci Numbers)

 

1. 피보나치 수열

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, …

처음의 두 숫자는 0 1이고그 다음부터는 이전 두 항의 합으로 이루어진다첫번째 항 0을 제외하는 경우도 있다.

 

2. 피보나치와 피보나치 문제

피보나치 ( 1170-1250)는 이탈리아의 수학자로서 <Liber Abaci>라는 책을 통해 힌두-아라비아 숫자 체계를 유럽에 전파한 것으로도 잘 알려져 있는 중세 유럽의 최고 지성이다또한 그는 이 책에서 다음과 같은 문제를 제시하였다.

 

l        1 1일에 태어난 암수 한 쌍의 토끼가 있다.

l        모든 달의 길이는 동일하다고 가정한다.

l        한 쌍의 토끼는 태어난지 두 달이 지나면 매달 두 마리의 새끼(암수)를 낳는다즉 첫번째 쌍은 3 1일에 처음으로 두 마리의 새끼를 낳는다.

l        그들에게서 태어난 새끼들도 두 달이 지나면 짝을 이루어 매달 두 마리의 새끼를 낳는다.

l        어떠한 토끼도 중간에 죽지 않는다.

l        일 년 후에 전체 토끼는 모두 몇 쌍일까?

 

피보나치는 근친교배환경먹이다산불임 등 일어날 수 있는 여러 상황은 모두 제거하고 문제를 단순화 시킴으로써 상황 속에 숨은 원리를 파악하고자 하였다.

 

그 결과 피보나치는 매달 관찰되는 토끼의 쌍이 특정한 규칙을 따르고 있음을 발견하고 12월 말에 144쌍의 토끼를 예상하였다.

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144

 

3. 자연계에서 발견되는 피보나치 수열

자연계에서 이러한 수열이 제법 흔하게 발견된다는 사실은 잘 알려져 있다여러 꽃잎의 갯수를 조사해보면 피보나치 수열이 자주 나타난다.

 

가상적으로 표현된 아래의 그림을 보면 식물 줄기의 가지와 잎의 갯수에서도 피보나치 수열이 나타난다.

 

아래 그림의 솔방울은 한쪽 방향으로 8개의 나선이 나타나고 다른 방향으로는 13개의 나선이 나타난다모두 피보나치 수열에서 나타나는 숫자들이다.

 

아래 그림의 파인애플은 한쪽 방향으로 13개의 나선이 나타나고 다른 방향으로는 21개의 나선이 나타난다.

 

아래 그림에서 해바라기 씨앗들이 이루는 나선 무늬는 양쪽 방향으로 몇 줄씩인지 세어보자.

 

그 외에도 과일 씨앗의 갯수도 피보나치 수열을 따르는 경우가 많다.

 

4. 식물계에 피보나치 수열이 흔한 이유

학자들에 따라 진화적인 측면에서 피보나치 수열의 효율성을 말하는 분들도 있지만식물성장의 역학적 측면에서 고찰한 의견도 있다.

우리는 이 이야기를 토끼의 번식에 관한 이야기로 시작했다토끼의 번식을 다시 생각해 본다면 수열의 출현은 자연스럽다즉 한쌍의 토끼만을 고려해보자그들은 평생을 살면서 매달 두 마리씩의 새끼를 낳지만 생후 2달이 되어서야 성장을 완료하기 때문에 둘째 달부터 출산이 가능하다는 것이다생물학적 진위를 논의함이 아니다수학적인 원리를 논의하는 것이다.

그렇다면 동일한 원리를 식물의 성장에 적용시키지 못할 이유가 없다식물의 줄기를 생각해보자이들이 두 갈래로 갈라지기 위해서는 (아니 좀더 정확하게 표현한다면 본 줄기에서 새끼 가지를 치는 것이 옳다고 생각하자.) 줄기의 굵기가 어느 정도 이상이어야 된다그리고 필요한 굵기 이상이 된 이후에는 정기적으로 가지치기를 할 수 있다. 따라서 새로 돋아난 새끼 가지도 어느 정도의 시간이 지난 후에는 정기적으로 가지를 칠 수 있다는 얘기이다그 원리가 토끼의 번식과 완전히 동일하다.

꽃잎의 모태가 되는 입자 (이런 표현을 써도 된다면)도 어느 정도 성장하고 나서 작은 새끼 입자 하나를 낳고(?) 그것이 다시 새로운 입자를 낳기 위해서는 특정한 시간이 필요하다고 가정한다면 꽃잎이나 씨앗의 갯수에서도 피보나치 수열이 나타나는 것에 대한 궁금증에 대한 어느 정도의 설명이 될 수 있을 것 같다.

 

5. 마치며

우리가 자연계에 대해서 아는 것이 너무 보잘것 없다는 사실에 전적으로 동의한다위에 열거한 많은 사례가 모두 동일한 원리로 확실하게 설명된다고 말하기는 어려울지도 모른다특히 솔방울이나 파인애플그리고 해바라기 씨앗에서 관찰되는 나선의 갯수에 대해서는 이들 씨앗들의 초기 분열과정에서 일어나는 (위와 비슷한밀고 당기는 동적 역학과정이 관여하지 않을까 정도의 추측 정도 만이 내게는 있을 뿐이다그러나 그러한 수열이 관찰되는 이상 그것을 무조건 우연의 일치로 생각할 수는 없다이러한 것들과 여기에 미처 언급하지도 못한 많은 사실에 대한 엄청난 비밀과 원리가 이 글을 읽으시는 여러분에 의해 밝혀지기를 바라는 마음 간절하다혹시 이 모든 것이 누군가에 의해 이미 명백하게 밝혀져 거의 모든 사람들이 아는 상식이 되어버린지 오래인데 나만 모르고 있었다면 나의 무지에 대한 용서를 구하며

 

피아노 건반의 한 옥타브 사이에는 5개의 까만 건반, 8개의 하얀 건반모두 13개의 건반이 있다우연인가어린 시절 아카시아 같은 나뭇잎을 하나씩 떼어내며 여러 가지 운을 물었던 기억이 있다서양에도 비슷하게 꽃잎을 떼어내며 운을 묻는 경우가 있나보다피보나치 수열을 알고 있다면 사랑을 얻을 수도 있지 않을까?

He loves me. He loves me not. He loves me.…

 

(위 사진들과 그림들은 인터넷의 여러 곳에서 찾아 편집한 것입니다)

 

2010 8 1 (큰 아들의 스물 세번째 생일을 축하하며…)

빗줄기././

반응형
반응형

1. 문제의 크기는 점점 작아져야 한다.

2. 재귀호출이 끝나는 조건이 있어야 한다

Factorial 구하기

문제의 크기 감소 : N! = N*(N-1)!

종료조건 0! = 1

int factorial( int n )

{

if( n == 0 ) // 만약 n =0 을초기 값으로 입력 했으면 1 이 나오야 함으로....

return 1;

else

return n * factorial( n-1);

}

반응형
반응형

Terminal node : 끝인 노드

Non-Terminal node : 끝이 아닌 노드

괄호의 식을 기준으로 operator 노드 양쪽에 operand 를 배치해 놓으면

위의 그림처럼 수식노드가 만들어지는데 이때의 이점은

순회 방법에 따라 순회 방식에 따라 전위,중위,후위의 수식구성을 만들 수 있다는 것

만약 위의 수식이 후위수식으로 표기 되어 있었다면

A B + CD - * E / F G * +

다음을 통해 Parse Tree 를 구성할 수 있다

1. Operand 는 Node를 만들어 Stack 에 Push

2. Operator 느 Node를 생서하여

1. Stack 에서 Pop 한 노드를 오른쪽 자식으로 하고

2. stack 에서 또 Pop 한 노드를 왼쪽자식으로 하고

3. Operator Node 를 Stack 에 Push

3. Stack 에 마지막으로 남은 노드가 Root 이다.

손으로 계산시

전위,중위,후위 에 대한 수식을 뽑아올때 Parse Tree 로 수식을 괄호 우선 순위에 맞게 트리로 구성 한후

순회 방식에 따라 수식을 구성하면 되다

반응형
반응형

 재귀적인 방법은 속도가 떨어짐으로 iteration 한 방법으로 다시 구성한다

PreOrder_UsedStack( Node* pNode ) // pNode 이중 리스트형태로 트리를 구상하고 있는 노드

{

ListStack<Node*> stack;

stack.push( pNode );

while( !stack.IsEmpty() ){

pNode = stack.Pop();

if( pNode != m_pNodeTail )

{

visit(pNode);

stack.Push( pNode->pRight );

stack.Push( pNode->pLeft );

}

}

}

스택에 쌓아 가면서 순회한다, 재귀로 구성되지 않는다

트리로 구서되어 있는 pNode 는 헤드 노드와 꼬리 노드를 가상노들 가지고 있는 이중연결 리스트 트리이다

참고 : http://www.cyworld.com/sjh0628/6852486

but 다른 순회 들을 스택으로 바꿀려면 이보다는 쉽지 않다

반응형
반응형

[트리순회는 재귀호출로 이해될 수 있다]

트리순회(Tree Traversal)

1. Pre-order Traversal

2. In-order Traversal

3. Post-order Traversal

4. Level-order Traversal

의 순회 방법들이 있는데 이러한 순회 방법이 있는 이유는

트리는 선형이 아닌 비선혀이기 때문에 모드 노드들을 거쳐가기 위한 방법이 필요하게 된다

그리하여 위와 같은 순회 방법이 나오게 되었다

( 반례 : list 의 경우 선형이기 때문에 특별한 노드를 거쳐가는 방법을 필요로 하진 않는다)

sub 트리 : 트리의 부분 집합 참고 : http://www.cyworld.com/sjh0628/6850615

서브트리는 자식을 하나라도 가지고 있는 노드가 있다면 그 노드를 중심으로 서브트라 볼 수 있다

전위순회 (Pre-order)

Pre-Order Traversal

1. Root 를 방문한다

2. 왼쪽 subtree를 방문한다

3. 오른쪽 subtree 를 방문한다

특징 : 루트가 가장 먼저 나온다

노드를 먼저 들린다, 그래서 Pre

PreOrderTraversal( Node* pNode ){

if( pNode != m_pNodeTail )

{

visit(pNode); // 해당 노드에서 왼쪽, 또는 오른쪽으로 갈지를 정하기 전에 해당 노드를 들린다

PreOrderTraversal(pNode->pLeft );

PreOrderTraversal(pNode->pRight );

}

}

tip : 노드와 처음 마주칠때 방문한다

중위순회 (in-order)

1. 왼쪽 Subtree 를 방문한다

2. Root 를 방문한다.

3. 오른쪽

특징 : 가장 왼쪽노드가 먼저 나온다

노드를 중간에 들린다, 그래서 In

InOrderTraversal( Node* pNode ){

if( pNode != m_pNodeTail )

{

InOrderTraversal(pNode->pLeft );

visit(pNode); // 왼쪽 먼저 트리를 찾아 갔다가 왼쪽이 없으면 방문하고 오른쪽을 가기 때문에 중위 순회

InOrderTraversal(pNode->pRight );

}

}

tip : 왼쪽으로 갔다가(왼쪽 끝까지 갔다가) 다시 올라올때 방문한다

후위순회 (post-order)

1. 왼쪽 subtree 를 방문한다.

2. 오른쪽 subtree 를 방문한다.

3. root 를 방문한다

특징

1. 가장 온쪽 노드가 첫번째 나온다

2.루트가 가장 나중에 나온다

노드를 마지막에 들린다, 그래서 Post

PostOrderTraversal( Node* pNode ){

if( pNode != m_pNodeTail )

{

PostOrderTraversal(pNode->pLeft );

PostOrderTraversal(pNode->pRight );

visit(pNode); // 왼쪽, 오른쪽 다 들린 후 마지막에 올라올때 들린다

}

}

tip : 현재 노드에서 오른쪽으로 갔다가 오른쪽 끝까지 간 후 다시 올라올때 방문한다

레벨순회 (Level-order)

특징 : 루트가 가장 먼저 나온다

큐를 이용

PreOrderTraversal( Node* pNode ){

ListQueue<Node*> queue;

queue.Put(pNode);

while( !queue.IsEmpty() ) // iteration

{

pNode = queue.Get();

if( pNode != m_pNodeTail )

{

visit(pNode);

queue.Put(pNode->pLeft );

queue.Put(pNode->pRight );

}

}

}

정리하면 A 에 딸려 있는 바로 아래 자식들을 큐에 순서대로 추가 한다( A를 큐에서 제거 후 B,C 추가)

순서적으로 B,C 의 순이니 추가된 B를 제거 후 B 의 바로 한칸 아래 자식들을 다시 큐에 추가 하면

C , D, E 의 순이 된다

그러면 다시 C 에 대한 자식 F 를 큐에 추가 하고 C 를 제거(즉 앞에 있는 것을 제거 하면

트리 각 레벨에서 왼쪽에서 오른쪽의 순으로 큐 뒤에 추가 하는 것이 된다

즉 큐에서 제거 되는 노드의 자식들을 큐 뒤에 추가 함으로써 위에서 아래로, 왼쪽에서 오른쪽으로의 노드를 순회 할 수 있게 된다

tip : 큐의 가장 처음 즉 queue.Get() 할때의 노드를 제거 하면서 제거되는 노드들의 자식(2개 또는 1개) 을 큐의 뒤에 추가해준다

자식을 추가 하기 전에 노드를 방문한다

반응형

'알고리즘 & 자료구조 > 알고리즘&자료구조' 카테고리의 다른 글

수식트리(Parse Tree)  (0) 2012.10.31
스택을 이용한 전위 순회 (트리)  (0) 2012.10.31
이진트리 모델링  (0) 2012.10.31
트리(Tree)  (0) 2012.10.31
계산기 프로그램 (중위->후위)  (0) 2012.10.31
반응형

생성자에서 헤드노드와 테일 노드를 만든다

파괴자에서는 추가된 노드를 모드 제거한 후 가상 노드(헤드, 꼬리) 를 제거한다

이중리스트 처럼 가상누드인 헤드와 테일 노드를 기준으로 이진트리를 생성한다

Tip 트리를 생성할때 Left, 또는 Right 중 가르키는 겂이 없을 때는 테일 노드를 가르키도록 한다

반응형
반응형

트리는 비선형 자료구조이다(선형이 아니다 )

Node( vertex) 와 Link(edge)로 구성됨

Node 는 정보(data)를 포함한다

Link는 두 Node 간의 연결을 나타냄

특징 :

Root 가 하나 존재 해야 한다 Root 는 최상의 Node

Root를 제외한 모든 Node 는 하나의 부모를 가져야 함

Node 는 여러개으 자식을 가질 수 있다

한 Node로 부터 다른 Node로 가는 경로는 유일해야 함

ex ) 윈도우 파일 구조( 윈도우 탐색기 )

leafNode

줄기의 끝 = 자식이 없는 노드 H,J,D,E,F,G , 최 하위 노드

Terminal node : 끝인 노드

Internal Node : Leaf Node 가 아닌 노드 , 즉 자식이 하나라도 있는 노드 B,C,I,A

sub tree : 트리의 부분 집합 {B,E,F,G} , {I,J} , {D}, ,

Path : 한 Node로 부터 다른 Node 로 가는 경로(중복없이 가능 경로)

만약 G와 C 가 연결 되어 있다면 G 에서 C 로 가는 경로가 한가지가 아닌 다른 경우의 수가 생김으로 그때는 트리가 되지 않느다

만약 연결됐다면 이때는 이것을 그래프 라고 한다

레벨(Level) : Root 에서 특정 노드로 가는 경로의 노드 수, 이때 Root 를 Level 1 로 친다

그래서 위 그래프의 레벨은 4 이다

최소공통선조(Least Common Ancedstors) ; 두 노드의 공통적인 선조중 가장 레벨이 높은 선조 노드

H,J 를 놓고 봤을때

H의 선조 = C,A

J 의 선조 = I ,C, A

공통 선조 C,A

가장 높은 선조는 Level 2 인 C 가 된다

이것이 중요한 이유는 경로는 항상 최소공통선조를 거쳐서 지나가기 때문!!!

자식 노드 : 자신의 아래에 연결되어 있는 노드 => C 의 자식 노드는 H,I

부모노드는 : 자신의 위에 있는 노드 =>I 의 부모는 C

조부모 : 자신의 부모의 부모노드 I 의 부모는 C , C의 부모는 A, 그래서 I 의 조부모 노드는 A 가 된다, J 의 조부모는 C

반응형
반응형



중위표기법 4+3 인 연산자가 중간에 있는 식을


먼저 후위 표기법으로 바꾼후 다음을 실행한다


1. Operand(숫자) 를 만나면 Operand를 Stack 에 PUsh

2. Operator 를 만나면 Stack 에서 Pop 을 두번해서 이 두 값으로 연산을 한 다음

연산된 결과를 다시 Stack에 Push 한다

3. 최종결과는 마지막에 Stack 에 남아 있는 값이다(마지막 연산된 결과를 PoP 하면 결과값)


연산자를 놓느 순서는


연산자 오른쪽에 먼저 그다음 연산자 왼쪽 순으로 놓는다

연산시 +,* 는 교환이 성립함으로 간단하게 한줄로 처리 할 수 있지만

/,- 는 교환 법칙이 성립하지 않음으로 Pop 을 할때 순서에 맞게끔 구성해야 한다

문자를 숫자로 바꾸는 것은 다음 링크를 참고 한다

반응형
반응형

문자를 숫자로 변환

만약 "365" 라는 문자가 있을 경우 이것을 숫자로 변환 하려면

int n=0,i=0;

char num[4]="365", c; // num[0] == 3

num[3]='\0';

c = num[0];

do{

n = n*10 + c - '0';

c = num[++i];

}while( c >= '0' && c<='9' ) // c 가 0~ 9 사이의 숫자 안에 있다면

// 널문자를 만나면 바져나온다

결과 3 , 36 , 365


int 변수의 숫자를 한개씩 때오기

// 문자의 숫자가 증가할 수록 매칭 숫자도 증가한다

int inc=10;
int total = 123;
int result;
for(int i=0;i<3;++i){

result = total%10;
total/=10;

}

반응형
반응형

계산기 : 수식을 입력받아 계산하여 출력한다

계산기를 만들려고 할때의 데이터들로 순회구조를 변환해 본다

operator +,-,*,/ 만 가능하다고 가정(사칙연산)

Operand : 연산자 양쪽에 있는 자료형

이때의 Operand 는 정수만 가능하다고 가정

* 중위 표기법 (Infix Notation) A + B

- 우선순위를 위해 괄호를 사용해야 함

*후위 표기법(Postfix Notation):

수식을 계산하기 위한 알고리즘이 단순!

- 괄호를 쓰지 않아도 연산 가능

AB+

*전위 표기법 (Prefix Notation)

- Operator 가 두 Operand 앞에 있음

+AB

중위를 후위 표기법으로 알고리즘을 풀기 위해 중위로된 식을 후위 표기법으로 변환 해야 한다

the way that 1. 중위 표기법에서 괄호가 씌워져 있는 경우에만

조건 A+B*C 에 대한 연산이 완전한 괄호로 되어 있어야 한다

ex) (A+(B*C))

알고리즘( 결과 저장소는 String 이라 가정 )

1. '(' 문자는 무시한다

2. Operand(숫자) 는 그대로 결과저장소에 추가( += )

-하나의 숫자(항) 이 입력되면 구분하기 위해 ' ' 을 추가로 입력해 준다

3. Operator(연산자) 는 Stack 에 PUsh 한다

- 저장소에 연산자를 저장할때 여산자와 구분을 하기 위해서 스택에서 빼오 연산자를 저장소에 넣은 후 ' ' 을 하나 연달아 넣어준다,

4. ')' 를 만나면 Stack 에서 Pop 하여 결과저장소에 추가 ( += )

the way that 2. 중위 표기법에서 괄호가 없는 경우

먼저 연산자 우선 순위를 정해야 한다

'(' : 0

+,- : 1

*,/ : 2

로 우선 순위를 매기고

반응형
반응형

큐, 환원큐 모두 양방향 리스트 또는 배열로 구현 할 수 있는데

*리스트로 짤 경우 양방향리스트로 구성해야 한다

배열로 하면 배열값을 밀어내야 하는 상황이 발생 함으로

양방향 리스트로 만드는 것이 일반적으로 효율적이다

환원큐가 비었을 때는 front == real 이며

꽉 찾을때는 하나의 공간을 두고 front 와 real 이 1 간격으로 배치하게 된다

real 에서 데이터 추가, front 에서 데이터 빼온다

real 에 추가시 현재 가르키고 있는 곳에 데이터를 추가 한 후 real++

front 에서는 현재 가르키고 있는 곳에서 데이터를 빼낸 후 front++

그냥 front의 값만 가져오려면 {리스트&배열}[ front ] 의 값을 가져오고

(+ 가 순방향이라고 가정했을때)

real 의 값만 가져오려면 {리스트&배열}[Dec(real-1 )] 을 가져오면 된다

환원큐의 인덱싱은 % 를 활용

m_size 가 배열 사이즈 만약 10 이라고 한다면

현재 가르키는 곳의 다음 번지를 알기 위하여

가르키는 곳 n 에 +1 을 한 후

전체 사이즈로 나누면 배열 사이즈 버위 내에서

순환하는 인덱스 번호를 얻어올 수 있다

감소 ( Dec() )는 (n-1) % m_size;

반응형
반응형

단일연결 리스트 자체가 스택이다

단 끝노드에서 끝방향 쪽으로 노드를 추가해가며 스택을 만들면 효율이 떨어진다

단일 연결리스트로 구성된 스택은

단인 열결 리스트의 초기 구성이

{헤드노드} ~ {꼬리노드}

로 구성되어 있음으로

노드 추가 함수인 AddNode() 을 헤드 노드에서 호출 하게 된다

{헤드노드}.AddNode( pNode );

원소 추가(Push)

이것을 스택으로 사용 할려면

list_instance.AddHead( pNode );

로 써 입력해주면 된다, 즉 헤드 노드 다음에 새로운 노드를 계속 추가 하는 것

GetVal() , 그렇다면 값을 가져오기 위해선?

list_instance.GetHeadVal() 은 헤드노드가 가르키고 있는 다음 노드의 값을 가져 오면 되고

만약 다음 노드가 꼬리 노드인 경우 에러처리 또는 예외 처리 하면 된다

제거(POP) 은 헤더노드 다음 노드를 제거 하면 된다

반응형
반응형

스택(배열)

데이터 집합을 배열에 저장

Top 의 위치를 저장하는 멤버 필요

생성자에서 디폴트 배열 크기를 정해준다

-특징

top 은 가장 최근 원소를 가르키는 것임으로 top 의 초기치는 -1 이 된다

배열에 가득 차면 에러를 반환하거나 더큰 배열로 옮기든가 해야 한다

스택(리스트:다일연결리스트)

단일 연결리스트 자체가 스택이다

연결리스트에 저장( 단순 연결 리스트로 구현 할 수 있다 )

add, remove

연결리스트 특성상 스택의 크기가 자유롭다

결론 :

일반적으로 리스트 스택을 많이 사용

반응형
반응형

 첫 노드부터 끝노드 까지는

반복문을 돌면서 레퍼런스로 넘어가는 다음 포인터를 넘어받아와 이 구문을 계속 돌리면 된다

끝에서 처음으로 가는 것은 이전의 노드를 얻어오는 함수를 호출하면서 포인터를 레퍼런스로 넘기면 된다

list == 이중리스트

*find( 값 )

값과 같은 값을 찾기 위해 위와 같은 조회 방식을 이용해 값을 비교해 가며 찾을 수 있다

Tip : 끝노드를 자기 자신을 가르키고 있으니 즉

tail 의 next 는 &tailNode 이니 디버그 상에서

어느 한 노드의 next 포인터를 따라가다보면 마지막엔 tail 노드의 주소 값이 계속해서 반복적으로 나타남을 알 수 있다

반응형
반응형

정의

- 양방향 연결 리스트

- Data 와 Next pointer, Prev Pointer 로 구성

헤더와 꼬리가 초기에 주어진다는 점은 단순연결 리스트와 동일

초기화는

헤더는 의 prev 는 자기자신 이고 next 는 테일을 가르킨다

테일에서는 next 가 자기자신을 가르키고 prev 가 헤드를 가르키도록 한다

각 노드에는 데이터를 답을 수 있는 templte< typename T > 인 T 로 정의 한다

Tip : insert 시 기준이 되는 노드와 멀리떨어져 있는 노드와 새로운 노드와의 연결을 먼저 연결한다

because 기준이 되는 노드와 새로운 노드를 먼저 연결하면 기존에 연결 되어 있던 노드의 포인터를 잃어버리기 때문

1. 삽입

이전 삽입 :

define void* POS

- 다음삽입( Insert Next) 도 InsertBefore 와 유사하게 마들어 질 수 있다

*삭제

이중리스트는 양쪽의 포인터 정보를 알기 때문에 자기 자신을 지울 수 있다

삭제할 노드의 양쪽 노드들이 삭제 할 노드를 가르키지 않고 기준 노드의 양쪽노드들을 서로가 가르키게 한다

그런 후 삭제기준 노드를 삭제

반응형
반응형


http://cafe.naver.com/jzsdn/2468




#ifndef __ICMSINGLETON_H
#define __ICMSINGLETON_H

#include <stdio.h>
#include <WINDOWS.H>

template< class C >
class ISingleTon
{
public:
ISingleTon()
{
if( !m_pInstance )
{
int nOffset = ( int )( C* )1 - ( int )( ISingleTon< C >* )( C* )1;
m_pInstance = ( C* )( ( int )this + nOffset );
}
}
virtual ~ISingleTon() {}

static bool CreateInstance()
{
if( !m_pInstance )
{
m_pInstance = new C;
}

if( m_pInstance )
return true;
return false;
}

static void ReleaseInstance()
{
SAFE_DELETE( m_pInstance );
}

static C* GetInstance()
{
return m_pInstance;
}

protected:
static C* m_pInstance;
};

template< class C >C *ISingleTon< C >::m_pInstance = NULL;

#endif

이건 싱클톤 패턴을 템플릿으로 구성한 인터페이스 클래스입니다.( Game Programming Gems에서 봤었던걸로 기억합니다. )

일일이 클래스에 GetInstance()와 ReleaseInstance()를 추가하는 수고를 덜수 있습니다.

CreateInstance()를 따로 둔건 쓸데없는 걱정일지도 모르지만 GetInstance()에서 m_pInstance의

유효성체크를 하는부분이 거슬려서입니다.

사용법은

#include "ISingleTon.h"

class CSession;

class CSessionManager : public ISingleTon < CCMSessionManager >

{

public:

CSessionManager();
virtual ~CSessionManager();

void AddSession( CSession* pSession );

......

}

#define g_SessionMng CSessionManager::GetInstance()

main()

{

CSessionManager::CreateInstance();

CSession* pSession = new CSession;

g_SessionMng->AddSession( pSession );

.......

}

반응형
반응형

http://openparadigm.tistory.com/20

빅오표기법은 간단하게 알고리즘의 성능을 평가할 수 있는 방법이다. 벤치마킹을 통해 실제적인 알고리즘 분석도 가능하지만 어떻게 모든 알고리즘을 전부 테스트하고 사용하겠는가.. 그냥 대충 '각'을 잡아본 후에 사용하는 것이 효율적이다.

이러한 알고리즘 테스트를 위한 여러가지 방법이 있다. 3가지 정도가 있는데 Ω,θ,O 이렇게 세가지이다.
- O표기법은 알고리즘의 최악의 성능을 표시해준다.
- Ω표기법은 알고리즘의 최고의 성능을 표시해준다.
- θ표기법은 정확한 알고리즘의 성능을 표시해준다.
그럼에도 불구하고 O표기법을 많이 사용하는 이유는 '아무리 최악의 상황이라도 이정도의 성능을 보장할 수 있다'라는 것을 보여주기 위해 O표기법을 사용하는 것이다. 괜히 Ω표기법으로 알고리즘을 평가해서 최악을 성능을 내는 경우가 발생하면 기분나쁘잖아 ㅡㅡ;; θ표기법은 사실 알고리즘을 계산으로는 정확히 알기 어렵기 때문에 그닦 많이 사용되진 않는다.

그럼 우리는 O표기법만 알아보자.
다음과 같은 코드가 있다.

void main(){
      int sum = 0;
      int i;

      for(i=1;i<=100;i++){
            sum=sum+i;
      }
      printf("1부터 100까지의 합: %d\n",sum);
}


요런 코드가 있다면 이것은 1부터 100까지 더하는 프로그램이 된다.
이걸 각각의 코드가 몇번 실행 되는지 확인해 보자.

int sum = 0;
1번실행
int i;
1번실행
for(i=1;i<=100;i++){
101번실행(100번실행후 한번 더 실행하는 과정에서 밖으로 빠진다.)
sum = sum + i;
100번실행
printf("1부터 100까지 합:",sum); 1번실행


결국 총 204번이 실행된다. 이것을 빅O표기법으로 표시하면 O(204)가 된다. 쉽죠잉?
"O표기법에서 상수의 존재는 알고리즘의 성능에 아무런 영향을 끼치지 못한다"는 것을 알아두길 바란다.
그럼 O(204)를 O(1)로 간단히 표시하겠다. 앞으로 O(100000)이든 O(3)이든 상수만 들어간 O는 모조리 O(1)로 생각하자.
왜냐하면 위와 같은 코드의 실행 시간은 10번 실행이 10000번 실행보다 빠른게 당연하겠지만 알고리즘 자체로 본다면 데이터의 양이 100 이런식으로 고정되어 있기 때문에(위에 100번 돈다고 했으니까) 다른 입력된 데이터가 많더라도 알고리즘은 여전히 100번만 돌게된다. 즉 입력데이터의 양에 상관없이 성능이 일정하다는 말이다.

근데 만약에 위의 for문이 for(i=1;i<=N;i++) 라고 되어 있다면 어떻게 될까? N번 도는 것이다. N이 얼마인지는 모르겠지만 외부에서 N을 입력받던 계산의 값으로 받던 하여튼 임의의 값이 들어오게 될것이다. 이럴때 우리는 O(N)이라고 표시한다.

여기서 다른 코드를 보자.



이건 최대값을 구하는 코드이다. array[0]을 max에 넣고 for문을 시작한다. array[1]부터 max와 비교하며 array[1]이 max보다 크다면 array[1]을 max에 넣는다. 아니라면 array[2]와 max를 비교한다. 이런식으로 계속 큰 값만 max에 넣으면 배열에 마지막에 가서는 가장 큰값이 max에 있을것이다.

이코드는 총 몇번이 실행되는가? 총 for문은 size-1만큼 실행된다. 편의상 size를 N으로 두자. N-1만큼 실행된다. 이걸 O표기법으로 표시하면 O(N-1)이 된다. 만약 N이 거의 수억천만조경의 값이라면 -1따위야 가뿐히 무시할 수 있는 값이다. 그래서 O(N)으로 표시한다.(O표기법에서 상수란 무용지물이란 말이다..) N의 값이 많아지면 많아질 수록 처리 시간도 그와 비례해서 늘어난다는 의미이다.

위의 코드와 같은 결과를 가지는 아래의 코드를 보자.

int returnMax(int array[], int size){
    if (size < 1) return -1;                       
    bool findMax;                                   
    for (int i = size-1; i > 0; i--) {             
         for (int j = 0; j < size; j++) {          
              if (array[j] > array[i]) {           
                   findMax = false;               
              }
         }
         if(findMax)                               
              break;
    }
    return array[i];
}


위의 코드도 최대 값을 반환한다. 코드를 말로 해석하려 했는데 너무 어렵다 ㅡㅡ;; 그냥 코드를 보시고 이해하길...
보면 알겠지만 이코드는 안쪽 for문에서 N번 반복하고 바깥쪽에서도 N번 반복한다. 다시말해 안쪽의 for문은 N번 반복을 N번이나 한다는 말이다. 물론 최대값이 배열 가장 앞에있는 최악의 경우에 말이다. 이것을 빅오로 나타내면 O(N²)으로 표시할 수 있다. 입력된 데이터의 양(N)에 따라 시간은 그 제곱배로 늘어난다는 말이다.

물론 이코드는 상황에 따라 다르다. 만약 가장 큰값이 가장 끝에 있다면 가장 끝값과 앞에 값들을 각각 한번씩만 비교하면 답이 나오기 때문이다. 이것은 O(N)이된다. 이건 최선의 경우일 때이다. 그리고 평균적으로 계산해 보자면 최대값이 배열 가운데있다면 N번 반복을 N/2번만 하면 되기에 결국 O(N²/2)로 나타낼 수 있다. 그럼 빨라진건가? 그렇지 않다 평균결과는 여전히 O(N²)이다. 왜냐하면 위에서 밝혔듯이 빅오에서 상수의 의미는 없다고 생각하는 것이 좋다고 했다. 책에는 그 이유를 '기계어와 CPU가 처리하는 시간에 따라 결과시간이 달라지기 때문' 이라고 밝혔지만 나도 정확이 이게 무슨말인지는 모르겠다. 여튼 빅오에서는 상수를 무시하고 최고차항이 얼마느냐에만 촛점을 맞추고 있다. 그래서 평균 시간도 O(N²)라고 정의해야한다(고 한다 ㅡㅡ;)

그밖에 경우는 아래와 같다.

O(logN) - 처리 데이터의 양이 증가할 수록 실행시간이 약간씩 증가한다. 하지만 logN의 그래프가 그렇듯이 증가 폭이 급격히 증가하진 않는다. 일반적으로 좋은 효율의 알고리즘이 여기에 해당한다.

O(NlogN) - 처리 데이터의 양의 증가에 대해 처리 시간은 정비례보다 약간 더 많이 증가를 한다. 이것도 효율이 괜찮은 알고리즘일 경우의 계산이다.

O(N³) - 데이터에 양에 대해 세제곱 만큼 시간이 증가하기에 좋은 알고리즘이 아니다. for문이 세번 중첩되면 이꼴이 난다.

O(2^N) - 2의 N제곱이 되는 알고리즘은 거의 최악의 알고리즘 중 하나이다.

아래에 각 결과의 처리시간에 대한 그래프를 가져와 봤다.

<출처 : http://media.photobucket.com/image/o%20notation/amit_9b/BIGO_GRAPH.jpg>


<Big O Notation Graph>

간단하게 빅오표기법을 사용하는 방법을 정리하자면
1. 입력값이 무엇인지 확인한다.(N)
2. 수행 연산 횟수를 N의 식으로 표현한다.
3. 가장 차수가 높은 항만 남기고 그 아래차수와 상수는 날려버린다.

주의점
1. 반복문이 중첩이 아니라 그냥 두개가 나란히 있는경우는 더 높은 차수의 for문을 알고리즘 성능으로 사용한다.
2. 상수가 곱해져 있고 옆에 N이 더 있더라도 최고차수만 사용한다.

+
재귀호출에서의 빅오표기법???? 재귀호출은 덧셈처럼 계산되어서 O(N)으로 사용된다는데 이건 이후에 다시 포스팅하자.

반응형
반응형
http://goo.gl/ZMRMU


이번에 알아볼 것은 힙(Heap)이다. 얼핏 힙을 들어보면 어려울 것만 같이 느껴질 수도 있으나

집중해서 보면 이해가 금방되는 구조다.

 

힙 트리는 간단하게 말하면 이진트리이나 다음과 같은 성질을 가지고 있다.

 

1. 아래에서 선택

   1-1. 부모 ≥ 자식들 (최대 힙)

   1-2. 부모 ≤ 자식들 (최소 힙)

2. 완전이진트리

3. 데이터 중복허용

 

 

힙은 조밀조밀하게 얽혀있는 완전이진트리이다.

 



                               완전이진트리                                                             완전이진트리

 

완전이진트리에 중간에 이상하게 허한 트리는 아닌 하나씩 촘촘하게 들어가는 트리를 말하니 이 점을 확실히 알아두자.

만약 5 노드 아래의 둘 중 하나 없다면 완전이진트리의 구조가 깨지는 것이다.

 

힙 트리는 조밀조밀하게 구성되어있다고 했다. 그러기에 힙을 표현하는 방법은 보통 배열을 이용해서 나타낸다.

(정적배열이든 동적배열이든 상관없다.)

 

중요하게 생각해야하는 가장 기초적인 지식은 트리의 배열 표현 방법이다.

배열로 이진트리를 표현하는 방법은

기본적으로 array 배열의 부모 노드의 인덱스가 i 라 하면,

자식노드는 array[i*2] (왼쪽 자식)와 array[i*2+1] (오른쪽 자식)이 된다.

 

왼쪽 자식의 인덱스 = (부모 인덱스)*2

오른쪽 자식의 인덱스 = (부모 인덱스)*2+1

 

힙트리도 이진트리라는 점을 상기하면 금방 이해할 수 있다. 보통 배열로 트리를 표현함에 있어서

자식의 위치에서 부모의 존재를 유추할 때는 반으로 나누면 된다. 그렇기에 루트노드는 1부터 시작한다.

아무리 나눠도 0으로 나눌 수 없으니까 말이다.

 

 

힙트리에는 두 가지 종류가 있다.

1-1. 부모 ≥ 자식들 (최대 힙)

1-2. 부모 ≤ 자식들 (최소 힙)

 

부모의 노드가 자식보다 큰 힙트리와 보모의 노드가 자식보다 작은 힙트리이다.

의도에 맞게 선택하여 프로그래밍 하면 된다.

 

이제 최대 힙을 이용하여 힙 트리를 설명하겠다.

 

02: #define MAX     100
03: typedef struct HeapTree04: {
05:         int heap[MAX];
06:         int size;
07: }HeapTree;
08: 
09: HeapTree *Heap;


 

힙 트리의 구조체이다. 일반적으로 힙트리는 배열이라고 했다. 여기서는 정적배열을 이용한다.

현재 힙트리의 크기를 알 수 있도록 size 라는 변수를 두었다.

 

힙도 트리다보니 삽입과 삭제의 방법이 중요하다. 일단 삽입에 대해서 이야기해보자.

최대 힙의 경우 새롭게 넣은 값이 부모보다 크다면 값을 교환을 해주면 된다. 하나의 조직에서 서열이 바뀌는 셈이다.

 

삽입의 방법은

 

1. 힙 트리의 나머지 공간에 넣는다.

2. 부모와 비교하며 자신보다 작으면 바꾼다.

3. 자신보다 크거나 같은 값이 나올 때까지 2번을 반복한다.

 

 

일단 위의 트리에 10을 넣어보자.

 

 

 

 

그렇다면 새롭게 추가된 10은 부모노드와 비교해가며 자신보다 부모가 클 때까지 서로 위치를 교환하며 주욱 올라간다.

 

 

 

 

다음과 같이 교환이 모두 끝났다면 제일 위의 노드는 바로 10이 된다.

 

 

 

 

그렇다면 7을 넣는다면 어떨까? 마찬가지로 차례로 트리의 끝에 추가된다.

 

 

 

 

그리고 바로 위의 부모노드 5보다 크니 교환을 해서 올라가나 다음 부모인 9보다 작으니 그 상태에서 멈춰버린다.

 

 

  

 

 

이것이 바로 힙트리의 삽입과정을 나타낸 것이다.

 

16: void InserData(HeapTree *Heap, int element)
17: {
18:         int i;
19: i=++Heap->size;
20: 
21:         while( (i!=1) && 
22:                         (element > Heap->heap[i/2]) )
23:         {
24:                 Heap->heap[i] = Heap->heap[i/2];
25:                 i/=2;
26:         }
27:         Heap->heap[i]=element;
28: }

 

삽입은 뭐 그리 대단한 것은 아니다. 일단 배열의 인덱스를 나타낼 i 를 선언하고

힙의 크기를 +1 시켜준다. (19번줄) 일단 삽입했다고 가정을 하고

 

끝단에서부터 부모로 올라가라면 2 씩 나눠서 올라가야 한다는 점을 상기시켜보자.

루트의 인덱스는 1이니 루트에 도착하지 않았고        ( i!=1 )

삽입한 요소가 부모보다 클 때 교환이 일어나야 한다. (element > Heap->heap[i/2])

 

 

24:                 Heap->heap[i] = Heap->heap[i/2];
25:                 i/=2;

 

 

24번줄은 부모노드를 자식노드로 끌어내리고 인덱스를 부모노드로 올라가는 것이다.

이렇게 루프가 끝나고 나서 현재의 위치에 요소를 대입하면 힙 트리에서 삽입이 끝난다.

 

 

이제 삭제를 알아보자. 힙 트리에서 삭제는 바로 루트를 빼내는 것이다.

루트를 빼내다보니 트리 구조가 무너지게 되는 것은 당연지사. 이를 위해 트리를 재구축 시키는 과정이 필요하다.

재구축 방법은 힙트리의 끝의 단말노드를 루트에 위치시키고 차례로 차식노드들과 비교해가며 재정렬하는 과정이 필요하다.

 

1. 루트를 꺼낸다.

2. 트리의 끝에 해당하는 단말노드를 임시적으로 루트로 위치시킨다.

3. 루트에 위치한 임시노드는 차례로 자식들과 비교하며 자신의 자리를 찾아간다.

 

 

아래의 힙트리에서 삭제연산을 하면 맨 위의 루트 10이 꺼내지게 된다.

 

 

 

 

꺼내진 자리를 힙 트리의 끝단에 위치한 노드 5가 그 자리를 대신한다.

 

 

 

그리고 임시노드 5는 자신의 위치를 찾아간다. 찾아갈 때 자식의 두 노드 중에서 어디로 나아갈지를 정해야하는데

자식들 중 큰 값이 있는 방향으로 나아간다. 임시노드 5는 자식노드인 9와 4 중, 9로 나아간다. (9>4)

 

 

 

 

 

이것을 코드로 표현하면 다음과 같이 된다.

 

30: int DeleteData(HeapTree *Heap)
31: {
32:         int parent=1, child=2;
33:         int element, temp;
34:         
35:         element=Heap->heap[1];
36:         temp=Heap->heap[Heap->size--];
37: 
38:         while( child <= Heap->size )
39:         {
40: if( Heap->heap[child] < Heap->heap[child+1] )
41:                         child++;
42: 
43:                 if( temp >= Heap->heap[child] ) break;
44: 
45:                 Heap->heap[parent]=Heap->heap[child];
46: 
47:                 parent=child;
48:                 child*=2;
49: 
50:         }
51:         Heap->heap[parent]=temp;
52:         return element;
53: }

 


차근차근 설명해볼까? 삭제를 할 때 꺼내지는 요소(element)는 루트이다. Heap->heap[1]

그리고 임시노드가 되는 것은 힙 트리의 끝에 있는 단말노드이므로 그 자리에서 떼어낸다.  Heap->heap[Heap->size--]

 

 

35:         element=Heap->heap[1];
36:         temp=Heap->heap[Heap->size--];

 

 

그리고 눈여겨 봐야할 것이 루트에 위치시킨 임시노드(temp)을 자신의 위치로 찾아가도록 하는 과정이다.

 

 

32:         int parent=1, child=2;

 

 

에서 일단 루트에서부터 찾아갈 것이니 parent=1 로, child=2로 선언한다.

 

 

38:         while( child <= Heap->size )


 

부모의 위치에서 자식들을 비교해야하기 때문에 힙트리의 크기(Heap->size)를 넘어서면 안 된다.

 

 

40: if( Heap->heap[child] < Heap->heap[child+1] )
41:                         child++;

 

 

위에서 임시노드는 자신의 위치를 찾아가기 위해 자식들과 비교하며 나아가는 방향을 정할 때

자식들 중 큰 값을 가지는 자식들로 방향을 정해서 내려간다고 했다.

 

만약 임시노드가 자식노드보다 크다면, 일단 힙트리의 조건을 만족하기에 교환을 멈춘다. (43번줄)

 

 

43:                 if( temp >= Heap->heap[child] ) break;

 

 

그것이 아니면 자식을 부모노드와 교환을 하며(45번줄) 계속 인덱스를 자식단으로 내려간다 (47-48번줄)

 

 

45:                 Heap->heap[parent]=Heap->heap[child];
46: 
47:                 parent=child;
48:                 child*=2;

 

 

루프가 끝났다는 말은 일단 임시노드의 진짜 자리를 찾았다는 의미가 되니 그 자리에 임시노드를 위치시킨다.

 

 

51:         Heap->heap[parent]=temp;

 

 

child 는 일단 *2 를 시킨다고 했으니 parent 가 바로 그 자리인 셈이다.

그리고 처음에 꺼낸 값을 리턴한다. (return element)

 

 

여기까지가 힙트리의 삽입과 삭제의 과정이다. 여기서 설명한 힙트리는 최대 힙이다. 

최대 힙은 컴퓨터과학 전분야에서 사용되는 우선순위라는 개념이다.

우선순위가 높은 프로세스에게 먼저 메모리를 할당하거나 처리할 때 힙 트리가 사용된다.

 

이러한 힙트리를 이용해서 힙 정렬도 할 수 있다.

 

힙 정렬은 그냥 힙트리에 데이터를 막 집어넣고 하나씩 뽑아내면 정렬이 된다!!!!

너무 간단하다! 트리이다보니 시간복잡도는 O(log2 (n)) 이다. 합병정렬(Merge sort)와 최악의 시간복잡도가 같다.

 

 

55: void HeapSort(HeapTree *Heap, int *array, int array_length)
56: {
57:         int i;
58: 
59:         for(i=0;i<array_length;i++)
60:                 InserData(Heap, *(array+i));
61: 
62:         for(i=0;i<array_length;i++)
63:                 *(array+i)=DeleteData(Heap);
64: }


 

힙 정렬 함수는 이렇게 간단하게 구현이 된다. 주욱 넣어서 주욱 빼내면 된다.

이것을 사용하는 메인함수를 만들어볼까?

 

67: int main()
68: {
69:         int i;
70:         int array[5]={3,9,1,4,8};
71:         HeapTree Heap;
72:         InitHeap(&Heap);
73: 
74:         HeapSort(&Heap,array,5);
75: 
76:         for(i=0;i<5;i++)
77:                 printf("%d ",array[i]);
78:         
79: 
80:         return 0;
81: }

 

 

 

오름차순으로 정렬을 하려면 최소 힙으로 고쳐주면 된다. 삽입함수와 삭제함수에서 비교문만 살짝 고치면 된다.

첨부소스를 참조하여 확실히 외워두자.

반응형
반응형

http://yeols.com/80074139499




힙 정렬(Heap Sort)

 

  • 고급 정렬 알고리즘(병합 정렬, 퀵 정렬, 힙 정렬) 중 하나.

    먼저, 힙 정렬을 알아보기 전에 힙 정렬에 필요한 이진 트리(Binary Tree)의 개념 부터 알고 넘어가자.

  • 이진 트리(Binary Tree)

    • 최대 두 개까지의 자식 노드를 가질 수 있는 트리
      • 하나의 노드는 0, 1 혹은 2개의 서브 트리를 가질 수 있다.
      • 좌 서브 트리(Left Subtree)
      • 우 서브 트리(Right Subtree)

      • 널 트리(Null tree)
        - 어떠한 노드도 가지고 있지 않은 트리

  • 포화 이진 트리(Full Binary Tree)

    • 높이 h 인 이진 트리에서 모든 단말 노드가 레벨 h 에 있는 트리
      • 최대 노드 수 : 

  • 완전 이진 트리(Complete Binary Tree)

    • 높이(h-1)까지는 포환 이진트리 이고, 마지막 레벨 h에서, 왼쪽에서 오른쪽으로 단말 노드를 채운 것

  • 편향 이진 트리(Skewed Binary Tree)

    • 이진 트리 중에서 최소 개수의 노드를 가지면서 왼쪽이나 오른쪽 서브 트리만 가지고 있는 트리

  • 이진 트리의 순차 자료구조 표현

    • 완전 이진 트리의 배열 표현 방법

  • 이진 트리의 순차 자료구조 표현

    • 편향 이진 트리의 배열 표현 방법


그럼 이제 힙(Heap)에 대해서 알아 보자.

  • 힙(Heap)

    • 완전 이진 트리에 있는 노드 중에서 키 값이 가장 큰 노드나 키 값이 가장 작은 노드를 찾기 위해서 만든 자료구조

    • 최대 힙(Max Heap)
      • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크거나 같은 크기의 관계
        - ( 부모 노드의 키 값 ≥ 자식 노드의 키 값 ) 의 관계를 가지는 노드들의 완전 이진 트리

    • 최소 힙(Min Heap)
      • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작거나 같은 크기의 관계
        - ( 부모 노드의 키 값 ≤ 자식 노드의 키 값 ) 의 관계를 가지는 노드들의 완전 이진 트리

  • 힙(Heap)의 예

  • 힙(Heap)이 아닌 예

  • 힙 정렬(Heap Sort)의 개념

    • 힙 자료구조를 이용한 정렬 방법

    • 힙(Heap)에서 항상 가장 큰 원소가 루트 노드가 되고 삭제 연산을 수행하면 항상 루트 노드의 원소를 삭제하여 반환
      • 최대 힙에서 대해서 원소의 개수만큼 삭제 연산을 수행하여 내림차순으로 정렬 수행
      • 최소 힙에 대해서 원소의 개수만큼 삭제 연산을 수행하여 오름차순으로 정렬 수행

    • 힙 정렬(Heap Sort) 수행 방법
      1. 정렬한 원소들을 입력하여 최대 힙 구성
      2. 힙에 대하여 삭제 연산을 수행하여 얻은 원소를 마지막 자리에 배치
      3. 나머지 원소에 대해서 다시 최대 힙로 재구성
           원소의 개수만큼 2~3을 반복 수행

 

  • 힙 정렬 수행 과정

    • 정렬되지 않은 [69, 10, 30, 2, 16, 8, 31, 22]의 자료들을 힙 정렬 방법으로 정렬하는 과정을 살펴보자.
      • 초기 상태 : 정렬할 원소가 8개 이므로 노드가 8개인 완전 이진 트리를 만들고, 최대 힙(Max Heap)으로 구성함

(최대힙 구성시 우측 서브트리의 숫자 20 >> 30 임을 알려드립니다.)

 

1. 힙에 사제 연산을 수행하여 루트 노드의 원소 69를 구해서 배열의 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

2. 힙에 삭제 연산을 수행하여 루트 노드의 원소 31을 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

3. 힙에 삭제 연산을 수행하여 루트 노드의 원소 30을 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

4. 힙에 삭제 연산을 수행하여 루트 노드의 원소 22를 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

5. 힙에 삭제 연산을 수행하여 루트 노드의 원소 16을 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

6. 힙에 삭제 연산을 수행하여 루트 노드의 원소 10을 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

7. 힙에 삭제 연산을 수행하여 루트 노드의 원소 8을 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

8. 힙에 삭제 연산을 수행하여 루트 노드의 원소 2을 구해서 배열의 비어있는 마지막 자리에 저장한다.

나머지 원소들에 대해서 최대 힙으로 재 구성함

 

 

 

  • 힙 정렬 알고리즘 분석

    • 메모리 사용공간
      • 원소 n개에 대해서 n개의 메모리 공간 사용
      • 크기 n의 힙 저장 공간

    • 연산 시간
      • 힙 재구성 연산 시간
        - n개의 노드에 대해서 완전 이진 트리는 log₂(n+1)의 레벨을 가지므로 완전 이진 트리를 힙으로 구성하는 평균 시간은 O(log n)
        - n개의 노드에 대해서 n번의 힙 재구성 작업 수행
      • 평균 시간 복잡도 : O(n log n)

반응형
반응형

http://www.codesos.com/book/algori/hash/hash.html

해슁의정의

여러개의 명칭(identifier)들이 무작위로 들어있는 테이블에서 특정 명칭을 찾고자 하는 경우

원하는 키값을 가지는 테이블 항목을 검색하기 위해 특정한 변환 함수를 이용하여 키값을 항목 의 주소로 직접 바꿔서 검색하는 방법을 해슁이라 한다. 이때 변환 함수는 해쉬 함수(hash function)라 한다.


해슁의필요성

명칭 테이블에서 키 값과 일치하는 명칭을 찾는 방법으로는 테이블에 있는 각각의 명칭을 키 값과 차례로 비교하는 방법이 있다.

이 방법을 사용하면 최악의 경우 n회의 비교가 필요하다. 해슁을 이용하면 해쉬 함수가 키값을 해당 주소로 단번에 변환해 주므로 매우 빠른 검색이 가능하다.


정적 해슁

정적 해슁은 고정 크기의 테이블을 이용하여 해슁하는 방법으로서 한번 저장된 명칭의 상대적 위치가 변경되지 않는다.

정적 해슁은 다음 순서로 설명한다.


해쉬 테이블(hash 테이블)

해슁을 이용하지 않는 경우에는 명칭들의 모든 가능한 조합을 수용할 수 있도록 명칭 테이블을 만들어야 한다.

조합 가능한 명칭들 중에 실제로 존재하는 명칭들의 수는 매우 적기 때문에 대부분의 공간은 낭비된다.
명칭 테이블의 기억공간 낭비를 막기 위해 해쉬 테이블을 사용한다.
해쉬 테이블은 b개의 버켓(bucket)으로 구성되고, 하나의 버켓은 s개의 슬롯(slot)으로 구성된다. 각각의 슬롯에는 명칭 테이블 항목처럼 하나의 명칭이 저장된다.

< 해쉬 테이블의 구조 >

해쉬 테이블의 크기는 필요에 따라 달리할 수 있는데 일반적인 명칭 테이블보다는 크기가 현저히 작다. 하나의 버켓에 여러개의 슬롯을 두는 이유는 서로 다른 두개의 명칭이 해쉬 함수에 의해 동일한 주소로 변환되는 경우 두 명칭을 같은 버켓에 저장하기 위해서이다.


해슁의 문제점

해슁을 하는 경우 서로 다른 두개 이상의 명칭이 해쉬 함수에 의해 동일한 주소로 변환되는 경우가 있다. 이 현상을 "충돌(collision)"이라 한다. 충돌이 자주 발생하면 탐색 시간이 길어지는 등 성능이 저하되므로 해쉬 함수를 수정하거나 해쉬 테이블의 크기를 적절히 조절해 주어야 한다.

충돌이 발생한 경우 같은 버켓에 있는 다른 슬롯에 명칭을 저장하면 된다. 그러나 슬롯의 갯수만큼 충돌이 생기면 빈 슬롯이 소진되어 오버플로우(overflow)가 생길 수 있다. 오버 플로우가 발생하면 해슁에 의해 원하는 명칭을 찾을 수 없게 되므로, 오버 플로우를 해결하기 위한 방법이 고안되어야 한다.


해쉬 함수

해쉬 함수는 입력된 키값을 해쉬 테이블의 주소로 변환시켜주는 함수이다. 해쉬 함수는 가능한 충돌이 적게 발생해야 하므로, 함수의 출력값이 해쉬 테이블의 주소 영역 내에서 고르게 분포되어야 한다.

주로 사용되는 해쉬 함수는 다음과 같다.

mid-square 함수
키 값을 제곱한 후에 중간에 몇 비트를 취해서 해쉬 테이블의 버켓 주소로 만들어 준다. 키 값을 제곱하면 결과값의 중간 비트들은 키 값의 모든 비트들로부터 영향을 받으므로 버켓 주소가 고르게 분산될 가능성이 높다.
다음은 검색 키가 5비트로 구성되어 있고, 버켓 주소도 5비트로 구성된 해쉬 테이블에서 mid-square 함수 해슁을 하는 예를 보여준다.

division 함수
키 값을 해쉬 테이블의 크기로 나누어서 그 나머지를 버켓 주소로 만들어준다.
버켓 주소 = 키 값 % 해쉬 테이블 크기
folding 함수
키 값을 버켓 주소 크기만큼의 비트수를 가지는 부분으로 분할한 후, 분할된 것을 합산하여 버켓 주소를 만들어준다.
다음 그림은 키 값이 "12321631067"이고 버켓 주소가 10진수 3자리로 구성되어 있는 경우의 예를 보여준다.


오버플로우의 해결방법

선형 개방 주소지정법

선형 개방 주소지정법은 충돌이 발생한 경우에 그 위치로부터 비어있는 다른 버켓을 찾아 그곳에 명칭을 저장하는 방법으로서 선형 탐색법(linear probing)이라고도 불린다.
선형 개방 주소지정법을 이용할 때 해쉬 테이블은 1차원 배열 형태를 가진다.

명칭을 삽입하는 경우

  1. 해당 키 값을 가지는 명칭을 해쉬 함수로 변환하였을 때 그 위치의 버켓이 비어있으면 삽입한다.
  2. 버켓이 비어있지 않으면 그 위치부터 해쉬테이블을 차례로 조사하여 빈 버켓을 찾은 후 그 위치에 명칭을 삽입한다.
  3. 해쉬 테이블의 끝까지 빈 버켓이 없으면 테이블의 처음부터 원래 삽입한 위치까지 빈 버켓을 찾는다.
  4. 빈 버켓이 전혀 없는 경우는 명칭을 삽입할 수 없으므로 오류 발생을 고지한다.

명칭을 검색하는 경우

  1. 해당 키 값을 가지는 명칭을 해쉬 함수로 변환하여 얻어진 위치에 원하는 명칭이 들어있는지 조사한다.
  2. 원하는 명칭이 없다면 삽입시에 충돌이 발생한 것이므로, 그 위치부터 차례로 해당 명칭을 검색한다.

선형 개방 주소 지정법의 장단점

장점

  • 해쉬 테이블의 구조가 간단하다.

단점

  • 충돌이 발생한 경우에, 최악의 경우 해쉬 테이블 전체를 검색해야 하는 경우가 발생하므로 비효율적이다.


체인 이용법

충돌이 발생한 경우에 동일 버켓에 들어가야할 명칭들을 연결 리스트로 저장해 두는 방법이다.

체인 이용법을 이용할 때는 해쉬 테이블의 각 버켓이 하나의 연결 리스트가 된다.

명칭을 삽입하는 경우

  1. 해당 키 값을 가지는 명칭을 해쉬 함수로 변환하여 주소를 얻어낸 후 그 위치에 있는 연결 리스트에 삽입한다.

명칭을 검색하는 경우

  1. 해당 키 값을 가지는 명칭을 해쉬 함수로 변환하여 주소를 얻어낸다.
  2. 얻어진 위치의 연결 리스트에 노드가 한개 뿐이면 충돌이 발생하지 않은 것이므로 그 값을 읽어온다.
  3. 얻어진 위치의 연길 리스트에 노드가 두개 이상이면, 그 연결 리스트의 노드들을 차례로 검색하여 그 값을 읽어온다.

체인 이용법의 장단점

장점

  • 충돌이 발생한 명칭들만을 연결 리스트에서 검색해 주면 되므로 속도가 빠르다.
  • 삽입 가능한 명칭의 수가 해쉬 테이블 크기에 영향을 받지 않는다.

단점

  • 구조가 복잡하고, 기억장소 소모량이 많다.


동적 해슁

동적 해슁의 필요성

항목과 삽입과 삭제가 빈번히 발생하는 응용에는 정적 해슁이 적합치 적합치 못하다. 고정된 크기의 해쉬 테이블을 사용하는 정적 해슁의 경우, 삽입이 많아지면 테이블이 가득차서 사용이 불가능하고 삭제가 많아지면 많은 공간이 사용되지 않으므로 낭비가 발생한다. 이러한 응용에 적합하도록 고안된 것이 동적 해슁(dynamic hashing) 또는 확장성 해슁(extendible hashing)이다.

동적 해슁의 구성

동적 해슁을 위해서 해쉬테이블 대신에 트라이(trie)라는 자료구조를 이용한다.

트라이는 입력키의 각 비트값에 따라 2개의 가지로 분기되도록 만들어진 것이다. 위의 그림은 두 비트의 키를 가지는 트라이다. 키 값의 우단 비트(LSB)부터 검사하여 그 값이 0이면 첫번째 레벨에서 위쪽으로 분기하고 , 1이면 아래쪽으로 분기한다. 그 다음에는 오른쪽 끝에서 두번째 비트를 검사하여 그 값이 0이면 두번째 레벨에서 윗쪽으로 1이면 아래쪽으로 분기한다. 실제 항목들이 저장되는 부분은 해쉬 테이블의 버켓처럼 여러개의 슬롯이 존재한다. 동적 해슁에서는 버켓을 페이지라 부른다.

동적 해슁에서의 검색

위에 나열된 것처럼 6비트로 이루어진 6개의 명칭이 트라이에 저장되어 있다고 하자. 트라이의 말단에 있는 페이지에는 두개의 슬롯이 있다. 이러한 경우 4개의 페이지에 명칭들을 저장할 수 있으므로 두비트의 키가 필요하다.

검색을 위해서 우단의 두 비트를 검사하여 비트값에 따라 위, 아래로 분기한다. 해당 페이지를 찾은 후 페이지내의 스롯에 저장된 명칭들을 차례로 비교하여 원하는 명칭을 검색해낸다.

동적 해슁에서의 삽입

<삽입전>

위의 그림에 나온 트라이에 "110101"이라는 명칭을 삽입해 본다. 먼저 명칭이 삽입될 페이지를 찾는다. 우단의 두 비트를 이용하여 검색하면 3번 페이지가 해당되는데 이 페이지에는 더이상 명칭을 저장할 슬롯이 없기 때문에 둘로 분할해야 한다. 페이지가 분할되어 레벨이 한 단계 깊어지면 검색에 필요한 비트 수가 한개 늘어난다.

<삽입후>

반응형
반응형

** 버튼입력 이벤트처리 **
copyrightⓒ Gamza

- Download #1 : InputSystem.zip (1.8 KB), Download : 119

게임은 이벤트처리방식으로 만들지 않는것이 일반적이라서 사용자의 입력을 직접 처리해 주어야 하죠. 특히나 버튼일 경우는 현재 키의 '눌림상태' 만으로 사용자가 한 행동 -아무일도 안하고 있는가,눌렀는가,떼었는가,누른채로 있는가- 을 알아내야 합니다.

이것에 대해서는 여러가지로 설명할수 있지만 제가 생각하기에 가장 이해하기 쉬운것은 진리표에의한 설명인거 같네요.

우선 사용자의 행동을 알아내기위해선 두가지 준비물이 필요합니다.
첫째, 이전 프레임의 키의 '눌림상태'
둘째, 현재 프레임의 키의 '눌림상태'

이것을 간단히 코딩해 본다면 다음처럼 되겠죠...

void main()
{
bool old_key;
bool cur_key;
while(1)
{
cur_key = get_current_keystate();
// A
old_key = cur_key;
}
}

자...이제 'A'라는 부분에 이르면 우리가 원하는 준비물이 각각 old_key와 cur_key에 들어가 있게되겠죠?
자 그럼, 사용자가 무슨짓(?)을 하고있는지 맞춰보도록 합시다.
old_key
cur_key
Event
key state
user action
0
0
None
not pressed
아무일도 안하고 있다
0
1
KeyDown
pressed
키를 눌렀다
1
0
KeyUp
not pressed
키를 떼었다
1
1
None
pressed
누른채로 가만히 있다
이제 상태를 알았으니 'A'라는 부분에 프로그램만 짜넣으면 됩니다.
가령 버튼을 누를때만 총알이 나가야 한다면...
if( !old_key && cur_key ) { 총알발사; }

표를 잘보시면 원리가 눈에 보이죠?
32개의 키의 상태를 하나의 변수에 넣어두면 이벤트 검사할때 좀더 유리하죠.
unsigned long KeyEvent32 = ( oldKeyState32 ^ curKeyState32 );
unsigned long KeyPressEvent32 = KeyEvent32 & curKeyState32;
unsigned long KeyReleaseEvent32 = KeyEvent32 & (~curKeyState32);
이런다음...
if( KeyReleaseEvent32 & 1 ) 첫번째 키가 떨어졌음.
다음처럼 32개의 키들의 이벤트발생 여부를 한방에 알아올수도 있습니다.
if( KeyEvent32 != 0 ) 뭔가 이벤트가 일어났다.
else 아무런 키 이벤트가 없다.

PS, 첨부된 파일은 지금 개발중인 시스템에서 사용될 입력시스템입니다. 참고가 되었으면 하는 마음에..

반응형
반응형

http://www.kocw.net/home/common/contents/document/04_ImageProcessing.pdf

위 주소에 있는 내용임

47 페이지를 보면 된다

x,y 중심으로 컨볼루션 필터배열과 매칭되는 이미지 픽셀을 곱해서 다 더 하면서 모든 이미지 영역을 처리한 것

그래서 가우시안 필터 연산시에 가우스 함수의 분포를 필터에 배치 시켜서 이것을 컨볼루션 하면 스무딩된 이미지가 나온다

반응형
반응형

http://www.gpgstudy.com/gpg3/gpg3s3-5draft.html


이 글은 조만간 출판될 Game Programming Gems 3 한국어판(류광 역, 정보문화사)의 일부입니다. 이 글에 대한 모든 권리는 정보문화사가 가지고 있으며, 사전 허락 없이는 웹 사이트 게시를 비롯한 어떠한 형태의 재배포도 금지되어 있습니다.

이 글은 최종 교정 전의 상태이므로 오타나 오역이 있을 수 있습니다. 또한 웹 페이지의 한계 상, 실제로 종이에 인쇄된 형태와는 다를 수 있습니다. 실제 책에서는 표나 수식이 좀 더 정확한 형태로 표시될 것이며 그림/도표 안의 영문도 적절히 한글화될 것입니다.

Game Programming Gem 3 한국어판에 대한 정보는 GPG 스터디(http://www.gpgstudy.com/)에서 얻을 수 있습니다.

3.5 AI 에이전트, 객체, 퀘스트를 위한 확장성있는 트리거 시스템

Steve Rabin, Nintendo of America, Inc.
steve_at_aiwisdom.com

게임의 1인용 버전에 식상해버린 플레이어는 웹에서 더 많은 레벨들이나 레벨 편집기를 찾게 된다. 그리고 게임이 성공하려면 그런 기대에도 부응할 수 있어야 한다. 확장성 있는 레벨들과 퀘스트들은 잘 설계된 게임에 대한 하나의 품질 보증서로 간주될 수 있으며, 게임의 일반적인 수명을 상당히 늘려줄 수 있다. 게임은 그것이 RTS이든 아니면 RPG나 액션이든, 플레이어가 어떠한 형태로든 레벨들을 커스텀화하거나 새로운 영역들을 추가하는 것이 가능해야 한다.

Baldur’s Gate, StarCraft, Dungeon Siege가 훌륭한 게임으로 평가되는 이유에는 플레이어가 새로운 퀘스트들을 만들거나 심지어는 AI를 수정하고 확장하는 것이 가능하다는 측면도 포함된다. 그러나 플레이어는 프로그래머가 아니므로 플레이어들에게 가상의 프로그래밍 언어를 배우도록 하거나 디버깅을 강제하는 것은 불가능하다. 평범한 플레이어도 스스로 레벨이나 퀘스트를 만들 수 있으려면 무엇보다도 단순함이 보장되어야 하는데, 그러한 단순함은 확장성있는 트리거 시스템을 통해서 제공할 수 있다.

트리거 시스템의 소개

트리거 시스템은 ‘조건을 평가하고 반응을 수행한다’라는 한 가지 목적을 가진 중앙 집중화된 코드이다. 일련의 조건들이 만족되면, 일련의 반응들이 수행된다. 이러한 간단한 시스템은 우아하고, 구현하기 쉬우며, 데이터 주도적[Rabin0]으로 만들기도 쉽다. 트리거 시스템은 다양한 범위의 문제들을 해결할 수 있으며, 특히 디자이너와 플레이어에 의해 수정될 수 있다는 점에서 바람직하다. 트리거 시스템의 매력은 무엇보다도 플레이어가 탐험할 흥미롭고 상호작용적이며 새로운 환경을 쉽게 만들 수 있도록 한다는 데 있다.

일단의 모험가들이 던전을 탐험하는 게임을 생각해보자. 모험대는 지하 묘지를 통과하는 도중 무너져 내린 기둥에 대장을 잃게 된다. 그리고 인상적인 석문에 도달했을 때에는 찬 바람이 불어 와 횃불도 꺼뜨린다. 마지막 횃불에 불을 붙인 후 석문을 보니 “무거운 심장이여 이 문을 통과하리니”라고 새겨진 문구가 눈에 띈다. 잠시 생각해본 후, 체중이 무거운 대원 하나를 심장 모양의 받침대에 올려놓았더니 문이 서서히 열린다.

간단한 조건-반응 패러다임을 이용하는 트리거 시스템으로 이러한 사건들을 지정하는 것이 가능하다. 대원이 특정 기둥 객체에 1미터 이내로 접근하면 바람 소리에 해당하는 음향 효과를 재생하고 그 주변의 횃불들을 꺼뜨린다던가, 플레이어의 파티 대원들 중 하나가 심장 모양의 받침대 위로 올라가면 문을 열고 돌이 갈리는 듯한 음향 효과를 재생하는 등은 모두 조건-반응 패러다임에 속한다.

트리거 시스템을 잘 구현한다면, 디자이너와 플레이어 모두가 독창적인 시나리오 및 퀘스트의 작성을 위해 여러 시간 동안 몰두하도록 만들 수 있는 장치들을 갖출 수 있다. StarCraft 맵 편집기는 참고할 만한 좋은 트리거 시스템의 예이다. 기존의 시스템을 연구하고 새로운 아이디어를 만들어 내는 것은 개발자의 기본적인 자세이다.

객체가 소유하는 트리거 시스템

트리거 시스템을 처음 만들 때에는 하나의 마스터 트리거 시스템([Orkin02]에 나온 것 같은)을 떠올리게 될 것이다. 그러나 좀 더 강력한 구조를 원한다면 어떠한 에이전트나 객체, 퀘스트도 소유할 수 있는 트리거 시스템 클래스를 생각해 볼 수 있다. 모든 객체가 트리거 시스템을 가져야 하는 것은 아니지만, 객체마다 자신의 트리거 시스템 인스턴스를 가질 수 있다면 객체 안에 데이터를 캡슐화하는 데에도 도움이 되며 시스템이 좀 더 유연하고 객체지향적이 될 수 있다. 또한 트리거라는 것 자체가 특정 객체에 대해 작동하게 되므로, 플레이어가 개념을 잡는 데에도 도움이 된다.

누군가가 다가오면 무너져 내리는 기둥을 생각해보자. 그러한 기둥을 레벨 편집기 안에서 정의하고 거기에 무너져 내리는 행동을 강조하는 하나의 트리거 정의를 부여한다. 그리고 디자이너나 플레이어가 게임의 여러 곳에 그러한 기둥들을 배치하면, 신통하게도 의도했던 대로의 모습을 볼 수 있게 된다. 이는 트리거 행동이 객체에 직접 부착되어 있기 때문에 가능한 것이다. 이런 방식에서는 어떠한 에이전트나 객체, 퀘스트도 전적으로 해당 개체만을 위해 존재하는 자신만의 고유한 트리거 시스템을 가질 수 있다.

조건의 정의

게임 안에서 수량화할 수 있는 이벤트나 상태라면 어떠한 것이라도 조건이 될 수 있다. 조건은 실행 파일 안에 고정되지만 인수들이나 레벨 편집기를 통해서 조절할 수 있는 여지도 매우 크다. 다음은 가능한 몇 가지 조건들이다.

  • 플레이어가 지점 (x, y, z)의 반경 R 이내에 존재
  • 플레이어가 위치 (x, y, z)의 사각형 영역 안에 존재
  • 플레이어와 적이 일정 거리 이상 근접함
  • 플레이어의 생명이 X% 이하
  • 플레이어의 인벤토리에 객체 X가 존재
  • 플레이어가 객체 X를 장착
  • 플레이어가 죽음
  • 플레이어가 적 X를 죽임
  • 플레이어가 메시지 X를 받음

부울 논리로 연결된 조건들

조건들을 AND, OR, NOT, XOR 같은 부울 연산자들로 결합할 수 있다면 조건들이 좀 더 유연해질 수 있다. 예를 들어 플레이어가 얼음검, 얼음방패, 얼음갑옷을 장착해야만 어떤 문이 열리게 되어 있다면 조건들은 AND로 결합되어야 한다. 또 어떤 문은 플레이어가 은열쇠나 해골열쇠 중 하나만 가지고 있어도 열릴 수 있다면 조건들이 OR로 결합되어야 한다. 그림 3.5.1과 3.5.2는 그러한 조건들을 트리 구조로 표현한 것이다.

                      AND        참이면 트리거 발동            문 열림

 얼음검 장착    얼음방패 장착    얼음갑옷 장착

그림 3.5.1 세 조건 모두 “참”이면 문이 열린다.

                     OR         참이면 트리거 발동              문 열림

 은열쇠를 가지고 있음    해골열쇠를 가지고 있음

그림 3.5.2 둘 중 하나라도 “참”이면 문이 열린다.

좀 더 복잡한 경우로, 플레이어가 얼음검, 얼음방패, 얼음갑옷을 장착하고 있으며 은열쇠 또는 해골열쇠를 가지고 있어야 문이 열리게 된다면 어떻게 될까? 그림 3.5.3이 그러한 조건들을 표현한 것이다.

이와 같은 그림들을 통한 시각화는 코드를 구조화하는 좋은 방법을 제시한다는 점에서 중요한 의의를 갖는다. 각 요소가 하나의 클래스인 경우 두 종류의 클래스가 필요한데, 하나는 Operator 클래스이고 또 하나는 Condition 클래스이다. Operator 클래스는 임의의 부울 연산자처럼 작동하도록 만들어야 할 것이다. 또한 여러 연산자, 피연산자들이 포함된 표현식을 나타내기 위해서는 다른 Operator 인스턴스들이나 Condition 인스턴스들로의 포인터들의 목록도 담아야 한다. Condition 클래스는 임의의 판정 가능한 조건들을 평가할 수 있어야 하며, 조건의 커스텀화를 위한 인수들을 담아야 한다.

              AND       참이면 트리거 발동               문 열림

 얼음검 장착  얼음방패 장착  얼음갑옷 장착              OR

                                     은열쇠를 가지고 있음   해골열쇠를 가지고 있음

그림 3.5.3 문을 좀 더 복잡한 조건들

반응의 정의

게임 안에서 변경하고자 하는 상태나 행동이라면 어떠한 것도 반응이 될 수 있다. 조건과 마찬가지로, 반응 자체는 실행 파일 안에 고정되지만 인수들 또는 레벨 편집기를 통해서 조절될 수 있다. 다음은 가능한 반응들의 목록이다.

  • 레벨/퀘스트 완수
  • 플레이어에게 X 포인트의 피해 또는 치료
  • 플레이어에게 경험치 X를 부여
  • 문을 연다/닫는다/잠근다/푼다
  • 종류 Y의 생물들을 X개 생성
  • 생물 X를 죽임
  • 음향 재생
  • 목록으로부터 임의의 음향을 재생
  • 플레이어/적 X를 중독
  • 플레이어/적 X를 마비
  • 플레이어/적 X를 보이지 않게 만든다
  • 플레이어/적 X를 무적으로 만든다
  • 트리거를 재설정
  • 플레이어에게 메시지 X를 보낸다

특정 조건들의 집합이 만족되면 그에 해당하는 반응이 수행된다. 이를 단 하나의 반응이 아니라 여러 개의 반응들의 집합으로 확장할 수도 있다. 그렇게 하면 하나의 트리거 발동이 여러 가지 것들에 동시에 영향을 미치게 할 수도 있고, 여러 반응들 중 임의의 것이 수행되도록 할 수도 있다.

트리거의 평가

트리거에 조건들이 정의되었다고 할 때, 다음으로 필요한 것은 한 트리거의 조건들을 평가해서 발동 여부를 판단하는 구조이다. 우선 고려할 것은, 특정 조건이 이벤트 주도적인지(트리거 시스템에 이벤트가 통지되기를 기다리는 것) 아니면 게임 세계를 주기적으로 점검해야 하는지를 결정하는 것이다. 두 방식 모두 지원하는 것이 유연성에 도움이 된다.

이벤트 주도적인 조건의 경우, 이벤트가 트리거 시스템에 통지될 수 있도록 하는 인터페이스가 필요하다. 가장 간단한 방식은 이벤트 메시지를 사용하는 것이다. 이벤트 메시지는 발생한 이벤트의 종류와 이벤트에 관련된 기타 데이터 등으로 구성된다. 이벤트 메시지에 대한 좀 더 자세한 내용은 [Rabin02]를 참고하기 바란다.

주기적 점검 방식의 조건이라면, 트리거 시스템 안에서 각각의 조건들을 점검하는 어떠한 갱신 함수를 호출하는 방식에 대해 생각해볼 수 있다. 그런 함수에서 이벤트 주도적 조건들에 대한 점검은 일어나지 않아야 한다.

이벤트 메시지나 주기적 점검 갱신이 트리거 시스템에 들어왔다면, 그것을 조건들에게 전파해야 한다. 그림 3.5.4는 이벤트 메시지와 주기적 점검 모두를 요구하는 하나의 조건 집합의 예이다. 왼쪽 조건은 충돌 이벤트를 기다리는 반면, 반면 오른쪽 조건은 주기적 점검 갱신이 들어왔을 때 조건을 점검한다.

이벤트 메시지나 주기적 점검 갱신이 트리거 시스템에 들어오면, 그것을 각 트리거의 루트 Operator 인스턴스에 전달한다. Operator는 그것을 자신의 자식들에게 넘겨주며, 자식들은 그에 대한 판단 결과를 “참” 또는 “거짓”으로 돌려준다. 각 자식 역시 그러한 요청을 자신의 자식들에 넘겨준다. 그러한 요청이 실제 조건에 도달하면 참/거짓 판정이 일어나게 되고, 그 결과들이 상위 Operator 인스턴스에 올라가서 부울 연산자에 의한 참/거짓 판정이 일어난다. 그런 식으로 참/거짓이 루트에게까지 올라가면 최종적인 결과가 만들어진다.

 이벤트 메시지 및 
 주기적 점검 갱신

                       AND                참이면 트리거 발동         쥐 10 마리 생성

 플레이어가 10 미터 이내     플레이어의 건강이 50% 이상

그림 3.5.4 하나의 트리거가 이벤트 주도적 조건(아래 왼쪽)과 주기적 점검 기반의 조건(아래 오른쪽)을 모두 가진 예

Operator 클래스는 자식들을 처리할 때 늦은 평가(lazy evaluation)를 사용해야 한다는 점을 명심하기 바란다. 어떠한 조건이 해당 연산자에 대해 만족되지 않은 경우, 트리거의 판정은 그 시점에서 끝난다. 예를 들어 그림 3.5.4에서 어떠한 이벤트 메시지가 왼쪽 조건에 전달되고 그것이 “거짓”으로 판정되었다면 오른쪽 조건으로는 그 이벤트 메시지가 전달되지 않도록 하는 것이다. 이렇게 하면 처리 시간을 줄이는 데 도움이 된다.

또 다른 중요한 점 하나는, 이벤트 주도적 조건의 경우 조건이 명시적으로 재설정되기 전까지는 이벤트들을 기억하고 있어야 한다는 점이다. 그림 3.5.4의 예에서는 플레이어가 10미터 이내로 접근하면 충돌 이벤트가 트리거에 전달되어야 한다. 왼쪽 조건은 그 이벤트를 기억해 두고(플레이어가 10 미터 바깥으로 나가기 전까지는), 이후의 이벤트 메시지나 주기적 점검 갱신에 대해 항상 “참”을 돌려줘야 한다.

어떠한 시점에서, 특정 트리거의 조건들이 모두 참을 돌려준다면 트리거가 발동된다. 트리거가 발동되면 발동되었다는 사실을 기억해서 연달아 다시 발동되는 일이 없도록 해야 한다.

단일 발동과 재설정 시간

모든 트리거들에는 추가적으로 두 개의 속성들이 더 필요하며, 이 속성들은 디자이너가 정의하게 된다.

 bool SingleShot;  // 트리거가 한 번만 발동되어야 하는지의 여부
 float ReloadTime; // 트리거가 여러 번 발동되어야 하는 경우,
                   // 재설정되기까지의 시간

이 두 속성들은 트리거가 한 번 이상 발동될 수 있도록 한다. SingleShot 속성은 트리거가 한 번만 발동되어야 하는지 아니면 여러 번 발동될 수 있는지의 여부를 가리킨다. SingleShot이 “거짓”일 때, ReloadTime은 트리거가 한 번 발동된 후 다시 발동될 수 있을 때까지, 즉 조건들을 초기화하고 다시 이벤트를 받게 될 때까지 걸리는 시간을 결정한다.

트리거를 플래그 및 카운터와 결합

트리거들이 함께 결합될 수 있으려면, 시스템 안의 모든 트리거들이 접근할 수 있으며 즉시 설정할 수 있는 상태들이 존재해야 한다. 따라서 트리거 시스템은 발동된 트리거들의 상태를 추적할 수 있는 일련의 플래그들과 카운터들을 갖출 필요가 있다. 이를 최대한 일반화하기 위해, 각각의 트리거가 문자열 이름으로 접근할 수 있는 임의의 플래그들을 만들 수 있도록 하자. 트리거 시스템은 플래그 이름이 참조될 때 플래그를 생성하거나, 생성되어 있다면 아니면 플래그를 설정한다. 생성된 플래그는 시스템이 종료될 때까지 유지된다.

다음과 같은 새로운 조건들을 생각해보자.

  • flag_name이 참(또는 거짓)인지?
  • flag_name이 짝수(또는 홀수)인지?
  • flag_name1 AND flag_name2가 참인지?
  • flag_name1 AND NOT flag_name2가 참인지?
  • flag_name1 OR flag_name2가 참인지?
  • flag_name1 XOR flag_name2가 참인지?
  • flag_name1 AND flag_name2 AND flag_name3이 참인지?
  • flag_name의 값이 X와 같은지?
  • flag_name의 값이 X보다 큰지?
  • flag_name의 값이 X보다 작은지?

이런 조건들이 추가된다면, 다음과 같은 새로운 반응들이 필요할 것이다.

  • flag_name의 값을 증가.
  • flag_name의 값을 감소.
  • flag_name의 값을 X로 설정.
  • flag_name2의 값을 flag_name1의 값으로 설정.
  • flag_name의 부울 값을 전환.
  • flag_name의 부울 값을 TRUE로 설정.
  • flag_name의 부울 값을 FALSE로 설정.

이러한 플래그들과 카운터들이 있으면 트리거 시스템은 특정 이벤트에 표시를 하거나 발동 횟수를 셀 수 있으며, 이를 통해서 예를 들면 플레이어가 특정 지역에 들린 횟수 등을 알 수 있다. 또한 이벤트들이 특정한 순서로 발생했을 때에만(예를 들면 세 개의 타일들을 특정한 순서로 밟았을 때 등) 트리거가 발동되도록 할 수도 있다. 이러한 플래그들과 카운터들은 상태 정보를 담으므로, 좀 더 많은 종류의 트리거들이 가능해진다.

그림 3.5.5는 플레이어가 어떤 문을 열지 못해서 특정 지역을 반복적으로 찾아오는 경우에 문 열기에 대한 단서를 제공하는 예이다. 세 개의 개별적인 트리거들이 “Visited”라는 이름의 카운터를 통해서 서로 협동적으로 작동한다는 점을 주목하기 바란다.

             AND           "Visited"를 증가             AND           "Visited"를 증가
                                                                                      
플레이어가        "Visited"가               플레이어가        "Visited"가              
지역 A 안에 있음  짝수                      지역 C 안에 있음  홀수                     

                             AND      문 D에 대한 
                                      단서를 떨어뜨림

                    "Visited"가   문 D가 
                    8과 같음      잠겼음

_그림 3.5.5 세 개의 트리거들이 “Visited"라는 이름의 카운터를 통해서 함께 작동하는 예. 플레이어가 문 D를 열지 못한 채로 영역 A와 C를 번갈아 8번 방문하면 단서가 제시된다.

플래그와 카운터가 추가되면, 트리거 시스템의 형태는 흑판 아키텍쳐[Isla02]와 매우 비슷해진다. 플래그들과 카운터들은 흑판이 되고 트리거들은 흑판의 내용을 조작하는 지식 원천(knowledge source)의 역할을 하게 되는 것이다. 그러나 트리거들은 대부분 흑판의 외부로부터 비롯된 데이터에 대해 작용하므로, 엄밀히 말해서 트리거 시스템이 곧 흑판 아키텍쳐인 것은 아니다.

트리거 시스템 대 스크립팅 언어

트리거 시스템의 기능성이 스크립팅 언어의 기능성과 비슷하다는 점을 눈치 챈 독자도 있을 것이다. 특히 상태 정보가 추가되면 더욱 비슷해진다. 양 쪽의 기능성이 겹치는 부분도 존재하나, 완전한 기능을 갖춘 스크립팅 언어에 비해 트리거 시스템은 다음과 같은 장점들을 가진다.

  • 트리거 시스템은 전적으로 GUI만으로도 설정, 조작이 가능하다. 이러한 종류의 단순화되고 안정적인 저작 환경의 제공에 중점을 둔 스크립팅 언어는 드물다. 트리거 시스템의 경우 트리거들이 조건과 반응에 따라 조직화되므로, 문법 오류 같은 문제를 걱정할 일이 없다.
  • 트리거 시스템은 사용자에게 좀 더 쉽게 다가갈 수 있다. 개념이 이해하기 쉬우므로 실제로 사용해 보고자 시도하는 사용자들의 수도 더 많을 수 있다.
  • 트리거 시스템은 안전하다. 트리거 시스템에서는 제한 조건들이 명시적으로 정해져 있으며 사용자는 오직 적은 수의, 집중된, 잘 테스트된 일련의 행동들만 수행할 수 있다. 따라서 사용자의 실수로 게임이 다운되는 경우는 별로 일어나지 않는다.
  • 트리거 시스템은 구현과 수정이 쉽다. 완전한 스크립팅 언어를 구현하는 데 소요되는 시간의 일부만으로도 트리거 시스템을 구현할 수 있다. 스크립팅의 경우 수개월에서 수년까지도 걸릴 수 있지만 트리거 시스템은 수 주면 충분하다[Tozour02], [Brockington02].
  • 트리거 시스템은 문서화가 쉽다. 트리거 시스템의 경우 문서 및 예제 작성이 상대적으로 간단하다. 문서나 예제는 사용자가 저작 시스템에 익숙해지기 위한 필수적인 요소이다.

한계

이 글에서 살펴 본 시스템의 주된 한계는 규모가변성(scalability)이 좋지 않다는 점이다. 그러나 이러한 문제는 무관한 트리거들을 걸러내는 추가적인 코드를 통해서 해결할 수 있다. 근접성을 통한 제외는 이미 그 효과가 상당함이 입증되었다.

또 다른 한계는 조건들과 반응들을 정의하기 위한 어휘가 실행파일 안에 고정되어 있다는 점이다. 사용자의 설정을 코드와 연결하는 부분은 프로그래머의 손을 거쳐야 한다. 이는 게임을 임의의 또는 불순한 의도의 조작으로부터 보호할 수 있다는 장점이 되기도 하다.

결론

부족한 개발 일정에 시달리는 게임 개발자들에게 있어 확장성 있는 트리거 시스템은 어찌 보면 사치스럽게 느껴질 수도 있을 것이다. 그러나 투자한 만큼의 가치와 깊이를 게임에 부여할 수 있다는 점은 잊지 말아야 할 것이다. 확장성있는 트리거 시스템은 또한 프로그래밍 능력을 갖추지 못한 레벨 디자이너도 로직을 구축할 수 있게 하는 효과적인 수단이다. 언뜻 보면 트리거 시스템이 너무 간단한 해결책으로 느껴질 수도 있겠지만, 목표는 디자이너와 플레이어에게 더 많은 능력을 부여하는 데 있음을 잊어서는 안 된다. 내용과 레벨 관련 로직을 정의하는 방식이 쉬우면 쉬울수록 게임은 더욱 확장되고 플레이어는 더 많은 재미를 얻을 수 있다.

참고자료

  • [Brockington02] Brockington, Mark, and Mark Darrah, “How Not To Implement a Basic Scripting Language,” AI Game Programming Wisdom, Charles River Media, Inc., 2002. 번역서는 “기본 스크립트 언어를 어떻게 구현하면 안 되는가”, AI Game Programming Wisdom, 정보문화사, 2003.
  • [Isla02] Isla, Damian, and Bruce Blumberg, “Blackboard Architectures,” AI Game Programming Wisdom, Charles River Media, Inc., 2002. 번역서는 “칠판 아키텍처”, AI Game Programming Wisdom, 정보문화사, 2003.
  • [Orkin02] Orkin, Jeff, “A General-Purpose Trigger System,” AI Game Programming Wisdom, Charles River Media, Inc., 2002. 번역서는 “다용도 트리거 시스템”, AI Game Programming Wisdom, 정보문화사, 2003.
  • [Poiker02] Poiker, Falco, “Creating Scripting Languages for Non-Programmers,” AI Game Programming Wisdom, Charles River Media, Inc., 2002. 번역서는 “비 프로그래머를 위한 스크립트 언어 만들기”, AI Game Programming Wisdom, 정보문화사, 2003.
  • [Rabin00] Rabin, Steve, “The Magic of Data-Driven Design,” Game Programming Gems, Charles River Media, Inc., 2000. 번역서는 “데이터 주도적 설계의 마법”, Game Programming Gems, 정보문화사, 2000.
  • [Rabin02] Rabin, Steve, “Enhancing a State Machine Language Through Messaging,” AI Game Programming Wisdom, Charles River Media, Inc., 2002. 번역서는 “메시지를 통한 상태기계 언어의 향상”, AI Game Programming Wisdom, 정보문화사, 2003.
  • [Tozour02] Tozour, Paul, “The Perils of AI Scripting,” AI Game Programming Wisdom, Charles River Media, Inc., 2002. 번역서는 “AI 스크립팅의 위험”, AI Game Programming Wisdom, 정보문화사, 2003. 

반응형

+ Recent posts