C语言特别篇--数组与函数的实践 + 函数递归

接下来我们将通过许多例子来更加深刻的理解数组与函数的相关知识(以下均是本人写题时错的或者自己觉得适合初学者有价值的,仅供参考):

一、二分查找

我们先来展示普通的二分查找代码,我们想找到一个数组元素对应的下标:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 0;
	scanf("%d", &k);
	int left = 0;
	int right = sizeof(arr) / sizeof(arr[1]) - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] > k)
		{
			right = mid - 1;
		}
		else if (arr[mid] < k)
		{
			left = mid + 1;
		}
		else
		{
			printf("对应下标是%d\n", mid);
			break;
		}
	}
	if (left > right)
	{
		printf("不在查找范围内\n");
	}
	return 0;
}

当我们输入一个7,屏幕上会打印"对应的下标是6",那我们该如何使用函数的知识来写呢?

#include<stdio.h>
int efcz(int arr[1], int left, int right, int k)
{
	int mid = 0;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] > k)
		{
			right = mid - 1;
		}
		else if (arr[mid] < k)
		{
			left = mid + 1;
		}
		else
		{
			return mid;
		}
	}
	return -1;//数组的下标没有负数这就表示没有找到
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 0;
	scanf_s("%d", &k);
	int left = 0;
	int right = sizeof(arr) / sizeof(arr[1]) - 1;
	int efcz();
	int ret = efcz(arr, left, right, k);
	if (ret == -1)
		printf("不在查找范围内\n");
	else
		printf("对应下标是%d\n", ret);
	return 0;
}

二、判断一个数是否为素数

想写这个代码之前我们要先明白什么是素数:素数,又称质数,是指在大于1的自然数中,除了 1 和它自身外,不能被其他自然数整除的数。

接下来我们便可以通过函数的方式来实现这串代码:

#include<stdio.h>
int is_sushu(int a)
{
	int i = 0;
	for (i = 2; i < a; i++)
	{
		if (a % i == 0)
			return 0;
	}
	return a;
}
int main()
{
	int a = 0;
	scanf_s("%d", &a);
	is_sushu(a);
	int ret = is_sushu(a);
	if (ret == 0)
		printf("不是素数\n");
	else
		printf("%d是素数\n", ret);
}

 这段代码的本质还是在考察我们对函数返回值的认知。

三、使用函数实现数组操作

创建一个整形数组,完成对数组的操作

  1. 实现print( )打印数组的每个元素
  2. 实现reverse( )函数完成数组元素的逆置。

要求:自己设计以上函数的参数,返回值。

#include<stdio.h>
void print(int arr[1], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
void reverse(int arr[1], int sz)
{
	int left = 0;
	int right = sz - 1;
	while (left <= right)
	{
		int tap = arr[left];
		arr[left] = arr[right];
		arr[right] = tap;
		left++;
		right--;
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[1]);
	print(arr, sz);
	reverse(arr, sz);
	print(arr, sz);
	return 0;
}

四、扫雷游戏

在线扫雷游戏:http://www.minesweeper.cn/

我们采用多文件的方式来实现这个游戏,首先我们添加一个test.c来写我们函数的主题,函数内容写在game.c里这也是我们游戏的运行逻辑,game.h来放我们的头文件,这样我们的代码逻辑就变得很清楚了。

1、初始化棋盘并打印看看效果
#include"game.h"
void menu()
{

}
void game()
{

}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default :
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
}

这是我们test.c文件中的大致框架,接下来我们要做的就是完善这个框架,我们这里的头文件是#include"game.h"是因为我们给#include<stdio.h>写在了game.h这个文件中。mine数组是我们希望存放雷的数组,而show数组是我们希望展示给玩家的数组。

这是我们目前想实现的一个框架,接下来就是自己去一步步实现这个功能,

 

这里game.c文件中第22行应该是for(i=1;i<=r;i++),第26行应该是for(j=1;j<=c;j++)。

这是我们写了其中的俩个部分,代码运行起来的结果如下:
 

我们可以看出目前是符合我们预期的效果的。

接下来我们要去思考如何布置雷呢?

 2、布置雷在mine数组中

我们采用rand函数来生成随机值来布置雷,我们运行代码看看俩次布置的雷是否处于不同位置:

我们可以看出我们生成了俩次雷,雷的位置不一样,这就符合了我们的预期,接下来我们就该去思考如何排查雷,这是游戏最关键的一步。

3、排查雷

 这时候我们需要定义一个新的函数叫findmine用来帮助我们排查雷,我们希望我们选中一个坐标,如果是雷则被炸死,如果不是雷则会告诉我们周围有几个雷这一系列基本的操作:

图中第73行有错误,应该是scanf("%d %d",&x,&y); 

这是我们写的主体函数现在已经基本符合我们对扫雷的要求但是还有一个缺点就是他现在while(1)这是一个死循环所以我们还需要设置一个胜利条件:

也就是我们设置一个变量win,当win等于我们的棋盘个数减去雷的数量的时候我们就取得了胜利。

这里我们写出代码:

下面我们整合代码:

*1、test.c
//test.c
#include"game.h"
void menu()
{
	printf("*************\n");
	printf("****1.play***\n");
	printf("****0.exit***\n");
	printf("*************\n");
}
void game()
{
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };

	//初始化棋盘
	initboard(mine, ROWS, COLS, '0');
	initboard(show, ROWS, COLS, '*');
	//布置雷
	setmine(mine, ROW, COL);
	
	// 打印棋盘
	//print(mine, ROW, COL);
	print(show, ROW, COL);
	
	//排查雷
	findmine(mine, show, ROW, COL);

}
int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请选择:>");
		scanf_s("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default :
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}
*2、game.h
//game.h
#include<stdio.h>
#include<stdlib.h>
#include<time.h>

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

#define easy_count 10

void initboard(char board[ROWS][COLS], int r, int c, char set);

void print(char board[ROWS][COLS], int r, int c);

void setmine(char mine[ROWS][COLS], int r, int c);

void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int r, int c);
*3、game.c
#include"game.h"
void initboard(char board[ROWS][COLS], int r, int c, char set)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			board[i][j] = set;
		}
	}
}
void print(char board[ROWS][COLS], int r, int c)
{
	int i = 0;
	for (i = 0; i <= r; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= r; i++)
	{
		int j = 0;
		printf("%d ", i);
		for (j = 1; j <= c; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}
void setmine(char mine[ROWS][COLS], int r, int c)
{
	int x = 0;
	int y = 0;
	//x的范围是1至9;y的范围是1至9
	int count = easy_count;
	while (count)
	{
		x = rand() % r + 1;
		y = rand() % c + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}
//
int getminecount(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int j = 0;
	int c = 0;
	for (i = -1; i <= 1; i++)
	{
		for (j = -1; j <= 1; j++)
		{
			if (mine[x + i][y + j] == '1')
				c++;
		}
	}
	return c;
}
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int r, int c)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < r * c - easy_count)
	{
		printf("请输入你要排查的坐标:");
		scanf_s("%d %d", &x, &y);
		if (x >= 1 && x <= r && y >= 1 && y <= c)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				print(mine, r, c);
				break;
			}
			else
			{
				if (show[x][y] = '*')
				{
					//统计mine数组中,x,y坐标周围有几个雷
					int c = getminecount(mine, x, y);
					show[x][y] = c + '0';
					print(show, ROW, COL);
					win++;
				}
				else
				{
					printf("该坐标已经被排查过了,请重新输入\n");
				}
			}
		}
		else
		{
			printf("输入的坐标不合法,请重新输入\n");
		}
	}
	if (win == r * c - easy_count)
	{
		printf("排雷成功\n");
		print(mine, r, c);
	}
}

五、VS一些实用的调试技巧

1、Debug与Release

首先我们要明白debug与release:
 

 Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序;

程序员在写代码的时候,需要经常性的调试代码,就将这里设置为debug ,这样编译产生的是 debug版本的可执行程序,其中包含调试信息,是可以直接调试的。

Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的, 以便用户很好地使用。

当程序员写完代码,测试再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为 release ,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。

2、调试快捷键

F9:创建断点和取消断点

断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂停执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。

条件断点:满足这个条件,才触发断点

