第三章 栈、队列和数组

本文详细介绍了栈和队列的数据结构,包括它们的顺序和链式存储方式,以及在括号匹配、表达式求值、递归和层次遍历等问题中的应用。同时讨论了数组和特殊矩阵,如对称矩阵、上三角矩阵的压缩存储,以及稀疏矩阵的特性与存储方法。
摘要由CSDN通过智能技术生成

一、栈(Stack)

只允许在一端插入或删除操作的线性表(先进后出)

栈的基本操作:

  • 初始化栈          InitStack(&S)
  • 判断栈是否为空    StackEmpty(S)
  • 进栈    Push(&S,x)
  • 出栈    Pop(&S,&x)
  • 获取栈顶元素       GetTop(S,&x)
  • 销毁栈    DestoryStack(&S)

公式:当n个不同元素进栈时,出栈元素不同排列的个数为\frac{1}{n+1} * \textrm{C}_{2n}^{n}

栈是一种受限的线性表,类似于线性表,也有两种存储方式。

1.栈的顺序存储结构

①顺序栈的实现

利用一组地址连续的存储单元存放自栈低到栈顶的数据元素。

设置一个指针top 指示当前栈顶元素的位置(核心

// 栈的顺序存储类型可描述为

#define MaxSize 50
typedef  struct {
    ElemType data[MaxSize];     //存放栈中元素
    int top;     //声明栈中指针
}SqStack;


整个顺序栈的所有操作都是围绕栈顶指针进行操作的。

  • 初始化
            设置S.top=-1;   栈顶元素:S.data[S.top]
  • 进栈
             栈不满时,先S.top++,后把值送给栈顶处。
  • 出栈
             栈非空时,先取栈顶元素,后S.top--
  • 栈空条件
             S.top==-1;
  • 栈满条件
             S.top==MaxSize-1
  • 栈长
             S.top+1

还有一种是设置S.top==0,这个时候需要注意所有判断的条件全部都变了,都在原有的基础上加一。

②顺序栈的基本操作

(1)初始化

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

(2)判栈空

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

(3)进栈

bool Push(SqStack &S,ElemType x){
    if(S.top==Maxsize-1)   //栈满,报错
        return false;
    S.data[++S.top]=x;    //指针先加1,再入栈
    return true;
}

(4)出栈

bool Pop(SqStack &S,ELemType &x){
    if(S.top==-1)
        return false;
    x=S.data[S.top--];    //指针先出栈,再减一
    return ture;
}

(5)读栈顶元素

bool GetTop(SqStack S,ELemType &x){
    if(S.top==-1)
        return false;
    x=S.data[S.top];    //x记录栈顶元素
    return ture;

}

若S.top刚开始初始化时为0,以上情况就会发生变化。  考题的时候要会随机应变

③共享栈

将两个帧的栈低分别设置在共享空间的两端。

栈空条件:

        top0=-1   top1=MaxSize

栈满条件:

         top1-top0=1

进栈:

        ++top0=x        --top1=x;

2.栈的链式存储结构

采用链式存储的栈称为链栈


优点:

        便于多个栈共享存储空间和提高效率,且不存在栈满上溢的情况。

实现方式:

        通常采用单链表实现,并规定所有操作都是在单链表的表头进行。规定链栈没有头结点,lhead指向栈顶元素。

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

二、队列(Queue)

是一种操作受限的线性表---先进先出

只允许在表的一端进行插入,在表的一端进行删除。


 

队列常见的基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列Q
  • QueueEmpty(Q):   判队列空,若队列Q为空返回true,否则返回false
  • EnQueue(&Q,x):    入队,若队列Q未满,插入到队尾
  • DeQueue(&Q,&x):   出队,若队列未空,将队头元素出队
  • GetHead(Q,&x):     若队列非空,获取队头元素

1.队列的顺序存储结构

①队列的顺序存储

实现方式:

        分配一块连续的存储单元存放队列中的元素,并声明两个指针:

                                                队头指针front,指向队头元素。

                                                队尾指针rear,指向队尾元素的下一个位置。

note:有些题目对front和rear的声明不一样,这个时候需要仔细分析条件。

//顺序存储类型可描述为:

#define MaxSize 50        // 定义队列元素中最大个数
typedef struct{
    ElemType data[MaxSize];    //用数组存放队列元素
    int front,reat;        // 队头指针和队尾指针
}SqQueue;


初始时: Q.front=Q.rear=0

进队操作:队若不满,将值给队尾,队尾指针加1

出队操作:队若不空,取队头元素,队头指针加1

队空条件:  Q.front==Q.rear==0

队满条件:  (暂时空着)

        假溢出现象:

                能否用Q.rear==MaxSize作为队列满的条件呢?

                显然不能,当队列先满而后出队只剩一个元素时,该条件仍然成立,此时data数组中仍然有可以存放元素的位置,我们把这种现象称为假溢出。

②循环队列

所以为了解决这种“假溢出”现象,这里引出循环队列的概念。

队列头尾相接的顺序结构称为循环队列。

条件设置

初始时:Q.front=Q.rear=0

入队:Q.front=(Q.front+1)%MaxSize

出队:Q.rear=(Q.rear+1)%MaxSize

队列长度:(Q.rear+MaxSize-Q.front)%MaxSize

Q:当队列为空时条件为Q.front=Q.rear,如果入队次数大于出队次数,那么很快就会有Q.rear=Q.front这一条件,此时队列已经满了。现在问题来了,同一个条件,却出现了两种不同的结论,这是不被允许的,那么该如何解决这种问题呢?



法一:牺牲一个单元来区分队空和队满。

        队空条件:Q.rear==Q.front

        队满条件:(Q.rear+1)%MaxSize==Q.front

        队中元素个数:(Q.rear-Q.front+MaxSize)%MaxSize

                note:rear可能比front大,也可能比front小,所以就需要加上MaxSize保证正数


法二:设置Size数据成员,表示元素个数
        队空:Q.Size=0;

        队满:Q.Size=MaxSize;


法三:增设tag数据成员,。出队时,设置tag=0。入队时,设置tag=1。

        队空:tag==0&&Q.front=Q.rear        

        队满:tag==1&&Q.front=Q.rear
        

  • 初始化
void InitQueue(SqQueue &Q)
        Q.rear=Q.front=0;    //初始化队首、队尾指针
  • 判断空
bool isEmpty(SqQueue Q)
    if(Q.rear==Q.front)      //判断队空核心语句
  • 入队
/**
    1.先判断是否队满
    2.队不满,则插入数据,队尾指针加1
*/

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 ture;
}
    
  • 出队
