定义:栈是限定仅在尾表进行插入和删除操作的线性表。允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈是一种特殊的线性表,只能在线性表的表尾进行插入和删除操作,表尾指的就是栈顶。
栈的插入操作,叫做进栈,压栈,入栈。栈的删除操作,叫做出栈,弹栈。
最先进栈的元素不一定是只能最后出栈的元素,栈对线性表中元素的进出时间没有限制。三个元素有五种出栈次序。
ADT 栈(stack)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系
Operation.
InitStack(*S):初始化,建立一个空栈
DestroyStack(*S):栈空,销毁
ClearStack(*S):将栈清空
StackEmpty(S):如果栈空,返回不同布尔值
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素
Push(*S,e):若栈存在,插入新元素e到栈S中并成为栈顶元素
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值
StackLength(S):返回栈S的元素个数
endADT
结构定义:
/*栈的结构定义*/
typedef int SElemType;
typedef struct
{
SElemType data[MAXSIZE];
int top; //用于栈顶指针,空栈是为-1
}SqStack;
进栈操作:
/*插入元素e为新的栈顶元素*/
Status Push(SqStack *S,SElemType e)
{
if (S->top == MAXSIZE-1) //栈满
{
return ERROR;
}
S->top++; //栈顶指针自增1
S->data[S->top]=e; //新插元素赋值给栈顶空间
return OK;
}
出栈操作:
/*插入元素e为新的栈顶元素*/
Status Push(SqStack *S,SElemType e)
{
if (S->top == MAXSIZE-1) //栈满
{
return ERROR;
}
S->top++; //栈顶指针自增1
S->data[S->top]=e; //新插元素赋值给栈顶空间
return OK;
}
栈顶指针在数组两端,向中间靠拢。当栈1空时,也就是top1= -1,当栈2空时,即top2=n。当top1+1=top2时栈满。
两站共享空间结构:
/*两栈共享空间结构*/
typedef struct
{
SElemType data[MAXSIZE];
int top1; //栈1栈顶指针
int top2; //栈2栈顶指针
}SqDoubleStack;
/*插入元素e为新的元素*/
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
if (S->top1 == S->top2) //栈满,不能再有新元素入栈
{
return ERROR;
}
if (stackNumber == 1) //栈1有元素进栈
{
S->data[++S->top1]=e; //先top1+1后给数组元素赋值
}
else if (stackNumber == 2) //栈2有元素进栈
{
S->data[--S->top2]=e; //先top2-1后给数组元素赋值
}
return OK;
}
/*若栈不空,则删除S栈顶元素,用e返回其值,返回OK,否则返回ERROR*/
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
if (stackNumber == 1)
{
if (S->top1 == -1)
{
return ERROR; //说明栈1已经是空栈,溢出
}
*e = S->data[S->top1--]; //将栈一的栈顶元素出栈,并使top1-1
}
else if (stackNumber == 2)
{
if (S->top2==MAXSIZE)
{
return ERROR; //说明栈2已经是空栈,溢出
}
*e = S->data[S->top2++]; //将栈二的栈顶元素出栈,并使top2+1
}
return OK;
}
单链表有头指针,栈顶指针也是必须的,所以可以让它们俩合二为一,所以好的方法是把栈顶放在单链表的头部。因此也不需要头结点了。
链栈的结构代码:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
进栈操作:
Status Push(LinkStack *S, SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; //把当前的栈顶元素赋值,给新结点的的直接后继
S->top=s; //将新结点赋值给栈顶指针
S->count++;
return OK;
}
出栈操作:
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if (StackEmpty(*S))
{
return ERROR;
}
*e = S->top->data;
p=S->top; //将栈顶结点赋值给P
S->top=S->top->next; //使得栈顶指针下移一位,指向后一结点
free(p);
S->count--;
return OK;
}
如果栈的使用过程中元素的变化不可预料,最好使用链栈,反而如果它的变化在可控范围内,建议使用顺序栈。时间复杂度都是O(1)。
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决问题的核心。
把一个直接调用自己或通过一系列的调用语句间接调用自己的函数,称为递归函数。
/*斐波那契的递归函数*/
int Fbi(int i)
{
if (i<2)
{
return i == 0?0:1;
}
return Fbi(i-1)+Fbi(i-2); //这里Fbi就是函数自己,它在调自己
}
int main()
{
int i;
for (int i = 0; i < 40; i++)
{
printf("%d", Fbi(i));
}
return 0;
}
逆波兰表示法,不需要括号的后缀表达式法,所有的符号都是在要运算数字的后面出现,利用栈可以对后缀表达式进行处理。
规则:从左到右遍历表达式的每个数字和符号,遇到数字就进栈,遇到符号,就将处于栈顶的两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
中缀表达式:平时所用的标准四则运算表达式。
中缀表达式转后缀表达式:从左到右遍历中缀表达式的每个数字和符号,遇到数字就输出,即成为后缀表达式的一部分;若是符号,则判断与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
计算机处理四则运算表达式:
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
ADT 队列(queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系
Operation.
InitQueue(*Q):初始化操作,建立一个空队列Q
DestroyQueue(*Q):队列存在则销毁它
ClearQueue(*Q):将队列Q清空
QueueEmpty(Q):若队列为空,返回TRUE
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q中的对头元素
EnQueue(*Q,e):若队列存在,插入新元素e到队列Q中并成为队尾元素
DeQueue(*Q,*e):删除队列Q中对头元素,并用e返回其值
QueueLength(Q):返回队列Q中元素个数
endADT
为了避免当只有一个元素时,对头和队尾重合时处理变得麻烦,所以引入两个指针,front指向队头元素,rear指向队尾元素的下一个位置。把队列的这种头尾相接的顺序存储结构称为循环队列。
通用的计算队列长度公式为:
(rear - front +QueueSize) %QueueSize
typedef int QElemType; //根据实际清空确定时int还是其他类型
/*循环队列的顺序存储结构*/
typedef struct
{
QElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不为空,指向队尾元素的下一个位置
}SqQueue;
/*初始化一个空队列Q*/
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
/*返回Q的元素个数,也就是队列的当前长度*/
int QElemType(SqQueue)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
/*若队列未满,则插入元素e为Q的新的队尾元素*/
Status EnQueue(SqQueue *Q,QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) //队列满的判断
{
return ERROR;
}
Q->data[Q->rear]=e; //将元素e赋值给队尾
Q->rear=(Q->rear+1)%MAXSIZE; //rear指针向后移一位
//若到最后则转到数组的头部
return OK;
}
/*若队列不空,则删除Q中对头元素,用e返回其值*/
Status DeQueue(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear) //队列空的判断
{
return ERROR;
}
*e = Q->data[Q->front]; //对头元素赋值给e
Q->front = (Q->front+1)%MAXSIZE; //front指针向后移一位置
//若到最后则转到数组的头部
return OK;
}
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出,简称为链队列。队头指针(front)指向链队列的头结点,队尾指针(rear)指向终端结点。空对列时front和rear都指向头结点。
链队列的结构:
typedef int QElemType;
typedef struct QNode //结点结构
{
QElemType data;
struct QNode *next
}QNode,*QueuePtr;
typedef struct //队列的链表结构
{
QueuePtr front,rear; //对头、队尾指针
}LinkQueue;
入队操作:
/*插入元素e为Q的新的队尾元素*/
Status 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; //把拥有元素e的新结点s赋值给原队尾元素的后继
Q->rear=s; //把当前的s设置为队尾结点,rear指向s
return OK;
}
出队操作:
/*若队列不空,删除Q的对头元素,用e返回其值,并返回OK,否则返回ERROR*/
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr p;
if (Q->front == Q->rear)
{
return ERROR;
}
p=Q->front->next; //将欲删除的对头结点暂存给p
*e=p->data; //将欲删除的对头结点的值赋值给e
Q->front->next=p->next; //将原对头结点的后继p->next赋值给头结点的后继
if (Q->rear==p) //若对头是队尾,则删除后将rear指向头结点。
{
Q->rear = Q->front;
}
free(p);
return OK;
}
主要讲解了栈和队列,它们均可以用线性表的顺序存储结构来实现,但都存在着顺序存储的一些弊端,因此它们各自使用技巧来解决这个问题。
对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。
对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入循环队列,使得对头和队尾可以在数组中循环变化。使得本来插入和删除是O(n)的时间复杂度变成了O(1)。