0617,递归问题(详细——好好好一入递归深似海)

 目录

第七章(函数)思维导图
总结:递归三问

01,电影院问题

理解递归的执行过程

02,FIBNACCI数列

不是说具有递归结构的问题,就可以用递归求解——存在大量的重复计算

 法一:自顶向下求解

BUG:

法一总结:

法二:自底向上求解

法二总结:

03,汉诺塔问题

递归的表达能力

04,约瑟夫环(简单版)

有些问题的递归结构很难发现,如果找到了这个问题的递归结构,对这个问题的理解就更深了,往往可以找到更好的求解方式

方法一:循环链表

方法二:递归公式

作业01:(汉诺塔)

解答:

作业02:  约瑟夫环(完整版)

答案:

作业03:(MAX,SEC_MAX)

错误代码01/不行的哦:

解法2:

答案:

作业04:(秒数转换):

 解答:

答案:

recursion——re/重复——cur/走,流动——sion/名词后缀
走重复的路

总结:递归三问

递归公式:
想不明白就从边界条件的下一层抽象出来
根据定义,大问题小问题的求解方式都是一样的,只是数据规模不一样
只考虑这一层和上一层(假定上一层已经求解)

思考的时候不要陷入细节,从问题的模式考虑?

01,电影院问题

理解递归的执行过程

乌漆嘛黑,你和你女朋友在第几排——大问题
”哥们,你在第几排“——递——子问题(求解方式和大问题一致,只是数据规模不一致)
”前面有鬼,第一排“——第二排——第三排——归——子问题的解合并成大问题的解

02,FIBNACCI数列

不是说具有递归结构的问题,就可以用递归求解——存在大量的重复计算

 法一:自顶向下求解

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

long long fib(int n) {
	if (n == 1 || n == 2) {
		return 1;
	}
	return fib(n - 1) + fib(n - 2);
}

int main(void) {
	int n;
	do {
		scanf("%d", &n);
		printf("%lld\n", fib(n));

	} while (n != 0);
	return 0;
}
BUG:
  1. 递归深度和堆栈溢出问题:

    • 当输入的 n 值较大时,例如 n 达到 40 或更高,递归调用 fib(n) 的深度会非常大,可能导致堆栈溢出。这是因为递归方式的 Fibonacci 计算会在堆栈中不断增加帧,直到超出系统允许的最大深度。
  2. 递归终止条件:

    • 当 n 等于 0 时,你的程序会继续运行并输出 fib(0) 的值。Fibonacci 序列的定义中通常认为 fib(0) 是 0,而不是 1。因此,你需要考虑在 fib 函数中处理 n == 0 的情况
法一总结:

不能在有限的时间内得到正确的结果

递归树——有大量的重复结点——重复的树——大量重复的计算

思考方式——自顶向下

法二:自底向上求解

自底向上求解

动态规划——算法设计思想,可以将指数级别的算法,优化成多项式级别的思想,——避免重复计算问题

假定!上一个问题已经求解
0,1,
2,3,5……

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

long long fib2(int n) {
	if (n == 0 || n == 1) {
		return 1;
	}
	int i;
	long long a = 1;
	long long b = 1;
	long long t;
	for (i = 2; i <= n; i++) {     //2的时候进入循环
		t = a + b;               //循环不变式——每次进入循环体之前都成立的条件
		a = b;                   //fib(i)未求解,fib(2)未求解,fib(n+1)未求解
		b = t;                                              //循环退出点————i=n+1 
	}                                                      //循环不变式可以保持到循环的结束
	return t;
}


int main(void) {
	int n;
	do {
		scanf("%d", &n);
		printf("%lld\n", fib2(n));
	} while (n != 0);
	return 0;
}
法二总结:

思考方式——自底向上

循环不变式——每次进入循环体之前都成立的条件,可以保持到循环的结束

03,汉诺塔问题

递归的表达能力

printf("Total step(s): %lld\n", (1LL << n) - 1);
根据汉诺塔问题的特性,当有n个盘子时,移动次数等于2^n - 1。在这里,(1LL << n) - 1 就是利用位运算得到移动的总步数 

