数据结构栈队列和数组
Posted st-2017
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构栈队列和数组相关的知识,希望对你有一定的参考价值。
栈
栈的定义
栈是限制在表的一端进行插入和删除的线性表。允许插入、删除的这一端称为栈顶,另 一个固定端称为栈底。当表中没有元素时称为空栈。
栈是运算受限的线性表,线性表的存储结构对栈也是适用的,只是操作不同而已。
利用顺序存储方式实现的栈称为顺序栈。
与线性表类似,栈的动态分配顺序存储结构如 下:
#define STACK_INIT_SIZE 100 //存储空间的初始分配量 #define STACKINCREMENT 10 //存储空间的分配增量 typedef struct{ SElemType *base; //在栈构造之前和销毁之后,base 的值为 NULL SElemType *top; //栈顶指针 int stacksize; //当前已分配的存储空间 }SqStack;
需要注意,在栈的动态分配顺序存储结构中,base 始终指向栈底元素,非空栈中的 top 始终在栈顶元素的下一个位置。
下面是顺序栈上常用的基本操作的实现。
(1)入栈:若栈不满,则将 e 插入栈顶。
int Push (SqStack &S, SElemType e) { if (S.top-S.base>=S.stacksize) {……} //栈满,追加存储空间 *S.top++ = e; //top始终在栈顶元素的下一个位置 return OK; }
int Pop (SqStack &S, SElemType &e) { if (S.top==S.base) return ERROR; e = *--S.top; return OK; }
出栈和读栈顶元素操作,先判栈是否为空,为空时不能操作,否则产生错误。通常栈空常作为一种控制转移的条件。
栈的应用举例
由于栈的“先进先出”特点,在很多实际问题中都利用栈做一个辅助的数据结构来进行求解。
1、数值转换
2、表达式求值
表达式求值是程序设计语言编译中一个基本的问题,它的实现也是需要栈的加入。下 面的算法是由运算符优先法对表达式求值。在此仅限于讨论只含二目运算符的算术表达式。
(1)中缀表达式求值
中缀表达式:每个二目运算符在两个运算量的中间,假设所讨论的算术运算符包括:+ 、 - 、、/、%、^(乘方)和括号()。
设运算规则为:
.运算符的优先级为:()——> ^ ——>*、/、%——> +、- ;
.有括号出现时先算括号内的,后算括号外的,多层括号,由内向外进行;
.乘方连续出现时先算右面的。
表达式作为一个满足表达式语法规则的串存储,如表达式“3*2^(4+2*2-1*3)-5”,它的求值过程为:自左向右扫描表达式,当扫描到 3*2 时不能马上计算,因为后面可能还有更高的运算,正确的处理过程是:需要两个栈:对象栈 s1 和运算符栈 s2。当自左至右扫描表达式的每一个字符时,若当前字符是运算对象,入对象栈,是运算符时,若这个运算符比栈顶运算符高则入栈,继续向后处理,若这个运算符比栈顶运算符低则从对象栈出栈两个运算量, 从运算符栈出栈一个运算符进行运算,并将其运算结果入对象栈,继续处理当前字符,直到遇到结束符。
为了处理方便,编译程序常把中缀表达式首先转换成等价的后缀表达式,后缀表达式的运算符在运算对象之后。在后缀表达式中,不在引入括号,所有的计算按运算符出现的顺序, 严格从左向右进行,而不用再考虑运算规则和级别。中缀表达式“3*2^(4+2*2-1*3)-5 ”的后 缀表达式为:“32422*+13-^*5-”
(2) 后缀表达式求值
计算一个后缀表达式,算法上比计算一个中缀表达式简单的多。这是因为表达式中即无括号又无优先级的约束。具体做法:只使用一个对象栈,当从左向右扫描表达式时,每遇到一个操作数就送入栈中保存,每遇到一个运算符就从栈中取出两个操作数进行当前的计算, 然后把结果再入栈,直到整个表达式结束,这时送入栈顶的值就是结果。
下面是后缀表达式求值的算法,在下面的算法中假设,每个表达式是合乎语法的,并且 假设后缀表达式已被存入一个足够大的字符数组 A 中,且以‘#’为结束字符,为了简化问题, 限定运算数的位数仅为一位且忽略了数字字符串与相对应的数据之间的转换的问题。
typedef char SElemType ; double calcul_exp(char *A){ //本函数返回由后缀表达式 A 表示的表达式运算结果 SqStack s ; ch=*A++ ; InitStack(s) ; while ( ch != ’#’ ){ if (ch!=运算符) Push (s , ch) ; else { Pop (s , &a) ; Pop (s , &b) ; //取出两个运算量 switch (ch).{ case ch= =’+’: c=a+b ; break ; case ch= =’-’: c=a-b ; break ; case ch= =’*’: c=a*b ; break ; case ch= =’/’: c=a/b ; break ; case ch= =’%’: c=a%b ; break ; } Push (s, c) ; } ch=*A++ ; } Pop ( s , result ) ; return result ; }
(3) 中缀表达式转换成后缀表达式:
将中缀表达式转化为后缀表达示和前述对中缀表达式求值的方法完全类似,但只需要运 算符栈,遇到运算对象时直接放后缀表达式的存储区,假设中缀表达式本身合法且在字符数组 A 中,转换后的后缀表达式存储在字符数组 B 中。
具体做法:遇到运算对象顺序向存储后缀表达式的 B数组中存放,遇到运算符时类似于中缀表达式求值时对运算符的处理过程,但运算符出栈后不是进行相应的运算,而是将其送入B 中存放。
3、栈与递归
在高级语言编制的程序中,调用函数与被调用函数之间的链接和信息交换必须通过栈进行。当在一个函数的运行期间调用另一个函数时,在运行该被调用函数之前,需先完成三件事:
(1)将所有的实在参数、返回地址等信息传递给被调用函数保存;
(2)为被调用函数的局部变量分配存储区;
(3)将控制转移到被调用函数的入口。
从被调用函数返回调用函数之前,应该完成:
(1)保存被调函数的计算结果;
(2)释放被调函数的数据区;
(3)依照被调函数保存的返回地址将控制转移到调用函数。
多个函数嵌套调用的规则是:后调用先返回,此时的内存管理实行“栈式管理”。
递归函数的调用类似于多层函数的嵌套调用,只是调用单位和被调用单位是同一个函数而已。
将递归程序转化为非递归程序时常使用栈来实现。
队列
队列的定义及基本运算
栈是一种后进先出的数据结构,在实际问题中还经常使用一种“先进先出”的数据结构: 即插入在表一端进行,而删除在表的另一端进行,将这种数据结构称为队或队列,把允许插入的一端叫队尾(rear) ,把允许删除的一端叫队头(front)。
队列的存储实现及运算实现
与线性表、栈类似,队列也有顺序存储和链式存储两种存储方法。
1.顺序队列
循环队列的类型定义如下:
#define MAXQSIZE 100 //大队列长度 typedef struct { QElemType *base; //动态分配存储空间 int front; //头指针,若队列不空,指向队列头元素 int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置 } SqQueue;
(1)入队:
int EnQueue (SqQueue &Q, QElemType e) { if((Q.rear+1)%MAXQSIZE == Q.front) return ERROR; Q.base[Q.rear] = e; Q.rear = (Q.rear+1) % MAXQSIZE; return OK; }
(2)出队:
int DeQueue (SqQueue &Q, QElemType &e) { if (Q.front = = Q.rear) return ERROR; e = Q.base[Q.front]; Q.front = (Q.front+1) % MAXQSIZE; return OK; }
(3)求循环队列元素个数:
int QueueLength(SqQueue Q){ return (Q.rear-Q.front+MAXQSIZE) %MAXQSIZE; }
2.链队列
链式存储的队称为链队列。和链栈类似,用单链表来实现链队列,根据队的先进先出原 则,为了操作上的方便,分别需要一个头指针和尾指针。
链队列的形式描述如下:
typedef struct QNode { // 结点类型 QElemType data; struct QNode *next; } QNode, *QueuePtr; typedef struct { //链队列类型 QueuePtr front; //队头指针 QueuePtr rear; //队尾指针 } LinkQueue;
定义一个指向链队列的指针:LinkQueue Q;
下面是链队列的基本运算的实现。
(1)入队
int EnQueue (LinkQueue &Q, QElemType e) { QNode *p; p = (QNode *)malloc(sizeof(QNode)); p->data = e; p->next = NULL; Q.rear->next = p; Q .rear = p; return OK; }
(2)出队
int DeQueue (LinkQueue &Q, QElemType &e) { if (Q.front == Q.rear) return ERROR; //队空,出队失败 p = Q.front->next; e = p->data; //队头元素放 e 中 Q.front->next = p->next; if(Q.rear==p) Q.rear= Q.front; //只有一个元素时,此时还要修改队尾指针 free (p); return OK; }
3.除了栈和队列之外,还有一种限定性数据结构是双端队列。
(1)双端队列:可以在双端进行插入和删除操作的线性表。
(2)输入受限的双端队列:线性表的两端都可以输出数据元素,但是只能在一端输入数 据元素。
(3)输出受限的双端队列:线性表的两端都可以输入数据元素,但是只能在一端输出数 据元素。
特殊矩阵的压缩存储
数组
数组可以看作线性表的推广。数组作为一种数据结构其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型,数组是一个具有固定格式和数量的数据有序集, 每一个数据元素有唯一的一组下标来标识,因此,在数组上不能做插入、删除数据元素的操作。
通常在各种高级语言中数组一旦被定义,每一维的大小及上下界都不能改变。
通常,数组在内存被映象为向量,即用向量作 为数组的一种存储结构,这是因为内存的地址空间是一维的,数组的行列固定后,通过一个 映象函数,则可根据数组元素的下标得到它的存储地址。
对于一维数组按下标顺序分配即可。对多维数组分配时,要把它的元素映象存储在一维存储器中,一般有两种存储方式:一是以行为主序(或先行后列)的顺序存放,另一种是以列为主序(先列后行)的顺序存放。
设有 m×n 二维数组 Amn,下面我们看按元素的下标求其地址的计算:
以“以行为主序”的分配为例:设数组的基址为 LOC(a11),每个数组元素占据 d 个地址单 元,那么 aij 的物理地址可用一线性寻址函数计算:
LOC(aij) = LOC(a11) + ( (i-1)*n + j-1 ) * d
这是因为数组元素 aij的前面有 i-1 行,每一行的元素个数为 n,在第 i 行中它的前面还有 j-1 个数组元素。
在 C 语言中,数组中每一维的下界定义为 0,则: LOC(aij) = LOC(a00) + ( i*n + j ) * d 推广到一般的二维数组:A[c1...d1][c2...d2],则 aij的物理地址计算函数为:
LOC(aij)=LOC(a c1 c2)+( (i- c1) *( d2 - c2 + 1)+ (j- c2) )d
以上是关于数据结构栈队列和数组的主要内容,如果未能解决你的问题,请参考以下文章