限制线性表插入和删除等运算的位置(只允许在端点位置操作)
栈是限定仅在表的一端进行插入和删除的线性表
队列是限定仅在表的一端进行插入、在表的另一端进行删除的线性表
栈和队列是两种常用的抽象数据类型、都属于线性结构
栈:
栈可以改变数据的处理顺序 栈是函数过程嵌套调用及递归调用管理的有效手段
定义: 只能在表的一端(栈顶)进行插入和删除运算的线性表
逻辑结构: 与线性表相同、仍为一对一关系
存储结构 : 用顺序栈或链栈存储均可、但以顺序栈更常见
运算规则: 只能在栈顶运算、且访问结点时依照后进先出(LIFO)或先进后出(FILO)的原则
实现方法: 编写入栈、出栈函数(关键)、具体实现顺序栈与链栈不同、基本操作:入栈、出栈、读取栈顶元素、建栈、判栈满、判栈空等
顺序栈:利用一组连续的存储单元依次存放自栈底到栈顶的数据元素,由于栈操作的特殊性、必须设一个位置指针top(栈顶指针)来动态指示栈顶元素在顺序栈中的位置
顺序栈的基本操作实现:
1、初始化
2、判栈空
3、判栈满
4、进栈
5、出栈
6、读栈顶元素
7、两栈共享的数据结构
1、初始化
void InitStack(SeqStack*S)
{
S->top=-1;//构造一个空栈
}
#define Stack_Size 50
typedef struct
{
StackElementType elem[Stack_Size];
int top;
}SeqStack;
2、判栈空
int IsEmpty(SeqStack *S)
{//判断栈S为空栈时返回值为真、反之为假
return (S->top==-1?TURE:FALSE);
}
#define Stack_Size 40
typedef struct
{
StackElementType elem[Stack_Size];
int top;
}SeqStack;
3、判栈满
int IsFull(SeqStack *S)
{//判断S为满时返回真、反之返回假
return(S->top==Stack_Size-1?TURE:FALSE);
}
#define Stack_Size 30
typedef struct
{
StackElementType elem[Stack_Size];
int top;
}SeqStack;
4、 进栈
int Push(SeqStack *S,StackElementType x)
{
if(S->top==Stack_Size-1)
return(FALSE);//栈已满
S->top++;
S->elem[S->top]=x;
return(TRUE);
}
#define Stack_Size 30
typedef struct
{
StackElementType elem[Stack_Size];
int top;
}SeqStack;
5、出栈
int Pop(SeqStack *S,StackElementType *x)
{//将栈S的栈顶元素弹出、放在x所指的存储空间中
if(S->top==-1)//栈为空
return(FALSE);
else
{
*x=S->elem[S->top];
S->top--;//修改栈顶指针
return(TRUE);
}
}
#define Stack_Size 30
typedef struct
{
StackElementType elem[Stack_Size];
int top;
}SeqStack;
6、读取栈顶元素
int GetTop(SeqStack S,StackElementType *x)
{//将栈顶S的元素弹出、放到X所指的存储空间中、但栈顶指针保持不变
if(S->top==-1)//栈为空
return(FALSE);
else
{
*x=S->elem[S->top];
return(TRUE);
}
}
#define Stack_Size 30
typedef struct
{
StackElementType elem[Stack_Size];
int top;
}SeqStack;
链栈:
链表作为存储结构实现的栈。为了方便、采用带头结点的单链表实现栈。因栈的插入和删除操作仅限制在表头位置进行、所以链表的表头指针就作为栈顶指针
top->next=NULL 代表空栈 链栈在使用完毕后、应释放其空间
链栈数据结构定义:
typedef struct node
{
StackElementType data;
struct node *next;
}LinkStackNode;
typedef LinkStackNode *LinkStact
//指向结点的指针
进栈:
typedef struct node
{
StackElementType data;
struct node *next;
}LinkStackNode,*LinkStack;
int Push(LinkStack top,StackElementType x)
{//将数据元素x压入栈top中
LinkStackNode *temp;
temp=(LinkStackNode *)malloc(sizeof(LinkStackNode));
if(temp==NULL) return(FALSE);/*申请空间失败*/
temp->data=x;
temp->next=top->next;
top->next=temp;/*修改当前栈顶指针*/
return(TRUE);
}
出栈:
typedef struct node
{
StackElementType data;
struct node *next;
}LinkStackNode,*LinkStack;
int Pop(LinkStack top, StackElementType *x)
{/*将栈top的栈顶元素弹出,放到x所指的存储空间*/
LinkStackNode * temp; temp=top->next;
if(temp==NULL)/*栈为空*
return(FALSE);
top->next=temp->next;
*x=temp->data;
free(temp);/*释放存储空间*/
return(TRUE);
}
栈、队列与一般线性表的区别:
栈、队列是一种特殊(操作受限)的线性表
区别:仅在于运算规则不同
一般线性表:
逻辑结构:一对一
存储结构:顺序表、顺序存取
运算规则:随机、顺序存取
栈:
逻辑结构:一对一
存储结构:顺序栈、链栈
运算规则:后进先出
队列:
逻辑结构:一对一
存储结构:顺序队、链队
运算规则:先进先出
检验括号是否匹配可用栈来实现
参数传递:
函数调用时传递给形参表的实参必须与形参在类型、个数、顺序上保持一致
值传递:单向传递、实参的值不会被改变(改变的是副本的值)、浪费内存空间 参数为整型、实型、字符型等
地址传递:双向传递、可以改变传递过来的参数内容、即形参的改变会影响实参、通过指针类型将结果带进带出、只是传递的地址、节省内存空间 参数为指针变量 参数为数组名
几点说明
1、传递一般变量给函数时.形参的变化不会影响实参;传递指针变量给函数时,形参变化实参也发生变化。
2、指针变量作参数,在内存中并没有产生实参的副本,它直接对实参操作;而一般变量作参数,形参与实参就占用不同的存储单元,所以形参变量的值是实参变量的副本。因此,当参数传递的数据量较大时,用指针变量比用一般变量传递参数的时间和空间效率都好。
3、需要注意的是:用指针变量作参数时,在主调函数的调用点处、必须用变量的地址作为实参。在被调函数中需要重复使用*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;
算法表达式求值:
表达式:操作数(operand)、运算符(operator)、界限符(deliniter)
运算符和界限符统称为运算符(操作符)
操作数:整型常数
运算符:加减乘除
界限符:表达式起始符“#”、表达式结束符“#”
表达式格式:#1+2*3-4/2#
算法实现:
两个工作栈:
操作符栈:存放操作符
操作数栈:存放操作数和中间结果
算法基本过程:
初始化操作数栈和操作符栈,并将表达式起始符#压入操作符栈;
依次读入表达式中的每个字符,若是操作数则直接进入操作数栈,若是操作符,则与操作符栈的栈顶操作符进行优先权比较,并做如下处理:
(⑴)若刚读入的操作符的优先级高于栈顶操作符,则让刚读入的操作符进入操作符栈;
(2)若刚读入的操作符的优先级低于或等于栈顶操作符,则将栈顶操作符退栈,送入0,同时将操作数栈退栈两次,得到两个操作数a、b,对a、b进行0运算后,将运算结果作为中间结果推入操作数栈;
当两个#相遇时,整个表达式求值完毕
知识回顾:
函数的调用:
当一个函数在运行期间调用另一个函数时、在运行该被调函数之前、需要完成以下三项任务
1、将所有实参、返回地址等信息传递给被调用函数保存
2、为被调用函数的局部变量分配存储区
3、将控制转移到被调用函数的入口
从被调用函数返回调用函数之前、应完成以下三项任务
1、保存返回计算结果(用函数名、引用参数)
2、释放被调用函数的数据区(局部量)
3、依照被调函数中保存的返回地址将控制转移到调用函数
多个函数嵌套调用的规则是:后调用先返回——栈
上述函数之间的信息传递和控制转移必须通过栈来实现。系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,而每当从一个函数退出时,就释放它的存储区。此时的内存管理实行“栈式管理”,当前正在运行的函数的数据区必在栈顶。
函数的调用是通过栈来实现的,满足栈的“后进先出的特点”
每个函数数据区含有:参数、局部变量、返回地址等信息
递归的基本概念:
定义:在定义自身的同时、又出现了对自身的调用
直接递归函数:函数在其定义体内直接被调用
间接递归函数:函数经过一系列的中间调用语句、通过其他函数间接调用自己
具有递归特性的例子:
递归定义的数学函数
具有递归特点的数据结构
可递归求解问题
具有递归特性的数据结构:二叉树、广义表
优点:递归算法是一种分而治之、把复杂问题分解为简单问题的求解方法。递归程序结构清晰、程序可读性强,且其正确性易于证明,给用户编程带来很大方便。适合解决比较精细的问题。
当问题满足如下条件时,可设计递归算法:
1、原向题可以层层分解为类似的子问题,子问题规模比原问题小;
2、规模最小的子问题具有直接解。
设计递归算法的方法是;
1.寻找分解方法,将原问题分解为类似的子问题求解;
2.设升递归出口,按最小规模子问题确定递归终止条件;
缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息。时间开销大。递归算法效率较低(时间和空间两方面),若处理数据规模较大时,要多加权衡。
递归的消除:递归->非递归
1单向递归→循环结构
单向递归是指递归函数虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调函数有
关,相互之间参数无关,并且这些递归调用语句处于算法的最后。
例:计算斐波那契数列的递归算法
2尾递归→循环结构
尾递归是指递归调用语句只有一个,而且是处于算法的最后,尾递归是单向递归的特例。
例:求n!的递归算法与非递归算法