目录
栈和队列
【知识框架】

一、栈的基本概念
1、栈的定义
2、栈的常见基本操作
二、栈的顺序存储结构
1、栈的顺序存储
2、顺序栈的基本算法
3、共享栈(两栈共享空间)
三、栈的链式存储结构
1、链栈
2、链栈的基本算法
3、性能分析
四、栈的应用——递归
1、递归的定义
2、斐波那契数列
五、栈的应用——四则运算表达式求值
1、后缀表达式计算结果
2、中缀表达式转后缀表达式
队列
一、队列的基本概念
1、队列的定义
2、队列的常见基本操作
二、队列的顺序存储结构
1、顺序队列
2、循环队列
3、循环队列常见基本算法
三、队列的链式存储结构
1、链队列
2、链队列常见基本算法
附录
参考资料

栈和队列

【知识框架】

数据结构--栈和队列_赋值



一、栈的基本概念

1、栈的定义

(Stack):是只允许在一端进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。

数据结构--栈和队列_赋值_02

  • 栈顶:线性表允许进行插入删除的那一端。
  • 栈底:固定的,不允许进行插入和删除的另一端。
  • 空栈:不含任何元素的空表。


2、栈的常见基本操作

  • Push(&S, x):进栈(栈的插入操作),若栈S未满,则将x加入使之成为新栈顶。
  • Pop(&S, &x):出栈(栈的删除操作),若栈S非空,则弹出栈顶元素,并用x返回。
  • GetTop(S, &x):读栈顶元素,若栈S非空,则用x返回栈顶元素。
  • DestroyStack(&S):栈销毁,并释放S占用的存储空间(“&”表示引用调用)。
  • InitStack(&S):初始化一个空栈S。
  • StackEmpty(S):判断一个栈是否为空,若栈为空则返回true,否则返回false。



二、栈的顺序存储结构

1、栈的顺序存储

采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置。

若存储栈的长度为StackSize,则栈顶位置top必须小于StackSize。当栈存在一个元素时,top等于0,因此通常把空栈的判断条件定位top等于-1。

栈的顺序存储结构可表示为

#define MAXSIZE 50  //定义栈中元素的最大个数
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
typedef struct{
ElemType data[MAXSIZE];
int top; //用于栈顶指针
}SqStack;

若现在有一个栈,StackSize是5,则栈的普通情况、空栈、满栈的情况分别如下图所示:

数据结构--栈和队列_赋值_03


2、顺序栈的基本算法

1.初始化

void InitStack(SqStack *S){
S->top = -1; //初始化栈顶指针
}

2.判栈空

bool StackEmpty(SqStack S){
if(S.top == -1){
return true; //栈空
}else{
return false; //不空
}
}

3.进栈

进栈操作push

/*插入元素e为新的栈顶元素*/
Status Push(SqStack *S, ElemType e){
//满栈
if(S->top == MAXSIZE-1){
return ERROR;
}
S->top++; //栈顶指针增加一
S->data[S->top] = e; //将新插入元素赋值给栈顶空间
return OK;
}

数据结构--栈和队列_循环队列_04

4.出栈

/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(SqStack *S, ElemType *e){
if(S->top == -1){
return ERROR;
}
*e = S->data[S->top]; //将要删除的栈顶元素赋值给e
S->top--; //栈顶指针减一
return OK;
}

5.读栈顶元素

/*读栈顶元素*/
Status GetTop(SqStack S, ElemType *e){
if(S->top == -1){ //栈空
return ERROR;
}
*e = S->data[S->top]; //记录栈顶元素
return OK;
}


3、共享栈(两栈共享空间)

1.共享栈概念

数据结构--栈和队列_循环队列_05

两个栈的栈顶指针都指向栈顶元素,top0=-1时0号栈为空,top1=MaxSize时1号栈为空;仅当两个栈顶指针相邻(top0+1=top1)时,判断为栈满。当0号栈进栈时top0先加1再赋值,1号栈进栈时top1先减一再赋值出栈时则刚好相反。


2.共享栈的空间结构

/*两栈共享空间结构*/
#define MAXSIZE 50 //定义栈中元素的最大个数
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
/*两栈共享空间结构*/
typedef struct{
ElemType data[MAXSIZE];
int top0; //栈0栈顶指针
int top1; //栈1栈顶指针
}SqDoubleStack;

3.共享栈进栈

对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要有一个判断是栈0还是栈1的栈号参数stackNumber。

/*插入元素e为新的栈顶元素*/
Status Push(SqDoubleStack *S, Elemtype e, int stackNumber){
if(S->top0+1 == S->top1){ //栈满
return ERROR;
}
if(stackNumber == 0){ //栈0有元素进栈
S->data[++S->top0] = e; //若栈0则先top0+1后给数组元素赋值
}else if(satckNumber == 1){ //栈1有元素进栈
S->data[--S->top1] = e; //若栈1则先top1-1后给数组元素赋值
}
return OK;
}

