강의노트 24. [정렬] 자료구조 - merge sort (병합정렬)

|

패스트캠퍼스 컴퓨터공학 입문 수업을 듣고 중요한 내용을 정리했습니다. 개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.

merge sort (병합정렬)

  • 합병 정렬 또는 병합 정렬(merge sort)은 O(n log n) 비교 기반 정렬 알고리즘이다.
  • 합병 정렬은 다음과 같이 작동한다.
    1. 리스트의 길이가 0 또는 1이면 이미 정렬된 것으로 본다. 그렇지 않은 경우에는
    2. 정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분 리스트로 나눈다.
    3. 각 부분 리스트를 재귀적으로 합병 정렬을 이용해 정렬한다.
    4. 두 부분 리스트를 다시 하나의 정렬된 리스트로 합병한다.

merge sort 구현예시

mergesort_cnt = 0
merge_two_section_cnt = 0

def MergeTwoSection(unsorted_list, start, mid, end):
    global merge_two_section_cnt
    merge_two_section_cnt+=1
    print(start, end, "in MergeTwoSection cnt : ", merge_two_section_cnt)
    #정렬하고자 하는 리스트의 왼쪽 부분 index
    leftIdx = start
    #정렬하고자 하는 리스트의 오른쪽 부분 첫번째 idx
    rightIdx = mid + 1
    #정렬될 데이터의 수
    numOfData = (end - start) + 1

    #정렬된 데이터를 임시 저장할 리스트
    #데이터 개수만큼 초기화한다
    temp_list=[0 for i in range(numOfData+1)]

    #왼쪽 혹은 오른쪽 부분이 모두 temp_list에 저장되고 난 후
    #다른 쪽의 나머지를 temp_list에 담기 위한 idx
    tempIdx = 0

    #데이터의 총 개수 만큼 돌다가
    #왼쪽 부분이든 오른쪽 부분이든
    #한쪽이 모두 temp_list에 담기면 빠져나간다
    for i in range(numOfData):
        #양쪽 인덱스의 값을 비교하여 작은 수를 임시 리스트에 저장
        if unsorted_list[leftIdx] < unsorted_list[rightIdx]:
            temp_list[i] = unsorted_list[leftIdx]
            leftIdx+=1

            #leftIdx가 mid보다 커진다면
            #왼쪽 부분은 이미 모든 데이터가
            #임시 리스트에 들어간 상황
            #for문을 빠져나간다
            if leftIdx > mid:
                tempIdx = i+1
                break
        else:
            temp_list[i] = unsorted_list[rightIdx]
            rightIdx+=1

            if rightIdx > end:
                tempIdx = i + 1
                break


    #왼쪽 부분이 모두 임시 리스트에 올라간 상황이라면
    #오른쪽 부분의 나머지를 모두 임시 리스트에 올린다
    if leftIdx > mid:
        for i in range(rightIdx, end+1):
            temp_list[tempIdx] =unsorted_list[i]
            tempIdx+=1
    #오른쪽 부분이 모두 올라간 상황이면
    #왼쪽 부분의 나머지를 올린다
    elif rightIdx > end:
        for i in range(leftIdx, mid+1):
            temp_list[tempIdx] = unsorted_list[i]
            tempIdx+=1

    #정렬된 리스트 temp_list를
    #정렬이 안된 원본 리스트에 업데이트
    tempIdx = 0
    for i in range(start, end+1):
        unsorted_list[i] = temp_list[tempIdx]
        tempIdx+=1

    #임시 리스트는 지운다
    del temp_list

def mergesort(unsorted_list, start, end):
    #재귀함수 호출 순서를 알아보기 위한 테스트 코드
    global mergesort_cnt
    mergesort_cnt+=1
    print(start, end, "in mergesort cnt : ", mergesort_cnt)
    if start >= end:
        return

    mid = (start + end) // 2

    mergesort(unsorted_list, start, mid)
    mergesort(unsorted_list, mid+1, end)

    MergeTwoSection(unsorted_list, start, mid, end)

