青少年编程与数学 02-018 C++数据结构与算法 04课题、栈与队列
课题摘要:
栈(Stack)是一种线性数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。这意味着最后添加到栈中的元素将是第一个被移除的元素。栈在计算机科学中有着广泛的应用,例如在函数调用、表达式求值和回溯算法中。
队列(Queue)是一种线性数据结构,它遵循先进先出(First In First Out,FIFO)的原则。这意味着最早添加到队列中的元素将是第一个被移除的元素。队列在计算机科学中有着广泛的应用,例如在任务调度、消息传递和缓冲区管理中。
一、栈
栈(Stack)是一种线性数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。这意味着最后添加到栈中的元素将是第一个被移除的元素。栈在计算机科学中有着广泛的应用,例如在函数调用、表达式求值和回溯算法中。以下是对栈的详细解释:
1. 栈的定义
栈是一种线性数据结构,它只允许在一端(称为栈顶)进行插入和删除操作。栈顶是栈中最后一个被添加的元素的位置。栈的另一端称为栈底,通常是固定的。
2. 栈的特点
- 后进先出(LIFO):最后添加的元素最先被移除。
- 栈顶操作:所有操作(插入和删除)都在栈顶进行。
- 动态大小:栈的大小可以动态变化,但通常有一个最大容量限制。
- 线性结构:栈中的元素是线性排列的,每个元素都有一个直接的前驱和后继。
3. 栈的基本操作
栈的主要操作包括:
push
:将一个元素添加到栈顶。pop
:从栈顶移除一个元素。peek
或top
:查看栈顶元素,但不移除它。is_empty
:检查栈是否为空。size
:返回栈中元素的数量。
示例
假设我们有一个栈,初始为空:
[]
执行以下操作:
push(1)
:[1]
push(2)
:[1, 2]
push(3)
:[1, 2, 3]
pop()
:[1, 2]
peek()
:返回 2
is_empty()
:返回 False
size()
:返回 2
4. 栈的实现
栈可以用数组或链表来实现。以下是两种实现方式的详细说明:
(1)数组实现
使用数组实现栈时,栈的大小通常是固定的,但可以通过动态数组(如C++的std::vector
)来实现动态大小。
#include <vector>
#include <stdexcept>
class Stack {
private:
std::vector<int> items;
public:
void push(int item) {
items.push_back(item);
}
void pop() {
if (!is_empty()) {
items.pop_back();
} else {
throw std::out_of_range("pop from empty stack");
}
}
int peek() const {
if (!is_empty()) {
return items.back();
} else {
throw std::out_of_range("peek from empty stack");
}
}
bool is_empty() const {
return items.empty();
}
size_t size() const {
return items.size();
}
};
(2)链表实现
使用链表实现栈时,栈的大小可以动态变化,但需要管理节点的分配和释放。
#include <iostream>
#include <stdexcept>
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
class Stack {
private:
Node* top;
public:
Stack() : top(nullptr) {}
~Stack() {
while (!is_empty()) {
pop();
}
}
void push(int data) {
Node* new_node = new Node(data);
new_node->next = top;
top = new_node;
}
void pop() {
if (is_empty()) {
throw std::out_of_range("pop from empty stack");
}
Node* temp = top;
top = top->next;
delete temp;
}
int peek() const {
if (is_empty()) {
throw std::out_of_range("peek from empty stack");
}
return top->data;
}
bool is_empty() const {
return top == nullptr;
}
size_t size() const {
size_t count = 0;
Node* current = top;
while (current) {
count++;
current = current->next;
}
return count;
}
};
5. 栈的应用
栈在计算机科学中有着广泛的应用,以下是一些常见的应用场景:
(1)函数调用
在编程语言中,函数调用通常使用栈来实现。每次调用一个函数时,都会在栈上创建一个栈帧(Frame),用于存储函数的局部变量和返回地址。当函数返回时,栈帧被移除。
(2)表达式求值
栈可以用于求值表达式,特别是处理括号匹配和操作符优先级。例如,使用两个栈(一个用于操作数,一个用于操作符)可以实现中缀表达式的求值。
(3)回溯算法
栈可以用于实现回溯算法,例如在迷宫问题中,栈可以记录路径,当遇到死路时,可以回溯到上一个节点。
(4)括号匹配
栈可以用于检查括号是否匹配。例如,对于字符串 "{[()]}"
,可以使用栈来检查括号是否正确匹配。
(5)深度优先搜索(DFS)
栈可以用于实现深度优先搜索,通过栈来记录访问的节点。
6. 栈的优缺点
优点:
- 简单高效:栈的操作(
push
、pop
、peek
)时间复杂度为O(1)。 - 适用广泛:栈在许多算法和数据处理中都非常有用。
缺点:
- 容量限制:如果使用固定大小的数组实现栈,可能会遇到栈溢出的问题。
- 功能有限:栈只能在一端进行操作,不支持随机访问。
7. 总结
栈是一种线性数据结构,遵循后进先出(LIFO)的原则。它可以通过数组或链表实现,支持高效的操作(如 push
、pop
和 peek
)。栈在函数调用、表达式求值、括号匹配和回溯算法中有着广泛的应用。理解栈的特性和操作方法,有助于更好地使用它来解决各种编程问题。
二、队列
队列(Queue)是一种线性数据结构,它遵循先进先出(First In First Out,FIFO)的原则。这意味着最早添加到队列中的元素将是第一个被移除的元素。队列在计算机科学中有着广泛的应用,例如在任务调度、消息传递和缓冲区管理中。以下是对队列的详细解释:
1. 队列的定义
队列是一种线性数据结构,它只允许在一端(称为队尾)进行插入操作,在另一端(称为队头)进行删除操作。队头是队列中最早添加的元素的位置,队尾是队列中最后添加的元素的位置。
2. 队列的特点
- 先进先出(FIFO):最早添加的元素最先被移除。
- 队头操作:删除操作(
dequeue
)在队头进行。 - 队尾操作:插入操作(
enqueue
)在队尾进行。 - 动态大小:队列的大小可以动态变化,但通常有一个最大容量限制。
- 线性结构:队列中的元素是线性排列的,每个元素都有一个直接的前驱和后继。
3. 队列的基本操作
队列的主要操作包括:
enqueue
:将一个元素添加到队尾。dequeue
:从队头移除一个元素。peek
或front
:查看队头元素,但不移除它。is_empty
:检查队列是否为空。size
:返回队列中元素的数量。
示例
假设我们有一个队列,初始为空:
[]
执行以下操作:
enqueue(1)
:[1]
enqueue(2)
:[1, 2]
enqueue(3)
:[1, 2, 3]
dequeue()
:[2, 3]
peek()
:返回 2
is_empty()
:返回 False
size()
:返回 2
4. 队列的实现
队列可以用数组或链表来实现。以下是两种实现方式的详细说明:
(1)数组实现
使用数组实现队列时,队列的大小通常是固定的,但可以通过动态数组(如C++的std::vector
)来实现动态大小。数组实现的队列需要处理数组的头部删除操作,这可能会导致效率问题。
#include <vector>
#include <stdexcept>
class Queue {
private:
std::vector<int> items;
public:
void enqueue(int item) {
items.push_back(item);
}
void dequeue() {
if (!is_empty()) {
items.erase(items.begin());
} else {
throw std::out_of_range("dequeue from empty queue");
}
}
int peek() const {
if (!is_empty()) {
return items.front();
} else {
throw std::out_of_range("peek from empty queue");
}
}
bool is_empty() const {
return items.empty();
}
size_t size() const {
return items.size();
}
};
(2)链表实现
使用链表实现队列时,队列的大小可以动态变化,且插入和删除操作的时间复杂度为O(1)。链表实现的队列需要管理节点的分配和释放。
#include <iostream>
#include <stdexcept>
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
class Queue {
private:
Node* front;
Node* rear;
public:
Queue() : front(nullptr), rear(nullptr) {}
~Queue() {
while (!is_empty()) {
dequeue();
}
}
void enqueue(int data) {
Node* new_node = new Node(data);
if (rear == nullptr) {
front = rear = new_node;
} else {
rear->next = new_node;
rear = new_node;
}
}
void dequeue() {
if (is_empty()) {
throw std::out_of_range("dequeue from empty queue");
}
Node* temp = front;
front = front->next;
if (front == nullptr) {
rear = nullptr;
}
delete temp;
}
int peek() const {
if (is_empty()) {
throw std::out_of_range("peek from empty queue");
}
return front->data;
}
bool is_empty() const {
return front == nullptr;
}
size_t size() const {
size_t count = 0;
Node* current = front;
while (current) {
count++;
current = current->next;
}
return count;
}
};
5. 队列的应用
队列在计算机科学中有着广泛的应用,以下是一些常见的应用场景:
(1)任务调度
在操作系统中,任务调度器使用队列来管理待处理的任务。任务按照到达的顺序排队等待处理。
(2)消息传递
在消息队列系统中,队列用于存储和传递消息。消息按照到达的顺序被处理。
(3)缓冲区管理
在 I/O 操作中,队列用于管理缓冲区。例如,打印任务可以被放入一个队列中,打印机按照队列的顺序处理这些任务。
(4)广度优先搜索(BFS)
在图算法中,队列用于实现广度优先搜索。队列存储待访问的节点,确保节点按照到达的顺序被访问。
(5)事件驱动编程
在事件驱动的编程模型中,队列用于管理事件。事件按照发生的顺序被处理。
6. 队列的优缺点
优点:
- 简单高效:队列的操作(
enqueue
、dequeue
、peek
)时间复杂度为O(1)。 - 适用广泛:队列在许多算法和数据处理中都非常有用。
缺点:
- 容量限制:如果使用固定大小的数组实现队列,可能会遇到队列溢出的问题。
- 功能有限:队列只能在一端插入,在另一端删除,不支持随机访问。
7. 总结
队列是一种线性数据结构,遵循先进先出(FIFO)的原则。它可以通过数组或链表实现,支持高效的操作(如 enqueue
、dequeue
和 peek
)。队列在任务调度、消息传递、缓冲区管理和广度优先搜索中有着广泛的应用。理解队列的特性和操作方法,有助于更好地使用它来解决各种编程问题。
三、双向队列(Deque)
双向队列(Double-Ended Queue,简称 Deque)是一种特殊的队列,它允许在队列的两端(队头和队尾)进行插入和删除操作。双向队列结合了栈和普通队列的特点,提供了更灵活的操作方式。以下是对双向队列的详细解释:
1. 双向队列的定义
双向队列是一种线性数据结构,允许在队列的两端进行插入和删除操作。它支持以下操作:
- 在队头插入元素(
appendleft
)。 - 在队尾插入元素(
append
)。 - 从队头删除元素(
popleft
)。 - 从队尾删除元素(
pop
)。
2. 双向队列的特点
- 灵活操作:支持在队列的两端进行插入和删除操作。
- 高效实现:所有操作的时间复杂度为O(1)。
- 动态大小:队列的大小可以动态变化,但通常有一个最大容量限制。
- 线性结构:队列中的元素是线性排列的,每个元素都有一个直接的前驱和后继。
3. 双向队列的基本操作
双向队列的主要操作包括:
append(x)
:在队尾插入一个元素x
。appendleft(x)
:在队头插入一个元素x
。pop()
:从队尾删除一个元素并返回。popleft()
:从队头删除一个元素并返回。peek()
或front()
:查看队头元素,但不移除它。peeklast()
或back()
:查看队尾元素,但不移除它。is_empty()
:检查队列是否为空。size()
:返回队列中元素的数量。
示例
假设我们有一个双向队列,初始为空:
[]
执行以下操作:
append(1)
:[1]
appendleft(2)
:[2, 1]
append(3)
:[2, 1, 3]
popleft()
:[1, 3]
pop()
:[1]
peek()
:返回 1
peeklast()
:返回 1
is_empty()
:返回 False
size()
:返回 1
4. 双向队列的实现
双向队列可以用数组或链表来实现。以下是两种实现方式的详细说明:
(1)数组实现
使用数组实现双向队列时,需要处理数组的头部删除操作,这可能会导致效率问题。但通过使用双端数组(如C++的std::deque
),可以高效地实现双向队列。
#include <deque>
#include <stdexcept>
class Deque {
private:
std::deque<int> items;
public:
void append(int item) {
items.push_back(item);
}
void appendleft(int item) {
items.push_front(item);
}
int pop() {
if (!is_empty()) {
int item = items.back();
items.pop_back();
return item;
} else {
throw std::out_of_range("pop from empty deque");
}
}
int popleft() {
if (!is_empty()) {
int item = items.front();
items.pop_front();
return item;
} else {
throw std::out_of_range("popleft from empty deque");
}
}
int peek() const {
if (!is_empty()) {
return items.front();
} else {
throw std::out_of_range("peek from empty deque");
}
}
int peeklast() const {
if (!is_empty()) {
return items.back();
} else {
throw std::out_of_range("peeklast from empty deque");
}
}
bool is_empty() const {
return items.empty();
}
size_t size() const {
return items.size();
}
};
(2)链表实现
使用链表实现双向队列时,队列的大小可以动态变化,且插入和删除操作的时间复杂度为O(1)。链表实现的双向队列需要管理节点的分配和释放。
#include <iostream>
#include <stdexcept>
struct Node {
int data;
Node* prev;
Node* next;
Node(int val) : data(val), prev(nullptr), next(nullptr) {}
};
class Deque {
private:
Node* front;
Node* rear;
public:
Deque() : front(nullptr), rear(nullptr) {}
~Deque() {
while (!is_empty()) {
pop();
}
}
void append(int data) {
Node* new_node = new Node(data);
if (rear == nullptr) {
front = rear = new_node;
} else {
rear->next = new_node;
new_node->prev = rear;
rear = new_node;
}
}
void appendleft(int data) {
Node* new_node = new Node(data);
if (front == nullptr) {
front = rear = new_node;
} else {
front->prev = new_node;
new_node->next = front;
front = new_node;
}
}
int pop() {
if (is_empty()) {
throw std::out_of_range("pop from empty deque");
}
Node* temp = rear;
rear = rear->prev;
if (rear == nullptr) {
front = nullptr;
} else {
rear->next = nullptr;
}
int data = temp->data;
delete temp;
return data;
}
int popleft() {
if (is_empty()) {
throw std::out_of_range("popleft from empty deque");
}
Node* temp = front;
front = front->next;
if (front == nullptr) {
rear = nullptr;
} else {
front->prev = nullptr;
}
int data = temp->data;
delete temp;
return data;
}
int peek() const {
if (is_empty()) {
throw std::out_of_range("peek from empty deque");
}
return front->data;
}
int peeklast() const {
if (is_empty()) {
throw std::out_of_range("peeklast from empty deque");
}
return rear->data;
}
bool is_empty() const {
return front == nullptr;
}
size_t size() const {
size_t count = 0;
Node* current = front;
while (current) {
count++;
current = current->next;
}
return count;
}
};
5. 双向队列的应用
双向队列在计算机科学中有着广泛的应用,以下是一些常见的应用场景:
(1)滑动窗口问题
在处理滑动窗口问题时,双向队列可以高效地维护窗口内的最大值或最小值。例如,使用双向队列可以实现一个时间复杂度为O(n)的滑动窗口最大值算法。
(2)回文检查
双向队列可以用于检查字符串是否为回文。通过在队头和队尾同时进行操作,可以高效地判断字符串是否对称。
(3)任务调度
在任务调度中,双向队列可以用于管理任务的优先级。高优先级的任务可以插入到队头,低优先级的任务可以插入到队尾。
(4)图的广度优先搜索(BFS)
在图算法中,双向队列可以用于实现广度优先搜索。队列存储待访问的节点,确保节点按照到达的顺序被访问。
6. 双向队列的优缺点
优点:
- 灵活操作:支持在队列的两端进行插入和删除操作。
- 高效实现:所有操作的时间复杂度为O(1)。
- 适用广泛:在许多算法和数据处理中都非常有用。
缺点:
- 实现复杂:链表实现的双向队列需要管理节点的分配和释放。
- 功能有限:虽然比普通队列灵活,但仍然不支持随机访问。
7. 总结
双向队列是一种灵活的线性数据结构,允许在队列的两端进行插入和删除操作。它可以通过数组或链表实现,支持高效的操作(如 append
、appendleft
、pop
和 popleft
)。双向队列在滑动窗口问题、回文检查、任务调度和广度优先搜索中有着广泛的应用。理解双向队列的特性和操作方法,有助于更好地使用它来解决各种编程问题。