【C/C++】深入解析 Stack 与 Queue 数据结构(详解):实现原理、应用场景与性能优化

文章目录

    • 引言
    • 栈(Stack)数据结构详解
      • [1. 栈的基本概念](#1. 栈的基本概念)
      • [2. 栈的实现原理](#2. 栈的实现原理)
      • [3. C++中的栈实现](#3. C++中的栈实现)
      • [4. 栈的应用场景](#4. 栈的应用场景)
      • [5. 栈的性能分析](#5. 栈的性能分析)
      • [6. 实战示例:括号匹配](#6. 实战示例:括号匹配)
    • 队列(Queue)数据结构详解
      • [1. 队列的基本概念](#1. 队列的基本概念)
      • [2. 队列的实现原理](#2. 队列的实现原理)
      • [3. C++中的队列实现](#3. C++中的队列实现)
      • [4. 队列的应用场景](#4. 队列的应用场景)
      • [5. 队列的性能分析](#5. 队列的性能分析)
      • [6. 实战示例:任务调度](#6. 实战示例:任务调度)
    • 栈与队列的比较与选择
    • 性能优化技巧
      • [1. 选择合适的底层容器](#1. 选择合适的底层容器)
      • [2. 避免不必要的内存分配](#2. 避免不必要的内存分配)
      • [3. 使用移动语义](#3. 使用移动语义)
      • [4. 并行处理](#4. 并行处理)
      • [5. 编译器优化](#5. 编译器优化)
    • 更多文章
    • 结论

引言

在软件开发中,数据结构 是组织和存储数据的核心方式。合理选择和使用数据结构不仅能提高程序的效率,还能简化代码逻辑,增强可维护性。其中,**栈(Stack)队列(Queue)**作为最基础的线性数据结构,广泛应用于各种算法和系统设计中。然而,掌握它们的底层实现、应用场景及性能优化策略,是每位C++开发者必备的技能。

【体验最新的GPT系列模型!支持Open API调用、自定义助手、文件上传等强大功能,助您提升工作效率!点击链接体验:CodeMoss & ChatGPT-AI中文版

栈(Stack)数据结构详解

1. 栈的基本概念

是一种**后进先出(LIFO, Last In First Out)**的数据结构。这意味着最后被压入栈中的元素将最先被弹出。栈常用于需要逆序处理数据的场景,如函数调用管理、表达式解析等。

基本操作

  • Push:向栈顶添加一个元素。
  • Pop:移除栈顶的元素。
  • Top/Peek:查看栈顶的元素而不移除它。
  • IsEmpty :检查栈是否为空。

2. 栈的实现原理

栈的实现可以基于数组或链表。基于数组的实现通常具有较低的内存占用和更快的访问速度,但需要预先设置栈的大小。基于链表的实现则更为灵活,能够动态调整大小,但每个元素需要额外的指针存储空间。

基于数组的实现

cpp 复制代码
class Stack {
private:
    int* arr;
    int top;
    int capacity;
public:
    Stack(int size = 100);
    ~Stack();
    void push(int x);
    int pop();
    int peek();
    bool isEmpty();
};

Stack::Stack(int size) {
    arr = new int[size];
    capacity = size;
    top = -1;
}

Stack::~Stack() {
    delete[] arr;
}

void Stack::push(int x) {
    if(top == capacity -1) {
        throw std::overflow_error("Stack Overflow");
    }
    arr[++top] = x;
}

int Stack::pop() {
    if(isEmpty()) {
        throw std::underflow_error("Stack Underflow");
    }
    return arr[top--];
}

int Stack::peek() {
    if(!isEmpty()) {
        return arr[top];
    }
    throw std::underflow_error("Stack is empty");
}

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

基于链表的实现

cpp 复制代码
struct Node {
    int data;
    Node* next;
};

class Stack {
private:
    Node* head;
public:
    Stack() : head(nullptr) {}
    ~Stack();
    void push(int x);
    int pop();
    int peek();
    bool isEmpty();
};

Stack::~Stack() {
    while(!isEmpty()) {
        pop();
    }
}

void Stack::push(int x) {
    Node* temp = new Node();
    temp->data = x;
    temp->next = head;
    head = temp;
}

int Stack::pop() {
    if(isEmpty()) {
        throw std::underflow_error("Stack Underflow");
    }
    Node* temp = head;
    head = head->next;
    int popped = temp->data;
    delete temp;
    return popped;
}

int Stack::peek() {
    if(!isEmpty()) {
        return head->data;
    }
    throw std::underflow_error("Stack is empty");
}

bool Stack::isEmpty() {
    return head == nullptr;
}

3. C++中的栈实现

C++标准库提供了std::stack,这是一个模板类,基于其他容器(如vectordequelist)实现。默认情况下,std::stack使用std::deque作为底层容器,但也可以选择其他容器。

使用std::stack的示例

cpp 复制代码
#include <iostream>
#include <stack>

int main() {
    std::stack<int> s;
    
    // Push elements
    s.push(10);
    s.push(20);
    s.push(30);
    
    // Display and pop elements
    while(!s.empty()) {
        std::cout << ' ' << s.top();
        s.pop();
    }
    
    return 0;
}

输出:

 30 20 10

std::stack提供了以下关键成员函数:

  • push(const T& value):压入元素。
  • pop():弹出栈顶元素。
  • top():访问栈顶元素。
  • empty():检查栈是否为空。
  • size():返回栈的大小。

4. 栈的应用场景

栈在计算机科学中有着广泛的应用,主要包括:

  • 函数调用管理:程序调用函数时,会将函数的返回地址、参数等信息压入调用栈,函数执行完毕后,从栈中弹出这些信息。
  • 表达式解析:如中缀表达式转换为后缀表达式、括号匹配等。
  • 深度优先搜索(DFS):在图或树的遍历中,DFS通常使用栈来记录访问路径。
  • 撤销操作:在编辑器等应用中,实现撤销(Undo)操作时,会用到栈来记录历史操作。

5. 栈的性能分析

栈的主要操作(Push、Pop、Top)通常具有常数时间复杂度(O(1)),无论是基于数组还是链表的实现。这使得栈在需要频繁进行这些操作的场景下,表现出色。

基于数组的栈

  • 时间复杂度
    • Push: O(1) 平均,O(n) 在需要扩展数组时
    • Pop: O(1)
    • Top: O(1)
  • 空间复杂度:预先分配的空间固定,但可以通过动态数组实现动态扩展。

基于链表的栈

  • 时间复杂度
    • Push: O(1)
    • Pop: O(1)
    • Top: O(1)
  • 空间复杂度:每个元素需要额外的指针存储空间,但无需预先分配固定大小的空间。

6. 实战示例:括号匹配

括号匹配是栈的经典应用之一。通过栈,我们可以有效地检查表达式中的括号是否配对正确。

问题描述 :给定一个包含括号的字符串,检查括号是否正确匹配。支持的括号类型包括(){}[]

解决思路

  1. 遍历字符串的每一个字符。
  2. 如果遇到左括号,将其压入栈中。
  3. 如果遇到右括号,检查栈顶是否有对应的左括号:
    • 如果有,则弹出栈顶。
    • 如果没有,则括号不匹配。
  4. 遍历结束后,检查栈是否为空:
    • 如果为空,括号匹配正确。
    • 如果不为空,括号不匹配。

代码实现

cpp 复制代码
#include <iostream>
#include <stack>
#include <string>
#include <unordered_map>

bool isValidParentheses(const std::string& s) {
    std::stack<char> stk;
    std::unordered_map<char, char> mapping = { {')', '('}, {'}', '{'}, {']', '['} };
    
    for(char c : s) {
        if(mapping.find(c) != mapping.end()) {
            if(!stk.empty() && stk.top() == mapping[c]) {
                stk.pop();
            } else {
                return false;
            }
        } else if(c == '(' || c == '{' || c == '[') {
            stk.push(c);
        }
        // 忽略其他字符
    }
    return stk.empty();
}

int main() {
    std::string expr = "{[()()]}";
    if(isValidParentheses(expr)) {
        std::cout << "括号匹配正确" << std::endl;
    } else {
        std::cout << "括号匹配错误" << std::endl;
    }
    return 0;
}

输出

括号匹配正确

复杂度分析

  • 时间复杂度:O(n),其中n是字符串的长度。
  • 空间复杂度:O(n),在最坏情况下,栈中可能存储所有的左括号。

队列(Queue)数据结构详解

1. 队列的基本概念

队列是一种**先进先出(FIFO, First In First Out)**的数据结构。这意味着最先进入队列的元素将最先被移出队列。队列广泛应用于需要按顺序处理任务的场景,如任务调度、广度优先搜索等。

基本操作

  • Enqueue:向队尾添加一个元素。
  • Dequeue:移除队头的元素。
  • Front/Peek:查看队头的元素而不移除它。
  • IsEmpty:检查队列是否为空。

【体验最新的GPT系列模型!支持Open API调用、自定义助手、文件上传等强大功能,助您提升工作效率!点击链接体验:CodeMoss & ChatGPT-AI中文版

2. 队列的实现原理

与栈类似,队列的实现也可以基于数组或链表。基于数组的队列通常采用循环数组来有效利用空间;基于链表的队列则通过前后指针实现高效的入队和出队操作。

基于数组的循环队列实现

cpp 复制代码
class Queue {
private:
    int* arr;
    int front;
    int rear;
    int capacity;
public:
    Queue(int size = 100);
    ~Queue();
    void enqueue(int x);
    int dequeue();
    int peek();
    bool isEmpty();
};

Queue::Queue(int size) {
    arr = new int[size];
    capacity = size;
    front = 0;
    rear = -1;
}

Queue::~Queue() {
    delete[] arr;
}

void Queue::enqueue(int x) {
    if((rear + 1) % capacity == front) {
        throw std::overflow_error("Queue Overflow");
    }
    rear = (rear + 1) % capacity;
    arr[rear] = x;
}

int Queue::dequeue() {
    if(isEmpty()) {
        throw std::underflow_error("Queue Underflow");
    }
    int item = arr[front];
    front = (front + 1) % capacity;
    return item;
}

int Queue::peek() {
    if(!isEmpty()) {
        return arr[front];
    }
    throw std::underflow_error("Queue is empty");
}

bool Queue::isEmpty() {
    return rear == -1;
}

基于链表的实现

cpp 复制代码
struct Node {
    int data;
    Node* next;
};

class Queue {
private:
    Node* front;
    Node* rear;
public:
    Queue() : front(nullptr), rear(nullptr) {}
    ~Queue();
    void enqueue(int x);
    int dequeue();
    int peek();
    bool isEmpty();
};

Queue::~Queue() {
    while(!isEmpty()) {
        dequeue();
    }
}

void Queue::enqueue(int x) {
    Node* temp = new Node();
    temp->data = x;
    temp->next = nullptr;
    if(rear == nullptr) {
        front = rear = temp;
        return;
    }
    rear->next = temp;
    rear = temp;
}

int Queue::dequeue() {
    if(isEmpty()) {
        throw std::underflow_error("Queue Underflow");
    }
    Node* temp = front;
    front = front->next;
    if(front == nullptr) {
        rear = nullptr;
    }
    int dequeued = temp->data;
    delete temp;
    return dequeued;
}

int Queue::peek() {
    if(!isEmpty()) {
        return front->data;
    }
    throw std::underflow_error("Queue is empty");
}

bool Queue::isEmpty() {
    return front == nullptr;
}

3. C++中的队列实现

C++标准库提供了std::queue,这是一个模板类,基于其他容器(如dequelist)实现。默认情况下,std::queue使用std::deque作为底层容器,但也可以选择其他容器。

使用std::queue的示例

cpp 复制代码
#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;
    
    // Enqueue elements
    q.push(10);
    q.push(20);
    q.push(30);
    
    // Display and dequeue elements
    while(!q.empty()) {
        std::cout << ' ' << q.front();
        q.pop();
    }
    
    return 0;
}

输出:

 10 20 30

std::queue提供了以下关键成员函数:

  • push(const T& value):入队元素。
  • pop():出队元素。
  • front():访问队头元素。
  • back():访问队尾元素。
  • empty():检查队列是否为空。
  • size():返回队列的大小。

4. 队列的应用场景

队列在计算机科学和工程中有着广泛的应用,主要包括:

  • 任务调度:操作系统中的任务调度器通常使用队列来管理待处理的任务。
  • 广度优先搜索(BFS):在图或树的遍历中,BFS使用队列来记录访问顺序。
  • 消息队列:在分布式系统中,消息队列用于异步通信和任务分发。
  • 缓冲区管理:如打印任务缓冲、网络数据包缓冲等。

5. 队列的性能分析

队列的主要操作(Enqueue、Dequeue、Front、Back)通常具有常数时间复杂度(O(1)),无论是基于数组还是链表的实现。这使得队列在需要频繁进行这些操作的场景下,表现出色。

基于数组的队列(循环队列)

  • 时间复杂度
    • Enqueue: O(1) 平均,O(n) 在需要扩展数组时
    • Dequeue: O(1)
    • Front: O(1)
    • Back: O(1)
  • 空间复杂度:预先分配的空间固定,但循环数组能有效利用空间,减少浪费。

基于链表的队列

  • 时间复杂度
    • Enqueue: O(1)
    • Dequeue: O(1)
    • Front: O(1)
    • Back: O(1)
  • 空间复杂度:无需预先分配固定大小的空间,但每个元素需要额外的指针存储空间。

6. 实战示例:任务调度

在多线程或异步编程中,任务调度是一个常见的需求。通过队列,我们可以实现一个简单的任务调度器,确保任务按照提交的顺序依次执行。

问题描述:构建一个简单的任务调度器,支持任务的提交和按顺序执行。

解决思路

  1. 使用std::queue存储待执行的任务。
  2. 提供一个接口用于提交任务。
  3. 提供一个接口用于执行下一个任务。

代码实现

cpp 复制代码
#include <iostream>
#include <queue>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>

class TaskScheduler {
private:
    std::queue<std::function<void()>> tasks;
    std::mutex mtx;
    std::condition_variable cv;
    bool stop;
    std::thread worker;
    
    void workerThread() {
        while(true) {
            std::function<void()> task;
            {
                std::unique_lock<std::mutex> lock(mtx);
                cv.wait(lock, [this]{ return !tasks.empty() || stop; });
                if(stop && tasks.empty()) return;
                task = tasks.front();
                tasks.pop();
            }
            task();
        }
    }
    
public:
    TaskScheduler() : stop(false), worker(&TaskScheduler::workerThread, this) {}
    
    ~TaskScheduler() {
        {
            std::unique_lock<std::mutex> lock(mtx);
            stop = true;
        }
        cv.notify_all();
        worker.join();
    }
    
    void submit(std::function<void()> task) {
        {
            std::unique_lock<std::mutex> lock(mtx);
            tasks.push(task);
        }
        cv.notify_one();
    }
};

int main() {
    TaskScheduler scheduler;
    
    // 提交任务
    scheduler.submit([](){ std::cout << "任务1执行\n"; });
    scheduler.submit([](){ std::cout << "任务2执行\n"; });
    scheduler.submit([](){ std::cout << "任务3执行\n"; });
    
    // 给出一些时间让任务执行
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    return 0;
}

输出

任务1执行
任务2执行
任务3执行

说明

  • TaskScheduler类内部维护一个任务队列,使用互斥锁和条件变量确保线程安全和高效的任务调度。
  • workerThread函数在后台线程中不断等待任务,并按顺序执行。
  • 当任务调度器销毁时,会通知后台线程停止工作。

复杂度分析

  • 提交任务:O(1),入队操作。
  • 执行任务:依赖任务本身的执行时间,入队和出队操作为O(1)。

栈与队列的比较与选择

在实际开发中,选择合适的数据结构是提升代码效率和可读性的关键。队列虽然都是线性数据结构,但它们的访问顺序不同,适用于不同的应用场景。

【体验最新的GPT系列模型!支持Open API调用、自定义助手、文件上传等强大功能,助您提升工作效率!点击链接体验:CodeMoss & ChatGPT-AI中文版

特性 栈(Stack) 队列(Queue)
访问顺序 后进先出(LIFO) 先进先出(FIFO)
常见应用 函数调用管理、表达式解析、DFS 任务调度、BFS、消息队列
实现方式 基于数组或链表 基于数组的循环队列或基于链表
操作复杂度 常数时间(O(1)) 常数时间(O(1))
内存使用 需要预先分配或动态调整 基于循环数组时内存利用率高,基于链表时每个元素有额外指针

选择建议

  • 使用栈

    • 当需要逆序处理数据时,如后进先出。
    • 在递归实现中,用于模拟系统调用栈。
    • 实现浏览器的后退功能。
  • 使用队列

    • 当需要按顺序处理任务时,如先到先服务。
    • 在广度优先搜索算法中。
    • 实现消息传递系统。

性能优化技巧

尽管栈和队列的基础操作已经具有高效的性能,但在特定场景下,进一步优化仍然可以带来显著的性能提升。以下是一些实用的优化技巧:

1. 选择合适的底层容器

C++中的std::stackstd::queue默认基于std::deque实现,但在某些情况下,选择其他容器(如std::vectorstd::list)可能更合适。

  • :如果不需要频繁的插入和删除操作,可以选择std::vector作为底层容器,因为它具有更好的缓存局部性。

    cpp 复制代码
    std::stack<int, std::vector<int>> s;
  • 队列 :通常std::deque已经足够高效,但在特定情况下,可以考虑自定义的循环数组实现以减少内存分配。

2. 避免不必要的内存分配

频繁的内存分配和释放会影响程序性能。通过预先分配足够的空间,可以减少内存分配的次数。

  • cpp 复制代码
    std::vector<int> vec;
    vec.reserve(1000); // 预先分配空间
    std::stack<int, std::vector<int>> s(vec);
  • 队列

    如果使用基于数组的实现,可以设计循环队列以有效利用已分配的空间。

3. 使用移动语义

在C++11及以上版本中,利用移动语义可以减少不必要的对象拷贝,提升性能。

cpp 复制代码
#include <stack>
#include <string>
#include <utility> // for std::move

int main() {
    std::stack<std::string> s;
    std::string str = "Hello, World!";
    s.push(std::move(str)); // 移动而非拷贝
    return 0;
}

4. 并行处理

对于队列,尤其是在多线程环境中,可以通过锁或无锁队列实现安全的并行访问,提升系统吞吐量。

无锁队列示例(基于C++11的原子操作):

cpp 复制代码
#include <atomic>
#include <memory>

template<typename T>
class LockFreeQueue {
private:
    struct Node {
        std::shared_ptr<T> data;
        Node* next;
        Node() : next(nullptr) {}
    };

    std::atomic<Node*> head;
    std::atomic<Node*> tail;

public:
    LockFreeQueue() {
        Node* dummy = new Node();
        head.store(dummy);
        tail.store(dummy);
    }

    ~LockFreeQueue() {
        while(Node* node = head.load()) {
            head.store(node->next);
            delete node;
        }
    }

    void enqueue(T value) {
        std::shared_ptr<T> newData = std::make_shared<T>(std::move(value));
        Node* newNode = new Node();
        newNode->data = newData;
        Node* oldTail = nullptr;
        while(true) {
            oldTail = tail.load();
            Node* tailNext = oldTail->next;
            if(tail.load() == oldTail) {
                if(tailNext == nullptr) {
                    if(std::atomic_compare_exchange_weak(&oldTail->next, &tailNext, newNode)) {
                        break;
                    }
                } else {
                    std::atomic_compare_exchange_weak(&tail, &oldTail, tailNext);
                }
            }
        }
        std::atomic_compare_exchange_weak(&tail, &oldTail, newNode);
    }

    std::shared_ptr<T> dequeue() {
        while(true) {
            Node* oldHead = head.load();
            Node* oldTail = tail.load();
            Node* headNext = oldHead->next;
            if(oldHead == head.load()) {
                if(oldHead == oldTail) {
                    if(headNext == nullptr) {
                        return std::make_shared<T>(); // Queue is empty
                    }
                    std::atomic_compare_exchange_weak(&tail, &oldTail, headNext);
                } else {
                    if(std::atomic_compare_exchange_weak(&head, &oldHead, headNext)) {
                        return headNext->data;
                    }
                }
            }
        }
    }
};

说明

  • 无锁队列通过原子操作确保并发安全,适用于高性能和多线程环境。
  • 需要深入理解原子操作和内存模型,确保正确性。

5. 编译器优化

利用编译器优化选项,可以进一步提升代码执行效率。常用的优化选项包括-O2-O3等。

bash 复制代码
g++ -O3 -std=c++17 -o program program.cpp

注意:过度优化可能导致调试困难,应在确保程序正确性的前提下进行优化。


更多文章

【OpenAI】获取OpenAI API Key的多种方式全攻略:从入门到精通,再到详解教程!!

【VScode】VSCode中的智能编程利器,全面揭秘ChatMoss & ChatGPT中文版

结论

栈与队列作为C++中两种基础且重要的数据结构,在软件开发中具有广泛的应用场景。通过深入理解它们的实现原理、使用方法及性能特点,开发者能够更加高效地选择和应用合适的数据结构,提升程序性能和代码质量。

相关推荐
alden_ygq1 小时前
go clean -modcache命令清理缓存
开发语言·缓存·golang
桃园码工1 小时前
第二章:编写第一个 Go 程序 1.Hello World 程序 --Go 语言轻松入门
开发语言·后端·golang
小贾Coder1 小时前
【速通GO】基础结构和语法
开发语言·后端·golang
Ttang231 小时前
数据结构——用链表实现栈和队列
数据结构·链表
skynetkang1 小时前
摄像头原始数据读取——gstreamer(gst_parse_launch)
linux·c++·音视频·gstreamer
糕冷小美n1 小时前
jeecgbootvue2重新整理数组数据或者添加合并数组并遍历背景图片或者背景颜色
数据结构·算法
CV万花筒1 小时前
点云欧式聚类,条件欧式聚类算法原理及推导
算法·数据挖掘·聚类
ZZZ_O^O2 小时前
【贪心算法-第三弹——Leetcode-179.最大数】
c++·学习·算法·leetcode·贪心算法
Fighting04292 小时前
java回文数
java·开发语言·力扣·策略模式
问道飞鱼2 小时前
【Java知识】Java进阶-策略模式实现
java·开发语言·策略模式