408数据结构知识点——第三章 栈、队列和数组


注:内容参考王道2024考研复习指导以及《数据结构》

栈的基本概念

栈的定义

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

逻辑结构与普通的线性表相同;插入、删除操作有区别。

名词:空栈、栈顶、栈顶元素、栈底、栈底元素

image-20240119082937652

常考题型

进栈顺序: a → b → c → d → e a \rightarrow b \rightarrow c \rightarrow d \rightarrow e abcde有哪些合法的出栈顺序?

n个不同元素进栈,出栈元素不同排列的个数为卡特兰(Catalan)数= 1 n + 1 C 2 n n \frac{1}{n+1}C^n_{2n} n+11C2nn

栈的基本操作

  • InitStack(&S):初始化一个空栈S。
  • StackEmpty(S):判断一个栈是否为空,若栈s为空则返回true,否则返回false。
  • Push(&S,x):进栈,若栈s未满,则将x加入使之成为新栈顶。
  • Pop(&S,&x):出栈,若栈s非空,则弹出栈顶元素,并用x返回。
  • GetTop(S,&x):读栈顶元素,但不出栈,若栈S非空,则用x返回栈顶元素。
  • DestroyStack(&S):销毁栈,并释放栈s占用的存储空间(“&”表示引用调用)。

栈的顺序存储结构

顺序栈的实现

#define MaxSize 10
typedef struct{//顺序栈的定义
    ElemType data[MaxSize];
    int top;//此处top指示真正的栈顶元素之上的下标地址
}SqStack;

顺序栈的基本操作

//初始化栈
void InitStack(SqStack &S){
    S.top=0;
}
//栈的判空操作
bool StackEmpty(SqStack S){
    return (S.top==0);
}
//进栈操作
bool Push(SqStack &S,ElemType x){
    if(top==MaxSize){
        return false;
    }
    S.data[S.top++]=x;
    return true;
}
//出栈操作
bool Pop(SqStack &S,ElemType &x){
    if(top==0){
        return false;
    }
    x=S.data[--S.top];
   return true;
}
//取栈顶元素
bool GetTop(SqStack &S,ElemType &x){
    if(top==0){
        return false;
    }
    x=S.data[S.top];//读取栈顶元素不需要指针下移
   return true;
}

共享栈

两个栈共享同一片空间,栈满条件为 t o p 0 + 1 = t o p 1 top0+1=top1 top0+1=top1

#define MaxSize 10
typedef struct{//顺序栈的定义
    ElemType data[MaxSize];
    int top0;
    int top1;
}SqStack;
//初始化栈
void InitStack(SqStack &S){
    S.top0=-1;
    S.top1=MaxSize;
}

共享栈是为了更有效的利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。

栈的链式存储结构

typedef struct LinkNode{//链栈的定义
    ElemType data;
    struct LinkNode *next;
}LNode,*LinkStack;
//初始化操作(不带头结点)
void InitStack(LinkStack &S){
    S=NULL;
}
//进栈操作
bool Push(LinkStack &S,ElemType x){
    LNode *p=(LNode *)malloc(sizeof(LNode));
    if (!p) {
        return false; // 内存分配失败
    }
    p->data=x;
    p->next=NULL;
    if(S==NULL){
        S=p;
        return true;
    }
    p->next=S;
    S=p;
	return true;
}
//出栈操作
bool Pop(LinkStack &S,ElemType &x){
    if(S==NULL){
        return false;
    }
    LNode *p=S;
    x=p->data;
    S=p->next;
    free(p);
    return true;
}
//取栈顶元素
bool GetTop(LinkStack &S,ElemType &x){
    if(S==NULL){
        return false;
    }
    x=S->data;
   return true;
}

队列

队列的基本概念

队列的定义

队列(Queue)只允许在一端进行插入,在另一端删除操作的线性表。

名词:队头、队头元素、队尾、队尾元素、空队列

image-20240119091456923

队列的基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列Q。
  • QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false
  • EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
  • DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。
  • GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。

