C语言练习——提高篇

目录

  • 汉诺塔问题
  • 公约数与公倍数
  • 在数字字符前插入$符号
  • 质因数分解
    • 思路
  • 求亲密数
  • 递归逆序字符
  • 递归对数组处理
  • n个整数,将前面各数后移m位,最后m个数移为前m个数
  • 三天打鱼,两天晒网
  • 进制转换
  • 归并,读取文件排序
  • 结构体写入文件
    • 文件读写函数
  • 哥德巴赫猜想
  • 约瑟夫环
  • 狼抓兔子
  • 马克思计算题
  • 爱因斯坦台阶
  • 牛顿迭代法
  • 乒乓球匹配
  • 百钱买百鸡
  • 魔方阵
  • 原地删除有序序列中重复元素
  • 三旗问题
    • 宏与函数
    • 思路
  • 黑洞数
  • 分鱼问题1
  • 分鱼问题2
  • 最简分数
  • 埃及分数
  • 整数转为字符串
  • 计算两个字符串最长子串长度
  • 计算两个字符串最长子序列长度
  • 农民过河
  • 求自守数
  • 子孙数
  • n个数中抽取r个组合
  • 全排列
  • 八皇后
  • 背包问题
  • 走马

汉诺塔问题

汉诺塔问题是递归的一个典型,假设有n个盘子,对于前n-1个盘子,必须借助中转柱移动到目标柱。而对于最后一个盘子,我们可以直接移动到目标柱。
可以定义一个函数:Hanoi(盘子数,开始柱,中转柱,目标柱)执行移动过程。
当盘子数大于1时,要借助中转柱移动,例如 move(2,A,B,C) 表示将2个盘子从A柱(开始柱)借助B柱(中转柱)移动到C柱(目标柱)。
C语言练习——提高篇_第1张图片

当盘子数为1时,则无需借助中转柱,直接移动到目标柱。move(1,A,B,C),直接输出A->C,代表移动到目标C。(注意这是递归的出口)

递归调用的过程:
这里需要注意,开始柱,中转柱,目标柱并不是一直就是ABC的,而是会根据递归的层次而改变。
要把四个盘子移动到C
C语言练习——提高篇_第2张图片
必须先移开上面三个盘子移动到B,然后再将最大的盘子从A移动到C。(递归一层)
C语言练习——提高篇_第3张图片
要移动3个就必须移动上面2个,但是B已经被我们预定用于存放前面移动的3个盘子,如果是将上面2个盘子移动到B,那就没办法把更大的第三个盘子放上去,所以这两个盘子机会是放在C上。(再递归一层)
C语言练习——提高篇_第4张图片
这时候,只剩下一个盘子在上面,要移动1个盘子的时候,就直接移动(达到递归出口)

#include 

void move(char x, char y) {
     
    printf("%c--->%c\n", x, y);
}

void Hanoi(int n, char A, char B, char C) {
     
    if (n == 1) {
     
        move(A, C); //1个盘子,直接打印出移动动作
    }
    else {
      // n > 1时,用抽象出的3步来移动
        Hanoi(n - 1, A, C, B); //step1. 把除了最大的盘子之外的盘子从A移到B
        move(A, C); //step2.把最大的盘子从A移到C
        Hanoi(n - 1, B, A, C); //step3.把除了最大的盘子之外的盘子从B移到C
    }
}

int main()
{
     
    Hanoi(3, 'A', 'B', 'C');
    return 0;
}

公约数与公倍数

求两个数a、b的最大公约数和最小公倍数
1,求公约数:使用辗转相除法
2,求公倍数:公倍数=a*b/公约数

辗转相除法:
对a、b进行求余数计算,然后将其中一个数和余数继续进行求余计算。重复直到余数为0。

a b
161 63
63 35
35 28
28 7
7 0

不断取余数,剥离的都是共同的因子,得到的数可以用a、b的线性组合来表示;且每次取得的余数,和原始a、b都有共同的公约数;
数越来越小,最后余数为0的时候,只需用该数就可以表示上面所有的数。而且是从大到小找到的第一个这样的数(最大公约数)

原数 线性组合 公约数表示
161 2*63+35 2 * 9 * 7+5*7
63 35+28 5 * 7+4 * 7
35 28+7 4 * 7+7
28 4*7+0 4*7
#define _CRT_SECURE_NO_WARNINGS
#include
//计算最大公约数
int gcd(int x, int y)
{
     
	int temp;
	while (x % y != 0) {
     
		temp = y;
		y = x % y;
		x = temp;
	}
	return y;
}

int main()
{
     
	int a, b, m, n;
	printf("请输入a,b:");
	scanf("%d%d", &a, &b);
	m = gcd(a, b);
	n = a * b / m;//根据公式计算最小公倍数
	printf("%d,%d的公约数是%d;公倍数是%d", a, b, m, n);
	return 0;
}

在数字字符前插入$符号

有两种思路,一:类似插入排序,遍历数组,将遇到的数字字符及其之后的元素都向后移一个单位,然后在原位置插入$ ;二:借助辅助数组,遍历辅助数组,遇到数组字符先插入$再插入元素。

#include
int Find(char A[], int n) {
     //从i开始向后搜寻到的第一个数字字符位置
	int i;
	for (i = n; A[i]; ++i) {
     
		if (A[i] >= '0' && A[i] <= '9')//范围不能错误
			return i;
	}
	return -1;
}
void deal(char A[])
{
     
	int i = 0, length = 0;
	while (A[length])
	{
     
		++length;
	}
	while (A[i])
	{
     
		int n;
		if ((n = Find(A, i)) == -1) {
     
			break;
		}
		for (int j = length; j >= n; j--)
			A[j + 1] = A[j];
		A[n] = '$';
		++length;
		i = n + 2;//插入$后必须后移两位,当前检查的字符和新插入的字符,都不需要再检查
	}
}

void deal2(char A[])
{
     
	char B[100];
	int i = 0, j = 0;
	while (A[i]) {
     //复制元素到辅助数组中
		B[i]= A[i++];
	}
	B[i] = '\0';

	i = 0;
	while (B[i])//遍历辅助数组,要往A数组插入新符号,因此要用两套指针指向两个数组
	{
     
		if (B[i] >= '0' && B[i] <= '9')//遍历到数字字符就多插入一个$
		{
     
			A[j++] = '$';
			A[j++] = B[i];
		}
		else//非数字字符就正常插入
			A[j++] = B[i];
		i++;
	}
}
int main() {
     
	char A[100] = "A1B234";//空间要充足
	deal2(A);
	printf("%s", A);
	return 0;
}

质因数分解

思路

对于数字有如下结论:
1,对于任意大于1的整数,都可以分解为有限个质数的乘积
2,合数是指在大于1的整数中除了能被1和本身整除外,还能被其他数(0除外)整除的数。
3,对一个较大的合数,总能被分解为有限个较小质数相乘的形式
则我们可以这样解决问题:

一,对于待分解的数n,从2-n遍历每一个整数,找到第一个可以整除n的数i:
1,如果n是质数,最后会遍历到n本身
2,如果n不是质数,则会遍历到一个较小的质数整除n;(根据结论3,合数总会被较小的质数分解,因此从小到大遍历一定先遍历到较小的质数)

二,i是可以分解n的最小质数,将i打印;

三,n=n/2,即从n中剔除掉i的影响,再继续从i遍历:
1,因为i之前的整数已经不能整除n,则也无法整除剔除了i的n;故从i继续遍历
2,如果新的n可以整除i,则继续打印i,从n中剔除i;直到n==i;说明n已经被完全分解
3,如果新的n不能整除i,则继续遍历直到找到下一个可以整除的质数,直到遍历到n

#define _CRT_SECURE_NO_WARNINGS
#include
int main()
{
     
	int n, i;
	printf("请输入一个数字:");
	scanf("%d", &n);
	printf("%d=", n);
	for (i = 2; i <= n; ++i) {
     
		while (n != i) {
     
			if (n % i == 0)
			{
     
				printf("%d*", i);
				n = n / i;
			}
			else
				break;
		}
	}
	printf("%d", n);
	return 0;
}

求亲密数

亲密数:A的全部因子(1 ~ A-1)之和等于B,B的全部因子(1 ~ B-1)之和等于A。
1,对于A找出所有的因子,计算累加和,因为亲密数累加和等于B,这里假设A有亲密数,将累加和记为B。
2,对B也计算所有因子的累加和,如果等于A,说明假设成立,否则A没有亲密数。
3,为了防止重复,打印的时候限制条件为A

#include

int main()
{
     
	int a, b, n, i;
	for (a = 1; a < 3000; ++a)
	{
     
		for (i = 1, b = 0; i < a; ++i) {
     
			if (a % i == 0)
				b += i;
		}
		for (i = 1, n = 0; i < b; ++i) {
     
			if (b % i == 0)
				n += i;
		}
		if (a == n && a < b)
			printf("%d-%d\n", a, b);
	}
	return 0;
}

递归逆序字符

1,对于字符的输入输出可以使用getchar,putchar等字符处理函数。
2,类似树的中序遍历,采用int n计数,先接受字符保存在ch,然后n-1,递归下一层,直到n==0代表输入完毕,递归到结尾,最后回溯就是逆序输出ch。

