优先队列
优先队列是一种特殊的队列数据结构,它的特点是每个元素都有一个优先级,出队操作按照优先级而不是入队顺序来决定。
当优先队列为从大到小排列时,队列元素的头部始终保持数值最大,并且可以通过队尾插入数据,队首移出数据等操作,始终保持队列首端元素数值最大。
实现方式
优先队列的主要实现方式是堆(Heap),但具体是大顶堆还是小顶堆取决于优先队列的用途
- 大顶堆:是一种 二叉堆,性质为:父节点的值 大于 子节点的值。
- 小顶堆::是一种 二叉堆,性质为:父节点的值 小于 子节点的值。
- 二叉堆:使用 完全二叉树 的树形结构实现的堆。
- 完全二叉树:是特殊的 二叉树,性质为:只有最后一层的节点没有满,且最后一层的节点只会出现在最后一层的左侧。
完全二叉树可以基于数组实现,对于除根节点之外的节点都有以下规则
- 非叶子节点的两个子节点索引分别是
leftIndex = parentIndex * 2 + 1
和rightIndex = parentIndex *2 +2
除根节点之外,每一个节点的父节点的索引是
parentIndex = (nodeIndex-1) / 2`
优先队列的构建
插入元素,从子节点开始,逐步向上检查是否满足堆的性质(大顶堆或小顶堆,后续以大顶堆为例),如果不满足则与父节点交换。
- 将新元素放到堆的最后一位,然后对其做上滤操作,插入节点与父节点做比较,大于父节点则与父节点交换,直到根节点为止
- 直到所有元素入队,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N log N N\log N </math>NlogN
出队列,从堆顶开始,逐步向下检查子节点是否满足堆的性质,若不满足则与较大的子节点交换。
- 每次出队将根节点弹出,根节点弹出后,再将最后一个节点与第一个节点交换,进行下滤,保证大顶堆
编码
下面使用Golang实现优先队列的编码
go
package main
import (
"fmt"
)
// PriorityQueue 定义优先队列结构
type PriorityQueue struct {
data []int // 堆数据存储
size int // 当前堆大小
lessFunc func(a, b int) bool // 比较函数,决定堆的顺序
}
// NewPriorityQueue 创建一个优先队列
func NewPriorityQueue(lessFunc func(a, b int) bool) *PriorityQueue {
return &PriorityQueue{
data: []int{},
size: 0,
lessFunc: lessFunc,
}
}
// Push 插入元素并调整堆(上滤)
func (pq *PriorityQueue) Push(val int) {
pq.data = append(pq.data, val) // 将元素添加到堆末尾
pq.size++
pq.siftUp(pq.size - 1) // 调整堆
}
// Pop 删除堆顶元素并调整堆(下滤)
func (pq *PriorityQueue) Pop() (int, error) {
if pq.size == 0 {
return 0, fmt.Errorf("priority queue is empty")
}
// 堆顶元素
top := pq.data[0]
// 将堆尾元素放到堆顶
pq.data[0] = pq.data[pq.size-1]
// 移除堆尾
pq.data = pq.data[:pq.size-1]
pq.size--
pq.siftDown(0) // 调整堆
return top, nil
}
// Peek 获取堆顶元素
func (pq *PriorityQueue) Peek() (int, error) {
if pq.size == 0 {
return 0, fmt.Errorf("priority queue is empty")
}
return pq.data[0], nil
}
// 上滤操作
func (pq *PriorityQueue) siftUp(index int) {
for index > 0 {
// 父节点索引
parent := (index - 1) / 2
// 如果当前节点与父节点满足堆的顺序,停止上滤
if !pq.lessFunc(pq.data[index], pq.data[parent]) {
break
}
// 否则交换父子节点并继续
pq.data[index], pq.data[parent] = pq.data[parent], pq.data[index]
index = parent
}
}
// 下滤操作
func (pq *PriorityQueue) siftDown(index int) {
for index*2+1 < pq.size {
left := index*2 + 1 // 左子节点索引
right := index*2 + 2 // 右子节点索引
smallest := left // 假设左子节点是较小的
// 如果右子节点存在且更符合堆顺序规则
if right < pq.size && pq.lessFunc(pq.data[right], pq.data[left]) {
smallest = right
}
// 如果当前节点与子节点满足堆的顺序,停止下滤
if !pq.lessFunc(pq.data[smallest], pq.data[index]) {
break
}
// 否则交换当前节点与较小的子节点
pq.data[index], pq.data[smallest] = pq.data[smallest], pq.data[index]
index = smallest
}
}
func main() {
// 创建大顶堆
maxHeap := NewPriorityQueue(func(a, b int) bool {
// 大顶堆:父节点大于子节点
return a > b
})
maxHeap.Push(3)
maxHeap.Push(4)
maxHeap.Push(5)
maxHeap.Push(6)
maxHeap.Push(1)
maxHeap.Push(7)
maxHeap.Push(8)
peek, _ := maxHeap.Peek()
fmt.Println("大顶堆 Peek:", peek)
fmt.Println("大顶堆 Pop:")
for maxHeap.size > 0 {
val, _ := maxHeap.Pop()
fmt.Print(val, " ")
}
fmt.Println()
// 创建小顶堆
minHeap := NewPriorityQueue(func(a, b int) bool {
// 小顶堆:父节点小于子节点
return a < b
})
minHeap.Push(3)
minHeap.Push(4)
minHeap.Push(5)
minHeap.Push(6)
minHeap.Push(1)
minHeap.Push(7)
minHeap.Push(8)
peek, _ = minHeap.Peek()
fmt.Println("小顶堆 Peek:", peek)
fmt.Println("小顶堆 Pop:")
for minHeap.size > 0 {
val, _ := minHeap.Pop()
fmt.Print(val, " ")
}
}
应用 力扣 295 数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如arr = [2,3,4]
的中位数是3
。
例如arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5以内的答案将被接受。
示例 1:
输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
解释
plain
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
提示:
-105 <= num <= 105
在调用 findMedian 之前,数据结构中至少有一个元素
最多 5 * 104 次调用addNum
和findMedian
go
package main
import (
"fmt"
)
func main() {
m := Constructor()
m.AddNum(1)
m.AddNum(2)
fmt.Println(m.FindMedian())
m.AddNum(3)
fmt.Println(m.FindMedian())
}
// MedianFinder
// https://leetcode.cn/problems/find-median-from-data-stream/
type MedianFinder struct {
left *PriorityQueue
right *PriorityQueue
}
func Constructor() MedianFinder {
return MedianFinder{
left: NewPriorityQueue(func(a, b int) bool {
return a < b
}),
right: NewPriorityQueue(func(a, b int) bool {
return a > b
}),
}
}
func (this *MedianFinder) AddNum(num int) {
if this.left.size == this.right.size {
this.right.Push(num)
this.left.Push(this.right.Pop())
} else {
this.left.Push(num)
this.right.Push(this.left.Pop())
}
}
func (this *MedianFinder) FindMedian() float64 {
if this.left.size == this.right.size {
return float64(this.left.Peek()+this.right.Peek()) / 2
} else {
return float64(this.left.Peek())
}
}
// PriorityQueue 定义优先队列结构
type PriorityQueue struct {
data []int // 堆数据存储
size int // 当前堆大小
lessFunc func(a, b int) bool // 比较函数,决定堆的顺序
}
// NewPriorityQueue 创建一个优先队列
func NewPriorityQueue(lessFunc func(a, b int) bool) *PriorityQueue {
return &PriorityQueue{
data: []int{},
size: 0,
lessFunc: lessFunc,
}
}
// Push 插入元素并调整堆(上滤)
func (pq *PriorityQueue) Push(val int) {
pq.data = append(pq.data, val) // 将元素添加到堆末尾
pq.size++
pq.siftUp(pq.size - 1) // 调整堆
}
// Pop 删除堆顶元素并调整堆(下滤)
func (pq *PriorityQueue) Pop() int {
if pq.size == 0 {
return 0
}
// 堆顶元素
top := pq.data[0]
// 将堆尾元素放到堆顶
pq.data[0] = pq.data[pq.size-1]
// 移除堆尾
pq.data = pq.data[:pq.size-1]
pq.size--
pq.siftDown(0) // 调整堆
return top
}
// Peek 获取堆顶元素
func (pq *PriorityQueue) Peek() int {
if pq.size == 0 {
return 0
}
return pq.data[0]
}
// 上滤操作
func (pq *PriorityQueue) siftUp(index int) {
for index > 0 {
// 父节点索引
parent := (index - 1) / 2
// 如果当前节点与父节点满足堆的顺序,停止上滤
if !pq.lessFunc(pq.data[index], pq.data[parent]) {
break
}
// 否则交换父子节点并继续
pq.data[index], pq.data[parent] = pq.data[parent], pq.data[index]
index = parent
}
}
// 下滤操作
func (pq *PriorityQueue) siftDown(index int) {
for index*2+1 < pq.size {
left := index*2 + 1 // 左子节点索引
right := index*2 + 2 // 右子节点索引
smallest := left // 假设左子节点是较小的
// 如果右子节点存在且更符合堆顺序规则
if right < pq.size && pq.lessFunc(pq.data[right], pq.data[left]) {
smallest = right
}
// 如果当前节点与子节点满足堆的顺序,停止下滤
if !pq.lessFunc(pq.data[smallest], pq.data[index]) {
break
}
// 否则交换当前节点与较小的子节点
pq.data[index], pq.data[smallest] = pq.data[smallest], pq.data[index]
index = smallest
}
}
上面就是利用优先队列完成力扣295. 数据流的中位数,利用数据结构的特性完成数据流的中位数,在做题的时候并没有想到使用优先队列,了解了优先队列的优势之后,处理这一个问题速度非常快。
数据结构和算法题分享结束啦,如果文章对你有帮助,点赞+收藏~~