11.stack和queue的模拟实现和相关习题(容器适配器和优先级队列)

1. stack的介绍和使用

1.1 stack的介绍

  1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。

  2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。

  3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
    empty:判空操作
    back:获取尾部元素操作
    push_back:尾部插入元素操作
    pop_back:尾部删除元素操作

  4. 标准容器vectordequelist均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque

image-20230225113400092

1.2 stack的使用

最小栈

image-20240415194808582

image-20240415195308539

解题思路:

1.创建2个栈结构,st就正常插入,minST就插入st的最小值

2.每当给st插入一个值时,就将这个值与minST的栈顶元素进行比较,如果小于或者等于minST的栈顶元素,那么就插入到minST

3.如果进行比较时minST为空,那么就直接插入

4.当st弹出的栈顶元素,为了保证minst的栈顶元素为最小元素,因此,如果st的栈顶元素和minst的栈顶元素相等,那么也要同时弹出minst的栈顶元素

image-20230225114831205

class MinStack {
public:
    // 此处可以不写构造函数,那么编译器就会生成默认的构造函数
    // 如果写了,那么自定义类型,经过初始化列表,就会被初始化
    // 自定义类型就是指 stack<int> _st  和  stack<int> _minst;
    MinStack() {}
    // 此处写的构造函数是默认构造函数,当初始化自定义类型的对象时,会调用它们各自的默认构造函数,来初始化它们(默认构造函数就是不需要传参的构造函数)
    
    void push(int val) {
        // 现将数据插入到栈_st中
        _st.push(val);

        // 如果此时_minst为空,则直接插入到_minst, 如果val小于或者等于_minst的栈顶元素,那么也将其插入到_minst中
        if(_minst.empty() || val <= _minst.top())
        {
            _minst.push(val);
        }

    }

    // 弹出元素,也是弹出_st的栈顶元素
    // 但是为了保证_minst的栈顶元素为最小元素,因此,如果_st的栈顶元素和_minst的栈顶元素相等,那么也要同时弹出_minst的栈顶元素   
    void pop() 
    {
        // 必须先进行判断,而不是先弹出_st的栈顶元素
        if(_st.top() == _minst.top())
             _minst.pop();
        
        _st.pop();     
    }
    
    // 栈顶元素为_st的栈顶元素
    int top() 
    {
        return _st.top();
    }
    
    // 最小元素存放在_minst的栈顶
    int getMin()
    {
        return _minst.top();
    }

    // 此处为自定义类型,因此不需要我们去写构造函数,使用默认构造函数就可以
    stack<int> _st;
    stack<int> _minst;
};

栈的压入、弹出序列

image-20240415200943267

解题思路:

入栈序列:1 2 3 4 5,所有的元素都存放在vector<int> pushV
出栈序列:4 5 3 2 1,所有的元素都存放在vector<int> popV

1.创建一个栈stack<int> st;

2.将pushV的元素依次压栈到st中,每压入一个元素,就将其与出栈序列popV的栈顶元素相比较
如果两个元素相等,则将st的栈顶元素出栈
如果两个元素不相等,则将入栈序列接着入栈

    // 1.前3个元素都与popV的栈顶元素不相等,因此将其都入栈
    // 2.第四个元素,与popV的栈顶元素比较,相等,因此将其从st出栈,并将popV的栈顶元素出栈
    // 3.再次比较st的栈顶元素和popV的栈顶元素,如果相等,则将其从st出栈,并将popV的栈顶元素出栈
    // 4.如果不相等,则继续循环,入栈下一个元素,如果栈为pushV空,那么结束循环
    // 5.此时,如果st.empty()为真,那么说明入栈序列与出栈序列相匹配,反之,则不匹配
st: 1 2 3 4
    
    // 入栈的第五个元素,,与popV的栈顶元素比较,相等,因此将其从st出栈,并将popV的栈顶元素出栈
