考研C语言程序设计_编程题相关(持续更新)

目录

一、程序设计经典编程题(C语言实现)

T1 求1~100的奇数

解法1 暴力求解:
在这里插入图片描述


解法2: 思考思考 只要知道第一个奇数 后面的+2+2+2不就行了?
在这里插入图片描述


总结:

  1. 解法1 循环一百次 判断一百次
  2. 解法2 循环五十次 不需要判断
  3. 初步领略一下算法的魅力 要多思考
  4. 类似的 下面这道题就可以从3开始打印 每次+3+3+3 而不需要遍历 判断 再打印了
    在这里插入图片描述
  5. 还有一种思路:
    在这里插入图片描述

T2 求n!

  1. 本题很简单 就是累乘
  2. 但是我眼睛瞎起来就把ret定义为0 答案恒为0了

参考代码:

	int n = 5;
	int i = 0;
	int ret = 1;//别写成0了
	for (i = 1; i <= n; i++)
		ret *= i;
	printf("%d\n", ret);

T3 求1!+2!+3!+…+10!

解法1:

  • 思路:两层循环 第一层循环产生1~10的数据 第二层循环求出1! 2! 3!..并累加
  • 注意这种写法 每次求单独求阶乘的时候 ret一定要置为1
    在这里插入图片描述
  • 这种解法不够好 因为有大量的重复计算 将在解法2改进
	int n = 3;//求1! + 2! + 3!
	int i = 0;
	int j = 0;
	int ret = 1;//每次求单独的阶乘的时候就用它
	int sum = 0;
	for (int j = 1; j <= n; j++)
	{
		ret = 1;//所以要记得置为1啊!! 要不然第求下一次阶乘的时候 ret就不是从1开始乘的了
		for (i = 1; i <= j; i++)
			ret *= i;
		sum += ret;
	}
	printf("%d", sum);

解法2:

  • 思路:其实在计算10!的时候 已经出现了1! 2! 3! … 9! 既然如此 就可以一边计算 一边累加 避免了重复计算
  • 解法2更好 是经过思考的
int main()
{
	int n = 10;
	int i = 0;//控制循环
	int ret = 1;//用于产生1! 2! 3!...10!
	int sum = 0;
	for (i = 1; i <= n; i++)
	{
		ret *= i;//在产生10!的过程中 其实已经产生了1! 2! 3!...
		sum += ret;//边乘边加
	}
	printf("%d ", sum);
	return 0;
}

T4 在一个有序数组中查找具体的某个数字n(二分查找)

解法1 直接暴力求解:
在这里插入图片描述


解法2 二分查找:

  1. 前提是:有序 所以不能乱给测试数组
  2. right = mid-1; left = mid+1; 我一开始没有+-1 会导致死循环
  3. int right = len - 1; -1别忘记了 right是下标
  4. 出循环之后有两种写法 可以利用flag 也可以直接判断

写法一:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,11 };//测试数组 一定是有序的
	int len = sizeof(arr) / sizeof(int);//数组元素个数
	
	int k = 11;//查找目标
	int flag = 0;//假设一开始默认找不到k 找到了就把第flag置为1
	
	int left = 0;
	int right = len - 1;
	int mid = 0;
	// = 也是可以取的 当left=right的时候 肯定还要进去判断一次
	while (left <= right)
	{
		//每次进循环 都要算出一个新的mid进行判断
		mid = (left + right) / 2;
		if (arr[mid] < k)
		//说明k在mid的右边
		{
			left = mid + 1;
		}
		else if (arr[mid] > k)
		//说明k在mid的左边
		{
			right = mid - 1;
		}
		else
		{
			//这里已经可以输出mid的信息了 但是我放在下面打印
			//把提示信息统一都放在循环外处理
			flag = 1;
			break;
		}
	}
	//程序执行到这 有两种可能
	//1.while全都判断完 自然结束 也就是最终没找到k 此时flag = 0
	//2.flag = 1之后 break到这里来
	if (flag)
		printf("找到了下标是:%d\n", mid);
	else
		printf("没找到");
	return 0;
}

写法二: 主要差异在于出循环之后 我怎么输入"没找到"比较合适

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
	int num = 0;
	while (2)
	{
		printf("输入你要查到的数字:\n");
		scanf("%d", &num);
		int left = 0;
		int right = (sizeof(arr) / sizeof(int))-1;
		int mid = 0;
		while (left <= right)
		{
			mid = (left + right) / 2;
			if (arr[mid] > num)
				right = mid-1;
			else if (arr[mid] < num)
				left = mid+1;
			else
			{
				printf("找到了 下标是%d\n", mid);
				break;
			}
		}
		//走到这里 有两种情况 1.找到了break出来 2.没找到 但是left不<=right 循环结束了
		//我直接判断一下此时mid对应的元素是不是num就能确定找没找到了 不需要flag也行
		if (arr[mid] != num)
			printf("没找到\n");
	}

	return 0;
}

T5 编写代码,演示多个字符从两端移动,向中间汇聚

题目描述:
打印hello world!
############
h##########!
he########d!
hel######ld!
hell###r#ld!
hello##orld!
hello world!


解法1:

  1. 双"指针" 一左一右 开始打印有效字符
  2. 每次肯定都是打印十个字符 打印几次由while来决定
  3. left和right把10个要打印的字符分成三部分 [0,left]打印对应的有效字符 [right,len-1]打印对应的有效字符 剩余部分打印#
  4. 相较于思路2 更节省空间

错因:

  1. 打印#的判断条件写错了 左右不分??!! 开区间(left,right)范围打印#
  2. 每一次for循环结束 不要忘记++和-- 也不要忘记换行
#include<stdio.h>
#include<windows.h>
#include<string.h>
int main()
{
	char ch[] = "hello world!";
	int len = strlen(ch);//12
	int left = 0;
	int right = len - 1;
	int i = 0;

	while (left<=right)
	{
		for (i = 0; i < len; i++)
		{
			if (i > left && i < right) //易错
				printf("#");
			else
				printf("%c", ch[i]);
		}
		printf("\n");
		Sleep(1000);
		left++;
		right--;
	}
	return 0;
}

解法2:

  1. 给出两个数组 一个是字符 一个是# 注意#和字符的个数肯定都是一样的
  2. 然后也是用了两个指针 一左一右依次把字符赋给放#的那个数组
  3. 打印的时候 用%s专门只打印#的那个数组即可

错因:

  1. len–>求的是元素个数 也就是字符个数 故下标最大值是len-1
  2. 判断部分是<= 当左右指针指向同一个元素的时候 也要进入循环的
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<windows.h>
#include<string.h>
int main()
{
	char ch1[] = "zhuchenyang66dds6!";
	char ch2[] = "******************";
	int len = strlen(ch1);
	int left = 0;
	int right = len - 1;
	for (left = 0, right = len - 1; left <= right; left++, right--)
	{
		ch2[left] = ch1[left];
		ch2[right] = ch1[right];
		printf("%s\n", ch2);
		Sleep(100);
	}
	return 0;
}

或者写成while循环 都是一样
Sleep()的头文件是windows.h
system()的头文件是stdlib.h
在这里插入图片描述

T6 模拟用户登录(三次机会)

题目描述:
编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码则提示登录成,如果三次均输入错误,则退出程序。


解法1 利用strcmp函数:

  1. 定义要输入的密码 正确密码 count是还剩下几次机会
  2. 只要count还没==0 就可以进去输入密码

我的错因:

  1. 一开始用的是下图这种定义方式 这样定义的话 其实数组大小是确定的 数组里放了一个空格 一个\n 元素个数(strlen)是1 大小(sizeof)是2字节 下面输入密码肯定会越界的

在这里插入图片描述
2. 比较字符串是否相等 不能用== 要用strcmp()函数 第一个参数比第二个参数大 就返回>0 相等的话返回0
在这里插入图片描述

#include<stdio.h>
#include<string.h>
int main()
{
	char pswd[100] = " ";
	char answer[100] = "zhukefu123";
	int count = 3;

	while (count>0)
	{
		printf("您还有%d次机会,请输入密码:\n",count);
		scanf("%s", pswd);
		if (strcmp(pswd, answer) != 0)
		{
			printf("密码错误!\n");
			count--;
			continue;
		}
		else
		{
			printf("密码正确!\n");
			break;
		}
	}
	if (count == 0)
		printf("您没有机会了!!\n");
	return 0;
}

解法2 直接暴力求解 相当于模拟实现了strcmp:

  1. 这里我犯了一个离谱的错误 多打了一个; 也就是这个if 啥也不干! 然后继续往下执行!
    在这里插入图片描述
  2. 下图这部分写错了 左闭右开的话 右边就是循环次数 也就是字符串长度 是不需要-1的 -1会导致少判断最后一个字符 也就是zcygst667也被算成密码正确了
    在这里插入图片描述
  3. char ch[10] = " "; 10给的太小了!!! 要么限定用户只能输入多少的字符(一般交给前端限制) 要么给大点 否则 如果我测试的时候输入了一个zcysdshjadhshkd 会有栈溢出err
  4. 这个错误算是值得注意的点 我要是用flag这种写法的话 每次给你一次新机会输入密码的时候 flag都应该重置为1!!!! 否则在以下情况会出现问题: 当我第一次输入zcygst667 进入for的时候 把flag置为0 如果我后面输入了正确密码zcygst666 虽然不会执行flag=0 但是for循环出来走到if (flag == 1)的时候 发现flag上次已经给他置为0了 所以不进入if 直接继续下一次循环了 仿佛跳过了这次正确的输入
  5. 主要就三个点的错误(下图已经修正)
    在这里插入图片描述
int main()
{
	int total = 8;
	int i = 0;
	char ch[1000];
	char real[1000] = "zcygst666";
	int flag = 1;
	while (total > 0)
	{
		flag = 1;
		scanf("%s", ch);
		if (strlen(real) == strlen(ch))
		{
			for (i = 0; i < strlen(ch); i++)
			{
				if (ch[i] != real[i])
				{
					printf("错误\n");
					total--;
					flag = 0;
					printf("还剩%d次机会\n", total);
					break;
				}
			}
			//程序走到这这里 
			//1.break出来的  flag已经是0
			//2.判断发现全相等 循环自然结束 flag仍然是1
			if (flag == 1)
			{
				printf("正确\n");
				break;
			}
		}
		else
		{
			printf("两个字符串长度都不一样!!错误\n");
			total--;
			printf("还剩%d次机会\n", total);
		}
	}
	
	return 0;
}


  • 其实我想说的是 针对解法2那个flag置为1的错误 还是要具体问题具体分析的 具体看自己的思路是什么 灵活编写代码
  • 比如下面的代码(一开始给flag初始化的是0 密码正确再改为1) 就不需要每次都置为1 所以要多写多练多反思 该置为1的时候 我自己心里有数就行
int main()
{
	int i = 10;
	char real[20] = "zcygst";
	char input[20];
	int flag = 0;
	while (i > 0)
	{
		scanf("%s", input);
		if (strcmp(real, input) != 0)
		{
            //密码错误 那就不要动flag 本身就是0!
			i--;
			printf("err还剩%d次机会\n", i);
		}
		else
		{
            //输对了才改为1
            flag = 1;
			printf("yes\n");
			break;
		}
	}
    //1.要么是break到这里的时候 此时flag为1
    //2.要么就是一次都没输对 不满足i>0跳到这里来 此时flag一直保持是0
	if (flag == 0)
		printf("你没机会了\n");
	return 0;
}

T7 输入三个数 并从大到小输出

思路:
确定三个数了 用排序似乎有点小题大做了
其实比较三次即可 保证a放的最大 c放的最小 然后打印abc即可
而且三次比较都要交换 不如封装成一个函数 但是注意这个函数就必须要传地址了

