这篇博客用来记录stack,queue的学习。
stack的模拟实现
stack的模拟实现比较简单,先上代码
#pragma once
#include<vector>
#include<list>
#include<deque>
#include<iostream>
using std::deque;
using namespace std;
namespace bit
{
template<class T, class Con = deque<T>>
class stack
{
public:
stack();
void push(const T& x)
{
_c.push_back(x);
}
void pop()
{
_c.pop_back();
}
T& top()
{
return _c.back(); //容器的尾在这里就是栈的栈顶
}
const T& top() const
{
return _c.back();
}
size_t size() const
{
return _c.size();
}
bool empty() const
{
return _c.empty();
}
private:
Con _c;
};
}
这里要提及的点是: 首先,看到模板类处传的默认参数是deque,<其实这里是容器适配器>
deque属于既包含vector的一些优点又有一些list的优点,但由于他样样通样样松导致他不被我们广泛使用,这里不对deque进行详细介绍。
queue的模拟实现
和stack的模拟实现类似,还是比较好写的,直接上代码:
#pragma once
#include<vector>
#include<list>
#include<deque>
#include<iostream>
using std::deque;
using namespace std;
namespace bit
{
template<class T, class Con = deque<T>>
class queue
{
public:
queue();
void push(const T& x)
{
_c.push_back(x);
}
void pop()
{
_c.pop_front();
}
T& back()
{
return _c.back();
}
const T& back() const
{
return _c.back();
}
T& front()
{
return _c.front();
}
const T& front() const
{
return _c.front();
}
size_t size() const
{
return _c.size();
}
bool empty() const
{
return _c.empty();
}
private:
Con _c;
};
};
queue和stack对比起来,不同的点在于queue有了队头和队尾的区别。出现front和back访问队头和队尾的元素,而栈上是top获取栈顶元素。 队列在pop处是有差别的,queue的pop是对队头front进行删除。
接下来,介绍的才是重点:
优先级队列
优先级队列,就是我们对队列进行排序。 排序的过程类似于前面学过的堆,我们先来看几个操作。
template <class T, class Container = vector<T>, class Compare = less<T> >
class priority_queue
{
public:
priority_queue();
bool empty()
{
return c.empty();
}
size_t size()
{
return c.size();
}
const T& top()
{
return c[0]; // 这里我们默认用的容器是vector,因为我们后面都有进行排序,所以
//c[0]就可以访问到第一个元素(最大或者最小根据所传类参数决定)
}
private:
Container c;
Compare comp;
上面代码有一个地方,可能会比较迷惑。 就是传参数后面有一个class Compare的东西。
这个东西有个名字,叫仿函数
仿函数是什么?
仿函数可以理解成对类对象进行了包装,使得类对象能够像函数一样去使用。 但本质是在类中进行了操作符的重载而实现一个类似于函数功能的作用。
那么,这里我们传入这个仿函数,在接下来插入或删除中就可以用上了!
了解了仿函数之后,我们再来看看优先级队列的插入和删除操作
插入操作,我们进行排序类似于堆,所以我们回忆一下大堆小堆的插入删除。
以前我们向上调整时,判断条件是对parent和child的值进行判断。
但如果我们改变排序的顺序,例如从升序改成降序,我们这里就可能有多处代码要改。那为了简便,我们有了仿函数的概念。
仿函数该怎么具体使用,让我们来看看:
首先,我们要明确的点是,之前说了,仿函数其实是一个类。我们用仿函数,是因为他里面通过运算符重载实现了我们想要的操作!
那么这里我们先定义这样的两个类:
template<class T>
class less //这个类用来排大堆,要注意这个单词虽然翻译过来是小,
//但我们这里用这个函数实现的结果是大堆
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class greater //这个类用来排小堆
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
那么,我们了解这个点之后,我们再将他放入代码中,去实现push和pop操作
下面这块代码紧接上面的代码
template <class T, class Container = vector<T>, class Compare = less<T> >
class priority_queue
{
public:
void Adjustup(size_t child)
{
Compare com; //这里很重要! 我们通过传入的Compare类创建了这样一个对象,
//之后利用这个对象,去比较大小
int parent = (child - 1) / 2;
while (child > 0)
{
//这里就是不同点了,我们通过传入两个参数来比较大小!!
// 这里三种写法都是对的,上面两种采用的是匿名对象的方式, 但还是推荐使用第三种!
//if(Compare().operator()(c[parent],c[child]))
//if(Compare()(c[parent],c[child]))
//if(c[parent]<c[child]) //以前我们的方法
if (com(c[parent], c[child]))
{
swap(c[parent], c[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void push(const T& x)
{
c.push_back(x);
Adjustup(c.size() - 1); // 这里传参不要传错了,不然会出现问题
}
void Adjustdown(size_t parent)
{
Compare com;
int child = parent * 2 + 1;
while (child < c.size())
{
if (child + 1 < c.size() && com(c[child],c[child + 1]))
child = child + 1;
if (com(c[parent], c[child]))
{
swap(c[child], c[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
swap(c[0], c[c.size() - 1]);
c.pop_back();
Adjustdown(0);
}
private:
Container c;
Compare comp;
};
可以看到,我们比较大小是直接通过对象,来充分使用operator()来比较大小,我们注意写好传入参数的先后顺序, 之后只需要控制传入的类类型就可以轻松控制排序。
接下来看看这段代码实现的结果吧!
void test1()
{
// 用默认的less排降序(大堆)
bit::priority_queue<int> pq1;
pq1.push(10);
pq1.push(20);
pq1.push(30);
pq1.push(40);
while (!pq1.empty())
{
cout << pq1.top() << ' ';
pq1.pop();
}
cout << endl;
// 用greater排升序(小堆)
bit::priority_queue<int, vector<int>, bit::greater<int>> pq2;
pq2.push(10);
pq2.push(20);
pq2.push(30);
pq2.push(40);
while (!pq2.empty())
{
cout << pq2.top() << ' ';
pq2.pop();
}
cout << endl;
}
接着,我们进行一下升级。 我们传入Date日期类类型,看看可不可以同样实现
对Date日期类进行排序
// 首先这一段是对Date类的操作
class Date
{
public:
friend ostream& operator<<(ostream& _cout, const Date& d);
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);
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
template<class T>
class greater //这个类用来排小堆
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
void test2()
{
// 日期类进行排升序
bit::priority_queue<Date, vector<Date>, greater<Date>> pq;
Date d1(2024, 4, 8);
pq.push(d1);
pq.push(Date(2024, 4, 10));
pq.push({ 2024, 2, 15 });
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
那么这依然是能实现的。
接下来有一个特殊情况,如果我们传入的是指针类型呢?那还能不能比较大小呢?
指针类型的比较大小
bit::priority_queue<Date*, vector<Date*>> pqptr;
pqptr.push(new Date(2024, 4, 14));
pqptr.push(new Date(2024, 4, 11));
pqptr.push(new Date(2024, 4, 15));
while (!pqptr.empty())
{
cout << *(pqptr.top()) << " ";
pqptr.pop();
}
cout << endl;
很遗憾,这里并不能实现我们的效果。
因为如此,毕竟的是指针本身的大小, 而这个大小属于随机的,因此比较出来结果也会是个随机值。因此我们必须为此写一个指针解引用去比较的仿函数
在上一段代码前我们需要传入
// 这里没有给模版(template<>),所以后面调用这个类时,就不需要传入对应的类模板参数,
// 直接调用这个类就可以了
class GreaterPDate
{
public:
bool operator()(const Date* p1, const Date* p2)
{
return *p1 > *p2; // 这里是大于,就是后面形成小堆,(这个类叫Greater)
}
};
bit::priority_queue<Date*, vector<Date*>, GreaterPDate> pqptr;
pqptr.push(new Date(2024, 4, 14));
pqptr.push(new Date(2024, 4, 11));
pqptr.push(new Date(2024, 4, 15));
while (!pqptr.empty())
{
cout << *(pqptr.top()) << " ";
pqptr.pop();
}
cout << endl;
如此,我们才能保证每次比较的是指针解引用后的结果。这样才能稳定形成结果!
当然,这里我写的是升序的代码,老样子,把Greater改成less的代码,就是降序的了!