美文网首页
数据结构与算法(六):栈

数据结构与算法(六):栈

作者: 顶级蜗牛 | 来源:发表于2023-06-25 16:22 被阅读0次

    相关文献:
    数据结构与算法(一):基础理论
    数据结构与算法(二):线性表的实现
    数据结构与算法(三):线性表算法设计练习
    数据结构与算法(四):斐波那契数列
    数据结构与算法(五):LRU
    数据结构与算法(六):栈
    数据结构与算法(七):栈/队列的算法解题思想
    数据结构与算法(八):队列
    数据结构与算法(九):二叉树/线索化二叉树
    数据结构与算法(十):哈夫曼树

    本章节研究内容:
    1.限定性数据结构--栈与队列的特点
    2.顺序栈的实现(栈结构的顺序存储方式实现)
    3.链式栈的实现(栈结构的链式存储方式实现)
    4.栈与递归

    一、栈与队列的特点

    栈和队列数据结构的特点

    • 栈:先进后出;
    • 队列:先进先出。

    它们对插入和删除操作的限定的

    • 栈是限定只能在表的一端进行插入和删除操作的线性表;
    • 队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。

    遍历数据速度不同:

    • 栈只能从头部取数据 也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性。
    • 队列则不同,他基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间。
    栈的示意图 队列的示意图

    二、顺序栈的实现

    顺序栈:采用顺序存储的栈称为顺序栈,它采用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时设定一个指针(top)指向当前栈顶元素的位置。通常采用数组来实现。

    顺序栈
    • 1.空栈时,top索引为-1;栈满 top == MAXSIZE-1
    • 2.入栈时,top索引+1;出栈时,top索引-1;
    • 3.入栈和出栈都是通过top索引来访问内存块。
    /**
     栈的顺序存储
     */
    #include "stdio.h"
    #include "stdlib.h"
    
    #include "math.h"
    #include "time.h"
    
    #define OK 1
    #define ERROR 0
    #define TRUE 1
    #define FALSE 0
    #define MAXSIZE 20 /* 存储空间初始分配量 */
    
    typedef int Status;
    typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
    
    /* 顺序栈结构 */
    typedef struct {
        SElemType data[MAXSIZE];
        int top; // 栈顶索引,空栈时索引为-1,因为开辟数组存储第一个元素索引在0 /* 用于栈顶指针 */
    }SqStack;
    
    //4.1 构建一个空栈S
    Status InitStack(SqStack *S){
        S->top = -1;
        return OK;
    }
    
    //4.2 将栈置空
    Status ClearStack(SqStack *S){
        //疑问: 将栈置空,需要将顺序栈的元素都清空吗?
        //不需要,只需要修改top标签就可以了.
        S->top = -1;
        return OK;
    }
    
    //4.3 判断顺序栈是否为空;
    Status StackEmpty(SqStack S){
        if (S.top == -1)
            return TRUE;
        else
            return FALSE;
    }
    
    //4.4 返回栈的长度
    int StackLength(SqStack S){
        return S.top + 1;
    }
    
    //4.5 获取栈顶
    Status GetTop(SqStack S,SElemType *e){
        if (S.top == -1)
            return ERROR;
        else
            *e = S.data[S.top];
        return OK;
    }
    
    //4.6 插入元素e为新栈顶元素
    Status PushData(SqStack *S, SElemType e){
        //栈已满
        if (S->top == MAXSIZE -1) {
            return ERROR;
        }
        
        //栈顶指针+1;
        S->top ++;
        //将新插入的元素赋值给栈顶空间
        S->data[S->top] = e;
        return OK;
    }
    
    //4.7 删除S栈顶元素,并且用e带回
    Status Pop(SqStack *S,SElemType *e){
        //空栈,则返回error;
        if (S->top == -1) {
            return ERROR;
        }
        
        //将要删除的栈顶元素赋值给e
        *e = S->data[S->top];
        //栈顶指针--;
        S->top--;
        
        return OK;
    }
    
    //4.8 从栈底到栈顶依次对栈中的每个元素打印
    Status StackTraverse(SqStack S){
        int i = 0;
        printf("此栈中所有元素");
        while (i<=S.top) {
            printf("%d ",S.data[i++]);
        }
        printf("\n");
        return OK;
    }
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        printf("顺序栈的表示与实现!\n");
        
        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));
        ClearStack(&S);
        printf("是否已经清空栈 %d, 栈长度为:%d\n",StackEmpty(S),StackLength(S));
    
        return 0;
    }
    
    

    三、链式栈的实现

    链式栈:是一种数据存储结构,可以通过单链表的方式来实现,使用链式栈的优点在于它能够克服用数组实现的顺序栈空间利用率不高的特点,但是需要为每个栈元素分配额外的指针空间用来存放指针域。

    • 1.top指针永远指向栈顶;
    • 2.无论入栈还是出栈都是操作top指针。
    /**
     链式栈的表示与实现
     */
    #include "stdio.h"
    #include "stdlib.h"
    
    #include "math.h"
    #include "time.h"
    
    #define OK 1
    #define ERROR 0
    #define TRUE 1
    #define FALSE 0
    #define MAXSIZE 20 /* 存储空间初始分配量 */
    
    typedef int Status;
    typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
    
    
    /* 链栈结构 */
    typedef struct StackNode
    {
        SElemType data;
        struct StackNode *next;
    }StackNode,*LinkStackPtr;
    
    /* 链式栈结构 */
    typedef struct
    {
        LinkStackPtr top;
        int count;
    }LinkStack;
    
    /*5.1 构造一个空栈S */
    Status InitStack(LinkStack *S)
    {
        S->top=NULL;
        S->count=0;
        return OK;
    }
    
    
    /*5.2 把链栈S置为空栈*/
    Status ClearStack(LinkStack *S){
        LinkStackPtr p,q;
        p = S->top;
        while (p) {
            q = p;
            p = p->next;
            free(q);
        }
        S->count = 0;
        return OK;
        
    }
    
    /*5.3 若栈S为空栈,则返回TRUE, 否则返回FALSE*/
    Status StackEmpty(LinkStack S){
        if (S.count == 0)
            return TRUE;
        else
            return FALSE;
    }
    
    /*5.4 返回S的元素个数,即栈的长度*/
    int StackLength(LinkStack S){
        return S.count;
    }
    
    /*5.5 若链栈S不为空,则用e返回栈顶元素,并返回OK ,否则返回ERROR*/
    Status GetTop(LinkStack S,SElemType *e){
        if(S.top == NULL)
            return ERROR;
        else
            *e = S.top->data;
        return OK;
    }
    
    /*5.6 插入元素e到链栈S (成为栈顶新元素)*/
    Status Push(LinkStack *S, SElemType e){
        
        //创建新结点temp
        LinkStackPtr temp = (LinkStackPtr)malloc(sizeof(StackNode));
        //赋值
        temp->data = e;
        //把当前的栈顶元素赋值给新结点的直接后继, 参考图例第①步骤;
        temp->next = S->top;
        //将新结点temp 赋值给栈顶指针,参考图例第②步骤;
        S->top = temp;
        S->count++;
        return OK;
    }
    
    /*5.7 若栈不为空,则删除S的栈顶元素,用e返回其值. 并返回OK,否则返回ERROR*/
    Status Pop(LinkStack *S,SElemType *e){
        LinkStackPtr p;
        if (StackEmpty(*S)) {
            return ERROR;
        }
        
        //将栈顶元素赋值给*e
        *e = S->top->data;
        //将栈顶结点赋值给p,参考图例①
        p = S->top;
        //使得栈顶指针下移一位, 指向后一结点. 参考图例②
        S->top= S->top->next;
        //释放p
        free(p);
        //个数--
        S->count--;
        
        return OK;
        
        
    }
    
    /*5.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[]) {
        // insert code here...
        printf("链栈定义与实现\n");
        
        int j;
        LinkStack s;
        int e;
        if(InitStack(&s)==OK)
            for(j=1;j<=10;j++)
                Push(&s,j);
        printf("栈中元素依次为:");
        StackTraverse(s);
        Pop(&s,&e);
        printf("弹出的栈顶元素 e=%d\n",e);
        StackTraverse(s);
        printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
        GetTop(s,&e);
        printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s));
        ClearStack(&s);
        printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));
        
        return 0;
    }
    

    四、栈与递归

    1.了解递归
    • 递归:若在一个函数(数据结构)里,直接或间接出现调用本身,则称它们为递归的。

    在什么时候使用递归?

    • a.数学定义是递归的
      很多的数学定义都是递归定义,比如:
    阶乘: 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);
    }
    
    • b.数据的结构是递归的
      链表、二叉树、二叉树列也是递归的
    • c.问题的解法是递归的
      汉罗塔问题、迷宫问题等
    分治法

    分治法其实就是用递归解决问题,但是它得满足三个条件才能使用分治法。

    • a.能将大问题转变为小问题,而新问题和原问题解法高度类似或相同,不同的仅仅是处理的对象,并且这些处理更小且变化是有规律的;(可拆解)
    • b.可以通过上述转化而使得问题简单化;(简化问题)
    • c.必须有一个明确的递归出口(递归边界).(终止递归的条件)
    2.递归过程与递归工作栈

    一个递归函数,在函数的执行过程中,需要多次进行自我调用,那么思考一下,此时的递归函数是如何执行的?

    在了解递归函数是如何执行之前,先了解一下任意两个函数之间的调用是如何进行的?
    在高级语言的程序中,调用函数和被调用函数之间的连接与信息交换都是通过栈来进行的。

    当在一个函数的运行期间调用另一个函数时,在运行被调用函数之前,系统要做三件事:

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

    从被调用函数返回到调用函数之前,系统同样要做三件事情:

    • 1.保存被调用函数的计算结果;
    • 2.释放被调用函数的数据区;
    • 3.依照被调用函数保存的返回地址将控制权移动到调用函数。

    当多个函数构成嵌套调用时,按照"先调用后返回"的原则,上述函数之间的信息传递和控制传递都必须通过"栈"来实现,即系统程序整个运行时的所需要的数据空间都安排在一个栈中,每当调用一个函数时,就在它的栈顶分配一个存储区,每当这个函数退出时,就释放它的存储区,则当前运行的函数的数据区必须在栈顶。

    来看看我们以下面代码例子:

    int main(int argc, const char * argv[]) {
        int m, n;
        first(m, n);
        // 1.入栈...
        // ...
        return 0;
    }
    
    void first(int s, int t) {
        int i;
        second(i);
        // 2.入栈...
        // ...
    }
    
    void second(int d) {
        int x, y;
        // ...
    }
    

    1.main函数里边调用first函数:当我们执行当前函数first时,则first函数的数据区在栈顶。

    2.first函数里边调用second函数:当我们执行second函数时,second函数的数据区在栈顶。

    回答递归函数是如何执行的:

    一个递归函数的运行过程类似多个函数的嵌套调用,只是调用函数和被调用函数都是同一个函数,因此,每次调用本身时,就会产生一个调用相关的一个重要概念就是递归函数运行的"层次".

    为了保证递归函数正确执行,系统需要设立一个"递归工作栈"作为整个递归函数运行期间使用的数据存储区,每一层递归所需信息构成一个工作记录,其中包括所有实参,所有局部变量以及上一层的返回地址.每进入一层递归,就产生一个新的工作记录压入栈顶,每退出一个递归,就从栈顶弹出一个工作记录,则当前执行层的工作记录必须是递归工作栈栈顶的工作记录,此称为"活动记录"。

    相关文章

      网友评论

          本文标题:数据结构与算法(六):栈

          本文链接:https://www.haomeiwen.com/subject/jkmbedtx.html