队列的顺序存储结构

队列的顺序存储

#define MaxSize 10
typedef struct{
    ElemType data[MaxSize];
    int front;
    int rear;
}SqQueue;

循环队列

顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。当队首指针Q.front=MaxSize-1后,再前进一个位置就自动到0,这可以利用除法取余运算(&)来实现。

循环队列的操作

//初始化操作
void InitQueue(SqQueuq &Q){
    Q.front=Q.rear=0;
}
bool QueueEmpty(SqQueue Q){
    return (Q.front==Q.rear);
}
//入队操作
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;//循环队列处理方式
    return true;
}
//出队操作
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 true;
}
bool  GetHead(SqQueue Q,ElemType &x){
    if(Q.front==Q.rear){
        return false;
    }
    x=Q.data[Q.front];
    return true;
}

判断队列已满/已空的方案(循环队列)

  1. 舍弃一个存储空间,如上代码中所示进行判断,此时队列元素个数 = ( r e a r + M a x S i z e − f r o n t ) % M a x S i z e =(rear+MaxSize-front) \% MaxSize =(rear+MaxSizefront)%MaxSize
  2. 在定义队列结构时,加入一个size指标,记录队列长度,队满条件为 s i z e = = M a x S i z e size==MaxSize size==MaxSize,此时队列元素个数 = s i z e =size =size
  3. 在定义队列结构时,加入一个tag指标(每次删除成功,tag=0;每次插入成功,tag=1),此时队满条件为 f r o n t = = r e a r & & t a g = 1 front==rear\&\& tag=1 front==rear&&tag=1

其他出题方式

改变队头和队尾指针的指向,面对不同的指向,如何判空和判满,此时如何计算队列中元素的个数。

队列的链式存储结构

队列的链式存储

队列的链式表示称为链队列,它实际上是一个同时有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点。

不带头结点时,判空(Q.front==NULL && Q.rear==NULL)

链式队列适合于数据元素变动比较大的情形,而且不存在队列满且产生溢出的问题。

程序中要使用多个队列,与多个栈的情形一样,使用链式队列,不会出现存储分配不合理和“溢出”的问题。

image-20240309230111306

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

typedef struct {
    LNode *front;
    LNode *rear;
}LinkQueue;

链式队列的基本操作

//初始化操作(带头结点)
void InitStack(LinkQueue &Q){
    Q.front=Q.rear=(LNode *)malloc(sizeof(LNode));
    q.font->next=NULL;
}
bool QueueEmpty(LinkQueue Q){
    return (Q,.front==Q.rear);
}
//入队
void EnQueue(LinkQueue %Q,ELemType x){
    LNode *p=(LNode *)malloc(sizeof(LinkNode));
    p->data=x;
    p->next=NULL;
    Q.rear->next=p;
    Q.rear=p;
}
//出队
void DeQueue(LinkQueue %Q,ELemType &x){
    if(Q.front==Q.rear){
        return false;
    }
    LNode *p=Q.ftont->next;
    x=p->data;
    Q.front->next=p->next;
    if(Q.rear==p){
        Q.rear=Q.front;
    }
    free(p);
    return true;
}

双端队列

只允许从两端插入、两端删除的线性表。

若只使用其中一端的插入、删除操作,则效果等同于栈。

image-20240119094630136

受限的双端队列

image-20240119094720213

考点:判断输出序列的合法性(注:在栈中合法的输出序列,在双端队列中必定合法)。

栈和队列的应用

栈在括号匹配中的应用

问题

假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序随意,即:

  1. ( [ ] ( ) )或[ ( [ ] [ ] ) ]为正确格式;
  2. [ ( ] )为错误格式;
  3. ( [ ( ) )或( ( ) ] )为错误格式。

分析

可用实现该特性。最后出现的左括号最先被匹配(LIFO),每出现一个右括号,就 “消耗”一个左括号。

依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。

匹配失败情况:①左括号单身②右括号单身③左右括号不匹配。

