서적/데이터 구조 및 알고리즘

Kodeco | Swift의 데이터 구조 및 알고리즘 - Linked List

ziziDev 2024. 6. 6. 02:14
반응형

 

 

오늘은 Linked List

에 대해서 다뤄보고자 합니다

 

 

 

kodeco

 

 

사이트(클릭클릭👆🏻) kodeco에 들어가면 다양한 자료들을 볼 수 있으니 강추👍🏻👍🏻👍🏻👍🏻👍🏻 드립니다

강의보단 책이 좋다는 평이 있어서 저도 책으로 구매하였답니다 :)

 


 

 

Linked List

 

 

Linked List는 선형으로 이루어진 단방향 순서로 나열된 배열인

값으로 이루어져 있습니다

 

Linked List는 배열(Array)에 비해서 장점이 몇 가지 있습니다

 

목록 앞에서 삽입과 제거가 됩니다

성능적으로 안정적입니다

 

 

위 다이어그램에서 알 수 있듯이 Linked List는 일련의 노드들로 구성된 사슬을 나타내고 있습니다

 

노드에는 두 가지 역할이 있습니다

 

값을 저장합니다

다음 노드를 참조하는 참조를 가지고 있습니다

nil 값은 리스트의 끝을 나타내고 있습니다

 

 

Noded에서는 12의 값을 저장하고 있는걸 볼 수 있습니다

 

Linked List를 구현하고 그것과관련하여 시간복잡도와 copy-on-write를 구현해보고자 합니다

 

Node

import UIKit

public class Node<Value> {
    public var value: Value
    public var next: Node?
    
    public init(value: Value, next: Node? = nil) {
        self.value = value
        self.next = next
    }
}

//사용자 객체를 문자열로 표현할 수 있는데 사용하고 있음
//프로토콜을 준수하는 타입은 description이라는 연산 프로퍼티를 제공
//문자열 표현을 정의할 수 있음
//디버깅이나 로깅 목적으로 객체를 읽기 쉬운 문자열 변환을 할 때 사용
extension Node: CustomStringConvertible {
    public var description: String {
        guard let next = next else {
          return "\(value)"
        }
        return "\(value) -> " + String(describing: next) + " "
      }
}

print("creating and linking nodes")
let node1 = Node(value: 1)
let node2 = Node(value: 2)
let node3 = Node(value: 3)

node1.next = node2
node2.next = node3

//1 -> 2 -> 3
print(node1)

 

세 개의 노드를 생성하고 연결한것을 확인할 수 있습니다

 

 

Example of creating and linking nodes --

1->2->3

 

import UIKit

public class Node<Value> {
    public var value: Value
    public var next: Node?
    
    public init(value: Value, next: Node? = nil) {
        self.value = value
        self.next = next
    }
}

//사용자 객체를 문자열로 표현할 수 있는데 사용하고 있음
//프로토콜을 준수하는 타입은 description이라는 연산 프로퍼티를 제공
//문자열 표현을 정의할 수 있음
//디버깅이나 로깅 목적으로 객체를 읽기 쉬운 문자열 변환을 할 때 사용
extension Node: CustomStringConvertible {
    public var description: String {
        guard let next = next else {
          return "\(value)"
        }
        return "\(value) -> " + String(describing: next) + " "
      }
}

print("creating and linking nodes")
let node1 = Node(value: 1)
let node2 = Node(value: 2)
let node3 = Node(value: 3)

node1.next = node2
node2.next = node3

//1 -> 2 -> 3
print(node1)


public struct LinkedList<Value> {
    //연결된 목록의 시작 끝을 저장하도록 head / tail 프로퍼티를 선언한다
    public var head: Node<Value>?
    public var tail: Node<Value>?
    
    public init() {}
    
    //연결할 노드들이 아예 없는 빈 리스트라면 isEmpty는 true로 변환된다
    public var isEmpty: Bool {
        head == nil
    }
}

extension LinkedList: CustomStringConvertible {
    
    public var description: String {
        guard let head = head else {
            return "Empty list"
        }
        return String(describing: head)
    }
}

