数据结构复习

写在最前,

写文章的初衷只是为了复习与记录自己的成长,笔者本人也还是学生,文章中难免会出现许多问题与错误,文章内容仅供参考,有不足的地方还请大家多多包涵并指正,谢谢~

第三章:限定性线性表——栈与队列

队列是两种重要的抽象数据类型,是一类操作受限制的线性表,其特殊性在于限制了插入和删除等操作的位置。栈与队列的共同特点是在线性表端点进行操作。从数据结构角度看,它们都是线性结构,应用面较广

3.1 栈

栈(Stack)通常将表中允许进行插入、删除操作的一端称为栈顶(Top),因此栈顶的当前位置是动态变化的,它由一个称为栈顶指针的位置指示器来指示。同时,表的另一端被称为栈底(Bottom)。当栈中没有元素时称为空栈。栈的插入操作被形象地称为进栈或入栈,删除操作称为出栈或退栈。 栈的修改是按后进先出的原则进行的,因此也被称为后进先出的线性表(Last In First Out,LIFO),简称LIFO表。示意图如下:

下面是栈的抽象数据类型定义:
ADT Stack {
         数据元素:可以是任意类型的数据,但必须属于同一个数据对象。
        结构关系:栈中数据元素之间是线性关系。
        基本操作:
        ① InitStack(S):
                操作前提:S为未初始化的栈。
                操作结果:将S初始化为空栈。
        ② ClearStack(S):
                操作前提:栈S已经存在。
                操作结果:将栈S置成空栈。
         IsEmpty(S):
                操作前提:栈S已经存在。
                操作结果:判栈空函数。若S为空栈,则函数返回TRUE,否则返回FALSE。
         IsFull(S):
                操作前提:栈S已经存在。
                操作结果:判栈满函数。若S栈已满,则函数返回TRUE,否则返回FALSE。
        ⑤Push(S,x):
                操作前提:栈S已经存在。
                操作结果:在S的顶部插入(亦称压入)元素x。若S栈未满,将x插入栈顶位置,并返回TRUE;若栈已满,则返回FALSE,表示操作失败。
        ⑥Pop(S,x):
                操作前提:栈S已经存在。
                操作结果:删除(亦称弹出)栈S的顶部元素,并用x带回该值,返回TRUE;若栈为空,返回值为FALSE,表示操作失败。
        ⑦GetTop(S,x):
                操作前提:栈S已经存在。
                操作结果:取栈S的顶部元素赋给x所指向的单元,也称读栈顶。与Pop(S,x)不同之处在于GetTop(S,x)不改变栈顶的位置。若栈为空,返回值为FALSE,表示操作失败。
}ADT Stack;
栈的表示与实现:栈作为一种特殊的线性表,在计算机中主要有两种基本的存储结构,顺序存储结构和链式存储结构。采用两种不同存储方式的栈分别简称为顺序栈和链栈。以下分别给出其定义与实现。
顺序栈:
/*顺序栈利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,
同时由于栈操作的特殊性,还必须附设一个位置指针top来动态地指示栈顶元素在顺序栈中的位置。
通常以top=-1表示空栈*/

//顺序栈的存储结构可以用一维数组来表示
#define Stack_Size 50 //设栈中元素个数为50
typedef struct{
	StackElementType elem[Stack_Size];  //用来存放栈中元素的一维数组
	int top;  //存放栈顶元素下标,top为-1表示空栈
}SeqStack;

//初始化
void InitStack(SeqStack *S){
	S->top=-1;  //构造一个空栈
}

//进栈
int Push(SeqStack *S,StackElementType x){
	//首先判断当前栈是否已满,如果栈已满,再进栈会上溢
	if(S->top==Stack_Size-1) return (FALSE);
	S->top++;  //修改栈顶指针
	S->elem[S->top]=x;  //x进栈,置入S栈新栈顶
	return(TRUE);
}

//出栈,将S栈顶元素弹出,放到x所指的存储空间中带出
int Pop(SeqStack *S,StackElementType *x){
	//首先判断当前栈是否为空,如果栈空,再出栈会下溢
	if(S->top==-1) 
		return(FALSE);
	else{
		*x=S->elem[S->top];  //栈顶元素赋值给x
		S->top--;  //修改栈顶指针
		return(TRUE);
	}
}

