笔记:数据结构——第三章 栈和队列

第三章 栈和队列

3.1 栈

3.1.1 栈的基本概念

  • 栈(stack)是一个特殊的线性表,是限定仅在一端(通常在表尾)进行插入和删除操作的线性表;又称为后进先出(Last In First Out)的线性表,简称LIFO结构

    例如:栈s=(a1,a2,…):a1称为栈底元素,an称为栈顶元素

  • 栈顶:允许插入和删除的一端,表尾(an端)称为栈顶Top

    栈底:不允许插入和删除的一端,表头(a1端)称为栈底Base

  • 逻辑结构:一对一关系,与普通线性表相同;存储结构:顺序栈(更常见)和链栈

    数据的运算:插入元素——称为入栈进栈压栈 PUSH(x);删除元素——称为出栈弹栈 pop(y)

3.1.2 栈的顺序存储实现

*顺序栈的定义
#define MaxSize 10
typedef struct{
    ElemType data[MaxSize]		//静态指针存放栈中元素
    int top;					//栈顶指针(一般指的是数组下表)
}SqStack;						//Sq:sequence顺序

void testStack(){
    SqStack S;					//声明一个顺序栈(分配空间,其大小为MaxSize*sizeof(ElemType))
}
*栈的初始化
void InitStaack(SqStack &S){
    S.top=-1;					//初始化栈顶指针
}

//判断栈空
bool StackEmpty(SqStack S){
    if(S.top==-1)
        return true;
    else
        return false;
}
*进栈操作
bool Push(SqStack &S,EleemType x){
    if(S.top==MaxSize-1)
        return false;
    S.top=S.top+1;				//此处两句等价于:S.data[++S.top]=x——先加再运算
    S.data[S.top]=x;			//先加1指向下一个,再使用top值入栈
    return ture;
}
*出栈操作
bool Pop(SqStack &S,ElemType &x){
     if(S.top==-1)
        return false;
    x=S.data[S.top];			//等价于:S.data[S.top--]=x——先运算再减
    S.top=S.top-1;
    return ture;
}
  • 数据还残留在内存中,只是逻辑上被删除了
*读栈顶元素
bool GetTop(SqStack &S,EleemType &x){
    if(S.top==-1)
        return false;
    x=S.data[S.top];			
    return ture;
}
*另一种方式
  • 初始时,栈顶指针指向0:S.top==0;
//进栈
S.data[S.top]=x;				//等价于:S.data[S.top++]=x
S.top=S.top+1;
//出栈
S.top=S.top-1;					//等价于:S.data[--S.top]=x
x=S.data[S.top];
  • 栈满的条件:top==MaxSize
*共享栈
#define MaxSize 10
typedef struct{
    ElemType data[MaxSize];
    int top0;					//0号栈顶指针,栈底-1开始
    int top1;					//1号栈顶指针,栈顶MaxSize开始
}ShStack;

//共享栈的初始化
void InitStack(ShStack &S){
    S.top0=-1;
    S.top1=MaxSize;
}
  • 两个栈共享一片空间,逻辑上实现了两个栈,物理上只使用了一片存储空间
  • 栈满的条件:top0+1==top1

3.1.3 栈的链式存储方式

  • 链栈的基本操作:类似于单链表的头插法和对头结点的删除操作
*链栈的定义
  • 和单链表类似,但一般推荐不带头结点的链栈
typedef struct Linknde{
    ElemType data;
    struct Linknode *next;
}*LiStack;

3.2 队列

3.2.1 队列的基本概念

  • 队列(Queue)是只允许在一端进行插入,在另一端删除的线性表;又称为先进先出(FIFO)的线性表

  • 队头:允许删除的一端;队尾:允许插入的一端

3.2.2 队列的顺序实现

*顺序队列的定义
#define MaxSize 10
typedef struct{
    ElemType data[MaxSize];				//用静态数组存放队列元素		
    int front;							//队头指针:指向队头元素
    int rear;							//队尾指针:指向队尾元素的后一个位置(下一个应该插入的位置)
}SqQueue;	
*顺序队列的初始化
#define MaxSize 10
typedef struct{
    ElemType data[MaxSize];				
    int front;					
    int rear;					
}SqQueue;	

void InitQueue(SqQueue &Q){
    Q.front=Q.rear=0;					//0既是队头位置,也是下一个插入的位置
    return OK;
}

//判断队列是否为空
bool QueueEmpty(SqQueue Q){
    if(Q.front==Q.rear)
        return true;
    else
        return false;
}
*循环队列——入队操作
  • 队列元素个数——(rear-front+MaxSize)%MaxSize;

  • 队空——队尾和队头指针指向同一位置front==rear;

    队满——队尾指针的下一位置是队头(rear+1)%MAXQSIZE==front;

bool EnQueue(SqQueue &Q,ElemType x){
    if((Q.rear+1)%MAXQSIZE==Q.front)		//判断队满
        return false;
    Q.data[Q.rear]=x;
    Q.rear=(Q.rear+1)%MaxQSize;				//队尾指针后移,指针加1取模
    return true;
}
  • 模运算:即取余运算,a%b==a除以b的余数,也表示为a MOD b
