栈与递归的实现

news2024/11/25 6:54:49

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.coloradmin.cn/o/1660390.html

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

相关文章

数组排序和去重 巨坑!!!紧急避雷

首先&#xff0c;我们要先知道数组的排序和去重操作怎么使用。此处采用C语言。 1.排序操作 #include<iostream> #include <vector> #include<algorithm>using namespace std; int main() {vector<int>arr { 9,1,7,8,6,7,3,6,3 };sort(arr.begin(), a…

加密 加签

加密&#xff1a;一种通过将数据转换成不可读形式的方法&#xff0c;以防止未授权的访问 加签&#xff1a;侧重于验证数据的完整性和来源的真实性&#xff0c;确保数据未被篡改且来源可靠 加密和加签的区别 加密加签目的保护数据的机密性验证数据的完整性和来源的真实性使用方…

windows连接CentOS数据库或Tomcat报错,IP通的,端口正常监听

错误信息 数据库错误&#xff1a; ERROR 2003 (HY000): Cant connect to MySQL server on x.x.x.x (10060) Tomcat访问错误&#xff1a; 响应时间过长 ERR_CONNECTION_TIMED_OUT 基础排查工作 【以下以3306端口为例&#xff0c;对于8080端口来说操作是一样的&#xff0c;只需…

界面组件DevExpress Blazor UI v23.2新版亮点:图表组件全新升级

DevExpress Blazor UI组件使用了C#为Blazor Server和Blazor WebAssembly创建高影响力的用户体验&#xff0c;这个UI自建库提供了一套全面的原生Blazor UI组件&#xff08;包括Pivot Grid、调度程序、图表、数据编辑器和报表等&#xff09;。 DevExpress Blazor控件目前已经升级…

【xxl-job | 第二篇】Windows源码安装xxl-job

文章目录 2.Windows源码安装xxl-Job2.1拉取源码2.2IDEA导入2.3初始数据库数据2.4修改properties配置2.5启动admin并进入任务管理后台2.6jar包运行&#xff08;部署到Linux服务器上&#xff09;2.6.1打包2.6.2在xxl-job-admin打开jar包目录2.6.3cmd运行jar包 2.Windows源码安装x…

【快手秘籍】24小时无人播剧狂赚法门!日干500+的秘密武器,合规安全无顾虑

快手无人播剧&#xff0c;之前很火启面打压了一段时间&#xff0c;最近政策又放松了&#xff0c;因为播电视电影这个板块在快手流量很大。 所以官方又放松了&#xff0c;作为普通人想在快手赚取一份收益&#xff0c;这个赛道最合适了只要花点电费&#xff0c;无其他成本。 快…

网络基础-华为VRP基础CLI操作

基本命令模式 华为设备的命令行模式包括用户视图和特权级模式。 用户视图&#xff08;User View&#xff09;&#xff1a;这是用户登录到华为设备时默认进入的模式。在用户视图下&#xff0c;用户可以执行一些基本的查看命令&#xff0c;但不能进行设备配置或管理。提示符通常…

Flutter笔记:Widgets Easier组件库(13)- 使用底部弹窗

Flutter笔记 Widgets Easier组件库&#xff08;13&#xff09;使用底部弹窗 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this …

设计软件有哪些?渲染软件篇(3),渲染100邀请码1a12

今天我们继续介绍几款渲染软件&#xff0c;方便大家了解 1、渲染100(http://www.xuanran100.com/?ycode1a12) 渲染100是网渲平台&#xff0c;为设计师提供高性能的渲染服务。通过它设计师可以把本地渲染移到云端进行&#xff0c;速度快价格便宜&#xff0c;支持3dmax、vray、…

推荐 6 个超好用的 iterm2 zsh 插件

大家好啊&#xff0c;今天给大家分享几个我日常使用的 iterm2 插件&#xff0c;每一个都很有用&#xff0c;希望能给帮助你提高使用命令行的效率&#xff5e; zsh-autosuggestions 插件地址&#xff1a;https://github.com/zsh-users/zsh-autosuggestions 效果展示 当你输入…

YOLOv9改进策略 | 添加注意力篇 | 利用YOLO-Face提出的SEAM注意力机制优化物体遮挡检测(附代码 + 修改教程)

一、本文介绍 本文给大家带来的改进机制是由YOLO-Face提出能够改善物体遮挡检测的注意力机制SEAM&#xff0c;SEAM&#xff08;Spatially Enhanced Attention Module&#xff09;注意力网络模块旨在补偿被遮挡面部的响应损失&#xff0c;通过增强未遮挡面部的响应来实现这一目…

MySQL表的增删查改【基础部分】

数据表的操作 新增 普通插入 insert into 表名 values(值,值...)注意&#xff1a; 此处的值要和表中的列相匹配 使用’‘单引号或者”“双引号来表示字符串 mysql> insert into student values(123,zhangsan); Query OK, 1 row affected (0.02 sec)指定列插入 insert …

【Sql-02】 求每个省份最新登陆的三条数据

SQL 输出要求数据准备sql查询结果 输出要求 要求输出&#xff0c;userid_1,logtime_1,userid_2,logtime_2,userid_3,logtime_3 数据准备 CREATE TABLE sqltest (province varchar(32) NOT NULL,userid varchar(250) DEFAULT NULL,logtime datetime ) ENGINEInnoDB DEFAULT C…

C#开发的网络速度计 - 开源研究系列文章 - 个人小作品

上次发布了一个获取网络速度的例子( https://www.cnblogs.com/lzhdim/p/18167854 )&#xff0c;就是为了这次这个例子。用于在托盘里显示网络速度的图标&#xff0c;并且能够显示网络速度。下面就介绍一下这个小应用的源码。 1、 项目目录&#xff1b; 2、 源码介绍&#xff1b…

【SpringBoot整合系列】SpringBoot整合RabbitMQ-基本使用

目录 SpringtBoot整合RabbitMQ1.依赖2.配置RabbitMQ的7种模式1.简单模式&#xff08;Hello World&#xff09;应用场景代码示例 2.工作队列模式&#xff08;Work queues&#xff09;应用场景代码示例手动 ack代码示例 3.订阅模式&#xff08;Publish/Subscribe&#xff09;应用…

ICode国际青少年编程竞赛- Python-2级训练场-识别循环规律2

ICode国际青少年编程竞赛- Python-2级训练场-识别循环规律2 1、 for i in range(3):Dev.step(3)Dev.turnRight()Dev.step(4)Dev.turnLeft()2、 for i in range(3):Spaceship.step(3)Spaceship.turnRight()Spaceship.step(1)3、 Dev.turnLeft() Dev.step(Dev.x - Item[1].…

国家软考办:2024年上半年软考考试安排

按照《2024年计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试工作安排及有关事项的通知》&#xff08;计考办〔2024〕1号&#xff09;文件精神&#xff0c;结合各地机位实际&#xff0c;现将2024年上半年计算机软件资格考试有关安排通告如下&#xff1a; 一、考…

小众行业风口:Q1季度擦窗机器人行业线上市场销售数据分析

今天给大家分享一个2024年的小众行业增长风口——擦窗机器人。 作为家居自动化里的重要一员&#xff0c;擦窗机器人可以简称为擦窗神器&#xff0c;是为了解决大户型家庭的外窗清洁痛点而存在。而目前&#xff0c;擦窗机器人行业正在走向成熟&#xff0c;且市场需求量居高不下…

寻找志同道合的小伙伴,让生活更加多彩

在繁忙的生活中&#xff0c;我们时常渴望找到一个可以倾诉心声、分享喜悦和烦恼的角落。有时候&#xff0c;一个简单的聊天就能让心情变得豁然开朗。而今天&#xff0c;我想向大家介绍一个可以让生活更加多彩的小天地——那是一个充满活力和温暖的QQ群。 群号&#xff1a;78004…

二、使用插件一键安装HybridCLR

预告 本专栏将介绍如何使用这个支持热更的AR开发插件&#xff0c;快速地开发AR应用。 专栏&#xff1a; Unity开发AR系列 插件简介 通过热更技术实现动态地加载AR场景&#xff0c;简化了AR开发流程&#xff0c;让用户可更多地关注Unity场景内容的制作。 热更方案 基于Hybri…