본문 바로가기

[내일배움캠프]/TIL

22.11.14)알고리즘을 심화학습한 11일차

#오늘배운 것

 

정렬 - 데이터를 순서대로 나열하는 방법 .이진 탐색을 가능 및데이터를 좀 더 효율적으로 탐색할 수 있게 한다.

버블정렬 : 첫 번째 자료와 두 번째 자료를, 두 번째 자료와 세 번째 자료를, 세 번째와 네 번째를, … 이런 식으로 (마지막-1)번째 자료와 마지막 자료를 비교하여 교환하면서 자료를 정렬하는 방식.

원소와 원소를 교환한다고 생각하면 된다.gif를 보면 좀 더 쉽게 이해할 수 있다.

※swap하는 방법

a, b = b, a

>>> a = 3
>>> b = 4
>>> a, b = b, a
>>> print(a)
4
>>> print(b)
3

선택정렬 - 말 그대로 선택해서 정렬한다. 전체적으로 훑어보고 작은숫자와 앞에 있는 숫자를 교체하는 방식.

버블 정렬보다 훨씬 더 적은 비교를 하는 것 같지만, 실제로는 각 배열을 계속해서 탐색하는 방식이라 2중 반복문을 사용해야 한다.

선택정렬 예시

input = [4, 6, 2, 9, 1]

#배열의 크기만큼 반복했다가 앞에서부터 한개씩 줄어들면서 반복하는 구조.
def selection_sort(array):
    n = len(array)
    #최솟값을 찾아 변경하기 때문에 인덱스변수를 하나 생성
    for i in range(n - 1): #맨 마지막에 남은 원소는 비교하지 않는다. 그래서 전체길이만큼 반복X
        min_ind = i
        for j in range(n - i): #하나씩 줄어드는 방식
            if array[i + j] < array[min_ind]:
                min_ind = i + j
        #내부 반복문이 끝나면 i와 min_ind교체한다.
        array[i], array[min_ind] = array[min_ind],  array[i]
    return
    #시간복잡도 : 반복문을 2번 썼기 때문에 O[N^2]만큼 걸린다.

selection_sort(input)
print(input) # [1, 2, 4, 6, 9] 가 되어야 합니다!

삽입 정렬 - 선택 정렬이 전체에서 최솟값을 "선택" 하는 거 였다면, 삽입 정렬은 전체에서 하나씩 올바른 위치에 "삽입" 하는 방식이다. 선택 정렬은 현재 데이터의 상태와 상관없이 항상 비교하고 위치를 바꾸지만, 삽입 정렬은 필요할 때만 위치를 변경하므로 더 효율적인 방식이다.

시간복잡도 : 선택정렬과 버블정렬은 반복문을 2번 썼기 때문에 O[N^2]만큼 걸린다.
삽입정렬의 경우 break문이 있으면 안의 반복문은 멈추게 되므로 N의 시간이 걸리는 것이다.

 

병합 정렬 - 배열의 앞부분과 뒷부분의 두 그룹으로 나누어 각각 정렬한 후 병합하는 작업을 반복하는 알고리즘

예를 들어서 A 라고 하는 배열이 1, 2, 3, 5 로 정렬되어 있고, B 라고 하는 배열이 4, 6, 7, 8 로 정렬되어 있다면 이 두 집합을 합쳐가면서 정렬하는 방법입니다. = 두집합을 합쳐서 숫자대로 정렬한다. 만약 A배열의 숫자가 끝날 경우 하나씩 C에 추가해준다.

정렬방법

array_a = [1, 2, 3, 5]
array_b = [4, 6, 7, 8]


def merge(array1, array2):
    array_c = []
    #인덱스를 저장한 변수
    array1_index = 0
    array2_index = 0
    #array1_index가 끝나기 전까지~array길이보다 작을 때
    while array1_index < len(array1) and array2_index < len(array2):
        if array1[array1_index] < array2[array2_index]:
            array_c.append(array1[array1_index])
            array1_index+=1
        else:
            array_c.append(array2[array2_index])
            array2_index+=1
    #남은 코드들을 한꺼번에 넣어주는 코드
    if array1_index == len(array1):
        while array2_index < len(array2):
            array_c.append(array2[array2_index])
            array2_index += 1
    if array2_index == len(array2):
        while array1_index < len(array1):
            array_c.append(array1[array1_index])
            array1_index += 1
    return array_c
#1. 두 개의 배열을 합치기 위해서는 각 배열의 값들을 하나씩 비교
#2. 더 작은 값을 새로운 배열에 하나씩 추가해갑니다.
#3. 그 후 한 배열이 끝난다면 남은 배열의 모든 값을 새로운 배열에 추가.
# >result 라는 새로운 배열을 만든 다음 while 문을 이용해서 비교 이후에 값을 추가

