재귀호출과 피보나치 수열 탐구

재귀호출은 함수가 그 내부에서 자신을 다시 호출하는 것이다. 이는 언뜻 이상하게 보일 수 있고, 경우에 따라서는 의도치 않은 동작을 하게 할 수 있어서 일반적으로는 지양되는 방법이기는 하나, 대신에 코드가 짧아질 수 있고 실행 로직 자체가 어느 정도 제한된 경우라면 충분히 사용할 수 있다. 특히 하스켈과 같은 함수형 언어에서는 반복문을 돌리는 로직이 없기 때문에 재귀호출을 하는 함수를 자주 사용하게 된다.

피보나치 수열을 생성하는 함수를 생각해보자. 하스켈에서는 다음과 같이 함수가 정의된다.

let fibs x y = x : fibs y (y+x)

이 함수는 재귀적으로 동작한다. 첫 번째 인수를 두 번째 인수와 두 인수의 합을 인자로 넘긴 재귀호출의 결과과 연결하게 된다. 문제는 이 호출이 무한히 반복된다는 점에 있다. 2차적으로 호출되는 fibs 함수는 계속해서 자신을 호출하며, 그 끝은 없다. 보통 하스켈에서는 다음과 같은 식으로 유한한 크기의 결과를 사용한다.

> take 5 (fibs 1 2)
[1, 2, 3, 5, 8]

이러한 무한 루프를 도는 식의 계산은 다른 프로그래밍 언어에서는 연산만 하다가, 스택 오버플로우를 내고 프로그램이 뻗어버리겠지만 하스켈은 “값이 필요해지는 최종 시점”에서만 계산을 수행하게 되므로 정상적으로 동작한다. 심지어 > fibs 1 2라고만 실행해서 그 끝을 주지 않으면 해당 함수는 “다음 차의 항”이 필요할 때 계속해서 연산하며 결과를 출력해 나간다. (Ctrl+C를 눌러서 중지할 수 있다.)

