《大话数据结构》学习记录4——栈与队列

四、栈与队列

栈是限定仅在表尾进行插入和删除操作的线性表。
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。

(一)栈

1、基本介绍

栈( s t a c k stack stack )是限定仅在表尾进行插入和删除操作的线性表。

  • 允许插入和删除的一端称为栈顶( t o p top top), 另一端称为栈底( b o t t o m bottom bottom), 含任何数据元素的栈称为空栈。栈又称为后进先出( L a s t I n F i r s t O u LastIn First Ou LastInFirstOut)的线性表,简称 L I F O LIFO LIFO 结构
  • 首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。
  • 它的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
  • 栈的插入操作,叫作进栈,也称压栈、入栈。
  • 栈的删除操作,叫作出栈,也有的叫作弹栈。
  • 栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。

2、栈的基本操作

同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。

  • InitStack ( *S):初始化操作. 建立一个空栈 S S S
  • DestroyStack ( *S ):若栈存在,则销毁它。
  • ClearStack ( *S) :将栈清空。
  • StackEmpty ( S ):若栈为空,返回 t r u e true true, 否则返回 f a l s e false false
  • GetTop(S,*e):若栈存在且非空,用 e e e 返回 S S S 的枝顶元素。
  • Push(*S,e):若栈S 存在,插入新元素 e e e 到栈 S S S 中并成为枝项元素。
  • Pop(*S,*e ):删除栈 S S S 中栈顶元素,井用 e e e 返回其值。
  • StackLength (S):返回栈 S S S 的元素个数。

3、栈的顺序存储结构及实现

  • 顺序存储:既然栈是线性表的特例,那么栈的顺序存储其实也是线性表顺序存储的简化,我们简称为顺序栈。线性表是用数组来实现的,可以用数组下标为 0 0 0 的一端作为栈底比较好,因为首元素都存在栈底,变化最小,所以让它作栈底。

  • 栈的长度:若存储栈的长度为 S t a c k S i z e StackSize StackSize, 则栈顶位置 t o p top top 必须小于 S t a c k S i z e StackSize StackSize。当栈存在一个元素时, t o p top top 等于 0 0 0, 因此通常把空栈的判定条件定为 t o p top top 等于 − 1 -1 1

    • 在这里插入图片描述
  • 栈的结构定义:

typedef int SElemType; /* SElemType 类型根据实际情况而定,这里假设为 int */
typedef struct
{
    SElemType data[MAXSIZE];
    int top; /* 用于枝顶指针 */
}SqStack;
(1)进栈操作

在这里插入图片描述

Status Push ( SqStack *S,SElemType e)
{
    if(S->top==MAXSIZE-1)   /*栈满*/
    {
        return ERROR;
    }
    S->top++;/*栈顶指针增加一*/
    S->data[S->top]=e;/*将新插入元素赋值给栈顶空间*/
    return OK;
}
(2)出栈操作
Status Pop( SqStack *S, SElemType *e)
{
    if(S->top == -1)
        .return ERROR;
    *e=S->data[S->top]; /* 将要删除的栈顶元素赋值给 e */
    S->top--; /* 栈顶指针减一 */
    return OK;
}

