数据结构学习-二叉堆(Binary Heap)

1,033 阅读3分钟

介绍

二叉堆是一个完全二叉树,满足完全二叉树的条件是除了最底层可以不填满节点,其它层级都需要填满。

在实际使用中,二叉堆能够:

  • 在一个集合中很方便的找出最大值或最小值;
  • 应用于堆排序;
  • 构建优先级队列;
  • 构建一些图形算法。

它有下面两种类型:

  • 最大堆,优先级越高值越大;
  • 最小堆,优先级越高值越小。

最大堆

img1.png

如上图所示的最大堆,它有以下特性

  • 父节点的值大于或者等于子节点的值;
  • 根节点的值最大。

最小堆

img2.png

如上图所示的最小堆,它有以下特性

  • 父节点的值小于或者等于子节点的值;
  • 根节点的值最小。

实现

基本代码

struct Heap<Element: Equatable> {

    // 底层数据存储
    var elements: [Element] = []
    
    // 排序算法,如果排序算法满足大于条件,那么是创建最大堆;如果是小于条件,那么创建最小堆
    let sort: (Element, Element) -> Bool
    
    init(sort: @escaping (Element, Element) -> Bool) {
        self.sort = sort
    }
    
    init(sort: @escaping (Element, Element) -> Bool,
            elements: [Element] = []) {
        self.sort = sort
        self.elements = elements
        
        if !elements.isEmpty {
            for i in stride(from: elements.count / 2 - 1, through: 0, by: -1) {
                siftDown(from: i)
            }
        }
    }
}

二叉堆里面的数据结构是这样的:

img3.png

底部数据存储的使用数组 var elements: [Element] = [], 如果数据添加进去是这样的:

elements = [20 10 16 8 5 9 7 1]

那么可以这样表示查看:

level 1:20
level 2:10 16
level 3:8 5 9 7
level 4:1

所以如果使用数组存储二叉堆的数据,那么二叉堆的实现可以有:

    var isEmpty: Bool {
        elements.isEmpty
    }
    
    var count: Int {
        elements.count
    }
    
    func peek() -> Element? {
        elements.first
    }
    
    func leftChildIndex(ofParentAt index: Int) -> Int {
        (2 * index) + 1
    }
    
    func rightChildIndex(ofParentAt index: Int) -> Int {
        (2 * index) + 2
    }
    
    func parentIndex(ofChildAt index: Int) -> Int {
        (index - 1) / 2
    }

移除根节点

对于最大堆的移除:

img1的副本.png

替换根节点与最后一个节点的位置,在二叉堆的数组变量里面也就是交换第一个元素和最后一个元素的值,然后移除交换后数组的最后元素。

对于最小堆的移除也是一样的:

img2的副本.png

当然不管最大堆还是最小堆移除数据以后,二叉堆还得满足是一个最大堆或者最小堆的条件,此时就需要从根节点开始下移位置使其满足二叉堆的条件。

代码实现:

extension Heap {
    
    mutating func remove() -> Element? {
        guard !isEmpty else {
            return nil
        }
        // 交换数组的第一个元素和最后一个元素的值
        elements.swapAt(0, count-1)
        defer {
            // 从根节点开始往下移动,保证数据删除以后还是二叉堆
            siftDown(from: 0)
        }
        return elements.removeLast()
    }
    
    mutating func siftDown(from index: Int) {
        var parent = index
        while true {
            let left = leftChildIndex(ofParentAt: parent)
            let right = rightChildIndex(ofParentAt: parent)
            var candidate = parent
            // 以最大堆为例子,如果 elements[left] > elements[candidate], 表明子节点大于父节点,此时需要把子节点的值和父节点替换;
            // 如果是最小堆,那么就是 elements[left] < elements[candidate]。
            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
        }
    }
}

移除操作的时间复杂度 O(log n)。

移除任意下标

代码实现:

extension Heap {
    
    mutating func remove(at index: Int) -> Element? {
        guard index < elements.count else {
            return nil
        }
        if index == elements.count - 1 {
            return elements.removeLast()
        } else {
            // 当前元素和数组最后元素替换
            elements.swapAt(index, elements.count - 1)
            defer {
                // 如果必要,下移
                siftDown(from: index)
                // 如果必要,上移
                siftUp(from: index)
            }
            return elements.removeLast()
        }
    }
}

为什么需要执行 siftDown(from: index) ?

假如要移除节点3,那么此时节点10和节点3位置替换以后,还需要下移节点10和节点5位置替换才能满足最小二叉堆条件

img7.png

为什么需要执行 siftUp(from: index) ?

假如要移除节点8,那么此时节点12和节点8位置替换以后,还需要上移节点12和节点10位置替换才能满足最大二叉堆条件

img6.png

数据插入

添加节点到二叉堆,里面的实际操作是添加节点值到数组里面,此时的添加值在数组的末尾,对于二叉堆来说,此时就需要比较当前添加节点的父节点的值的大小了。如果是最大堆,那么如果添加的值小于或者等于父节点的值,那么不用移动,如果大于父节点的值,那么子节点需要上移,使其满足二叉堆的判定条件(最小堆是小于父节点需要上移)。

extension Heap {
    
    mutating func insert(_ element: Element) {
        // 开始添加到数组末尾
        elements.append(element)
        siftUp(from: elements.count - 1)
    }
    
    mutating func siftUp(from index: Int) {
        var child = index
        var parent = parentIndex(ofChildAt: child)
        // 如果是最大堆,如果当前节点大于父节点,那么位置替换;如果是最小堆,如果当前节点小于副节点,那么位置替换
        while child > 0 && sort(elements[child], elements[parent]) {
            elements.swapAt(child, parent)
            child = parent
            parent = parentIndex(ofChildAt: child)
        }
    }
}

插入操作的时间复杂度 O(log n)。

搜索

由于底部的数据存储使用的是数组,其实一个简单的实现就是遍历整个数组了,此时时间复杂度是 O(n)。下面是一个二叉堆的搜索优化实现:

extension Heap {
    
    func index(of element: Element, startingAt i: Int) -> Int? {
        if i >= count {
            return nil
        }
        if sort(element, elements[i]) {
            return nil
        }
        if element == elements[i] {
            return i
        }
        // 从左节点处
        if let j = index(of: element, startingAt: leftChildIndex(ofParentAt: i)) {
            return j
        }
        // 从右节点处
        if let j = index(of: element, startingAt: rightChildIndex(ofParentAt: i)) {
            return j
        }
        return nil
    }
}

时间复杂度 O(n)

测试

    // 最大堆
    var heap = Heap(sort: >, elements: [1, 23, 34, 3, 43, 32, 3, 44, 442])
    print(heap.elements)
    
    print("\n开始移除:\n")
    
    while !heap.isEmpty {
        print(heap.remove()!)
    }

输出:

[442, 44, 34, 23, 43, 32, 3, 1, 3]

开始移除:

442
44
43
34
32
23
3
3
1