考研408复习笔记—— 数据结构(五)

5 篇文章 17 订阅
5 篇文章 0 订阅

编写不易,希望各位看到能点个赞。

若发布的内容有什么错误,欢迎留言探讨。

前篇跳转

考研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);	//栈非空则匹配失败
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值