数据结构之栈与队列

为什么要使用栈

实际上,可能当时没有“栈”的时候,需求的是一种LIFO(Last In First Out)的模式。这种模式有很多用途。

例如:编程中的函数调用

有如下程序

int main() {
  a();
  return 0;
}

void a() {
  b();
}

void b() {
  c();
}

void c() {
}

那么整个程序的函数活动时间可以表示为:

main()  a()  b()  c()
   -                         main()
   |
   +>     -                    a()
   .      |
   .      +>   -                 b()
   .      .    |
   .      .    +>   -              c()
   .      .    .    |
   .      .    +   <-              return from c()
   .      .    |
   .      +   <-                 return from b()
   .      |
   +     <-                    return from a()
   |
   -                         return from main()

作者:RednaxelaFX
链接:https://www.zhihu.com/question/34499262/answer/59415153
来源:知乎

函数调用需要的是一种嵌套关系——调用者的生命期总是要长于被调用者的生命期,并且后者在前者之内。这样,被调用者的局部信息所占空间的分配总是后于调用者的(后入),而其释放则总是先于调用者的(先出),需要用LIFO顺序实现。

又例如:四则运算表达式求值

有加减乘除,甚至还带有大中小括号的四则运算。人一眼可以看出运算的顺序,但机器又是要怎么实现呢?
(待补)

因此,有这种“后进先出”的模式需求,人们也就设计出“栈”这种数据结构啦

栈的使用

说明:栈的插入操作(Push),叫作进栈,也称压栈、入栈。栈的删除操作(Pop),叫作出栈,也有的叫作弹栈。

栈的顺序存储结构及实现

最重要的功能是push、pop、peek(返回栈顶的元素)、empty、search(看某个元素离栈顶多远距离)

顺序栈的结构定义
typedef int SElemType; //假定SElemType类型是int
typedef struct
{
    SElemType data[MAXSIZE];  //定义数组长度
    int top;  //top指示栈顶元素在数组中的位置
}SqStack;

NOTE:当栈存在一个元素时,top等于0,因此通常把空栈的判定条件定为top等于-1

push
/* 插入元素e为新的栈顶元素 */
Status Push(SqStack *S, SElemType e)
{
    if(S->top == MAXSIZE-1)  //栈满
        return ERROR;
    S->top++;  //栈顶指针增加一
    S->data[S->top]=e;  //将新插入元素赋值给栈顶空间
    return OK;
}
pop
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqStack *S,SElemType *e)
{
    if(S->top==1)
        return ERROR;
    *e=S->data[S->top];  //将要删除的栈顶元素赋值给e
    S->top--;  //栈顶指针减一
    return OK;
}

栈的链式存储结构及实现

链栈结构定义
typedef struct StackNode
{
    SElemType data;
    Struct StackNode *next; //next指向StackNode结构体,并且next是StackNode结构体的一部分
}StackNode,*LinkStackPtr;  //LinkStackPtr指向结构体StackNode

typedef struct LinkStack
{
    LinkStackPtr top;  //top作为LinkStackPtr结构指向结构体StackNode
    int count;  //计数
}LinkStack;

栈顶放在单链表的头部。另外有了栈顶在头部,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的

push
/* 插入元素e为新的栈顶元素 */
Status Push(LinkStack *S,SElemType e)
{
    LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode))
    s->data = e;
    s->newxt = S->top;  //把当前的栈顶元素赋值给新结点的直接后继
    S->top=s;  //将新的结点s赋值给栈顶指针
    S->count++;
    return OK;
}
pop
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(LinkStack *S,SElemType *e)
{
    LinkStackPtr p;
    if(StackEmpty(*S))
        return ERROR;
    *e=S->top->data;
    p=S->top;  //将栈顶结点赋值给p
    S->top=S->top->next;  //使得栈顶指针下移一位,指向后一结点
    free(p);  //释放结点p
    S->count--;
    return OK;
}