4、两栈共享空间

  • 优点:顺序存储还是很方便的,因为它只准栈顶进出元素,所以不存在线性表插入和删除时需要移动元素的问题。
  • 缺点:必须事先确定数组存储空间大小,万一不够用了,就需要编程手段来扩展数组的容量,非常麻烦。
    • 特殊情况解决方法:如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,极有可能是第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多存储空间空闲。————可以用一个数组来存储两个栈。
      • 数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为 0 0 0 处;另一个栈的栈底为数组的末端,即下标为数组长度 n − 1 n-1 n1 处。这样,两个栈如果增加元素,就是两端点向中间延伸。

      • 在这里插入图片描述

      • 们是在数组的两端,向中间靠拢。 t o p 1 top1 top1 t o p 2 top2 top2 是栈1 和栈 2的栈顶指针,可以想象,只要它们俩不见面,两个栈就可以一直使用。两个栈见面之时,也就是两个指针之间相差 1 时,即 t o p 1 + 1 = = t o p 2 top1 + 1 == top2 top1+1==top2 为栈满。

      • /* 两栈共享空间结构 */
        typedef struct
        {
            SElemType data[MAXSIZE];
            int top1; /* 栈 1 栈顶指针 */
            int top2; /* 栈 2 栈顶指针 */
        }SqDoubleStack;
        
        /* 插入元素 e 为新的栈顶元素 */
        Status Push ( SqDoubleStack *S, SElemType e, int stackNumber)
        {
            if (S->top1+1==S->top2)/* 栈已满,不能再 push 断元素了 */
                return ERROR;
            if ( stackNumber==l ) /* 栈1 有元素进栈 */
                S->data [++S->topl] = e; /* 若栈1 则先 topi+1 后给数组元素鼠值 */
            else if ( stackNumber==2 ) /* 栈 2 有元素进枝 */
                S->data [--S->top2]=e; /* 若栈2 则先 top2-1 后给数组元素赋值 */
            return OK;
        }
        
        /* 若栈不空,则删除 S 的栈顶元素,用 e 返回其值,并返回 0K; 否则返回 ERROR */
        Status Pop ( SqDoubleStack *S, SElemType *e, int stackNumber )
        {
            if ( stackNumber==l )
            {
                if ( S->toplsaas-1)
                    return ERROR;   /* 说明栈1 已经是空栈,溢出 */
                *e=S->data[S->top1--]; /* 将栈1 的栈顶元素出栈*/
            }
            else if ( stackNumber==2 )
            {
                if ( S->top2=MAXSIZE )
                    return ERROR;       /* 说明栈 2 已经是空栈,溢出 */
                *e=S->data [S->top2++]; /* 将栈 2 的栈顶元素出栈 */
            }
            return OK;
        }
        
      • 使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。

5、栈的链式存储结构及实现

  • 栈的链式存储结构,简称为链栈。把栈顶放在单链表的头部,对于链栈来说,基本不存在栈满的情况
    • 在这里插入图片描述

    • typedef struct StackNode
      {
          SElemType data;
          struct StackNode *next;
      }StackNode,*LinkStackPtr;
      
      typedef struct LinkStack
      {
          LinkStackPtr top;
          int count;
      }LinkStack;
      
(1)进栈操作

假设元素值为 e e e 的新结点是 s s s, t o p top top 为栈顶指针

  • 在这里插入图片描述
  • /* 插入元素 e 为新的栈顶元素 */
    Status Push ( LinkStack *S, SElemType e)
    {
        LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
        s->data=e;
        s->next = S->top;/* 把国前的栈顶元素鼠值给新结点的直接后继,如图中① */
        S->top=s;       /* 将新的结点 s 赋值给校顶指针,如图中② */
        S->count++;
        return OK;
    }   
    
(2)出栈操作

变量 p p p 用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放 p p p 即可

  • 在这里插入图片描述

  • /* 若栈不空,则删除 S 的栈顶元素,用 e 返回其值,并返回0K; 否则返回 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;
    }
    
  • 如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些

6、栈的作用

栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。

7、栈的应用——递归

(1)斐波那契数列

前面相邻两项之和,构成了后一项
在这里插入图片描述

  • 打印
    •  /*常规迭代法*/
      int main()
      {
         int i;
         int a[40];
         a[0]=0;
         printf("%d ",a[0]);
         printf("%d ",a[1]);
         for(i = 2;i < 40;i++)
         {
            a[i] =a[i-1] + a[i-2];
            printf( "%d" ,a[i]);
         }
         return 0;
      }
      
    •  /*递归法*/
       int Fbi( int i)
       {
           if( i < 2 )
               return i == 0 ? 0 : 1; //三元运算符的使用。如果 i 等于 0,表达式的值将是 0,否则将是 1。
           return Fbi(i-1)+ Fbi(i-2);  /*这里 Fbi 就是函数自己. 它在调用自己 */
       }
      
       int main ( )
       {
           int i;
           for(int i = 0;i < 40;i++)
               printf(%d ”, Fbi(i));
           return 0;
       }
      
    • 迭代使用的是循环结构,递归使用的是选择结构。递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。
