트라이

트라이는 문자열이 트리형태로 저장된 자료구조이다.

특정 문자열을 탐색할때 특화된 알고리즘으로, 트리는 K진 트리이다. (K는 문자열의 첫번째 문자의 종류)

 

예로 문자열 "apple", "banana", "avocado" 세가지가 있다고 하면 아래와 같은 트리형태로 만들 수 있다.

트리의 최상단노드는 비워두고, 그아래로 k개의 접두사를 갖는 자식노드들을 만든다.

문자열 세개중 중복되지 않는 접두사는 a,b로 최상단노드는 두개의 자식노드를 갖는다.

우리가 문자열을 비교할때, 앞에서부터 차례로 읽어나가듯이, 주어진 문자열을 순차적으로 연결시킨다.

그리고 문자열을 모두 연결시켰다면 마지막노드에 해당노드가 마지막임을 저장해둔다.(Bool 변수를 이용)

 

주어진 문자열에서 특정 문자열을 찾을때, 최상단 노드부터 시작하여 문자열을 검사해나간다.

이때 문자열이 연결되어 있지 않거나, 문자를 모두 찾아갔지만 해당 노드가 끝이아니라면, 해당 문자열은 찾을 수 없다.

 

트라이 자료구조는 특정문자열을 수정하거나 찾는데 O(N)만큼 걸리지만(트리형태의 자료구조에서 문자수만큼 연결하면된다.)

각 노드의 자식노드는 문자열의 최대 경우의수(소문자 26개, 대문자 26개)를 저장하기위해 메모리를 할당해야하므로

메모리가 추가적으로 많이필요하다.

 

트라이를 코드로 구현하면 다음과 같다.

import Foundation

// 트라이 자료구조
class Trie {
    // 트라이의 자식노드 메모리 할당(여기서는 소문자만 취급한다.)
    // 소문자의 아스키코드로 접근하기위해서 소문자의 갯수만큼 할당
    var child: [Trie?] = Array(repeating: nil, count: 26)
    // 해당 노드가 문자열의 마지막인지?
    var end = false
    
    // 트라이에 문자열을 넣는다. 외부에서 문자열String을 받음
    // 받은 문자열을 한개의 문자들로 나누고 insertCharater함수에 넘겨준다.
    // index는 0: 최상단노드부터 추가해주기 위해서
    func insert(_ s: String) {
        let arr: [Character] = Array(s)
        insertCharacter(arr, 0)
    }
    // 문자 배열과 인덱스를 넘겨받고 실행
    func insertCharacter(_ arr: [Character],_ index: Int) {
        // 해당 문자열을 모두 입력했다면 현재 노드가 마지막임을 설정하고 종료
        if arr.count == index {
            self.end = true
            return
        }
        // 아스키코드로 변환 범위: 0...25 (자식노드배열에 넣기위해)
        let num = toNumber(arr[index])
        
        // 현재 문자열의 아스키코드로 자식노드를 생성해준다.
        // apple을 입력해놓고 avocado를 입력할때, a는 이전에 생성해두었지만, v는 생성해두지 않았기에 생성
        if child[num] == nil {
            child[num] = Trie()
        }
        //남은 문자도 트리에 연결하기 위해 재귀호출
        child[num]?.insertCharacter(arr, index+1)
    }
    // 해당 문자를 아스키코드로 변환하지만 0~25 범위에 존재하게끔 소문자의 첫번째 문자 a의 아스키코드를 뺌
    func toNumber(_ c: Character) -> Int{
        return Int(c.asciiValue! - Character("a").asciiValue!)
    }
    
    // 문자열String을 받으면 해당 문자가 존재하는지 bool값 리턴
    // 문자열String을 역시 문자배열로 변환하여 내부함수 findCharacter를 실행
    func find(_ s: String) -> Bool{
        let arr: [Character] = Array(s)
        return findCharacter(arr,0)
    }
    // 문자열이 존재하는지 파악하기위해 재귀호출
    func findCharacter(_ arr: [Character],_ index: Int) -> Bool {
        // 문자열이 모두 존재하고, 마지막 문자가 끝나는노드라면 true
        // 문자열이 모두 존재했지만, 마지막 문자가 끝나지않는 노드라면 false
        if arr.count == index {
            if self.end { return true }
            return false
        }
        // 해당 문자를 아스키코드로 변환
        let num = toNumber(arr[index])
        // 해당문자의 자식노드가 존재하지않다면 더 찾을 수 없으므로 false
        if child[num] == nil { return false }
        // 그다음 문자도 확인
        return child[num]!.findCharacter(arr, index+1)
    }
}

 

'Swift > 자료구조' 카테고리의 다른 글

세그먼트 트리 Swift  (0) 2024.04.29
분리집합과 Union-Find Swift  (1) 2024.04.28
Swift 덱(Deque) 구현  (0) 2024.04.03
Swift 큐(Queue) 구현  (0) 2024.04.03
Swift 우선순위 큐(Priority Queue) 구현  (0) 2023.05.17

 

세그먼트 트리

세그먼트 트리는 트리형태의 자료구조로 해당 노드에는 구간합이 저장되어 있다.

일반적으로 배열에서 특정 구간의 합을 구해야 한다면 O(N)이 걸린다.

세그먼트 트리는 노드마다 구간의 합이 저장되어 있기때문에 해당 노드에 접근하여 반환하면 되므로 O(logN)이 걸리게 된다.

이처럼 세그먼트 트리는 여러개의 데이터가 연속적으로 있을 때, 특정 구간의 합을 구하는데 효율적인 자료구조이다.

 

세그먼트 트리만들어보기

배열 A = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]이 주어졌다. 편의상 인덱스는 1부터 시작한다.

1 2 3 4 5 6 7 8 9 10

이를 트리로 만들어보자.

위 그림은 배열 A를 트리로 만들어본 형태다.

왼쪽노드는 index*2, 오른쪽노드는 index*2+1이 되고, 트리의 특성상 합을 구하면 O(logN)이 걸리게 된다.

구간힙 트리로 만들어보자.

맨 위 최상단노드는 모든 원소의 합이 들어간다.

