栈与递归的实现

1. 栈的概念及结构

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。

进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底。

栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则,因此栈又被称作后进先出的线性表。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。

出栈:栈的删除操作叫做出栈。出数据也在栈顶。

根据上述定义,每次进栈的元素都被放在原栈顶元素之上而成为新的栈顶,而每次出栈的总是当前栈中“最新”的元素,即最后进栈的元素。

在下面栈的结构示意图中,元素是以a1,a2,a3,……,an的顺序进栈的,而出栈的次序却是an,……,a3,a2,a1。

在日常生活中也可以见到很多后进先出的例子,例如:

手枪子弹夹中的子弹,铁路调度站以及函数栈帧的创建与销毁等……

栈的基本操作除了进栈(栈顶插入),出栈(删除栈顶元素)外,还有建立栈(栈的初始化),判空,获取栈中数据个数以及取栈顶元素等。

2. 栈的实现

栈作为一种特殊的线性表,在计算机中主要有两种基本的存储结构:顺序存储结构和链式存储结构。

采用顺序存储结构的栈简称顺序栈,采用链式存储结构的栈简称为链栈。

相比之下,顺序栈要比链栈更优,因为顺序栈在尾上插入数据的代价较小。

结构与接口函数定义

typedef int STDataType;

typedef struct Stack
{
	STDataType* data;
	int top;
	int capacity;
}Stack;

//初始化
void STInit(Stack* pst);
//销毁
void STDestroy(Stack* pst);
//插入
void STPush(Stack* pst, STDataType x);
//删除
void STPop(Stack* pst);
//获取栈顶数据
STDataType STTop(Stack* pst);
//判断是否为空
bool STEmpty(Stack* pst);
//剩余数据个数
int STSize(Stack* pst);

接口函数的实现

void CheckCapacity(Stack* pst)
{
	assert(pst);
	if (pst->top == pst->capacity)
	{
		int newcapacity = (pst->capacity == 0) ? 4 : (pst->capacity * 2);
		STDataType* tmp = (STDataType*)realloc(pst->data, sizeof(STDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		pst->data = tmp;
		pst->capacity = newcapacity;
	}
}

//初始化
void STInit(Stack* pst)
{
	assert(pst);
	pst->data = NULL;
	//top指向栈顶数据的下一个位置
	pst->top = 0;
	pst->capacity = 0;
}

//销毁
void STDestroy(Stack* pst)
{
	assert(pst);
	free(pst->data);
	pst->capacity = 0;
	pst->top = 0;
}

//插入
void STPush(Stack* pst, STDataType x)
{
	assert(pst);
	CheckCapacity(pst);
	pst->data[pst->top++] = x;
}

//删除
void STPop(Stack* pst)
{
	assert(pst);
	if(pst->top > 0)
	pst->top--;
}

//获取栈顶数据
STDataType STTop(Stack* pst)
{
	assert(pst);
	assert(!STEmpty(pst));

	return pst->data[pst->top - 1];
}

//判断是否为空
bool STEmpty(Stack* pst)
{
	assert(pst);
	return pst->top == 0;
}

//剩余数据个数
int STSize(Stack* pst)
{
	assert(pst);
	return pst->top;
}

3. 栈与递归的实现

前面提到,函数栈帧的创建与销毁过程也是以栈这一数据结构为基础的。

而将函数栈帧的创建与销毁利用到极致的便是递归这一解决问题的手段。

递归算法就是在算法中直接或间接调用算法本身的算法。

如果一个函数在其定义体内直接调用自己,则称为直接递归函数;如果一个函数经过一系列的中间调用语句,通过其他函数间接调用自己,则称为间接递归函数

使用递归算法有以下两个前提:

1. 原问题可以层层分解为类似的子问题,且子问题比原问题的规模小。

2. 规模最小的子问题具有直接解。

设计递归算法的原则是用自身的简单情况来定义自身,方法如下:

1. 寻找分解方法:将原为你转化为子问题求解。例如,n! = n(n-1)!

2. 设计递归出口:根据规模最小的子问题确定递归终止条件。例如,求解n!,当n = 1时,n! = 1。

有许多问题利用递归来解决会十分简单,如快速排序,汉诺塔问题,图的深度优先搜索等。

其递归算法比迭代算法在逻辑上更简明。

可以看出,递归既是强有力的数学方法,也是程序设计中一个很有用的工具。

递归算法具有以下两个特征:

1. 递归算法是一种分而治之,把复杂问题分解为简单问题的问题求解方法,对求解某些复杂问题,递归分析方法是有效的。

2. 递归算法的效率较低。

为此,在求解某些问题时,希望用递归算法分析问题,用非递归算法求解具体问题。 

栈非常重要的一个应用就是在程序设计语言中用来实现递归。

3.1 消除递归的原因

(1)有利于提高算法的时空性能,因为递归执行时需要操作系统提供隐式栈来实现递归,所以效率较低。

(2)无应用递归语句的语言设施环境条件,有些计算机语言不支持递归功能,如FORTRAN语言中无递归机制。

(3)递归算法中频繁的函数调用不利于调试与观察。

3.2 递归过程的实现

递归进层(i→i+1层)时系统需要做三件事:

(1)保留本层参数与返回地址。

(2)为被调用函数的局部变量分配存储区,给下层参数赋值。

(3)将程序转移到被调用函数的入口。

而从被调用函数返回调用函数之前,递归退层(i←i+1层)时系统也应完成三件事:

(1)保存被调用函数的计算结果。

(2)释放被调用函数的数据区,恢复上层参数。

(3)依照被调用函数保存的返回地址,将控制转移回调用函数。

当递归函数调用时,应按照“后调用先返回”的原则处理调用过程,因此上述函数之间的信息传递和控制转移必须通过栈来实现。

系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,而每当从一个函数退出时,就释放它的存储区。

显然,当前正在运行的函数的数据区必在栈顶。

一个递归函数的运行过程中,调用函数和被调用函数是同一个函数,因此,与每次调用相关的一个重要概念就是递归函数运行的“层次”。

假设调用该递归函数的主函数为第0层,则从主函数调用递归函数为进入第1层……从第i层递归调用该函数为进入其“下一层”,即第i+1层;反之,退出第i层递归应返回至其“上一层”,即第i -1层。

为了保证递归函数正确执行,系统需设立一个递归工作栈,作为整个递归函数运行期间使月用的数据存储区。

每层递归所需信息构成一个工作记录,其中包括所有实在参数,所有局部变量以及上一层返回地址。

每进入一层递归,就产生一个新的工作记录压入栈顶;每退出一层递归,就从栈顶弹出一个工作记录。

因此当前执行层的工作记录必为递归工作栈栈顶的工作记录,该记录称为内活动记录,指示活动记录的栈顶指针称为当前环境指针。

由于递归工作栈是由系统来管理的,无须页用户操心,所以用递归法编制程序非常方便。

在理解了递归的机制之后,我们就可以尝试将一些递归算法改写为非递归算法。

接下来,我们以二叉树的遍历算法为例。

3.3 二叉树的遍历算法

3.3.1 递归算法

先序遍历:

//先序遍历的递归算法
void PreOrder1(BTNode* b)
{
    if(b == NULL)
    return;

    //访问根结点
    printf("%d ", b->_data);
    //访问左子树
    PreOrder1(b->_left);
    //访问右子树
    PreOrder1(b->_right);
}

中序遍历:

//中序遍历的递归算法
void MidOrder1(BTNode* b)
{
    if(b == NULL)
    return;

    //访问左子树
    MidOrder1(b->_left);
    //访问根结点
    printf("%d ", b->_data);
    //访问右子树
    MidOrder1(b->_right);
}

后序遍历:

//后序遍历的递归算法
void AftOrder1(BTNode* b)
{
    if(b == NULL)
    return;

    //访问左子树
    AftOrder1(b->_left);
    //访问右子树
    AftOrder1(b->_right);
    //访问根结点
    printf("%d ", b->_data);
}
3.3.2 非递归算法

先序遍历:

//先序遍历的非递归算法1
void PreOrder2(BTNode* b)
{
    Stack st;
    STInit(&st);
    BTNode* p = b;
    if(b != NULL)
    {
        STPush(&st, p);//根结点入栈
        while(!STEmpty(&st))
        {
            p = STTop(&st);
            printf("%d ", p->_data);
            STPop(&st);

            if(p->_right != NULL)
            STPush(&st, p->_right);
            if(p->_left != NULL)
            STPush(&st, p->_left);
        }
    }
    STDestroy(&st);
}

//先序遍历的非递归算法2
void PreOrder3(BTNode* b)
{
    Stack st;
    STInit(&st);
    BTNode* p = b;
    while(!STEmpty(&st) || p != NULL)
    {
        //访问根结点并依次访问左孩子
        while(p != NULL)
        {
            STPush(&st, p);
            printf("%d ", p->_data);
            p = p->_left;
        }
        //退回上一层,找右孩子
        if(!STEmpty(&st))
        {
            p = STTop(&st);
            STPop(&st);
            p = p->_right;
        }
    }
    
    STDestroy(&st);
}

中序遍历:

//中序遍历的非递归算法
void MidOrder2(BTNode* b)
{
    Stack st;
    STInit(&st);
    BTNode* p = b;
    while(!STEmpty(&st) || p != NULL)
    {
        //将p及左孩子依次入栈
        while(p != NULL)
        {
            STPush(&st, p);
            p = p->_left;
        }
        //退回上一层并访问根结点,找右孩子
        if(!STEmpty(&st))
        {
            p = STTop(&st);
            STPop(&st);
            printf("%d ", p->_data);
            p = p->_right;
        }
    }
    STDestroy(&st);
}

后序遍历:

//后序遍历的非递归算法
void AftOrder2(BTNode* b)
{
    Stack st;
    STInit(&st);
    BTNode* p = b;
    BTNode* asked = NULL;//指向刚刚访问过的结点
    bool flag = true;//为真表示正在处理栈顶结点
    do
    {
        //p及左孩子依次进栈
        while(p != NULL)
        {
            STPush(&st, p);
            p = p->_left;
        }
        asked = NULL;
        flag = true;

        while(!STEmpty(&st) && flag)
        {
            p = STTop(&st);
            if(p->_right == asked)//右孩子刚被访问过或者为空
            {
                printf("%d ", p->_data);
                STPop(&st);
                asked = p;
            }
            else
            {
                p = p->_right;
                flag = false;
            }
        }
    } while (!STEmpty(&st));
    
    STDestroy(&st);
}
3.3.3 先序遍历的非递归算法解读

先序遍历2与中序遍历类似,只是访问的时机不同,而后序遍历的非递归算法较为麻烦,这里不做过多解释。

先序遍历1:

//先序遍历的非递归算法1
void PreOrder2(BTNode* b)
{
    Stack st;
    STInit(&st);
    BTNode* p = b;
    if(b != NULL)
    {
        STPush(&st, p);//根结点入栈
        while(!STEmpty(&st))
        {
            p = STTop(&st);
            //访问
            printf("%d ", p->_data);
            STPop(&st);

            if(p->_right != NULL)
            STPush(&st, p->_right);
            if(p->_left != NULL)
            STPush(&st, p->_left);
        }
    }
    STDestroy(&st);
}

当b不为空时,我们首先让根结点入栈。

每次循环,我们都先访问根结点(当前栈顶元素),然后将右孩子与左孩子分别入栈(后进先出,要先访问左子树就要先入右孩子)。

该种算法的思路与递归算法十分类似,然而,解决问题的路径却不相同。

在递归算法中,左子树被全部访问完之后,负责访问右子树的函数才会入栈;而在非递归的算法中,由于语言的限制,我们必须在一次循环中就将左右孩子都入栈,但是依靠栈后进先出的特点,我们可以通过先入右孩子再入左孩子的方式来保证左孩子一定比右孩子先入栈。

这样的思路并不对所有情况成立,比如,这样的思路就很难解决中序遍历。

为此,我们用适用于先序遍历和中序遍历的思路写了先序遍历2算法。

先序遍历2:

//先序遍历的非递归算法2
void PreOrder3(BTNode* b)
{
    Stack st;
    STInit(&st);
    BTNode* p = b;
    while(!STEmpty(&st) || p != NULL)
    {
        //访问根结点并依次访问左孩子
        while(p != NULL)
        {
            STPush(&st, p);
            //访问
            printf("%d ", p->_data);
            p = p->_left;
        }
        //退回上一层,找右孩子
        if(!STEmpty(&st))
        {
            p = STTop(&st);
            STPop(&st);
            p = p->_right;
        }
    }
    
    STDestroy(&st);
}

这种算法的思路就是模拟函数调用的顺序来访问结点。

首先将左孩子(包括根结点)依次入栈并访问,遇到某结点左子树为空,则通过退回上一层(出栈)的方式找到该结点,并将访问的方向转到其右子树。

这种思路解决问题的路径就与递归算法完全一样,若要进行中序遍历,只需要改变访问的时机,具体参考上一模块的有关代码。

3.4 总结

递归的思路十分地巧妙,有利于我们分析与解决十分困难的问题,但其算法本身存在效率低下的问题,所以我们希望通过非递归的方式来实现递归解决问题的思路。

当递归函数调用时,应按照“后调用先返回”的原则处理调用过程,因此栈成为了解决的一问题的不二人选。

通过栈来实现递归,并没有特定的套路,需要在理解递归机制的基础上进行分析,解决问题的路径可能与递归算法相同也可能不同。

由于语言的限制,在解决某些特定的需求时,可能需要完善一些较为复杂的细节(后序遍历的非递归算法)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/610684.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

教你解决PUBG绝地求生登不进去 无法进入游戏 启动很慢的问题

尽管《绝地求生》(PUBG)以它那扣人心弦的战术竞技和逼真模拟的战场氛围风靡全球,揽获无数玩家的喜爱,但一些玩家在经历了一场血脉喷张的生存较量后,却不得不面对一个不那么愉悦的后续:游戏在结算阶段后出现…

docker学习笔记(五):harbor仓库搭建与简单应用

harbor私有仓库 简介 Docker容器应用的开发和运行离不开可靠的镜像管理,虽然Docker官方也提供了公共的镜像仓库,但是从安全和效率等方面考虑,部署私有环境内的Registry也是非常必要的。Harbor是由VMware公司开源的企业级的Docker Registry管…

文献速递:深度学习医学影像心脏疾病检测与诊断--基于迁移学习的生成对抗网络用于静态和动态心脏PET的衰减校正

Title 题目 Transfer learning‑based attenuation correction for static and dynamic cardiac PET using a generative adversarial network 基于迁移学习的生成对抗网络用于静态和动态心脏PET的衰减校正 01 文献速递介绍 心脏正电子发射断层扫描(PET&#xf…

JAVA入门1.1.0

前言: 不一样的编程——基于两个大前提,语言随便选一个,作者选java和c,在后续的内容会有c和java的共同使用 第一大前提:编程语言起源于语言 第二大前提:计算机理解不了语言的含义 这两大前提构成了不一样的…

Word设置代码块格式

前言 Word中无法像Markdown和LaTeX一样插入代码块,若要在Word中插入代码块可以手动设置代码块格式或自动粘贴代码块格式。若不追求完美高亮效果,可使用前者方案;若追求完美的高亮效果,可使用后者方案。下文介绍这2种方案。 手动…

渲染农场评测:6大热门云渲染平台全面比较

在3D行业中,选择一个合适的云渲染平台可能会令许多专业人士感到难以抉择。为此,我们精心准备了6家流行云渲染平台的详尽评测,旨在为您的决策过程提供实用的参考和支持。 目前,市面上主要的3D网络渲染平台包括六大服务商&#xff0…

SQL编程

用户变量的语法使用 #MySQL变量的定义与使用 #一、标识符命名规范 #1、字母加数字,但不允许使用数字开头 #2、不允许使用关键字或保留字 #3、符号只可以使用“_”或“$" #二、变量的声明 #set用于声明变量,update声明修改的表,set是声明…

OpenGL入门第三步:矩阵变换、坐标系统

1、矩阵变换 这里矩阵变换,使用4*4的矩阵,既可以表示位移,也可以表示缩放。 原因: 添加4维矩阵变量 initializeGL()函数:在着色器里面添加变换矩阵,改变坐标位置 设计一个随时间变换 ,所有重写TimerEvent 调用update触发paintGL()函数: 2、坐标系统

NSSCTF中的web学习(md5())

目录 MD5的学习 [BJDCTF 2020]easy_md5 [LitCTF 2023]Follow me and hack me [LitCTF 2023]Ping [SWPUCTF 2021 新生赛]easyupload3.0 [NSSCTF 2022 Spring Recruit]babyphp MD5的学习 md5()函数: md5($a):返回a字符串的散列值 md5($a,TRUE)&…

C#中字典Dictionary与自定义类型CustomType之间的转换

C#中字典Dictionary与自定义类型CustomType之间的转换 思路: 可以使用反射System.Reflection来获取类的具体属性, 属性名称就映射字典的键Key。 新建控制台程序DictionaryCustomClassConversionDemo 第一步、新建关键转换类ConversionUtil。 类Con…

GB2312发码测试

编码表:https://www.toolhelper.cn/Encoding/GB2312 GB2312 编码表 GB 2312 标准由中国国家标准总局 1980 年发布,GB 即国标,共收录 6763 个汉字,其中一级汉字 3755 个,二级汉字 3008 个。 GB 2312 的出现&#xff0…

领导关怀 | 西湖区委常委韩斌等一行领导调研创邻科技

5月8日,西湖区委常委韩斌、紫金港科技城管委会党委委员、副主任陈杰、西湖区委办副主任韩鹰等一行领导走访创邻科技,关心企业发展近况。创邻科技COO吴菁、CTO周研陪同参观交流。 韩斌常委一行首先参观了企业荣誉展示区和办公区。参观过程中,…

3D Gaussian Splatting for Real-Time Radiance Field Rendering 论文阅读

如此热门的项目,网络上有很多大牛分析了这篇文章的做法,在这里简单记录一下个人粗浅的理解。 关于各种数学表达式的推导,论文和参考资料中都提供了较为详细的解读,本人能力有限,这一部分理解不够深刻,先不做…

Git 如何管理标签命令(tag)

1.查看本地仓库tag --1.查看本地仓库tag UserDESKTOP-2NRT2ST MINGW64 /e/GITROOT/STARiBOSS/STARiBOSS-5GCA (gw_frontend_master) $ git tag 1stBossUpgrade V10.0.1_20220224_test V10.0.1_20220301_test tag-gwfrontend-V1.0.12-230625 tag-gw_frontend-23.08.29 tag-gw_f…

【python基础】python经典题目100题

文章目录 前言初阶题目1.字符串2.列表3.元组4.字典5.运算6.random 模块7.open函数8.time模块时间9.其他 进阶题目 前言 本文主要是python经典题目100题,适合入门的新手。仅作自己学习的记录。 初阶题目 1.字符串 题目1:怎么找出序列中的最⼤最⼩值&am…

360极速浏览器X全新Chromium内核极致顺滑,绿色便携版 v22.3.1002.64

01 软件介绍 360极速浏览器X是一款基于Chromium 95的高级双核浏览器,支持IE内核,并优化了用户体验与性能。包括无广告弹窗,新增的阅读模式,个性化标签页壁纸,以及专业导航功能,旨在提供更快、更高效的浏览…

Ubuntu意外断电vmdk损坏--打不开磁盘“***.vmdk”或它所依赖的某个快照磁盘。

背景:电脑资源管理器崩溃卡死,强行断电重启,结果虚拟机打不开了,提示打不开磁盘“***.vmdk”或它所依赖的某个快照磁盘。 删除lck文件:失败vmware-vdiskmanager修复 :提示无法修复最终用 VMFS Recovery挂载…

找不到vcruntime140_1.dll怎么办,介绍5种简单有效的解决方法

当您的电脑系统提示找不到vcruntime140_1.dll文件时,这通常意味着系统在尝试运行某个应用程序或游戏时,无法定位到这个至关重要的动态链接库(DLL)文件。此情况可能源于几个不同的原因,包括但不限于:文件被误…

中信证券:量子产业蓄势待发,看好相关投资机会!

在1994年,数学家彼得肖尔(Peter Shor)首次提出了现在广为人知的肖尔算法,那时许多人认为量子计算机的概念遥不可及、纯属幻想。然而,到了2024年,全球正深入探讨量子科技在现实世界的应用,以及所…

智启未来:富唯智能AI-ICDP引领的可重构柔性装配产线

在全球制造业竞争日益激烈的今天,如何快速响应市场变化、提高生产效率、降低生产成本,成为了企业面临的重要挑战。随着产品个性化时代的到来,装配产品频繁变换,多品种小批量的生产模式逐渐成为主流。在这一背景下,富唯…
最新文章