算法流程图

image-20240120085050883

代码实现

bool bracketCheck(char str[],int length){
    SqStack S;
    InitStack(S);
    for(int i=0;i<length,i++){
        if(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;
            }
        }
    }
    return StackEmpty(S);
}

栈在表达式求值中的应用

算数表达式组成

  • 操作数(operand):常数、标识符
  • 表达式运算符(operator):算术、逻辑、关系
  • 界限符(delimiter):括号、结束符

算数表达式种类

  • 中缀表达式:运算符在两个操作数中间
  • 后缀表达式(逆波兰式):运算符在两个操作数后面
  • 前缀表达式(波兰式):运算符在两个操作数前面

中缀表达式转后缀(手算)

  1. 确定中缀表达式中各个运算符的运算顺序(“左优先”原则:只要左边的运算符能先计算,就优先算左边的)
  2. 选择下一个运算符,按照「左操作数、右操作数、运算符」的方式组合成一个新的操作数
  3. 如果还有运算符没被处理,就继续2

中缀表达式转后缀(机算)

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

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

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

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

用栈实现后缀表达式的计算

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

若计算前缀表达式,则应该从右往左扫描,其余与计算后缀表达式相同,注意先出栈的是左操作数

算符间的优先关系

image-20240120134407503

代码实现(中缀表达式计算)

OperandType EvaluateExpression(){
    SqStack OPTR,OPND;
    InitStack (OPTR);//运算符栈
    Push(OPTR,'#') ;
    InitStack (OPND);//操作数栈
    ch=getchar();
    while (ch!= '#' || GetTop(OPTR)! = '#') {
        if (! In(ch)){
            Push(OPND,ch); ch=getchar();
        } // ch不是运算符则进栈
    else
        switch (Precede(GetTop(OPTR),ch)) {//比较优先权
            case '<' : //当前字符ch压入OPTR栈,读入下一字符ch
                Push(OPTR, ch);
                ch = getchar();
                break;
            case '>' ://弹出OPTR栈顶的运算符运算,并将运算结果入栈
                Pop(OPTR, theta);
                Pop(OPND, b);
                Pop(OPND, a);
                Push(OPND,Operate(a, theta, b));
                break;
            case '=' :    //脱括号并接收下一字符
                Pop(OPTR,x);
                ch = getchar();
                break;
        } // switch
  } // while
    return GetTop(OPND);
} // EvaluateExpression

栈在递归中的应用

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

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

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

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

缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算。

队列在舞伴问题中的应用

问题:假设在舞会上,男士和女士各自排成一队。舞会开始时,依次从男队和女队的对头各出一人配成舞伴。如果两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲。现要求写以算法模拟上述舞伴配对问题。

分析:显然,先入队的男士或女士先出队配成舞伴。因此该问题具有典型的先进先出特征,可以利用队列作为算法的数据结构。首先构造两个队列依次从队头元素出队配成舞伴某队为空,则另外一对等待着则是一下舞曲第一个可获得舞伴的人。

队列在层次遍历中的应用

该过程的简单描述如下:

  1. 根结点入队。
  2. 若队空(所有结点都已处理完毕),则结束遍历;否则重复3操作。
  3. 队列中第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回2

队列在广度优先遍历中的应用

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

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

缓冲区的逻辑结构

image-20240310093804492

多队列出队/入队操作的应用

image-20240310093748364

数组和特殊矩阵

数组的定义

