系列前言
本系列是博主自己学习算法知识的笔记。博主会尽最大可能把复杂的问题算法问题讲的通俗易懂,既为看官,也为自己,虽学无止境,望精益求精。
本篇预备知识
经典算术题
用程序解决算术题是我们计算机被发明出来的初衷。让我们从基础的算数应用开始,将我们所学的知识都用起来吧!
计算 n!
知识补充: n!读作“n的阶乘”,意思是从1一直乘到n,得到的结果就是n!
例如:3!= 3 × 2 × 1 = 6; 5! = 5 × 4 × 3 × 2 × 1 = 120
思路分析:
n!中需要的数字是公差为1的等差数列,然后每次都执行的同样的乘法操作,优先考虑使用循环结构(我用的是for循环)来解决这道题
思路转化成代码:
首先是n的阶乘,那我们就先需要一个变量从键盘接收这个n。我们实际上就是需要计算1×2×3× … ×n,可以从1开始,每次乘比它自己大1的数,所以我们需要一个循环变量来改变每次乘的值,需要另一个变量来记录累乘以后的值,最后输出累乘以后的值
给出这种方法的代码(从1乘到n):
#include<stdio.h>
int main()
{
int i = 0;
int n = 0;
int ret = 1;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d", ret);
}
我们还可以不计算1×2×3× … ×n的值,反过来计算n × … × 3 × 2 × 1,这样的话我们for循环的初值就是n,for循环的调整部分就是i- -(i=i-1)
给出这种方法的原码(从n乘到1):
#include<stdio.h>
int main()
{
int i = 0;
int n = 0;
int ret = 1;
scanf("%d", &n);
for (i = n; i > 0; i--)
{
ret *= i;
}
printf("%d", ret);
}
计算 1!+ 2!+ 3!+ … + n!
思路分析:
这道题其实是在第一题的基础上增加了一些东西,上一题我们已经通过代码计算出了n!,现在需要我们求出1!+2!+3!+ … +n!,可以看到,整个式子中出现的数字又是一个公差为1的等差数列,而且如果我们把1!、2!、n!看成一组确定数,那其实就是重复的进行加法运算。整体的程序逻辑框架同样考虑循环结构(我用的是for循环)。由于上一题计算n!的时候也是用了循环,所以我们可能会出现循环里面套循环的情况,也就是循环嵌套
思路转化成代码
同样,先思考我们需要哪些变量。上一题中需要的变量都不能动(键入的n、循环变量i、记录累乘的变量ret)。由于又来了一个循环,所以需要一个新的循环变量j;然后每次计算出的n!要累加到一个数上,它的结果作为整个式子的结果,所以需要一个累加量sum。程序的思路就是for循环里面套for循环,内部的循环负责计算n!,外部的循环负责把每次计算的结果累加。由于每次计算累乘的时候都需要从1开始,所以在每次内循环执行前先把ret初始化
给出原码:
#include<stdio.h>
int main()
{
int i = 0;
int j = 0;
int n = 0;
int ret = 1;
int sum = 0;
scanf("%d", &n);
for (j = 1; j <= n; j++)
{
ret = 1;//一定要在计算n!前将ret初始化为1
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d", sum);
return 0;
}
算法优化:
思路一代码的效率不高,主要原因是使用了循环嵌套增加了代码的时间复杂度,我们看看一层循环能不能解决问题。
改进思路:
由于每次计算出前一个n!时,(n+1)!其实就是(n+1)*(n!),也就是说我们可以直接把上一次计算好的阶乘数拿来用,不必要再从头算起了。操作就是把每次循环中初始化ret的语句删除,这样每次上次计算好的ret都可以拿来用了
给出原码:
#include<stdio.h>
int main()
{
int i = 0;
int n = 0;
int ret = 1;
int sum = 0;
scanf("%d",& n);
for (i = 1; i <= n; i++)
{
ret *= i;
sum += ret;
}
printf("%d", sum);
return 0;
}
这样的算法优化我们自己想不一定能想到,博主也是看别人的学来的。编程的世界很大,作为初学者很多知识都是沉淀了很久的,在精力有限的情况下,我们既要培养自己独立思考的能力,也要学会模仿学习,避免重复造轮子的情况。
计算两数的最大公约数
思路分析:
约数就是因数。最大公约数的定义是“相同且最大的因数”。既然是因数,那它就肯定不会超过两个数中的任何一个数,既然是公因数,那它就得同时能被两数整除,既然是最大公因数,我们可以从大往小遍历各个数,第一个出现的公因数就是最大公因数
思路转化成代码:
既然是两个数,那么先得有两个变量从键盘接收这两个数的值;然后我们需要比较出较小的那个数,把它的值作为我们遍历的起点,就需要第三个中间变量来完成换值的操作。所以我们程序应当分两步走,先把两个数中较小的那个数的值取出,然后从这个值开始从大到小遍历,找出第一个公因数
给出原码:
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
int c = 0;
scanf("%d %d", &a, &b);
if (a > b)
{
c = a;
a = b;
b = c;
}
int i = 0;
for (i = a; i > 0; i--)
{
if (a % i == 0 && b % i == 0)
{
printf("%d", i);
}
}
return 0;
}
讲解几个点吧:
if (a > b)
{
c = a;
a = b;
b = c;
}
这个代码块的作用是保证a中存储的一定是小值,b中的一定是大值。整体的思路就是:如果a<b,什么都不执行,如果a>b,就交换a和b的值。
for (i = a; i > 0; i--)
这个语句的作用是用小的数往下遍历
if (a % i == 0 && b % i == 0)
{
printf("%d", i);
}
这个代码块的作用是判断i是不是两个数的公约数,是的话就打印出来
算法优化:
计算最大公约数的一个古老的问题,它有一个非常经典且高效的算法:辗转相除法
为好学的小伙伴给出原理:
辗转相除法的原理
我们直接来看这个方法是怎么使用的:
给出两数a、b,欲求两数最大公约数。设c=a%b,若c等于0,则c为两数最大公约数,若c不等于0,则设d=b%c,若d等于0,则d为两数最大公约数,若d不等于0,则设e=c%d … 也就是说,每次都让三个数中第二小的数去对最小的数取模,直到模为零,第二小的数就是最大公约数。
给出原码:
#include<stdio.h>
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
int t = 0;
while (t = m % n)
{
m = n;
n = t;
}
printf("%d", n);
}
这种算法还有一个好处就是不需要比较两个数的大小,因为如果是用小的数对大的数取模,算法就会自动交换两个数。
会计算最大公约数以后我们如何计算两个数的最小公倍数呢?
这里给出一个用最大公约数计算最小公倍数的思路:
假设a、b的最大公约数是c,最小公倍数是d a×b=a/c×c×b/c×c=(a/c×b/c×c)×c=d×c
即:两数相乘=两数的最大公约数×两数的最小公倍数
打印闰年
网上对闰年的定义有很多,我取最常见的一种来说:
闰年即可以被4整除的,但不能被100整除的,又可以被400整除的公历年份
思路分析:
我们可以遍历1000-2000的所有数,然后对每个数都审查一下,符合条件的打印出来,就是闰年
思路转化成代码:
遍历1000-2000的所有数,这里需要使用循环结构;然后判断每个数是不是同时满足这三个条件,这里需要用到选择结构;由于是多重复合条件,可能需要用到逻辑表达式
#include<stdio.h>
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
printf("%d ", year);
}
}
打印素数
素数就是质数,因数只有1和它自己的数就是素数
思路分析:
同样是属于在一个范围内找出满足特定条件的数的问题。思路就是遍历每个数然后做判断。这里的判断是因数只有1和它自己,那我们就让因数从2开始增加,如果第一个因数就是它自己,说明它就是素数
思路转化成代码:
遍历所有数需要循环结构;检查素数需要循环结构来试验因数和选择结构来判断第一个因数是不是它自己
给出原码:
#include<stdio.h>
int main()
{
int i = 0;
int num = 0;
for (i = 100; i < 200; i++)
{
for (num = 2; num <= i; num++)
{
if (i % num == 0)
break;
}
if (num == i)
printf("%d ", i);
}
return 0;
}
算法优化一阶:
我们考虑一个常识性的问题:偶数是不是素数?答案是否定的。所以我们可以直接遍历100-200间的奇数
#include<stdio.h>
int main()
{
int i = 0;
int num = 0;
for (i = 101; i < 200; i+=2)
{
for (num = 2; num <= i; num++)
{
if (i % num == 0)
break;
}
if (num == i)
printf("%d ", i);
}
return 0;
}
算法优化二阶:
再考虑一个问题,如果a×b=c,那a和b就同时是c的因数,我们把a和b叫做“一对因数”,那么,这一对因数中较小的数就不可能超过这个数的平方根。
所以我们判断因数的范围就不需要从2开始遍历到这个数了,只需要遍历到这个数的平方根即可
计算平方根的函数是sqrt函数,sqrt(a)就是a的平方根。使用sqrt函数需要引用头文件math.h
给出原码:
#include<stdio.h>
#include<math.h>
int main()
{
int i = 0;
int num = 0;
int a = 0;
for (i = 101; i < 200; i+=2)
{
for (num = 2; num <= sqrt(i); num++)
{
a = 0;
if (i % num == 0)
a++;
if (1 == a)
break;
}
if (0 == a)
printf("%d ", i);
}
return 0;
}
二分查找法
查找法最基础的应用就是在一个有序数组中找到指定数。
比如我们有一个1-10的有序升序数组,我们需要把7找出来并且输出到屏幕上
顺序查找法
思路分析:
我们可以直接从前往后遍历数组,把每个数和7做比较,相等的那个数就是7
思路转化成代码:
存储有序数组需要用到数组。遍历需要用到循环。比较是否等于7需要用到选择结构做判断。打印需要printf.
顺序查找原码:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 7;
int i = 0;
for (i = 0; i < 10; i++)
{
if (arr[i] == k)
{
printf("%d", arr[i]);
}
}
return 0;
}
二分查找法
有的小伙伴要问了:我都有一种查找法了,我为啥还要学另一种呢?我们来想象这样一个有序数组,它是从1开始到232(约等于43亿),如果我们从中随便找一个数,比如3267467238,用顺序查找法我们需要查找3267467238次
如果我们使用二分查找法呢?答案是无论是什么数,至多只需要32次就能查找出来。是不是突然感受到算法的作用了呢?
二分查找法的思路和原码讲解:
二分查找法关键在“二分”二字,它的思路是每次直接取出这个有序数组的最中间的那个数,然后让它和我们指定查找的数做比较,如果这个中间数小了,说明比中间数小的所有数(包括这个中间数)都不是指定数,可以直接排除,所以剩下的就只剩值为“中间数+1”的数到最大数这个范围内的数;如果这个中间数大了,说明比中间数大的所有数(包括这个中间数)都不是指定数,可以直接排除,所以剩下的就只剩值为“中间数-1”的数到最小数这个范围内的数;如此往复的比较,循环夹逼,终有一次,会发现中间数就是我们需要找的数,那么我们就找到了这个数
二分查找原码:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
int left = 0;
int right = sz - 1;
while (left<=right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
printf("找到啦!数字是:%d\n", arr[mid]);
break;
}
}
if (left > right)
{
printf("没找到,是不是数字超出范围了呢\n");
}
return 0;
}
我们来逐句理解一下:
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
定义一个有序数组
int k = 7;
定义我们需要查找的那个数
int sz = sizeof(arr) / sizeof(arr[0]);
计算数组元素的个数(数组元素总长度/数组中每一个元素的长度)
int left = 0;
int right = sz - 1;
定义左右下标,初始化为整个数组的左右下标(数组下标从0开始)
while (left<=right)
由于是升序数组,当左下标小于等于右下标时进入循环
来看看循环体:
int mid = (left + right) / 2;
定义中间数
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
printf("找到啦!数字是:%d\n", arr[mid]);
break;
}
这段对应前文的“二分查找法思路”
if (left > right)
{
printf("没找到,是不是数字超出范围了呢\n");
}
这段代码实际上增加了程序的健壮性(健壮性的意思就是程序可以应对各种情况不至于因为bug崩溃)。如果我们的指定数不在数组中,就找不到。这是由于当指定数不在数组中时,程序从左右夹逼到最后仍然找不到数就会出现交叉的情况,这个情况就说明没找到数,指定数应该是超出范围了。
二分查找法将查找的速度从线性级别提升到了指数级别,意义重大,希望小伙伴们重点掌握。
第一期到此为止。算法能力是优秀程序员的基础素养,虽然枯燥,但是避无可避。人类许多伟大的成就都是来自于日常的坚持,加油兄弟!