st: 1 2 3 5
    // 此时,pushV的所有元素都已经入栈了
    
    // 再次比较st的栈顶元素和popV的栈顶元素,如果相等,则将其从st出栈,并将popV的栈顶元素出栈
st: 1 2 3
st: 1 2
st: 1
    // 如果st.empty()为真,那么说明入栈序列与出栈序列相匹配,反之,则不匹配
st: empty
class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        stack<int> st;
        size_t popi = 0;

        for(auto e : pushV)
        {
            st.push(e);

            // 我们需要保证st不为空,如果栈为空,则证明出栈序列和入栈序列匹配
            // 如果不设置  !st.empty() , 当栈为空,然后调用st.pop(),编译器会报警
            while(!st.empty() && st.top() == popV[popi])
            {
                st.pop();
                ++popi;
            }
        }

        return st.empty();
    }
};

逆波兰表达式求值

image-20240415203302984

  • 波兰表达式

    波兰表达式(也称为前缀表达式)是一种将操作符置于操作数之前的表达式表示方法。在波兰表达式中,每个操作符之后都跟着其对应的操作数,这样可以明确地表示操作符与操作数之间的关系,而无需使用括号来指定操作的顺序。波兰表达式的求值方法是从左到右扫描表达式,并根据操作符的优先级进行计算。

    举例来说,考虑下面这个简单的波兰表达式:

    + 3 * 4 5
    

    在这个表达式中,操作符 + 跟在操作数 3 的前面,而操作数 45 分别跟在操作符 * 的前面。按照波兰表达式的规则,这个表达式的含义是先计算乘法操作 4 * 5,然后将结果与操作数 3 进行加法运算。因此,这个表达式的求值过程如下:

    1. 计算 4 * 5,得到结果 20
    2. 将结果 20 与操作数 3 进行加法运算,得到最终结果 23

    因此,波兰表达式 + 3 * 4 5 的值为 23

    波兰表达式求值的优点是可以避免使用括号来指定操作的优先级,同时也减少了求值过程中的操作符和操作数之间的混淆。

  • 逆波兰表达式

    逆波兰表达式(也称为后缀表达式)是一种将操作符置于操作数之后的表达式表示方法。在逆波兰表达式中,每个操作符之前都跟着其对应的操作数,这样可以明确地表示操作符与操作数之间的关系,而无需使用括号来指定操作的顺序。逆波兰表达式的求值方法是从左到右扫描表达式,并根据操作符的优先级进行计算。

    举例来说,考虑下面这个简单的逆波兰表达式:

    3 4 5 * +
    

    在这个表达式中,操作数 3 在操作符 + 的前面,而操作数 45 则分别在操作符 * 的前面。按照逆波兰表达式的规则,这个表达式的含义是先计算乘法操作 4 * 5,然后将结果与操作数 3 进行加法运算。因此,这个表达式的求值过程如下:

    1. 计算 4 * 5,得到结果 20
    2. 将结果 20 与操作数 3 进行加法运算,得到最终结果 23

    因此,逆波兰表达式 3 4 5 * + 的值为 23

    逆波兰表达式求值的优点是可以避免使用括号来指定操作的优先级,同时也减少了求值过程中的操作符和操作数之间的混淆。逆波兰表达式在计算机科学中被广泛用于算术表达式的求值,以及栈和递归的实现。

  • 中缀表达式

    中缀表达式是我们通常书写算术表达式的方式,即操作符位于两个操作数之间。例如,2 + 3 * 4 就是一个中缀表达式。

    在中缀表达式中,操作符的优先级和结合性需要通过括号来明确。例如,2 + 3 * 4* 的优先级比 + 高,因此 3 * 4 先计算,结果再与 2 相加。

    中缀表达式通常需要解析器或者编译器进行解析和求值。相比于逆波兰表达式或者波兰表达式,中缀表达式在书写上更直观,但在解析和计算时需要处理操作符优先级和括号的问题。

  • stoi()的用法

    在C++中,stoi()函数用于将字符串转换为对应的整数类型。具体而言,stoi()函数接受一个字符串参数,然后将其解析为一个整数,并返回解析后的整数值。

    int stoi (const string& str, size_t* idx = 0, int base = 10);
    
    • str:要转换的字符串。
    • idx:可选参数,用于存储转换过程中第一个无法转换的字符的索引位置。如果不需要索引位置,可以将该参数设为nullptr。
    • base:可选参数,指定转换所使用的进制,默认为10进制。

    例如:

    #include <iostream>
    #include <string>
    
    int main() {
        std::string str = "12345";
        int num = std::stoi(str);
        std::cout << "The integer value is: " << num << std::endl;
        return 0;
    }
    

    在这个例子中,字符串 “12345” 被转换为整数,并存储在变量 num 中。stoi() 函数默认使用十进制来解析字符串,因此它将字符串 “12345” 转换为整数 12345。

    如果字符串无法转换为整数,或者转换过程中遇到无效字符,则会抛出std::invalid_argument异常。如果转换结果超出了目标整数类型的表示范围,则会抛出std::out_of_range异常。

