1. 시간복잡도와 공간 복잡도란?
시간 복잡도 - 시간 복잡도란 주어진 문제를 해결하기 위한 연산횟수를 말한다.
공간복잡도 - 알고리즘이나 프로그램이 실행되는 동안 사용하는 메모리의 공간의 크기를 의미한다.
2. 시간 복잡도 표기법
시간 복잡도는 아래와 같이 3가지의 표기법이 존재하지만 주로 사용되는 표기법은 빅-오(BigO-notation)표기법을 사용하여 표현한다. 왜냐면 수행 시간을 계산할 때 worst case를 기준으로 만족하는 수행 시간을 계산하기 위해서이다.
- 빅-오메가(best case) : 최선일 때의 연산 횟수를 나타낸 표기법
- 빅-세타(average case) : 보통일 때의 연산 횟수를 나타낸 표기법
- 빅-오 (worst case) : 최악일 때의 연산 횟수를 나타낸 표기법
알고리즘의 최악의 경우를 고려함으로써 예상치 못한 상황에서도 안정적인 성능을 보장하기 위해서 최악의 경우에도 효율적으로 동작하는 알고리즘을 고민하기위해서이다.
일반적인 BigO 표기법
O(1): 상수 시간 복잡도. 입력 크기에 상관없이 일정한 실행 시간을 갖는 경우.
public class ConstantTimeExample {
public static int constantTimeExample(int[] arr) {
return arr[0];
}
}
O(log n): 로그 시간 복잡도. 주로 이진 검색과 같은 알고리즘 등의 경우
이는 보통 이진 탐색과 같은 알고리즘에서 볼 수 있는데
레벨 0: 8
/ \
레벨 1: 3 10
/ \ / \
레벨 2: 1 6 9 14
/ \
레벨 3: 4 7
위와 같은 이진 트리에서 탐색을 한다고 할 때
루트 노드에서 첫번째 자식 노드 층으로 이동하게 되면 탐색해야할 노드가 절반으로 줄어들게 된다.
만약 탐색을 해야할 자료의 개수가 2^n개 라면 n번의 탐색을 통해서 원하는 값을 찾을 수 있게되고
이를 수식으로 나타내면
따라서 이를 O(log n)으로 표기한다.
public class BinarySearch {
public static int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
}
O(n): 선형 시간 복잡도. 입력 크기에 비례하는 선형적인 실행 시간을 갖는 경우.
public class LinearTimeExample {
public static void linearTimeExample(int[] arr) {
for (int element : arr) {
System.out.println(element);
}
}
}
O(n log n): 선형 로그 시간 복잡도. 효율적인 정렬 알고리즘 등의 같은 경우
병합정렬에서는 배열을 반으로 나눈후, 나눠진 각각을 재귀적으로 정렬한 뒤 병합하는과정을 거친다.
이는 정렬해야하는 데이터의 크기(n)에 크기(n)만큼의 로그(log n)를 곱한만큼의 시간이 소요된다.
public class MergeSort {
public static void mergeSort(int[] arr, int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
private static void merge(int[] arr, int low, int mid, int high) {
int n1 = mid - low + 1;
int n2 = high - mid;
int[] left = new int[n1];
int[] right = new int[n2];
for (int i = 0; i < n1; ++i) {
left[i] = arr[low + i];
}
for (int j = 0; j < n2; ++j) {
right[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = low;
while (i < n1 && j < n2) {
if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
while (i < n1) {
arr[k++] = left[i++];
}
while (j < n2) {
arr[k++] = right[j++];
}
}
}
O(n^2): 이차 시간 복잡도. 중첩된 반복문이 있는 알고리즘의 경우
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
// swap arr[j+1] and arr[i]
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
O(2^n): 지수 시간 복잡도. 재귀적 알고리즘 중 부분 집합 생성 등의 경우
public class ExponentialTimeExample {
public static int recursiveExponential(int n) {
if (n <= 1) {
return n;
}
return recursiveExponential(n - 1) + recursiveExponential(n - 2);
}
}