세그먼트 트리

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

일반적으로 배열에서 특정 구간의 합을 구해야 한다면 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

문제

초기에 𝑛+1개의 집합 {0},{1},{2},…,{𝑛}이 있다. 여기에 합집합 연산과, 두 원소가 같은 집합에 포함되어 있는지를 확인하는 연산을 수행하려고 한다.

집합을 표현하는 프로그램을 작성하시오.

입력

첫째 줄에 𝑛, 𝑚이 주어진다. 𝑚은 입력으로 주어지는 연산의 개수이다. 다음 𝑚개의 줄에는 각각의 연산이 주어진다. 합집합은 0 𝑎 𝑏의 형태로 입력이 주어진다. 이는 𝑎가 포함되어 있는 집합과, 𝑏가 포함되어 있는 집합을 합친다는 의미이다. 두 원소가 같은 집합에 포함되어 있는지를 확인하는 연산은 1 𝑎 𝑏의 형태로 입력이 주어진다. 이는 𝑎와 𝑏가 같은 집합에 포함되어 있는지를 확인하는 연산이다.

출력

1로 시작하는 입력에 대해서 𝑎와 𝑏가 같은 집합에 포함되어 있으면 "YES" 또는 "yes"를, 그렇지 않다면 "NO" 또는 "no"를 한 줄에 하나씩 출력한다.

내가 푼 풀이

문제 자체는 어려워보이지 않았다

 

접근방법: Dictionary 자료구조 이용(실패)

- 딕셔너리를 이용하려고 했는데 알고보니 문제를 잘 이해하지 못하고 도전했다.

- 합집합을 한 결과가 해당 집합이 되는줄 알고, 예로 1과 3을 합집합연산하면 (1,3), (3,1)이 생기는줄 알았다.

 

접근방법: Union-Find

- 주어진문제에서 서로다른 원소들은 전체집합을 이루고, 합집합을 한다면 union으로 합쳐주고, 같은 원소인지 판단하려면

두개의 해당 원소들의 루트노드가 같은지 확인하면 됬다.

- 이번 기회에 Union-Find 알고리즘을 알게되었는데, 서로소 집합에대해 포함하는지 효율적으로 탐색할 수 있다고 알게되었다.

 

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

import Foundation

// 입력받기
let input = readLine()!.split(separator: " ").map{Int(String($0))!}
let n = input[0], m = input[1]
// Union-Find
// 처음의 원소는 자기 자신을 루트노드로 갖는다.
// parent[x] = a: x의 부모노드는 a다.
var parent = Array(repeating: 0, count: n+1)
for i in 1...n {
    parent[i] = i
}
// 두개의 서로소집합을 합친다.
// 단순히 y의 루트노드가 x를 가리키면 된다.
// 이 문제에서는 연산의 크기가 크지않아서 따로 설정하지 않았지만, 이 알고리즘은 트리의 depth에 따라 연산속도가 달라진다.
// 그래서 합치는 과정에서 depth가 큰노드가 작은노드를 루트노드로 합치는 방향이 더 좋다.
func union(_ x: Int, _ y: Int) {
    var rootX = find(x)
    var rootY = find(y)
    parent[rootY] = rootX
}
// 해당 원소의 루트노드를 찾을때까지 재귀호출
func find(_ x: Int) -> Int {
    if parent[x] == x { return x}
    parent[x] = find(parent[x])
    return parent[x]
}
// 연산 실행
for i in 0..<m {
    let op = readLine()!.split(separator: " ").map{Int(String($0))!}
    // 합집합은 서로 합쳐준다.
    if op[0] == 0 {
        union(op[1], op[2])
    } else {
        // 두 원소가 같은 집합에 있다는것은 둘다 같은 루트노드를 가리킴을 의미한다.
        let p1 = find(op[1])
        let p2 = find(op[2])
        print(p1 == p2 ? "YES" : "NO")
    }
}

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-2357 최솟값과 최댓값 Swift  (0) 2024.04.29
BOJ-2042 구간 합 구하기 Swift  (0) 2024.04.29
BOJ-2493 탑 Swift  (1) 2024.04.28
BOJ-1966 프린터 큐 Swift  (1) 2024.04.28
BOJ-2473 세 용액 Swift  (0) 2024.04.27

문제