print(merge(array_a, array_b)

 

병합정렬 - 분할정복의 개념을 적용한다.

분할정복 :

문제를 작은 2개의 문제로 분리하고 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결한다. 리스트의 원소들을 각각 분할해서 합칠 때 정렬시킨다고 생각하면 된다.(merge함수로 array 인덱스를 따라가 정렬한다)

재귀함수형식으로 정의해 본다면 [MergeSort(시작점, 끝점)]

따로 탈출 조건이 필요하다.ex) 문자열의 길이가 1보다 작거나 같을 때

                  #1,2,3,5                  #4,6,7,8
MergeSort(0, N) = Merge(MergeSort(0, N/2) + MergeSort(N/2, N))
#merge의 반 반이 합쳐지면서 정렬되는 병합과정을 한다면 재귀함수형식으로 될수잇다.
#0부터 N까지 정렬한 걸 보기 위해서는 
0부터 N/2 까지 정렬한 것과 N/2부터 N까지 정렬한 걸 합치면 된다.
#예시
def merge_sort(array):
    if len(array) <= 1:
        return array
    mid = (0 + len(array)) // 2
    left_array = merge_sort(array[:mid])   # 왼쪽 부분을 정렬하고
    right_array = merge_sort(array[mid:])  # 오른쪽 부분을 정렬한 다음에
    merge(left_array, right_array)         # 합치면서 정렬하면 됩니다!
merge함수의 경우 arr1,arr2를 다 볼때까지가 반복의 기준이다.그래서 arr1와 arr2만큼 시간복잡도가 걸린다.
arr1,arr2,는 결국 arr를 반으로 쪼갠것이기 때문에 N(0)을 시간을 가진다.
margesort함수의 경우 arr가 N이라고 가정했을 때 각각 쪼갠 것을 N/2 2개를 비교하면서 합친다.
그래서 N/2 * 2로 계산해 N만큼 걸린다. 탈출조건을 봤을 떄 K단계를 거치면 N의 길이가반씩 쪼개지면서 1이된다.
이것을 수식으로 나타내면 N/^2K = 1 > K = log2N이다
즉 K단계만큼 반복하는데 각 단계는 0(N)의 시간복잡도를 가진다. log2N * 0(N) > 0(NlogN)

 

스택 - 한쪽 끝으로만 자료를 넣고 뺄 수 있는 자료 구조

last In First Out( LIFO) :늦게들어간 자료가 빨리나오고 먼저들어간 자료가 늦게나온다.

컴퓨터의 ctrl+z 되돌리기를 생각하면 된다.

스택 자료구조 메소드들 : 

push(data) : 맨 앞에 데이터 넣기
     data라는 인자를 받는다.
pop() : 맨 앞의 데이터 뽑기
     인자가 없는 이유: 맨위의 데이터만 뽑기 떄문
peek() : 맨 앞의 데이터 보기
isEmpty(): 스택이 비었는지 안 비었는지 여부 반환해주기
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None


class Stack:
    def __init__(self):
        self.head = None

    def push(self, value):                 # 현재 [4] 밖에 없다면
        new_head = Node(value)             # [3] 을 만들고!
        new_head.next = self.head          # [3] -> [4] 로 만든다음에
        self.head = new_head               # 현재 head의 값을 [3] 으로 바꿔준다.

    # pop 기능 구현
    def pop(self):
        if self.is_empty():                  # 만약 비어있다면 에러!
            return "Stack is empty!"
        delete_head = self.head              # 제거할 node 를 변수에 잡습니다.
        self.head = self.head.next           # 그리고 head 를 현재 head 의 다음 걸로 잡으면 됩니다.
        return delete_head                   # 그리고 제거할 node 반환

    def peek(self):
        if self.is_empty():
            return "stack is empty"
        return self.head.data

    # isEmpty 기능 구현
    def is_empty(self):
        return self.head is None

※스택 실전 사용법 : 실제 코드에서는 파이썬의 list 를 이용해서 스택으로 사용

스택을 빈 리스트에 넣어주면 append함수로 데이터을 쌓고 top으로 pop처럼 맨 위 데이터를 꺼낸다.

range(a,b,c) a = range(시작하는 점, 끝나는 점, 연산을 하나씩 줄여갈 건가)

스택의 경우 링크드리스트로 유사하게 구현이 가능하다.

스택큐 - 들어가고 나오는 곳이 정해져있는 자료구조

 

큐 - 한쪽 끝으로 자료를 넣고, 반대쪽에서는 자료를 뺄 수 있는 선형구조.

데이터를 한쪽 끝으로 넣고, 반대쪽에서 빠져 나온다.

First In First Out (FIFO): 가장 처음에 들어간 데이터는 빨리 나오고, 가장 마지막에 들어간 데이터는 가장 늦게 나온다.

순서대로 처리되어야 하는 일에 필요하다.데이터 넣고 뽑는 걸 자주하는 자료 구조로 구현한다.

스택과 마찬가지로 링크드리스트 형식으로 구현한다.