F5:启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用。

F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。

F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,直接完成函数调用。

CTRL+F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

VS更多快捷键了解:http://blog.csdn.net/mrlisky/article/details/72622009

3、常见的错误
*1、编译型错误

编译型错误⼀般都是语法错误,这类错误⼀般看错误信息就能找到⼀些蛛丝马迹的,双击错误信息也能初步的跳转到代码错误的地方或者附近。编译错误,随着语言的熟练掌握,会越来越少,也容易解决。

*2、链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。⼀般是因为:

标识符名不存在、拼写错误、头文件没包含、引用的库不存在...

*3、运行时错误

这个需要我们自行根据代码进行调试来判断错误了。

六、彩蛋 (函数递归)

1、初识递归

首先我们要先明白什么是递归,递归我们可以拆开成递推回归说大白话就是函数自己调用自己
接下来我们写一个最简单的递归程序:
 

#include<stdio.h>
int main()
{
	printf("hello,world\n");
	main();
	return 0;
}

这与我们第一次学习C语言时候的代码非常相似,但是这串代码不断打印hello,world直到栈溢出,

每一次函数的调用都会在内存的栈区申请空间,如果这样的函数无限的递归下去便会造成栈溢出(大家先简单了解一下,下一期我们写一篇博客细细介绍栈溢出......),递推的思想就是一步步拆解问题,其本质就是一个大事化小的一个过程。

2、递归的条件

递归在书写的时候,有2个必要条件:

<1>、递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。

<2>、每次递归调用之后越来越接近这个限制条件。

3、实列
*1、求n的阶乘

题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。

n的阶乘的公式:n! = n ∗ (n − 1)!  

举例: 5! = 5*4*3*2*1            4! = 4*3*2*1           所以:5! = 5*4!

n的阶乘和n-1的阶乘是相似的问题,但是规模要少了n。有⼀种有特殊情况是:当 n==0 的时候,n的阶乘是1,而其余n的阶乘都是可以通过上面的公式计算。

接下来我们给出思维导图:

这便是递归的来源,下面我们便可以通过代码来实现了:
<1>、迭代

#include<stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = 1;
	for (int i = 1; i <= n; i++)
	{
		ret = ret * i;
	}
	printf("%d ", ret);
	return 0;
}

 <2>、递归

#include<stdio.h>
int fac(int n)
{
	if (n == 0)
		return 1;
	else//n > 0
		return fac(n - 1) * n;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = fac(n);
	printf("%d ", ret);
	return 0;
}

这是他的推演导图:

从这幅图中我们也可以更加清晰明白:递推时会在栈区占用空间,在回归时原来占用的空间也会随之被销毁。 

*2、 顺序打印一个整数的每一位

题目:输入一个整数m,按照顺序打印整数的每一位。

比如:
输入:1234        输出:1 2 3 4
输入:520          输出:5 2 0

我们采用递归便可非常简单的实现:

#include<stdio.h>
void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	print(n);
	return 0;
}

这是他的推演导图:

*3、 求第n个斐波那契数

在写这个例子之前我们先要知道什么是斐波那契数:1  1  2  3  5  8  13  21  34  55...我们不难从中发现规律:前俩个数字之和等于后面一个,所以我们可以构建框架:

 

下面便是代码的实现:

#include<stdio.h>
int fib(int n)
{
	if (n > 0 && n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = fib(n);
	printf("%d ", ret);
	return 0;
}

 这时候大家肯定觉得函数递归这么好用可以给一个问题简化成这样,但是并非所有题目都可以使用函数递归,前面我们也说过了函数递归是有限制条件的,而且我们利用这段代码求第50个斐波那契数的时候就会发现程序走的很慢,需要很长时间这就是函数递归的缺点之一。

接下来我们使用迭代的方法来试试

#include<stdio.h>
int fin(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n >= 3)
	{
		a = b;
		b = c;
		c = a + b;
		n--;
	}
	return c;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int r = fin(n);
	printf("%d\n", r);
	return 0;
}

我们发现迭代的方式相比较于递归的方式计算更加迅速。

七、下期预告

函数栈帧的创建和销毁......

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值