从三道简单的例题看递归

从三道例题谈谈递归

写在前面

递归三要素:边界条件,递归前进段,递归返回段。递归作为最基础的算法之一,最常见的表现形式就是函数直接调用自身。递归的难点在于如何设计“退路”,也就是找到边界条件,处理好递归返回段。本文将从三道例题出发简单聊聊递归。

例题一:阶乘

给定一个正整数n,求它的阶乘n!,即 n * (n-1) * (n-2)……* 2 * 1

这是一道想法直观、实现简单的运用递归的例题

我们用F(n)来表示n的阶乘,那么 F(n)=n * F(n-1)


  1. 首先明确边界条件,当某层递归F(m)中m不等于1时,继续执行递归前进段,在F(m)中调用F(m-1)
    在这里插入图片描述
  2. 到达F(1)后,开始执行递归返回段,也就是退路
    在这里插入图片描述

C语言代买如下

int F(int n)
{
	if (n != 1)
		return n * F(n - 1);
	else
		return 1;
}

例题二:快速幂

编写函数myPow(double x, int n),实现计算x的n次幂
其中,x∈[-100.0,100.0],n∈[-231,231-1]

一个简单粗暴的想法是逐个相乘,比如 28=2 * 2 * 2 * 2 * 2 * 2 * 2 * 2

但是可以进一步优化,如果已经求得 24,只需将这个数求平方就能得到结果;而如果已经求得 22,也只需将其平方就能得到24

这样,我们自然就有了运用递归的想法


这道题的边界条件也很明显

我们用F(n)来表示xn,那么F(n)=[F(n/2)]2,而F(n/2)=[F(n/2/2)]2,依次类推,直到F(2) = [F(1)]2,到达边界,开始执行递归返回段

我们以26为例,画出递归的过程如下:

  1. 递归前进段
    在这里插入图片描述
  2. 在上图中,由于整型运算中3 / 2 = 1,而[F(1)]2=F(2)而不是我们想要的F(3),所以这个程序中还有一个很重要的设计:在递归返回段中,判断当前得到的幂是否是所期望的,如果不是,只需再乘以一个底数x
    在这里插入图片描述
    C语言代码如下
//递归函数
//value_current是当前值,pow_expect是当前所期望的次幂
int Recurse(double x, int n, double *value_current, int pow_expect)
{
	int pow_current = 1;	//当前次幂
	
	if (pow_expect != 1)	//边界条件
	{
		pow_current = Recurse(x, n, value_current, pow_expect / 2);	//递归前进段
	}

	if (pow_current == pow_expect - 1)	//当前次幂与当前所期望次幂不相等,需要补充乘以一个底数x
	{
		(*value_current) *= x;
	}

	if (pow_expect != n)	//递归返回段
	{
		(*value_current) *= (*value_current);
		return pow_expect * 2;
	}

	return -1;	//递归结束
}

double myPow(double x, int n)
{
	double value_current = x;
	int n_symbol = 1;	//指数n的符号
	int int_min_flag = 0;

	//特殊值判断
	if (x == 0)
		return 0;
	else if (x == 1)
		return 1;
	else if (x == -1)
	{
		if (n % 2)
			return -1;
		else
			return 1;
	}

	//因为n的取值可能是-2^31,将其符号取反后是2^31,超过了int的取值范围,所以要进行一些操作
	if (n == INT_MIN)
	{
		n += 1;
		int_min_flag = 1;
	}

	if (n < 0)
	{
		n_symbol = -1;
		n = -n;
	}
	else if (n == 0)
	{
		return 1;
	}

	Recurse(x, n, &value_current, n);

	if (int_min_flag)
	{
		value_current *= x;
	}

	if (n_symbol == 1)
		return value_current;
	else
		return 1/value_current;
}

例题三:生成有效括号

这是LeetCode题库中的22题,原题如下:

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

例如,给出 n = 3,生成结果为:
[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]


明确边界条件

在生成括号的过程中,
(1)左括号数>=右括号数
(2)左括号数/有括号数<=n

当边界条件都满足时,执行递归前进段。本题中,递归前进段的设计是:能放置左括号就先放置左括号,然后再放置右括号。

当任一边界条件不满足时,就进入递归返回段。本题中的退路设计是:去掉最后放置的括号。


为了叙事方便,我们以n=2为例,画出递归的过程如下:

  1. 执行递归前进段,能放置左括号就先放置左括号
    在这里插入图片描述

  2. 连续放置3个括号后,不满足边界条件2,进入递归返回段,去掉最后放置的括号,返回((后又进入递归前进段,添加右括号
    在这里插入图片描述

  3. 同上,此处得到了第一个有效的排列组合(()),之后再进入递归前进段得到的排列组合都将违反边界条件2,所以将一路返回到(
    在这里插入图片描述

  4. (的右支与左支大同小异
    在这里插入图片描述

  5. 得到第二个有效的排列组合()()
    在这里插入图片描述

  6. 右括号的数量大于左括号的数量,不满足边界条件1

在这里插入图片描述

至此,我们得到了n=2的全部有效的括号的排列组合:(())()()


C语言代码如下

//递归函数
//一维字符数组temp用于临时存放正在生成中的括号的组合,left_num、right_num是当前左、右括号数量
int Recurse(char *temp, int *temp_index, char **ret, int *ret_row, int n, int *left_nums, int *right_nums)
{
	if (*left_nums < n)	//边界条件
	{
		temp[*temp_index] = '(';	//递归前进段1:放置左括号
		(*temp_index)++;
		(*left_nums)++;
		Recurse(temp, temp_index, ret, ret_row, n, left_nums, right_nums);
		temp[*temp_index] = '\0';	//递归返回段:去掉最后放置的括号
		(*temp_index)--;
		(*left_nums)--;
	}

	if (*right_nums < *left_nums)	//边界条件
	{
		temp[*temp_index] = ')';	//递归前进段2:放置右括号
		(*temp_index)++;
		(*right_nums)++;
		Recurse(temp, temp_index, ret, ret_row, n, left_nums, right_nums);
		temp[*temp_index] = '\0';	//递归返回段:去掉最后放置的括号
		(*temp_index)--;
		(*right_nums)--;
	}

	if (*left_nums == n && *right_nums == n)	//左括号数=有括号数=n,则生成了有效的排列组合
	{
		ret[*ret_row] = (char *)malloc((2 * n + 1) * sizeof(char));
		strcpy(ret[*ret_row], temp);	//将有效的排列组合复制到二维字符数组ret中
		ret[*ret_row][2 * n] = '\0';
		(*ret_row)++;
	}

	return 0;
}

char ** generateParenthesis(int n, int* returnSize) 
{
	char *ret[20000];
	char *temp = (char *)malloc((2 * n + 1) * sizeof(char));
	int left_nums = 0, right_nums = 0, ret_row = 0, temp_index = 0;
	
	Recurse(temp, &temp_index, ret, &ret_row, n, &left_nums, &right_nums);
	*returnSize = ret_row;

	free(temp);

	return ret;
}

结语

正如开头所说,递归的难点在于如何找到边界条件、如何设计退路

在前两到例题中,递归仿佛像是一个“上下井”的过程,递归前进段是沿着井往下走,边界条件是井底,触到井底后就一直往上走,即递归返回段

但第三道例题有在井里反复上下的感觉,原因是在Recurse()函数中两次调用自身,与二叉树的深度优先遍历十分相像。

本文意在与大家聊聊我在学习递归时的一些心得体会,如有疏漏,望指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值