Per aspera ad astra.
循此苦旅
|

Eulbo_1018

园龄:6个月粉丝:0关注:0

第三章 栈、队列和数组

栈的基本概念

栈的定义

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

  • 栈顶:线性表允许进行插入删除的那一端
  • 栈底:固定的,不允许进行插入和删除的另一端
  • 空栈:不含任何元素的空表

栈的操作特性可以概括为:后进先出(LIFO)

栈的基本操作

InitStack(&S)//初始化栈,构造一个空栈S,分配内存空间

DestroyStack(&S)//销毁栈,销毁并释放栈S占用的空间

Push(&S,x)//进栈,若栈S未满,则将x加入使之成为新的栈顶

Pop(&S.&x)//出栈,若栈S非空,则弹出栈顶元素,并用x返回

StackEmpty(S)//判断一个栈是非为空,若栈S为空则返回true,否则返回false

GetTop(S,&x)//读栈顶元素,若站S非空,则用X返回栈顶元素

栈的数学性质

当n个不同元素进栈时,出栈元素的不同排列的个数为1n+1C2nn,这个公式称为卡特兰数公式

栈的顺序存储结构

顺序栈的实现

采用顺序存储的栈成为顺序栈,利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,用时附设一个指针(top)指示当前栈顶元素的位置

#define MaxSize 10//定义栈中元素的最大个数
typedef struct {
ElemType data[MaxSize];//静态数组存放栈中元素
int top;//栈顶指针
} SqStack;

栈顶指针:S.top,初始化设置S.top==-1;栈顶元素:S.data[S.top]

进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶

出栈操作:栈非空时,先取栈顶元素,再将栈顶指针减1

栈空条件:S.top==-1;栈满条件:S.top==MaxSize-1;栈长:S.top+1

顺序栈的基本操作

初始化

void InitStack(SqStack &S) {
S.top == -1;
}

判断空

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;
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--;
return true;
}

读栈顶元素

bool GetTop(SqStack S, ElemType &x) {
if (S.top == -1) {
return false;
}
x = S.data[S.top];
return true;
}

共享栈

利用栈底位置相对不变的特性,可让两个顺序栈共享一个以为数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间延伸。
image

两个栈的栈顶指针都指向栈顶元素,top1=-1时0号栈为空,top1=MaxSize时1号栈为空;仅当两个栈顶指针相邻(top1-==top0+1)时,判断为栈满。当0号栈进栈时top0先加1再赋值,1号栈进栈时top1先减1再赋值;出栈时则相反。
共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢,其存取数据的时间复杂度均为O(1),所以对存取效率没有影响。

上溢是指存储器满,还往里写;下溢是指存储器空,还往外读

栈的链式存储结构

采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提个效率,且不存在栈满上溢的情况,通常采用单链表实现,并规定所有操作都是在单链表的表头进行,Lhead指向栈顶元素

typedef struct LinkNode {
ElemType data;
struct LinkNode* next;
} LiStack;

采用链式存储,便于结点的插入和删除,栈顶的操作与链表类似,入栈和出栈的操作都在链表的表头进行。

队列

队列的基本概念

队列的定义

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

  • 队头:允许删除的一端,又称队首
  • 队尾:允许插入的一端
  • 空队列:不含任何元素的空表
    队列的操作特性可以概括为:先进先出(FIFO)

队列的基本操作

InitQueue(&Q)//初始化队列,构造一个空队列Q

DestroyQueue(&Q)//销毁队列,销毁并释放队列Q所占用的内存空间

QueueEmpty(Q)//判队列空,若队列为空返回true,否则返回false

EnQueue(&Q,x)//入队,若队列Q未满,将x加入,使之成为新的队尾

DeQueue(&Q,&x)//出队,若队列Q非空,删除队头元素,并用x返回

GetHead(Q,&x)//读队头元素,若队列Q非空,则将队头元素赋给x

队列的顺序存储结构

队列的顺序存储

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并设两个指针:队头指针front指向队头元素,队尾指针rear指向队尾元素的下一个位置或者队尾元素

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

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

进队操作:队不满时,先送值到队尾元素,再将队尾指针加1

出队操作:队非空时,先取队头元素值,再将队头指针加1