4.共享栈出栈

/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(SqDoubleStack *S, ElemType *e, int stackNumber){
if(stackNumber == 0){
if(S->top0 == -1){
return ERROR; //说明栈0已经是空栈,溢出
}
*e = S->data[S->top0--]; //将栈0的栈顶元素出栈,随后栈顶指针减1
}else if(stackNumber == 1){
if(S->top1 == MAXSIZE){
return ERROR; //说明栈1是空栈,溢出
}
*e = S->data[S->top1++]; //将栈1的栈顶元素出栈,随后栈顶指针加1
}
return OK;
}

三、栈的链式存储结构

1、链栈

采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头节点,Lhead指向栈顶元素

数据结构--栈和队列_循环队列_06

对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL的时候。

/*栈的链式存储结构*/
/*构造节点*/
typedef struct StackNode{
ElemType data;
struct StackNode *next;
}StackNode, *LinkStackPrt;
/*构造链栈*/
typedef struct LinkStack{
LinkStackPrt top;
int count;
}LinkStack;



2、链栈的基本算法

1.链栈的进栈



/*插入元素e为新的栈顶元素*/
Status Push(LinkStack *S, ElemType e){
LinkStackPrt p = (LinkStackPrt)malloc(sizeof(StackNode));
p->data = e;
p->next = S->top; //把当前的栈顶元素赋值给新节点的直接后继
S->top = p; //将新的结点S赋值给栈顶指针
S->count++;
return OK;
}

对于链栈的进栈push操作,假设元素值为e的新节点是s,top为栈顶指针,示意图如下:

数据结构--栈和队列_循环队列_07

2.链栈的出栈

/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(LinkStack *S, ElemType *e){
LinkStackPtr p;
if(StackEmpty(*S)){
return ERROR;
}
*e = S->top->data;
p = S->top; //将栈顶结点赋值给p
S->top = S->top->next; //使得栈顶指针下移一位,指向后一结点
free(p); //释放结点p
S->count--;
return OK;
}

链栈的出栈pop操作,也是很简单的三句操作。假设变量p用来存储要删除的栈顶结点,将栈顶指针下移以为,最后释放p即可,如下图所示:

数据结构--栈和队列_循环队列_08

3.性能分析

  • 链栈的进栈push和出栈pop操作都很简单,时间复杂度均为O(1)。
  • 如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。


四、栈的应用——递归

1、递归的定义

递归是一种重要的程序设计方法。简单地说,若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。

2、斐波那契数列

古典问题:兔子繁殖问题

说如果兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子 来。
假设所有兔都不死,那么一年以后可以繁殖多少对兔子呢?
第一个月初有一对刚诞生的兔子
第二个月之后(第三个月初)它们可以生育
每月每对可生育的兔子会诞生下一对新兔子
兔子永不死去

我们拿新出生的一对小兔子分析一下:第一个月小兔子没有繁殖能力,所以还是一对;两个月后,生下一对小兔子数共有两对;三个月以后,老兔子又生下一对,因为小兔子还没有繁殖能力,所以一共是三对…依次类推得出这样一个图

数据结构--栈和队列_循环队列_09

数据结构--栈和队列_赋值_10

/*斐波那契数列的实现*/
int Fib(int n){
if(n == 0){
return 0; //边界条件
}else if(n == 1){
return 1; //边界条件
}else{
return Fib(n-1) + Fib(n-2); //递归表达式
}
}

必须注意递归模型不能是循环定义的,其必须满足下面的两个条件

  • 递归表达式
  • 边界条件

数据结构--栈和队列_赋值_11


五、栈的应用——四则运算表达式求值

1、后缀表达式计算结果

表达式求值是程序设计语言编译中一个最基本的问题,它的实现是栈应用的一个典型范例。中缀表达式不仅依赖运算符的优先级,而且还要处理括号。后缀表达式的运算符在操作数后面,在后缀表达式中已考虑了运算符的优先级,没有括号,只有操作数和运算符

数据结构--栈和队列_进栈_12

数据结构--栈和队列_进栈_13


2、中缀表达式转后缀表达式

我们把平时所用的标准四则运算表达式,即a + b − a ∗ ( ( c + d ) / e − f ) + g a+b-a*((c+d)/e-f)+ga+b−a∗((c+d)/e−f)+g叫做中缀

表达式。因为所有的运算符号都在两数字的中间,现在我们的问题就是中缀到后缀的转化。

规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后
缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,
是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,
并将当前符号进栈,一直到最终输出后缀表达式为止。

队列