KOI 통신연구소는 레이저를 이용한 새로운 비밀 통신 시스템 개발을 위한 실험을 하고 있다. 실험을 위하여 일직선 위에 N개의 높이가 서로 다른 탑을 수평 직선의 왼쪽부터 오른쪽 방향으로 차례로 세우고, 각 탑의 꼭대기에 레이저 송신기를 설치하였다. 모든 탑의 레이저 송신기는 레이저 신호를 지표면과 평행하게 수평 직선의 왼쪽 방향으로 발사하고, 탑의 기둥 모두에는 레이저 신호를 수신하는 장치가 설치되어 있다. 하나의 탑에서 발사된 레이저 신호는 가장 먼저 만나는 단 하나의 탑에서만 수신이 가능하다. 

예를 들어 높이가 6, 9, 5, 7, 4인 다섯 개의 탑이 수평 직선에 일렬로 서 있고, 모든 탑에서는 주어진 탑 순서의 반대 방향(왼쪽 방향)으로 동시에 레이저 신호를 발사한다고 하자. 그러면, 높이가 4인 다섯 번째 탑에서 발사한 레이저 신호는 높이가 7인 네 번째 탑이 수신을 하고, 높이가 7인 네 번째 탑의 신호는 높이가 9인 두 번째 탑이, 높이가 5인 세 번째 탑의 신호도 높이가 9인 두 번째 탑이 수신을 한다. 높이가 9인 두 번째 탑과 높이가 6인 첫 번째 탑이 보낸 레이저 신호는 어떤 탑에서도 수신을 하지 못한다.

탑들의 개수 N과 탑들의 높이가 주어질 때, 각각의 탑에서 발사한 레이저 신호를 어느 탑에서 수신하는지를 알아내는 프로그램을 작성하라. 

입력

첫째 줄에 탑의 수를 나타내는 정수 N이 주어진다. N은 1 이상 500,000 이하이다. 둘째 줄에는 N개의 탑들의 높이가 직선상에 놓인 순서대로 하나의 빈칸을 사이에 두고 주어진다. 탑들의 높이는 1 이상 100,000,000 이하의 정수이다.

출력

첫째 줄에 주어진 탑들의 순서대로 각각의 탑들에서 발사한 레이저 신호를 수신한 탑들의 번호를 하나의 빈칸을 사이에 두고 출력한다. 만약 레이저 신호를 수신하는 탑이 존재하지 않으면 0을 출력한다.

내가 푼 풀이

- 주어진 탑의 마지막순서부터 첫번째 순서로 이동하며, 자신의 탑보다 높은 탑이 있다면 수신가능

- 수신가능한 탑의 인덱스들을 출력.

 

접근방법: 스택

- 이 문제는 이전에 풀었던 오큰수 문제와 유사하다.

- 스택을 이용해서 배열을 탐색할때, 스택의 top이 현재원소보다 작다면 스택에 넣어주고, 크다면 스택의 선입후출방식을 이용해 작은값들을 전부 빼준다.

- 수신가능한 탑이 존재하지 않으면 0

 

import Foundation

// 입력받기
let N = Int(readLine()!)!
var arr = Array(readLine()!.split(separator: " ").map{Int(String($0))!}.reversed())
var stack = [Int]()
var result = Array(repeating: 0, count: N)

// 배열 arr은 주어진 탑의 순서를 뒤집어놓았다.
// arr순회할때마다, 스택이 비어있거나 스택의 top이 현재 값보다 작다면 스택에 추가한다.
// 스택의 top보다 현재 주어진 값이 더 크다면 스택에 있는 모든 값들을 현재값들과 비교하여 pop한다.
for i in 0..<arr.count {
    if stack.isEmpty {
        stack.append(i)
        continue
    }
    while !stack.isEmpty && arr[i] > arr[stack[stack.count - 1]] {
        // 정답의 인덱스는 첫번째가 1이므로 i+1 (0은 수신불가표시하기위해)
        result[stack.removeLast()] = i+1
    }
    stack.append(i)
}

// 배열 뒤집어놓은것을 다시 돌려놓고, 출력
for i in 0..<result.count {
    if result[i] == 0 { continue }
    result[i] = (N+1) - result[i]
}
print(Array(result.reversed()).map{String($0)}.joined(separator: " "))

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-2042 구간 합 구하기 Swift  (0) 2024.04.29
BOJ-1717 집합의 표현 Swift  (0) 2024.04.28
BOJ-1966 프린터 큐 Swift  (1) 2024.04.28
BOJ-2473 세 용액 Swift  (0) 2024.04.27
BOJ-2343 기타 레슨 Swift  (1) 2024.04.27