//⭐️CustomStringConvertible
//struct Point {
//    let x: Int, y: Int
//}
//
//extension Point: CustomStringConvertible {
//    var description: String {
//        return "(\(x), \(y))"
//    }
//}

 

링크드 리스트를 추가해서 연결된 목록에 각각 첫 번째 노드와 마지막 노드를 참조하는 head와 tail이 있습니다

 

 

 

이렇게 선언해 준 후 목록에 값을 추가해볼까요?

 

push

목록 앞에 값을 추가

 

append

목록 끝에 값을 추가

 

insert(after:)

특정 목록 노드 뒤에 값을 추가

 

빈 목록에 push하게되면 head와 tail 둘다 새 노드를 향하고 있을겁니다

 

public mutating func push(_ value: Value) {
  head = Node(value: value, next: head)
  if tail == nil {
    tail = head
  }
}

 

링크드리스트 안에 Push method를 선언해줍니다

 

print("⭐️push⭐️")

//generic 사용해서 타입 지정
var list = LinkedList<Int>()

list.push(3)
list.push(2)
list.push(1)


print("✏️list : \(list)")
//⭐️push⭐️
//✏️list : 1 -> 2 -> 3

 

 

처음 3을 추가했을 때

 

3

 

그다음 2를 push하게 되면

 

2를 가진 새로운 노드를 생성하게되고

이 노드의 next속성이 현재 머리인 노드3을 가리키게 합니다

 

2 - 3

 

그 다음 1을 push하게 되면

동일하게 next 속성이 현재 hea인 노드 2를 가리키게 됩니다

 

그래서 최종적으로

1 - 2 - 3

으로 출력이 되는걸 볼 수 있습니다

 

append Operations

|

 

public mutating func append(_ value: Value) {
    guard !isEmpty else {
        push(value)
        return
    }

    tail!.next = Node(value: value)
    tail = tail!.next
}

 

링크드리스트 안에 확장 메서드를 넣었습니다

위 코드를 분석하자면

 

guard문을 통해서 빈 리스트라면 else구문에 들어가서

value를 하나 push하여 새로운 값의 리스트의 머리를 추가합니다

 

이렇게되면 값이 1개밖에 없기 때문에 꼬리와 머리 둘 다 새 노드를 가리키게 됩니다

 

그리고 만약에 빈 리스트가 아닌경우에는 꼬리 다음에 새로운 노드를 생성합니다

그리곤 꼬리 다음인 새로운 노드를 꼬리로 변경해줍니다

 

example(of: "append") {
  var list = LinkedList<Int>()
  list.append(1)
  list.append(2)
  list.append(3)

  print(list)
}
//⭐️append⭐️
//1 -> 2 -> 3

 

 

insert(after:)

|

특정 목록 노드 뒤에 값을 추가

 

우선 코드를 짜기전 과정에 대해서 알아봅시다

리스트에서 특정 노드를 찾고 새로운 노드를 삽입합니다

 

//주어진 인덱스 기준으로 리스트에서 노드를 검색
    public func node(at index: Int) -> Node<Value>? { //return nil이 될 수도 있음 -> optional type
        //머리에서 시작
        var currentNode = head
        var currentIndex = 0
        
        //머리에서 시작하여 인덱스를 하나씩 이동시키며 입력한 인덱스에 도달할 때까지 반복
        //빈 리스트이거나 범위를 벗어난 인덱스는 nil을 반환
        while currentNode != nil && currentIndex < index {
            currentNode = currentNode!.next
            currentIndex += 1
        }
        return currentNode
    }
    
    @discardableResult //메서드의 반환 값을 호출자가 무시할 수 있음 컴파일러가 경고를 표시하지 않음
    public mutating func insert(_ value: Value, after node: Node<Value>) -> Node<Value> {
        //꼬리와 노드와 동일하지 않을때 else문이 아닌 아래 node.next 시행됨
        guard tail !== node else {
            //만약 꼬리와 node가 동일하다면 else 구문에 들어와서 확장 메서드를 사용하게됨
            append(value)
            //꼬리 업데이트 작용이됨
            return tail!
        }
        
        //그렇지 않을 때 노드를 단순히 새로 만들면서 연결만 하게되는걸 볼 수 있음
        node.next = Node(value: value, next: node.next)
        
        return node.next!
    }

 