#include<stdio.h>

void swap(int* x,int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int a = 1;
	int b = 1;
	int c = 1;
	printf("请输入三个数:>\n");
	scanf("%d %d %d", &a, &b, &c);
	//目标:让a里放最大 b其次 c最小
	//前两个if可以保证a最大 第三个if让c最小
	if (a < b)
		swap(&a, &b);
	if (a < c)
		swap(&a, &c);
	if (c > b)
		swap(&b, &c);
	//从大到小输出
	printf("%d %d %d", a, b, c);

	return 0;
}

T8 打印100~200的素数(质数)

思路:

  1. 素数只能被1和本身整除 如果能找到一个非1非本身的因数 就可以判断不是素数
  2. 判断i是不是素数的时候 可以从闭区间[2,i-1]试除
  3. 优化1:只要试除[2,根号i] 比如16 如果有因数 一定是成对出现的 比如1-16 2-8 4-4 如果只看一半的话 肯定是不会大于根号16的(≤)
  4. 优化2:偶数肯定不是素数 直接从101开始 每次i+2 从奇数里面找素数
    在这里插入图片描述
  5. double sqrt (double x);需要math.h的头文件 不放心的话循环里也可以强转成int
  6. 以上的方法 都叫"试除法" 推荐博文:<<素数求解的N种境界>>
#include<stdio.h>
#include<math.h>
int main()
{
	int i = 0;
	int j = 0;
	int flag = 1;
	//产生100-200的素数
	for (i = 100; i <= 200; i++)
	{
		//每次产生的i 开始都先认为他是个素数
		flag = 1;
		//这里是<=啊 写<的话 49也被判断成素数了
		for (j = 2; j <= (int)sqrt(i); j++)
		{
			if (i % j == 0)
			{
				flag = 0;//那就不是素数
				break;//只要有一次能进来 就直接break了
			}
		}
		//走到这儿来 可能是
		//1.break到这儿来 此时flag=0 
		//2.循环条件结束 此时flag仍未1 也就是i%j一次都不等于0 没机会把flag置为0 说明是素数
		if (flag)
			printf("%d ", i);
	}
	return 0;
}

T9 打印1000~2000之间的闰年

1. 能被4整除 但是不能被100整除的 是闰年
2. 能被400整除 也是闰年
3. 其实可以i+=4 从1.2.的规则来看 闰年肯定能被4整除

	int i = 0;
	for (i = 1000; i <= 2000; i++)
	{
		if ((i % 400 == 0) || ((i % 4 == 0) && (i % 100 != 0)))
			printf("%d ", i);
	}

注意了 如果要用if来写的话
就是俩个if(并列关系 都需要判断的)
而不能是else if(非此即彼 只有一个入口)
如果写的是else if 2000本身是闰年 但是判断他不是 因为只能进去第一个if
在这里插入图片描述

T10 求最大公约数(含递归)

解法1:暴力求解
两个数的最大公约数的范围肯定是[1,较小值]
比如18和6 最大公约数不可能超过6 所以从1到6找就行

	int m = 18;
	int n = 24;
	//假设m和n的较小值为最大公约数
	int min = m > n ? n : m;
	//然后从较小值开始 往下试除
	int i = 0;
	for ( i = min; i >= 1; i--)
	{
		if ((m % i == 0) && (n % i == 0))
			break;
	}
	printf("%d", i);
  • 下图是while的写法
    在这里插入图片描述

解法2:辗转相除法:
主要用到的一个结论就是:(a,b)的最大公约数等于(b,a%b)的最大公约数
最大公约数:即两个数据中公共约数的最大者。
思路:
例子:18和24的最大公约数
第一次:a = 18 b = 24 c = a%b = 18%24 = 18 再使:a = 24 b=18
第二次:a = 24 b = 18 c = a%b = 24%18 = 6 再使:a = 18 b = 6
第三次:a = 18 b = 6 c=a%b = 18%6 = 0 循环结束 此时的b就是最大公约数
下面有两种不同的风格写法 建议写第二种

int main()
{
	int m = 18;
	int n = 24;
	int k = m % n;
	while (k != 0)
	{
		m = n;
		n = k;
		k = m % n;
	}
	printf("%d", n);
	return 0;
}
int main()
{
	int a = 18;
	int b = 24;
	int c = 0;
	//当某一次 c = a%b = 0  为假了 就不进去循环了 此时b就是最大公约数
	while(c=a%b)
	{
		a = b;
		b = c;
	}
	printf("%d\n", b);
	return 0;
}


解法3:递归法
可以看出 求18 24->求24 18->求18 6(递归的出口)
递归的关键在于能否找到子问题

#include<stdio.h>

void IsCommon(int a, int b)
{
	if (a % b == 0)
	{
		printf("最大公约数是%d ", b);
		return;
	}
	else
		IsCommon(b, a % b);
}

int main()
{
	int a;
	int b;
	
	while (1)
	{
		scanf("%d %d", &a, &b);
		IsCommon(a, b);
	}

}

补充:辗转相除法的证明

详细的证明过程可以参考:
辗转相除法原理

注意除了参考视频里的两个结论,具体实现证明还需要引入一个公理:整数a,b的最大公约数d1一定大于or等于整数a,b,c的最大公约数d2的(当c有一个约数恰好是d1时取等号)
在这里插入图片描述

T11 求最小公倍数

设两数的最大公因数=X
则:两数可以表示为:aX,bX(a,b一定互质)
所以aX,bX的最小公倍数=abX=(aX*bX)/X
即最小公倍数=两数之积/最大公约数

那么也可以进一步证明出:两个数的乘积 = 最大公因数与最小公倍数的乘积
aX*bX = X*(abX)=(aX)*(bX)

代码思路:
暴力求解的话和前面类似 不过这次是拿两者较大的值开始试
比如6和8的最小公倍数 就用[8,6*8]的范围试
或者先求出最大公约数 然后最小公倍数就 = 两数之积/最大公约数
注意:如果用int,最小公倍数可能会溢出 建议结果用long long保存

T12 打印99乘法口诀表

就提几个注意点:

  1. %数字d 不要写成数字%d
  2. -2是左对齐两位(就记成按照我平时的习惯 就喜欢写-2左对齐) 2是右对齐两位
  3. 不要乱用对齐 这里前两位乘数就不可能有两位 不需要对齐 只有结果可能是两位 在这里插入图片描述

这就是 %2d的样子 是右对齐
在这里插入图片描述

int main()
{
	for (int i = 1; i <= 9; i++)
	{
		for (int j = 1; j <= i; j++)
		{
			printf("%d*%d=%d ", i, j, i * j);
		}
		printf("\n");
	}
	return 0;
}

T13 不排序 求十个整数中的最大值

思路:

  1. 定义一个数组 放10个整数
  2. 假设第一个元素是最大值max
  3. 然后循环 让max和第二个开始的元素一一比较 如果遇到比max大的就重新赋值给max

错因:

  • max应定义为arr[0] 而不是一个具体的数 比如max=0 如果arr都是负数呢?
  • 在数组里挑一个最大的 肯定是以数组里元素的为标杆 不能随便找一个标杆 在我们班找一个最能打的 能找泰森作为标杆吗?
  • 如果是自己输入的数据 那max一定要等输入完再定义为arr[0]
    在这里插入图片描述

T14 求1/1-2/1+1/3-4/1…-100/1

思路:
虽然下面两种想法代码写出来一模一样 但是思考方式不一样

  1. 分子恒为1 产生正负交替的分母 累加即可:sum += 1.0 / (i*symbl);
  2. 分子恒为1 分母每次递增1 让整体的计算结果正负交替:sum += symbol * (1.0 / i);
    在这里插入图片描述
  • 注意:1.0默认是double 计算出来的sum也就是double
    如果定义的是float double->float会报警告 最好double sum 然后用%lf打印
    在这里插入图片描述

思路很多 还可以分两部分计算
奇数项是+ 偶数项是-
在这里插入图片描述

T15 计算给定数组中多个数的最大公约数

问题描述:
前面只会求两个数的最大公约数 那怎么求一组数的最大公约数呢?


思路:
M=aX N=bX 且ab互质 则gcd(M,N)=X
若再gcd(X,K)=z
则gcd(M,N,K)=z

在这里插入图片描述

第1次循环:求gcd(0,1) result放的是 0和1的最大公约数
第2次循环:求gcd(0和1的最大公约数,2) result放的是 0和1和2的最大公约数
第3次循环:求gcd(0和1和2的最大公约数,3) result放的是 0和1和2和3的最大公约数

int gcd(int m, int n)
{
	int c = 0;
	//当某一次 c = a%b = 0  为假了 就不进去循环了 此时b就是最大公约数
	while (c = m % n)
	{
		m = n;
		n = c;
	}
	return n;
}

int main()
{
    int arr3[] = { 26, 39, 52, 65, 78, 91 };
    //用于存放结果
    int result = arr3[0];

    for (int i = 1; i < 6; i++)
    {
        result = gcd(result, arr3[i]);
    }

    printf("%d ", result); // 输出最大公约数
    return 0;
}

这是我写的代码
看似利用了数组 循环次数也少了 但属实没必要呀!
微不足道的提升 而且不如前面的代码好理解!!
除非他题目真的要求 不准新建变量 倒确实可以这么写
自己画图看看的话 其实两个代码的思路是完全一样的 前面的思路更清晰!!!
在这里插入图片描述

T16 求整数1-100中数字9出现的次数

错因:
下面的错误在于 个位和十位 都要被判断到
假如是99 进去第一个if 判断个位是9 ++之后 就不会进入else if了
直接开始判断100 少判断了一个99的十位9
if elseif else 是一个整体 只有一个入口
而这道题就是要每一位都作为一个入口 每一位都要判断一下
在这里插入图片描述
写||也不对 99还是只判断了一次
在这里插入图片描述

两种情况都需要判断的情况 不能用if elseif
if - if 符合要求 个位要判断一下 十位也要判断一下:

int main()
{
	int count = 0;
	for (int i = 1; i <= 100; i++)
	{
		if (i / 10 == 9)
			count++;
		if (i % 10 == 9)
			count++;
	}
	printf("%d", count);
	return 0;
}

T17 递归实现n的k次方(n,k都是整数 考虑k是负数)

思路:

  1. 任何数的0次方都为1
  2. 2的10次方 = 2*(2的9次方) = 2*2*(2的8次方) = 2*2....*(2的1次方)
    则递推方程 (n,k) = n*(n,k-1)
  3. 因为是return 所以下图加不加else 其实是一样的
    在这里插入图片描述
    在这里插入图片描述
  1. 一个瑕疵:n,k是整数 为什么不考虑负整数?
    在这里插入图片描述
  2. 既然这样 当k=0 return 1.0 整体也更好
    不仅1.0*其他数会提升成double 1.0也和返回值的double对应了
    在这里插入图片描述
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
double myPow(int num,int k)
{
	if (k == 0)
		return 1.0;
	//如果k是负数 那就是返回-k次方的倒数
	else if (k < 0)
		return 1.0 / myPow(num, -k);
	else
		return num * myPow(num, k - 1);
}

int main()
{
	printf("%.3f ", myPow(2, -3));
	printf("%.3f ", myPow(2, -2));
	printf("%.3f ", myPow(2, -1));
	printf("%.3f ", myPow(2, 0));
	printf("%.3f ", myPow(2, 1));
	printf("%.3f ", myPow(2, 2));
	printf("%.3f ", myPow(2, 3));
	printf("%.3f ", myPow(2, 4));
	printf("%.3f ", myPow(2, 5));
	return 0;
}

T18 递归求一个正整数的每一位数字的和

题目描述:
在这里插入图片描述

思路:
大问题:(1729)
拆成子问题 = (172) + 9 = (17) + 2 + 9 = (1) + 7 + 2 + 9

