[프로그래머스] 이중우선순위큐

Dual Priority QueueHeap
avatar
2025.03.24
·
6 min read

문제 링크

문제 설명

이중 우선순위 큐는 다음 연산을 할 수 있는 자료구조를 말합니다.

명령어

수신 탑(높이)

I 숫자

큐에 주어진 숫자를 삽입합니다.

D 1

큐에서 최댓값을 삭제합니다.

D -1

큐에서 최솟값을 삭제합니다.

이중 우선순위 큐가 할 연산 operations가 매개변수로 주어질 때, 모든 연산을 처리한 후 큐가 비어있으면 [0,0] 비어있지 않으면 [최댓값, 최솟값]을 return 하도록 solution 함수를 구현해주세요.

제한사항

  • operations는 길이가 1 이상 1,000,000 이하인 문자열 배열입니다.

  • operations의 원소는 큐가 수행할 연산을 나타냅니다.

    • 원소는 “명령어 데이터” 형식으로 주어집니다.- 최댓값/최솟값을 삭제하는 연산에서 최댓값/최솟값이 둘 이상인 경우, 하나만 삭제합니다.

  • 빈 큐에 데이터를 삭제하라는 연산이 주어질 경우, 해당 연산은 무시합니다.


문제 이해

주어진 operations를 순서대로 처리한 후, 큐가 비어있다면 [0, 0]을 반환하고, 비어 있지 않다면 [최댓값, 최솟값]을 반환하는 문제이다.

문제에서 중요한 단서

  • 빈 큐에 데이터를 삭제하라는 연산이 주어질 경우, 해당 연산은 무시 → 큐가 비어있는지 확인해야 함

문제 해결 접근 방법

  1. 이중 우선순위 큐를 구현하기 위해 배열을 사용해 볼 수 있다.

    func solution(_ operations: [String]) -> [Int] {
        var queue: [Int] = []
    
        for operation in operations {
            let element = operation.split(separator: " ")
            let op = element[0]
            let number = Int(element[1])!
    
            if op == "I" {
                queue.append(number)
            } else if op == "D" {
                if !queue.isEmpty {
                    if number == 1 {
                        queue.remove(at: queue.firstIndex(of: queue.max()!)!)
                    } else {
                        queue.remove(at: queue.firstIndex(of: queue.min()!)!)
                    }
                }
            }
        }
    
        return queue.isEmpty ? [0, 0] : [queue.max()!, queue.min()!]
    }

    배열을 사용하기 때문에, 삽입 연산 자체는 빠르지만, 매 연산마다 최대/최솟값을 탐색하기 때문에 입력이 많아질 수록 비효율적이다.

  2. Heap을 사용하면 조금 더 효율적으로 최대/최솟값을 관리할 수 있다.

    struct Heap {
        private var elements: [Int] = []
        private let sort: (Int, Int) -> Bool
    
        init(sort: @escaping (Int, Int) -> Bool) {
            self.sort = sort
        }
    
        var isEmpty: Bool { elements.isEmpty }
        var count: Int { elements.count }
        var first: Int? { elements.first }
    
        mutating func insert(_ value: Int) {
            elements.append(value)
            siftUp(from: elements.count - 1)
        }
    
        mutating func remove() -> Int? {
            guard !elements.isEmpty else { return nil }
            elements.swapAt(0, elements.count - 1)
            let value = elements.removeLast()
            siftDown(from: 0)
            return value
        }
    
        private mutating func siftUp(from index: Int) {
            var child = index
            var parent = (child - 1) / 2
            while child > 0 && sort(elements[child], elements[parent]) {
                elements.swapAt(child, parent)
                child = parent
                parent = (child - 1) / 2
            }
        }
    
        private mutating func siftDown(from index: Int) {
            var parent = index
            let count = elements.count
            while true {
                let left = 2 * parent + 1
                let right = 2 * parent + 2
                var candidate = parent
    
                if left < count && sort(elements[left], elements[candidate]) { candidate = left }
                if right < count && sort(elements[right], elements[candidate]) { candidate = right }
    
                if candidate == parent { return }
                elements.swapAt(parent, candidate)
                parent = candidate
            }
        }
    }
    
    func solution(_ operations: [String]) -> [Int] {
        var maxHeap = Heap(sort: >)
        var minHeap = Heap(sort: <)
        var countMap = [Int: Int]()
    
        for operation in operations {
            let components = operation.split(separator: " ")
            let command = components[0]
            guard components.count == 2, let number = Int(components[1]) else { continue }
    
            if command == "I" {
                maxHeap.insert(number)
                minHeap.insert(number)
                countMap[number, default: 0] += 1
            } else if command == "D" {
                if number == 1 {
                    while let max = maxHeap.first, countMap[max] == 0 { maxHeap.remove() }
                    if let max = maxHeap.remove() { countMap[max]! -= 1 }
                } else if number == -1 {
                    while let min = minHeap.first, countMap[min] == 0 { minHeap.remove() }
                    if let min = minHeap.remove() { countMap[min]! -= 1 }
                }
            }
        }
    
        while let max = maxHeap.first, countMap[max] == 0 { maxHeap.remove() }
        while let min = minHeap.first, countMap[min] == 0 { minHeap.remove() }
    
        if maxHeap.isEmpty || minHeap.isEmpty { return [0, 0] }
        else { return [maxHeap.first!, minHeap.first!] }
    }

    위 코드에서는 Heap을 사용하여 삽입/삭제 연산이 일정한 효율을 유지하며, 이중 우선순위 큐의 성능을 극대화한다.

  3. 하지만, 이 문제는 굳이 Heap을 구현하지 않아도 통과되므로 코드를 짧게 작성하는 게 더 효율적인 것 같다.







- 컬렉션 아티클