【基础算法】递归算法

news2024/12/22 17:17:24

递归算法是一种直接或间接调用原算法的算法,一个使用函数自身给出定义的函数被称为递归函数。利用递归算法可以将规模庞大的问题拆分成规模较小的问题,从而使问题简化。无论是递归算法还是递归函数,最大的特点都是“自己调用自己”。

斐波那契数列

斐波那契数列的规律是:第一项是1,第二项是1,以后每一项都等于前两项之和。我们的问题是:斐波那契数列的第n项是多少?


F ( n ) = { 1 n = 1 1 n = 2 F ( n − 1 ) + F ( n − 2 ) n ≥ 3 F(n)=\begin{cases} 1\quad n=1\\ 1\quad n=2\\ F(n-1)+F(n-2)\quad n\geq3 \end{cases} F(n)= 1n=11n=2F(n1)+F(n2)n3
在计算斐波那契数列的第nF(n)时,首先需要得到F(n-1)F(n-2)的值,而F(n-1)F(n-2)也可以通过这个公式计算,所以斐波那契数列具有递归特性,可以使用递归算法计算出数列第n项的值。

#include<iostream>

int fibonacci(int n) {
	if (n == 1 || n == 2) {
		return 1;
	}
	else {
		return fibonacci(n - 1) + fibonacci(n - 2);
	}
}
int main() {
	//检验结果
	std::cout << fibonacci(10);
}

在使用递归算法的时候需要注意:

  • 每个递归函数都必须有一个非递归定义的初始值作为递归结束标志。就像上述fibonacci()函数,当n==1||n==2时函数返回1,不再调用自己。如果一个递归函数中没有定义非递归的初始值,那么该递归调用是无法结束的,也就得不到结果。
  • 递归算法解决的问题需要具有递归特性,就像上述fibonacci()函数,fibonacci(n)的值可以通过fibonacci(n-1)fibonacci(n-2)的值相加得到,其本质就是一种反复调用自身的过程。
  • 虽然通过递归算法结构简单,已于理解和实现,但是由于需要反复调用自身,所以运行效率较低,时间复杂度和空间复杂度较高,在使用时应考虑效率和性能问题。

数组的全排列


编写一个程序,将数组中的元素进行全排列,并输出每一种排列方式。例如数组中的元素为{1,3,5},那么程序可以输出该数组元素的6种排列方式,分别为{1,3,5},{1,5,3},{3,1,5},{3,5,1},{5,1,3},{5,3,1}


解决数组全排列问题最经典的方法是递归算法,因为数组的全排列问题具有很明显的递归特性。可以将数组全排列问题形式化定义为以下模型:
设数组 R R R包含 n n n个元素,定义符号 R i = R − r i R_i=R-{r_i} Ri=Rri R i R_i Ri表示原数组 R R R去掉元素 r i r_i ri后的新数组。数组 R R R的全排列 P e r m ( R ) Perm(R) Perm(R)可定义如下:

  • n==1时, P e r m ( R ) = { r } Perm(R)=\{r\} Perm(R)={r},其中 r r r为数组 R R R中的唯一元素。
  • n>1时, P e r m ( R ) Perm(R) Perm(R)由全排列 ( r 1 ) P e r m ( R 1 ) (r_1)Perm(R_1) (r1)Perm(R1) , ( r 2 ) P e r m ( R 2 ) (r_2)Perm(R_2) (r2)Perm(R2), ( r n ) P e r m ( R n ) (r_n)Perm(R_n) (rn)Perm(Rn) 构成。

很显然,上面全排列的定义具有递归特性,依此递归定义可以设计出如下递归算法:

void perm(vector<int> vec, vector<int> tmpResult, vector<vector<int>>& result) {
	if (vec.size() == 1) {
		tmpResult.push_back(vec[0]);
		result.push_back(tmpResult);
	}
	else {
		for (int i = 0; i < vec.size(); i++) {
			tmpResult.push_back(vec[i]);
			vec.erase(vec.begin() + i);
			perm(vec, tmpResult, result);
			vec.insert(vec.begin() + i, tmpResult.back());
			//恢复到迭代前的状态
			tmpResult.pop_back();
		}
	}
}

第一个if语句即是递归的结束条件,当待排序数组只剩一个元素时,直接插入到临时结果数组中,然后将临时结果添加到结果数组中。
如果不满足if语句,则说明需要继续排列。使用循环取出当前数组的每一个元素,添加到临时结果数组中:

每次递归调用只修改原数组中的一个数据,在调用完perm()后需要将数组恢复到迭代前的状态。
上面的代码易于理解,但使用了至少三个vector容器,空间复杂度较高。
理解原理和代码之后,我们就可以做出简化,通过调整指针位置,使用纯数组进行解决:

#include<stdio.h>
void swap(int* a, int* b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//数组指针,开始下标,结束下标
void permutation(int* arr, int start, int end) {
	if (start == end) {
		for (int i = 0; i <= end; i++) {
			printf("%d ", arr[i]);
		}
		puts("");
	}
	else {
		for (int i = start; i <= end; i++) {
			//交换起始元素和当前元素
			swap(arr + start, arr + i);
			//递归生成后续元素的排列
			permutation(arr, start + 1, end);
			//恢复到迭代前的状态
			swap(arr + start, arr + i);
		}
	}
}
int main() {
	int a[] = { 1,3,5,7 };
	permutation(a, 0, 3);
	return 0;
}

这种算法的本质还是将数组的每个元素取出压入结果数组,对剩余元素重复“取出-压入-重复”的操作。
结果数组与原数组共用内存空间,通过指针位置调整边界。
如果文件后缀名为.cpp,则默认使用C++编译器,不能在函数内使用sizeof(arr)/sizeof(arr[0])的方法获取数组大小,sizeof(arr)得到的是指针大小。此时,函数参数中的end不能省略。
如果使用C++的方法实现全排列,除了上面两种方法,还可以使用C++封装好的标准库函数next_permutation

#include<iostream>
#include<vector>
#include<algorithm>
int main() {
	std::vector<int> vec = { 1,3,5 };
	do {
		for (auto it = vec.begin(); it != vec.end(); it++) {
			std::cout << *it << " ";
		}
		std::cout << std::endl;
	} while (std::next_permutation(vec.begin(), vec.end()));
	return 0;
}

使用标准库有以下几个需要注意的地方:

  • next_permutationalgorithm头文件下,使用时需要包含此头文件,已及所使用的STL头文件。
  • 通常使用do...while结构,如果直接使用while,循环代码块内会丢失默认的排序情况。
  • 无论循环代码块内执行什么操作,退出循环之后,容器会恢复到进入循环之前的状态。

梵塔问题


有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:

  • 每次只能移动一个圆盘;
  • 大盘不能叠在小盘上面。

提示:可将圆盘临时置于 B 杆,也可将从 A 杆移出的圆盘重新移回 A 杆,但都必须遵循上述两条规则。
问:如何移?最少要移动多少次?


题目分析

梵塔问题只能用递归算法来解决。我们可以考虑移动的步骤:

  1. 将A针上的N-1个圆盘借助C针移动到B针上。
  2. 将A底部的圆盘移到C针上。
  3. 将B针上的N-1个圆盘借助A针移动到C针上。

完成这三步就可以将A针上的64个圆盘全部移到C针上,而且在移动过程中始终保持大盘在下小盘在上的顺序。关键在于第1步和第3步如何执行。

这显然成为一个新的梵塔问题,只不过这个梵塔问题的规模要小一些,从N个盘子变成N-1个盘子:

  1. 将A针上的N-1个盘子借助C针移到B针上。
  2. 将B针上的N-1个盘子借助A针移到C针上。

问题1的解决步骤如下:

  1. 将A针上的N-1-1个圆盘借助B针移动到C针上。
  2. 将A底部的倒数第二个圆盘移到C针上。
  3. 将C针上的N-1-1个圆盘借助A针移动到B针上。

问题2的解决步骤如下:

  1. 将B针上的N-1-1个圆盘借助C针移动到A针上。
  2. 将B底部的倒数第二个圆盘移到C针上。
  3. 将A针上的N-1-1个圆盘借助B针移动到C针上。

上述问题1和问题2的解决步骤中,第1步和第3步又构成了两个新的梵塔问题,只是问题的规模又缩小了一些,从N-1个盘子缩小到N-2个盘子。
这两个问题的解决方案与上面一样,仍然分三步移动圆盘不断将问题的规模缩小,直到第1步和第3步移动的盘子个数为1。这显然是一个递归问题,也就是梵塔问题中嵌套着更小规模的梵塔问题。

#include<iostream>
//要移动的盘子数量,从from借助by移动到to
void move(int n, char from, char by, char to) {
	if (n == 1) {
		std::cout << from << " to " << to<<std::endl;
	}
	else {
		//第一步,将A针上的n-1个盘子借助C针移动到B上
		move(n - 1, from, to, by);
		//第二步,将A针底部的盘子移动到C上
		std::cout<< from << " to " << to << std::endl;
		//第三步,将B针上的n-1个盘子借助A针移动到C上
		move(n - 1, by, from, to);
	}
}
int main() {
	move(3, 'A', 'B', 'C');
	return 0;
}

该函数是一个递归函数,递归结束的条件是n==1,此时只需要移动一个圆盘,无需借助by针,可以直接从from针上移到to针上。如果n!=1,则要将问题继续分解,也就是递归地调用函数move()。按照之前分析的步骤,先将A针上的N-1个圆盘借助C针移动到B针上,然后将A底部的圆盘移到C针上,最后将B针上的N-1个圆盘借助A针移动到C针上。
对于N个盘子,需要移动 2 n − 1 2^n-1 2n1次,因此上面的代码中只模拟了3个盘子的情况。

总结

递归问题求解分两个部分:

  1. 分析问题求解的步骤,如梵塔问题,按照分析得到的步骤写算法即可。
  2. 分析递归结束的条件,放到递归函数的前面,以便及时退出。

尤其是第一点,我经常会有无从下手的情况,不知道怎么写,总想一步找到一个最优解。总结这三个递归算法之后,发现其实真就按照分析的思路来,把这些步骤转换成计算机语言就可以。
递归挺费脑子的,还是得多练多总结。

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

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

相关文章

nRF52832蓝牙概述

基本概念 RSSI&#xff08;Received Signal Strength Indicator&#xff09;是接收信号的强度指示。 接收包RSSI是指无线模块发送信息后&#xff0c;接收端的无线模块接收到数据后&#xff0c;当前接收数据的信号强度的寄存器值&#xff0c;也就是接收模块获取到发送模块当前发…

Vector - CAPL - 数据库和CAPL_01

目录 获取CAN总线报文信息 静态访问报文信息 动态访问报文信息 静态访问数据库信息 DBLookup&#xff08;Access Message & Signal&#xff09; 1、报文类型信息 2、类型信息 3、节点信息 获取CAN总线报文信息 我们在做CAN网络管理或者通信的测试的过程中&#xf…

LLM prompt提示构造案例

参考&#xff1a; https://github.com/PlexPt/awesome-chatgpt-prompts-zh 吴恩达 prompt工程应用&#xff1a; https://www.bilibili.com/video/BV1No4y1t7Zn prompt构造案例代码 prompt """文本分类任务&#xff1a;将一段用户给外卖服务的评论进行分类…

LSTM已死,Transformer永生(面试问答RNN/LSTM/Transformer)

计算机视觉面试题-Transformer相关问题总结&#xff1a;https://zhuanlan.zhihu.com/p/554814230 计算机视觉面试31题 CV面试考点&#xff0c;精准详尽解析&#xff1a;https://zhuanlan.zhihu.com/p/257883797 1. 循环神经网络&#xff08;Recurrent Neural Networks, RNN&am…

渲染颜色透明度异常分析 Unity RenderTexture

解决 Unity Canvas 相机 RenderTarget 渲染颜色透明度异常的问题 问题 重现场景 新建一个 Canvas-RT , Canvas Render Mode 改为 Screen Space - Camera 。 为 Canvas-RT 添加一个 RawImage-Origin 对象&#xff0c;修改颜色透明度为 120 。 修改 Camera-RT 中的 Target Textur…

如何实现Nginx+Tomcat反向代理与负载均衡

目录 一、正向代理与反向代理 正向代理 反向代理 二、负载均衡 什么是负载均衡 代码仓库定义 分流策略 权重 least_conn&#xff08;最少连接&#xff09; ip_hash&#xff08;负载均衡模式&#xff09; fair&#xff08;第三方负载均衡模式&#xff09; url_hash&…

算法设计与分析 课程期末复习简记

目录 网络流 线性规划 回溯算法 分支限界 贪心算法 动态规划 分治算法 算法复杂度分析 相关概念 网络流 下面是本章需要掌握的知识 • 流量⽹络的相关概念 • 最⼤流的概念 • 最⼩割集合的概念 • Dinic有效算法的步骤 • 会⼿推⼀个流量⽹络的最⼤流 下面对此依次进行复…

基于AUTOSAR的ECU启动阶段哪些事儿

AUTOSAR实战篇:EcuM启动时序大总结 前言 小T出品,必是精品! EcuM启动时序大总结,你值得拥有! 正文 正如小T前文中《AUTOSAR基础篇之EcuM》中讲到的那样,AUTOSAR架构中将ECU的上下电过程统一由单一的模块来进行统一管理,即EcuM模块。 虽然不同硬件的初始化过程不一样,但…

Redis是什么

Redis是什么 https://blog.csdn.net/Little_Oranges/article/details/121870705 1.简介 基于字典格式的。基于内存&#xff0c;高性能的。键值对的方式进行存储。可以存储多种数据结构类型的数据。 2.应用场景 缓存排行榜计数器分布式会话分布式锁社交网络最新列表消息系统 …

华为OD机试真题 JavaScript 实现【查找充电设备组合】【2023Q1 100分】

目录 一、题目描述二、输入描述三、输出描述四、补充说明五、JavaScript算法源码六、效果展示1、输入2、输出3、说明一、题目描述 某个充电站,可提供n个充电设备,每个充电设备均有对应的输出功率。任意个充电设备组合的输出功率总和,均构成功率集合P的1个元素。功率集合P的…

Qt Quick系列(8)—Model-View—视图信号

&#x1f680;作者&#xff1a;CAccept &#x1f382;专栏&#xff1a;Qt Quick 文章目录 前言代码示例源码关键知识点 总结 前言 在Qt Quick的Model-View中内置视图有很多&#xff0c;有Repeater、ListView、GridView…&#xff0c;而他们也有着自己的信号&#xff0c;比如…

WebDAV之派盘本地个人云+Documents

Documents是一款由Readdle开发的文档管理和编辑工具&#xff0c;支持PDF阅读、音频播放、图像浏览和标注、以及多种常见文档格式的编辑操作等。派盘是一款本地私有云产品&#xff0c;基于WebDAV、FTP、SMB等多种协议&#xff0c;提供文件存储、分享、同步、备份等服务&#xff…

【Squid一】Squid代理服务器应用

Squid代理服务器应用 1.Squid代理服务器1.1 正向代理的工作机制1.2 代理服务器的概念及其作用1.3 代理服务器主要作用1.4 Squid代理的类型 2.CDN2.1 CDN概述2.2 CDN优势2.3 CDN对网络的优化作用2.4 CDN访问过程2.5 CDN网络的组成要素 3.安装Squid服务3.1 使用脚本启动和关闭squ…

guest内核不响应导致磁盘卸载问题排查

用户问题 客户报障磁盘卸载不了&#xff0c;而且是经常出现卸载不了的情况&#xff0c;客户比较着急&#xff0c;同时PDD也是大客户。 排查过程 查看宿主机上虚拟机信息 1、用户虚拟机有14块磁盘&#xff0c;而且这14块都是以legacypci的方式插入虚拟机&#xff0c;我印象中…

MATLAB | 拉普拉斯分布/拉普拉斯噪声的生成

一、实验目标 生成拉普拉斯分布的噪声&#xff0c;并分析它的概率密度函数 二、解决思路 &#xff08;1&#xff09;拉普拉斯分布可以由指数分布生成 拉普拉斯的概率密度函数为 f ( x ; μ , λ ) 1 2 λ e − ∣ x − μ ∣ λ f(x;\mu,\lambda)\frac{1}{2 \lambda} e^{…

创建启动前端vue与后端python/flask,前后端分离,相互传递参数

创建启动vue 确保你已经安装了Node.js和npm 安装vue npm install -g vue/cli创建vue项目&#xff1a; vue create my-project cd my-project启动vue npm run serve如果安装vue报错&#xff1a;管理员权限模式打开powershell Windows PowerShell 版权所有&#xff08;C&#…

windows怎么查看目标文件.o and windows - 如何使用/安装 GNU binutils (objdump)

GNU binutils-objdump工具 一、windows怎么查看目标文件.o二、安装GNU binutils (objdump)三、使用GNU binutils (objdump)参考资料 一、windows怎么查看目标文件.o 可以使用GNU binutils (objdump)进行查看编译生成的目标文件.o。 二、安装GNU binutils (objdump) 点击下载…

Apache Airflow 多个 Provider 存在漏洞

项目介绍 Airflow 是一个使用 python 语言编写的 data pipeline 调度和监控工作流的平台。 Airflow 是通过 DAG&#xff08;Directed acyclic graph 有向无环图&#xff09;来管理任务流程的任务调度工具&#xff0c; 不需要知道业务数据的具体内容&#xff0c;设置任务的依赖…

辅助驾驶功能开发-功能规范篇(21)-4-XP行泊一体方案功能规范

XPilot Parking 自动泊车系统 • 超级自动泊车辅助(Super AutoParking Assist)、语音控制泊车辅助(Autoparking with Speech) - 产品定义 超级自动泊车辅助是⼀个增强的自动泊车辅助系统。在超级自动泊车辅助系统中,识别车位将会变得实时可见, 并且不可泊入的⻋位也将…

zynq系列器件使用vivado配置国产内存

zynq系列器件使用vivado配置国产内存 一、镁光公司器件命名的含义二、紫光公司器件命名的含义二、国产ddr&#xff08;SCB13H8G162BF-13KI&#xff09;和镁光&#xff08;MT41K512M8-125&#xff09;ddr参数对比三、vivado参数填入 一、镁光公司器件命名的含义 以MT41K512M8-1…