이렇게 선언해준 후

특정 노드에 추가해보도록합시다

print("inserting at a particular index")

  var list2 = LinkedList<Int>()
  list2.push(3)
  list2.push(2)
  list2.push(1)

    
  print("Before inserting: \(list2)") //Before inserting: 1 -> 2 -> 3
  var middleNode = list2.node(at: 1)! //미들노드 2를 가리키고 있음
  for _ in 1...4 {
    middleNode = list2.insert(-1, after: middleNode) //2번째 뒤에 -1이 추가되는 구조
  }

  print("After inserting: \(list2)")
//1 2 -1 3

 

현재 간단히 한 번 사이클 돌린걸 주석처리로 작성했습니다

 

나머지 또한 for구문을 거치면서 어떻게 변하는지 작성해보겠습니다

 

이런 순서로 링크드 리스트에서  1 2 -1 3

의 1번째 노드는 2 입니다

한 바퀴 포문을 더 돌리게되면

1 2 -1 -1 3

이됩니다

 

 

그다음 3번 째 for문입니다

 

middleNode 즉 1번째 노드는

2 입니다

첫번째라고해서 제일앞이 아닌 배열처럼 두번째를 가지고오면 됩니다

 

포문을 거치면서

1 2 -1 -1 -1 3 

으로 변경된것을 확인할 수 있습니다

 

그리곤 마지막 포문을 지나게되면

 

1 2 -1 - 1 - 1 -1 3

 

으로 변경된걸 확인할 수 있습니다

 

 

 

성능에 대해서 알아보자면

단순히 추가 확장 푸시의 경우엔 시간 복잡도가 O(1)

 

한 요소만 접근하기 대문에 이럴 수 있습니다

 

하지만 마지막 node(at:)의 경우

인덱스를 while문을 돌리며 접근하고 있기 때문에

O(i)가 나올 수 밖에 없습니다

 

Removing values from the list

 

pop

리스트에서 앞에 있는 값을 제거합니다

 

removeLast

리스트에서 마지막 값을 제거합니다

 

remove(at:)

원하는 요소에 들어가서 제거할 수 있습니다

 

 

 

pop

 

pop 코드에 대해서 분석해볼까요?

@discardableResult
public mutating func pop() -> Value? {
  defer {
    head = head?.next
    if isEmpty {
      tail = nil
    }
  }
  return head?.value
}

 

여기서 눈 여겨 볼 만한건

defer입니다

함수가 종료되기 직전 실행이되는데

그 안에서 head의 다음 헤드를 업데이트 합니다

첫번째 요소를 제거하고 헤드를 다음 요소로 변경해주는 작업입니다

 

만약 리스트가 비어있다면 tail을 nil로 할당해줍니다

그리곤 현재 head 노드 값을 반환합니다

 

print("⭐️pop⭐️")
  var list3 = LinkedList<Int>()
list3.push(3)
list3.push(2)
list3.push(1)

print("Before popping list: \(list3)")
let poppedValue = list3.pop()
print("After popping list: \(list3)")
print("Popped value: " + String(describing: poppedValue))
//⭐️pop⭐️
//Before popping list: 1 -> 2 -> 3
//After popping list: 2 -> 3
//Popped value: Optional(1)

 

pop을하게되면

도느 헤드를 1 -> 2로 변경되는걸 볼 수 있습니다

그리곤 기존 head 노드는 더이상 참조되지 않는걸 확인할 수 있습니다

 

이건 스위프트에서 ARC가 있기에 가능한 부분입니다

그래서 해당 노드를 메모리에서 자동적으로 해제가 가능하구용

 

 