//读取栈顶元素,将栈S栈顶元素读出,放到x所指的存储空间中,栈顶指针不变
int GetTop(SeqStack *S,StackElementType *x){
	if(S->top==-1)
		return(FALSE);
	else{
		*x=S->elem[S->top];  //栈顶元素赋给x
		return(TRUE);
	}
}
/*注:参数说明SeqStack *S可改为SeqStack S,也就是将传地址方式改为传值方式。
传值比传地址容易理解,但传地址比传值更节省时间和空间*/
多栈共享技术:栈的应用广泛,经常会出现一个程序中需要同时使用多个栈的情况。若使用顺序栈,会因为对栈空间大小难以准确估计,产生有的栈溢出有的栈空闲的情况。故可让多个栈共享一个足够大的数组空间,通过利用栈的动态特性来使其存储空间互相补充,这就是多栈共享技术。
在顺序栈的共享技术中,最常用的是两个栈的共享技术,即 双端栈

双端栈结构示意图
它主要利用了栈“栈底位置不变,栈顶位置动态变化”的特性。首先申请一个共享的一维数组空间S[M],将两个栈的栈底分别放在一维数组的两端,分别是0,M-1。由于两个栈顶是动态变化的,这样可以实现互补,使得每个栈可用的最大空间与实际使用的需求有关。由此可见,两栈共享要比两个栈分别申请M/2的空间利用率要高。
两栈共享的数据结构定义及实现如下: 
//双端栈
#define M 100
typedef struct{
	StackElementType Stack[M];  //Stack[M]为栈区
	StackElementType top[2];  //top[0]和top[1]分别为两个栈顶指示器
}DqStack;

//初始化
void InitStack(DqStack *S){
	S->top[0]=-1;
	S->top[1]=M;
}

//进栈,把数据元素x压入i号堆栈
int Push(DqStack *S,StackElementType x,int i){
	if(S->top[0]+1==S->top[1])  return(FALSE);  //栈已满
	switch (i) {
	case 0:  //0号栈
		S->top[0]++;
		S->Stack[S->top[0]]=x;
		break;
	case 1:  //1号栈
		S->top[1]--;
		S->Stack[S->top[1]]=x;
		break;
	default:  //参数错误
		return(FALSE);
	}
	return(TRUE);
}

//出栈,从i号堆栈中弹出栈顶元素并送到x中
void Pop(DqStack *S,StackElementType *x,int i){
	switch (i) {
	case 0:
		if(S->top[0]==-1)  return(FALSE);
		*x=S->Stack[S->top[0]];
		S->top[0]--;
		break;
	case 1:
		if(S->top[1]==M)  return(FALSE);
		*x=S->Stack[S->top[1]];
		S->top[1]++;
		break;
	default:
		return(FALSE);
	}
	return(TRUE);
}
链栈:即采用链表作为存储结构实现的栈。为便于操作,这里采用带头结点的单链表实现栈。由于栈的插入和删除操作仅限制在表头位置进行,所以链表的表头指针就作为栈顶指针。结构图如下:

top为栈顶指针,时钟指向当前栈顶元素前面的头结点。若top->next==NULL则代表栈空。

链栈的结构定义及实现如下:

typedef struct node{
	StackElementType data;
	struct node *next;
}LinkStackNode;
typedef LinkStackNode *LinkStack;

//进栈
int Push(LinkStack top,StackElementType x){
	LinkStackNode *temp;
	temp=(LinkStackNode *)malloc(sizeof(LinkStackNode));
	if(temp==NULL)  return(FALSE);  //申请空间失败
	temp->data=x;
	temp->next=top->next;
	top->next=temp;  //修改当前栈顶指针
	return(TRUE);
}

//出栈
int Pop(LinkStack top,StackElementType x){
	LinkStackNode *temp;
	temp=top->next;
	if(temp==NULL)  return(FALSE);  //栈为空
	top->next=temp->next;
	*x=temp->data;
	free(temp);  //释放存储空间
	return(TRUE);
}
多栈运算:上面介绍过顺序栈的多栈共享技术,但是在处理实际问题时,有时采用顺序栈方式处理极不方便,采用多个单链表实现链栈会是很好的实现途径。将多个链栈的栈顶指针放在一个一维指针数组中来统一管理,从而实现同时管理和使用多个栈。下面给出多链栈定义:
#define M 10  //M个链栈
typedef struct node{
	StackElementType data;
	struct node *next;
}LinkStackNode,*LinkStack;
LinkStack top[M];

top[0],top[1],top[2],...top[M-1]分别是M个链栈的栈顶指针,分别指向M个不同的链栈,下图是多链栈的示意图:

 3.2 队列