int DigitSum(int num)
{
	if (num <= 9)
		return num;
	else
		return num % 10 + DigitSum(num / 10);
}
int main()
{
	printf("%d\n", DigitSum(1234));
	printf("%d\n", DigitSum(1729));
	return 0;
}

*T19 字符串逆序(不是逆序打印 是真的逆序)

题目描述:

  • 不让使用库函数就是想让你自己写一个strlen 在C语言专栏函数章节已经介绍过
  • 唯一要注意的是参数已经给你定死了 只有一个char*
  • 注意:这里只是训练一下递归 其实这道题用递归大可不必!!
    因为这道题逼着我用递归 逼着我只有一个参数 所以才写出下面比较复杂的思路的
    在这里插入图片描述

先把递归的正确答案和思路写在前面 后面也介绍了其他思路:

  1. 按照C语言专栏函数部分说的那样 第一步我先假设出函数功能 假设reverse(char str) 能逆序一个字符串*
  2. 然后开始拆分自己问题 首先交换收尾字符 然后reverse(中间剩余字符)
    此时bug出现了: 如下图 abcdef\0->fbcdea\0 再传一个str+1进去 子问题成了逆序bcdea了
    也就是说 在程序眼中
    从str+1往后看 看到的是bcdea\0 处理的是bcdea无法准确处理我们想象中的那个子问题(bcde)
    因为我只有一个参数str 如果说我用str代表一整个字符串的话 肯定要去找\0 受到\0的约束!!
    在这里插入图片描述

不要慌 这说明子问题拆分的还不够彻底 所以重新拆分:

  1. 假设出函数功能 假设reverse(char* str) 能逆序str~\0之间的一个字符串

  2. “不要彻底交换”:
    把首字符暂存给tmp
    用末字符覆盖首字符
    然后先不把tmp的首字符给直接给末字符 而是把\0给末字符 构造一个正确的子问题

  3. 处理新的子问题 也就是reverse(掐头去尾的子串) 把中间的子串进行逆序

  4. reverse()完之后 别忘了用之前tmp暂存的首字符 去覆盖那个为了构造正确子问题而新增的'\0'
    其实也就是把tmp赋给原先末字符的位置

  5. 考虑递归的结束条件 if什么条件我才需要执行第三步的reverse(子问题)?
    显然:"\0"没有子问题 "a"没有子问题 "ab"有子问题!
    也就是下一个子问题的字符串的长度>=2
    也就是strlen(str+1)>=2

  6. 好好理解一下为什么是strlen(str+1)>=2
    其实写strlen(str)>=2也是对的 但是这只是恰好对 或者说并不严格符合前面的思路
    就拿"abc"来说 其实一上来先处理了ac了
    然后继续reverse子问题的时候 只要确保a和c中间的子串>=2 然后reverse(ac中间的子串)即可
    如果写的是strlen(str)>=2 画图会发现:
    最后一次的操作其实是reverse(b\0) 对字符b做了一系列没用的操作 仿佛让b原地跟自己换了个位置

再来理解一下过程 下面红色高亮的部分就是主要过程:
abcdef\0 这是原始字符串 经过初始的处理 构造出正确的子问题:fbcde\0\0 tmp暂存a
此时子问题就是reverse(bcde) 也就是reverse(str+1) 则处理这个子问题 即f reverse(bcde\0) \0 tmp暂存a
假设处理完了子问题reverse(bcde) 也就是:fedcb\0\0 tmp暂存a
还需要再把a给倒数第二个\0 才成了fedcba
下面的代码 其实就是按照这个思路写下去的

void reverse(char* str)
{
	//abcdef\0 -->fbcde\0\0  tmp = a
	char* begin = str;
	char* terminalFore = str + strlen(str) - 1;
	char tmp = *str;
	*str = *terminalFore;
	*terminalFore = '\0';

	//\0; a; ab; abc 需要
	//fbcde\0\0  tmp = a ---> fedcb\0\0 tmp暂存a
	if (strlen(str + 1) >= 2)
		reverse(++str);
	
	//fedcb\0\0 tmp暂存a --> fedcba
	*terminalFore = tmp;
}

int main()
{
	char ch[] = "abcdef";
	printf("%s\n", ch);

	reverse(ch);
	printf("%s\n", ch);
	return 0;
}

迭代法其实比递归更容易读懂
循环交换首字符和末字符就行

在这里插入图片描述


下面是我写的递归(假如不限制传参个数 就是这么简单 就不会像一开始那么复杂了):
但是题目限制了参数!! 严格来说我直接就是错的!!
我的思路也很简单:
倒置abcdef–>af交换+倒置bcde–>af交换+be交换+倒置cd
这个思路和前面第一次一开始上来的代码是一样的
只不过这里我传了两个参数 所以能正确找到子问题(也就是逆序中间的子串) 而不被\0所影响
错误1:reverse2(pleft++, pright–); 写成了后置的 这样会导致死递归(后置是先使用 这导致不会每次都接近出递归条件)
问题2:把交换写在了函数开始 如下图 (也就是一进去不管啥情况都做一下交换)
这种情况无法处理只有两个字符的子问题 因为会交换两次 相当于没交换
在这里插入图片描述

感觉这个递归和迭代没啥区别 迭代反而不容易错 不要为了递归而递归

void reverse2(char* pleft, char* pright)
{
	if (pleft < pright)
	{
		char tmp = *pleft;
		*pleft = *pright;
		*pright = tmp;
		reverse2(++pleft, --pright);
	}
}

int main()
{
	char ch[] = "abcdef";
	int sz = strlen(ch);
	char* right = ch + sz - 1;
	//reverse1(ch,sz);
	reverse2(ch,right);
	printf("%s\n", ch);
}

T20 不创建临时变量 模拟先实现strlen()

思路:不创建临时变量 只能用递归法(创建临时变量用迭代法其实更简单)
在这里插入图片描述

注意:不要忘记return 0
不写return 0 不仅是语法上的错误 更是逻辑上的错误
本题的递归出口就是:遇到\0 就要返回0了
也就说每次传参都离\0越来越近

参考代码:

int MyStrlen(char* ch)
{
	//hello 1+ello 1+1+llo 1+1+1+lo 1+1+1+1+o 1+1+1+1+1+0
	if (*ch == '\0')
	//递归出口
		return 0;
	else
		return 1 + MyStrlen(++ch);
}

int main()
{
	char ch[] = "hello";
	printf("%d ", MyStrlen(ch));
	return 0;
} 

思考:关于T19和T20++副作用的问题

首先肯定都不可以用后置++ 这是毫无疑问的
但是请思考:
T20可以写MyStrlen(++ch) 请问T19可不可以写成reverse(++str)? 也就是前置++?
答案是可以的 按照本文T19那段递归参考代码 完全是可以的

但是请看下图 如果这样写 还可不可以?
这就不可以了!!!
因为前面T19 其实在str++之后 再也没有用到str了
但是下图这种写法 我还要用str 我想要的是一开始的str 你不能把他++之后再给我!!
所以最好都写成str+1 ch+1
因为这一步传参只是希望传入ch或者str的下一个地址
并没有想改变他们本身的值
而++操作符本身是带有自增的副作用
在这里插入图片描述

T21 完成整型数组的初始化,打印,逆序 三个函数

题目描述:
在这里插入图片描述

思路:
跟逆序字符串很类似 所以这类问题用迭代绝对比递归简单的
前面逆序字符串只是逼着我们学习一下递归的思想
可以写成while循环 也可以写成for循环 完整代码在下面
在这里插入图片描述

参考代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void print(int* arr, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
		printf("%d ", arr[i]);
	printf("\n");
}

void init(int* arr, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
		arr[i] = 0;
}

void reverse(int* arr, int len)
{
	int left = 0;
	int right = len - 1;
	int tmp = 0;
	for (left = 0, right = len - 1; left < right; left++, right--)
	{
		tmp = arr[left];
		arr[left] = arr[right];
		arr[right] = tmp;
	}
}
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,0};
	int len = sizeof(arr) / sizeof(arr[0]);
	print(arr, len);

	reverse(arr, len);
	print(arr, len);

	init(arr, len);
	print(arr, len);

	return 0;
}

T22 打印菱形图案

题目描述+思路1:
每一行都打印13个元素 用if把要打印*的位置找出来 else就打印空格
在这里插入图片描述>头脑清醒 找对规律 就不会错
当然有很多种找规律的角度 这是我的思路
在这里插入图片描述

思路1参考代码: 注意看注释

int main()
{
	int line = 0;//上三角的高
	int i = 0;
	int j = 0;
	while (scanf("%d", &line) != EOF)
	{
		//上三角 line 7
		for (i = 0; i < line; i++)
		{
			for (j = 0; j < 2 * line - 1; j++)
			{
				//i=0 第一行:(line - 1) - 0   (line - 1) + 0  这个范围打印* 其余打印空格
				//i=1 第二行:(line - 1) - 1   (line - 1) + 1  这个范围打印* 其余打印空格
				//找规律:(line - 1) - i      (line - 1) + i
				//line -1 - i                 line -1 + i
				if ((line -1 - i) <= j && j <= (line -1 + i))
				{
					printf("*");
				}
				else
				{
					printf(" ");
				}
			}
			printf("\n");
		}


		//下三角 line-1=6
		for (i = 0; i < line - 1; i++)
		{
			for (j = 0; j < 2 * line - 1; j++)
			{
				//i=0 第一行:(line - 1) - 5   (line - 1) + 5  这个范围打印* 其余打印空格
				//i=1 第二行:(line - 1) - 4   (line - 1) + 4  这个范围打印* 其余打印空格
				//找规律:(line - 1) - [(line-1)-i-1]        (line - 1) + [(line-1)-i-1]
				//i+1                        2line-i-3
				if ((i + 1) <= j && j <= (2 * line - i - 3))
				{
					printf("*");
				}
				else
				{
					printf(" ");
				}
			}
			printf("\n");
		}
	}

	return 0;
}

思路2:
也是分成思路1的上下俩三角形 line也是上三角的高
但是 打印每一行的时候 找规律的思路不一样
下图是上三角
先打印空格 找空格的规律
再打印* 找*的规律
自己数数找规律 然后跟i关联起来就行
在这里插入图片描述
然后是下三角 同样先找空格的规律 再找*的规律
在这里插入图片描述


思路3:
第一次写想了很久的一个复杂化的思路:
我的思路是利用距离D 来找规律的 得到一个判断的不等式
代码写少了就会这样
太复杂了 我已经不想复盘了!!!
第二次写这道题 想到的就是思路一 已经满足了

参考代码:很复杂 不建议看

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>

int main()
{
	int i = 0;
	int j = 0;
	int line = 7;
	while (1)
	{
		scanf("%d", &line);
		//上半部分 1,2,3,4.....,line 正三角  有line行
		//列都是2 * line - 1列 每一列(空格+* 有这么多)
		for (i = 1; i <= line; i++)
		{
			for (j = 1; j <= 2 * line - 1; j++)
			{
				//打印*的条件:
				//1.当j和最中间的那个元素(也就是当j=line的时候)之间的距离 小于D的时候 打印*
				//2.发现D=i-1
				//3.j和line的距离表示为|j-line|
				//4.|j-line|<=i-1
				if ((line + 1 - i <= j) && (j <= line - 1 + i) || (j == line))
				{
					printf("*");
				}
				else
				{
					printf(" ");
				}
			}
			printf("\n");


		}

		//下半部分 行数比上半部分少1    
		//列都是2 * line - 1列 每一列(空格+* 有这么多)
		for (i = 1; i <= line - 1; i++)
		{

			//打印*的条件:
			//1.当j和最中间的那个元素(也就是当j=line的时候)之间的距离 小于D的时候 打印*
			//2.发现D+i=line-1---->D=line-i-1
			//3.j和line的距离表示为|j-line|
			//4.|j-line|<=line-i-1
			for (j = 1; j <= 2 * line - 1; j++)

			{
				if ((i + 1 <= j) && (j <= 2 * line - 1 - i) || (j == line))
				{
					printf("*");
				}
				else
				{
					printf(" ");
				}
			}
			printf("\n");
		}
	}
	return 0;
}