实在想不懂,从N=2的情况归纳递归表达式

1 A-B,将N-1个 盘子从目标柱子——》辅助柱子     
2 A-C,将N   个 盘子从源柱子   ——》目标柱子
1 B-C,将N-1个 盘子从辅助柱子——》目标柱子

//说的都是前一个,假定前一个问题已经解决,即假定已经完成N-2个盘子移到目标柱子

#include <stdio.h>

void move(int n, char source, char target, char auxiliary) {     //源,目标,辅助柱子
    if (n == 1) {
        printf("Move disk 1 from %c to %c\n", source, target);   //退出点  1——A-C
        return;
    }

    move(n - 1, source, auxiliary, target);      //把N-1个盘子  从 源柱子 移动到 辅助柱子       move(n-1)次
    printf("Move disk %d from %c to %c\n", n, source, target);      //把N盘子 ,从源柱子移动到 目标柱子  1次
    move(n - 1, auxiliary, target, source);     //把N-1个  从 辅助柱子 移动到 目标柱子       move(n-1)次
}

int main() {
    int n;// 汉诺塔的盘子数量___BUG
    do {
        scanf_s("%d", &n);
        if (n == 0) {
            break;
        }
        move(n, 'A', 'C', 'B');
        printf("Total step(s): %lld\n", (1LL << n) - 1);//move(n-1)*2-1次
    } while (1);
    return 0;
}



//所以,总步数(T(k + 1)) 为:
//[T(k + 1) = T(k) + 1 + T(k) = 2T(k) + 1]
//
//根据归纳假设(T(k) = 2 ^ k - 1),代入上式:
//[T(k + 1) = 2(2 ^ k - 1) + 1 = 2 ^ {k + 1} - 2 + 1 = 2 ^ {k + 1} - 1]
//
//因此,(T(k + 1) = 2 ^ {k + 1} - 1) 成立。

 

04,约瑟夫环(简单版)

有些问题的递归结构很难发现,如果找到了这个问题的递归结构,对这个问题的理解就更深了,往往可以找到更好的求解方式

方法一:循环链表

时间复杂度——2*2(n-1)——O(N)
空间复杂度——O(n)

方法二:递归公式

边界条件:只有一个的人的时候,return 1;剩下两个人的时候,刀掉第二个,return 1
递归公式思考:

当人数为偶数时      
1,2,3,4,5,6,7,8,9,10,11,12
1,--,3,--,5,--,7,--,9,--,11,--          第一轮🔪掉所有的偶数,刚好又从1开始
1,--,2,--,3,--,4,--,5,--,6,--            重新编号继续刀人
1,--,--,--,3,--,--,--,5,--,--,--,       第二轮:刀掉所有重新编号的偶数
递——重新编号,一直刀刀剩下最后一个人
归——最后一个人,最后的编号,逐级返回(找到和上一级编号的关系),得到真正的编号
return x=6-->11        f(x)=2x-1

当人数为奇数时 
1,2,3,4,5,6,7,8,9,10,11       第一轮,刀掉所有的偶数
1,--,3,--,5,--,7,--,9,--,11       重新编号继续刀人。PS:新的编号,应该从11开始
2,--,3,--,4,--,5,--,6,--,1        
0,--,1,--,2,--,3,--,4,--,5         或者编号改一下?
--,--,1,--,--,--,3,--,--,--,5        继续刀掉偶数
归——真正的编号x=9,返回的编号x=4    
return x=4-->9     f(x)=2x+1                     要搞清楚编号传递的先后顺序,然后找个好带的推

n为偶数   joseph(n)=2*joseph(n/2)-1
n为奇数   joseph(n)=2*joseph(n/2)+1

时间复杂度:O(logN)
空间复杂度:O(logN)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int joseph(int n) {
	if (n == 1 || n == 2) {
		return 1;
	}
	if (n & 0x1) {  //奇数1
		return (joseph(n >> 1) << 1) + 1;
	}
	else {
		return (joseph(n >> 1) << 1 )+ 1;
	}
}

