第 4 章 栈与队列

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

4.1 开场白

4.2 栈的定义

4.2.1 栈的定义

栈是限定仅在表尾进行插入插入和删除操作的线性表
  允许插入和删除的一端称为栈顶(top),另一端称为栈底(button)。不含任何数据元素的栈称为空栈,栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
  栈是一个特殊的线性表,特殊之处在于限制了插入和删除的位置,它始终只在栈顶进行,这也就使得:栈底是固定的,最先进栈的只能在栈底。
  栈的插入操作:叫做进栈,也称压栈、入栈;删除操作:叫做出栈,也叫做弹栈。
  

4.2.2 进栈出栈变化形式

最先进栈的元素,不一定是最后出栈。在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以了。

4.3 栈的抽象数据类型

ADT 栈 (stack)
Data
    同线性表。元素具有相同的类型,相邻元素具有前驱和后继的关系。
Operation
    InitStack(*S):初始化操作,建立一个空栈;
    DestroyStack(*S):若栈存在,则销毁它;
    ClearStack(*S):将栈清空;
    StackEmpty(S):若栈为空,返回TRUE,否则返回false;
    GetTop(S, *e):若栈存在且非空,用e 返回S的栈顶元素;
    Push(*S,e):若栈存在,插入新元素e到S中并成为栈顶元素;
    Pop(*S,e):删除栈S中栈顶元素,并用e返回其值;
    StackLength(S):返回栈S中的元素个数。

4.4 栈的顺序存储结构及实现

4.4.1 栈的顺序存储结构

简称为顺序栈。数组实现,下标为0的一端作为栈底。
栈的结构定义

#define MAXSIZE 20;
typedef int SElemType;
typedef struct{
    SElemType   data[MAXSIZE];
    int         top; //用于栈顶指针;
}SqStack;

4.4.2 栈的顺序存储结构——进栈操作

//插入元素e为新的栈顶元素
Status Push(SqStack *S, SElemType e)
{
    if (S->top == MAXSIZE - 1)
        return ERROR;
    S->top++;
    S->data[S->top] = e;
    return OK;
}

4.4.3 栈的顺序存储结构——出栈操作

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
Status Pop(SqStack *S, SElemType *e)
{
    if (S->top == -1)
        return ERROR;
    *e = S->data[S->top];
    S->top--;
    return OK;
}

两者的时间复杂度都是O(1),由于不存在线性表的插入和删除需要移动元素问题, 栈的顺序存储结构还是很方便的。但是有一个很大的缺陷:必须事先确定数组存储空间的大小。

4.5 两栈共享空间

两个相同类型的栈,可以最大限度第利用其事先开辟的存储空间来进行操作。关键思路:它们是在数组的两端,向中间靠拢。
空间结构代码:

//两栈共享空间结构
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)
        return ERROR;
    if (stackNumber == 1)
        S->data[++S->top1] = e;
    else if (stackNumber == 2)
        S->data[++S->top2] = e;
    return OK;
}

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
    if (stackNumber == 1)
    {
        if (S->top == -1)
            return ERROR;
        *e = S->data[S->top1--];
    }
    else if (stackNumber == 2)
    {
        if (S->top2 == MAXSIZE)
            return ERROR;
        *e = S->data[S->top2++];
    }
    return OK;
}

两栈共享空间,适用于两个栈的空间需求具有相反关系时,即一个栈增长时另一个栈在缩短的情况。

4.6 栈的链式存储结构及实现

4.6.1 栈的链式存储结构

简称为链栈。栈顶放在单链表的头部,对于链栈来说,不需要头指针的,用栈顶代替。
链栈的结构