그다음 두 번째 세번째 노드를 구해주는데 두번째노드는 1번부터 5번까지의 구간합, 세 번째 노드는 6번부터 10번까지의 구간합이 들어간다.

그 다음 자식노드도 구해보자. 

두번째 노드는 1~5의 구간합이고, 세번째노드는 6~10의 구간합이다.

중간인덱스를 구해 두 번째 노드의 왼쪽자식노드는 1~3, 오른쪽자식노드는 4~5

세 번째 노드의 왼쪽자식노드는 6~8, 오른쪽자식노드는 9~10 이 된다.

이렇게 마지막 노드까지 구하면 구간합 트리는 아래와 같다.

 

이전의 배열의 크기는 10이었고 트리로 만들어도 크기는 달라지지 않았지만, 구간힙 트리로 만들면서 크기가 변경되었다.

이처럼 배열을 구간힙트리로 만들려면 배열의 크기보다 크면서 가장 인접한 제곱수 * 2만큼의 크기를 할당해야 한다.

예시의 배열크기는 10이어서 10보다 큰 인접한 제곱수 4^2 = 16이고, 16*2 = 32이다. 최소 32의 크기를 할당해야 하지만, 더 넉넉하게 주려면 배열의 크기보다 4배 크면 된다.(최소로만 크기를 할당해도 상관은 없다.)

 

※종합

1. 연속된 데이터의 특정범위를 빠르게 구하기 위해 세그먼트 트리를 작성

2. 세그먼트 트리의 크기는 적어도 주어진 데이터크기보다 크며 가장 인접한 제곱수의 2배만큼 할당(넉넉히 4배만큼 줘도 상관은 없다)

3. 세그먼트 트리의 각 노드는 특정 구간의 합이 저장되어 있어 선형탐색(O(N))보다 효율적(O(logN))

 

이를 코드로 구현해 보자.

 

1. 세그먼트 트리 만들기

// 연속된 데이터 예시
// 트리의 특성(왼쪽자식인덱스:index*2, 오른쪽자식인덱스:index*2+1)를 이용하기위해 배열 첫번째는 임의의데이터를 넣는다.
var arr = Array(0...10)
// 크기 할당
var segTree = Array(repeating: 0, count: arr.count*4)

// 세그먼트 트리 만들기 (재귀호출)
// 구간합을 구해서 더한값이 부모노드가 됨
// 재귀호출로 맨 아래의 노드(start=end)까지 도달 후 더해서 부모노드를 채워가는 형식으로 진행됨
// start: 배열arr의 첫번째인덱스, end: 배열arr의 마지막인덱스
// index: 세그먼트트리의 인덱스
func segmentTreeInit(_ start: Int, _ end: Int, _ index: Int) -> Int{
	// 구간의 start == end는 해당 위치의 원소를 의미
    // 배열 arr의 3부터3까지의 구간합은 결국 arr[3]이다.
    if start == end {
        segTree[index] = arr[start]
        return segTree[index]
    }
    // 구간을 둘로 나누어 트리 특성상 왼쪽노드의 위치는 index*2, 오른쪽노드는 index*2+1
    var mid = (start + end) / 2
    // 재귀호출
    // 해당 구간합은 그 위치의 왼쪽 자식노드와 오른쪽자식노드의 합이다.
    segTree[index] = segmentTreeInit(start, mid, index*2) + segmentTreeInit(mid+1, end, index*2+1)
    return segTree[index]
}

 

결과:

segmentTreeInit(1, arr.count-1, 1)
print(segTree)
print(segmentTreeInit(1, arr.count-1, 1))

// [0, 55, 15, 40, 6, 9, 21, 19, 3, 3, 4, 5, 13, 8, 9, 10, 1, 2, 0, 0, 0, 0, 0, 0, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// 55

 

 

2. 특정 구간의 합 구하기

세그먼트 트리를 만드는 이유가 되기도 하는 특정구간의 합을 구해보자.

특정 구간의 합을 구하는 과정은 세그먼트트리의 최상단 노드부터 시작하며,

해당노드가 구간 안에 존재한다면, 모두 더해준다.

해당 구간을 찾아가는 방법은 재귀호출로 찾아갈 것이다.

예를 들어 4~6구간의 합을 구한다고 하자.

파랑노드를 찾아가는 조건은 재귀호출마다 해당 노드의 구간이 구하려는 범위 내에 존재하면 그 노드로 탐색하는 것이다.

- [4~6] 구간을 구할 때, 왼쪽은 [1~5] 오른쪽은 [6~10]이므로 범위 내에 존재한다. 진입

- [1~5]에서 왼쪽 [1~3]은 범위내에 존재하지 않는다 0

- [1~5]에서 오른쪽 [4~5]는 범위내에 존재한다. 해당노드를 더해줌 9

- [6~10]에서 오른쪽노드는 범위 내에 존재하지않고, 왼쪽노드는 존재한다. 왼쪽노드 진입

- [6~8]에서 왼쪽노드가 범위내에 존재 왼쪽노드진입

- [6~7]에서 왼쪽노드가 범위내에 존재 왼쪽노드 진입

- [6]은 범위내에 존재. 더해준다.

-나머지 범위들은 모두 범위밖에 구간합이므로 0을 더해준다.

 

코드로 구현하면 다음과 같다.

// 특정 구간합 구하기
// start: 배열arr의 시작인덱스, end: 배열arr의 마지막인덱스
// index: 세그먼트 트리의 인덱스(재귀호출을위해 1로 입력)
// left: 구하려는 범위의 시작인덱스, right: 구하려는범위의 마지막인덱스
func segmentTreeSum(_ start: Int,_ end: Int,_ index: Int,_ left: Int,_ right: Int) -> Int{
	// left...right 범위를 벗어났다면 0리턴
    if left > end || right < start { return 0 }
    // left...right 범위내에 있는 구간합이라면 출력
    if left <= start && right >= end { return segTree[index] }
    // 자식노드로 재귀호출
    // 범위내의 구간합을 찾는다.
    let mid = (start + end) / 2
    return segmentTreeSum(start, mid, index*2, left, right) + segmentTreeSum(mid+1, end, index*2+1, left, right)
}

 

