시리즈:수학인듯 과학아닌 공학같은 컴퓨터과학/알고리즘 기초: 두 판 사이의 차이

309번째 줄: 309번째 줄:


== 탐욕법 ==
== 탐욕법 ==
그리디 알고리즘 또는 욕심쟁이 알고리즘 등으로도 불리운다. 현재 봤을 때 가장 최적해같아 보이는 것을 선택하는 알고리즘이다. 근사해를 구할 때 쓰이면 가장 좋으나, 최적해를 구할 때에는 역추적을 해야만 한다. 가장 사람을 잘 흉내내는 알고리즘이다.{{--|이런 욕심쟁이들.}}


== 조합 탐색 ==
== 조합 탐색 ==

2015년 5월 22일 (금) 21:29 판

문서의 내용이 너무 쉬워서 오늘부터 프로그래밍 할 수 있을 것 같습니다.

이 문서에는 독자적으로 연구한 내용이 들어갑니다. 다른 사람의 의견을 존중하면서 무례하지 않도록 작성해 주시고, 의견 충돌 시 토론 문서에서 토론해 주세요.

틀:토막글

들어가기 전

빨간 컵에 칠성사이다가 담겨져 있고 파란 컵에 스프라이트가 담겨져있다. 여기서 파란 컵에 칠성사이다를 넣고 빨간 컵에 스프라이트를 넣을려면 어떻게 해야 될까? 해답은 간단하다. 다른 컵(여기선 노란 컵이라 하겠다.)에 빨간 컵의 칠성사이다를 노란 컵에 붓고 빨간 컵이 비게 되면 파란 컵의 스프라이트를 빨간 컵에 붓고 파란 컵이 비게 되면 노란 컵에 담았던 칠성사이다를 파란 컵에 부으면 된다. 갑자기 노란 컵이 튀어나와서 쌩뚱맞다고 생각하는 사람이 있을 수 있겠지만 다른 컵을 쓰면 안된다고는 안했다. 알고리즘은 문제를 해결하기 위한 것이지 수수께끼넌센스 문제가 아니다.

이 이야기가 왜 나왔냐면 프로그래밍을 할 때 아래 소스 처럼 2개의 변수 값을 서로 바꿀 경우 변수 2개 가지고 끙끙 거리지 말고 간단하게 변수 하나 더 사용해서 쉽게 해결하면 되기 때문이다. 이제 제 3의 변수를 쓰지 않고 변수 2개의 값을 맞바꾸는 알고리즘을 생각해보자.

using System;

namespace 사이다컵바꾸기
{
    class Program
    {
        static void Main(string[] args)
        {
            // 빨간 컵과 파란 컵에 칠성사이다와 스프라이트가 담겨져 있다.
            string 빨간컵 = "칠성사이다";
            string 파란컵 = "스프라이트";

            Console.WriteLine("=====바꾸기 전=====");
            Console.WriteLine("빨간컵 : {0}", 빨간컵);
            Console.WriteLine("파란컵 : {0}", 파란컵);

            // 새롭게 준비한 노란 컵
            string 노란컵;

            // 스프라이트를 빨간 컵에 붓기 위해 칠성사이다를 노란컵에 붓는다.
            // 그 다음 파란 컵의 스프라이트를 빨간 컵에 붓는다.
            // 그 다음 노란 컵에 임시로 담아둔 칠성 사이다를 파란 컵에 붓는다.
            노란컵 = 빨간컵;
            빨간컵 = 파란컵;
            파란컵 = 노란컵;

            Console.WriteLine("=====바꾼 후=====");
            Console.WriteLine("빨간컵 : {0}", 빨간컵);
            Console.WriteLine("파란컵 : {0}", 파란컵);
        }
    }
}


여담으로, 위 취소선의 답은 의외로 간단하다. 정수가 저장되어 있는 a와 b가 있을 때, a와 b를 더한 다음 a에 저장한다. 그 다음 a에서 b를 뺀 값을 b에 저장한다. 그 다음 a에서 b를 뺀 값을 a에 저장하면 된다. 숫자가 아니고 객체면? 포인터 주소값을 더하고 빼면... ... 사실 덧셈보다 더 빠른 방법은 XOR 회로를 사용하는법이 있다. 값이 같지 않으면 a=a^b b=a^b a=a^b를 넣어두면 정수가 아니더라도, 산술적 연산이 아니라 비트 배열 자체가 바뀌기 때문에 값이 교환된다.하지만 병렬 프로그래밍에서는 무리DA☆ZE★

알고리즘의 시간 복잡도