이 코드를 C로 그대로 옮기기는 힘들다. (크기가 무한이 늘어날 수 있는 배열을 만들 수 없기 때문이다. 대신에 세 번째 인자로 배열의 크기를 받는다면 다음과 같이 작성할 수는 있다.

unsigned int * fibs(unsigned int x , unsigned int y, unsigned int length){
    unsigned int *fib_list = malloc( length * sizeof(unsigned int));
    unsigned int i=0, a = x, b = y, c;
    while(i< length) {
        fib_list[i] = a;
        c = b;
        b = a + b;
        a = c;
        i++;
    }
    return fib_list;
}

사실 위의 구현은 매우 좋지 않은데, 함수 내부에서 malloc을 써서 새로운 메모리를 할당하고 있기 때문이다. 보통 이 경우에는 아래와 같이 함수 외부에서 미리 할당하고 그 포인터를 받아 사용하도록 디자인하는 것이 좋다.

typedef unsigned int * fib_t;
void fibs(fib t, unsigned int x, unsigned int y, unsigned int length){
    unsigned int i=0, r;
    while(i < length) {
        l[i] = x;
        r = x;
        x = y;
        y = r+y;
        i++;
    }
}

파이썬의 경우, 함수형 언어의 패러다임에서 들여온 제너레이터라는 기능이 있어서 피보나치 수열은 비교적 쉽게 구현이 가능하다. (게다가 무한루프로 보이지만 안전하게 동작한다.)

def fibs(x, y):
    while True:
        yield x;
        x, y = y, x+y
g = fibs(1, 2)
for i in range(100):
    print next(g)

둘다 재귀호출을 사용하지 않고 있다. 만약 재귀호출을 사용해야 한다면 어떤 식으로 코드를 구성할까? C의 경우 계속해서 새로운 리스트를 만들어서 할당해야 하는 괴로움이 있다. 이는 연결 리스트 객체를 새롭게 정의하여 구성하는 것이 보다 깔끔한 구성이 될 수 있다. 배열을 리턴하는 함수를 작성하는 경우에는 재귀호출로 얻는 배열의 모든 원소를 뒤로 밀어내어 앞쪽에 원소를 추가하는 작업을 해야 하는데, 이것이 상당히 비싼 작업이 될 수 있다. 어떤 경우는 코드가 너무 길어질 것 같아서 패스… 하려고 했는데 맨 아래에서 다시 추가해보도록 하겠다.

파이썬으로는 재귀적으로 다음과 같이 구현할 수 있겠다.

def fibs(x, y, len):
    if len == 0:
        return []
    else:
        return [x] + fibs(y, x+y, len-1)
if __name__ == '__main__':
    print fibs(1, 2, 100)

C로는 연결 리스트를 활용해서 위 파이썬 코드와 비슷한 일을 하도록 한다. 객체 지향 스타일로 연결리스트를 생성하고 값을 리스트의 맨 앞에 끼워넣는 식의 작업을 한다.

#include <stdio.h>
#include <mem.h>
typedef struct _node {
    unsigned int value;
    struct _node* next;
} *node;
typedef struct _list {
    unsigned int length;
    node head;
} *list;
list newList(){
    list x = malloc(sizeof(list));
    x->head = NULL;
    x->length = 0;
    return x;
}
void dellocList(list l){
    while(l->head != NULL){
        node k = l->head;
        l->head = l->head->next;
        free(k);
        l->length--;
    }
    free(l);
    return;
}
void addValue(list l, unsigned int a) {
    node n = malloc(sizeof(node));
    n->value = a;
    n->next = l->head;
    l->head = n;
    l->length++;
}
void inspectList(list l){
    int i=0;
    node holder = l->head;
    while(i<l->length){
        printf("%d, ", holder->value);
        holder = holder->next;
        i++;
    }
}
list fibs(list l, unsigned int x, unsigned int y, unsigned int len) {
    if(len==0)
        return l;
    else {
        addValue(fibs(l, y, x+y, len-1), x);
        return l;
    }
}
void convertListToArray(list l, unsigned int *k) {
    int i=0;
    node temp = l->head;
    while(temp != NULL) {
        k[i]=temp->value;
        temp = temp->next;
        i++;
    }
}
int main(int argc, const char* argv){
    list l = newList();
    fibs(l, 1, 2, 20);
    inspectList(l);
    dellocList(l);
    return 0;
}

두 종류의 객체를 사용한다. 하나는 node와 다른 하나는 list이다. list는 일종의 연결 리스트이다. 연결 리스트의 구현을 위해서는 각 노드 객체를 구현해야 하는데, 이는 _node라는 구조체의 포인터를 사용했다. node 객체의 명시적인 생성은 하지 않는 것으로 간주하고, list는 newListdeallocList라는 두 개의 함수를 사용해서 각각 생성하고 해제한다. 또한 노드는 리스트의 처음에 값을 추가하는 addValue 함수에서 새롭게 메모리를 할당해서 저장하는데, 연결 리스트의 특성상 모든 노드들의 포인터는 순회시에 할 수 있으므로 리스트를 해제하는 시점에 내부의 모든 노드를 해제한 후 리스트를 해제하도록 했다.

끝으로 리스트를 순회하면서 정수(부호없는)의 배열로 각 노드의 값을 복사해서 연결리스트의 내용을 배열로 옮겨주는 convertListToArray() 함수를 추가했다.

(추가) 배열을 사용한 재귀호출 – C

배열을 사용한 재귀호출은 다음과 같이 구현할 수 있다. C에서 이러는 거 별로 좋아하진 않지만, 어쨌든 파이썬의 리스트로 구현하는 방식과 비슷한 구현이다.

#include <stdio.h>
#include <mem.h>
#include <string.h>
void fibs(unsigned int *l, unsigned int x, unsigned int y, unsigned int len)
{
    if(len <= 1){
        return;
    }
    else {
        fibs(l, y, x+y, len-1);
        memcpy(l+1, l, (len-1)*sizeof(unsigned int));
        l[0] = x;
        return;
    }
}
int main(int argc, const char** argv){
    int len = 40;
    unsigned int *l = malloc(sizeof(unsigned int)*len);
    fibs(l, 1, 1, len);
    int i;
    for(i=0;i<len;i++) {
        printf("%u, ", l[i]);
    }
    free(l);
    return 0;
}

피보나치 수열의 경우에는 좀 특수하게 앞의 두 항의 값만 있으면 그 다음 항을 계산하는 건 간단하니, 다음과 같이 객체(구조체)하나에서 다음 값을 매번 계산하도록 할 수도 있다. 파이썬의 제너레이터와는 다르지만 동작은 비슷하게 하게 된다. 이 방법이 그나마 가장 안전해보이기는 한다. (대신에 변수타입의 값 범위 내에서만 안전하다;;)

#include <stdio.h>
#include <mem.h>
typedef struct _fibgen {
    unsigned int a, b, temp;
} * fibgen;
typedef void* fib_t;
fib_t newFib(void){
    fibgen t = malloc(sizeof(fibgen));
    t->a=1;
    t->b=1;
    t->temp=0;
    return (fib_t)t;
}
void deallocFibgen(fib_t t) {
    free(t);
}
unsigned int next(fib_t t){
    fibgen f = (fibgen)t;
    f->temp = f->a;
    f->a = f->b;
    f->b += f->a;
    return f->temp;
}
int main(int argc, const char** argv){
    fib_t f = newFib();
    int i;
    for(i=0;i<40;i++){
        printf("%u, ", next(f));
    }
    deallocFibgen(f);
    return 0;
}

Read more

워드프레스에서 고스트로 이전

워드프레스에서 고스트로 이전

이 글을 쓰면서도 믿기 힘든 사실인데, 블로그라는 걸 처음 시작한지가 20년이 되었습니다. 이글루스에서 처음 시작했다가, SK컴즈가 인수한다고 발표함과 동시에 워드프레스로 플랫폼을 옮겼죠. 워드프레스오 옮긴 이후에는 호스팅 환경을 이리 저리 옮기긴 했지만 거의 18년 가까이 워드프레스를 사용해온 것 같습니다. 그 동안 워드프레스는 블로깅 툴에서 명실상부한 범용CMS로 발전했습니다. 사실 웬만한 홈페이지들은 이제

By sooop
띄어쓰기에 대한 생각

띄어쓰기에 대한 생각

업무 메일을 쓸 때 가장 많이 쓰는 말 중에 하나가 메일 말미에 ‘업무에 참고 부탁 드립니다.‘인데요, 어느 날부터 아웃룩에서 이 ‘부탁 드립니다’가 틀렸다고 맞춤법 지적을 하기 시작했습니다. 맞는 말은 ‘부탁드립니다’라고 붙여 쓰는 거라고. 사실 아래아한글 시절부터 이전의 MS워드까지, 워드프로세서들의 한국어 맞춤법 검사 실력은 거의 있으나 마나 한

By sooop

구글 포토에서 아이클라우드로 탈출한 후기

한 때 구글 포토가 백업 용량을 무제한으로 제공해 주겠다고해서, 구글 포토를 사용해서 사진을 백업해왔습니다. 물론 이 이야기의 결말은 저나 이 글을 읽고 있는 여러분이나 모두 알고 있습니다. 사실 AI에게 학습 시킬 이미지 데이터를 모으기 위한 것일 뿐이라거나 하는 이야기는 그 당시에도 있었습니다만, 에이 그래도 구글인데 용량은 넉넉하게 주겠지…하는 순진한

By sooop

Julia의 함수 사용팁

연산자의 함수적 표기 Julia의 연산자는 기본적으로 함수이며, 함수 호출 표기와 같은 방식으로 호출하는 것이 가능합니다. 또한 그 자체로 함수이기 때문에 filter(), map() 과 같이 함수를 인자로 받는 함수에도 연산자를 그대로 적용하는 것이 가능합니다. 특히 + 연산자는 sum() 함수와 같이 여러 인자를 받아 인자들의 합을 구할 수 있습니다. 2 + 3 # = 5 +(2,

By sooop