TigerCow.Door


이전글

#1_블록체인 무엇인가::소프트웨어 시스템 아키텍처

#2_블록체인 무엇인가::분산 P2P 시스템

#3_블록체인 무엇인가::블록체인의 과제

#4_블록체인 무엇인가::소유권의 본질



안녕하세요. 문범우입니다.

이번 포스팅에서는 분산 P2P 시스템의 무결성 침해와 관련된 가장 중요한 사례 중 하나인 이중사용(Double Spending)문제에 대해서 알아보겠습니다.


1. 이중사용(Double Spending)


이중사용은 쉽게 말해서 동일한 재화, 물건에 대해 두번 사용한다는 것 입니다.

보다 쉽게 이해하기 위해 부동산을 판매하는 상황으로 가정해보겠습니다.


A가 B에게 집을 팔았습니다.

이로 인해서 집에 대한 소유권은 A에서 B로 이전 되었음이 P2P 시스템의 어느 원장에 기록되었습니다. 그럼 우리가 이전에 학습한 바와 같이 원장을 관리하는 피어가 즉시 다른 피어에게 이러한 사실을 알립니다. 그리고 그 피어는 또 다른 피어에게 사실을 알립니다.

하지만, 이때 A가 아직 사실을 받지 못한 원장에 접근해서 집에 대한 소유권 이전을 A에서 C로 이전되었음이라고 기록했습니다. 마침 해당 원장을 관리하는 피어는 다른 사실에 대해 들은바가 없으므로 해당 기록을 받아들이게 됩니다.


즉, A가 B에게 집을 판 사실이 모든 피어에게 전달되기 전, 다른 피어에게 접근해 동일한 집을 판매하는 행위가 이중사용입니다.



2. 이중사용의 3가지 의미


이전에 알아본 블록체인도 다양한 의미를 가지고 있듯이 이중사용도 크게 3가지의 의미를 지니고 있습니다.


1. 디지털 재화를 복사해 발생하는 문제

실제로 컴퓨터의 데이터를 복사하는데는 별다른 제약이 존재하지 않습니다. 바로 이런 특성을 이용해서 동일한 디지털 재화를 반복해서 복사함으로써 지불하는데 사용할 수 있습니다.

이는 실물 화폐를 복사하는 범죄와 동일한 것 입니다.


2. 원장의 분산 P2P 시스템에서 발생하는 이중사용 문제

이는 위에서 알아본 예시에 대한 관점입니다. 모든 피어들이 정보를 전달받기까지 시간이 걸리는 문제로 인해 일부 피어가 서로 상이한 정보를 갖게 되는 것 입니다.

모든 피어가 동시에 최신정보를 얻지 못한다면, 최신 정보를 획득한 누군가에 의해서 정보가 악용될 가능성이 있습니다.


3. 순수 분산 P2P 시스템의 무결성이 침해된 이중사용 문제

이중사용 문제를 좀 더 추상화해서 크게 본다면, 분산 P2P 시스템 내의 데이터의 일관성을 유지하는 문제로 생각해볼 수 있습니다. 즉, 데이터의 일관성은 시스템 무결성의 한 측면이므로 이중 사용 문제는 결국 시스템의 무결성이 침해된 사례로 볼 수 있습니다.



3. 이중사용 문제를 해결하는 방법


1. 디지털 재화를 복사해 발생하는 문제 해결

사실상 디지털 재화나 화폐를 복사해 한 번 이상 사용하는 문제는 소유권의 본질과 관련이 있습니다. 즉, 특정 재화나 화폐의 소유권이 누구에게 있는지에 대한 관리가 필요한 것 입니다. 이렇게 특정 데이터와 그 소유자를 매핑하는 수단이 필요하는데 이러한 것은 올바르게 작동되는 원장으로써 해결될 수 있습니다.


2. 원장의 분산 P2P 시스템에서 발생한 이중사용 해결

사실상 이러한 문제에 대한 해결은 블록체인 구조로 가능합니다.

우리가 지난 포스팅에서 블록체인과 원장의 P2P 시스템 간의 관계를 알아보았던 것과 같이, 현재 다양한 포스팅에서 사용되고 있는 블록체인이란 용어 자체가 원장의 분산 P2P 시스템의 이중사용 문제를 없애주는 해결책인 셈 입니다.