결과:

print(segmentTreeSum(1, arr.count-1, 1, 4, 6))
// 15

 

3. 특정 원소의 값 수정하기

세그먼트 트리는 구간합이 저장되어 있으므로, 특정원소의 값을 수정한다면 그 원소의 구간합 노드들을 전부 수정해 주면 된다.

세그먼트 트리의 특정원소를 수정할 때에는 새로운 값으로 초기화하는 방법이 아닌 증가, 감소하는 방향으로 구한다.

만약 아예 새로운 값으로 초기화한다면 새로 변경된 배열을 토대로 세그먼트 트리를 다시 만들어주면 된다.

 

예를 들어 배열 세 번째의 값을 6으로 만든다면

위 그림과 같이 구간합노드 중 세번째 값이 해당범위에 존재하면 모두 바꿔준다.

재귀호출로 특정원소까지 갈 때마다 증가 혹은 감소되는 값을 더해주며 내려간다.

 

코드로 구현하면 다음과 같다.

// 특정원소의 값 수정(증가 혹은 감소)
// start: 배열arr의 시작인덱스, end: 배열arr의 마지막인덱스
// index: 세그먼트트리의 인덱스
// node: 변경하려는 원소의 위치
// newValue: 변경값(증가 혹은 감소)
func segmentUpdate(_ start: Int,_ end: Int,_ index: Int,_ node: Int,_ newValue: Int) {
    // 수정하려는 값의 위치가 범위를 벗어나면 종료
    if node > end || node < start { return }
    // 수정하려는 값의 위치가 범위내에 존재하면 해당 구간합도 수정해준다.
    // 이때 수정하려는값을 더해준다.
    segTree[index] += newValue
    // 해당위치에 도달했다면 종료
    // 예: 배열arr의 3번째 원소를 수정한다면 3이 포함된 구간합을 모두 수정 후, 구간합 3~3의 노드까지 도달하면 종료
    if start == end { return }
    let mid = (start + end) / 2
    // 세그먼트트리의 왼쪽자식노드와 오른쪽자식노드 모두 접근
    segmentUpdate(start, mid, index*2, node, newValue)
    segmentUpdate(mid+1, end, index*2+1, node, newValue)
}

결과:

// 3번째 위치한 원소의 값을 3 증가시킨다. (arr[3]을 6으로 변경)
segmentUpdate(1, arr.count-1, 1, 3, 3)
print(segTree)
//     1   2       4                9 -> 변경된 값
// [0, 58, 18, 40, 9, 9, 21, 19, 3, 6, 4, 5, 13, 8, 9, 10, 1, 2, 0, 0, 0, 0, 0, 0, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

 

이렇게 세그먼트리를 이용한다면 특정 구간의 합을 구하거나 수정할 때, 선형탐색 O(N) 보다 더 효율적이다 O(logN)

 

전체코드:

import Foundation

// 연속된 데이터 예시
// 트리의 특성(왼쪽자식인덱스:index*2, 오른쪽자식인덱스:index*2+1)를 이용하기위해 배열 첫번째는 임의의데이터를 넣는다.
var arr = Array(0...10)
// 크기 할당
var segTree = Array(repeating: 0, count: arr.count*4)

// 세그먼트 트리 만들기 (재귀호출)
// 구간합을 구해서 더한값이 부모노드가 됨
// 재귀호출로 맨 아래의 노드(start=end)까지 도달 후 더해서 부모노드를 채워가는 형식으로 진행됨
// start: 배열arr의 첫번째인덱스, end: 배열arr의 마지막인덱스
// index: 세그먼트트리의 인덱스
func segmentTreeInit(_ start: Int, _ end: Int, _ index: Int) -> Int{
    // 구간의 start == end는 해당 위치의 원소를 의미
    // 배열 arr의 3부터3까지의 구간합은 결국 arr[3]이다.
    if start == end {
        segTree[index] = arr[start]
        return segTree[index]
    }
    // 구간을 둘로 나누어 트리 특성상 왼쪽노드의 위치는 index*2, 오른쪽노드는 index*2+1
    var mid = (start + end) / 2
    // 재귀호출
    // 해당 구간합은 그 위치의 왼쪽 자식노드와 오른쪽자식노드의 합이다.
    segTree[index] = segmentTreeInit(start, mid, index*2) + segmentTreeInit(mid+1, end, index*2+1)
    return segTree[index]
}

// 특정 구간합 구하기
// start: 배열arr의 시작인덱스, end: 배열arr의 마지막인덱스
// index: 세그먼트 트리의 인덱스(재귀호출을위해 1로 입력)
// left: 구하려는 범위의 시작인덱스, right: 구하려는범위의 마지막인덱스
func segmentTreeSum(_ start: Int,_ end: Int,_ index: Int,_ left: Int,_ right: Int) -> Int{
    // left...right 범위를 벗어났다면 0리턴
    if left > end || right < start { return 0 }
    // left...right 범위내에 있는 구간합이라면 출력
    if left <= start && right >= end { return segTree[index] }
    // 자식노드로 재귀호출
    // 범위내의 구간합을 찾는다.
    let mid = (start + end) / 2
    return segmentTreeSum(start, mid, index*2, left, right) + segmentTreeSum(mid+1, end, index*2+1, left, right)
}

// 특정원소의 값 수정(증가 혹은 감소)
// start: 배열arr의 시작인덱스, end: 배열arr의 마지막인덱스
// index: 세그먼트트리의 인덱스
// node: 변경하려는 원소의 위치
// newValue: 변경값(증가 혹은 감소)
func segmentUpdate(_ start: Int,_ end: Int,_ index: Int,_ node: Int,_ newValue: Int) {
    // 수정하려는 값의 위치가 범위를 벗어나면 종료
    if node > end || node < start { return }
    // 수정하려는 값의 위치가 범위내에 존재하면 해당 구간합도 수정해준다.
    // 이때 수정하려는값을 더해준다.
    segTree[index] += newValue
    // 해당위치에 도달했다면 종료
    // 예: 배열arr의 3번째 원소를 수정한다면 3이 포함된 구간합을 모두 수정 후, 구간합 3~3의 노드까지 도달하면 종료
    if start == end { return }
    let mid = (start + end) / 2
    // 세그먼트트리의 왼쪽자식노드와 오른쪽자식노드 모두 접근
    segmentUpdate(start, mid, index*2, node, newValue)
    segmentUpdate(mid+1, end, index*2+1, node, newValue)
}

    
segmentTreeInit(1, arr.count-1, 1)
print(segmentTreeSum(1, arr.count-1, 1, 4, 6))
segmentUpdate(1, arr.count-1, 1, 3, 3)
print(segTree)

 

 

