인프런 - 영리한 프로그래머를 위한 알고리즘 강좌
Ch01. 알고리즘 분석
시간복잡도
- 알고리즘을 분석
- 알고리즘의 자원(resource) 사용량을 분석
- 자원이란 실행시간, 메모리, 저장장치, 통신 등
- 여기서는 실행시간의 분석에 대해서 다룸.
- 시간복잡도
- 실행시간은 실행환경에 따라 달라짐(하드웨어, 운영체제, 언어, 컴파일러 등등)
- 그래서 실행시간을 측정하는 대신 연산의 실행 횟수를 카운트한다.
- 연산의 실행 횟수는 입력 데이터의 크기에 관한 함수로 표현.
- 데이터 크기가 같더라도 실제 데이터에 따라서 달라짐.
- worst-case analysis(최악의 경우 시간 복잡도)
- average-case analysis(평균 시간복잡도)
- 점근적(Asymptotic) 분석
- 점근적 표기법을 사용
- 데이터의 개수가 n->무한대 일때 수행시간이 증가하는 growth rate(성장율)로 시간복잡도를 표현하는 기법
- 세타 표기, O-(빅오) 표기 등을 사용
- 유일한 분석법도 아니고 가장 좋은 분석법도 아님
- 다만 (상대적으로) 가장 간단하며
- 알고리즘의 실행환경에 비의존적
- 점근적 표기법을 사용
- 점근적 분석의 예 : 상수 시간복잡도
- 입력으로 n개의 데이터가 저장된 배열 data가 주어지고, 그 중 n/2번째 데이터를 반환한다.
public int simple( int data[], int n) { int k = n/2; return data[k]; }
- n에 관계없이 상수 시간이 소요된다. 이 경우 알고리즘의 시간복잡도는 O(1)이다.
- 입력으로 n개의 데이터가 저장된 배열 data가 주어지고, 그 중 n/2번째 데이터를 반환한다.
- 점근적 분석의 예 : 선형 시간복잡도
- 입력으로 n개의 데이터가 저장된 배열 data가 주어지고, 그 합을 구하여 반환한다.
public int sum(int data[], int n) { int sum = 0; for (int i = 0; i < n; i++) { sum += data[i]; } return sum; }
- 이 알고리즘에서 가장 자주 실행되는 문장은
sum += data[i]
이며, 실행 횟수는 항상 n번이다. - 가장 자주 실행되는 문장의 실행횟수가 n번이라면 모든 문장의 실행 횟수의 합은 n에 선형적으로 비례하며, 모든 연산들의 실행횟수의 합도 역시 n에 선형적으로 비례한다.
- 선형 시간복잡도를 가진다고 말하고 O(n)이라고 표기한다.
- 입력으로 n개의 데이터가 저장된 배열 data가 주어지고, 그 합을 구하여 반환한다.
- 선형 시간복잡도 : 순차탐색
- 배열 data에 정수 target이 있는지 검색한다.
public int search(int data[], int target) { for (int i=0; i<data.length; i++) { if(data[i] == target) { // 이 알고리즘에서 가장 자주 실행되는 문장이며, 실행횟수는 최악의 경우 n번이다. return i; } } return -1; }
- 최악의 경우 시간복잡도는 O(n)이다.
- 배열 data에 정수 target이 있는지 검색한다.
- 점근적 표기법
- 알고리즘에 포함된 연산들의 실행 횟수를 표기하는 하나의 기법
- 최고차항의 차수만으로 표시
- 따라서 가장 자주 실행되는 연산 혹은 문장의 실행횟수를 고려하는 것으로 충분
- 점근 표기법
- O- 표기 : upper bound를 표현
- Ω- 표기 : lower bound를 표현
- Θ-표기 : upper bound와 lower bound를 동시에 표현
- 다항시간(polynomial-time) 알고리즘
- An algorithm is efficient if its running time is polynomial.
이진검색과 정렬
- 이진검색
- 배열 data에 n개의 문자열이 오름차순으로 정렬되어있다.
int binarySearch(int n, char* data[], char *target) { int begin = 0, end = n-1; while(begin <= end) { int middle = (begin + end) / 2; int result = strcmp(data[middle], target); if (result == 0) { return middle; } else if (result < 0) { begin = middle+1; } else { end = middle-1; } } return -1; }
- 한 번 비교할 때마다 남아있는 데이터가 절반으로 줄어든다. 따라서 시간복잡도는 O(log2n)이다.
- 데이터가 연결리스트에 오름차순으로 정렬되어있다면?
- 연결리스트에서는 가운데(middle) 데이터를 O(1)시간에 읽을 수 없음.
- 따라서 이진검색을 할 수 없다.
- 순차검색의 시간복잡도는 O(n)이고 이진검색은 O(log2n)이다. 둘의 차이는 매우 크다.
- 배열 data에 n개의 문자열이 오름차순으로 정렬되어있다.
- 버블 정렬
void bubbleSort(int data[], int n) { for (int i=n-1; i>0; i--) { for (int j=0; j<i; i++) { if (data[j] > data[j+1]) { int tmp = data[j]; data[j] = data[j+1]; data[j+1] = tmp; } } } }
- 시간 복잡도는?
- 삽입정렬
void insertion_sort(int n, int data[]) { for (int i=1; i<n; i++) { int tmp = data[i]; int j = i-1; while ( j>=0 && data[j] > data[i]) { data[j+1] = data[j]; j--; } data[j+1] = tmp; } }
- 시간 복잡도는?
- 퀵소트(quicksort) 알고리즘
- 최악의 경우 O(n*n), 하지만 평균 시간복잡도는 O(nlog2n)
- 최악의 경우 O(nlog2n)의 시간복잡도를 가지는 정렬 알고리즘
- 합병정렬(merge sort)
- 힙 정렬(heap sort)등
- 스택을 이용한 미로찾기
int maze() { Stack s = create(); Position cur; cur.x = 0; cur.y=0; int init_dir = 0; // 어떤 위치에 도착했을 때 처음으로 시도해 볼 이동 방향 while(1) { maze[cur.x][cur.y] = VISITED; if (cur.x == n-1 && cur.y == n-1) { break; } bool forwarded = false; for (int dir = init_dir; dir<4; dir++) { if(movable(cur, dir)) { // 어떤 셀도 2번 방문되지 않으며, 이 문장에 의해서 4번 이상 검사되지 않는다. push(s, dir); cur = move_to(cur, dir); forwarded = true; init_dir=0; break; } } if (!forwarded) { maze[cur.x][cur.y] = BACKTRACKED; if (is_empty(s)) { break; } int d = pop(s); cur = move_to(cur, (d+2)%4); init_dir = d+1; } } }
- 시간복잡도는 O(n*n)이며 이는 선형시간이다.
- 큐를 이용한 미로 탐색 ```
Queue queue = create_queue(); Position cur; cur.x = 0; cur.y = 0; enqueue(queue, cur); maze[0][0] = -1; bool found = false; while(!is_empty(queue)) { Position cur = dequeue(queue); for (int dir=0; dir<4; dir++) { if (movable(cur, dir)) { Position p = move_to(cur, dir); maze[p.x][p.y] = maze[cur.x][cur.y] - 1; if (p.x == n-1 && p.y == n-1) { printf(“Found the path.\n”); found = true; break; } enqueue(queue, p); // 어떤 셀도 2번이상 큐에 들어가지 않는다. } } }
```
Comments