(2)递归定义

一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。
和栈的关系:在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。

8、栈的应用——四则运算表达式求值

(1)后缀( 逆波兰 )表示法定义
  • 括号运算和栈:括号都是成对出现的,有左括号就一定会有右括号,对于多重括号,最终也是完全嵌套匹配的。这用栈结构正好合适,只有碰到左括号,就将此左括号进栈,不管表达式有多少重括号,反正遇到左括号就进栈,而后面出现右括号时,就让栈顶的左括号出栈,期间让数字运算,这样,最终有括号的表达式从左到右巡查一遍,栈应该是由空到有元素,最终再因全部匹配成功后成为空栈的结果。
  • 不需要括号的后缀表达法:(逆波兰)
    • 后缀表达式:9 + (3-1) × 3 + 10 + 2 ——> 9 3 1-3 * + 10 2/+
    • 所有的符号都是在要运算数字的后面出现
    • 从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果
(2)中缀表达式转后缀表达式
  • 标准四则运算表达式: “ 9 + ( 3 − 1 ) × 3 + 10 + 2 ” “9+ (3-1) \times3+10+2” “9+(31)×3+10+2” 叫做中缀表达式。
    • 从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号 (乘除优先加减) 则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
    • 想让计算机具有处理我们通常的标准(中缀)表达式的能力,最重要的就是两步:
        1. 将中缀表达式转化为后缀表达式(栈用来进出运算的符号)
        1. 将后缀表达式进行运算得出结果(栈用来进出运算的数字)

(二)队列

1、基本介绍

队列 ( q u e u e queue queue) 是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

  • 队列是一种先进先出 ( F i r s t I n F i r s t O u t FirstIn First Out FirstInFirstOut) 的线性表,简称 F I F O FIFO FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
  • 在这里插入图片描述

2、队列操作

  • InitQueue ( *Q ):初始化操作,建立一个空队列 Q Q Q
  • DestroyQueue( *Q):若队列 Q Q Q 存在,则销毁它
  • ClearQueue( *Q ):将队列 Q Q Q 清空
  • QueueEmpty ( Q):若队列 Q Q Q 为空,返回 t r u e true true, 否则返回 f a l s e false false
  • GetHead ( Q,*e ):若队列 Q Q Q 存在且非空,用 e e e 返回队列 Q Q Q 的队头元素。
  • EnQueue ( *Q,e):若队列 Q Q Q 存在,插入新元素 e e e 到队列 Q Q Q 中并成为队尾元素。
  • DeQueue( *Q,*e ):删除队列 Q Q Q 中队头元素,并用 e e e 返回其值。
  • QueueLength ( Q ):返回队列 Q Q Q 的元素个数

3、循环队列

(1)队列顺序存储的不足

一个队列有 n n n 个元素,则顺序存储的队列需建立一个大于 n n n 的数组,并把队列的所有元素存储在数组的前 n n n 个单元,数组下标为 0 0 0 的一端即是队头。

  • 入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素

    • 在这里插入图片描述
  • 出列是在队头,即下标为 0 0 0 的位置,那也就意味着,队列中的所有元素都得向前移动

    • 在这里插入图片描述
  • 若出列不移动,不去限制队列的元素必须存储在数组的前 n n n 个单元这一条件,出队的性能就会大大增加

    • 在这里插入图片描述

    • 为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针, f r o n t front front 指针指向队头元素, r e a r rear rear 指针指向队尾元素的下一个位置,这样当 f r o n t front front 等于 r e a r rear rear 时,此队列不是还剩一个元素,而是空队列。

    • 数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,我们的队列在前面删除过的地方还是空闲的。我们把这种现象叫做“假溢出”。

    • 解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。

(2)循环队列介绍