Reference:

https://m.blog.naver.com/ndb796/221282210534

 

41. 세그먼트 트리(Segment Tree)

이번 시간에 다룰 내용은 여러 개의 데이터가 연속적으로 존재할 때 특정한 범위의 데이터의 합을 구하는 ...

blog.naver.com

 

'Swift > 자료구조' 카테고리의 다른 글

트라이(Trie) Swift  (0) 2024.04.30
분리집합과 Union-Find Swift  (1) 2024.04.28
Swift 덱(Deque) 구현  (0) 2024.04.03
Swift 큐(Queue) 구현  (0) 2024.04.03
Swift 우선순위 큐(Priority Queue) 구현  (0) 2023.05.17

분리집합

분리 집합은 서로 다른원소를 갖는 집합이다. 그래서 각각 분리집합의 공통원소는 없다.

전체집합 U라고 하고, 분리집합 A와 B가 있다면, 아래의 수식이 성립한다.

- U = A U B

- A  B = Ø

(A와B는 U의 부분집합이다.)

 

 

Union-Find

분리집합끼리의 표현을 효율적으로 구현하고, 조작하기 위해 생긴 알고리즘이다.

서로다른 분리집합을 합치는 Union연산과 임의의 원소가 어디의 집합에 속하는지 알기위한 find연산이 주어져서 Union-Find라고 불린다.

이 알고리즘을 사용하기위해 3가지의 연산을 구현해야한다.

 

1. 초기화: 기본적으로 구현하고 조작하기위해 분리집합을 구현해야한다. 분리집합의 원소는 자기 자신을 루트노드로 갖는다.

2. Union(x,y): 두 원소 x와 y가 주어졌을때, 서로 포함하는 분리집합을 합친다.

3. Find(x): x원소의 루트노드를 찾는다. 혹은 집합을 반환한다.

 

분리집합은 기본적으로 트리형태로 구현한다. 일반적인 트리는 부모노드가 자식노드를 가리키지만 분리집합은 부모노드만 추적하면 된다.

배열을 사용하여 구현해보자.

배열을 이용해 분리집합으로 구현하면 처음엔 원소는 자기원소를 루트노드로 갖는다.(편의를 위해 0은 비우고만든다.)

parent 1 2 3 4 5 6
index 1 2 3 4 5 6

parent[index] = x 의 표현은 index라는 원소의 루트노드는 x다.

(보통 배열을 사용하면 위와 반대로 index위치에 원소가 존재한다라고 생각했는데 여기서는 index가 원소고, 위치에 있는 원소가 부모노드원소가 되는것이다.)

초기화 할때는 자기 자신이 루트노드가 되므로 모형으로 보면 다음과 같다.

 

분리집합 [1,3,5]와 [2,4,6]을 만든다.

parent 1 2 1 2 1 2
index 1 2 3 4 5 6

Union(1,3): 원소3을 원소1의 루트노드로 연결한다.

Union(1,5): 원소 5를 원소1의 루트노드로 연결한다.

이렇게 하면 원소1,3,5를 갖는 분리집합이 완성된다.

 

Find(1): 원소1의 루트노드를 출력한다. -> 원소1은 자기 자신을 루트노드를 갖고있으므로 1을 반환

Find(5): 원소5의 루트노드를 출력한다 -> 원소5는 원소1을 루트노드로 갖고있다. 1을반환

예시의 deph는 2단계라 바로 루트노드를 출력하지만, 더 깊다면 재귀호출을 통해 해당 집합의 루트노드를 반환한다.

 

두개의 분리집합을 합치는연산 Union을 사용하면 다음 그림과 같다.

parent 1 1 1 1 1 1
index 1 2 3 4 5 6

Union(1,2): 원소 1이 속한 집합과 원소2가 속한집합을 합친다.

합치는과정은 해당 루트노드가 합치려는 집합의 루트노드로 연결만 해주면된다.

합쳐졌다면 원소2,4,6 또한 루트노드 1을 가리키게 된다.

 

Union-Find의 시간복잡도는 Union연산은 연결만 해주므로 O(1)이 들지만, Find연산은 트리의 depth 만큼 재귀호출을 하므로

최소 O(1)에서 최대 O(depth)가 걸리게 된다.

 

이를 코드로 구현해보자.

import Foundation

// Union-Find
// 분리집합을 저장할 배열 parent
// parent[i] = a : i의 부모노드는 a다.
var parent = Array(repeating: 0, count: 7)

// 처음 초기화할땐 자기 자신을 루트노드로 갖는다.
for i in 1...6 {
    parent[i] = i
}

// 원소 x가 포함된 집합과 y가 포함된 집합을 합친다.
// 이때 합쳐진 집합의 루트노드는 x의 루트노드가 된다.
func union(_ x: Int, _ y: Int) {
    var rootX = find(x)
    var rootY = find(y)
    parent[rootY] = rootX
}
// 원소 x의 루트노드를 반환한다. (재귀호출)
// 재귀호출은 트리의 depth만큼 실행된다.
func find(_ x: Int) -> Int {
    if parent[x] == x { return x}
    parent[x] = find(parent[x])
    return parent[x]
}

 

 

Reference:

https://gunjoon.tistory.com/140

 

Disjoint Set (분리 집합)