3. 분산 P2P 시스템의 무결성이 침해된 이중사용 해결

무결성의 내용과 의미는 분산 P2P 시스템의 용도가 무엇인지에 따라서 결정됩니다. 즉, 간단한 파일 공유 시스템의 경우에는 디지털 화폐의 소유권을 비교하는 것과는 다른 측면의 무결성이 고려될 수 있습니다.

결국 분산 P2P 시스템의 응용분야에 따라 블록체인이 아닌 다른 기술이나 데이터 구조 혹은 다른 알고리즘이 무결성을 확보하고 유지하기에 더 적합할 수 있습니다.



이렇게 이중 사용 문제에 대해서 알아보았습니다.

다음 포스팅에서는 블록체인이 어떻게 무결성을 확보하고 유지하는지에 대해서 알아보도록 하겠습니다.




블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc


안녕하세요. 문범우입니다.

오랜만에 파이썬 관련 내용을 포스팅하게 되었습니다.

최근 자바 언어에 대해 다시 공부하면서 멀티 쓰레딩 개념을 학습중인데, 파이썬에서 해당 내용을 다뤄보지 않은 것 같아 간략하게나마 공부하고 이를 정리해보았습니다.


즉, 이번 글에서는 파이썬에서의 멀티 프로세싱, 멀티 쓰레딩에 대해서 알아보도록 하겠습니다.



글에 앞서서, 멀티 프로세싱, 멀티 쓰레딩 등, 동시성 프로그래밍에 대한 개념적인 내용은 아래 글을 참고하시면 되겠습니다.

https://doorbw.tistory.com/26


먼저 전체적인 코드입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from functools import partial 
from threading import Thread
import multiprocessing
import time
 
def singleCount(cnt,name):
    for i in range(1,10000001):
        cnt += 1
        if(i%2500000 == 0):
            print(name,":",i)
 
lists = ['1','2','3','4']
# single process start
cnt = 0
print(" # # SINGLE PROCESSING # # ")
start_time = time.time()
for each in lists:
    singleCount(cnt,each)
print("SINGLE PROCESSING TIME : %s\n" %(time.time()-start_time))
 
# multi process start
cnt = 0
print(" # # MULTI PROCESSING # # ")
start_time = time.time()
pool = multiprocessing.Pool(processes=4)
func = partial(singleCount, cnt)
pool.map(func, lists)
pool.close()
pool.join()
print("MULTI PROCESSING TIME : %s\n" %(time.time()-start_time))
 
#multi threading start
cnt = 0
print(" # # MULTI THREADING # # ")
start_time = time.time()
th1 = Thread(target=singleCount, args=(cnt,"1"))
th1.start()
th1.join()
th2 = Thread(target=singleCount, args=(cnt,"2"))
th2.start()
th2.join()
th3 = Thread(target=singleCount, args=(cnt,"3"))
th3.start()
th3.join()
th4 = Thread(target=singleCount, args=(cnt,"4"))
th4.start()
th4.join()
print("MULTI THREADING TIME : %s\n" %(time.time()-start_time))
 
cs


코드에서는 싱글 프로세싱, 멀티 프로세싱, 멀티 쓰레딩 총 3개의 로직이 구현되어 있으며 이에 대한 결과는 아래와 같습니다.


* 싱글 프로세싱


* 멀티 프로세싱


* 멀티 쓰레딩


위의 결과를 보시면 사실상 싱글 프로세싱과 멀티 쓰레딩의 시간차이는 크게 없고, 멀티 프로세싱에서만 시간 효율이 존재함을 알 수 있습니다.


파이썬에서는 GIL(Global Interpreter Lock)이라는 동작때문에 사실상 여러개의 스레드가 동일한 자원에 대해 접근하지 못합니다.

즉, 우리가 기대한 것과 달리 하나의 스레드가 종료함에 따라 다른 스레드가 진행되는 것이죠. 이러한 GIL때문에 오히려 멀티 쓰레딩이 싱글 쓰레딩보다 I/O작업이 많아 짐에 따라 시간소요가 커질 수 있기도 합니다.

이에 대해 보다 자세한 내용은 아래 링크에서 확인하실 수 있습니다.

https://medium.com/@mjhans83/python-gil-f940eac0bef9


그리고, 멀티 프로세싱은 개념적으로 공부한 것과 같이 다수의 프로세스를 띄워 작업을 처리하기 때문에 당연히 싱글 프로세싱보다 처리 시간이 단축되는 것을 볼 수 있습니다.



오랜만에 다시금 동시성 프로그래밍 개념들에 대해 공부하다보니 또 헷갈리는 내용들과 질문들이 생기게 되었습니다..

위의 글에서 설명이 부족한 이유는 아마 아직 해결되지 못한 궁금증과 질문들 때문일겁니다.. 따로 위에는 적어두지 않았지만 추후에 보다 깊이 이해하게 된다면 한번더 제대로 정리하고 싶어지네요 :-(


잘못된 점이나 궁금하신 점들 언제든지 연락주시면 저도 많이 부족하지만 같이 이야기해보면서 답을 찾아나가면 좋을 것 같습니다 :)

블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc

 

안녕하세요. 문범우입니다.

최근 파이썬을 활용할 일이 많이 없었는데, 엊그제 문득 필요한게 생각나서 후다닥 파이썬으로 만들어보았습니다.


뭐라고 이름을 지어야할지 모르겠는데..

많은 분들도 만들어서 사용하시기도 하는 것으로 알고 있습니다.

제가 자주 보는 커뮤니티에서의 특정 게시판 상위 n개에 대해서 크롤링하여 링크를 포함한 헤드라인만 정해진 시각에 제 메일로 보내는 프로그램입니다.


저는 OKKY라는 커뮤니티에서 스터디, 뉴스, 행사에 관련된 3개 게시판에 대해 상위 5개 또는 3개의 글을 정해진 시각(오전, 점심, 오후)에 메일로 보내도록 하였습니다.


메일 내용을 더 꾸밀 수도 있겠지만..

일단은 심플하게 아래와 같이 메일이 전송됩니다 :)




실제로 이렇게 해두고 나니, 정해진 시각에 한번쯤 더 쳐다보게 되고 요새 바빠서 다양한 행사에 대해 관심가지지 못하고 있는데 이를 보완할 수 있을 것 같다는 생각이 듭니다.


관련 코드는 github에 올려두어 아래 링크에서 확인해보실 수 있습니다.


https://github.com/doorBW/event_crawl


코드에서도 확인하실 수 있으며, 사용된 내용들은 크게


1. requests와 bs4를 이용한 웹 크롤링

2. SMTP 서버를 통한 메일 보내기

3. crontab 활용하여 일정시간에 쉘스크립트 실행시키기


입니다. 각각에 대해서는 구글에 검색해보시면 다양한 자료를 찾아보실 수 있으니 추가적인 설명은 접어두도록 하겠습니다.


코드가 깔끔할지는 모르겠으나, 필요하신분들 참고하셔도 될 것 같습니다.

추가적으로 궁금한 점등의 문의사항은 언제든 댓글이나 카톡, 이메일로 연락주세요 :)

블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc

안녕하세요. 문범우입니다.

지난 포스팅에서 시간복잡도, 공간복잡도 등에 대해서 알아보며 Big-O 표기법에 대해서 살펴보았습니다.

이번에는 실제로 특정 코드나 알고리즘을 대상으로 그 시간복잡도를 분석해보는 실습을 진행해보도록 하겠습니다.


아래에서 다루게 될 예제들은 ''코딩인터뷰 완전 분석"(게일라크만맥도웰 지음, 이창현 옮김)_인사이트출판 서적에서 일부 참고 및 발췌하였습니다.



 > N에 대한 정확한 사용


우리는 이전 포스팅에서도 그러했듯이 Big-O 표기법으로 나타낼때에 흔히 O(N), O(log N) 과 같이 나타냅니다. 그런데 이때 N에 대해 정확하게 이해하지 못하였다면 추후 잘못된 분석을 할 수 있습니다.

아래와 같은 상황을 생각해보겠습니다.


여러개의 문자열로 구성된 배열이 있습니다. 

