本文将带你深入了解数据结构栈和队列,这两种基础的线性数据结构在算法中的重要性不言而喻。我们将会详细介绍栈和队列的概念、分类、实现以及应用场景,在理解栈和队列的基础上,还将探讨如何通过栈和队列来高效地解决算法问题。
无论你是初学者还是进阶者,本文将为你提供简单易懂、实用可行的知识点,帮助你更好地掌握栈和队列,进而提升算法解题的能力,开启数据结构与算法的奇妙之旅。
目录
栈的基本概念
栈的顺序存储与链式存储实现
栈在表达式中的应用
队列的基本概念
队列的顺序和链式实现
栈(Stack)是一种线性数据结构,它的特点是只能在末端(栈顶)进行插入和删除操作,并且访问元素时也是从栈顶开始。栈的行为类似于弹簧被压缩的过程,当一个元素被插入栈中时,它就被压入栈底;而当一个元素被删除时,它就从栈顶弹出。
由于栈的后进先出(Last-In-First-Out,LIFO)的特性,因此它常用于需要“回溯”的算法实现,例如递归、深度优先搜索等。在计算机科学中,栈也被广泛应用于表达式求值、函数调用、内存管理等方面。
栈可以通过数组或链表来实现。通过数组实现的栈称为顺序栈,而通过链表实现的栈称为链式栈。无论通过何种方式实现,栈的基本操作包括入栈(push)、出栈(pop)、查看栈顶元素(top)以及判断栈是否为空(empty)。
栈的基本操作如下:
InitStack(&S):初始化栈。构造一个空栈S,分配内存空间。
DestroyStack(&L):销毁栈。销毁并释放栈S所占用的内存空间。
Push(&S,x):进栈,若栈S未满,则将x加入使之成为栈顶。
Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。
GetTop(S,&x):读栈顶元素。若栈S非空,则用x返回栈顶元素
其他常用操作:
StackEmpty(S):判断一个栈S是否为空。若S为空,则返回true,否则返回false。
栈的常考题型为:
回顾重点,其主要内容整理成如下内容:
栈可以通过两种方式来实现:顺序存储和链式存储。
顺序存储的栈,即顺序栈,通常使用数组来实现。在顺序栈中,栈底在数组的低地址处,栈顶在数组的高地址处。栈顶指针(top)是一个整型变量,它指向当前栈顶元素所在的位置。当栈为空时,top的值为-1。入栈操作就是将元素插入到top位置的后面,然后将top指针向上移动一个位置;出栈操作就是将top指针向下移动一个位置,然后将该位置的元素删除。
const int MAX_SIZE = 100; // 定义栈的最大容量
int stack[MAX_SIZE]; // 定义栈数组
int top = -1; // 初始化栈顶指针为-1,表示栈为空
// 入栈操作
void push(int x) {
if (top == MAX_SIZE - 1) { // 栈满,无法继续插入元素
cout << "Error: Stack overflow!" << endl;
return;
}
top++; // 栈顶指针上移
stack[top] = x; // 将元素x插入栈顶位置
}
// 出栈操作
void pop() {
if (top == -1) { // 栈空,无法进行出栈操作
cout << "Error: Stack underflow!" << endl;
return;
}
top--; // 栈顶指针下移
}
// 获取栈顶元素
int getTop() {
if (top == -1) { // 栈为空,栈顶指针无效
cout << "Error: Stack is empty!" << endl;
return -1;
}
return stack[top]; // 返回栈顶元素
}
// 判断栈是否为空
bool isEmpty() {
return top == -1; // 栈顶指针为-1时,栈为空
}
栈的顺序存储还有一种共享栈的方式。共享栈是一种特殊的顺序栈,它可以同时存储两个栈。具体来说,共享栈有两个栈顶指针,分别指向两个栈的栈顶元素。它们从两端向中间生长,当它们相遇时就表示栈满了。共享栈常用于两个栈需要共用一段连续的存储空间的情况,例如某些操作系统的内核栈和用户栈都共用一个物理内存区域。下面是使用 C++ 实现共享栈的完整代码示例:
#include
using namespace std;
const int MAX_SIZE = 100; // 定义栈的最大容量
int stack[MAX_SIZE]; // 定义存储栈元素的数组
int top1 = -1; // 定义第一个栈的栈顶指针,初始值为-1
int top2 = MAX_SIZE; // 定义第二个栈的栈顶指针,初始值为MAX_SIZE
// 向第一个栈中插入元素
void pushStack1(int x) {
if (top1 + 1 == top2) { // 共享栈已满
cout << "Error: Stack overflow!" << endl;
return;
}
top1++; // 第一个栈的栈顶指针上移
stack[top1] = x; // 在第一个栈中插入元素
}
// 向第二个栈中插入元素
void pushStack2(int x) {
if (top1 + 1 == top2) { // 共享栈已满
cout << "Error: Stack overflow!" << endl;
return;
}
top2--; // 第二个栈的栈顶指针下移
stack[top2] = x; // 在第二个栈中插入元素
}
// 从第一个栈中删除元素
void popStack1() {
if (top1 == -1) { // 第一个栈为空
cout << "Error: Stack underflow!" << endl;
return;
}
top1--; // 第一个栈的栈顶指针下移
}
// 从第二个栈中删除元素
void popStack2() {
if (top2 == MAX_SIZE) { // 第二个栈为空
cout << "Error: Stack underflow!" << endl;
return;
}
top2++; // 第二个栈的栈顶指针上移
}
// 获取第一个栈的栈顶元素
int getTop1() {
if (top1 == -1) { // 第一个栈为空,栈顶指针无效
cout << "Error: Stack is empty!" << endl;
return -1;
}
return stack[top1]; // 返回第一个栈的栈顶元素
}
// 获取第二个栈的栈顶元素
int getTop2() {
if (top2 == MAX_SIZE) { // 第二个栈为空,栈顶指针无效
cout << "Error: Stack is empty!" << endl;
return -1;
}
return stack[top2]; // 返回第二个栈的栈顶元素
}
// 判断第一个栈是否为空
bool isEmpty1() {
return top1 == -1; // 第一个栈的栈顶指针为-1时,栈为空
}
// 判断第二个栈是否为空
bool isEmpty2() {
return top2 == MAX_SIZE; // 第二个栈的栈顶指针为MAX_SIZE时,栈为空
}
int main() {
pushStack1(1);
pushStack1(2);
pushStack2(3);
pushStack2(4);
cout << "Top of Stack 1: " << getTop1() << endl;
cout << "Top of Stack 2: " << getTop2() << endl;
popStack1();
popStack2();
cout << "Top of Stack 1: " << getTop1() << endl;
cout << "Top of Stack 2: " << getTop2() << endl;
return 0;
}
在主函数中,我们演示了如何使用 pushStack1()、pushStack2()、popStack1()、popStack2()、getTop1() 和 getTop2() 等操作来对共享栈进行插入、删除和获取元素等操作,并输出了两个栈的栈顶元素。
回顾重点,其主要内容整理成如下内容:
栈的链式存储实现方式是使用链表来存储栈中的元素。栈顶指针 Top 指向链表的头节点,每插入一个元素就在链表的头部插入一个节点,每删除一个元素就删除链表头部的节点,这样就实现了栈的先进后出的特性。 以下是使用 C++ 语言实现的栈链式存储的完整代码:
#include
using namespace std;
// 定义链表的节点结构体
struct Node {
int data; // 节点中存储的数据
Node* next; // 指向下一个节点的指针
};
// 定义链表的头指针和栈顶指针
Node* head = NULL;
Node* top = NULL;
// 向栈中插入元素
void push(int x) {
Node* newNode = new Node; // 创建新节点
newNode->data = x; // 将元素值赋给新节点的 data 域
newNode->next = top; // 新节点的 next 指向当前栈顶节点
top = newNode; // 更新栈顶指针
}
// 从栈中删除元素
void pop() {
if (top == NULL) { // 栈为空,无法删除
cout << "Error: Stack is empty!" << endl;
return;
}
Node* temp = top; // 暂存当前栈顶节点的地址
top = top->next; // 更新栈顶指针
delete temp; // 释放暂存节点内存空间
}
// 获取栈顶元素
int getTop() {
if (top == NULL) { // 栈为空,栈顶指针无效
cout << "Error: Stack is empty!" << endl;
return -1;
}
return top->data; // 返回栈顶节点的 data 域
}
// 判断栈是否为空
bool isEmpty() {
return top == NULL; // 栈顶指针为空时,栈为空
}
int main() {
push(1);
push(2);
push(3);
cout << "Top of Stack: " << getTop() << endl;
pop();
pop();
cout << "Top of Stack: " << getTop() << endl;
return 0;
}
在主函数中,我们演示了如何使用 push()、pop() 和 getTop() 等操作来对栈进行插入、删除和获取元素等操作,并输出了栈顶元素。
栈在表达式中的应用非常广泛,可以用来进行中缀表达式的转换、计算后缀表达式等操作。中缀表达式是指形如 a+b 或 (a+b)*c-d/(e+f) 这样的表达式,其中运算符位于两个操作数的中间。但是在计算机中,通常采用后缀表达式(也叫逆波兰表达式)来表示算术表达式,即把运算符放在操作数后面,例如 a b + c * d /。
队列是一种线性数据结构,具有先进先出(FIFO)的特点。与栈不同,队列只允许在队尾插入元素,在队头删除元素。队列通常用于需要按照时间顺序处理事物的场合,例如多任务并发处理或者打印机缓存任务等,它们按照顺序依次加入队列,并按照先进先出的顺序进行处理。
队列的基本操作如下:
InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间。
EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除对头元素,并用x返回。
GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。
回顾重点,其主要内容整理成如下内容:
队列可以用数组或链表等数据结构实现,分别称为顺序队列和链式队列。
顺序队列是基于数组的实现,使用一个数组来存储队列中的元素,使用头尾指针分别指向队列的头部和尾部。新元素插入到队列尾部,旧元素从队列头部删除,每次插入或删除元素后需要更新头尾指针。缺点是当队列元素个数达到数组容量时,需要进行数据搬移,时间复杂度为 O(n)。以下是一个 C++ 语言实现的顺序队列的简单示例代码:
#include
using namespace std;
const int MAXSIZE = 100; // 队列的最大容量
int queue[MAXSIZE]; // 用数组实现队列
int head = 0, tail = 0; // 头尾指针
// 向队列尾部插入元素
void enqueue(int x) {
if (tail == MAXSIZE) { // 队列已满
cout << "Error: Queue is full!" << endl;
return;
}
queue[tail++] = x; // 将新元素添加到队列尾部,同时更新尾指针
}
// 从队列头部删除元素
void dequeue() {
if (head == tail) { // 队列为空
cout << "Error: Queue is empty!" << endl;
return;
}
head++; // 更新头指针,删除队首元素
}
// 获取队头元素
int getFront() {
if (head == tail) { // 队列为空
cout << "Error: Queue is empty!" << endl;
return -1;
}
return queue[head]; // 返回队首元素
}
// 判断队列是否为空
bool isEmpty() {
return head == tail; // 头尾指针相等时,队列为空
}
int main() {
enqueue(1);
enqueue(2);
enqueue(3);
cout << "Front of Queue: " << getFront() << endl;
dequeue();
dequeue();
cout << "Front of Queue: " << getFront() << endl;
return 0;
}
需要注意的是,在顺序队列中,每次删除元素时并没有真正将元素从数组中移除,而是单纯地将头指针向后移动。因此,当头指针后面有很多无用元素时,需要调整数组内部的元素位置,以节省内存空间。
回顾重点,其主要内容整理成如下内容:
链式队列是基于链表的实现,使用一个链表来存储队列中的元素,使用头指针指向队首节点,尾指针指向队尾节点。新元素插入到链表尾部,旧元素从链表头部删除,每次插入或删除元素只需操作指针即可,无需进行数据搬移。链式队列相对于顺序队列的优点是没有容量限制,但由于链表需要额外的指针存储,空间开销较大一些。 以下是一个 C++ 语言实现的顺序队列的简单示例代码:
#include
using namespace std;
struct Node {
int data;
Node* next;
};
Node* head = NULL; // 头指针指向队列头部
Node* tail = NULL; // 尾指针指向队列尾部
// 向队列尾部插入元素
void enqueue(int x) {
Node* newNode = new Node;
newNode->data = x;
newNode->next = NULL;
if (tail == NULL) { // 队列为空
head = tail = newNode;
return;
}
tail->next = newNode; // 将新节点添加到队列尾部
tail = newNode; // 更新尾指针
}
// 从队列头部删除元素
void dequeue() {
if (head == NULL) { // 队列为空
cout << "Error: Queue is empty!" << endl;
return;
}
Node* temp = head;
head = head->next; // 更新头指针
if (head == NULL) { // 如果队列中只有一个元素,删除后需要更新尾指针
tail = NULL;
}
delete temp;
}
// 获取队头元素
int getFront() {
if (head == NULL) { // 队列为空
cout << "Error: Queue is empty!" << endl;
return -1;
}
return head->data;
}
// 判断队列是否为空
bool isEmpty() {
return head == NULL; // 头指针为空时,队列为空
}
int main() {
enqueue(1);
enqueue(2);
enqueue(3);
cout << "Front of Queue: " << getFront() << endl;
dequeue();
dequeue();
cout << "Front of Queue: " << getFront() << endl;
return 0;
}
需要注意的是,在链式队列中,每次删除元素时需要注意特殊情况,比如队列中只有一个元素的情况。还需要注意内存泄漏问题,每次删除元素后需要释放对应节点的内存空间。
回顾重点,其主要内容整理成如下内容:
双端队列的介绍:双端队列(deque)是一种可以在队列两端进行插入和删除操作的数据结构。它可以被看作是两个栈拼接而成,因此常常被称为“双端栈”。
回顾重点,其主要内容整理成如下内容: