알고리즘

선택 알고리즘

엘리펀튼는코끼리 2021. 2. 9. 16:47

t1. 평균 선형 시간 선택 알고리즘

i번째 원소를 찾는 알고리즘은 앞에서 퀵소트의 파티션을 이용한다. 아래는 그 과정을 나타낸 그림이다.

파티션을 하면 기준원소가 몇번째에 있는지는 확실히 알 수 있다. 이를 이용해서 i번째 원소를 찾는 것이다. 이에 대한 알고리즘은 아래와 같다.

아래는 직접 구현해본 코드이다. 

def partition(A, p, r):
    standard = A[r]
    part_1 = p
    
    for part_3 in range(p, r):
        if A[part_3] <= standard:
            temp = A[part_1]
            A[part_1] = A[part_3]
            A[part_3] = temp
            part_1 += 1
    
    temp = A[r]
    A[r] = A[part_1]
    A[part_1] = temp
    
    return part_1

def select(A, p, r, i):
#select ith element in sorted array A[p,,,r]
    #if there's only one element in array A, then return that element
    if p == r:
        return A[p]
    
    q = partition(A, p, r)
    
    #k : kth element in sorted array A
    k = q-p+1
    
    #if k>i implement the function again until k=i
    if k > i:
        return select(A, p, q-1, i)
    
    #if k=i then return that element
    elif i == k:
        return A[q]
    
    #if k<i then implement the function again start with q+1,
    #we already found kth element so find (i-k)th element 
    else:
        return select(A, q+1, r, i-k)

이 알고리즘의 수행시간은 T(n)≤max[T(k-1), T(n-k)] + Θ(n)이다. max[T(k-1), T(n-k)]는 파티션이후 기준원소가 k번째이라는 것을 알았을 때 k<i이면 앞부분에서 다시 찾아야 함으로 T(k-1)이 걸리고, k>i이면 뒷부분에서 다시 찾아야 함으로 T(n-k)가 걸리게 된다. 그리고 상한선을 찾음으로 max를 이용해 둘중 나쁜 경우를 찾으면 된다. 또한 Θ(n)은 partition할때 걸리는 시간이다. 하지만 이것을 정리하면 T(n) = O(n)이라는 것이 나온다. 그 증명은 아래와 같다 .

위에서 T(n)=O(n)이고 하한선은 Ω(n)임으로 T(n) = Θ(n)이다. 그냥 정렬하고 선택을 하면 아무리 잘해도 Θ(nlogn)이나오는데 이 알고리즘대로라면 Θ(n)으로 시간을 줄 일 수 있는 것이다. 다만 최악의 경우는 조금 다른데 만약 partition이후가 계속 한쪽으로 몰린다면 시간 복잡도는 T(n) = T(n-1) + Θ(n)이 나와서 T(n) = Θ(n^2)이 나오게 된다. 또한 partition이후의 균형이 자주 깨져도  T(n) = Θ(n^2)가 나오게 된다. 

 

2. Medians of medians

이 알고리즘은 위에서 균형이 깨지는 경우를 어느정도 커버를 해준다. 알고리즘은 아래와 같다.

위에서 5번의 경우를 그림으로 나타내면 아래와 같다.

이렇게 중앙값의 중앙값을 찾은 다음 그 값을 partition의 standard element로 이용해 나머지 초록색을 제자리로 옮기는 방식이다. 이를 직접 구현한 코드는 아래와 같다. 

#sort the given list and return the position of median
def sort_and_get_median(data, start, end):
    data[start:end+1] = sorted(data[start:end+1])
    return (start+end)//2
##################################################################
def choose_pivot(data, start, end):
    if end-start<5:
        return sort_and_get_median(data, start, end)
    
    #find the median and move that in front of list
    cur = start
    for i in range(start, end+1, 5):
        median_loc = sort_and_get_median(data, i, min(i+4, end))
        data[cur] , data[median_loc] = data[median_loc], data[cur]
        cur += 1
    
    #now the median elements are placed from 0 to cur
    return quickselect_pos(data, start, cur-1, (cur+start-1)//2)
##################################################################
#return pivot position's right location
def partition(data, start, end, pivot_pos):
    data[end], data[pivot_pos] = data[pivot_pos], data[end]
    standard = data[end]
    part_1 = start
    
    for part_3 in range(start, end+1):
        if data[part_3]<standard:
            temp = data[part_1]
            data[part_1] = data[part_3]
            data[part_3] = temp
            part_1 += 1
            
    temp = data[part_1]
    data[part_1] = data[end]
    data[end] = temp
    
    return part_1
###################################################################
#return selected position not value
def quickselect_pos(data, start, end, k):
    if start == end:
      return start
    
    pivot = choose_pivot(data, start, end)
    partition_ = partition(data, start, end, pivot)
    
    if partition_ == k:
        return partition_
    elif partition_ > k:
        return quickselect_pos(data, start, partition_-1, k)
    elif partition_ < k:
        return quickselect_pos(data, partition_+1, end, k)
##################################################################   
#return selected kth value of the list
def quickselect(data, start, end, k):
    if start == end:
        return data[start]
    
    pivot = choose_pivot(data, start, end)
    
    partition_ = partition(data, start, end, pivot)

    if partition_ == k:
        return data[partition_]
    elif partition_ > k:
        return quickselect(data, start, partition_-1, k)
    elif partition_ < k:
        return quickselect(data, partition_+1, end, k)
    

이 알고리즘의 최악의 경우를 생각하면 다음과 같다. 중간값의 중간값(M)을 구한다음 partition을 이용하여 그 중간값의 위치를 얻으려고 한다. 어차피 파란색과 빨간색 동그라미는 위치가 정해져있지만 초록색이 정확한 위치가 안정해져 M의 오른쪽 또는 왼쪽으로 이동하는데 이때 모든 초록색이 M의 한쪽(여기서 왼쪽이라하자)으로 이동하는 경우이다. 이런 경우에 M보다 큰 원소는 아래의 이유로 이다. 

그렇다면 나머지 M보다 작은 원소는 전체 n에서 을 뺀 이다. 그러므로 최악의 경우에도 의 비율을 유지하는 것이다. 이렇게 비율이 유지되면 선형시간 알고리즘이 된다.

각 단계의 시간 복잡도를 살펴보자. 1단계에서는 Θ(1)이다. 2단계에서는 5개의 그룹으로 묶는데 n개의 원소를 돌면서 함으로 Θ(n)이다. 3단계에서는 n/5개의 그룹을 돌면서 중앙값을 찾음으로 Θ(n)이고 5단계는 partition임으로 Θ(n)이다. 4단계와 6단계는 자기 호출인데 4단계는 n/5개의 그룹을 가지고 selection함으로 T(n/5)이고 6단계는 적합한쪽이 최악의 경우라고 생각하면 T(7n/10+2)이다. 그러므로 시간복잡도는 아래와 같이 나타낼 수 있다.

이에 대한 증명은 아래와 같다.

따라서 이방법으로 최악의 경우에도 Θ(n)을 유지할 수 있다.