第三章栈、队列和数组
3.1栈
3.1.1栈的基本概念
1,定义
线性表是具有相同数据类型的n(n20)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。
栈(Stack)是只允许在一端进行插入或删除操作的线性表。
术语
- 栈顶:
允许插入和删除的一端
- 栈底:
不允许插入和删除的一端
- 空栈:
特性:后进先出(LIFO)
2,基本操作
InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间
Listlnsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i&e):删除操作。删除表L中第1个位置的元素,并用e返回删除元素的值。
LocateElem(L.e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
其他常用操作:
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
3.1.2栈的顺序存储实现(top=-1,指向当前元素)
顺序存储,用静态数组实现,并需要记录栈顶指针。
//顺序栈的定义
#define MaxSize 10
typedef struct
{
ElmType data[MaxSize];//静态数组存放栈中元素
int top;//栈顶指针,指向此时栈顶元素的位置
}SqStack;
1,基本操作
1,创(初始化)
void InitStack(SqStack &S)
{
S.top = -1;//初始化栈顶指针
}
2,增(进栈)
先判断栈是否满了
//新元素入栈
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;
}
3,删(出栈)
数据还残留在内存中,只是逻辑上被删除了
//出栈操作
bool Pop(SqStack &S, ElemType &x)
{
if(S.top == -1)\\栈空,报错
return false;
x = S.data[S.top];
S.top--;
return true;
}
4,查(获取栈顶元素)
//读栈顶元素
bool GetTop(SqStack S, ElemType &x)
{
if(S.top == -1)//栈空报错
return false;
x = S.data[s.top];
retur true;
}
5,判空、判满
//判断栈空
bool StackEmpty(SqStack S)
{
if(S.top == -1)
return true;
else
return false;
}
2,两种实现
初始化时top=0和top=-1
前者代表栈顶指针指向下一个要插入的位置
后者指向当前元素的位置
3,共享栈
两个栈共享同一片内存空间,两个栈从两边往中间增长
初始化
0号栈栈顶指针初始时 top0=-1;
1号栈栈顶指针初始时 top1=MaxSize;
栈满条件
top0+ 1 == top1;
3.1.3栈的链式存储实现
跟单链表差不多,限制只能头插,删除也只能删除第一个元素
1,定义
用链式存储方式实现的栈
2,带头结点
两种实现方式
不带头结点(推荐)
3,重要基本操作
创(初始化)
增(进栈)
删(出栈)
查(获取栈顶元素)
如何判空、判满?
3.2队列
3.2.1队列的基本概念
1,定义
队列(Queue)是只允许在一端进行插入,在另一端删除的线性表。
特点:
先进入队列的元素先出队(FIFO)。
术语:
队头,队尾,空队列
队头:允许删除的一端
队尾:允许插入的一瑞
2,基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间
EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。
其他常用操作:
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
3.2.2队列的顺序存储实现
1,队列的实现
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front,rear;//队头指针和队尾指针
}SqQueue;
分配一块连续的存储单元存放队列中的元素,并附设两个指针
队尾指针:指向队尾元素的后一个位置(下一个应该插入的位置)。
队头指针:指向队头元素。
2,基本操作
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
出队操作:队不空时,先取队头元素值,再将队头指针加1
1,初始化
//初始化队列
void InitQueu(SqQueue &Q)
{
//初始化时,队头、队尾指针指向0
Q.read = Q.front =0;
}
2,判空
bool QueueEmpty(SqQueue Q)
{
if(Q.rear == Q.front0)
return true;
else
return false;
}
3,增/删(入队/出队操作)
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
出队操作:队不空时,先队头元素值,再将队头指针加1
假溢出:这种溢出并不是真正的溢出,在data数组中依然存在可以存放元素的空位置
3,循环队列
把存储队列元素的表从逻辑上视为一个环,称为循环队列
初始时:Q.front=Q.rear=0
队首指针进1:Q.front=(Q.front+1)%MaxSize
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize.
判断条件:
队空:Q.front=Q.rear
队满:
牺牲一个单元来区分队空和队满,即”队头指针在队尾指针的下一位置作为队满的标志”。
类型中增设表示元素个数的数据成员。
类型中增设tag数据成员,以区分是队满还是队空
3.2.3队列的链式存储实现
1,队列的实现
typedef struct LinkNode//链式队列节点
{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{ //链式队列
LinkNode *front, *rear;//队列的头和尾指针
}LinkQueue;
2,基本操作
1,创(初始化)
//初始化队列(带头节点)
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)
{
//初始化时front、rear都指向NULL
Q.front = NULL;
Q.front = NULL;
}
//判断队列是否为空(带头节点)
bool IsEmpty(LinkQueue Q)
{
if(Q.front == NULL)
return true;
else
return false;
}
2,增(入队)
//新元素入队(带头结点)
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;//新节点插入到rear之后
Q.rear = s;//修改表尾指针
}
}
3,删(出队)
//队头元素出队(带头节点)
bool DeQueu(LinkQueue &Q, ElemType &x)
{
if(Q.front == Q.rear)//空队
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;//修改头节点的next指针
if(Q.rear == p)//如果此次是最后一个节点出队
Q.rear = Q.front;//修改rear指针的指向
free(p);
return true;
}
//队头元素出队(不带头节点)
bool DeQueu(LinkQueue &Q, ElemType &x)
{
if(Q.front == Q.rear)//空队
return false;
LinkNode *p = Q.front;
x = p->data;
Q.front = p->next;//修改头节点的next指针
if(Q.rear == p)//如果此次是最后一个节点出队
{
Q.rear = NULL;
Q.front = NULL;
}
free(p);
return true;
}
4,查(获取头元素)
5,判满
链式存储——一般不会队满,除非内存不足
3.2.4双端队列
栈:只允许从一端插入和删除的线性表
队列:只允许从一端插入、另一端删除的线性表
双端队列:只允许从两端插入、两端删除的线性表(若只使用其中一端的插入,删除操作,效果等同于栈)
输入受限的双端队列:允许从两端删除、从一端插入的队列
输出受限的双端队列:允许从两端插入、从一端删除的队列
考点:对输出序列合法性的判断(在栈中合法的输出序列,在双端队列中必定合法)
3.3栈和队列的应用
3.3.1栈在括号匹配中的应用
用栈实现括号匹配:
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。
匹配失败情况:①左括号单身②右括号单身③左右括号不匹配
3.3.2栈在表达式求值中的应用(上)
1,概念
运算符、操作数、界限符(DIY概念:左操作数/右操作数)
2,三种表达式
中缀表达式:运算符在操作数中间
后缀表达式(逆波兰式):运算符在操作数后面
前缀表达式(波兰式):运算符在操作数前面
3,后缀表达式考点
1,中缀转后缀(手算)
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「左操作数右操作数运算符]的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
“左优先”原则:只要左边的运算符能先计算,就优先算左边的(可保证运算顺序唯一)
2,后缀转中缀(手算)
从左往右扫描,每遇到一个运算符,就将<左操作数 右操作数 运算符>变为(左操作数 运算符 右操作数)的形式。
3,后缀表达式的计算(机算)
用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③(注意:先出栈的是“右操作数”)
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
4,前缀表达式
1,中缀转前缀
中缀转前缀的手算方法:
① 确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「运算符左操作数右操作数】的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②右边
“右优先”原则:只要右边的运算符能先计算,就优先算右边的
2,计算
用栈实现前缀表达式的计算:
①从右往左扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③(注意:先出栈的是“左操作数”)
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
3.3.3栈在表达式求值中的应用(下)
1,用栈实现中缀表达式转后缀表达式:
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:
①遇到操作数。直接加入后缀表达式。
②遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
③遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
2,用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
3,用栈实现中缀表达式的计算:
初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
3.3.4栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个“函数调用栈”存储:
①调用返回地址②实参③局部变量
递归调用时,函数调用栈可称为“递归工作栈
”每进入一层递归,就将递归调用所需信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息
缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算。
3.3.5队列的应用
树的层次遍历
图的广度优先遍历
实现先来先服务管理调度策略
3.4特殊矩阵的压缩存储
1,对称矩阵
1,特点
若n阶方阵中任意一个元素aij都有ai,j = aj,i则该矩阵为对称矩阵
2,压缩存储策略:
只存储主对角线+下三角区(或主对角线+上三角区)
按行优先或者按列优先
2,三角矩阵
1,特点
下三角矩阵:除了主对角线和下三角区,其余的元素都相同
上三角矩阵:除了主对角线和上三角区,其余的元素都相同
2,压缩
按行优先/列优先规则依次存储非常量区域,并在最后一个位置存放常量c
3,三对角矩阵(带状矩阵)
1,特点
当li-jl>1时,有ai,j=0(1<= i, j<=n)
2,压缩
按行优先/列优先规则依次存储带状区域
4,稀疏矩阵
1,特点
非零元素远远少于矩阵元素的个数
2,策略
策略一:
顺序存储——三元组<行,列,值>
策略二:
链式存储——十字链表法