解题思路:

如:3 4 5 * +
    1.判断vector<string>& tokens的元素是操作符还是操作数,操作数入栈到st栈对象中
    2.如果是操作符,那么此时st至少有两个操作数(根据后缀表达式)
    3.将左操作数和右操作数出栈,并按中缀顺序进行计算,再将计算结果入栈到st对象中
    4.返回st的栈顶元素
    
注:vector<string>& tokens = {"-1","2","4"};  
// vector中存放的是字符串

for(auto str : tokens)
{
    // 第一次循环
    str 就是 "-1"
    // 如果使用str[0] 来判断是否是运算符号,像 ‘-1’ 是无法判定的,
    // 这是因为在 C++ 中,负号 ‘-’ 既可以表示负数,也可以表示减号运算符。因此,当字符串以负号开头时,我们无法仅仅通过检查第一个字符来确定它是一个负数还是一个减号运算符 
}
// 由题目要求这个函数就是将后缀表达式,改为我们常用的中缀表达式

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        
        // 创建一个栈对象st
        stack<int> st;

       for(auto str : tokens)
       {
           // 比较两个字符串的大小,其实比较的是ASCLL值
           // c++ 重载了 <, >, !=, == 等等
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                // 因为操作数入栈时,是左操作数先入栈,右操作数后入栈
                // 因此,出栈时,是右操作数先出栈,左操作数后出栈
                // 并且,操作符是紧跟在两个操作数之后的
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();

                // swicth传参,必须传一个整型(语法规定)
                // str[0]的类型是char,char也是整型家族的一员
                switch(str[0])
                {
                        // 用单引号引出的一个字符本质上代表的就是一个整数,整数的数值由编译器的字符集来表示。比如ASCLL字符集的编译器下 ,字符 'A' 的含义就是 十进制的 65。
                    case '+':
                        // 
                        st.push(left+right);
                        break;
                    case '-':
                        st.push(left-right);
                        break;
                    case '*':
                        st.push(left*right);
                        break;
                    case '/':
                        st.push(left/right);
                        break;
                }
            }
            else
            {
                // 如果str不是操作符,那么用stoi()将str这个字符串转化为整型,并将其入栈到st中
                 st.push(stoi(str));
            }
       }
 
        return st.top();
    }
};

1.3 stack的模拟实现

//template<class T, class Container = vector<T>>
template<class T, class Container = deque<T>>
class stack
{
public:
    void push(const T& x)
    {
        _con.push_back(x);
    }

    void pop()
    {
        _con.pop_back();
    }

    const T& top()
    {
        return _con.back();
    }

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

    size_t size()
    {
        return _con.size();
    }
private:
    Container _con;
};
  • Stack.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

// #define N 100
// typedef int STDataType;
// struct Stack
// {
//	 STDataType a[N];
//	 int top;
// };


// 将数据类型定义为STDataType
typedef int STDataType;

// 将栈结构的类类型定义为ST
typedef struct Stack
{
	STDataType* a;
	int top;  // 栈顶元素的下标
	int capacity;  // 栈的容量
}ST;   

// 初始化栈
void StackInit(ST* ps);
// 销毁栈
void StackDestroy(ST* ps);
// 入栈
void StackPush(ST* ps, STDataType x);
// 出栈
void StackPop(ST* ps);

// 取栈顶元素
STDataType StackTop(ST* ps);

// 判断栈是否为空
bool StackEmpty(ST* ps);

// 栈的大小
int StackSize(ST* ps);
  • Stack.c
#include "Stack.h"

void StackInit(ST* ps)
{
	assert(ps);
    // 将栈在堆上的空间,也就是a指向的空间初始化,将a初始化为空
	ps->a = NULL;
    // 将栈顶元素的下标和栈的容量都初始化为0
	ps->top = ps->capacity = 0;
}

// 销毁栈
void StackDestroy(ST* ps)
{
	assert(ps);
    // 释放栈空间
	free(ps->a);
    // 为了防止野指针,将a置为空
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

// 入栈
void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	// 在入栈前,需要判断栈的容量是否足够,如果不够,那么需要进行扩容
	if (ps->top == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity*sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		ps->a = tmp;
		ps->capacity = newCapacity;
	}

	ps->a[ps->top] = x;
	ps->top++;
}

// 出栈
void StackPop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	
	--ps->top;
}

// 取栈顶元素
STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	
	return ps->a[ps->top - 1];
}

// 栈是否为空
bool StackEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}

// 栈的大小
int StackSize(ST* ps)
{
	assert(ps);

	return ps->top;
}
  • main.c
#include <stdio.h>
#include "Stack.h"

// 解耦 -- 低耦合 高内聚
// 数据结构建议不要直接访问结构数据,一定要通过函数接口访问
void TestStack()
{
	ST st;
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	printf("%d ", StackTop(&st));
	StackPop(&st);
	printf("%d ", StackTop(&st));
	StackPop(&st);

	StackPush(&st, 4);
	StackPush(&st, 5);

	while (!StackEmpty(&st))
	{
		printf("%d ", StackTop(&st));
		StackPop(&st);
	}
	printf("\n");
}

int main()
{
	TestStack();

	return 0;
}

2.queue的介绍和使用

2.1 queue的介绍

  1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。

  2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。

  3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
    empty:检测队列是否为空
    size:返回队列中有效元素的个数
    front:返回队头元素的引用
    back:返回队尾元素的引用
    push_back:在队列尾部入队列
    pop_front:在队列头部出队列

  4. 标准容器类dequelist满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque

image-20240415223142929

2.2 queue的使用

用队列实现栈

image-20240415223246103

5.栈和队列的模拟实现以及相应习题-CSDN博客中有详细解答

2.3 queue的模拟实现

#pragma once
#include<vector>
#include <list>

namespace bit
{
    // 因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实现queue,
	//template<class T, class Container = list<T>>
	template<class T, class Container = deque<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_front();
		}

		const T& front()
		{
			return _con.front();
		}

		const T& back()
		{
			return _con.back();
		}

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

		size_t size()
		{
			return _con.size();
		}
	private:
		Container _con;
	};
}

5.栈和队列的模拟实现以及相应习题-CSDN博客中有详细队列的实现过程

3. 容器适配器(双端队列)

3.1什么是适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
image-20240415220046021

3.2 STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STLstackqueue默认使用deque,比如:

image-20230225170040671

4.3 deque的简单介绍(了解)

4.3.1 deque的原理介绍

  • deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

image-20240415220414352

  • deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

image-20230225170421804

image-20240415220538663

  • 双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

image-20240415220630625

  • 那deque是如何借助其迭代器维护其假想连续的结构呢?

image-20230225170451572

4.3.2 deque的缺陷