if __name__ == "__main__":
    unsorted = [8, 3, 7, 1, 2, 6, 4, 5]
    end = len(unsorted)-1
    mergesort(unsorted, 0, end)
    print(unsorted)

강의노트 23. [정렬] 자료구조 - quick sort (퀵소트)

|

패스트캠퍼스 컴퓨터공학 입문 수업을 듣고 중요한 내용을 정리했습니다. 개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.

quick sort (퀵소트)

  • 참고글
  • 수업자료
  • 현업에서 많이 사용되는 빠른 정렬 기법
  • 분할정복기법 (Divide and conquer)을 활용한다. (문제를 분할하여 해결, 재귀 함수를 이용)
  • 기준값을 하나 택하고 순회를 돌면서 기준값보다 작은 데이터는 좌측에, 기준값보다 큰 데이터는 우측으로 분할하고 다시 퀵 정렬을 적용한다.


퀵소트-버블소트 비교

quick sort 성능

  • 시간 복잡도에 대한 자료 - 추천!
  • 퀵 정렬의 평균 복잡도 : O(N log N)
  • 퀵 정렬의 최악의 경우 복잡도 : O(N^2)
  • 퀵 정렬의 복잡도는 다른 알고리즘 Heap Sort 또는 Merge Sort보다 뛰어난 것은 아니다.
  • 하지만 실제로 수행했을 때 다른 정렬 알고리즘보다도 빨라서 가장 많이 쓰이는 알고리즘이 되었다.
  • Quick sort는 정렬 알고리즘 중 성능이 좋다고 알려져 있고, worst case로 Big-O를 따지는 다른 경우와 다르게 average case로 계산한다.
  • (Quick sort의 BigO 를 구하는 방법은 상기 수업자료에도 자세히 설명되어 있음)

구현예시 1

# 파티션 (pivot)

def partition(data, start, end):
    pivot = data[start]

    idx_low = start + 1
    idx_high = end

    # low와 high가 교차해야한다. (중요)
    while idx_low <= idx_high:
        # low가 end보다 같거나 작고, low index에 해당하는 값이 pivot값 보다 작거나 같으면 low는 오른쪽으로 계속 이동
        while idx_low <= end and data[idx_low] <= pivot:
            idx_low += 1

        # high가 start+1 보다 같거나 크고, higt index에 해당하는 값이 pivot값 보다 크거나 같으면 high는 왼쪽으로 계속 이동
        while idx_high >= (start + 1) and data[idx_high] >= pivot:
            idx_high -= 1

        # low 값과 high 값을 교환
        if idx_low <= idx_high:
            data[idx_low], data[idx_high] = data[idx_high], data[idx_low]

    # low와 high가 교차하면 pivot과 high의 값을 교환
    data[start], data[idx_high] = data[idx_high], data[start]

    return idx_high

def quick_sort(data, start, end):
    if start >= end:
        return

    idx_pivot = partition(data, start, end)

    quick_sort(data, start, idx_pivot - 1)
    quick_sort(data, idx_pivot + 1, end)

if __name__ == '__main__':
    li = [2, 5, 4, 1, 8, 10, 5, 3, 6, 6, 5, 7, 9, 12, 11] # [1, 2, 3, 4, 5, 5, 5, 6, 6, 7, 8, 9, 10, 11, 12]

    quick_sort(li, 0, len(li) -1)

    print(li)

구현예시 2

  • 퀵소트의 성능 향상을 위해 pivot 값을 결정할 때, 맨 왼쪽, 가운데, 오른쪽 값을 비교해 중간 값을 pivot 으로 만든다.
#성능 비교를 위한 변수
call_of_partition = 0