/**
    1.先判断队是否空
    2.队不空,队首元素出队,队头指针加一
*/

bool DeQueue(SqQueue &Q,ElemType &x){
    if(Q.front==Q.rear)
        return false;
    x=Q.data[Q.front];
    Q.front=[Q.front+1]%MaxSize;        //队头指针加一
    return ture;
}

2.队列的链式存储结构

①队列的链式存储

引入两个指针:头指针、尾指针


头指针指向队头结点,尾指针指向队尾结点。
 

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

typedef struct{
    LinkNode *front,*rear;        //队列的队头和队尾指针
}LinkQueue;


// 不带头结点时,Q.front==NULL&& Q.rear==NULL   --->链式队列位空

Q:链式队列队空的判断
 

不带头结点:

            入队时,建立一个新结点,将新结点插入到链表的尾部,并让Q.rear指向这个新插入的结点(若一开始原队列为空,则令Q.front也指向该结点)。
             出队时,首先判断队是否为空,若不空,则取出队头元素,将其从链表中摘除,并让Q.front指向下一个结点(若该结点为最后一个结点,则置Q.front和Q.rear 都为NULL)

带头结点:(这种比较方便)
              

②链式队列的基本操作

  • 初始化
void InitQueue(LinkQueue &Q){  //初始化带头结点的链队列
    Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNOde));    //建立头结点
    Q.front->next=NULL;        //初始为空
}
  • 判队空
bool IsEmpty(LinkQueue Q){
    if(Q.front==Q.rear)        //判空条件
        
}
  • 入队
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;             //修改尾指针
}
  • 出队
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 ture;
}

3.双端队列

指两端都可以进行插入和删除的线性表

常考题型:输出受限的双端队列

三、栈和队列的应用

1.栈在括号匹配中的应用

场景: 检验代码时,若没有发现成对的括号是会报错的。

算法思想:

                1.初始设置一个空栈

                1.遇到左括号入栈

                2.遇到右括号,取栈顶元素看能否与之消掉,不能消算法结束,匹配失败

                                                                  若能消,继续,算法结束时,栈为空,匹配成功

2.栈在表达式求值中的应用

①算术表达式

场景:计算一条算式的值。


概念:
        中缀表达式:操作符以中缀形式处于操作数的中间。
                        eg: 3+4  ,是人们常用的算术表达式,但不容易被计算机解析。

        前缀表达式:运算符在操作数的前面。

                        eg:+34 ,计算过程中必须用括号将操作符和对应的操作数扩起来,用于指示运算的次序。
 

        后缀表达式:运算符在操作数的后面
                        eg:34+,该表达式考虑了运算符的优先级,没有括号,只有操作数和运算符。

②中缀表达式转后缀表达式

手算思想:
        1.加括号,确定符号运算位置

        2.运算符后移

        3.去掉括号,得到后缀表达式

eg:

        


