【数据结构】第03章——栈与队列,矩阵的压缩存储

知识总览

image-20230311151155273

定义

栈(Stack)是只允许在一端进行插入或删除操作的线性表。

重要术语:栈顶(允许插入和删除的一端),栈底(不允许插入和删除的一端),空栈

特点:先进入栈的元素后出栈。

示意图如下:

image-20230312125556246

基本操作

image-20230312125744651

  • 查:栈的使用场景中大多只访问栈顶元素。

常考题型:

image-20230312125837073

  • 注意:当进栈操作与出栈操作穿插进行的时候,这时候出栈的顺序会有所不同

思维导图

image-20230312130057317

顺序栈

知识总览

image-20230312130157179

基本操作
  • 顺序表的定义

    image-20230312161641996

  • 顺序表的初始化

    image-20230312162523377

    //初始化操作
    #define MaxSize 10//定义栈中元素的最大个数
    typedef struct
    {
        ElemType data[MaxSize];//静态数组存放栈中元素
        int top;//栈顶指针
    }SqStack;
    
    //初始化栈
    void InitStack(SqStack& S)
    {
        S.top=-1;//初始化栈顶指针
    }
    
    //判断栈空
    bool StackEmpty(SqStack S)
    {
        if(S.top==-1)
            return true;//栈空
        else
            return false;//不空
    }
    
    void testStack()
    {
        SqStack S;//声明一个顺序栈(分配空间)
        InitStack(S);
        //...后续操作...
    }
    
  • 进栈

    image-20230312162618156

    • 注意:前置++和后置++的区别要了解!

    • 顺序栈已满的情况

      image-20230312162815145

      #define MaxSize 10//定义栈中元素的最大个数
      typedef struct
      {
          ElemType data[MaxSize];//静态数组存放栈中元素
          int top;//栈顶指针
      }SqStack;
      
      //新元素入栈
      bool Push(SqStack& S,ElemType x)
      {
          if(S.top==MaxSize-1)//栈满,报错
              return false;
          S.top=S.top+1;//指针先加1
          S.data[S.top]=x;//新元素入栈
          return true;
      }
      
  • 出栈

    image-20230312162904769

    • 注意:这里的代码逻辑实现还是得仔细地理解清楚,有一点复杂,数据还是会残留在内存中,只是逻辑上被删除了
  • 读取栈顶元素

    • 方式1:

    image-20230312163256694

    • 注意:这里的读取栈顶元素的操作与出栈的操作有点类似,但是唯一的区别就是不要让top指针减减,另外这里的实现方式是直接让top指针指向栈顶元素

    • 方式2:

      image-20230312163612463

      image-20230312163951977

    • 注意:时刻要注意top指针是指向哪个位置的,是指向栈顶元素,还是指向栈顶元素的后面一个位

  • 代码实现:

    #include<bits/stdc++.h>
    using namespace std;
    #define MaxSize 100 //定义栈中元素的最大个数
    typedef struct SqStack{
        int data[MaxSize]; //存放栈中的元素
        int top; //栈顶指针
    }SqStack;
    
    //初始化
    void InitStack(SqStack &S){
        S.top = -1;
    }
    
    //判栈空
    bool Empty(SqStack S){
        if(S.top == -1){
            return true;
        }else{
            return false;
        }
    }
    
    //入栈
    void Push(SqStack &S, int x){
        if(S.top == MaxSize-1){
            cout<<"栈满"<<endl;
            return;
        }
        S.data[++S.top] = x;
    }
    
    //出栈
    void Pop(SqStack &S, int &x){
        if(S.top == -1){
            cout<<"栈空"<<endl;
            return;
        }
        x = S.data[S.top--];
    }
    
    //读栈顶元素
    int GetTop(SqStack S){
        if(S.top == -1){
            cout<<"栈空"<<endl;
            return -1;
        }else{
            return S.data[S.top];
        }
    }
    
    //遍历栈
    void PrintStack(SqStack S){
        while(S.top != -1){
            cout<<S.data[S.top--]<<" ";
        }
        cout<<endl;
    }
    
    //销毁栈
    void DestroyStack(SqStack &S){
        S.top = -1;
    }
    
    int main(){
        SqStack S;
        InitStack(S);
        Push(S,1);//入栈
        Push(S,2);
        Push(S,3);
        Push(S,4);
        cout<<"栈顶元素为:"<<GetTop(S)<<endl;
        cout<<"出栈顺序为:";
        PrintStack(S);
        int x;
        Pop(S,x);
        cout<<x<<"出栈"<<endl;
        cout<<"栈中剩余元素:";
        PrintStack(S);
        Pop(S,x);
        cout<<x<<"出栈"<<endl;
        cout<<"栈中剩余元素:";
        PrintStack(S);
        if(!Empty(S)){
            cout<<"当前栈不为空"<<endl;
        }else{
            cout<<"当前栈为空"<<endl;
        }
        return 0;
    }
    
