C语言程序与设计——函数(二)递归练习

news2024/9/28 22:12:32

在上一篇文章中接触到了递归这种编程方法,下面我们将用几个程序加深以下对递归的理解。

递归实际上就是程序调用自身的编程技巧
递归程序的组成:

  1. 边界条件处理
  2. 针对于问题的处理过程递归过程
  3. 结果返回

二分查找

首先分析二分查找的查找逻辑:

  1. 需要三个指针分别指向数组:head,tail和mid。
  2. 当arr[mid] > aim : 说明目标数据在mid 和head之间,让head指向mid ,mid 重新计算
  3. 当arr[mid] < aim : 说明目标数据在mid 和head之间,让tail指向mid ,mid 重新计算
  4. 如果出现head > tail 的情况说明,说明该数组没有该值,则返回false
    在这里插入图片描述

所以由此,我们设计二分查找函数的时候,可以确认需要传入四个参数,分别为数组arr、头指针head、尾指针tail以及目标值aim。
tip:使用二分查找时需要保证数组内的数据是单调的,我写的这一版是单调递增的。

#include<stdio.h>
#define SIZE 10

int binary_search(int *arr, int aim,int start, int len){
    if(len < start ){
        return 0;
    }
    int mid = (start + len) >> 1;
    if(*(arr + mid) == aim) return mid;
	else if(*(arr + mid) > aim) return binary_search(arr, aim, start, mid - 1);
    else if(*(arr + mid) < aim) return binary_search(arr, aim, mid + 1, len);
}

int main(){
    int arr[SIZE] = {4,5,8,9,10,15,23,34,56,96};
    int n;
    while(~scanf("%d", &n)){
        if(!binary_search(arr, n, 0, SIZE)) {
            printf("it is not existed!\n");
            continue;
        }
        printf("arr[%d] = %d\n", binary_search(arr, n, 0, SIZE), n);
    }
}

运行结果:
在这里插入图片描述

二分查找的两种特殊情况

在二分查找中有两种特殊情况

000000000011111111111:查找该数列的第一个1
111111111110000000000:查找该数列的最后一个1
这两种情况在对于抽象现实的问题,可以提供解决思路,例如当我们去筛选一组单调数据中满足条件的数据时,找到第一个(不)满足要求的数据位置,即找到分界位置。

二分查找数列需满足的条件就是需要数列是单调的,但是当数列出现两个或多个相同的元素时,该数列依然是单调的,但是我们所查找到的元素位置就是不确定的,那么对于找到分界位置,是极具意义的,具体寻找过程如下图演示过程。

- 000000000011111111111:查找该数列的第一个1

在这里插入图片描述
在这里插入图片描述

按照图示过程,可以看出,当mid值为1时,将tail指针指向mid,当mid值为0时,head指向mid+1的位置。当三个指针重合的时候即为所求位置,另外可以看出mid是向下取整的。在这其中呢还需要考虑两个特殊情况,即全为0和全为1的情况,需要考虑返回的结果与位置的正确性。这里着重需要考虑全为0的情况,全为0时依旧会返回一个索引位置,但是整个数列是都不满足的,所以我在返回处加了一组特判解决,也可以采用更优雅的方式——虚拟位(会在文末演示)。

#include<stdio.h>
#define SIZE 10
//000000000000111111111111找第一个1
int bin_search1(int *arr, int aim, int len){
    int head = 0, tail = len, mid = (head + tail) >> 1;
    while(head < tail){
       printf("head = %d, mid = %d, tail = %d\n", head, mid, tail);
        if(*(arr + mid) == aim) tail = mid;
        else if(*(arr + mid) < aim) head = mid + 1;
        mid = (head + tail ) >> 1;
    } 
    return arr[head] == aim? head:-1;
}
//递归写法
int b_search1(int *arr, int aim,int head, int tail){
    if(head >= tail){
        return arr[head] == aim ? head : -1;
    }
    int mid = (head + tail) >> 1;
    if(*(arr + mid) == aim) return b_search1(arr, aim, head, mid);
    else if(*(arr + mid) < aim) return b_search1(arr, aim, mid + 1, tail);
}
int main(){
    int arr[SIZE + 5] = {0};
    int n, num;
    scanf("%d", &n);
    for(int i = 0;i < n; i++){
        scanf("%d", &arr[i]);
    }
    printf("the first 1 is located %d\n", b_search1(arr ,1 ,0 ,n));      
}

