///
Search
▶️

기본 자료구조(Basic Data-Structure)

1. 기본 자료구조(Basic Data-Structure)

Basic Data-Structure
자료구조는 자료를 저장하는 구조를 말합니다.
Ex) Stack, Queue, Tree, Graph ...
특정 목적에 따라 자료를 저장하는 방법이 달라 목적(각 장단점에 의거)에 맞게 사용할 수 있어야 합니다.
저장되어 있는 자료구조를 바탕으로 의미있는 결과를 도출하는 과정을 알고리즘이라고 합니다.
가장 기본이 되는 자료구조 : 변수, 배열

2. 배열 VS 연결 리스트

배열(Array / ArrayList)
연결 리스트(Linked List)
장점
탐색이 빠름
배열에 비해 삽입/삭제 연산이 빠름
단점
삽입/삭제 연산이 느림
배열에 비해 탐색이 느림
탐색 시간 복잡도
O(1)
O(N)
삽입 시간 복잡도
평균적으로 O(N)
평균적으로 O(1)
삭제 시간 복잡도
평균적으로 O(N)
평균적으로 O(1)

3. 캡슐화(Encapsulation)

Encapsulation
자료구조 구현의 핵심이라고 할 수 있습니다.
자료구조를 사용하는 사람은 자료구조가 어떻게 동작하는지 알 필요가 없도록 설계해야 합니다.
구현한 사람외에 사용하는 사람은 사용법만 알아야 합니다.

4. 구조체(Struct)

Struct
구조체는 캡슐화를 통해 구현하며, 캡슐화를 구현하기 위해 사용합니다.
하나의 자료형(Type)을 정의할 수 있습니다.
여러 프로퍼티와 메소드로 이루어져 있어, 관련된 데이터끼리 묶을 수 있습니다.

5. 스택(Stack)

Stack
Computer Science의 대표적인 자료구조 중 하나입니다.
Linear(선형) 자료구조이며, 자료를 쌓으면서 저장하는 자료구조입니다.
Last In First Out(LIFO) 특징을 가지고 있어, 순서를 뒤집거나 발자취를 기록할 때 사용할 수 있습니다.
Push/Pop 연산이 가능합니다.
Stack Overflow : Stack은 정해진 크기가 있어, 용량이 다 찼음에도 불구하고 데이터를 삽입할 경우를 말합니다.
Stack Underflow : Stack이 비었음에도 불구하고 데이터를 삭제하려고 하는 경우를 말합니다.
//MARK: - Type struct Stack { //MARK: - Property var array: [Int] var len: Int var capacity: Int //MARK: - Initializer init(_ capacity: Int) { self.capacity = capacity self.array = Array(repeating: 0, count: capacity) self.len = 0 } //MARK: - Method mutating func push(_ data: Int) -> Void { if len >= capacity { print("Overflow") } else { self.array[self.len] = data self.len += 1 } } mutating func pop() -> Void { if self.len <= 0 { print("Underflow") } else { self.array[self.len - 1] = 0 self.len -= 1 } } func top() -> Int { return self.len <= 0 ? -1 : self.array[len - 1] } }
Swift
복사

6. 큐(Queue)

Queue
Stack과 함께 Computer Science의 기초 자료구조 중 하나입니다.
First In First Out(FIFO) 구조이며, Linear(선형) 자료구조입니다.
Queue Overflow : Queue의 용량보다 데이터를 더 넣으려고 하는 경우를 말합니다.
Queue Underflow : Queue에 데이터가 없는데 데이터를 삭제하려고 하는 경우를 말합니다.
단순 투 포인터로 큐를 구현하게 되는 경우 push / pop 연산시 두 포인터가 모두 증가만 하게되므로. 앞의 공간에 대한 낭비가 발생할 수 있습니다.

7. 원형 큐의 개념과 구현방법(Circular Queue)

