数据结构入门学习③——栈和队列

前言:

本篇博客主要介绍有关栈和队列数据结构相关知识

思维导图介绍:

开始之前,先介绍一下这篇博客主要介绍的主体内容:

栈和队列:

回顾线性表:

在刚才的思维导图里我们也了解到了——栈和队列都属于线性表,那么这里复习一下什么是线性表呢?

线性表——根据数据结构的分类来讲——

1.对于它的逻辑结构而言——它是线性结构

2.而它的物理结构有两种——顺序存储结构和链式存储结构

它通俗的来讲就是把数据按顺序存储在内存中的一种数据结构

那现在就让我们进入正题吧——

基本定义:

栈和队列都属于一种限制出入的线性表,不过它们所限制的方式不同

属于限制线性表先进入到表里的最后才可以获取到——也就是“先进后出”,类比装在弹夹里的子弹,先装进去的最后才打出来。

队列是限制——一端是进入表中的,一端是出表的,讲究"先进先出"的规则,这个其实可以类比排队的 ,先排队的总是可以先进行活动,而一端来人,一端出人。

使用案例:

进制转换

这里的主要是在十进制转化为其余进制而使用的——对于十进制转化为二、八、十六进制,它们都有一个基本的共识——除R取余(这里的R指转化成进制的2、8、16)

而对于这个取余,我们一般都是倒着取,也就是——先除R得到的那个余数在最后一位。

这个很明显的符合我们刚才介绍的栈的特性——先进后出。

因此栈可以在这个进制转换中使用,具体使用方法等介绍完栈的知识再做讲解


括号的匹配

假如给你几个括号,让你判断它们是否都配对成功了。