removeLast operations

 

 @discardableResult
    public mutating func removeLast() -> Value? {
        guard let head = head else { return nil }   //guard let 구문을 사용해 안전하게 해제시켜준 후
        
        guard head.next != nil else { return pop() } //리스트 크기가 하나이면 pop()을 호출하여 제거해준다
        
        //리스트 크기가 2개 이상이면 여길 실행
        //우선 동일하게 head를 넣어준 후
        var prev = head
        var current = head
        
        //current를 옮기며 변경시켜준다 마지막 노드로 이동 합니다
        while let next = current.next {
            prev = current
            current = next
        }
        
        //prev는 마지막 앞이므로 next를 사용해 nil을 할당해준다
        prev.next = nil
        //그리곤 마지막 앞의 노드를 꼬리로 잡아주고
        tail = prev
        //마지막 제거된 노드의 값을 반환하고 있습니다
        return current.value
    }

 

그리고 작성한 코드를 실행해 봅시당

 

print("⭐️removing the last node⭐️")
  var list4 = LinkedList<Int>()
  list4.push(3)
  list4.push(2)
  list4.push(1)

  print("Before removing last node: \(list4)")
  let removedValue = list4.removeLast()

  print("After removing last node: \(list4)")
  print("Removed value: " + String(describing: removedValue))

//⭐️removing the last node⭐️
//Before removing last node: 1 -> 2 -> 3
//After removing last node: 1 -> 2
//Removed value: Optional(3)

 

 

remove(afer:)operation

 

 

좀전에 배웠던 작업과 비슷하게 수행하게 됩니다

특정 노드를 제거하려면 그 노드의 이전 노드를 찾아야합니다

 

이전 노드와 다음 노드 포인터를 제거하고 전 후 노드를 이어주는 next를 잘 이어야 하겠죠?

그리고 참조가 끊어지면 자동적으로 참조 카운팅이 사라지면서 메모리에서 해제됩니다

 

 @discardableResult
    public mutating func remove(after node: Node<Value>) -> Value? {
        defer {

            if node.next === tail { //동일한 객체 유무
                //마지막 노드 변경
                tail = node
            }
            //노드 넥스트를 노드 다음다음으로 설정
            //중간의 노드 참조 하지 않음
            node.next = node.next?.next
        }
        return node.next?.value
    }

 

다음다음 노드를 넣어주고

다음 노드를 해제시키는걸 볼 수 있습니다

 

print("⭐️removing a node after a particular node⭐️")
  var list5 = LinkedList<Int>()
list5.push(3)
list5.push(2)
list5.push(1)

print("Before removing at particular index: \(list5)")
let index = 1
let node = list5.node(at: index - 1)!
let removedValue1 = list5.remove(after: node)

print("After removing at index \(index): \(list5)")
print("Removed value: " + String(describing: removedValue1))

//⭐️removing a node after a particular node⭐️
//Before removing at particular index: 1 -> 2 -> 3
//After removing at index 1: 1 -> 3
//Removed value: Optional(2)

 

코드 정리

import UIKit

public class Node<Value> {
    public var value: Value
    public var next: Node?
    
    public init(value: Value, next: Node? = nil) {
        self.value = value
        self.next = next
    }
}

//사용자 객체를 문자열로 표현할 수 있는데 사용하고 있음
//프로토콜을 준수하는 타입은 description이라는 연산 프로퍼티를 제공
//문자열 표현을 정의할 수 있음
//디버깅이나 로깅 목적으로 객체를 읽기 쉬운 문자열 변환을 할 때 사용
extension Node: CustomStringConvertible {
    public var description: String {
        guard let next = next else {
          return "\(value)"
        }
        return "\(value) -> " + String(describing: next) + " "
      }
}

print("creating and linking nodes")
let node1 = Node(value: 1)
let node2 = Node(value: 2)
let node3 = Node(value: 3)

node1.next = node2
node2.next = node3

//1 -> 2 -> 3
print(node1)
//하나씩 연동하기엔 무리가 있어보임


public struct LinkedList<Value> {
    //연결된 목록의 시작 끝을 저장하도록 head / tail 프로퍼티를 선언한다
    public var head: Node<Value>?
    public var tail: Node<Value>?
    
    public init() {}
    
    //연결할 노드들이 아예 없는 빈 리스트라면 isEmpty는 true로 변환된다
    public var isEmpty: Bool {
        head == nil
    }
    
