数据结构03-栈与队列
1.栈
1.1栈的基本概念
栈其实是一种很特殊的线性表,栈是一种只允许在一端进行插入或删除操作的线性表
几个基本术语:
1.空栈:没有元素的栈
2.栈底:不允许插入和删除的一端
3.栈顶:允许插入和删除的一端
栈是后进先出的一种特殊的线性表,他的删除插入操作只能在栈顶执行
进栈顺序:
a1,a2,a3,a4,a5
出栈顺序:
a5,a4,a3,a2,a1
1.2栈的顺序存储
顺序栈的代码定义
#define MaxSize 10
typedef struct{
Elemtype data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
}SqStack;
顺序栈的常用操作
初始化操作
//初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
void testStack(){
SqStack S; //声明一个顺序栈
InitStack(S);
}
判空操作
//判空
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; //指针先加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=S.top-1; //栈顶指针减1
return true;
}
读取栈顶元素操作
//读栈顶元素
bool GetTop(SqStack S,Elemtype &x){
if(S.top==-1)
return false; //栈空,报错
x=S.data[S.top]; //x记录栈顶元素
return true;
}
1.3 共享栈
共享栈,顾名思义就是两个栈共享同一片空间
#define MaxSize 10
typedef struct {
Elemtype data[MaxSize];
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
//初始化栈
void InitStack(ShStack &S){
S.top0=-1;
S.top1=MaxSize;
}
1.4栈的链式存储
栈的链式存储,我们可以想象成一个特殊的单链表,即插入和删除操作只允许在头结点发生的单链表。
链栈的代码定义
typedef struct Linknode{
Elemtype data; //数据域
struct Linknode *next; //指针域
}* LiStack; //栈类型定义
链栈的基本操作(带头结点)
初始化
void InitStack(LiStack &L){
L = new Linknode; //创建一个头结点
L->next=NULL;
}
判空
bool isEmpty(LiStack L){
if(L->next==NULL){
return true;
}else
return false;
}
进栈操作
void Push(LiStack &L,int x){
struct Linknode* s ;
s->data=x;
//头插法
s->next = L->next;
L->next=s;
}
出栈操作
bool Pop(LiStack &L,int &x){
if(L->next==NULL)
return false; //栈空不能出栈
x=L->next->data;
L->next=L->next->next;
return true;
}
链栈的基本操作(不带头结点)
初始化
void InitStack(LiStack &L){
L==NULL; //不带头结点
}
判空
bool isEmpty(LiStack L){
if(L==NULL)
return true;
else
return false;
}
进栈操作
void Push(LiStack &L,int x){
Linknode* s;
s->data=x;
//插入到表首,并变更表首
s->next=L;
L=s;
}
出栈操作
bool Pop(LiStack &L,int &x){
if(L==NULL)
return false; //空栈错误
x=L->data;
L=L->next;
return true;
}
2.队列
2.1队列的基本概念
队列,是只允许在一端进行插入,在另一端删除的线性表
几个术语:
1.对头:允许删除的一端
2.队尾:允许插入的一端
3.空队列:没有元素的队列
队列是先进先出的,而栈是后进先出的,两者是相反的
2.2队列的顺序实现
顺序队列的代码定义
typedef struct {
ElemType data[MaxSize]; //用静态数组存放队列元素
int front,rear; //队头指针和队尾指针
}SqQuene;
顺序队列的基本操作
初始化
void InitQuene(SqQuene &Q){
//初始化时,队头队尾指针均指向0
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; //队尾指针加一对MaxSize取模,实现队尾指针的循环
return true;
}
取模的意义,在逻辑上把线状的存储空间变成了环装的存储空间
出队操作
bool DeQueue(SqQueue &Q,Elemtype &x){
if(Q.rear==Q.front)
return false; //队空报错
x=Q.data[Q.front]; //删除一个队头元素,并用x返回对头元素的值
Q.front=(Q.front+1)%MaxSize;
}
查询操作
对于队列来说。一般都是查询其队头元素的值
bool GetHead(SqQueue Q,Elemtype x){
if(Q.rear==Q.front)
return false; //队空报错
x=Q.data[Q.front]; //获得队头元素的值
return true;
}
计算队列元素个数的公式
(rear+MaxSize-front)%MaxSize
其他注意事项
在上文的例子中,我们的队尾指针都是指向当前队尾的下一个位置,但有时会遇到队尾指针指向当前队尾,此时要另加判断,大体情况都类似。
2.3队列的链式实现
链式队列的代码定义
//链式队列结点
typedef struct LinkNode{
Elemtype data;
struct LinkNode* next;
}LinkNode;
//链式队列
typedef struct{
LinkNode* front;
LinkNode* rear;
}LinkQueue;
链式队列即可以带头结点,也可以不带头结点
链式队列的基本操作
初始化(带头结点)
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){
Q.front=NULL;
Q.rear==NULL;
}
bool isEmpty(LinkQueue Q){
if(Q.front==NULL)
return true;
else
return false;
}
入队(带头结点)
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;
Q.rear=s;
}
}
出队(带头结点)
bool DeQueue(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;
free(p); //释放结点空间
return true;
}
出队(不带头结点)
bool DeQueue(LinkQueue &Q,int &x){
if(Q.front==NULL)
return false;
LinkNode* p=Q.front;
x=p->data;
Q.front=p->next;
if(Q.rear==p){
Q.front=NULL;
Q.rear=NULL;
}
free(p);
return true;
}
2.4双端队列
双端队列:只允许从两端插入,两端删除的线性表
另外还有两种受限的双端队列
3.栈的应用
3.1括号匹配
什么是括号匹配?
及书写代码时,所有的括号都应该成对出现。
下面举一个例子
根据如图的括号匹配,可以总结出:
1.最后出现的左括号最先被匹配(与栈后进先出相似)
2.没出现一个右括号,就消耗一个左括号(与出栈类似)
算法实现
用栈实现括号匹配:
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配
匹配失败的情况:
1.左括号单身
2.右括号单身
3.左右括号不匹配
下图是大致思路
代码实现
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct {
char data[MaxSize];
int top;
}SqStack;
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(isEmpty(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 isEmpty(S); //检索完全部括号后,栈空说明匹配成功
}
}
3.2表达式求值
表达式的组成部分:操作数,运算符,界限符
我们熟知的,其实是中缀表达式
中缀表达式
a+b,a+b-c,a+b-cd
后缀表达式
ab+,ab+c-,ab+ cd-
前缀表达式
+ab,-+abc,-+ab*cd
中缀转后缀(手算)
中缀转后缀的手算算法:
1.确定中缀表达式中各个运算符的运算顺序
2.选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数
3.如果还有运算符没被处理,就继续2
注:由于运算顺序不唯一,因此对应的后缀表达式也不唯一
但是当我们在设计算法时,应具有确定性,为了保证不出现两种不同的答案,引入一个左规则:只要左边的运算符能先计算,就优先算左边的,所以我们得到的结果就是左边的表达式。
后缀表达式的计算(手算)
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数
后缀表达式的计算(机算)
1.从左往右扫描下一个元素,直到处理完所有元素
2.若扫描到操作数则压入栈,并回到1,否则执行3
3.若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
中缀转前缀(手算)
1.确定中缀表达式中各个运算符的运算顺序
2.选择下一个运算的运算符,按照[运算符 左操作数 右操作数]的方式组合成一个新的操作数
3.如果还有运算符没被处理,就继续2
右优先原则:只要右边的运算符能先计算,就优先算右边的
前缀表达式的计算(机算)
1.从右往左扫描下一个元素,直到处理完所有元素
2.若扫描到操作数则压入栈,并回到1,否则执行3
3.若扫描到运算符,则弹出连个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
中缀转后缀(机算)
1.初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
2.从左往右扫描
3.遇到操作数,直接加入后缀表达式
4.遇到界限符,遇到“(”直接入栈,遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止,注:“(”不用加入后缀表达式
5.遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空就停止,之后再把当前运算符入栈。
中缀表达式的计算(用栈实现)
伪代码思路:
1.初始化两个栈,操作数栈和运算符栈
2.从左往右,若扫描到操作数,压入操作数栈
3.若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈顶)
3.3栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束
函数调用时,需要用一个栈储存:
1.调用返回地址(执行结束后返回值返回到哪里)
2.实参
3.局部变量
递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需的信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息
Eg1:递归求阶乘
//计算阶乘的函数
int factorial (int n){
if(n==1||n==0)
return 1;
else
return n*factorial(n-1);
}
int main (){
int x=factorial(10);
printf("%d",x);
}
递归的缺点一:太多层递归可能会导致栈溢出!
Eg2:递归求斐波那契数列
int Fib(int n){
if(n==0)
return 0;
else if(n==1)
return 1;
else
return Fib(n-2)+Fib(n-1);
}
int main (){
for(int i=0;i<10;i++){
printf("%d ",Fib(i));
}
}
递归的缺点二:可能包含很多重复计算
4队列的应用
这两个后续章节再展开,先了解
1.树的层次遍历
2.图的广度优先遍历
队列在操作系统中的应用
1.CPU资源的分配
在日常使用时,已经就绪的进程,会排列成一个队列等待CPU的调用,类似于上文所述的队列
2.打印数据缓冲区
多台电脑使用同一台打印机,先来的打印请求最先执行,其余的请求排成一个队列等待打印机的执行
5 特殊矩阵的压缩存储
5.1对称矩阵
由于上三角区和下三角区是对称的,采用的压缩存储策略:只存储主对角线加下三角区(或主对角线加上三角区)
首先,按照行优先将各元素存入一维数组中
数组大小:
1+2+……n=(1+n)*n/2
映射函数:矩阵下标->一维数组下标(方便调用)
ai,j是第i(i-1)/2+j个元素
由于数组下标从0开始,所以ai,j->B[k]转换公式为:
k=i(i-1)/2+j-1
对称矩阵特性:
ai,j=aj,i
5.2三角矩阵
压缩存储策略:
按行优先原则将橙色区元素存入一维数组中,并在最后一个位置存储常量c
下三角
上三角
5.3三对角矩阵
压缩存储策略:按行优先,只存储带状部分
5.4稀疏矩阵
压缩存储策略一:
三元组<行,列,值>
压缩存储策略二:
十字链表法