队列(Queue)是另一种限定性的线性表,只允许在表的一端插入元素,而在另一端删除元素,所以队列具有先进先出(First In First Out,FIFO)的特性。在队列中,允许插入的一端称为队尾(Raar),允许删除的一端则称为队头(Front)。

下面是队列的抽象数据类型定义:

ADT Queue{

        数据元素:可以是任意类型的数据,但必须属于同一个数据对象。

        结构关系:队列中数据元素之间是线性关系。

        基本操作

        ①InitQueue(Q):

        操作前提:Q为未初始化的队列。

        操作结果:将Q初始化为一个空队列。

        ②IsEmpty(Q):

        操作前提:队列Q已经存在。

        操作结果:若队列为空,则返回TRUE,否则返回FALSE。

        ③IsFull(Q):

        操作前提:队列Q已经存在

        操作结果:若队列为满,则返回TRUE,否则返回FALSE。

        ④EnterQueue(Q,x):

        操作前提:队列Q已经存在

        操作结果:在队列Q的队尾插入x。操作成功,返回值为TRUE,否则返回值为FALSE。

        ⑤DeleteQueue(Q,x):

        操作前提:队列Q已经存在

        操作结果:将队列Q的队头元素出队,并用x带回其值。操作成功,返回值为TRUE,否则返回值为FALSE。

        ⑥GetHead(Q,x):

        操作前提:队列Q已经存在

        操作结果:取队列Q的队头元素(该元素不出队),并用x带回其值。操作成功,返回TRUE,否则返回值为FALSE。

        ⑦ClearQueue(Q):

        操作前提:队列Q已经存在

        操作结果:将队列Q置为空队列

}ADT Queue;

队列的链式表示和实现:
用链表表示的队列简称 链队列。为操作方便,这里采用带头结点的链表结构,并设置一个队头指针front和一个队尾指针rear。队头指针始终指向头结点,队尾指针指向最后一个元素。空的链队列的队头指针和队尾指针均指向头结点。

 通常将队头指针和队尾指针封装在一个结构体中,并将该结构体类型重新命名为链队列类型。定义及实现如下:

typedef struct Node{
	QueueElementType data;  //数据域
	struct Node *next;  //指针域
}LinkQueueNode;
typedef struct{
	LinkQueueNode *front;  //队列头指针
	LinkQueueNode *rear;  //队列尾指针
}LinkQueue;

//初始化
int InitQueue(LinkQueue *Q){
	Q->front=(LinkQueueNode *)malloc(sizeof(LinkQueueNode));
	if(Q->front!=NULL){
		Q->rear=Q->front;
		Q->front->next=NULL;
		return(TRUE);
	}
	else return(FALSE);  //溢出
}

//入队,将数据元素x插入到队列Q中
int EnterQueue(LinkQueue *Q,QueueElementType x){
	LinkQueueNode *NewNode;
	NewNode=(LinkQueueNode *)malloc(sizeof(LinkQueueNode));
	if(NewNode!=NULL){
		NewNode->data=x;
		NewNode->next=NULL;
		Q->rear->next=NewNode;
		Q->rear=NewNode;
		return(TRUE);
	}
	else return(FALSE);  //溢出
}

//出队,将队列Q的队头元素出队,并存放到x所指的存储空间中
int DeleteQueue(LinkQueue *Q,QueueElementType x){
	LinkQueueNode *p;
	if(Q->front==Q->rear)  return(FALSE);
	p=Q->front->next;
	//队头元素出队
	Q->front->next=p->next;
	if(Q->rear==p)  Q->rear=Q->front;  //如果队中只有一个元素,则p出队后成为空队
	*x=p->data;
	free(p);  //释放存储空间
	return(TRUE);
}
循环队列:循环队列是队列的一种 顺序表示和实现方法,但 只是在逻辑上而不是存储结构上实现循环。在队列的顺序存储结构中,用一组地址连续的存储单元依次存放从队头到队尾的元素,如一维数组Queue[MAXSIZE]。此外,由于队列中队头和队尾的位置都是动态变化的,因此需要附设两个指针front和rear,分别指示队头元素和队尾元素在数组中的位置。初始化队列时,令front=rear=0;入队时,直接将新元素送入尾指针rear所指的单元,然后尾指针增1;出队时,直接取出队头指针front所指的元素,然后头指针增1。显然,在非空顺序队列中,队头指针始终指向当前的队头元素,而队尾指针始终指向真正队尾元素后面的单元。当rear==MAXSIZE时认为队满。但此时不一定是真的队满,因为随着部分元素的出队,数组前面会出现一些空单元,如图(d)所示。由于只能在队尾入队,使得上述空单元无法使用。这种现象称为假溢出,真正队满的条件应是 rear-front==MAXSIZE