    public mutating func push(_ value: Value) {
        head = Node(value: value, next: head)
        //공간이 하나일 때 head / tail 동일하게 만들어줌
        if tail == nil {
            tail = head
        }
    }
    
    public mutating func append(_ value: Value) {
        guard !isEmpty else {
            push(value)
            return
        }
        
        tail!.next = Node(value: value)
        
        tail = tail!.next
    }
    
    //주어진 인덱스 기준으로 리스트에서 노드를 검색
    public func node(at index: Int) -> Node<Value>? { //return nil이 될 수도 있음 -> optional type
        //머리에서 시작
        var currentNode = head
        var currentIndex = 0
        
        //머리에서 시작하여 인덱스를 하나씩 이동시키며 입력한 인덱스에 도달할 때까지 반복
        //빈 리스트이거나 범위를 벗어난 인덱스는 nil을 반환
        while currentNode != nil && currentIndex < index {
            currentNode = currentNode!.next
            currentIndex += 1
        }
        return currentNode
    }
    
    @discardableResult //메서드의 반환 값을 호출자가 무시할 수 있음 컴파일러가 경고를 표시하지 않음
    public mutating func insert(_ value: Value, after node: Node<Value>) -> Node<Value> {
        //꼬리와 노드와 동일하지 않을때 else문이 아닌 아래 node.next 시행됨
        guard tail !== node else {
            //만약 꼬리와 node가 동일하다면 else 구문에 들어와서 확장 메서드를 사용하게됨
            append(value)
            //꼬리 업데이트 작용이됨
            return tail!
        }
        
        //그렇지 않을 때 노드를 단순히 새로 만들면서 연결만 하게되는걸 볼 수 있음
        node.next = Node(value: value, next: node.next)
        
        return node.next!
    }
    
    @discardableResult
    public mutating func pop() -> Value? {
        defer {
            head = head?.next
            if isEmpty {
                tail = nil
            }
        }
        return head?.value
    }
    
    @discardableResult
    public mutating func removeLast() -> Value? {
        guard let head = head else { return nil }   //guard let 구문을 사용해 안전하게 해제시켜준 후
        
        guard head.next != nil else { return pop() } //리스트 크기가 하나이면 pop()을 호출하여 제거해준다
        
        //리스트 크기가 2개 이상이면 여길 실행
        //우선 동일하게 head를 넣어준 후
        var prev = head
        var current = head
        
        //current를 옮기며 변경시켜준다 마지막 노드로 이동 합니다
        while let next = current.next {
            prev = current
            current = next
        }
        
        //prev는 마지막 앞이므로 next를 사용해 nil을 할당해준다
        prev.next = nil
        //그리곤 마지막 앞의 노드를 꼬리로 잡아주고
        tail = prev
        //마지막 제거된 노드의 값을 반환하고 있습니다
        return current.value
    }
    
    @discardableResult
    public mutating func remove(after node: Node<Value>) -> Value? {
        defer {

            if node.next === tail { //동일한 객체 유무
                //마지막 노드 변경
                tail = node
            }
            //노드 바로 다음 노드를 제거함
            node.next = node.next?.next
        }
        return node.next?.value
    }
    
}

extension LinkedList: CustomStringConvertible {
    
    public var description: String {
        guard let head = head else {
            return "Empty list"
        }
        return String(describing: head)
    }
}

//⭐️CustomStringConvertible
//struct Point {
//    let x: Int, y: Int
//}
//
//extension Point: CustomStringConvertible {
//    var description: String {
//        return "(\(x), \(y))"
//    }
//}
print("⭐️push⭐️")

//generic 사용해서 타입 지정
var list = LinkedList<Int>()

list.push(3)
list.push(2)
list.push(1)


print("✏️list : \(list)")
//⭐️push⭐️
//✏️list : 1 -> 2 -> 3

print("⭐️append⭐️")

var list1 = LinkedList<Int>()
list1.append(1)
list1.append(2)
list1.append(3)

print(list1)


