자료구조와 알고리즘

자료구조란? 데이터가 있고, 그 데이터에 대해 행할 수 있는 연산들로 이루어진 구조

알고리즘이란? 주어진 문제의 해결을 위한 자료구조와 연산 방법에 대한 선택


선형 배열

배열이란? 원소들을 순서대로 늘어놓은 것

 

원소 삽입

 

L = [ 1, 3, 5, 7, 9 ]
index 0 1 2 3 4

 

L.insert(3,6) 적용 시

 

3번 인덱스의 앞을 우선 찾아내게 되고,

맨 뒤의 수를 오른쪽으로 한 칸 옮기게 된다. (이 과정에서 배열의 크기가 하나 늘어나 크기가 6인 배열이 된다.)
그 후 7을 또 오른쪽으로 한 칸 옮기고
마지막으로 6을 삽입함

 

결과

 

L = [ 1, 3, 5, 6, 7, 9 ]
index 0 1 2 3 4 5


원소 삭제

 

L = [ 1, 3, 5, 6, 7, 9 ]
index 0 1 2 3 4 5

 

del(L[2]) 적용 시

 

2번 인덱스에 있던 5를 삭제하고,

오른편에 있던 6과 7과 9를 한 칸씩 옮긴다.

그 후 맨마지막 5번 인덱스에 있던 9를 옮긴다. (이 과정에서 배열의 크기가 하나 줄어 크기가 5인 배열이 된다.)

 

결과

 

L = [ 1, 3, 6, 7, 9 ]
index 0 1 2 3 4

 

del()과 pop()은 비슷한 동작을 시행하지만,

del()은 시행한 후 빠져나간 원소의 값이 반환되지 않고,

pop()은 시행한 후 빠져나간 원소의 값이 반환된다는 차이를 보인다.

 

원소 삽입과 삭제는 리스트의 길이가 길면 비례해서 연산이 오래걸린다. (선형 시간)

=> O(n)만큼의 시간이 걸린다.


정렬

수 정렬

 

L = [ 5, 1, 3, 7, 2 ]

 

sorted(L) 적용 시

 

L = [ 5, 1, 3, 7, 2 ]

 

sorted(L)은 리스트 자체에 변화를 주지 않는다.

 

L.sort() 적용 시

 

L = [ 1, 2, 3, 5, 7 ]

 

L.sort()는 L 리스트 자체에 변화를 준다. (바로 적용이 된다.)

 

L = sorted(L, reverse=True) OR L.sort(reverse=True) 적용 시

 

L = [ 7, 5, 3, 2, 1]


문자열 정렬

 

L = [ 'abcd', 'xyz', 'spam']

 

위 리스트를 그냥 정렬하게 되면 사전순으로 정렬하게 된다.

그렇다면 문자열 길이별로 정렬하려면 어떻게 하여야 할까?

 

키를 활용한다.

sorted(L, key = lambda x : len(x)) 적용 시

 

L = [ 'xyz, 'abcd', 'spam' ]


탐색

 

탐색 알고리즘 (1) - 선형 탐색

 

리스트에 비례하는 시간 소요

=> O(n)


 

탐색 알고리즘 (2) - 이진 탐색

 

한 번 비교할 때마다 리스트를 반씩 줄인다. (divide & conquer)

=> O(log n)

 

이진 탐색 코드 구현

 

lower = 0
upper = len(L) - 1
while lower <= upper:
    middle = (lower + upper)//2 #중간 인덱스 찾기
    if L[middle] == target:
    	return middle
    elif L[middle] < target: #중간값이 target값보다 작을 경우
    	lower = middle+1 #중간 인덱스보다 1만큼 큰 값으로 lower값 조정
    else: #중간값이 target값보다 클 경우
    	upper = middle-1 #중간 인덱스보다 1만큼 작은 값으로 upper값 조정

재귀 알고리즘

재귀함수(Recursive function)란? 하나의 함수에서 자신을 다시 호출해 작업을 수행하는 것

재귀함수 설계 시, 종결 조건에 유의하여야 한다.

 

재귀 알고리즘의 효율

def sum(n):
    if n<=1:
    	return n
    else:
    	return n+sum(n-1)

=> O(n)

 

vs

 

def sum(n):
    s = 0
    while n>=0:
    	s+=n
        n-=1
    return s

=> O(n)

 

vs

 

def sum(n):
    return n*(n+1)//2

=> O(1)

 

※ 효율성 측면에 유의하여 알고리즘을 잘 설계해야 한다! 


피보나치 순열

 

재귀함수를 활용한 경우

def solution(x):
    if x==0:
        return 0
    elif x==1:
        return 1
    else:
        return solution(x-1) + solution(x-2)

 

피보나치 순열 재귀함수 테스트 결과

반복적 함수를 활용한 경우

def solution(x):
    a, b = 0, 1
    
    if x==0:
        return 0
    
    for i in range(x-1):
        a,b = b, a+b
            
    return b

피보나치 순열 반복적 함수 테스트 결과

※ 꼭 재귀라고 효율성이 좋은 것만은 아니다. 문제에 따라 적절한 자료구조를 선택해야 한다는 좋은 본보기가 되어준다.


재귀적 이진 탐색

 

def binsearch(L, x, lower, upper):
    if l>u:
        return -1
    mid = (lower + upper) // 2
    if x==L[mid]:
        return mid
    elif x<L[mid]:
        return solution(L,x,l,mid-1)
    else:
        return solution(L,x,mid+1,u)

알고리즘의 복잡도

☆ 시간 복잡도란? 문제의 크기와 이를 해결하는 데 걸리는 시간 사이의 관계

  • 평균 시간 복잡도 : 임의의 입력 패턴(랜덤한 패턴)을 가정했을 때 소요되는 시간의 평균
  • 최악 시간 복잡도 : 가장 긴 시간을 소요하게 만드는 입력에 따라 소요되는 시간 (이를 기준으로 알고리즘의 복잡도를 계산한다.)

공간 복잡도란? 문제의 크기와 이를 해결하는 데 필요한 메모리 공간 사이의 관계

 

Big-O Notation이란? 점근 표기법의 하나. 알고리즘의 복잡도를 표현할 때 흔히 쓰인다.

  • 선형 시간 알고리즘 - O(n)
  • 로그 시간 알고리즘 - O(log n)
    • ex) 이진 탐색 알고리즘
  • 이차 시간 알고리즘 - O(n²)
    • ex) 삽입 정렬

 

