目录
- 一、程序设计经典编程题(C语言实现)
- T1 求1~100的奇数
- T2 求n!
- T3 求1!+2!+3!+...+10!
- T4 在一个`有序数组`中查找具体的某个数字n(二分查找)
- T5 编写代码,演示多个字符从两端移动,向中间汇聚
- T6 模拟用户登录(三次机会)
- T7 输入三个数 并从大到小输出
- T8 打印100~200的素数(质数)
- T9 打印1000~2000之间的闰年
- T10 求最大公约数(含递归)
- 补充:辗转相除法的证明
- T11 求最小公倍数
- T12 打印99乘法口诀表
- T13 不排序 求十个整数中的最大值
- T14 求1/1-2/1+1/3-4/1.....-100/1
- T15 计算给定数组中多个数的最大公约数
- T16 求整数1-100中数字9出现的次数
- T17 递归实现n的k次方(n,k都是整数 考虑k是负数)
- T18 递归求一个正整数的每一位数字的和
- *T19 字符串逆序(不是逆序打印 是真的逆序)
- T20 不创建临时变量 模拟先实现strlen()
- 思考:关于T19和T20++副作用的问题
- T21 完成整型数组的初始化,打印,逆序 三个函数
- T22 打印菱形图案
- T23 判断是不是自幂数(水仙花数)
- T24 求Sn=a+aa+aaa+... 其中a是一个0~9的整数 共有n项
- T25 换汽水喝(两种思维)
- T26 调整数组使奇数全部都位于偶数前面
- T27 预测比赛名次
- T28 找凶手
- T29 杨辉三角
- T30 杨氏矩阵
- T31 字符串左旋
- T32 判断一个字符串是不是由另一个字符串旋转得到
- 二、力扣
- 三、牛客网
- BC49 判断两个数的大小关系
- BC84 计算y的值
- BC101 班级成绩输入输出
- BC23 时间转换
- BC50 计算单位阶跃函数
- BC117 小乐乐走台阶
- BC112 小乐乐求和(注意数据类型的格式)
- BC114 小乐乐排电梯(考察细心)
- JZ15 二进制(补码)中1的个数
- JZ15拖展1 判断某个正整数n是不是2的k次方
- JZ15拖展2 分别打印整数num二进制补码的奇数位和偶数位
- JZ15拖展3 求两个int的二进制补码 有多少个位不同
- BC54 获得月份天数
- BC90 矩阵计算
- BC111 小乐乐与进制转换
- BC107 矩阵转置
- BC98 序列中删除指定数字
- HJ106 字符逆序
- BC106 上三角矩阵判定
- BC105 矩阵相等判定
- BC100 有序序列合并
- BC96 有序序列判断(牛客网有bug)
- HJ108 求最小公倍数
- OR62 倒置字符串
一、程序设计经典编程题(C语言实现)
T1 求1~100的奇数
解法1 暴力求解:
解法2: 思考思考 只要知道第一个奇数 后面的+2+2+2不就行了?
总结:
解法1 循环一百次 判断一百次
解法2 循环五十次 不需要判断
- 初步领略一下算法的魅力 要多思考
- 类似的 下面这道题就可以从3开始打印 每次+3+3+3 而不需要遍历 判断 再打印了
- 还有一种思路:
T2 求n!
- 本题很简单 就是累乘
- 但是我眼睛瞎起来就把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 二分查找:
- 前提是:
有序
所以不能乱给测试数组- right = mid-1; left = mid+1;
我一开始没有+-1 会导致死循环
- int right = len - 1;
-1别忘记了 right是下标
- 出循环之后有两种写法 可以利用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:
- 双"指针" 一左一右 开始打印有效字符
- 每次肯定都是打印十个字符 打印几次由while来决定
- left和right把10个要打印的字符分成三部分 [0,left]打印对应的有效字符 [right,len-1]打印对应的有效字符 剩余部分打印#
- 相较于思路2 更节省空间
错因:
- 打印#的判断条件写错了 左右不分??!!
开区间(left,right)范围打印#
- 每一次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:
- 给出两个数组 一个是字符 一个是# 注意#和字符的个数肯定都是一样的
- 然后也是用了两个指针 一左一右依次把字符赋给放#的那个数组
- 打印的时候 用%s专门只打印#的那个数组即可
错因:
- len–>求的是元素个数 也就是字符个数
故下标最大值是len-1
判断部分是<=
当左右指针指向同一个元素的时候 也要进入循环的
#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函数:
- 定义要输入的密码 正确密码
count是还剩下几次机会
- 只要count还没==0 就可以进去输入密码
我的错因:
- 一开始用的是下图这种定义方式 这样定义的话 其实数组大小是确定的 数组里放了一个空格 一个\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:
- 这里我犯了一个离谱的错误
多打了一个; 也就是这个if 啥也不干! 然后继续往下执行!
- 下图这部分写错了 左闭右开的话
右边就是循环次数
也就是字符串长度是不需要-1的 -1会导致少判断最后一个字符
也就是zcygst667也被算成密码正确了
- char ch[10] = " "; 10给的太小了!!! 要么限定用户只能输入多少的字符(一般交给前端限制) 要么给大点 否则
如果我测试的时候输入了一个zcysdshjadhshkd 会有栈溢出err
- 这个错误算是值得注意的点 我要是用flag这种写法的话
每次给你一次新机会输入密码的时候 flag都应该重置为1!!!!
否则在以下情况会出现问题: 当我第一次输入zcygst667 进入for的时候 把flag置为0 如果我后面输入了正确密码zcygst666 虽然不会执行flag=0 但是for循环出来走到if (flag == 1)的时候 发现flag上次已经给他置为0了 所以不进入if 直接继续下一次循环了 仿佛跳过了这次正确的输入- 主要就三个点的错误(下图已经修正)
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非本身的因数 就可以判断不是素数
- 判断i是不是素数的时候
可以从闭区间[2,i-1]试除
优化1:只要试除[2,根号i]
比如16 如果有因数 一定是成对出现的 比如1-16 2-8 4-4 如果只看一半的话 肯定是不会大于根号16的(≤)- 优化2:
偶数肯定不是素数
直接从101开始 每次i+2从奇数里面找素数
- double sqrt (double x);需要math.h的头文件 不放心的话循环里也可以强转成int
- 以上的方法
都叫"试除法"
推荐博文:<<素数求解的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乘法口诀表
就提几个注意点:
- %数字d 不要写成数字%d
- -2是左对齐两位(就记成按照我平时的习惯 就喜欢写-2左对齐) 2是右对齐两位
- 不要乱用对齐 这里前两位乘数就不可能有两位 不需要对齐 只有结果可能是两位
这就是 %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 不排序 求十个整数中的最大值
思路:
- 定义一个数组 放10个整数
- 假设第一个元素是最大值max
- 然后循环 让max和第二个开始的元素一一比较 如果遇到比max大的就重新赋值给max
错因:
- max应定义为arr[0] 而不是一个具体的数 比如max=0 如果arr都是负数呢?
- 在数组里挑一个最大的 肯定是以数组里元素的为标杆 不能随便找一个标杆 在我们班找一个最能打的 能找泰森作为标杆吗?
- 如果是自己输入的数据 那max一定要等输入完再定义为arr[0]
T14 求1/1-2/1+1/3-4/1…-100/1
思路
:
虽然下面两种想法代码写出来一模一样 但是思考方式不一样
- 分子恒为1 产生正负交替的分母 累加即可:sum += 1.0 / (i*symbl);
- 分子恒为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是负数)
思路:
- 任何数的0次方都为1
2的10次方 = 2*(2的9次方) = 2*2*(2的8次方) = 2*2....*(2的1次方)
则递推方程 (n,k) = n*(n,k-1)- 因为是return 所以下图加不加else 其实是一样的
- 一个瑕疵:n,k是整数 为什么不考虑负整数?
- 既然这样 当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*
- 注意:这里只是训练一下递归 其实这道题用递归大可不必!!
因为这道题逼着我用递归 逼着我只有一个参数 所以才写出下面比较复杂的思路的
先把递归的正确答案和思路写在前面 后面也介绍了其他思路:
- 按照
C语言专栏函数部分
说的那样 第一步我先假设出函数功能 假设reverse(char str) 能逆序一个字符串*- 然后开始拆分自己问题 首先交换收尾字符 然后reverse(中间剩余字符)
此时bug出现了:
如下图 abcdef\0->fbcdea\0 再传一个str+1进去 子问题成了逆序bcdea了
也就是说 在程序眼中
从str+1往后看 看到的是bcdea\0 处理的是bcdea无法准确处理我们想象中的那个子问题(bcde)
因为我只有一个参数str 如果说我用str代表一整个字符串的话 肯定要去找\0 受到\0的约束!!
不要慌 这说明子问题拆分的还不够彻底 所以重新拆分:
先假设出函数功能 假设reverse(char* str)
能逆序str~\0之间的一个字符串
“不要彻底交换”:
先把首字符暂存给tmp
再用末字符覆盖首字符
然后先不把tmp的首字符给直接给末字符 而是把\0给末字符 构造一个正确的子问题处理新的子问题 也就是reverse(掐头去尾的子串) 把中间的子串进行逆序
reverse()完之后 别忘了用之前tmp暂存的首字符 去覆盖那个
为了构造正确子问题而新增的'\0'
其实也就是把tmp赋给原先末字符的位置考虑递归的结束条件 if什么条件我才需要执行第三步的reverse(子问题)?
显然:"\0"没有子问题 "a"没有子问题 "ab"有子问题!
也就是下一个子问题的字符串的长度>=2
也就是strlen(str+1)>=2好好理解一下为什么是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. 消失的数字题目链接
解法一:首先^遵循交换律 且0^N=N N^N=0 把所有的元素都^起来 相同的都成0 剩下一个单独的 就是消失的数字 相当于找单身狗
解法二:既然题目描述里的n 其实应该等于numsSize 也就是我应该有的元素是0,1,2,3,4,n(numsSize) 把他们全都加起来去减数组的值 结果就是那个孤儿
解法二还可以优化 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级的所有跳法(已知)
- 只是思路不一样 其实写出来的代码都是一模一样的
则代码思路:
- 定义Search(n)是搜索跳n个台阶 有多少种跳法
- 子问题Search (n-1)+Search(n-2)
出口是: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 也不是空格 才需要++继续找