共享栈
  • c定义:个栈共享同一片空间。

  • 实现逻辑如下:

    image-20230312164611847

    //共享栈的实现
    #define MaxSize 10//定义栈中元素的最大个数
    typedef struct
    {
        ElemType data[MaxSize];//静态数组存放栈中元素
        int top0;//0号栈栈顶指针
        int top1;//1号栈栈顶指针
    }ShStack;
    
    //初始化栈
    void InitStack(ShStack& S)
    {
        S.top0=-1;//初始化栈顶指针
        S.top1=MaxSize;
    }
    
    //栈满的条件
    //top0+1==top1;
    
    • 逻辑是这样的:首先在栈里面定义0号栈和1号栈,并且让0号栈的指针刚开始是-1,1号栈的指针刚开始是MaxSize,然后如果往0号栈放入元素的话,那么从下往上就是递增的,如果从上往下放入元素的话,那么从上往下就是递增的,这样就会提高内存的利用率。注意栈满的条件——top0+1==top1
  • 清空一个栈(逻辑上实现)

    逻辑上清空一个栈,其实只需要让top指针指向初始化的那个位置就可以了!

思维导图

image-20230312165521326

链栈

  • 知识总览

    image-20230312165621559

定义
  • 代码实现

    image-20230312171010405

  • 其实链栈与单链表其实是非常地相似的,链栈的栈顶就相当于是单链表的链头,只不过对链栈的操作只能够在栈顶的一端进行罢了,其它的操作与单链表基本上是一样的,至于单链表的操作,上面就有!

基本操作
#include <iostream>
 
using namespace std;
typedef int SElemType;
//链栈的存储结构,与单链表相同
typedef struct StackNode
{
    SElemType date;//数据域
    struct StackNode *next;//指针域
} StackNode,*LinkStack;
 
//链栈的初始化
int InitStack(LinkStack &S)
{
    //构造一个空栈S,栈顶指针置空
    S=NULL;
    cout<<"链栈构造成功!"<<endl;
    return 1;
 
}
 
//链栈的入栈
int Push(LinkStack &S,SElemType e)
{
    //在栈顶插入元素e
    LinkStack p=new StackNode;//生成新节点
    p->date=e;//将新节点数据域置为e
    p->next=S;//将新节点插入栈顶,也就是把栈顶的地址给新节点的指针域,使其指向他
    S=p;//修改栈顶指针为p
    cout<<"入栈成功!"<<endl;
    return 1;
}
 
//链栈的出栈
int Pop(LinkStack &S,SElemType &e)
{
    //删除S的栈顶元素,用e返回其值
    if(S==NULL)//栈空
    {
        cout<<"出栈失败,栈空!"<<endl;
        return 0;
    }
    e=S->date;//将栈顶元素赋给e
    LinkStack p=S;//用p临时保存栈顶元素空间,以备释放
    S=S->next;//修改栈顶指针
    delete p;//释放原栈顶元素空间
    cout<<"出栈成功!"<<endl;
    return e;
}
 