print("inserting at a particular index")

  var list2 = LinkedList<Int>()
  list2.push(3)
  list2.push(2)
  list2.push(1)

    
  print("Before inserting: \(list2)") //Before inserting: 1 -> 2 -> 3
  var middleNode = list2.node(at: 1)! //미들노드 2를 가리키고 있음
  for _ in 1...4 {
    middleNode = list2.insert(-1, after: middleNode) //2번째 뒤에 -1이 추가되는 구조
  }

  print("After inserting: \(list2)")
//1 2 -1 3

print("⭐️pop⭐️")
  var list3 = LinkedList<Int>()
list3.push(3)
list3.push(2)
list3.push(1)

print("Before popping list: \(list3)")
let poppedValue = list3.pop()
print("After popping list: \(list3)")
print("Popped value: " + String(describing: poppedValue))
//⭐️pop⭐️
//Before popping list: 1 -> 2 -> 3
//After popping list: 2 -> 3
//Popped value: Optional(1)


print("⭐️removing the last node⭐️")
  var list4 = LinkedList<Int>()
  list4.push(3)
  list4.push(2)
  list4.push(1)

  print("Before removing last node: \(list4)")
  let removedValue = list4.removeLast()

  print("After removing last node: \(list4)")
  print("Removed value: " + String(describing: removedValue))

//⭐️removing the last node⭐️
//Before removing last node: 1 -> 2 -> 3
//After removing last node: 1 -> 2
//Removed value: Optional(3)


print("⭐️removing a node after a particular node⭐️")
  var list5 = LinkedList<Int>()
list5.push(3)
list5.push(2)
list5.push(1)

print("Before removing at particular index: \(list5)")
let index = 1
let node = list5.node(at: index - 1)!
let removedValue1 = list5.remove(after: node)

print("After removing at index \(index): \(list5)")
print("Removed value: " + String(describing: removedValue1))

//⭐️removing a node after a particular node⭐️
//Before removing at particular index: 1 -> 2 -> 3
//After removing at index 1: 1 -> 3
//Removed value: Optional(2)

 

 

 

 

Performance analysis

 

 

pop / remover(after:)

은 요소를 찾아가서 끊기 때문에 시간복잡도가 O(1)이지만

마지막 제거는 while문으로 돌면서 마지막노드를 찾아가기 때문에

리스트가 많을 수록 시간이 늘어나기때문에

O(n)의 시간이 걸릴 수 밖에 없다

 

 

다음으로

Swift Collection protocol로 구현하는 방법입니다

 

연결리스트를 Collection protocol로 구현함으로 유한한 시퀀스로서 비파괴적인 순차 접근을 제공하고 서브스크립트를 통해서

배열처럼 인덱스로 접근이 가능하게됩니다

 

extension LinkedList: Collection {
// 1
public var startIndex: Index {
  Index(node: head)
}
// 2
public var endIndex: Index {
  Index(node: tail?.next)
}
// 3
public func index(after i: Index) -> Index {
  Index(node: i.node?.next)
}
// 4
public subscript(position: Index) -> Value {
  position.node!.value
}

  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 }
    }
  }
}

 

거의 다 정리했는데 티스토리 오류때문에 다시 작성해야하는게 화가 나지만...

작성해보겠습니당..

 

설명하자면

좌 우 인덱스가 동일하면 false 즉 크기가 1이겠죵?

 

그게 아니라면 시퀀스를 통해서 노드에 추가하는걸 볼 수 있습니다

그리고 comparable을 통해서 비교하는걸 볼 수 있습니다

 

print("using collection")
  var list6 = LinkedList<Int>()
  for i in 0...9 {
      list6.append(i)
  }

  print("List: \(list6)")
  print("First element: \(list6[list6.startIndex])")
  print("Array containing first 3 elements: \(Array(list6.prefix(3)))")
  print("Array containing last 3 elements: \(Array(list6.suffix(3)))")

  let sum = list6.reduce(0, +)
  print("Sum of all values: \(sum)")

print("array 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)")

//array cow
//array1: [1, 2]
//array2: [1, 2]
//---After adding 3 to array 2---
//array1: [1, 2]
//array2: [1, 2, 3]


이렇게 copy-on-write도 가능하며

서브스크립트로 통해 배열처럼 작성하여 노드에 접근이 가능합니다

 

 

 

 

반응형