前言

在开始学习栈和队列的具体实现之前,让我们先思考一个更根本的问题:为什么会有栈和队列?

答案就藏在我们身边:

  • 当你使用浏览器的后退按钮,总是能回到刚刚看过的页面;当你在代码中调用函数,系统总能准确返回到之前的位置——这背后就是的“后进先出”思想。

  • 当你在食堂排队打饭,总能保证先来的人先打到饭并离开;当打印机处理文档,先发送的任务先被执行——这体现的正是队列的“先进先出”逻辑。

        计算机科学家们从这些生活智慧中抽象出栈和队列,不是为了增加学习负担,而是为了用最优雅的方式解决程序世界中最常见的两类问题:“暂缓与回溯” 与 “排队与公平”

        理解了它们为何而生,我们才能真正掌握这两种数据结构的精髓。现在,就让我们一起探索它们的实现与应用。

(声明:本文仅使用裸数组和链表来实现栈和队列,不涉及C++面向对象部分)

一、栈:后进先出的数据结构

1.1 栈的基本概念

栈是一种限制访问端点的线性表,它只允许在表的一端进行插入和删除操作。这一端被称为栈顶,另一端称为栈底

就如同一个杯子,杯子的顶端相当于栈顶,底端就相当于是栈底,要想把杯子里面的东西取出来,只能从杯子的上面往外倒

  • 核心操作:push(入栈)、pop(出栈)、top(查看栈顶)

1.2 栈的数组实现

#include <iostream>
using namespace std;
#define MAX 5//最大数量
int stackArr[MAX];
int top = -1;//栈顶的索引,-1表示栈空

bool isEmpty(){
    return top == -1;
}

bool isFull(){
    return top == MAX-1;//判断栈顶是否等于最大数量的索引
}

void push(int e){
    if(isFull()) return; //如果栈已经满了,则不能入栈
    top++; //栈顶索引+1
    stackArr[top]=e;
}

void pop(){
    if(isEmpty()) return; //如果栈为空,则不能出栈
    top--; //栈顶索引-1,由于是数组,不需要担心内存泄漏,所以不需要改变原栈顶的值
}

void printStack(){
        if (isEmpty()) {
        cout << "栈为空" << endl;
        return;
    }
    for(int i =top;i>=0;i--) cout << stackArr[i] <<" ";
    cout << endl;
}


int main() {
    push(10);
    push(20);
    push(30);
    printStack();  // 应输出:30 20 10

    pop();
    printStack();  // 应输出:20 10

    pop();
    pop();
    pop();  // 尝试对空栈执行pop操作

    printStack(); // 应输出:栈为空

    // 测试栈满的情况
    push(1);
    push(2);
    push(3);
    push(4);
    push(5);
    push(6);
    printStack();//应输出 5 4 3 2 1
    return 0;
}

运行结果如下

由此可见,用数组实现栈的代码较为简洁,且占用空间小,然而问题是:它的大小是我们定义的固定值,如果想做到动态大小,就要用链表实现

1.3 栈的链表实现

#include <iostream>
#include <cstdlib>
using namespace std;

struct ListNode{
    int data;
    ListNode* next;
};
ListNode* top=NULL;//定义栈顶链表指针

 bool isEmpty(){
    return top == NULL;
}

void push(int e){
    ListNode* newNode=(ListNode*)malloc(sizeof(ListNode));
    newNode->data=e;
    newNode->next=top;//由于top是栈顶指针也是链表的头指针,所以是新节点指向原节点
    top=newNode;
}

void pop(){
    if(isEmpty()) return; //如果栈为空,则不能出栈
    ListNode* temp=top;
    top=top->next;
    free(temp);
}

void printStack(){
        if (isEmpty()) {
        cout << "栈为空" << endl;
        return;
    }
    ListNode* temp=top;
    while(temp!=NULL){
        cout << temp->data <<" ";
        temp=temp->next;
    }
    cout <<endl;
}

int main() {
    push(10);
    push(20);
    push(30);
    printStack();  // 应输出:30 20 10

    pop();
    printStack();  // 应输出:20 10

    pop();
    pop();
    pop();  // 尝试对空栈执行pop操作

    printStack(); // 应输出:栈为空

    push(1);
    push(2);
    push(3);
    push(4);
    push(5);
    push(6);
    printStack();//应输出 6 5 4 3 2 1
    return 0;
}

运行结果如下

由此可见,链表实现栈的灵活性强,栈为动态大小,但是需要额外分配空间

二、队列:先进先出的数据结构

2.1队列的基本概念

队列是一种限制访问端点的线性表,它只允许在表的一端进行插入(队尾),在另一端进行删除(队首)

就像是排队买东西,在没有插队的情况下,你只能从后面开始排,然后从最前面离开

  • 核心操作:enqueue(入队)、dequeue(出队)、front(查看队首)

2.2 队列的循环数组实现

由于我们在使用普通数列实现队列的时候,设置了头尾指针,这样就会导致出队时头指针后移,最终会导致数组的可使用长度不断减小,直至出现不能进行出队入队的“假溢出”现象

但如果我们把数组变成一个环,即循环数组,就可以避免这个问题

#include <iostream>
using namespace std;

