队列和栈以及deque
1. 栈的实现
特点:先进后出、后进先出
1.1 顺序栈
顺序栈是依赖数组实现的。
其代码实现如下:
class SequenceStack {
public:
SequenceStack(int size = 10);
~SequenceStack();
public:
// 入栈
void push(int val);
// 出栈
void pop();
// 返回栈顶元素
int top() const;
// 栈是否为空
bool empty() const;
// 返回栈大小
int size() const;
// 打印
void print() const;
private:
// 数组扩容
void expand(int size);
private:
int* pStack_;
int top_; // 栈顶索引
int cap_; // 当前栈容量
};
SequenceStack::SequenceStack(int size)
: pStack_(new int[size])
, top_(0)
, cap_(size) {
}
SequenceStack::~SequenceStack() {
delete[] pStack_;
pStack_ = nullptr;
}
void SequenceStack::push(int val) {
if (top_ == cap_)
expand(cap_ * 2);
pStack_[top_++] = val;
}
void SequenceStack::pop() {
if (top_ == 0) return;
top_--;
}
int SequenceStack::top() const {
if (top_ == 0)
throw "Stack is empty";
return pStack_[top_ - 1];
}
bool SequenceStack::empty() const {
return top_ == 0;
}
int SequenceStack::size() const {
return top_;
}
void SequenceStack::print() const {
for (int i = top_ - 1; i >= 0; i--) {
cout << pStack_[i] << " ";
}
cout << endl;
}
void SequenceStack::expand(int size) {
int* p = new int[size];
memcpy(p, pStack_, top_ * sizeof(int));
delete[] pStack_;
pStack_ = p;
cap_ = size;
}
1.2 链式栈
链式栈是依赖链表实现的。
其代码实现如下:
class LinkedStack {
public:
LinkedStack();
~LinkedStack();
// 出栈
void push(int val);
// 入栈
void pop();
// 返回栈顶
int top() const;
// 返回栈大小
int size() const;
// 栈是否为空
bool empty() const;
// 打印
void print() const;
private:
struct Node {
Node(int data = 0)
: data_(data)
, next_(nullptr) {
}
int data_;
Node* next_;
};
Node* head_;
int size_;
};
LinkedStack::LinkedStack()
: head_(new Node)
, size_(0) {
}
LinkedStack::~LinkedStack() {
Node* cur = head_->next_;
while (cur) {
head_->next_ = cur->next_;
delete cur;
cur = head_->next_;
}
delete head_;
}
void LinkedStack::push(int val) {
Node* node = new Node(val);
node->next_ = head_->next_;
head_->next_ = node;
size_++;
}
void LinkedStack::pop() {
if (size_ == 0) return;
Node* node = head_->next_;
head_->next_ = node->next_;
size_--;
delete node;
}
int LinkedStack::top() const {
if (size_ == 0)
throw "Stack is empty";
return head_->next_->data_;
}
int LinkedStack::size() const {
return size_;
}
bool LinkedStack::empty() const {
return size_ == 0;
}
void LinkedStack::print() const {
Node* node = head_->next_;
while (node) {
cout << node->data_ << " ";
node = node->next_;
}
cout << endl;
}
2. 队列的实现
特点:先进先出,后进后出
2.1 环形队列
环形队列依赖 数组 实现,但必须实现环形。
其代码实现如下:
class CircleQueue {
public:
CircleQueue(int size = 10);
~CircleQueue();
// 入队
void push(int val);
// 出队
void pop();
// 取队头元素
int front() const;
// 取队尾元素
int back() const;
// 队列是否为空
bool empty() const;
// 返回队列大小
int size() const;
// 打印
void print() const;
private:
// 扩容
void expand(int size);
private:
int* que_;
int cap_; // 空间容量
int front_; // 队头
int rear_; // 队尾
int size_; // 队内元素个数
};
CircleQueue::CircleQueue(int size)
: que_(new int[size])
, cap_(size)
, front_(0)
, rear_(0)
, size_(0) {
}
CircleQueue::~CircleQueue() {
delete[] que_;
que_ = nullptr;
}
void CircleQueue::push(int val) {
if ((rear_ + 1) % cap_ == front_) {
expand(cap_ * 2);
}
que_[rear_] = val;
rear_ = (rear_ + 1) % cap_;
size_++;
}
void CircleQueue::pop() {
if (front_ == rear_) {
throw "Queue is empty";
}
front_ = (front_ + 1) % cap_;
size_--;
}
int CircleQueue::front() const {
if (front_ == rear_) {
throw "Queue is empty";
}
return que_[front_];
}
int CircleQueue::back() const {
if (front_ == rear_) {
throw "Queue is empty";
}
return que_[(rear_ - 1 + cap_) % cap_];
}
bool CircleQueue::empty() const {
return front_ == rear_;
}
int CircleQueue::size() const {
return size_;
}
void CircleQueue::print() const {
for (int i = front_; i != rear_; i = (i + 1) % cap_) {
cout << que_[i] << " ";
}
cout << endl;
}
void CircleQueue::expand(int size) {
int* ptr = new int[size];
int j = 0;
for (int i = front_; i != rear_; i = (i + 1) % cap_, j++) {
ptr[j] = que_[i];
}
delete[] que_;
que_ = ptr;
cap_ = size;
front_ = 0;
rear_ = j;
}
2.2 链式队列
链式队列则依赖 链表 实现。
其代码实现如下:
class LinkedQueue {
public:
LinkedQueue();
~LinkedQueue();
// 入队
void push(int val);
// 出队
void pop();
// 返回队头元素
int front() const;
// 返回队尾元素
int back() const;
// 队列是否为空
bool empty() const;
// 返回队列大小
int size() const;
// 打印队列
void print() const;
private:
struct Node {
Node(int data)
: data_(data)
, next_(nullptr) { }
int data_;
Node* next_;
};
Node* head_;
Node* tail_;
int size_;
};
LinkedQueue::LinkedQueue()
: head_(new Node(0))
, tail_(head_)
, size_(0) {
}
LinkedQueue::~LinkedQueue() {
Node* cur = head_->next_;
while (cur) {
head_->next_ = cur->next_;
delete cur;
cur = head_->next_;
}
delete head_;
head_ = nullptr;
tail_ = nullptr;
}
void LinkedQueue::push(int val) {
Node* node = new Node(val);
tail_->next_ = node;
tail_ = node;
size_++;
}
void LinkedQueue::pop() {
if (size_ == 0)
throw "Queue is empty";
Node* node = head_->next_;
head_->next_ = node->next_;
delete node;
size_--;
if (size_ == 0) {
head_->next_ = head_;
tail_ = head_;
}
}
int LinkedQueue::front() const {
if (head_->next_ == head_)
throw "Queue is empty";
return head_->next_->data_;
}
int LinkedQueue::back() const {
return tail_->data_;
}
bool LinkedQueue::empty() const {
return size_ == 0;
}
int LinkedQueue::size() const {
return size_;
}
void LinkedQueue::print() const {
if (head_->next_ == head_) return;
Node* cur = head_->next_;
while (cur) {
cout << cur->data_ << "->";
cur = cur->next_;
}
cout << endl;
}
3. 常见的算法问题
3.1 括号匹配问题
如下图所示,对于括号的匹配问题,可以使用栈结构来模拟括号匹配。
其基本的过程如下:
- 如果遇到左括号就入栈
- 如果遇到右括号
- 如果栈为空则返回 false
- 如果栈顶元素与当前的右括号不匹配则返回 false
- 如果栈顶元素与当前的右括号匹配则弹出栈顶元素
练习题目: 20. 有效的括号 - 力扣(LeetCode)
3.2 逆波兰表达式求解
逆波兰表达式是一种后缀表达式,所谓后缀就是指运算符写在后面。
- 通常所使用的算式称为中缀表达式,如 (1 + 2) * (3 + 4)
- 该算式的逆波兰表达式为:( (1 2 +) (3 4 +) * )
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即使写成 1 2 + 3 4 + * 也可以依据次序计算出正确的结果
- 适合栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中
例如对于后缀表达式:1 2 + 3 4 + * ,其计算过程如下表:
当前符号 | 数字栈 | 计算结果 |
---|---|---|
1 | 1 | |
2 | 1 2 | |
+ | 3 | 1 + 2 = 3 |
3 | 3 3 | |
4 | 3 3 4 | |
+ | 3 7 | 3 + 4 = 7 |
* | 3 * 7 = 21 |
将一个中缀表达式转化为后缀表达式的过程如下:
- 从左到右依次扫描中缀表达式
- 遇到数字则直接放入后缀表达式
- 如果遇到操作符
- 如果栈为空,则直接将操作符放入符号栈
- 如果栈不为空,则比较当前符号和栈顶符号元素的优先级
- 如果当前符号比栈顶符号的优先级高,则直接入栈
- 如果当前符号比栈顶符号的优先级相等
- 如果当前符号为从左到右结合,即 +、-、*、/ 等,那就将当前符号加入后缀表达式
- 如果当前符号为从右到左结合,即 ^ 等,那就入栈
- 如果当前符号比栈顶符号的优先级低,那就将当前符号放入后缀表达式
- 如果遇到的是 (,则直接入栈
- 如果遇到的是 ),则依次出栈,直到碰到 (
比如对于中缀表达式: ((a + b) c) / ((a - b) a^b) ,其转化过程如下表所示:
当前符号 | 后缀表达式 | 符号栈 |
---|---|---|
( | ( | |
( | (( | |
a | a | (( |
+ | a | ((+ |
b | ab | ((+ |
) | ab+ | ( |
* | ab+ | (* |
c | ab+c | (* |
) | ab+c* | |
/ | ab+c* | / |
( | ab+c* | /( |
( | ab+c* | /(( |
a | ab+c*a | /(( |
- | ab+c*a | /((- |
b | ab+c*ab | /((- |
) | ab+c*ab- | /( |
* | ab+c*ab- | /(* |
a | ab+c*ab-a | /(* |
^ | ab+c*ab-a | /(*^ |
b | ab+c*ab-ab | /(*^ |
) | ab+c*ab-ab^*/ | / |
3.3 用栈模拟队列
如果要用栈来模拟队列,那么根据栈 "先进后出" 和队列 "先进先出" 的特性,我们需要使用两个栈,一个栈用来存储数据,一个栈用来辅助,比如依次入队 1,2,3,4,那么此时栈的情况如下:
如果此时要弹出队头元素,那么需要将栈 s1 中的元素全部出栈并入栈到栈 s2 中,直到栈 s1 中仅剩下一个元素,如下图所示:
然后将栈 s1 中仅剩的一个元素出栈,即为出队操作,最后再将栈 s2 中的元素依次出栈并入栈到 s1 中即可,如下图所示:
3.4 用队列模拟栈
如果要使用队列来模拟栈,有如下两种方式:
- 用一个队列来模拟栈
- 用两个队列来模拟栈
3.4.1 用一个队列模拟栈
如果使用一个队列模拟栈时,假设当前栈中有 n 个元素,那么每次入栈时需要出队 n-1 个元素,并将其入队,例如目前栈中仅有一个元素 1,如下图所示:
此时,入栈一个元素 2,那么此时栈中有 2 个元素,需要依次出队 1 个元素,并将其入队即可,如下图所示:
3.4.2 用两个队列模拟栈
如果使用两个队列模拟栈时,假设目前只入队了一个元素 1,如下图所示:
此时,入栈元素 2,那么需要将其入队到空元素的那个队列中,即上图中的 q2,然后依次将非空队列中的元素出队并入队到另一个队列中,即将队列 q1 中的元素依次出队并入队到队列 q2 中,如下图所示:
此时队列 q1 就会变为空队列,如果继续入栈元素 3,那么就需要将元素 3 入队到队列 q1 中,然后将队列 q2 中的元素依次出队并入队到队列 q1 中,如下图所示:
那么此时,队列 q2 就会变成空队列,重复上述操作即可实现入栈操作。
4. STL实现
4.1 deque
deque 是一种双向开口的线性连续空间,即可以在头尾两端分别做元素的插入和删除操作,且都是常数时间。除此之外,其没有所谓容量概念,因为它是动态的以分段连续的空间组合而成,随时可以增加一段新的连续空间并链接起来。
虽然 deque 也提供 Random Access Iterator
,但是它的迭代器并不是普通指针。
deque 采用一块所谓的 map(并非 STL 的 map)作为主控,这里所谓的 map 是一小块连续空间,其中每个元素都是指针,指向另一段较大的连续线性空间,称为【缓冲区】。缓冲区才是 deque 的储存空间主体。SGI STL 允许我们指定缓冲区大小,默认值 0 表示将使用 512 bytes 的缓冲区。
template<class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
public:
typedef T value_type;
typedef T* pointer;
protected:
typedef pointer* map_pointer;
protected:
map_pointer map; // 指向map
size_type map_size; // map可以容纳多少指针
};
通过源代码可以看出,map 其实是一个 T**,也就是说它是一个指针,所指之物又是一个指针,指向型别为 T 的一块空间。
deque 除了维护一个先前说过的指向 map 的指针外,也维护 start 和 finish 两个迭代器,分别指向第一个缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。此外,它还需要记录目前 map 的大小,因为一旦 map 所提供的节点不足,就必须重新配置更大的一块 map。
deque 自行定义了两个专属的空间配置器:
// 专属空间配置器: 每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
// 专属空间配置器: 每次配置一个指针大小
typedef simple_alloc<pointer, Alloc> map_allocator;
4.2 stack&queue
stack 是一种先进后出的数据结构。由于 stack 系以底部容器完成其所有工作,而具有这种 “修改某物接口,形成另一种风貌” 之性质者,称为 adapter(配接器),因此 stack 往往被归类为 container adapter(容器适配器)。同样的,queue 是一种先进先出的数据结构,也以既有容器 deque 为底部结构,也是一个容器适配器。
stack 所有元素的进出都必须符合 “先进后出” 的条件,只有 stack 的顶端元素,才有机会被外界取用。stack 不提供遍历功能,也不提供迭代器。同样的,queue 也不提供遍历功能,也不提供迭代器。
除了使用 deque 作为底层容器外,stack 和 queue 也可以使用双向链表 list 作为底层容器。