6、数据结构与算法 - 栈结构

大纲

  1.  限定性数据结构 - 栈与队列的特点
  2.  顺序栈实现(栈结构的顺序存储方式实现)
  3.  链式实现(栈结构的链式存储方式实现)
  4.  栈与递归
  5.  采用递归算法解决问题条件


 一、栈的特点:

 先进后出
 出栈和进站都是从栈顶进行操作。也就是说只能读到栈顶,不能像线性表可以读取任何位置上的元素。
 
 队列
 先进先出,只能充队尾插入,从队列头出队列
 
 栈的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件事:

  1.  将所有的实参,返回地址等信息调用传递给被调用函数保存。
  2.  为被调用函数的局部变量分配存储空间
  3.  将控制转移给被调用函数入口 

 而从被调用函数返回调用函数之前,系统同样需要完成3件事:

  1.  保存被调用函数的计算结果;
  2.  释放被调用函数的数据区
  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。
 
 逻辑结构:线性结构,结合结构,树形结构,图形结构
 物理结构:顺序存储结构,链式存储结构
 
 递归都能改成迭代,最好是使用迭代,递归不太好。

发布了104 篇原创文章 · 获赞 13 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/105469515
今日推荐