队列满的判断条件不能直接用Q.rear==MaxSize,因为在队列的一系列入队操作中会穿插出队操作,因此,当Q.rear==MaxSize时并不能确定Q.front是否还指向第一个元素,如果已经执行了出队操作,就会有队头指针指向元素的前面是空的,此时就算Q.rear==MaxSize的条件成立也不能说明队列满了。此时入队出现“上溢出”,但这种溢出并不是真正的溢出,再data数组中依然存在可以存放元素的空位置,所以是一种“假溢出”。

循环队列

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

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

队首指针进1:Q.front = (Q.front+1) % MaxSize

队尾指针进1:Q.rear = (Q.front+1) % MaxSize

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

出队入队时:指针都按顺序方向进1

image

循环队列的判空和判满

区分是队空还是队满的情况,有三种处理方式:

  1. 牺牲掉一个单元来区分队空和队满,入队是少用一个队列单元,这是一种较为普遍的做法,约定以“队头指针在队尾指针的下一个位置作为队满的标志”

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

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

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

  2. 类型中增设size数据成员,表示元素个数,删除成功size减1,插入成功size加1。队空时Q.size == 0;队满时Q.front == MaxSize,两种情况都有Q.front == Q.rear

  3. 类型中增设tag数据成员,以区分是队满还是队空,删除成功置tag = 0,若导致Q.front == Q.rear,则为队空;插入成功置tag = 1,若导致Q.front == Q.rear,则为队满。

循环队列的基本操作

初始化

void InitQueue(SqQueue &Q) {
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;//队尾指针加1取模
return true;
}

出队

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

读取队头元素

bool GetHead(SqQueue Q, ElemType &x) {
if (Q.rear == Q.front) {//队空报错
return false;
}
x = Q.data[Q.front];
return true;
}

队列的链式存储结构

队列的链式存储

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

typedef struct LinkNode {//链式队列结点
ElemType data;
struct LinkNode *next;
} LinkNode;
typedef struct { //链式队列
LinkNode *front, *rear; //队列的头指针和队尾指针
} LinQueue;

链式队列的基本操作

初始化(带头结点)

void InitQueue_h(LinkQueue &Q) {
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}

初始化(不带头结点)

void InitQueue(LinkQueue &Q) {
Q.front = NULL;
Q.rear = NULL;
}

判队空(带头结点)

bool IsEmpty_h(LinkQueue Q) {
if (Q.front == Q.rear) {
return true;
} else {
return false;
}
}

判队空(不带头结点)

bool IsEmpty(LinkQueue Q) {
if (Q.front == NULL) {
return true;
} else {
return false;
}
}

入队(带头结点)

void EnQueue_h(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;//新结点插入到rear结点之后
Q.rear = s;//修改rear指针
}
}

出队(带头结点)

bool DeQueue_h(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;
}

出队(不带头结点)

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.rear = NULL;//rear改为NULL
Q.front = NULL;//front改为NULL
}
free(p);//释放空间
return true;
}

双端队列

双端队列是指允许两端都可以进行插入和删除操作的线性表。将左端

  • 输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列(一端插入,两端删除
  • 输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列。(两端插入,一端删除)若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈。

栈和队列的应用

栈在括号匹配中的应用

算法思想如下:

  1. 初始设置一个空栈,顺序读入括号
  2. 若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的栈中所有为消解的期待的急迫性降了一级
  3. 若有右括号,则或使置于栈顶的最急迫期待得以消解,或是不合法的情况(括号序列不匹配,退出程序)。算法结束时,栈为空,否则括号序列不匹配

代码实现如下:

#include<stdio.h>
#include<stdlib.h>
#define MaxSize 10
typedef struct {
char data[MaxSize];
int top;
} SqStack;
//初始化栈
void InitStack(SqStack &S);
//判断栈为空
bool StackEmpty(SqStack S);
//新元素入栈
bool Push(SqStack &S, char x);
//栈顶元素出栈
bool Pop(SqStack &S, char &x);
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);
}

栈在表达式求值中的应用

算术表达式

中缀表示式 后缀表达式 前缀表达式
运算符在两个操作数中间 运算符在两个操作数后面 运算符在两个操作数前面
a+b ab+ +ab
a+b-c ab+c-(或者a bc- +) - +ab c(或者+a -bc)
a+b-c*d ab+ cd* - - +ab *cd

