
기본 Swift에는 Python의 collections.deque가 없다.
Python에서는 collections.deque 덕분에 스택, 큐, 데크의 모든 연산을 O(1)의 시간 복잡도로 빠르게 처리할 수 있다.
그렇다면 Swift에서도 알고리즘 문제를 효율적으로 풀기 위해,
Deque 자료구조를 직접 하나 구현해두면 기본적인 큐/스택/데크 의 연산을 해결할 수 있당
데크는 주로 연결 리스트로 만드는데, 여기서는 스택 2개를 활용해보도록 하겠다.
참고로 Swift에서도
Collections라이브러리가 있다고 한다...! 링크
import Collections var deque: Deque<String> = ["Ted", "Rebecca"] deque.prepend("Keeley") deque.append("Nathan") print(deque) // ["Keeley", "Ted", "Rebecca", "Nathan"]하지만 사용이 불가능한 상황이 있을 수 있기도 하고 학습용으로 한번 구현해보기로.
말로 한번 정리해보자. 일반 Array에서 앞에다가 집어넣고 빼고 하면 무조건 시간복잡도가 O(n)이 되기 때문에 일반적인 방법으로는 안된다.
대신 앞쪽, 뒤쪽 두 개의 배열을 생성한다.
dequeArray: 앞쪽 전용enqueArray: 뒤쪽 전용pushFirst는 dequeArray에 append 한다. 여기서, 물리적으로 배열의 맨 앞에 넣는 게 아니라 그냥 append해서 뒤로 넣는다.pushLast는 enqueArray에 append 한다.pop 할 때가 중요하다.
참고로 여기서 ❗️논리적 허점❗️이 있다. 더 밑에 기술할 예정.
reversed를 좀 더 부연 설명하면,
let arr = [1, 2, 3] let rev = arr.reversed()이렇게 한다고 배열이 복사가 되지는 않는다. 메모리상에서 rev는 arr을 참조하고, rev에 접근하면 arr의 인덱스를 뒤에서부터 접근하여 거꾸로 읽을 뿐이다. '읽기 전용'이 된다는 뜻.
하지만 다음과 같이 배열에 담아버리면,let copied = Array(arr.reversed())여기서는 완전한 복사가 발생하여 새로운 배열을 만들고, 시간복잡도가 O(n)이 된다.
결국 필요 없다는걸 조금 나중에 알게 됐지만...
popFirst와 반대로 생각하면 된다.
struct Deque<T> {
var dequeArray = [T]()
var enqueArray = [T]()
mutating func pushFirst(k: T) {
dequeArray.append(k)
}
mutating func pushLast(k: T) {
enqueArray.append(k)
}
mutating func popFirst() {
if dequeArray.isEmpty {
let arr = enqueArray.reversed()
}
}
}
여기까지 구현해보다 막혔다. 위에서 쓴 ❗️논리적 허점❗️이 여기서 나왔다.
enqueArray를 뒤집어서 pop 해서 값을 출력하는것까지는 문제가 없다. 그런데 실제로 enqueArray에서 값을 제거 하려면 결국 새 배열을 만들던 deque에 복사하던 뒤집어진 배열을 복사해야 하고, 그렇게 되면 시간복잡도가 다시 O(n) 으로 돌아가는 것이다.
dfs, bfs 하면서 코드로 보기만 했던 인덱스 이동을 이래서 하는구만 하고 깨달았음.
실제로 값을 제거하는 것이 아니라, 인덱스를 이동해서 제거된 것처럼 보이게 하는 것이다.
그러면 인덱스를 추가해서 논리를 좀 더 보완해보자.
는 그대로 간다.
미리 배열을 적당한 크기로 만들어 놓고 반 잘라서 앞뒤를 나눌수도 있지만,
나는 일단 앞 배열(dequeArray) 과 뒷 배열(enqueArray)로 나누는 구조는 그대로 채택해서 가겠다.
이제 reversed는 필요 가 없다.
대신,
dequeIndex: dequeArray에서 몇개나 pop이 됐는지 관리enqueIndex: enqueArray에서 몇개나 pop이 됐는지 관리를 사용한다.
.isempty()가 아니라 dequeArray.count > dequeIndex 인지 확인해야 한다는 뜻이다.
popFirst의 반대로 하면 된다. 이제 쓰기 힘들다.
import Foundation
struct Deque<T> {
// stored property
var dequeArray = [T]()
var enqueArray = [T]()
var dequeIndex = 0
var enqueIndex = 0
// computed property
var isEmpty: Bool {
return dequeArray.count <= dequeIndex && enqueArray.count <= enqueIndex
}
var count: Int {
return dequeArray.count - dequeIndex + enqueArray.count - enqueIndex
}
// push
mutating func pushFirst(k: T) {
dequeArray.append(k)
}
mutating func pushLast(k: T) {
enqueArray.append(k)
}
// pop
mutating func popFirst() -> T? {
if isEmpty {
return nil
}
if dequeArray.count <= dequeIndex {
let first = enqueArray[enqueIndex]
enqueIndex += 1
return first
}
return dequeArray.popLast()
}
mutating func popLast() -> T? {
if isEmpty {
return nil
}
if enqueArray.count <= enqueIndex {
let last = dequeArray[dequeIndex]
dequeIndex += 1
return last
}
return enqueArray.popLast()
}
}
참고로 여기서 기술했던 것처럼 배열의 크기가 커지면 메모리를 위해 일정 간격으로 .removeFirst()를 이용해 잘라줘야 하는데, 문제풀이에 지장이 없어 일단 뺐다.
++
또 참고로,
computed Property로 작성되는 기준은 Swift API Design Guide - Conventions에 설명되어 있는데,
Document the complexity of any computed property that is not O(1). People often assume that property access involves no significant computation, because they have stored properties as a mental model. Be sure to alert them when that assumption may be violated.
즉 계산량이 거의 없고, 내부 상태 기준으로 간단히 판단할 수 있는 경우에는 computed property를 쓰는 것이 자연스럽다는 것이다. 이런 기준에 따라, isEmpty, count처럼 시간복잡도가 O(1)이고 side effect도 없는 경우엔 computed property로 만드는 것이 권장된다고 한다. 궁금해서 찾아봄.