运行结果:
在这里插入图片描述

- 111111111110000000000:查找该数列的最后一个1
在这里插入图片描述
可以看到与上一中情况不同的时tail的更新变成了mid - 1,head更新为mid。而且mid也变成了向上取整(若不使用向上取整,当head与tail相邻的时候需要会陷入死循环)

#include<stdio.h>
#define SIZE 5
//111111111111000000000000找最后一个1
int bin_search2(int *arr, int aim, int len){
    int head = 0, tail = len, mid = (head + tail + 1) >> 1;
    while(head < tail){
       printf("head = %d, mid = %d, tail = %d\n", head, mid, tail);
        if(*(arr + mid) ==  aim) head = mid;
        else tail = mid - 1;

        mid = (head + tail + 1) >> 1;
    }
    return *(arr + head) == aim ? head:-1;
}
//递归写法
int b_search2(int *arr, int aim, int head, int tail){
    if(head >= tail){
        return *(arr + head) == aim ? head : -1;
    }
    int mid = (head + tail + 1) >> 1;
    if(*(arr + mid) == aim) return b_search2(arr, aim, mid, tail);
    else return b_search2(arr, aim, head, mid - 1);
}

int main(){
    int arr[SIZE + 5] = {0};
    int n, num;
    scanf("%d", &n);
    for(int i = 0;i < n; i++){
        scanf("%d", &arr[i]);
    }
    printf("the last 1 is located %d\n", b_search2(arr ,1, 0 ,n));
}

运行结果:
在这里插入图片描述

总结
当我们在一个单调的且只有两种元素的序列找分界线的时,mid取整方向与“1”所在方向相反,mid取整方向很重要,否则会使得程序陷入死循环。同时应该考虑全为0或全为1的两种特殊情况。

欧几里得算法(辗转相除法)

  1. 欧几里得算法又名辗转相除法
  2. 迄今为止已知最古老的算法,距今2324年
  3. 用于快速计算两个数字的最大公约数
  4. 还可以用于快速求解 a ∗ x + b ∗ y = 1 a*x+b*y = 1 ax+by=1方程的一组整数解