문제

여러분도 알다시피 여러분의 프린터 기기는 여러분이 인쇄하고자 하는 문서를 인쇄 명령을 받은 ‘순서대로’, 즉 먼저 요청된 것을 먼저 인쇄한다. 여러 개의 문서가 쌓인다면 Queue 자료구조에 쌓여서 FIFO - First In First Out - 에 따라 인쇄가 되게 된다. 하지만 상근이는 새로운 프린터기 내부 소프트웨어를 개발하였는데, 이 프린터기는 다음과 같은 조건에 따라 인쇄를 하게 된다.

  1. 현재 Queue의 가장 앞에 있는 문서의 ‘중요도’를 확인한다.
  2. 나머지 문서들 중 현재 문서보다 중요도가 높은 문서가 하나라도 있다면, 이 문서를 인쇄하지 않고 Queue의 가장 뒤에 재배치 한다. 그렇지 않다면 바로 인쇄를 한다.

예를 들어 Queue에 4개의 문서(A B C D)가 있고, 중요도가 2 1 4 3 라면 C를 인쇄하고, 다음으로 D를 인쇄하고 A, B를 인쇄하게 된다.

여러분이 할 일은, 현재 Queue에 있는 문서의 수와 중요도가 주어졌을 때, 어떤 한 문서가 몇 번째로 인쇄되는지 알아내는 것이다. 예를 들어 위의 예에서 C문서는 1번째로, A문서는 3번째로 인쇄되게 된다.

입력

첫 줄에 테스트케이스의 수가 주어진다. 각 테스트케이스는 두 줄로 이루어져 있다.

테스트케이스의 첫 번째 줄에는 문서의 개수 N(1 ≤ N ≤ 100)과, 몇 번째로 인쇄되었는지 궁금한 문서가 현재 Queue에서 몇 번째에 놓여 있는지를 나타내는 정수 M(0 ≤ M < N)이 주어진다. 이때 맨 왼쪽은 0번째라고 하자. 두 번째 줄에는 N개 문서의 중요도가 차례대로 주어진다. 중요도는 1 이상 9 이하의 정수이고, 중요도가 같은 문서가 여러 개 있을 수도 있다.

출력

각 테스트 케이스에 대해 문서가 몇 번째로 인쇄되는지 출력한다.

내가 푼 풀이

- 큐를 구현하고, 위 동작들을 메서드로 구현해준다.

 

import Foundation

// 입력받기
let T = Int(readLine()!)!

for i in 0..<T {
    let input = readLine()!.split(separator: " ").map{Int(String($0))!}
    let N = input[0], M = input[1]
    var arr = readLine()!.split(separator: " ").map{Int(String($0))!}
    var queue = [(idx: Int, ipt: Int)]()
    var count = 0
    for j in 0..<arr.count {
        queue.append((idx: j, ipt: arr[j]))
    }
    // 궁금한 문서 뽑을때까지 반복
    while true {
        if isImportant(queue) {
            count += 1
            if pop(&queue).idx == M { break }
        } else {
            moveBack(&queue)
        }
    }
    print(count)
}

// 첫번째원소가 중요도가 가장 높은지 파악
func isImportant(_ queue: [(idx: Int, ipt: Int)]) -> Bool {
    guard let top = top(queue) else { return false }
    var max = Int.min
    for i in queue {
        if i.ipt > max { max = i.ipt }
    }
    if max == top.ipt { return true } else { return false }
}
// 첫번째 원소 출력
func top(_ queue: [(idx: Int, ipt: Int)]) -> (idx: Int, ipt: Int)? {
    if queue.isEmpty { return nil }
    return queue[0]
}
// 첫번째 원소 Pop
func pop(_ queue: inout [(idx: Int, ipt: Int)]) -> (idx: Int, ipt: Int) {
    var a = Array(queue.reversed())
    let num = a.removeLast()
    queue = Array(a.reversed())
    return num
}
// 첫번째 원소 뒤로 이동
func moveBack(_ queue: inout [(idx: Int, ipt: Int)]) {
    let num = pop(&queue)
    queue.append(num)
}

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-1717 집합의 표현 Swift  (0) 2024.04.28
BOJ-2493 탑 Swift  (1) 2024.04.28
BOJ-2473 세 용액 Swift  (0) 2024.04.27
BOJ-2343 기타 레슨 Swift  (1) 2024.04.27
BOJ-2458 키 순서 Swift  (1) 2024.04.25

