文章目录
- 引言
- 栈(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
,这是一个模板类,基于其他容器(如vector
、deque
、list
)实现。默认情况下,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. 实战示例:括号匹配
括号匹配是栈的经典应用之一。通过栈,我们可以有效地检查表达式中的括号是否配对正确。
问题描述:给定一个包含括号的字符串,检查括号是否正确匹配。支持的括号类型包括()
、{}
和[]
。
解决思路:
- 遍历字符串的每一个字符。
- 如果遇到左括号,将其压入栈中。
- 如果遇到右括号,检查栈顶是否有对应的左括号:
- 如果有,则弹出栈顶。
- 如果没有,则括号不匹配。
- 遍历结束后,检查栈是否为空:
- 如果为空,括号匹配正确。
- 如果不为空,括号不匹配。
代码实现:
#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
,这是一个模板类,基于其他容器(如deque
、list
)实现。默认情况下,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. 实战示例:任务调度
在多线程或异步编程中,任务调度是一个常见的需求。通过队列,我们可以实现一个简单的任务调度器,确保任务按照提交的顺序依次执行。
问题描述:构建一个简单的任务调度器,支持任务的提交和按顺序执行。
解决思路:
- 使用
std::queue
存储待执行的任务。 - 提供一个接口用于提交任务。
- 提供一个接口用于执行下一个任务。
代码实现:
#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::stack
和std::queue
默认基于std::deque
实现,但在某些情况下,选择其他容器(如std::vector
或std::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++中两种基础且重要的数据结构,在软件开发中具有广泛的应用场景。通过深入理解它们的实现原理、使用方法及性能特点,开发者能够更加高效地选择和应用合适的数据结构,提升程序性能和代码质量。