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