1. 栈和队列的定义和特点
栈和队列是限定插入和删除只能在“表的端点”进行的线性表。(栈:插入和删除都在表尾;队列:插入在表尾,删除在表头)栈和队列是线性表的子集,是插入和删除位置受限的线性表。
由于栈的插入和删除只能在表尾进行,因此栈满足先进后出的特点(手枪弹夹),如果求解问题的过程具有“后进先出”的特点时,那求解算法中就要用到栈了。需要用栈求解的问题如下:
而队列的插入在表尾进行,删除在表头进行,因此队列满足先进先出的特点(排队),如果求解问题的过程具有“先进先出”的特点时,那求解算法中就要用到队列了。需要用队列求解的问题如下:
1.1 栈的定义和特点
栈(stack)是一种特殊的线性表,是限定仅在一段(通常是表尾)进行插入和删除操作的线性表。又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈的结构示意图:
思考:假设三个元素a,b,c的入栈顺序是a,b,c,则他们的出栈顺序有几种?有可能是c,a,b吗?
①c,b,a;②a,b,c;③a,c,b;④b,a,c;⑤b,c,a。
不可能为c,a,b。
栈的相关概念:
1.2 队列的定义和特点
队列(queue)是一种先进先出(First In First Out)的线性表。在表尾插入,在表头删除。
队列的相关概念:
2. 案例引入
2.1 进制转换
将十进制数159转换为八进制数:
159除以8,商19余7;
19除以8,商2余3;
2除以8,商0余2.
为了得到转换后的八进制数,我们必须逆序输出所求出的余数。此时刚好可是利用栈后进先出的特点。
2.2 括号匹配的检验
检验(()])是否正确:
将左括号压栈,右括号直接判断与栈顶元素是否匹配,若匹配则出栈,继续检验下一个元素;若不匹配,则说明为错误格式。
2.3 表达式求值
表达式求值是程序设计语言变异种一个最基本的问题,它的实现也需要运用栈。这里介绍的算法是由运算符优先级确定运算顺序的对表达式求值算法——算符优先算法。
2.4 舞伴问题
3. 栈的表示和操作的实现
3.1 栈的抽象数据类型的类型定义
初始化操作
InitStack(&S)
操作结果:构造一个空栈S
销毁栈操作
DestroyStack(&S)
初始条件:栈S已存在
操作结果:栈S被销毁
判断S是否为空
EmptyStack(S)
初始条件:栈S已存在
操作结果:若栈为空,返回True,否则返回False
求栈的长度
StackLEngth(S)
初始条件:栈S已存在
操作结果:返回栈S的元素个数
取栈顶元素
GetTop(S,&e)
初始条件:栈S已存在
操作结果:用e返回栈顶元素
栈置空操作
StackClear(&S)
初始条件:栈S已存在
操作结果:将S置为空栈
入栈操作
StackPush(&S,e)
初始条件:栈S已存在
操作结果:插入新元素e为栈顶元素
求栈的长度
StackPop(&S,&e)
初始条件:栈S已存在且非空
操作结果:删除S的栈顶元素an,并用e返回
3.2 顺序栈的表示与实现
存储方式:利用一组连续的存储单元依次存放自栈底到栈顶的数据元素,栈底一般在低地址端。(与一般线性表的顺序存储结构完全相同)。
附设Top指针(指向栈顶元素)和base指针(指向栈低底元素),另外用Stacksize表示栈的最大容量。(Top指针一般指向栈顶元素之上的下标位置)
模拟入栈和出栈、判断空栈和栈满的操作:
使用数组作为顺序栈存储方式的特点:简单、方便,但易产生溢出(数组大小固定)。
上溢:栈已满,又要压入元素;
下溢:栈以空,还要弹出元素。
注意:上溢是一种错误,使问题的处理无法进行;而下溢一般认为是一种结束条件,即问题处理结束。
代码实现:
#define MAXSIZE 100
typedef int SElemType;
//定义栈的结构体类型
typedef struct SqStack
{
SElemType* base;
SElemType* Top;
int stacksize;
}SqStack;
//栈的初始化
void SqStackInit(SqStack* S)
{
(*S).base = (SElemType*)malloc(sizeof(SElemType) * MAXSIZE);
assert((*S).base != NULL);
(*S).Top = (*S).base;
(*S).stacksize = MAXSIZE;
}
//判断栈是否为空
int SqStackEmpty(SqStack S)
{
if (S.base == S.Top)
return 1; //为空返回1
return 0; // 不为空返回0
}
//获取栈的长度
int SqStackLength(SqStack S)
{
return S.Top - S.base;
}
//清空栈
void SqStackClear(SqStack* S)
{
assert(S != NULL);
S->Top = S->base;
}
//销毁栈
void SqStackDestory(SqStack* S)
{
assert(S != NULL);
free(S->base);
S->stacksize = 0;
S->Top = S->base = NULL;
}
//栈的插入
void SqStackPush(SqStack* S,SElemType e)
{
assert(S != NULL);
if (S->Top - S->base == S->stacksize)
{
printf("栈已满,无法插入!");
return;
}
*S->Top++ = e;
}
//栈的删除
void SqStackPop(SqStack* S)
{
assert(S != NULL);
if (S->Top == S->base)
{
printf("该栈为空栈,无法出栈!");
return;
}
*S->Top--;
}
3.3 链栈的表示与实现
链栈的表示:
代码如下:
//链栈结构体的类型定义
typedef int SElemType;
typedef struct StackNode
{
SElemType data;
StackNode* next;
}StackNode, * LinkStack;
//链栈的初始化
void LinkStackInit(LinkStack* S)
{
*S = NULL;
}
//判断链栈是否为空
int LinkStackEmpty(LinkStack S)
{
if (S == NULL)
return 1; //为空返回1
return 0; //补为空返回0
}
//链栈的入栈操作
void LinkStackPush(LinkStack* S, SElemType e)
{
assert(S != NULL);
StackNode* p = (StackNode*)malloc(sizeof(StackNode));
p->data = e;
p->next = *S;
*S = p;
}
//链栈的出栈操作
void LinkStackPop(LinkStack* S)
{
assert(S != NULL);
if (*S == NULL)
{
printf("该链表为空栈,无法出栈!");
return;
}
StackNode* p = *S;
*S = (*S)->next;
free(p);
}
//打印链栈
void LinkStackShow(LinkStack S)
{
StackNode* p = S;
while (p)
{
printf("%d ", p->data);
p = p->next;
}
}
//获取栈顶元素
SElemType GetTop(LinkStack S)
{
if (S)
return S->data;
printf("该链栈为空栈,无法取首元素!");
return false;
}
3.3 栈与递归
若一个过程直接地或间接地自己调用自己,则称这个过程是递归的过程。
以下三种情况常常用到递归:
3.3.1 递归问题——用分治法求解
分治法:对于一个较为复杂的问题可以分解为几个简单的且解法相同或类似的子问题来求解。
3.3.2 函数调用过程
调用前:①将实参、返回地址等传递给被调用函数;②为被调用函数的局部变量分配存储区;③将控制转移到被调用函数的入口。
调用后:①保存被调用函数的计算结果:②释放被调用函数数据区;③依照被调用函数保存的返回地址将控制权转移到调用函数。
例:求解n的阶乘
int Fact(int n) //递归函数
{
if (n == 1)
return n;
return n * Fact(n - 1);
}
int main()
{
int n;
scanf_s("%d", &n);
int s = Fact(n);
printf("%d \n", s);
return 0;
}
递归的优点:结构清晰,程序易读;
递归的缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息,时间开销大。
3.3.3 将递归转化为非递归的方法
方法1:将尾递归、单向递归转化为循环结构;(尾递归:程序中只有一句递归语句,且在末尾;单向递归:)
/*尾递归改写为循环性结构*/
int Fact(int n) //尾递归函数,求阶乘
{
if (n == 1)
return n;
return n * Fact(n - 1);
}
int FactCir(long n) //循环结构,求阶乘
{
int t = 1;
for (int j = 2; j <= n; j++)
t = t * j;
return t;
}
/*单向递归改写为尾递归*/
long Fib(long n) //单向递归函数,斐波那契数列
{
if (n == 1 || n == 2)
return 1;
return Fib(n - 1) + Fib(n - 2);
}
long FibCir(long n) //循环结构,斐波那契数列
{
long t1, t2, t3;
t1 = t2 = 1;
for (int i = 3; i <= n; i++)
{
t3 = t1 + t2;
t1 = t2;
t2 = t3;
}
return t2;
}
方法2:自用栈模拟系统的运行栈。
4. 队列的表示和实现
队列示意图:
相关术语:
队列的抽象数据类型定义:
4.1 队列的顺序表示和实现
设数组的大小为MAXQSIZE,当rear=MAXQSIZE时,进行入队,则会发生溢出现象。
那么我们有没有办法能够解决假溢出的问题呢?在这里我们引入循环队列来解决假溢出问题:将base[0]接在base[MAXQSIZE-1]之后,若rear+1等于MAXQSIZE,则令rear=0。那么如何实现呢?
实现方法:利用模(mod ,C语言中运算符:%)运算.
q.rear=(q.rear+1)%MAXQSIZE;
q.front=(q.front+1)%MAXQSIZE;
使用循环队列可以解决假溢出的问题,但是随之而来的又有另一个问题:当循环队列为空时,q.rear=q.front;当循环队列满了之后,q.rear也与q.front相等。那么又该如何解决这个问题呢?
在这里我们少用一个元素空间来解决这个问题:
//队满判断条件
(q.rear+1)%MAXQSIZE == q.front
解决了这两个问题之后,我们一起进入代码的书写:
#define MAXQSIZE 6
typedef int QElemType;
//顺序队列的结构体定义
typedef struct SqQueue
{
QElemType* data;
int front;
int rear;
}SqQueue;
//顺序队列的初始化
void SqQueueInit(SqQueue* q)
{
q->data = (QElemType*)malloc(sizeof(QElemType) * MAXQSIZE);
assert(q->data != NULL); //判断申请空间是否成功
q->front = q->rear = 0; //让front和rear指向位置0
}
//获取队列长度
int SqQueueGetLength(SqQueue q)
{
return (q.rear - q.front + MAXQSIZE) % MAXQSIZE; //返回队列长度
}
//入队操作
void SqQueueEn(SqQueue* q ,QElemType e)
{
assert(q != NULL);
if (((q->rear + 1) % MAXQSIZE) == q->front) //判断队满
{
printf("队列已满,无法入队!");
return;
}
q->data[q->rear] = e; //插入元素e
q->rear = (q->rear + 1) % MAXQSIZE; //rear指向下一个元素空间
}
//出队操作
void SqQueueDe(SqQueue* q)
{
assert(q != NULL);
if (q->front == q->rear) //判断是否为空队
{
printf("线性表为空,无法出队!");
return; //若为空队,直接返回
}
q->front = (q->front + 1) % MAXQSIZE; //让front指向后一个元素空间即可完成出队
return;
}
//打印队列
void PrintSqQueue(SqQueue q)
{
int f = q.front; //创建f保存front指向的元素空间下标
while (f != q.rear)
{
printf("%d ", q.data[f]); //输出f指向的元素空间的元素
f = (f + 1 + MAXQSIZE) % MAXQSIZE; //f指向下一个元素空间
}
printf("\n");
}
4.2 队列的链式表示和实现
//链队的结构体定义
typedef int QElemType;
typedef struct QNode
{
QElemType data;
struct QNode* next;
}QNode,*QueuePtr ;
typedef struct LinkQueue
{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
//链队的初始化
void LinkQueueInit(LinkQueue* q)
{
q->front = q->rear = (QNode*)malloc(sizeof(QNode)); //申请结点
assert(q->front != NULL);
q->front->next = NULL;
}
//销毁连队
void LinkQueueDestory(LinkQueue* q)
{
assert(q != NULL);
while (q->front)
{
q->rear = q->front->next;
free(q->front);
q->front = q->rear;
}
}
//入队操作
void LinkQueueEn(LinkQueue* q,QElemType e) //入队操作
{
QNode *p = (QNode*)malloc(sizeof(QNode)); //申请结点
assert(p != NULL);
p->data = e; //初始化新结点
p->next = NULL;
q->rear->next = p; //插入队尾
q->rear = p;
}
//出队操作
void LinkQueueDe(LinkQueue* q)
{
assert(q != NULL);
if (q->front == q->rear) //判断队列是否为空
{
printf("队列已空,无法出队!");
return;
}
QNode* p = q->front->next;
q->front->next = p->next;
free(p);
}
//获取头结点
QElemType GetHead(LinkQueue q)
{
if (q.front == q.rear) //判断队列是否为空
{
printf("队列为空,无法获取!");
return;
}
return q.front->next->data;
}