//取栈顶元素
int GetTop(LinkStack S)
{
    //返回S的栈顶元素,不修改栈顶指针
    if(S!=NULL)
    {
        cout<<"取栈顶元素成功!"<<endl;
        return S->date;//返回栈顶元素的值,栈顶指针不变
    }
}
//多元素入栈
void InPutStack(LinkStack &S)
{
    cout<<"你想几个元素入栈:";
    int n,e;
    cin>>n;
    for(int i=1; i<=n; i++)
    {
        cout<<"请输入第"<<i<<"个入栈元素:";
        cin>>e;
        Push(S,e);
    }
}
void OutPutStack(LinkStack &S)
{
    cout<<"- - - - -"<<endl;
    LinkStack p=S;
 
    while(p!=NULL)
    {
        cout<<p->date<<endl;
        p=p->next;
    }
    cout<<"- - - - -"<<endl;
}
int main()
{
    LinkStack S;
    InitStack(S);
    InPutStack(S);
 
    OutPutStack(S);
 
    int d;
    int data=Pop(S,d);
    cout<<data<<endl;
 
    OutPutStack(S);
 
}
思维导图

image-20230312171521014

队列

  • 知识总览

image-20230312213153257

定义

image-20230312213439391

  • 队列:是只允许在一端进行插入,在另一端删除的线性表。
  • 重要术语:队头(允许删除的一端),队尾(允许插入的一端),空队列。
  • 特点:先进入队列的元素先出队(先进先出)。

image-20230312215141575

基本操作

image-20230313102416888

思维导图

image-20230313102443913

