- 데이터를 오름차순으로 정렬해보자!

$O(N^2)$ 정렬

선택 정렬 (Selection sort)

- 가장 작은 수를 첫 번째 인덱스로 선택 그 다음으로 작은 수를 두 번째 인덱스로 선택

- 이런식으로 가장 큰 수까지 마지막 인덱스로 선택하면 정렬 끝

선택 정렬 알고리즘

1 크기가 $n$인 정렬되지 않은 리스트가 있다

List = [5, 8, 7, 1, 2]

2 첫 번째 인덱스와 나머지 $n - 1$개의 수를 비교하여 가장 작은 수와 위치를 바꾼다

3 5와 비교하여 1이 가장 작으므로 5와 1의 위치를 바꾼다

List = [1, 8, 7, 5, 2]

4 이제 두 번째 인덱스와 나머지$n - 2$개의 수를 비교하여 남은 수 중 가장 작은 수와 위치를 바꾼다

5 8과 비교하여 2가 가장 작으므로 8과 2의 위치를 바꾼다

List = [1, 2, 7, 5, 8]

6 이런 식으로 $n-1$번째 인덱스와 나머지 1개의 수를 비교하여 오름차순 정렬을 마친다

List = [1, 2, 5, 7, 8]

선택 정렬 알고리즘 코드 구현

List = [5, 8, 7, 1, 2]
n = len(List)

for i in range(n - 1):
    min_idx = i
    
    for j in range(i + 1, n):   
        if List[j] < List[min_idx]:
            min_idx = j
            
    List[i], List[min_idx] = List[min_idx], List[i]
    print(List)     
[1, 8, 7, 5, 2]
[1, 2, 7, 5, 8]
[1, 2, 5, 7, 8]
[1, 2, 5, 7, 8]

버블 정렬 (Bubble sort)

- 연속된 인덱스를 비교하여 더 큰 값을 오른쪽으로 보냄

- 한 사이클을 돌면 가장 큰 값이 맨 뒤에 위치

- 사이클마다 남은 수 중 가장 큰 값이 뒤에 위치함

버블 정렬 알고리즘

1 크기가 $n$인 정렬되지 않은 리스트가 있다

List = [5, 8, 7, 1, 2]

2 첫 번째 인덱스와 두 번째 인덱스를 비교하여 더 큰값을 오른쪽에 위치시킨다

3 5와 8을 비교하면 8이 더 크므로 8을 오른쪽을 보낸다

List = [5, 8, 7, 1, 2]

4 이제 두 번째 인덱스와 세 번째 인덱스를 비교한다

5 8과 7을 비교하면 8이 더 크므로 8을 오른쪽으로 보낸다

List = [5, 7, 8, 1, 2]

6 이런식으로 한 사이클을 돌면 8이 마지막에 위치한다

List = [5, 7, 1, 2, 8]

7 다시 사이클을 돌면 7이 8 왼쪽에 위치한다

List = [5, 1, 2, 7, 8]

8 이런식으로 $n - 1$ 번의 사이클을 돌면 자료가 오름차순으로 정렬된다

List = [1, 2, 5, 7 ,8]

버블 정렬 알고리즘 코드 구현

List = [5, 8, 7, 1, 2]
n = len(List)

for i in range(n - 1, 0, -1):
    for j in range(i):
        if List[j + 1] < List[j]:
            List[j + 1], List[j] = List[j], List[j + 1]
    
    print(List)
[5, 7, 1, 2, 8]
[5, 1, 2, 7, 8]
[1, 2, 5, 7, 8]
[1, 2, 5, 7, 8]

삽입 정렬 (Insertion sort)

- 자료 배열의 모든 요소를 앞에서부터 차례대로 이미 정렬된 배열 부분과 비교함

- 자신의 위치를 찾아 삽입함

- 일반적으로 선택 정렬, 버블 정렬 보다 빠름

삽입 정렬 알고리즘

1 크기가 $n$인 정렬되지 않은 리스트가 있다

List = [5, 8, 7, 1, 2]

2 두 번째 원소를 부분 리스트에서 적절한 위치에 삽입

List = [5, 8, 7, 1, 2]

3 5와 8을 비교하면 8이 더 크므로 8을 오른쪽을 보낸다

List = [5, 8, 7, 1, 2]

4 세 번째 원소를 부분 리스트에서 적절한 위치에 삽입

5 7과 8을 비교하면 8이 더 크고 5와 7을 비교하면 7이 더 크므로 5와 8사이에 위치한다

