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
•
삽입 연산이 배열이 빠르긴 하지만, 삭제 연산에서 차이가 많이 발생하므로 우선순위 큐를 구현해야 한다면 힙으로 구현하는 것이 바람직하다고 할 수 있습니다.
•
힙의 경우 완전이진트리이기 때문에, 배열을 이용하여 구현할 수 있습니다.