vector比较,deque的优势是:

  • 头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
  • 但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vectorlistdeque的应用并不多,而目前能看到的一个应用就是,STL用其作为stackqueue的底层数据结构。

4.4 为什么选择deque作为stack和queue的底层默认容器

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()pop_back()操作的线性结构,都可以作为stack的底层容器,比如vectorlist都可以;queue是先进先出的特殊线性数据结构,只要具有push_backpop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stackqueue默认选择deque作为其底层容器,主要是因为:

1. stack和queue不需要遍历(因此`stack`和`queue`没有迭代器),只需要在固定的一端或者两端进行操作。
    2. 在stack中元素增长时,`deque`比`vector`的效率高(扩容时不需要搬移大量数据);`queue`中的元素增长时,`deque`不仅效率高,而且内存使用率高。
    结合了`deque`的优点,而完美的避开了其缺陷。

4.优先级队列的介绍和使用(也就是堆结构)

4.1 priority_queue的介绍

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

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

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

  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
    empty():检测容器是否为空
    size():返回容器中有效元素个数
    front():返回容器中第一个元素的引用
    push_back():在容器尾部插入元素
    pop_back():删除容器尾部元素

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

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

4.2 priority_queue的使用

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。

数组中第K个大的元素

image-20240415223736222
image-20230226142851447

// std::priority_queue
// 优先级队列priority_queue的类模板
template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type> > 
      class priority_queue;
// class T 数据类型的模板
// class Container = vector<T> 容器的模板
// class Compare = less<typename Container::value_type> 比较函数的模板



// std::priority_queue::priority_queue
// 优先级队列priority_queue的构造函数
template <class InputIterator>
         priority_queue (InputIterator first, InputIterator last,   // 迭代器
                         const Compare& comp = Compare(),           // 比较函数
                         const Container& ctnr = Container());      // 容器
// 解题思路1:建大堆
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 优先级队列默认建的是大堆,使用迭代区间来建立优先级队列
        priority_queue<int> pq(nums.begin(), nums.end());
        while(--k)
        {
            pq.pop();
        }

        // const value_type& top() const;
        return pq.top();
    }
};
// 解题思路2:建小堆
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        // 优先级队列默认建的是大堆,使用迭代区间来建立优先级队列
        // 如果需要建小堆,我们需要指定模板参数
        /*
        template <class T, class Container = vector<T>,
             class Compare = less<typename Container::value_type> > class priority_queue;
        */
        priority_queue<int,vector<int>, greater<int>> pq(nums.begin(), nums.begin()+k);
        
        // 剩余的n-k个数,依次替换堆顶元素
        for(size_t i = k; i < nums.size(); ++i)
        {
            if(nums[i] > pq.top())
            {
                pq.pop();
                pq.push(nums[i]);
            }
        }

        // 此时,小堆的堆顶元素为第k大元素
        return pq.top();
    }
};

4.3 优先级队列的模拟实现

仿函数举例1

// 仿函数/函数对象
namespace qwy
{
	template<class t>
	class less
	{
	public:
        // 仿函数
		bool operator()(const t& x, const t& y) const
		{
			return x < y;
		}
	};

	template<class t>
	class greater
	{
	public:
        // 仿函数
		bool operator()(const t& x, const t& y) const
		{
			return x > y;
		}
	};
}

// 仿函数没有成员变量,则大小为1个字节,用来占位;因此传参时,是否使用引用都是可以的
template<class t, class compare>
// void bubblesort(t* a, int n, const compare& com)
void bubblesort(t* a, int n, compare com)
{
	for (int j = 0; j < n; ++j)
	{
		int exchange = 0;
		for (int i = 1; i < n - j; ++i)
		{
			// if (a[i] < a[i - 1]) 
            // 此处是使用仿函数进行比较的,如果a[i] < a[i - 1] 为真,那么将其进行交换
			if (com(a[i], a[i - 1]))  
			{
				swap(a[i - 1], a[i]);
				exchange = 1;
			}
		}

		if (exchange == 0)
		{
			break;
		}
	}
}

