栈与递归的实现

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.mzph.cn/news/836875.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

光纤VS紫外:如何选择最适合您生产线的激光打标机?

光纤激光打标机和紫外激光打标机在制造业中都有其独特的应用,但两者在原理、特点和应用范围上存在一些差异。 光纤激光打标机是一种采用光纤输出激光,并通过高速扫描振镜系统实现打标功能的新一代激光打标机系统。它电光转换效率高,达到30%以…

C++进阶:AVL树详解及模拟实现(图示讲解旋转过程)

C进阶:AVL树详解及模拟实现(图示讲解旋转过程) 之前在搜索二叉树最后早就埋下伏笔,来介绍AVL树和红黑树,今天就先来第一个吧 文章目录 1.AVL树介绍1.1概念介绍1.2核心性质 2.项目文件规划3.整体框架(节点和…

JDK 1.8 HashMap扩容机制

我们首先来看利用无参构造函数创建HashMap如何扩容。首先创建一个无参构造出来的hashmap HashMap hashMap new HashMap();该构造函数源码如下: public HashMap() {this.loadFactor DEFAULT_LOAD_FACTOR; // all other fields defaulted}此时,该构造函…

linux笔记5--shell命令2

文章目录 一. linux中的任务管理1. 图形界面2. 命令① top命令② grep命令③ ps命令补充: ④ kill命令图形界面杀死进程 二. 挂载(硬盘方面最重要的一个知识点)1. 什么是挂载2. 关于挂载目录① Windows② linux查看硬件分区情况(/dev下):更改挂载目录结束…

揭秘 HTTP 代理:增强在线活动的安全性和匿名性

HTTP 代理在保护您的在线隐私、增强安全性以及允许访问受限内容方面发挥着关键作用。了解 HTTP 代理的工作原理以及如何有效地利用它们可以让您掌控自己的在线状态和浏览体验。让我们深入研究 HTTP 代理的世界,探索它们的优势、应用程序以及最大化其效用的最佳实践。…

【Unity Shader入门精要 第6章】基础光照(二)

1. 获取环境光 unity shader中可以通过 UNITY_LIGHTMODEL_AMBIENT获取当前环境光颜色信息。 fixed4 frag(v2f i) : SV_Target {return UNITY_LIGHTMODEL_AMBIENT; }2. 漫反射 2.1 兰伯特模型 创建Chapter_6_Diffuse_Lambert作为测试材质创建Chapter_6_Diffuse_Lambert作为测…

ollama api只能局域网访问,该怎么破?

安装ollama: ollama离线安装,在CPU运行它所支持的那些量化的模型-CSDN博客文章浏览阅读178次,点赞2次,收藏6次。ollama离线安装,在CPU运行它所支持的哪些量化的模型 总共需要修改两个点,第一:Ollama下载地址;第二:Ollama安装包存放目录。第二处修改,修改ollama安装目…

洛谷官方提单——【入门4】数组——python

洛谷官方提单——【入门4】数组 小鱼比可爱题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示代码 小鱼的数字游戏题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示数据规模与约定 代码 【深基5.例3】冰雹猜想题目描述输入格式输出格式样例 #1样例输入 …

matlab打开文件对话框

在使用matlab GUI制作时,为了便于用户交互使用,经常设置文件打开对话框,让用户根据实际需要选择打开的文件。下面以打开一张图片为例,matlab代码如下: [temp_filepath,temp_filename]uigetfile(*.jpg,请选择要打开的图…

从心理学角度看,GPT 对人有什么影响?

开启个性化AI体验:深入了解GPT的无限可能 导言 GPT 与我们日常生活的融合标志着技术进步的重大飞跃,为提高效率和创新提供了前所未有的机遇。然而,当我们与这些智能系统日益紧密地交织在一起时,探索它们对个人产生的细微的心理影响…

电子杂志制作攻略,轻松打造高质量数字出版物

随着数字科技的飞速发展,电子杂志作为一种新型的数字出版物,已经越来越受到人们的青睐。它不仅具有丰富的内容、多样的形式,还具有便捷的传播和阅读方式。如今,电子杂志已经逐渐成为企业、媒体和个人展示自身品牌、传播信息的重要…

下载驱动包提示 通常不会下载 未验证的下载

打开设置 添加允许站点 如果还是下载不了,那只能换资源了

发那科数控机床远程监控数据上云

发那科数控机床远程监控数据上云 在当今数字化、网络化的工业生产环境中,发那科数控机床作为全球领先的高端制造设备之一,其远程监控数据上云已成为实现智能制造、提升生产效率和优化资源管理的重要手段。本文将深入探讨发那科数控机床远程监控数据上云…

ACE框架学习4

目录 ACE Proactor框架 异步I/O工厂类 ACE_Handler类 前摄式Acceptor-Connector类 ACE_Proactor类 ACE Streams框架 ACE_Model类 ACE_Streams类 ACE Proactor框架 ACE Proactor框架实现了proactor模式,也就是异步网络模式,允许事件驱动…

axios异步操作第一篇

1 同步请求和异步请求 客户端给java后台程序发送请求,发送请求的方式有两种: 同步请求 同步请求发送方式: 1 浏览器地址栏,输入url:http://localhost:8080/web-app/xxxServlet 2 3 删除 4 javascript:location.hr…

运营商的mpls专线

在当今高速发展的数字化时代,网络已成为企业发展不可或缺的基础设施。作为企业网络 连接的重要组成部分,MPLS专线在运营商的推动下逐渐成为了企业选择的首选。 MPLS(Multi-Protocol Label Switching)是一种基于标签的交换技术&am…

探索渲染农场的高性能奥秘

在当今数字化的时代,渲染农场正逐渐成为许多行业不可或缺的强大工具。那么,为什么我们说渲染农场是高性能的计算机系统呢?让我们深入剖析其中关键要点。 “渲染农场”拥有大规模的计算资源。它由众多高性能的计算机节点组成,这些…

理解伽马分布

伽马分布 关键词:Gamma Distribution 文章目录 一、说明二、Gamma 分布的基础概念2.1 Gamma 分布的物理意义2.2 对比泊松与伽马分布2.3 伽马分布参数 三、具有伽马分布的高效牙科调度四、后记 一、说明 在本文中,我们将探讨统计学中的基本概率分布之一…

【C++】命名空间、缺省参数、函数重载、引用

文章目录 1.认识命名空间2.命名空间的使用3.C的输入和输出4.缺省参数4.1缺省参数的概念4.2缺省参数的分类 5.函数重载6.引用6.1引用的概念6.2引用的特性6.3常引用(重点题目)6.4引用和指针的区别 1.认识命名空间 C总计63个关键字,C语言32个关键字 下面让我们学习一…

周进院长受邀出席2024第四届屈光手术国际论坛获多项荣誉称号!

周进院长受邀出席2024第四届屈光手术国际论坛获“全国首批EVOICL(V5)新技术临床应用专家”等多项荣誉称号! 5月10-12日,由爱尔眼科医院集团主办、长沙爱尔眼科医院协办的2024第四届屈光手术国际论坛(IRSS 2024&#x…