栈
1.栈的基本概念
栈(Stack):是只允许在一端进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
不能插入和删除的一端为栈底(Bottom)
- 栈顶(top):线性表允许进行插入删除的那一端
- 栈底(bottom):固定的,不允许进行插入和删除的那一端
- 空栈:不含任何元素的空表
栈顶元素总是最后进栈的,并且是最先出栈的,栈底元素最先进栈最后出栈,因此栈有着后进先出的特性,也称为后进先出表。
2.栈的基本操作
InitStack(&S):初始化一个空栈S
StackEmpty(S):判断一个栈是否为空,若栈为空则返回true,否则返回false
Push(&S, x):进栈(栈的插入操作),若栈S未满,则将x加入使之成为新栈顶
Pop(&S, &x):出栈(栈的删除操作),若栈S非空,则弹出栈顶元素,并用x返回
GetTop(S, &x):读栈顶元素,若栈S非空,则用x返回栈顶元素
DestroyStack(&S):栈销毁,并释放S占用的存储空间(“&”表示引用调用)
3.栈的顺序存储结构及基本运算的实现
1)栈的顺序存储结构
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置
若存储栈的长度为StackSize,则栈顶位置top必须小于StackSize。当栈存在一个元素时,top等于0,因此通常把空栈的判断条件定位top等于-1
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //存放栈中元素
int top; //栈顶指针
}SqStack;
2)栈的基本运算
1.初始化
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
2.判栈空
bool StackEmpty(SqStack S){
if(S.top == -1) //栈空
return true;
else //栈非空
return false;
}
3.进栈
bool Push(SqStack &S,ElemType x){
if(S.top == MaxSize-1) //栈满,报错
return false;
S.data[++S.top] = x; //指针先加1,再入栈
return true;
}
4.出栈
bool Pop(SqStack &S,ElemType &x){
if(S.top == -1) //栈空,报错
return false;
x = S.data[S.top--]; //先出栈,指针再减1
return true;
}
5.读栈顶元素
bool GetTop(SqStack S,ElemType x){
if(S.top == -1) //栈空,报错
return false;
x = S.data[S.top]; // x 记录栈顶元素
return true;
}
6.共享栈
共享栈(Shared Stack)是一种特殊的栈数据结构,它允许两个或多个栈共享同一个数组作为存储空间。在该数组中,两个栈从两端开始向中间生长,可以说是“双头生长”。
具体来说,假设我们有一个长度为n的数组,将其分为左右两部分,分别作为两个栈的存储空间。左栈从数组下标0开始向右增长,右栈从数组下标n-1开始向左增长。当两个栈的栈顶指针相遇时,表示数组中间位置已经被两个栈填满,此时入栈操作就不能再继续进行。
共享栈的实现和普通栈类似,但需要注意以下几点:
共享栈需要额外记录左、右栈的栈顶指针topL、topR;
左栈在增长过程中与右栈相遇,即topL=topR+1,则表示左栈已满;
右栈在增长过程中与左栈相遇,即topR=topL-1,则表示右栈已满;
入栈、出栈等操作也需要通过topL、topR来判断具体操作哪个栈。
以下是实现共享栈基本操作示例:
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int topL;
int topR;
} SharedStack;
void init(SharedStack* stack) {
stack->topL = -1;
stack->topR = MAX_SIZE;
}
int is_empty_left(SharedStack* stack) {
return stack->topL == -1;
}
int is_empty_right(SharedStack* stack) {
return stack->topR == MAX_SIZE;
}
int is_full(SharedStack* stack) {
return stack->topL == stack->topR - 1;
}
void push_left(SharedStack* stack, int item) {
if (is_full(stack)) {
printf("Stack overflow!\n");
return;
}
stack->data[++stack->topL] = item;
}
void push_right(SharedStack* stack, int item) {
if (is_full(stack)) {
printf("Stack overflow!\n");
return;
}
stack->data[--stack->topR] = item;
}
int pop_left(SharedStack* stack) {
if (is_empty_left(stack)) {
printf("Left stack underflow!\n");
return -1; // 可以根据需求返回合适的错误码或采取其他处理方式
}
return stack->data[stack->topL--];
}
int pop_right(SharedStack* stack) {
if (is_empty_right(stack)) {
printf("Right stack underflow!\n");
return -1; // 可以根据需求返回合适的错误码或采取其他处理方式
}
return stack->data[stack->topR++];
}
int main() {
SharedStack stack;
init(&stack);
push_left(&stack, 1);
push_right(&stack, 2);
push_left(&stack, 3);
push_right(&stack, 4);
printf("%d\n", pop_left(&stack)); // 输出:3
printf("%d\n", pop_right(&stack)); // 输出:4
printf("%d\n", pop_left(&stack)); // 输出:1
printf("%d\n", pop_right(&stack)); // 输出:2
return 0;
}
在上述代码中,定义了一个共享栈的数据结构SharedStack,其中包含一个整型数组data作为存储栈元素的容器,并使用topL、topR来表示左、右栈的栈顶指针。
代码中主要包含了以下函数:
init函数初始化共享栈,将左右栈的栈顶指针设为-1和MAX_SIZE。
is_empty_left和is_empty_right函数用于判断左、右栈是否为空。
is_full函数用于判断共享栈是否已满。
push_left和push_right函数分别用于将元素入左、右栈。
pop_left和pop_right函数分别用于出左、右栈并返回栈顶元素。
main函数中,首先创建了一个SharedStack类型的变量stack,并通过调用init函数对行初始化。然后使用push_left和push_right函数依次将元素1、2、3、4入共享栈。接着通过连续调用pop_left和pop_right函数将元素出栈并输出结果。
运行示例代码将输出:
3
4
1
2
队列
1.队列的基本概念
队列:队列简称队,是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列种插入元素称为入队或进队;删除元素称为出队或离队。
- 对头(front):允许删除的一端,又称队首。
- 队尾(rear):允许插入的一端。
- 空队列:不含任何元素的空表。
2.队列的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q
QueueEmpty(Q):判队列空,队列Q为空返回true,否则返回false
EnQueue(&Q, x):入队,队列Q未满,将x加入,使成为新的队尾
DeQueue(&Q, &x):出队,队列Q非空,删除队头元素,并用x返回
GetHead(Q, &x):读队头元素,队列Q非空,则将队头元素赋值给x
3.队列的顺序存储结构
1)顺序队列
1.顺序队列
队列的顺序存储类型可描述为:
#define MAXSIZE 50 //定义队列中元素的最大个数
typedef struct{
ElemType data[MAXSIZE]; //存放队列元素
int front,rear;
}SqQueue;
初始状态(队空条件):Q->front == Q->rear == 0。
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1。
图d,队列出现“上溢出”,却又不是真正的溢出,是一种“假溢出”现象
2)循环队列
解决假溢出的方法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
当队首指针Q->front = MAXSIZE-1后,再前进一个位置就自动到0,这可以利用除法取余运算(%)来实现。
初始时:Q->front = Q->rear=0
队首指针进1:Q->front = (Q->front + 1) % MAXSIZE
队尾指针进1:Q->rear = (Q->rear + 1) % MAXSIZE
队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE
可队列不是使用链表实现的动态队列么?没有空间的时候会开辟空间,这难道还会产生假溢出么?
这个个问题,的确是的,当进行动态创建队列的时候,也只不过是向后继续不断的申请内存空间,即时前面出队操作释放掉了前面的空间,但是指针依旧会向后进行移动,直到达到系统预留给程序的内存上界被强行终止,这对于极为频繁的队列操作和程序而言是致命的,这时候,就需要对队列进行优化,使用更优秀的结构——循环队列
循环队列的思维很简单,就是给定队列的大小范围,在原有队列基础上,只要队列的后方满了,就从这个队列的前面进行插入,以达到重复利用空间的效果,由于循环队列的设计思维更像一个环,因此常使用一个环图来表示,但注意其不是一个真正的环,循环队列依旧是单线性的
那么,循环队列队空和队满的判断条件是什么呢?
显然,队空的条件是 Q->front == Q->rear 若入队元素的速度快于出队元素的速度,则队尾指针很快就会赶上队首指针,此时队满时也有 Q ->front == Q -> rear
为了区分队空还是队满的情况,有三种处理方式:
(1)牺牲一个单元来区分队空和队满
入队时少用一个队列单元,这是种较为普遍的做法,约定以“队头指针在队尾指针的下一位置作为队满的标志”
队满条件: (Q->rear + 1)%Maxsize == Q->front
队空条件仍: Q->front == Q->rear
队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize
(2)类型中增设表元素个数的数据成员
队空条件为 Q->size == O ;
队满的条件为 Q->size == Maxsize 。
这两种情况都有 Q->front == Q->rear
(3)类型中增设tag 数据成员,以区分是队满还是队空
tag 等于0时,若因删除导致 Q->front == Q->rear ,则为队空;
tag 等于 1 时,若因插入导致 Q ->front == Q->rear ,则为队满。
3)队列的基本运算
1.进队
进行入队(push)操作的时候,我们首先需要特判一下队列是否为空,如果队列为空的话,需要将头指针和尾指针一同指向第一个结点,即frnotallow=n;rear=n。
当如果队列不为空的时候,我们只需要将尾结点向后移动,通过不断移动next指针指向新的结点构成队列即可。
其代码可以表示为:
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
} Queue;
void init(Queue* queue) {
queue->front = 0;
queue->rear = 0;
}
int is_empty(Queue* queue) {
return queue->front == queue->rear;
}
int is_full(Queue* queue) {
return queue->rear == MAX_SIZE;
}
void enqueue(Queue* queue, int item) {
if (is_full(queue)) {
printf("Queue overflow!\n");
return;
}
queue->data[queue->rear++] = item;
}
int dequeue(Queue* queue) {
if (is_empty(queue)) {
printf("Queue underflow!\n");
return -1; // 可以根据需求返回合适的错误码或采取其他处理方式
}
return queue->data[queue->front++];
}
int main() {
Queue queue;
init(&queue);
enqueue(&queue, 1);
enqueue(&queue, 2);
enqueue(&queue, 3);
printf("%d\n", dequeue(&queue)); // 输出:1
printf("%d\n", dequeue(&queue)); // 输出:2
printf("%d\n", dequeue(&queue)); // 输出:3
return 0;
}
定义一个队列的数据结构Queue,包含一个整型数组data作为存储队列元素的容器,使用front、rear来分别表示队列的前端和后端指针。
代码中主要包含了以下函数:
init函数用于初始化队列,将front和rear都设为0。
is_empty函数用于判断队列是否为空。
is_full函数用于判断队列是否已满。
enqueue函数用于将元素入队列,将元素添加到队列的末尾。
dequeue函数用于出队列,将队列的前端元素取出并返回。
main函数中,首先创建一个Queue类型的变量queue,通过调用init函数对其进行初始化。然后使用enqueue函数依次将元素1、2、3入队列。接着通过连续调用dequeue函数将元素出队列并输出结果。
2. 出队
出队操作是指从队列中移除并返回队列的第一个元素。
出队(pop)操作,是指在队列不为空的情况下(请注意一定要进行队列判空的操作),进行一个判断,如果队列只有一个元素了(即头尾指针均指向了同一个结点),直接将头尾两指针制空(NULL)并释放这一个结点即可。
当队列含有二以上个元素时,我们需要将队列的头指针指向头指针当前指向的下一个元素并释放掉当前元素即可。
其代码可以表示为:
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
} Queue;
void init(Queue* queue) {
queue->front = 0;
queue->rear = 0;
}
int is_empty(Queue* queue) {
return queue->front == queue->rear;
}
int is_full(Queue* queue) {
return queue->rear == MAX_SIZE;
}
void enqueue(Queue* queue, int item) {
if (is_full(queue)) {
printf("Queue overflow!\n");
return;
}
queue->data[queue->rear++] = item;
}
int dequeue(Queue* queue) {
if (is_empty(queue)) {
printf("Queue underflow!\n");
return -1; // 可以根据需求返回合适的错误码或采取其他处理方式
}
return queue->data[queue->front++];
}
int main() {
Queue queue;
init(&queue);
enqueue(&queue, 1);
enqueue(&queue, 2);
enqueue(&queue, 3);
printf("%d\n", dequeue(&queue)); // 输出:1
printf("%d\n", dequeue(&queue)); // 输出:2
printf("%d\n", dequeue(&queue)); // 输出:3
return 0;
}
定义一个队列的数据结构Queue,包含一个整型数组data作为存储队列元素的容器,使用front、rear来分别表示队列的前端和后端指针。
代码中主要包含了以下函数:
init函数用于初始化队列,将front和rear都设为0。
is_empty函数用于判断队列是否为空。
is_full函数用于判断队列是否已满。
enqueue函数用于将元素入队列,将元素添加到队列的末尾。
dequeue函数用于出队列,将队列的前端元素取出并返回。
在main函数中,我们首先创建了一个Queue类型的变量queue,并通过调用init函数对其进行初始化。然后使用enqueue函数依次将元素1、2、3入队列。接着通过连续调用dequeue函数将元素出队列并输出结果。
运行代码将输出:
1
2
3
小结:
栈和队列是计算机科学中常见且重要的数据结构。栈是一种后进先出(LIFO)的数据结构,操作限定在栈顶进行,适用于函数调用、表达式求值等场景。队列是一种先进先出(FIFO)的数据结构,操作限定在队头和队尾进行,适用于任务调度、缓冲区管理等场景。掌握栈和队列的原理和应用对于算法设计和程序开发至关重要。它们能提高代码效率、可读性,并解决各类实际问题。灵活运用栈和队列,可以提升算法质量和复杂问题处理能力。通过学习和实践,我们能更好地利用栈和队列解决实际问题,开发高效、健壮的软件系统。
参考图片及部分代码来源: https://www.dotcpp.com/