#include

void Palin1(int n)
{
     
	char ch;
	if (n >= 1) {
     
		ch = getchar();
		Palin1(n - 1);
		putchar(ch);
	}
}

void Palin2(int n)
{
     
	char ch;
	if (n <= 1) {
     
		ch = getchar();
		putchar(ch);
	}
	else {
     
		ch = getchar();
		Palin2(n - 1);
		putchar(ch);
	}
}
int main()
{
     
	Palin1(5);
	return 0;
}

递归对数组处理

递归结束最后一次对当前值处理,其余是将下层递归结果与当前层元素处理。因为递归结束后会回溯,可以很方便的进行逆序

{
     
	if (n == 1) {
     
		return R[n - 1]0 ;
	}
	else
		return(R[n - 1] + get_sum1(R, n - 1));
}

利用递归对数组求和,最值,均值以及分解因子等操作

#include
//计算和,教材方法
int get_sum1(int R[], int n) {
     
	if (n == 1) {
     
		return R[n - 1];
	}
	else
		return(R[n - 1] + get_sum1(R, n - 1));
}
//计算和
int get_sum(int R[], int n) {
     
	if (n >= 1) {
     
		return(R[n - 1] + get_sum(R, n - 1));
	}
	else
		return 0;
}
//求最大值
int get_max(int R[], int n) {
     
	if (n == 1) {
     
		return R[n - 1];
	}
	else {
     
		int m = get_max(R, n - 1);
		return(R[n - 1] > m ? R[n - 1] : m);
	}
}
//求均值
float get_avg(int R[], int n) {
     
	if (n >= 1) {
     
		return(R[n - 1] + get_avg(R, n - 1) * (n - 1)) / n;
	}
	else
		return 0;
}
//【递归】求int n的长度,逆序输出n
int solution(int n) {
     
	if (n/10 == 0) {
     
		printf("%d", n % 10);//最后一次输出换行
		return 1;
	}
	else {
     
		printf("%d", n % 10);
		return (1 + solution(n / 10));
	}
}
//【循环】求int n的长度,逆序输出n
void solution2(int n) {
     
	int num = 0;
	while (n) {
     
		printf("%d", n % 10);
		++num;
		n = n / 10;
	}
	printf("--%d\n", num);
}
//【递归】对int n分解因式,求和
int digitsum(int n) {
     
	if (n / 10 == 0)
		return n;
	return((n % 10) + digitsum(n / 10));
}
//【循环】对int n分解因式,求和
void digitsum2(int n)
{
     
	int Dsum = 0;
	while (n) {
     
		Dsum += (n % 10);//取出最后一位数字,累和
		n = n / 10;//删去最后一位数字
	}
	printf("%d\n", Dsum);
}

int main()
{
     
	int R[] = {
      1,22,3,43,5 };
	printf("%d\n", get_sum(R, 5));
	printf("%d\n", get_max(R, 5));
	printf("%f\n", get_avg(R,5));
	printf("--%d\n", solution(41272));
	solution2(41272);
	printf("%d\n", digitsum(1729));
	digitsum2(1729);
	return 0;
}

n个整数,将前面各数后移m位,最后m个数移为前m个数

1,对n个数,最后m个数的范围是:n-m+1~n (1 2 3 4 5 ,最后三个数是3 4 5,3=5-2+1);以数组为例最后m个数就是:n-m ~n-1
2,借助辅助数组来完成移动
3,数组中,后移元素是++;前移元素是–
4,后m个变为前m个:设移动x个元素,则n-m+x=0,x=-(n-m),即前移n-m个元素

#include

void move(int A[], int n, int m) {
     
	int B[255];
	int i;
	for (i = 0; i < n; ++i)
	{
     
		B[i] = A[i];
	}
	for (i = 0; i < n - m; ++i)
	{
     
		A[i + m] = B[i];
	}
	for (i = n - m; i < n; ++i) {
     
		A[i + m - n] = B[i];
	}
}

int main() {
     
	int A[] = {
      1,2,3,4,5 };
	move(A, 5, 3);
	int i = 0;
	while (i < 5) {
     
		printf("%d", A[i++]);
	}
	return 0;
}

三天打鱼,两天晒网

渔夫从2019年1月1日起,每5天打三天渔,晒两天网。编程实现任意输入日期,输出事打渔还是晒网
1,计算输入日期与起始日期之间的天数:计算起始年到输入年前一年的天数,加上输入年的天数
2,天数i%5,结果为1,2,3说明在打渔,否则在晒网(0,4)。(5天一个循环,1,2,3说明是一个周期后的第1 2 3天,在打渔;0 4代表第4 5 天)

#define _CRT_SECURE_NO_WARNINGS
#include

int leap(int y) {
     
	if ((y % 400 == 0) || (y % 4 == 0 && y % 100 != 0))
		return 1;
	else
		return 0;
}

int number(int y, int m, int d) {
     
	int sum = d, i = 0;
	for (i = 2019; i < y; ++i) {
     
		if (leap(i))
			sum += 366;
		else
			sum += 365;
	}
	i = 1;
	while (i < d) {
     
		switch (i)
		{
     
		case 1:
		case 3:
		case 5:
		case 7:
		case 8:
		case 10:
		case 12:
			sum += 31;
			break;
		case 2:
			if (leap(y)) sum += 29;
			else sum += 28;
			break;
		default:
			sum += 30;
			break;
		}
		++i;
	}
	return sum;
}

int main()
{
     
	int y, m, d;
	printf("请输入日期:");
	scanf("%d%d%d", &y, &m, &d);
	int n = number(y, m, d);
	if (n % 5 == 0 || n % 5 == 4)
		printf("在晒网");
	else
		printf("在打渔");
	return 0;
}

进制转换

1,其他进制转十进制
乘n进1,对n进制从低位到高位,取各位数,依次乘以n^x(x为位数),最后求和。
伪代码:
sum+=(n%10)*pow(2,i);
n=n/10;
++i;

例:将二进制的(101011)B转换为十进制的步骤如下:

  1. 第0位 1 x 2^0 = 1;

  2. 第1位 1 x 2^1 = 2;

  3. 第2位 0 x 2^2 = 0;

  4. 第3位 1 x 2^3 = 8;

  5. 第4位 0 x 2^4 = 0;

  6. 第5位 1 x 2^5 = 32;

  7. 读数,把结果值相加,1+2+0+8+0+32=43,即(101011)B=(43)D。

C语言练习——提高篇_第5张图片

int deal(char a[])
{
     
	int m = 0,i = 0;
	while(a[i])
	{
     
		if(a[i] >= 'A' && a[i] <= 'F')
			m+=(a[i] - 'A' + 10) * pow(16,i);
		else
			m+=(a[i] - '0') * pow(16,i);
		++i;
	}
	return m;
}

2,十进制转其他进制
除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。

伪代码:
n % 2 求余数
n=n/2; 因为计算机是向上取整,直接除得到的就是商
C语言练习——提高篇_第6张图片

int deal(int n){
     
	int a = 0,i = 0;
	while(n)
	{
     
		if(i == 0)
			a = n % 2;
		else
			a+=(n % 2) * 10;
		n = n / 2;
		++i;
	}
	return n;
}

归并,读取文件排序

分别从a和b中读书字符串,存入数组中;然后对数组执行排序;最后将排序后的字符依次存入c文件中。

#define _CRT_SECURE_NO_WARNINGS
#include
int together(char* f1, char* f2, char* f3) {
     
	FILE* fp = NULL;
	char ch[100] = {
      0 }, c, temp;
	int i = 0, j = 0;
	//读取文件
	if ((fp = fopen(f1, "r")) == NULL)
	{
     
		perror(f1);
		return 0;
	}
	while ((c = getc(fp)) != EOF)
	{
     
		ch[i++] = c;
	}
	fclose(fp);
	if ((fp = fopen(f2, "r")) == NULL)
	{
     
		perror(f2);
		return 0;
	}
	while ((c = getc(fp)) != EOF)
	{
     
		ch[i++] = c;
	}
	fclose(fp);
	//排序
	for (i = 1; ch[i]; ++i)
	{
     
		if (ch[i] < ch[i - 1]) {
     
			temp = ch[i];
			for (j = i - 1; j >=0 && ch[j] > temp; --j) {
     
				ch[j + 1] = ch[j];
			}
			ch[j + 1] = temp;
		}
	}
	//写入文件
	if ((fp = fopen(f3, "w")) == NULL)
	{
     
		perror(f3);
		return 0;
	}
	i = 0;
	while (ch[i])
	{
     
		fputc(ch[i++], fp);
	}
}
int main()
{
     
	char a[] = "a.csv";
	char b[] = "b.csv";
	char c[] = "c.csv";
	together(a, b, c);
	return 0;
}

结构体写入文件

计算几名学生各自的平均分,然后将学生的所有数据写入文件中
1,fwrite写入文件是二进制代码形式,所以用文本打开为乱码
C语言练习——提高篇_第7张图片
2,在结构体中多定义一个avg,记录平均分
3,使用完记得关闭文件

