二叉堆(heap)

堆数据结构对于获取最大值的K个数或者获取最小的K个数比较方便。

什么是堆

堆是一个完全二叉树,并且它可以使用一个数组来表示。
堆有两种:

  • 最大堆(Max heap):任意节点的值总是 ≥ 子节点的值
  • 最小堆(Min heap):任意节点的值总是 ≤ 子节点的值
    heap1.png

堆的应用

  • 计算集合的最小或最大元素
  • 堆排序
  • 构建优先级队列
  • 使用优先级队列构造图算法,例如Prim或Dijkstra的算法

堆的共同操作

堆的定义

struct Heap {

  var elements: [Element] = []
  let sort: (Element, Element) -> Bool

  init(sort: @escaping (Element, Element) -> Bool) {
    self.sort = sort
  }
} 

怎么去展示堆

堆可以用数组去展现

heap2.png

对以上满二叉树进行同级遍历,把参数依次放入数组,得到如下结果:
heap3.png

如果节点是满的,则下一级的节点数量是上一级节点数量的两倍。
使用数组通过索引获取节点,这样就避免了遍历树来获取元素。对于数组中第i个元素来讲:

  • 其左子节点的索引是 2i + 1
  • 其右子节点的索引是 2i + 2
  • 其父节点的索引是 floor((i - 1) / 2)
    heap4.png

    我们向堆中添加以下方法和属性:
var isEmpty: Bool {
  return elements.isEmpty
}

var count: Int {
  return elements.count
}

func peek() -> Element? {
  return elements.first
}

func leftChildIndex(ofParentAt index: Int) -> Int {
  return (2 * index) + 1
}

func rightChildIndex(ofParentAt index: Int) -> Int {
  return (2 * index) + 2
}

func parentIndex(ofChildAt index: Int) -> Int {
  return (index - 1) / 2
}

从堆中删除元素

我们以最大堆为例,删除堆中的最大值即删除堆中的元素10

heap5.png

删除堆中的最大值,第一步需要交换根节点和最后一个节点。
heap6.png

当你交换完这两个元素,然后删除最后一个元素。
由于 3 小于子节点 8,违背了最大堆的性质,我们需要修复最大堆,对节点3进行下滤:
heap7.png

将节点3与其左右节点较大的节点 8 进行交换。
heap8.png

和8交换完之后,由于还不符合最大堆的性质,需要继续对节点3进行下滤操作,直到符合最大堆的性质为止。

删除操作的实现

在堆中实现如下方法:

mutating func remove() -> Element? {
  guard !isEmpty else { // 1
    return nil
  }
  elements.swapAt(0, count - 1) // 2
  defer {
    siftDown(from: 0) // 4
  }
  return elements.removeLast() // 3
}
  • 1,检查堆是否为空,如果为空,则返回 nil
  • 2,交换根元素最后一个元素
  • 3,删除最后一个元素
  • 4,对最大堆或者最小堆进行修正,对根节点进行下滤操作,来保证符合堆的性质。
mutating func siftDown(from index: Int) {
  var parent = index // 1
  while true { // 2
    let left = leftChildIndex(ofParentAt: parent) // 3
    let right = rightChildIndex(ofParentAt: parent)
    var candidate = parent // 4
    if left < count && sort(elements[left], elements[candidate]) {
      candidate = left // 5
    }
    if right < count && sort(elements[right], elements[candidate]) {
      candidate = right // 6
    }
    if candidate == parent {
      return // 7
    }
    elements.swapAt(parent, candidate) // 8
    parent = candidate
  }
}
  • 1,存储parent 的索引
  • 2,一直执行修正操作,直到return为止
  • 3,获取 左子节点右子节点的索引
  • 4,candidate变量用来记录需要和 parent进行交换的索引
  • 5,如果 有左子节点,且优先级比父节点高,则将左子节点的索引设置为 candidate
  • 6,如果 有右子节点,且优先级比父节点高,则将右子节点的索引设置为 candidate
  • 7,如果 candiate仍然是 parent,则说明已符合堆的性质,则你需要进行移动操作了。
  • 8,将 candiateparent进行交换,然后继续进行移动操作。

复杂度:整个删除操作的时间复杂度是 O(log n)。在数组中交换元素的复杂度是O(1),当在堆中执行下滤操作时,和树的高度有关系,时间复杂度为 O(log n)

在堆中插入元素

在以下堆中插入元素 7

heap9.png

首先,将新插入的值放置堆的尾部
heap10.png

为满足最大堆的性质,需要对 新节点7进行上滤操作:将当前节点和父节点进行比较,如果不满足堆的性质,就进行位置交换
heap11.png

heap12.png

插入操作的实现

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

如上所示,插入的实现就比较简洁,直白。

  • 1,在数组的尾部添加新节点。
  • 2,只要子节点的优先级高于父节点,shiftUp就会对子节点和父节点进行交换。