队列的头尾相接的顺序存储结构称为循环队列。
在这里插入图片描述

  • 问题又出来了,刚才说,空队列时, f r o n t front front 等于 r e a r rear rear, 现在当队列满时,也是 f r o n t front front 等于 r e a r rear rear, 那么如何判断此时的队列究竟是空还是满呢?
  • 办法一是设置一个标志变量 f l a g flag flag, 当 f r o n t = = r e a r front == rear front==rear, 且 f l a g = 0 flag = 0 flag=0 时为队列空,当 f r o n t = = r e a r front == rear front==rear, 且 f l a g = 1 flag = 1 flag=1 时为队列满。
  • 办法二是当队列空时,条件就是 f r o n t = = r e a r front == rear front==rear, 当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。
    • 在这里插入图片描述

    • 由于 r e a r rear rear 可能比 f r o n t front front 大,也可能比 f r o n t front front 小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。队列的最大尺寸为 Q u e u e S i z e QueueSize QueueSize, 那么队列满的条件是 (rear+1)%QueueSize == front(取模 “%” 的目的就是为了整合 r e a r rear rear f r o n t front front 大小为一个问题)

代码

    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+l)%MAXSIZE == Q->front)/* 队列满的判断 */
            return ERROR;
        Q->data[Q->rear]=e; /* 将元素 e 赋值给队尾 */
        Q->rear= ( Q->rear+l)%MAXSIZE;  /*rear 指针向后移一位置*/
                                        /*若到最后则转到数组头部 */
        return OK;
    }

    /* 若队列不空,则列除 Q 中队头元去. 用 e 返回其值 */
    Status DeQueue ( SqQueue *Q,QElemType *e)
    {
        if ( Q->front== Q->rear) /* 队列空的判斯 */
            return ERROR;
        *e=Q->data[Q->front]; /* 将队头元素赋值给 e */
        Q->front=(Q->front+l)%MAXSIZE;  /* front 指针向后移一位五. */
                                        /*若到最后则转到数组头部 */
        return OK;
    }

4、队列的链式存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。

  • 队头指针指向链队列的头结点,而队尾指针指向终端结点

    • 在这里插入图片描述
  • 空队列时, f r o n t front front r e a r rear rear 都指向头结点

    • 在这里插入图片描述
typedef int QElemType; /* QElemType 类型根据实际情况而定,这里假设为 int */
typedef struct QNode /* 结点结构 */
{
    QElemType data;
    struct QNode *next;
}QNode,*QueuePtr;

typedef struct /* 队列的链表结构 */
{
    QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;
(1)队列的链式存储结构一入队操作

入队操作时,其实就是在链表尾部插入结点
在这里插入图片描述

/* 插入元素 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->nextss=s;  /* 把拥有元素 e 新站点 s 赋值给原队尾站点的后继. */
                        /*见上图中① */
    Q->rear=s;          /* 把当前的 s 设置为队尾结点,rear 指向 s, 见上图中② */
    return OK;
}
(2)队列的链式存储结构一出队操作

出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将 r e a r rear rear 指向头结点
在这里插入图片描述

/* 若队列不空,热除 Q 的队头元素,用 e 返回其值,并返回 0K, 否则返回 ERROR */
Status DeQueue(LinkQueue *Q, QElemType *e)
{
    QueuePtr p;
    if(Q->front==Q->rear)
        return ERROR;
    p=Q->front->next;   /* 将欲都除的队头结点暂存给 p, 见上图中① */
    *e=p->data;         /* 将欲翻除的队头结点的值k值给 e */
    Q->front->next=p->next; /*将原队头结点后继 p->next 赋值给头结点后继,*/
                            /*见上图中*/
    if ( Q->rear=-p)        /*若队头是队尾,则删除后将 rear 指向头结点, 见上图中③ */
         Q->rear=Q->front;
    free ( p );
    return OK;
}

5、对比

循环队列与链队列的比较,可以从两方面来考虑

  • 从时间上,其实它们的基本操作都是常数时间,即都为 O ( 1 ) O(1) O(1)的。不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
  • 对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。

总的来说,在可以确定队列长度最失值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

frozendure

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值