이때 각각의 문자열에 대해 먼저 정렬을 하고 이후 전체 문자열을 사전순으로 다시 정렬하는 알고리즘이 있습니다.

이러한 알고리즘의 수행시간은 어떻게 될까요?


간단히 생각해보면, 각 문자열을 정렬하는데 O(N*logN)의 시간이 소요되므로 모든 문자열에 대해서는 O(N*N*logN)이 소요될 것입니다. 그리고 전체 문자열들에 대해 사전순으로 정렬해야 하므로 O(N*logN) 이 추가됩니다.

즉, 해당 알고리즘의 수행시간은 O(N*N*logN + N*logN) = O(N*N*logN) 이라고 생각할 수 있습니다.


하지만 이는 완벽하게 틀린 분석입니다.

어디서 부터 잘못됐는지 아시겠나요?

바로 N에 대한 정확하지 않은 사용입니다.

위의 분석에서는 문자열의 길이와 문자열의 개수 모두 N으로 중복해서 사용하였습니다. 이는 아예 다른 시간복잡도를 가져오게 되는 큰 실수입니다.


제대로된 분석을 위해서는 다음과 같이 변수를 정의하고 진행합니다.


- 가장 길이가 긴 문자열의 길이를 s라고 하자.

- 배열의 길이를 a라고 하자.


이렇게 된다면, 각 문자열을 정렬하는데 O(s * log s) 가 소요되며 모든 문자열에 대해서 생각해 보면, O(a * s * log s) 가 소요됩니다.

그리고 문자열 비교시, 두개를 서로 비교할 때 O(s) 만큼 시간이 소요되고 이것을 총 O(a * log a) 번해야 하므로 O(a * s * log a) 만큼의 시간이 소요 됩니다.

따라서 전체 시간 복잡도는, O(a * s * (log a + log s)) 입니다.




> 재귀 호출 패턴 분석


아래는 균형 이진 탐색 트리에서 모든 노드의 값을 더하는 간단한 코드입니다. 수행시간을 한번 분석해보죠.


1
2
3
4
5
6
int sum(Node node){
    if(node == null){
        return 0;
    }
    return sum(node.left) + node.value + sum(node.right);
}
cs



1. 재귀호출 패턴분석


위의 코드가 단지 이진 트리라는 이유로, 또는 재귀호출이라는 이유로 시간복잡도에 로그가 들어간다고 생각하면 안됩니다. 

실제로 재귀 함수에 분기가 여러개 존재한다면 수행 시간은 일반적으로 O(분기^깊이)가 됩니다. 즉, 두 개의 분기만 존재하는 재귀함수라면 일반적으로 O(2^깊이)의 시간복잡도를 가지게 됩니다.


그렇다면 깊이는 어떻게 될까요? 위의 코드에서 나온 트리는 균형 이진 탐색 트리입니다. 즉, 총 N개의 노드가 있다면 깊이는 대략적으로 log N 의 값을 가지게 됩니다.

그럼 수식에 따라 시간복잡도는 O(2^(log_2 N))이 되는 것을 알 수 있습니다.

하지만 조금 더 살펴보도록 합시다.

결과에 나온 log_2 N 는 과연 무엇을 의미할까요?

2^(log_2 N) = P 라고 생각해본다면, 양변에 로그를 취해

log_2 2^(log_2 N) = log_2 P 가 됩니다. 그러면 로그의 특징에 따라서,

log_2 N * log_2 2 = log_2 P

log_2 N = log_2 P

N = P

가 됩니다.

따라서 총 시간복잡도는 O(N) 이 됩니다.



2. 코드가 무엇을 의미하는가


위에서는 재귀 호출 패턴 분석을 통해 시간복잡도를 분석해보았습니다.

하지만 조금 더 직관적인 방법으로는, 코드가 무엇을 하는지를 살펴보면 알 수 있습니다.

위의 코드는 트리의 각 노드를 한 번씩 방문한 뒤 각 노드에서 상수 시간에 해당하는 일을 하게 됩니다. 즉, 수행시간은 노드의 개수와 선형관계에 있음을 알 수 있고, 이에 따라 N개의 노드가 있을때 알고리즘의 시간복잡도는 O(N)임을 알 수 있습니다.




> 순열(permutation)의 개수를 구하는 코드


