前言
作者:小蜗牛向前冲
名言:我可以接受失败,但我不能接受放弃
如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。
目录
一、stack的基本知识
1、什么是栈
2、栈的基本使用
3、栈的模拟实现
二、queue的基本知识
1、什么是队列
2、队列的基本用法
3、队列的模拟实现
三、优先队列
1、优先队列的基本知识
2、仿函数
3、priority_queue的模拟实现
4、反向迭代器的模拟实现
本期学习目标:了解栈和队列的基本知识,简单的模拟实现栈和队列,认识双端队列和priority_queue的使用,知道什么是仿函数,模拟实现反向迭代器。
一、stack的基本知识
1、什么是栈
栈(Stack)是一种常见的数据结构,它遵循后进先出(LIFO)的原则。栈是一种STL中的容器,类似于现实生活中的堆叠物体,只能在顶部进行插入和删除操作。
栈具有两个主要操作:
- 入栈(Push):将元素添加到栈的顶部,成为新的栈顶。
- 出栈(Pop):从栈的顶部移除元素,并返回被移除的元素。
s
由于栈遵循后进先出的原则,最后一个入栈的元素将是第一个出栈的元素,即最新的元素先被访问或移除。
栈还具有一个特殊的属性——栈顶指针(Top),它指向栈的顶部元素。当执行入栈操作时,栈顶指针上移;而在执行出栈操作时,栈顶指针下移。
栈常常用于解决与层次结构相关的问题,例如函数调用、表达式求值、括号匹配、回溯算法等。在计算机内存管理中,栈也被用于存储函数的局部变量、参数以及程序执行期间的临时数据。
需要注意的是,栈具有一定的容量限制,称为栈的大小。当栈已满时,执行入栈操作将导致栈溢出(Stack Overflow);而当栈为空时,执行出栈操作将导致栈下溢(Stack Underflow)
2、栈的基本使用
对栈(stack)来说在C++中进行来封装,他的底层我们看做是用一个类来实现的,所以他有自己的成员函数和成员变量。通过前面我们对vector和list的学习相信大家对STL容器的都比较熟悉了,下面我们主要和大家介绍一下stack的重点接口.
栈的重要接口:
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 判断栈是否为空 |
size() | 返回栈中有多少的元素 |
top() | 返回栈顶中的元素 |
push() | 将元素val压入栈中 |
pop() | 将stack中尾部弹出 |
栈代码使用演示:
在演示之前,看一个代码:
for (auto e : st)
{
}
//代码不可用
运行之后程序会报错,这是为什么呢? 这是因为使用aoto for循环的本质其中也运用到了迭代器,也就是要有begin(),end()的成员函数,但是stack是没有的。
那我们将怎么样去打印出stack中的数据呢?
这里我就用用到top()获取栈顶元素,和pop()将栈顶元素出栈就可以了。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<stack>
using namespace std;
int main()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
cout << endl;
return 0;
}
3、栈的模拟实现
为了更好的理解栈,下面我将模拟实现栈。模拟实现栈时,我们不妨思考一下我们是用数组实现还是用链表实现栈呢?
我们知道数组有数组的好处,链表也有链表的的好处,那我们可以二个都用可以吗?
其实是可以的,这就不得不提一下适配器模式,那什么是适配器模式呢?
其实就是将以有的东西封装转化成你想要的东西。
template<class T, class Container>
class stack
{
public:
private:
Container _con;
};
这里我们只要多传一个模板参数,Container就可以了。
我们在用自己定义栈,创建对象时多传一个参数就可以了,这样我们就在底层上既实现用list实现的栈,又实现了vector实现的栈
stack<int,linst<int>> st;//链表实现
stack<int,vector<int>> st;//数组实现
下面我们只要完善好成员函数就可以实现自己的栈了
namespace pjb
{
template<class T, class Container >
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;
};
}
测试 :
二、queue的基本知识
1、什么是队列
队列(Queue)是一种数据结构,它遵循先进先出(First-In-First-Out,FIFO)的原则。队列中的数据项按照插入的顺序排列,最先插入的数据项首先被移除。
队列可以比喻成现实生活中的排队等候的场景。新来的人必须排在队尾,而排在队头的人最先离开队列。
队列有两个基本操作:
- 入队(Enqueue):将数据项添加到队列的末尾。
- 出队(Dequeue):将队列的第一个数据项移除,并返回该数据项。
队列常常用于模拟系统中的排队行为,如任务调度、消息传递等。在编程中,队列可以使用数组或链表等数据结构来实现。
但是要注意并非所有队列都是先进先出的,有一个叫优先级的队列就不是这样的,后面在为大家介绍,我们先了解基本队列的用法。
2、队列的基本用法
队列我们在使用这个容器的时候,重点关注几个接口就好了
队列的重要接口:
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
队列代码演示:
int main()
{
queue<int> qe;
qe.push(1);
qe.push(2);
qe.push(3);
qe.push(5);
qe.push(6);
while (!qe.empty())
{
cout << qe.front() << " ";
qe.pop();
}
}
3、队列的模拟实现
这里我们常常说,vector和list都有各自的优缺点:
vector:
缺点:头部中部插入删除效率低,扩容有消耗
list:
缺点:不支持随机访问,CPU高速缓命中低。
其实vector和list的缺点和优点是互补的,那有没有一个容器兼容二者的优点呢?
C++中有一个容器 deque(双端队列)(Double Ended Queue,简称为Deque)是一种允许从两端进行插入和删除操作的队列。它可以在队列的前端和后端同时进行插入和删除操作。
双端队列支持以下几种基本操作:
- 在队头插入元素(Insert at Front):将一个元素插入到双端队列的前端。
- 在队尾插入元素(Insert at Rear):将一个元素插入到双端队列的后端。
- 从队头删除元素(Delete from Front):从双端队列的前端删除并返回一个元素。
- 从队尾删除元素(Delete from Rear):从双端队列的后端删除并返回一个元素。
- 获取队头元素(Get Front):返回双端队列的前端元素。
- 获取队尾元素(Get Rear):返回双端队列的后端元素。
双端队列优缺点:
优点:
- 与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不 需要搬移大量的元素,因此其效率是必vector高的。
- 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段
缺点:
- 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到 某段小空间的边界,导致效率低下
所以当我们传模板的第二个参数时可以直接传deque。
namespace pjb
{
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& fornt()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
三、优先队列
1、优先队列的基本知识
优先队列(Priority Queue)是一种特殊的队列(是一种容器适配器),它根据元素的优先级来确定出队的顺序。与普通队列不同,优先队列中的元素被赋予了优先级,优先级高的元素会先被出队。
优先队列的基本操作包括:
- 入队(Enqueue):将元素插入到优先队列中,并根据其优先级进行适当的排序。
- 出队(Dequeue):移除具有最高优先级的元素,并返回该元素。
- 获取队头元素(Get Front):获取具有最高优先级的元素,但不从队列中移除。
优先队列可以使用不同的数据结构来实现,其中一个常见的实现方式是使用堆(Heap)。堆是一种完全二叉树,具有一些特殊的性质使得它适用于实现优先队列。在堆中,树的每个节点都比它的子节点具有更高的优先级。
优先队列的特点:
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成 堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意: 默认情况下priority_queue是大堆。
优先队列的操作函数:
函数声明 | 接口说明 |
priority_queue()/priority_queue(first,last) | 构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回 false |
top( ) | 返回优先级队列中最大(最小元素),即堆顶元素 |
push() | 在优先队列中插入元素 |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
注意:
- 默认情况下,priority_queue是大堆。
- 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载
2、仿函数
仿函数(Functor)是一种具有函数行为的对象,实际上是将函数行为封装在一个类或结构体中。它可以像函数一样被调用,可以作为函数参数传递,也可以作为返回值返回。
在C++中,仿函数可以通过重载operator()
运算符来实现函数调用操作。通过这种方式,仿函数可以像普通函数一样被调用,而且可以带有自己的状态和行为。
class AddFunctor {
public:
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
AddFunctor add;
int result = add(3, 4); // 调用仿函数
// 结果为7
return 0;
}
那我们不由的想到函数和仿函数有什么区别吗?
仿函数(Functor)实际上是一个类对象,它的对象可以像函数一样被调用。
函数是一段可执行的代码块,可以通过函数名直接调用。
区别:
类对象:仿函数是一个类对象,而函数是一个独立的代码块。仿函数的行为可以通过重载函数调用运算符
operator()
来定义,使得它可以被像函数一样调用。状态保持:仿函数可以在自己的内部保留状态信息,这些信息可以在不同的函数调用之间保持,并影响后续的函数调用。而函数通常不具备状态保持的能力,每次调用都是独立的,无法直接记住之前的状态。
可定制性:由于仿函数是一个类对象,我们可以根据需要定制它的行为,通过它的成员变量和函数实现更复杂的逻辑。函数则相对简单,无法直接修改其内部的逻辑。
应用场景:仿函数常用于STL的算法中,比如排序、查找、遍历等操作,因其灵活的行为定义和可定制性。函数则广泛应用于各种编程场景,无论是简单的数学运算还是复杂的业务逻辑
3、priority_queue的模拟实现
在模拟实现优先队列,我们重点关注向上调整和向下调整的方法,来进行建堆。
这里我们也用到我们上面讲的仿函数:
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template<class T>
class greator
{
bool operator()(const T& x, const T& y)const
{
return x > y;
}
};
大家可能不由的会想,这个就不是一个简单的大小比较吗?有必要写的这么复杂吗?,这种仿函数有必要被定义出来吗?
其实不然,虽然我们在写简单大小比较时候,是会增加了代码的复杂性,但是在实际的开发中,我们经常遇到需要对数据进行复杂排序或计算等操作,此时使用仿函数就可以极大地提高程序的灵活性和可扩展性。
以排序为例,STL提供了多种排序算法,如sort
、stable_sort
等,这些算法均可通过传入一个比较函数来定义排序规则。使用仿函数定义排序规则时,我们可以根据自己的需要灵活地定制排序规则,从而实现更加复杂的排序操作
#pragma once
#include<algorithm>
namespace pjb
{
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template<class T>
class greator
{
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)
{
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
{
adjust_down(i);
}
}
}
//向上调整法
void adjust_up(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if(com(_con[parent]<_con[child])
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void adjust_down(size_t parent)
{
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//找最大的孩子
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
child++;
}
//父亲节点<孩子节点交换
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
//交换堆头和堆尾
std::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;
};
}
4、反向迭代器的模拟实现
template<class Iterator,class Ref,class Ptr>
class ReverseIterator
{
typedef ReverseIterator<Iterator, Ref, Ptr>Self;
public:
ReverseIterator(Iterator it)
:_it(it)
{}
Ref operator*()
{
Iterator tmp = _it;
return *(--tmp);
}
Ptr operator->()
{
return &(operator*());
}
Self& operator--()
{
++_it;
return *this;
}
bool operator!=(const Self& s)const
{
return _it != s._it;
}
private:
Iterator _it;
};