삽입 정렬보다 낮은 복잡도를 가지는 정렬 알고리즘

병합정렬의 예


연결 리스트 (Linked List)

추상적 자료구조 

  • Data
    • 정수, 문자열, 레코드, ...
  • A set of operations
    • 삽입, 삭제, 순회, ...
    • 정렬, 탐색, ...

연결 리스트 구현

class Node:

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


class LinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = None
        self.tail = None


    def getAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            return None

        i = 1
        curr = self.head
        while i < pos:
            curr = curr.next
            i += 1

        return curr


    def insertAt(self, pos, newNode):
        if pos < 1 or pos > self.nodeCount + 1:
            return False

        if pos == 1:
            newNode.next = self.head
            self.head = newNode

        else:
            if pos == self.nodeCount + 1:
                prev = self.tail
            else:
                prev = self.getAt(pos - 1)
            newNode.next = prev.next
            prev.next = newNode

        if pos == self.nodeCount + 1:
            self.tail = newNode

        self.nodeCount += 1
        return True


    def popAt(self, pos):
        delete_node = self.getAt(pos)

        if pos < 1 or pos > self.nodeCount:  
            raise IndexError 

        if pos == 1:    
            if self.nodeCount == 1:    
                self.head=None
                self.tail=None
                
            else:                          
                self.head = self.head.next
            
        else:                          
            prev = self.getAt(pos - 1)
            prev.next = delete_node.next
            if pos == self.nodeCount:   
                prev.next = None
                self.tail = prev
                
        self.nodeCount -= 1
        return delete_node.data

    def traverse(self):
        result = []
        curr = self.head
        while curr is not None:
            result.append(curr.data)
            curr = curr.next
        return result

조금 변형된 연결 리스트 (Dummy head 추가)

그림 출처 : http://cslabserver2.cs.mtsu.edu/labs/2170/lab10/dummy.html

dummy head가 추가된 링크드 리스트

class Node:

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


class LinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = None
        self.head.next = self.tail


    def traverse(self):
        result = []
        curr = self.head
        while curr.next:
            curr = curr.next
            result.append(curr.data)
        return result


    def getAt(self, pos):
        if pos < 0 or pos > self.nodeCount:
            return None

        i = 0
        curr = self.head
        while i < pos:
            curr = curr.next
            i += 1

        return curr


    def insertAfter(self, prev, newNode):
        newNode.next = prev.next
        if prev.next is None:
            self.tail = newNode
        prev.next = newNode
        self.nodeCount += 1
        return True


    def insertAt(self, pos, newNode):
        if pos < 1 or pos > self.nodeCount + 1:
            return False

        if pos != 1 and pos == self.nodeCount + 1:
            prev = self.tail
        else:
            prev = self.getAt(pos - 1)
        return self.insertAfter(prev, newNode)


    def popAfter(self, prev):
        curr = prev.next
        
        if prev.next == None:
            return None
        
        if curr.next == None:
            self.tail = prev
        
        prev.next = curr.next
        self.nodeCount-=1 
        
        return curr.data


    def popAt(self, pos):
        if pos < 1 or pos > self.nodeCount :
            raise IndexError
            
        prev = self.getAt(pos-1)
        
        return self.popAfter(prev)
    
    def concat(self, L):
        self.tail.next = L.head.next
        if L.tail:
            self.tail = L.tail
        self.nodeCount+=L.nodeCount

