栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
我们允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为 空栈。栈又称为后进先出的线性表,简称LIFO结构。
理解栈的定义需要注意:
1.他是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。是一种特殊的线性表,他的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行,这也就使得:栈底是固定的,最先进栈的只能在栈底。
栈的插入操作,叫作进栈,也称压栈、入栈。
栈的删除操作,叫作出栈,也有叫作弹栈。
最先进栈的元素,是不是就只能是最后出栈呢?
答案是不一定,要看什么情况。栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
案例:我们现在是有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.
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
栈的结构定义
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
SElemType data[MAXSIZE] ;
int top; /* 用于栈顶指针 */
}SqStack;
进栈的push
/* 插入元素e 为新的栈顶元素 */
status push ( SqStack *S ,SElemType e )
{
if( s->top == MAXSIZE -1 ) { /* 栈满 */
return ERROR;
}
S -> top ++; /* 栈顶指针加一 */
S ->data[ S -> top ] = e; /* 将新插入元素复制给栈顶空间 */
return OK;
}
/* 若栈不空,则删除S的栈顶元素,用 e 返回其值,并返回OK;否则返回ERROR */
status Pop ( SqStack *S , SElemType *e )
{
if ( S ->top == -1 ){
return ERROR;
}
*e = S -> data[S -> top ]; /* 将要删除的栈顶元素赋值给e */
S -> top --; /* 栈顶指针减一 */
return OK;
}
进栈和出栈都没有涉及循环语句,因此时间复杂度均为 O( 1 )。
/* 两栈共享空间结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top1; /* 栈1 栈顶指针 */
int top2; /* 栈2 栈顶指针 */
}SqDoubleStack;
对于两栈共享空间的push方法,我们除了要插入元素值参数值外,还需要添加一个判断栈1还是栈2的参数stackNumber。
/* 插入元素 e 为新的栈顶元素 */
status push ( SqDoubleStack *S , SElemType e, int stackNumber )
{
if ( S -> top1 +1 == top2 ) { /* 栈已满,不能在push了 */
return ERROR;
}
if ( stackNumber == 1 ){ /* 栈1 有元素 */
S -> data[ ++S -> top1 ] = e; /* 若栈1则先top1 +1 后给数据元素赋值 */
} else if ( stackNumber == 2 ){ /* 栈2有元素 */
S ->data[ -- S -> top2 ] = e; /* 若栈2则先 top2 - 1后给数据元素赋值 */
}
return OK;
}
对于两栈共享空间的pop方法,参数就只是判断栈1 和栈2 的参数stackNumber。
/* 若栈不空,则删除S的栈顶元素,用e 返回其值,并返回ok;否则返回error */
status pop ( SqDoubleStack *S , SElemType *e , int stackNumber )
{
if ( stackNumber == 1 ){
if ( S ->top1 == -1 ){
return ERROR; /* 说明栈1已经是空栈了,溢出 */
}
*e = S ->data [ S->top1 -- ]; /* 将栈1的栈顶元素出栈 */
}else if ( stackNumber == 2 ){
if ( S ->top2 == MAXSIZE ){
return ERROR; /* 说明栈2已经是空栈了,溢出 */
}
*e = S ->data [ S->top2 ++ ]; /* 将栈2的栈顶元素出栈 */
}
return OK;
}
栈的链式存储结构,简称为链栈。
链栈的结构代码:
typedef struct stackNode
{
SElemType data;
struct stackNode *next;
}StackNode ,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack ;
/* 插入元素 e 为新的栈顶元素 */
status push( LinkStack *S , SElemType e )
{
LinkStackPtr s = ( LinkStackPtr ) malloc ( sizeof( StackNode ) );
s -> data = e;
s -> next = S -> top; /* 把当前元素的栈顶元素赋值给新结点的直接后继,如图① */
S -> top = s; /* 将新的结点s赋值给栈顶指针,如图② */
S -> count ++;
return OK;
}
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回ok;否则返回ERROR */
status pop( LinkStack *S ,SElemtype *e )
{
LinkStackPtr p;
if ( StackEmpty ( *S ) ){
return ERROR;
}
*e = S -> top ->data;
p = S -> top; /* 将栈顶结点赋值给p,如图③ */
S -> top = S -> top ->next; /* 是的栈顶指针下移一位,指向后一节点,如图④ */
free( p ); /* 释放结点p */
S -> count --;
return OK;
}
链栈的进栈和出栈操作都很简单,没有任何循环操作,时间复杂度均为O(1)。
如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好用链栈,反之,如果他的变化在可控范围内,建议使用顺序栈会更好一些。
栈的引人简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。
斐波那契数列数学定义
常规的迭代实现
int main(){
int i;
int a[ 40 ];
a[ 0 ] = 0;
a[ 1 ] = 1;
printf( "%d", a[ 0 ] );
printf( "%d" ,a[ 1 ] );
for ( i = 2; i < 40 ;i++ ){
a[ i ] = a[ i-1 ] + a[ i-2 ];
printf( "%d",a[ i ] );
}
return 0;
}
使用递归方式实现
/* 斐波那契数列的递归函数 */
int Fbi ( int i ){
if( i < 2 ){
return i == 0 ? 0 : 1;
}
return Fbi ( i-1 ) + Fbi ( i -2 ); /* 这里的Fbi就是函数自己,他在调用自己 */
}
int main(){
for ( int i = 0; i < 40 ; i++){
printf ( " %d " , Fbi( i ) );
}
return 0;
}
在高级语言中,调用自己和其他函数并没有本质的不同。我们把一个直接调用自己或通过一系列的调用语句间接的调用自己的函数,称做 递归函数。
每个递归定义必须至少有一个条件,满足时递归不在进行,即不在引用自身而是返回值并退出。
迭代和递归的区别:
迭代使用的是循环结构。递归使用的是选择结构。递归能够使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。
递归和栈的关系
递归过程退回的顺序是他前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构。
简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
一种不需要括号的后缀表达法,称为 逆波兰
案例: 9 + ( 3 - 1 ) * 3 + 10 / 2
后缀发表示: 9 3 1 - 3 * + 10 2 / +
后缀表达式,叫后缀的原因在于所有的符号都是在要运算数字的后面出现。
后缀表达式 : 9 3 1 - 3 * + 10 2 / +
计算规则:从左到右遍历表达式的每个数据和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
我们平时所用的标准四则运算表达式叫做中缀表达式。因为所有的运算符号都在两数字的中间。
中缀表达式转后缀表达式的规则:
从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断与栈顶符号的优先级,是有括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
从刚才的推到中会发现,要想让计算机具有处理我们通常的标准表达式的能力,最重要的就是两步:
1.将中缀表达式转化为后缀表达式(栈用来进出运算符号)。
2.将后缀表达式进行运算得出结果(栈用来进出运算的数字)。
/* CSDN的编辑实在太难用了,以后会逐渐转向博客园 */
传送地址:https://www.cnblogs.com/cjn93/p/9342845.html