Disjoint Set 분리 집합 또는 서로소 집합은 각각의 집합이 공통 원소를 가지고 있지 않은 집합이다. 즉 전체집합 U에 대해, U의 분리 집합 A ,B는 다음 조건을 만족한다. A, B는 U의 부분 집합이다. A, B

gunjoon.tistory.com

https://victorqi.gitbooks.io/swift-algorithm/content/union-find.html

 

Union-Find · Swift Algorithm

 

victorqi.gitbooks.io

 

'Swift > 자료구조' 카테고리의 다른 글

트라이(Trie) Swift  (0) 2024.04.30
세그먼트 트리 Swift  (0) 2024.04.29
Swift 덱(Deque) 구현  (0) 2024.04.03
Swift 큐(Queue) 구현  (0) 2024.04.03
Swift 우선순위 큐(Priority Queue) 구현  (0) 2023.05.17

덱 (Deque)

- 양방향으로 데이터를 삽입및 삭제가 가능 한 데이터 구조이다.

- 양쪽으로 push,pop이 가능하며 시간복잡도는 O(1)이다.

- push,pop을 자주 사용하는 알고리즘에서 효율이 좋다.

- Swift는 지원하지않고, 직접 구현해야하며, 두개의 배열로 구현한다.

 

 

import Foundation

// Deque
struct Deque<T> {
    var enqueue: [T]
    var dequeue: [T] = []

    init(enqueue: [T]) {
        self.enqueue = enqueue
    }
    var count: Int {
        return enqueue.count + dequeue.count
    }
    var isEmpty: Bool {
        return enqueue.isEmpty && dequeue.isEmpty
    }

    mutating func first() -> T? {
        if dequeue.isEmpty {
            dequeue = enqueue.reversed()
            enqueue.removeAll()
        }
        return dequeue.last
    }
    mutating func pushFront(_ element: T) {
        dequeue.append(element)
    }
    mutating func pushRear(_ element: T) {
        enqueue.append(element)
    }
    mutating func popFront() -> T? {
        if dequeue.isEmpty {
            dequeue = enqueue.reversed()
            enqueue.removeAll()
        }
        return dequeue.popLast()
    }
    mutating func popRear() -> T? {
        if enqueue.isEmpty {
            enqueue = dequeue.reversed()
            dequeue.removeAll()
        }
        return enqueue.popLast()
    }
}

'Swift > 자료구조' 카테고리의 다른 글

세그먼트 트리 Swift  (0) 2024.04.29
분리집합과 Union-Find Swift  (1) 2024.04.28
Swift 큐(Queue) 구현  (0) 2024.04.03
Swift 우선순위 큐(Priority Queue) 구현  (0) 2023.05.17
Swift 힙(Heap) 구현  (0) 2023.05.17

선입선출의 형식인 데이터구조 (FIFO)

- Enqueue: 큐 맨 뒤에 요소 추가

- Dequeue: 큐 맨 앞의 데이터를 반환

- Peek: Front에 위치한 데이터 값 반환

- Front: 큐의 앞부분 (가장 먼저 들어온 데이터가 위치함)

- Rear: 큐의 뒷부분 ( 가장 최근에 들어온 데이터가 위치함)

 

dequeue하는 과정에서 removeFirst() 메소드를 이용한다면 시간복잡도가 O(n)이 걸리게된다.

이를 해결하기위해 index를 이용해 front부분을 확인하여 데이터를 반환하는 방법도 있지만,

reversed()도 O(1)이 걸리므로 이 방법을 사용했다.

import Foundation

struct Queue<T> {
    var elements: [T] = []
    
    // 큐 갯수
    var count: Int {
        return elements.count
    }
    // 큐의 가장 먼저 나갈 데이터
    var peek: T? {
        if elements.isEmpty {
            return nil
        } else {
            return elements[0]
        }
    }
    // 큐의 앞부분
    var front: T? {
        if elements.isEmpty {
            return nil
        } else {
            return elements[0]
        }
    }
    // 큐의 뒷부분
    var rear: T? {
        if elements.isEmpty {
            return nil
        } else {
            return elements.last!
        }
    }
    // 큐 비어있는지
    var isEmpty: Bool {
        return elements.isEmpty
    }
    // 큐 데이터 삽입
    mutating func enqueue(element: T) {
        elements.append(element)
    }
    // 큐 데이터 삭제(반환)
    mutating func dequeue() -> T? {
        if elements.isEmpty {
            return nil
        } else {
            var arr = Array(elements.reversed())
            let returnValue = arr.popLast()
            elements = Array(arr.reversed())
            return returnValue
        }
    }
}

'Swift > 자료구조' 카테고리의 다른 글

분리집합과 Union-Find Swift  (1) 2024.04.28
Swift 덱(Deque) 구현  (0) 2024.04.03
Swift 우선순위 큐(Priority Queue) 구현  (0) 2023.05.17
Swift 힙(Heap) 구현  (0) 2023.05.17
Swift 스택(Stack) 구현  (0) 2023.04.17

(내가 보려고 만든 번역)

(오역이 있다면 제보 부탁합니다)

 

링크: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols

 

Documentation

 

docs.swift.org

 

 

Protocols

-> 적합한 유형을 구현해야하는 요구사항을 정의한다.

 

- 프로토콜은 특정 작업이나 기능에 적합한 메소드와 프로퍼티들,그리고 다른 요구사항들의 청사진을 정의한다.

- 프로토콜은 해당 요구사항의 실제 구현을 제공하기 위해 클래스, 구조체, 열거형에 채택할 수 있다.

- 프로토콜의 요구사항을 충족하는 모든 유형은 해당 프로토콜을 준수한다고 한다.

 

구현해야하는 요구사항을 지정하는것 외에도 프로토콜을 확장할 수 있다.

- 요구사항의 일부를 구현

- 추가기능 구현

Protocol Syntax

클래스, 구조체, 열거형에 모두 유사한 방식으로 프로토콜을 정의

protocol SomeProtocol {
    // protocol definition goes here
}

 

이름 뒤에 콜론으로 구분하여 프로토콜을 배치할 수 있고, 채택한다고 명시한다.

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

 

