Open ShannonChenCHN opened 3 years ago
堆是一种特殊的树。
只要满足以下两个条件,它就是一个堆:
大顶堆(最大堆):每个节点的值都大于或等于子树中每个节点值的堆。 小顶堆(最小堆):每个节点的值都小于或等于子树中每个节点值的堆。
要实现一个堆,我们需要知道:
因为堆是一个完全二叉树,而完全二叉树通常又比较适合用数组来存储,所以我们可以用数组来存储堆。
如图所示,我们可以发现规律:对于数组中任意下标为 i
的节点,其左子节点下标正好是 i∗2
,其右子节点的下标为 i∗2+1
,其父节点的下标为 i/2
。
注:在本文的讲解以及代码中,我们都假设,堆中的数据是从数组下标为 1 的位置开始存储。那如果从 0 开始存储,实际上处理思路是没有任何变化的,唯一变化的就是计算子节点下标和父节点下标的公式。如果节点的下标是
i
,那左子节点的下标就是2∗i+1
,右子节点的下标就是2∗i+2
,父节点的下标就是(i−1)/2
。
堆最核心的两个操作分别是:
(1) 往堆中插入一个元素
堆化(heapify):当我们往堆中插入一个新元素后,这个树可能就不再满足应有的特性了,所以,我们就需要进行调整,让其重新满足堆的特性,这个过程就叫做堆化。
堆化有两种方式:
就从下往上的堆化来讲,以大顶堆为例,堆化就是顺着节点所在的路径,与父节点对比大小。如果不满足子节点小于或等于父节点的大小关系,我们就互换两个节点。一直重复这个过程,直到父子节点之间满足刚说的那种大小关系。
代码实现如下:
/// 大顶堆
class MaxHeap<T: Comparable> {
var nodes: [T?] = [T?]()
init() {
nodes.append(nil)
}
/// 插入一个新节点
func insert(_ data: T) {
nodes.append(data)
shiftUp()
}
/// 自下往上堆化
func shiftUp() {
var currentIndex = nodes.count - 1
while currentIndex > 1 {
let parentIndex = currentIndex / 2
if nodes[currentIndex]! > nodes[parentIndex]! {
nodes.swapAt(currentIndex, parentIndex)
currentIndex = parentIndex
} else {
break
}
}
}
}
(2) 删除堆顶元素
我们把最后一个节点移到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是从上往下的堆化方法。
代码实现:
/// 大顶堆
class MaxHeap<T: Comparable> {
var nodes: [T?] = [T?]()
init() {
nodes.append(nil)
}
/// 删除堆顶元素
func remove() {
guard nodes.count <= 1 else {
return
}
// 将堆顶元素替换成最后一个元素
nodes[1] = nodes.last!
// 对新的堆顶元素进行堆化
shiftDown()
}
/// 自上往下堆化
func shiftDown() {
var currentIndex = 1
while true {
// 从 根-左-右 中找出最大的元素作为新的堆顶元素
var maxIndex = currentIndex
let leftChildIndex = currentIndex * 2
let rightChildIndex = currentIndex * 2 + 1
// 堆顶元素比左子节点小
if leftChildIndex <= nodes.count - 1 &&
nodes[currentIndex]! < nodes[leftChildIndex]! {
maxIndex = leftChildIndex
}
// 堆顶元素比右子节点小
if rightChildIndex <= nodes.count - 1 &&
nodes[maxIndex]! < nodes[rightChildIndex]! {
maxIndex = rightChildIndex
}
// 如果当前元素本身就是最大元素,就不用继续堆化了
if maxIndex == currentIndex {
break
}
nodes.swapAt(maxIndex, currentIndex)
currentIndex = maxIndex
}
}
}
(3) 复杂度分析
一个包含 n 个节点的完全二叉树,树的高度不会超过 logn
。堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是 O(logn)
。插入数据和删除堆顶元素的主要逻辑就是堆化,所以,往堆中插入一个元素和删除堆顶元素的时间复杂度都是 O(logn)
。
我们可以把堆排序的过程大致分解成两个大的步骤,建堆和排序。
两种思路:
因为第二种方式比较常见,所以这里只讨论第二种思路。
代码实现如下:
/// 大顶堆
class MaxHeap<T: Comparable> {
var nodes: [T?] = [T?]()
init() {
nodes.append(nil)
}
func buildHeap(_ array: [T]) {
nodes = [nil]
nodes += array
// 从最后一个非叶子节点开始,从下向上依次堆化
let lastParentIndex = (nodes.count - 1) / 2
for i in stride(from: lastParentIndex, through: 1, by: -1) {
shiftDown(i)
}
}
/// 自上往下堆化
func shiftDown(_ index: Int) {
var currentIndex = index
while true {
// 从 根-左-右 中找出最大的元素作为新的堆顶元素
var maxIndex = currentIndex
let leftChildIndex = currentIndex * 2
let rightChildIndex = currentIndex * 2 + 1
// 堆顶元素比左子节点小
if leftChildIndex <= nodes.count - 1 &&
nodes[currentIndex]! < nodes[leftChildIndex]! {
maxIndex = leftChildIndex
}
// 堆顶元素比右子节点小
if rightChildIndex <= nodes.count - 1 &&
nodes[maxIndex]! < nodes[rightChildIndex]! {
maxIndex = rightChildIndex
}
// 如果当前元素本身就是最大元素,就不用继续堆化了
if maxIndex == currentIndex {
break
}
nodes.swapAt(maxIndex, currentIndex)
currentIndex = maxIndex
}
}
}
第二种思路的时间复杂度为 O(n)。
建堆结束之后,数组中的数据并不是递增或者递减排序的。不过,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。
我们可以把数组分成两部分,已排序部分和未排序部分。首先我们把堆顶元素跟最后一个元素交换,那最大元素就放到了下标为 n 的位置,而前面的 n - 1 个元素就是未排序部分。
接下来堆中就只剩下了未排序部分的元素,然后我们再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标为 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。
代码实现:
/// 大顶堆
class MaxHeap<T: Comparable> {
var nodes: [T?] = [T?]()
init() {
nodes.append(nil)
}
/// 自上往下堆化
func shiftDown(from index: Int, until endIndex: Int) {
var currentIndex = index
while true {
// 从 根-左-右 中找出最大的元素作为新的堆顶元素
var maxIndex = currentIndex
let leftChildIndex = currentIndex * 2
let rightChildIndex = currentIndex * 2 + 1
// 堆顶元素比左子节点小
if leftChildIndex <= endIndex &&
nodes[currentIndex]! < nodes[leftChildIndex]! {
maxIndex = leftChildIndex
}
// 堆顶元素比右子节点小
if rightChildIndex <= endIndex &&
nodes[maxIndex]! < nodes[rightChildIndex]! {
maxIndex = rightChildIndex
}
// 如果当前元素本身就是最大元素,就不用继续堆化了
if maxIndex == currentIndex {
break
}
nodes.swapAt(maxIndex, currentIndex)
currentIndex = maxIndex
}
}
/// 排序
func sort() {
var lastUnsortedIndex = nodes.count - 1
while lastUnsortedIndex > 1 {
nodes.swapAt(lastUnsortedIndex, 1)
lastUnsortedIndex -= 1
shiftDown(from: 1, until: lastUnsortedIndex)
}
}
}
整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。
堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
2021.03.14 03:45 pm ~ 05:30 pm
每次 add 时对数组进行排序,返回第 k 个元素。
思路:建小顶堆,当堆大小为 k 时,堆顶元素即为第 k 大的元素。add()
函数调用时,如果当前堆大小小于 k,就将新元素添加到最后,然后再调用 shiftUp()
进行堆化。如果当前堆大小已经达到 k 了,则判断新元素是否大于堆顶元素,大于就用新元素替换堆顶元素,然后再调用 shiftDown()
进行堆化,否则,不做任何操作。
时间复杂度:
init
时间复杂度为:O(nlogk) ,其中 n 为初始化时 nums 的长度,logk 为每次堆化的时间复杂度。add
时间复杂度为:O(log k)。也就是一次堆化的时间复杂度。
空间复杂度:O(k)。需要使用堆存储前 k 大的元素。class KthLargest {
private var nodes: [Int] = [Int]()
private let capacity: Int
init(_ k: Int, _ nums: [Int]) {
self.capacity = k
for num in nums {
add(num)
}
}
func add(_ val: Int) -> Int {
if nodes.count < capacity { // size 没超过 k
nodes.append(val)
shiftUp()
} else {
if nodes.first! < val {
nodes[0] = val
shiftDown()
}
}
return peek()
}
private func peek() -> Int {
return nodes.count > 0 ? nodes.first! : 0
}
private func shiftUp() {
var currentIndex = nodes.count - 1
while currentIndex >= 1 {
let parentIndex = (currentIndex - 1) / 2
if nodes[currentIndex] < nodes[parentIndex] {
nodes.swapAt(parentIndex, currentIndex)
currentIndex = parentIndex
} else {
break
}
}
}
private func shiftDown() {
var currentIndex = 0
while currentIndex < nodes.count {
var minIndex = currentIndex
var leftChildIndex = currentIndex * 2 + 1
var rightChildIndex = currentIndex * 2 + 2
if leftChildIndex < nodes.count && nodes[leftChildIndex] < nodes[minIndex] {
minIndex = leftChildIndex
}
if rightChildIndex < nodes.count && nodes[rightChildIndex] < nodes[minIndex] {
minIndex = rightChildIndex
}
if minIndex == currentIndex {
break
} else {
nodes.swapAt(minIndex, currentIndex)
currentIndex = minIndex
}
}
}
}
2021.03.19 07:30 am ~ 08:30 am
一般有两种方式:建大顶堆依次删除 k 个元素,或者建大小为 k 的小顶堆。这里采用更常用的后者。
思路:依次将 nums 中的元素添加到堆中,在添加每个元素时,如果堆中元素个数小于 k 个,则直接插入到堆中,否则,判断新添加的元素是否比堆顶元素大,是则替换掉堆顶元素,否则 pass。
时间复杂度:O(nlogk)。堆化的时间复杂度跟树的高度成正比,也就是 O(logk),所以,每次 insert 的时间复杂度是 O(logk)。 空间复杂度:O(k)。需要使用堆存储前 k 大的元素。
class Solution {
func findKthLargest(_ nums: [Int], _ k: Int) -> Int {
let heap: MinHeap = MinHeap(nums, k)
return heap.peek()
}
}
class MinHeap {
private var nodes: [Int] = [Int]()
private let capacity: Int
init(_ nums: [Int], _ k: Int) {
self.capacity = k
for num in nums {
insert(num)
}
}
func peek() -> Int {
return nodes.count > 0 ? nodes.first! : 0
}
func insert(_ num: Int) {
if nodes.count < capacity { // size 没超过 k
nodes.append(num)
shiftUp()
} else {
if nodes.first! < num {
nodes[0] = num
shiftDown()
}
}
}
private func shiftUp() {
var currentIndex = nodes.count - 1
while currentIndex >= 1 {
let parentIndex = (currentIndex - 1) / 2
if nodes[currentIndex] < nodes[parentIndex] {
nodes.swapAt(parentIndex, currentIndex)
currentIndex = parentIndex
} else {
break
}
}
}
private func shiftDown() {
var currentIndex = 0
while currentIndex < nodes.count {
var minIndex = currentIndex
var leftChildIndex = currentIndex * 2 + 1
var rightChildIndex = currentIndex * 2 + 2
if leftChildIndex < nodes.count && nodes[leftChildIndex] < nodes[minIndex] {
minIndex = leftChildIndex
}
if rightChildIndex < nodes.count && nodes[rightChildIndex] < nodes[minIndex] {
minIndex = rightChildIndex
}
if minIndex == currentIndex {
break
} else {
nodes.swapAt(minIndex, currentIndex)
currentIndex = minIndex
}
}
}
}
2021.03.20 04:30 pm ~ 05:00 pm
输入:[2,1,3] 2
输出:[2,1]或者[1,2]
输入:[2,1,3] 0
输出:[]
思路:构建一个大小为 k 的大顶堆,依次向堆中添加所有元素,每次添加元素时,如果堆中节点个数小于 k 个,则直接添加并堆化即可;如果堆中节点个数已经达到 k 个,则判断待添加的元素是否小于堆顶元素,是则替换堆顶元素并堆化,否则直接 pass。
时间复杂度:O(nlogk)。其中 n 是数组 arr 的长度,因为每次往堆中假如新元素时时都需要堆化,堆化的时间复杂度跟树的高度成正比,也就是 O(logk),所以,每次 add 的时间复杂度是 O(logk)。最坏情况下数组里 n 个数都会插入,所以一共需要 O(nlog k) 的时间复杂度。 空间复杂度:O(k)。需要使用堆存储前 k 大的元素。
class Solution {
var nodes: [Int] = [Int]()
var capacity: Int = 0
func getLeastNumbers(_ arr: [Int], _ k: Int) -> [Int] {
guard k > 0 else {
return []
}
capacity = k
for num in arr {
add(num)
}
return nodes
}
func add(_ num: Int) {
if nodes.count < capacity {
nodes.append(num)
shiftUp()
} else {
if nodes.first! > num {
nodes[0] = num
shiftDown()
}
}
}
func shiftUp() {
var currentIndex = nodes.count - 1
while currentIndex >= 1 {
let parentIndex = (currentIndex - 1) / 2
if nodes[currentIndex] > nodes[parentIndex] {
nodes.swapAt(currentIndex, parentIndex)
currentIndex = parentIndex
} else {
break
}
}
}
func shiftDown() {
var currentIndex = 0
while currentIndex < nodes.count {
var maxIndex = currentIndex
let leftChildIndex = currentIndex * 2 + 1
let rightChildIndex = currentIndex * 2 + 2
if leftChildIndex < nodes.count && nodes[leftChildIndex] > nodes[maxIndex] {
maxIndex = leftChildIndex
}
if rightChildIndex < nodes.count && nodes[rightChildIndex] > nodes[maxIndex] {
maxIndex = rightChildIndex
}
if maxIndex == currentIndex {
break
} else {
nodes.swapAt(maxIndex, currentIndex)
currentIndex = maxIndex
}
}
}
}
优先队列
A priority queue is a queue where the most important element is always at the front.
The queue can be a max-priority queue (largest element first) or a min-priority queue (smallest element first).
参考:
题目列表
堆
总结