一、队列的基本概念

1、队列的定义

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

先进先出

数据结构--栈和队列_赋值_14

队头(Front):允许删除的一端,又称队首。

队尾(Rear):允许插入的一端。

空队列:不包含任何元素的空表。


2、队列的常见基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列Q。
  • QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
  • EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。
  • DeQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。
  • GetHead(Q, &x):读队头元素,若队列Q非空,则将队头元素赋值给x。


二、队列的顺序存储结构

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针 front指向队头元素,队尾指针 rear 指向队尾元素的下一个位置。


1、顺序队列

#define MAXSIZE 50  //定义队列中元素的最大个数
typedef struct{
ElemType data[MAXSIZE]; //存放队列元素
int front,rear;
}SqQueue;

2、循环队列

解决假溢出的方法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列

  • 初始时:Q->front = Q->rear=0。
  • 队首指针进1:Q->front = (Q->front + 1) % MAXSIZE。
  • 队尾指针进1:Q->rear = (Q->rear + 1) % MAXSIZE。
  • 队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE。


数据结构--栈和队列_赋值_15



3、循环队列常见基本算法

1.循环队列的顺序存储结构

typedef int ElemType;   //ElemType的类型根据实际情况而定,这里假定为int
#define MAXSIZE 50 //定义元素的最大个数
/*循环队列的顺序存储结构*/
typedef struct{
ElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;

2.循环队列的初始化

/*初始化一个空队列Q*/
Status InitQueue(SqQueue *Q){
Q->front = 0;
Q->rear = 0;
return OK;
}

3.循环队列判队空

/*判队空*/
bool isEmpty(SqQueue Q){
if(Q.rear == Q.front){
return true;
}else{
return false;
}
}

4.求循环队列长度

/*返回Q的元素个数,也就是队列的当前长度*/
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}

5.循环队列入队

/*若队列未满,则插入元素e为Q新的队尾元素*/
Status EnQueue(SqQueue *Q, ElemType e){
if((Q->real + 1) % MAXSIZE == Q->front){
return ERROR; //队满
}
Q->data[Q->rear] = e; //将元素e赋值给队尾
Q->rear = (Q->rear + 1) % MAXSIZE; //rear指针向后移一位置,若到最后则转到数组头部
return OK;
}

6.循环队列出队

/*若队列不空,则删除Q中队头元素,用e返回其值*/
Status DeQueue(SqQueue *Q, ElemType *e){
if(isEmpty(Q)){
return REEOR; //队列空的判断
}
*e = Q->data[Q->front]; //将队头元素赋值给e
Q->front = (Q->front + 1) % MAXSIZE; //front指针向后移一位置,若到最后则转到数组头部
}


三、队列的链式存储结构

1、链队列

队列的链式存储结构表示为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表,只不过它只能尾进头出而已

数据结构--栈和队列_循环队列_16

空队列时,front和real都指向头结点。

数据结构--栈和队列_循环队列_17

2、链队列常见基本算法

1.链队列存储类型

/*链式队列结点*/
typedef struct {
ElemType data;
struct LinkNode *next;
}LinkNode;
/*链式队列*/
typedef struct{
LinkNode *front, *rear; //队列的队头和队尾指针
}LinkQueue;

2.链队列初始化

void InitQueue(LinkQueue *Q){
Q->front = Q->rear = (LinkNode)malloc(sizeof(LinkNode)); //建立头结点
Q->front->next = NULL; //初始为空
}

数据结构--栈和队列_进栈_18

3.链队列入队

Status EnQueue(LinkQueue *Q, ElemType e){
LinkNode s = (LinkNode)malloc(sizeof(LinkNode));
s->data = e;
s->next = NULL;
Q->rear->next = s; //把拥有元素e新结点s赋值给原队尾结点的后继
Q->rear = s; //把当前的s设置为新的队尾结点
return OK;
}

数据结构--栈和队列_赋值_19

4.链队列出队

出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点。

数据结构--栈和队列_赋值_20

/*若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR*/
Status DeQueue(LinkQueue *Q, Elemtype *e){
LinkNode p;
if(Q->front == Q->rear){
return ERROR;
}
p = Q->front->next; //将欲删除的队头结点暂存给p
*e = p->data; //将欲删除的队头结点的值赋值给e
Q->front->next = p->next; //将原队头结点的后继赋值给头结点后继
//若删除的队头是队尾,则删除后将rear指向头结点
if(Q->rear == p){
Q->rear = Q->front;
}
free(p);
return OK;
}

附录

参考资料

1、严蔚敏、吴伟民:《数据结构(C语言版)》

2、程杰:《大话数据结构》

3、王道论坛:《数据结构考研复习指导》

4、托马斯·科尔曼等人:《算法导论》