#define _CRT_SECURE_NO_WARNINGS
#include
//定义结构体
typedef struct student {
     
	char name[5] = {
      '\0','\0','\0','\0','\0' };
	int num;
	float grade[3];
	float avg;//存储该生的平均分
}student;

int deal()
{
     
	int i, j;
	float n = 0;
	student s[5] = {
      {
     "N1",001,{
     59,67,85},0},{
     "N2",002,{
     88,89,90},0},{
     "N3",003,{
     99,100,98},0},{
     "N4",004,{
     76,86,75},0},{
     "N5",005,{
     44,55,59},0} };
	FILE* fp = NULL;
	if ((fp = fopen("stud.txt", "w")) == NULL)
		return 0;
	for (i = 0; i < 5; ++i)
	{
     
		for (j = 0; j < 3; ++j)
		{
     
			n += s[i].grade[j];
		}
		s[i].avg = n / 3;
		if (fwrite(&s[i], sizeof(student), 1, fp) != 1)
		{
     
			perror(s[1].name);
			return 0;
		}
	}
	fclose(fp);//记得关闭文件
	return 1;
}

int main()
{
     
	deal();
	return 0;
}

文件读写函数

1,fopen
读取文件,失败会返回NULL

if ((fp = fopen("stud.txt", "w")) == NULL)
		return 0;

2,fscanf
从文件中读取数据,赋值给变量
%s,读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。

	char str1[100], str2[100];
	FILE* fp = NULL;
	if ((fp = fopen("stud.txt", "r")) == NULL)
		return 0;
	rewind(fp);   // fp回到开始位置
	fscanf(fp, "%s %s %s %d", str1, str2, str3, &year);
	printf("Read String1 |%s|\n", str1);
	printf("Read String2 |%s|\n", str2);

C语言练习——提高篇_第8张图片
C语言练习——提高篇_第9张图片

3,fprintf
格式化数据输出,存入文件,类似printf函数,也是利用format 字符串控制格式。

fprintf(fp, "\n%s %d %f %f %f %f\n", s[i].name,s[i].num,s[i].grade[0], s[i].grade[1], s[i].grade[2], s[i].avg);

4,fputs()
字符串输出,存入文件中

fputs("这是 C 语言。", fp);

5,fgetc()
读取文件中的一个字符数据,读完一个后自动指针后移一位,读到最后返回EOF

	while ((c = fgetc(fp)) != EOF)
	{
     
		ch[i++] = c;
	}

哥德巴赫猜想

任意2000以内,不小于4的正偶数,可以分解为两个素数之和。
1,从4开始遍历到2000的正偶数i
2,遍历从2开始所有小于i的数j(质数至少要从2开始)
3,相对于i,将i分解为j和i-j
4,判断j和i-j是否均为质数,是就满足条件

#include

int fun(int a)
{
     
	int i;
	for (i = 2; i < a; i = i + 2)
	{
     
		if (a % i == 0)
			return 0;
	}
	return 1;
}

void deal()
{
     
	int i, j;
	for (i = 4; i <= 2000; ++i) {
     
		for (j = 2; j < i; ++j) {
     
			if (fun(j) && fun(i - j)) {
     
				printf("%d=%d+%d\n", i, j, i - j);
				break;
			}
		}
		if (j == i)
		{
     
			printf("%d错误\n", i);
			continue;
		}
	}
}

int main()
{
     
	deal();
	return 0;
}

约瑟夫环

n个人围成一圈,从1开始报号,m为上限值,报到m就将那个人出列,然后从下一个人开始重新从1报号,直到所有人出列。

方法一:用数组下标代替报号的人,要执行循环需要进行求余,因为求余总是在0-n-1之间,需要将n+1然后进行求余,同时将A[0]设为已经报号,以免对0报号。

1,用数组标记对应下标的人是否已经报号,报过就跳过,但是数组是从0开始,我们报号是从1开始,并且每次求余循环都会回到0。则一开始就将A[0]标记为已报号,这样第一次报号的时候能跳过0,从编号1开始报号;并在以后的报号中求余再回到0时直接跳过。

2,每轮报号都验证当前遍历到的人是不是已经报过号,报过就循环遍历直到找到下一个没有报过号的人。使该人报号(遍历到旁边一个人),报号次数加一。下一轮遍历就以个旁边的人为起始,看他是不是已经报过号了,不断循环。只处理m-1次,但是遍历到了m-1次报号者的下一个人。

3,第m次,检验遍历到的人是否已经报过,如果报过就遍历到下一次未报号的人。将其出列。

#include
void deal(int m, int n)
{
     
	int k = n, i, j = 0;//从编号为1的人开始报号
	n = n + 1;//求余多一个
	int A[100] = {
      1 };//A[i]记录是否报号,i的范围为1-n,A[0]提前标记掉,一方面是初始第一次的报号,并且下次报号遇到0会跳过
	while (k) {
     //n轮报号
		i = 1;
		while (i < m) {
     //一轮报号,只报m-1次
			while (A[j] == 1)//验证j(上一次报号者旁边一个人)是不是已经出局,剔除已经出局的人
				j = (j + 1) % n;
			j = (j + 1) % n;//j(未出局的人)报号,遍历到报号者的旁边一个人
			++i;//次数+1
		}
		//寻找第m次报号的人
		while (A[j] == 1)//上次报号者旁边一个人,与第m次报号者之间可能还隔着一些已经出局的人,需要剔除
			j = (j + 1) % n;
		A[j] = 1;//报号为m的人
		printf("%d出局\n", j);
		k--;//本轮报号结束
	}
}

方法二:数组元素代表报号者,数组0-n-1中存储报号者的编号,每次第m个报号者出局,对应的元素设为0

1,执行n轮报号,每轮执行m次报号。
2,先执行m-1次报号,每次报号,先跳过已经报号的人,然后报号者下标后移一位,报号次数加一。第m次报号,已经指向m-1个报号者的后一位,直接跳过已经报号的人,即为第m次报号的人,将其出局。

void deal1(int m,int n)
{
     
	int k = 0, i, j;
	int A[100];
	for (i = 0; i < n; ++i)//数组内元素存储人的编号(1-n)
		A[i] = i + 1;
	for (i = 0; i < n; ++i) {
     //n次报号
		j = 1;
		while (j < m) {
     //报号m-1次
			while (A[k] == 0)//跳过已经报号的人
				k = (k + 1) % n;//在0-n-1循环
			k = (k + 1) % n;//k报号,跳到旁边一个人
			++j;//报号次数加一
		}
		while (A[k] == 0)//跳过已经报号的人
			k = (k + 1) % n;
		printf("%d出局\n", A[k]);//第m个报号者出局
		A[k] = 0;
	}
}

int main()
{
     
	deal(3, 10);
	printf("===================\n");
	deal1(3, 10);
	return 0;
}

狼抓兔子

一只兔子躲在十个洞中的一个里,狼抓兔子,第一个洞没有,隔一个洞到第三个洞抓,再没找到,就隔两个洞到第六个找,如果一直找不到兔子,兔子在哪儿?
1,bool数组问题

bool A[100] = {
      0 };//全部初始化为false
bool B[100] = {
      1 };//只有第一个初始化为true,余下的都为false

在这里插入图片描述
2,与上一题不同,上题是每次遍历到一个报号要将其出局(输出),因此可以用元素代表,先输出,然后将其标记为0即可;本题则是在多轮淘汰后,将剩余未淘汰的输出。

一,用数组元素代表 编号
循环找100次,剩下还未找到的就是一直找不到的洞
先将数组标记为未寻找状态,为1-10
将当前遍历的元素设为0,代表已经搜索过
然后向后移动n格去寻找(循环),n从2开始,逐次加1

#include

void Find()
{
     
	int A[10];
	int i, n = 2, k = 100;
	for (i = 0; i < 10; ++i)
		A[i] = i + 1;

	i = 0;
	while (k) {
     
		A[i] = 0;
		i = (i + n) % 10;
		++n;
		--k;
	}

	for (i = 0; i < 10; ++i) {
     
		if (A[i] != 0)
			printf("%d\n", i + 1);
	}
}
int main()
{
     
	Find();
	return 0;
}

利用bool数组,批量设置为未搜索状态。每次搜索设置为true。其他都一样

void Find()
{
     
	bool A[100] = {
      0 };
	int i, n = 2, k = 100;

	i = 0;
	while (k) {
     
		A[i] = 1;
		i = (i + n) % 10;
		++n;
		--k;
	}

	for (i = 0; i < 10; ++i) {
     
		if (A[i] != 1)
			printf("%d\n", i+1);
	}
}

马克思计算题

有30人,有男、女和小孩,每个男花3元,女花2元,小孩花1元,共花50元,求男女小孩各几人。
1,根据条件列出方程组,设男x,女y,小孩z人。
x+y+z=30
3x+2y+z=50
x>0,y>0,z>0

2,解方程组,对x遍历,则将y和z都表示为x的函数
y=20-2x
z=30-x-y=30-x-20+2x=10+x
1< x <9

#include
void man_num()
{
     
	int x, y, z;
	for (x = 1; x < 10; ++x) {
     
		y = 20 - 2 * x;
		z = x + 10;
		if (3 * x + 2 * y + z == 50 )
			printf("%d-%d-%d\n", x, y, z);
	}
}

int main()
{
     
	man_num();
	return 0;
}

爱因斯坦台阶

爱因斯坦曾经提出过这样一道有趣的数学题:有一个长阶梯,若每步跨2阶,最后剩下1阶;若每步跨3阶,最后剩2阶;若每步跨5阶,最后剩下4阶;若每步跨6阶,最后剩5阶;只有每步跨7阶,最后刚好一阶也不剩。请问该阶梯至少有多少阶。

1,台阶是从0起步,跨一级台阶是从0-1 1-2这样;跨2级台阶是从0-2 2-4这样以2为单位执行。则跨两级最后剩1级,每次跨2级,最后剩1级-》i%2==1;

2,i<7的时候明显不成立。每次跨7阶,明显要7阶以上才行。
C语言练习——提高篇_第10张图片

#define _CRT_SECURE_NO_WARNINGS
#include
int main()
{
     
	int n, sum, i, falg = 1;
	while (falg) {
     
		printf("请输入N:");
		scanf("%d", &n);
		printf("在1-%d之间的阶数为:\n", n);
		sum = 0;
		for (i = 7; i <= n; ++i) {
     //0-7肯定不满足条件
			if (i % 7 == 0 && i % 6 == 5 && i % 5 == 4 && i % 3 == 2 && i % 2 == 1) {
     
				++sum;
				printf("%d\n", i);
			}
		}
		printf("在1-%d之间,满足条件的数量为%d\n", n, sum);
		printf("继续输入1,否则输入0");
		scanf("%d", &falg);
	}
}

牛顿迭代法

1,切线是曲线的线性逼近
A点的切线是f(x)的线性逼近。离A点距离越近,这种逼近的效果也就越好,切线与曲线之间的误差越小。
C语言练习——提高篇_第11张图片
2,逼近公式
牛顿迭代法的原理是不断用f(x)上一点pk的切线来逼近方程f(x)的根。在曲线上找一A点,做一个A的切线,切线的根xk+1与曲线的根x *,还有一定的距离。再从这个切线的根出发,做一根垂线,和曲线相交于B点,继续重复刚才的工作。
C语言练习——提高篇_第12张图片

B点比之前A点更接近曲线的根点,经过多次迭代后会越来越接近曲线的根。
C语言练习——提高篇_第13张图片
思路
借助下面的公式进行迭代,先设置一个在根附近的初试值,然后用x0接受初值进行计算,x接受获得的新值;然后进行下一轮迭代,直到新旧值距离很小,说明收敛结束迭代。
在这里插入图片描述

#include
#include

float solution(float a, float b, float c, float d) {
     
	float x0, x = 1.5, f, fd, h;
	do {
     
		x0 = x;//x0接受上次的值
		f = a * x0 * x0 * x0 + b * x0 * x0 + c * x0 + d;//原函数
		fd = 3 * a * x0 * x0 + 2 * b * x0 + c;//倒数
		h = f / fd;
		x = x0 - h;//x接受迭代后的新值
	} while (fabs(x - x0) > 1e-5);//两次迭代值距离小于特定值,结束迭代
	return x;
}

int main()
{
     
	printf("%f", solution(1, 2, 3, 4));
	return 0;
}

乒乓球匹配

