优先队列及其应用

优先队列

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

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

实现方式

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

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

相关推荐
uhakadotcom12 分钟前
构建高效自动翻译工作流:技术与实践
后端·面试·github
斯汤雷12 分钟前
Matlab绘图案例,设置图片大小,坐标轴比例为黄金比
数据库·人工智能·算法·matlab·信息可视化
Asthenia041219 分钟前
深入分析Java中的AQS:从应用到原理的思维链条
后端
Asthenia041233 分钟前
如何设计实现一个定时任务执行器 - SpringBoot环境下的最佳实践
后端
云 无 心 以 出 岫43 分钟前
贪心算法QwQ
数据结构·c++·算法·贪心算法
兔子的洋葱圈1 小时前
【django】1-2 django项目的请求处理流程(详细)
后端·python·django
俏布斯1 小时前
算法日常记录
java·算法·leetcode
Asthenia04121 小时前
如何为这条sql语句建立索引:select * from table where x = 1 and y < 1 order by z;
后端
独好紫罗兰1 小时前
洛谷题单3-P5719 【深基4.例3】分类平均-python-流程图重构
开发语言·python·算法
ihgry1 小时前
SpringBoot+Mybatis实现Mysql分表
后端