T23 判断是不是自幂数(水仙花数)

题目描述:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路:直接暴力求
最好把函数解耦 后面会解释原因
有以下两个错因:
计算几位数的时候 这里写成了num>9
吸取教训 以后类似的情况 举例子带进去算算就清晰了!!!
在这里插入图片描述
此外 pow的返回值类型是double 如果不想看到这个警告 可以强转一下
在这里插入图片描述

参考代码:

#include<math.h>

//计算num是几位数
int numOfDigit(int num)
{
	int count = 0;
	//153 1     15  2    1 3
	while (num > 0)
	{
		count++;
		num /= 10;
	}
	return count;
}

//判断
int isSelfPower(int num)
{
	int sum = 0;//求和
	int count = numOfDigit(num);//位数
	int flag = num;//保存原来num的值

	//1234  4       123   3       12 2     1 1
	while (num > 0)
	{
		sum += (int)pow(num % 10, count);
		num /= 10;
	}
	return sum == flag;
}

int main()
{
	int i = 0;
	for (i = 1; i <= 100000; i++)
	{
		if (isSelfPower(i))
			printf("%d ", i);
	}
	return 0;
}

把各个函数功能解耦 有什么好处?
如果全都写在同一个函数里 就会反复使用到同一个参数num
容易写出下图这种bug
如果解耦成不同的函数 就可以避免
在这里插入图片描述


一些细节问题:
一开始也看到了 0这个特殊的数 也是个自幂数
正常思路应该是 0是1位数 那么01=0 所以满足条件
虽然我给出的参考代码 没有严格按照规则去判断 但是可以完成任务
我的代码:当i=0的时候 sum=0 count=0 flag=0
直接rerun flag==sum 刚好满足条件 所以判断0也是自幂数
如果觉得这样不好 那就在isSelfPower里一上来就写一句
if(num == 0) return 1
也没问题的
在这里插入图片描述

T24 求Sn=a+aa+aaa+… 其中a是一个0~9的整数 共有n项

思路1:
2 + 22 + 222 + 2222 + 22222
2*(1+11+111+1111+11111)
()里是固定的一个数 把他求出来 然后乘上2就行了 (ret = sum * a;)

参考代码:

#include<math.h>
int main()
{
	int a = 0;
	int n = 0;
	while (scanf("%d %d", &a, &n) != EOF)
	{
		int sum = 0;//求1+11+111+1111.....
		int tmp = 0;
		int ret = 0;

		//求1+11+111+1111.....
		for (int i = 0; i < n; i++)
		{
			tmp = tmp * 10 + 1;
			sum += tmp;
		}

		ret = sum * a;
		printf("%d\n", ret);
	}

	return 0;
}

思路2:
找规律 其实就是把思路1一步到位了
在这里插入图片描述
在这里插入图片描述

T25 换汽水喝(两种思维)

题目描述:
其实感觉有歧义 小时候大家肯定都做过这个"脑筋急转弯"
最后有一个空瓶的时候 可以"赊账" 喝完再抵掉
但是本题不考虑"赊账" 简单点
后面会继续介绍一种思维方式:其实能得出真正意义上的最多能喝几瓶水(值等于能赊账的值)
在这里插入图片描述


思路1:
既然不考虑赊账 那直接暴力求解了
错因:
头脑不清晰 思路不明确 仿佛在迷信式写代码
改一下调试一下 不对继续改 思路完全不清晰
把while循环里的两句话先后都写反了 那就错了
其实就两个核心:换来喝->更新空瓶数量->换来喝->更新空瓶数量…
更新空瓶的思路也要清晰:上次没换完剩的瓶子+刚喝完几瓶 又新换了几瓶
如果觉得直接赋初值很突兀 可以这么写:
达到的效果是一样的
在这里插入图片描述

参考代码:

int main()
{
	//初始值直接给出
	int BottleSum = 20;
	int left = 20;

	//开始换气水喝
	while (left >= 2)
	{
		//换汽水 然后喝掉 把喝掉的瓶数加给sum
		BottleSum += (left / 2);

		//更新剩下的空瓶:1.上次没换完剩的+2.刚喝完换到手的多的
		//然后继续循环 再把更新后的空瓶换汽水喝
		left = (left % 2) + (left / 2);
	}
	printf("%d", BottleSum);

	return 0;
}

思路2:动脑思考
1元=1瓶水=2空瓶 则1空瓶=0.5元
那20元最多产生40个空瓶(价值交换) 也就是最多能喝40瓶水
事实上上述代码不考虑"赊账" 答案是39 假如考虑"赊账" 答案真的就是40
所以40才是理论上的最多值
或者也可以说自己找到了规律:2*money-1
其实这就是寻求算法求解的过程
在这里插入图片描述

T26 调整数组使奇数全部都位于偶数前面

题目描述:
在这里插入图片描述

思路1:双指针(不允许创建新的数组)
左边找偶数 右边找奇数 找到了就交换
下面是我第二次写还犯的错误 必须要注意
本质就在于 如果left<right 说明还没处理完
如果left=right了 说明全都处理完了 就不需要再继续了

其实 最下面的if 如果说不满足if 只有一种可能 就是left=right
多一个if 只是避免了自己跟自己交换这个没意义的动作 不加其实答案效果是一样的
在这里插入图片描述
在这里插入图片描述

思路1参考代码:

void Print(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
		printf("%d ", arr[i]);
	printf("\n");
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int len = sizeof(arr) / sizeof(arr[0]);

	Print(arr, len);

	int left = 0;
	int right = len - 1;
	int tmp = 0;
	
	while (left < right)
	{
		//找偶数
		while ((arr[left] % 2 == 1) && (left < right))
			left++;

		//找奇数
		while ((arr[right] % 2 == 0) && (left < right))
			right--;

		if (left < right)
		{
			tmp = arr[left];
			arr[left] = arr[right];
			arr[right] = tmp;
			left++;
			right--;
		}
	}
	
	Print(arr,len);

	return 0;
}

思路2:暴力

如果是 22222222222222222 复杂度很高 是O(n2)
如果是双指针 最坏的情况也就是遍历一遍数组 复杂度是O(n)

思路2参考代码:

void func(int* arr,int len)
{
	for (int i = 0; i < len; i++)
	{
		//发现一个偶数 你不应该在前面
		if (arr[i] % 2 == 0)
		{
			//从i+1开始 去找第一个出现的奇数 跟他交换
			for (int j = i+1; j < len; j++)
			{
				if (arr[j] % 2 == 1)
				{
					int tmp = arr[i];
					arr[i] = arr[j];
					arr[j] = tmp;
					break;
				}
			}
		}
	}
}

思路3:双指针的另一种视角
和下面的BC98可以说是一模一样的思路
只不过BC98在 “复用当前数组 直接覆盖INDEX就行”
这里 不能覆盖 而是交换
在这里插入图片描述

思路3参考代码:

void Print(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
		printf("%d ", arr[i]);
	printf("\n");
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int len = sizeof(arr) / sizeof(arr[0]);
	Print(arr, len);

	int i = 0;//遍历的指针
	int INDEX = 0;//有效位置--放奇数的

	for (i = 0; i < len; i++)
	{
		if (arr[i] % 2 != 0)//找有效数组-->找奇数
		{
			//不能直接覆盖 而是交换
			int tmp = arr[i];
			arr[i] = arr[INDEX];
			arr[INDEX] = tmp;
			INDEX++;
		}
	}

	Print(arr, len);
	return 0;
}

T27 预测比赛名次

题目描述:
在这里插入图片描述

思路:
核心就在于怎么用代码表示"只对了一半"
也就是一真一假 1和0 那就是1+0=1
其次怎么让名次唯一
让名次唯一:
我试了A+B+C+D+E==15 不行
我试了(A != B) && (B != C) && (C != D) && (D != E) 也不行
A * B * C * D * E == 120 这个值肯定是唯一的 彳亍!!

参考代码:

#include<stdio.h>

int main()
{
	int A = 0;
	int B = 0;
	int C = 0;
	int D = 0;
	int E = 0;

	//穷举所有名次可能
	for (A = 1; A <= 5; A++)
	{
		for (B = 1; B <= 5; B++)
		{
			for (C = 1; C <= 5; C++)
			{
				for (D = 1; D <= 5; D++)
				{
					for (E = 1; E <= 5; E++)
					{
						if (
							((B == 2) + (A == 3) == 1)
							&& ((B == 2) + (E == 4) == 1)
							&& ((C == 1) + (D == 2) == 1)
							&& ((C == 5) + (D == 3) == 1)
							&& ((E == 4) + (A == 1) == 1)
							&& (A * B * C * D * E == 120)
							)

						{
							printf("A:%d B:%d C:%d D:%d E:%d\n", A, B, C, D, E);
						}
					}
				}
			}
		}
	}
	return 0;
}

T28 找凶手

题目描述:
在这里插入图片描述

思路1:
ABCD四个人 每个人都有0或者1两种取值
0代表清白 1代表是凶手
四个人的描述都加起来 结果应该是3 因为有三个人说真话
但是凶手只有一个 所以A+B+C+D = 1

思路1参考代码:

int main()
{
	int A = 0;
	int B = 0;
	int C = 0;
	int D = 0;
	for (A = 0; A <= 1; A++)
	{
		for (B = 0; B <= 1; B++)
		{
			for (C = 0; C <= 1; C++)
			{
				for (D = 0; D <= 1; D++)
				{
					if (
						((A == 0) + (C == 1) + (D == 1) + (D == 0)) == 3
						&& (A + B + C + D) == 1
						)
					{
						printf("%d %d %d %d\n", A, B, C, D);

					}
				}
			}
		}
	}
	return 0;
}

思路2:
定义出killer 假设killer是A B C D
然后写出四个人对应的描述
加起来结果是3 看看哪个猜测是对的
其实这个代码更符合我们人的思考模式
在这里插入图片描述

T29 杨辉三角

题目描述:
在这里插入图片描述

思路:
先在数组里放入如下内容
第一列都是1 最右边都是1
其余的元素计算得出即可
然后打印的时候 做一个格式控制
//一共打印LINE(5)行
//第一行打印之前 先打印4个空格
//第二行打印之前 先打印3个空格
//第三行打印之前 先打印2个空格
//第四行打印之前 先打印1个空格
//第五行打印之前 先打印0个空格
在这里插入图片描述
在这里插入图片描述

参考代码:

#define LINE 5
int main()
{
	    //1          //4 
	   //1 1        //3   
	  //1 2 1      //2      
	 //1 3 3 1    //1     
	//1 4 6 4 1  //0     

	int arr[LINE][LINE] = { 0 };
	//放元素
	for (int i = 0; i < LINE; i++)
	{
		for (int j = 0; j <= i; j++)
		{
			if ((j == 0) || (i == j))
				arr[i][j] = 1;
			else
				arr[i][j] = arr[i - 1][j] + arr[i - 1][j - 1];
		}
	}
	//打印
	for (int i = 0; i < LINE; i++)
	{
		//先打印空格
		for (int k = 0; k < LINE - 1 - i; k++)
		{
			printf(" ");
		}
		//再打印有效元素
		for (int j = 0; j <= i; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

T30 杨氏矩阵

题目描述:
在这里插入图片描述

思路:
假设矩阵是
1 2 3
4 5 6
7 8 9
每次都以最右上角的那个元素为准(是一行里最大的 同时也是一列里最小的)
如果要找的元素比3大 那可以去掉一行
如果要找的元素比3小 那可以去掉一列
每次去掉1行/1列之后 都以新的右上角的元素为准 如此循环
最多N次 都可以遍历整个矩阵
既然右上角可以 那每次以左下角为准 也可以
但是左上角和右下角就不可以 无法直接去掉一行或者一列
&ROW和&COL不仅可以作为输入参数 还可以接收返回值 把返回值带出去
这是一种不错的设计思路

参考代码:

int find_num(int arr[3][4], int* pROW, int* pCOL, int tar)
{
	int ROW = 0;
	int COL = *pCOL - 1;
	while ((ROW <= *pROW - 1) && (COL >= 0))
	{
		//去掉一列
		if (arr[ROW][COL] > tar)
		{
			COL--;
		}
		else if (arr[ROW][COL] < tar)
		{
			ROW++;
		}
		else
		{
			*pROW = ROW;
			*pCOL = COL;
			return 1;
		}
	}
	return 0;
}

int main()
{
	int arr[3][4] = { {1,3,5,6}, {7,8,9,12}, {20,30,70,90} };
	int tar = 11;
	int ROW = 3;
	int COL = 4;

	int ret = find_num(arr, &ROW, &COL, tar);
	if (ret)
		printf("找到了:arr[%d][%d]\n", ROW, COL);
	else
		printf("没找到\n");
}

思考:如果不止一个目标数呢?
其实很简单
当找到了一个目标数 由于目标数所在的行和列都是单调递增的 所以不可能重复
那么直接跳过一行 并且跳过一列 继续寻找即可
在这里插入图片描述

参考代码:

int main()
{
	int arr[4][3] = { {1,2,3},{9,20,30},{20,30,40},{30,40,60} };
	int tar = 30;
	int ROW = 0;
	int COL = 2;

	while ((ROW <= 3) && (COL >= 0))
	{
		if (arr[ROW][COL] > tar)
		{
			COL--;
		}
		else if (arr[ROW][COL] < tar)
		{
			ROW++;
		}
		else
		{
			printf("找到了:arr[%d][%d]\n", ROW, COL);
			ROW++;
			COL--;
		}
	}
	return 0;
}

T31 字符串左旋

题目描述:
在这里插入图片描述


思路1:暴力法
假如是ABCD 左旋一个字符 就是BCDA
那么首先把第一个字符暂存 然后BCD往前挪 最后再把暂存的A放到最后
如果左旋的是两个 那么继续对B进行此操作
每次都把第一个字符暂存 然后后面的字符往前挪 最后把暂存的字符放在最后

思路1参考答案:

void move(char* arr, int k)
{
	//暴力法思路:
	//1.左旋1个 就是把第一个元素先存着 然后后面整体往前挪动 然后再把第一个元素放到最后
	//2.左旋k个 就是重复k次上面的动作
	int len = strlen(arr);
	k = k % len;
	for (int i = 0; i < k; i++)
	{
		//1.存第一个元素
		char tmp = *arr;
		//2.后面整体往前挪动
		for (int j = 0; j < len - 1; j++)
		{
			*(arr + j) = *(arr + j + 1);
		}
		//3.把第一个存的元素给最后位置
		*(arr + len - 1) = tmp;
	}
}

思路2:找规律 或者我认为这就是算法的力量
比如ABCD 我希望左旋两个 那就把ABCD分为AB CD
先把两部分分别逆序 得到BA DC
再把整体逆序
得到CDAB 就已经完成了左旋两个字符
同时也和暴力法一样 左旋n个和左旋n%4个是一模一样的
为了防止越界每次都需要%4

参考代码:

void reverse(char* start, char* end)
{
	char tmp = ' ';
	while (start < end)
	{
		tmp = *start;
		*start = *end;
		*end = tmp;
		start++;
		end--;
	}
}

int main()
{
	char str[] = "abcdefg";
	int len = strlen(str);

	int num = 0;
	printf("你要左旋几个字符?\n");
	scanf("%d", &num);
	num = num % len;

	reverse(str, str + num - 1);
	reverse(str + num, str + len - 1);
	reverse(str, str + len - 1);

	printf("%s", str);
	return 0;
}

T32 判断一个字符串是不是由另一个字符串旋转得到

题目描述:
在这里插入图片描述


思路1:暴力法
首先明确一点:这里说的是旋转 没有说明左旋还是右旋
仍然以ABCD为例
左旋一个:BCDA
右旋三个:BCDA
所以暴力的时候可以一律都当做左旋来处理
而这里的思路用暴力就很简单了
参考T31 每次左旋1个 然后判断一下 (字符串判断 一定要用strcmp) 循环字符串长度次
注意一个易错点:
在这里插入图片描述

参考代码:

#include<stdio.h>
#include<string.h>
void reverse(char* start, char* end)
{
	char tmp = ' ';
	while (start < end)
	{
		tmp = *start;
		*start = *end;
		*end = tmp;
		start++;
		end--;
	}
}

void left_move(char*str,int num)
{
	int len = strlen(str);
	num = num % len;

	reverse(str, str + num - 1);
	reverse(str + num, str + len - 1);
	reverse(str, str + len - 1);
}

//判断ch2是不是由ch1旋转得到
int is_move(char* ch1, char* ch2)
{
	if (strlen(ch1) == strlen(ch2))
	{
		for (int i = 0; i < strlen(ch1); i++)
		{
			//每次循环 左旋一个 然后判断
			left_move(ch1, 1);
			if (strcmp(ch1, ch2) == 0)
			{
				return 1;
			}
		}
		return 0;
	}

	else
	{
		return 0;
	}
}

int main()
{
	char ch1[] = "AABCD";
	char ch2[] = "BCDAA";
	if (is_move(ch1, ch2))
		printf("是的\n");
	else
		printf("不是\n");
	return 0;
}

思路2:利用库函数
首先要知道 假设是ABCD 那么ABCD旋转的所有可能性
无论是左旋还是右旋 都是ABCDABCD的一个子串
那就先给ABCD追加一个自己
然后判断是否是追加后的子串即可

参考代码:

#include<stdio.h>
#include<string.h>

int is_move(char* str1, char* str2)
{
	if (strlen(str1) != strlen(str2))
		return 0;
	//再往下 就能保证长度是相等的了
	strncat(str1, str1, strlen(str1));

	if (strstr(str1, str2) != NULL)
	{
		return 1;
	}

	//能走到这 就说明没有return 1 
	//也就是没有子串 那肯定就不是旋转所得
	return 0;
}

int main()
{
	char ch1[100] = "AABCD";//要追加 ch1空间给大点
	char ch2[] = "ADAAB";
	if (is_move(ch1, ch2))
		printf("是的\n");
	else
		printf("不是\n");
}

二、力扣

面试题 17.04. 消失的数字题目链接

  1. 解法一:首先^遵循交换律 且0^N=N N^N=0 把所有的元素都^起来 相同的都成0 剩下一个单独的 就是消失的数字 相当于找单身狗
  2. 解法二:既然题目描述里的n 其实应该等于numsSize 也就是我应该有的元素是0,1,2,3,4,n(numsSize) 把他们全都加起来去减数组的值 结果就是那个孤儿
  3. 解法二还可以优化 1-n求和直接用公式 都不需要遍历
//我的代码:
int missingNumber(int* nums, int numsSize) {
    int ret = 0;
    int sum = 0;
    for (int i = 0; i < numsSize; i++) {
        scanf("%d", &nums[i]);
        ret = ret ^ nums[i];
        ret = ret ^ i;
    }
    ret = ret ^ numsSize;

    //思路二:
    // for (int i = 0; i <= numsSize; i++) {
    //     sum += i;
    // }

    // for (int i = 0; i < numsSize; i++) {
    //     scanf("%d", &nums[i]);
    //     sum -= nums[i];
    // }
    return ret;
}

三、牛客网

BC49 判断两个数的大小关系

点我做题

  • 这题很简单 主要是学习一下牛客网多组输入的写法
  • 读取失败返回EOF 如果不是EOF那肯定读取成功
  • 循环条件也可以写成:while (scanf("%d %d", &n1, &n2) != EOF)
#include<stdio.h>
int main() {
    int n1 = 0;
    int n2 = 0;
    while (scanf("%d %d", &n1, &n2) == 2) {
        if (n1 == n2)
            printf("%d=%d\n", n1, n2);
        else if (n1 > n2)
            printf("%d>%d\n", n1, n2);
        else
            printf("%d<%d\n", n1, n2);
    }
    return 0;
}

BC84 计算y的值

点我做题

  • 那么有的朋友就要问了 这么简单的题放进来侮辱我的智商? 举报点踩拉黑了

  • 主要还是因为我第一次写的时候居然写了2个if1个else 一般是讨论单值函数的! 只有一个入口!
    在这里插入图片描述

  • 正确:x和y是一一映射关系 只有一个入口

#include <stdio.h>

int main() {
    int x;
    int y;
    while (scanf("%d", &x) != EOF) {
        if (x < 0)
            y = 1;
        else if (x == 0)
            y = 0;
        else
            y = -1;
        printf("%d\n",y);
    }
    return 0; 
}

BC101 班级成绩输入输出

点我做题

  • 注意点1:每次循环求和之前 sum置为0不要忘记了 否则第一位同学是对的 第二位同学的总分 是接着上一位的sum继续算的
  • 注意点2:下面如果写scanf(“%f”,&score); 就是错的 读取到的都是0
  • 对于scanf来说 double->%lf; float->%f
  • 对于printf来说 浮点数double和float都用%f是没问题的
  • 注意点3:看清题目 明明是保留1位小数了!! %.1f
    我写的参考答案:
    还是稍微有点投机取巧的 因为这是OJ 一边输入 一边输出 一边求和
#include<stdio.h>
int main()
{
    double score;
    double sum = 0;
    for(int i = 0;i<5;i++)
    {
        sum = 0;
        for(int j = 0;j<5;j++)
        {
            scanf("%lf",&score);
            printf("%.1f ",score);
            sum+=score;
        }
        printf("%f",sum);
        printf("%c",'\n');
    }
    return 0;
}

弄一个数组逻辑上更好:
下面这个代码还有错 输出那部分的while忘记给j++了 死循环了
在这里插入图片描述

BC23 时间转换

点我做题

思路1:分别算时分秒

  • 注意是3661s一共是几小时几分钟几秒 而不是分别转换成时分秒
  • 小时:3661/3600 看看里面有几个3600s 也就是几个h
  • 分钟:3661/60 看看里面有几个60s 也就是几分钟 再%60 因为满60分钟就要进位给h了 剩下的就是不满60分钟不能进位的 也就是分钟数
  • 秒:其实很简单 直接%60 或者/1%60 逻辑更统一 先看看有多少个1s 然后再%60 因为满60s就要进位给分钟了 剩下的就是秒数部分
#include <stdio.h>
int main() {
    int seconds = 0;
    scanf("%d", &seconds);
    printf("%d %d %d", 
    seconds / 3600, 
    seconds / 60 % 60, 
    seconds % 60);
    return 0;
}
#include <stdio.h>
int main() {
    int a;
   while (scanf("%d", &a) != EOF) {
       printf("%d %d %d", a / 3600, a / 60 % 60, a / 1 % 60);
   }
   return 0;
}

思路2:连着算

  • 一开始算小时数肯定都一样 区别在于后面算分秒的时候 是接着上次的结果算的
  • 3661%3600 就把能凑成1h的秒数都去掉了 剩下的秒数/60就是分钟数部分 %60就是秒数部分
  • 认为思路二更清晰
    在这里插入图片描述

BC50 计算单位阶跃函数

点我做题

  • 突然发现printf里可以直接打印字符串 总感觉以前都没注意到过
  • 可参考C语言复习第0章 4.7
    在这里插入图片描述

BC117 小乐乐走台阶

点我做题

本题在C语言专栏里的:C语言函数递归经典例题:汉诺塔和小青蛙跳台阶已经详细介绍过
这里主要再提供一种思考方式

  • 常见的思路就是跳n级 分为第一次跳1级和第一次跳2级
    在这里插入图片描述

  • 或者可以这么想:当我跳到了第n级 我可能是从第n-1级跳上来的 也可能是从n-2级跳上来的
  • 那么跳到第n级的所有跳法就是:跳到第n-1级的所有跳法+跳到第n-2级的所有跳法
  • 继续往下递推 可以一直推到:跳到第3级的所有跳法就是 跳到第2级的所有跳法+跳到第1级的所有跳法(已知)
  • 只是思路不一样 其实写出来的代码都是一模一样的

则代码思路:

  1. 定义Search(n)是搜索跳n个台阶 有多少种跳法
  2. 子问题Search (n-1)+Search(n-2)
  3. 出口是:n==1 n==2
#include <stdio.h>

int Search(a) {
    if ((a == 1) || (a == 2))
        return a;
    else
        return Search(a - 1) + Search(a - 2);
}

int main() {
    int a;
    while (scanf("%d", &a) != EOF) { 
        int ret = Search(a);
        printf("%d", ret);
    }
    return 0;
}

BC112 小乐乐求和(注意数据类型的格式)

点我做题

错因:
这题很简单 用累加或者求和公式都可以
就是要注意题目描述:输入一个正整数n (1 ≤ n ≤ 109) 这个数很大
求完和肯定超过int的范围了 所以要用足够大的整型 这里直接给long long
但是还是要注意:给了long long scanf和printf都要是%lld 在这里插入图片描述

补充:
在C语言里 用/2总是会很担心的 因为有可能是整数/ 可能不是我们想象中的除法
那么这题为什么可以直接/2呢?
也就是说 分子一定是偶数?
等差数列Sn = (a1+an)*n/2 = n*[2a1+(n-1)d]/2
即证n*[2a1+(n-1)d]是偶数
1.如果n是偶数 显然成立
2.如果n是奇数 n-1肯定是偶数 (n-1)d肯定是偶数 2a1+(n-1)d肯定还是偶数 显然也成立
在这里插入图片描述

BC114 小乐乐排电梯(考察细心)

点我做题

  • 这题一点也不难 完全就是考察细不细心的 是到楼上 不是等几分钟能坐上电梯
    在这里插入图片描述
    在这里插入图片描述

JZ15 二进制(补码)中1的个数

点我做题

首先说一下 之前一直写的是IO型 要从main函数开始写
这里是接口型 默认后台有main函数了 只需要实现一个函数/接口
实现它规定好的函数的功能即可

思路1:
最开始想到的思路
让判断目标num的最低位按位与上00000000000000000000000000000001
如果结果是1 就说明最低位是1 否则就是0
然后循环32次 每次右移 一共右移32次 让每一位都成为一次最低位
错因:
这题最容易犯的一个错误就是 ==的优先级是比&高的:
如果按照我的逻辑 应该是:
在这里插入图片描述
我忘记加黄色的括号了 但是牛客网给过了 为什么呢?
因为下面这个等价于if( (num>>i) & 1 ) 结果本来就只可能是0或者1
而且是1的时候 count恰好要+1 这是恰好蒙出了另一种写法
在这里插入图片描述
其次 一开始还把&写成了&& 低级错误

思路1参考代码:

int NumberOf1(int num) {
    int count = 0;
    int flag = 1;
    for (int i = 0; i < 32; i++) {
    //感觉这个flag也多余 直接((num >> i) & 1) == 1 好了
        if ( ((num >> i) & flag) == 1 )
            count++;
    }
    return count;
}

思路2:
由于牛客网帮我规定好了函数名 参数类型 返回值等 所以我想到了思路1
如果不受限制于参数类型 有没有投机取巧的办法呢?
那我可不可以这么思考:
假设传参传了一个-1 传过来肯定接收其补码 也就是32个1
但是我直接用unsigned int接收 把这32个1直接当成原反补同码的正数
那这道题就直接转成成求一个正整数的二进制形式了
可以类比求1234的十进制的每一位来思考这道题
一个是%10 /10 这里就是%2 /2
其实这里如果参数用int 那只能处理正整数
为了能处理负整数 才想到使用unsigned int来接收的
在这里插入图片描述
在这里插入图片描述

思路2参考代码:

int NumberOf1(unsigned int num) {
	int count = 0;
	while (num > 0)
	{
		if (num % 2 == 1)
			count++;
		num /= 2;
	}
	return count;
}

#include <stdio.h>
int main()
{
	printf("%d ", NumberOf1(-1));
	return 0;
}


思路3:非常的妙

比较难想到的思路
每次把最右边的1给消除 看看消除几次能全都成0 那就有几个1
n = n&(n-1)这个动作 就可以消除n右边起第一个1
n-1 其实就是让右边第一个1变成0(肯定是把最右边的一个1借给下一位了 变成0)
然后再&n 此时对n本身来说:右边第一个1再往右边 肯定都是0
所以 最终的n&(n-1)的效果就是让右边第一个1开始 全都变成0
比如:n=10000 n-1=10000-1=01111
10000&01111=00000
此方法效率高 有几个1 就循环几次
在这里插入图片描述

int NumberOf1(int n) {
    int count = 0;
    //n!=0 就说明至少有一位1
    //就进来循环
    while (n) {
        //消除1个1
        n &= (n - 1);
        //count就+1
        count++;
    }
    return count;
}

JZ15拖展1 判断某个正整数n是不是2的k次方

如果一个正整数可以写成2k 那他的二进制(原反补同码)中 一定只有1个1
可以用JZ15的函数 判断if(NumberOf1(n) ==1)
或者其实只需要1次判断:
if (num & (num - 1) == 0) 那就是的了 即消除1个1 就没有1了 在这里插入图片描述

JZ15拖展2 分别打印整数num二进制补码的奇数位和偶数位

  • 核心就是下面圈出来的地方
  • 但是要自己规定好哪些叫偶数位 是高权重开始数 还是低权重开始数
  • 同时也要注意打印的顺序 i是从0开始 还是从30开始
    在这里插入图片描述

JZ15拖展3 求两个int的二进制补码 有多少个位不同

题目描述:
在这里插入图片描述


解法1: 常规思路 直接看代码

int countDiffer1(int m, int n)
{
	int count = 0;
	for (int i = 0; i < 32; i++)
	{
		//一位一位比较
		if (((n >> i) & 1) != ((m >> i) & 1))
		{
			count++;
		}
	}
	return count;
}

解法2:利用JZ15的结论:

int NumberOf1(int n) {
	int count = 0;
	//n!=0 就说明至少有一位1
	//就进来循环
	while (n) {
		//消除1个1
		n &= (n - 1);
		//count就+1
		count++;
	}
	return count;
}

int countDiffer2(int m, int n)
{
	//^:相同为0 相异为1
	return NumberOf1(m ^ n);
}

BC54 获得月份天数

点我做题

思路1:
给我年和月 我必须先判断是不是闰年 才能给出当月有多少天
然后直接给出两个数组 保存闰年的十二个月的天数和平年的
但是由于给我月份是从1开始的 而数组的下标是从0开始的 故每个数组第一个元素置为-1 为了和月份统一

思路1参考代码:

#include<stdio.h>
void dayNum(int year, int month) {
    //闰年的二月多一天 所以稀有
    int LeapYear[] = {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int Year[] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if ((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))) {
        printf("%d\n", LeapYear[month]);
    } else
        printf("%d\n", Year[month]);

}
int main() {
    int year, moth;
    while (scanf("%d %d", &year, &moth) != EOF) {
        dayNum(year, moth);
    }
    return 0;
}

思路2:
其实没有必要定义两个数组
只有一个特殊情况:就是2月 闰年的2月有28+1=29天
只需要给出平年的数组 然后特殊对待一下闰年的二月即可

思路2参考代码:

#include<stdio.h>
void dayNum(int year, int month) {
    int Year[] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    //闰年需要特殊讨论一下二月
    if ((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))) {
        if (month == 2)
            printf("%d\n", Year[month] + 1);
        else
            printf("%d\n", Year[month]);
    } else
        printf("%d\n", Year[month]);

}

int main() {
    int year, moth;
    while (scanf("%d %d", &year, &moth) != EOF) {
        dayNum(year, moth);
    }
    return 0;
}
  • 这个思路不错 代码写成下面这样 是最棒的:
#include<stdio.h>
int dayNum(int year, int month) {
    int Year[] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = Year[month];

    //闰年需要特殊讨论一下二月
    if (((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))) && month == 2)
        day++;
    return day;
}

int main() {
    int year, moth;
    while (scanf("%d %d", &year, &moth) != EOF) {
        printf("%d\n", dayNum(year, moth));
    }
    return 0;
}

思路3:
利用switch的穿透 有点丑陋
直接看代码

思路3参考代码:

#include <stdio.h>
void DayNum(int year, int moth) {
    switch (moth) {
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
            printf("%d\n", 31);
            break;
        case 2:
            if ((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0)))
                printf("%d\n", 29);
            else
                printf("%d\n", 28);
            break;
        default:
            printf("%d\n", 30);
            break;
    }
}

