Queue 란?

  • Queue 자료구조를 그림을 통해 이해해 보자
  • 각 아이템들이 들어간 순서대로 나오는 구조를 이야기한다
  • First In First Out ( FIFO )

Swift 에서 Queue 구현

  • 구조체로 Queue를 선언한 코드를 먼저 살펴보자
// Queue 선언
struct Queue<T> {
    private var elements: [T] = []
    
    public var count: Int {
        return elements.count
    }
    
    public var isEmpty: Bool {
        return elements.isEmpty
    }
    
    public mutating func enqueue(_ element: T) {
        return elements.append(element)
    }
    
    public mutating func dequeue() -> T? {
        return isEmpty ? nil : elements.removeFirst()
    }
}

// Queue 사용
var intQueue = Queue<Int>()
print(intQueue)
// Queue<Int>(elements: [])

intQueue.enqueue(3)
print(intQueue)
// Queue<Int>(elements: [3])

print(intQueue.count)
print(intQueue.isEmpty)
// 1
// false

intQueue.dequeue()
print(intQueue.isEmpty)
// true
  • 구조체를 이용해 [ count , isEmpty, enqueue, dequeue ] 를 간단히 구현 할수 있었다

 

문제점

구현 한거 아니야? 뭐가 문제야!!!

응.. 문제야..

  • 위에서 선언한 방식으로 Queue를 사용하는데 ‘기능상' 문제는 없다, 하지만 메모리 관리 측면에서 너무 비효율적이다
  • 비효율 적인 이유

  • dequeue 를 수행할 때 removeFirst() 내장 함수를 사용하게 되는데 이것이 문제가 된다.
  • 첫 요소를 제거한 후 모든 요소들의 인덱스를 한칸씩 앞으로 당겨야한다.

⇒ 위 그림에서는 한번만 수행되지만 만약 dequeue가 여러번 수행된다면..? 엄청난 메모리 낭비를 하게 될것이다

요소 하나하나 제거할 때마다 만약 queue내의 item이 100개, 1000개 라면,, 요소 하나를 제거하기 위해 수없이 많이 인덱스를 한칸씩 당겨주어야 한다

 

해결 방법

  • head 라는 놈을 추가해 주고, dequeue될때마다 head 인덱스를 nil 로 바꿔준다
  • 그리고 일정 수준 이상으로 nil 이 채워지면, 0인덱스부터 head 까지의 요소를 한번에 제거하고 head 를 0으로 초기화 해준다

  • 이런 방법을 통해서 dequeue를 매번 하지않고, 쌓아 두었다가 일정 수준 이상에 도달하면 한번에 제거하기 때문에 메모리 측면에서 기존 방식보다 더 효율적이다

향상된 Queue 구현

struct Queue<T> {
		// nil 이 포함되기 때문에 [T?] 의 Optional 타입으로 선언
    private var elements: [T?] = []
    private var head = 0
    
		// nil 인경우 count를 안하기 위해 for case 문을 사용
    public var count: Int {
        var  elementCount = 0
        for case .some in elements { elementCount += 1 }
        return elementCount
    }
    
    public var isEmpty: Bool {
        return elements.isEmpty
    }
    
    public mutating func enqueue(_ element: T) {
        return elements.append(element)
    }
    
    public mutating func dequeue() -> T? {
				// head 가 큐의 아이템 갯수보다 크거나, head가 가르키는 곳이 nil 일경우 nil 반환
        guard head < elements.count, let element = elements[head] else { return nil }
        elements[head] = nil
        head += 1

				// Queue아이템의 1/3 보다 클경우마다 인덱스 0부터 head 까지 요소를 제거하고 head 0으로 초기화
        if head > (elements.count / 3) {
            elements.removeFirst(head)
            head = 0
        }
        return element
    }
}
  • 기존 방법과 다르게 nil 이 포함되기 때문에 옵셔널 타입의 배열로 선언해야 한다 [T?]
  • 아이템의 1/3 보다 head가 클 경우마다 nil 요소들을 지우게 설정했지만, 적절하게 조절해서 사용하면 될거같다
  • Count 의 경우, nil 인경우 count를 안하기 위해 for case 문을 사용 Nil 이 아닌 경우만 세서 반환한다.
var iQ = Queue<Int>()

iQ.enqueue(3)
iQ.enqueue(4)
iQ.enqueue(5)

iQ.count
// 3

iQ.dequeue()
// 3
iQ.count
// 2

iQ
// [nil, 4, 5] , head = 1
iQ.dequeue()
// 4
iQ
// [5], head = 0

