【C++】stack和queue:优先级队列的使用及底层原理

目录

[一 priority-queue 的介绍](#一 priority-queue 的介绍)

[1 priority-queue的文档介绍](#1 priority-queue的文档介绍)

[2 priority-queue的介绍](#2 priority-queue的介绍)

[二 priority-queue的使用](#二 priority-queue的使用)

[1 示例使用](#1 示例使用)

[2 核心接口实现](#2 核心接口实现)

[1 push](#1 push)

[2 top](#2 top)

[3 pop](#3 pop)

[4 empty](#4 empty)

[3 迭代器区间初始化](#3 迭代器区间初始化)

[三 仿函数](#三 仿函数)

[1 仿函数的定义](#1 仿函数的定义)

[2 仿函数可以像函数一样被使用](#2 仿函数可以像函数一样被使用)

[3 不同传参的区别](#3 不同传参的区别)

[4 日期类举例](#4 日期类举例)

[5 拓展 :sort && qsort](#5 拓展 :sort && qsort)

[6 其他仿函数](#6 其他仿函数)

[1 remove remove_if](#1 remove remove_if)

[2 find_if](#2 find_if)

[四 priority-queue完整代码](#四 priority-queue完整代码)


一 priority-queue 的介绍

1 priority-queue的文档介绍

priority-queue的文档介绍

翻译:

  1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素 中最大的。

  2. 此上下文类似于堆,在堆中可以随 时插入元素,并且只能检索最大堆元素(优先队列中位于顶 部的元素)。

  3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue 提供一组特定的成员函数来访问其元素。元素从特定容器的"尾部"弹出,其称为优先队列的 顶部。

  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过 随机访问迭代器访问,并支持以下操作:

empty():检测容器是否为空

size():返回容器中有效元素个数

front():返回容器中第一个元素的引用

push_back():在容器尾部插入元素

pop_back():删除容器尾部元素

  1. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue 类实例化指定容器类,则使用vector。

  2. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用 算法函数make_heap、push_heap和pop_heap来自动完成此操作。

2 priority-queue的介绍

priority_queue(优先队列)是 C++ 标准库中的一种容器适配器,它基于堆(heap) 数据结构实现,能够保证每次取出的元素都是当前队列中优先级最高的元素(默认是最大的元素)

第二个模板参数是容器适配器(容器适配器都不支持迭代器),第三个参数是仿函数(后面讲解)

核心接口:


二 priority-queue的使用

1 示例使用

cpp 复制代码
#include<queue>
#include<iostream>
using namespace std;

int main()
{
   //priority_queue<int> pq; //默认是大的优先级高(大堆)
    priority_queue<int, deque<int>, greater<int>> pq;//调整小的优先级高
    pq.push(3);
    pq.push(1);
    pq.push(5);
    pq.push(7);
    pq.push(2);

    while (!pq.empty())
    {
        cout << pq.top() << " ";
        pq.pop();
    }
    cout << endl;
}

输出结果为1,2,3,5,7 为小堆

2 核心接口实现

priority-queue的私有成员变量只有一个:

cpp 复制代码
private:
		Container _con;

1 push

任何一个数组都可以看作完全二叉树----->算出下标关系

用Push需要用到向上调整算法,因为需要确保它依然是大堆,如果忘了的同学可以看博主之前这一篇复习一下:

【数据结构】二叉树和堆

cpp 复制代码
void push(const T& x)
	{
		_con.push_back(x);
		adjust_up(_con.size() - 1);
	}

从最后一个位置插入,开始向上调整算法:最后一个位置是size()-1

cpp 复制代码
void adjust_up(int child)
{
  int parent = (child-1)/2;
  while(child > 0)
  {
    if(_con[child] > _con[parent])
    {
      swap(_con[child] , _con[parent]);//C++算法库中有,不需要自己实现
      child = parent;//继续向上调整,把父亲给孩子
      parent = (child-1)/2;
    }
    else
    {
      break;
    }
   }
}

上图假设插入的数字是75,向上调整结束之后,75会在根节点的位置

注意:大堆只是父节点比孩子节点大,但是不一定是递减排序!

2 top

cpp 复制代码
const T& top()
	{
			return _con[0];
	}

3 pop

先交换根节点和最后一个节点,尾删。之后从0节点开始向下遍历,确保删除之后依然是大堆

cpp 复制代码
void pop()
	{
		swap(_con[0], _con[_con.size() - 1]);
		_con.pop_back();//尾删

		adjust_down(0);//从0节点开始向下调整
	}
cpp 复制代码
void adjust_down(inr parent)
{
  int child = parent*2+1;//左孩子
  while(child < _con.size())
  {
    if(child+1 < _con.szie() && _con[child+1] > _con[child])
    {
      ++child;//默认左孩子大,但如果右孩子比左孩子大,则++child
    }
    if(_con[child] > _con[parent])
    {
      swap[_con[child],_con[parent]);
      parent = child;//继续向下调整
      child = parent*2+!;
     }
    else
     {
        break;
     }
   }
}  

那怎么从大堆变成小堆:只用将第二个if语句的大于号换成小于号

4 empty

cpp 复制代码
bool empty()
	{
			return _con.empty();
	}

3 迭代器区间初始化

priority-queue支持迭代器区间初始化,但是栈和队列都不支持

迭代器区间,可以传数组,也可以传指向数组的指针

cpp 复制代码
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
		:_con(first, last)
{
	// 建堆
	for (int i = (_con.size()-1-1)/2; i >= 0; i--)//向下调整算法建堆,从最后一个节点的父节点开始
	{
		adjust_down(i);
	}
}

三 仿函数

1 仿函数的定义

在 C++ 中,仿函数(Functor) 也称为函数对象(Function Object) ,是一种 "行为类似函数" 的对象。它的核心是通过在类或结构体中重载 operator() 运算符,使得该类的实例可以像普通函数一样被调用。

仿函数的本质

仿函数不是函数,而是一个对象,但它可以通过 对象名(参数) 的形式调用,就像函数调用一样

仿函数就是一个类,只是仿函数重载了一个特殊的运算符:(),只要重载了这个运算符的类都可以称为仿函数

仿函数的使用在其他地方没有什么用,但是在这里,可以把他看成一个开关,当是小于号的时候是大堆,是大于号的时候是小堆

2 仿函数可以像函数一样被使用

cpp 复制代码
#include <iostream>
using namespace std;

template <class T> 
struct Less
{
    bool operator() (const T& x, const T& y) const { return x < y; }
};

int main()
{
    Less<int> less;
    cout << less(1, 2) << endl;       // 方式1:像函数一样调用
    cout << less.operator()(1, 2) << endl;  // 方式2:显式调用operator()
    return 0;
}

代码解释

  1. 仿函数(函数对象)的定义struct Less 是一个模板结构体,内部重载了 operator() 运算符。这使得 Less 的对象可以像函数一样被调用,因此称为 "函数对象"。

  2. 核心逻辑operator() 的功能是比较两个值 xy,返回 x < y 的结果(即 truefalse)。

  3. 使用方式

    • 先创建 Less<int> 类型的对象 less(指定模板参数为 int,表示比较整数)。
    • 两种调用方式:
      • less(1, 2):简化写法,编译器会自动转换为调用 operator()
      • less.operator()(1, 2):显式调用重载的运算符

3 不同传参的区别

(1)priority-queue.h中

cpp 复制代码
// 强制编译器生成默认构造函数
priority_queue() = default;  // 注意:是default,不是defult

void adjust_up(int child)
{
    Compare com;  // 创建仿函数对象,用于比较
    int parent = (child - 1) / 2;
    while (child > 0)
    {
        // 调用仿函数比较父节点和子节点
        if (com(_con[parent], _con[child]))  // 注意:此处缺少闭合括号
        {
            swap(_con[child], _con[parent]);  // 注意:是swap,不是sawp
            child = parent;
            parent = (child - 1) / 2;
        }
        else
        {
            break;
        }
    }
}

(2)test.cpp

cpp 复制代码
#include "priority_queue.h"

int main()
{
    int a[] = { 30,4,2,66,3 };
    bit::priority_queue<int> pq(a, a+5);//建大堆
    //bit::priority_queue<int, vector<int>, bit::Greater<int>> pq(a, a + 5);//建小堆
    pq.push(3);
    pq.push(1);
    pq.push(5);
    pq.push(7);
    pq.push(2);

    while (!pq.empty())
    {
        cout << pq.top() << " ";
        pq.pop();
    }
    cout << endl;
}

注释那一行是建小堆,上一行是建大堆

由此我们可以看出,当要建大堆的时候,传一个参数,调用的是Less,建小堆的时候,传三个参数,调Greater

但是我们还需要实现less和Greater

4 日期类举例

cpp 复制代码
// 仿函数
// 日期类:比较大小
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {
    }

    bool operator<(const Date& d) const
    {
        return (_year < d._year) ||
               (_year == d._year && _month < d._month) ||
               (_year == d._year && _month == d._month && _day < d._day);
    }

    bool operator>(const Date& d) const
    {
        return (_year > d._year) ||
               (_year == d._year && _month > d._month) ||
               (_year == d._year && _month == d._month && _day > d._day);
    }
};
cpp 复制代码
int main()
{
    jq::priority_queue<Date*, vector<Date*>, PDateLess> pq;  // 现在就不再是按指针去比较,而是按指针变量指向的内容去比较的
    // new出来的地址带有很强的随机性
    pq.push(new Date(2025, 10, 18));
    pq.push(new Date(2025, 10, 19));
    pq.push(new Date(2025, 10, 17));

    while (!pq.empty())
    {
        cout << *pq.top() << " ";
        pq.pop();
    }
    cout << endl;
}

但是我们运行出来之后发现每次运行的结果都不一样。这是因为new开辟的地址有很强的随机性,比较的时候是按照指针去比较,没有意义。所以我们需要自己去实现仿函数控制。

cpp 复制代码
// 自定义仿函数:比较Date*指针指向的对象大小(按<比较)
struct PDateLess
{
    bool operator()(const Date* p1, const Date* p2) const
    {
        // 解引用指针,比较实际Date对象
        return *p1 < *p2; 
    }
};

5 拓展 :sort && qsort

cpp 复制代码
vector<int> v1 = {1,2,3,4,5,6}

greater<int> gt; // 降序
// sort(v1.begin(), v1.end()); // 升序
// sort(v1.begin(), v1.end(), gt); //用仿函数比较更加灵活
sort(v1.begin(), v1.end(), greater<int>()); //加了()

for (auto e : v1)
{
    cout << e << " ";
}
cout << endl;

// 输出: 6 5 4 3 2 1

在C语言中,qsort是函数指针

看开口方向:< 升序 >降序

6 其他仿函数

1 remove remove_if

cpp 复制代码
    // remove: 查找 + 删除 (find + erase)
    // remove_if (也是一个仿函数)
    list<int> lt1 = { 1,6,1,7,3,8,9,3 };
    lt1.remove(1); // 默认的remove,给一个值进行删除
    for (auto e : lt1)
    {
        cout << e << " ";
    }
    cout << endl;
    // 输出: 6 7 3 8 9 3

    return 0;
}

2 find_if


四 priority-queue完整代码

cpp 复制代码
#pragma once
#include<vector>

namespace bit
{
	// 默认大的优先级高
	template<class T, class Container = std::vector<T>>
	class priority_queue
	{
	public:
		template <class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			:_con(first, last)
		{
			// 建堆
			for (int i = (_con.size()-1-1)/2; i >= 0; i--)
			{
				adjust_down(i);
			}
		}

		// 强制编译器生成默认构造
		priority_queue() = default;

		void adjust_up(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_con[child] > _con[parent])
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

		void adjust_down(int parent)
		{
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() && _con[child + 1] > _con[child])
				{
					++child;
				}

				if (_con[child] > _con[parent])
				{
					swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}

		void push(const T& x)
		{
			_con.push_back(x);
			adjust_up(_con.size() - 1);
		}

		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();

			adjust_down(0);
		}

		const T& top()
		{
			return _con[0];
		}

		bool empty()
		{
			return _con.empty();
		}

	private:
		Container _con;
	};
}
相关推荐
让我们一起加油好吗3 小时前
【数论】费马小定理
c++·算法·数论·1024程序员节·费马小定理·逆元
是苏浙3 小时前
零基础入门C语言之操作符详解2
c语言·开发语言
m0_748233644 小时前
单调队列【C/C++】
c语言·c++·算法·1024程序员节
总有刁民想爱朕ha4 小时前
银河麒麟v10批量部署Python Flask项目小白教程
开发语言·python·flask·银河麒麟v10
yi碗汤园4 小时前
【一文了解】八大排序-插入排序、希尔排序
开发语言·算法·unity·c#·1024程序员节
沐知全栈开发4 小时前
React 表单与事件
开发语言
君鼎5 小时前
C++通用业务标准库中常用接口函数总结
c++·1024程序员节
杨筱毅5 小时前
【穿越Effective C++】条款5:了解C++默默编写并调用哪些函数——编译器自动生成的秘密
c++·effective c++·1024程序员节
W.Buffer5 小时前
设计模式-单例模式:从原理到实战的三种经典实现
开发语言·javascript·单例模式