클래스가 슈퍼클래스가 있는 경우, 채택하는 프로토콜 앞에 슈퍼클래스를 나열하고 그 뒤로 쉼표로 구별하여 추가한다.

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

 

- 프로토콜은 타입이기때문에 이름을 대문자로 시작한다.

 

Property Requirements

- 프로토콜은 인스턴스 프로퍼티나 특정 이름과 타입의 프로퍼티를 제공하기위해 모든 알맞은 타입을 요구할 수 있다.

- 프로토콜은 프로퍼티가 저장 프로퍼티인지, 계산 프로퍼티인지 지정하지 않고, 필수 프로퍼티 이름과 타입만 지정한다.

- 또한 각 프로퍼티가 gettable이어야하는지 gettable 및 settable이어야 하는지 지정한다.

 

- 프로토콜이 gettable 및 settable 프로퍼티을 요구하는 경우, 해당 속성은 상수 저장 프로퍼티나, 읽기 전용 계산된 프로퍼티로 충족될 수 없다.

- 프로토콜이 gettable 프로퍼티만 요구하는 경우, 모든 종류의 프로퍼티가 가능하며, 자신의 코드에 맞게 프로퍼티도 설정이 가능하다?

 

- 프로퍼티는 항상 키워드 var 접두사가 붙은 변수 프로퍼티으로 선언된다.

-  gettable 및 settable은 { get set }을, gettable 프로퍼티는 { get }을 유형 선언뒤에 표시한다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

 

- 프로토콜 내에서 static 키워드는 항상 접두사 키워드 앞에 붙인다.

- 이 규칙은 클래스에서 구현될때도 타입 프로퍼티 앞에 class or static 키워드가 붙는 경우에도 적용된다.

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

 

단일 인스턴스 프로퍼티가 있는 프로토콜의 예시

protocol FullyNamed {
    var fullName: String { get }
}

 

- FullyNamed 프로토콜은 완전한 이름을 제공하기 위해 적합한 타입이 필요하다.

- 프로토콜은 적합한 타입의 특성에 대해 다른 어떤 것도 지정하지 않는다.

- 단지 타입이 자체적으로 전체 이름을 제공할 수 있어야 한다는 것만 지정한다.

- 프로토콜은 모든 타입에 fullname 이라는 string 형식의 gettable 인스턴스 프로퍼티가 있어야 한다고 명시되어있다.

 

프로토콜을 채택하고 사용하는 간단한 구조체의 예시

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

 

- 여기 예시는 특정한 사람의 이름을 나타내는 Person이라는 구조체가 정의되었다.

- 첫번째줄에 FullyNamed 프로토콜을 채택한다고 명시되어있다.

- Person의 각 인스턴스는 fullName이라는 String 타입의 단일 저장 프로퍼티가 있다.

- 이는 프로토콜의 단일 요구사항과 일치하며, 프로토콜의 요구사항을 준수하고있음을 의미한다.( 준수하고있지 않다면 컴파일 타임에 에러 보고)

 

좀더 복잡한 예시

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

 

- 이 클래스는 fullName 프로퍼티를 Starship에 대한 계산된 읽기 전용 프로퍼티로 프로토콜의 요구사항을 구현했다.

- 각 클래스 인스턴스는 필수인 name과 옵셔널 prefix를 저장한다.

- fullName 프로퍼티는 prefix 값이 존재하면 사용하고, name 앞에 추가한다.

 

Method Requirements

- 프로토콜은 특정 인스턴스 메소드와 타입을 준수하여 구현되는 타입 메소드를 요구할 수 있다.

- 이러한 메소드는 일반 인스턴스 및 타입 메소드와 정확히 동일한 방식으로 프로토콜 정의의 일부로 작성되지만, 중괄호나 메소드 본문은 없다.

- 일반 방법과 동일한 규칙에 따라 가변 매개변수가 허용된다.

- 그러나 기본값은 프로토콜 정의 안에서 메소드 파라미터로 지정될 수 없다.

 

- 타입 프로퍼티와 마찬가지로 메소드 요구사항에도 접두사 static이 붙을 수 있다.

protocol SomeProtocol {
    static func someTypeMethod()
}

- 다음 예제는 단일 인스턴스 메소드 요구사항이 있는 프로토콜을 정의

protocol RandomNumberGenerator {
    func random() -> Double
}

 

- RandomNumberGenerator 프로토콜은 호출될때마다 Double 값을 리턴하는 Random 인스턴스 메소드를 요구한다.

- 프로토콜의 일부로 지정되지 않았지만, 이 값은 최소 0.0 에서 최대 1.0 사이의 숫자로 지정된다.

- RandomNumberGenerator 프로토콜은 각 난수가 어떻게 생성되는지 지정하지 않았다.

- 단지 Random 메소드가 채택된 다른 곳에서 새로운 난수를 생성하는 표준 방법을 제공하기만 하면 된다.

 

RandomNumberGenerator 프로토콜을 채택한 클래스

- 이 클래스는 선형 합동 생성기로 일려진 난수 생성기를 구현한다.

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

 

Mutating Method Requirements

- 메소드가 자신이 속한 인스턴스를 수정해야하는 경우가 있다.

- 값 유형(구조체, 열거형)의 경우 mutating 키워드를 func 키워드 앞에 배치하여 해당 메소드가 속한 인스턴스의 모든 프로퍼티가 수정가능함을 알린다.

 

- 프로토콜을 채택하는 모든 타입의 인스턴스를 수정하려면, 프로토콜 정의 내에서 일부 메소드를 mutating 키워드를 붙여야한다.

- 이를 통해 구조체와 열거형이 프로토콜을 채택하고 해당 메소드의 요구사항을 충족할 수 있다.

- 클래스에 대한 해당 메소드 구현을 작성할때 mutating 키워드를 작성할 필요가 없다. 구조체와 열거형에서만 사용된다.

 

아래 예시는 toggle 메소드가 정의된 Togglable 프로토콜을 정의한다.

- 이름에서 알다시피 toggle 메소드는 해당 프로퍼티를 수정하여 적합한 상태로 전환하거나 반전시키는 것이다.

- toggle 메소드는 Togglable 프로토콜 정의부분에서 mutating 키워드를 붙였다.