List = [5, 7, 8, 1, 2]

6 네 번째 원소를 부분 리스트에서 적절한 위치에 삽입

7 1이 부분 리스트 중 가장 작으므로 맨 앞에 삽입

List = [1, 5, 7, 8, 2]

8 마지막 원소를 부분 리스트에서 적절한 위치에 삽입

9 2는 부분 리스트 중 1 다음으로 작으므로 1 오른쪽에 삽입

List = [1, 2, 5, 7, 8]

삽입 정렬 알고리즘 코드 구현

List = [5, 8, 7, 1, 2]
n = len(List)

for i in range(1, n):
    j = i - 1
    key = List[i]
    while List[j] > key and j >= 0:
        List[j + 1] = List[j]
        j = j - 1
    List[j + 1] = key
    print(List)
[5, 8, 7, 1, 2]
[5, 7, 8, 1, 2]
[1, 5, 7, 8, 2]
[1, 2, 5, 7, 8]

$O(N\log N)$ 정렬

병합 정렬 (Merge sort)

- 리스트 안에 있는 요소들을 왼쪽, 오른쪽 두 그룹으로 나눔

- 각 그룹을 또 왼쪽, 오른쪽 두 그룹으로 나눔, 이를 요소가 1개 남을 때까지 반복함

- 나누어진 두 개의 리스트를 병합함

- 이를 정렬될 때까지 반복함

병합 정렬 알고리즘

1 크기가 $n$인 정렬되지 않은 리스트가 있다

List = [5, 8, 7, 1, 2, 3, 9, 4]

2 그룹을 두 그룹으로 나눈다

[5, 8, 7, 1], [2, 3, 9, 4]

3 각 그룹을 두 그룹으로 나눈다

[5, 8], [7, 1], [2, 3], [9, 4]

4 이를 요소가 1개 남을 때까지 반복한다

[5], [8], [7], [1], [2], [3], [9], [4]

5 이제 나눈 순서의 역순으로 두 그룹씩 오름차순으로 병합한다

[5, 8], [1, 7], [2, 3], [4, 9]

6 이를 정렬이 끝날 때까지 반복한다

[1, 5, 7, 8], [2, 3, 4, 9]
List = [1, 2, 3, 4, 5, 7, 8, 9]

병합 정렬 알고리즘 코드 구현

- 위의 병합 정렬 알고리즘을 보면 두 그룹으로 나누고 병합하는 과정을 반복한다

- 그렇기에 재귀 함수를 사용하여 구현했음 ---> 리스트의 길이가 클 경우 많은 재귀 함수 호출이 이루어지기에 시간이 매우 오래걸림(내 생각)

- 먼저 left, right로 나눈 후 나눠진 left를 가지고 또 left, right로 나눈다

- left를 나누는 것이 끝나면 이제서야 right를 가지고 left, right로 나눈다

  • 이 코드는 아래 코드보다 느림

- list.pop(0)은 $O(N)$임

List = [5, 8, 7, 1, 2, 3, 9, 4]

def mergeSort(x):
    if len(x) <= 1:
        return x

    mid = len(x) // 2
    left = x[:mid]
    right = x[mid:]

    next_left = mergeSort(left)
    next_right = mergeSort(right)
    return merge(next_left, next_right)

def merge(left, right):
    sorted_list = []

    while left and right:
        if left[0] < right[0]:
            sorted_list.append(left.pop(0))
        else:
            sorted_list.append(right.pop(0))

    while left:
        sorted_list.append(left.pop(0))

    while right:
        sorted_list.append(right.pop(0))

    return sorted_list

mergeSort(List)

- 그래서 pop(0) 함수를 사용하지 않음

- 아래의 코드가 이해가 잘 안될 수 있다

- 그래서 어떻게 split하고 merge가 되는지 알아보기로 하자

- 밑의 출력을 보니 처음으로 merge()에 대입된 left와 right는 [5][8]임을 알 수 있다

- 처음으로 넣은 값은 [5, 8, 7, 1, 2, 3, 9, 4]인데 신기하다

- 자세히 살펴보자

List = [5, 8, 7, 1, 2, 3, 9, 4]
k = 0

def merge_sort(x): ## 나누기
    n = len(x)
    if n <= 1:
        return x
    
    mid = n // 2
    left = x[:mid] ## mid를 기준으로 왼쪽
    right = x[mid:] ## mid를 기준으로 오른쪽
    
    next_left = merge_sort(left) ## 재귀적으로 나누기
    next_right = merge_sort(right) ## 재귀적으로 나누기
    
    global k
    k += 1
    print('return 횟수 %s' %k)
    
    return merge(next_left, next_right)