*循环队列——出队操作
bool DeQueue(SqQueue &Q,ElemType &x){
    if(Q.rear==Q.front)						//判断队空
        return false;
    x=Q.data[Q.front];
    Q.front=(Q.f+1)%MaxQSize;				//队头指针后移
    return true;
}

//获得队头元素(只删除后移的代码)
bool GetQueue(SqQueue &Q,ElemType &x){
    if(Q.rear==Q.front)	
        return false;
    x=Q.data[Q.front];
    return true;
}
*判断队列已满/已空
  • 方案一:牺牲一个存储空间:

    队列元素个数——(rear-front+MaxSize)%MaxSize;

    队空——front==rear;队满——(rear+1)%MAXQSIZE==front;

  • 方案二:不牺牲存储空间(设置size)

#define MaxSize 10
typedef struct{
    ElemType data[MaxSize];					
    int front;					
    int rear;
    int size;							//设置一个计数size
}SqQueue;	
  • 初始化:rear=front=0; size=0;插入成功:size++;删除成功:size--;

    队满条件:size==MaxSize;队空条件size=0;

  • 方案三:不牺牲存储空间(设置tag)

#define MaxSize 10
typedef struct{
    ElemType data[MaxSize];					
    int front;					
    int rear;
    int tag;							//设置一个计数tag
}SqQueue;	
  • 初始化:rear=front=0; tag=0;插入成功:tag=1(入队);删除成功:tag=0(出队);

    队满条件:front==rear&&tag==1;队空条件tag=0

*其他出题方法(队尾)
  • 队尾指针指向队尾元素的位置:初始时:Q.rear=-1;
//入队操作(颠倒顺序)
  Q.data[Q.rear]=x;
  Q.rear=(Q.rear+1)%MaxQSize;		

//出队操作
  x=Q.data[Q.front];
  Q.front=(Q.f+1)%MaxQSize;	

//判空
  (Q.rear+1)%MAXQSIZE==Q.front;			//头指针在尾指针的后一个位置(类似循环)

//判满(方案一:牺牲一格存储单位)
  (Q.rear+2)%MAXQSIZE==Q.front;			//头指针在尾指针的后两个位置
//判满方案二:增加辅助变量

3.2.3 队列的链式实现

*链式队列的定义
typedef struct LinkNode{				//定义链式队列的结点
	ElemType data;
	struct LinkNode *next;
}LinkNode;	

typedef struct{							//定义链式队列
    LinkNode *front,*rear;				
}LinkQueue;
  • 链队列——链式存储实现的队列
*链式队列的初始化(带头结点)
typedef struct LinkNode{
	ElemType data;
	struct LinkNode *next;
}LinkNode;	

typedef struct{
    LinkNode *front,*rear;
}LinkQueue;

//初始化队列(带头结点)
void InitQueue(LinkQueue &Q){
    Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));		//初始时,头尾指针都指向头结点
    Q.front->next=NULL;
}

//判断队列是否为空
bool IsEmpty(LinkQueue){
    if(Q.front==Q.rear)
        return true;
    else
        return false;
}

void testLinkQueue(){
    LinkQueue();						//声明一个队列
    InitQueue(Q)						//初始化队列
    //...后续操作...
}
*链式队列的初始化(不带头结点)
void InitQueue(LinkQueue &Q){
    Q.front=NULL;						//初始时,头尾指针都指向NULL
    Q.rear=NULL;
}

bool IsEmpty(LinkQueue){
    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;
    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;
    Q.front->next=p->next;
    if(Q.rear==p)						//此次是最后一个结点出队
        Q.rear=Q.front;					//头尾指针均指向头结点,为空状态
    free(p);
    return true;
}
*链式队列——出队(不带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){
    if(Q.front==NULL)
        return false;
    LinkNode *p=Q.front;				//出队为头指针结点
    x=p->data;
    Q.front=p->next;
    if(Q.rear==p){						//此次是最后一个结点出队
        Q.rear=NULL;
    	Q.front=NULL;
    }
    free(p);
    return true;
}
  • 队列满的情况:顺序存储——预分配的空间耗尽时队满;链式存储——一般不会满队,除非内存不足

3.2.4 双端队列

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

    输入受限的双端队列:只允许从一端插入、两端删除的线性表;输出受限的双端队列:只允许从两端插入、一端删除的线性表

  • 考点:判断输出序列的000合法性——考特兰数:C n(上) 2 n(下) /(n+1)

3.3 栈的运用

3.3.1 栈在括号匹配中的应用

  • IDE :可视化编程环境

  • 栈的特性——最后出现的左括号最先被匹配

    遇到左括号:入栈;遇到右括号:出栈“消耗”一个左括号

#define MaxSize 10
typedef struct{
    char data[MaxSize];
    int top;							//栈顶指针
}