甲队abc三人,乙队xyz三人,a不与x比,c不与xz比,输出比赛名单
1,不能在for循环中间加多个条件
j != i不满足的话,for循环直接结束,相当于break;而我们要的是continue,因此要在循环下一层加j != i这个条件,即使不满足,也只跳过这一次然后继续循环

	for (i = 'x'; i <= 'z'; ++i) {
     
		for (j = 'x'; (j <= 'z' && j != i); ++j) {
     
			for (k = 'x'; k <= 'z' && k != i && k != j; ++k) {
     

2,保持甲队出场顺序不变,只对乙队遍历三层,保证所有的组合都遍历到,这样再添加限制条件,就可以筛选出答案。

#include

int main()
{
     
	char i, j, k;
	for (i = 'x'; i <= 'z'; ++i) {
     
		for (j = 'x'; j <= 'z' ; ++j) {
     
			if (j != i) {
     
				for (k = 'x'; k <= 'z' ; ++k) {
     
					if (k != i && k != j) {
     
						//printf("%c-%c-%c\n", i, j, k);
						if (i != 'x' && k != 'x' && k != 'z') {
     
							printf("a-%c\n", i);
							printf("b-%c\n", j);
							printf("c-%c\n", k);
						}
					}
				}
			}
		}
	}
	return 0;
}

百钱买百鸡

小鸡一元三只,则每只1/3元,只能三只三只的买。需要加限制条件k%3==0。

魔方阵

%的运算优先级高于加减乘除,需要加括号

In [1]:5%5
Out[1]: 0

In [2]:k=6

In [3]:k-1%5
Out[3]: 5

In [4]:(k-1)%5
Out[4]: 0

二维数组做函数参数,至少给出列的数量

void solution(int A[][10], int n)
{
     
}

思路
1,以元素为单位,从1-n^2依次安放各元素在数组中的位置
2,n必须为奇数,1放在第一行的中间(偶数就没法放了)
3,从2-n^2,下一个元素在上一元素右上角,若上一元素为A[ i ] [ j ];则下一元素为A[ i -1] [ j+1 ]
4,若上一元素在第一行,则下一元素移到最后一行;A[ n-1] [ j+1 ];若上一元素在最后一列,下一元素在第一列。A[ i -1] [ 0 ]
5,若上一元素可以被n整除,下一元素在上一元素下方。A[ i -1] [ j ]

#include
void solution(int A[10][10], int n)
{
     
	int i, j, k;
	i = 0;
	//n需要为基数
	if (n % 2 == 0) n++;
	j = n / 2;
	A[i][j] = 1;
	for (k = 2; k <= 25; ++k) {
     
		int i0, j0;
		//下一数在上一数的右上方,越界就回到起始行/列
		i0 = (i - 1 >= 0) ? (i - 1) : (n - 1);//必须大于等于0,临界点包括0
		j0 = (j + 1 < n) ? (j + 1) : 0;
		//上一数为n的倍数,下一数在上一数下方
		if ((k - 1 )% n == 0) {
     //上一个数是n的整数倍,下一个数在上一个数下面;求余运算要括号
			i0 = (i + 1 < n) ? (i + 1) : 0;
			j0 = j;
		}
		//放置下一数
		A[i0][j0] = k;
		//记录下一数位置
		i = i0;
		j = j0;
	}
}
int main() {
     
	int A[10][10];
	for (int i = 0; i < 5; ++i)
		for (int j = 0; j < 5; ++j)
			A[i][j] = 0;
	solution(A, 5);
	for (int i = 0; i < 5; ++i)
	{
     
		for (int j = 0; j < 5; ++j) {
     
			printf("%d ", A[i][j]);
		}
		printf("\n");
	}
}

原地删除有序序列中重复元素

1,原地删除:不使用额外的数组空间,在原地修改删除输入数组;即不使用辅助数组,空间复杂度为0(1)。
2,思路:就地删除,直接把后面的元素往前覆盖,但是不需要遇到一个重复的元素就把所有元素前移一遍。可以换个思路,遇到不重复的就将这个元素移到去重序列后面,遇到重复的就只需将数组长度减一,从逻辑上删除该元素即可。

首先,去重序列只包含A[0],将A[0]记待去重元素,向后遍历,遇到与A[0]相同的元素都从逻辑上将其删去,即序列长度减一。

直到遇到第一个与A[0]不同的元素,将其移动到A[0]后面,归入去重序列,并记为新的待去重元素。继续将新的待去重元素与后续元素对比。

不断将不重复元素并入去重序列中,直到遍历完所有元素,返回去重序列的长度。

#include
int deleteTarget(int A[], int n)
{
     
	int i, j, k, t, len;
	j = 1;//已去重序列结束标识
	len = n;//去重后元素数量
	t = A[0];//待去重元素
	for (i = 1; i < n; ++i)
	{
     
		if (A[i] != t) {
     //扩充去重序列
			A[j++] = A[i];//不重复元素移动到已去重序列后一位
			t = A[i];//待去重元素更新为该元素
		}
		else
			len--;
	}
	return len;
}

int main()
{
     
	int A[] = {
      1,2,3,3,4,5,5,5,6 };
	int n=deleteTarget(A, 9);
	for (int i = 0; i < n; ++i)
		printf("%d", A[i]);
	return 0;
}

三旗问题

宏与函数

宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它不参与编译,也不占用内存。

在SQ()函数中,先把实参 i 传递给形参 y,然后再自增 1,这样每循环一次 i 的值增加 1,所以最终要循环 5 次。

在SQ1()中,宏调用只是简单的字符串替换,SQ(i++) 会被替换为 ((i++)*(i++)),这样每循环一次 i 的值增加 2,所以最终只循环 3 次。
在这里插入图片描述

由此可见,宏和函数只是在形式上相似,本质上是完全不同的。

#include 
#define SQ1(y) ((y)*(y))
int SQ(int y) {
     
    return ((y) * (y));
}
int main() {
     
    printf("函数计算:\n");
    int i = 1;
    while (i <= 5) {
     
        printf("%d^2 = %d\n", (i - 1), SQ(i++));
    }
    printf("宏计算:\n");
    i = 1;
    while (i <= 5) {
     
        printf("%d^2 = %d\n", i, SQ1(i++));
    }
    return 0;
}

2,在宏先用到了A[0],此时还没有定义A[];但由于不对宏进行计算,只是到了main中对应处直接替换,此时A[]已经定义了。因此实现了形式上先计算后定义。
在这里插入图片描述

#include 
#define deal(){
       int a=A[0];A[0]=a+1;}

int SQ(int y) {
     
    return ((y) * (y));
}
int main() {
     
    int A[] = {
      1,2 };
    deal();
    return 0;
}
 

思路

1,变量定义后,可以a=b=c=d=0;
2,char A[]={‘a’,‘b’,‘c’};不会自动添加’\0’

按照bwr顺序排列,b w指向0;r指向n-1

w向后遍历,遇到‘w’就跳过

遇到‘b’就和b所指位置交换,因为b不会在w前面,只会指向’w’,故交换后b指向’b’,w指向’w’,因此两者都后移。

遇到‘r’,r向前遍历,直到不指向’r’,(并且注意r必须小于w。否则越界出错。)r再和w交换。因为r在w前面,都是未整理的元素,因此w交换到的不一定是‘w’,交换后r前移,w不移动,下一轮做检测。

#include
#define swap(x,y){
       int temp=A[x];A[x]=A[y];A[y]=temp;};

void sort(char A[])
{
     
	int i, n, b, w, r;
	r = b = w = n = 0;
	while (A[n]!='\0')
		++n;
	r = n - 1;
	while (w <= r)
	{
     
		if (A[w] == 'w')
			++w;
		else if (A[w] == 'b')
		{
     
			swap(w, b);
			++b;
			++w;
		}
		else {
     
			while (A[r] == 'r' && w < r)//bwwrrr时,不加w < r,r就会遍历到w上,再交换wr就变成bwrwrr
				--r;
			swap(w, r);
			--r;
		}
	}
	for (i = 0; i < n; ++i) {
     
		printf("%c ", A[i]);
	}
}

int main()
{
     
	char A[] = {
      'w','w','b','b','r','w','b','w','r','r','b','\0' };//自己创建是不会自动添加'\0'
	sort(A);
	return 0;
}

黑洞数

对一个各位数字不全相同的整数,进行有限次的重排求差(将该数重新排序为最大值和最小值然后相减求差)后,最后结果一定为黑洞数,此后无论进行多少次重排求差,结果都一样。对一个三位数,以结果相等作为判断黑洞数的依据

1,对一个数将其个十百位分别提取出来,排列为最大值和最小值,然后计算差值。
2,对差值再进行重排求差,循环,直到两次结果一样

#include
void swap(int &x, int &y) {
     
	int temp = x;
	x = y;
	y = temp;
}
int maxn(int a, int b, int c)//引用型
{
     
	if (a < b)
		swap(a, b);
	if (a < c)
		swap(a, c);
	if (b < c)
		swap(b, c);
	return(a * 100 + b * 10 + c);
}

int minn(int a, int b, int c)
{
     
	if (a < b)
		swap(a, b);
	if (a < c)
		swap(a, c);
	if (b < c)
		swap(b, c);
	return(c * 100 + b * 10 + a);
}

void solution(int n)
{
     
	int a, b, c, m1, m2, n0;
	a = n / 100;
	b = (n / 10) % 10;
	c = n % 10;
	m1 = maxn(a, b, c);
	m2 = minn(a, b, c);
	n = m1 - m2;
	while (1)
	{
     
		n0 = n;
		a = n / 100;
		b = (n / 10) % 10;
		c = n % 10;
		m1 = maxn(a, b, c);
		m2 = minn(a, b, c);
		n = m1 - m2;
		if (n == n0) {
     //必须按题意结束
			printf("%d", n);
			break;
		}
	}
}

int main()
{
     
	solution(538);
	return 0;
}

分鱼问题1

五个人去捕鱼,捕鱼后休息,每个人依次起来将鱼丢掉1条然后分为5份,自己带走一份;问至少捕鱼多少条、
采用递归的思路解决问题,输入一个数,每次这个数-1都能被分为5份,然后将其中4/5份递归到下一层;如果能递归5次,则这个数满足条件。
可以从1依次尝试,直到有一个数满足分鱼条件

#include
int fish(int n, int x) {
     
	if ((x - 1) % 5 == 0)//拿掉1条还能分5次
	{
     
		if (n == 1)//分了5次
			return 1;
		else//递归,丢掉1条,留下4/5
			return fish(n - 1, (x - 1) / 5 * 4);
	}
	return 0;
}

int main()
{
     
	int i = 0, flag = 0, x;
	do {
     
		i = i + 1;//从每人分一条1开始试,直到第一次找到满足条件的数量
		x = i * 5 + 1;//最少6条,多一条,然后每人分1条;
		if (fish(5, x)) {
     //满足5次递归即可,且每次都能分成5份
			flag = 1;
			printf("鱼最少%d", x);
		}
	} while (flag != 1);
	return 0;
}

分鱼问题2

ABC三人分鱼,一共21筐,7筐装满鱼,7筐装半筐鱼,7筐为空;如何分鱼使得每人都分到7筐鱼,并且其中3.5筐装了鱼。

1,本题实际为枚举类题目,要找清每种可能的情况,然后枚举出来即可。可以利用二维数组代表分鱼的情况,每行代表每个人,每列代表三种筐。枚举出满筐和半筐的所有情况,最后剩余的就是空筐数量(7-满筐-半筐)。只输出符合条件的组合即可。

2,确定条件
每行每列只和都为7,即每人分7筐,且每类筐都有7个。
每一行的满筐数+半筐数*0.5=3.5
每个人满筐数不能超过3,半筐数至少为1,且为奇数
避免重复的分配方案,1-2-3 出现就不能出现 2-1-3;只需规定后一个人的满筐数和半筐数都大于等于前者,即为有序排列
C语言练习——提高篇_第14张图片
3,枚举思路
首先确定满筐的数量
从0-3依次枚举第一个人满筐的数量;在此基础上枚举第二个人和第三个人满筐数量(第二个人数量为i的话,第三个人数量直接为7-第一人-i),并保证后者数量大于等于前者

确定半筐的数量
从1 3 4中枚举第一个人半筐的数量,进而枚举第二三个人的半框数量,这里就无需验证后者大于前者了,因为半框*0.5+满筐=3.5,满筐有序半筐也有序

确定空筐数量
依次验证每个人的满筐和半框数量和小于7,半框*0.5+满筐=3.5;满足条件得到空筐=7-满筐-半筐;如果三个人的排序都符合条件就输出排序

#include
int a[3][3], count;
int main()
{
     
	int  i, j, k, m, n, flag;
	for (i = 0; i < 3; ++i)//枚举第一个人满筐
	{
     
		a[0][0] = i;
		for (j = i; j <= 7 - j && j <= 3; ++j)//枚举第二三个人满筐
		{
     
			a[1][0] = j;
			if ((a[2][0] = 7 - j - a[0][0]) > 3)//第三个人满筐数不能大于3
				continue;
			if (a[2][0] < a[1][0])//避免重复
				break;
			for (k = 1; k <= 5; ++k)//枚举第一个人半框,1 3 5
			{
     
				a[0][1] = k;
				for (m = 1; m < 7 - k; ++m)//枚举第二三个人半框
				{
     
					a[1][1] = m;
					a[2][1] = 7 - k - m;
					for (flag = 1, n = 0; flag && n < 3; ++n) {
     //枚举每个人的空筐
						if (a[n][0] + a[n][1] < 7 && a[n][0] * 2 + a[n][1] == 7)//相加等于7,且半筐+满筐=3.5
							a[n][2] = 7 - a[n][0] - a[n][1];
						else
							flag = 0;
					}
					if (flag) {
     
						for (n = 0; n < 3; ++n)
						{
     
							printf("%c %d %d %d\n", 'A' + n, a[n][0], a[n][1], a[n][2]);
						}
					}
				}
			}
		}
	}
	return 0;
}

最简分数

如果一个分数不是最简分数,则分子分母有除1外的公因数,如下面能提取公因数2。
4 10 = 2 5 \boxed{\frac4 {10}=\frac 2 5} 104=52
则对1任一个分数,可以从2遍历到分子,如果分子分母出现公因数,则不是最简分数。

#include
int main()
{
     
	int i, j;
	for (i = 1; i < 40; ++i)
	{
     
		for (j = 2; j <= i; ++j)//一定要遍历到i本身,验证分子是不是分母的因数
		{
     
			if (40 % j == 0 && i % j == 0)//两者有除1之外的公因数则不是最简分数
				break;
		}
		if (j > i)
			printf("%d/40\n", i);
	}
	return 0;
}

埃及分数

1,方法1:【贪心算法:即在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解】
对于一个分数,我们从大到小遍历所有分数(1/1 1/2 1/3…),找出第一个比该分数小的分数,然后从中剔除该分数,继续遍历,直到全部被分离出去为止。
以用11/8为例:
比11/8小一点的埃及分数是 1/2 ,剔除1/2后剩5/22。
比5/22小一点的埃及分数是 1/5 ,剔除1/5后剩3/110。
比3/110小一点的埃及分数是 1/37 ,剔除1/37后剩1/4070。
无法继续分离,结束,则,8/11=1/2+1/5+1/37+1/4070

int deal1()
{
     
	int a = 8, b = 11;
	int i, j;
	//scanf_s("%d%d", &a, &b);
	printf("%d/%d=", a,b);
	while (a != 1)
	{
     
		if (b % a == 0)
		{
     
			a = 1;
			b = b / a;
		}
		else
		{
     
			for (i = 1; i <= b; ++i)
			{
     
				float t1 = (float)1 / i;
				float t2 = (float)a / b;
				if (t1 < t2)
				{
     
					printf("1/%d+", i);
					a = a * i - b;
					b = b * i;
					for (j = a; j > 0; --j)
					{
     
						if (b % j == 0 && a % j == 0)
						{
     
							b = b / j;
							a = a / j;
							break;
						}
					}
				}
			}
		}
	}
	printf("1/%d\n", b);
	return 0;
}

2,方法2:也是贪心算法,每次都从原分数中分离出最大的埃及分数,直到分离结束。
找到最大的埃及分数:设真分数 a/b ,b 除以 a得整数部分为c,余数部分d。
在这里插入图片描述
在这里插入图片描述
d是a得余数,那么a肯定要比d大。
在这里插入图片描述
1 / (c + 1)一定是 a / b所包含的最大埃及分数。
在这里插入图片描述

分离埃及分数:设 e = c + 1,一个真分数减去一个最大埃及分数之后,原来的a 变成了,a * e - b,原来的b变成了 b * e
C语言练习——提高篇_第15张图片
最后一种情况:如果分子是3而且分母是偶数,直接分解成两个埃及分数1/(b/2)和1/b,结束。

3/b=2/b+1/b
因分母为偶数,所以变量b—定是2的倍数,2/b经过约分之后能得到一个埃及分数,加上1/b。原分数分解为两个埃及分数。分离结束。

int deal2()
{
     
	int a = 8, b = 11, c;
	int i, j;
	//scanf_s("%d%d", &a, &b);
	printf("%d/%d=", a, b);
	while (1)
	{
     
		if (b % a)//分子%分母!=0,则能从原分数中分解出一个分母为(b/a)+1的埃及分数(8/11可以分解出1/2)
		{
     
			c = b / a + 1;
		}
		else//分子%分母==0,则该分数经过化简即可转换为埃及分数(如2/4=1/2)
		{
     
			c = b / a;
			a = 1;
		}
		if (a == 1)//分母为1,直接输出结束
		{
     
			printf("1/%d\n", c);
			break;
		}
		else
			printf("1/%d+", c);
		//减去该埃及分数,更新分子分母,继续遍历。
		a = a * c - b;
		b = b * c;
		if (a == 3 && b % 2 == 0)//3/b=2/b+1/b
		{
     
			printf("1/%d+1/%d\n", b / 2, b);
			break;
		}
	}
	return 0;
}

整数转为字符串

课后题是直接输出,我这里是存入字符数组,但是只能反向存储,输出也反过来输出即可,转换时要加字符0才能正确存储对应的编码

#include
void fun(int x)
{
     
	if (x > 9)
	{
     
		fun(x / 10);
	}
	printf("%d", x % 10);
}

void deal(int i, char c[], int n)//倒着存储的
{
     
	if (n > 9) {
     
		c[i] = n % 10+'0';//优先输出高位,要加上字符0才能正确存储
		deal(i+1, c, n / 10);
	}
	else {
     
		c[i] = n+'0';//只剩个位数的时候直接存入数组,但是要加\0结尾
		c[i+1] = '\0';
	}
	
}

int main()
{
     
	int a = 12345;
	char c[100];
	fun(a);
	deal(0, c, a);
	puts(c);
	return 0;
}

计算两个字符串最长子串长度

1,直接遍历,遇到相同的字符就单独拎出来遍历。

void Find(char A[], char B[])
{
     
	int i, j,k1, k2;
	int MAX_num = 0, num = 0;//记录子串数量
	for (i = 0; A[i]; ++i)//遍历字符串
	{
     
		for (j = 0; B[j]; ++j)
		{
     
			if (A[i] == B[j])//遇到相同的字符
			{
     
				k1 = i + 1;//在相同字符串后继续遍历
				k2 = j + 1;
				num = 1;//记录长度
				while (A[k1] && B[k2])
				{
     
					if (A[k1++] == B[k2++])
					{
     
						++num;
					}
					else
						break;
				}
				if (num > MAX_num)//记录最长子串数
					MAX_num = num;
			}
		}
	}
	printf("%d", MAX_num);
}

2,利用二维数组记录
思路:先对二维数组初始化,计算第0行第0列的情况,即与另一字符串首元素的情况。然后遍历两个字符串,遇到相同的元素就更新长度,不同的元素就设为0。

二维数组:
L[i][j]表示以s1[i] s2[j]结尾的相同子串的最大长度;
L[i][j]与L[i+1][j+1]只相差s1[i+1] s2[j+1]两对字符;
如下例:
S1 1234
S2 12312
L(1,4)=2,代表S1[1]、S2[4] 结尾的相同子串长度为2,即“12”
L(3,3)=0,虽然有重复的部分,但是代表的子串“1234”与“1231”整体来看是不同,长度为0,而不能记为3

C语言练习——提高篇_第16张图片
对比模式类似这样:
“1” “1” ,记1
“12” “12”,记2
“123” “123”,记3
“1234” “1231”,记0
只要结尾有一点不同,整个子串就不同,记住每一格代表的是整个子串,不能只看子串的某一部分;

void f(char s1[], char s2[])
{
     
	int MAX_num = 0;
	int s1_length = strlen(s1);
	int s2_length = strlen(s2);
	int i, j;
	int L[20][20];//记录子串长度,L[i][j]表示以s1[i] s2[j]结尾的相同子串的最大长度;
	//初始化矩阵,与另一字符串首元素对比
	for (i = 0; i < s1_length; ++i)
	{
     
		if (s1[i] == s2[0])
			L[i][0] = 1;
		else
			L[i][0] = 0;
	}
	for (j = 0; j < s2_length; ++j)
	{
     
		if (s2[j] == s1[0])
			L[0][j] = 1;
		else
			L[0][j] = 0;
	}
	//遍历字符串
	for (i = 1; i < s1_length; ++i)
	{
     
		for (j = 1; j < s2_length; ++j)
		{
     
			//L[i][j]与L[i+1][j+1]只相差s1[i+1] s2[j+1]两对字符
			if (s1[i] == s2[j])
			{
     
				L[i][j] = L[i - 1][j - 1] + 1;//相同,在上一步基础上长度+1
			}
			else
				L[i][j] = 0;//不同,则以s1[i+1] s2[j+1]结尾的子串不同
			//获取最长子串
			if (L[i][j] > MAX_num)
			{
     
				MAX_num = L[i][j];
			}
		}
	}
	printf("%d", MAX_num);
}

计算两个字符串最长子序列长度

与上一题不同,这里的序列是相对次序相同即可,即使中间有干扰元素也不影响。
如下例:
S1 123456
S2 246
其中2 4 6就是其中的一个公共子序列。

在这里插入图片描述
与之前的子串不同,这里遇到不相同的元素不记为0,而是继承上一步的长度,因为序列是可以隔着元素传递长度的
“12” “24”就是从“12” “2” 和“1” “24”中继承长度最长数量1。这样即使隔了几个不同的元素,前面“2”和“2”的匹配数量也会传递下去。

以第6列为例,对比模式类似这样:
“123456” “2” , 有相同子序列“2”,记1(从"12345" “2"继承而来 )
“123456” “24”,有相同子序列“24”,记2(从"12345” “24”;“123456” “2"继承而来 )
“123456” “246”,有相同子序列“246”,记3(从"12345” “246”;“123456” "24"继承而来 )

#include
#include
int Max(int a, int b) {
     
	return(a > b ? a : b);
}
void f(char s1[], char s2[])
{
     
	int MAX_num = 0;
	int s1_length = strlen(s1);
	int s2_length = strlen(s2);
	int i, j;
	int L[30][30];//记录子串长度,L[i][j]表示以s1[i] s2[j]结尾的相同子串的最大长度;
	//初始化矩阵,与另一字符串首元素对比
	for (i = 0; i < s1_length; ++i)
	{
     
		if (s1[i] == s2[0])
			L[i][0] = 1;
		else
			L[i][0] = 0;
	}
	for (j = 0; j < s2_length; ++j)
	{
     
		if (s2[j] == s1[0])
			L[0][j] = 1;
		else
			L[0][j] = 0;
	}
	//遍历字符串
	for (i = 1; i < s1_length; ++i)
	{
     
		for (j = 1; j < s2_length; ++j)
		{
     
			//L[i][j]与L[i+1][j+1]只相差s1[i+1] s2[j+1]两对字符
			if (s1[i] == s2[j])
			{
     
				L[i][j] = L[i - 1][j - 1] + 1;//相同,在上一步基础上长度+1
			}
			else
				L[i][j] = Max(L[i-1][j], L[i][j-1]);//不同,保留上一步的长度,不清零
			//获取最长子串
			if (L[i][j] > MAX_num)
			{
     
				MAX_num = L[i][j];
			}
		}
	}
	printf("%d", MAX_num);
}
int main()
{
     
	char a[] = "5fad3";
	char b[] = "9134f1asd4213";
	f(a, b);
	return 0;

}

农民过河

本质还是枚举,将所有情况都试一遍,如果遇到不符合条件的就回到上一步,然后尝试其他组合。直到到达终点。

数据结构:用二维数组存储四者的情况:0在西岸,1在东岸。初始四者都为0,每次农夫必定改变状态,其他三种可以都不改变状态或者只改变一个(和农夫同岸)的状态。最终目标是有限次的改变使得四者都变为1。

(1)每一步递归4种情况:什么都不带,依次带三种动物。如果是什么都不带,直接递归下一层,如果携带的动物和农夫在同一岸,携带该动物过河,递归下一层;不在同岸则遍历下一种情况。
(2)遇到以下情况就说明此步骤有误,直接return 回溯到上一步,尝试其他情况:
当前状态与之前的某一次状态一样,说明来回过河n次后回到起点。
羊单独与狼或白菜相处
(3)若当前步骤符合条件,就继续下一步,直到所有情况都枚举完毕。

最终成功到达对岸的两种情况。
C语言练习——提高篇_第17张图片
注意第二种,执行到第9步回溯了,最终执行8步到达对岸。
C语言练习——提高篇_第18张图片

#include
#include
#include
#define N 15
int a[N][4] = {
      {
     0,0,0,0} };//记录在南北岸情况
int b[N];//记录农夫过河情况
char name[4][N] = {
     //四种过河的情况
	" ",
	"和狼",
	"和羊",
	"和菜"
};
int search(int step)
{
     
	int i;
	if (a[step][0] + a[step][1] + a[step][2] + a[step][3] == 4)//全部过河
	{
     
		for (i = 0; i <= step; ++i)//输出每一步
		{
     
			printf("东岸:");//东岸情况
			if (a[i][0] == 0) printf("狼 ");
			if (a[i][1] == 0) printf("羊 ");
			if (a[i][2] == 0) printf("菜 ");
			if (a[i][3] == 0) printf("农民 ");
			if (a[i][0] && a[i][1] && a[i][2] && a[i][3])printf("none");
			printf("\t");
			printf("西岸:");//西岸情况
			if (a[i][0] == 1) printf("狼 ");
			if (a[i][1] == 1) printf("羊 ");
			if (a[i][2] == 1) printf("菜 ");
			if (a[i][3] == 1) printf("农民 ");
			if (!(a[i][0] || a[i][1] || a[i][2] || a[i][3]))printf("none");
			printf("\n");
			if (i < step)printf("第%d次", i);//次数
			if (i >= 0 && i < step)//过河的情况
			{
     
				if (a[i][3] == 0)
				{
     
					printf("\t--->农民");
					printf("%s\n", name[b[i] + 1]);
				}
				else
				{
     
					printf("\t<---农民");
					printf("%s\n", name[b[i] + 1]);
				}
			}
			printf("\n\n");
		}
		printf("************************\n\n\n");
		return 0;
	}
	for (i = 0; i < step; ++i)//若回到原点,和之前的情况一样,说明陷入循环,退出
	{
     
		if (memcmp(a[i], a[step], 16) == 0)
			return 0;
	}
	if (a[step][1] != a[step][3] && (a[step][2] == a[step][1] || a[step][0] == a[step][1]))//狼和羊或者羊和菜单独相处时
		return 0;
	for (i = -1; i <= 2; ++i)//遍历四种过河的情况
	{
     
		b[step] = i;//记录情况
		memcpy(a[step + 1], a[step], 16);//复制上一步
		a[step + 1][3] = 1 - a[step + 1][3];//改变农夫的状态
		if (i == -1)//农夫不带东西独自过岸
			search(step + 1);
		else if (a[step][i] == a[step][3])//农夫与遍历的东西在同岸
		{
     
			a[step + 1][i] = a[step + 1][3];//携带该物过河
			search(step + 1);
		}
		else//不在同岸,遍历下一个
			continue;
	}
	return 0;
}
int main()
{
     
	search(0);
	return 0;
}

求自守数

一个数平方的尾数等于该数自身。本题为大数问题,因为计算机无法处理过大的数,需要一定技巧处理。
以376为例:
C语言练习——提高篇_第19张图片
对本题,只关心乘积的后三位,并不是所有数都影响最后三位数
产生影响的分别是:

被乘数 乘数
第一个部分积 最后三位 倒数第一位
第二个部分积 最后两位 倒数第二位
第三个部分积 最后一位 倒数第三位

注意乘数在十位及以上都有权值,如7在十位是当成70来乘的。
C语言练习——提高篇_第20张图片
对于一个n位数,我们只需计算起作用部分的乘积,截取后n位然后相加。得到的结果如果等于原数,则为自守数。

被乘数 乘数
第一个部分积 最后n位 倒数第一位
第二个部分积 最后n-1位 倒数第二位
第n个部分积 最后1位 倒数第n位

用k来提取被乘数,2位数k为100,三位数k为1000;
用b来提取乘数,从10开始。
在这里插入图片描述

#include

int main()
{
     
	long mul, number, k, a, b;
	for (number = 0; number < 100000; ++number)//依次尝试每个数
	{
     
		for (mul = number, k = 10; (mul /= 10 ) > 0; k *= 10);//计算系数,1 10 100
		a = k;//截取部分积的系数
		mul = 0;//积的最后几位
		b = 10;//截取积相应位时的系数
		while (k >1)
		{
     
			mul = (mul + (number % k) * (number % b - number % (b / 10))) % a;
			k /= 10;
			b *= 10;
		}
		if (number == mul)
			printf("%ld\n", number);
	}
	return 0;
}

子孙数

子孙数:左边加上一个小于等于原始数的数,对新加的数继续加小于等于原始数的数,直到加不了。
6
16
26 16
36 16

因为只需要统计数目,则只需要对首元素处理。递归遍历每个小于等于原始数的数,并计数,最后返回得到了总的计数,要注意一开始第一个数要计1。

#include
int f[100];//存储记忆
int fun(int x)
{
     
	int i;
	if (f[x] != 0)//计算过就直接返回
		return f[x];
	else {
     
		int s = 1;//初始化为1,代表对当前数计1
		for (i = 1; i <= x / 2; ++i)//遍历每个子孙数,只需考虑首元素
			s += fun(i);
		f[x] = s;//记录当前的情况
		return f[x];
	}
}

int main()
{
     
	int i, j, n;
	scanf_s("%d", &n);//初始数
	f[1] = 1;//降为1的时候,不能再继续降低,只有其自身1个数
	int s = fun(n);
	printf("%d\n", s);
	return 0;
}

n个数中抽取r个组合

A[]中存n个数据,B中存选取r个数的下标
取r个数,第一个数在r-n中选取,假设下标为j
第二个数在r-1-j-1中选取(即下一个数的选取范围比上一个数小)
这样选r次即可选择出第一个组合。将其输出。然后继续挑选直到所有的组合都被选取。

#include
//a为原始数据
//b用于存储选取r个数在a中的下标
void combine(int n, int r, char a[], int b[], int R)//选R个,待选r个
{
     
	if (r == 0)//选择完r个数
	{
     
		int i = 0;
		for (i = 0; i < R; ++i)//输出组合
			printf("%c ", a[b[i]]);
		printf("\n");
	}
	else {
     
		for (int j = n; j >= r; --j) {
     //选取的范围r-n
			b[r - 1] = j - 1;
			combine(j - 1, r - 1, a, b, R);
		}
	}
}

int main()
{
     
	char a[] = "1234";
	int b[] = {
      0,1,2,3 };
	combine(4, 3, a, b, 3);
	return 0;
}

全排列

从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。(即从n个数中任取n个排列)

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

如对n个数进行全排列,先对第一个数进行选择,从1-n个数中选择一个数,与第一个数交换;然后对第二个数进行选择,从2-n个数选择一个数与第二个数交换;这样直到最后一个数,即确定了一个排列顺序。
为了找到所有排列,对第一个数要循环选择1-n个数,并且当找下一个方案时,要恢复到上一次选择时的排列顺序(回溯到上一次的状态,选择另一个路径)。

#include
#include

void swap(char& a, char& b)//交换ab的字符
{
     
	char temp = a;
	a = b;
	b = temp;
}

void AllRange(char* str, int k, int m)//计算str的全排列,m为str中字符数量.k为当前选择的元素
{
     
	if (k == m)
	{
     
		static int s_i = 1;//静态,函数结束后不会被销毁,下次调用函数值还保留
		printf("第%d个排列是%s\n", s_i++, str);
	}
	else
	{
     
		for (int i = k; i <= m; i++)//循环遍历k-m个元素,与第k个元素交换
		{
     
			swap(str[i], str[k]);//将第i个元素与第k个元素交换
			AllRange(str, k + 1, m);//递归
			swap(str[i], str[k]);//回溯,还原到交换前的状态
		}
	}
}

int main()
{
     
	char str[] = "123";
	AllRange(str, 0, strlen(str) - 1);
	return 0;
}

八皇后

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

我们选择一个一个的放置皇后。每当我们在选择移动一个皇后的位置的时候,其它已经放置好了的皇后的位置是不需要移动的,如果当某个皇后在属于它的行都不满足条件时,我们才需要回溯过去改变上一个皇后的位置。我们需要的就是循环的进行回溯操作,直到所有的皇后都放置完成。

1,存储结构:我们只需要定义一个一维数组就完全够用了。因为一行只能放一个皇后,我们用一维数组的下标表示皇后的行,值表示列。
(cnt,rol),则是当前处理的行列,我们循环进行摆放工作,因为不知道要循环多少次,使用while(1).

2,皇后摆放阶段
判断的是当前皇后摆放的位置能否被其他已经放置好了的皇后所攻击(通过循环和每一个已经摆放好的皇后进行比较),如果被攻击,则不能摆放;不能被攻击则进行摆放,记录好当前的行列值,然后让下一个皇后从第0列开始摆放,如果摆放满八个皇后,则打印出这种解法,并且寻找下一种解法。

3,回溯算法
当摆放好八个皇后时,首先cnt - - ;就是撤回摆放好的皇后(摆满八个皇后时,cnt是指向下一行,因此第一次cnt–,是撤回第八个皇后);去试一下下一列是否可以摆放,如果超过八列,则再回溯到上一个皇后。
当前摆放的皇后被其他皇后攻击时,先尝试下一列,如果当前列都试过了,就回溯到上一个皇后。寻找其他方案

4,while(1)退出条件
第一行的皇后已经摆放在第七列,且当前正在摆放第二行皇后,并且已经到了第六列位置,当前位置与上一个皇后在对角线上,不能摆放,而且也不能继续往后遍历了,也不能回溯了,因为第一行已经遍历到最后了,而第一行无法继续回溯上一行,即尝试完了所有的方案。
C语言练习——提高篇_第21张图片

#include 

int main() {
     
	int queen[8] = {
      0 };		//用来储存皇后的位置,下标为行,值为列 queen[0]=1表示第0行第1列

	int cnt = 0;			//表示摆放了几个皇后,也表示摆放皇后的行数。
	int col = 0;			//表示在这一列上摆放了皇后
	int sum = 0;			//总共有几种摆法
	while (1) {
     
		//在(cnt,col)这个坐标摆放皇后,以行为单位一行一行放置皇后,每次都从第一列开始遍历

		if (cnt == 1 && col == 6 && queen[0] == 7) {
     //表示第一行的皇后已经摆放在第七列,且当前在摆放第二行皇后,并且已经到了第六列位置,
			//再往后已经摆放不下皇后,退出循环
			break;
		}
		int isAttack = 0;		//用来表示皇后们之间是否能够攻击的到,如果攻击的到就是1,否则就为0
		int i = 0;
		//检测当前摆放位置能否被其他已经摆放的皇后攻击
		for (i = 0; i < cnt; i++) {
     
			if (queen[i] == col) {
     	//表示在同一列上
				isAttack = 1;
			}
			int div_row = cnt - i;		//表示斜线上的纵坐标之差
			int div_col = queen[i] - col;		//表示斜线上横坐标之差
			if (div_row == div_col || div_row == -div_col) {
      	//表示在同一斜线上
				isAttack = 1;
			}
		}
		if (isAttack == 0) {
     	//表示可以放置
			queen[cnt] = col;		//记录皇后当前的列数
			cnt++;					//开始摆放下一个皇后
			col = 0;				//下一个皇后从第一列开始遍历
			if (cnt == 8) {
     			//如果摆满了八个皇后就打印出他们的摆法
				for (i = 0; i < 8; i++) {
     
					printf("%d  ", queen[i] + 1);
				}
				printf("\n");
				sum++;				//摆放种数+1
				do {
     		//越界问题	//回朔
					cnt--;		//撤回正在摆放的皇后
					col = queen[cnt] + 1;		//往下一个列寻找摆放位置
				} while (col >= 8);
			}
		}
		else {
     			//表示不能摆放
			col++;
			while (col >= 8) {
     			//回朔
				cnt--;				//退一格
				col = queen[cnt] + 1;	//上一个皇后往后移一格
			}
		}
	}
	printf("总共有%d种摆法\n", sum);
	return 0;
}

背包问题

初始化表格,列代表容量,行代表物品;第0行列代表初始什么都不装的状态
在这里插入图片描述
遍历每个物品,并对每个物品遍历每个容量;
如果当前遍历的容量装不下该物品,就继承上一步,相同容量的价值(不装当前物品)
如果容量充足,则将不装当前物品与装当前物品的价值比较,保留价值大的那个方案。
这样每步都会保留价值最大的方案,直到遍历完就是最优方案
在这里插入图片描述

#include
int number=4, capacity=8;//物品数量 背包容量
int w[100]= {
      0,2,3,4,5 }, v[100] = {
      0,3,4,5,6 };//第i个物品的重量和价值
int V[100][100], item[100];//填的表,标记装入背包的物品
//函数必须先声明后使用,寻找具体放入背包的物品
void FindWhat(int a, int b)
{
     
	if (a >= 0)//从最后一行遍历到第0行
	{
     
		if (V[a][b] == V[a - 1][b])//如果与上一步一样,说明没有转当前物品
		{
     
			item[a] = 0;//标记当前物品为未装入背包
			FindWhat(a - 1, b);
		}
		else if (b - w[a] >= 0 && V[a][b] == V[a - 1][b - w[a]] + v[a])//如果容量充足,且是由上一步不装+当前物品价值而来,说明装了当前物品
		{
     
			item[a] = 1;//标记当前物品为装入背包
			FindWhat(a - 1, b - w[a]);

		}
	}
}
//填表工作
void FindMax()
{
     
	int i=0, j=0;
	for (i = 1; i <= number; ++i)
	{
     
		for (j = 1; j <= capacity; ++j)
		{
     
			if (j < w[i])//包当前容量不足
				V[i][j] = V[i - 1][j];//不装当前物品
			else//包当前容量足够
			{
     
				//不装当前物品的价值,与转当前商品(背包容量减少,价值增加)的价值对比,无论如何当前写入的都是最大价值的那个方案
				if (V[i - 1][j] > (V[i - 1][j - w[i]] + v[i]))
					V[i][j] = V[i - 1][j];//不装当前物品
				else
					V[i][j] = V[i - 1][j - w[i]] + v[i];//装当前物品
			}
		}
	}
	//最后的ij会保留最大价值
	FindWhat(i-1, j-1);
}



int main() {
     
	int i;
	//初始化,第0行、列
	for (i = 0; i < number; ++i)
	{
     
		V[i][0] = 0;
	}
	for (i = 0; i < capacity; ++i)
	{
     
		V[0][i] = 0; 
	}
	FindMax();
	//输出放入的物品和价值
	for (i = 1; i <= number; ++i)
	{
     
		if (item[i])
		{
     
			printf("%d:%d ", i,v[i]);
		}
	}
	return 0;
}

走马

有一个中国象棋中的“马”,在半张棋盘的左上角出发,右下角跳去。规定只允许向右跳(可上,可下,但不允许向左跳)求从起点A(1,1)到终点B(m,n)共有多少种不同的跳法
1,坐标系,起点为A(1,1),建立坐标系
C语言练习——提高篇_第22张图片
2,限制
马只能在棋盘内,因此位置限制为:srcx >= 0 && srcx <= 4 && srcy >= 0 && srcy <= 8
马从A出发不能走回头路,用数组chessboard[][]记录走过的坐标,chessboard[srcx][srcy] == 0才行

3,回溯
马只能向右走,只有四种走法,用两个数组dx[4],dy[4]控制每次走的方向;
遍历尝试每一种方法,遇到限制或者到达终点就回溯并重置状态,尝试每一种方案,如果到达终点则方案数量加1。

#include
static int chessboard[5][9] = {
      0 };//记录走过的位置
static int count = 0;//方案数量
const int dx[4] = {
      2,1,-1,-2 };//走的方向
const int dy[4] = {
      1,2,2,1 };
void horse_count(int srcx, int srcy, int destx, int desty)
{
     
	if (srcx >= 0 && srcx <= 4 && srcy >= 0 && srcy <= 8 && chessboard[srcx][srcy] == 0)//在棋盘范围内,并且没有走过这个位置
	{
     
		if (srcx == destx - 1 && srcy == desty - 1)//到达终点
		{
     
			++count;
			return;
		}
		chessboard[srcx][srcy] = 1;//标记已经走过
		int i;
		for (i = 0; i < 4; ++i)//遍历尝试下一步
		{
     
			horse_count(srcx + dx[i], srcy + dy[i], destx, desty);
		}
		chessboard[srcx][srcy] = 0;//回溯并重置
	}
}
int main()
{
     
	int m, n;
	scanf_s("%d %d", &m, &n);
	horse_count(0, 0, m, n);
	printf("%d", count);
	return 0;
}

你可能感兴趣的:(C与数据结构习题整理)