通过本次实验,掌握蛮力算法的基本思想。
实验环境:
硬件:PC机
软件:windows操作系统,C语言
实验内容:
求Smith数:若一个合数的质因数分解式逐位相加之和等于其本身逐位相加之和,则称这个数为Smith数。给定一个正整数N,求大于N的最小Smith数。
实验学时:1
实验过程:
1.算法设计
1. 定义一个名为ifSmitch的函数,它接收一个整数参数n。
函数内部初始化一个变量sum为1,因为任何数至少有1作为其因数。
使用for循环从2遍历到n/2,检查每一个数i是否为n的因子(即n能否被i整除,即n % i == 0)。
如果i是n的因子,将其加到sum中。
循环结束后,检查sum是否等于n。如果是,则说明找到了满足条件的数,返回1;否则返回0。
2. 在main函数中,
首先读取用户输入的任意一个起始整数(在此例中未使用,直接从1开始查找)。
初始化一个循环标志变量i为1,使用while循环不断自增n,并调用ifSmitch函数检查n是否满足条件。
当找到满足条件的数时(即ifSmitch(n)返回1),将循环标志i设置为0,跳出循环。
最后输出满足条件的这个自然数n。
2.程序清单
#include<stdio.h>
int ifSmitch(int n){
int i ;
int sum = 1;
for(i = 2 ; i <= n/2 ; i++){
if(n%i==0){
sum+=i;
}
}
if(sum == n){
return 1;
}
return 0;
}
int main()
{
int n;
scanf("%d",&n);
int i =1;
while(i==1){
n++;
if(ifSmitch(n)==1){
i = 0;
}
}
printf("%d\n",n);
}
3.复杂度分析
(1)时间复杂度
ifSmitch函数的时间复杂度主要取决于内部的for循环。循环条件是i <= n/2,这意味着循环体将被执行大约n/2次(忽略边界情况,当n为偶数时实际执行次数少一次)。在循环体内,每次迭代都有一个if判断语句,其复杂度为O(1)。因此,整个for循环部分的时间复杂度为O(n)。
然而,main函数中有一个while循环,该循环持续增加n的值,直到找到满足条件的数为止。理论上讲,若要找到这样一个数,所需时间无法预估,因为这依赖于具体的数值分布。但针对每一次调用ifSmitch(n)函数的过程而言,其时间复杂度为O(n)。考虑到实际情况,这个while循环有可能运行很久,但我们通常关注的是单次调用ifSmitch函数的平均时间复杂度,所以在最坏的情况下,对于未知的终止条件,我们不能准确给出while循环的整体时间复杂度。
- 空间复杂度
ifSmitch函数的空间复杂度为O(1)。因为它仅使用了固定数量的局部变量(如i和sum),这些变量的数量并不随着输入n的增大而变化。
main函数中也没有动态分配额外的存储空间,所以其空间复杂度也是O(1)。
综上所述,ifSmitch函数的时间复杂度为O(n),空间复杂度为O(1);main函数中涉及while循环的部分,就单次调用ifSmitch函数而言,其时间复杂度为O(n),而整个main函数的空间复杂度同样为O(1)。但由于while循环的终止条件不确定,实际上找到符合条件的数所需的实际步数可能会很大,所以整个程序的实际运行时间难以确定。
- 运行结果
实验内容:
给定一个整数数组A=(a0,a1,…,an-1),若i<j且ai>aj,则<ai,aj>就为一个逆序对,例如数组(3,1,4,5,2)的逆序对有<3,1>,<3,2>,<4,2>,<5,2>。设计一个穷举算法求A中的逆序对的个数。(分别用基本蛮力算法和递归蛮力算法实现)
实验学时:1
实验过程:
- 算法设计
1. 算法一(upsetDown1):采用了基本蛮力算法(也称为双重循环遍历法)。首先定义一个变量sum用于统计逆序对的数量。通过两层循环遍历数组,外层循环从第一个元素开始,内层循环从当前外层元素的下一个元素开始。在内层循环中,如果发现当前外层元素大于内层元素,则逆序对数量加1。最后输出逆序对的总数。
2. 算法二(upsetDown2):采用递归蛮力算法。同样是为了统计逆序对的数量,这里的逻辑与第一种方法相似,但以递归方式进行。函数接收四个参数:数组、数组大小、当前逆序对计数器sum和当前处理到的数组下标i。当递归到达数组末尾时(即i == n),返回当前的sum值。在每次递归调用中,都会遍历从当前元素arr[i]之后的所有元素,进行比较并累加逆序对数量,然后递归地处理下一个元素。
在main函数中:
首先获取用户输入的数组元素个数。
动态分配内存存储数组元素。
接收用户输入的数组元素。
调用upsetDown1函数计算并输出逆序对数量。
再次调用upsetDown2函数,并将结果存入s,然后输出递归方式得到的逆序对数量。
2.程序清单
#include<stdio.h>
#include<stdlib.h>
//基本蛮力算法
void upsetDown1(int arr[],int n){
int sum = 0;
int i,j;
for( i = 0 ; i < n ; i++){
for(j = i+1 ; j < n ; j++){
if(arr[i]>arr[j]){
sum++;
}
}
}
printf("逆序对的个数为%d\n",sum);
}
//递归蛮力算法
int upsetDown2(int arr[] , int n, int sum ,int i){
if(i == n){
return sum;
}
int j;
for(j = i+1 ; j < n ; j++){
if(arr[i]>arr[j]){
sum++;
}
}
i++;
upsetDown2(arr,n,sum,i);
}
int main()
{
int n;
printf("请输入正数数组元素个数\n");
scanf("%d", &n);
int* arr = (int*)malloc(n * sizeof(int));
printf("请输入数组元素\n");
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
upsetDown1(arr , n);
int sum = 0;
i=0;
int s = upsetDown2(arr , n , sum , i);
printf("逆序对的个数为%d\n",s);
return 0;
}
3.复杂度分析
(1)时间复杂度
先假设两个函数都是遍历一个大小为n的数组并对每一对元素执行某种操作。
1. 对于upsetDown1函数,其时间复杂度为O(n^2)的推导如下:
假设有一个长度为n的数组,外层循环会对每个元素执行n次操作。
内层循环则对应于剩下的n-1个元素,总共进行了 n * (n - 1) 次操作。
因此,当n趋于无穷大时,基本操作次数的增长速度与 n^2 成正比,即时间复杂度为( O(n^2)。
2. 对于upsetDown2函数,虽然采用了递归的形式,但实质上每次递归也是处理了两个子序列,所以同样进行了两两元素的比较,时间复杂度同样是 O(n^2)。
(2)空间复杂度
依旧是先假设两个函数都是遍历一个大小为n的数组并对每一对元素执行某种操作。
1. upsetDown1函数的空间复杂度为O(n),是因为除了输入数组之外,没有额外创建数组或其他数据结构来存储中间结果,所以空间需求固定为输入数组所占的空间。
2.upsetDown2函数的空间复杂度同样为O(n),这是因为递归调用需要使用系统栈来保存每一层递归的信息,最坏情况下递归栈的深度可达n,即最多同时存在n个函数调用帧,每个帧可能包含若干局部变量等信息,故空间复杂度也是线性的,为 O(n)。
4.运行结果
实验总结:
设计并实现求解Smith数和计算数组逆序对个数的两种蛮力算法后,我深入理解了基础算法设计与实现的重要性,同时也意识到蛮力法在处理小规模问题时简洁直观,但在面对大规模数据时效率低下。尤其是对于求Smith数的问题,由于未利用数论性质优化,可能导致搜索时间过长。对于逆序对问题,虽然递归蛮力算法结构巧妙,但并未减少时间复杂度,依然为O(n^2)。
今后努力方向:在算法设计上,将进一步探索和学习更高效的方法,如动态规划、分治策略以及哈希表等数据结构的应用,以提高算法效率。此外,针对Smith数问题,可研究如何结合质因数分解和数字特性来优化算法。在实践中,要注重理论与实践相结合,提升算法优化意识,以便在实际场景中灵活运用合适算法解决问题。