Algorithm 소팅

Categories: Tags:


서론

기본적으로 소팅의 원리와 동작은 이해했고 구현에 대한 부분만 연습할 겸 적어봤습니다.

참조

요약

구분 시간복잡도 공간복잡도 안정성 비고
선택 정렬 n^2 제자리 정렬 불안정
삽입 정렬 n^2 제자리 정렬 안정 어느 정도 정렬된 상태에서 효율성 좋음
버블 정렬 n^2 제자리 정렬 안정
셸 정렬 n^1.5 제자리 정렬 불안정 삽입정렬은 발전시킨 정렬
병합 정렬 nlogn 정렬된 리스트를 담을 공간이 필요 O(n) 안정
퀵 정렬 nlogn 제자리 정렬 불안정 nlogn 정렬 중 가장 빠름
힙 정렬 nlogn 정렬된 리스트를 담을 공간이 필요 O(n) 불안정 가장 큰 값 몇개만 필요할때 효율적

선택 정렬 (Selection)

def swap(a, b):
    nlist[a], nlist[b] = nlist[b], nlist[a]


def selection_sort(nlist):
    # 처음부터 끝까지 돌면서
    for i in range(len(nlist)):
        minidx = i
        # 정렬된곳 빼고 최솟값을 찾아서
        for j in range(i, len(nlist)):
            if nlist[j] < nlist[minidx]:
                minidx = j
        # 바꿉니다.
        swap(i, minidx)
    return nlist

삽입 정렬 (Insertion)

def insertion_sort(nlist):
    # 두번째 원소부터 끝까지 돌면서
    for i in range(1, len(nlist)):
        # 앞의 원소들이랑 비교하면서
        for j in range(i, 0, -1):
            if nlist[j] < nlist[j-1]:
                # 바꿉니다.
                swap(j, j-1)
            # 앞의 것보다 안작을 때 브레이크를 걸어줘야 더 이상 확인안하기 때문에
            # 최적의 경우 n이 나올 수 있음 (어느정도 정렬된 리스트는 효과가 좋다)
            else:
                break
    return nlist

버블 정렬 (Bubble)

def bubble_sort(nlist):
    # 처음부터 끝까지 돌면서
    for i in range(len(nlist)):
        # 정렬된곳 빼고 뒤로 가면서 한번씩 비교하면서
        for j in range(len(nlist)-i-1):
            if nlist[j+1] < nlist[j]:
                # 바꿉니다.
                swap(j+1, j)
    return nlist

셸 정렬 (Shell)

def shell_insertion_sort(start, gap):
    # 시작+gap부터 원소부터 끝까지 돌면서
    for i in range(start+gap, len(nlist), gap):
        # 한gap앞의 원소들이랑 비교하면서
        for j in range(i, start, -gap):
            if nlist[j] < nlist[j-gap]:
                # 바꿉니다.
                swap(j, j-gap)
            # 앞의 것보다 안작을 때 브레이크를 걸어줘야 더 이상 확인안하기 때문에
            # 최적의 경우 n이 나올 수 있음 (어느정도 정렬된 리스트는 효과가 좋다)
            else:
                break


def shell_sort(nlist):
    # 삽입정렬을 발전시킨 정렬로 삽입정렬을 gap이 1이었다.
    # 여기선 gap을 리스트 길이의 절반씩 나눠가며 진행한다.
    gap = len(nlist)//2
    # 갭을 절반씩 줄여나가면서 정렬할것임.
    while gap > 0:
        # gap만큼 띄워서 부분리스트를 만들면 부분리스트 갯수는 gap개가 됨
        for i in range(gap):
            # 그 부분리스트들을 삽입정렬함
            # print(nlist[i::gap])
            shell_insertion_sort(i, gap)
            # print(nlist[i::gap])
        gap = gap//2
    return nlist

병합 정렬 (Merge)

