Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >递归与循环的效率迷思

递归与循环的效率迷思

作者头像
用户2615200
发布于 2019-07-02 10:14:49
发布于 2019-07-02 10:14:49
1.4K00
代码可运行
举报
运行总次数:0
代码可运行

本文简单比较了一下相同逻辑下,递归实现和循环实现的效率差异

已经不记得最初是从哪里获取的信息了,自己总有一个印象是递归的效率比循环差,因为递归有很大的函数调用开销,再加上递归可能存在的堆栈溢出问题(本文暂不考虑该问题),所以书写代码时还是尽量使用循环为好.

简单举个加法的例子(求解前 n 个自然数的和):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C#

// recur version
int AddRecur(int val)
{
	if (val > 0)
	{
		return val + AddRecur(val - 1);
	}
	
	return 0;
}

// iter version
int AddIter(int val)
{
	var ret = 0;
	
	for (int i = 1; i <= val; ++i)
	{
		ret += i;
	}
	
	return ret;
}

简单 Profile 一下,发现循环版本比递归版本要快 64% 左右 ~

再举个耳熟能详的例子: 斐波那契数列

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C#

// recur version
long FibonacciRecur(long index)
{
	if (index <= 1)
	{
		return index;
	}
	else 
	{
		return FibonacciRecur(index - 1) + FibonacciRecur(index - 2);
	}
}

// iter version
long FibonacciIter(long index)
{
	if (index <= 1)
	{
		return index;
	}
	else 
	{
		long pre = 0;
	    long cur = 1;
	    long next = 0;
	    
	    for (long i = 2; i <= index; ++i)
	    {
	    	// calc next
	    	next = pre + cur;
	    	// update pre and cur
	    	pre = cur;
	    	cur = next;
	    }
	    
	    return next;
	}
}

继续 Profile 一下,发现循环版本比递归版本要快了 96% 左右 ! 不过稍有递归经验的朋友都会看出,上面的递归实现会做很多的重复计算,更好的方式就是缓存一下中间的计算结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C#

Dictionary<long, long> s_buffer = new Dictionary<long, long>();
		
long FibonacciRecur(long index)
{
	if (index <= 1)
	{
		return index;
	}
	else 
	{
		long pre = 0;
		if (!s_buffer.TryGetValue(index - 1, out pre)) 
		{
		    pre = FibonacciRecur(index - 1);
            s_buffer[index - 1] = pre;		    
		}
		
		long cur = 0;
		if (!s_buffer.TryGetValue(index - 2, out cur))
		{
		    cur = FibonacciRecur(index - 2);
            s_buffer[index - 2] = cur;		    
		}
		
		return pre + cur;
	}
}

改动之后,循环版本比递归版本就只快 64% 左右了 ~

试验到现在,似乎都印证了我之前的印象: 递归比循环慢,写代码就要写循环~

我们最后来看个真实的(也更复杂的)示例:查找指定名字的子节点(假设我们有一颗树形结构的节点树,给出根节点,查找某个指定名字的子节点)

以下是一个简易的树形结构实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C#

public class Node 
{
	string m_name;
	List<Node> m_children = new List<Node>();
	
	public Node(string name, params Node[] children)
	{
		m_name = name;
		m_children.AddRange(children);
	}
	
	public string GetName()
	{
		return m_name;
	}
	
	public int GetChildCount()
	{
		return m_children.Count;
	}
	
	public Node GetChild(int index)
	{
		if (index >= 0 && index < m_children.Count) 
		{
			return m_children[index];
		}
		
		return null;
	}
}

查找树形结构的指定节点一般可以采用 BFSDFS,这里我们使用 DFS :

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C#

Node FindChildRecur(Node parent, string name)
{
	if (parent != null)
	{	
		var childCount = parent.GetChildCount();
		for (int i = 0; i < childCount; ++i)
		{
			var child = parent.GetChild(i);
			if (child.GetName() == name) 
			{
				return child;
			}
			else 
			{
				var childNode = FindChildRecur(child, name);
				if (childNode != null)
				{
					return childNode;
				}
			}
		}
	}
	
	return null;
}

如果要将上面的递归代码改为循环代码,方法就没有之前那么简明了,需要我们自行来模拟调用栈:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C#

Stack<Node> s_stack = new Stack<Node>();
		
Node FindChildIter(Node parent, string name)
{
	if (parent != null)
	{
		s_stack.Clear();
		s_stack.Push(parent);
		
		while (s_stack.Count > 0)
		{
			var curParent = s_stack.Pop();
			
			var childCount = curParent.GetChildCount();
			for (int i = childCount - 1; i >= 0; --i)
			{
				var child = curParent.GetChild(i);
				if (child.GetName() == name)
				{
					return child;
				}
				else
				{
					if (child.GetChildCount() > 0)
					{
					    s_stack.Push(child);
					}
				}
			}
		}
	}
	
	return null;
}

考虑到递归调用的高消耗,似乎我们应该将之前的递归代码改写为这种循环形式,但是 Profile 之后发现,其实循环版本还略慢于递归版本,原因就在于(模拟)调用栈的引入抵消了(甚至超过了)函数调用的开销.

其实一般而言,栈内存的操作消耗都要小于堆内存的操作消耗,上面例子中引入的(模拟)调用栈其实就是一种堆操作,考虑到 CLR(C#) 的可能影响,我也用 C++ 进行了一样的实现对比,最终结果也是一致的,甚至在 C++ 中实现的循环版本还要显著慢于其递归版本.

还有一个问题之前没有提及,就是代码可读性问题,从我个人经验来讲,递归代码的可读性大体上还是要优于循环代码的.

结论

一般而言,将递归代码改写为循环代码可以提高效率,但是一旦改写过程中引入了堆操作,那么结果往往是相反的.

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019年06月27日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
unity3d:查找子物体,并输出路径,GetComponentsInChildren,递归查找,栈查找
分为两步: 1.找到子物体的transform 2.通过child.parent = root,输出路径
立羽
2023/08/24
1.1K0
unity3d:查找子物体,并输出路径,GetComponentsInChildren,递归查找,栈查找
爆肝整理高频js手写题请查收
发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。
helloworld1024
2022/11/01
1.2K0
Swift 中的反射 Mirror
前言 Mirror是Swift中的反射机制,对于C#和Java开发人员来说,应该很熟悉反射这个概念。反射就是可以动态的获取类型以及成员信息,同时也可以在运行时动态的调用方法和属性等。
Swift社区
2021/11/26
5.2K0
Swift 中的反射 Mirror
一步步带你读懂 CoordinatorLayout 源码
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details/71616547
程序员徐公
2018/09/17
7060
一步步带你读懂 CoordinatorLayout 源码
10w字!前端知识体系+大厂面试总结(算法篇)
但是在两个月的算法练习中,第一次体会到编程不仅仅是技术,还是艺术,感受到了编程是一件很酷的事情
winty
2023/01/09
5270
10w字!前端知识体系+大厂面试总结(算法篇)
文心一言 VS 讯飞星火 VS chatgpt (256)-- 算法导论18.3 2题
首先,我会提供 B-Tree 删除操作的伪代码,然后我会给出使用 Go 语言实现的简化版本。请注意,一个完整的 B-Tree 实现会涉及很多细节和边界情况,这里只是展示一个基本的框架。
福大大架构师每日一题
2024/05/17
1300
文心一言 VS 讯飞星火 VS chatgpt (256)-- 算法导论18.3 2题
二叉树高频题
Given a binary tree, return the preorder traversal of its nodes' values.
王脸小
2019/10/24
7290
腾讯前端二面高频手写面试题总结
上图就很好的解释了 LRU 算法在干嘛了,其实非常简单,无非就是我们往内存里面添加或者删除元素的时候,遵循最近最少使用原则
helloworld1024
2023/01/05
7480
数据结构整理 顶
DefaultArray(data=[66,99,88], size=3 DefaultArray(data=[100,66,99,11], size=4 11 DefaultArray(data=[11], size=1
算法之名
2020/02/18
7000
数据结构整理
                                                                            顶
二叉树的各种操作(递归和非递归遍历,树深度,结点个数等等)
前序遍历顺序为: 根结点->左子树->右子树,所以对于正在访问的根结点,可以直接访问,访问完之后,按照相同的方式访问左子树,再访问右子树,过程如下 :
Java_老男孩
2020/01/02
1.1K0
Kotlin 函数式编程 + 递归 : 构建 "树" 数据结构通用模型
注意到这里的@Id 生成策略是我们自定义的: com.ak47.cms.cms.tree.CustomUUIDGenerator
一个会写诗的程序员
2019/07/01
1.1K0
Kotlin 函数式编程 + 递归 : 构建 "树" 数据结构通用模型
LeetCode 题目解答—— 第 416 到 460 题
从第 416 到第 460 题,跳过了需要付费的题目。付费的题目会单独放在一篇里面。
四火
2022/07/19
9560
LeetCode 题目解答—— 第 416 到 460 题
前端二面必会手写面试题
script标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于GET请求
helloworld1024
2022/12/20
6330
树的遍历总结
有返回值的递归遍历有两种情况: 1. 要维持一个树的结构,最后返回根节点,即返回值是TreeNode
Tim在路上
2020/08/05
1.7K0
树的遍历总结
二叉树由浅至深(上)
树有很多种表示方式,如:双亲表示法,孩子表示法、孩子兄弟表示法等等。这里简单了解其中最常用的孩子兄弟表示法。
海盗船长
2020/08/27
2870
2022必会的前端手写面试题
要求写出 区号+8位数字,或者区号+特殊号码: 10010/110,中间用短横线隔开的正则验证。 区号就是三位数字开头。
buchila11
2022/05/01
7800
Unity-UGUI无限循环列表
简介: UGUI使用ScrollView、GridLayoutGroup实现无限循环列表,支持数据刷新,支持跳转,支持动态插入/删除
祝你万事顺利
2019/07/26
5K0
View的有效曝光监控(上)|RecyclerView 篇
我:之前我是把我们广告的曝光监控放在广告的模型层,然后在bindview的时候做一次曝光的,然后内部做了一次曝光防抖动,避免多次曝光。
逮虾户
2020/10/15
1.3K0
VisualTreeHelper
Silverlight中只有可视化树,没有WPF中的逻辑树,这一点可从SL的sdk文档中得到印证: 可视化树概念也存在于 WPF 中,它与 Silverlight 的可视化树概念类似。然而,一个显著的差异是 WPF 还提供一个附加的筛选器或对象树(称为"逻辑树")的概念。逻辑树概念与某些属性系统行为相关。Silverlight 不通过帮助器类来公开此逻辑树。Silverlight 中的确存在某些(但并非所有)相关的属性行为,但由于没有用于访问这些行为的帮助器 API,因此,逻辑树概念在 Silverligh
菩提树下的杨过
2018/01/23
8360
VisualTreeHelper
LeetCode 二叉树系统题解
TIPS:前中后序遍历区别在于三字中的中间那个字,前、中、后序分别对应左、根、右。
Yano_nankai
2019/11/10
9680
LeetCode 二叉树系统题解
相关推荐
unity3d:查找子物体,并输出路径,GetComponentsInChildren,递归查找,栈查找
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验