#정렬된 리스트에서 맨 앞 값을 pivot으로 지정한다면
#예를 들어 1
#성능이 매우 안 좋을 수 있다
#그러므로 pivot 값을 정하는 함수를 따로 만들어
#pivot 값이 가장 작은 값이 되지 않도록 한다
def get_pivot(li, start, end):
    mid = (start + end)//2
    list_idx = [start, mid, end]
    #버블 정렬로 리스트에서의 값을 비교해
    #[가장 작은 값의 index, 가운데 값의 index, 가장 큰 값의 index]
    #순으로 정렬한다
    if li[list_idx[0]] > li[list_idx[1]]:
        list_idx[0], list_idx[1] = list_idx[1], list_idx[0]
    if li[list_idx[1]] > li[list_idx[2]]:
        list_idx[1], list_idx[2] = list_idx[2], list_idx[1]
    if li[list_idx[0]] > li[list_idx[1]]:
        list_idx[0], list_idx[1] = list_idx[1], list_idx[0]
    #정렬이 완료되면 가운데 idx의 값이 중간 값이므로 이를 반환해
    #pivot으로 정하도록 한다
    return list_idx[1]



def partition(li, start, end):
    #partition 함수가 몇 번 호출되었는가를 계산
    global call_of_partition
    call_of_partition += 1



    #pivot 값을 정한다
    idx_pivot = get_pivot(li, start, end)
    #pivot을 리스트의 start index에 두기 위해
    #리스트의 start 값과 pivot idx의 값을 교환한다
    li[start], li[idx_pivot] = li[idx_pivot], li[start]



    #pivot 여기서는 start index의 값을 의미한다
    pivot = li[start]
    #pivot 값을 기준으로 왼쪽에 낮은 값들이 존재할 수 있도록
    #low idx는 검색 도중 pivot보다 큰 값이 있으면
    #그 값을 가리킨다
    low = start + 1
    #pivot 값을 기준으로 오른쪽에 높은 값들이 존재할 수 있도록
    #high idx는 pivot 값보다 작은 값이 있으면
    #그 값을 기리킨다
    high = end

    #반드시 low와 high idx가 교차하게 만든다
    #그 전까지는 교환 규칙에 따라 계속 루프가 돌도록 한다
    while low <= high:
        #low idx는 pivot보다 큰 값을 가진 idx를 만나기 전까지
        #오른쪽으로 이동한다
        #대신 low가 end를 벗어나면 안된다
        while low <= end and li[low] <= pivot:
            low+=1

        #high idx는 pivot보다 작은 값을 가진 idx를 만나기 전까지
        #왼쪽으로 이동한다
        #대신 high가 start + 1을 벗어나면 안된다
        while high >= (start + 1) and li[high] >= pivot:
            high-=1

        #low와 high가 교차 전이라면 두 idx가 가리키는 값을 서로 바꾼다
        if low <= high:
            li[low], li[high] = li[high], li[low]

    #교차했다면 pivot의 idx와 high idx의 값을 서로 교환한다
    #high가 가리키는 idx가 정렬되었을 때의 pivot 값의 위치이기 때문
    li[start], li[high] = li[high], li[start]

    #최종적으로 pivot 값의 최종 idx를 반환
    return high

def quicksort(li, start, end):
    #재귀함수 이므로 탈출 조건이 필요하다
    if start >=end:
        return;

    #정렬된 pivot값의 idx를 가져온다
    idx_pivot = partition(li, start, end)

    #idx_pivot은 정렬이 된 상태 이므로 이를 기준으로
    #앞 부분과 뒷 부분을 quicksort를 통해(재귀함수) 계속 정렬해주면 된다
    quicksort(li, start, idx_pivot - 1)
    quicksort(li, idx_pivot + 1, end)


if __name__ == "__main__":
#     li = [2, 5, 4, 1, 8, 10, 5, 3, 6, 6, 5, 7, 9, 12, 11]

    #정렬된 리스트에서의 성능 평가를 위한 리스트
    li = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

    quicksort(li, 0, len(li)-1)

    print(li)
    print("call_of_partition : %d \n" % call_of_partition)

170420-0423_TIL

|

4월 23일 (일)

오늘 한 일 (회고)

  • Stack을 이용한 후위표기법 계산기 구현연습
  • django 인증부분 연습 (User Authentication)
  • codility 알고리즘 문제풀이
  • 미니 프로젝트를 함께 진행할 분을 찾아서 함께 스터디를 시작하기로 했다.

4월 22일 (토)

오늘 할 일 (계획)