队列

为什么要使用队列

同“栈”的设计类似,人们需要一种“先进先出(First In First Out)”的数据结构。

队列的顺序存储

由于建立一个数组存放数据时,若元素出队列,以数组的特性,后面的元素必须移动补空,否则会造成空间的浪费。但此时一个元素出队列,时间复杂度为O(n),为了提高队列数组的存储效率,于是出现的循环队列

循环队列

我们把队列这种头尾相接的顺序存储结构称为循环队列。
引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,当front等于rear时,队列为空。

队列为满的情况

由于如果“循环一圈”后,front==rear的情况会出现,此时我们分不清队列是空的还是满的,因此我们修改条件,保留一个元素空间,并认为该队列已满,如图所示
这里写图片描述
那么问题来了,front和rear的大小都不确定,机器又是如何确定队列是否已满以及队列的实际长度的呢?
通过公式(rear+1)%QueueSize == front确定队列是否已满,其中QueueSize是队列的最大尺寸
通用的计算队列的长度公式为:(rear-front+QueueSize)%QueueSize

代码

循环队列的顺序存储结构

typedef int QElemType; //QElemType类型根据实际情况而定,这里假设为int
typedef struct
{
    QElemType data[MAXSIZE];
    int front;  //头指针
    int rear;  //尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;

初始化代码

/*初始化一个空队列Q*/
Status InitQueue(SqQueue *Q)
{
    Q->front=0;
    Q->rear=0;
    return OK;
}

循环队列求队列长度

/*返回Q的元素个数,也就是队列的当前长度*/
int QueueLength(SqQueue Q)
{
    return (Q.rear-Q.front+MAXSIZE)%MAXSIZE
}

循环队列的入队列

/*若队列未满,则插入元素e为Q新的队尾元素*/
Status EnQueue(SqQueue *Q,QElemType e)
{
    if((Q->rear+1)%MAXSIZE == Q->front)  //队列满的判断
        return ERROR;
    Q->data[Q->rear]=e;  //将元素e赋值给队尾
    Q->rear=(Q->rear+1)%MAXSIZE;  //rear指针向后移一位置
                                  //若到最后则转向数组头部
    return OK;
}

NOTE:但是顺序存储,若不是循环队列,算法的时间性能是不高的,但循环队列有面临着数组可能会溢出的问题,所以我们还需要研究一下不需要担心队列长度的链式存储结构

队列的链式存储结构

这里写图片描述

typedef int QElemType;
typedef struct QNode  //结点结构
{
    QElemType data;
    struct QNode *next;
}QNode,*QueuePtr;

typedef struct  //队列的链表结构
{
    QueuePtr front,rear;  //队头、队尾指针
}LinkQueue;

入队操作

这里写图片描述

/* 插入元素e为Q的新的队尾元素 */
Status EnQueue(LinkQueue *Q,QElemType e)
{
    QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
    if(!s)  //存储分配失败
        exit(OVERFLOW)
    s->data=e;
    s->next=NULL;
    Q->rear->next=s;  //把拥有元素e新结点s赋值给原队尾结点的后继
    Q->rear=s;  //把当前的s设置为队尾结点,rear指向s
    return OK;
}

出队操作

这里写图片描述

/* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */
Status DeQueue(LinkQueue *Q,QElemType *e)
{
    QueuePtr p;
    if(Q->front==Q->rear)
        return ERROR;
    p=Q->front->next;  //将欲删除的队头结点暂存给p
    *e=p->data;  //将欲删除的队头结点的值赋值给e
    Q->front->next=p->next;  //将原队头结点后继p->next赋值给头结点后继
    if(Q->rear==p)
        Q->rear=Q->front;
    free(p);
    return OK;
}

总结

栈是一种后进先出的数据结构,队列是一种先进先出的数据结构。
本文图文参考程杰《大话数据结构》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值