int main()
{
	qwy::less<int> lessfunc;
	qwy::greater<int> greaterfunc;

    // 下面两种调用仿函数对象的方法是一样的
	// lessfunc(1, 2);
	// lessfunc.operator()(1, 2);

	int a[] = { 2, 3, 4, 5, 6, 1, 2, 4, 9 };

    // 两种结果是一样的(都是升序),使用匿名对象或者有名对象
    // lessfunc -> 有名对象
    // qwy::less<int>()  -> 匿名对象
    
	bubblesort(a, sizeof(a) / sizeof(int), lessfunc);
	// bubblesort(a, sizeof(a) / sizeof(int), qwy::less<int>());


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

    // 使用greaterfunc仿函数对象,那么bubblesort排列出的数组就是降序
	bubblesort(a, sizeof(a) / sizeof(int), greaterfunc);
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

仿函数举例2

// 日期类
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);
	}

    // 友元函数
	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

// 仿函数(这个仿函数当*d1 < *d2为真,则仿函数的返回值为真)
struct PDateLess
{
	bool operator()(const Date* d1, const Date* d2)
	{
		return *d1 < *d2;
	}
};

// 仿函数 (这个仿函数当*d1 > *d2为真,则仿函数的返回值为真)
struct PDateGreater
{
	bool operator()(const Date* d1, const Date* d2)
	{
		return *d1 > *d2;
	}
};



void TestPriorityQueue()
{
	// 优先级队列默认创建的是大堆
    // 需要用户在自定义类型中提供 < 操作符的重载,这是因为创建大堆时,采用了向下调整的算法
    // 在向下调整算法中,需要比较两个日期类的大小,因此需要重载< 操作符,这样才可以完成两个日期类的比较
    // 1.创建一个优先级队列的对象q1
	priority_queue<Date> q1;
    
	q1.push(Date(2018, 10, 29));
	q1.push(Date(2018, 10, 28));
	q1.push(Date(2018, 10, 30));
    
    // 取堆顶元素
	cout << q1.top() << endl;

	// 如果要创建小堆,需要用户提供 > 操作符的重载
	priority_queue<Date, vector<Date>, greater<Date>> q2;
	q2.push(Date(2018, 10, 29));
	q2.push(Date(2018, 10, 28));
	q2.push(Date(2018, 10, 30));
    
    // 取栈顶元素(在日期类中,也有流插入的重载)
	cout << q2.top() << endl;


	// 大堆
    // 此时我们需要比较不是日期类的指针,
    // new Date(2018, 10, 29) 的时候,返回的指针是随机的
    // 因此我们需要自己重新写一个仿函数,给priority_queue,传递我们自己的仿函数
    // 新的仿函数,比较的是 解引用的指针,所指向的日期类对象
    
    // 并且需要传递仿函数PDateGreater
    // 对于仿函数PDateGreater *d1 > *d2返回值为真
    // 对于对于仿函数PDateLess  *d1 < *d2返回值为真
	priority_queue<Date*, vector<Date*>, PDateLess> q3;
	q3.push(new Date(2018, 10, 29));
	q3.push(new Date(2018, 10, 28));
	q3.push(new Date(2018, 10, 30));
	cout << *q3.top() << endl;

	// 小堆
    // 注:template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type> > class priority_queue;
	priority_queue<Date*, vector<Date*>, PDateGreater> q4;
	q4.push(new Date(2018, 10, 29));
	q4.push(new Date(2018, 10, 28));
	q4.push(new Date(2018, 10, 30));
	cout << *q4.top() << endl;
}


int main()
{
	TestPriorityQueue();

	return 0;
}

向上调整

image-20240416121449443