//链栈的结构代码如下:
typedef struct StackNode
{
    SElemType   data;
    struct StackNode * next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack
{
    LinkStackPtr top;
    int count;
}LinkStack;

4.6.2 栈的链式存储结构——进栈操作

//插入元素e为新的栈顶元素
Status Push(LinkStack *S, SElemType e)
{
    LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
    s->data = e;
    s->next = S->top;
    S->top = s;
    S-count++;
    return OK;
}

4.6.3 栈的链式存储结构——出栈操作

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;
Status Pop(LinkStack *S, SElemType *e)
{
    LinkStackPtr p;
    if (StackEmpty(*S))
        return ERROR;
    *e = S->top->data;
    p = S->top;
    S->top = S->top->next;
    free(p);
    S->count--;
    return OK;
}

时间复杂度都为O(1)。
对比顺序栈和链栈:

  • 时间复杂度都为O(1);
  • 顺序栈需要事先确定一个固定的长度,存在空间浪费,但优势是存取定位很方便。链栈则要求每个元素都有指针,增加了内存开销,但对于栈的长度无限制
  • 如果栈的使用过程中元素变化不可预料,最后有链栈;反之,它的变化在可控范围内,建议使用顺序栈。

4.7 栈的使用

栈的引入简化了程序设计的问题,划分了不同的关注层次,使得思考范围缩小,更加聚焦与我们要解决的问题核心。

4.8 栈的应用——递归

4.8.1 斐波那契数列实现

f(n)=0,1,F(n1)+F(n2), n = 0 n = 1 n>1

打印出前40位斐波那契数列

int main()
{
    int i;
    int a[40];
    a[0] = 0;
    a[1] = 1;
    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 main()
{
    int i;
    int a[40];
    a[0] = 0;
    a[1] = 1;
    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;
}

4.8.2 递归定义

把一个直接调用自己或通过一系列的调用语句间接调用自己的函数,称做递归函数。
每个递归定义必须至少有一个条件,满足时递归不再进行,即不在引用自身而是返回值退出。
迭代使用循环结构,递归使用的是选择结构。递归能使程序的结构更清晰、简单,但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代不需要反复调用函数和占用额外的内存。应该视情况不同选择不同的代码。
编译器使用栈实现递归。

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

4.9.1 后缀(逆波兰)表示法定义

不需要括号的后缀表示法,称为逆波兰表示。是表达式的一种新的显示方式,非常巧妙地解决了程序实现四则运算的难题。
对于“9+(3-1)*3+10 / 2”,用后缀表示法式“ 9 3 1- 3* + 10 2/ +”

4.9.2 后缀表达式计算结果

用后缀表示法式“ 9 3 1- 3* + 10 2/ +”
规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
下面推导如何让9+(3-1)*3+10 / 2”,用后缀表示法式“ 9 3 1- 3* + 10 2/ +”

4.9.3中缀表达式转后缀表达式

平时用的标准四则运算表达式称中缀表达式。
中缀表达式9+(3-1)*3+10 / 2”,转化为后缀表达式“ 9 3 1- 3* + 10 2/ +”
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
要想让计算机具有处理标准表达式的能力:

  1. 将中缀表达式转化为后缀表达式;
  2. 将后缀表达式进行运算得出结果。

4.10 队列的定义

队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端是队尾,允许删除的一端称为队头

4.11 队列的抽象数据类型

ADT 栈 (Queue)
Data
    同线性表。元素具有相同的类型,相邻元素具有前驱和后继的关系。
Operation
    InitQueue(*Q):初始化操作,建立一个空队列Q;
    DestroyQueue(*Q):若队列存在,则销毁它;
    ClearQueue(*Q):将队列清空;
    QueueEmpty(Q):若为空,返回TRUE,否则返回false;
    GetHead(Q, *e):若队里存在且非空,用e 返回队列的队头元素;
    EnQueue(*Q,e):若队列存在,插入新元素e到Q中并成为队尾元素;
    DeQueue(*Q*e):删除队列中队头元素,并用e返回其值;
    QueueLength(Q):返回队列中的元素个数。

4.12 循环队列

4.12.1 队列顺序存储的不足

插入队尾时间为O(1),删除队头时间为O(n)。

4.12.2 循环队列定义

把队列的这种头尾相接的顺序存储结构称为循环队列。
引入两个指针front指针指向队头元素,rear指向队尾元素的下一个位置。若队列的最大尺寸为QueueSize,则队列慢的条件为(rear+1)%QueueSize == front
通用的计算队列长度公式为:
(rear-front + QueueSize) % QueueSize

typedef int QElemType;
typedef struct
{
    QElemType   data[MAXSIZE];
    int         front;  //头指针
    int         rear;   //尾指针,指向队尾元素的下一个位置
}SqQueue;

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

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

//插入元素e为新的队尾元素
Status EnQueue(SqQueue *Q, QElemType e)
{
    if ((Q->rear + 1) % MAXSIZE == Q->front)
        return ERROR;
    Q->data[Q->rear] = e;
    Q->rear = (Q->rear+1) %MAXSIZE;

    return OK;
}

//若队列不空,则删除Q中队头元素,用e返回其值
Status DeQueue(SqQueue *Q, QElemType *e)
{
    if (Q->front == Q->rear)
        return ERRROR;
    *e = Q->data[Q->front];
    Q->front = (Q->front + 1) % MAXSIZE;

    return OK;
}

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

就是线性表的单链表,只不过它只能尾进头出而已, 简称为链队列。
链队列的结构为:

typedef int QElemType;
typedef struct QNode{
    QElemType       data;
    struct QNode    *next;
} QNode, *QueuePtr;

typedef struct
{
    QueuePtr front, rear;
}LinkQueue;

4.13.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->next = s;
    Q->rear =s;
    return OK;
}

4.13.2 队列的链式存储结构——出队操作

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

对于循环队列与链队列的比较,从时间上,它们的时间都为O(1)。空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题,而链队不存在这个问题,所以,链队列更加灵活。

4.14 总结回顾

4.15 结尾语

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值