#define MAX 3

int queue[MAX];
int front = 0;//出队处
int rear = 0;//入队处
int count = 0;  // 记录队列中元素个数
bool isEmpty() {
    return count == 0;
}

bool isFull() {
    return count == MAX;
}

void enqueue(int value) {
    if (isFull()) {
        return;
    }
    queue[rear] = value;
    rear = (rear + 1) % MAX;  // 关键:循环移动
    //运算逻辑:若rear+1=max,即后移后越界,则rear=0从头开始,否则相当于rear++
    count++;
}

void dequeue() {
    if (isEmpty()) {
        return;
    }
    front = (front + 1) % MAX;  // 关键:循环移动(原理同上)
    count--;
}

void printQueue() {
    if (isEmpty()) {
        cout << "Queue is empty" << endl;
        return;
    }
    int current = front;//头指针
    for (int i = 0; i < count; i++) {
        cout << queue[current] << " ";
        current = (current + 1) % MAX;//以环的顺序输出count个值
    }
    cout << endl;
}

int main() {

    enqueue(10);
    enqueue(20);
    enqueue(30);
    printQueue();  // 输出:10 20 30

    dequeue();
    dequeue();
    dequeue();
    printQueue();  // 输出:Queue is empty

    // 现在可以继续入队!
    enqueue(40);
    enqueue(50);
    enqueue(60);
    printQueue();  // 输出:40 50 60


    return 0;
}

运行结果如下

2.3 队列的链表实现

#include <iostream>
#include <cstdlib>
#include <queue>
using namespace std;

struct ListNode {
    int data;
    ListNode* next;
};
ListNode* front = NULL;//出队指针
ListNode* rear = NULL;//入队指针

bool isEmpty() {
    return front ==NULL;
}

void enqueue(int value) {
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    newNode->data = value;
    newNode->next = NULL;
    if (isEmpty()) {
        rear = newNode;
        front = newNode;
    }else {
        rear->next = newNode;
        rear = newNode;
    }
}

void dequeue() {
    if (isEmpty()) {
        return;
    }
    ListNode* temp = front;
    front = front->next;
    // 如果删除的是最后一个元素,需要重置rear
    if (front == NULL) {
        rear = NULL;
    }
    free(temp);
}

void printQueue() {
    if (isEmpty()) {
        cout << "Queue is empty" << endl;
        return;
    }
    ListNode* current = front;//头指针
    while (current != NULL) {
        cout << current->data << " ";
        current = current->next;
    }
    cout << endl;
}

int main() {

    enqueue(10);
    enqueue(20);
    enqueue(30);
    printQueue();  // 输出:10 20 30

    dequeue();
    dequeue();
    dequeue();
    printQueue();  // 输出:Queue is empty

    // 现在可以继续入队!
    enqueue(40);
    enqueue(50);
    enqueue(60);
    printQueue();  // 输出:40 50 60


    return 0;
}

显然,链表避免了数组需要采取循环模式的麻烦,只需要及时释放子节点即可

三、C++ STL中的栈和队列

3.1 STL栈的使用

#include <stack>
#include <iostream>

void testSTLStack() {
    stack<int> s;
    
    // 入栈操作
    s.push(1);
    s.push(2);
    s.push(3);
    
    cout << "栈大小: " << s.size() << endl;  // 3
    cout << "栈顶元素: " << s.top() << endl; // 3
    
    // 出栈操作
    s.pop();
    cout << "弹出后栈顶: " << s.top() << endl; // 2
    
    // 遍历栈
    while (!s.empty()) {
        cout << s.top() << " ";
        s.pop();
    }
    // 输出: 2 1
}

3.2 STL队列的使用

#include <queue>

void testSTLQueue() {
    queue<int> q;
    
    // 入队操作
    q.push(1);
    q.push(2);
    q.push(3);
    
    cout << "队头元素: " << q.front() << endl; // 1
    cout << "队列大小: " << q.size() << endl;  // 3
    
    // 出队操作
    q.pop();
    cout << "出队后队头: " << q.front() << endl; // 2
    
    // 遍历队列
    while (!q.empty()) {
        cout << q.front() << " ";
        q.pop();
    }
    // 输出: 2 3
}

3.3 双端队列deque

#include <deque>

void testDeque() {
    deque<int> dq;
    
    // 两端都可以操作
    dq.push_back(1);   // 队尾插入
    dq.push_front(2);  // 队头插入
    dq.push_back(3);
    
    // dq: 2 1 3
    cout << "队头: " << dq.front() << endl; // 2
    cout << "队尾: " << dq.back() << endl;  // 3
    
    dq.pop_front(); // 删除队头
    // dq: 1 3
}

通过本文的探索,我们完成了对栈和队列的完整学习:

  • 理解原理:通过数组和链表的亲手实现,我们深入理解了先进后出和先进先出的工作机制

  • 掌握工具:学习了STL中stack、queue和deque的便捷用法,提升开发效率

  • 明确选择:懂得了何时应该手写实现来理解原理,何时应该使用STL来专注业务逻辑

记住:理解底层原理让你成为更好的程序员,善用工具让你成为更高效的程序员。这两种能力同样重要!

更多推荐