为了解决假溢出现象并使队列空间得到充分利用,一个巧妙的办法是将顺序队列的数组看成一个环状的空间,即规定最后一个单元的后继为第一个单元,我们形象地称之为循环队列。假设队列数组为Queue[MAXSIZE],当rear+1==MAXSIZE时,令rear=0,即可求得最后一个单元Queue[MAXSIZE-1]的后继:Queue[0],通过数学中的取模(求余)运算来支持实现:rear=(rear+1)mod MAXSIZE,显然,当 rear+1==MAXSIZE时rear=0,同样可求得最后一个单元Queue[MAXSIZE-1]的后继:Queue[0]。所以借助于取模(求余)运算,可以自动实现队尾指针、队头指针的循环变化。进队操作时,队尾指针的变化是;rear=(rear+1)mod MAXSIZE;而出队操作时,队头指针的变化是:front=(front+1)mod MAXSIZE。下图给出了循环队列的几种情况:

在上图(c)所示循环队列中,队列头元素是e3,队列尾元素是e5,当e6、e7和e8相继入队后,队列空间均被占满,如图(b)所示,此时队尾指针追上队头指针,所以有front==rear。反之,若e3、e4和e5相继从图(c)的队列中删除,则得到空队列,如图(a)所示,此时队头指针追上队尾指针,也存在关系式front==rear。可见,只凭front==rear无法判别队列的状态是“空”还是“满”。对于这个问题,可有两种处理方法。
一种方法是损失一个元素空间的方法。当队尾指针所指向的空单元的后继单元是队头元素所在的单元时,则停止人队。这样来,队尾指针永远追不上队头指针,所以队满时不会有front==rear。队列“满”的条件变为(rear+1)mod MAXSIZE==front,判队空的条件不变,仍是rear=front。
另一种方法是增设一个标志量tag,以区别队列是“空"还是“满”。初始化操作即产生一个空的循环队列,设Q->front=Q->rear=0,tag=0;当tag==0且Q->front==Q->rear时,表示队空。当tag=1且Q->front==Q->rear时,表示队满。这种方法不损失空间。 

下面主要介绍损失一个存储空间以区分队列空与满的方法实现:
//循环队列的类型定义
#define MAXSIZE 50  //队列的最大长度
typedef struct{
	QueueElementType element[MAXSIZE];  //队列的元素空间
	int front;  //头指针指示器
	int rear;  //尾指针指示器
}SeqQueue;

//初始化,将*Q初始化为一个空的循环队列
void InitQueue(SeqQueue *Q){
	Q->front=Q->rear=0;
}

//入队,将元素x入队
int EnterQueue(SeqQueue *Q,QueueElementType x){
	if((Q->rear+1)%MAXSIZE==Q->front)  //尾指针加1追上头指针,标志队列已经满了
		return(FALSE);
	Q->element[Q->rear]=x;
	Q->rear=(Q->rear+1)%MAXSIZE;  //重新设置队尾指针
	return(TRUE);
}

//出队,删除队列的队头元素,用x返回其值
int DeleteQueue(SeqQueue *Q,QueueElementType *x){
	if(Q->front==Q->rear)  return(FALSE);  //队列为空
	*x=Q->element[Q->front];
	Q->front=(Q->front+1)%MAXSIZE;  //重新设置队头指针
	return(TRUE);
}
除了栈和队列之外,还有一种限定性数据结构是双端队列(Deque)。双端队列是限定插入和删除操作都可以在表的两端进行的线性表。这两端分别称作端点1和端点2。在实际使用中,还可以有输出受限的双端队列,即一个端点允许插入和删除,另一个端点只允许插入的双端队列,以及输入受限的双端队列,即一个端点允许插入和删除,另一个端点只允许删除的双端队列。如果限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变成为两个栈底相邻接的栈了。尽管双端队列看起来比栈和队列更灵活,但实际上在应用程序中远不及栈和队列常用,故在此不做详细讨论。
内容小结:

写在最后,

因本系列文章主要为复习,故重点关注数据结构概念知识与理论知识,本章内容涉及到应用举例由于篇幅原因就不再展开,后续单独有文章《栈的应用举例和栈与递归的实现》《队列的的应用举例》说明,笔记仅作为参考,若读者发现内容有误请私信指正,谢谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你代码有bug!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值