알고리즘 설계 기법의 종류
- 전체를 그냥 다 보자. (Brute Force - 완전 탐색)
- 배열 : for 문, while 문
- 그래프 ( 관계가 있는 데이터)
- DFS, BFS
- 상황마다 좋은 것을 고르자 : Greedy
- 규칙 + 증명 → 구현
- 큰 문제를 작은 문제로 나누어 부분적으로 해결하자(Dynamic Programming)
- 분할 정복과 다르게 작은 문제가 중복
- 중복된 문제의 해답을 저장해놓고 활용하자 (Memoization)
- 큰 문제를 작은 문제로 나누어 부분적으로 해결하자 (분할 정복)
- 정체 중 가능성 없는 것을 빼자 (Backtracking - 백트래킹)
→ 이 기본들을 기반으로 더 고급 알고리즘들이 개발됨
분할 정복
대표적으로 병합 정렬, 퀵 정렬 + 이진 검색
만약 n개의 동전 중에 1개의 가짜 동전 있고, 진짜 동전에 비교해 아주 조금 가볍다. 양팔저울을 최소로 사용해서 찾는 방법은?
유래는 나폴레옹의 전략, 중앙에 들어가서 양 끝으로 격파해나감
전략
- 분할(Divide) : 해결할 문제를 여러 개의 작은 부분으로 나눈다.
- 정복(Conquer) : 나눈 작은 문제를 각각 해결한다
- 통합(Combine): (필요하다면) 해결된 해답을 모은다.
하나라도 틀리면 전체 결과가 잘못 나옴
재귀함수로 많이 구현한다.
Top-down approach
예시) $C^n$을 구하는 방법
반복(iterative) 알고리즘 $O(n)$
iterative_power(x,n)
result <- 1
for i in 1 ->n
result <- result * x
return result
분할 정복 기반의 알고리즘 $O(log_2n)$
짝수와 홀수로 나누어서 계산해 나감
$C^8 = ((C^2)^2)^2$
짝수 일 때 $C^n = C^{n/2} * C^{n/2}$ = $C^n$
홀수 일 때 $C^n = C^{(n-1)/2} * C^{(n-1)/2} * C$ = $C^n$
Recursive_Power(x,n)
if n == 1: return x
if n is even
y <- recursive_poser(x,n/2)
return y * y
else:
y <- recursive_power(x,(n-1)/2)
return y * y * x
병합 정렬 Merge Sort
- 여러 개의 정렬된 자료의 집합을 병합하여 한 개의 정렬된 집합으로 만드는 방식
- 분할 정복 알고리즘 활용
- 자료를 최소 단위의 문제까지 나눈 후에 차례대로 정렬하여 최종 결과를 얻어냄
- ⇒ 나누기 + 정렬
- top-down 방식
- 시간 복잡도
- $O(nlogn)$ : $logN$이 나오는 경우도 있고 $n$ 이 나올 수도 있기 때문에
알고리즘
분할 과정
merge_sort(list m)
if length(m) == 1: return m
list left, right
middle <- length(m) /2
for x in m before middle
add x to left
for x in m after or equal middle
add x to right
left <- merge_sort(left)
right <- merge_sort(right)
return merge(left, right)
병합 과정
merge(List left, List right)
List result
while length(left) > 0 OR length(right) > 0
if length(left) > 0 AND length(right) > 0
if first(left) <= first(right)
append popfirst(left) to result
else
append popfirst(right) to result
elif length(left) > 0
append popfirst(left) to result
elif length(right) > 0
append popfirst(right) to result
return result
- 파이썬 코드
def merge_sort(a):
# 문제를 절반으로 나누어서 각각을 해결
if len(a) == 1: # 길이가 1일때 리턴
return a
else: # 길이가 1 보다 클 때재귀호출하는 부분
mid = len(a) // 2
left = a[:mid]
right = a[mid:]
left = merge_sort(left)
right = merge_sort(right)
return merge(left, right)
def merge(left,right):
# 두 개의 정렬된 부분집합을 정렬하여 하나의 집합으로 리턴
result = []
# 병합과정
# 각각의 최소값들(가장 앞쪽값)을 비교해서 더 작은 요소를 결과에 추가
# 두 부분집합에 요소가 없어질 때까지 반복
while left or right:
if left and right: # 양 쪽이 둘 다 남아 있을 때
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
elif left: # 왼쪽만 남아 있으면
result.append(left.pop(0))
elif right:
result.append(right.pop(0))
return result
퀵 정렬
주어진 배열을 두 개로 분할하고, 각각을 정렬한다.
병합 정렬과 동일? X
- 다른 점 1 : 병합 정렬은 그냥 두 부분으로 나누는 반면에, 퀵 정렬은 분할할 때, 기존 아이템(pivot item) 중심으로 분할한다.
- 기준보다 작은 것은 왼편, 큰 것은 오른편에 위치시킨다.
- 다른 점 2 : 각 부분 정렬이 끝난 후, 병합정렬은 ‘병합’이란 후처리 작업이 필요하나, 퀵 정렬은 필요로 하지 않는다.
퀵 정렬의 가장 최악의 배열상태 : $O(n^2)$, (=기존 리스트가 역순으로 정렬되있는 경우)
Hoare-Partition (호어 파티션) 알고리즘
partition(A[], L , r)
p <- A[L] // p : 피봇 값
i <- L, j <- r
while i <= j
while i <= j and A[i] <= p : i ++ # 피봇보다 큰 값이 나올때까지 i를 더해감
while i <= j and A[j] >= p : j -- # 피봇보다 작은 값이 나올때까지 j를 뺴감
if i < j : swap(A[i], A[j])
swap(A[L], A[j]) # 피봇과 i ==j 인 부분을 교환
return j
파이썬 코드
'''
3 2 4 6 9 1 8 7 5
'''
def partition(a,l,r):
p = a[l]
i,j = l, r
while i <= j:
while i <= j and a[i] <= p: i += 1
while i <= j and a[j] >= p: j -= 1
if i < j:
a[i],a[j] = a[j],a[i]
a[l], a[j] = a[j], a[l]
return j
def quick_sort(a,l,r):
if l < r:
pivot = partition(a,l,r)
quick_sort(a,l,pivot-1)
quick_sort(a,pivot+1,r)
arr = list(map(int,input().split()))
quick_sort(arr,0,len(arr)-1)
print(arr)
Lomuto partition (로무토 파티션) 알고리즘 : 호어 파티션보다 안좋다.
partition(A[], p, r)
x <- A[r] # 피봇 고르기
i <- p -1
for j in p -> r-1
if A[j] <= x
i ++, swap(A[i], A[j]) # 값이 피봇보다 작다면? i,j 둘다 증가,
# 크다면? j 만 증가됨
swap(A[i+1], A[r])
return i + 1
풀이
arr = [3 2 4 6 9 1 8 7 5] 피벗 : 5 고 i = 0 j = 1 일 때 arr [1] ==> 2 2 < 5 ==> 조건 만족하면서 i ++ 되서 i,j 둘 다 1이 되서 스왑에 의미가 없음 하지만 i = 2 일 때
j는 3,4,5번째 항까지 올라가서 if를 만족하게 됨
⇒ i = 3 과 j =5의 값을 교환하게 됨
이진 검색
- 정렬된 데이터를 기준으로 특정 값이나 범위를 검색하는데 사용 $O(logn)$
- 이진 검색을 활용한 심화 학습 키워드 : Lower Bound, Upper Bound
- 정렬된 배열에서 특정 값 이상 또는 이하가 처음으로 나타나는 위치를 찾는 알고리즘
- 특정 데이터의 범위 검색 등에서 활용
# 이진 탐색
arr = [324,32,22134,16,48,93,922,316]
# 1. 정렬된 상태의 데이터
arr.sort()
# 2. 이진 검색 - 반복문 버젼
def binarySearch(target):
# 제일 왼쪽, 오른쪽 인덱스 구하기
low = 0
high = len(arr) -1
# 탐색 횟수
cnt = 0
# 해당 숫자를 찾으면 종료
# 더이상 쪼갤 수 없을 때까지 반복
while low <= high:
mid = (low + high) //2
cnt += 1
# 가운데 숫자가 정답이면 종료
if arr[mid] == target:
return mid, cnt
elif arr[mid] > target:
high = mid - 1
elif arr[mid] < target:
low = mid + 1
# 못 찾으면 -1 반환
return -1, cnt
# 3. 이진 검색 - 재귀 함수 버젼
def binarySearch2(low,high, target):
# 기저 조건
if low > high:
return -1
# 다음 재귀전에 뭘 해야되는가
# 정답 판별
mid = (low + high) // 2
if target == arr[mid]:
return mid
# 다음 재귀 함수 호출(파라미터로 뭘 넘길것인가)
if target < arr[mid]:
return binarySearch2(low, mid-1, target)
else:
return binarySearch2(mid + 1, high, target)
# 재귀함수에서 돌아왔을 때 어떤 작업을 해야될까
# 이진 검색에서는 없음
정리
병합 정렬
- 외부 정렬의 기본이 되는 정렬 알고리즘이다.
- 멀티코어(Multi-Core) CPU 나 다수의 프로세서에서 정렬 알고리즘을 병렬화하기 위해 병합 정렬 알고리즘이 활용된다.
퀵 정렬
- 매우 큰 입력 데이터에 대해서 좋은 성능을 보이는 알고리즘이다.
'TIl' 카테고리의 다른 글
그래프 (0) | 2024.04.01 |
---|---|
백트래킹 (0) | 2024.04.01 |
Django Template (0) | 2024.03.14 |
Django, Web Application, Framework, (3) | 2024.03.14 |
Responsive Web (2) | 2024.03.11 |