队列和栈
·队列
一. 队列的实现
(这里只谈论循环队列, 普通队列空间浪费太严重)
-
建议使用类来封装,因为stl中的queue和stack也都是封装好的。
-
循环队列反正是使用vector来实现的,毕竟能实现随机访问。
-
有头指针和尾指针(用来指向最后一个元素的后一个位置),还有一个表示数据的数目。(由于空元素的存在,数组实际的大小为该元素值 + 1——所有操作都要考虑这一点)
-
实现的几个方法
-
判断队空,队满。
-
出队,入队。
-
取队首,队尾元素。
-
二. 队列的应用
队列似乎没什么用,只有在BFS的时候才可以整。。不过哪怕这样他也很有用了。
三. 模拟队列
#include<iostream>
#include<vector>
using namespace std;
template<typename T>
class MyCircularQueue {
private:
size_t head_ = 0;
size_t rear_ = 0;
vector<T> data_;
int size_;
public:
// Rule of Three
// 虽然我们想开辟一个长为size的vector,但事实上由于空元素的存在,要开辟为size_ + 1个
// 注意template的使用
MyCircularQueue<T>(int size) : size_(size) { data_.resize(size_ + 1); } // Cntr, 对vector进行resize是value-initialize
MyCircularQueue<T>(const MyCircularQueue<T> &) = default;
~MyCircularQueue<T> () = default;
// 下面只进行6个操作:入队,出队,判断队空,队满,获得队首,队尾元素
// 判断队空
bool IsEmpty() { return (head_ == rear_); }
// 判断队满
bool IsFull() { return (rear_ + 1) % (size_ + 1) == head_; }
// 入队
bool EnQueue(const T &x) {
if (IsFull()) {
return false;
}
data_[rear_] = x;
rear_ = (rear_ + 1) % (size_ + 1);
return true;
}
// 出队
bool DeQueue() {
if (IsEmpty()) {
return false;
}
head_ = (head_ + 1) % (size_ + 1);
return true; // 别忘了返回true
}
// 获得队尾元素
T &GetRear() { return data_[(rear_ - 1) % (size_ + 1)]; } // rear_始终指向最后一个元素的后一个位置
// 获得队首元素
T &GetFront() { return data_[head_]; }
};
int main() {
MyCircularQueue<int> *my_queue = new MyCircularQueue<int>(3);
for (int i = 1; i <= 3; ++i) {
my_queue->EnQueue(i);
}
// 这里的boolalpha是用来控制把布尔型变量输出成false和true的
cout << boolalpha << my_queue->EnQueue(4) << endl; // 这里应该输出false
cout << my_queue->GetRear() << endl; // 应该输出3
my_queue->DeQueue();
my_queue->EnQueue(4);
cout << my_queue->GetRear() << endl; // 应该输出4
return 0;
}
·栈
一. 栈的实现
栈的实现特别简单,因为出栈入栈都是在尾部进行操作的。
而这一点和动态数组(指vector)如出一辙:push_back, pop_back。
stl为了实现算法的高效性甚至都没有给vector提供front方法。这点也和stack不谋而合。
栈只要实现四个方法就好了:
-
判断栈是否为空
-
入栈,出栈
-
取栈顶元素
二. 栈的用途
栈的用处似乎不这么大,因为我们随时可以编写递归调用OS的call stack。——当然要考虑溢出。
栈的用处似乎也特别大,因为有很多题目我们第一眼看就会想到栈。这是哪些题目呢?
比如说:从前往后遍历,当遇到某一个特殊元素时,处理前面的元素。
在栈底的元素,优先级都老低了;越往栈顶优先级越高,碰到出栈信号立刻出栈。
这些问题可以查看我的leetcode博客。
三. 模拟栈
// 动态数组和栈完美契合
// 你要知道vector是完全没有在数组最前面操作的方法的
// 但是其pop_back和push_back简直完美
#include<iostream>
#include<vector>
using namespace std;
template<typename T>
class Stack {
private:
// 惭愧啊!惭愧!说到动态数组,我的第一想法竟然是new
// stack的实现确实比队列简单多了,因为只要一个vector即可
// 另外,如果有top方法,pop就不需要返回栈顶元素了(各司其职嘛)
vector<T> data_;
public:
// 只要实现最简单的四个方法即可
// 出栈入栈
void Push(T &x) { data_.push_back(x); }
bool Pop() {
if (data_.empty()) {
return false;
}
data_.pop_back();
return true;
}
// 判断栈是否为空
bool IsEmpty() { return data_.empty(); }
// 获取栈顶元素
// (像这种函数是无法判断队空的,除非使用特殊的约定;因为只能返回一个类型)
T &Top() { return data_.back(); } // 我曹还真的有back方法啊
};
int main() {
Stack<int> my_stack;
for (int i = 1; i <= 4; ++i) {
my_stack.Push(i);
}
cout << boolalpha << my_stack.IsEmpty() << endl;
my_stack.Pop();
cout << "栈顶元素:" << my_stack.Top() << endl;
return 0;
}
· 其他
-
你要知道栈和队列都是封装好了的,也就是说,你只能通过那几个方法进行栈和队列的操作。也就是说,你不能用for遍历栈和队列,包括resize等许多algorithm都不能使用。
-
遍历和入栈的关系:以前的我总是隐隐约约地感觉遍历和栈是一对矛盾。现在我有点弄懂了。通过遍历,可以使更好的元素入栈。——啥意思?比如解析一个算数表达式字符串,碰到数字时,我总要把整个数字(而不是单个字符)入栈。如果抛开遍历,还是采用栈来存储该字符串中的字符。那会特别麻烦(因为要不断地反转)
-
关于栈和队列的数量:没有什么是一个栈或者队列解决不了的;如果有,那就两个。