Sequence
, Collection
, BidirectionalCollection
, RandomAccessCollection
등 이 있다. Sequence 타입은 해당 요소에 대한 순차적 접근을 제공한다.
무한하거나 유한하다.
한 번 이상 이터레이트할 수도 있지만 한 번 이상 이터레이트가 가능할지에 대해서는 장담할 수 없다
이터레이트(Iterate): 반복하다라는 의미로 요소를 순회하며 반복 작업을 수행하는 과정을 말한다.
모든 Collection은 Sequence를 상속받아 구현되었으며, Collection은 Sequence가 지닌 두 가지 문제점을 해결했다.
우선, 모든 Collection은 유한하며, 몇 개의 요소로 구성되어 있는지 항상 알 수 있다.
또한 Sequence에서는 오직 한 번만 이터레이트가 가능했던 것을 Collection에서는 원하는 만큼 할 수 있다.
BidirectionalCollection은 한 가지만 제외하고는 Collection과 매우 유사하다. Collection이 Sequence를 상속받은 것처럼, BidirectionalCollection은 Collection을 상속받았지만 정방향뿐만 아니라 역방향으로도 탐색할 수 있다는 차이가 있다.
역방향으로 이동하는 기능을 위해 index(before: ) 함수가 추가되었다.
RandomAccessCollection은 빠르게 요소에 접근할 수 있게 도와준다. 순서대로 하나씩 요소에 접근하는 대신, 해당 요소로 바로 접근할 수 있다.
Collection 중간의 요소를 다른 것으로 교체할 수 있는 기능을 제공한다.
요소들의 값을 설정할 수 있는 기능을 제공한다.
Sequence
프로토콜을 채택할 수 있다.Collection
프로토콜을 채택할 수 있다.Collection
은 서브스크립트를 통한 접근도 가능하며, 이는 인덱스를 컬렉션 내의 값에 매핑할 수 있다는 고급 용어이다. Collection
프로토콜 메서드의 성능을 결정하는 주요 지표는 Index
를 값에 매핑하는 속도이다. Array
와 같은 다른 저장 옵션과는 달리, 연결 리스트는 정수 인덱스를 사용하여 O(1) 서브스크립트 연산을 달성할 수 없다.서브스크립트 연산: 컬렉션, 리스트, 시퀀스 등의 요소에 접근하기 위해 사용되는 짧은 문법이다. 서브스크립트를 사용하면, 인스턴스 뒤에 대괄호를 사용해 인덱스를 넣어 값을 가져오거나 설정할 수 있다. 예를 들어, Swift의 배열(Array)에서 서브스크립트를 사용해 특정 위치의 요소를 가져오거나 변경할 수 있다.
var numbers = [10, 20, 30, 40, 50]
let firstNumber = numbers[0] // 10을 가져옴
numbers[1] = 25 // 배열의 두 번째 요소를 25로 변경
extension LinkedList: Collection {
public struct Index: Comparable {
public var node: Node<Value>?
static public func ==(lhs: Index, rhs: Index) -> Bool {
switch (lhs.node, rhs.node) {
case let (left?, right?) :
return left.next === right.next
case (nil, nil):
return true
default:
return false
}
}
static public func < (lhs: Index, rhs: Index ) -> Bool {
guard lhs != rhs else {
return false
}
let nodes = sequence(first: lhs.node) { $0?.next }
return nodes.contains { $0 === rhs.node }
}
}
public var startIndex: Index {
Index(node: head)
}
public var endIndex: Index {
Index(node: tail?.next)
}
public func index(after i: Index) -> Index {
Index(node: i.node?.next)
}
public subscript(position: Index) -> Value {
position.node!.value
}
}
startIndex
는 linked list의 head이다. Collection
에서 endIndex
는 마지막으로 접근 가능한 값의 바로 다음 인덱스이므로 tail?.next로 할당한다. index(after:)
는 node의 next node를 반환한다. subscript
는 Index를 collecton 값에 매핑하는데 사용한다. 사용자 정의 인덱스를 생성했으므로 노드의 값을 참조하여, 일정한 시간 내에 쉽게 인덱스를 생성할 수 있다. COW는 프로그래밍에서 효율적인 데이터 관리를 위해 사용되는 기법 중 하나이다. 이 전략은 데이터의 복사본을 만들 때 실제 복사가 필요한 시점까지 기다렸다가, 데이터에 변경이 발생할 때에만 복사본을 생성한다.
COW를 사용하면 메모리 사용량을 줄일 수 있고, 프로그램의 성능을 향상시킬 수 있다. 데이터를 복사할 때마다 전체 데이터를 복사하는 대신, 데이터를 변경해야 하는 시점에서만 복사를 수행하므로 효율적이다.
let array1 = [1, 2]
var array2 = array1
print("array1: \(array1)")
print("array2: \(array2)")
print("---After adding 3 to array 2---")
array2.append(3)
print("array1: \(array1)")
print("array2: \(array2)")
array1: [1, 2]
array2: [1, 2]
---After adding 3 to array 2---
array1: [1, 2]
array2: [1, 2, 3]
var list1 = LinkedList<Int>()
list1.append(1)
list1.append(2)
var list2 = list1
print("List1: \(list1)")
print("List2: \(list2)")
print("After appending 3 to list2")
list2.append(3)
print("List1: \(list1)")
print("List2: \(list2)")
List1: 1 -> 2
List2: 1 -> 2
After appending 3 to list2
List1: 1 -> 2 -> 3
List2: 1 -> 2 -> 3
private mutating func copyNodes() {
guard !isKnownUniquelyReferenced(&head) else {
return
}
guard var oldNode = head else {
return
}
head = Node(value: oldNode.value)
var newNode = head
while let nextOldNode = oldNode.next {
newNode!.next = Node(value: nextOldNode.value)
newNode = newNode.next
oldNode = nextOldNode
}
tail = newNode
}
mutating
키워드로 표시된 모든 다른 메서드를 찾아서 각 메서드의 최상단에서 copyNodes
를 호출한다. Linked List(1) 참고
- push
- append
- insert(after: )
- pop
- removeLast
- remove(after: )
isKnownUniquelyReferenced
라는 함수가 있다. 이 함수는 객체가 정확히 하나의 참조를 가지고 있는지 여부를 확인하는 데 사용할 수 있다. guard !isKnownUniquelyReferenced(&head) else {
return
}
print("Removing middle node on list2")
if let node = list2.node(at: 0) {
list2.remove(after: node)
}
print("List2: \(list2)")
---Example of linked list cow---
List1: 1 -> 2
List2: 1 -> 2
After appending 3 to list2
List1: 1 -> 2
List2: 1 -> 2 -> 3
Removing middle node on list2
List2: 1 -> 2 -> 3
private mutating func copyNodes(returningCopyOf node: Node<Value>?) -> Node<Value>? {
guard !isKnownUniquelyReferenced(&head) else {
return nil
}
guard var oldNode = head else {
return nil
}
head = Node(value: oldNode.value)
var newNode = head
var nodeCopy: Node<Value>?
while let nextOldNode = oldNode.next {
if oldNode === node {
nodeCopy = newNode
}
newNode!.next = Node(value: nextOldNode.value)
newNode = newNode!.next
oldNode = nextOldNode
}
return nodeCopy
}
@discardableResult
public mutating func remove(after node: Node<Value>) -> Value? {
guard let node = copyNodes(returningCopyOf: node) else { return nil }
defer {
if node.next === tail {
tail = node
}
node.next = node.next?.next
}
return node.next?.value
}
@discardableResult
public mutating func insert(_ value: Value,
after node: Node<Value>
) -> Node<Value> {
if node === head {
let node = copyNodes(returningCopyOf: node)!
guard tail !== node else {
append(value)
return tail!
}
node.next = Node(value: value, next: node.next)
return node.next!
} else {
copyNodes()
}
guard tail !== node else {
append(value)
return tail!
}
node.next = Node(value: value, next: node.next)
return node.next!
}
// 1
var list55 = list
list.removeLast()
list.insert(5, after: list.node(at: 3)!)
// 2
var list55 = list
list.insert(5, after: list.node(at: 3)!)
list.removeLast()
@discardableResult
public mutating func insert(_ value: Value,
after node: Node<Value>
) -> Node<Value> {
guard let node = copyNodes(returningCopyOf: node) else {
guard tail !== node else {
append(value)
return tail!
}
node.next = Node(value: value, next: node.next)
return node.next!
}
guard tail !== node else {
append(value)
return tail!
}
node.next = Node(value: value, next: node.next)
return node.next!
}
재귀 함수를 활용하면 쉽게 풀 수 있다.
노드를 끝까지 재귀함수로 돌고 마지막 노드에서 부터 출력하면 되는데 LinkedList는 node를 가리키는게 head와 tail밖에 없으니, node를 직접 건드릴 수 있는 함수를 생성하고 private 하게 설정하면 안전한 함수를 만들 수 있다.
private func reverseNode<T>(_ node: Node<T>?) {
guard let node = node else { return }
reverseNode(node.next)
print(node.value)
}
func reverseLinkedList<T>(_ list: LinkedList<T>) {
reverseNode(list.head)
}
처음 문제를 풀땐 Collection protocol이 적용되었으니 .count 메서드를 활용해서 문제를 해결했다.
하지만 Collection protocol이 적용되지 않았을 경우도 생각해서 문제를 해결해야 하는데, 그 방법은 Runner's techique 라는 방법이 있다.
Runner's techique는 연결 리스트를 순회할 때 두 개의 포인터를 사용하는 기술이다. 보통 두 개의 포인터 중 하나의 다른 하나보다 앞서게 하여 리스트를 한 번만 순회하지만, Runner's technique를 사용하면 리스트를 한 번 더 순회하여 원하는 노드를 찾을 수 있다.
이를 통해 시간 복잡도를 개선할 수 있다.
while문에서, 다음 노드를 nextFast에 바인딩한다. 만약 다음 노드가 존재한다면, fast를 nextFast의 다음 노드로 업데이트하여 리스트를 두 번 탐색한다. slow 포인터는 한 번만 업데이트된다.
// Runner's techique 활용 전 / Collection Protocol 적용 필수
func getMiddle<T>(_ list: LinkedList<T>) -> Node<T>? {
var middleCount = Int(ceil(Double(list.count + 1)/ Double(2)))
var count = 1
var currentNode = list.head
while (currentNode?.next) != nil {
count += 1
currentNode = currentNode?.next
if count == middleCount {
return currentNode
}
}
return currentNode
}
// Runner's techique 활용 후 / Collecton Protocol 적용 필수 x
func getMiddle<T>(_ list: LinkedList<T>) -> Node<T>? {
var slow = list.head
var fast = list.head
// slow 한 번 이동할때 fast 두 번 이동 -> 1:2 비율
while let nextFast = fast?.next {
fast = nextFast.next
slow = slow?.next
}
return slow
}
이러한 함수는 Swift 기본 함수처럼 LinkedList 내부 함수로 만드는 것이 더 효율적일 것 같다.
extension LinkedList {
mutating func reverse() {
tail = head
var prev = head
var current = head?.next
prev?.next = nil
while current != nil {
let next = current?.next
current?.next = prev
prev = current
current = next
}
head = prev
}
}
두 함수 비교시 비교연산자를 활용하기 때문에 <\T> 형의 Comparable 프로토콜을 채택해야 한다.
- 두 연결리스트의 isEmpty 상태를 확인
최초 head 값 선정- 둘 중 다 순회 돌기 전까지 while문 실행
- 남은 리스트 값 이어 붙이기
// 제너릭을 활용하기 앞서 <T>의 Comparable 프로토콜 선언해줘야 한다!!
func mergeSorted<T: Comparable>(_ left: LinkedList<T>, _ right: LinkedList<T>) -> LinedList<T> {
guard !left.isEmpty else {
return right
}
guard !right.isEmpty else {
return left
}
var newHead: Node<T>?
// 순회 돌 변수
var tail: Node<T>?
var currentLeft = left.head
var currentRight = right.head
if let leftNode = currentLeft, let rightNode = currentRight {
if leftNode.value < rightNode.value {
newHead = leftNode
currentLeft = leftNode.next
} else {
newHead = rightNode
currentRight = rightNode.next
}
tail = newHead
}
while let leftNode = currentLeft, let rightNode = currentRight {
if leftNode.value < rightNode.value {
tail?.next = leftNode
currentLeft = leftNode.next
} else {
tail?.next = rightNode
currentRight = rightNode.next
}
tail = tail?.next
}
if let leftNode = currentLeft {
tail?.next = leftNode
}
if let rightNode = currentRight {
tail?.next = rightNode
}
var list = LinkedList<T>()
list.head = newHead
list.tail = {
while let next = tail?.next {
tail = next
}
return tail
}()
return list
}
reverse() 함수와 마찬가지로 내부함수로 실행하는 것이 효율적이다.
기존 LinkedList를 돌면서 주어진 값과 같은지 비교검사를 해야하기때문에 Equtable 프로토콜을 채택해야 한다.
extension LinkedList where Value: Equtable {
mutating func removeAll(_ value: Value) {
while let head = self.head, head.value == value {
self.head = head.next
}
var prev = head
var current = head?.next
while let currentNode = current {
guard currentNode.value != value else {
prev?.next = currentNode.next
current = prev?.next
continue
}
prev = current
current = current?.next
}
tail = prev
}
}
제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.
감사합니다.