第三章 栈和队列
2021/12/19 23:28:31
本文主要是介绍第三章 栈和队列,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
栈和队列的基本概念
栈的基本概念
-
栈的定义:是一种只能在一端进行插入或删除操作的线性表,其中允许进行插入或删除操作的一端称为栈顶。栈顶由一个称为栈顶指针的位置指示器(其实就是一个变量,对于顺序栈,就是记录栈顶元素所在数组位置标号的整型变量;对于链式栈,就是记录栈顶元素所在结点地址的指针)来指示。它是动态变化的,表的另一端为栈底,栈底是固定不变的,栈的插入和删除操作称为入栈和出栈。
-
栈的特点:由栈的定义可以看出,栈的主要特点是先进后出(FILO)。
-
栈的存储结构:可以通过顺序表和链表来存储栈,栈可以依照存储结构分为两种:顺序栈和链式栈。
-
栈的数学特性:当n个元素以某种顺序进栈,并且可在任意时刻出栈(在满足先进后出的前提下)时,所获得的元素排列的数目N恰好满足函数
Catalan()
的计算,即\(N=\frac{1}{n+1}C_{2n}^{n}\)。
队列的基本概念
- 队列的定义:也是一种操作受限的线性表,其限制为仅允许在表中的一端进行插入,在表的另一端进行删除。可进行插入的一端称为队尾(rear),可以进行删除的一端称为队头(front)。向队列中插入新的元素称为进队,新元素进队后就成为新的队尾元素;从队列中删除元素称为出队,元素出队后,其后继元素称为新的队头元素。
- 队列的特点:由队列的定义得到,队列主要特点为先进先出(FIFO)。
- 队列的存储结构:可用顺序表和链表来存储队列,队列按存储结构分为顺序队和链队。
栈和队列的存储结构实现
栈和队列的结构体定义
- 顺序栈的定义
#define maxSize 100 typedef struct SeqStack{ int data[maxSize]; int top; }SeqStack;
- 链栈节点定义
//有头结点的链栈 typedef struct LNode{ int data; struct LNode *next; }LNode;
- 顺序队列定义
#define maxSize 100 //队列是循环队列。 typedef struct SeqQueue{ int data[maxSize]; int front; //队首指针 int rear; //队尾指针 }SeqQueue;
- 队列结点定义
typedef struct QNode{ int data; struct QNode *next; }QNode;
- 链队定义
//链式队列不是循环队列 typedef struct LinkQueue{ QNode *front; //队头 QNode *rear; //队尾 }LinkQueue;
栈只需要创建结点,因为栈只有一个控制结点,使用单独的结点进行控制。
顺序栈
顺序表的要素
- 几个状态
- 栈空状态:
st.top == -1
,有的书上规定st.top == 0
为栈空条件,但是这样会浪费一个元素大小的空间。 - 栈满状态:
st.top == maxSize-1
,maxSize为栈中最大元素的个数。则maxSize-1为栈满时栈顶元素在数组中的位置,因为数组下标从0开始,因此规定栈顶指针top为-1时栈空。即top==0的数组位置也可以存有数据元素。 - 非法状态(上溢和下溢):栈满就是一种继续入栈就会上溢的状态,对应的栈下溢就是栈空的时候继续出栈所导致的结果。
- 栈空状态:
- 两个操作
- 元素x进栈的操作:
++(st.top);st.data[st.top]=x;
。规定了top为-1时栈为空,则元素进栈操作必须先移动指针,再进入元素。 - 元素x出栈的操作:
x=st.data[st.top];--(st.top);
。 进栈操作的次序决定了出栈操作的次序。由于进栈操作是先变动栈顶指针,再存入元素,因此出栈操作必须先取出元素,再变动指针。
- 元素x进栈的操作:
操作定义
//初始化栈 SeqStack* initStack(); //判断栈是否为空,栈为空时返回1 int isEmpty(SeqStack stack); //判断栈是否为满,如果满时返回1 int isFull(SeqStack stack); //进栈操作,进栈成功返回1 int push(SeqStack* stack,int data); //出栈操作,x为出栈元素的结果 int pop(SeqStack* stack,int* x);
操作实现
SeqStack* initStack(){ SeqStack* stack = (SeqStack*) malloc(sizeof(SeqStack)); stack->top = -1; return stack; } int isEmpty(SeqStack* stack){ if(stack->top == -1){ return 1; }else{ return 0; } } int isFull(SeqStack* stack){ if(stack->top == maxSize-1){ return 1; }else{ return 0; } } int push(SeqStack* stack,int data){ if(isFull(stack) == 1){ return 0; }else{ ++stack->top; stack->data[stack->top] = data; return 1; } } int pop(SeqStack* stack,int* x){ if(isEmpty(stack) == 1){ return 0; } *x = stack->data[stack->top]; --stack->top; return 1; }
链栈
链栈的要素
- 两个状态
- 栈空状态:
lst->next == NULL
- 栈满状态:不存在栈满的状况(假设内存无限大的情况)。
- 栈空状态:
- 两个操作
- 元素(由指针p所指)进栈操作:
p->next = lst->next; lst->next = p;
(其实就是头插法插入节点) - 出栈操作(出栈元素保存在x中):
p = lst->next;x = p->data;lst->next = p->next;
(其实就是单链表的删除操作)
- 元素(由指针p所指)进栈操作:
操作定义
//初始化链栈 LNode* initStack(); //判断栈空,返回1,栈满 int isEmpty(LNode* lst); //进栈操作 void push(LNode* lst,int x); //出栈操作,如果成功返回1 int pop(LNode *lst,int* x);
操作实现
LNode* initStack(){ LNode* lst = (LNode*) malloc(sizeof(LNode)); lst->next = nullptr; return lst; } int isEmpty(LNode* lst){ if(lst->next == nullptr){ return 1; }else{ return 0; } } void push(LNode* lst,int x){ LNode *p; p = (LNode*) malloc(sizeof(LNode)); p->next = nullptr; //头插法插入节点 p->data = x; p->next = lst->next; //原先的节点在后面。弹出时,将首节点弹出即可。 lst->next = p; } int pop(LNode *lst,int* x){ LNode *p; if(lst->next == nullptr){ return 0; } p = lst->next; *x = p->data; lst->next = p->next; //删除首节点的元素 free(p); return 1; }
顺序队
循环队列:在顺序队中,通常让队尾指针rear指向刚进队的元素位置,让队首指针front指向刚出队的元素位置。因此元素进队时,rear要向后移动;元素出队的时候,front也要向后移动。这样经过一系列的出队和进队操作后,两个指针最终会达到数组末端maxSize-1处。虽然队中已经没有元素,但是仍然无法让元素进队,这就是“假溢出”,要解决这个问题,可以把数组弄成一个环,让rear和front沿着环走,这样就不会出现”假溢出“。这就产生了循环队列。
要实现指针在递增的过程中沿着环形道路行走,拿front指针来说,可以循环执行front = (front+1)%maxSize
,若front的初始值为0,在一个无限循环中,front的取值范围只能是0~maxSize-1。
因为必须要求有表头,所以循环链表必须损失一个存储空间,如果表头也存入元素,则队满的条件为front==rear,即和队空条件相同,那么无法区分队空和队满。
循环队列的要素
- 两个状态
- 队空状态:
qu.rear == qu.front
- 队满装态:
(qu.rear+1)%maxSize == qu.front
- 队空状态:
- 两个操作
- 元素x进队操作(移动队尾指针):
qu.rear=(qu.rear+1)%maxSize;qu.data[qu.rear]=x;
- 元素x出队操作(移动队头指针):
qu.front=(qu.front+1)%maxSize;x=qu.data[qu.front];
- 元素x进队操作(移动队尾指针):
操作定义
//初始化队列 SeqQueue* initSeqQueue(); //判断队空 int isQueueEmpty(SeqQueue *queue); //进队操作 int enQueue(SeqQueue *queue,int x); //出队操作 int deQueue(SeqQueue* queue,int *x);
操作实现
SeqQueue* initSeqQueue(){ SeqQueue* queue = (SeqQueue*) malloc(sizeof(SeqQueue)); queue->rear = queue->front = 0; return queue; } int isQueueEmpty(SeqQueue *queue){ if(queue->rear == queue->front){ return 1; }else{ return 0; } } //元素进队操作(移动队尾指针): // queue->rear = (queue->rear+1)%maxSize; queue->data[queue->rear]=x; int enQueue(SeqQueue *queue,int x){ if((queue->rear+1)%maxSize == queue->front){ //队满 return 0; } queue->rear = (queue->rear + 1) % maxSize; queue->data[queue->rear] = x; return 1; } //元素出队操作(移动队首指针) //queue->front = (queue->front + 1)%maxSize; *x = queue->data[queue->front]; int deQueue(SeqQueue* queue,int *x){ if(queue->front == queue->rear){ //队空 return 0; } queue->front = (queue->front + 1)%maxSize; *x = queue->data[queue->front]; return 1; }
链队
链队就是采用链式存储结构存储队列,这里采用单链表实现,链表的特点就是不存在队列满上溢的情况。
链队的要素
- 两个状态
- 队空状态:
lqu->rear == NULL
或者lqu->front == NULL
- 队满状态:不存在队列满的情况。
- 队空状态:
- 两个操作
- 元素进队操作:
lqu->rear->next=p;lqu->rear=p;
- 元素出队操作:
p=lqu->front;lqu->front=p->next;x=p->data;
- 元素进队操作:
操作定义
//初始化队列 LinkQueue* initQueue(); //判断队空 int isQueueEmpty(LinkQueue* queue); //入队操作 void enQueue(LinkQueue* queue,int x); //出队操作 void deQueue(LinkQueue* queue,int *x);
操作实现
LinkQueue* initQueue(){ LinkQueue* queue = (LinkQueue*) malloc(sizeof(LinkQueue)); queue->front = queue->rear = nullptr; return queue; } int isQueueEmpty(LinkQueue* queue){ if(queue->rear == nullptr || queue->front == nullptr){ return 1; }else{ return 0; } } void enQueue(LinkQueue* queue,int x){ QNode *p; p = (QNode*) malloc(sizeof(QNode)); p->data = x; p->next = nullptr; if(queue->rear == nullptr){ queue->rear = queue->front = p; }else{ queue->rear->next = p; queue->rear = p; } } void deQueue(LinkQueue* queue,int *x){ QNode *p; if(queue->rear == nullptr){ return; }else{ p = queue->front; } if(queue->front == queue->rear){ //队列中有一个节点时出队要特殊处理 queue->front = queue->rear = nullptr; } else{ queue->front = queue->front->next; } *x = p->data; }
栈的应用
括号匹配
int match(char exp[],int n){ char stack[maxSize]; int top = -1; for (int i = 0; i < n; ++i) { if(exp[i] == '('){ stack[++top] = '('; } if(exp[i] == ')'){ if(top == -1){ return 0; //不匹配 }else{ top--; //如果栈不空,则出栈。 } } } if(top == -1){ //栈空,则说明括号匹配 return 1; }else{ return 0; //否则,不匹配 } }
计算后缀表达式结果
int com(char exp[]){ int i,a,b,c; int stack[maxSize]; int top = -1; char Op; for(i = 0;exp[i] != '\0';++i){ if(exp[i] >= '0' && exp[i] <= '9'){ //如果遇到操作数,则入栈等待处理。 stack[++top] = exp[i] - '0'; }else{ //碰到运算符,开始计算。 Op = exp[i]; b = stack[top--]; a = stack[top--]; c = op(a,Op,b); stack[++top] = c; } } return stack[top]; } int op(int a,char Op,int b){ if(Op == '+'){ return a+b; } if(Op == '-'){ return a-b; } if(Op == '*'){ return a*b; } if(Op == '/'){ if(b == 0){ printf("Error"); return 0; }else{ return a/b; } } return 0; }
共享栈和双端队列
共享栈
相比于普通的顺序栈,共享栈主要是为了提高内存的利用率和减少溢出的可能性而设计的,共享栈有很多特性。下面通过一个例题来了解共享栈。
为了增加内存空间的利用率和减少溢出的可能性,当两个栈共享一片连续的内存空间时,应将两栈的栈底分别设在这片内存空间的两端,这样当两个栈的栈顶在栈空间的某一位置相遇时,才产生溢出。
解析:两个栈共享一片连续的存储空间,可知这两个栈都是顺序栈,进一步可知,为顺序栈分配好的连续空间大小在栈的操作过程中不变,并且这个连续的存储空间有恒定不变的两端。于是可以想到,这两个栈的栈底分别位于存储空间两端。确定了栈底,则两栈栈顶必在存储空间内,显然当两栈顶相遇时,存储空间耗尽,会产生溢出。
双端队列
双端队列是一种插入和删除操作在两端都可以进行的线性表,可以把双端队列看成栈底连在一起的两个栈。它们与两个栈共享存储空间的共享栈不同的是:两个栈的栈顶指针时向两端延伸的。由于双端队列允许在两端插入和删除元素,因此需要设立两个指针:end1和end2,分别指向双端队列中两端的元素。
允许在一端进行插入和删除,另一端只允许删除的双端队列称为输入受限的双端队列。允许在一端进行插入和删除,另一端只允许插入的双端队列称为输出受限的双端队列。
这篇关于第三章 栈和队列的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-29设计Element UI表单组件居然如此简单!
- 2024-12-28一步到位:购买适合 SEO 的域名全攻略
- 2024-12-27OpenFeign服务间调用学习入门
- 2024-12-27OpenFeign服务间调用学习入门
- 2024-12-27OpenFeign学习入门:轻松掌握微服务通信
- 2024-12-27OpenFeign学习入门:轻松掌握微服务间的HTTP请求
- 2024-12-27JDK17新特性学习入门:简洁教程带你轻松上手
- 2024-12-27JMeter传递token学习入门教程
- 2024-12-27JMeter压测学习入门指南
- 2024-12-27JWT单点登录学习入门指南