오늘 한 일 (회고)

  • 생활코딩 Semantic UI 수업을 들었다.
    미니 프로젝트로 만들고 싶은 서비스가 생겼는데, UI 프레임워크로 bootstrap 대신에 사용해 볼까 생각중이다. (사실 시급한건 이것 보다 django 공부..)
  • 미니 프로젝트 아이디어 정리 (Mindnode 활용)
  • 4월부터 컴퓨터 공학 입문 수업을 듣기 시작하면서 복습하는데 대부분의 시간을 사용하고 있다. (그만큼 어렵다..ㅠㅠ) 요즘엔 겨우 지하철에서 AskDjango 강의 듣는게 전부인데, 일부러라도 시간을 내서 django로 코딩을 해야겠다는 생각이 든다.

내일 할 일

  • Stack을 이용한 후위표기법 계산기 구현연습 (꼭 하자!)
  • django 인증부분 연습하기 (User Authentication)
  • 영어공부 게시판 인증기능 개선 (next인자 추가, profile 모델 추가)

4월 21일 (금)

오늘 할 일 (계획)

  • 배열을 사용하여 heap 구현
  • Stack을 이용한 후위표기법 계산기 구현
  • tree 복습

오늘 한 일 (회고)

  • 컴퓨터 공학 입문수업 참여 (네트워크)
  • tree, heap 복습 및 구현 연습
  • python socket을 활용한 아주 간단한 채팅 server, client 구현

내일 할 일

  • 빈브라더스 커피공장 투어 (친구따라 인천까지 간다..)
  • Stack을 이용한 후위표기법 계산기 구현 (꼭 하자)
  • 생활코딩 Semantic UI 듣기

4월 20일 (목)

오늘 한 일 (회고)

  • 컴퓨터 공학 입문수업 참여
  • quick sort (퀵소트) - 강의노트 정리
  • bubble sort, insertion sort, selection sort 정리

내일 할 일

  • Stack을 이용한 후위표기법 계산기 구현 연습
  • tree
  • 배열을 사용하여 heap 구현해보기

강의노트 22. 자료구조 - tree(트리), heap(힙)

|

패스트캠퍼스 컴퓨터공학 입문 수업을 듣고 중요한 내용을 정리했습니다. 개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.

tree (트리)

  • 참고글
  • 그래프와 함께 비선형 자료구조 의 일종으로 계층 구조 를 갖고 있는 자료구조 (선형 자료구조 : 리스트, 스택, 큐)
  • 트리의 주 목적은 탐색
  • 이진 트리(binary tree) : 최대 2개의 자식 노드를 가질 수 있다.
    • 완전 이진트리 (complete binary tree)
    • 포화 이진트리 (full binary tree)
    • 사향 이진트리 (skewed binary tree)
  • 트리를 표현하는 용어
    • 차수(degree, 특정 노드의 자식 노드의 수)
    • 높이(height, 루트 노드에서 단말 노드까지의 깊이)
    • 레벨(level, 각 층의 번호-루트노드:레벨1)

complete binary tree (완전 이진트리)

  • 노드가 위에서 아래로, 왼쪽에서 오른쪽으로 채워진다.
  • 잎 노드 두개의 레벨 차가 1 이하이다.
  • heap 구현시 완전 이진트리를 기본으로 하며, 전통적으로 배열을 이용해 구현한다.
views
출처 : https://github.com/ythwork

heap (힙)

  • 참고글 - 최소힙에서의 추가, 삭제
  • 수업자료
  • 힙(heap)은 완전이진트리(Complete binary tree)를 기본으로 한 자료구조(tree-based structure) (시간복잡도 : O(log N))
  • 일반적으로 배열을 사용하여 구현한다.
  • 완전이진트리는 최댓값 및 최솟값을 찾아내는 연산을 빠르게 하기 위해 고안
  • 다음과 같은 힙 속성(property)을 만족한다.
    • A가 B의 부모노드(parent node) 이면, A의 키(key)값과 B의 키값 사이에는 대소관계가 성립한다.
views
출처 : https://github.com/ythwork