어떤 알고리즘이 실행될 때에 입력과 알고리즘의 실행 시간의 함수 관계를 의미한다. 입력에 따라 알고리즘의 속도가 차이가 난다. 입력되는 숫자의 정렬 작업을 하는 알고리즘 a와 b가 있다고 할 때, 숫자의 개수 등에 따라서 a가 b보다 더 빠르게 정렬 작업을 끝마칠 수도 있고 b가 a보다 더 빠르게 정렬 작업을 끝마칠 수도 있다는 의미이다.

이러한 속도는 알고리즘의 계산 횟수에 따라 달라지는데, 이를 정확히 측정하는 것은 매우 어렵다. 그러므로 보통 이를 나타내는데는 Big O 표기법으로 대략의 시간을 표시한다. 입력된 자료의 수가 [math]\displaystyle{ n }[/math]일 때 계산 횟수가 [math]\displaystyle{ 3n+4 }[/math]이면 [math]\displaystyle{ O(n) }[/math], [math]\displaystyle{ 2n^3+n^2 }[/math][math]\displaystyle{ O(n^3) }[/math]등으로 나타내는 방식.

이 시간 복잡도가 [math]\displaystyle{ O(2^n) }[/math]이런 모습이나, [math]\displaystyle{ O(n!) }[/math]이런 모습으로 나오면 풀이시간에 답이 없어진다.

예를 들어 소수와 소수의 곱을 소인수 분해하는 것이다. 77같이 작은 수는 7 * 11로 쉽게 할 수 있지만, 4,951,760,154,835,678,088,235,319,297를 소인수 분해하라고 하면 일단 좌절한다. 이럴 때는 마음을 안정시키고 소수를 세는거다 여담으로 답은 2,305,843,009,213,693,951 * 2,147,483,647이다. 하지만 반대로 답이 2,305,843,009,213,693,951 * 2,147,483,647 확인하는 것은 쉽다. 이에 관한 것은 P-NP 문제 참고.

알고리즘의 정당성 증명

알고리즘을 설계하는 일반적인 방법

아래 나오는 모든 코드는 C언어를 기본으로[1][2], 다른 언어들은 알아서 추가해주길 바란다.

플로우 차트

기호

자료구조

선형 자료구조

선형 랜덤 접근 가능

배열

가장 쉬운 자료구조이자 가장 언어에 따라 달라지는 자료구조. c언어에서는 대괄호를 사용한다.