int main() {
    int year, moth;
    while (scanf("%d %d", &year, &moth) != EOF) {
        DayNum(year, moth);
    }
    return 0;
}

这是另一种写法 只是引入了变量day:
在这里插入图片描述

BC90 矩阵计算

点我做题

思路:
这题还是很简单的 一边输入 一边判断 一边累加(没必要创建一个二维数组)
写在这就是想说:
写代码的时候一定要清醒 先理清楚思路
比如这里 m n谁是行 是谁列
谁在外层 谁在内层 然后再顺着思路写
就可以一气呵成
提示:如果非要创建数组 发现又不支持变长数组怎么办?
其实创建一个足够大的数组就行 然后只使用该数组的局部(n行m列)
在这里插入图片描述

参考代码:

#include <stdio.h>
int main() {

    int n, m;
    scanf("%d %d", &n, &m);
    int i = 0;
    int j = 0;
    int num  = 0;
    int sum = 0;
    for (i = 0; i < n; i++) {
        for (j = 0; j < m; j++) {
            scanf("%d", &num);
            if (num > 0)
                sum += num;
        }
    }
    printf("%d\n", sum);
    return 0;
}

BC111 小乐乐与进制转换

点我做题

我个人的一些理解:
这和求1234的每一位数字 是一模一样的道理
在这里插入图片描述

思路1:递归
其实一开始我是在思考用while循环的
但是我发现while似乎只能倒序打印每一个六进制位 所以就换了递归
错因:
不能加else 不是非此即彼的关系!!! 是子问题+打印第一位!!! 不是非此即彼!!!
还记得下图吗?在递归里讲的按顺序打印一个数的每一位
思路可以说是一模一样 千万不能写else
因为递归的思路就是大问题=子问题+打印个位
紧接着打印个位数字 两者不是if else的关系
在这里插入图片描述

思路1参考代码:

#include <stdio.h>
void sixFunc(int num) {
    if (num > 5)
        sixFunc(num / 6);
    printf("%d", num % 6);
}

int main() {
    int num = 0;
    scanf("%d", &num);
    sixFunc(num);
    return 0;
}

