王道数据结构笔记03—栈与队列(C语言)

1.栈

1.1栈的基本概念

栈其实是一种很特殊的线性表,栈是一种只允许在一端进行插入或删除操作的线性表

几个基本术语:
1.空栈:没有元素的栈
2.栈底:不允许插入和删除的一端
3.栈顶:允许插入和删除的一端
在这里插入图片描述

栈是后进先出的一种特殊的线性表,他的删除插入操作只能在栈顶执行
进栈顺序:
a1,a2,a3,a4,a5
出栈顺序:
a5,a4,a3,a2,a1

1.2栈的顺序存储

顺序栈的代码定义

#define MaxSize 10
typedef struct{
    Elemtype data[MaxSize];      //静态数组存放栈中元素
    int top;                //栈顶指针
}SqStack;

顺序栈的常用操作

初始化操作

//初始化栈
void InitStack(SqStack &S){
    S.top = -1;             //初始化栈顶指针
}

void testStack(){
    SqStack S;             //声明一个顺序栈
    InitStack(S);          
}

判空操作

//判空
bool StackEmpty(SqStack S){
    if(S.top==-1)
        return true;
    else
        return false;
}

进栈操作

//新元素入栈
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;
}

出栈操作

//出栈操作
bool Pop(SqStack &S,Elemtype &x){
    if(S.top==-1)
        return false;    //栈空,报错
    x=S.data[S.top];      //栈顶元素出栈
    S.top=S.top-1;        //栈顶指针减1
    return true;
}

读取栈顶元素操作

//读栈顶元素
bool GetTop(SqStack S,Elemtype &x){
    if(S.top==-1)
        return false;   //栈空,报错
    x=S.data[S.top];    //x记录栈顶元素
    return true;
}

1.3 共享栈

共享栈,顾名思义就是两个栈共享同一片空间

#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;
}

在这里插入图片描述

1.4栈的链式存储

栈的链式存储,我们可以想象成一个特殊的单链表,即插入和删除操作只允许在头结点发生的单链表。

链栈的代码定义

typedef struct Linknode{
    Elemtype data;                  //数据域
    struct Linknode *next;    //指针域
}* LiStack;                   //栈类型定义

链栈的基本操作(带头结点)

初始化

void InitStack(LiStack &L){
    L = new Linknode;         //创建一个头结点
    L->next=NULL;
}

判空

bool isEmpty(LiStack L){
    if(L->next==NULL){
        return true;
    }else
    return false;
}

进栈操作

void Push(LiStack &L,int x){
    struct Linknode* s ;
    s->data=x;
    //头插法
    s->next = L->next;
    L->next=s;
}

出栈操作

bool Pop(LiStack &L,int &x){
    if(L->next==NULL)
        return false;       //栈空不能出栈
    x=L->next->data;
    L->next=L->next->next;
    return true;
}

链栈的基本操作(不带头结点)

初始化

void InitStack(LiStack &L){
    L==NULL;                 //不带头结点
}

判空

bool isEmpty(LiStack L){
    if(L==NULL)
        return true;
    else
        return false;
}

进栈操作

void Push(LiStack &L,int x){
    Linknode* s;
    s->data=x;
    //插入到表首,并变更表首
    s->next=L;
    L=s;
}

出栈操作

bool Pop(LiStack &L,int &x){
    if(L==NULL)
        return false;      //空栈错误
    x=L->data;
    L=L->next;
    return true;
}

2.队列

2.1队列的基本概念

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

几个术语:
1.对头:允许删除的一端
2.队尾:允许插入的一端
3.空队列:没有元素的队列
、

队列是先进先出的,而栈是后进先出的,两者是相反的

2.2队列的顺序实现

顺序队列的代码定义

typedef struct {
    ElemType data[MaxSize];     //用静态数组存放队列元素
    int front,rear;            //队头指针和队尾指针
}SqQuene;

顺序队列的基本操作

初始化

void InitQuene(SqQuene &Q){
    //初始化时,队头队尾指针均指向0
    Q.rear=Q.front=0;
}

判空

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;          //队尾指针加一对MaxSize取模,实现队尾指针的循环
    return true;
}

取模的意义,在逻辑上把线状的存储空间变成了环装的存储空间
在这里插入图片描述

出队操作

bool DeQueue(SqQueue &Q,Elemtype &x){
    if(Q.rear==Q.front)
        return false;                  //队空报错
    x=Q.data[Q.front];                  //删除一个队头元素,并用x返回对头元素的值
    Q.front=(Q.front+1)%MaxSize;
}

查询操作
对于队列来说。一般都是查询其队头元素的值

bool GetHead(SqQueue Q,Elemtype x){
    if(Q.rear==Q.front)
        return false;                  //队空报错
    x=Q.data[Q.front];                  //获得队头元素的值
    return true;
}

计算队列元素个数的公式

(rear+MaxSize-front)%MaxSize

其他注意事项
在上文的例子中,我们的队尾指针都是指向当前队尾的下一个位置,但有时会遇到队尾指针指向当前队尾,此时要另加判断,大体情况都类似。

2.3队列的链式实现

链式队列的代码定义

//链式队列结点
typedef struct LinkNode{
    Elemtype data;
    struct LinkNode* next;
}LinkNode;
//链式队列
typedef struct{
    LinkNode* front;
    LinkNode* 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 InitQueue(LinkQueue &Q){
    Q.front=NULL;
    Q.rear==NULL;
}
bool isEmpty(LinkQueue Q){
    if(Q.front==NULL)
        return true;
    else
        return false;
}

入队(带头结点)

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;                    //修改表尾指针
}

入队(不带头结点)

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;
        Q.rear=s;
    }
}

