시간 복잡도(Time Complexity)와 공간 복잡도(Space Complexity)

시간 복잡도는 속도에 관한 것이며 공간 복잡도는 메모리 사용량에 관한 것이다. 시간 복잡도는 연산 횟수로 구한다.


데이터의 개수가 n이하는 알고리즘 A가 유리하고 n이상은 알고리즘 B가 유리한것을 확인할 수 있다.
따라서 상황에 맞게 적절한 알고리즘을 택해야 한다. 다음의 코드를 살펴보자.

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
#include <stdio.h>
 
int LSearch(int arr[], int len, int target) { // Linear Search 함수
    for (int i = 0; i < len; i++) {
        if (arr[i] == target) // 찾으면 해당 위치의 인덱스 반환
            return i;
    }
    return -1;
}
int main(void) {
    int arr[] = { 25319 };
    int idx;
 
    idx = LSearch(arr, sizeof(arr) / sizeof(int), 9);
    if (idx == -1
        printf("탐색 실패\n");
    else 
        printf("타겟 인덱스: %d \n", idx);
 
    idx = LSearch(arr, sizeof(arr) / sizeof(int), 6);
    if (idx == -1)
        printf("탐색 실패\n");
    else
        printf("타겟 인덱스: %d \n", idx);
 
    return 0;
}
cs

이 경우 최악의 시간복잡도는 O(N)이다. 최선, 평균, 최악이 있지만 최악을 기준으로 잡는다.
이번에는 이진 탐색(Binary Search)알고리즘을 보자. 순차 탐색에 비해 좋은 성능을 내지만 정렬이 되어있어야 한다는 제약 조건이 존재한다.

이진탐색 (Binary Search)

이진탐색을 먼저 그림으로 나타내면 다음과 같다.

3을 찾기 위해 반씩 줄여나가며 총 3회 탐색하는 모습


이진탐색의 코드는 다음과 같다.

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
#include <stdio.h>
 
int BSearch(int arr[], int len, int target) {
    int first = 0;
    int last = len - 1;
    int mid;
 
    while (first <= last) { // fist와 last가 뒤집어지면 종료
        mid = (first + last) / 2;
        if (arr[mid] == target)
            return mid;
        else if (arr[mid] > target) // 중간 값이 target보다 큰 경우
            last = mid - 1// mid 좌측에서 탐색 진행
        else // 중간 값이 target보다 작은 경우
            first = mid + 1;
    }
    return -1// 탐색하지 못한 경우
}
 
int main(void) {
    int arr[] = { 1357911 };
    int idx;
 
    idx = BSearch(arr, sizeof(arr) / sizeof(int), 7);
    if (idx == -1)
        printf("탐색 실패 \n");
    else
        printf("타겟 인덱스 위치: %d \n", idx);
 
    idx = BSearch(arr, sizeof(arr) / sizeof(int), 8);
    if (idx == -1)
        printf("탐색 실패 \n");
    else
        printf("타겟 인덱스 위치: %d \n", idx);
 
    return 0;
}
cs

이 경우 최악의 시간 복잡도는 O(logN)이다.

각 빅 - 오 표기법들의 성능 비교

각 빅-오 표기법들의 성능은 다음과 같다.


순서대로 O(1) < O(logN) < O(N) < O(NlogN) < O(𝑁^2) < O(2^N) < O(N!) 이다.

'자료구조' 카테고리의 다른 글

5. 스택  (0) 2022.01.10
4. 연결 리스트  (0) 2021.07.19
3. 배열 기반 리스트  (0) 2021.07.19
2. 재귀 (Recursion)  (0) 2021.06.27

https://www.acmicpc.net/problem/2110

 

2110번: 공유기 설치

첫째 줄에 집의 개수 N (2 ≤ N ≤ 200,000)과 공유기의 개수 C (2 ≤ C ≤ N)이 하나 이상의 빈 칸을 사이에 두고 주어진다. 둘째 줄부터 N개의 줄에는 집의 좌표를 나타내는 xi (0 ≤ xi ≤ 1,000,000,000)가

www.acmicpc.net


문제 해결을 위한 과정

이 문제의 경우 이진 탐색으로 해결할 수 있는 문제였습니다. 그러나 일반적인 이진 탐색을 이용한 방법의 경우 start, end를 이용하여 mid값을 잡아주고 찾아야 하는 target이 있는 반면 이 문제는 target이라는 것이 딱히 없습니다.

따라서 target을 설정하는 것이 아닌 이진 탐색의 방법을 응용해야 한다는 것입니다.

문제 해결의 과정은 다음과 같습니다. 

 

1. 집과 집 사이의 거리의 최솟값을 start로 최댓값을 end로 지정한다.

(문제에서 집 여러 개가 같은 좌표를 가지는 일은 없기 때문에 공유기 사이의 거리의 최솟값은 1, 최댓값은 입력받은 집들의 좌표를 정렬한 후 맨 마지막 원소와 맨 처음 원소 사이의 거리)

 

2. start와 end를 이용해 mid값을 구하고 해당 mid값을 공유기들 사이의 거리의 최솟값으로 정하였을때 C개만큼 설치할 수 있는지 확인한다.

 

3-1. C개만큼 설치할 수 없을 때는 공유기 사이의 거리가 큼. 따라서 end를 mid - 1로 설정하여 2번 과정 반복

3-2. C개만큼 설치할 수 있을 때는 공유기 사이의 거리를 하나씩 증가하여 최댓값을 찾음. 따라서 start를 mid + 1로 설정하여 2번 과정 반복


소스코드

 

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
N, C = map(int, input().split())
data = []
for i in range(N):
  data.append(int(input()))
 
data.sort()
 
start = 1 # 공유기 사이 거리 최솟값
end = data[-1- data[0# 공유기 사이 거리 최댓값
ans = []
 
while start <= end:
  prev = data[0]
  mid = (start + end) // 2
  count = 1
  for i in range(1, N):
    if prev + mid <= data[i]:
      prev = data[i]
      count += 1
  if count >= C:
    start = mid + 1
    ans.append(mid)
  else:
    end = mid - 1
 
print(max(ans))
    
        
cs

https://programmers.co.kr/learn/courses/30/lessons/60060

 

코딩테스트 연습 - 가사 검색

 

programmers.co.kr


문제 해결을 위한 과정

이 문제의 경우 이진 탐색을 이용하여 푸는 문제라는 것을 알 수만 있다면 쉽게 해결할 수 있는 문제였습니다. 가장 먼저 문제의 조건을 보면 "?" 즉 와일드 문자가 들어갈 수 있는 곳은 접두사 즉 맨 앞, 혹은 접미사 즉 맨 뒤에만 존재할 수 있다고 합니다. 이 조건이 키포인트입니다.

 

1. 먼저 10001행을 가진 리스트 array와 10001행 리스트를 가진 리스트 reverse_array를 생성합니다. 그 후 words 리스트를 조회하면서 각 원소의 글자 수에 맞는 행에 array와 reverse_array에 삽입해 줍니다.

ex) frodo의 경우 5글자 이므로 array[5].append(frodo), reverse_array[5].append(frodo)를 수행해 줍니다. 

 

2. 1의 과정을 완료한 후 queries리스트를 조회하면서 원소가 ?로 시작하지 않으면 array의 해당 행에서 몇 개와 매치될수 있는지 찾고 그 값을 answer리스트에 append 해준다. 반대로 ?로 시작하면 해당 쿼리를 뒤집은 후 reverse_array의 해당 행에서 몇개와 매치될 수 있는지 찾는다. (단 이때 찾는 함수는 https://bgspro.tistory.com/27?category=981927 에서 소개한 count_by_range 함수를 이용하고 left_index와 right는 각각 "?"를 a로 치환한, 'z'로 치환한 값으로 설정한다.)

ex) 만약 fro?? 인 경우 ?로 시작하지 않고 5글자 이므로 array[5]에서 count_by_range(array, query.replace('?', 'a'), query.replace('?', 'b')를 수행한다. 

 

3. 위의 과정을 완료한 후 answer리스트를 return 한다.


문제 해결을 위한 팁

리스트를 손쉽게 역순으로 바꿀 수 있는 방법이 있습니다. 바로 인덱싱을 이용하는 방법인데 예시는 다음과 같습니다.

array = [1, 2, 3, 4, 5]의 경우 array [::-1]을 하게 되면 [5, 4, 3, 2, 1]로 바꿀 수 있습니다.]


소스코드

answer.append(ans)</div><div

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
from bisect import bisect_left, bisect_right
 
def count_by_range(a, left_value, right_value):
    left_index = bisect_left(a, left_value)
    right_index = bisect_right(a, right_value)
    return right_index - left_index
 
def solution(words, queries):
    answer = []
    array = [[] for _ in range(10001)]
    reverse_array = [[] for _ in range(10001)]
    
    for word in words:
        array[len(word)].append(word)
        reverse_array[len(word)].append(word[::-1])
        
    for i in range(10001):
        array[i].sort()
        reverse_array[i].sort()
        
    for i in queries:
        if i[0!= "?":
            ans = count_by_range(array[len(i)], i.replace('?''a'), i.replace('?','z'))
        else:
            ans = count_by_range(reverse_array[len(i)], i[::-1].replace('?','a'), i[::-1].replace('?','z'))
        answer.append(ans)
    return answer
cs

'알고리즘 > 프로그래머스' 카테고리의 다른 글

신고 결과 받기(Python)  (0) 2022.02.20
실패율 (Python)  (0) 2021.01.11
블록 이동하기 (Python)  (0) 2020.12.07
괄호 변환 (Python)  (0) 2020.12.05
자물쇠와 열쇠 (Python)  (0) 2020.12.03
개념

이진 탐색이란 정렬되어있는 데이터 집합을 이분화하여 탐색하는 방법이다. 이때 정렬된 데이터가 키 포인트인데 정렬이 되어있지 않다면 쓸 수 없다. start, end, mid를 이용하여 target을 탐색을 하는데 여기서 mid는 (start + end) // 2 한 값 즉 중간값이다. 이제 3가지 경우가 존재하는데 각각의 경우는 다음과 같다.

 

1. array[mid] == target 

2. array[mid] > target 

3. array[mid] < target

 

1번의 경우 단순하게 해당 mid값을 return 해주면 된다.

2번의 경우 중간값에 해당하는 값이 찾고자 하는 값 보다 크기 때문에 중간값 좌측의 구간만 다시 탐색을 해주면 된다. 따라서 end = mid - 1

3번의 경우 중간값에 해당하는 값이 찾고자 하는 값 보다 작기 때문에 중간값 우측의 구간만 다시 탐색을 해주면 된다.

따라서 start = mid + 1

구현은 재귀의 경우가 반복문의 경우보다 느리다고 알고 있기 때문에 더 효율적인 반복문의 형태로 구현을 한다.


소스코드

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def binary_search(array, target, start, end):
  while start <= end:
    mid = (start + end) // 2
    if array[mid] == target:
      return mid
    elif array[mid] > target:
      end = mid - 1
    else:
      start = mid + 1
  return -1 # target을 array에서 찾지 못한 경우
 
n, x = map(int, input().split())
data = list(map(int, input().split()))
result = binary_search(data, x, 0, n-1)
if result == -1:
  print("찾으시는 숫자가 존재하지 않습니다")
else:
  print(result + 1)
cs
개념

파이썬은 일반적인 이진탐색보다 손 쉽게 이진탐색을 사용할 수 있는 라이브러리인 bisect를 지원합니다. 

물론 이진탐색을 하기 위해서는 리스트가 먼저 정렬이 되어있어야 합니다. 

bisect에는 여러가지 메소드가 있지만 bisect_left(), bisect_right()메소드를 가장 많이 사용합니다.

두 메소드의 장점은 O(logN)의 시간복잡도로 동작할 수 있다는 장점이 있습니다.

 

bisect_left(a, x) :  리스트 a에서 x가 들어갈 가장 왼쪽 인덱스를 반환합니다.

bisect_right(a, x) : 리스트 a에서 x가 들어갈 가장 오른쪽 인덱스를 반환합니다.

 

1 2 3 3 3 4 7

위의 리스트를 a라고 가정했을때 bisect_left(a, 3)이면 2를 반환합니다.

위의 리스트를 a라고 가정했을때 bisect_right(a, 3)이면 5를 반환합니다.

 

이걸 이용해서 bisect_right값과 bisect_left값을 빼면 3 즉 원소의 개수를 파악할 수 있는 count_by_range() 함수를 정의할 수 도 있습니다. 


소스코드
1
2
3
4
5
6
7
8
9
10
11
12
from bisect import bisect_left, bisect_right
 
def count_by_range(array, left_value, right_value):
  left_index = bisect_left(array, left_value)
  right_index = bisect_right(array, right_value)
  result = right_index - left_index
  return result
 
array = [1233347]
print(bisect_left(array, 3)) # 3이 들어갈 가장 좌측 인덱스
print(bisect_right(array, 3)) # 3이 들어갈 가장 우측 인덱스
print(count_by_range(array, 33))
cs

 

+ Recent posts