문제

KOI 부설 과학연구소에서는 많은 종류의 산성 용액과 알칼리성 용액을 보유하고 있다. 각 용액에는 그 용액의 특성을 나타내는 하나의 정수가 주어져있다.  산성 용액의 특성값은 1부터 1,000,000,000까지의 양의 정수로 나타내고, 알칼리성 용액의 특성값은 -1부터 -1,000,000,000까지의 음의 정수로 나타낸다.

같은 양의 세 가지 용액을 혼합한 용액의 특성값은 혼합에 사용된 각 용액의 특성값의 합으로 정의한다. 이 연구소에서는 같은 양의 세 가지 용액을 혼합하여 특성값이 0에 가장 가까운 용액을 만들려고 한다. 

예를 들어, 주어진 용액들의 특성값이 [-2, 6, -97, -6, 98]인 경우에는 특성값이 -97와 -2인 용액과 특성값이 98인 용액을 혼합하면 특성값이 -1인 용액을 만들 수 있고, 이 용액이 특성값이 0에 가장 가까운 용액이다. 참고로, 세 종류의 알칼리성 용액만으로나 혹은 세 종류의 산성 용액만으로 특성값이 0에 가장 가까운 혼합 용액을 만드는 경우도 존재할 수 있다.

산성 용액과 알칼리성 용액이 주어졌을 때, 이 중 같은 양의 세 개의 서로 다른 용액을 혼합하여 특성값이 0에 가장 가까운 용액을 만들어내는 세 용액을 찾는 프로그램을 작성하시오.

입력

첫째 줄에는 전체 용액의 수 N이 입력된다. N은 3 이상 5,000 이하의 정수이다. 둘째 줄에는 용액의 특성값을 나타내는 N개의 정수가 빈칸을 사이에 두고 주어진다. 이 수들은 모두 -1,000,000,000 이상 1,000,000,000 이하이다. N개의 용액들의 특성값은 모두 다르고, 산성 용액만으로나 알칼리성 용액만으로 입력이 주어지는 경우도 있을 수 있다.

출력

첫째 줄에 특성값이 0에 가장 가까운 용액을 만들어내는 세 용액의 특성값을 출력한다. 출력해야하는 세 용액은 특성값의 오름차순으로 출력한다. 특성값이 0에 가장 가까운 용액을 만들어내는 경우가 두 개 이상일 경우에는 그 중 아무것이나 하나를 출력한다.

내가 푼 풀이

접근방법: 이분탐색

이전에 두용액문제를 풀고왔더니 너무 쉽게 파악해버렸다

두 용액을 섞을땐 용액의수가 최대 10만이였는데 이 문제에선 최대 5000 이다.

단순히 두 용액을 고정하고 한 용액을 순회하며 섞어봐도 정답은 나오지만 5000^3 = 1250억번 연산이므로 시간초과가 무조건 뜰것이다.

그래서 두개의 용액을 고정하고 한 용액을 구할때 이분탐색을 이용했다.

시간복잡도는 O(N^2logN)으로 이문제에선 약 9700만번연산(1억보다 적다)으로 시간안에 풀어졌다.

 

이분탐색

용액을 섞었을때, 음수면 start인덱스가 mid+1 양수면 end인덱스가 mid-1 해주어 0과 가장 가깝게 만든다.

 

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

import Foundation

// 입력받기
let N = Int(readLine()!)!
var arr = readLine()!.split(separator: " ").map{Int(String($0))!}
var minSum = Int.max
var answer = [Int]()
// 이분탐색 사용을위해 정렬
arr.sort(by: <)


// 이분탐색
for i in 0..<N {
    for j in i+1..<N {
        // 두용액을 고정한다.
        let first = arr[i]
        let second = arr[j]
        let sum = first + second
        var s = j+1
        var e = N-1
        // 나머지 한용액을 이분탐색을 이용해 구해준다.
        while s <= e {
            let m = (s+e)/2
            let total = sum + arr[m]
            // 0이되면 바로 값 갱신 후 탈출
            if total == 0 {
                answer = [arr[i],arr[j],arr[m]]
                minSum = total
                break
            }
            // 합쳐진 용액이 음수면 나머지 용액을 더 큰수로, 양수면 더 작은수로
            if total < 0 {
                s = m+1
            } else {
                e = m-1
            }
            // 최솟값이 나오면 항상 값을 갱신해준다.
            if minSum > abs(total) {
                minSum = abs(total)
                answer = [arr[i],arr[j],arr[m]]
            }
        }
        
    }
}