顺序队列

  • 知识总览

    image-20230313102933464

  • 实现

    image-20230313103129057

  • 初始化

    image-20230313103218527

    //初始化操作
    #define MaxSize 10//定义队列中元素的最大个数
    typedef struct
    {
        ElemType data[MaxSize];//用静态数组存放队列元素
        int front,rear;//队头指针和队尾指针
    }SqQueue;
    
    //初始化队列
    void InitQueue(SqQueue& Q)
    {
        //初始时 队头,队尾指针指向0
        Q.rear=Q.front=0;
    }
    
    //判断队列是否为空
    bool QueueEmpty(SqQueue Q)
    {
        if(Q.rear==Q.front)//队空条件
            return true;
        else
            return false;
    }
    
    void testQueue()
    {
        //声明一个队列(顺序存储)
        SqQueue Q;
        InitQueue(Q);
        //...后续操作...
    }
    
  • 入队(只能从队尾入队(插入))

    image-20230313103701510

    • 注意:这里为什么要进行取模运算呢?就是因为要让rear指针重新队尾指向对头,这里首先让对头的元素先出队,那么front指针就要前移,所以就会有三个三个存储数据的空闲空间,这个时候,如果想要往里面放入元素的话,就要让rear指针重新指向对头,所以这里加上了一个取模操作,很好地解决了这个问题!
  • 循环队列

    image-20230313104755251

    • 判断队列为空的操作就是当front指针和rear指针指向同一个位置的时候,就会被判断为队列为空,所以在上面的循环队列中,当队列元素存满的时候,rear指针和front指针会指向同一个位置,这与我们所想要实现的目的不符合,所以必须要牺牲一个存储单元。

    • 循环队列实现代码

      image-20230313105055456

      //判断队列是否为空
      bool QueueEmpty(SqQueue Q)
      {
          if(Q.rear==Q.front)//队空条件
              return true;
          else
              return false;
      }
      
      //入队
      bool EnQueue(SqQueue& Q,ElemType x)
      {
          if((Q.rear+1)%MaxSize==Q.front)
              return false;//队满则报错
          Q.data[Q.rear]=x;//新元素插入队尾
          Q.rear=(Q.rear+1)%MaxSize;//队尾指针加1取模
          return true;
      }
      
  • 出队(只能让队头元素出队)与查找操作

    image-20230313110346381

    //出队(删除一个队头元素,并用x返回)
    bool DeQueue(SqQueue& Q,ElemType& x)
    {
        if(Q.rear==Q.front)//队空报错
            return false;
        x=Q.data[Q.front];
        Q.front=(Q.front+1)%MaxSize;
        return true;
    }
    
    //获得队头元素的值,用x返回
    bool GetHead(SqQueue Q,ElemType& x)
    {
        if(Q.rear==Q.front)//队空则报错
            return false;
        x=Q.data[Q.front];
        return true;
    }
    
    • 实现逻辑:使用取余操作,进而实现front指针循环进行移动,直到最后两个指针最后指向同一个位置的时候,那么此时队列就空了,至于查找操作的话,一般是查找队头元素,所以直接把其赋给x就好了,在最后返回x就行了。
  • 判断队列已满/已空

    image-20230313111103277

    • 注意:判断这个队列已满/已空的条件是要牺牲一个单元的存储空间的,但是如何去利用上这一块空间呢?我们可以在定义一个队列的时候加上一个size(表示当前队列长度)。(不能直接使用这块空间,这样的话,就会导致判断队空和队满的代码逻辑是一样的,这样就会出错!)

    • 进阶版(利用上面的所遗留的那一小块空间)

      image-20230313111803741

    • 进阶版2(使用插入和删除进行判断)

      image-20230313115941767

      • 注意:这里是定义了一个tag变量,然后每次删除操作成功时,都令tag=0;每次插入操作成功时,都令tag=1;
      • 上面的所有方法实现的原理就是队尾指针指向队尾元素的下一个位置,队头指针指向的是队头元素。
  • 其他出题方法(例如队尾指针指向队尾元素,队头指针指向队头元素)

    image-20230314115458214

    • 上面图片中的右边这种方式就是新的出题方法,即是让队尾指针指向队尾元素,队头指针指向队头元素,在进行插入元素的时候,首先让队尾指针向后移一个位置,然后在进行插入。

    • 在初始化的时候,队尾指针指向哪里合适?

    • 解决办法如下:

      • 让front指针指向0这个位置,让rear指针指向n-1这个位置,示意图如下:

        image-20230314120217585

        • 让rear指针指向那个位置确实很妙,判空的条件也很简单,就是看front前面的那个存储单元是不是rear指针就可以了!至于判满的话,就要像上面的方法一样,加辅助变量或者牺牲一个存储单元来实现。(若牺牲一个存储单元,那么判满的情况就是当front前面的两个存储单元时rear指针,那就意味这队列已满,判空和上图的判空是一样的)。
思维导图

image-20230314140330963

