第三章:关于栈和队列的学习
栈和队列
栈和队列是两种重要的线性结构。
从数据结构角度看,它们也是线性表,不过它们的基本操作是线性表操作的子集,所以属于限定性的线性表;从数据类型角度看,它们是和线性表不一样的抽象数据类型;在面向对象的程序设计中属于多型数据类型。
栈(Stack)
栈:后进先出的线性表(LIFO结构),限定仅在表尾插入或删除的线性表。表尾端是栈顶(top),表头端是栈底(bottom);不含元素的栈叫空栈。
栈的表示和实现
栈有两种存储结构的表示方法,一种是顺序栈(顺序存储结构),另一种是链栈(链式存储结构)。
顺序栈
顺序栈:利用一组地址连续的存储单元存放从栈底到栈顶的数据元素。结构体设置栈底指针(* base)、栈顶指针(*top)、当前可用最大容量(stacksize)3个变量。
base指针为NULL时,表示栈结构不存在。
top指针=base指针时,说明栈空。
每次新增元素,top指针加1,指向下一个位置。所以非空栈中,top指针始终在栈顶元素的下一个位置。
#include<stdio.h>
#include<stdlib.h>
#define STACK_INIT_SIZE 100//存储空间初始分配量
#define STACKINCREMENT 10//分配增量
typedef int SElemType;
typedef int Status;
typedef struct{
SElemType *base;
SElemType *top;
int stacksize;
}sqStack;
Status InitStack(sqStack &s){
s.base = (SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType));
if(!s.base){
exit(1);
}
s.top=s.base;
s.stacksize=STACK_INIT_SIZE;
return 0;
}
Status GetTop(sqStack s,SElemType &e){//获取栈顶元素
if(s.top==s.base){
return 1;
}
e=*(s.top-1);
return 0;
}
Status Push(sqStack &s,SElemType e){//插入元素e为新的栈顶元素
if(s.top-s.base>=s.stacksize){//如果栈满,扩充空间
s.base = (SElemType *)realloc(s.base,(s.stacksize+STACKINCREMENT)*sizeof(SElemType));
if(!s.base){
exit(1);
}
s.top=s.base+s.stacksize;
s.stacksize+=STACKINCREMENT;
}
*s.top++=e;//赋值后栈顶指针+1
return 0;
}
Status Pop(sqStack &s){//删除栈顶元素
if(s.top==s.base){
return 1;
}
s.top--;//栈顶指针-1,给e赋值
}
void display(sqStack s){
while(s.base<s.top){
printf("%d ",*(s.base));
s.base++;
}
}
int main(){
sqStack s;
SElemType p;
InitStack(s);
Push(s,1);
Push(s,2);
Push(s,3);
Push(s,4);
GetTop(s,p);
printf("%d\n",p);
display(s);
Pop(s);
printf("\n");
display(s);
return 0;
}
链式栈
采用链式存储结构的栈,如下图所示。
栈的应用示例
由于栈的后进先出的特性,栈可以在程序设计中广泛应用。
数制转换
数制转换:进制转换问题,比如十进制到二进制。
从N进制到d进制转换公式:N=(N div d)*d+ N mod d(div为取整运算,mod为取余运算)。
void conversion(){
sqStack h;
SElemType N;
InitStack(h);
scanf("%d",&N);
while(N){
Push(h,N%8);
N=N/8;
}
display(h);
}
括号匹配检验
void matching(sqStack &s,SElemType e){//括号匹配
SElemType q;
GetTop(s,q);///获取栈顶元素 q
if(s.base==NULL){//如果栈空,直接插入
if(e==')'||e==']'){
printf("匹配表达式不正确");
exit(1);
}
else if(e=='('||e=='['){
Push(s,e);
printf("插入了%c\n",e);
}
}
else{
if(e=='('||e=='['){
Push(s,e);
printf("插入了%c\n",e);
}
else if(e==')'){
if(q=='('){
Pop(s);//删除栈顶元素
printf("插入%c时,匹配并取出了%c\n",e,q);
}else{
printf("匹配表达式不正确");
exit(1);
}
}
else if(e==']'){
if(q=='['){
Pop(s);
printf("插入%c时,匹配并取出了%c\n",e,q);
}else{
printf("匹配表达式不正确");
exit(1);
}
}
else if(e=='#'){//#作为结束标志
if(s.top>s.base){
printf("匹配表达式不正确");
}
else if(s.top=s.base){
printf("匹配表达式正确");
}
}
}
}
int main(){
sqStack p;
InitStack(p);
matching(p,'[');
matching(p,'(');
matching(p,'[');
matching(p,']');
matching(p,'[');
matching(p,']');
matching(p,')');
matching(p,']');
matching(p,'#');//输入#作为结束标志
return 0;
}
行编辑程序
//为此,可设这个输人缓冲区为一个栈结构,每当从终端接受了1个字符之后先作如下
//判别:如果它既不是退格符也不是退行符,则将该字符压人栈顶;如果是一个退格符
//,则从栈顶删去一个字符;如果它是1个退行符,则将字符栈清为空栈。
void DestoryStack(sqStack &s){
free(s.base);
s.base = NULL;
s.top = NULL;
s.stacksize = 0;
}
void ClearStack(sqStack &s){
while(s.top!=s.base){
s.top--;
}
}
void LineEdit(){//行编辑
sqStack p;
InitStack(p);
char ch=getchar();
while(ch!=EOF){
while(ch!=EOF&&ch!='\n'){//EOF用ctrl+z表示在window中
switch(ch){
case '#':Pop(p);break;
case '@':ClearStack(p);break;
default:Push(p,ch);break;
}
ch=getchar();
}
display(p);
ClearStack(p);
if(ch!=EOF){
ch=getchar();
}
}
DestoryStack(p);
}
int main(){
LineEdit();
return 0;
}
迷宫求解
参考往期文章: 迷宫求解,用顺序栈结构实现.
表达式求值
参考往期文章: 表达式求值.
栈与递归的实现
栈还有一个重要的应用是在程序设计语言中实现递归,一个直接调用自己或通过一系列的调用语句间接调用自己的函数,就是递归函数。
递归函数
有很多数学函数都是通过递归定义的,比如阶乘函数、斐波那契数列、Ackerman函数。
另外,二叉树、广义表等数据结构,由于结构本身的递归特性,它们也可以通过递归描述。
其外,某类问题,比如汉诺塔、八皇后问题使用递归求解比迭代更简单。
递归的应用:汉诺塔
(首先看下图描述)当n=1时,只需将圆盘从X移到Z即可;当n>1时,问题可拆解为:
- 设法将最底部圆盘上的n-1个圆盘移到Y,Z作辅助塔座。
- 将最底部圆盘移到Z
- 将n-1个圆盘从Y移到Z,X作辅助塔座。
其中第1步又可拆解为
4. 设法将第2底圆盘上的n-2个圆盘移到Z,Y作辅助塔座。
5. 将第2底圆盘移到Y
6. 将n-1个圆盘从Z移到Y,X作辅助塔座。
可以发现都具有相同特征属性,只是问题规模-1而已。
#include <stdio.h>
#include <windows.h>
void Hanoi(int n, char a,char b,char c);
void Move(int n, char a, char b);
int count;
int main()
{
int n;
printf("汉诺塔的层数:\n");
scanf("%d",&n);
Hanoi(n, 'A', 'B', 'C');
Sleep(20000);
return 0;
}
void Hanoi(int n, char a, char b, char c)
{
if (n==1)
{
Move(n, a, c);//只有一个圆盘时,直接从a挪到c
}
else
{
Hanoi(n - 1, a, c, b);//将n-1个圆盘从a挪到b
Move(n, a, c);//将第n个圆盘挪到c
Hanoi(n - 1, b, a, c);//将n-1个圆盘从b挪到c
}
}
void Move(int n, char a, char b)
{
count++;
printf("第%d次移动 Move %d: Move from %c to %c !\n",count,n,a,b);
栈在递归中的作用
在高级语言中,调用函数与被调用函数之间的链接和信息交换是通过栈来进行的。递归函数也涉及多层函数的嵌套,只是调用函数和被调用函数是一个函数罢了。
一个函数调用另一个函数,在运行被调函数前,要:
- 将函数的实在参数、返回地址等信息传给被调函数保存。
- 为被调函数的局部变量分配空间。
- 将控制转移到被调函数入口。
被调函数运行完之后,在回到调用函数之前,要:
- 保存被调函数的计算结果。
- 释放之前分配的空间。
- 根据返回地址,将控制转到调用函数的相应位置。
因为“后调用(函数)先返回(值)”;系统将整个程序运行时所需的空间安排在一个栈中,每调用一个函数时,系统在栈底分配(push)一个存储区,存储信息。每返回一个函数,就释放(Pop)它的存储区。
比如汉诺塔示例,系统在程序运行时的分配:
队列(Queue)
队列:和栈相反,先进先出的线性表(FIFO结构),限定仅在一端插入另一端删除的线性表。允许插入的一端叫队尾(rear),允许删除的一端叫队头(front)
除了栈和队列以外。还有一种限定性的线性表叫双端队列,是限定插入和删除可以在表的两端进行的数据结构。
队列的表示和实现
队列有两种存储结构的表示方法,一种是循环队列(顺序存储结构),另一种是链队列(链式存储结构)。
链队列
链队列:用链表来表示,一个链队列显然需要2个分别指向队头和队尾的指针(头指针和尾指针)。
另外为了操作方便,加一个头结点,头指针指向头结点。
所以空队列时,头指针和尾指针都指向头结点。
#include<stdio.h>
#include<stdlib.h>
typedef int Status;
typedef int QElemType;
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct{
QueuePtr front;//队头指针
QueuePtr rear;//队尾指针
}LinkQueue;
Status InitQueue(LinkQueue &q){//构造一个空队列q
q.front=q.rear=(QueuePtr)malloc(sizeof(QNode));
if(!q.front){
exit(0);
}
q.front->next=NULL;
return 0;
}
Status DestroyQueue(LinkQueue &q){//销毁队列q
while(q.front){//因为先进先出
q.rear=q.front->next;
free(q.front);//所以删除头指针指的结点
q.front=q.rear;
}
return 0;
}
Status EnQueue(LinkQueue &q,QElemType e){//在队尾插入元素
QueuePtr p = (QueuePtr)malloc(sizeof(QNode));
if(!p){
exit(0);
}
p->data = e;
p->next=NULL;
q.rear->next = p;
q.rear = p;
return 0;
}
Status DeQueue(LinkQueue &q,QElemType &e){//在队头删除元素
if(q.front == q.rear){
return 1;
}
QueuePtr p=q.front->next;//p指向第一个结点(除头结点)
e=p->data;
q.front->next=p->next;//头结点的下个结点是第二个结点
if(q.rear==p){//if p指向尾结点,删掉p后(尾结点后)
q.rear=q.front;// 尾指针要赋值,指向头指针恢复空队列状态
}
free(p);
return 0;
}
void display(LinkQueue q){
QueuePtr p=q.front->next;
printf("队列:");
while(p!=NULL){
printf("%d ",p->data);
p=p->next;
}
}
int main(){
LinkQueue q;
QElemType e;
InitQueue(q);
EnQueue(q,1);
EnQueue(q,2);
EnQueue(q,3);
EnQueue(q,4);
DeQueue(q,e);
display(q);
return 0;
}
循环队列
顺序队列:采用顺序存储结构的队列,和链队列相似,需要front和rear这2个指针;初始化队列时,front=rear=0;增加元素时,尾指针+1;删除元素时,头指针+1。所以循环队列中,头指针front一直指向队列头元素,尾指针始终指向队列尾元素的下一位。
所谓循环队列,我们可以看到上图图d中还有部分空间未利用,我们可以将上图臆想为一个如下图环状的空间。
由上图可知,队空和队满时都是q.front=q.rear无法区分。有2种方法,一种是另设一个变量区分队空和队满。另一种是少用一个元素的空间(最常用)。
#include<stdio.h>
#include<stdlib.h>
#define MAXQSIZE 100
typedef int Status;
typedef int QElemType;
typedef struct{
QElemType *base;
int front;//头指针,if队列不空,指向队列头元素
int rear;//尾指针,if队列不空,指向尾元素下一个位置
}SqQueue;
Status InitQueue(SqQueue &q){//构造一个空队列q
q.base=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
if(!q.base){
exit(0);
}
q.front=q.rear=0;
return 0;
}
int QueueLength(SqQueue q){
return (q.rear-q.front+MAXQSIZE)%MAXQSIZE;
}
Status EnQueue(SqQueue &q,QElemType e){//在队尾插入元素
if((q.rear+1)%MAXQSIZE == q.front){
return 1;
}
q.base[q.rear]=e;
q.rear=(q.rear+1) % MAXQSIZE;
return 0;
}
Status DeQueue(SqQueue &q,QElemType &e){//在队头删除元素
if(q.front == q.rear){
return 1;
}
e = q.base[q.front];
q.front=(q.front+1) % MAXQSIZE;
return 0;
}
void display(SqQueue q){
for(int i= q.front;i<q.rear;i++){
printf("%d ",q.base[i]);
}
}
int main(){
SqQueue q;
QElemType e;
InitQueue(q);
EnQueue(q,1);
EnQueue(q,2);
EnQueue(q,3);
EnQueue(q,4);
DeQueue(q,e);
display(q);
return 0;
}