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

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

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

  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));
    }
}

相关推荐

  1. 函数(C语言)

    2024-03-14 15:44:07       39 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-14 15:44:07       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-14 15:44:07       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-14 15:44:07       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-14 15:44:07       20 阅读

热门阅读

  1. C#引用C++dll

    2024-03-14 15:44:07       18 阅读
  2. CDQ分治+树状数组,LOJ6270. 数据结构板子题

    2024-03-14 15:44:07       19 阅读
  3. SpringBoot线程池的使用和扩展

    2024-03-14 15:44:07       18 阅读
  4. 力扣80:删除排序数组中的重复项 II

    2024-03-14 15:44:07       19 阅读
  5. AI辅助研发:2024年科技与工业领域的新革命

    2024-03-14 15:44:07       24 阅读
  6. Redis的快速入门【全方位进攻】

    2024-03-14 15:44:07       20 阅读
  7. mac笔记本执行定时任务

    2024-03-14 15:44:07       20 阅读
  8. Rust 的 inline 内联编译策略

    2024-03-14 15:44:07       18 阅读
  9. Elastic script_score的使用

    2024-03-14 15:44:07       17 阅读