数组是由n(n≥1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在n个线性关系中的序号称为该元素的下标,下标的取值范围称为数组的维界。

数组与线性表的关系

数组是线性表的推广。一维数组可视为一个线性表;二维数组可视为其元素是定长数组的线性表,以此类推。数组一旦被定义,其维数和维界就不再改变。

因此,除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。

数组的存储结构

一维数组

image-20240120140646862

各数组元素大小相同,且物理上连续存放。

数组元素a[i] 的存放地址 = L O C + i ∗ s i z e o f ( E l e m T y p e ) ( 0 ≤ i < 10 ) = LOC + i * sizeof(ElemType) (0≤i<10) =LOC+isizeof(ElemType)(0i<10)

:除非题目特别说明,否则数组下标默认从0开始 注意审题!

二维数组

image-20240120140748204

行优先存储:M行N列的二维数组中,则

b{i,j}的存储地址 = L O C + ( i ∗ N + j ) ∗ s i z e o f ( E l e m T y p e ) = LOC + (i*N + j) * sizeof(ElemType) =LOC+(iN+j)sizeof(ElemType)

列优先存储:M行N列的二维数组中,则

b{i,j}的存储地址 = L O C + ( j ∗ M + i ) ∗ s i z e o f ( E l e m T y p e ) = LOC + ( j*M+ i ) * sizeof(ElemType) =LOC+(jM+i)sizeof(ElemType)

普通矩阵的存储

对于普通矩阵可用二维数组存储,但是某些特殊矩阵可以压缩存储空间。

image-20240310100847144

特殊矩阵的压缩存储

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

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

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

对称矩阵

若 n 阶方阵中任意一个元素 a i , j a_{i,j} ai,j都有 a i , j = a j , i a_{i,j} = a_{j,i} ai,j=aj,i,则该矩阵为对称矩阵。

image-20240120141855398

压缩存储策略:只存储主对角线+下三角区(或主对角线+上三角区)。按行优先原则将各元素存入一维数组中。

image-20240120141244379

从矩阵下标推出一维数组下标,即 a i , j ( i ≥ j ) → B [ k ] a_{i,j}(i \geq j) \rightarrow B[k] ai,j(ij)B[k],则有 k = i ( i − 1 ) 2 + j − 1 k=\frac{i(i-1)}{2}+j-1 k=2i(i1)+j1
出题方法

  • 存储上三角?下三角?
  • 行优先?列优先?
  • 矩阵元素的下标从0?1?开始
  • 数组下标从0?1?开始

三角矩阵

下三角矩阵:除了主对角线和下三角区,其余的元素相同

上三角矩阵:除了主对角线和上三角区,其余的元素相同

image-20240120142005213

压缩存储策略:按行优先原则将橙色区元素存入一维数组中。并在最后一个位置存储常量C。

image-20240120142102069

从矩阵下标推出一维数组下标,即 a i , j ( i ≥ j ) → B [ k ] a_{i,j}(i \geq j) \rightarrow B[k] ai,j(ij)B[k],则有

  • 下三角矩阵KaTeX parse error: Undefined control sequence: \mbox at position 56: …-1)}{2}+j-1 & \̲m̲b̲o̲x̲{for} & i \leq…

  • 上三角矩阵KaTeX parse error: Undefined control sequence: \mbox at position 65: …)}{2}+(j-i) & \̲m̲b̲o̲x̲{for} & i \leq…

三对角矩阵

image-20240120143207125

压缩存储策略:按行优先(或列优先)原则,只存储带状部分

image-20240120143230405

从矩阵下标推出一维数组下标,即 a i , j ( i ≥ j ) → B [ k ] a_{i,j}(i \geq j) \rightarrow B[k] ai,j(ij)B[k],则有 k = 2 i + j − 3 k=2i+j-3 k=2i+j3

若已知数组下标k如何得到i,j?

分析:前i-1行共 3(i-1)-1 个元素,前i行共 3i-1 个元素,显然, 3(i-1)-1 < k+1 ≤ 3i-1

image-20240120143629799

稀疏矩阵的压缩存储

稀疏矩阵的三元组表既可以采用数组存储,又可以采用十字链表存储。当存储稀疏矩阵时,不仅要保存三元组表,而且要保存稀疏矩阵的行数、列数和非零元素的个数。

image-20240120143703935

压缩存储策略

  • 顺序存储——三元组 <行,列,值>

    稀疏矩阵压缩存储后便失去了随机存取特性。

    image-20240310143059763

  • 十字链表法

image-20240120143928596

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值