出队(带头结点)

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;
    free(p);                         //释放结点空间
    return true;
}

出队(不带头结点)

bool DeQueue(LinkQueue &Q,int &x){
    if(Q.front==NULL)
        return false;
    LinkNode* p=Q.front;
    x=p->data;
    Q.front=p->next;
    if(Q.rear==p){
        Q.front=NULL;
        Q.rear=NULL;
    }
    free(p);
    return true;
}

2.4双端队列

双端队列:只允许从两端插入,两端删除的线性表

在这里插入图片描述

另外还有两种受限的双端队列
在这里插入图片描述

3.栈的应用

3.1括号匹配

什么是括号匹配?
及书写代码时,所有的括号都应该成对出现。
下面举一个例子
在这里插入图片描述
根据如图的括号匹配,可以总结出:
1.最后出现的左括号最先被匹配(与栈后进先出相似)
2.没出现一个右括号,就消耗一个左括号(与出栈类似)

算法实现

用栈实现括号匹配:
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配
匹配失败的情况:
1.左括号单身
2.右括号单身
3.左右括号不匹配

下图是大致思路

代码实现

#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(isEmpty(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 isEmpty(S);      //检索完全部括号后,栈空说明匹配成功
    }
}

3.2表达式求值

表达式的组成部分:操作数,运算符,界限符

我们熟知的,其实是中缀表达式
中缀表达式
a+b,a+b-c,a+b-cd
后缀表达式
ab+,ab+c-,ab+ cd
-
前缀表达式
+ab,-+abc,-+ab*cd

中缀转后缀(手算)

中缀转后缀的手算算法:
1.确定中缀表达式中各个运算符的运算顺序
2.选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数
3.如果还有运算符没被处理,就继续2

在这里插入图片描述
注:由于运算顺序不唯一,因此对应的后缀表达式也不唯一
在这里插入图片描述
但是当我们在设计算法时,应具有确定性,为了保证不出现两种不同的答案,引入一个左规则:只要左边的运算符能先计算,就优先算左边的,所以我们得到的结果就是左边的表达式。

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

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

在这里插入图片描述

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

1.从左往右扫描下一个元素,直到处理完所有元素
2.若扫描到操作数则压入栈,并回到1,否则执行3
3.若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1

中缀转前缀(手算)

1.确定中缀表达式中各个运算符的运算顺序
2.选择下一个运算的运算符,按照[运算符 左操作数 右操作数]的方式组合成一个新的操作数
3.如果还有运算符没被处理,就继续2

右优先原则:只要右边的运算符能先计算,就优先算右边的
在这里插入图片描述

前缀表达式的计算(机算)

1.从右往左扫描下一个元素,直到处理完所有元素
2.若扫描到操作数则压入栈,并回到1,否则执行3
3.若扫描到运算符,则弹出连个栈顶元素,执行相应运算,运算结果压回栈顶,回到1

中缀转后缀(机算)

1.初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
2.从左往右扫描
3.遇到操作数,直接加入后缀表达式
4.遇到界限符,遇到“(”直接入栈,遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止,注:“(”不用加入后缀表达式
5.遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空就停止,之后再把当前运算符入栈。

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

伪代码思路:

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

3.3栈在递归中的应用

函数调用的特点:最后被调用的函数最先执行结束
函数调用时,需要用一个栈储存:
1.调用返回地址(执行结束后返回值返回到哪里)
2.实参
3.局部变量

递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需的信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息

Eg1:递归求阶乘

//计算阶乘的函数
int factorial (int n){
    if(n==1||n==0)
        return 1;
    else
        return n*factorial(n-1);
}
int main (){
    int x=factorial(10);
    printf("%d",x);
}

在这里插入图片描述
递归的缺点一:太多层递归可能会导致栈溢出!
Eg2:递归求斐波那契数列

int Fib(int n){
    if(n==0)
        return 0;
    else if(n==1)
        return 1;
    else
        return Fib(n-2)+Fib(n-1);
}
int main (){
    for(int i=0;i<10;i++){
        printf("%d ",Fib(i));
    }
}

在这里插入图片描述
递归的缺点二:可能包含很多重复计算

4队列的应用

这两个后续章节再展开,先了解

1.树的层次遍历
2.图的广度优先遍历

队列在操作系统中的应用
1.CPU资源的分配
在日常使用时,已经就绪的进程,会排列成一个队列等待CPU的调用,类似于上文所述的队列
2.打印数据缓冲区
多台电脑使用同一台打印机,先来的打印请求最先执行,其余的请求排成一个队列等待打印机的执行

5 特殊矩阵的压缩存储

5.1对称矩阵

在这里插入图片描述
由于上三角区和下三角区是对称的,采用的压缩存储策略:只存储主对角线加下三角区(或主对角线加上三角区)
首先,按照行优先将各元素存入一维数组中
在这里插入图片描述

数组大小:
1+2+……n=(1+n)*n/2
映射函数:矩阵下标->一维数组下标(方便调用)
ai,j是第i(i-1)/2+j个元素
由于数组下标从0开始,所以ai,j->B[k]转换公式为:
k=i(i-1)/2+j-1
对称矩阵特性:
ai,j=aj,i

5.2三角矩阵

在这里插入图片描述
压缩存储策略:
按行优先原则将橙色区元素存入一维数组中,并在最后一个位置存储常量c
下三角
在这里插入图片描述
上三角
在这里插入图片描述

5.3三对角矩阵

在这里插入图片描述
压缩存储策略:按行优先,只存储带状部分
在这里插入图片描述

5.4稀疏矩阵

在这里插入图片描述
压缩存储策略一:
三元组<行,列,值>
在这里插入图片描述

压缩存储策略二:
十字链表法
在这里插入图片描述

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值