大纲
- 限定性数据结构 - 栈与队列的特点
- 顺序栈实现(栈结构的顺序存储方式实现)
- 链式实现(栈结构的链式存储方式实现)
- 栈与递归
- 采用递归算法解决问题条件
栈
一、栈的特点:
先进后出
出栈和进站都是从栈顶进行操作。也就是说只能读到栈顶,不能像线性表可以读取任何位置上的元素。
队列
先进先出,只能充队尾插入,从队列头出队列
栈的top 信息
栈需要开辟一段连续的内存空间。如上图,栈有2个元素的时候,栈顶指针top指向的 1。空栈 的栈顶-1(内存中是内有-1的,这里是一个标致,标致是空栈)
常见的匹配问题都可以用栈来实现
二、栈实现
. 语法是结构体属性变量 -> 指针
#include <stdio.h>
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
#define MAXSIZE 20 //存储空间初始分配量
typedef int Status;//Status 是函数的类型,其值是函数结果状态代码l,如OK等
typedef int SElemType;//ElemType 类型根据实际情况而定,这里假设为int
1、顺序栈实现
定义结点
//定义结点
typedef struct {
SElemType data[MAXSIZE];//开辟 MAXSIZE 大小的空间
int top;//栈顶指针 表示的是栈顶,是一个指针不是指针变量
}sqStack;
1、构建一个空的栈
//1、构建一个空的栈
Status InitStack(sqStack *S){
S->top = -1;//空的栈,-1 只是一个标记标识是空的栈
return OK;
}
2、将一个栈置空
//2、将一个栈置空
Status ClearStack(sqStack *S){
S->top = -1;//类似顺序存储的length = 0
return OK;
}
3、判断一个栈是否为空
//3、判断一个栈是否为空
Status StackEmpty(sqStack S){
if (S.top == -1) return OK;
return ERROR;
}
4、栈的长度
//4、栈的长度
int StackSLength(sqStack S){
return S.top + 1;//计数是从1开始不是0.所以要加1
}
5、获取栈顶(不代表出栈,只是想知道栈顶元素是什么)
//5、获取栈顶(不代表出栈,只是想知道栈顶元素是什么)
Status GetTop(sqStack S,SElemType * e){
if (S.top == -1)return ERROR;
*e = S.data[S.top];
return OK;
}
6、压栈(入栈)
//6、压栈(入栈)
Status PushData(sqStack *S, SElemType e){
if (S->top == MAXSIZE-1) return ERROR;//满了
S->top++;
S->data[S->top] = e;
return OK;
}
7、出栈
//7、出栈
Status Pop(sqStack *S, SElemType *e){
if (S->top == -1) return ERROR;
*e = S->data[S->top];
S->top--;
return OK;
}
8、栈的遍历
//8、栈的遍历
Status StackTraverse(sqStack S){
int i=0;
printf("此栈所有的元素:");
while (i<=S.top) {
printf("%d ",S.data[i++]);
}
printf("\n");
return OK;
}
2、链式存储栈实现
栈里面的结点
//栈里面的结点
typedef struct StackNode{
SElemType data;
struct StackNode * next;
}StackNode, *LinkStackPtr;
链式栈的结构
//链式栈的结构
typedef struct {
LinkStackPtr top;//指针,指向栈顶
int count;//栈里面有多少个元素
}LinkStack;
1、创建一个空栈
//1、创建一个空栈
Status InitStack(LinkStack *S){
S->top = (LinkStackPtr)malloc(sizeof(StackNode));
if (S->top == NULL) return ERROR;//这个地方判断的是否开辟成功
S->top = NULL;//这个地方是栈置空,也就是刚创建是一个空栈
S->count = 0;
return OK;
}
2、栈置为空
//2、栈置为空
Status ClearStack(LinkStack *S){
LinkStackPtr p,q;
p = S->top;
while (p) {
q = p;
p = p->next;
free(q);
}
S->count = 0;
return OK;
}
3、判断栈是否为空
//3、判断栈是否为空
Status StackEmpty(LinkStack S){
if (S.count == 0) return TRUE;//判断栈的长度
return OK;
}
4、栈的长度
//4、栈的长度
int StackLength(LinkStack S){
return S.count;
}
5、获取栈顶元素
//5、获取栈顶元素
Status GetTop(LinkStack S, SElemType *e){
if (S.top == NULL) return ERROR;
*e = S.top->data;
return OK;
}
6、压栈(入栈)
//6、压栈(入栈)
Status PushData(LinkStack *S,SElemType e){
// 是链式结构所以不需要判断栈满
LinkStackPtr temp = malloc(sizeof(StackNode));
if (!temp) return ERROR;
temp->data = e;
temp->next = S->top;
S->top = temp;
S->count ++;
return OK;
}
7、出栈
//7、出栈
Status Pop(LinkStack *S, SElemType *e){
// 判断栈是否为空,可以调函数也可以直接写。栈内可以调用但是少用
if (S->count == 0) return ERROR;
// if (StackEmpty(*S)) return ERROR;
LinkStackPtr temp;
temp = S->top;
*e = S->top->data;
S->top = S->top->next;
S->count--;
free(temp);
return OK;
}
8、遍历
//8、遍历
Status StackTraverse(LinkStack S){
LinkStackPtr p;
p = S.top;
while (p) {
printf("%d ",p->data);
p = p->next;
}
printf("\n");
return OK;
}
int main(int argc, const char * argv[]) {
sqStack S;
int e;
if (InitStack(&S) == OK) {
for (int j=1; j<10; j++) {
PushData(&S, j);
}
}
printf("顺序栈中元素为:\n");
StackTraverse(S);
Pop(&S, &e);
printf("弹出栈顶元素为:%d\n",e);
StackTraverse(S);
printf("是否为空栈:%d\n",StackEmpty(S));
GetTop(S, &e);
printf("栈顶元素:%d \n栈长度:%d\n",e,StackLength(S));
CleatStack(&S);
printf("是否已经清空栈 %d,栈长度为:%d\n",StackEmpty(S),StackLength(S));
return 0;
}
//***************************************************
顺序栈中元素为:
此栈所有的元素:1 2 3 4 5 6 7 8 9
弹出栈顶元素为:9
此栈所有的元素:1 2 3 4 5 6 7 8
是否为空栈:0
栈顶元素:8
栈长度:8
是否已经清空栈 1,栈长度为:0
三、栈和递归
1、递归:
若在一个函数、过程或数据结构定义的内部直接或间接出现定义本身的应用(在内部调用自己本身),则称他们是递归的 或者 是递归定义。所有的递归都能改成循环。递归比迭代更容易,但是会影响性能。
什么时候用递归?
1、定义是递归的:
比如很多数学定义本身就是递归定义。例如:阶乘、斐波拉契数列
阶乘:
阶乘Fact(n)
(1)、若n = 0,则返回1;
(2)、若n > 1,则返回n* Fact(n-1);
Long Fact(Long n){
if(n=0) return -1;
else return n*fact(n-1);
}
二阶斐波拉契数列 Fib(n)
(1)、若n = 1 或者 n = 2,则返回1;
(2)、若 n > 2,则Fib(n-1) + Fib(n-2);
Long Fib(long n){
if(n==1 || n==2) return 1;
else return Fib(n-1)+Fib(n-2);
}
对于类似这种复杂问题,若能够分解成几个简单且解法相同或类似的子问题,来求解,便成为递归求解。
例如求解4时,先求解3.然后再进一步分解进行求解,这种求解方式叫做 ”分治法“。
采用 ”分治法“进行递归求解的问题需要满足三个条件:
(1)、能将一个问题转换成一个小问题和原问题解法相同或雷同,不同的仅仅是处理的对象,并且这些处理更小且变化有规律的。
(2)、可以通过上述转换而使得问题简化
(3)、必须有一个明确的递归出口,或称为递归边界
例如:
void p(参数){
if(递归结束条件成立) 可直接求解;//递归终止条件
else p(较小的参数);//递归步骤
}
2、数据结构是递归的
其数据结构本身具有递归的特性
例如:
链表结构是递归,二叉树也是递归
对于链表,其结点LNode 的定义由数据域data 和指针域next 组成。而指针域next 是一种指向LNode 类型的指针,即LNode 的定义中又用到了其自身,所以链表是一种递归的数据结构;
在递归算法中,如果当递归结束条件成立,只执行return 操作时,分治法求解递归问题算法一般形式可以简化为
void p(参数){
if(递归结束条件不成立)
p(较小参数);
}
例:
void TraverseList(linkList p){
//递归终止
if(p){
printf("%d",p->data);
TraverseList(p->next);//p指向后继结点继续递归
}
}
3、问题的解法是递归的
有一类问题,虽然问题本身并没有明显的递归结构,但是采样递归求解比迭代求解更简单,如 Hanoi塔问题,八皇后问题,迷宫问题
斐波拉契数列,an + a(n+1) = a(n+2);
调用函数和被调用函数是通过什么来进行数据交换的,通过栈空间。
//斐波拉契数列
int Fbi(int i){
if (i<2) return i;
return Fbi(i-1)+Fbi(i-2);
}
int main(int argc, const char * argv[]) {
//斐波拉契数列
for (int i=0; i<10; i++) {
printf("%d ",Fbi(i));
}
printf("\n");
return 0;
}
C语言中调用函数和被调用函数,会有连接和交换信息。那么他们之间的连接和交换信息是通过什么来实现的?
通过 栈空间。
1、调用函数在碰到被调用函数的时候,需要将所有的实参和返回地址信息保存起来。
2、被调用函数要给局部变量开辟空间(Fbi(int i) 这里的i)
3、被调用函数返回的时候要保存计算结果
如上图,调用的时候一层一层的拆分去调用,直到遇到 1 和 0 的时候才返回。返回的时候一层一层的去返回。
系统是将整个运行的数据空间安排在一个栈里面(在栈里分配一定的空间),每调用一个函数的时候,都会为栈顶分配一个空间。每一个函数都会有一个对应的栈空间
如上所以尽量不要用递归去解决问题
递归过程与递归工作栈
一个递归函数,在函数的执行过程中,需要多次进行自我调用。那么一个递归函数是如何执行的?
在了解递归函数是如何指向之前,先来了解一下任意的2个函数之间调用是如何进行的。
在高级语言的程序中,调用函数和被调用函数之间的连接与信息交换都是通过栈来进行的。
通常,当在一个函数的运行期间调用另一个函数时,在运行被调用函数之前,系统需要先完成3件事:
- 将所有的实参,返回地址等信息调用传递给被调用函数保存。
- 为被调用函数的局部变量分配存储空间
- 将控制转移给被调用函数入口
而从被调用函数返回调用函数之前,系统同样需要完成3件事:
- 保存被调用函数的计算结果;
- 释放被调用函数的数据区
- 依照被调用函数保存的返回地址将控制移动到调用函数
当多个函数构成嵌套调用时,按照”先调用后返回“的原则,上述函数之间的信息传递和控制转移必须通过”栈“来实现,即系统将整个程序运行时的所需要的数据空间都安排在一个栈中,每当调用一个函数时,就在他的栈顶分配一个存储区,每当这个函数退出时,就释放它的存储区,则当前运行时的函数的数据区必在栈顶。
在主函数main中掉员工函数first,而在函数first 又嵌套调用了second 函数。则当我们执行当前在执行的first 函数时,栈空间里保存了这些信息;而当我们执行second 则图(b)保存了这些信息
一个递归函数的运行过程类似多个函数嵌套调用;只是调用函数和被调用函数是同一个函数。因此,和每次调用相关的一个重要概念是递归函数运行的”层次“。假设调用该函数的主函数为第0层,则从主函数调用递归函数进入第1层,从第i 层递归调用本函数为进入下一层。即第i+1 层,反正退出第i 层递归应返回上一层 即i-1层。
为了保证递归函数正确执行,系统需要设立一个”递归工作栈“作为整个递归函数运行期间使用的数据存储区。每一层递归所需信息构成一个工作记录,其中包括所有的实参,所有的局部变量以及上一层的返回地址。每进入一层递归,就产生一个新的工作记录压入栈顶。每退出一个递归,就从栈顶弹出一个工作记录,则当前执行层的工作记录必须是递归工作栈栈顶的工作记录,称为”活动记录“。
个人理解:
(1)、main也是函数,开辟了m n的这块空间(现在这是栈顶),然后调用first 函数。
(2)、first函数里面有3个临时变量 s t i,开辟一个空间(现在这是栈顶)。然后又调用了second 函数。
(3)、second 函数里面有3个临时变量 d x y,开辟一个空间(现在这是栈顶)
(4)、当second 执行完,回到first (栈顶改变,现在 s t i 是栈顶)。
(5)、当first 执行完,回到main。
逻辑结构:线性结构,结合结构,树形结构,图形结构
物理结构:顺序存储结构,链式存储结构
递归都能改成迭代,最好是使用迭代,递归不太好。