栈和队列与线性表的关系
栈和队列是两种重要的线性结构。栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表。
栈
抽象数据类型栈的定义
栈是限定仅在表尾进行插入或删除操作的线性表。因此,对栈来说,表尾端有特殊含义,称为栈顶,表头段称为栈底。不含元素的空表称为空栈。
假设栈S=(
a
1
a_1
a1 ,
a
2
a_2
a2 ,
a
3
a_3
a3 …
a
n
a_n
an),则称
a
1
a_1
a1为栈底元素,
a
n
a_n
an为栈顶元素。栈中元素按
a
1
a_1
a1 ,
a
2
a_2
a2 ,
a
3
a_3
a3 …
a
n
a_n
an的次序进栈,退栈的第一个元素应为栈顶元素。
也就是说栈的修改是按后进先出的原则进行的。因此又称栈为后进先出的线性表
栈的基本操作
栈的基本操作除了在栈顶进行插入或删除外,还有栈的初始化,判空及取栈顶元素等。
栈的定义
#define STACK_INIT_SIZE 100//存储空间初始分配量
#define STACKINCREMENT 10//存储空间分配增量
typedef struct
{
(栈中元素的类型)*base;//在栈构造之前和销毁后,base的值为NULL
(栈中元素的类型)*top;//栈顶指针 指向的是栈顶元素的下一个地址
int stacksize;//当前已分配的存储空间,以元素为单位
}SqStack;
构造一个空栈
InitStack(&S)
{
操作结果:构造一个空栈S
}
int InitStack(SqStack &S)
{
S.base=(栈中元素的类型*)malloc(STACK_INIT_SIZE*sizeof(栈中元素的类型));
S.top=S.base;
S.stacksize=STACK_INIT_SIZE;
return OK;
}
销毁栈
DestroyStack(&S)
{
初始条件:栈S已存在
操作结果:栈S被销毁
}
清空栈
ClearStack(&S)
{
初始条件:栈S已存在
操作结果:将S清为空栈
}
判断栈是否为空
StackEmpty(S)
{
初始条件:栈S已存在
操作结果:若栈S为空栈,则返回TRUE,否则返回FALSE
}
判断栈S的长度
StackLength(S)
{
初始条件:栈s已存在
操作结果:返回S的元素个数,即栈的长度
}
判断栈顶元素
GetTop(S,&e)
{
初始条件:栈S已存在
操作结果:用e返回S的栈顶元素
}
int GetTop(SqStack S,(栈中元素的类型)&e)
{
//若栈不空,则用e返回S的栈顶元素,并返回OK,否则返回ERROR
if(S.top==S.base) return ERROR;
e=*(S.top-1);
return OK;
}
插入新的元素到栈顶
Push(&S,e)
{
初始条件:栈S已存在
操作结果:插入元素e为新的栈顶元素
}
int Push(SqStack &S,(栈中元素的类型)e)
{
//插入元素e为新的栈顶元素
if(S.top-S.base>=S.stacksize)//如果栈满了,就追加存储空间
{
S.base=(栈中元素的类型*)realloc(S.base,(S.stacksize+STACKINCREMENT)*sizeof(栈中元素的类型));
S.top=S.base+S.stacksize;//更新扩大后的栈的栈顶地址
S.stacksize+=STACKINCREMENT;
}
*S.top++=e;//将新的元素赋值给栈顶。并且top指针向后移动一位
return OK;
}
弹出栈顶元素
Pop(&S,&e)
{
初始条件:栈S存在并且非空
操作结果:删除S的栈顶元素,并用e返回其值
}
int Pop(SqStack &S,栈中元素的类型&e)
{
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR
if(S.top==S.base) return ERROR;
e=*--S.top;//将top指向栈顶元素,并且赋值给e
return OK;
}
我们称插入元素的操作为入栈,删除栈顶元素的操作为出栈
剩下的那些没有写明的操作和普通的线性表类似可以看我写的线性表的文章。
线性表
栈的表示和实现
与线性表类似,栈也有两种存储表示方式。顺序存储和链式存储
顺序存储
即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指示栈顶元素在顺序栈中的位置。通常的做法是以
t
o
p
=
0
top=0
top=0表示空栈,但是由于栈在使用过程中所需最大空间的大小很难估计,因此,一般来说,在初始化设空栈时不应限定栈的最大容量。一个较合理的做法是先为栈分配一个基本容量,然后在应用过程中,当栈的空间不够使用时再逐段扩大,上面的代码中可见如何扩大
栈的链式表示也和普通线性表一样,也可以看我的线性表的文章
栈的应用举例
栈与递归的实现
栈还有一个重要应用是在程序设计语言中实现递归,一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数
队列
抽象数据类型队列的定义
和栈相反,队列是一种先进先出的线性表,它只允许在表的一端进行插入,而在另一段删除元素。最早进入队列的元素最早离开。在队列中允许插入的一端叫做队尾,允许删除的一端叫做队头。假设队列为q=( a 1 a_1 a1 , a 2 a_2 a2 , a 3 a_3 a3 … a n a_n an),那么, a 1 a_1 a1就是队头元素, a n a_n an 则是队尾元素,队列中的元素是按照 a 1 a_1 a1 , a 2 a_2 a2 , a 3 a_3 a3 … a n a_n an的顺序进入的,退出队列也只能按照这个顺序依次退出。
队列的基本操作
队列的操作与栈的操作类似,也有8个,不同的是删除是在表的头部(即队头)进行
用链表表示的队列简称为链队列,一个链队列显然需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能惟一确定。这里,和线性表的单链表一样为了操作方便,我们也给链队列添加一个头结点,并令头指针指向头结点,由此,空的链队列的判决条件为头指针和尾指针均指向头结点。链队列的操作即为单链表的插入和删除操作的特殊情况,只是尚需修改尾指针或头指针。
typedef struct QNode
{
(数据类型) data;
struct QNode *next;//指向下一个结点
}QNode,*QueuePtr;//元素类型的指针
typedef struct
{
QueuePtr front;//队头指针
QueuePtr rear;//队尾指针
}LinkQueue;
初始化队列
InitQueue(&Q)
{
操作结果:构造一个空队列Q
}
int InitQueue(LinkQueue &Q)
{
//构造一个空队列Q
Q.front = Q.rear = (QueuePtr) malloc (sizeof(QNode));
Q.front -> next = NULL;
return OK;
}
销毁队列
DestroyQueue(&Q)
{
初始条件:队列Q已存在
操作结果:队列Q被销毁,不再存在
}
int DestroyQueue(LinkQueue &Q)
{
while(Q.front)
{
Q.rear=Q.front->next;//队尾指针从队头开始
free(Q.front);
Q.front=Q.rear;
}
return OK;
}
清空队列
ClearQueue(&Q)
{
初始条件:队列Q已存在
操作结果:将Q清为空队列
}
QueueEmpty(Q)
{
初始条件:队列Q已存在
操作结果:若Q为空队列,则返回TRUE,否则FALSE
}
QueueLength(Q)
{
初始条件:队列Q已存在
操作结果:返回Q的元素个数,即队列的长度
}
GetHead(Q,&e)
{
初始条件:队列Q已存在
操作结果:用e返回Q的队头元素
}
EnQueue(&Q,e)
{
初始条件:队列Q已存在
操作条件:插入元素e为Q的新的队尾元素
}
int EnQueue(LinkQueue &Q,(元素数据类型) e)
{
p = (QueuePtr) malloc (sizeof(QNode));
p -> data = e;
p -> next = NULL;
Q.rear -> next = p;//此时队尾指针所指向的元素的下一个元素设定为p
Q.rear = p;//将队尾指针的指向后移一位
return OK;
}
DeQueue(&Q,&e)
{
初始条件:队列Q为非空队列
操作条件:删除Q的队头元素,并用e返回其值
}
int DeQueue(LinkQueue &Q, (元素数据类型) &e)
{
//若队列不空,则删除队头元素,用e返回其值,并返回OK
//否则返回ERROR
if(Q.front == Q.rear) return ERROR;
p = (QueuePtr) malloc (sizeof(QNode));
p = Q.front -> next;//p等于头结点,因为front不是指向头结点 front的next才是
e = p -> data;
Q.front -> next = p-> next;//跳过头节点 等于删除
free(p);
return OK;
}
在上述算法描述中,要注意删除队列头元素算法中的特殊情况。一般情况下,删除队列头元素时仅需修改头节点中的指针,但当队列中最后一个元素被删除后,队列尾指针也丢失,因此需要对队尾指针重新赋值(指向头结点)
上面讲的是链表实现,栈队列还可以用顺序存储结构来表示和实现
和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需附设两个指针front和rear分别指示队列头元素和队列尾元素的位置。
初始化建空队列时,令
f
r
o
n
t
=
r
e
a
r
=
0
front=rear=0
front=rear=0,每当插入新的队列元素时,“尾指针增1”,每当删除队列头元素时,“头指针增1”。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。
循环队列
但是由于顺序栈中我们需要初始化大小,既然有初始化就代表一开始的大小是有范围的,也就是有没办法再插入元素的状态,例如在顺序栈中我们数组大小不够用的情况下可以存储再分配扩大数组空间,但是在队列中无法如此,因为队列是先进先出的特性,所以可能用完空间的时候,前面有部分空间并未占满,这样对于存储再分配而言就略显麻烦。一个较巧妙的方法就是将顺序队列臆想为一个环状的空间,也就是循环队列
后续再补 枯竭了
双端队列
除了栈和队列外,还有一种限定性数据结构:双端队列
双端队列是限定插入和删除操作在表的两端进行的线性表。这两端分别称做端点1和端点2,在实际使用过程中还可以有输出受限的双端队列(即一个端点允许插入和删除,另一个端点只允许插入的双端队列)和输入受限的双端队列(即一个端点允许插入和删除,另一个端点只允许删除的双端队列)。而如果限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈了