文章目录
注:内容参考王道2024考研复习指导以及《数据结构》
栈
栈的基本概念
栈的定义
栈(Stack)是只允许在一端进行插入或删除操作的线性表。
逻辑结构与普通的线性表相同;插入、删除操作有区别。
名词:空栈、栈顶、栈顶元素、栈底、栈底元素
常考题型:
进栈顺序: a → b → c → d → e a \rightarrow b \rightarrow c \rightarrow d \rightarrow e a→b→c→d→e有哪些合法的出栈顺序?
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)是只允许在一端进行插入,在另一端删除操作的线性表。
名词:队头、队头元素、队尾、队尾元素、空队列
队列的基本操作
- 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;
}
判断队列已满/已空的方案(循环队列):
- 舍弃一个存储空间,如上代码中所示进行判断,此时队列元素个数 = ( 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+MaxSize−front)%MaxSize。
- 在定义队列结构时,加入一个size指标,记录队列长度,队满条件为 s i z e = = M a x S i z e size==MaxSize size==MaxSize,此时队列元素个数 = s i z e =size =size。
- 在定义队列结构时,加入一个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)
链式队列适合于数据元素变动比较大的情形,而且不存在队列满且产生溢出的问题。
程序中要使用多个队列,与多个栈的情形一样,使用链式队列,不会出现存储分配不合理和“溢出”的问题。
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;
}
双端队列
只允许从两端插入、两端删除的线性表。
若只使用其中一端的插入、删除操作,则效果等同于栈。
受限的双端队列:
考点:判断输出序列的合法性(注:在栈中合法的输出序列,在双端队列中必定合法)。
栈和队列的应用
栈在括号匹配中的应用
问题:
假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序随意,即:
- ( [ ] ( ) )或[ ( [ ] [ ] ) ]为正确格式;
- [ ( ] )为错误格式;
- ( [ ( ) )或( ( ) ] )为错误格式。
分析:
可用栈实现该特性。最后出现的左括号最先被匹配(LIFO),每出现一个右括号,就 “消耗”一个左括号。
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。
匹配失败情况:①左括号单身②右括号单身③左右括号不匹配。
算法流程图:
代码实现
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):括号、结束符
算数表达式种类:
- 中缀表达式:运算符在两个操作数中间
- 后缀表达式(逆波兰式):运算符在两个操作数后面
- 前缀表达式(波兰式):运算符在两个操作数前面
中缀表达式转后缀(手算):
- 确定中缀表达式中各个运算符的运算顺序(“左优先”原则:只要左边的运算符能先计算,就优先算左边的)
- 选择下一个运算符,按照「左操作数、右操作数、运算符」的方式组合成一个新的操作数
- 如果还有运算符没被处理,就继续2
中缀表达式转后缀(机算):
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能遇到三种情况:
- 遇到操作数。直接加入后缀表达式。
- 遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
- 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(” 或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
用栈实现后缀表达式的计算:
- 从左往右扫描下一个元素,直到处理完所有元素;
- 若扫描到操作数则压入栈,并回到1;否则执行3;
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1;
- 注:先出栈的是右操作数。
若计算前缀表达式,则应该从右往左扫描,其余与计算后缀表达式相同,注意先出栈的是左操作数。
算符间的优先关系:
代码实现(中缀表达式计算)
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)。
函数调用时,需要用一个栈存储:
- 调用返回地址
- 实参
- 局部变量
递归调用时,函数调用栈可称为“递归工作栈”,每进入一层递归,就将递归调用所需信息压入栈顶;每退出一层递归,就从栈顶弹出相应信息。
缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算。
队列在舞伴问题中的应用
问题:假设在舞会上,男士和女士各自排成一队。舞会开始时,依次从男队和女队的对头各出一人配成舞伴。如果两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲。现要求写以算法模拟上述舞伴配对问题。
分析:显然,先入队的男士或女士先出队配成舞伴。因此该问题具有典型的先进先出特征,可以利用队列作为算法的数据结构。首先构造两个队列依次从队头元素出队配成舞伴某队为空,则另外一对等待着则是一下舞曲第一个可获得舞伴的人。
队列在层次遍历中的应用
该过程的简单描述如下:
- 根结点入队。
- 若队空(所有结点都已处理完毕),则结束遍历;否则重复3操作。
- 队列中第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回2
队列在广度优先遍历中的应用
队列在计算机系统中的应用
多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略。
缓冲区的逻辑结构
多队列出队/入队操作的应用
数组和特殊矩阵
数组的定义
数组是由n(n≥1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在n个线性关系中的序号称为该元素的下标,下标的取值范围称为数组的维界。
数组与线性表的关系:
数组是线性表的推广。一维数组可视为一个线性表;二维数组可视为其元素是定长数组的线性表,以此类推。数组一旦被定义,其维数和维界就不再改变。
因此,除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。
数组的存储结构
一维数组
各数组元素大小相同,且物理上连续存放。
数组元素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+i∗sizeof(ElemType)(0≤i<10)
注:除非题目特别说明,否则数组下标默认从0开始 注意审题!
二维数组
行优先存储: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+(i∗N+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+(j∗M+i)∗sizeof(ElemType)
普通矩阵的存储
对于普通矩阵可用二维数组存储,但是某些特殊矩阵可以压缩存储空间。
特殊矩阵的压缩存储
压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。
特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
对称矩阵
若 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,则该矩阵为对称矩阵。
压缩存储策略:只存储主对角线+下三角区(或主对角线+上三角区)。按行优先原则将各元素存入一维数组中。
从矩阵下标推出一维数组下标,即
a
i
,
j
(
i
≥
j
)
→
B
[
k
]
a_{i,j}(i \geq j) \rightarrow B[k]
ai,j(i≥j)→B[k],则有
k
=
i
(
i
−
1
)
2
+
j
−
1
k=\frac{i(i-1)}{2}+j-1
k=2i(i−1)+j−1。
出题方法:
- 存储上三角?下三角?
- 行优先?列优先?
- 矩阵元素的下标从0?1?开始
- 数组下标从0?1?开始
三角矩阵
下三角矩阵:除了主对角线和下三角区,其余的元素相同
上三角矩阵:除了主对角线和上三角区,其余的元素相同
压缩存储策略:按行优先原则将橙色区元素存入一维数组中。并在最后一个位置存储常量C。
从矩阵下标推出一维数组下标,即 a i , j ( i ≥ j ) → B [ k ] a_{i,j}(i \geq j) \rightarrow B[k] ai,j(i≥j)→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…
三对角矩阵
压缩存储策略:按行优先(或列优先)原则,只存储带状部分
从矩阵下标推出一维数组下标,即 a i , j ( i ≥ j ) → B [ k ] a_{i,j}(i \geq j) \rightarrow B[k] ai,j(i≥j)→B[k],则有 k = 2 i + j − 3 k=2i+j-3 k=2i+j−3
若已知数组下标k如何得到i,j?
分析:前i-1行共 3(i-1)-1 个元素,前i行共 3i-1 个元素,显然, 3(i-1)-1 < k+1 ≤ 3i-1
稀疏矩阵的压缩存储
稀疏矩阵的三元组表既可以采用数组存储,又可以采用十字链表存储。当存储稀疏矩阵时,不仅要保存三元组表,而且要保存稀疏矩阵的行数、列数和非零元素的个数。
压缩存储策略:
-
顺序存储——三元组 <行,列,值>
稀疏矩阵压缩存储后便失去了随机存取特性。
-
十字链表法