思路2:其实使用循环是可以做到的
虽然不能直接顺序打印出来 但是至少可以得到每一个六进制位
那么就先把他们暂存在数组里 然后倒着打印不就行了?
错因:
arr[i] = num % 6;必须写在i++前面
因为i是从0开始 统计的是元素个数
肯定是先使用下标0 再i++
换个角度理解 已经放了1个元素 个数i才需要++
其次最后倒着打印的时候 j是从i-1开始的 因为i代表元素个数
多画图 就不会因为下标出错
i可以直接理解成元素个数; 也可以根据循环 判断它最终会指向最后一个元素的下一个

思路2参考代码: 记得看注释

#include <stdio.h>

int main() {
    int num = 0;//输入一行
    scanf("%d", &num);

    int arr[10] = {0};//存六进制的每一个数字 但是是倒序
    int i = 0;//统计一共多少位数字

    //这里要写num>0 而不是>5
    //哪怕只剩1个1 也要再计算一次 只有当num=0的时候 才结束循环
    while (num > 0) {
        //放1个 加个1 i统计的是个数
        //而且arr[i] = num % 6;必须写在i++前面
        //因为i是从0开始的 i代表的是个数 放一个元素 才轮到i++
        arr[i] = num % 6;
        i++;
        num = num / 6;
    }
    //注意 统计的是个数 也就是数组元素个数
    //所以最大下标是i-1 而不是i
    for (int j = i - 1; j >= 0; j--)
        printf("%d", arr[j]);
    return 0;
}

这是思路2的另一种写法:
我不喜欢这么写 虽然看上去牛逼 但我是我还是觉得前面的写法可读性好一点
在这里插入图片描述


这里写一个提醒:
牛客网肯定是特殊处理过多组输入了
在这里插入图片描述

BC107 矩阵转置

点我做题

思路:
首先搞清楚 什么是矩阵转置
比如a12 本来他是第一行 第二列的元素
转置之后 他成为第二行第一列的元素
在这里插入图片描述
只要把转置的概念弄清楚
有了初始矩阵之后 按照转置的规则去打印这些元素就行
这种题就是建议多画图 多写写 把规律搞清楚了 内外层循环用什么控制 打印的下标是什么 谁在变化
在这里插入图片描述
错因:
虽然我搞清楚了 但是显然是昏了头 一开始打印和取地址的时候 居然没有写ij!!
在这里插入图片描述

参考代码:

#include <stdio.h>

int main() {
    int n = 0;//行
    int m = 0;//列
    scanf("%d %d", &n, &m);
    int arr[n][m];

    int i = 0;
    int j = 0;

    //输入
    for (i = 0; i < n; i++) {
        for (j = 0; j < m; j++) {
            scanf("%d", &arr[i][j]);
        }
    }

    //直接打印转置的结果
    for (i = 0; i < m; i++) {
        for (j = 0; j < n; j++) {
            printf("%d ", arr[j][i]);
        }
        printf("\n");
    }

    return 0;
}

BC98 序列中删除指定数字

点我做题

思路1:OJ题 可以投机取巧:
不是真的删掉 而是直接遍历打印
如果遇到想要删掉的元素 就不打印
数组本身是没有发生任何变化的

思路1参考代码:

#include <stdio.h>

int main() {
    int num = 0;
    scanf("%d", &num);

    int arr[num];
    for (int i = 0; i < num; i++)
        scanf("%d", &arr[i]);

    int tar = 0;
    scanf("%d", &tar);

    //直接打印出来 数组其实没有变
    for (int i = 0; i < num; i++) {
        if (arr[i] != tar)
            printf("%d ", arr[i]);
    }
    return 0;
}

思路2:双指针 把不需要删除的元素一个一个存起来 同时直接覆盖需要删除的元素
核心思路就在于 遇到要删除的元素 就不把该元素存入INDEX 而是继续往下循环 只有不是要删除的元素 才需要存入INDEX
比如1 2 3 4 4 4 5 6 想删除4 那么只要是4的位置 都可以"被有效元素覆盖"
这里用到两个指针
一个是遍历元素的指针i
一个是记录有效下标的指针INDEX(INDEX指向的位置 说明合法 可以把非删除元素放到INDEX里 放完之后++即可)
最终INDEX其实 恰好指向最后一个合法元素的下一位
换句话说 INDEX相当于合法元素的个数
在这里插入图片描述
直接看代码 会很好理解:

思路2参考代码: 注意看注释

#include <stdio.h>

int main() {
    int num = 0;//序列里一共有几个数
    scanf("%d", &num);

    int arr[num];//牛客支持变长数组 所以可以使用
    //否则那就定义一个大一点的数组 然后使用其中num的局部
    for (int i = 0; i < num; i++)
        scanf("%d", &arr[i]);

    //想要删除的数
    int flag = 0;
    scanf("%d", &flag);

    //进行删除操作
    int INDEX = 0;//有效指针 INDEX可以放元素
    int i = 0;//遍历指针
    for (i = 0; i < num; i++) {
        if (arr[i] != flag) {
            arr[INDEX] = arr[i];
            INDEX++;
        }

    }

    //遍历完 INDEX其实恰好指向最后一个合法元素的下一位
    //换句话说 INDEX相当于合法元素的个数
    for ( i = 0; i < INDEX; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

思路3:暴力
第一次我想到的方法 复杂度蛮高的 花了好几分钟才弄清楚我自己之前的思路
再次提醒:好好画图!!! 才不会搞错下标 循环条件 哪里++哪里–等等
大致思路:从后往前遍历 找到要删除的元素 就把后面的有效元素往前覆盖
注意: 找到要删除的元素 开始往前覆盖的时候 判断部分是i <= num - 2
为什么是i<=num-2 请看注释 画画图思考一下就清楚了
还有 count记录的是"删掉了几个元素" 那么num-count就是剩下几个有效元素

参考代码:看看注释 了解一下大致思路

#include <stdio.h>

int main() {
    int num = 0;
    scanf("%d", &num);

    int arr[num];
    for (int i = 0; i < num; i++)
        scanf("%d", &arr[i]);

    int tar = 0;
    scanf("%d", &tar);
    
    //tmp是用来遍历序列的
    //从后往前遍历
    int tmp = num - 1;

	//count记录删掉了几个元素 待会打印要用
    int count = 0;

	//说实话既然是遍历数组 还不如写一个for循环
    while (tmp >= 0) {
    	//当遇到目标删除元素
    	//比如1 2 3 4 4 4 5 6 想删除4
    	//从后往前 遇到第一个4 
    	//就从第1个4开始 把5 6 往前覆盖-->1 2 3 4 4 5 6 
        if (arr[tmp] == tar) {
        	//注意这里是i>=num-2
        	//先用5(arr[i+1]) 覆盖4(arr[i])
        	//再用6(arr[i+1]) 覆盖5(arr[i])
        	//如果写i<=num-1 反而越界了
            for (int i = tmp; i <= num - 2; i++) {
                arr[i] = arr[i + 1];
            }
            count++;
        }
        //自己画图 画图!!!!!!
        //处理完倒数第一个4 tmp继续往前走 继续往前处理
        tmp--;
    }
    
    //count记录的是删除了几个元素
    //那么num-count就是剩下几个有效元素
    for (int i = 0; i < num - count; i++)
        printf("%d ", arr[i]);

    return 0;
}

关于暴力的一些思考:

其实可以发现 前面我用的while遍历 就是下面这个for 是从后往前的 恰好避免了跳过元素的情况
如果是从前往后遍历 还是用1 2 3 4 4 4 5 6的例子
首先遇到第一个4 开始覆盖:1 2 3 4(tmp指向它) 4 5 6
然后temp++ 就把4给跳过了 指向了下一个4 漏覆盖了一个4
所以暴力必须倒着遍历 才能避免这种情况
但是 话说回来:
如果善于画图 多画图 就可以发现并避免这个bug!!!
在这里插入图片描述在这里插入图片描述

HJ106 字符逆序

点我做题

在第一部分已经有本题的详细介绍
这里主要介绍一下牛客网scanf()字符串不能输入空格的情况
scanf()遇到空格或者\0 就不读了 缓冲区里会留下空格或\0
在这里插入图片描述

因为这道OJ题的测试用例是带有空格的字符串
方法1:
即一直读 直到读到\0 空格也读取
在这里插入图片描述

方法2:
使用另一个函数 gets()
在这里插入图片描述

另外还要提一个我经常错的地方:
在这里插入图片描述

参考代码:

#include <stdio.h>
#include <string.h>

int main() {
    char ch[10000] = " ";
    // gets(ch);
    scanf("%[^\n]", ch);
    
    char* begin = ch;
    char* end = begin + strlen(ch) - 1;
    char tmp = ' ';
    
    while (begin < end) {
        tmp = *begin;
        *begin = *end;
        *end = tmp;
        begin++;
        end--;
    }
    printf("%s", ch);
    return 0;
}

还能这么写: 字符串也可以用下标访问

#include <stdio.h>
#include <string.h>

int main() {
    char ch[10000] = " ";
    gets(ch);
    // scanf("%[^\n]", ch);
    
    int left = 0;
    int right = left + strlen(ch) - 1;
    char tmp = ' ';
    //ch[left] = *(ch+left)
    while (left < right) {
        tmp = ch[left];
        ch[left] = ch[right];
        ch[right] = tmp;
        left++;
        right--;
    }
    printf("%s", ch);

    return 0;
}

BC106 上三角矩阵判定

点我做题

思路1:在main函数里也可以利用return的特性
但是必须要说以下几个错误 已经犯过好几次了!!!
我知道牛客网支持C99的变长数组 所以就用了 如下图:
n还没有输入值 就用n定义数组? 总是不能通过这道题!!! 看了半天的逻辑 又找不到问题
在这里插入图片描述
这才是正确写法:
在这里插入图片描述但是其实也可以不用变长数组 定义一个足够大的数组 用它的局部就行
在这里插入图片描述在这里插入图片描述

思路1参考代码:
只要找到一个非0 就可以确定是NO 其实就可以结束了 所以直接return

#include <stdio.h>

int main() {
    int n = 0;
    scanf("%d", &n);

    int arr[n][n];
    int sum = 0;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            scanf("%d", &arr[i][j]);
        }
    }

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (arr[i][j] != 0) {
                printf("NO\n");
                return 0;
            }
        }
    }
    //如果函数能执行到这 说明前面的return 0一次都没执行过
    //也就是YES
    printf("YES\n");
    return 0;
}

思路2:遍历下三角 求和看看是不是0
但是这种思路 每次都要全都遍历完 还要求和 效率肯定不高

#include <stdio.h>

int main() {
    int n = 0;
    int arr[10][10];
    int sum = 0;
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            scanf("%d", &arr[i][j]);;
        }
    }

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i;  j++) {
            sum += arr[i][j];
        }
    }
    if (sum == 0) {
        printf("YES\n");
    } else {
        printf("NO\n");
    }
    return 0;
}

思路3:用flag
反正但凡遇到一个非0 就可以得出结论了 就不需要再继续了

#include <stdio.h>

int main() {
    int n = 0;
    int arr[10][10];
    int sum = 0;
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            scanf("%d", &arr[i][j]);;
        }
    }

    int flag = 1;//假定是的 YES
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i;  j++) {
            if (arr[i][j] != 0) {
                flag = 0;
                break;
            }
        }
        //走到这 可能是break来的 也有可能是循环结束来的
        //如果是循环结束来的 那就继续回到前面i 去下一行判断
        //如果是break来的 那就说明NO了(flag=0) 再break出去
        if (flag == 0)
            break;
    }
    if (flag == 1) {
        printf("YES\n");
    } else {
        printf("NO\n");
    }
    return 0;
}

思路4:借用goto语句
既然这样 还不如直接用return 0
在这里插入图片描述

BC105 矩阵相等判定

点我做题

思路:
和BC106可以说是一模一样 下面直接提供我个人最喜欢的写法