한 쪽으로만 진행이 가능하다는 단점이 있다. 이를 보완한 것이...


양방향 연결 리스트(Doubly Linked Lists)

양방향 연결 리스트란? 앞으로도 뒤로도 즉, 양쪽으로 진행이 가능한 리스트

그림 출처 : https://www.studytonight.com/data-structures/doubly-linked-list

doubly linked list

head와 tail에 dummy node를 달아준다.

class Node:

    def __init__(self, item):
        self.data = item
        self.prev = None
        self.next = None


class DoublyLinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = Node(None)
        self.head.prev = None
        self.head.next = self.tail
        self.tail.prev = self.head
        self.tail.next = None


    def traverse(self):
        result = []
        curr = self.head
        while curr.next.next:
            curr = curr.next
            result.append(curr.data)
        return result


    def getAt(self, pos):
        if pos < 0 or pos > self.nodeCount:
            return None

        if pos > self.nodeCount // 2:
            i = 0
            curr = self.tail
            while i < self.nodeCount - pos + 1:
                curr = curr.prev
                i += 1
        else:
            i = 0
            curr = self.head
            while i < pos:
                curr = curr.next
                i += 1

        return curr


    def insertAfter(self, prev, newNode):
        next = prev.next
        newNode.prev = prev
        newNode.next = next
        prev.next = newNode
        next.prev = newNode
        self.nodeCount += 1
        return True


    def insertAt(self, pos, newNode):
        if pos < 1 or pos > self.nodeCount + 1:
            return False

        prev = self.getAt(pos - 1)
        return self.insertAfter(prev, newNode)
        
    def insertBefore(self, next, newNode):
        prev = next.prev
        newNode.next = prev.next
        newNode.prev = prev
        prev.next = newNode
        next.prev = newNode
        self.nodeCount+=1
        return True


    def popAfter(self, prev):
        curr = prev.next
        next = curr.next
        prev.next = next
        next.prev = prev
        self.nodeCount-=1
        
        return curr.data


    def popBefore(self, next): #수정필요
        curr = next.prev
        prev = curr.prev
        prev.next = next
        next.prev = prev
        self.nodeCount-=1
        
        return curr.data


    def popAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            raise IndexError
            
        prev = self.getAt(pos-1)
        next = self.getAt(pos)
        
        return self.popAfter(prev)
        #return self.popBefore(next)
        
    def concat(self, L):
        self.tail.prev.next = L.head.next
        L.head.next.prev = self.tail.prev
        self.tail = L.tail
        self.nodeCount += L.nodeCount

스택

스택이란? 자료를 보관할 수 있는 구조

 

한 쪽 끝에서 밀어 넣어야 하고 

=> push

같은 쪽에서 뽑아 꺼내야 한다.

=> pop

 

※ LIFO구조 (Last in First out)

그림 출처 :  https://swexpertacademy.com/

스택 구조 gif

 

스택 연산

  • size() - 현재 스택에 들어 있는 데이터 원소 수
  • isEmpty() - 현재 스택이 비어 있는지 판단
  • push(x) - 데이터 원소 x를 스택에 추가
  • pop() - 스택의 맨 위 저장된 데이터 원소 제거 및 반환
  • peek() - 스택의 맨 위 저장된 데이터 원소 반환 (제거하지 않음)

배열을 이용해 구현

 

class ArrayStack:
    def __init__(self):
        self.data = []
    def size(self):
        return len(self.data)
    def isEmpty(self):
        return self.size() == 0
    def push(self, item):
        self.data.append(item)
    def pop(self):
        return self.data.pop()
    def peek(self):
        return self.data[-1]

연결 리스트를 이용해 구현

 

from doublylinkedlist import Node
from doublylinkedlist import DoublyLinkedList

class LinkedListStack
    def __init__(self):
        self.data = DoublyLinkedList()
    
    def size(self):
        return self.data.getLength()
        
    def isEmpty(self):
        return self.size()==0
        
    def push(self, item):
        node = Node(item)
        self.data.insertAt(self.size() + 1, node)
        
    def pop(self):
        return self.data.popAt(self.size())
        
    def peek(self):
        return self.data.getAt(self.size()).data   

'AI > KDT 인공지능' 카테고리의 다른 글

[04/26] 인공지능 수학 - 선형 시스템  (0) 2021.04.26
[4/26] 주피터 노트북  (0) 2021.04.26
[4/22] 코딩테스트 연습 (2)  (0) 2021.04.22
[4/21] 코딩테스트 연습  (0) 2021.04.21
[4/21] 자료구조와 알고리즘(2)  (0) 2021.04.21

+ Recent posts