void adjust_up(size_t child)
{
	Compare com;
    // 父亲节点的下标 = (孩子节点的下标 - 1) / 2
	size_t parent = (child - 1) / 2;
    
    // 当孩子节点的下标 = 0 时,说明孩子节点已经调整到了堆顶,则向上调整完毕
	while (child > 0)
	{
		//if (_con[parent] < con[child])
		if (com(_con[parent], _con[child]))
		{
            // 交换孩子节点和父亲节点的值
			swap(_con[child], _con[parent]);
            
            
            // 迭代
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

向下调整

image-20240416123136471

// Container _con; 就是实现优先队列的底层容器(一般就是使用vector来实现的)
void adjust_down(size_t parent)    // 传递过来的是最后一个父亲节点的下标
{
    // 创建一个仿函数对象
	Compare com;
    
    // 左孩子的下标 = 父亲的下标 * 2 + 1
	size_t child = parent * 2 + 1;
    
    // child < n; n就是堆的最大下标
    // 当 child > _con.size() 时,说明已经向下调整完毕了,此时孩子节点的下标,已经比数组的最大下标还大
	while (child < _con.size())
	{
        // 1.建大堆
        // 保证child+1 < _con.size() 是为了保证右孩子存在
		// 如果 (child+1 < _con.size() && _con[child] < _con[child+1]) 为真,那么右孩子是最大值(与左孩子相比较)
        // 那么更新child为右孩子节点的下标,否则不需要更新
        // child对应的节点就是两个孩子节点的最大值
        // 如果父亲节点的值小于孩子节点的值,那么就需要将父亲节点向下调整(根据大根堆的性质)
        
        // 如果仿函数是Funcless,那么_con[child] < _con[child+1],Funcless函数的返回值为真

        // 2.建小堆
        //  如果 (child+1 < _con.size() && _con[child] > _con[child+1]) 为真,那么右孩子是最小值(与左孩子相比较)
        // 那么更新child为右孩子节点的下标,否则不需要更新
        // child对应的节点就是两个孩子节点的最小值
        // 如果父亲节点的值大于孩子节点的值,那么就需要将父亲节点向下调整(根据小根堆的性质)
        
        // 如果仿函数是graterless,那么_con[child] < _con[child+1],Funcless函数的返回值为真
		if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
		{
			child++;
		}

        // 1.建大堆
		// if (_con[parent] < _con[child])
        // 2.减小堆
        // if (_con[parent] > _con[child])
        
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
            
            // 迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

完整的模拟实现

namespace qwy
{
    // 仿函数的类模板
	template<class T>
	class less
	{
	public:
		bool operator()(const T& x, const T& y) const
		{
			return x < y;
		}
	};

    // 仿函数的类模板
	template<class T>
	class greater
	{
	public:
		bool operator()(const T& x, const T& y) const
		{
			return x > y;
		}
	};

	// 大堆
	template<class T, class Container = vector<T>, class Compare = less<T>>
	class priority_queue
	{
	public:
        // 默认构造函数
		priority_queue()
		{}

        // 构造函数,需要传参(参数是一个迭代器区间)
		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);
			}
		}
        
        // 向下调整
        void adjust_down(size_t parent)
		{
			Compare com;
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{
				//if (child+1 < _con.size() && _con[child] < _con[child+1])
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
				{
					child++;
				}

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

        // 向上调整
		void adjust_up(size_t child)
		{
			Compare com;
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[parent] < con[child])
				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

        // 每次向_con的尾部push一个元素,我们就需要对堆尾部的元素做向上调整
		void push(const T& x)
		{
			_con.push_back(x);

			adjust_up(_con.size() - 1);
		}

		

        // 出队
        // 为了不破坏堆的结构,如果需要弹出堆顶的元素,我们需要将_con的首元素和最后一个元素进行交换,再将其最后一个元素删除,再对堆顶的首元素做向下调整
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();

			adjust_down(0);
		}

        // 取队顶元素
		const T& top() const
		{
			return _con[0];
		}

        // 队列是否为空
		bool empty() const
		{
			return _con.empty();
		}

        // 队列的大小
		size_t size() const
		{
			return _con.size();
		}
        
	private:
		Container _con;
	};
}
  • 34
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值