队列和栈以及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^*/ /

练习题目: 150. 逆波兰表达式求解 - 力扣(LeetCode)

3.3 用栈模拟队列

如果要用栈来模拟队列,那么根据栈 "先进后出" 和队列 "先进先出" 的特性,我们需要使用两个栈,一个栈用来存储数据,一个栈用来辅助,比如依次入队 1,2,3,4,那么此时栈的情况如下:

如果此时要弹出队头元素,那么需要将栈 s1 中的元素全部出栈并入栈到栈 s2 中,直到栈 s1 中仅剩下一个元素,如下图所示:

然后将栈 s1 中仅剩的一个元素出栈,即为出队操作,最后再将栈 s2 中的元素依次出栈并入栈到 s1 中即可,如下图所示:

练习题目: 232. 用栈实现队列 - 力扣(LeetCode)

3.4 用队列模拟栈

如果要使用队列来模拟栈,有如下两种方式:

  1. 用一个队列来模拟栈
  2. 用两个队列来模拟栈

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 就会变成空队列,重复上述操作即可实现入栈操作。

练习题目:225. 用队列实现栈 - 力扣(LeetCode)

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 作为底层容器。

posted @ 2022-12-14 14:06  Leaos  阅读(47)  评论(0编辑  收藏  举报