heap의 종류

  • 최대 힙 : 부모노드의 키값이 자식노드의 키값보다 항상 큰 힙
  • 최소 힙 : 부모노드의 키값이 자식노드의 키값보다 항상 작은 힙
  • 키값의 대소관계는 오로지 부모노드와 자식노드 간에만 성립하며, 특히 형제 사이에는 대소관계가 정해지지 않는다

heap 구현

index

  • One based index heap
  • 루트노드 index로 0이 아닌 1을 지정하여 구현을 좀 더 편하게 할 수 있다.
  • parent index = child index // 2
  • left child index = parent index * 2
  • right child index = paretn index * 2 + 1

insert() 구현

  1. 맨 마지막 노드에 새로운 데이터 추가 (완전 이진트리 유지 - 최고 깊이에서 가장 오른쪽에 노드 추가)
  2. 부모 노드와 비교, 최소 힙 인 경우 부모보다 신규 노드가 작다면(우선 순위가 높다면) 교환
  3. 반복하여 부모 노드와 비교
  4. 루트 노드에 도달했거나 (부모가 없음) 부모 노드가 더 작거나 같다면 멈춘다.

delete() 구현

  1. 루트 노드를 반환 (최소 힙에서 최솟값을 뽑아낸다는 건 루트 노드를 뽑아낸다는 것과 동일)
  2. 맨 마지막 노드를 루트 노드로 이동 (A)
  3. 두 자식 노드를 비교해 우선 순위가 높은 것을 선택 (최소 힙인 경우 작은 수) (B)
  4. A 와 선택된 자식노드 (B) 를 비교하여 우선순위가 높은 것을 부모노드로 이동
  5. 반복하여 자식 노드와 비교
  6. 터미널 노드에 도달했거나 (자식이 없음) 자식노드가 더 크거나 같다면 멈춘다.

배열을 사용한 heap 구현코드

class Heap:
    # 개체들이 공유하는 메소드는 클래스 메소드로 만들어도 된다.
    def get_parent_idx(self, idx):
        return idx // 2

    def get_left_child_idx(self, idx):
        return idx * 2

    def get_right_child_idx(self, idx):
        return idx * 2 + 1

    # 중요함수- 보통은 유저 프로그래머가 직접 구현, 인터페이스만 제공
    # 우선순위에 따라 반환값이 달라진다.
    def get_prior(self, value1, value2):
        '''
        value1 이 우선순위가 높으면 -1 리턴
        value2 가 우선순위가 높으면 1 리턴
        두 우선순위가 같다면 0을 리턴
        '''
        # min_max == 1 ==> 최소 힙
        if self.min_max == 1:
            if value1 < value2:
                return -1
            elif value1 > value2:
                return 1
            else:
                return 0
        else:  # self.min_max == 2 ==> 최대 힙
            if value1 > value2:
                return -1
            elif value1 < value2:
                return 1
            else:
                return 0

    def __init__(self, s_min_max = 'min'):
        self.dynamic_arr = [None, ] # one based index, 다른 언어는 가변배열을 사용한다. C++은 백터 사용, 효율 좋게 짜는 것이 중요(가변배열이 수정시마다 매번 탐색, 복사, 삭제를 하면 메모리 낭비)
        self.num_of_data = 0 # 마지막 데이터의 index

        if s_min_max == 'max':
            self.min_max = 2
        else:
            self.min_max = 1

    def is_empty(self):
        if self.num_of_data == 0:
            return True

        return False

    def size(self): # ADT에 없는 함수이지만 테스트용으로 만듬
        return self.num_of_data

    # insert 메서드에서 사용
    # 부모 값과 비교해서, 우선순위가 부모 보다 높으면 True 낮으면 False
    def is_go_up(self, idx, data):
        if idx <= 1:
            return False

        parent_value = self.dynamic_arr[self.get_parent_idx(idx)]

        if self.get_prior(parent_value, data) == 1: # 부모 우선순위가 작다면
            return True
        else:
            return False

    def insert(self, data):
        if self.is_empty():
            self.dynamic_arr.append(data)
            self.num_of_data += 1
            return

        self.dynamic_arr.append(data)
        self.num_of_data += 1

        idx_data = self.num_of_data

        while self.is_go_up(idx_data, data):
            parent_idx = self.get_parent_idx(idx_data)
            self.dynamic_arr[idx_data] = self.dynamic_arr[parent_idx]

            idx_data = parent_idx

        self.dynamic_arr[idx_data] = data

    # 우선순위가 높은 자식 노드의 인덱스를 반환
    def which_is_prior_child(self, idx):
        left_idx = self.get_left_child_idx(idx)

        if left_idx > self.num_of_data:
            return None
        elif left_idx == self.num_of_data:
            return left_idx

        right_idx = self.get_right_child_idx(idx)

        left_value = self.dynamic_arr[left_idx]
        right_value = self.dynamic_arr[right_idx]

        if self.get_prior(left_value, right_value) == -1:
            return left_idx
        else:
            return right_idx


    def is_go_down(self, idx, data):
        child_idx = self.which_is_prior_child(idx)

        if not child_idx:
            return False

        child_value = self.dynamic_arr[child_idx]

        if self.get_prior(child_value, data) == -1:
            return True
        else:
            return False

    def delete(self):
        if self.is_empty():
            return None

        ret_data = self.dynamic_arr[1]
        last_data = self.dynamic_arr[self.num_of_data]
        self.num_of_data -= 1

        idx_data = 1

        while self.is_go_down(idx_data, last_data):
            child_idx = self.which_is_prior_child(idx_data)
            self.dynamic_arr[idx_data] = self.dynamic_arr[child_idx]
            idx_data = child_idx

        self.dynamic_arr[idx_data] = last_data
        self.dynamic_arr.pop()

        return ret_data