int array1[]; //int *array1과 비슷함.
int array2[10]; //크기를 가지는 배열
int array3[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //초기화한 배열
해시

해시는 해시 함수로 구현한다. 해시 함수는 마치 배열처럼, 어떤 자료를 찾을 때 O(1)의 시간만을 소요한다. 해시함수에 키 값을 넣어 주소값을 얻고, 그 주소에 위치한 버킷에 저장된 데이터를 가져오는 방식을 사용한다. 하지만, 언제나 O(1)이 보장되는 것은 아니다. 자세한 것은 해시 테이블 참고.

선형 랜덤 접근 불가능

스택

후입선출(後入先出) 자료구조 스택이다. push 함수와 pop 함수를 갖는 게 정석이며, 추가로 옵션함수(empty, full 등)을 가질 수 있다. 아래는 배열로 구현한 스택이다.[3]

//헤더 부분
void push(int); //push 함수 정의
int pop();

//소스 부분
int array[500]; //스택의 최대 크기는 500
int top = -1; //스택의 현재 크기는 0;

void push(int n)
{
    array[++top] = n;
}
int pop()
{
    return array[top--];
}

사용법은 스택에 넣고 싶은 것을 push로 넣고, pop로 빼면 된다.

push(1), push(2), push(3);
printf("%d %d %d", pop(), pop(), pop());

출력 결과는 3 2 1과 같을 것이다.

선입선출(先入先出) 자료구조 큐이다. put 함수와 get 함수를 가진다. 밑의 큐는 연결 리스트로 구현된 큐이다. 배열로 하는 경우에는 보통 순환 큐라고 해서 맨 끝과 앞을 연결(나머지 연산자 % 이용)하는 방식으로 구현한다. 소스는 다음과 같다.

void put(int);
int get();

struct linkedList
{
    int value;
    struct linkedList *next;
};
struct linkedList *front;
struct linkedList *rear;

//포인터가 있어서 이해하기 힘들지만 찬찬히 뜯어보면 그리 어렵지 않은 내용이다.
void put(int n)
{
    if(front == NULL)
    {
        *front = {n, NULL};
        rear = front;
    }
    else
    {
        rear -> *next = {n, NULL};
        rear = rear -> next;
}

int get()
{
    int res = front -> value;
    front = front -> next;
    return res;
}
데크
링크드 리스트

연결 리스트라고도 불린다. 값과 다음 노드를 가지고 있다.[4] 가장 간단하게 구현한 것은 위의 (...). 다시 한 번 뜯어보고 옵션을 넣어본다면 실력 향상에 도움이 될 것이다.

비선형 자료구조

아래 참조

그래프
트리

문자열

분할 정복

동적 계획법

정렬 알고리즘

크기순, 사전순과 같이 정렬하는 알고리즘을 말한다. 효율적인 알고리즘들 중 대부분은 데이터가 사전에 정렬되어 있을 것을 요구하는 것이 많다.

일단 이걸 보고 뭘 배우면 좋을 지 생각해보자.

안정 정렬

비교한 값이 같을 때 서로 바뀌지 않는 정렬법을 안정 정렬이라고 한다.

거품(Bubble) 정렬

세상에서 가장 만만한 정렬(...). 바로 뒤의 원소와 비교해 더 크다면 뒤로 간다. 뒤에서부터 차례차례 정렬돼가는 것을 보면…. 하지만 가능하면 쓰지는 말자. 성능으로 치면 가장 구린 정렬. 캐시 히트 레이트로나 여러가지 면으로 매우 좋지 않지만 워낙 알고리즘 자체가 간단하기 때문에 보통 입문용으로 가르치곤 하지만...

int arr[] = {5, 3, 7, 8, 6, 4, 2, 1, 0, 9};

void sort()
{
    int i, j;
    for(i = 0; i < 10; i++)
        for(j = i; j < 9; j++)
            if(arr[j] > arr[j + 1]) //j번째 원소가 다음 원소보다 크다면 바꾼다.
            {
                arr[j] = arr[j] ^ arr[j + 1];
                arr[j + 1] = arr[j] ^ arr[j + 1];
                arr[j] = arr[j] ^ arr[j + 1];
            }
}

병합(Merge) 정렬

분할정복기법의 대표적 예이다. 소스는 은근 쉽다.

int arr[] = {5, 3, 7, 8, 6, 4, 2, 1, 0, 9};
int tmparr[10];

void sort(int, int);
void merge(int, int);

void sort(int start, int end)
{
    if(end > start)
    {
        sort(start, (start + end) / 2);
        sort((start + end) / 2 + 1, end);
    }
    merge(start, end);
}

void merge(int start, int end) //여기가 진짜다.
{
    int i = start, j = (start + end) / 2 + 1;
    int count = start;
    while(i <= (start + end) / 2 && j <= end)
    {
        if(arr[i] > arr[j])
        {
            tmparr[count++] = arr[j++];
        }
        else
        {
            tmparr[count++] = arr[i++];
        }
    }
    while(i <= (start + end) / 2)
    {
        tmparr[count++] = arr[i++];
    }
    while(j <= end)
    {
        tmparr[count++] = arr[j++];
    }
    for(i = start; i <= end; i++)
    {
        arr[i] = tmparr[i];
    }
}

불안정 정렬

비교한 값이 같을 때 서로 바뀌는 정렬법을 불안정 정렬이라고 한다.

선택(Selection) 정렬

삽입(Insertion) 정렬

퀵(Quick) 정렬

정렬의 왕이다. 평균 [math]\displaystyle{ O(n \log n) }[/math]인 정렬 알고리즘들 중 가장 빠르다. 피봇을 잡고 하는 건 너무 어렵고, 예를 들어 설명하자면 5, 3, 7, 8, 6, 4, 2, 1, 0, 9가 있을 때, 5를 기준으로 일단 정렬한다. 사람이 한다면 아마 3, 4, 2, 1, 0, 5, 7, 8, 6, 9가 될 것이다. 이 상태에서 5의 앞을 다시 정렬한다. 2, 1, 0, 3, 4와 5와 7, 8, 6, 9가 남았다. 이 때, 4는 3 뒤에 있는 오직 한 개의 수이므로 정렬이 되었다고 본다. 이런 식으로 하다 보면 0, 1, 2, 3, 4, 5, 6, 7, 8, 9의 올바른 순열이 나올 것이다. 소스는 다음과 같다.[5]

int arr[] = {5, 3, 7, 8, 6, 4, 2, 1, 0, 9};

void sort(int, int);

void sort(int start, int end)
{
    int i = start + 1;
    int j = end;

    if(end < start)
        return;
    while(i < j)
    {
        while(arr[i] <= arr[start])
        {
            i++;
        }
        while(arr[j] >= arr[start])
        {
            j--;
        }
        if(i < j)
        {
            arr[i] = arr[i] ^ arr[j];
            arr[j] = arr[i] ^ arr[j];
            arr[i] = arr[i] ^ arr[j];
        }
    }
    if(arr[start] > arr[j])
    {
        arr[start] = arr[start] ^ arr[j];
        arr[j] = arr[start] ^ arr[j];
        arr[start] = arr[start] ^ arr[j];
    }

    sort(start, j - 1);
    sort(j + 1, end);
}

힙(Heap) 정렬

그 밖의 정렬 알고리즘

버킷(Bukket) 정렬

자릿수(Radix) 정렬

탐욕법

그리디 알고리즘 또는 욕심쟁이 알고리즘 등으로도 불리운다. 현재 봤을 때 가장 최적해같아 보이는 것을 선택하는 알고리즘이다. 근사해를 구할 때 쓰이면 가장 좋으나, 최적해를 구할 때에는 역추적을 해야만 한다. 가장 사람을 잘 흉내내는 알고리즘이다.이런 욕심쟁이들.

조합 탐색

수치해석

정수론

계산 기하

트리

구현과 순회

이진 검색 트리

이진 검색 트리는 고급 자료구조를 위해서 다양하게 활용되기도 한다. 그에 관해서는 다음 문서를 참고.

힙은 트리의 일종으로 부모 노드 값이 항상 자식 노드 값보다 큰 트리를 말한다. 힙소트 할때 쓰는 트리이다. 보통 이진트리로 구현한다. 힙에 새 원소를 넣고 빼는 데에는 [math]\displaystyle{ O(n \log n) }[/math]의 시간 복잡도를 가진다. 뭔가 힙보다 힙정렬이 더 앞에 있는 것 같은 건 눈의 착각이다.

C로 힙을 구현하는 방법은 크게 두 가지, 배열과 포인터가 있다. 포인터로 하는 방법은 직관적이지만 많은 자원(메모리 또는 시간)이 소모된다. 하지만 배열은 힙의 각 노드를 배열의 어떤 인덱스에 대응시킬지 고민해야 하지만 훨씬 자원을 효율적으로 사용할 수 있다.

구간 트리

트라이

그래프

수학, 과학 시간에 많이 보던 그 그래프와는 사뭇 다르다. 알고리즘을 배우다 보면 어쩌면 가장 많이 나오는 부분일 지도 모른다. 주로 노드와 엣지로 표현된다. 왠지 노드는 동그라미이고 엣지는 선이다. '그래프 구조'로 검색하면 그런 그림을 볼 수 있다. 간단한 구조체를 만들어 보자.

struct Edge //노드보다는 엣지로 나타내는 편이 대부분의 경우에 더 좋다.
{
    int snode; //시작 노드
    int enode; //종료 노드
    //int cost; //가중치를 가지는 엣지에 사용된다.
};

struct Edge가 하나 이상 있는 것이 바로 그래프이다.

순회 알고리즘

깊이 우선 탐색

Depth First Search, DFS

너비 우선 탐색

Breadth First Search, BFS

최단 경로

플로이드-워셜(Floyd-Warshall) 알고리즘

데이크스트라(Dijkstra) 알고리즘

이거 왜 그리디에 없고 여기 있냐 최단거리를 찾는 알고리즘이다. 움짤이 여럿 돌아다니니 한 번 보자.

Shortest path Dijkstra vs BellmanFord

c부터 조사하지 않는 게 수상하긴 하지만 윗 것은 데이크스트라, 아랫 것은 벨먼 포드 알고리즘이다. 이것만 보고도 이해할 수 있다면 참 좋겠다.

벨먼-포드(Bellman-Ford) 알고리즘

위상 정렬

최소 비용 스패닝 트리

크러스컬(Kruskal) 알고리즘

프림(Prim) 알고리즘

네트워크 플로우

문자열 알고리즘

문자열 검색

KMP 알고리즘
보이어-무어 알고리즘
최종병기 접미사 트리

추가바람

각주

  1. 일단 C언어로는 작성을 해 두자.
  2. C언어의 문법(특히 포인터)은 미리 공부를 해 두자.
  3. C++ STL의 그것보다 빠를 것이다. 다만 확장이 어려울 뿐.
  4. 옵션으로는 이전 노드, 끝 노드의 다음이 시작 노드인 등일 수 있다.
  5. 물론 최적화는 되어 있지 않다.

틀:쉽게 배우는 프로그래밍 입문