考研408复习笔记—— 数据结构(五)
编写不易,希望各位看到能点个赞。
若发布的内容有什么错误,欢迎留言探讨。
前篇跳转
三、栈和队列
3.1 栈(Stack)
1、定义
栈是只允许在一端进行插入或删除操作的线性表。
栈顶:允许插入和删除的一端
栈底:不允许进行插入和删除的一端
空栈:没有存任何数据元素的栈
特点:后进先出(LIFO)
逻辑结构于线性表相同,但是插入与删除操作有所改变
2、基本操作
InitStack(&S):初始化
栈。构建一个空栈,分配存储空间。
DestroyStack(&S):销毁
栈,销毁并释放栈S所占用的内存空间。
Push(&S,x):进栈
,若栈未满,将x加入栈中,使其成为栈顶
。
Pop(&S,x):出栈
,若栈非空,弹出栈顶元素,用x返回。
GetTop(S,&x):读取栈顶元素
,若栈非空,用x返回栈顶元素。
StackEmpty(S):判断是否为空栈。
常考形式,给定进栈顺序,判断可能的出栈顺序。
3.2 顺序栈(栈的顺序存储)
1、定义
顺序栈:使用顺序存储
方式实现的栈。与同样使用顺序存储的顺序表相似。
利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时使用一个指针(top)指示当前栈顶元素位置。
定义代码如下:
#define MaxSize 10 //定义栈的最大元素
typedef struct {
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
} SqStack;
2、初始化
相对来说,在主函数中调用栈的定义时,就已经从内存中申请了一个栈的空间,因此在初始化时,我们只用将栈顶指针进行设置。令其等于-1,表示栈内不存在元素。同时我们在判断空栈中也就能直接通过top来查看是否为空栈。代码如下:
void InitStack(SqStack &S){
S.top=-1; //初始化栈顶指针
}
bool StackEmpty(SqStack S){
if(S.top==-1) return true;
reutrn false;
}
3、进栈操作
在我们设定的栈中,栈顶指针top始终指向着栈顶的最后一个元素
。因此我们进行进栈操作时,先将栈顶指针
后移一位,然后将数据存入后移完成
的空间中,这样就完成了数据的进栈操作。
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;
}
4、出栈操作
相对链表来说,这就是删除操作,不过对于栈,只能删除栈顶的元素。因此,我们只需要将栈顶的元素传出,然后再将栈顶指针前移一位
即可。
但是相对于链表
来说,在内存空间中
,原本
栈顶位置的空间依旧存在,数据也同样存在。只是再逻辑上已经被删除了。后面在进行入栈操作将会直接覆盖
该位置的内容。
bool Pop(SqStack &S,ElemType &x){
if(S.top==-1) return false; //栈空,报错
x=S.data[S.top]; //传出栈顶元素
S.top = s.top-1; //指针减一
return true;
}
5、读取栈顶元素
该操作与出栈操作类似,只是不需要前移top指针。
bool GetTop(SqStack S,ElemType &x){
if(S.top==-1) return false; //栈空,报错
x=S.data[S.top]; //传出栈顶元素
return true;
}
上述的函数,是在设计栈时,令top指针指向栈顶的位置
所使用的代码。同样我们也可以令top指向下一个可以插入数据的位置,这样只需要更改相应的top的逻辑,具体根据题目的要求来更改。
6、空间问题
相对链式存储
,顺序栈同样的存在空间不能自动扩张的问题。如若开始分配大空间则会可能导致空间浪费,分配的过小又可能导致使用过程中的空间不足。
为了解决这个问题,我们可以使用共享栈
,让一个栈的空间中存在两个栈顶指针,从空间的两头分别进行元素的存储。这样可以提高内存的利用率,但是共享栈同样可能被全部用完,因此最好的方法还是采用了链式存储的栈。
3.3 链栈(栈的链式存储)
1、定义:使用链式存储
方式实现的栈。相当于一个只从头结点
进行插入删除操作的单链表,无论插入与删除都只能从头结点进行。但是链栈没有头结点,Lhead指向栈顶元素。
创建代码如下:
typedef struct Linknode{
ElemType data;
struct Linknode *next;
} *LiStack;
定义与单链表没有太大区别,同样也可以设计为带头结点和不带头结点两种方式,具体的使用区别也仅再判空操作有些许差异。
具体的基本操作与单链表相同,可参照单链表自己动手实现。具体包括:创建、增删改查以及判空、判满。
3.4 队列(Queue)
1、定义
队列,是一种操作受限的线性表,简称队。
具体限制:只允许在表的一端进行插入,而在另一端只能进行删除。
2、基本概念
队头:只允许删除的一端
队尾:只允许插入的一端
空队列:不含任何元素的队列
入队:从队尾插入元素
出队:从队头弹出元素
特点:先进先出(FIFO)
3、基本操作
InitQueue(&Q):初始化
队列。构建一个空队列。
DestroyQueue(&Q):销毁
队列,销毁并释放队列Q所占的内存空间。
EnQueue(&Q,x):入队
,若队列Q未满,将x加入栈中,使其成为队尾
。
DeQueue(&Q,&x):出队
,若队列Q非空,弹出队头
元素,用x返回。
GetHead(Q,&x):读取队头元素
,若队列Q非空,用将队头元素赋值给x。
QueueEmpty(Q):队列判空,若为空返回true,否则返回false。
3.5 队列的顺序存储
定义代码如下:
#define Maxsize 10 //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front,rear; //队头和队尾指针
} SqQueue;
通过该方式声明队列后,会在内存中申请一片连续的空间作为队列的存储位置。
初始化:
让队头指针和队尾指针指向数组开始的位置0。
void InitQueue(SqQueue &Q){
Q.rear=Q.front=0; //初始时队头和队尾指针指向0
}
判空:
根据队列的性质,就可以通过队头指针和队尾指针的位置来判断队列是否为空。
bool QueueEmpty(SqQueue Q){
if(Q.rear==Q.front){ //队列为空
return true;
}
else{
return true;
}
}
入队:
队列的添加数据操作只能从队尾进行添加,而队列是通过静态数组来实现的,内存空间有限。因此在添加前要先判断队列是否已经满了,确认未满后才可以进行添加。
bool EnQueue(SqQueue &Q,ElemTypex){
if(队列已满)
return false; //队满则插入失败
Q.data[Q.rear] = x; //将x插入队尾
Q.rear = (Q.rear+1); //队尾指针后移
return true;
}
但是,在顺序存储中,随着队尾的不断插入将会把内存空间不占满,因此我们需要对队尾指针进行一次取余处理,让它能调用队头空出的位置。此时为了防止队列爆满导致队尾指针与队头指针重合,我们需要牺牲一个空间来让队列正常使用。
bool EnQueue(SqQueue &Q,ElemTypex){
if(Q.rear+1%MaxSize == Q.front)
return false; //队满则插入失败
Q.data[Q.rear] = x; //将x插入队尾
Q.rear = (Q.rear+1)%MaxSize; //队尾指针后移,并取余
return true;
}
通过这种方式,将原本线性的存储空间变成了一个可以重复使用的环,因此也称为循环队列。
出队:
(带头结点)
1、先对队列进行判空,如果为空则终止出队操作。
2、然后获取本次删除节点位置,带头结点的队列,表示要删除头结点之后的节点。
3、将头结点的后继指针指向删除节点的后继。
4、判断是否为表尾节点,是表尾几点,则需要令后继指针指向头结点。
bool DeQueue(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(!.rear == p)
Q.rear = Q.front; //若为表尾节点,修改头结点的rear
free(p);
return true;
}
不带头结点的出队
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.front == Q.rear)
return false; //空队
LinkNode *p = Q.front;
x = p->data; //获取队头元素
Q.front-next = p-next; //修改头结点的next指针
if(!.rear == p)
Q.rear = NULL; //若为表尾节点,修改rear与front
Q.front = NULL;
free(p);
return true;
}
3.6 双端队列
双端队列:只允许从两端插入、两端删除的线性表。
输入受限的双端队列:只允许一端插入、两端删除的线性表。
输出受限的双端队列:只允许两端插入、一端删除的线性表。
常见考察方式:
给定一个元素输入序列如A、B、C、D、E,判断哪些输出序列是合法的,那些是非法的。
栈也有相同的考法。出栈的合法序列种数可以依靠卡特兰数来进行计算。
n为数据元素的数量。
3.7 栈的应用
3.7.1 括号匹配问题
根据括号的使用原则,每一个左括号需要一个右括号来进行对应。因此在计算机中要进行匹配,会先检测到一个或者多个左括号,然后根据后进先出
(LIFO)的原则进行匹配。因此我们使用拥有相同后进先出
原则的栈来解决这类问题。
根据规则,在遇到输入为左括号便存入栈中,而每匹配到一个右括号便令一个左括号出栈。
匹配失败的情况:
1、左括号出栈后,比对两个括号类型。若两个括号类型相同,则匹配成功;若类型不同,则后续的括号皆为非法括号。
2、碰到右括号,但是栈已空,则后续括号也为非法括号。
3、括号匹配完成,但是栈未空,则匹配依旧失败。
代码实现:
bool brackeCheck(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); //栈非空则匹配失败
}