def merge(left, right): ## 병합하기
    i = 0 
    j = 0
    sorted_list = list()  
    
    print(left)
    print(right)
    
    while i < len(left) and j < len(right): ## left와 right중 더 작은 값 넣기
        if left[i] <= right[j]:
            sorted_list.append(left[i])
            i += 1
        else:
            sorted_list.append(right[j])
            j += 1
            
    ## left와 right 중 남은 값을 넣어주기    
    while i < len(left):
        sorted_list.append(left[i])
        i += 1
        
    while j < len(right):
        sorted_list.append(right[j])
        j += 1
        
    return sorted_list
   
print('\n정렬된 배열:', merge_sort(List)) 
return 횟수 1
[5]
[8]
return 횟수 2
[7]
[1]
return 횟수 3
[5, 8]
[1, 7]
return 횟수 4
[2]
[3]
return 횟수 5
[9]
[4]
return 횟수 6
[2, 3]
[4, 9]
return 횟수 7
[1, 5, 7, 8]
[2, 3, 4, 9]

정렬된 배열: [1, 2, 3, 4, 5, 7, 8, 9]

- 우선 mergeSort 함수에서 return은 총 7번 일어남을 알 수 있다

- 각 상황에서 어떤일이 일어나는지 알아보자

1. 우리는 print(mergeSort(List))를 통해 mergeSort 함수에 List라는 input을 넣었다

2. mergeSort에는 [5, 8, 7, 1, 2, 3, 9, 4]이 대입됐다

3. left는 [5, 8, 7, 1], right는 [2, 3, 9, 4]이다

4. next_left는 mergeSort([5, 8, 7, 1]), next_right는 mergeSort([2, 3, 9, 4])이다

5. merge(next_left, next_right)값을 return한다

6. 근데 merge(next_left, next_right)를 return하려고 보니까 next_left, next_right 값을 알아야한다

7. 4번으로 돌아가서 보면 mergeSort([5, 8, 7, 1])mergeSort([2, 3, 9, 4])를 구해야 한다 ---> 그럼 구하면 되지

8. mergeSort에 [5, 8, 7, 1]이 대입된다

9. 그러면 mergeSort([5, 8, 7, 1])merge(mergeSort([5, 8]), mergeSort([7, 1]))를 return한다

10. 근데 mergeSort([5, 8]), mergeSort([7, 1])값은 뭐지?? ---> 이것도 구해야 한다

11. mergeSort([5, 8])을 구하면 next_left = [5], next_right = [8]이다

12. merge(next_left, next_right)merge([5], [8])이 되고 드디어 merge함수에 left와 right가 대입된다 ---> 그래서 처음 left와 right로 출력된 값이 [5][8]이었던 것: return1

13. merge([5], [8])[5,8]인 sorted_list를 return한다 ---> mergeSort([5, 8])[5, 8]을 return한다 즉, mergeSort([5, 8]) = [5, 8]

14. 이제 mergeSort([5, 8])를 구했으니 mergeSort([7, 1])값을 구할 차례이다

15. mergeSort([7, 1])merge([7], [1])이고 [1, 7]을 return한다 ---> mergeSort([7, 1]) = [1, 7]: return2

16. 이제 8번을 보자. 8번은 merge([5, 8, 7, 1])이고 merge(mergeSort([5, 8]), mergeSort([7, 1]))를 return한다

17. 이때는 mergeSort([5, 8])mergeSort([7, 1])를 모르는 상태였지만 이제는 구해서 알고 있다

18. merge([5, 8], [1, 7])을 구해보면 sorted_list로 [1, 5, 7, 8]을 return한다: return3

19. 이제 mergeSort([2, 3, 9, 4])을 구할 차례이다. 이는 위에서 한 방식대로 따라하면 된다

20. 결과적으로 print(mergeSort(List))[1, 2, 3, 4, 5, 7, 8, 9]을 출력하게 된다

- 메모리 아끼는 병합 정렬 참고: https://www.daleseo.com/sort-merge/

퀵 정렬 (Quick sort)

- 피벗을 하나 정하고 피벗을 기준으로 왼쪽에는 피벗보다 작거나 같은 원소가 오른쪽에는 피벗보다 큰 원소가 위치하도록 배열을 변경한다

