扒开衣服看递归--递归的本质

本文深入讲解了递归函数的概念及应用,包括递归的基本原理、结束条件与推进条件,通过实例解析递归算法如何解决复杂问题,如Fibonacci数列、阶乘计算及二叉树遍历。

编程语言中,函数Func(Type a,……)直接或间接调用函数本身,则该函数称为递归函数。
– 《百度百科》

我们一般运用递归算法来解决以下的几种问题:

  1. 数据的定义是按递归定义的。(Fibonacci函数,n的阶乘)
  2. 问题解法按递归实现。(回溯)
  3. 数据的结构形式是按递归定义的。(二叉树的遍历,图的搜索)

可能到现在很多同学还没弄明白什么是递归,我们先来举个简单的例子。我的第一本计算机方面的书《C++程序语言设计》(谭浩强)中给出了一个这样的例子:

五个人坐在一起,第五个人比第四个人大2岁,第四个比第三个大2岁,每个人都比后面一个人大2岁,第一个人是10岁,问第五个人是几岁

我们脑海里很快想出了他们之间的关系:

age(5) = age(4) + 2;
age(4) = age(3) + 2;
age(3) = age(2) + 2;
age(2) = age(1) + 2;
age(1) = 10;

我们可以推导出一下的式子:

age(n) =  10   (n = 1)  //结束条件
age(n) = age(n - 1) +2 (n > 1) //推进条件。

每个递归函数总要具备两个条件:

  1. 结束条件
  2. 推进条件

结束条件用来告诉函数什么时候该停止,推进条件找出函数的一般条件,如本题的每个人比前面一个人大两岁,来进行函数推进。

我们可以写出以下的代码:

int age(int n)
{
   if(n == 1) c = 10;
   else return age(n - 1) + 2;
}

这段函数是怎么运作的呢,就是按照我们上面给出的递推关系,为了更好方便大家理解,画出一个递推的关系图。

这里写图片描述

那么下面就来扒开衣服看本质,递归在内存中到底是怎么一种运作方式呢。根据上面的图,我们可以感觉到,递归的本质类似栈的性质,先进后出。

引用SpeedMe的一张图来解释

引用SpeedMe的一张图来解释。

函数的递归调用和普通函数调用是一样的。当程序执行到某个函数时,将这个函数进行入栈操作,在入栈之前,通常需要完成三件事。

  1. 将所有的实参、返回地址等信息传递给被调函数保存。
  2. 为被调函数的局部变量分配存储区。
  3. 将控制转移到北调函数入口。

当一个函数完成之后会进行出栈操作,出栈之前同样要完成三件事。

  1. 保存被调函数的计算结果。
  2. 释放被调函数的数据区。
  3. 依照被调函数保存的返回地址将控制转移到调用函数。

上述操作必须通过栈来实现,即将整个程序的运行空间安排在一个栈中。每当运行一个函数时,就在栈顶分配空间,函数退出后,释放这块空间。所以当前运行的函数一定在栈顶。

在 C# 编程语言中,递归是一种常见的编程技术,它通过函数直接或间接地调用自身来解决复杂问题。递归的核心在于将一个大规模的问题逐步分解为更小的、结构相似的子问题,最终通过解决这些子问题来解决原始问题。实现递归的关键在于定义清晰的终止条件以及递归调用逻辑。 ### 递归的基本结构 递归函数通常包含两个部分: 1. **递归终止条件**:确保递归不会无限进行下,当满足特定条件时,递归停止。 2. **递归调用**:函数调用自身,并且每次调用都使问题规模缩小,逐渐接近终止条件。 以下是一个简单的递归示例,用于计算整数的递增操作: ```csharp public int Inc(int n) { // 递归终止条件 if (n == 10) { return n; } // 使递归趋于结束的语句 n = n + 1; // 递归开始语句 return Inc(n); } ``` ### 阶乘计算的递归实现 阶乘是递归的经典应用场景之一。阶乘的定义为:`n! = n * (n-1)!`,其中 `0! = 1` 是终止条件。以下是使用递归计算阶乘的 C# 实现: ```csharp public int Factorial(int n) { // 递归终止条件 if (n == 0 || n == 1) { return 1; } // 递归调用 return n * Factorial(n - 1); } ``` ### 快速排序的递归实现 快速排序是一种基于递归的排序算法,其核心思想是选择一个“枢轴”元素,将数组划分为两部分,一部分小于枢轴,另一部分大于枢轴,然后递归地对这两部分进行排序。以下是快速排序的递归实现: ```csharp public void QuickSort(int[] arr, int left, int right) { if (left >= right) return; int pivotIndex = Partition(arr, left, right); QuickSort(arr, left, pivotIndex - 1); // 对左子数组排序 QuickSort(arr, pivotIndex + 1, right); // 对右子数组排序 } private int Partition(int[] arr, int left, int right) { int pivot = arr[right]; int i = left - 1; for (int j = left; j < right; j++) { if (arr[j] < pivot) { i++; Swap(arr, i, j); } } Swap(arr, i + 1, right); return i + 1; } private void Swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ``` ### 树结构的递归实现 递归也常用于处理树形结构数据。例如,在 C# 中,可以使用递归构建 `TreeView` 控件。以下是一个简单的树结构模型类定义: ```csharp public class TreeModel { public string ID { get; set; } public string PARENTID { get; set; } public string NAME { get; set; } public List<TreeModel> TREECHILDREN { get; set; } } ``` 递归构建树结构的示例代码如下: ```csharp public List<TreeModel> BuildTree(List<TreeModel> allNodes, string parentId) { var children = allNodes.Where(n => n.PARENTID == parentId).ToList(); foreach (var node in children) { node.TREECHILDREN = BuildTree(allNodes, node.ID); } return children; } ``` ### 递归的注意事项 1. **终止条件**:必须明确定义递归的终止条件,否则会导致无限递归,最终引发 `StackOverflowException`。 2. **性能优化**:递归可能会导致重复计算,可以考虑使用 **记忆化递归** 或 **动态规划** 来优化性能。 3. **递归深度**:递归调用栈的深度是有限的,对于非常大的问题,可能需要使用 **尾递归优化** 或改用 **迭代** 实现。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值