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

ops/2024/11/29 19:42:22/

文章目录

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

引言

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

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

在这里插入图片描述

栈(Stack)数据结构详解

1. 栈的基本概念

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

基本操作

  • Push:向栈顶添加一个元素。
  • Pop:移除栈顶的元素。
  • Top/Peek:查看栈顶的元素而不移除它。
  • IsEmpty:检查栈是否为空。
    在这里插入图片描述

2. 栈的实现原理

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

基于数组的实现

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;
}

基于链表的实现

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的示例

#include <iostream>
#include <stack>int main() {std::stack<int> s;// Push elementss.push(10);s.push(20);s.push(30);// Display and pop elementswhile(!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. 遍历结束后,检查栈是否为空:
    • 如果为空,括号匹配正确。
    • 如果不为空,括号不匹配。

代码实现

#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. 队列的实现原理

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

基于数组的循环队列实现

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;
}

基于链表的实现

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的示例

#include <iostream>
#include <queue>int main() {std::queue<int> q;// Enqueue elementsq.push(10);q.push(20);q.push(30);// Display and dequeue elementswhile(!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. 提供一个接口用于执行下一个任务。

代码实现

#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作为底层容器,因为它具有更好的缓存局部性。

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

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

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

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

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

3. 使用移动语义

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

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

4. 并行处理

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

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

#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等。

g++ -O3 -std=c++17 -o program program.cpp

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


更多文章

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

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

结论

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


http://www.ppmy.cn/ops/137729.html

相关文章

Rust vtable(Rust虚表、Rust虚函数表)动态绑定、Rust多态调用、通过类型引用创建trait对象(自动实例化)

文章目录 Rust vtable原理深度解析1. 什么是 vtable&#xff1f;1.1 Trait 对象和 vtableTrait对象指针结构- 一个指向数据的指针&#xff08;指向具体类型实例的数据&#xff09;- 一个指向 vtable 的指针&#xff0c;vtable 存储了该类型所有 trait 方法的函数指针 示例&…

架构第十一章:zabbix

监控体系 1.监控知识概述 &#xff08;1&#xff09;对系统不间断的实时监控 &#xff08;2&#xff09;实时反馈系统和服务状态 &#xff08;3&#xff09;保证系统和服务可靠、安全 &#xff08;4&#xff09;保证业务持续稳定运行 实时 反馈 可靠 安全 2.怎么进行监控&…

跨平台应用开发框架(1)----Qt(组件篇)

目录 1.Qt 1.Qt 的主要特点 2.Qt的使用场景 3.Qt的版本 2.QtSDK 1.Qt SDK 的组成部分 2.安装 Qt SDK 3.Qt SDK 的优势 3.Qt初识 1.快速上手 widget.cpp mian.cpp widget.h Helloworld.pro 2.对象树 3.坐标系 4.信号和槽 1. 信号和槽的基本概念 2. 信号和槽的…

[蓝桥杯 2021 省 AB2] 小平方

题目描述 小蓝发现&#xff0c;对于一个正整数 nn 和一个小于 nn 的正整数 vv&#xff0c;将 vv 平方后对 nn 取余可能小于 nn 的一半&#xff0c;也可能大于等于 nn 的一半。 请问&#xff0c;在 11 到 n−1n−1 中, 有多少个数平方后除以 nn 的余数小于 nn 的一半。 例如&…

使用Eureka实现服务注册与发现的具体案例详解

1. Eureka 的基本概念 1.1 什么是 Eureka&#xff1f; Eureka 是一个基于 REST 的服务注册和发现平台&#xff0c;主要分为以下两个组件&#xff1a; Eureka Server&#xff1a;作为服务注册中心&#xff0c;负责维护服务实例信息。Eureka Client&#xff1a;服务消费者与服…

语言模型中的多模态链式推理

神经网络的公式推导 简介摘要引言多模态思维链推理的挑战多模态CoT框架多模态CoT模型架构细节编码模块融合模块解码模块 实验结果运行代码补充细节安装包下载Flan-T5数据集准备rougenltkall-MiniLM-L6-v2运行 简介 本文主要对2023一篇论文《Multimodal Chain-of-Thought Reason…

springboot项目使用maven打包,第三方jar问题

springboot项目使用maven package打包为可执行jar后&#xff0c;第三方jar会被打包进去吗&#xff1f; 答案是肯定的。做了实验如下&#xff1a; 第三方jar的项目结构及jar包结构如下&#xff1a;&#xff08;该第三方jar采用的是maven工程&#xff0c;打包为普通jar&#xf…

faiss库中ivf-sq(ScalarQuantizer,标量量化)代码解读-5

训练过程 通过gdb调试得到这个ivfsq的训练过程&#xff0c;我尝试对这个内容具体训练过程进行解析&#xff0c;对每个调用栈里面的逻辑和代码进行解读。 步骤函数名称调用位置说明1faiss::IndexIVF::train/faiss/IndexIVF.cpp:1143开始训练&#xff0c;判断是否需要训练第一级…