- 위에서 나눠진 왼쪽 배열과 오른쪽 배열에 대해서도 위의 연산을 재귀적으로 반복한다

퀵 정렬 알고리즘

1 크기가 $n$인 정렬되지 않은 리스트가 있다

List = [5, 8, 7, 1, 3, 2, 9, 4]

2 피벗을 하나 정한다 (마지막 원소를 피벗으로 정하겠다)

3 피벗을 기준으로 왼쪽에는 피벗보다 작은 값만 오른쪽에는 피벗보다 큰 값만 있도록 배열을 변경한다

[{1, 3, 2}, {4}, {8, 7, 9, 5}] ## { }는 이해를 돕기 위해 표시한 것

4 이를 왼쪽 배열과 오른쪽 배열에도 원소가 하나 남을 때까지 재귀적으로 반복한다

[1, 2, 3, 4, 5, 7, 8, 9]

퀵 정렬 알고리즘 코드 구현

def partition(arr, left, right):
    ## left는 입력된 배열의 가장 왼쪽 인덱스. right는 가장 오른쪽 인덱스
    pivot = arr[right] ## 피벗은 주어진 배열(arr[left:right+1], 끝 값 포함 X)에서 마지막 위치(right)의 원소이다
    i = left - 1 ## 피벗보다 작은 값을 넣을 배열의 위치(인덱스) 
    for j in range(left, right): ## 피벗을 제외한 배열의 인덱스(left~right-1)
        if arr[j] <= pivot: ## 피벗보다 작거나 같은 경우
            i += 1 ## 값을 하나 넣을 거니까 배열의 위치에 +1
            arr[i], arr[j] = arr[j], arr[i] ## 피벗보다 작은 값을 i 인덱스의 값과 swap 한다(메모리 절약을 위해 새로운 list를 안 만듦 ---> in-place algorithm) 
            print(f'현재 배열의 상태: {arr}') ## 배열이 정렬되는 과정을 확인하자

    ## for문이 끝나면 입력된 배열이 다음과 같다 ---> [pivot보다 작거나 같은 값, (p) pivot보다 큰 값, pivot]
    ## 이제 pivot을 (p) 위치에 있는 값과 swap 하면 된다
    ## 여기서 (p)의 인덱스는 i + 1이다
    arr[i + 1], arr[right] = arr[right], arr[i + 1] 
    
    return i + 1 ## 피벗의 인덱스를 리턴한다

def quick_sort(arr, left, right):
    if left < right:
        pivot_idx = partition(arr, left, right) ## 피벗을 기준으로 왼쪽에는 작거나 같은 값을 오른쪽에는 큰 값이 위치하도록 한다 
        quick_sort(arr, left, pivot_idx - 1) ## 다시 왼쪽 배열에 대해서 partition 
        quick_sort(arr, pivot_idx + 1, right) ## 다시 오른쪽 배열에 대해서 partition
List = [5, 8, 7, 1, 3, 2, 9, 4]
quick_sort(List, 0, len(List) - 1)
print(f'정렬된 배열: {List}')
현재 배열의 상태: [1, 8, 7, 5, 3, 2, 9, 4]
현재 배열의 상태: [1, 3, 7, 5, 8, 2, 9, 4]
현재 배열의 상태: [1, 3, 2, 5, 8, 7, 9, 4]
현재 배열의 상태: [1, 3, 2, 4, 8, 7, 9, 5]
현재 배열의 상태: [1, 2, 3, 4, 5, 7, 9, 8]
정렬된 배열: [1, 2, 3, 4, 5, 7, 8, 9]

- 참고: in-place algorithm 이란?

- 주어진 배열 외에 사용하는 메모리 양을 나타내는 공간복잡도가 $O(1)$임을 의미한다

- 위의 퀵 정렬을 응용하면 배열에서 $k$번째로 작은 값을 select하는 알고리즘을 만들 수 있다

- Quickselect: https://en.wikipedia.org/wiki/Quickselect

$O(N)$ 정렬

계수 정렬

- 카운팅 정렬이라고도 한다

- 양수만 가능, 값의 범위가 크면 안됨(메모리 크기를 넘기면 안됨)

- 수의 범위가 작다면(입력으로 주어지는 값들의 개수: 0 ~ 1이라고 수의 범위가 작은 것이 아님... 0 ~ 1사이의 수는 무한개이다) 카운팅 정렬을 통해 빠르게 정렬할 수 있음

- 비교 정렬이 아님 ---> 위의 코드들은 다른 요소값과 비교하는데 카운팅 정렬은 비교없이 데이터를 정렬함

