堆数据结构对于获取最大值的K个数或者获取最小的K个数比较方便。
什么是堆
堆是一个完全二叉树
,并且它可以使用一个数组来表示。
堆有两种:
-
最大堆(Max heap)
:任意节点的值总是 ≥ 子节点的值 -
最小堆(Min heap)
:任意节点的值总是 ≤ 子节点的值
堆的应用
- 计算集合的最小或最大元素
- 堆排序
- 构建优先级队列
- 使用优先级队列构造图算法,例如Prim或Dijkstra的算法
堆的共同操作
堆的定义
struct Heap {
var elements: [Element] = []
let sort: (Element, Element) -> Bool
init(sort: @escaping (Element, Element) -> Bool) {
self.sort = sort
}
}
怎么去展示堆
堆可以用数组去展现
对以上满二叉树进行同级遍历,把参数依次放入数组,得到如下结果:
如果节点是满的,则下一级的节点数量是上一级节点数量的两倍。
使用数组通过索引获取节点,这样就避免了遍历树来获取元素。对于数组中第
i
个元素来讲:
- 其左子节点的索引是
2i + 1
- 其右子节点的索引是
2i + 2
- 其父节点的索引是
floor((i - 1) / 2)
我们向堆中添加以下方法和属性:
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
删除堆中的最大值,第一步需要交换根节点和最后一个节点。
当你交换完这两个元素,然后删除最后一个元素。
由于 3 小于子节点 8,违背了最大堆的性质,我们需要修复最大堆,对节点3进行
下滤
:
将节点3与其左右节点较大的节点 8 进行交换。
和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,将
candiate
和parent
进行交换,然后继续进行移动操作。
复杂度:整个删除操作的时间复杂度是 O(log n)。在数组中交换元素的复杂度是O(1),当在堆中执行下滤操作时,和树的高度有关系,时间复杂度为 O(log n)
在堆中插入元素
在以下堆中插入元素 7
首先,将新插入的值放置堆的尾部
为满足最大堆的性质,需要对 新节点7进行
上滤
操作:将当前节点和父节点进行比较,如果不满足堆的性质,就进行位置交换
插入操作的实现
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》