print("\(answer[0]) \(answer[1]) \(answer[2])")

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-2493 탑 Swift  (1) 2024.04.28
BOJ-1966 프린터 큐 Swift  (1) 2024.04.28
BOJ-2343 기타 레슨 Swift  (1) 2024.04.27
BOJ-2458 키 순서 Swift  (1) 2024.04.25
BOJ-11404 플로이드 Swift  (0) 2024.04.25

문제

강토는 자신의 기타 강의 동영상을 블루레이로 만들어 판매하려고 한다. 블루레이에는 총 N개의 강의가 들어가는데, 블루레이를 녹화할 때, 강의의 순서가 바뀌면 안 된다. 순서가 뒤바뀌는 경우에는 강의의 흐름이 끊겨, 학생들이 대혼란에 빠질 수 있기 때문이다. 즉, i번 강의와 j번 강의를 같은 블루레이에 녹화하려면 i와 j 사이의 모든 강의도 같은 블루레이에 녹화해야 한다.

강토는 이 블루레이가 얼마나 팔릴지 아직 알 수 없기 때문에, 블루레이의 개수를 가급적 줄이려고 한다. 오랜 고민 끝에 강토는 M개의 블루레이에 모든 기타 강의 동영상을 녹화하기로 했다. 이때, 블루레이의 크기(녹화 가능한 길이)를 최소로 하려고 한다. 단, M개의 블루레이는 모두 같은 크기이어야 한다.

강토의 각 강의의 길이가 분 단위(자연수)로 주어진다. 이때, 가능한 블루레이의 크기 중 최소를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 강의의 수 N (1 ≤ N ≤ 100,000)과 M (1 ≤ M ≤ N)이 주어진다. 다음 줄에는 강토의 기타 강의의 길이가 강의 순서대로 분 단위로(자연수)로 주어진다. 각 강의의 길이는 10,000분을 넘지 않는다.

출력

첫째 줄에 가능한 블루레이 크기중 최소를 출력한다.

내가 푼 풀이

접근방법: 이분탐색

- 담을수 있는 블루레이의 길이는 최소 1부터 최대 기타강의의 모든길이를 더한값이 된다.

- 이 범위를 이분탐색으로 최소크기를 구한다.

 

 

주의해야할 점)

- 기타강의는 순서가 바뀌면안되므로 주어진 순서를 지켜야한다.

- 블루레이의 크기가 기타강의 한개의 길이보다 작을 수 있다.

- 기타강의를 빼먹지않고 모두 블루레이에 담아야한다.

 

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

import Foundation

// 입력받기
let input = readLine()!.split(separator: " ").map{Int(String($0))!}
let N = input[0], M = input[1]
var arr = readLine()!.split(separator: " ").map{Int(String($0))!}
var start = 1
var end = arr.reduce(0, +)

// 이분탐색
while start <= end {
    // 이분탐색의 중간값
    let mid = (start + end) / 2
    var sum = 0
    var count = 0
    var left = 0
    var pass = false
    
    // 블루레이에 강의를 최대한 담아본다.
    // 강의길이가 mid값보다 크다면 아에 담을 수 없으므로 이 길이는 사용할 수 없다 (더 길게해서 담아봐야함)
    while left < arr.count {
        sum += arr[left]
        if sum > mid {
            let num = sum - arr[left]
            if num != 0 && num <= mid {
                count += 1
            } else {
                pass = true
                break
            }
            sum = arr[left]
        }
        left += 1
    }
    // 최대한 담아보고 남은 강의가 있고, mid보다 작다면 블루레이에 담을 수 있으므로 +1
    // 남은 강의가 있지만 블루레이보다 크다면 담을 수 없다. pass = true
    if sum != 0 && sum <= mid {
        count += 1
    } else {
        pass = true
    }
    // 블루레이 크기가 너무작아 모든강의를 담을 수 없었다면 블루레이 크기를 늘린다.
    if pass {
        start = mid + 1
    } else {
        // 모든 강의를 담아봤지만 정한 갯수보다 작거나 같다면 갯수를 늘리기위해 블루레이 크기를 줄인다.
        // 모든 강의를 담아봤지만 정한 갯수보다 크다면 갯수를 줄이기위해 블루레이 크기를 늘린다.
        if count <= M {
            end = mid - 1
        } else {
            start = mid + 1
        }
    }
}
print(start)

 

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-1966 프린터 큐 Swift  (1) 2024.04.28
BOJ-2473 세 용액 Swift  (0) 2024.04.27
BOJ-2458 키 순서 Swift  (1) 2024.04.25
BOJ-11404 플로이드 Swift  (0) 2024.04.25
BOJ-4485 녹색 옷 입은 애가 젤다지? Swift  (0) 2024.04.24

 

 

 

