目录
第七章(函数)思维导图
总结:递归三问
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:
-
递归深度和堆栈溢出问题:
- 当输入的
n
值较大时,例如n
达到 40 或更高,递归调用fib(n)
的深度会非常大,可能导致堆栈溢出。这是因为递归方式的 Fibonacci 计算会在堆栈中不断增加帧,直到超出系统允许的最大深度。
- 当输入的
-
递归终止条件:
- 当
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-Creturn;}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;// 汉诺塔的盘子数量___BUGdo {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) { //奇数1return (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-Cmove(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=44,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; }