Circular Queue
선형큐의 공간 낭비의 단점을 해결하기 위한 큐입니다.
선형큐에 비해 공간 활용 능력이 우수합니다.
원소의 개수를 유지하는 별도의 변수가 필요합니다.
front, rear 투 포인터가 끝에 도달하게되면 push / pop 연산시 다시 처음으로 되돌리는 원형의 형태입니다.
//MARK: - Type struct Queue { //MARK: - Property private var array: [Int] private var f: Int private var r: Int private var capacity: Int //MARK: - Initializer init(_ capacity: Int) { self.capacity = capacity self.array = Array(repeating: 0, count: capacity) self.f = 0 self.r = 0 } //MARK: - Method mutating func push(_ data: Int) -> Void { if self.r >= self.capacity { print("Overflow") } else { self.array[self.r] = data self.r += 1 } } mutating func pop() -> Void { if self.r - self.f <= 0 { print("Underflow") } else { self.array[self.f] = 0 self.f += 1 } } func front() -> Int { return self.r - self.f <= 0 ? -1 : self.array[self.f] } func size() -> Int { return self.r - self.f } }
Swift
복사

8. 스택 & 큐 정리(Stack & Queue Summary)

Summary
특정 자료구조가 무엇인지 아는 것은 중요하지 않고, 의도에 맞게 사용하는 능력이 중요합니다.
올바른 괄호 판단 문제는 대표적인 stack 자료구조를 활용하는 문제입니다.
Stack과 Queue에는 상태(status) 즉, 해야하는 작업이 저장된다고 볼 수 있습니다.
Stack의 경우 함수 호출에 대한 복귀 주소 즉, 발자취를 기록을 해두며, 이를 Call Stack이라고 합니다.(Stack의 Scheduling : 재귀호출)
Queue의 경우 Stack과 달리 상태의 의존관계가 없을 때 즉, 해야 하는 작업을 진행할 때 다른 작업에 영향을 받지 않고 순서대로 처리하며, 대표적으로 스케쥴링, 병렬화에 많이 사용됩니다.

9. 트리(Tree)

Tree
트리 역시 자료를 담는 Computer Science의 기초 자료구조 중 하나입니다.
계층형(Hierarchy) 자료구조입니다.
정점(Node), 간선(Edge)로 이루어져 있으며, 간선은 두 정점을 잇는 선을 말합니다.
부모와 자식관계를 가지며 레벨은 높이를 뜻합니다.
트리는 그 안에 또 트리가 존재하며(재귀적 성질), 이를 서브트리(Subtree)라고 합니다.
최상위 부모노드를 root라고 합니다.

10. 트리의 순회(Tree Traversal)

Tree Traversal
트리 내에 어떠한 자료가 담겨 있는지를 알기 위한 방법입니다.
순회하는 방법은 많지만, 재귀적 성질을 이용해서 순회하는 방법이 젤 중요합니다.

11. 이진 트리 재귀적 순회 방법

Tip
자식노드가 2개 이하인 트리를 Binary Tree(이진 트리)라고 함
전위 순회 : Root - L(Left Subtree) - R(Right Subtree)
중위 순회 : L(Left Subtree) - Root - R(Right Subtree)
후위 순회 : L(Left Subtree) - R(Right Subtree) - Root
세가지 순회방법이 목적은 트리 내의 자료를 탐색하는 것으로 같지만, 특성이 다릅니다.
//MARK: - Type struct Node { //MARK: - Property var left: Int var right: Int //MARK: - Initializer init(_ left: Int, _ right: Int) { self.left = left self.right = right } } //MARK: - Function func preOrder(_ tree: [Node], _ root: Int) -> Void { if root == -1 { return } print("\\(root) ", terminator: "") preOrder(tree, tree[root].left) preOrder(tree, tree[root].right) } func inOrder(_ tree: [Node], _ root: Int) -> Void { if root == -1 { return } inOrder(tree, tree[root].left) print("\\(root) ", terminator: "") inOrder(tree, tree[root].right) } func postOrder(_ tree: [Node], _ root: Int) -> Void { if root == -1 { return } postOrder(tree, tree[root].left) postOrder(tree, tree[root].right) print("\\(root) ", terminator: "") }
Swift
복사

12. 우선순위 큐(Priority Queue)

Priority Queue
Tree를 활용하는 대표적인 예제입니다.
원소를 제거할 때, 우선순위가 가장 높은 원소를 제거합니다.
배열로 우선순위 큐를 구현하게 되면, 삽입 연산은 효율적이지만 삭제 연산의 경우 비효율적일 수 있습니다.

13. 힙(Heap)

