자료구조는 데이터가 어떤 구조로 저장되고 어떻게 사용되는지를 나타낸다.
개발자는 프로그램을 작성할 때 먼저 자료구조를 선택해 데이터를 어떻게 저장하고 사용할지 결정하고, 이에 맞는 알고리즘을 통해 데이터를 가공하고 원하는 결과를 얻는 과정을 거치게 된다. 그렇기 때문에 상황에 맞는 적절한 자료구조를 택하고 이에 맞는 적절한 알고리즘을 적용할 수 있는 능력이 필요하다.
어떠한 문제를 해결하기 위해서 사용할 수 있는 알고리즘은 여러 종류가 있다. 그렇기 때문에 더 좋은 알고리즘을 선택해야 하는데, 그렇다면 좋은 알고리즘은 무엇일까?
좋은 알고리즘은 사용자의 요구에 따라 다를 수 있다. 메모리를 적게 사용하는 방향, 속도가 빠른 방향 등 사용자에 따라 중요시하는게 다르지만 일반적으로는 알고리즘의 속도를 성능의 척도로 사용한다.
성능의 척도를 나타내기 위해서는 시간 복잡도를 사용할 수 있다. 시간 복잡도는 특정 알고리즘이 어떤 문제를 해결하는데 걸리는 시간을 말한다. 하지만 시간을 측정해서 알고리즘을 평가하기엔 사용자마다 컴퓨터 사양이 다르기 때문에, 코드에서 성능에 많은 영향을 주는 부분을 찾아 실행시간을 예측한다.
그렇다면 코드에서 많은 영향을 주는 부분은 어떤 것일까?
반복문이다. 반복문이 여러번 반복될수록 실행시간이 길어진다. 따라서 특정 알고리즘의 성능을 평가할 때는 해당 알고리즘의 반복문을 분석하여 시간 복잡도를 추정하는 것이 일반적이다.
알고리즘의 복잡도를 표현하기 위해서는 여러 표기법을 사용할 수 있다.
빅 오(Big O) 표기법은 알고리즘의 최악의 경우를 표현한다.
빅 오메가 (Big Ω) 표기법은 최선의 경우를 표기한다.
빅 세타 (Big Θ) 표기법은 평균의 경우를 표기한다.
주로 빅 오 표기법을 사용하며, 빅 오 표기법의 특징에는 아래와 같은 것들이 있다.
3n^2 + 4n + 5
라면, 이를 O(n^2)
으로 표현합니다.배열을 선언할 때에는 배열의 크기를 같이 알려주어야 한다.
int arr[10] = {1, 2, 3, 4, 5};
운영체제는 메모리에서 숫자 10개가 들어갈 수 있는 연속된 빈 공간을 찾아 순서대로 1, 2, 3, 4, 5를 할당한다.
운영체제는 배열의 시작 주소를 기억한다. 배열의 접근은 배열의 길이에 상관없이 한 번에 가져오기 때문에 O(1)의 성능을 가진다.
배열은 읽기와 쓰기 즉, 참조에서 좋은 성능을 보인다.
하지만 데이터의 삽입, 삭제의 성능은 좋지 않다.
배열을 선언할 때 운영체제는 미리 확보한 메모리 공간에 쓰레기 값을 덮어 씌우면서 할당된 공간을 차지한다.
이때, 프로그래머가 배열의 크기를 넘어서 할당하게 된다면 운영체제는 크기가 15인 연속된 메모리 공간을 다시 찾아서 할당하고, 기존의 1부터 10까지의 데이터를 복사해서 재할당 해주어야 한다.
자바스크립트에서의 배열은 상황에 따라 연속적, 불연속적으로 할당하며 불연속적인 경우 내부적으로 연결되어 있다.
배열 | 연결리스트 | |
---|---|---|
크기 | 고정 | 동적 |
주소 | 연속 | 불연속 |
데이터 참조 | O(1) | O(n) |
삽입과 삭제 | O(n) | O(n) |
어떠한 데이터와 그 데이터에 대한 연산
printAll()
clear()
insertAt(index, data)
insertLast(data)
deleteAt(index)
deleteLast()
getNodeAt(index)
// LinkedList.mjs
class Node {
// data는 필수값이지만 next는 입력하지 않는다면 null 할당
constructor(data, next = null) {
this.data = data;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.count = 0;
}
printAll() {
let currentNode = this.head;
let text = '[';
while (currentNode !== null) {
text += currentNode.data;
currentNode = currentNode.next;
if (currentNode !== null) {
text += ',';
}
}
text += ']';
console.log(text);
}
clear() {
this.head = null;
this.count = 0;
}
insertAt(index, data) {
if (index > this.count || index < 0) {
throw new Error('범위를 넘어갔습니다.');
}
let newNode = new Node(data);
if (index == 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let currentNode = this.head;
for (let i = 0; i < index - 1; i++) {
currentNode = currentNode.next;
}
newNode.next = currentNode.next;
currentNode.next = newNode;
}
this.count++;
}
insertLast(data) {
this.insertAt(this.count, data);
}
deleteAt(index) {
if (index >= this.count || index < 0) {
throw new Error('제거할 수 없습니다.');
}
let currentNode = this.head;
if (index == 0) {
let deleteNode = this.head;
this.head = this.head.next;
this.count--;
return deleteNode;
} else {
for (let i = 0; i < index - 1; i++) {
currentNode = currentNode.next;
}
let deleteNode = currentNode.next;
currentNode.next = currentNode.next.next;
this.count--;
return deleteNode;
}
}
deleteLast() {
return this.deleteAt(this.count - 1);
}
getNodeAt(index) {
if (index >= this.count || index < 0) {
throw new Error('범위를 넘어갔습니다.');
}
let currentNode = this.head;
for (let i = 0; i < index; i++) {
currentNode = currentNode.next;
}
return currentNode;
}
}
export { Node, LinkedList };
스택은 Last In, First Out (LIFO) 규칙을 가지고 있는 리스트이다. 즉, 먼저 들어간 데이터가 나중에 나오는 규칙이다.
실행 취소 기능, 문자열을 역순으로 정렬하는 기능, 괄호 검사 등의 사용 사례를 가지고 있다.
push()
pop()
peek()
isEmpty()
import { LinkedList } from './LinkedList.mjs';
class Stack {
constructor() {
this.list = new LinkedList();
}
push(data) {
this.list.insertAt(0, data);
}
pop() {
try {
return this.list.deleteAt(0);
} catch (e) {
return null;
}
}
peek() {
return this.list.getNodeAt(0);
}
isEmpty() {
return this.list.count == 0;
}
}
export { Stack };
큐는 리스트로 First In, Last Out 동작을 가능하게 하는 자료구조이다.
운영체제에서도 큐가 쓰이는데, 운영체제는 프로세스의 작업 요청을 요청이 들어온 순서대로 큐에 넣고 CPU가 이를 순서대로 꺼내 처리한다.
이를 운영체제에서는 FIFO 스케줄링이라고 한다.
// 기존 단방향 연결 상태였던 LinkedList를 이중연결리스트로 변경하여 이전 노드를 참조할 수 있도록 수정
class Node {
constructor(data, next = null, prev = null) {
this.data = data;
this.next = next;
this.prev = prev;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.count = 0;
}
printAll() {
let currentNode = this.head;
let text = '[';
while (currentNode != null) {
text += currentNode.data;
currentNode = currentNode.next;
if (currentNode != null) {
text += ', ';
}
}
text += ']';
console.log(text);
}
clear() {
this.head = null;
this.count = 0;
}
insertAt(index, data) {
if (index > this.count || index < 0) {
throw new Error('범위를 넘어갔습니다.');
}
let newNode = new Node(data);
if (index == 0) {
newNode.next = this.head;
if (this.head != null) {
this.head.prev = newNode;
}
this.head = newNode;
} else if (index == this.count) {
newNode.next = null;
newNode.prev = this.tail;
this.tail.next = newNode;
} else {
let currentNode = this.head;
for (let i = 0; i < index - 1; i++) {
currentNode = currentNode.next;
}
newNode.next = currentNode.next;
newNode.prev = currentNode;
currentNode.next = newNode;
newNode.next.prev = newNode;
}
if (newNode.next == null) {
this.tail = newNode;
}
this.count++;
}
insertLast(data) {
this.insertAt(this.count, data);
}
deleteAt(index) {
if (index >= this.count || index < 0) {
throw new Error('제거할 수 없습니다.');
}
let currentNode = this.head;
if (index == 0) {
let deletedNode = this.head;
if (this.head.next == null) {
this.head = null;
this.tail = null;
} else {
this.head = this.head.next;
this.head.prev = null;
}
this.count--;
return deletedNode;
} else if (index == this.count - 1) {
let deletedNode = this.tail;
this.tail.prev.next = null;
this.tail = this.tail.prev;
this.count--;
return deletedNode;
} else {
for (let i = 0; i < index - 1; i++) {
currentNode = currentNode.next;
}
let deletedNode = currentNode.next;
currentNode.next = currentNode.next.next;
currentNode.next.prev = currentNode;
this.count--;
return deletedNode;
}
}
deleteLast() {
return this.deleteAt(this.count - 1);
}
getNodeAt(index) {
if (index >= this.count || index < 0) {
throw new Error('범위를 넘어갔습니다.');
}
let currentNode = this.head;
for (let i = 0; i < index; i++) {
currentNode = currentNode.next;
}
return currentNode;
}
}
export { Node, DoublyLinkedList };
enqueue()
dequeue()
front()
isEmpty()
import { DoublyLinkedList } from './DoublyLinkedList.mjs';
class Queue {
constructor() {
this.list = new DoublyLinkedList();
}
enqueue(data) {
this.list.insertAt(0, data);
}
dequeue() {
try {
return this.list.deleteLast();
} catch (e) {
return null;
}
}
front() {
return this.list.tail;
}
isEmpty() {
return this.list.count == 0;
}
}
export { Queue };
"Double-Ended Queue"의 약자로, 양쪽 끝에서 항목의 추가와 제거가 모두 가능한 자료구조이다. 큐와 스택의 특성을 합친 것 처럼 볼 수 있다.
printAll()
addFirst()
removeFrist()
addLast()
removeLast()
isEmpty()
import { DoublyLinkedList } from './DoublyLinkedList.mjs';
class Deque {
constructor() {
this.list = new DoublyLinkedList();
}
printAll() {
this.list.printAll();
}
addFirst(data) {
this.list.insertAt(0, data);
}
removeFirst() {
return this.list.deleteAt(0);
}
addLast(data) {
this.list.insertAt(this.list.count, data);
}
removeLast() {
return this.list.deleteLast();
}
isEmpty() {
return this.list.count == 0;
}
}
export { Deque };