若是中缀转前缀,符号需要保存到括号的前面。

计算机中,中缀表达式需要借助一个栈用于保存暂时还不能确定运算顺序的运算符。

算法思想:

        从左到右依次扫描中缀表达式的每一项。

                ①遇到操作数,直接加入后缀表达式

                ②遇到界限符

                        若为左括号入栈

                        若为右括号,依次弹出栈中所有运算符,并加入后缀表达式。

                ③遇到运算符

                        若此刻没有运算符直接入栈

                        若有运算符,查看栈顶运算符合和当前运算符的优先级

                                优先级大于或等于,依次弹出栈内所有运算符,并加入后缀表达式。

                                优先级小于栈顶运算符,入栈即可。

③后缀表达式求值

算法思想:        

        从左往右依次扫描表达式的每一项

                若该项为操作数,压入栈中。

                若该项为运算符,则从栈中退出两个操作数Y和X。并将运算结果压入栈顶,等所有项都处理完后,栈顶存放的就是结果。

3.栈在递归中的应用

递归:若在一个函数、过程或数据结构的定义中又应用了它自己,则称它为递归。


递归的方法要进行压栈处理。包括方法中的参数,返回值。

4.队列在层次遍历中的应用

问题描述:

        在信息处理中,有一大类问题需要逐层或逐行进行处理。

eg:层次遍历二叉树的过程

        算法思想:

                1.根结点入队

                2.若队空(所有结点都已处理完毕),则结束遍历,否则重复3操作

                3.队列中第一个结点出队,并访问之。

                                                        若其有左孩子,则将左孩子入队

                                                        若其有右孩子,则将右孩子入队返回2操作
 

5.队列在计算机系统中的应用

应用一:解决主机与外部设备之间速度不匹配的问题
        eg:主机和打印机之间速度不匹配。主机输出数据给打印机打印,输出的数据比打印数据要快很多,可以设置一个打印输出缓冲区,主要把打印输出的数据依次写入这个缓冲区中,写满后CPU去完成其他的事情。然后打印机从缓冲区中排队处理数据。


应用二:多用户引起资源竞争的问题
        eg:多个用户在同一个时间都想要CPU去运行自己的程序,他们都向CPU请求资源,这个时候就需要排队处理每个用户的请求。

四、数组和特殊矩阵

1.数组

由相同类型的数据元素构成的有限序列。

对于数组的考题,场出现的题目是对二维数组的考察,考察二维数组下标的位置关系。

        Q:区分列优先存储还是行优先存储,对应的内存位置信息是不一样的。

2.特殊矩阵

压缩存储:为多个值相同的元素只分配一个存储空间,对零元素不分配空间。

特殊矩阵:具有许多相同矩阵元素或零元素,并且这些元素的分布有一定规律性的矩阵。

                eg: 对称矩阵、上(下)三角矩阵、对角矩阵


特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些规律性分布,值相同的多矩阵元素压缩到一个存储空间中。

①对称矩阵

a_{ij} = a_{ji}



分析:对于n阶对称矩阵,上三角元素和下三角元素相同,若仍采用二维数组,会浪费几乎·一半空间。


解决办法:舍弃上三角元素或下三角元素,其余元素存到一维数组里面。

所以说,我们就有必须要将在一维数组中的元素的位置和矩阵中元素的位置做出对应,这样才能还原。

假如我们现在只存下半部分的元素。

第一行:1个

第二行:2个

第三行:3个

...

第i行:j个

那么这个i行的j位于一维数组中的哪个下标K(一维数组下标从0开始)呢?

等差数列求和:  

        \frac{i(i-1)}{2} +j-1  =K  (这里一定要注意,要减一)
 

②三角矩阵


特征:某三角区中元素均为同一常量,在存元素时,不仅要存下三角区和主对角线上得元素,还要留一个位置存常量值。

假如说存下三角矩阵,数组下标K和矩阵a_{ij}之间的关系为(参考上面的式子)


假如说存上三角矩阵:

第1行:n个元素

第2行:n-1个元素

第3行:n-2个元素

...

第i-1行:n-i+2个元素

第i行:j-i+1(注意)

 所以上半区a_{ij}一共有

 

③三对角矩阵

特征:非零元素处于以主对角线为中心的3条对角线区域中。

a_{ij}在数组中的对应关系为:

第1行:2个元素

第2行:3个元素

第3行:3个

...

第i-1行:3个

第i行:1个或2个

这个时候分析K与a_{ij}之间关系的时候需要进行上面分析即可
 

3.稀疏矩阵

定义:矩阵中零元素个数远远大于非零元素个数。

存储这类矩阵数据只需要用一个三元组即可。


当存储稀疏矩阵时,不仅要保存三元组表,而且要保存稀疏矩阵的行数、列数和非零元素的个数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值