栈与队列
栈
- 栈是限定仅在表尾进行插入和删除操作的线性表。(撤销、后退功能常用结构)
- 允许插入和删除的一端称为栈顶,也是表尾,另一端称为栈底,没有数据元素称为空栈,栈成为后进先出的线性表,LIFO结构。
- 栈元素具有线性关系,即前驱后继关系。
- 栈的插入操作,叫进栈、压栈、入栈;删除操作,叫出栈、弹栈。
栈的抽象数据类型
ADT 栈(Stack)
// 理论上线性表的操作特性它都具备
Data
同线性表,元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitStack(*S) 建立一个空栈
DestroyStack(*S) 若栈存在,销毁它
ClearStack(*S) 清空栈
StackEmpty(S) 栈为空,返回true,否则false
GetTop(S, *e) 若栈存在且非空,用e返回S的栈顶元素
Push(*S, e) S存在,插入新元素e到栈S中,为栈顶元素
Pop(*S, *e) 删除栈顶元素,并用e返回值
StackLength(S) 返回栈S的元素个数
endADT
栈的顺序存储结构及实现
栈的顺序存储是线性表顺序存储的简化------顺序栈。用数组实现,下标为0的一端作为栈底,首元素变化最小,因此作为栈底。
栈的结构
typedef int SElemType; //根据实际而定
typedef struct
{
SElemType data[MAXSIZE];
int top; //用于栈顶指针
}SqStack;
进栈push
int Push(SqStack *S, SElemType e)
{
is(S->top == MAXSIZE - 1)//判断栈满
{
return 0;
}
S->top++; //栈顶指针加1
S->data[S->top] = e; //将新元素赋值给栈顶空间
return 1;
}
出栈pop
int Pop(SqStack *S, SElemType *e)
{
is(S->top == -1)//判断栈空
{
return 0;
}
*e = S->data[S->top]; //将要删除的栈顶元素赋值给e
S->top--; //栈顶指针减1
return 1;
}
两栈共享空间
顺序栈存在一个问题,就是必须实现确定数组的存储空间大小,否则扩展容量比较麻烦。为此,两个相同类型的栈可以共享空间。方法为: 同一个数组存储两个栈,让一个栈的栈底做数组的始端,下标为0,另一个栈的栈底为数组的末端,下标为n-1(n为数组长度)。即存储时从数组的两端向中间靠拢,两个栈顶指针不碰面,就可以继续使用。
进栈
int Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
if(S->top1 + 1 == S->top2) //栈满
{
return 0;
}
if(stackNumber == 1)
{
S->data[++S->top1] = e; //top1+1后赋值新元素
}
else if(stackNumber == 2)
{
S->data[--S->top2] = e; //top2-1后赋值新元素
}
return 1;
}
出栈
int Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
if(stackNumber == 1)
{
if(S->top1 == -1)
{
return 0; //空栈
}
*e = S->data[S->top1--];
}
else if(stackNumber == 2)
{
if(S->top2 == MAXSIZE)
{
return 0; //栈空
}
*e = S->data[S->top2++];
}
return 1;
}
栈的链式存储结构及实现
栈的链式存储结构,简称链栈。
单链表有头指针,将栈顶指针和头指针合二为一,将栈顶放在单链表的头部。对于链栈来说,除非内存已经没有可以使用的空间了,否则不存在栈满的情况。
链栈的结构:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
进栈
int Push(LinkStack *S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = S->top; //将当前栈顶元素赋值给新结点的直接后继
S->top = s;
S->count++;
return 1;
}
出栈
int Pop(LinkStack *S, SElemType *e){
LinkStackPtr p;
if(StackEmpty(*S))
{
return 0;
}
*e = S->top->data;
p = S->top;
S->top = S->top-next;//使得栈顶指针下移一位。
free(p); //释放结点p
S->count--;
return 1;
}
对于顺序栈和链栈,时间复杂度是一样的,都是O(1);对于空间性能,顺序栈需要事前确定一个定长,存取时定位方便,而链栈要求每个元素都有指针域,增加了内存开销,但是对于栈的长度无限制。对于元素变化不可预料的,最好使用链栈。
栈的作用
许多高级语言都对栈结构进行了封装,不用关心实现细节,直接使用push和pop方法即可。
栈的应用
递归
一个函数直接调用自己或者通过一系列的调用语句间接地调用自己的函数,称为递归函数。每个递归定义必须至少有一个条件,满足时递归不再进行。
递归和迭代的区别:
- 迭代使用的是循环结构,不需要反复调用函数和占用额外的内存
- 递归使用的是选择结构,让程序的结构看起来清晰,但是大量的递归调用会建立函数的副本,消耗大量的时间和内存。
在递归的前行阶段,函数的局部变量、参数值以及返回地址都被压入栈中,在退回阶段,栈顶的局部变量、参数值、返回地址被弹出。
四则运算表达式
-
后缀表示法(逆波兰)
所有的符号都是在运算数字的后面出现。
例如:9+(3-1) * 3+10/2—>9 3 1 - 3 * + 10 2 / +
计算顺序:9、3、1进栈—>1、3出栈3-1=2入栈—>3进栈—>3、2出栈 3 * 2=6进栈—>6、9出栈 6+9=15进栈—>10、2进栈—>2、10出栈10/2=5进栈—>5 、15出栈 5+15=20进栈—>20出栈,栈空。
-
中缀表达式转后缀表达式
正常用的标准表达式都是中缀表达式
规则:(此时栈用来进出运算的符号)从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,成为后缀表达式的一部分;若是符号,判断其与栈顶符号的优先级,是右括号或者优先级低于栈顶,则栈顶元素依次输出(栈顶在变),并将当前符号进栈,一直到最终输出的后缀表达式为止。
队列
队列是只允许在一端进行插入操作、另一端进行删除操作的线性表。
- 队列是一种先进先出的线性表,FIFO结构,允许插入的一端是队尾,允许删除的一端是队头。
队列的抽象数据类型
ADT 队列(Queue)
// 理论上线性表的操作特性它都具备
Data
同线性表,元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q) 建立一个空队列
DestroyQueue(*Q) 若队列存在,销毁它
ClearQueue(*Q) 清空队列
QueueEmpty(Q) 队列Q为空,返回true,否则false
GetHead(Q, *e) 若队列Q存在且非空,用e返回Q的队头元素
EnQueue(*Q, e) S存在,插入新元素e到队列Q中,为队尾元素
DeQueue(*Q, *e) 删除队头元素,并用e返回值
QueueLength(Q) 返回队列Q的元素个数
endADT
循环队列
队列顺序存储的不足
入队操作其实就是在队尾加一个元素,不需要移动任何元素,时间复杂度为O(1),但是出队操作是在队头,下标为0的位置,其他元素都得向前移动,时间复杂度为O(n)。为了解决这个问题,可以让队头不一定在下标为0的位置。
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个元素,当front=rear时,为空队列。
当队头不在0位置,出栈之后不用前移,而追加元素后,rear指针已经越界,此时队头位置为空,队尾已经满,再追加元素,会产生“假溢出现象”。
循环队列就是头尾相接的顺序存储结构。队尾没有空间,rear指针可以指向下标为0的位置。
**问题:**rear = front时,队满或者队空,如何判断具体时队满还是队空呢?
- 方法一:设置一个标志变量flag,当front == rear时, 如果flag=0时为队空,如果flag=1时为队满。
- 方法二:当队空时,条件是rear = front,当队列满时,保留一个元素空间,也就是队列满,数组还有一个空闲单元。
注意:rear可能比front大,也可能小,可能rear与front只差一个位置,但是它们相差一圈。若最大尺寸为QueueSize,队满条件应该为(rear+1)%QueueSize == front。
队列长度:
- rear>front , 队列长 = rear-front
- rear<front , 队列长 = (rear-front+QueueSize)%QueueSize
入队
int EnQueue(SqQueue *Q, QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front)//判断队列满
{
return 0;
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear+1)%MAXSIZE;//rear指针后移一位,若到最后则转向数组头
//这也是%MAXSIZE的用处
return 1;
}
出队
int DeQueue(SqQueue *Q, QElemType *e)
{
if(Q->rear == Q->front) //判断队空,此时采用方法二区别队列空、满
{
return 0;
}
*e = Q->data[Q->front];
Q->front = (Q->front+1)%MAXSIZE;//front指针后移一位,若到最后则
//转到数组头部。
return 1;
}
队列的链式存储结构及实现
队列的链式结构,其实就是线性表的单链表,只不过是尾进头出,简称链队列。为了操作方便,将队头指针指向链队列的头结点,而队尾指针指向终端结点。当队列为空时,front和rear都指向头结点。
链队列的结构为:
typedef int QEmleType;
typedef struct QNode // 结点结构
{
QElmeType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct //队列的链表结构
{
QueuePtr front,rear;
}LinkQueue;
链队列—入队
int EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s) //存出分配失败
exit(OVERFLOW);
s->data = e;
s->next = NULL;
Q->rear->next = s; //新结点s分配给原队尾结点的后继
Q->rear = s; //当前的s设置为队尾结点,rear指向s
return 1;
}
链队列—出队
int DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if(Q->front == Q->rear)
return 0;
p = Q->front->next; //将要删除的队头结点暂存在p
*e = p->data;
Q->front->next = p->next; //将原队头结点后继赋值给头结点后继
if(Q->rear == p) //若队头时是队尾,删除后将rear指向头结点。
Q->rear = Q->front;
free(p);
return 1;
}
循环队列和链队列比较
- 时间上,两者基本操作都是常数时间,循环队列需要提前申请好空间,使用期间不释放,对于链队列,每次申请和释放存在时间开销。
- 空间上,循环队列必须要有一个固定长度,所以就有了存储元素个数和空间浪费的问题,而链队列尽管需要一个指针域,会产生开销,但是不存在上述问题。
注:内容来自《大话数据结构》,图使用Visio2016绘画后截图所得。