def merge_sort(nlist, left, right):
    # left,right 포함
    # 분할정복을 사용하기 때문에 end조건
    if left == right:
        return
    mid = (left+right)//2
    merge_sort(nlist, left, mid)
    merge_sort(nlist, mid+1, right)

    sorted = [0]*len(nlist)
    idx = left
    ls = left
    le = mid
    rs = mid+1
    re = right

    # 왼쪽 파트와 오른쪽 파트를 작은 거부터 순서대로 넣음
    while ls <= le and rs <= re:
        if nlist[ls] <= nlist[rs]:
            sorted[idx] = nlist[ls]
            ls += 1
        else:
            sorted[idx] = nlist[rs]
            rs += 1
        idx += 1

    # 한쪽 파트가 다넣어졌을때 남은 파트를 넣는 부분
    if ls <= le:
        for i in range(ls, le+1):
            sorted[idx] = nlist[i]
            idx += 1
    if rs <= re:
        for i in range(rs, re+1):
            sorted[idx] = nlist[i]
            idx += 1
    for i in range(left, right+1):
        nlist[i] = sorted[i]

    return nlist

퀵 정렬 (Quick)

def quick_sort(nlist, left, right):
    # left,right 포함
    # 분할정복을 사용하기 때문에 end조건
    if left >= right:
        return

    pvt = left
    ls = left
    rs = right
    # 피벗을 기준으로 왼쪽에는 작은숫자 오른쪽에는 큰숫자를 남김
    while ls <= rs:
        while ls <= right and nlist[ls] <= nlist[pvt]:
            ls += 1
        while rs > left and nlist[rs] >= nlist[pvt]:
            rs -= 1
        # 왼쪽포인터와 오른쪽포인터가 교차하지 않았으면 바꿀게 있단 뜻
        if ls <= rs:
            swap(nlist, ls, rs)
        # 교차했으면 이제 왼쪽엔 작은것만 남고 오른쪽엔 작은것만 남았단 뜻
        # 피벗과 rs를 바꿔준다.
        else:
            swap(nlist, pvt, rs)
    quick_sort(nlist, left, rs-1)
    quick_sort(nlist, rs+1, right)

    return nlist

테스트 결과

nlist = random.sample(range(1, 1001), 1000)
start = time.time()
selection_sort(nlist)
print(f'selection_sort\tn=1000\ttime: {time.time()-start:.5f}')
nlist = random.sample(range(1, 10001), 10000)
start = time.time()
selection_sort(nlist)
print(f'selection_sort\tn=10000\ttime: {time.time()-start:.5f}')
selection_sort  n=1000  time: 0.02601
selection_sort  n=10000 time: 2.44855

insertion_sort  n=1000  time: 0.05555
insertion_sort  n=10000 time: 5.34221

bubble_sort     n=1000  time: 0.06702
bubble_sort     n=10000 time: 6.97594

shell_sort      n=1000  time: 0.00400
shell_sort      n=10000 time: 0.06703

merge_sort      n=1000  time: 0.00400
merge_sort      n=10000 time: 0.12603

quick_sort      n=1000  time: 0.00200
quick_sort      n=10000 time: 0.02400

리스트의 길이가 10배가 되었을 때 n^2 시간 복잡도인 정렬들은 시간이 약 100배가 되는 것을 확인할 수 있습니다. shell 정렬의 경우 n^1.5보다 성능이 좋게 나왔고 merge 정렬은 nlogn, quick 정렬도 nlogn보다 성능이 좋게 나온 것을 볼 수 있습니다.

생각

제자리 정렬을 구현하기 위해선 신경을 좀 써야 합니다. 리스트 슬라이싱을 사용하면 훨씬 쉽게 풀 수 있지만 메모리가 복사되기 때문에 제자리 정렬이 아니게 됩니다. 소팅에대해서도 공부했는데 분할정복에서 인덱스를 다루면서 재귀를 어떻게 부르는지에 대해서 많이 생각해 볼 수 있었습니다.


댓글남기기