
- Tree 형태의 자료구조로, max, min 값을 빠르게 구하는데 특별한 특성을 가지고 있음
- heap은 완전한 이진트리이다 ⇒ binary heap 이라고 알려짐
: 배열을 이용해 생성될 수 있다- 가장 마지막 level을 제외하고, 모든 level이 가득 차 있어야 한다.
높은 우선순위를 가지는 higher value를 구성요소로 가진다
→ 최대값을 빠르게 찾아야 하는 경우 유용 (ex. 우선순위가 높은 작업을 처리할 때 자주 사용)

- Parent Node들은 Children 값들보다 크거나 같은 값을 반드시 포함해야 한다.
- Root Node는 항상 가장 큰 값을 가진다.
높은 우선순위를 가지는 lower value를 구성요소로 가진다.
→ 최소값을 빠르게 찾아야 하는 경우 유용함 (ex. 최소 비용 경로 탐색 알고리즘 (Dijkstra’s algorithm) )
→ 각 value를 음수 취해서 max heap 으로 처리하면 좋다

- Parent Nodes 는 항상 Children들의 값보다 작거나 같은 값을 포함해야 한다.
- Root Node는 반드시 가장 작은 값을 가진다.
struct Heap<Element: Equatable> {
var elements: [Element] = [] // heap 안에 요소들을 가지고 있기 위한 배열
let sort: (Element, Element) -> Bool // heap이 어떻게 정렬되어야 하는지 정의한 메서드
init(sort: @escaping (Element, Element) -> Bool) {
// 두 개의 요소를 비교해서 그 결과에 따라 true or false를 반환한다.
// true를 반환하면 첫번재 요소가 우선순위 더 높고, false 반환 시 반대.
self.sort = sort
}
}
(왜 배열로 표시하냐? 추후 swap 작업을 좀 더 편리하게 하기 위해!)


var isEmpty: Bool {
elements.isEmpty
}
var count: Int {
elements.count
}
func peek() -> Element? {
elements.first
}
func leftChildIndex(ofParentAt index: Int) -> Int {
(2 * index) + 1
}
func rightChildIndex(ofParentAt index: Int) -> Int {
(2 * index) + 2
}
func parentIndex(ofChildAt index: Int) -> Int {
(index - 1) / 2
}
mutating func remove() -> Element? {
guard !isEmpty else { // 1. heap이 비어있는지 check
return nil
}
elements.swapAt(0, count - 1) // 2. root노드와 마지막 element를 swap
defer {
siftDown(from: 0) // 4. 삭제를 수행한 이후에 siftdown하면서 다시 max heap 혹은 min hep 형태로 만들어준다! (completion handler로 들어가겠지)
}
return elements.removeLast() // 3. 마지막 element 삭제하고 반환
}mutating func siftDown(from index: Int) {
var parent = index // 1. 부모 인덱스 저장 (parent는 현재 노드가 힙 속성을 만족하도록 처리해야 하는 노드)
while true { // 2. 반복문 시작 (제거할 적절한 위치를 찾으면 종료되는 반복문)
// 3. 현재 부모노드의 왼쪽 자식과, 오른쪽 자식의 인덱스를 계산해서 받는다.
// 이 때, 자식노드가 존재할 수도, 존재하지 않을 수도 있다.
// ex. 힙의 끝에 도달하면 자식 노드가 없을 수도 있음
let left = leftChildIndex(ofParentAt: parent)
let right = rightChildIndex(ofParentAt: parent)
// 4. 후보 인덱스 초기화
var candidate = parent
// 5. 왼쪽 노드가 존재하고, 왼쪽 자식의 우선순위가 부모보다 높으면
if left < count && sort(elements[left], elements[candidate]) {
// candidate 변수에 왼쪽 자식의 인덱스 저장
candidate = left
}
// 6. 오른쪽 자식 노드가 존재하고, 오른쪽 자식의 우선순위가 부모보다 높으면
if right < count && sort(elements[right], elements[candidate]) {
// candidate 변수에 오른쪽 자식 인덱스 저장
candidate = right // 6
}
// candidate가 parent와 같아지면 while 문 탈출
if candidate == parent {
return // 7
}
// 8. 윗 줄에서 탈출하지 못한 경우, parent와 candidate 교체
elements.swapAt(parent, candidate)
// 9. 그리고 그 candidate를 새로운 parent 로 설정 -> 비교 계속 됨
parent = candidate
}
}


mutating func insert(_ element: Element) {
elements.append(element)
siftUp(from: elements.count - 1)
}mutating func siftUp(from index: Int) {
var child = index
var parent = parentIndex(ofChildAt: Child)
while child > 0 && sort(elements[child], elements[parent]) }{
elements.swapAt(child, parent)
child = parent
parent = parentIndex(ofChildAt: child)
}
} struct Heap<Element: Equatable> {
var elements: [Element] = [] // heap 안에 요소들을 가지고 있기 위한 배열
let sort: (Element, Element) -> Bool // heap이 어떻게 정렬되어야 하는지 정의한 메서드
init(sort: @escaping (Element, Element) -> Bool) {
// 두 개의 요소를 비교해서 그 결과에 따라 true or false를 반환한다.
// true를 반환하면 첫번재 요소가 우선순위 더 높고, false 반환 시 반대.
self.sort = sort
}
}
mutating func remove(at index: Int) -> Element? {
// 1. 파라미터로 받는 index가 Heap 내부 elements 배열의 개수 안에 있는지 체크
// 아니라면 nil 반환해서 탈출
guard index < elements.count else {
return nil
}
// 2. index 가 배열의 가장 마지막 요소일 때, 마지막 값을 반환
if index == elements.count - 1 {
return elements.removeLast()
}
// 3. 마지막 요소가 아니라면, 마지막 요소와 index 요소를 swap
else {
elements.swapAt(index, elements.count - 1)
defer {
/* 5-1. 루트에 지금 가장 작은 값이 있을 확률이 높음.
자식노드로 내려가면서 노드의 적절한 위치를 찾는다.
*/
siftDown(from: index)
/* 5-2. siftDown 이후, 자식 노드에서 부모 노드로 올라가며 힙 속성을 유지한다.
*/
siftUp(from: index) // 다시 올라가면서 부모보다 큰 child 가 있는 경우를 재조정
}
// 4. 교체된 이후에 마지막 요소가 되니까 삭제
return elements.removeLast()
}
}
위 메서드에서 mutating이 붙는 이유와, 이렇게 구조체 인스턴스의 값을 변경할 때 성능에는 영향을 미치지 않을지에 대한 궁금증이 생겼다.
mutating과 구조체, 그리고 COW (Copy-on-Write)
mutating키워드가 붙는 이유
- struct 구조체는 값 타입이라서 struct 내부의 인스턴스 속성을 수정할 수 없다. (기본적으로 제공되지 않는다. )
- 그러나 위 메서드처럼 인스턴스를 수정해야 할 필요가 있을 때는 메서드 앞에
mutating키워드를 붙이면 된다.- 그럼 성능 면에서는 어떨까? / 기존과 어떻게 다르게 동작하는걸까?
mutating메서드는 복사된 구조체 인스턴스에서 변경을 수행하고, 그 변경된 인스턴스를 다시 돌려주는 방식으로 동작함- COW (Copy-on-Write)
- 구조체는 데이터가 복사되는 과정에서 값의 크기에 따라 복사 비용이 소요될 수 있어 성능에 영향을 미칠 수 있음
- 하지만 Swift에서 사용하는 Copy-on-Write 최적화를 사용하면, 구조체가 복사되더라도 실제로 변경이 일어나지 않는 한, 복사는 이뤄지지 않고 “참조만 공유됨”
- 결국,
mutating메서드 호출해서 값을 실제로 변경하려는 시도를 할 때, 그 순간에 “진짜 복사”가 발생한다.
⇒ 이 최적화로 인해 복사 비용 크게 발생하지 않고 구조체는 효율적으로 동작이 가능함
DefervsEscaping Closure
defer
- 코드의 실행 흐름과 관계없이 반드시 실행되어야 하는 코드가 있을 때 유용함
ex. 자원 해제, 파일 닫기, 네트워크 연결 해제 등 어떤 작업의 마무리 작업 시- 함수가 끝날 때 반드시 실행됨 → 함수가 여러 경로로 종료될 수 있는 상황에서 꼭 실행해야 하는 코드가 있을 때 유용함
(오류, 예외처리 등으로 함수가 예기치 못하게 종료 될 떄도 실행할 수 있으므로)- 장점
- 반드시 실행을 보장함
Escaping Closure
- 코드의 실행을 나중으로 미루거나, 필요한 곳에서 호출을 예약할 때 유용함
- 주로 비동기 작업에서 자주 사용됨
- 명시적인 호출이 필요하다!!
func index(of element: Element, startingAt i: Int) -> Int? {
if i >= count {
return nil // 1.
}
// 2. 찾고 있는 요소가 현재 요소보다 우선순위가 높은지 체크
if sort(element, elements[i]) {
return nil // 왜 nil을 반환할까? 찾고 있는 요소가 current보다 우선순위가 높다는게 어떤 의미일까?
}
if element == elements[i] {
return i // 3. 요소를 찾았을 때 해당 인덱스 반환
}
// 4. 재귀적으로 왼쪽 노드를 쭉 동일하게 호출
if let j = index(of: element, startingAt: leftChildIndex(ofParentAt: i) {
return j
}
// 5. 재귀적으로 오른쪽 노드를 쭉 호출 (같아질 때 까지)
if let j = index(of: element, startingAt: rightChildIndex(ofParentAt: i)) {
return j
}
return nil // 6. 모두 실패하는경우 nil 반환
}
주석 2번 단계에서의 의문 : 현재 노드가 우선순위가 찾고자 하는 요소보다 높다는게 무슨 의미일까?
- 현재 노드보다 더 높은 우선순위를 가진 요소를 존재하지 않는다. (항상 부모가 자식보다 높은 우선순위를 가지니까 더 내려가봐야 찾을 수 없음)
- 현재 노드가 element 보다 우선순위가 높다면, element가 힙에 존재할 가능성이 없으므로 탐색 계속할 필요가 없다

- 요소들이 들어온 순서대로 처리되는 것이 아닌, 요소들의 “우선순위”에 따라 처리되는 자료구조.
- 우선순위가 높은 요소가 먼저 처리된다. (응급실 같은 것)
→ 주어진 요소들 중 최댓값 또는 최솟값을 빠르게 찾고 처리해야 하는 상황에 유용함
- 활용
- Dijkstra’s algorithm : 최단 경로 계산 시, 최소 비용을 계산함
- A pathfinding algorithm : 탐색 중 가장 짧은 경로를 찾을 때 탐색할 경로의 우선순위를 관리함
- Heap sort : 요소들을 정렬
- Huffman coding : 압축 트리 만들 떄, 아직 부모 노드가 없는 가장 작은 빈도를 가진 두 노드를 계속 찾아 결합하는데 사용함
1. Max-priority queue
가장 큰 값이 큐의 앞에 있어서 가장 먼저 처리됨
2. Min-priority queue
가장 작은 값이 큐의 앞에 있어서 가장 먼저 처리됨
0번째 index 제거 (root 노드 : 우선순위 가장 높은 요소를 제거하는 것)