bool bracketCheck(char str[],int length){
    SqStack S;							//定义一个栈
    InitStack(S);						//初始化一个栈
    for(i=0;i<length;i++){				//下标从0开始
        if(str[i]=='('||str[i]=='{'||str[i]=='['){
            Push(S,str[i]);				//入栈左括号
        }else{
            if(EmptyStack(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);				//检索完后,栈空才说明成功
}
  • 匹配失败的情况:左右括号不匹配;左右括号单身

3.3.2 栈在表达式求值中的应用

  • 算数表达式的三个组成部分:操作数、运算符、界限

    例子:a+b-c*d(先算a加b,再算c乘d,再算减法)

  • 中缀表达式:a+b-c*d;后缀表达式:ab+cd *-;前缀表达式:- +ab * cd

    后缀表达式(逆波兰表达式)——运算符在两个操作数后面

    前缀表达式(波兰表达式)——运算符在两个操作数前面

*中缀表达式转前缀表达式
  • 手算:运算符和两个操作数组合成为一个新的操作数,再参与下次运算

    “右优先”原则:只要右边的运算符能先计算,就优先算右边的;让前缀表达式的运算符从右到左依次生效

*前缀表达式的计算
  • 手算:从右开始,第一个运算符及其右边两个操作数开始运算
  • 机算:
    1. 从右往左依次扫描元素
    2. 遇到操作数:直接压入栈
    3. 遇到运算符:弹出两个栈顶元素,执行相应运算,结果压回栈顶
  • 注意:先弹出栈的是“左操作数”;除法和减法部分需要注意
*中缀表达式转后缀表达式
  • 手算:两个操作数和运算符组合成为一个新的操作数,再参与下次运算

    “左优先”原则:只要左边的运算符能先计算,就优先算左边的,可保证运算顺序一致

  • 机算:

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

    从左到右依次扫描各个元素,直到末尾(多种方法)

    1. 遇到操作数:直接加入后缀表达式
    2. 遇到界限符:遇到“(”直接入栈;遇到“)”依次弹出栈内运算符,并加入后缀表达式,直到遇到“(”
    3. 遇到运算符:依次弹出栈中优先级高于等于当前运算符的所有运算符,并加入后缀表达式,若遇到“(”或栈空则停止。之后再把当前运算符入栈
    4. 上述处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式
  • 注意:后缀表达式中没有左右括号

*后缀表达式的计算
  • 手算:从左开始,第一个运算符及其左边两个操作数开始运算
  • 机算:
    1. 从左往右依次扫描元素
    2. 遇到操作数:直接压入栈
    3. 遇到运算符:弹出两个栈顶元素,执行相应运算,结果压回栈顶
  • 注意:先弹出栈的是“右操作数”
*中缀表达式的计算
  • 中缀表达式计算:中缀转后缀+后缀表达式求值

  • 初始化两个栈——操作数栈和运算符栈

    1. 遇到操作数:压入操作栈
    2. 遇到运算符或界限符:按照“中缀转后缀”压入运算符栈;期间每弹出运算符,就需要再弹出两个数运算,再压回操作数栈
    3. 最后的运算结果在操作数栈顶

3.3.3 栈在递归中的应用

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

    函数调用时,需要用一个栈(函数调用栈)存储——调用返回地址、实参、局部变量

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

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

    (进入:return到下一个要计算的函数;退出:return到上一个函数的结果)

*递归算法求阶乘
int factorial(int i){
    if(n==0||n==1)
        return 1;					//当n减到1时,返回1到factorial(1+1)函数中算(1+1)*fac(1)
    else
        return n*factorial(n-1);	//先递减进入函数,再递加计算每一层函数
}

int main(){
    //...其他代码
    int x=factorial(10);
}
  • 缺点:太多层递归会导致栈溢出,空间复杂度太高;可能包含很多重复运算

    可以自定义栈将递归算法改造成非递归算法

3.3.4 队列的应用

  • 队列应用——树的层次遍历、图的广度优先遍历

  • 队列在操作系统中的应用——CPU:先来先服务(FCFS)

3.4 特殊矩阵的压缩存储

  • 数组的存储结构:一维数组、二维数组

    特殊矩阵:对称矩阵、三角矩阵、三对角矩阵、稀疏矩阵

  • 注意:除非特殊说明,否则数组下标默认从0开始

  1. 二维数组:b[2] [4]表示2行4列的数组
  2. 普通矩阵:可用二维数组存储;注意描述矩阵元素时通常从1开始,数组从0开始
  3. 对称矩阵:存储位置行数和列数相等,关于对角线对称;下三角区(i>j)\上三角区(i<j),计算时可用只存储主对角线+上(下)对角线;按行优先原则
  4. 三角矩阵:存储在主对角线和上(下)三角区,其余元素都为常量c;计算时按行优先原则与对称矩阵相似,并在最后一个位置存储常量c
  5. 三对角矩阵:行号和列号绝对值差大于1,其余为0;按行优先原则,只有头尾存储2个元素,其余为3个元素;计算时先求行号,列号最多与行号差1
  6. 稀疏矩阵:顺序存储——三元组<行,列,值>;链式存储——十字链表法<行,列,值><指向同列下一元素,指向同行下一元素>;包含向右域right和向下域down分别指向对应行列的第一个元素
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值