中缀表达式转后缀表达式

中缀表达式转后缀表达式的手算方法

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

运算符顺序不唯一,因此对应的后缀表达式也不唯一

为了保证计算的输出结果确定,按照“左优先”原则:只要左边的运算符能先运算,就优先算左边的

中缀表达式转后缀表达式计算方法

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

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

  • 遇到操作数。直接加入后缀表达式
  • 遇到界限符。若为“(”,则直接入栈,若为“)”,则依次弹出栈中的运算符,并加入后缀表达式,直到弹出“(”为止。注意:“(”直接删除,不加入后缀表达式
  • 遇到运算符。若其优先级高于除“(”外的暂定运算符,则直接入栈,否则,从栈顶开始依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,直到遇到一个优先级低于它的运算符或遇到“(”为止,之后将当前运算符入栈

按上述方法扫描所有字符后,将占中剩余运算符依次弹出,并加入后缀表达式

中缀表达式转前缀表达式

中缀表达式转前缀表达式的手算方法

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

运算符顺序不唯一,因此对应的后缀表达式也不唯一

为了保证计算的输出结果确定,按照“右优先”原则:只要右边的运算符能先运算,就优先算右边的

后缀表达式求值

后缀表达式的手算方法:

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

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

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

前缀表达式求值

前缀表达式的手算方式:

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

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

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

中缀表达式求值

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

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

栈在递归中的应用

适合用“栈”解决的问题:可以把原始问题转化为属性相同,但规模较小的问题

栈在函数调用中的作用和工作原理

在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等,而其效率不高的原因是递归调用过程中包含很多重复的计算。

可以将递归算法转换为非递归算法,通常需要借助栈来实现这种转换,但并不是绝对(计算斐波那契额迭代实现只需要一个循环即可实现)

队列在层次遍历中的应用

当需要进行逐层或逐行处理,这类问题的解决方法往往值在处理当前层或当前行时就对下一层进行预处理,把处理顺序安排好,等到当前层或当前行处理完毕,就可以处理下一层或者下一行,使用队列是为了保存下一步的处理顺序。如二叉树的层次遍历,图的广度优先算法

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

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

缓冲区的逻辑结构

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

数组和特殊矩阵

数组的定义

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

数组的存储结构

对于多维数组,有两种映射方式:按照行优先和按列优先。以二维数组为例,按行优先存储的基本思想是:先行后列,先存储行号较小的元素,行号相等先存储列号较小的元素。设二位数组的行下标和列下标的范围分别为[0,h1]与[0,h2],存储结构关系式为:

LOC(ai,j)=LOC(a0,0)+[i×(h2+1)+j]×L

当以列优先方式存储时,得出存储结构关系式为

LOC(ai,j)=LOC(a0,0)+[j×(h1+1)+i]×L

特殊矩阵的压缩存储

对称矩阵的压缩存储

若n阶方阵中存储一个元素aij都有aij=aji,则该矩阵为对称矩阵

image

存储策略:只存储主对角线+下三角区(或主对角线+上三角区),按照行优先原则将各元素存入到长度为(1+n)×n2的一维数组中

下三角压缩:

k={i(i1)2+j1线j(j1)2+i1

三角矩阵的压缩存储

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

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

压缩策略:按照行优先原则将各元素存入到长度为(1+n)×n2+1的一维数组中,并在最后一个位置存储常数

下三角矩阵:

k={i(i1)2+j1线n(n+1)2

上三角矩阵:

k={(2ni+2)(i1)2+(ji)线n(n+1)2

三对角矩阵的压缩存储

三对角矩阵又称带状矩阵,当|ij|>1时,aij=0

在三对角矩阵中,所有非零元素都集中在以主对角线为中心的三条对角线的区域,其他区域的元素都为零。

压缩策略:将三条对角线上的元素啊按行优先方式存放在长度为3n-2的一维数组中

k=2i+j3

稀疏矩阵

非零元素的个数远远少于矩阵元素的个数

压缩策略:将非零元素及其相应的行和列构成一个三元组
稀疏矩阵压缩存储后便失去了随机存储特性

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

posted @   Eulbo_1018  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起