例如:{([])}这个就是配对成功了,{)[(}而这个显然不是

那么如何解决这一道题目呢——使用栈

这里先基本介绍一下使用方法——

先把一半的括号插入到栈中,然后再插另一半,插一个的时候要判断,是否和栈中的括号匹配,如果不匹配就直接报错,如果匹配则对应的括号出栈,直到所有元素出栈则可以说明所有的括号都匹配成功


表达式求值

对于一个简单的表达式,由于符号的优先级不同,则运算数据也不同,而利用表达式求值可以使用栈的方法,这个设计的算法比较复杂,在讲解完栈后再讲解。


舞伴问题

假如要你要开办一个舞会,舞会上让男女配对跳舞,则男生和女生站两列,她们位置一致则匹配成功,排在前面的,就先和对应的搭档开始跳舞,如果有一对的人比较多,配对不成,则他就成为下一首歌的第一个和异性匹配并跳舞的人。

栈(stack):

定义:

具有先进后出限制的线性表。

有顺序栈和链栈两种形式。

创建栈:

顺序栈的创建——

设置一个结构体,里面存放两个指针——一个指向栈顶,一个指向栈尾。和一个数组大小

构建基本代码:

typedef struct {
    Stackelem *top;
    Stackelem *base;
    int stacksize;
}Sqstack;

注意这里的Stackelem是指放在栈中的元素类型 

两种溢出形式

对于栈而言,有两种溢出形式:

一般而言上溢出只发生在顺序栈中,而链栈由于可以自己手动分配空间,基本不存在上溢出的问题。

1.上溢出,入栈元素过多,超过栈的最大空间,则发生错误,上溢出。

2.下溢出,一直出栈,但是栈里已经没有元素了,因此发生错误,下溢出。

但是,一般把上溢出看为错误,而把下溢出看为一种结束条件

满栈和空栈:

那么如何判断这个栈是否满?或者是否为空?

这里画一个图:

如图所示,这是一个基本的顺序栈,而在这个栈中,初始状态——也就是base和top指向第一个位置,这时候栈内为空,而当top所指向的空间是开辟空间最后一个了,那么栈满了。

那现在理论解释明白了,代码该怎么表示呢?

——

对于栈空:当top==base时栈为空

对于栈满:当top-base==stacksize时栈满

栈空和栈满在下面的一些基本操作中也会用到,所以这里先提前介绍一下如何判断栈空或是栈满的情况。

顺序栈

基本操作:

初始化

初始化就是先开辟一块大小确定的空间,然后base指针指向这块空间,判断是否开辟成功,如果空间开辟失败,则错误退出,如果开辟成功,则让top指针=base指针,让stacksize等于开辟空间的大小

代码:

int initstack(Sqstack& s)
{
	s.base = new int[MAXSIZE];//c++开辟新空间的语句,利用new开辟,相当于c语言中的malloc
	if (s.base)
	{
		return -1;
	}
	s.top = s.base;
	s.size = MAXSIZE;
	return 0;
}
销毁栈

销毁栈,就是需要把栈所占的空间给释放掉,而刚开始开辟的时候是动态开辟,这里就用到delete关键字,这个关键字作用和c语言中的free一样,都是释放掉动态开辟的内存,直接delete base,然后把top和base指针置空,栈长=0即可。

代码:

Status Destorystack (SqStack &s)

{

        if(s.base)

        {

        delete s.base;

        s.stacksize=0;

        s.base=s.top=NULL;

        }

        return OK;

}

置空栈

把栈中元素置空,而不管空间的事,这个操作就直接让top=base即可,这样top和base之间的元素为0,置空栈操作成功

Status ClearStack(SqStack s)

{

        if(s.base)

        {

         s.top=s.base;

        }

        return OK;

};

入栈

假如栈为空则——入栈就是先把top指针的位置给赋值成传进来的值,然后把top指针往上移一位。

 Status Push(Sqstack &s,Stackelem e)

{

       if(s.top-s.base==s.stacksize)

                return ERROR;

        *s.top=e;

        s.top++;

        return 0;

}

出栈

设定一个变量,把top下面位置存放的数据赋值给这个变量,然后top指向这个位置即可

 Status Push(Sqstack &s,Stackelem e)

{

       if(s==NULL)

                return ERROR;

        e=s.--top;

        return 0;

}

判断是否为空

上面已经介绍了如何判断栈是否为空了,就是当top和base相等时栈为空。

代码:

Status  Stackempty(SqStack s)

{

        if(s.top==s.base)

         {

                return true;

        }else{

                return false;

        }

}

链栈

构建:

链栈和顺序栈的最大区别就是物理结构不同,顺序栈用到了c++中的数列,而链栈要用到前面的链表,在链表中存放数据和指针。

这意味之前的结构体构建到这里不能用了,因此这里介绍一下新的构建:

结构体里要包含数据域和指针域

typedef struct StackNode

{

        stackelem data;

        struct StackNode *nextl

}StackNode,*LinkStack;

LinkStack s;//s代表指向结构体类型变量的指针

 

🎄注意:链表的头指针就是栈顶,这个链栈没有头结点,而这个一般不会溢出——出现栈满的情况。

基本操作:

初始化:

Status InitStack(LinkStack &s)

{

        s=NULL;

        return OK;

}

对于链栈的初始化就和顺序栈的有很大区别了,向初始化函数传入头指针,让头指针置为空,即头指针=NULL即可——这里会有人问了,为什么让头指针置为空?不应该是让指针所指的next(指针域)为空吗?

其实,这里要注意一点——链栈的链表没有头结点

🎄头指针和头结点不一样,头结点初始化是要把头结点的next域置为空,而头指针是直接把自己=NULL,置为空,因为指针所占用的空间是存储所指的位置的,指针位置置为空,则对于这个链栈的初始化来说——完成。

入栈

入栈就是先开辟一块空间,空间类型为自建的结构体类型,然后让这块空间的指针域指向头指针所指向的空间,然后让头指针指向新开辟的空间——这样就可以只借助一个头指针来完成栈的操作了

 Status Push(LinkStack &s,stackelem e)

{

        LinkStack a=new StackNode;

        a->data=e;

        a->next=s;

        s=a;

        return OK;

}

出栈

出栈就是先提取出来要出栈的数据域的值,然后让头指针指向出栈的下一个,最后释放掉出栈的空间。

Status Pop(LinkStack &s)

{

       if(s=NULL){

                return ERROR;

        }

        e=s->data;

        p=s;

        s=p->next;

        delete p;

        return OK;

}

队列(queue):

定义:

队列是一种限制进出的线性表,“先进先出”(FIFO),在生活中这个性质比较常见些,比如排队买饭……

而队列是限制一端进,一端出,每一端都有不同的操作。

常见应用:

  • 多用户系统中,多用户排队,按照来的顺序来循环使用CPU和主存。
  • 按照用户的优先级拍成多个队,每个优先级一个队列
  • 实时控制系统,信号按照接受的先后依次处理。
  • 网络电文传输,按照到达的实践依次进行
  • ……

顺序队列

构建表示:

上图就展示了一个队列拥有的基本特征,其中的rear是指向队尾,front是指向队头。

rear端进行进入队列操作,而front端进行出队列操作,所以这里可以引入——如何判断队列为空:

当front和rear指向同一位置时,队列为空。

 那顺序队列的类型构建该怎么构建呢?

示例代码如下:

#define MAXQSIZE 100
typedef struct{
    QElemType *base;//动态分配存储空间
    int rear;
    int front;
}SQueue;

这里写的的rear尾指针和front头指针为int类型,这时候会有些疑惑——不是说这个它们是指针吗?怎么写成整型的形式?

这是因为这里顺序队列所借指的是数组,而且不是动态分配的数组?所以数组的下标和指针有着一样的作用,则可用整型代表更安全。

一般顺序队列

操作:
初始化:

初始化顺序队列,就是把头指针和尾指针指向初始位置即可

代码如下:

Status  InitQueue (SQueue &q)

{

       q.base=new  QElemType[MAXQSIZE];//这里的QElemType是指所队列存放数据的类型,按需求填入不同数据类型。

        if(!q.base)        exit(-1);

        q.front=q.rear=0;

        return OK;

}

入队: 

入队,就是让入队的数据赋值给rear指针指向的区域,再让指针++

即:

q.base[rear]=x;

q.rear++;

出队:  

先把front指针指向的空间存储数据赋值给某个变量,再把front头指针往上移动一位即可

简易代码:

e=q.base[q.front];

q.front++;

注意当front==rear时队列为空,不能再出队。

❀BUT

当我们执行这些操作直到队列满,在空间内,我们发现还有一些空间没有利用,这块空间是出队列时front往上移动而产生的结果,那这块空间我们能不能再利用呢?就像排队一样,排完队的人出队后,后面的人就会自动往前补上。

答案是——当然可以。

下面就介绍队列中比较常用的顺序队列——循环队列

循环队列

这个队列的设置是为了解决假上溢出的较好的方法,那什么是假上溢呢?其实刚才就提到了——当出队后使头指针下的位置为空,而队列内——即头指针和尾指针之间的位置已经满了,如果再入队则会导致溢出,BUT这种溢出被称为假溢出,因为所开辟的空间内还有未用的存储单元,所以解决办法就是循环队列——

将队列设想为一个循环的表:

在这样一个表中可以重复利用开辟的存储空间,当rear=maxqsize时,假如开始端空着,则可以从头利用空间,而当front=maxqsize时也可以如此,这便称为循环队列

操作:

那如何实现这循环队列呢?即当rear+1=maxqsize ,则令rear=0;

——

利用取余符号%来实现一个圈的循环

让rear=rear%maxqsize,当rear<maxqsize时rear仍=rear,当rear=maxqsize时rear=0

同理front也是。

插入元素:

那循环队列如何插入元素?

q.base[q.rear]=x;//x是插入的元素

q.rear=(q.rear+1)%MAXQSIZE

//注意:这里+1的原因是插入元素后,rear需要往上移动一位

删除元素:

e=q.base[q.front];

q.front=(q.front+1)%MAXQSIZE

//这里的+1的原因如上。

队空:

如何判断循环队列为空?

q.rear==q.front

队满:

那如何判断队满呢?

思考一下发现队满也是q.rear==q.front

那如何区分所谓的队空和队满呢?

方法如下:

  • 1.把循环队列空出一格出来,意思是少用一个元素空间
  • 2.设置一个变量,记录元素的个数来判断区分队满和队空
  • 3.设置一个标志区分队空和队满

而最常用的就是少用一块空间,

这样:

当空队时front==rear

当队满时(rear+1)%MAXQSIZE==front

那这个式子为什么用%符号了?——因为循环队列,不清楚到底rear大还是front大,rear指向队列中空出的区域,当rear+1就是可能和front相等,也可能和front差一个MAXQSIZE,那用取余符号列出的式子就可以概括这两种情况,完成判断队满的操作了。

具体的操作:
初始化:

Status  InitQueue (SQueue &q)

{

       q.base=new  QElemType[MAXQSIZE];//这里的QElemType是指所队列存放数据的类型,按需求填入不同数据类型。

        if(!q.base)        exit(-1);

        q.front=q.rear=0;

return OK;

}

 求队列长度:

int LengthQueue(SQueue q)

{

        return ((q.rear-q.front+MAXQSIZE)%MAXQSIZE);

}

这个式子其实有点难理解,下面就进行具体的讲解一下这个式子到底怎么得出的

——

这里列举以下几种情况

如上图,我们列出了它们分别的计算式和结果,计算式的本质式分别为:rear-front,rear-front+Maxqsize,rear-front+Maxqsize

那么现在是想怎么把这两个式子合为一个——取余符号

即rear-front+MAXQSIZE再整体取余MAXQSIZE

入队:

Status EnQueue (SQueue &q,QElemType e)

{

if((q.rear+1)%MAXQSIZE==q.front)

        return ERROR;

q.base[q.rear]=e;

q.rear=(q.rear+1)%MAXQSIZE;

return OK;

}

出队:

Status OutQueue (SQueue &q,QElemType x)

{

if(q.rear+1==q.front)

        return ERROR;

x=q.base[q.front];

q.front=(q.front+1)%MAXQSIZE;

return OK;

}

链队

基本操作:

链队即使用数据结构中的物理结构——链式结构来实现队列的一个数据结构

注意:front为头指针,它指向头结点,而头结点中没有data数据域。

类型定义:

#define MAXQSIZE 100

typedef  struct Qnode{

        QElemType data;

        struct Qnode*next;

}Qnode,*Queueptr;

typedef struct 
{

        Queueptr front;

        Queueptr rear;

}LinkQueue;

初始化链队:

传入LinkQueue自定义类型,并动态开辟一块空间,空间类型为Qnode。front和rear指向同一块空间

Status InitListQueue(LinkQueue &q)

{

        q.front=q.rear=new QNode;

        q.front->next=NULL;

        return OK;

}

入队:

Status InQueue(LinkQueue &q,ElemType e)

{

        LinkQueue s=new QNode;

        s->data=e;

        s->next=NULL;

        q.rear->next=s;

        q.rear=s;

        return OK;

}

出队:

Status OutQueue(LinkQueue &q,ElemType x)

{

       if(q.front==q.rear)        return ERROR;

        p=q.front->next;

        x=p->data;

        front->next=p->next;

        if(q.rear==p)        q.rear=q.front;

        delete p;

        return OK;

}

销毁: 

Status DeleteQueue(LinkQueue &q)

{

        while(q.front)

        {

                p=q.front->next;

                delete q.front;

                q.front=p;

         }

}

总结:

好了,到这里,栈和队列的知识就介绍完毕,欢迎点赞收藏,或者关注专栏,以免找不到哦🤭

文章如有问题,可以私信或者评论区留言,感谢观看完毕o(* ̄▽ ̄*)ブ

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值