- 메소드가 호출될때 적합한 인스턴스의 상태를 변경할 것으로 예상된다.

protocol Togglable {
    mutating func toggle()
}

 

- 구조체나 열거형에 Togglable 프로토콜을 구현하는 경우, 해당 구조체나 열거형은 mutating으로 표시된 toggle() 메소드를 구현하여 프로토콜을 준수할 수 있다.

 

아래 예시는 OnOffSwitch 열거형을 정의한다.

- 이 열거형은 두가지 상태 사이를 전환한다. ( case On , case Off )

- 이 열거형의 toggle은 Togglable 프로토콜과 일치하도록 mutating을 표시하여 구현하도록 한다.

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on

 

Initializer Requirements

우선순위 큐(Priority Queue)

  • 힙을 이용해서 가장 높은 우선순위에 있는 요소를 제일 처음에 위치시키는 자료구조이다.
  • 가장 우선순위가 제거되면, 그다음으로 높은 우선순위인 요소가 앞에 위치하게 된다.
  • 구현을 위해서 힙이 구현되어있어야 한다.
  • 우선순위큐의 삽입 삭제 시간복잡도는 O(logN)으로 일반 배열을 사용하는 것보다 빠르다.

 

힙 (Heap) Swift 구현

https://jenikeju.tistory.com/129

 

Swift 힙(Heap) 구현

Heap Heap이란 완전이진트리의 일종으로 우선순위 큐를 위해 만들어진 자료구조이다. 여러 가지의 값 중, 최댓값과 최솟값을 빠르게 찾아내기 위한 자료구조이다. 중복값을 허용한다. 완전이진트

jenikeju.tistory.com

 

 

우선순위 큐 구현코드

struct PriorityQueue<T: Comparable> {
    var heap: Heap<T>

    init(_ elements: [T] = [], _ sort: @escaping (T,T) -> Bool) {
        heap = Heap(elements: elements, sortFunction: sort)
    }

    var count: Int {
        return heap.count
    }

    var isEmpty:Bool {
        return heap.isEmpty
    }

    func top() -> T? {
        return heap.peek
    }

    mutating func clear() {
        while !heap.isEmpty {
            heap.remove()
        }
    }

    mutating func pop() -> T? {
        return heap.remove()
    }

    mutating func push(_ element: T) {
        heap.insert(node: element)
    }
}

'Swift > 자료구조' 카테고리의 다른 글

분리집합과 Union-Find Swift  (1) 2024.04.28
Swift 덱(Deque) 구현  (0) 2024.04.03
Swift 큐(Queue) 구현  (0) 2024.04.03
Swift 힙(Heap) 구현  (0) 2023.05.17
Swift 스택(Stack) 구현  (0) 2023.04.17

Heap

  • Heap이란 완전이진트리의 일종으로 우선순위 큐를 위해 만들어진 자료구조이다.
  • 여러 가지의 값 중, 최댓값과 최솟값을 빠르게 찾아내기 위한 자료구조이다.
  • 중복값을 허용한다.

완전이진트리

- 마지막 레벨을 제외하고 모든 레벨에 노드가 꽉 차있다.

- 자식노드는 최대 2개만 가질 수 있고, 노드가 왼쪽부터 채워져야 한다.

- 배열을 사용한 표현이 가능하다.

 

 

 

Swift로 Heap 구현

  • 힙을 저장하는 자료구조는 배열이다.
  • 쉽게 구현하기 위해 배열의 첫 번째 인덱스인 0은 사용하지 않는다.
  • 코드로 보기 전에 Heap의 성질들을 본다.
  • 힙은 구조체로 구현한다.
struct Heap<T: Comparable> {
	// 힙의 원소를 넣을 배열 선언
    // 최대힙,최소힙 정렬함수 선언
    private var elements: [T] = []
    private let sortFunction: (T,T) -> Bool
    
    // 힙의 인덱스 0 은 사용하지 않는다
    // 인덱스 0의 값만 있으면 비어있다고 판단
    var isEmpty: Bool {
        return elements.isEmpty || elements.count == 1
    }
    
    // 루트노드의 원소 출력
    var peek: T? {
        if self.elements.isEmpty { return nil}
        return self.elements[1]
    }
    
    // elements[0]을 제외한 원소갯수
    var count: Int {
        if elements.isEmpty {
            return 0
        } else {
            return elements.count - 1
        }
    }
    
    // buildHeap() 함수는 힙의 재구성 함수
    // 인덱스0은 사용하지 않으므로 임의의 값 넣어서 칸 차지하기
    init(elements: [T] = [], sortFunction: @escaping (T,T) -> Bool) {
        if !elements.isEmpty {
            self.elements = [elements.first!] + elements
        } else {
            self.elements = elements
        }
        self.sortFunction = sortFunction
        if elements.count > 1 {
            self.buildHeap()
        }
    }
}
// 처음 힙이 선언되었을때 정렬함수를 통해 힙을 구성하는 함수이다.
// diveDown 함수는 입력된 index의 자식노드들과 정렬함수를 통해 값 비교 후 교환하는 함수이다.
// self.element.count / 2 는 노드중 자식노드를 가진 노드들을 전부 탐색한다.

	mutating func buildHeap() {
        for index in (1...(self.elements.count / 2)).reversed() {
            self.diveDown(from: index)
        }
    }

 

사진 출처: https://ko.wikipedia.org/wiki/%ED%9E%99_(%EC%9E%90%EB%A3%8C_%EA%B5%AC%EC%A1%B0) 

부모 노드와 자식 노드의 관계

왼쪽 자식 index = (부모 index) * 2
오른쪽 자식 index = (부모 index) * 2 + 1
부모 index = (자식 index) / 2
    // 왼쪽자식노드의 인덱스
    func leftChild(of index: Int) -> Int {
        return index * 2
    }
    // 오른쪽자식노드의 인덱스
    func rightChild(of index: Int) -> Int {
        return index * 2 + 1
    }
    // 부모노드의 인덱스
    func parent(of index: Int) -> Int {
        return index / 2
    }

 

힙의 삽입

1. 새로운 노드를 넣을 때, 배열의 마지막에 넣는다.

2. 새로운 노드를 부모노드와 비교하여 교환한다.

3. 부모노드는 새로운 노드의 index / 2 이므로 서로 swap 한다.

 

    // 힙이 비어있다면, 노드 추가 2번 (첫번째 인덱스를 1로 갖기 위해)
    // 힙이 비어있지 않다면 배열의 마지막에 노드 추가
    // 최대힙, 최소힙 정렬기준에따라 추가된 노드와 부모노드 비교 및 교환
    mutating func insert(element: T) {
        if self.elements.isEmpty {
            self.elements.append(element)
            self.elements.append(element)
            return
        }
        self.elements.append(element)
        self.swimUp(from: self.elements.endIndex - 1)
    }
// 힙의 정렬기준 sortFuction에 따라 부모노드와 자식노드 교환
	mutating func swimUp(from index: Int) {
        var index = index
        while index != 1 && self.sortFunction(self.elements[index], self.elements[self.parent(of: index)]) {
            self.elements.swapAt(index, self.parent(of: index))
            index = self.parent(of: index)
        }
    }

 

힙의 삭제

1. 힙의 루트노드를 마지막 원소와 바꾼다. swap

2. 마지막원소를 꺼내고 루트노드부터 자식노드와 교환한다.

3. 힙을 재구성한다.

 

 // 삭제할 루트노드 출력
 // 루트노드와 마지막노드를 바꾼후, 마지막 노드를 꺼낸다.
 // 루트노드를 자식노드들과 정렬기준으로 교환
 	mutating func remove() -> T? {
        if self.elements.isEmpty { return nil}
        self.elements.swapAt(1, self.elements.endIndex - 1)
        let deleted = self.elements.removeLast()
        self.diveDown(from: 1)

        return deleted
    }
// 자식노드들의 인덱스를 가져와 부모노드와 정렬함수를 통해 값 비교후 교환한다.
// 부모노드가 정렬기준으로 자리를 찾을 때 까지 계속 자식노드들과 비교후 교환한다.
	mutating func diveDown(from index: Int) {
        var higherPriority = index
        let leftIndex = self.leftChild(of: index)
        let rightIndex = self.rightChild(of: index)

        if leftIndex < self.elements.endIndex && self.sortFunction(self.elements[leftIndex], self.elements[higherPriority]) {
            higherPriority = leftIndex
        }
        if rightIndex < self.elements.endIndex && self.sortFunction(self.elements[rightIndex], self.elements[higherPriority]) {
            higherPriority = rightIndex
        }

        if higherPriority != index {
            self.elements.swapAt(index, higherPriority)
            self.diveDown(from: higherPriority)
        }
    }

 

힙의 시간복잡도 ( O(logn) )

- 배열의 최대 최소를 구하면 O(n)이 걸렸지만, 힙에 넣고 정렬한다면 더 빠르게 찾을 수 있다.

 

 

Heap 구현 코드

+ index 계산을 위해 첫 원소의 인덱스값을 1로 설정하므로 이에 IsEmpty와 insert부분 수정

import Foundation

struct Heap<T: Comparable> {
    var elements: [T] = []
    var sortFunction: (T,T) -> Bool
    var isEmpty: Bool {
        return elements.isEmpty || elements.count == 1
    }
    var peek: T? {
        if elements.isEmpty {
            return nil
        } else {
            return elements[1]
        }
    }
    var count: Int {
        if elements.isEmpty {
            return 0
        } else {
            return elements.count - 1
        }
    }
    init(elements: [T] = [], sortFunction: @escaping (T,T) -> Bool) {
        if !elements.isEmpty {
            self.elements = [elements.first!] + elements
        } else {
            self.elements = elements
        }
        self.sortFunction = sortFunction
        if elements.count > 1 {
            self.buildHeap()
        }
    }
    func leftChild(of index: Int) -> Int {
        return index * 2
    }
    func rightChild(of index: Int) -> Int {
        return index * 2 + 1
    }
    func parent(of index: Int) -> Int {
        return index / 2
    }

    mutating func swimUp(from index: Int) {
        var idx = index
        while idx != 1 && self.sortFunction(self.elements[idx], self.elements[parent(of: idx)]) {
            self.elements.swapAt(idx, self.parent(of: idx))
            idx = self.parent(of: idx)
        }
    }
    mutating func insert(element: T) {
        if self.elements.isEmpty {
            self.elements.append(element)
            self.elements.append(element)
            return
        }
        self.elements.append(element)
        self.swimUp(from: self.elements.endIndex - 1)
    }
    mutating func diveDown(from index: Int) {
        var higherPriority = index
        let leftIndex = self.leftChild(of: index)
        let rightIndex = self.rightChild(of: index)

        if leftIndex < self.elements.endIndex && self.sortFunction(self.elements[leftIndex], self.elements[higherPriority]) {
            higherPriority = leftIndex
        }
        if rightIndex < self.elements.endIndex && self.sortFunction(self.elements[rightIndex], self.elements[higherPriority]) {
            higherPriority = rightIndex
        }
        if higherPriority != index {
            self.elements.swapAt(index, higherPriority)
            self.diveDown(from: higherPriority)
        }
    }
    
    mutating func buildHeap() {
        for i in (1...(self.elements.count / 2)).reversed() {
            self.diveDown(from: i)
        }
    }
    mutating func remove() -> T? {
        if self.isEmpty {
            return nil
        }
        self.elements.swapAt(1, self.elements.endIndex - 1)
        var returnValue = self.elements.removeLast()
        self.diveDown(from: 1)
        
        return returnValue
    }
}

'Swift > 자료구조' 카테고리의 다른 글

분리집합과 Union-Find Swift  (1) 2024.04.28
Swift 덱(Deque) 구현  (0) 2024.04.03
Swift 큐(Queue) 구현  (0) 2024.04.03
Swift 우선순위 큐(Priority Queue) 구현  (0) 2023.05.17
Swift 스택(Stack) 구현  (0) 2023.04.17

+ Recent posts