链式队列

  • 知识总览

    image-20230314140421420

  • 链式队列的实现

    image-20230314140835654

    • 注意:由于队列只能够在队尾插入元素,所以这里干脆直接定义了一个rear指针来指向队尾元素,这样在插入的时候就会方便很多,同样地,在进行删除操作的时候也会简单很多,时间复杂度也降低了不少!
  • 链式队列的初始化(带头节点)

    image-20230314141327386

    typedef struct LinkNode
    {
        ElemType data;
        struct LinkNode* next;
    }LinkNode;
    
    typedef struct
    {
        LinkNode *front,*rear;
    }LinkQueue;
    
    //初始化队列(带头节点)
    void InitQueue(LinkQueue& Q)
    {
        //初始时front,rear都指向头节点
        Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
        Q.front->next=NULL;
    }
    
    //判断队列是否为空
    bool IsEmpty(LinkQueue Q)
    {
        if(Q.front==Q.rear)
            return true;
        else
            return false;
    }
    
    void testLinkQueue()
    {
        LinkQueue Q;//声明一个队列
        InitQueue(Q);//初始化队列
        //...后续操作...
    }
    
  • 不带头节点

    image-20230314141633661

    //初始化队列(不带头节点)
    void InitQueue(LinkQueue& Q)
    {
        //初始时front,rear都指向NULL
        Q.front=NULL;
        Q.rear=NULL;
    }
    
    //判断队列是否为空(不带头节点)
    bool IsEmpty(LinkQueue Q)
    {
        if(Q.front==NULL)
            return true;
        else
            return false;
    }
    
    • 注意:在进行判空操作的时候,既可以判断队头指针是否为空,也可以进行判断队尾指针是否为空!
  • 入队(插入)操作(带头节点)

    image-20230314142531408

    //新元素入队
    void EnQueue(LinkQueue& Q,ElemType x)
    {
        LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
        s->data=x;
        s->next=NULL;
        Q,rear->next=s;//新结点插入到rear之后
        Q.rear=s;//修改表尾指针
    }
    
    • 注意:这里自己可以对着右侧的逻辑图来进行推导,另外要进行说明的是Q.rear->next=s中的rear也可以是front,因为这种带头节点的在之前初始化的时候,就已经令Q.front=Q.rear,所以进行替换是没有问题的!(但是千万注意哦——这里的front和rear进行替换是只针对于第一个存放数据的节点,至于第二个,第三个,就不能这么干了),所以最好还是写rear吧,这样最保险!
  • 入队(插入)操作(不带头节点)

    image-20230314143250328

    //新元素入队(不带头节点)
    void EnQueue(LinkQueue& Q,ElemType x)
    {
        LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
        s->data=x;
        s->next=NULL;
        if(Q.front==NULL)//在空队列中插入第一个元素
        {
            Q.front=s;//修改队头队尾指针
            Q.rear=s;//不带头节点的队列,第一个元素入队时需要特别处理
        }
        else
        {
            Q.rear->next=s;//新结点插入到rear节点之后
            Q.rear=s;//修改rear指针
        }
    }
    
  • 以上就是入队操作,当然上面的操作,rear指针都是指向队尾元素,这样在进行入队的操作时,新数据元素将会被放在rear节点之后的那个节点,但是有些题也可能会让rear指针指向队尾元素的后一个节点,这是需要注意的!

  • 出队(删除)操作(带头节点)

    image-20230314144011423

    //队头元素出队(带头结点)
    bool DeQueue(LinkQueue& Q,ElemType& x)
    {
        if(Q.front==Q.rear)
            return false;//空队
        LinkNode* p=Q.front->next;
        x=p->data;//用变量x返回队头元素
        Q.front->next=p->next;//修改头节点的next指针
        if(Q.rear==p)//此次是最后一个节点出队
            Q.rear=Q.front;//修改rear指针
        free(p);//释放节点空间
        return true;
    }
    
    • 如果不能理解的话,就可以画一个示意图进行逻辑分析!
  • 出队(删除)操作(不带头节点)

    image-20230314144232322

    //队头元素出队(不带头节点)
    bool DeQueue(LinkQueue& Q,ElemType& x)
    {
        if(Q.front==NULL)
            return false;//空队
        LinkNode* p=Q.front;//p指向此次出队的节点
        x=p->data;//用变量x返回队头元素
        Q.front=p->next;//修改front指针
        if(Q.rear==p)//此次是最后一个节点出队
        {
            Q.front=NULL;//front指向NULL
            Q.rear=NULL;//rear指向NULL
        }
        free(p);//释放节点空间
        return true;
    }
    
  • 队满的条件

    image-20230314144323049

    • 因为:顺序队列时静态数组,大小是确定的,但是链式队列是动态内存分配,一般不会队满,除非内存不足!
思维导图

image-20230314144734416

  • 要想实现计算出队列的长度:那么就只能从队头节点,依次往后进行遍历,这样所花费的时间复杂度是O(n),但是如果这个队列长度是经常要使用的话,那么就可以在定义的时候,加一个length的变量,在入队的时候加一,出对的时候减一就可以了!