Heap
Heap 자료구조는 부모의 우선순위가 항상 자식보다 높은 완전이진트리입니다.
자식노드가 2개 이하인 이진트리 이면서, 값이 왼쪽부터 채워지는 트리를 완전이진트리라고 합니다.
Heap에 값을 삽입하게 되면, 완전이진트리 특성에 따라 왼쪽부터 채워지고, 부모와 자식 사이의 우선순위를 비교하여 트리를 재구성해야 합니다.
노드가 n개일 때, 높이가 대략 log n이므로 높이가 1개 증가할 때 마다 노드는 약 2배 증가하게 됨을 알 수 있습니다.
즉, 삽입 연산 시 최악의 경우 트리의 높이 만큼 비교 연산을 수행 하므로, O(log n)의 시간복잡도를 가집니다.
Heap에서 삭제 연산을 하게 되면, 우선순위가 가장 높은 root를 삭제하고, 가장 끝에 있는 자식 노드를 root로 옮기고 우선순위를 비교하여 트리를 재구성해야 합니다.
삽입 연산과 마찬가지로 root에서 최대로 트리의 높이 만큼 내려올 수 있으므로, O(log n)의 시간복잡도를 가집니다.

14. 우선순위 큐 구현(배열)

//MARK: - Type struct PriorityQueue { //MARK: - Property private var data: [Int] private var len, capacity: Int //MARK: - Initiailzer init(_ capacity: Int) { self.capacity = capacity self.data = Array(repeating: 0, count: capacity + 10) self.len = 0 } //MARK: - Method mutating func push(_ x: Int) -> Void { self.data[self.len] = x self.len += 1 } mutating func pop() -> Void { var min: Int = self.data[0], minIndex: Int = 0 for i in 0..<self.len { if min > self.data[i] { min = self.data[i] minIndex = i } } for i in minIndex..<self.len { self.data[i] = self.data[i + 1] } self.len -= 1 } func top() -> Int { var min: Int = self.data[0] for i in 0..<self.len { min = min > self.data[i] ? self.data[i] : min } return min } }
Swift
복사

15. 우선순위 큐 구현(힙)

//MARK: - Type struct PriorityQueue { //MARK: - Property var data: [Int] var len, capacity: Int //MARK: - Initializer init(_ capacity: Int) { self.capacity = capacity self.data = Array(repeating: 0, count: self.capacity + 10) self.len = 1 } //MARK: - Method mutating func push(_ x: Int) -> Void { self.data[self.len] = x self.len += 1 var currentIndex: Int = self.len - 1 while currentIndex > 1 { if self.data[currentIndex] < self.data[currentIndex >> 1] { let temp: Int = self.data[currentIndex] self.data[currentIndex] = self.data[currentIndex >> 1] self.data[currentIndex >> 1] = temp currentIndex >>= 1 } else { break } } } mutating func pop() -> Void { self.data[1] = self.data[self.len - 1] self.data[self.len - 1] = 0 self.len -= 1 var currentIndex: Int = 1 while true { var childIndex: Int = -1 if self.len - 1 < (currentIndex << 1) { break } else if (0 < (currentIndex << 1) && (currentIndex << 1) <= self.len - 1) && (currentIndex << 1 + 1) > self.len - 1 { childIndex = currentIndex << 1 } else { childIndex = (currentIndex << 1) < (currentIndex << 1 + 1) ? (currentIndex << 1) : (currentIndex << 1 + 1) } if self.data[currentIndex] > self.data[childIndex] { let temp: Int = self.data[currentIndex] self.data[currentIndex] = self.data[childIndex] self.data[childIndex] = temp currentIndex = childIndex } else { break } } } func top() -> Int { return self.data[1] } }
Swift
복사

16. 우선순위 큐의 구현 요약

배열
삽입
O(1)
O(log n)
삭제
O(n)
O(log n)
탐색
O(n)
O(1)
Tip
삽입 연산이 배열이 빠르긴 하지만, 삭제 연산에서 차이가 많이 발생하므로 우선순위 큐를 구현해야 한다면 힙으로 구현하는 것이 바람직하다고 할 수 있습니다.
힙의 경우 완전이진트리이기 때문에, 배열을 이용하여 구현할 수 있습니다.