아래의 코드는 입력된 문자열로 나타낼 수 있는 순열의 개수를 구하는 코드입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
void permutation(String str){
    permutation(str, "")
}
 
void permutation(String str, String prefix){
    if(str.length() == 0){
        System.out.println(prefix);
    }else {
        for(int i=0; i<str.length(); i++){
            String rem = str.substring(0,i) + str.substring(i+1);
            permutation(rem, prefix + str.charAt(i));
        }
    }
}
cs


코드를 보면 꽤 까다로울 수 있습니다.

이에 대한 시간복잡도 분석을 먼저 스스로 해보시고 보시는 것도 추천드립니다 :)


먼저 위의 코드를 분석하기 위해 permutation 함수가 얼마나 호출되는지, 각 호출마다 걸리는 시간이 얼마나 걸리는지를 나누어 생각하면 쉽게 분석할 수 있습니다.


1. 순열이 완성되는 시점에서의 permutation 함수 호출

순열이 완성되는 시점에서는 코드의 7번째 라인이 실행되는 것인데, 이 또한 permutation 함수가 호출되었기 때문에 입니다. 따라서 특정 문자열에 대한 순열의 개수만큼, permutation 함수가 호출될 것이며 이 값은 n! 입니다.


2. 순열 생성이 완성되기 전에 permutation 함수 호출

동시에 9~12번째 코드라인이 얼마나 호출되어 반복되는지를 살펴보아야 합니다.

트리구조로 생각한다면 1번에서 알아본 n! 는 결국 리프노드의 개수입니다.

그리고 루트에서 리프노드까지의 거리는 n일 것이며, 이를 통해 트리의 총 노드는 절대로 n*n! 을 넘지 못합니다.


3. 한 번의 permutation 호출에서의 소요 시간

한번의 호출이 있었을 때 연산 시간이 얼마나 걸리는지를 파악합니다.

7번째 줄에서는 문자열 전체를 출력하기 때문에 O(N) 시간이 걸리며, 10번째 줄에서 시행되는 substring 함수 또한 O(N) 시간이 걸리게 됩니다.

( 단순 함수 호출이기에 O(1)로 생각하실 수 있으나 java 업데이트 후 해당 함수가 O(N)의 소요시간을 갖게 되었습니다. )


이렇게 1,2,3으로 나누어 분석한 결과 permutation 함수의 총 시간 복잡도는 O(N * N * N!) 임을 알 수 있습니다.




> 피보나치 수 1


아래의 코드는 N번째 피보나치 수(Fibonacci number)를 구하는 코드입니다.