双端队列

  • 知识复习(栈和队列的定义)

    image-20230314150228118

    • 栈能实现的功能,双端队列也肯定能实现!
  • 双端队列的多种形式

    image-20230314151351732

  • 经典考题(判断输出序列合法性)

    • 技巧1:只要是在栈里面都能够合法的序列,那么在双端序列里面也应该是合法的!

    image-20230314154323597

    • 上图中的是判断栈中的合法性序列,可以使用卡特兰数进行判断合法序列总共有多少种,但是具体是哪几种,还是要自己手动列出来,判断逻辑是很简单的,自己实现就行了!
  • image-20230314154608178

  • 上图中的是输入(插入)受限的双端队列,由于右侧的插入和删除是和栈是一摸一样的,所以在栈中能够合法的序列,在这里也能够合法,其中画了下划线的是在栈中不合法,但是在这里却合法的输出序列!

  • image-20230314155316552

  • 上图是输入(插入)受限的双端队列,同样地,右侧的插入和删除还是和栈一模一样,所以在栈中能够合法的序列,在这里也能够合法,其中画了下划线的是在栈中不合法,但是在这里却合法的输出序列!

  • 注意:这种题型,画出逻辑示意图就和好解决,只需要自己把逻辑理解清楚就可以了,自己要有自己的解题方法!

思维导图

image-20230314155953229

栈的应用


括号匹配

image-20230314161714216

  • LIFO即后进先出,和栈的特性是一样的。

  • 算法演示:

    image-20230314162051631

    • 实现逻辑:首先队括号进行扫描,遇到了左括号就把它存入栈中,当遇到右括号的时候就会弹出栈顶元素,去和当前这个右括号进行匹配,依次进行,如果括号是两两匹配的,那么这个算法实现就成功了!

    • 错误演示1:

      image-20230314162325746

    • 错误演示2:

      image-20230314162422696

    • 错误演示3:

      image-20230314162525774

  • 算法实现流程图

    image-20230314162824994

  • 代码实现

    image-20230314164833916

    #define MaxSize 10//定义栈中元素的最大个数
    typedef struct
    {
        char data[MaxSize];//静态数组存放栈中元素
        int top;//栈顶指针
    }SqStack;
    
    bool bracketCheck(char str[],int length)
    {
        SqStack S;
        InitStack(S);//初始化一个栈
        for(int i=0;i<length;i++)
        {
            if(str[i]=='('||str[i]=='['||str[i]=='{')
                Push(S,str[i]);//扫描到左括号,入栈
            else
            {
                if(StackEmpty(S))//扫描到右括号。且当前栈空
                    return false;//匹配失败
                char topElem;
                Pop(S,topElem);//栈顶元素出栈
                if(str[i]==')'&&topElem!='(')
                    return false;
                if(str[i]==']'&&topElem!='[')
                    return false;
                if(str[i]=='}'&&topElem!='{')
                    return false;
            }
        }
        return StackEmpty(S);//检索完全部括号后,栈空说明匹配成功
    }
    
    //初始化栈
    void InitStack(SqStack &S)
    //判断栈是否为空
    bool EmptyStack(SqStack S)    
    //新元素入栈
    bool Push(SqStack &S,char x)    
    //栈顶元素出栈,用x返回
    bool Pop(SqStack &S,char &x)    
    
    • 注意:这里的代码实现使用的是静态数组的方式也就是顺序栈,但是可以使用链栈,这样对内存的使用更加地方便!
知识回顾

image-20230314165103760

  • 在考试的时候,可以直接把这些东西提前记住,然后默写出来!

