Algorithm/자료구조

[Swift] 자료구조 - Queue

iOSDEv 2024. 3. 4. 15:00
Queue

Stack과 상반되는 녀석이라고 생각한 요놈

게임쪽에서 일 할 때 Queue는 좀 많이 사용했던것 같아요
캐릭터의 대화에서 한 글자씩 나오도록 할 때 Queue를 이용하여서
만들었던 기억이 있답니다

 

예전에 큐를 공부하면서 기억남은것들은

선형큐도 있지만 원형큐 등등 있다는걸 배웠던걸로 기억하는데여

우선 원형큐를 구현하기 위해서는 head와 tail을 기억하고 있어야합니다

그렇게 해야 원형을 유지할 수 있기 때문이죠 번호가 부여된 강강 술래 너낌스..

 

근데 Swift에서 Queue를 지원하지 않더라구요

시스템적으로 DispatchQueue는 있으면서.. 왜 없니...?

 

그래서 직접 구조체로 하단의 코드로 만들 수 있답니다 :)

 

 

두 가지 버전으로 만들어 보았습니당

우선 사용해 볼 건 

SimpleQueue로 구현해 보겠습니다

 

 

두번째 버전은

시간 복잡도 개선 버전의

queue를 만들었습니다

 

 

비슷한 예시 두가지를 만들어보았어요

 

이러한 예시로 확인할 수 있는건

enqueue를 하게되면 마지막 인덱스 뒤에 들어가게되고

 

dequeue를 하게되면 제일 앞에있는 인덱스가 사라지게됩니다

 

이걸 우리는 FIFO : First In First Out 먼저 들어간 오브젝트가 제일 먼저 나간다

라고 합니다!

stack과는 다르죠? stack경우는 나중에 들어간 녀석이 제일 빨리 나오는데 ㅎㅎ

 

배열을enqueue / dequeue / front(peek) 시간복잡도로 나타내면 

O(1)이라고 할 수 있다

 

하지만 dequeue의 시간복잡도를 O(1)이라고 했지만

제일 처음 인덱스를 삭제하고 앞당기기 때문에

당겨지는 과정 때문에 O(n)이라고 할 수 있기 때문에

이러한 과정에서 오버헤드가 발생할 수 있답니다

 

그래서 이러한 현상이 발생하지 않도록

큐 구현에서 링크드 리스트를 사용해서 요소를 저장하고

첫번째 요소를 제거하게되면 O(1)만큼의 시간 복잡도가 소요된답니다

 

but

 

큐에서 특정 요소를 찾는다고하면

시간 복잡도가 O(n) 입니다

왜냐?

크기만큼 인덱스를 다 돌아다니며 검색해야하기 때문이죠!

 

추후 배울 링크드리스트에서 배우겠지만

미리 코드만 살짝 보고 간다면

 

// 노드 클래스 정의
public class Node<T> {
    var value: T
    var next: Node<T>?

    init(_ value: T) {
        self.value = value
    }
}

// 큐 구조체 정의
public struct Queue<T> {
    private var front: Node<T>? // 큐의 맨 앞
    private var rear: Node<T>?  // 큐의 맨 뒤

    // 큐가 비어있는지 확인하는 프로퍼티
    public var isEmpty: Bool {
        return front == nil
    }

    // 큐에 있는 요소의 수를 확인하는 프로퍼티
    public var count: Int {
        var current = front
        var count = 0

        // 큐의 맨 앞부터 끝까지 탐색하면서 요소의 수를 셈
        while current != nil {
            count += 1
            current = current?.next
        }

        return count
    }

    // 큐에 요소를 추가하는 메서드
    public mutating func enqueue(_ element: T) {
        let newNode = Node(element)

        // 큐가 비어있으면 맨 앞과 맨 뒤가 새 노드를 가리킴
        if front == nil {
            front = newNode
            rear = newNode
        } else {
            // 큐가 비어있지 않으면 맨 뒤의 다음 노드로 새 노드를 지정하고, 맨 뒤를 새 노드로 업데이트
            rear?.next = newNode
            rear = newNode
        }
    }

    // 큐에서 요소를 제거하고 반환하는 메서드
    public mutating func dequeue() -> T? {
        // 큐의 맨 앞의 노드를 꺼냄
        guard let currentFront = front else {
            return nil
        }

        // 큐의 맨 앞을 다음 노드로 업데이트
        front = currentFront.next

        // 만약 큐가 비어있게 되면 맨 뒤도 nil로 업데이트
        if front == nil {
            rear = nil
        }

        return currentFront.value
    }

    // 큐의 맨 앞에 있는 요소를 확인하는 메서드
    public func peek() -> T? {
        return front?.value
    }
}

// 예제 사용
var myQueue = Queue<Int>()

// 큐에 요소 추가
myQueue.enqueue(1)
myQueue.enqueue(2)
myQueue.enqueue(3)

// 큐의 요소 수 출력
print("Queue Count: \(myQueue.count)") // Output: Queue Count: 3

// 큐의 맨 앞에 있는 요소 확인
if let frontElement = myQueue.peek() {
    print("Front element: \(frontElement)") // Output: Front element: 1
}

// 큐에서 요소를 제거하고 출력
while let dequeuedElement = myQueue.dequeue() {
    print("Dequeued element: \(dequeuedElement)")
}

// 큐의 요소 수 출력
print("Queue Count: \(myQueue.count)") // Output: Queue Count: 0

 

 

 

