§ Array
§ Linked List
§ Binary Tree, Binary Search Tree, Red-Black Tree (레드블랙트리는 개념만!)
§ Heap
§ Hash Table, Set, Map
§ Stack
§ Queue
§ Trie
§ Graph (both directed and undirected)
논리적 설계 (데이터 모델링)
물리적 설계 (데이터 구조화)
선형 자료구조 (Linear)
- 하나의 자료 뒤에 하나의 자료가 존재하는 것이다.
비선형 자료구조 (NonLinear)
- 하나의 자료 뒤에 여러개의 자료가 존재할 수 있는 것이다.
- 자료들 간의 앞뒤 관계가 1:n, 또는 n:n 의 관계
- 트리와 그래프가 대표적이며 계층적 구조를 나타내기에 적절하다.
시간 복잡도?
- '얼마나 빠르게 실행되느냐'
- 알고리즘의 로직을 코드로 구현할 때, 시간 복잡도를 고려한다는 것은 '입력 값의 변화에 따라 연산을 실행 할 때, 연산 횟수에 비해 시간이 얼만큼 걸리는가?'라는 말
효율적인 알고리즘을 구현한다는 것 == 입력 값이 커짐에 따라, 증가하는 시간의 비율을 최소화하는 알고리즘을 구성했다는 것
시간 복잡도는 주로 빅-오 표기법을 사용해서 나타낸다.
빅-오 표기법은 최악의 셩우를 고려 하여 , 프로그램이 실행되는 과정에서 소요되는 최악의 시간까지 고려할 수 있기 때문에 많이 사용된다.
Big-O 표기법 종류
공간 복잡도
- '얼마나 많은 자원이 필요한가?'
=> 좋은 알고리즘? - '시간도 적게 걸리고 자원의 사용도 적어야 하는 것.'
하지만, 시간과 공간은 반비례적인 경향이 있기 때문에 알고리즘의 척도는 시간 복잡도를 위주로 판단합니다.
그러니까 시간 복잡도만 괜찮다면 공간 복잡도는 어느 정도 이해를 해준다는 것! 특히나 요즘과 같은 대용량 시대에는!
- 선형 자료구조 (Array, Linked List, stack, queue)
장점: index를 통한 원소 접근 용이
단점: 삽입, 삭제등에 대해 cost높다.
활용:
코드 예시:
시간복잡도: operation별로 나오는게 맞음! -> O(1)
공간 복잡도:
- 단방향 연결 리스트: 마지막 포인터는 null값을 가리키다.
- 양방향 연결 리스트: 데이터를 삭제할 때나 리스트를 양 방향으로 순회할 때 효율적
- 순환 연결 리스트: 마지막 노드는 첫 번째 노드와 연결 (버퍼링-다양한 처리를 원활하게 실행시키려고 버퍼라는 임시 저장 공간에 데이터를 저장하는 작업)에 많이 쓰임
장점: Array 삽입/삭제 비효율성 극복
(모두 노드가 하나 이상인 경우를 기본으로 함)
단방향 연결 리스트
1. head에 새 노드 삽입: O(1) (루프가 필요하지 않음)
2. tail에 새 노드 삽입: O(1) (루프가 필요하지 않음)
3. 중간에 삽입: O(n) - 먼저 n번째 자리를 찾고, 중간에 넣을 것을 n - 1 번째께 가르키도록 해야 하기 때문에 루프가 한번 돈다.
4. 삭제
- head node 삭제: O(1)
- tail node 삭제: O(1)
- 중간꺼 삭제: O(n)
5. 탐색: O(n) - 찾고자 하는 노드가 있다면 head부터 순차적으로 찾아감.
양방향 연결 리스트
순환 연결 리스트
새 node가 기존 node를 가리키도록 하고, 새 node를 head로 지정합니다.
tail node가 새 node를 가리키도록 함.
1. head에 추가: O(n) - 위치를 찾아가는 작업이 최대니까
2. tail에 추가: O(n)
3. 중간에 추가: O(n)
4. head 삭제: O(n)
5. tail 삭제: O(n)
6. 중간 삭제: O(n)
장점: 쉽게 시각화
단점: 특정 요소를 검색하는 속도를 제한한다.
활용: 역추적이나 문자열을 반전시키는 응용 프로그램 (미로찾기, 괄요 유효성 체크), 함수호출, 스케줄링, 인터럽트 메커니즘 등 다양한 기본 컴퓨닝 프로세스에서 사용
코드 예시:
시간복잡도:
공간 복잡도:
class Stack {
constructor() {
this.arr = [];
}
push(item) {
this.arr.push(item);
}
pop() {
return this.arr.pop();
}
peek() {
return this.arr[this.arr.length - 1];
}
}
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
stack.pop(); // 3
stack.peek(); // 2
stack.peek(); // 2
삽입: 맨 위에 있는거 찾을 때 O(1)
특정 데이터를 찾을 때 O(n)
삭제: O(1)
탐색: O(n)
장점:
단점:
활용: 작업 우선순위, Heap
코드 예시: 순서대로 처리해야하는 작업을 임시로 저장해두는 버퍼로 많이 사용된다.
시간복잡도:
공간 복잡도:
class Queue {
constructor() {
this._arr = [];
}
enqueue(item) {
this._arr.push(item);
}
dequeue() {
return this._arr.shift();
}
}
const queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.dequeue(); // 1
삽입: O(1)
삭제: O(1)
탐색: O(n)
비선형 자료구조
데이터를 삽입, 삭제한다는 것 이전에 표현이 중요.
node: 트리를 구성하고있는 원소 그 자체 (key-value기본임? 아니면 이진 탐색에서만 그런거임 ㅡㅡ )
edge: node와 node를 연결하는 선
root(node): tree에서 최상위 노드
terminal(leaf node): tree에서 최하위 노드
internal(node): leaf node를 제외한 모든 노드
node에는 data를 저장하며, 이때 저장된 데이터를 식별하는데 사용되는 key와 value를 포함할 수 있다.(key-value구조)
tree를 탐색하는 과정을 '순회(traversal)'이라고 한다.
장점:
단점:
활용: 작업 우선순위, Heap
코드 예시:
시간복잡도: O(logn)
공간 복잡도:
장점:
단점:
활용:
코드 예시:
시간복잡도: O(logn)
공간 복잡도:
이진 트리 데이터 구조의 한 종류
값이 최대 혹은 최소인 노드에 빠르게 접근해야하는 응용 프로그램에 적합.
완전 이진 트리의 일종으로, '우선순위 큐'를 위해 만들어진 자료구조입니다.
앞에서 먼저 말한 큐는 힙을 사용해서 구현할 수 있으며, 힙의 구조를 설계하ㅡㄴ데는 2가지 방법이 있음. - 최대 힙, 최소 힙
탐색: O(1)
삽입: O(logn)
1. 힙에 새로운 노드가 들어오면 일단 힙의 마지막 노드에 이어서 삽입한다.
제거: O(logn)
힙에서의 부모 노드와 자식 노드의 관계
왼쪽 자식의 인덱스 = (부모의 인덱스) 2
오른쪽 자식의 인덱스 = (부모의 인덱스) 2 + 1
부모의 인덱스 = (자식의 인덱스) / 2
priority queue
- 가장 우선순위가 높은 데이터
- 우선순위 큐는 Array, Linked List, Heap으로 구현 가능한데, heap으로 구현하는 것이 가장 효율적이다.
- 우선 순위 Queue를 위해 만들어진 자료구조이다.
- 배열을 이용해서 구현할 수 있다.
class Heap {
constructor() {
this.heap = []
}
// heap 부모-자식 관계
getLeftChildIndex = (parentIndex) => parentIndex * 2 + 1
getRightChildIndex = (parentIndex) => parentIndex * 2 + 2
getParentIndex = (childIndex) => Math.floor((childIndex - 1) / 2)
heapifyUp = () => {
let index = this.heap.length - 1 // 계속해서 변하는 index 값
const lastInsertedNode = this.heap[index]
// 루트노드가 되기 전까지
while (index > 0) {
const parentIndex = this.getParentIndex(index)
// 부모 노드의 key 값이 마지막에 삽입된 노드의 키 값 보다 크다면
// 부모의 자리를 계속해서 아래로 내린다.
if (this.heap[parentIndex].key > lastInsertedNode.key) {
this.heap[index] = this.heap[parentIndex]
index = parentIndex
} else break
}
// break 를 만나서 자신의 자리를 찾은 상황
// 마지막에 찾아진 곳이 가장 나중에 들어온 노드가 들어갈 자리다.
this.heap[index] = lastInsertedNode
}
insert = (key, value) => { // 우선순위를 비교하기 위해서 key, value 로 받는다.
const node = { key, value } // 객체로 node 를 만들고
this.heap.push(node) // push 한다.
this.heapifyUp() // 배열에 가장 끝에 넣고, 다시 min heap 의 형태를 갖추도록 한다.
}
heapifyDown = () => {
let index = 0
const count = this.heap.length
const rootNode = this.heap[index]
// 계속해서 left child 가 있을 때 까지 검사한다.
while (this.getLeftChildIndex(index) < count) {
const leftChildIndex = this.getLeftChildIndex(index)
const rightChildIndex = this.getRightChildIndex(index)
// 왼쪽, 오른쪽 중에 더 작은 노드를 찾는다
// rightChild 가 있다면 key의 값을 비교해서 더 작은 값을 찾는다.
// 없다면 leftChild 가 더 작은 값을 가지는 인덱스가 된다.
const smallerChildIndex =
rightChildIndex < count && this.heap[rightChildIndex].key < this.heap[leftChildIndex].key
? rightChildIndex
: leftChildIndex
// 자식 노드의 키 값이 루트노드보다 작다면 위로 끌어올린다.
if (this.heap[smallerChildIndex].key <= rootNode.key) {
this.heap[index] = this.heap[smallerChildIndex]
index = smallerChildIndex
} else break
}
this.heap[index] = rootNode
}
remove = () => {
const count = this.heap.length
const rootNode = this.heap[0]
if (count <= 0) return undefined
if (count === 1) this.heap = []
else {
this.heap[0] = this.heap.pop() // 끝에 있는 노드를 부모로 만들고
this.heapifyDown() // 다시 min heap 의 형태를 갖추도록 한다.
}
return rootNode
}
peek = () => this.heap[0] // 항상 최상위 노드가 peek 가 된다.
}
장점:
단점:
활용: 전체 자료를 정렬하는 것이 아니라, 가장 큰 값 몇개만 필요할 때, - 시뮬레이션 시스템, 네트워크 트래픽 제아, 운영체제에서의 작업 스케쥴링, 수치 해석적인 계산
코드 예시:
시간복잡도: 힙 트리의 전체 높이가 거의 log₂n(완전 이진 트리이므로)이므로 하나의 요소를 힙에 삽입하거나 삭제할 때 힙을 재정비하는 시간이 log₂n만큼 소요된다. O(log₂n)
공간 복잡도:
https://www.npmjs.com/package/node-btree
선형 자료구조는 순차적으로 요소에 접근하지만 트리 자료구조는 다른 방식을 사용해야한다.
전위 순회
- 제일 먼저 root node 방문
- 루트노드 - 왼쪽 서브트리 - 오른쪽 서브트리
- 각 서브트리의 root node에 접근
-> F B A D C E G I H
중위 순회
후위 순회
- root node를 마지막에 접근하는 순회방법.
- 왼쪽 서브트리 - 오른쪽 서브트리 - 루트노드
-> A C E D B F H I G F
1. F의 왼쪽 서브트리 탐색
2. B의 왼쪽 서브트리 탐색
3. A노드 방문
4. B의 오른쪽 서브트리 탐색
5. D의 왼쪽 서브트리 탐색
6. C노드 방문
7. D의 오른쪽 서브트리 탐색
8. E노드 방문
9. D노드 방문
10. B노드 방문
11. F의 오른쪽 서브트리 탐색
12. G의 오른쪽 서브트리 탐색
13. I의 왼쪽 서브트리 탐색
14. H노드 방문
15. I노드 방문
16. G노드 방문
17. F노드 방문
출처: https://code-lab1.tistory.com/9 [코드 연구소:티스토리]
키와 값으로 구성 된 검색 시스템
해시 테이블에는 모든 키에 대응하는 값이 있다. (->시간 복잡도 O(1), 장점) 이런 데이터구성은 검색 수행 속도를 크게 증가시킨다.
어떤 문자열을 넣으면 -> 해시함수는 index와 key-value를 생성한다. 그런데 이 방식은 효율적이지마 해시 값과 배열 크기 때문에 해시 충돌이 발생할 가능성이 있다. 이를 방지하기 위해 체이닝(단순한 배열이 아닌 'linked list'안 배열애 저장하는 방식) 으로 해시 테이블을 구현한다.
해싱 vs 암호화
암호화: 뒤죽박죽 섞어서 읽을 수 없게 만든 후 key가 있는 사람만 정보를 사용할 수 있게함. 이 과정에서 암호화 알고리즘을 사용해서 정보를 암호화-복호화 하는 것을 전제로 함. -> 양방향
해싱: data를 입력받아 고정 길이의 출력을 생성 + 원래의 데이터가 필요하지 않음. -> 단방향
장점:
단점:
활용: 디지털 서명->data의 유효성 검증 -> 수신 된 data가 누구에게서 왔는지 확인, 사용자 인증 ...
코드 예시:
시간복잡도: O(1)
공간 복잡도:
https://helloworldjavascript.net/pages/282-data-structures.html