if __name__ == "__main__":
    heap = Heap("min")
    heap.insert(3)
    heap.insert(5)
    heap.insert(1)
    heap.insert(10)
    heap.insert(8)
    heap.insert(7)
    heap.insert(4)
    heap.insert(5)
    heap.insert(2)
    heap.insert(6)
    heap.insert(9)

    for i in range(1, heap.size()+1):
        print(heap.dynamic_arr[i], end = '  ')
        # 1  2  3  5  6  7  4  10  5  8  9  

    print("\n")

    for i in range(1, heap.size()+1):
        print(heap.delete(), end = '  ')       
        # 1  2  3  4  5  5  6  7  8  9  10   

priority queue (우선순위 큐)

  • 일반적인 큐는 FIFO 자료구조 이기 때문에 먼저 들어온 데이터를 먼저 내보냈다면, 우선순위 큐는 우선순위가 높은 데이터를 먼저 내보내는 자료구조
  • 새로운 노드를 삽입하면 우선순위에 맞게 위치에 삽입 (Enqueue)
  • 제거를 할 때는 가장 우선 순위가 높은 맨 앞에 노드를 빼면서 삭제(Dequeue)
class PriorityQueue(Heap):
    enqueue = Heap.insert
    dequeue = Heap.delete

if __name__ == "__main__":
    pq = PriorityQueue("min")
    pq.enqueue(3)
    pq.enqueue(7)
    pq.enqueue(2)
    pq.enqueue(1)
    pq.enqueue(9)
    pq.enqueue(5)
    pq.enqueue(8)
    pq.enqueue(10)
    pq.enqueue(5)
    pq.enqueue(6)
    pq.enqueue(4)

    for i in range(1, pq.size()+1):
        print(pq.dynamic_arr[i], end = '  ')
        # 1  2  3  5  4  5  8  10  7  9  6  


    print("\n")

    for i in range(1, pq.size()+1):
        print(pq.dequeue(), end = '  ')
        # 1  2  3  4  5  5  6  7  8  9  10  

170419_TIL

|

오늘 한 일 (회고)

  • 블로그 홈 수정
    • TIL 메뉴 접는 방식으로 수정 (TIL을 시작하고 2달이 지나니 글 개수가 너무 많이졌다)
  • LinkedList 구현 연습
  • Queue 구현 (Node 활용)

내일 할 일

  • Stack을 이용한 후위표기법 계산기 구현 연습
  • tree
  • 배열을 사용하여 heap 구현해보기