- 입력으로 주어지는 input의 개수는 큰데 주어지는 값의 개수가 적다면 메모리 관점에서 효율적이다

- 예로 input이 최대1억개인데 값이 1 ~ 10까지라면 위에서 다룬 정렬은 1억크기의 배열이 필요하지만 카운팅 정렬에 경우는 크기가 10인 배열을 만들면 된다

- 하지만, 최대 수를 기준으로 배열을 만든다(최대값이 100인 경우 크기가 100인 리스트 생성)

- 그렇기에 [0, 1, 1, 1, 100]인 리스트를 정렬한다고 보면 숫자는 3개 뿐이지만 최대값이 100이므로 크기가 100인 리스트를 만들어야 한다 ---> 심한 메모리 낭비

계수 정렬 알고리즘

1 최대 값이 k인 크기가 $n$인 정렬되지 않은 리스트가 있다

List = [5, 8, 7, 1, 1, 3, 9]

2 k = 10 이라고 가정하자. [0] * (k+1) 리스트를 만든다 --> 파이썬에서 인덱스는 0부터 시작하기 때문

count = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

3 List 요소의 값을 x라 하면 count[x]의 값을 +1 해준다

count = [0, 2, 0, 1, 0, 1, 0, 1, 1, 1, 0]

4 count 리스트에서 자기(x) 앞에 몇개의 숫자가 있는지를 바탕으로 x의 위치를 결정하여 정렬한다. 예로 3의 경우 자기 앞에 숫자 2개가 있으므로 3번째이다

List = [1, 1, 3, 5, 7, 8, 9]

계수 정렬 알고리즘 코드 구현

List = [5, 8, 7, 1, 1, 3, 9] 

def counting_sort(arr):
    count = [0] * (max(arr) + 1)
    sorted_list = [0] * len(arr) 
    
    for i in arr:
        count[i] += 1
        ## arr에 있는 수를 몇개인지 카운팅함
        
    for j in range(1, max(arr) + 1):
        count[j] += count[j - 1]
        ## count[j] 앞에 몇 개의 숫자가 있는지 저장
        ## count[5] = 10이라면 5가 x번 등장했다고 할 때 5앞에 10-x개의 숫자가 있다는 의미이므로 sorted_list[10-x : x]에 5가 위치한다. (x 번째 포함 no, x-1번째 까지)
        
    for k in range(len(arr)):
        sorted_list[count[arr[k]] - 1] = arr[k] ## 인덱스는 0부터 시작하므로 -1을 해줌
        count[arr[k]] -= 1
    
    return sorted_list

print(counting_sort(List))
[1, 1, 3, 5, 7, 8, 9]

시간 복잡도 정리

Name $T_{\operatorname{comp}}(n)$ $T_{\operatorname{swap}}(n)$ Time(worst case) : $T(n)$ Space In-place(Extra) Stable Adaptive
Selection $\Theta(n^2)$ $\Theta(n)$ $\Theta(n^2)$ $\Theta(n)$ Yes : $\Theta(1)$ No No
Bubble $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n)$ Yes : $\Theta(1)$ Yes No
Bubble+ $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n)$ Yes : $\Theta(1)$ Yes Yes
Insertion $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n)$ Yes : $\Theta(1)$ Yes Yes
Merge $\Theta(n\log n)$ $\Theta(n\log n)$ $\Theta(n\log n)$ $\Theta(n)$ No : $\Theta(n)$ Yes No
Quick $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n)$ Yes : $\Theta(\log n)$ No No
Heap $\Theta(n\log n)$ $\Theta(n\log n)$ $\Theta(n\log n)$ $\Theta(n)$ Yes : $\Theta(1)$ No No
Counting No comparison No swap $\Theta(n+k)$ $\Theta(n+k)$ No : $\Theta(n+k)$ Yes No
Radix No comparison No swap $\Theta(w(n+k))$ $\Theta(n+k)$ No : $\Theta(n+k)$ Yes No

재귀 함수 시간복잡도 닫힌 해

- 시간복잡도의 형태가 재귀 함수꼴로 되어 있어 해를 구할 줄 알아야 된다(ex: 병합 정렬)

- 아래는 이를 구하는 방법들이다

반복 대치 (Repeated Substitution)

- $T(n)=T(n-1)+C$ 라고 하자

- $T(n) = T(n-1)+C = (T(n-2)+C) + C = \cdots = T(1) + (n-1)C$