#include <stdio.h>
int main() {
    int m;
    int n;
    scanf("%d %d", &n, &m);
    int arr1[n][m];
    int arr2[n][m];

    //输入arr1
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            scanf("%d", &arr1[i][j]);
        }
    }

    //输入arr2 同时判断
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            scanf("%d", &arr2[i][j]);
            if (arr1[i][j] != arr2[i][j]) {
                printf("No\n");
                return 0;
            }
        }
    }

    //如果能走到这 说明从未return 0过  那就是YES
    printf("Yes\n");
    return 0;
}

BC100 有序序列合并

点我做题

思路1:我认为算暴力法
由于 两个数组本身就是升序的 合并之后要求升序 所以可以这么写:
arr1升序 arr2升序 定义arr3出来
arr1和arr2的元素一个一个比较 较小的放到arr3当中 直到arr1或arr2有一个已经放完了
如果有一个已经放完了 假如是arr1先放完了 说明arr2剩下的元素比arr1最大的元素都大
直接把arr2剩下的元素全都按顺序放进arr3即可
在这里插入图片描述

这里的时间复杂度是O(m+n)
我写代码整个思路的过程中 无非就是把arr1遍历一遍 把arr2遍历一遍
遍历的同时比较了大小并同时把小的元素一个一个给arr3
就是遍历了m+n次
在这里插入图片描述

思路1参考代码:

#include <stdio.h>
int main() {
    int n;
    int m;
    scanf("%d %d", &n, &m);
    int arr1[n];
    int arr2[m];
    //输入
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr1[i]);
    }
    for (int i = 0; i < m; i++) {
        scanf("%d", &arr2[i]);
    }

    //合并
    int arr3[m + n];
    int INDEX = 0;
    //指针i遍历n--arr1
    //指针j变量m--arr2
    int i, j;
    for (i = 0, j = 0; i < n && j < m;) {
        if (arr1[i] < arr2[j]) {
            arr3[INDEX] = arr1[i];
            INDEX++;
            i++;
        } else {
            arr3[INDEX] = arr2[j];
            INDEX++;
            j++;
        }
    }
    //如果是i=n导致结束 就把j剩下的放到arr3
    if (i == n) {
        for (int k = j; k < m; k++) {
            arr3[INDEX] = arr2[k];
            INDEX++;
        }
    }
    //如果是j=m导致结束 就把i剩下的放到arr3
    if (j == m)
        for (int k = i; k < n; k++) {
            arr3[INDEX] = arr1[k];
            INDEX++;
        }

		//k<m+n也可以
    for (int k = 0; k < INDEX; k++)
        printf("%d ", arr3[k]);
    return 0;
}

也可以用while循环 思路是一样的:
在这里插入图片描述


思路2:牛客网的OJ 可以投机取巧
不一定要创建arr3 直接打印就行
在这里插入图片描述在这里插入图片描述

BC96 有序序列判断(牛客网有bug)

点我做题

思路1:利用return 纯纯的暴力
这是我一开始想到的办法 很丑陋 但是能解决OJ题
大致思路就是如果升序 那么应该可以比较N-1次>=
如果是降序 那么应该可以比较N-1次<=
判断count的个数即可

思路1参考代码:

#include <stdio.h>

int main() {
    int N = 0;
    scanf("%d", &N);
    int arr[N];
    int count = 0;
    for (int i = 0; i < N; i++) {
        scanf("%d", &arr[i]);
    }

    //判断1
    for (int i = 0; i < N - 1; i++) {
        if (arr[i] <= arr[i + 1])
            count++;
    }
    if (count == N - 1) {
        printf("sorted\n");
        return 0;
    }

    //判断2
    count = 0;
    for (int i = 0; i < N - 1; i++) {
        if (arr[i] >= arr[i + 1])
            count++;
    }
    if (count == N - 1) {
        printf("sorted\n");
        return 0;
    }

    printf("unsorted\n");
    return 0;
}


思路2:
对于一个数组 提供两种状态 flag1和flag2
flag1如果为1 就表示升序
flag2如果为1 就表示降序
那么数组有四种状态 1 0 ; 0 1 ; 1 1 ;0 0(0 0是比较特殊的一种状态 要结合代码理解)
一开始置为 0 0 然后开始比大小
如果一直到结束都还是0 0 说明这个数组每个元素都相等 那也算有序
所以1 0;0 1;0 0是有序 只有1 1是无序
显然可以使用&&
这里要提一下我对相等这种情况的思考: 相等到底该归为哪一类呢?
我们说相等可以算升序 也可以算降序 反正是有序
升降就取决于其它真正有大小之分的元素的比较了
可以说是 "跟随/保留"其他元素的比较结果
所以==的时候 最好的处理办法就是不做任何明确处理:让他保留其它元素的比较结果
我下面是把else写出来了 然后do nothing
当然完全可以只写if和else if 不写else 也是可以的
不写else其实就相当于:判断相等的时候啥也不干 即保留其它元素比较的结果

思路2参考代码:

#include <stdio.h>

int main() {
    int N = 0;
    scanf("%d", &N);
    int arr[N];
    int count = 0;
    for (int i = 0; i < N; i++) {
        scanf("%d", &arr[i]);
    }

    int flag1 = 0;//升序状态标识
    int flag2 = 0;//降序状态标识

    for (int i = 0; i < N - 1; i++) {
        if (arr[i] < arr[i + 1])
            flag1 = 1;
        else if (arr[i] > arr[i + 1])
            flag2 = 1;
        else// == 的情况
            ;//do nothing 保留之前的状态
    }

    //只有1 && 1 = 1的情况 肯定就是无序
    if (flag1 && flag2) {
        printf("unsorted\n");
    } else {
        //1 0
        printf("sorted");
    }
    return 0;
}

整理之后的最优代码:
一边输入 一边判断 不需要全都输完再判断(那就需要先读一个arr[0] 再从arr[1]开始判断)
用if和else if( 而不是if 和else) 其实就跟思路2一样的 只不过没有显式的写出else do nothing
当然写成这样的话 两两比较一定要当心数组越界 先把arr[0]输入进去 然后从arr[1]开始比较

#include <stdio.h>
int main() {
    int a[55], n, flag1 = 0, flag2 = 0, i;
    scanf("%d", &n);
    for (i = 0; i < n; i++) {
        scanf("%d", &a[i]);
        if (i > 0) {
            if (a[i] < a[i - 1]) {
                flag1 = 1;
            } else if (a[i] > a[i - 1]) {
                flag2 = 1;
            }
            //其实也就相当于else do nothing
        }
    }

    //只有当flag1和flag2都为1的时候序列无序
    if (flag1 && flag2)
        printf("unsorted\n"); 
    else
        printf("sorted\n");
}



说说牛客网的BUG:

下面这个代码是有bug的 但是牛客网居然给过了
显然是牛客的一些疏忽
if 和 else就是非此即彼 如果发现if不满足 肯定要进去else 两个肯定要进去一个
但是if 和 else if 都会判断一下 如果都不满足 就去else 如果没有else 那就相当于啥也不干 两个可以一个都不进去
对于3 4 5 6 6这种序列 当判断到最后两个6==6的时候 显然if 和 else if才是对的

在这里插入图片描述

HJ108 求最小公倍数

点我做题

利用辗转相除法参考代码:

#include <stdio.h>

int gcd(int m, int n) {
    int b = 0;
    while (b = m % n) {
        m = n;
        n = b;
    }
    return n;
}
int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    printf("%d", a * b / gcd(a, b));
    return 0;
}

暴力法参考代码:

#include <stdio.h>
int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    int max = (a > b ? a : b);
    for (int i = max; i <= a * b; i++) {
        if ((i % a == 0) && (i % b == 0)) {
            printf("%d", i);
            return 0;
        }
    }
}

如果用while循环写
要注意: !的优先级比&&高 不要忘记加括号
在这里插入图片描述在这里插入图片描述


暴力法的优化:
比如求3和5的最小公倍数 那就从3*(1,2,3,4,5…)里面找
再优化一点 从较大的5*(1,2,3,4,5…)里面找更快 因为每次的跨度大
5*(1,2,3,4,5…)试除3 看看能不能被整除 能被整除就是找到了

#include <stdio.h>

int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    
    int max = (a > b ? a : b);
    int min = (a + b) - max;
    int i = 1;//辅助找最小公倍数
    
    while (!(max * i % min == 0)) {
        i++;
    }

    printf("%d", i * max);
    return 0;
}

OR62 倒置字符串

点我做题

思路1:暴力法
核心思路: 逆序每个单词 然后逆序整个字符串 就能达到效果
比如 I can fly! 有三个字符串I ;can ;fly!
分别逆序:I nac !ylf
整个逆序:fly! can I
下面的代码就是实现了这个思路
在这里插入图片描述
这里写不写else 都是同一个意思:
能走到else 说明刚刚逆序的不是最后一个对象 需要继续寻找下一个对象
在这里插入图片描述

思路1参考代码:

#include <stdio.h>
#include<string.h>

void reverse(char* left, char* right) {
    while (left < right) {
        char tmp = *left;
        *left = *right;
        *right = tmp;
        left++;
        right--;
    }
}

int main() {
    char arr[100] = " ";
    gets(arr);
    //begin和end 用于标识一个处理对象
    char* begin = arr;
    char* end = arr;

    while (1) {
        //寻找空格标志 处理每个单词
        while (*end != ' ') {
            if (*end == '\0') {
                break;
            }
            end++;
        }

        //程序走到这有可能是:
        //1.end == ' ' 循环结束走到这
        //2.end == '\0' 遇到结束标志break到这
        //发现无论是哪种情况 begin和end-1之间都是我要逆序的对象(画图分析)
        reverse(begin, end - 1);

        //每次处理完一个对象 我必须判断一下刚刚处理的是不是最后一个对象
        //如果是的话 那就说明小对象处理完了 要break出去逆序整个大字符串
        if (*end == '\0')
            break;

        //如果走到这 说明前面逆序的还不是最后一个对象
        //需要继续处理下一个对象 那就需要更新首尾标识
        //让begin和end都指向新的对象的首字符 然后让end去寻找空格
        begin = end + 1;
        end = end + 1;
    }

    //如果走到这 说明前面的if已经被执行 break来的
    //还需要逆序一次整个字符串
    reverse(arr, arr + strlen(arr) - 1);
    printf("%s", arr);
    return 0;
}

思路2参考代码:
其实和思路1一模一样 只不过可以先逆序整个字符串 再逆序一个一个对象
对比思路1 核心思路部分是一模一样的

#include <stdio.h>
#include<string.h>

void reverse(char* left, char* right) {
    while (left < right) {
        char tmp = *left;
        *left = *right;
        *right = tmp;
        left++;
        right--;
    }
}
int main() {
    char arr[100];
    gets(arr);
    //整体reverse
    char* left = arr;
    char* right = arr + strlen(arr) - 1;
    reverse(left, right);

    char* start = arr;//每次记录起始位置
    char* end = arr;//协助寻找末尾位置

//这里必须要定义一个find 相当于临时变量的作用
//要不然的话 会修改start的值 传参就错了
    while (1) {
        //寻找末尾
        while (*end != ' ' && *end != '\0') {
            end++;
        }
        //找到了
        //reverse当前锁定的元素
        reverse(start, end - 1);
        //判断是不是最后一次reverse
        if (*end == '\0')
            break;

        //如果不是 更新收尾 继续reverse
        else
            //这里的写法要小心 如果要继续找 那就是先把find++
            //然后再作为下一轮reverse的start的值
            start = ++end;
    }
    printf("%s", arr);
    return 0;
}

另一种写法:阅读代码理解即可
里层while是&&不要写成||了
只要遇到了空格或者\0 都是一个要处理的对象
只有他既不是\0 也不是空格 才需要++继续找
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值