复杂度:整个插入操作的复杂度是 O(log n)。对数组元素进行交换的时间复杂度是 O(1),对元素执行上滤时的复杂度是 O(log n)

删除任意节点index的元素

先交换数组中index处和最后一个元素,然后将最后数组的最后一个元素删除,在 index处,执行上滤和下滤,保证符合二叉堆的性质。

mutating func remove(at index: Int) -> Element? {
  guard index < elements.count else {
    return nil // 1
  }
  if index == elements.count - 1 {
    return elements.removeLast() // 2
  } else {
    elements.swapAt(index, elements.count - 1) // 3
    defer {
      siftDown(from: index) // 5
      siftUp(from: index)
    }
    return elements.removeLast() // 4
  }
}
  • 1,如果索引超出了数组范围,则返回nil
  • 2,如果删除的是堆最后一个节点,则删除数组中最后一个元素即可。
  • 3,交换堆中的要删除的节点和堆的最后一个元素。
  • 4,交换完后,删除堆中的最后一个元素。
  • 5,在 index节点处,执行上滤和下滤,保证符合堆的性质。

在堆中查找元素

在二叉搜索树中,查找元素的时间复杂度是 O(log n),由于堆是由数组构成的,在查找元素时,就不能按照树型结构查找了。

复杂度:在堆中查找元素,有可能需要遍历所有的元素,最坏的情况是 O(n)

func index(of element: Element, startingAt i: Int) -> Int? {
  if i >= count {
    return nil // 1
  }
  if sort(element, elements[i]) {
    return nil // 2
  }
  if element == elements[i] {
    return i // 3
  }
  if let j = index(of: element, startingAt: leftChildIndex(ofParentAt: i)) {
    return j // 4
  }
  if let j = index(of: element, startingAt: rightChildIndex(ofParentAt: i)) {
    return j // 5
  }
  return nil // 6
}
  • 1,如果 index 大于等于 elements的数量,则查找失败返回nil
  • 2,如果element的优先级大于 i处元素的优先级,在堆中的底部就不会存在所查找的元素。
  • 3,如果和索引 i处的元素相等,则返回索引 i 即可。
  • 4,在 i 的左子节点中递归查找
  • 5,在 i 的右子节点中递归查找

构建堆

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

堆是一个完全二叉树,对于完全二叉树而言

  • 1,从第一个叶子节点开始,他后面所有的节点都是叶子节点。
  • 2,第一个叶子节点的索引就是非叶子节点的数量。
  • 3,叶子节点个数 num = (n + 1) / 2,非叶子节点的个数: n / 2
    在构建堆的时候,我们无需对叶子节点进行 下滤 操作,只需要对所有的父节点进行下滤操作即可。

Challenage

Challenage1

编写一个函数,在一个无序数组中,查找 nth小的整数
示例

let integers = [3, 10, 18, 5, 21, 100]

如果 n = 3,得到的结果为 10
参考:

  func testHeap_challenage() {
        let integers = [3, 10, 18, 5, 21, 100]
        var heap = Heap(sort: { (element1, element2) -> Bool in
            return element1 > element2
        }, elements: integers)
        let n = 3
        var current = 0
        for _ in 0...n {
            current = heap.remove() ?? 0
        }
        
        XCTAssertEqual(current, 10)
    }

Challenage2

给定一个数组,判断是不是最小堆

参考:

func leftChildIndex(ofParentAt index: Int) -> Int {
  return (2 * index) + 1
}

func rightChildIndex(ofParentAt index: Int) -> Int {
  return (2 * index) + 2
}

func isMinHeap (elements: [Element]) -> Bool {
  guard !elements.isEmpty else {
    return true
  }
  // 对每一个非叶子节点进行检查,查看是否符合最小堆的性质
  for i in stride(from: elements.count / 2 - 1, through: 0, by: -1) {
    let left = leftChildIndex(ofParentAt: i)
    let right = rightChildIndex(ofParentAt: i)
    if elements[left] < elements[i] {
      return false
    }
    
    if elements[right] < elements[i] {
      return false
    }
  }
  
  return true
}

Challenage3

给定一数组,得出其前 K 个最大的数
示例:

let integers = [1,2,5,7,19,189,44,32,45,67,4,9]

当K = 3时,所得集合元素为 [45, 189, 67]

    func testTopK() {

        let integers = [1,2,5,7,19,189,44,32,45,67,4,9]
        let elements: [Int] = [Int]()
        var heap = Heap(sort: { (element1, element2) -> Bool in
            return element1 < element2
        }, elements: elements)

        for i in 0.. value ?? 0 {
                    heap.remove()
                    heap.insert(integers[i])
                }
            }
        }

        print(heap.elements)
    }

使用最小堆,保存最大的元素。

最后附上本文的相关代码DataAndAlgorim

参考链接《Data Structures & Algorithms in Swift》

你可能感兴趣的:(二叉堆(heap))