1.栈的理解
栈(stack)时限定仅在尾表进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom) ,不含任何数据元素的栈称为空栈。
栈又称为后进先出(LastIn First Out)的线性表,简称LIFO结构。
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表
2.栈的插入与删除
栈的特殊之处就是在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这就使得:栈底是固定的,最先进栈的只能在栈底。
栈的插入操作,叫做进栈,也称压栈、入栈。
栈的删除操作,叫做出栈,也有的叫作弹栈。
举例来说,现有3个整型数字元素1、2、3依次进栈,会有哪些出栈次序?
· 第一种:1、2、3进,再3、2、1出。这是最简单的最好理解的一种,出栈次序为321
· 第二种:1进,1出,2进,2出,3进,3出。也就是进一个出一个,出栈次序为123
· 第三种:1进,2进,2出,1出,3进,3出。出栈次序为213
· 第四种:1进,1出,2进,3进,3出,2出。出栈次序为132
· 第五种:1进,2进,2出,3进,3出,1出。出栈次序为231
3.栈的抽象数据类型
ADT 栈(stack)
DATA
同线性表。元素具有相同的类型,相邻元素具有前驱后继关系。
Operation
InitStack(*S):初始化操作,建立一个空栈S
DestroyStack(*S):若栈存在,则销毁它
ClearStack(*S):将栈清空
StackEmpty(S ) :若栈为空,返回true,否则返回false
GetTop(S,* e):若栈存在且非空,用e返回S的栈顶元素
Push(*S,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素
Pop(*S, * e):删除栈S中栈顶元素,并用e返回其值
StackLength(S):返回栈S的元素个数
endADT
4.进栈与出栈操作
栈的结构定义
typedefint SElemType;
typedefstruct
{
SElemTypedata[MAXSIZE];
inttop;(用于栈顶指针)
}SqStack;
进栈操作push
StatusPush (SqStack *S, SElemType e)
{
if(s -> top == MAXSIZE - 1 ) //栈满
{
returnERROR;
}
S-> top++; // 栈顶指针加一
S-> data[S -> top] = e; // 将新插入元素赋给栈顶空间
returnOK;
}
出栈操作Pop
StatusPop(SqStack *S, SElemType *e)
{
if(S-> top == -1 )
returnERROR;
* e= S -> data[S -> top];
S-> top --;
returnOK;
}
5.链栈的结构代码
栈的链式存储结构,简称为链栈,对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那此时的计算机操作系统已经面临死机崩溃的情况,而不是这个链栈是否溢出的问题
但对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top == NULL的时候
typedefstruct StackNode
{
SElemTypedata;
structStackNode *next;
}StackNode,* LinkStackPtr;
typedefstruct LinkStack
{
LinkStackPtrtop;
intcount;
}LinkStack;
6.栈的链式存储结构- 进栈及出栈
Push:
StatusPush (LinkStack *S, SElemType e)
{
LinkStackPtrs = (LinkStackPtr) malloc(sizeof(StackNode));
s ->data = e;
s-> next = S -> top;
S-> top = s;
S-> count++;
returnOK;
}
Pop:
StatusPop(LinkStack *S, SElemType *e)
{
LinkStackPtrp;
if(StackEmpty(*s))
returnERROR;
*e =S ->top->data;
p =S -> top;
S-> top = S -> top -> next;
free(p);
S-> count--;
returnOK;
}
7.栈的作用
栈的引用简化了程序设计问题,划分了不同关注层次,使思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力区考虑数组的下标增减等细节问题,反而掩盖了问题的本质。
所以现在的许多高级语言,比如JAVA,C#等都有对栈结构的封装,你可以不用关心它的实现细节,就可以直接使用Stack的push和pop方法,非常方便。
8.递归定义
一个直接调用自己或通过一系列的调用语句间接的调用自己的函数,称作递归函数。每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。
递归能使程序结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但大量的递归调用会建立函数副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。
9.后缀表达式
运用后缀表达式进行计算的具体做法:
建立一个栈S 。从左到右读表达式,如果读到操作数就将它压入栈S中,如果读到n元运算符(即需要参数个数为n的运算符)则取出由栈顶向下的n项按操作数运算,再将运算的结果代替原栈顶的n项,压入栈S中 。如果后缀表达式未读完,则重复上面过程,最后输出栈顶的数值则为结束。
10.栈的应用----四则运算表达式求值(转)
计算机实现转换:
将中缀表达式转换为后缀表达式的算法思想:
·开始扫描;
·数字时,加入后缀表达式;
·运算符:
a. 若为'(',入栈;
b. 若为')',则依次把栈中的的运算符加入后缀表达式中,直到出现'(',从栈中删除'(' ;
c.若为 除括号外的其他运算符,当其优先级高于除'('以外的栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止。
·当扫描的中缀表达式结束时,栈中的的所有运算符出栈;
人工实现转换
这里我给出一个中缀表达式:a+b*c-(d+e)
第一步:按照运算符的优先级对所有的运算单位加括号:式子变成了:((a+(b*c))-(d+e))
第二步:转换前缀与后缀表达式
前缀:把运算符号移动到对应的括号前面
则变成了:-( +(a *(bc)) +(de))
把括号去掉:-+a*bc+de 前缀式子出现
后缀:把运算符号移动到对应的括号后面
则变成了:((a(bc)* )+ (de)+ )-
把括号去掉:abc*+de+- 后缀式子出现
发现没有,前缀式,后缀式是不需要用括号来进行优先级的确定的。如表达式:3+(2-5)*6/3
后缀表达式 栈
3_________________+
3 ________________+(
3 2 _______________+(-
3 2 5 -_____________ +
3 2 5 - _____________+*
3 2 5 - 6 * ___________+/
3 2 5 - 6 *3 __________+/
3 2 5 - 6 *3 /+________
("_____"用于隔开后缀表达式与栈)
另外一个人认为正确的转换方法:
遍历中缀表达式的每个节点,如果:
1、 该节点为操作数:
直接拷贝进入后缀表达式
2、 该节点是运算符,分以下几种情况:
A、 为“(”运算符:
压入临时堆栈
B、 为“)”运算符:
不断地弹出临时堆栈顶部运算符直到顶部的运算符是“(”为止,从栈中删除'('。并把弹出的运算符都添加到后缀表达式中。
C、 为其他运算符,有以下步骤进行:
比较该运算符与临时栈栈顶指针的运算符的优先级,如果临时栈栈顶指针的优先级大于等于该运算符的优先级,弹出并添加到后缀表达式中,反复执行前面的比较工作,直到遇到一个栈顶指针的优先级低于该运算符的优先级,停止弹出添加并把该运算符压入栈中。
此时的比较过程如果出现栈顶的指针为‘(’,则停止循环并把该运算符压入栈中,注意:‘(’不要弹出来。
遍历完中缀表达式之后,检查临时栈,如果还有运算符,则全部弹出,并添加到后缀表达式中。
1.队列的定义
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。假设队列是q = ( a1, a2, ......, an),那么a1就是队头元素,而an是队尾元素。这样我们删除时,总是从a1开始,而插入时,列在最后。排在第一个的优先出列,最后来的当然排在队伍最后。
2.队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(*Q):若队列为空,返回true,否则返回false。
GetHead(Q, * e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q, e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q, *e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。
endADT
3.循环队列
头尾相接的循环,队列的这种头尾相接的顺序存储结构称为循环队列。
通用的计算队列长度公式为:
(rear- front + QueueSIZE)% QueueSIZE
循环队列的顺序存储结构代码如下:
typedefint QElemType;
typedefstruct
{
QElemTypedata[MAXSIZE];
intfront;
intrear;
}SqQueue;
4.循环队列相关代码
循环队列的初始化代码:
StatusInitQueue(SqQueue *Q)
{
Q-> front = 0;
Q-> rear = 0;
returnOK;
}
循环队列求长度代码:
intQueueLength(SqQueue Q)
{
return( Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循环队列的入队操作代码:
StatusEnQueue (SqQueue * Q, QElemType e)
{
if (( Q -> rear + 1) % MAXSIZE == Q -> front) //队列满的判断
returnERROR;
Q-> data[Q -> rear] = e;
Q-> rear = (Q -> rear +1) %MAXSIZE; //rear指针向后移一位
returnOK;
}
循环队列出队操作代码:
StatusDeQueue( SqQueue *Q, QElemType *e)
{
if( Q-> front == Q -> rear) //队列空的判断
returnERROR;
*e =Q -> data[Q -> front];
Q-> front = (Q -> front + 1) % MAXSIZE;
returnOK;
}
5.队列的链式存储结构
链队列的结构:
typedefint QElemType;
typedefstruct QNode // 结点结构
{
QElemTypedata;
structQNode *next;
}QNode,*QueuePtr;
typedefstruct //队列的链表结构
{
QueuePtrfront, rear; //队头、队尾指针
}LinkQueue;
6.队列的链式存储结构 ---- 入队操作
入队操作时,其实就是在链表尾部插入结点,代码:
StatusEnQueue (LinkQueue *Q, QElemType e)
{
QueuePtrs = (QueuePtr) malloc(sizeof(QNode));
if(!s)//存储分配失败
exit(OVERFLOW)
s-> data = e;
s-> next = NULL;
Q-> rear -> next = s; //把s 中的e赋值给原队尾结点的后继
Q-> rear = s; // 当前的s设置为队尾结点,rear指向s
returnOK;
}
7.队列的链式存储结构 ---- 出队操作
出队操作时,就是头结点的后继结点出队,将头结点的后继改成它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点,代码:、
StatusDeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtrp;
if(Q-> front == Q -> rear)
returnERROR;
p = Q-> front -> next; //将欲删除的队头结点暂存给p
* e =p -> data; // 将与删除的队头结点的值赋给e
Q-> front -> next = p -> next; //将原队头结点后继p -> next 赋值给头结点后继
if( Q-> rear == p) //若队头是队尾,则删除后将rear指向头结点
Q-> rear = Q -> front;
free(p);
returnOK;
}
8.顺序队列
建立顺序队列结构必须为其静态分配或动态申请一片连续的存储空间,并设置两个指针进行管理。一个是队头指针front,它指向队头元素;另一个是队尾指针rear,它指向下一个入队元素的存储位置。
每次在队尾插入一个元素是,rear增1;每次在队头删除一个元素时,front增1。随着插入和删除操作的进行,队列元素的个数不断变化,队列所占的存储空间也在为队列结构所分配的连续空间中移动。
当front=rear时,队列中没有任何元素,称为空队列。当rear增加到指向分配的连续空间之外时,队列无法再插入新元素,但这时往往还有大量可用空间未被占用,这些空间是已经出队的队列元素曾经占用过得存储单元。
顺序队列中的溢出现象:
1."下溢"现象:当队列为空时,做出队运算产生的溢出现象。“下溢”是正常现象,常用作程序控制转移的条件。
2."真上溢"现象:当队列满时,做进栈运算产生空间溢出的现象。“真上溢”是一种出错状态,应设法避免。
3."假上溢"现象:由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为"假上溢"现象。
9.循环队列
在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进:无论插入或删除,一旦rear指针增1或front指针增1 时超出了所分配的队列空间,就让它指向这片连续空间的起始位置。
自己真从MaxSize-1增1变到0,可用取余运算rear%MaxSize和front%MaxSize来实现。这实际上是把队列空间想象成一个环形空间,环形空间中的存储单元循环使用,用这种方法管理的队列也就称为循环队列。
除了一些简单应用之外,真正实用的队列是循环队列。
在循环队列中,当队列为空时,有front=rear,而当所有队列空间全占满时,也有front=rear。
为了区别这两种情况,规定循环队列最多只能有MaxSize-1个队列元素,当循环队列中只剩下一个空存储单元时,队列就已经满了。因此,队列判空的条件时front=rear,而队列判满的条件时front=(rear+1)%MaxSize。
10.栈与队列
栈(stack)是限定仅在表尾进行插入和删除操作的线性表
队列(queue)是只允许在一段进行插入操作,而在另一端进行删除操作的线性表
它们均可用线性表的顺序结构存储结构来实现,但都存在着顺序存储的一些弊端,
对于栈来说,如果两个相同的数据类型栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化的利用数组的空间。
对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗。
栈:
. 顺序栈
. 两栈共享空间
. 链栈
队列:
. 顺序队列
. 循环队列
. 链队列