栈和队列在逻辑上属于线性表的范畴,只是运算受到了严格的限制,称它们为运算受限的线性表。
一,栈
栈是限定仅在表尾进行插入和删除运算的线性表。我们把表尾称为栈顶(TOP),表头称为栈底。当栈中没有元素时称为空栈。入栈是指在栈顶插入数据元素,出栈是指在栈顶删除数据元素。
对于栈来说,最后进栈的元素,最先出栈,故把栈称为后进先出的数据结构,或先进后出。
栈的用途:汇编处理程序中的句法识别,表达式计算,回溯问题,在函数调用时的参数传递和函数值的返回。
1,栈的顺序存储表示--顺序栈
用数组来存储。由于栈底是固定不变的,而栈顶是随进栈出栈操作动态变化的,因此为了实现对栈的操作,必须记住栈顶的当前位置。另外,栈是有容量限制的。
struct Stack { datatype elements[maxsize]; int Top; //表示当前栈顶位置 };
置空栈时设S->top=-1;
注意出栈时的操作:
datatype *Pops(struct Stack *S){ S->top--; ret=(datatype *)malloc(sizeof(datatype)); *ret=S->elements[S->top+1]; return ret; } //如果返回值是整型的话就没必要这么做
多个栈共享存储空间:
两个栈共享存储空间时,将两个栈的栈底设在存储空间的两端,让两个栈的栈顶各自向中间延伸。
2,栈的链式存储表示--链栈
top是栈顶指针,它唯一的确定一个链栈。当top等于NULL时,该链栈为空栈。
struct Node{ datatype element; struct Node *next; }; struct Node *top;
入栈操作:
struct Node *push(struct Node *top,datatype e) { struct Node *p; p=(struct Node *)malloc(sizeof(struct Node)); p->element=e; p->next=top; top=p; return top; }
3,栈的应用
(过程递归调用)
要正确实现程序的递归调用,必须解决参数的传递和返回地址问题。进行调用时,每递归一次,都要给所有参变量重新分配存储空间,并要把前一次调用的实参和本次调用后的返回地址保留。递归结束时要逐层释放这些参数所占的存储空间,并按后调用先返回的原则返回各层相应的返点。
实现这种存储分配和管理最有效的工具是栈。
(地图染色问题--“回溯问题”)
算法的基本思想是:从行政区1开始染色,每个区域用所有颜色依次试验,若当前颜色与周围颜色都不重色,则用栈记下该区域颜色序号;否则依次用下一个颜色进行试探。若出现所有颜色均与相邻区域的颜色重色,则必须退栈回溯,修改当前栈顶的颜色序号,再进行试探。
在计算机实现上述算法时,用一个关系矩阵R[N][N]来描述各行政区域间的相邻关系。除关系矩阵外,还需要设置一个栈S,用来记录行政区域的所染颜色的序号S[i]=k; k表示行政区域i+1所染颜色的序号。
#define N 7 void MapColor(int R[][N],int n,int S[]){ int color,area,k; s[0]=1; //第一个行政区域染色1号,共四种颜色 area=1; //从第二个行政区域开始试探染色 color=1; //从颜色1开始试探 while(area<N) { while(color<=4) { k=0; //指示已染色区域 while((k<area)&&(s[k]*R[area][k]!=color)) k++; //判断当前area区域与k区域是否重色 if(k<area) //area区域与k区域重色 color++; else { s[area]=color; //保存area区域的颜色 area++; if(area>N) break; color=1; } } if(color>4) //area区找不到合适的颜色 { area-=1; //区域回溯并修改area-1域所使用的颜色 color=s[area]+1; } } }
(表达式求值)
在用高级语言编写的源程序中,一般都含有表达式,如何将他们翻译成能够正确求值的指令序列,是语言处理程序要解决的基本问题,下面介绍“算符优先法”。
任何一个表达式都是由操作数,操作符和界限符组成的。在这里仅讨论简单算术表达式的求值问题,仅包含加减乘除。
算符之间的优先关系表如下:
需要指出的是,为了使算法简洁,在表达式的最左边会虚设一个‘#’构成表达式的一对括号。表中'(' = ')'表示当左右括号相遇时,括号内的运算已经完成;同理,'#'='#'表示整个表达式求值完毕。空格处表示表达式中不允许它们相继出现,假设下面的输入不会出现这种情况。
我们设置两个栈,一个称作optr,用来存放运算符;另一个称作opnd,用以存放运算结果。
算法的基本思想:
(1)置操作数栈为空栈,表达式起始符#为运算符optr的栈底元素。
(2)依次读入表达式中的每个字符,若是操作数则进opnd栈,若是运算符,则在和栈optr的栈顶元素比较优先权后做相应的操作,直至整个表达式求值完毕为止。
int evaluateexpression(){ int a,b,result,ret; char ch,theta; setnull(opnd); setnull(optr); //置optr,opnd为空栈 push(optr,'#'); //置栈底元素'#' ch=getche(); while((ch!='#')||(gettop(optr!='#')){ //整个表达式未扫描完毕 if(ch!='+'&&ch!='-'&&ch!='*'&&ch!='/'&&ch!='('&&ch!=')') { push(opnd,ch); ch=getche(); //读入的不是运算符或结束符,则入栈opnd } else switch(precede(gettops(optr,ch)){ //判断读入运算符的优先级 case '<':push(optr,ch); ch=getche(); break; case '=':pop(optr); ch=getche(); break; case '>':theta=pop(optr); b=pop(opnd); a=pop(opnd); result=operate(a,theta,b); push(opnd,result); break; } } ret=getpop(opnd) return ret; }
二,队列
只允许在表的一段进行插入,而在另一端进行删除。允许删除的一端称为队头,允许插入的一端称为队尾。
队列被称为先进先出的线性表。
当队列中没有元素时称为空队列。
1,队列的存储结构
(1)顺序队列
运算受限的线性表,使用数组来存放当前队列的元素。由于队列的队头和队尾都是变化的,因此需要设置两个下标来指示当前队头和队尾元素在数组中的位置。
struct sequence{ datatype data[maxsize]; int front,rear; }; struct sequence *sq;
为方便起见,规定头指针front总是指向当前队头元素的前一个位置,尾指针指向当前队尾元素。
一开始,队列的头,尾指针指向向量空间下界的前一个位置,在此设置为-1.
当前队列中的元素个数为(sq->rear)-(sq->front).若sq->rear==sq->front,则队列长度为0,即当前队列是空队列。
队满条件是当前队列长度等于向量空间大小,即(sq->rear)-(sq->front)=maxsize。
但是,如果当前尾指针等于数组的上界,即使队列不满(即当前队列长度小于maxsize),再做入队操作也会引起溢出。这种情况称之为“假上溢”。
解决方案:设想向量是一个首尾相接的圆环,即sq->data[0]在sq->data[maxsize-1]之后,我们称之为循环队列。
在循环意义下的尾指针加一操作:if(sq->rear+1>=maxsize) sq->rear=0;
else sq->rear++;
或利用模运算:sq->rear=(sq->rear+1)%maxsize;
若头指针从后面追赶上了尾指针,即sq->front==sq->rear,则当前队列为空。
若尾指针从后面追赶上了头指针,即sq->rear==sq->front,则队列已满。
因此不能用此方法判断队列为空还是为满。
解决方案:入队前测试尾指针在循环意义下加1之后是否等于头指针,若相等则认为队满。
(sq->rear+1)%maxsize==sq->front ,队空sq->rear==sq->front.
这样做使sq->data[sq->front]是空闲的。
置空队列 sq->front=maxsize-1; sq->rear=maxsize-1; 使之都指在位置0的前一个位置。
去队头元素:sq->data[(sq->front+1)%maxsize] //头指针的下一个位置
2,链队列
表头插入和表尾删除的单链表。增加一个尾指针,指向链表的最后一个结点。一个链队列由一个头指针和一个尾指针唯一确定。
typedef int datatype; typedef struct node{ datatype data; struct node *next; }linklist;
typedef struct{ linklist *front,*rear; }linkqueue; linkqueue *q;
队空:q->rear==q->front;
用两个指针指向一个单链表,相当于单链表增加了两个节点(头结点照常),front 指向头结点,而rear指向尾结点。
因此一个队列为空时,其头指针和尾指针均指向头结点。
置空队 q->front=(linklist *)malloc(sizeof(linklist); //申请头结点 q->front->next=NULL; //front和rear是相对于单链表多余的两个指针,分别指向头和尾,因此q->front是一个指针,指向头结点,q->front->next是开始结点 q->rear=q->front; return q;
入队 q->rear->next=(linklist *)malloc(sizeof(linklist)); q->rear=q->rear->next; q->rear->data=x; q->rear->next=NULL;
出队 s=q->front->next; if(s->next==NULL){ //当前链队列长度为1 q->front->next=NULL; q->rear=q->front; } else q->front->next=s->next;
3,队列的应用,
(1)划分子集问题
将集合中的元素划分为互不相交的子集,同时要求划分的子集数最少。
实际应用中比如安排运动会比赛项目的日程,使一个运动员参加的不同项目不在同一天举行,同时使比赛日程最短。
假设共有九个项目,汇报后有冲突的项目给出。
采用循环筛选法:从集合的第一个元素开始,凡与第一个元素无冲突且与该组中其他元素无冲突的元组划归一组,再将剩余元素按相同方式划归。。。
在计算机实现时,将集合中的冲突关系设置一个冲突矩阵,二维数组,有冲突为1,否则为0.
设置循环队列cq[n],用来存放集合中的元素;数组result[n]用来存放每个元素的分组号;newr[n]为工作数组。
void function(int n,int R[][n],int cq[n],int result[]){ int front,rear,group,pre,I,i; front=n-1; rear=n-1; for(i=0;i<n;i++) { newr[i]=0; cq[i]=i+1; } group=1; pre=0; //前一个出队元素编号 do{ front=(front+1)%n; I=cq[front]; if(I<pre){ //开始下一次筛选准备 group=group+1; result[I-1]=group; for(i=0;i<n;i++) newr[i]=R[I-1][i]; } else if(newr[I-1]=group){ //发生冲突元素入队 rear=(rear+1)%n; cq[rear]=I; } else{ result[I-1]=group; for(i=0;i<n;i++) newr[i]+=R[I-1][i]; } pre=I; }while(rear!=front) }(2)离散仿真事件