- 그러므로 $T(n)=O(n)$

수학적 귀납법 (Mathematical induction)

- $T(n) \leq 2T\left(\dfrac{n}{2}+10\right)+n$ 라고 하자

- $T(n)\leq cn\log n$ 이라고 추정한다(어떤 함수로 추정할지 잘 생각해야 됨) $\to$ $T(n)=O(n\log n)$

- $T(i)\leq ci\log i ,\;n_0\leq i< k$ 라고 가정한다

- 이제 $T(k)\leq ck\log k$ 일 때도 성립하는지 증명한다

$$\begin{aligned} T(k) &\leq 2T\left(\dfrac{k}{2}+10\right)+k\\[10pt] &\leq 2c\left(\dfrac{k}{2}+10\right)\log\left(\dfrac{k}{2}+10\right)+k\\[10pt] &=ck\log\left(\dfrac{k}{2}+10\right) + 20c\log\left(\dfrac{k}{2}+10\right)+k\\[10pt] &\leq ck\log \dfrac{2k}{3} + 20c\log\dfrac{2k}{3}+k\\[10pt] &=ck\log k +c(\log 2 -\log 3)k + 20c\log\dfrac{2k}{3}+k \, ---\, (1)\\[10pt] &\leq ck\log k\, ---\, (2) \end{aligned}$$

- 위에 $(1)$번 식에서 $(2)$번 식으로 어떻게 전개되는지 간단히 설명

- 일단 $\text{(1)번 식} \leq \text{(2)번 식}$ 이라는 것은 $c(\log 3 -\log 2)k\geq 20c\log\dfrac{2k}{3}+k $을 뜻한다

- $c(\log 3 -\log 2)k\geq 20c\log\dfrac{2k}{3}$ $\to$ $k$를 매우 크게 하면 된다($k$를 무한히 크게 하면 무한히 큰 차이가 생김)

- $c(\log 3 -\log 2)k\geq k$ $\to$ $c$를 매우 크게 하면 된다($c$를 무한히 크게 하면 무한히 큰 차이가 생김)

  • 결론

- $\dfrac{k}{2}+10\leq k,\, \dfrac{k}{2}+10\leq \dfrac{2k}{3}$을 만족하면서 충분히 큰 $k$와 충분히 큰 $c$를 고르면 $\text{(1)번 식} \leq \text{(2)번 식}$ 이 성립한다

마스터 정리 (Master theorem)

- $T(n)=aT\left(\dfrac{n}{b}\right)+f(n)$ 일 때 적용 가능하다

- $a\geq 1,\,b> 1$이며 $f(n)$은 다항식 형태

- $h(n)=n^{\log_b a}$ $\to$ $n=1$인 sub-problems 개수

- case 1) $\lim\limits_{n\to\infty}\dfrac{f(n)}{h(n)}=0 \Longrightarrow T(n)=\Theta\left(h(n)\right)$

- case 2) $\lim\limits_{n\to\infty}\dfrac{f(n)}{h(n)}=\infty$ and $af\left(\dfrac{n}{b}\right) < f(n) \Longrightarrow T(n)=\Theta\left(f(n)\right)$

- case 3) $\lim\limits_{n\to\infty}\dfrac{f(n)}{h(n)}=\Theta(1) \Longrightarrow T(n)=\Theta\left(h(n)\log_2 n\right)$

- exception $\dfrac{f(n)}{h(n)}=\Theta\left(\log^k n\right)\Longrightarrow T(n)=\Theta\left(h(n)\log^{k+1} n\right)$

  • 변수를 치환해서 마스터 정리를 적합시키기

- $T(n)=2T\left(\sqrt{n}\right)+\log_2 n$

- 위의 식에는 마스터 정리를 적용시키지 못한다($\sqrt{n}=n^{0.5}$과 $f(n)=\log_2 n$이 문제임, $n^1$ 이고 $f(n)$은 다항식이어야 한다)

- $m =\log_2 n \Longleftrightarrow 2^m=n$

- $T(2^m)=2T\left(2^{0.5m}\right)+m$

- $P(m) = T(2^m)$ 이라고 하자

- $P(m)=2P\left(\dfrac{1}{2}m\right)+m$ $\to$ 마스터 정리를 적용시킬 수 있다!

- 마스터 정리를 적용시키면 $P(m)=\Theta(m\log m)$이 되고 $m$을 다시 $\log_2 n$로 치환시키면 아래와 같다

- $T(n)=\Theta\left(\log n\log(\log n)\right)$