두 번째 예시를 살펴봅시다

시간 복잡도를 개선하여 작성된 코드입니다

배열의 길이가 50개 넘거나 삭제된 비율이 25%넘어가면 큐를 재 조정한답니다

 

removefirst대신 nil로 초기화 해버리기 때문에 앞당길 작업을 일정 조건 동안은 

작업하지 않아도 됩니다

 

 

 

 

마지막으로 원형큐를 구현해보고자 합니다

public struct CircularQueue<T> {
    private var elements: [T?]
    private var head: Int = 0
    private var tail: Int = 0
    private var capacity: Int

    // 큐의 초기화
    public init(capacity: Int) {
        self.capacity = max(capacity, 1) // 최소 크기는 1로 설정
        self.elements = Array(repeating: nil, count: self.capacity)
    }

    // 큐가 비어있는지 확인하는 프로퍼티
    public var isEmpty: Bool {
        return head == tail && elements[head] == nil
    }

    // 큐가 가득 찼는지 확인하는 프로퍼티
    public var isFull: Bool {
        let nextIndex = (tail + 1) % capacity
        return nextIndex == head
    }

    // 현재 큐에 있는 요소의 수를 계산하는 프로퍼티
    public var count: Int {
        if head <= tail {
            return tail - head
        } else {
            return capacity - head + tail
        }
    }

    // 큐에 요소를 추가하는 메서드
    public mutating func enqueue(_ element: T) {
        guard !isFull else {
            // 큐가 가득 찼을 경우 요소를 추가하지 않음
            return
        }

        elements[tail] = element
        tail = (tail + 1) % capacity
    }

    // 큐에서 요소를 제거하고 반환하는 메서드
    public mutating func dequeue() -> T? {
        guard !isEmpty, let element = elements[head] else {
            // 큐가 비어있을 경우 또는 head에 해당하는 요소가 nil일 경우 nil 반환
            return nil
        }

        elements[head] = nil
        head = (head + 1) % capacity

        return element
    }

    // 큐의 맨 앞에 있는 요소를 확인하는 메서드
    public func peek() -> T? {
        return isEmpty ? nil : elements[head]
    }
}

// 예제 사용
var circularQueue = CircularQueue<Int>(capacity: 5)

circularQueue.enqueue(1)
circularQueue.enqueue(2)
circularQueue.enqueue(3)

print("Queue Count: \(circularQueue.count)") // Output: Queue Count: 3

if let frontElement = circularQueue.peek() {
    print("Front element: \(frontElement)") // Output: Front element: 1
}

while let dequeuedElement = circularQueue.dequeue() {
    print("Dequeued element: \(dequeuedElement)")
}

print("Queue Count: \(circularQueue.count)") // Output: Queue Count: 0

 

 

위에서 말했다시피 번호부여된 강강술래라고 생각하시면 됩니다

선형 큐의 문제점을 보완한 큐 입니당

맨 끝까지 데이터가 차고 맨 앞에 공간이 있담녀 맨 앞에 데이터를 넣는 형식입니다

그래서 맨뒤와 맨 앞의 요소가 연결되어 원형이라고 불러요

 

Swift 언어는 주로 애플의 플랫폼에서 사용되므로, Swift로 작성된 애플리케이션은 macOS, iOS, watchOS, tvOS 등의 운영체제에서 동작합니다. 여러 운영체제에서 활용되는 여러 시나리오에서 Swift Queue를 찾을 수 있습니다. 아래는 몇 가지 예시입니다:
멀티스레드 환경에서의 비동기 프로그래밍:Swift에서의 GCD(Grand Central Dispatch)는 멀티스레딩을 지원하는데, 이 때 큐가 사용됩니다.
백그라운드에서 수행되는 비동기 작업들이나 메인 스레드에서 수행되기를 기다리는 작업 등을 큐를 통해 관리합니다.

애플리케이션 이벤트 처리:애플리케이션에서 발생하는 이벤트나 작업들은 큐를 통해 관리될 수 있습니다.
예를 들어 사용자 입력, 네트워크 응답, 데이터베이스 작업 등은 큐를 활용하여 비동기적으로 처리될 수 있습니다.

애플리케이션의 데이터 처리 및 업데이트:애플리케이션에서 데이터의 큐를 사용하여 처리하거나 업데이트하는 경우가 있습니다.
예를 들어 큐를 활용하여 데이터를 비동기적으로 불러오거나 업데이트하는 작업을 관리할 수 있습니다.

프로듀서-컨슈머 모델:큐는 프로듀서-컨슈머 모델에서 사용될 수 있습니다. 여러 스레드 간에 데이터를 안전하게 전달하고 처리하기 위해 큐를 활용할 수 있습니다.작업 큐 및 비동기 처리:비동기 작업이나 백그라운드 작업을 처리할 때 큐가 사용됩니다.
예를 들어, 이미지 다운로드, 파일 처리, 데이터 동기화 등을 비동기적으로 큐를 통해 수행할 수 있습니다.애플리케이션의 이벤트 처리 및 태스크 스케줄링:애플리케이션에서의 이벤트 처리나 특정 작업을 예약하고 실행하기 위해 큐를 활용할 수 있습니다.

-gpt-

 

 

❤️잘못된 부분은 알려주시면 감사하겠습니다❤️

 

 

⭐️참고 사이트⭐️

https://github.com/kodecocodes/swift-algorithm-club/blob/master/Queue/Queue-Optimized.swift

https://chat.openai.com/