iQ.count
// 1

iQ.dequeue()
// 5
iQ
// [], head = 0

iQ.dequeue()
// nil
iQ.count
// 0
iQ.dequeue()
// nil

 

참고 링크

https://babbab2.tistory.com/84

문제

 

풀이 코드

// 변수 선언
var inputs = readLine()!
var commandResult = ""
let commandCount = Int(readLine()!)!

for _ in 0..<commandCount {
    let command = readLine()!
    
    switch command {
    case "L":
        if !inputs.isEmpty { commandResult.append(inputs.popLast()!) }
    case "D":
        if !commandResult.isEmpty { inputs.append(commandResult.popLast()!) }
    case "B":
        if !inputs.isEmpty { inputs.removeLast() }
    default:
        inputs.append(command.last!)
    }
}

print(inputs + commandResult.reversed())

 

풀이 과정

// 변수 선언
var result = readLine()!
let commandCount = Int(readLine()!)!
var cursor = result.count

for _ in 0..<commandCount {
    let index = result.index(result.startIndex, offsetBy: cursor)
    let command = readLine()!.split(separator: " ")
    
    switch command[0] {
    case "P":
        result.insert(contentsOf: command[1], at: index)
        cursor += 1
    case "L":
        if cursor - 1 >= 0 { cursor -= 1 }
    case "D":
        if cursor + 1 <= result.count { cursor += 1 }
    default:
        if cursor > 0 {
            cursor -= 1;
            let removeIndex = result.index(result.startIndex, offsetBy: cursor)
            result.remove(at: removeIndex) }
    }
}

print(result)

처음, 위와 같은 방식으로 문제를 풀었다, 하지만 시간초과 가 발생했습니다.

이리 저리 고쳐 봤지만, 시간 초과 문제를 해결하지 못했습니다.

그래서 검색해본 결과

  1. split을 쓰면 시간이 오래걸림
  2. 내가 짠 코드에서 String.index 로 변환하고, 다시 변수 생성하고 등등의 과정으로 오랜 시간이 소요됨

이라는 결론을 낼 수 있었고, Stack 구조체를 이용하여 풀어야 한다고 생각했지만,

그런데 전에 풀었던 방식으로 구조체로 사용하게되면 string 을 각각 문자로 변환하고 그걸 또 스택에 넣어야 하는 과정속에서 또 다시 시간초과가 날 거 같아서 그냥 배열로 처리를 해주었습니다.

입력된 결과에서 현 커서의 위치가 이동될 때 마다 새로운 문자열(commandResult)에 Push 해 주거나 원 문자열에서 pop 해주면서 계산해주어 풀이했습니다.

문제

 

풀이 코드

// Stack 구조체 선언
public struct Stack<T> {
    private var elements = [T]()
    public init() {}
    
    public mutating func pop() -> T? {
        return self.elements.popLast()
    }
    
    public mutating func push(_ elements: T) {
        self.elements.append(elements)
    }
    
    public func top() -> T? {
        return self.elements.last
    }
    
    public var empty: Bool {
        return self.elements.isEmpty
    }
}

// 변수 선언
let n = Int(readLine()!)!
var sequence = [Int]()
var result = [String]()
var sequenceStack = Stack<Int>()
var pushValue = 1

// 입력값 저장 배열
for _ in 0..<n { sequence.append(Int(readLine()!)!) }

// 1부터 현재의 value 까지 채워넣고, 스택의 마지막 값과 현재 value 의 값을 비교하여 같으면 pop, 다르면 해당 수열을 만들 수 없으므로 NO 출력
for value in sequence {
    while pushValue <= value {
        sequenceStack.push(pushValue)
        result.append("+")
        pushValue += 1
    }
    if sequenceStack.top()! == value {
        sequenceStack.pop()
        result.append("-")
    } else { print("NO"); break }
}

// 스택이 비어있는 상태면 정상적인 계산 완료
if sequenceStack.empty {
    print(result.joined(separator: "\n"))
}

 

풀이 과정

조금 어려웠다 일단 문제 이해가 어려웠고, 그다음 구현이 어려웠다.. 그렇게 몇시간을 고민하다가 다른 분들의 코드를 보고 힌트를 얻어 구현했다..

 

일단 문제를 해설하자면 입력으로 주어진 순서의 값들이, 스택에 1부터 오름차순으로 push 해서 pop 할때와의 순서와 같아야 하고, 그렇게 할수 없는 경우 NO를 출력하면 된다

 

나도 이게 무슨소리인지 몰랐지만

정리하면 예시로 나온 4 3 6 8 7 5 2 1 로 보자

처음에  4가 pop으로 나와야하니 스택에

1 2 3 4 가 들어가고 4 가 pop 된다 그러면 

+ + + + - 까지 되고 그다음 숫자인 3이 pop되야하니 그냥 pop하면 된다 그러면

+ + + + - - 까지가 4 3 이 출렷 된다! 그다음 숫자는 6이기 때문에 우선 6까지 채워줘야 하므로

+ + + + - - + + 이렇게 하면 스택에 1 2 5 6 이 있고, 여기서 pop을 하면 1 2 5 가 스택에 남는다

+ + + + - - + + - 이렇게 반복하여서 Push 할땐 + 를, pop할땐 - 를 해주면 된다,

 

그러다가 중간에 pop을 해도 해당 순서로 정렬할수 없는경우 가차없이 NO를 출력하면 된다!

 

여기에 해당하는 조건은 stack.top() 이 현재 출력해야할 수보다 크면 순서대로 정렬할 수 없기 때문에 NO를 출력하면 된다!!

 

문제를 이해하고나면 그리 많이 어려운 문제는 아니였는데,, 

이해하는데도 오래걸리고 1부터 채워서 어떻게 출력하지? 라는 고민때문에 다른분의 코드를 보고 힌트를 얻은게 참 아쉽다

문제

 

풀이 코드

// [BOJ] 9012 - 괄호
public struct Stack<T> {
    private var elements = [T]()
    public init() {}
    
    public mutating func pop() -> T? {
        return self.elements.popLast()
    }
    
    public mutating func push(_ element: T) {
        self.elements.append(element)
    }
    
    public var empty: Bool {
        return self.elements.isEmpty
    }
}

let T = Int(readLine()!)!

for _ in 0..<T {
    let ps = readLine()!
    var psStack = Stack<Character>()
    var result = "YES"
    
    for item in ps {
        if item == "(" { psStack.push(item) }
        else {
            if psStack.empty { result = "NO";break }
            else { psStack.pop() }
        }
    }
    if !psStack.empty { result = "NO" }
    print(result)
}

 

풀이 과정

Stack 을 이용하여 문제를 풀어보았다.

전체적인 프로세스는 다음과 같다.

 

1. "(" 문자가 들어올때 스택에 저장한다.

2. ")" 문자가 들어올때 두가지 경우로 판단한다

   2-1. 현재 스택이 비어있다면 괄호의 쌍이 맞지 않기 때문에 NO 를 저장
   2-2 현재 스택에 데이터[ "(" ] 가 있는 상태면 한 쌍이 되니 pop 을 이용해 데이터를 스택에서 제거한다

3. 2번과정이 전부 돌아 간후, stack 내부에 데이터가 있다면 쌍이 맞지않는 [ "(" ] 가 있는것이기 때문에 NO 를 출력한다.

4. 3번 과정까지 NO 가 아니라면 쌍이 맞는 괄호이므로 YES 가 출력된다.

 

이렇게 스택을 이용하여 풀이를 하였지만,

아무래도 가독성이 너무 떨어지는것 같다.

 

사실 처음에는 단순히 +1 . -1 해서 판단 하려 했으나, 이번주 알고리즘 스터디 내용이 Stack 이기 때문에 스택을 이용하여 풀이를 해보았다

문제

 

풀이 코드

// [BOJ] 9093 - 단어 뒤집기

let T = Int(readLine()!)!
var sentReversed : String

for _ in 0..<T {
    sentReversed = ""
    let sentence = readLine()!.split(separator: " ")
    sentence.forEach {
        sentReversed.append($0.reversed() + " ")
    }
    print(sentReversed)
}

 

풀이 과정

크게 두가지 메서드를 이용하여 구현 했다.

1. ForEach 

2. reversed

 

foreach 를 사용하여 배열 내부의 모든 요소를 검사하여 각 단어의 반전을 주었다!

 

reversed 메서드를 사용하여 쉽게 순서를 뒤집었는데

 

reverse 와 reversed 가 있다. 간단히 살펴보면 다음과 같다

 

1. reverse

- 원래의 배열 자체를 반전하여 변화시킴

- 시간복잡도 O(n) 

 

2. reversed

- 호출 할때만 반전되고 원래의 배열 자체는 변화하지 않음

- 시간복잡도 O(1)

 

효율성을 위하여 reversed 를 사용 하였다!

+ Recent posts