函数递归实例——1~N个数中选择M个数组合

目录

一、实例介绍

二、组合数字从大到小输出

三、组合数字从小到大输出

四、结语

这里是一些废话,虽然函数递归的作用原理是不变的,但是在不同的情况下使用还是有很多需要注意的地方。对于刚刚入门算法的萌新们(就比如我)来说,在发现程序最后输出的结果与自己意想中的结果不同时,多多使用编译器的调试功能,一步一步分析。

一、实例介绍

从N个数中选择M个数做组合,如果使用循环的方法,即只要保证M个数都能遍历N个数,且M个数各不相同。

例如从1~5中选择3个数字做组合,C5/3最后的值就是10,也就是说,最终输出的组合一共会有10组。使用循环的方法的话,只需要三层for循环,这也是最直接的方法。

但要是M这个数字过大,for循环层数增加,代码量必定增大,同时也耗费我们的精力,在仅考虑这些方面的情况下,我们可以尝试使用递归的方法。

而递归就是函数在自己的函数体内不断调用自己。使用同一段代码来处理相同形式的问题,这让我们想到了什么呢?没错,就是函数调用!函数是可以调用自己本身的,这也是我们今天要讲的递归函数。

总而言之,也就是在大问题可以一层层递进将自己分解成相同形式的小问题的情况下,为了减少代码的书写量,合理利用已有的代码资源,即可使用递归函数。

这里组合输出,可以从1开始,也可以从最大的数字开始。下面来介绍这两种不同输出方式的解答。

二、组合数字从大到小输出

这是educoder上关于从1~5中选择3个数字进行组合的输入输出要求,但是我们今天介绍使用的代码,适应于1~N中M个数字的组合。

函数递归实例——1~N个数中选择M个数组合_第1张图片

/*
程序设计思路:
我们在从大到小输出的时候,变的数都是最低位的,就像从19~10一样,固定高位即十位的数,变动个位的数 

将一个组合的数字作为一个数组,因为每一个组合独立输出即可,所以,不需要用到二维数组
只需要变动我们定义的数组,用后面的值将前面赋过的值进行覆盖即可 

*/



#include 
int a[100];    //定义用来存储组合数值的数组,这是全局变量。如果将数组定义到自定义函数中,每一次调用函数,都会造成数组的刷新即重新定义,之前数组中的数值全部丢失
		       //如果将数组定义在main函数中,自定义函数赋值给数组,因为数组没有传入自定义函数,因此在自定义函数中赋值给数组,将会造成编译错误
			   //如果是将数组定义在main函数中,但是与自定义函数做地址传递,同样可以达到我们想要实现的效果。
			   //这里只介绍数组作为全局变量的使用 
 
void combrecur(int n, int m)     //自定义函数,也是我们的递归函数 
{
    int i,j;					 
    for(i=n;i>=m;i--)			//因为是从大到小输出,因此,最高位的最小值就是m,低位的最小值依次减小,与combrecur(i-1,m-1)调用之后代表的下一层进入低位并且m=m-1刚好对应 
    {
        a[m]=i;					//m在传入的时候,被赋予的意思就是组合中有多少个数字,我们将当前高位的数值赋给a[m]则说明之后输出需要反向输出
								//为什么不把最高位的数值赋给a[1]或者a[0]呢?
								//这是因为我们让程序简单化,如果使用a[m-r]或者a[m-r+1]的方法,需要定义全局变量r,并将最初的m值赋给r,所以只是输出的方向问题而已,我们不需要其他操作 
        if(m>1)					//这里也说明了,我们a[0]不会存储组合的数字,m=1的时候,就是组合中最后一个数字,此时不需要再递归函数。 
            combrecur(i-1,m-1);  //递归调用,因为n个数都是从1开始的连续的自然数,所以确定下一个高位的数字时,只需要将n-1即可。 
        else
        {
            for(j=a[0];j>0;j--)  //m=1时进入else的语句段中,main函数中a[0]即为组合中数字的个数,这里反向输出,达到从大到小的目的 
                printf("%d ",a[j]);
            printf("\n");          
        }						//一组组合输出,并不代表最里层的 combrecur(n-1,m-1)结束,它的结束,一定代表for循环的结束,之后才会回到上一层的combrecur(n,m),因此,这里的a[1]=i会不断改变,也对应了只改变低位的要求。
								//返回到上一层 combrecur(n,m),m=2,从执行完if语句开始,依旧在for循环中,m=2,i--,因此相比于最低层的combrecur(n-1,m-1)中a[2]的值,这里a[2]的值会减一,在因为m>1进入新一轮的combrecur(n-1,m-1)中
								//同理a[3],a[4]……都是这种变化方式 
    }
}

int main()
{
    int n,m;
    scanf("%d %d",&n,&m);
    a[0]=m;						//固定住m原本的值,主要用于控制输出 
    combrecur(n,m);

}

函数递归实例——1~N个数中选择M个数组合_第2张图片

这样就可以顺利解决,1~N中M个数字组合从大到小输出的问题

三、组合数字从小到大输出

这是educoder上对于组合数字从小到大输出的实例要求,我们的代码依旧是解决1~N中M个数字的组合问题

函数递归实例——1~N个数中选择M个数组合_第3张图片

/*
相比于从大到小输出,这里需要注意的地方更多。
程序设计思路:
这里是从小到大,类似于10~19的输出,依旧是固定住最高位的数字,更改最低位的数字 

*/ 

#include 
int a[100];
int s;										//这里的s是用来固定n的,因为之后n的变化有特殊情况的,记录n初始值的话,可以用来辅助控制最高位的大小 
void combloop1(int n, int m)
{
    int i,j;
    if(n==s && m==a[0])						//这里是为了在main函数调用的时候,直接初始化n的值,在给i赋值的时候,是用n来控制的,第一轮最高位从最小值1开始输出											 
        n=1;
								

    for(i=n;i<=s-m+1;i++)				//因为是从小到大输出,因此组合最高位的最大值只能是s-m+1。那为什么不是n-m+1或者s-a[0]+1呢?因为我们组合的数字最大可以是s,是不会变的,n代表的只是当前赋值给数组元素的数值
    									//a[0]与s都是固定值,如果这里使用固定的值的话,无法体现出最高位与低位之间可以使用的数据范围的变化 
										//最低位的最大值可以是s,即m=1时,i的值才能为s。从最高位到最低位,每进入下一层函数,低位代表的m=m-1,在这个式子中刚好可以代表低位的最大值加一即,(s-(m-1)+1)-(s-m+1)=1
    {
        a[m]=i;							//这里使用a[m]的原因和之前的一样 
													
        if(m>1)
            combloop1(i+1,m-1);				//这里是因为组合数字从小到大输出,因此可用于赋值的i增加,代表输出次序的m减小 
            
        else
        {
            for(j=a[0];j>0;j--)
                printf("%d ",a[j]);                      
            printf("\n");          
        }
    }
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    s=n;							//与从大到小输出的区别就在于,这里要有记录n即最大值的变量 
    a[0]=m;
    combloop1(n,m);
}

函数递归实例——1~N个数中选择M个数组合_第4张图片

这样,按照从小到大的顺序输出组合也就完成了。

附一张个人理解的简单的递归函数调用示例图

函数递归实例——1~N个数中选择M个数组合_第5张图片

四、结语

递归对于小萌新来说比较难掌握和使用,相比于循环的简单直接,它显得十分欠揍,但是有时候还是很实用的,例如解决经典的汉诺塔问题。大家都可以尝试着去写一写。

若本文的分析有误,请于评论区多多指教<抱拳>。


你可能感兴趣的:(算法,c语言)