欧几里得算法是,两个整数a和b通过不断的相除取余从而最后得出a与b的最大公约数,下面是欧几里得算法的公式推导
{ a = ε 1 r b = ε 2 r ,(其中 ε 1 ε 2 互素) a % b = ⌊ a − k b ⌋ = ⌊ ε 1 − k ε 2 ⌋ r ⇒ 可证明 r 为 a 和 b 的引述,只需证明 r 最大 若使 r 最大,则只需证明 ε 1 与 k ε 2 互素即可 ⇒ g c d ( ε 1 , k ε 2 ) = 1 \begin {cases} a = \varepsilon_1 r\\ b = \varepsilon_2r \end {cases} ,(其中\varepsilon_1\varepsilon_2互素) \\ a \% b = \lfloor a - kb \rfloor =\lfloor\varepsilon_1 - k\varepsilon_2\rfloor r \Rightarrow 可证明r为a和b的引述,只需证明r最大 \\若使r最大,则只需证明\varepsilon_1 与k\varepsilon_2互素即可 \Rightarrow gcd(\varepsilon_1 , k\varepsilon_2) = 1 {a=ε1rb=ε2r,(其中ε1ε2互素)a%b=akb=ε1kε2r可证明rab的引述,只需证明r最大若使r最大,则只需证明ε1kε2互素即可gcd(ε1,kε2)=1
由此可以求出a,b的最小公因数,由上述公式也可退出求解a,b的最大公倍数
已知 { a = ε 1 r b = ε 2 r ,(其中 ε 1 ε 2 互素) a ∗ b = ε 1 r ∗ ε 2 r = ε 1 ε 1 r 2 因为 r 为 a , b 的公约数,且 ε 1 ε 2 互素,则最小公倍数为 ε 1 ε 2 r = a ∗ b r 已知\begin {cases} a = \varepsilon_1 r\\ b = \varepsilon_2r \end {cases} ,(其中\varepsilon_1\varepsilon_2互素) \\a * b = \varepsilon_1 r * \varepsilon_2 r = \varepsilon_1\varepsilon_1 r^2 \\ 因为r为a,b的公约数,且\varepsilon_1\varepsilon_2互素,则最小公倍数为\varepsilon_1\varepsilon_2r = \frac{a*b}{r} 已知{a=ε1rb=ε2r,(其中ε1ε2互素)ab=ε1rε2r=ε1ε1r2因为ra,b的公约数,且ε1ε2互素,则最小公倍数为ε1ε2r=rab
代码很简洁,如下:

#include<stdio.h>

int gcd(int a, int b){
    return b ? gcd(b, a % b) : a;
}

int lcm(int a, int b){
    return a * b / gcd(a , b);
}

int main(){
    int a, b;
    while(~scanf("%d %d", &a, &b)){
        printf("the greatest common divisor is %d and the lcm is %d about %d and %d \n", gcd(a, b),lcm(a, b), a , b);
    }
}

运行结果:
在这里插入图片描述

拓展计算平方根

我们已经了解二分查找的查找方式,那么现在我们拓展一下,使用二分查找来计算平方根

思路:

  1. 传入被开方数n,查找范围就是从0~n
  2. 考虑到精度问题,定义一个误差范围,当tail与head的差小于误差时就可输出
  3. 注意数据类型,这里推荐全都使用double

#include<stdio.h>
#include<math.h>
#define EPSL 1e-6

double my_sqrt(double n){

    double mid = n / 2.0, tail = n, head = 0;
    while(tail - head > EPSL){
        if(mid * mid < n) head = mid;
        else tail = mid;
        mid = (head + tail) / 2.0;
    }
    return mid;
}

int main(){
    double n;
    while(~scanf("%lf", &n)){
        printf("my_sqrt(%lf) = %lf\n", n, my_sqrt(n));
        printf("sqrt(%lf) = %lf\n", n, sqrt(n));
    }
}

在上述代码中存在一个bug,该程序只能计算大于1的程序,这里我们可以通过加一个特判的形式,通过if用两部分代码来进行实现全部情况,但是这里有一个更为简洁的方式就是令tail = n + 1。
因为主要问题在于小于0的被开方数是小于开放结果的,所以导致我们head和tail往相反的两边跑,使得不能逼近开方结果,所以只要先令tail大于1也就可以使得head和tail从两侧逼近开方结果,下面结果与c语言的sqrt()函数有些许的小出入,是我们定义的精度问题。可以让精度更小就更贴近真实值。
在这里插入图片描述

牛顿法

在计算机中实现sqrt()函数,开方运算的实际上就是使用的牛顿法,在这里我们也实现一下。
如下图所示,以求 4 \sqrt4 4 为例子,我们定义一个函数
f ( x ) = x 2 − 4 f(x) = x^2 - 4 f(x)=x24
求出f(x)的零点,大于0的 x 1 x_1 x1即为平方结果,首先就是我们可以在函数上取 ( 4 , f ( 4 ) ) (4, f(4)) (4,f(4))的点然后通过求导得出该点切线的斜率,通过点斜式的方法求出切线方程 l 1 l_1 l1,然后求出 l 1 l_1 l1的零点,求得 x 1 x_1 x1然后将 x 1 x_1 x1带入到原函数 f ( x ) f(x) f(x)中求得第二点 ( x 1 , f ( x 1 ) ) (x_1, f(x_1)) (x1,f(x1))在通过之前的方法求出 x 3 x_3 x3,如此往复使得, x n x_n xn不断逼近所求平方根,当 x n − 0 < E S P L x_n - 0 < ESPL xn0<ESPL时, x n x_n xn即为所求

#include<stdio.h>
#include<math.h>
#define EPSL 1e-6

double y(double x, double n){
    return x * x - n;
}
double partial_y(double x){
    return 2 * x;
}

double newton(double (*F)(double, double), double (*f)(double),double n){
    double x = 1.0;
    while(fabs(F(x, n)) > EPSL){
        x -= F(x, n) / f(x);
    }
    return x;
}

int main(){
    double n;
    while(~scanf("%lf", &n)){
        printf("mysqrt(%lf) = %lf\n", n, newton(y, partial_y,n));
        printf("sqrt(%lf) = %lf\n", n, sqrt(n));
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1506480.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统笔记(进程)

注&#xff1a; 下面图片资源来源于 王道计算机考研 操作系统 1.进程概念 进程&#xff08;process&#xff09;&#xff1a;是动态的&#xff0c;是程序的一次执行过程&#xff08;同一程序多次执行&#xff0c;会产生多个进程&#xff09;程序&#xff1a;是静态的&…

Linux:锁和线程同步的相关概念以及生产者消费者模型

文章目录 加锁的基本原则死锁死锁的概念死锁的条件 线程同步生产者消费者模型模型的理解 理解cp问题条件变量 本篇总结的是关于Linux中锁的相关概念以及生产者消费者模型 加锁的基本原则 加锁的基本原则&#xff1a;谁加锁谁解锁&#xff0c;不要把加锁和解锁这样的操作放在两…

Java线程池ThreadPoolExecutor源码阅读

文章目录 概述线程池提交任务流程线程池提交任务源码阅读 源码阅读属性字段工作线程worker 线程池方法runWorker(Worker w) 运行工作线程getTask() 获取任务tryTerminate() 尝试终止线程池interruptWorkers、interruptIdleWorkers 中断工作线程reject(Runnable command) 拒绝任…

数组名的理解,看这一篇就够了!!!

&#xff01;&#xff01;&#xff01;以下是会涉及到的知识的讲解&#xff1a; 一&#xff1a;数组名的理解&#xff1a; 数组名是数组首元素的地址&#xff0c;但是有2个例外&#xff1a; 1. sizeof(数组名)&#xff0c;这里的数组名表示整个数组&#xff0c;计算的是整个…

LeetCode59:螺旋矩阵Ⅱ

题目描述 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]] 代码 class Solution { public:vector…

查看pip当前关联python版本及位置

好久没用python了&#xff0c;把各种pip指向的环境忘光光啦&#xff0c;这里记录一下查看pip当前关联的python版本及位置的方法&#xff1a; pip -V结果&#xff1a; 我一般不用这个版本的python&#xff0c;去环境变量看了一下&#xff0c;原来是anaconda的Scripts自带pip&a…

Vue class和style绑定:动态美化你的组件

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

EB tersos 24.0.1 添加MCU模块失败

1、问题&#xff1a; 新建工程&#xff0c;添加MCU模块总是失败&#xff0c;错误信息如下&#xff1a; 2、解决方案 创建工程时只保留Resource模块&#xff0c;直接点击Finish&#xff0c;其他模块之后再添加 在工程创建成功后再单独添加需要的模块

StableDiffusion3 官方blog论文研究

博客源地址&#xff1a;Stable Diffusion 3: Research Paper — Stability AI 论文源地址&#xff1a;https://arxiv.org/pdf/2403.03206.pdf Stability.AI 官方发布了Stable diffusion 3.0的论文研究&#xff0c;不过目前大家都沉浸在SORA带来的震撼中&#xff0c;所以这个水…

力扣530. 二叉搜索树的最小绝对差

思路1&#xff1a;中序遍历&#xff0c;递归排序成有序数组&#xff1b;因为是有序&#xff0c;只需要求相邻两个值的最小差值。 class Solution {ArrayList <Integer> list new ArrayList();int ans 100001;//题目最大 100000public int getMinimumDifference(TreeNo…

docker学习笔记——Dockerfile

Dockerfile是一个镜像描述文件&#xff0c;通过Dockerfile文件可以构建一个属于自己的镜像。 如何通过Dockerfile构建自己的镜像&#xff1a; 在指定位置创建一个Dockerfile文件&#xff0c;在文件中编写Dockerfile相关语法。 构建镜像&#xff0c;docker build -t aa:1.0 .(指…

异步编程实战:使用C#实现FTP文件下载及超时控制

博客标题: 异步编程实战&#xff1a;使用C#实现FTP文件下载及超时控制 如果你的函数不是async&#xff0c;你仍然可以实现相同的超时功能&#xff0c;但你将不得不依赖更多的同步代码或使用.Result或.GetAwaiter().GetResult()来阻塞等待任务完成&#xff0c;这可能导致死锁的风…

Breach-2.1

靶场环境说明 该靶场是静态IP地址&#xff0c;需要更改网络配置&#xff0c;攻击机kali做了两张网卡&#xff1b; 信息收集 # nmap -sT --min-rate 10000 -p- 192.168.110.151 -oN port.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-09 10:47 CST Stats: 0:00:…

java通过poi-tl生成word

我看公司之前做电子合同&#xff0c;使用TIBCO jaspersoft做的报表模板&#xff0c;如果是给自己公司开发或者给客户做项目&#xff0c;这个也没有什么&#xff0c;因为反正模板是固定的&#xff0c;一次性开发&#xff0c;不用担心后续的问题。即使后期有调整&#xff0c;改一…

深入解读 Elasticsearch 磁盘水位设置

本文将带你通过查看 Elasticsearch 源码来了解磁盘使用阈值在达到每个阶段的处理情况。 跳转文章末尾获取答案 环境 本文使用 Macos 系统测试&#xff0c;512M 的磁盘&#xff0c;目前剩余空间还有 60G 左右&#xff0c;所以按照 Elasticsearch 的设定&#xff0c;ES 中分片应…

总结:Spring创建Bean循环依赖问题与@Lazy注解使用详解

总结&#xff1a;Spring创建Bean循环依赖问题与Lazy注解使用详解 一前提知识储备&#xff1a;1.Spring Bean生命周期机制&#xff08;IOC&#xff09;2.Spring依赖注入机制&#xff08;DI&#xff09;&#xff08;1&#xff09;Autowired注解标注属性set方法注入&#xff08;2&…

面具安装LSP模块时提示 Unzip error错误的解决办法

面具(Magisk Delta)安装LSP模块时提示 Unzip error错误的解决办法 ​​ 如果前面的配置都正常的话&#xff0c;可能是LSP版本有问题重新去Github下载一个最新版的吧&#xff1b;我是这么解决的。 我安装1.91那个版本的LSP就是死活安装不上&#xff0c;下载了1.92的版本一次就…

Golang-channel合集——源码阅读、工作流程、实现原理、已关闭channel收发操作、优雅的关闭等面试常见问题。

前言 面试被问到好几次“channel是如何实现的”&#xff0c;我只会说“啊&#xff0c;就一块内存空间传递数据呗”…所以这篇文章来深入学习一下Channel相关。从源码开始学习其组成、工作流程及一些常见考点。 NO&#xff01;共享内存 Golang的并发哲学是“要通过共享内存的…

⭐每天一道leetcode:83.删除排序链表中的重复元素(简单;链表遍历、删除经典题目)

⭐今日份题目 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例1 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2] 示例2 输入&#xff1a;head [1,1,2,3,3] 输出&#xff1a;[1,2,3] …

Linux 进程程序替换

&#x1f493;博主CSDN主页:麻辣韭菜-CSDN博客&#x1f493;   ⏩专栏分类&#xff1a;http://t.csdnimg.cn/G90eI⏪   &#x1f69a;代码仓库:Linux: Linux日常代码练习&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Linux知识   &#x1f51d;&#x1f5…