int main(void) {
	printf("joseph(5)=%d\n", joseph(5));
	printf("joseph(8)=%d\n", joseph(8));
}

/*
约瑟夫环:n 个人站成一圈,每 m 个人处决一个人。
假设这 n 个人的编号为 1, 2, ..., n,并且从 1 开始数,问最终活下来的人编号是多少? (拓展题)
int joseph(int n, int m);*/

作业01:(汉诺塔)

有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:

1. 每次只能移动一个圆盘;
2. 大盘不能叠在小盘上面。

提示:可将圆盘临时置于 B 杆,也可将从 A 杆移出的圆盘重新移回 A 杆,但都必须遵循上述两条规则。

问:最少需要移动多少次?如何移?

解答:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int move(int n,char origin,char middle,char target) {
	if (n == 1) {
		printf("move %d :%c->%c\n",n, origin, target);
	}
	//1 A-B
	//2 A-C
	//1 B-C
	move(n - 1, origin, middle,target);
	printf("move %d :%c->%c\n", n-1,origin, target);
	move(n, middle, target, origin);
}

int main(void) {
	int n;
	do {
		scanf("%d", &n);
		move(n,'A','B','C');
		printf("number of times: %d\n", n << 1 - 1);
	} while (n!= 0);
	return 0;
}

作业02:  约瑟夫环(完整版)

约瑟夫环:n 个人站成一圈,每 m 个人处决一个人。假设这 n 个人的编号为 1, 2, ..., n,并且从 1 开始数,问最终活下来的人编号是多少? (拓展题)
int joseph(int n, int m);

答案:

1,2,3,4,5,6,7       n=7,m=3       结果是4

1,2,--,4,5,6,7       刀掉3,重新编号
5,6,--,1,2,3,4        重新编号之后,新/源 编号的关系(1+m)%n --(1+3)%7=4   

4,5,--,0,1,2,3       存在特例,(4+3)%7==0改从0开始,(3+3)%m+1=7

递归公式: f(x)=(x+m)%n+1;

边界条件:当只剩下一个人的时候,编号为0-x(从0开始编号)       

return (joseph_helper(n - 1, m) + m) % n;

找到小解和大解之间的关系,直接套????

// 循环链表:空间复杂度O(n), 时间复杂度:O(mn)
//    递归: 空间复杂度O(n), 时间复杂度:O(n)

#include <stdio.h>

int joseph_helper(int n, int m) {
	// 从0开始编号
	// 边界条件
	if (n == 1) return 0;
	return (joseph_helper(n - 1, m) + m) % n;
}

int joseph(int n, int m) {
	// 从1开始编号 
    // 委托
	return joseph_helper(n, m) + 1;
}

int main(void) {
	printf("joseph(7, 3) = %d\n", joseph(7, 3));
	return 0;
}

作业03:(MAX,SEC_MAX)

查找数组中最大的元素和第二大的元素,并将它们分别存放在由 largest 和 second_largest 指向的变量中。

void find_two_largest(int arr[], int n, int* largest, int* second_largest);

注意:为了简化程序,数组不存在重复元素。

错误代码01/不行的哦:

基础不牢地动山摇
定义了两个野指针  指针定义:int *p=&a,int *q=p;     ……还有什么好说的,丢人

需要的数据和数据类型INT,所以先创建变量INT
1,定义了野指针,数据没有载体
2,将指针指向数组第一个元素的话,会改变数组的值,不能完整的循环

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define SIZE(a) (sizeof(a)/sizeof(a[0]))

int* largest;
int* second_largest;