다만 큐는 끝과 시작의 노드를 전부 가지고 있어야 하므로 self.head 와 self.tail 을 가진다.

자료구조 메소드

enqueue(data) : 맨 뒤에 데이터 추가하기
dequeue() : 맨 앞의 데이터 뽑기(빠지는건 맨앞에서 빠지는 넣는건 맨 뒤에서 넣는다)
peek() : 맨 앞의 데이터 보기
isEmpty() : 큐가 비었는지 안 비었는지 여부 반환해주기

예시

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None


class Queue:
    def __init__(self):
        self.head = None
        self.tail = None

    def enqueue(self, value):
        #아무것도 넣지 않았다면 None이 tail이자 head이니 오류가 발생한다.
        # #if문으로 처리한다.
        if self.is_empty():
            self.head = new_node  # head 에 new_node를
            self.tail = new_node  # tail 에 new_node를 넣어준다.
            return
        new_node = Node(self, value)
        self.tail.next = new_node
        self.tail = new_node
        return

    def dequeue(self):
        if self.is_empty():
            return "Queue is empty!" 
        delete_head = self.head  # 제거할 node 를 변수에 잡습니다.
        self.head = self.head.next # 그리고 head 를 현재 head 의 다음 걸로 잡으면 됩니다.
        return delete_head.data    

    def peek(self):
        if self.is_empty():
            return "Queue is empty!"
        return self.head.data

    def is_empty(self):
        return self.list.head is None

해쉬테이블 - 데이터의 검색과 저장이 아주 빠르게 진행된다. 딕셔너리를 해쉬 테이블이라고 부르기도 한다.

해쉬 - 알고리즘을 통해 문자열을 고정된 길이의 데이터로 만든다.효율적인 자료구조인 딕셔너리를 만들때 사용

딕셔너리는 내부적으로는 배열을 사용한다.딕셔너리 내부의 배열이란 저장소에서 빠른과 느린이 저장되어 있는데 그 인덱스를 어떻게 찾아야할까? = 해쉬함수를 이용한다.

해쉬 함수 : 임의의 길이를 갖는 메시지를 입력하여 고정된 길이의 해쉬값을 출력하는 함수.파이썬에서 제공하고있다.

계산하는 법 :배열의 길이로 나눈 나머지 값 - hash(key) % len(self.items)

dict = {"fast" : "빠른", "slow": "느린"}

>>items[hash("fast") % 8] = "빠른"
형식으로 인덱스를 저장등록하는 것도 있다.
시간복잡도의 경우 상수만큼 계산속도가 난다. 0(1)!

>>items[hash("fast") % 8] = 데이터를 찾아서 출력한다.
class Dict:
    def __init__(self):
        self.items = [None] * 8

    #put = 딕셔너리에 새로운 key와 value를 추가해준다.
    def put(self, key, value):
        index = hash(key) % len(self.items)
        self.items[index] = value
        return

    def get(self, key):
        index = hash(key) % len(self.items)
        self.items= index
        return
#임의의 문자열이 해쉬 함수에 나오더라도 인덱스값이 동일해질 때가 있다.
#인덱스 값이 같으면 똑같은 배열 위치에 value값을 업데이트 시켜 이전 값을 덮어 씌워버릴 수도 있다.
#해결방법 1. 링크드리스트 사용. 테스트를 추가함으로써 노드로 이어준다. 
# 다만 키값도 같이 저장해줘야함. 추후에 찾을 때 어떤 키를 저장했는지 알아야 한다.

수정방법

class LinkedTuple:
    def __init__(self):
        self.items = [None] * 8
    #add는 키와 값을 동시에 받음.각각의 튜플을 추가하는 방식.
    def add(self, key, value):
        self.items.append((key,value)) #새로운 튜플 추가
    #튜플 내 값을 찾기위해 get추가
    def get(self, key):
        for k, v in self.items:
            if key == k:
                return v
            
class LinkedDict:
    def __init__(self):
        self.items = [] # 안에 LinkedTuple()이 반복되고 있다.
        for i in range(8):
            self.items.append(LinkedTuple())
    
    def put(self, key, value):
        #키를 가지고 인덱스를 찾아야 한다.
        index = hash(key) % len(self.items)
        #LinkedTuple()이 빈값에 들어가 있으니 새로운 키와 값을 추가하면 된다,
        self.items[index].add(key,value)
        return
    
    def get(self, key):
        index = hash(key) % len(self.items)
        self.items[index].get(key)
        return
#체이닝방식이다. 각 배열마다 링크드리스트를 연결해 놓은 방식이다.
#그리고 키가 동일하다면 아이템 뒤 아이템으로 만들어놓고, 
# 키가 도달한다면 그 키에대한 링크드 리스트를 조회해서 값으로 반환해준다.

#해결방법 2. 개방주소법 : 배열의 다음 남는 공간에 값을 넣는 것이다.