분할정복

분할 정복은 문제를 부분문제로 나누어 문제를 해결할 수 있는단위까지 나눈 후, 해결하고 다시 조합하는 방법이다.

분할정복은 세단계로 구성되어있다.

 

분할: 문제를 분할할 수 있을 만큼 같은 유형의 문제로 분할한다.

정복: 분할된 작은 문제를 해결한다.

조합: 해결된 작은 부분문제들을 조합하여 정답을 출력한다.

 

 

※ 분할정복과 동적 계획법 차이:

분할정복은 큰 문제를 여러개의 작은 문제로 나누어 해결하고, 이를 모두 조합한다.

동적계획법은 작은 문제부터 해결하여 기록하고, 기록된 정보를 통해 큰문제를 해결해나간다.

 

분할정복은 대표적으로 합병정렬과 퀵 정렬이 있다.

 

 

합병정렬

분할정복의 대표적인 정렬 알고리즘이다.

 

합병정렬의 단계

1. 주어진 배열의 크기가 0또는 1이라면 이미 정렬이 되있다고 간주하고, 그렇지 않다면 2개의 부분배열로 나눈다.

2. 부분배열의 크기가 정렬할 수 있을만큼 작지않다면 1번을 재호출한다.

3. 나눠진 부분배열을 합친다. 이때 순서에 맞춰서 정렬한다.

4. 부분배열을 조합하여 결과를 출력한다.

 

합병정렬의 특징:

- 합병정렬의 조합단계에서 따로 저장해둘 배열로부터 값을 가져오므로 입력과 같은 공간의 임시배열이 필요하다.

- 재귀호출로 인해 오버헤드가 발생할 수 있다.

- 연결리스트를 정렬할때는 다른 정렬방법보다 더 효율적이다.

 

Swift 코드로 구현

 

// 합병 정렬
func mergeSort(arr: inout [Int], p: Int, q: Int) -> [Int] {
	// 따로 저장해둘 공간(입력된배열의 크기만큼)
    var result = Array(repeating: 0, count: arr.count)
    // 배열의 크기가 1 또는 0이 될때까지 최대로 나누어준다.
    if p < q {
        // 배열의 중간인덱스
        var k = (p + q) / 2
        // 배열을 둘로 나누었을때, 왼쪽배열과 오른쪽배열
        var leftArr = mergeSort(arr: &arr, p: p, q: k)
        var rightArr = mergeSort(arr: &arr, p: k+1, q: q)
        
        // 배열이 최대로 나누어졌을때 아래 행동이 시작된다.
        // 분할된 배열들을 정렬하며 합친다.
        var i = p
        var j = k+1
        var d = p
        
        // 왼쪽배열과 오른쪽배열중 한 배열이 모두 정렬이 완료될때까지
        while i <= k && j <= q {
            // 왼쪽배열 원소가 더크면 오른쪽배열 원소를 넣고, 오른쪽배열 원소가 더 크면 왼쪽배열 원소를 넣는다.
            if leftArr[i] < rightArr[j] {
                result[d] = leftArr[i]
                i += 1
            } else {
                result[d] = rightArr[j]
                j += 1
            }
            d += 1
        }
        // 한쪽의 배열이 모두 정렬되었을때, 다른 한쪽은 원소가 남아있기때문에 그대로 붙여준다.
        // 남아있는 원소는 이미 이전에 정렬되어있는 원소기때문에 그대로 붙여도 좋다.
        if i > k {
            for index in j...q {
                result[d] = rightArr[index]
                d += 1
            }
        } else if j > q {
            for index in i...k {
                result[d] = leftArr[index]
                d += 1
            }
        }
        
        // 정렬된 원소를 배열에 넣고 리턴
        for i in p...q {
            arr[i] = result[i]
        }
    }
    return arr
}

 