void find_two_largest(int arr[], int n, int* largest, int* second_largest) {
	int i; int j;
	for (i = 0; i < n; i++) {
		for (j = 0; j <n-i ; j++) {
			if (arr[i] > arr[j]) {
				int temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
	}
	*largest = arr[n - 1];
	*second_largest = arr[n - 2];
}

int main(void) {
	int arr[] = { 3,5,343,2,4,6,232,2,3,0 };
	find_two_largest(arr, SIZE(arr), &largest, &second_largest);
	printf("largest %d,second_largest %d\n", *largest ,*second_largest);
	return 0;
}

解法2:

好好好,倒也不必指针了,写到第四题就清醒了

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define SIZE(a) (sizeof(a)/sizeof(a[0]))



void find_two_largest(int arr[], int n, int* largest, int* second_largest) {
	if (arr[0] > arr[1]) {
		*largest = arr[0];
		*second_largest = arr[1];
	}
	else {
		*largest = arr[1];
		*second_largest = arr[0];
	}
	int i;
	for (i = 2; i < n; i++) {
		if (arr[i] > *largest) {
			int temp = *largest;
			*largest = arr[i];
			*second_largest = temp;
		}
		else if (arr[i] > *second_largest && arr[i] < *largest) {
			*second_largest = arr[i];
		}
	}
}

int main(void) {
	int arr[] = { 3,5,343,2,4,6,232,2,3,0 };
	int max = 0; int sec = 0;
	//int* largest=&max;//野指针
	//int* second_largest =&sec;
	find_two_largest(arr, SIZE(arr), &max, &sec);  
	printf("largest %d,second_largest %d\n",max ,sec);
	return 0;
}

答案:

ELSE-IF逻辑的必要性,和正确性,减少比较的次数

int main(void) {
    int largest, second_largest;
    int arr[] = {9, 5, 2, 7, 1, 3, 4, 6, 8, 0};
    find_two_largest(arr, 10, &largest, &second_largest);
    printf("largest = %d, second_largest = %d\n", largest, second_largest);
    
    return 0;
}

void find_two_largest(int arr[], int n, int* largest, int* second_largest) {
	*largest = arr[0] >= arr[1] ? arr[0] : arr[1];
	*second_largest = arr[0] < arr[1] ? arr[0] : arr[1];
	
	for (int i = 2; i < n; i++) {
		if (arr[i] > *largest) {
			*second_largest = *largest;
			*largest = arr[i];
		} else if (arr[i] > *second_largest) {
			*second_largest = arr[i];
		}
	}
}

作业04:(秒数转换):

void split_time(long total_sec, int* hour, int* minute, int* second);

total_sec 表示从午夜12:00:00开始计算的秒数。请将 total_sec 转化以时(0-23)、分(0-59)、秒(0-59)表示的时间,并存放到函数外部由指针 hour, minute, second 指向的变量中。并在外部,打印出当前的时间

 解答:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void split_time(long total_sec, int* hour, int* minute, int* second) {
	*hour = total_sec / (60 * 60);
	*minute = total_sec/60;
	*second = total_sec % 60;
	while (*hour >= 23) {
		*hour %= 24;
	}
	while (*minute >= 59) {
		*minute %= 60;
	}
}
int main(void) {
	long total_sec;
	do{
		int t_hour, t_minute, t_second;
		scanf("%ld", &total_sec);
		split_time(total_sec, &t_hour, &t_minute, &t_second);
		printf("%-4.2d:%-4.2d:%-4.2d\n", t_hour, t_minute, t_second);
	} while (total_sec != 0);
	return 0;
}

/*
void split_time(long total_sec, int* hour, int* minute, int* second);
total_sec 表示从午夜12:00:00开始计算的秒数。请将 total_sec 转化以时(0-23)、分(0-59)、秒(0-59)表示的时间,
并存放到函数外部由指针 hour, minute, second 指向的变量中。并在外部,打印出当前的时间*/

答案:

*second = total_sec % 60;
*minute = (total_sec / 60) % 60;
*hour = (total_sec / 60 / 60) % 24;
int main(void) {
    long total_sec = 9527;
    int hour, minute, second;
    // 指针可以做为返回值来用
    split_time(total_sec, &hour, &minute, &second);
    printf("%d:%d:%d\n", hour, minute, second);
    return 0;
}

void split_time(long total_sec, int* hour, int* minute, int* second) {
	*second = total_sec % 60;
	*minute = (total_sec / 60) % 60;
	*hour = (total_sec / 60 / 60) % 24;
}

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值