• 数据结构之栈与队列


    概要

    参考《大话数据结构》,把常用的基本数据结构梳理一下。

    本节介绍栈与队列。

     


       栈:栈是限定仅在表尾进行插入和删除操作的线性表

    我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last in First Out)的线性表,简称 LIFO 结构。

    栈的插入操作,叫作进栈,也称压栈、入栈。类似子弹入弹夹。

    栈的删除操作,叫做出栈,也有的叫作弹栈。如同弹夹中的子弹出夹。

     

    栈的顺序存储结构

     
    既然栈是线性表的特例,那么栈的顺序存储其实也就是线性表顺序存储的简化,我们简称为顺序栈。

    下面我们直接利用完整的代码解释相关操作。

    #include<iostream>
    
    using namespace std;
    
    typedef int SElemType; // SElwmType 类型根据实际情况而定,这里假设为 int
    const int MAXSIZE = 5;
    
    //栈的顺序存储结构
    struct SqStack
    {
        SElemType data[MAXSIZE];
        int top;  //用于栈顶指针,当存在一个元素时,top = 1,通常把空栈的判定条件定为 top = -1
    };
    
    //初始化一个空栈
    void InitStack(SqStack* S )
    {
        S->top = -1;
    }
    
    //进栈操作
    void EnStack(SqStack* S, SElemType e)
    {
        if(S->top+1 == MAXSIZE)   //栈满
            cout<<"栈已经满了,禁止插入"<<endl;
        else
        {
            S->top++;   //栈顶指针增 1
            S->data[S->top] = e;  //将元素 e 赋值给栈顶
        }
    }
    
    //出栈操作
    //若栈不空,则删除 S 中栈顶元素,用 e 返回其值
    void DeStack(SqStack* S, SElemType *e)
    {
        if(S->top == -1)   //栈为空的判断
            cout<<"栈为空"<<endl;
        else
        {
            *e = S->data[S->top];  //将栈顶元素赋值给 e
            S->top--; // 栈顶指针减 1
        }
    }
    
    int main()
    {
        SqStack* myS = new SqStack;  // 不加动态内存会运行错误
        InitStack(myS);   //初始化空栈
    
        SElemType e = 3;
        EnStack(myS, e);   //入栈操作
        cout<<myS->data[myS->top]<<endl;  //输出 3
    
        SElemType *f = new SElemType;
        DeStack(myS, f);
        cout<<*f<<endl; //输出 3
    
        return 0;
    }
    

    进栈入栈操作没有涉及到任何循环语句,因此时间复杂度均是 (O(1)).
     

    栈的链式存储结构

     
    栈的链式存储结构简称链栈

    栈只是栈顶来做插入和删除操作,栈顶放在链表的头部还是尾部呢?由于单链表有头指针,而栈顶指针也是必须的,那干吗不让它合二为一呢,所以比较好的办法是把栈顶放在单链表的头部。另外,都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的。

    对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间。但对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是 top=NILL 的时候。

    下面我们直接利用完整的代码解释相关操作。

    #include<iostream>
    
    using namespace std;
    
    typedef int SElemType; // SElwmType 类型根据实际情况而定,这里假设为 int
    //const int MAXSIZE = 5;
    
    //栈的链式存储结构
    struct StackNode
    {
        SElemType data;
        StackNode* next;  //从栈顶的方向指向栈底
    };
    
    struct LinkStack
    {
        StackNode* top;  //用于栈顶指针
        int counts;  // 记录栈中元素个数
    };
    
    //初始化一个空栈
    void InitStack(LinkStack* S )
    {
        S->counts = 0;
    }
    
    //进栈操作
    void EnStack(LinkStack* S, SElemType e)
    {
        StackNode* s = new StackNode;
        s->data = e;
        s->next = S->top;  //把当前的栈顶元素赋值给新结点的直接后继
    
        S->counts++;   //栈顶指针增 1
        S->top = s;  //将新的结点 s 赋值给栈顶指针
    
    }
    
    //出栈操作
    //若栈不空,则删除 S 中栈顶元素,用 e 返回其值
    void DeStack(LinkStack* S, SElemType *e)
    {
        if(S->counts == 0)   //栈为空的判断
            cout<<"栈为空"<<endl;
        else
        {
            *e = S->top->data;  //将栈顶元素赋值给 e
            S->top = S->top->next; // 栈顶指针减 1
            S->counts--;  //元素数减一
        }
    }
    
    int main()
    {
        LinkStack* myS = new LinkStack;  // 不加动态内存会运行错误
        InitStack(myS);   //初始化空栈
    
        SElemType e = 3;
        EnStack(myS, e);   //入栈操作
        cout<<myS->top->data<<endl;  //输出 3
    
        SElemType *f = new SElemType;
        DeStack(myS, f);
        cout<<*f<<endl; //输出 3
    
        return 0;
    }
    

    进栈入栈操作没有涉及到任何循环语句,因此时间复杂度均是 (O(1)). 如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈更好一些

     

    栈的应用---递归

     
    栈有一个很重要的应用:有程序设计语言中实现了递归。

    用递归实现斐波那契数列。代码如下

    int Fbi(int i)
    {
        if(i == 0 || i == 1)
            return i;
    
        if (i > 1)
        {
            return Fbi(i-1)+Fbi(i-2);   //调用自己
        }
    }
    int main()
    {
        cout<< Fbi(5);
        return 0;
    }
    

    为了理解,我们可以不要把一个递归函数中调用自己的函数看作是在调用自己,而就当它是在调用“另一个”函数,只不过,这个函数和自己长得一样而已。我们来模拟代码中 Fbi(i) 函数当 i=5 的执行过程,如下图

    前面我们看到递归是如何执行它的前行和退回阶段的。递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复前行过程中存储起来的某些数据,这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然 很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。简单地说,就是在前行阶段,寻每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。

     


    队列

       队列:是只允许在一端进行插入操作,而在另一端进行删除操作的线性表

    我们把允许插入的一端称为队尾,允许删除的一端称为队头。队列是一种先进先出(First in First Out)的线性表,简称 FIFO 结构。
     

    队列的顺序存储结构及实现

     

    我们假设一个队列按数组的形式存储,当队列进行插入和删除操作时,队列中的所有元素都得向前移动,以保证队列的队头。也就是下标为 0 的位置不为空,此时时间复杂度为 (O(n)),这像现实中大家排队买票,前面的人买好了离开,后面的人就是全部向前一步,补上空位,似乎没什么不好。可想想,如果不去限制队列的元素必须存储在数组的前 (n) 个单元这一条件,出队的性能就会大大增加。也就是说,队头不需要一定在下标为 0 的位置。
     
    为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针, front 指针指向队头元素,rear 指针指向队尾元素的下一个位置,这样当 front 等于 rear 时,此队列不是还剩一个元素,而是空队列。
     
    队列的整体移动会面临一个问题,就是有可能跑到数组之外。假设一个队列的总个数不超过 5 个,但目前总共 3 个元素已经移动到数组末尾,再进行插入操作的话,就会产生数组越界的错误,可实际上,队列在下标为 0 和 1 的地方还是空闲的。这种现象叫做“假溢出”。 为了解决这个问题,就再从头开始,也就是头尾相接的循环。我们把队列的头尾相接的顺序存储结构称为循环队列
     
    下面我们直接利用完整的代码解释相关操作。

    #include<iostream>
    
    using namespace std;  
    
    typedef int QElemType; // QElwmType 类型根据实际情况而定,这里假设为 int
    const int MAXSIZE = 5;
    
    //循环队列的顺序存储结构
    struct SqQueue
    {
        QElemType data[MAXSIZE];
        int fro;  //头指针
        int rear;   //尾指针,若队列不空,指向队尾元素的下一个位置
    };
    
    //初始化一个空队列
    void InitQueue(SqQueue* Q )
    {
        Q->fro = 0;
        Q->rear = 0;
    }
    
    //循环队列求队列长度代码如下
    int QueueLength(SqQueue Q)
    {
        return(Q.rear - Q.fro + MAXSIZE) % MAXSIZE;
    }
    
    //循环队列的入队操作
    void EnQueue(SqQueue* Q, QElemType e)
    {
        //为了避免当 rear = fon 时,不知道队列是空还是满,故总留一个空位置,所以下面判断有 +1
        if((Q->rear+1)%MAXSIZE == Q->fro)   //队列满的判断
            cout<<"队列已经满了,禁止插入"<<endl;
        else
        {
            Q->data[Q->rear] = e;  //将元素 e 赋值给队尾
            Q->rear = (Q->rear+1) % MAXSIZE;  // rear 指针后移一个位置,若到最后则转到数组头部
        }
    }
    
    //循环队列的出队操作
    //若队列不空,则删除 Q 中队头元素,用 e 返回其值
    void DeQueue(SqQueue* Q, QElemType *e)
    {
        if(Q->fro == Q->rear)   //队列空的判断
            cout<<"队列为空"<<endl;
        else
        {
            *e = Q->data[Q->fro];  //将队头元素赋值给 e
            Q->fro = (Q->fro+1) % MAXSIZE; // fro 指针向后移动一位,若到最后则转到数组头部
        }
    }
    
    int main()
    {
        SqQueue* myQ = new SqQueue;  // 不加动态内存会运行错误
        InitQueue(myQ);   //初始化队列
    
        QElemType e = 3;
        EnQueue(myQ, e);   //入队操作
        cout<<myQ->data[myQ->fro]<<endl;  //输出 3
    
        QElemType *f = new QElemType;
        DeQueue(myQ, f);
        cout<<*f<<endl; //输出 3
    
        return 0;
    }
    
    

    下面研究不需要担心队列长度的链式存储结构。

     

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

     
    队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。
     
    下面我们直接利用完整的代码解释相关操作。

    #include<iostream>
    
    using namespace std;
    
    typedef int QElemType; // SElwmType 类型根据实际情况而定,这里假设为 int
    //const int MAXSIZE = 5;
    
    //队的链式存储结构
    struct QueueNode
    {
        QElemType data;
        QueueNode* next;  //指向队尾的方向
    };
    
    struct LinkQueue
    {
        QueueNode* fro;  //指向队头
        QueueNode* rear;  //指向队尾
        //int counts;  // 记录队中元素个数
    };
    
    void InitQueue(LinkQueue *Q)     //构造一个空的队列
    {
        QueueNode *q = new QueueNode;   //申请一个结点的空间
        q->next = nullptr;   //当作头结点
        //队首与队尾指针都指向这个结点,指针域为NULL
        Q->rear = q;
        Q->fro = Q->rear;
    }
    //进队操作
    void EnQueue(LinkQueue* Q, QElemType e)
    {
    
        QueueNode* q = new QueueNode;
        q->data = e;
        q->next = nullptr;  //最后一个元素指向空指针
    
        Q->rear->next = q;   //把原先队尾指针指向新添加的节点
        Q->rear = q;  //把当前的 s 设置为队尾
    }
    
    //出队操作
    //若队不空,则删除 Q 中栈顶元素,用 e 返回其值
    void DeQueue(LinkQueue* Q, QElemType *e)
    {
        if(Q->fro == Q->rear)
            cout<<"空队"<<endl;
        else
        {
            *e = Q->fro->next->data;  //将队头元素赋值给 e.  Q->fro 是头结点,但是不存数据,所以得有 next
            Q->fro = Q->fro->next; //队头指针减 1
        }
    }
    
    int main()
    {
        LinkQueue* myQ = new LinkQueue;  // 不加动态内存会运行错误
        InitQueue(myQ);
        QElemType e = 3;
        EnQueue(myQ, e);   //入栈操作
        cout<<myQ->rear->data<<endl;  //输出 3
    
        QElemType *f = new QElemType;
        DeQueue(myQ, f);
        cout<<*f<<endl; //输出 3
    
        if(myQ->rear == myQ->fro)
            cout<<"目前是空队列"<<endl;
    
        return 0;
    }
    

     


    总结

    栈和队列都是特殊的线性表,只不过对插入和删除操作做了限制。

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

    它们都可以用线性表的顺序存储结构来实现,但都存在着顺序存储的一些弊端。对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。

    它们也都可以通过链式存储结构来实现,实现原则上与线性表基本相同。

  • 相关阅读:
    天轰穿C#教程之C#基础的学习路线
    天轰穿C#教程之大话C#
    天轰穿C#教程之#pragma介绍[原创]
    天轰穿.NET教程之第一个控制台应用程序
    .NET笔试题整理(转)
    天轰穿.NET教程之基类库
    天轰穿C#教程之C#有哪些特点?
    程序猿!?应该有哪些目标?
    浅谈编程程序员应该具备的职业素养 [转载]
    .NET访问MySQL数据库方法(转)
  • 原文地址:https://www.cnblogs.com/zhoukui/p/8583285.html
Copyright © 2020-2023  润新知