表达式求值

  • 知识总览

    image-20230314165559695

    • 其中后缀表达式的考点会更多一些,因为它的应用层面更加的宽广一些!
  • 算术表达式

    由三部分组成:操作数,运算符,界限符(是必不可少的,它反映了计算的先后顺序)。

    • 举个例子

      image-20230314170042077

  • 中缀表达式:运算符在两个操作数中间。

  • 后缀表达式:规则是运算符在两个操作数后面。

  • 前缀表达式:运算符在两个操作数前面。

    示意图如下:

    image-20230314174959501

    • 注意:后缀表达式和前缀表达式并不是唯一的,在进行转换的时候,把谁和谁看成一个整体并不是唯一的,所以它们的表达式并不是唯一的,这一点要记住!
  • 中缀表达式转后缀表达式(手算):

    1. 确定中缀表达式中各个运算符的运算顺序(运算顺序不唯一,因此对应的后缀表达式也不唯一)。
    2. 选择下一个运算符,按照==【左操作数 右操作数 运算符】==的方式组合成一个新的操作数。
    3. 如果还有运算符未被处理,就继续执行2。
    • 举个例子:

      image-20230314175739969

    • 自己可以手动实现一下,加深对这个转换的理解!

    • 举个例子:

    • image-20230314182248869

    • 上图中,左侧和右侧的两种形式都是正确的,但是要是让计算机来进行计算的话,那么左边的就是机器算的结果!

    • 私房菜:“左优先”原则,不要Freestyle,保证手算和机算结果相同

    • “左优先”原则:只要左边的运算符能先计算,就优先算左边的(可以保证运算顺序唯一),但还是要遵循先乘除后加减的操作。

    • 后缀表达式的计算(手算)

      image-20230315095737159

      • 后缀表达式的手算方法

        从左往右进行扫描,每遇到一个运算符号,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数

        注意:两个操作数的左右顺序。

        特点:最后出现的操作数先被运算

    • 后缀表达式的计算(机算)

      用栈(这个栈用于存放当前暂时还不能确定运算次序的操作数)实现后缀表达式的计算:

      1. 从左往右扫描下一个元素,直到处理完所有元素。
      2. 若扫描到操作数则压入栈,并回到1,否则执行3。
      3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1。
        • 注意:先出栈的是右操作数
        • 若表达式合法,则最后栈中只会留下一个元素,就是最终的结果。
        • 后缀表达式适用于基于栈的编程语言,如Forth,Postscript。
    • 中缀表达式转后缀表达式(机算)

      算法实现原理:

      初始化一个栈,用于保存暂时还不能确定运算顺序的运算符

      从左到右处理各个元素,直到末尾。可能遇到三种情况:

      1. 遇到操作数。直接加入后缀表达式。
      2. 遇到界限符。遇到“(”,直接入栈;遇到“)”,则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式(后缀表达式是没有界限符的)。
      3. 遇到运算符。依次弹出栈中优先级(例如:乘除高于加减)高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或空栈则停止,之后再把当前运算符入栈。

      按照上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。

      总结:整体的逻辑还是有些难以理解,但是综合几个例子来看,认真的去分析,其实还是可以弄清楚的。

    • 示例1:

      image-20230315202838632

    • 示例2:

      image-20230315202904347

    • 示例3:

      image-20230315203126495

    • 代码实现:

      
      
    • 中缀表达式的计算(用栈实现)(实现原理:两个算法的结合——中缀转后缀+后缀表达式求值)

      用栈实现中缀表达式的计算:

      1. 初始化两个栈,操作数栈运算符栈
      2. 若扫描到操作数,压入操作数栈。
      3. 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
  • 中缀表达式转前缀表达式(手算)

    • 确定中缀表达式中各个运算符的运算顺序

    • 选择下一个运算符,按照==【运算符 左操作数 右操作数】==的方式组合成一个新的操作数。

    • 如果还有运算符没被处理,就继续2。

      • “右优先”原则:只要右边的运算符能先计算,就优先算右边的。
      • image-20230315165805092
    • 前缀表达式的计算(机算)

      用栈实现前缀表达式的计算:

      1. 从右往左扫描下一个元素,直到处理完所有元素。
      2. 若扫描到操作数则压入栈,并回到1;否则执行3。
      3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1。
        • 注意:先出栈的是“左操作数”。
思维导图

image-20230315171216242

image-20230316164303458

递归

函数调用的特点:最后被调用的函数最先执行结束(LIFO)。

函数调用时,需要用一个栈(函数存储栈)存储:

  1. 调用返回地址。
  2. 实参。
  3. 局部变量。

函数调用背后的过程示意图:

image-20230318131905070

  • 栈在递归当中的应用

    适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题。

    • eg1:计算正整数的阶乘n!

      image-20230318132421717

      image-20230318132623157

      • 递归调用时,“函数调用栈”可称为“递归工作栈”。
      • 每进入一层递归,就将递归调用所需信息压入栈顶。
      • 每退出一层递归,就从栈顶弹出相应信息。
      • 缺点:太多层递归可能会导致栈溢出
      • 可以自定义栈将递归算法改造成非递归算法。
    • eg2:求斐波那契数列

      image-20230318132432997

      image-20230318134614164

      • 缺点:可能包含很多重复计算。
知识回顾

image-20230318135103950

对列的应用

树的层次遍历

  • 注:在“树”章节中会详细学习。

图的广度优先遍历

  • 注:在“图”章节中会详细学习。

队列在操作系统中的应用

多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)(与队列的先进先出道理是一样的)是一种常用策略。

  • eg1:CPU资源的分配。

    image-20230318170326872

  • eg2:打印数据缓冲区

    image-20230318170405644

矩阵的压缩存储

知识总览

image-20230318170448762

  • 一维数组的存储结构

    image-20230318170753059

  • 二维数组的存储结构

    image-20230318171112593

    image-20230318171215222

    image-20230318171323087

    • 注意:内存是连续的,线性的;二维数组也具有随机存储的特点。
  • 普通矩阵的存储

    可用二维数组存储;注意:描述矩阵元素时,行,列号通常从1开始。而描述数组时通常下标从0开始。(具体看题目给的条件,注意审题!

  • 某些特殊矩阵可以压缩存储空间

  • 对称矩阵的压缩存储

    image-20230318172052102

    方阵:行数和列数相等

    image-20230318172254908

    • 接下来探讨一个这样的问题,这么把矩阵的行号和列号转换成一维数组的下标?

      image-20230318172621154

      • 可以看到,右下角之所以会有一个-1,是因为一维数组的下标是从0开始的,如果是从1开始的话,那么就不要这个减一(具体情况,具体分析!)

      • 注意:可能会有其它的出题方法就是可以存储不同的三角区域,也可以按照不同的存储原则(行优先和列优先)进行存储,但是要想实现转换函数,必须清楚它是第几个元素,只要清楚了它是第几个元素,那么在一维数组里面就很好表示了!

      • 不同的出题方式

        image-20230318173334332

  • 三角矩阵(上三角矩阵与下三角矩阵)的压缩存储

    image-20230318173546705

    • 使用方式(下三角矩阵)

      image-20230318173730267

    • 使用方式(上三角矩阵)

      image-20230318173916127

    • 在以上的两幅图当中,关于矩阵和一维数组的下标对应方法,这个公式并不需要死死记住,可以现场进行推导!(但是还是要关注一维数组的起始下标)。

  • 三对角矩阵(带状矩阵)的压缩存储

    image-20230318174511827

    • 若已知数组下标k,这么得到i,j?

      image-20230318174850770

      • 得到了i的值之后,就可以根据之前k与i,j的对应关系得到j的值!

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfdjIeWx-1690794718210)(C:\Users\袁嘉佚\AppData\Roaming\Typora\typora-user-images\image-20230318175200485.png)]

  • 稀疏矩阵(非0元素远远少于矩阵元素的个数)的压缩存储

    压缩存储策略1:

    image-20230318175528562

    • 如果需要访问其中的某一个元素,只能顺序地依次扫描这些三元组,也就是说会失去随机存储的特性
    • 其中的i,j,v的值可以使用结构体来实现

    压缩存储策略2:

    image-20230318180337771

    • 注意:十字链表法实现的原理是首先弄一个数组出来,然后在数组里面放入指针(分为向下域和向右域),然后在非0的数据建立一个节点,节点里面不止是要存放数据元素,还要存放指向同列的下一个元素的指针和指向同行的下一个元素的指针

思维导图

image-20230318180756867

  • 常见考题

    image-20230318180854085

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值