一.栈
1.栈的逻辑结构与概念
-
栈的定义
-
栈:只允许在一端进行插入和删除操作的线性表
-
特点:LIFO,后进先出的线性表
-
相关概念:
栈顶:线性表中允许进行插入和删除的那一端
栈底:固定的,不允许进行插入和删除操作的那一端
空栈:不含任何元素的空表
-
-
基于栈的一些基本操作
- 初始化 InitStack(&S)
- 判空 StackEmpty(S)
- 进栈 Push(&S,x)
- 出栈 Pop(&S,&x)
- 读栈顶 GetTop(S,&x)
- 销毁栈 ClearStack(&S)
2.栈的顺序存储结构
-
结构描述:(可类比顺序表)使用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)来指示当前栈顶的位置。
#define MaxSize 50 typedef struct{ ElemType data[MaxSize]; int top; } SqStack;
2.顺序栈上基本操作的实现
-
初始化
void InitStack(&S){ S.top = -1; }
-
判空
bool StackEmpty(S){ if(S.top == -1) return true; else return false; }
-
进栈
bool Push(SqStack &S,ElemType x){ if(S.top == MaxSize-1) return false; 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; }
-
-
共享栈
-
定义:利用栈底不变的特性,可以让两个顺序栈共享一个一维的数据空间,两个顺序栈的栈底分设在数组的两端,栈顶向共享空间的中间延伸。
-
特点:可以更有效的利用存储空间/存取效率并未受到影响
-
操作:栈空:top0 = -1;top1 = MaxSize;
栈满:top0+1 = top1;
其余的出入栈操作和一般的顺序栈没有差别,但是要注意1号顺序栈指针变化的顺序。
-
3.栈的链式存储结构
-
定义:链栈:采取链式存储的栈结构,采用单链表来实现,并且限制所有的操作都只能在单链表的表头进行;注意:规定链栈中没有头结点。
-
特点:可以使多个栈共享存储空间;提高栈的效率;不存在栈满上溢的情况
-
程序定义:
typedef struct LinkNode{ ElemType data; struct LinkNode *next; }*LinkStack;
-
基本操作的实现:插入、删除操作等都与链表相类似,但是要注意此处实现的链表是没有头结点的,对于空表判断,表头元素的操作有特殊性。
bool Push(LinkStack &S,ElemType e){ LinkNode *s; s = (LinkNode*)malloc(sizeof(LNode)); s->data = e; s->next = S; S = s; return true; } bool Pop(LinkStack &S,ElemType &e){ if(S==NULL) return false; e = S->data; S = S->next; return true; }
二.队列
1.队列的逻辑结构与概念
-
队列的定义
-
队列:只允许在一端进行插入,在另一端进行删除操作的线性表
-
特点:FIFO,先进先出的线性表
-
相关概念:
队头:线性表中允许进行删除的那一端
队尾:线性表中允许进行插入的那一端
空队列:不含任何元素的空表
-
-
基于栈的一些基本操作
- 初始化 InitQueue(&Q)
- 判空 QueueEmpty(Q)
- 入队 EnQueue(&Q,x)
- 出队 DeQueue(&Q,&x)
- 读队头元素 GetHead(Q,&x)
- 销毁队列 ClearQueue(&Q)
2.队列的顺序存储结构
-
结构描述:(可类比顺序表)使用一组地址连续的存储单元存放自队头到队尾的数据元素,同时附设两个指针(front,rear)来分别指示队头元素和队尾元素的下一个位置。
#define MaxSize 50 typedef struct{ ElemType data[MaxSize]; int front,rear; } SqQueue;
2.顺序栈上基本操作的实现
-
初始化
void InitQueue(&Q){ Q.front = Q.rear = 0; }
-
判空
bool QueueEmpty(Q){ if(Q.front == Q.rear) return true; else return false; }
-
入队
bool EnQueue(SqQueue &Q,ElemType x){ if(Q.rear== MaxSize) return false; Q.data[Q.rear++] = x; return true; }
-
出队
bool DeQueue(SqQueue &Q,ElemType &x){ if(Q.rear == Q.front) return false; x = Q.data[Q.front++]; return true; }
-
读队首元素
bool GetHead(SqQueue Q,ElemType &x){ if(Q.front == Q.rear) return false; x = Q.data[Q.front]; return true; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gObMCnQd-1592968543390)(C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20200622093514354.png)]
3.循环队列
-
定义:前文讲述了顺序队列存在“假溢出”的情况,因此我们可以将顺序队列臆造成一个环状的空间,把存储队列元素的表从逻辑上(注意:这里强调是逻辑上,因为实际上这个空间可能还是线性的)看成是一个环,需要借助取余运算。
-
特点:入队和出队的过程中,指针在不断循环推进,那么此时判断队满和队空的条件均为Q.front = Q.rear。
- 解决方式一:牺牲一个单元来区分队空和队满,入队的时候当队尾元素在队头指针的上一个元素的时候,就认为队列已经满了。(常用,以下实现均采用这个方法)
- 解决方式二:在队列中增加一个表示队列中元素个数的数据成员,通过元素个数判满判空。
- 解决方式三:增设tag数据成员,对两种情况进行分类判断。
-
操作:
-
初始化
void InitQueue(&Q){ Q.front = Q.rear = 0; }
-
判空
bool QueueEmpty(Q){ if(Q.front == Q.rear) 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; 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.front == Q.rear) return false; x = Q.data[Q.front]; return true; }
-
-
3.队列的链式存储结构
-
定义:链队列:采取链式存储的队列结构,是一个含有队头指针和队尾指针的单链表。其中,队头指针指向队头节点,队尾指针指向队尾节点(注意:这一点与队列的顺序实现是不一样的)。
-
特点:队列的数据结构会有大量的数据变动的操作,用链式结构特别适合,且不存在队列满和溢出的情况,若程序中需要多个队列和多个栈,那么也可以考虑使用链式结构,避免存储分配不合理+溢出。
-
程序定义:
typedef struct {//定义链队的节点 ElemType data; struct LinkNode *next; }LinkNode; typedef struct{ LinkNode *front,*rear;//链队的结构,由两个指针来决定 }
-
基本操作的实现:插入、删除操作等都与链表相类似,但是要注意此处实现的链表是没有头结点的,对于空表判断,表头元素的操作有特殊性。
//初始化 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) return true; return false; } //入队,因为链栈无需判断队满,所以返回类型可以是无值型 void EnQueue(LinkQueue &Q,ElemType x){ 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.rear == Q.front) return false; p = Q.front->next; x = p->data; Q.front->next = p->next; if(Q.rear == p) Q.rear = Q.front;//要先判断原队列中是否只有一个节点,否则释放q之后,Q.rear就丢失了 free(p); return true; }
三.栈和队列的应用
1.栈在括号匹配中的应用
bool BracketsCheck(char *str){
InitStack(S);
int i = 0;
while(str[i]!='\0'){
switch(str[i]){
//左括号入栈
case '(':Push(S,'(');break;
case '[':Push(S,'[');break;
case '{':Push(S,'{');break;
//有括号,栈顶检测
case ')':Pop(S,e);
if(e!='(') return false;
break;
case ']':Pop(S,e);
if(e!='[') return false;
break;
case '}':Pop(S,e);
if(e!='{') return false;
break;
default:
break;
}
i++;
}
if(!isEmpty(S)){
printf("括号不匹配!\n");
return false;
}
else{
printf("括号匹配\n");
return true;
}
}
2.队列的应用
队列的应用多以一下三个形式展开:
①结合树的结构,队列在层次遍历中的配合
“根节点入队,如果队空则结束遍历,否则一直持续以下步骤:取出队头节点,先访问该节点,依次将该节点的左右子节点入队”②队列解决主机与外部设备之间速度不匹配的问题
利用队列构造一个缓冲区③队列解决多用户引起的资源竞争问题
构造一个等待队列,按照每个设备请求的时间先后进行排序。
-
题目描述:汽车轮渡口,过江渡船每次载10辆车过江,过江车辆分为客车和火车,上渡船有以下规定:同类车先到先上船;客车先于货车上船,且每上4辆客车,才允许放上一辆货车;若等待的客车不足4辆,则以货车代替;若无货车等待,允许客车都上船。
-
代码实现
数组Q表示渡船,容量为10;客车和货车的队列为Q1和Q2,;若Q1充足,则取4个Q1后再取1个Q2,直到Q的长度为10;若Q1不充足,直接用Q2对齐。
Queue q; Queue q1,q2; void manager(){ int i = 0,j = 0;//i表示客车数量,j表示渡船上的车辆总数 while(j<10){ if(!QueueEmpty(q1)&&i<4){ DeQueue(q1,x); EnQueue(q,x); i++; j++; } else if(1==4&&!QueueEmpty(q2)){ DeQueue(q2,x); EnQueue(q,x); j++; i = 0; } else{ while(j<10&&i<4&&!QueueEmpty(q2)){ DeQueue(q2,x); EnQueue(q,x); i++;//用货车代替客车 j++; } i = 0; } if(QueueEmpty(q1)&&QueueEmpty(q2)) j = 11;//置1之后可跳出循环 } }
四.特殊矩阵的压缩存储
1.数组的定义
数组:由n个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素受到n个线性关系的约束,每个元素在n个线性关系中的序号称为该元素的下标,并称该元素为n维数组。
数组vs.线性表:数组是线性表的推广,一维数组可以看做是一个线性表;二维数组可以看做元素是线性表的一个线性表。数组一旦被定义,维数和界数都不会再改变,数组一般只有结构初始化、销毁、元素存取、元素修改的操作。
2.数组的存储结构
(1)一维数组:
以一维数组A[0…n-1]为例,其中每个元素占据的存储单元为L
(2)多维数组:
- 假设二维数组的行下标和列下标的范围分别为[I1,h1],[I2,h2]
-
按行优先的映射方式——先行后列,先存储行号较小的元素,在行号相等的情况下再存储列号较小的元素。
-
按列优先的映射方式——先列后行,先存储列号较小的元素,在列号相等的情况下,再存储行号较小的元素。
3.矩阵的压缩存储
压缩存储:对多个值相同的元素分配一个存储空间,对零元素不分配存储空间
特殊矩阵:具有较多相同值或者零值的元素,且这些矩阵元素的空间分布具有一定的规律性
-
对称矩阵
-
矩阵结构:n阶方阵,Aij=Aji,矩阵被划分为上三角区、主对角线和下三角区,只需要存储一个三角区和对角线
-
推导:只存储对称矩阵的对角线区和下三角区,将矩阵中的aij元素存储在一维数组bk的位置上
第一行a11第二行a21 a22
第三行a31 a32 a33
…
第i行 ai1 ai2… aij-1
按照累加关系,则元素aij的位置在k=1+2+3+…+(i-1)+(j-1)=i(i-1)/2+(j-1) -
下标对应关系
k = i(i-1)/2+(j-1) i≥j
k=j(j-1)/2+(i-1) j<i
注意:上述公式与推导基于行列下标均从1开始计数
-
-
三角矩阵
-
矩阵结构:n阶方阵,矩阵的上三角区的所有元素是同一常量(注意:并不一定要是0),在存储完下三角区和对角线区的元素之后再用一个存储空间存储上三角区的元素即可
-
推导:与对称矩阵类似,不再赘述
-
下标对应关系<下三角矩阵>
k = i(i-1)/2+(j-1) i≥j
k = n(n+1)/2 i<j
下标对应关系<上三角矩阵>
k = n+(n-1)+…+(n-i+2)+(j-i+1)
k = (i-1)(2n-i+2)/2+(j-i+1) i≤j
k = n(n+1)/2 i>j上述公式的推导也是基于数组元素下标从1开始
-
-
三对角矩阵(带状矩阵)
- 矩阵结构:所有非零元素都集中在以主对角线为中心的3条对角线的区域中,其余区域的元素均为0;用代数描述即为当|i-j|>1时,有aij = 0
- 按照行优先的方式将三对角矩阵中的元素存储在数组中,除了第一行只有两个元素,其余每行都有3个元素,在求aij对应的k时在第i行的元素个数应为i-(j-1)
从二维数组到一维数组:k=2i+j-3
从一维数组到二维数组:i=(k+1)/3+1,j=k-2i+3
4.稀疏矩阵
稀疏矩阵:矩阵中的非零元素个数相对矩阵中元素的总个数而言非常少
稀疏矩阵的压缩存储:将非零元素的行标、列表以及值对应存储下来,稀疏矩阵存储后便失去了随机存取特性。
五.后言
- 参考文档:《王道2019年数据结构考研复习指导》,本文系复习专业课时整理所记,如有问题,欢迎讨论。
- 思维导图