优先队列及其应用

优先队列

优先队列是一种特殊的队列数据结构,它的特点是每个元素都有一个优先级,出队操作按照优先级而不是入队顺序来决定。

当优先队列为从大到小排列时,队列元素的头部始终保持数值最大,并且可以通过队尾插入数据,队首移出数据等操作,始终保持队列首端元素数值最大。

实现方式

优先队列的主要实现方式是堆(Heap),但具体是大顶堆还是小顶堆取决于优先队列的用途

  • 大顶堆:是一种 二叉堆,性质为:父节点的值 大于 子节点的值。
  • 小顶堆::是一种 二叉堆,性质为:父节点的值 小于 子节点的值。
  • 二叉堆:使用 完全二叉树 的树形结构实现的堆。
  • 完全二叉树:是特殊的 二叉树,性质为:只有最后一层的节点没有满,且最后一层的节点只会出现在最后一层的左侧。

完全二叉树可以基于数组实现,对于除根节点之外的节点都有以下规则

  • 非叶子节点的两个子节点索引分别是leftIndex = parentIndex * 2 + 1rightIndex = 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 次调用addNumfindMedian

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. 数据流的中位数,利用数据结构的特性完成数据流的中位数,在做题的时候并没有想到使用优先队列,了解了优先队列的优势之后,处理这一个问题速度非常快。

数据结构和算法题分享结束啦,如果文章对你有帮助,点赞+收藏~~

相关推荐
徐子童16 分钟前
《从零开始入门递归算法:搜索与回溯的核心思想 + 剑指Offer+leetcode高频面试题实战(含可视化图解)》
算法
天宫风子38 分钟前
抽象代数小述(二之前)
经验分享·笔记·算法·生活·抽象代数
向上的车轮1 小时前
“傅里叶变换算法”来检测纸箱变形的简单示例
算法
九亿AI算法优化工作室&1 小时前
乡村地区无人机医药配送路径规划与优化仿真
人工智能·算法·matlab·回归
米粉03051 小时前
算法图表总结:查找、排序与递归(含 Mermaid 图示)
数据结构·算法·排序算法
人类发明了工具2 小时前
【优化算法】协方差矩阵自适应进化策略(Covariance Matrix Adaptation Evolution Strategy,CMA-ES)
线性代数·算法·矩阵·cma-es
黑色的山岗在沉睡2 小时前
LeetCode100.4 移动零
数据结构·算法·leetcode
方博士AI机器人2 小时前
算法与数据结构 - 二叉树结构入门
数据结构·算法·二叉树
-qOVOp-2 小时前
zst-2001 上午题-历年真题 算法(5个内容)
算法
全栈凯哥2 小时前
Java详解LeetCode 热题 100(17):LeetCode 41. 缺失的第一个正数(First Missing Positive)详解
java·算法·leetcode