자료구조란 컴퓨터가 데이터를 효율적으로 다룰 수 있게 도와주는 데이터 보관 방법과 데이터에 관한 연산의 총체를 뜻한다.
자료구조는 단순(primitive) 자료구조와 복합(non-primitive) 자료구조로 나뉜다.
단순 자료구조란 여러 프로그래밍 언어에서 기본적으로 지원하는 int, float, boolean 등의 자료형을 말한다. 원시 자료구조라고 부르기도 한다. 단순 자료구조는 대부분 더 이상 나눌 수 없는 원자적인 값들로 이루어져 있다.
자료형과 자료구조의 차이:
자료형이란 컴파일러 또는 인터프리터에게 프로그래머가 데이터를 어떻게 사용하는 지를 알려주는 일종의 데이터 속성(attribute)이다. 자료형은 자료구조에 비해 훨씬 더 구체적이며, 특정 언어의 자료형은 해당 언어의 원시적인 자료형(int, float, string 등) 뿐만 아니라 언어에서 지원하는 모든 자료형을 포함한다.
복합 자료구조란 단순 자료구조를 기반으로 만들어낸 배열, 스택, 트리 등과 같은 자료구조를 말한다. 복합 자료구조는 크게 선형(linear)과 비선형(non-linear)로 나뉜다.
선형 자료구조는 데이터 요소를 순차적으로 연결하는 자료구조 이며 데이터 간의 관계가 1:1 이다. 대표적인 종류는 다음과 같다.
비선형 자료구조는 데이터 요소를 비순차적으로 연결하며 데이터 간의 관계가 1:N 또는 N:M 등으로 복잡하게 연결되고 계층 구조를 갖는 자료구조이다. 대표적인 종류는 다음과 같다.
추상 자료형 또는 추상적 자료형이란 자료구조의 동작 방법을 표현하는 형식을 말한다. 이는 자료구조가 갖춰야 할 일련의 연산이라고 할 수 있다.
리스트의 예를 들면, 리스트는 데이터에 순차적으로 접근해서 그 데이터를 다룰 수 있는 기능을 제공해야 한다. 리스트의 특정 위치에 있는 노드에 접근(get)하거나, 리스트의 마지막에 데이터를 추가(append)하거나, 마지막에 있는 데이터를 삭제하거나(pop), 중간에 삽입(insert)하거나, 삭제(remove)하는 기능이 필요하다. 이러한 정의를 한 것이 추상 자료형이다.
그러나 추상 자료형은 전체적인 구조와 기능들에 대한 구체적인 구현 방법을 명시하지 않는다. 자료구조의 내부적인 구성이 어떻게 돼있는 지, 연산 작업은 어떻게 실행되는지, 연산에 대한 시간 복잡도 등 세부적인 사항을 다루지 않는다.
즉, 추상 자료형은 데이터의 구조와 동작에 대한 청사진을 제시하는 것이고 자료구조는 추상 자료형을 구체적으로 구현한 것이다.
리스트라는 추상 자료형을 구체적으로 구현한 자료구조가 바로 연결 리스트라고 볼 수 있다.
추상 자료형(ADT)과 대표적인 자료구조의 예시는 다음과 같다.
ADT | 자료구조 예시 |
---|---|
리스트 | (단순, 이중, 원형 등)연결 리스트 |
스택 | 배열 기반 스택, 연결 리스트 기반 스택 |
큐 | 환형 큐, 링크드 큐 |
트리 | 이진 트리, LCRS 트리, 레드-블랙 트리 |
그래프 | 방향 그래프, 무방향 그래프 |
힙 | 배열 기반 힙, 연결 리스트 기반 힙 |
대표적으로 사용되는 시간 복잡도의 점근 표기법으로 O(Big O, Big Oh, Big Omicron, 빅 오), Ω(Big Omega, 빅 오메가), Θ(Big Theta, 빅 세타) 가 있다.
빅 오(O) 표기는 알고리즘 수행 시간의 상한(upper bound)이다. 위 그림의 과 같은 어떤 복잡한 함수의 실행 시간이 주어질 때, 가장 늦게 실행된다면 이만큼 걸린다는 의미이다.
빅 오메가(Ω) 표기는 알고리즘 수행 시간의 하한(lower bound)이다. 위 그림의 이 가장 빨리 실행된다면 이만큼 걸린다는 의미이다.
빅 세타(Θ) 표기는 평균적인(tight bound) 알고리즘 수행 시간을 나타낸다. 집합으로 표현하면 빅 오 표기법과 빅 오메가 표기법의 교집합이라 할 수 있다.
위의 표기법은 시간 복잡도 표기법 of ~ 라고 읽는다. 예를 들어 는 big O of 1이라고 읽으면 된다.
빅 오가 최악의 경우의 실제 수행시간을 나타낸다고 오해하기 쉬우나 이는 잘못된 것이다. 빅 오는 수행 시간의 상한을 의미할 뿐 최악의 경우에는 항상 빅 오 만큼 시간이 걸린다는 의미는 아니다. 함수의 실제 수행시간과 점근 표기법은 별개로 생각해야 한다.
예를 들어 최악의 경우 시간 복잡도가 이다 라는 말은 최악의 경우에는 필요한 연산량이 이내라는 의미이다. 빅 오는 수행시간의 상한이므로 실제 수행시간이 이보다 작기만 하면 되기 때문에 최악의 경우 시간 복잡도가 라는 명제또한 참이 된다. 하지만 실제 수행시간과 의 오차보다 의 오차가 더 크므로 이러한 명제는 별 의미가 없다.
알고리즘의 수행 시간이 최선인 상황은 드물고, 평균적인 상황은 알아내기 어렵기 때문에 대개 알고리즘의 시간 복잡도는 빅 오 표기법으로 나타낸다. 코딩 문제 또한 시간이나 메모리 제한이 있기 때문에 이를 만족하는 빅 오를 고려하며 푸는 것이 좋다.
입력 크기가 충분히 커야(위 그래프에서 로 표시된 것) 점근 표기법대로 나오는 것을 확인할 수 있다. 예를 들어 1GB 정도 크기의 파일을 100Mbps 정도의 속도로 전송한다면 그냥 인터넷으로 보내면 되지만 페타바이트나 엑사바이트 급의 아주 큰 용량을 전송해야 한다면 파일이 담긴 디스크를 비행기로 수송하는 것이 더 효율적이다. 온라인 전송의 시간 복잡도는 파일의 크기에 비례하므로 이지만 비행기로 디스크를 수송하는 것의 시간 복잡도는 파일의 크기와 상관없이 거의 일정하므로 이기 때문이다.
점근 표기법으로 나타낸 알고리즘의 시간 복잡도는 몇 가지의 패턴을 갖는데, 효율적인 순서로 정리하면 다음과 같다.
이 순서는 점근 표기법의 종류에 상관없이 공통적으로 적용된다.
동적 프로그래밍 또는 동적 계획법은 크기가 가장 작은 부분 문제부터 해를 구해서 저장해 놓고, 이를 이용해서 더 큰 문제의 해를 점진적으로 만들어 가는 상향식 접근 방법이다.
DP라는 이름은 설계기법의 의미를 잘 반영하지 못하기 때문에 본질적인 의미를 살려서 기억하며 풀기라고 설명하기도 한다.
분할정복과 다르게 DP에선 작은 문제들이 서로 독립적일 필요가 없다. 따라서 초기 문제들의 해를 저장해두고 이를 다음 문제의 해를 구하는 데 활용할 수 있다.
동적 프로그래밍 방법은 주로 최적성의 원리(principle of optimality)가 성립하는 최적화 문제에 주로 사용된다. 최적성의 원리란 주어진 문제에 대한 최적해는 주어진 문제의 소문제에 대한 최적해로 구성된다는 원리이다.
즉, 동적 프로그래밍 방법을 적용하기에 앞서 문제에서 최적성의 원리가 성립하는지 먼저 증명하는 단계가 필요하다. 만약 어떤 문제가 최적성의 원리를 만족하면, 그 문제에 대한 최적해를 소문제에 대한 최적해의 형식으로 나타낼 수 있는 점화식을 만들 수 있다.
동적 프로그래밍 방법의 전체적인 처리 과정은 다음과 같다.
동적 프로그래밍의 대표적인 예시는 팩토리얼 연산, 피보나치 수열, 플로이드 알고리즘, 하노이의 탑, 연쇄 행렬 곱셈(Matrix chain multiplication), 최장 공통 부분 수열(Longest Common Subsequence) 등이 있다.
동적 프로그래밍의 핵심은 동일한 계산을 반복해야 할 경우 한 번 계산한 결과를 메모리에 저장해 두고 재사용하는 방식으로 중복 계산을 방지하는 것이다. 이를 메모이제이션(Memoization)이라고 하며 캐싱(caching)과 비슷한 개념이다.
메모이제이션과 비슷하지만 값을 미리 계산하는 기법을 타뷸레이션(Tabulation)이라고 한다.
메모이제이션은 값이 필요할 때 연산이 이루어지는 느긋한 연산(lazy evaluation)이고, 타뷸레이션은 필요하지 않은 값도 미리 연산을 해놓는 즉시 연산(eager evaluation)이라 할 수 있다.
두 기법을 비교하면 타뷸레이션의 초기화 오버헤드가 더 크지만, 일반적으로 시간 복잡도는 메모이제이션보다 타뷸레이션이 더 우수하다.
반드시 동적 프로그래밍으로 풀어야 하는 문제가 아닌 경우에도 시간이나 메모리의 제한으로 인해 메모이제이션이나 타뷸레이션 같은 동적 프로그래밍 기법이 필요할 때가 있다.