数据结构第五篇——栈和队列
Posted 从零开始的智障生活
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构第五篇——栈和队列相关的知识,希望对你有一定的参考价值。
目录
前言
每接触一种数据结构类型,就要从逻辑结构,物理结构,数据的运算这三个方面进行考虑,深入理解定义
一、栈的定义和特点
栈(stack)是只允许在一端进行插入或删除操作的线性表。
- 栈顶(top):允许插入删除的一端(一般是表尾)
- 栈底(bottom):不允许插入删除的一端(一般是表头)
- 空栈:不含任何元素的空表。
- 原则:后进先出 (Last In First Out, LIFO) 。
可以类比叠盘子。
超纲了解:卡特兰数
一个n个元素皆不相同的序列,给定入栈顺序,但出栈与入栈同时进行,出栈的排列总共有(卡特兰数Catalan)。
问题:入栈顺序a->b->c->d->f, 在入栈的同时出栈,哪些出栈顺序是合法的?
二、栈的逻辑结构以及基本操作
2.1 用抽象数据类型来定义栈的数据结构
2.2 顺序栈的定义及其特点
顺序栈是指利用顺序存储结构实现的栈。
即利用一组地址连续的存储单元依次存放自栈底到 栈顶的数据元素,同时附设指针top指示栈顶元素在顺序栈中的位置。
2.3 顺序存储结构对栈基本操作的实现
与顺序表类似,顺序栈类型的表示方法有两种:静态分配和动态分配。静态分配:用静态的“数组”存放数据;动态分配:用指针指向一片连续的区域。
栈顶用从0记的话初始化top为-1;
栈顶用从1记的话初始化top为0;
声明顺序栈类型: 静态分配方式 | #define Maxsize 10 //定义栈中最多元素 typedef struct { int data[Maxsize]; //存放栈中元素 int top;// 栈顶指针 }SStack; | 静态分配方式,大小固定不变; 顺序栈;栈顶指针类型与数据域类型相同。 |
//初始化 void InitStack(SStack *S){ S.top=-1; } | // 初始化2 void InitStack(SStack *S){ S.top = 0; } | 由于data[i] (0<=i<=Maxsize-1) 数据都是有效的所以这里初始化top为-1, |
// 清空栈 bool ClearStack(SStack *S){ S.top=-1; return true; } | // 清空栈 bool ClearStack(SStack *S){ S.top=0; return true; } | 如果top初始化为0,那么top指向的就是下一个可以插入的位置。也就是top[0]可入栈。 |
//进栈 bool Push(SStack *S,int x){ if(S.top==Maxsize-1) return false;// 栈满,报错 S.data[++S.top] = x; return true; } | bool Push(SStack *S,int x){ // 栈满,报错 if(S.top==Maxsize) return false; S.data[S.top++]=x; return true; } | |
// 出栈 bool Pop(SStack *S,int &x){ if(S.top==-1) return false;// 栈空,报错 x = S.data[S.top--]; return true; } | bool Pop(SStack *S,int &x){ // 栈空报错 if(S.top==0) return false; x = S.data[--S.top]; return true; } | |
// 判断栈空 bool StackEmpty(SStack S){ if(S.top==-1) return true; else return false; } | // 判断栈空 bool StackEmpty(SStack S){ if(S.top==0) return true; else return false; } |
共享栈:利用栈底位置相对不变的特性,可以让两个顺序栈共享一个数据空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶项共享空间的中间延伸。
栈满的标志是top0==top1-1;
2.4 链栈的定义及其特点
链栈:采用链式存储方式实现的栈。
链栈的优点是:便于多个栈共享存储空间和提高其效率,且不存在栈满溢出的情况。
链栈的内容与链表相似,但也都有带头结点,与不带头结点的区别,注意区别。
2.5 链式存储结构对栈基本操作的实现
暂略。
三、队列的定义和特点
队列:只允许在一端进行插入,而在另一端进行删除的线性表。
- 队尾(rear):允许插入的一端;
- 队头(front):允许删除的一端;
- 空队列:
- 原则(特点,先进先出):先进先出(First In First Out,FIFO)。
四、队列的逻辑结构以及基本操作
4.1 用抽象数据类型来定义队列的数据结构
4.2 顺序队列的定义及其特点
在队列的顺出存储结构中,除了用一组地址连续的存储单元一次存放从队头到队尾的元素外,还需要附设连个整形变量front和rear分别指示队头元素和队尾元素的位置(后面分别称为头指针和尾指针)。
4.3 顺序存储结构对队列基本操作的实现
声明队列类型: 静态数组形式 | #define Maxsize 10 typedef struct{ int data[Maxsize]; int front; int rear; }SQ; | |
初始化队列 | void InitQ(SQ *Q){ Q->front = 0;// 指向队头 Q->rear = 0;// 队尾指向下一个要插入的位置(下一个应该插入的位置) } | |
队列是否为空 | bool Empty(SQ Q){ if(Q.front==Q.rear) return true; else return false; } | |
入队 | bool EnQueue(SQ *Q,int x){ if((Q->rear+1)%Maxsize==Q.front) return false; Q->data[Q.rear]=x;//插入队尾 Q->rear=(Q->rear+1)%Maxsize;//队尾指针后移 return true; } 因为这里判断队列是否为满的方法是:当前队尾(下一个要插入的位置)的下一节点是否为队头,容易注意到这里Q->rear是下一个要插入的位置,然而如果我们直接用Q->rear%Maxsize==Q->front去判断是否为满,因为这个判断方式与判断队空是等价的,所以会产生冲突,所以就选择了牺牲循环队列队头的前一个元素的空间,(Q->rear+1)%Maxsize==Q.front,如果这个下一个要插入元素的下一个元素是队头,那么我们就不能入队,也就是下一个的下一个如果是队头,所以实际上最多只能存放9个有效数据。 | 由于队列的特性,可能即使队尾是Maxsize也是非空。 利用取余运算,模运算将无限的整数域映射到有限的整数集合上{0,1,2,...,b-1}上。 {0,1,2,..,Maxsize-1}将存储空间在逻辑上变成了“环状”。 如果Q->rear==9那么(Q->rear+1)%Maxsize==0 即使(Q->rear+1)%Maxsize==0,也不能说明是队满,因为队列的环状结构,让队头的位置可能已经不再是0下标的位置 所以下一个要插入的位置如果是队头Q.front,那么这就满了。 所以队列不像栈一样,将值设为-1,而是将Q.front直接指向第一个结点。再看左边是关键:为什么会浪费一个结点? |
出队 | bool DeQueue(SQ *Q,int *x){ // 队空 if(Q->rear==Q->front) return false; x=Q->data[Q->front];// 出队 Q->front = (Q.front+1)%Maxsize;// 队头向后移动 return true; } | 如果 Q->front = (Q.front+1)%Maxsize; 造成队头和队尾重合,那么这个队列就是空的。 |
访问队头元素 | bool GetHead(SQ Q,int *x){ if(Q.rear==Q.front) return false; x = Q.data[Q.front]; return true; } | |
判断为满1 | bool Full1(SQ Q){ if((Q.rear+1)%Maxsize==Q.front) return true; else return false } | 这种判断方法会导致一个结点的内存浪费 |
判断为满2 | typedef struct{ int data[Maxsize]; int front,rear; int size; }SQ2; bool Full2(SQ2 Q){ if(size==Maxsize) return true; else return false; } | 增加一个变量size来保存队列长度。用来判断队空和队满。 初始化size=0; 入队size++ 出队size-- 队满size==Maxsize 对空size==0 |
判断为满3 | typedef struct { int data[Maxsize]; int front,rear; int tag; }SQ3; bool Full3(SQ3 Q){ if(Q.rear==Q.front&&tag==1) return true; else return false; } | 增加一个标志tag,初始化tag=0; 每次删除成功时,令tag=0; 每次插入成功是,令tag=1; 注意: 只有删除操作能让队空; 只有插入操作能让队满; 所以现在即使我们下一个要插入的结点是队头,我们只要知道上一次操作是插入,那么队列肯定是满的,即: 队满:Q.rear==Q.front&&tag==1 队空:Q.rear==Q.front&&tag==0 |
上面的操作实现过程是基于初始化时,让队尾指针指向的是下一个要插入的位置;当然还有其他情况,比如让队尾指向当前队列的最后一个元素,那么操作的方法又会有所不同。
初始化: 让队尾指向当前队列的最后一个元素 | void InitQ2(SQ *Q){ Q->rear = Maxsize-1; Q->front=0; } | 为了方便我们可以让Q->rear 初始化为n-1的位置,那么插入的时候自然就到n%n,也就是0 的位置 |
入队: 牺牲一个存储单元 | bool EnQueue2(SQ *Q,int x){ // 队满 if((Q->rear+2)%Maxsize==Q->front) return false; // Q->rear指向的是队列最后一个元素 Q->rear = (Q->rear+1)%Maxsize; Q->data[Q->rear]=x; return true; } | 判断队空: (Q->rear+2)%Maxsize==Q->front 判断队满:
|
4.4 链式队列的定义及其特点
链队是指采用链式存储结构实现的队列。
通常链队用单链表来表示。
一个链队显然需要两个指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一确定。
这里和线性表的单链表一样,为了操作方便起见,给链表添加一个头结点,并令指针始终指向头结点。
4.5 链式存储结构对队列基本操作的实现
队列的链式存储结构 | typedef struct LQNode{// 链式队列结点 int data; struct QNode *next; }QNode; typedef struct{ //队头和队尾(头指针和尾指针) QNode *front,*rear; }LinkedQueue; | |
初始化 带头结点 不带头结点 | // 初始化(带头结点) void InitLQ(LinkedQueue *L){ // 生成新节点作为头结点 L->front=(QNode *)malloc(sizeof(QNode)); // 队头和队尾都指向此节点 L->rear=L->front; // 头结点域置空 L->front->next = NULL; } // 链队为空 // 为空的条件:Q.front==Q.rear或Q.front->next==NULL bool Empty(LinkedQueue L){ if(Q.front==Q.rear) return true; else return false; } | //初始化(不带头结点) void InitLQ_noh(LinkedQueue *L){ L->front=NULL; L->rear=NULL; } //判断队列是否为空(不带头结点) //判断为空的条件L.front==L.rear或L.front==NULL bool Empty_noh(LinkedQueue L){ if(L.front==NULL) return true; else return false; } |
入队: 带头结点,只需要修改队尾; 不带头结点,如果是第一个插入,需要修改队头队尾。 | //入队(带头结点) bool EnQueue(LinkedQueue *L,int x){ LQNode *s=(LQNode *)malloc(sizeof(LQNode));//新节点 s->data=x; s->next=NULL; // 新节点插入尾结点后面 L->rear->next=s; // 将新节点作为尾结点 L->rear=s; return true; } | 带头结点,只要将新节点插入队尾,并将新节点作为队尾。 |
入队 | bool EnQueue_noh(LinkedQueue *L,int x){ LQNode *s=(LQNode *)malloc(sizeof(LQNode)); s->data=x; s->next=NULL; // 队列为空,那么队头,队尾为NULL if(L->front==NULL){ L->front=s; L->rear=s; } else{ L->rear->next=s; L->rear=s; } return true; } | 不带头结点,如果是空队列,那么就不能使用L->rear->next,因为这就相当于NULL->next。 只要将新节点作为队头,队尾即可。 |
出队: 带头结点 | bool DeQueue(LinkedQueue *L,int *x){ if(L->front==NULL) return false;//空队 //要出队的结点 LQNode *p=L->front->next;// x=p->data; L->front->next=p->next; // 队头是队尾的特别情形 if(p==L->rear) L->rear=L->front; free(p); return true; } | 出队的是头结点的后一个结点; 特别的是出队的结点是尾结点。 |
队满 | 链式队列除非空间不够否则一般不会出现队满的情形 |
4.6 双端队列的用法
双端队列:允许在两端进行插入删除操作的线性表。
相当于是整合了队列和链表的特性。
题型:若数据元素输入序列为(1,2,3,4),输入的同时进行输出,则哪些输出序列是在栈中合法的,哪些是在栈中非法的?双端队列呢?
步骤:首先从第一个输出去判断已经输入的结点,以及输出此结点后重复这一步骤,知道判断不合法或者合法。
判断在双端队列中是否合法,我们知道,在栈中合法的输出序列合法,那么双端队列肯定是合法的,所以我们只需要在栈中不合法的序列中找在双端队列中不合法的序列。比如(3,1,4,2)在栈中不合法,
第一个输出是3也就意味着:已经输入(1,2,3),由于是双端队列,所以输出队尾输出3,队头再输出1;然后输出的是4,所以,已经输入(1,2,3,4),目前队列还剩(2,4),输出4,再输出2;所以是合法的。
还有(4,2,1,3)在栈中不合法,
第一个输出是4,那么已经输入(1,2,3,4),那么还剩(1,2,3)然后只能输出1或3,所以输出2明显错误。
注意,这里的队列是双端队列,但却是输入受限的双端队列,因为给出的输入序列,都是从一个端口输入的。
五、栈的经典应用
5.1 栈的经典应用1:括号匹配问题
情形1 出现的括号是相同类型的层层叠加,没有同级关系,比如:
( | ( | ( | ( | ) | ) | ) | ) |
容易发现:最后出现的左括号最先被匹配(LIFO)。
于是发现可以用“栈”,实现该特性。
情形2出现的括号是相同类型的,但有同级关系,比如:
( | ( | ( | ) | ) | ( | ) | ) |
容易发现:每出现一个右括号,就“消耗”(出栈)一个最后发现的左括号。
于是发现还是可以用“栈”,实现该特性。
情形3:出现的括号是不同类型的,比如:
void test(){ int a[10][10]; int x= 10*(20*(1+3)-(3-2)); printf("加油奥利给"; } |
给出一段文本,要求匹配括号,括号的种类有:{},[],(),比如说:上面的文本。
我们要对这里的括号进行匹配:我们根据括号出现的顺序进行排列:
{ | [ | ] | [ | ] | ( | ( | ) | ( | ) | ) | ( | } |
规则如下:
- 遇到左括号就入栈;
- 遇到右括号就将最后一个入栈括号出栈,要求这个出栈的括号类型与右括号必须相同;
错误情形:
- 遇到右括号,栈中最后一个括号与之类型不同;
- 当扫描遇到一个右括号,栈空时;
- 当文本扫描结束,栈非空;
#include <stdio.h> // void test(){ // int a[10][10]; // int x= 10*(20*(1+3)-(3-2)); // printf("加油奥利给"; // } #define Maxsize 10//定义栈中最大元素· typedef struct{ int data[Maxsize]; int top; }SqStack; //初始化栈 void InitSqStack(SqStack *S); // 入栈 bool Push(SqStack *S,int x); // 出栈 bool Pop(SqStack *S, int *x); // 是否为空 bool IsEmpty(SqStack S); // 传入一个包含文本内容的数组,和数组长度。 bool BracketCheck(char str[],int length){ SqStack S; InitSqStack(&S);初始化一个栈 for(int i=0;i<length;i++){ if(str[i]=='('||str[i]=='{'||str=='[') Push(&S,str[i]); else{ 数据结构初阶第五篇——栈和队列(实现+图解) |