입력 & 결과:

var A = [37, 10, 22, 30, 35, 13, 25, 24]
print(mergeSort(arr: &A, p: 0, q: A.count-1))
// [10, 13, 22, 24, 25, 30, 35, 37]

 

위 코드 재귀호출의 흐름은 다음과 같다.

1. 왼쪽배열을 분할

2. [37, 10] 정렬 후 결합

3. [22,30] 정렬 후 결합

4. 두 배열을 정렬 후 결합

5. 왼쪽배열: [10,22,30,37]

6. 오른쪽배열을 분할

7. [35,13] 정렬 후 결합

8. [25,24] 정렬 후 결합

9 두 배열을 정렬 후 결합

10. 오른쪽 배열: [13, 24, 25, 35] 

11. 왼쪽배열과 오른쪽배열을 정렬 후 결합하여 리턴

 

이름 최선 평균 최악 메모리
합병정렬 O(nlogn) O(nlogn) O(nlogn) O(n)

 

 

퀵정렬

퀵정렬은 임의의 수 하나를 골라, 그 수보다 작으면 왼쪽에, 크다면 오른쪽에 정렬하는 방법이다.

 

퀵정렬 과정

정복: 임의의수 n을 기준으로 n보다 작은수는 왼쪽배열, n보다 큰수는 오른쪽배열에 배치

분할: 배치된 배열에서 왼쪽배열과 오른쪽배열 재귀호출

 

퀵정렬은 정복을 먼저하고 분할하여 재귀호출을 한다.

 

세부과정

1. 기준이되는 원소를 선택

2. 기준원소의 인덱스를 저장하고, 나머지 원소를 검사하며 기준원소보다 작다면 인덱스+1 후 원소와 저장된인덱스와 자리바꿈

3. 모든 원소 검사 후 기준원소와 인덱스와 자리바꿈

(여기까지 완료된다면 배열은 기준원소를 기준으로 왼쪽에는 작은값, 오른쪽엔 큰값 배치됨) (정복)

4. 기준원소를 제외한 왼쪽배열과 오른쪽배열을 재귀호출한다.(분할)

5. 1-4를 반복하여 결과 도출

 

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

// 퀵 정렬
func quickSort(arr: inout [Int], left: Int, right: Int) -> [Int] {
    // 배열의 크기가 2 이상이면 정복, 아니면 그냥 출력
    if left < right {
        // 기준점 선택(가장왼쪽)
        var pivot = arr[left]
        // 기준점이 위치할 곳을 저장
        var j = left
        // 기준점을 제외한 배열의 원소들을 검사
        for i in left+1...right {
            // 원소가 기준점보다 작다면 j위치에 있는 원소와 바꿈
            // 원소가 기준점보다 크다면 패스
            // 이렇게 반복하면 j위치보다 이전에 위치한 원소들은 기준점보다 작은 원소들이다.
            if arr[i] < pivot {
                j += 1
                arr.swapAt(i, j)
            }
        }
        // 기준점과 j위치와 원소 바꾸기
        // 기준점은 중간에 위치하게되고, 기준점 왼쪽은 기준점보다 작은원소, 오른쪽은 기준점보다 큰원소가 배치됨
        arr.swapAt(j, left)
        // 분할
        // 왼쪽배열로 재귀호출
        // 오른쪽배열로 재귀호출
        quickSort(arr: &arr, left: left, right: j-1)
        quickSort(arr: &arr, left: j+1, right: right)
    }
    
    return arr
}

입력 및 결과:

var A = [25, 10, 22, 30, 35, 13, 37, 24]
print(quickSort(arr: &A, left: 0, right: A.count-1))
// [10, 13, 22, 24, 25, 30, 35, 37]

 

위 코드 재귀호출의 흐름은 다음과 같다.

1. 기준점:25 를 기준으로 [작은값, 25, 큰값] 배치

2. [작은값], [큰값] 재귀호출

3. 1-2 반복(기준점을 제외한 원소가 없을때까지)

 

이름 최선 평균 최악 메모리
퀵정렬 O(nlogn) O(nlogn) O(n^2) O(nlogn)

 

퀵정렬의 최악의경우는 이미 정렬된 데이터를 정렬할 경우이다.

 

+ Recent posts