1
2
3
4
5
int fib(int n){
    if (n <= 0return 0;
    else if (n == 1return 1;
    return fib(n-1+ fib(n-2);
}
cs


이는 위에서 우리가 알아보았던 재귀 호출 패턴 분석을 이용하면 O(2^N) 의 시간복잡도를 가지게 됩니다.


하지만 이를 좀 더 세밀하게 분석해보면 약 O(1.6^N)의 시간복잡도가 나오게 됩니다. 구체적으로 다루지는 않겠지만, 우리가 재귀 호출에 대해서 트리구조로 그려볼 때, 호출 스택의 밑바닥에서 가끔식 한번의 호출만 필요한 경우 등이 그 이유일 것입니다.




> 피보나치 수 2


이번에도 위와 같은 동작을 하는, 피보나치 수와 관련된 코드이지만 이번에는 이전에 계산된 결과 값을 정수 배열에 저장하고 이를 이용하는 방식의 알고리즘 입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
void allFib(int n){
    int[] memo = new int[n+1];
    for (int i=0; i<n; i++){
        System.out.println(i+": "+fib(i,memo));
    }
}
 
int fib(int n, int[] memo){
    if (n <= 0return 0;
    else if (n == 1return 1;
    else if (memo[n] > 0return memo[n];
    memo[n] = fib(n-1, memo) + fib(n-2, memo);
    return memo[n];
}
cs


위의 코드에 대해 임의의 n 값으로 실제 호출 스택을 그려본다면 fib(i)를 호출할 때마다 fib(n-1)과 fib(n-2)의 계산이 이미 끝나 있고 이를 단순히 배열에서 가져오는 연산을 진행하는 것을 알 수 있습니다.

즉, 이러한 과정은 상수시간 안에 동작하고 상수 시간의 동작을 총 N번 반복하기 때문에 해당 알고리즘의 시간복잡도는 O(N)을 가지게 됩니다.


이러한 방법은 메모이제이션(Memoization) 이라고 불리는 방법으로써, 지수 시간이 걸리는 재귀 알고리즘을 최적화할 때 쓰는 흔한 방법 중 하나 입니다.

비슷한 방법 중 하나로는 동적계획법(Dynamic Programming) 이 있습니다.


물론 메모이제이션과 동적계획법이 아예 같은 개념인 것도 아니고, 아예 다른 개념인 것도 아닙니다.

필요하다면 추후 다루게 되겠지만 간단하게 살펴보면, 메모이제이션은 결과를 구하고 이를 할용하면서 문제를 해결하는 것이고, 동적계획법은 큰 문제에 대해 작은 부분으로 나누어 작은 것부터 해결해 나가는 것 입니다.



이렇게 지난 포스팅에서 알아본 시간복잡도에 대한 개념에 이어 여러가지 예제들을 함께 살펴보았습니다.

해당 예제들을 통해 실수 할 수 있는 부분들을 캐치하여 추후 자신이 분석하고자 하는 알고리즘에 대해 보다 정확한 분석이 이루어 져야 합니다.

블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc

안녕하세요. 문범우입니다.

최근 Java로 알고리즘 스터디를 시작하게 되었습니다.

단순히 문제풀이 보다는 이론적인 내용들을 살펴보며 관련된 문제를 푸는 방식으로, 기초부터 다시 살펴보려합니다.

이번에는 직접적인 알고리즘 내용에 앞서, 알고리즘 분석 즉 시간복잡도와 공간복잡도에 대해 이야기를 먼저 진행해보겠습니다.



> 왜 알아야 하는가?


Big-O 표기법은 알고리즘의 효율성을 나타내는 지표 혹은 언어이다.

이를 통해 자신이 작성한 알고리즘이 이전보다 빨라졌는지 느려졌는지 판단하는데 도움이 될 것이다.

물론 이 외에 다른 개발자들과 특정 알고리즘에 대해 이야기하거나 효율성 판단등에 의해서 Big-O 표기법을 통해 보다 원활하게 의사소통을 진행할 수 있다.


실제로 Big-O 표기법 이외에 다른 표기법도 있으나 이에 대해서는 혼동을 피하기 위해 제일 마지막에서 언급하겠다.


또한 Big-O 표기법에 대해 이해하는 방식이 조금씩 상이할 수 있지만 그 정의를 정확하게 이해한다면 문제되지 않을 것이다.



> Big-O 표기법의 정의


Big-O 표기법은 사전적으로, 시간(또는 공간)의 상한을 이야기한다.

즉, 시간복잡도와 공간복잡도의 상한에 대해서 이야기하는 것이다.

(주로 시간복잡도를 다루게 될 것이므로 시간을 대표로 설명하겠다.)


정의대로 이야기한다면 시간복잡도에 대해서 이야기할 어떤 것의 최악의 경우(가장 느렸을 경우)에 대한 시간을 이야기 하는 것이다.

즉, 평균적으로 10시간이 걸리는 업무가 있지만 특정 상황에 따라 20시간이 걸린다고 생각해보자. 이때 그 업무에 대한 시간의 상한은 20시간이다. 아무리 오래 걸려도 20시간이면 되기때문이다.

이때 이야기 하는 '아무리 오래 걸려도' 라는 키워드가 결국 Big-O이다.


그럼 이러한 의문이 들 수 있다.


"그럼 그 업무는 아무리 오래 걸려도 100시간 안에 할 수 있으니, 시간의 상한이 100시간이라고 해도 맞는건가요?"


그렇다. 정의에 따라서는 전혀 틀린말이 아니고 수학적으로도 옳은 표기이다.

하지만, 대체로는 그렇게 이야기하지 않는다.


Big-O가 대략적으로 어떤걸 의미하고자 하는지 이해가 갔다면 조금 더 수식적으로 살펴보자.


이를 위해서 위에서 언급한 예시를 조금 더 구체화 해보자.

특정 업무에 대해서 처리할 문서가 n개라고 가정하자.


이때 만약 A라는 사람은 정직하게 일해서 문서당 소요시간이 1시간이다.

근데 B라는 사람은 아직 경력이 부족해서 문서당 소요시간이 2시간이다.

슬프게도 C라는 사람은 호기심이 많아 문서개수에 대해 제곱시간이 소요된다.

놀랍게도 D라는 사람은 자동화 툴을 만들어 놓아서 문서개수에 상관없이 10시간이 소요된다.


이러한 사람들에 대해 각각 Big-O로 소요시간을 나타내보자.


A는 O(N)이다.

문서 1개당 소요시간이 1시간이므로 문서가 늘어나면 소요시간도 선형적으로 늘어난다.


B는 O(2N)이다.

A와 비슷하게 선형적으로 늘어나지만 그 값이 2배일 뿐이다.


C는 O(N^2)이다.

A와 B에 비해 호기심이 많은 C는 문서당 제곱시간이 소요된다고 하였기에 위와 같이 표기된다.


D는 O(10)이다.

자동화 툴을 만들어 놓은 D는 문서 개수와 상관없이 무조건 10시간이 소요된다.


Big-O표기는 위와 같이 표기하는 것이다.

하지만 아직 한가지가 남았다. Big-O 에서는 작은 것들은 신경쓰지 않는다.


정말 간단하게 이야기한다면 Big-O 표기법은, 제일 영향력있는 놈만 신경쓰겠다는 이야기다.

실제로 알고리즘에 큰 영향을 주는 것만 고려하겠다는 이야기인 것이다.


이것은 특정 수식에서 최고차항을 제외한 나머지를 무시한다는 의미이기도 하며, 최고차항에 대한 상수배 또한 무시하겠다는 것이다.


최고차항에 대한 상수배를 무시하겠다는 것은

즉, Big-O 정의에 따르면, O(N)과 O(2N)은 모두 동일하게 O(N)으로 표기된다.

그렇다면, O(10)은 어떻게 표기될까?

상수 시간이므로 O(1) 로 표기된다.


그리고 최고차항을 제외한 나머지를 무시하겠다는 것은,

O(N^2 + 10N + 3) 으로 표시된 값이 있을때 이를 O(N^2)과 같이 표기하겠다는 것이다.



위와 같이 알아본 Big-O 표기법은 초기에 언급한 것과 같이 단순히 시간복잡도를 분석하는데만 사용되지 않는다. 시간 또는 공간 모두 분석하는데 사용되며 공간에 대해 분석하는 것을 공간복잡도라고 이야기한다.


공간복잡도에 대해서 추가적인 설명을 하자면 다음과 같다.


> 공간복잡도

재귀 호출에서 사용하는 스택 공간 또한 공간 복잡도 계산에 포함된다.

하지만 함수를 단지 n번 호출했다고 해서 무조건 O(n) 공간을 사용하는 것은 아니다.



Big-O 표기법에서 주로 사용되는 표현들에 대한 대소비교를 해본다면 다음과 같다.(시간복잡도, 공간복잡도 순서)


O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!) < O(nⁿ)


그럼 마지막으로, 처음에 이야기했던 Big-O 이외의 표기법들에 대해 간단히 알아보자.


+ Big-Omega

빅 오메가라고 부르는 표기법은 시간에 대한 등가 또는 하한의 개념을 나타낸다.

즉 모든 process는 Ω(1)로 표기할 수 있다. 결국 오메가로 표기되는 알고리즘은 Ω 수행시간보다 빠를 수 없다.


+ Big theta

빅 세타라고 부르는 표기법은 Big-O와 Big-Omega 모두를 이야기한다.

즉, 특정 알고리즘의 시간복잡도가 O(N) 이면서 Ω(N) 이라면 이는 θ(N)이라고 표기할 수 있다.




이렇게 Big-O에 대한 개념적인 내용을 정리해보았습니다.

추후 필요하다고 생각되면 특정 코드나 내용들에 대해 시간복잡도와 공간복잡도를 계산해보는 내용에 대해서도 정리해보겠습니다.



블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc