有意思的全排列递归算法

引言

递归思想是计算机算法中非常重要的一个思想,大家在刷力扣或者打算法竞赛时会经常和递归打交道。本文以全排列函数的算法,深入浅出的带领大家一起学习如何用简单的思想掌握递归算法。

递归的本质

二叉树的深度优先遍历、图的连通性检查等等常见的算法题中,解题的核心方式就是使用递归让计算机去遍历每一个需要访问的数据元素。
不知道大家最开始是如何理解递归函数的呢?是在脑中一点一点的模拟递归的过程?还是一脸懵只要是记住递归模板能解题就行?
其实只需要记住一句话,不管是多么复杂的递归算法,都能像庖丁解牛一般,将其掌握:

写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。

这句话来自极客时间王争的数据结构和算法之美,下面我就以全排列的算法,一步步解析递归算法:

全排列算法

参考题目——力扣 LeetCode-46题-全排列
题目描述:给定一个没有重复数字的序列,返回其所有可能的全排列。
解题思路:
假设有一个序列{1,2,3},其不重复的排列组合方式有6种,用人脑可以很快的想出来哪6种,但是用计算机如何解决呢?

递归的本质是将复杂问题分解为一个个可重复的简单问题

确定递推公式
第一位有 n 种可能性,确定了第一位后就是求解剩下 n - 1 个数据的排列问题,这样就可以往下一直分解问题,直到序列结尾处,也就是终止条件。
写非递归代码
写选出第一位的元素的代码

//全局变量数组
int arr[]={
    
    1,2,3};
void f()
{
    
    
	for(int i=0;i<3;++i)
	{
    
    
		//交换位置i和位置k的元素
		{
    
    int t=arr[i];arr[i]=arr[k];arr[k]=t;}
		//打印数组
		for(int i=0;i<3;++i)
			cout<<arr[i]<<" ";
		cout<<endl;
		//再次交换,恢复之前的元素排列
		{
    
    int t=arr[i];arr[i]=arr[k];arr[k]=t;}
	}
}

打印输出

[1, 2, 3]
[2, 1, 3]
[3, 2, 1]

加入递归
上面的代码是对序列的首元素进行选择,然后是第二个元素。第二个元素选择的时候,序列中可选择的元素已经少了一个;选择第三个元素的时候,序列中可选择元素少了两个。选择的方式都是一样的,用一个for循环遍历可选择的序列,那么我们可以将上面的程序改为:

int arr[]={
    
    1,2,3};
void f(int k)
{
    
    
	//递归的终止条件(递归出口)
	if(k==3)
	{
    
    
		//打印数组
		for(int i=0;i<3;++i)
			cout<<arr[i]<<" ";
		cout<<endl;
	}
	for(int i=k;i<3;++i)
	{
    
    
		//交换位置i和位置k的元素
		{
    
    int t=arr[i];arr[i]=arr[k];arr[k]=t;}
		//递归,参数值+1,代表去选择下一个位置的元素
		f(k+1);
		//再次交换,恢复之前的元素排列
		{
    
    int t=arr[i];arr[i]=arr[k];arr[k]=t;}
	}
}

打印输出

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 2, 1]
[3, 1, 2]

总结

如果要通过人脑去过一遍递归的过程,那是很困难的,当然也没这个必要。求解的递归的关键点是:

  • 一个问题是否可以分解为多个子问题,然后子问题又可以继续划分;

  • 分解后的子问题除了数据规模不一样,但具体解法还是一样。在全排列的求解过程中,每个子问题的解法一样,那就先解出一个子问题(求第一位有多少种情况),然后再加入递归代码,大脑中不用去模拟递归的一个过程,你就想子问题的解法都是一样的,计算机只不过是在通过栈做重复的事情而已;

  • 找到子问题终止条件。

Question:如果序列中有重复元素,又该如何用递归算法解决问题呢?快在评论区写下你的答案,让大家看看你是否已经能够解决该类递归问题

参考文章
轻松理解全排列算法的递归解法

おすすめ

転載: blog.csdn.net/qq_42518941/article/details/115191582