C语言程序设计学习笔记:P8-数组


一、数组

1.1 初试数组

之前我们写了一个程序计算用户输入的数字的平均数。在该程序中,不需要记录输入的每一个数,只需每读入一个数将其累加到记录总和的sum变量中,同时将记录输入数字数量的变量cnt加1。最后sum/cnt即可求出平均数。
在这里插入图片描述


那如何写一个程序计算用户输入的数字的平均数,并输出所有大于平均数的数?
思路

针对这个问题,我们必须先记录每一个输入的数字,然后计算平均数。之后,再检查记录下来的每一个数字,与平均数比较,决定是否输出。那我们如何记录所有的数字呢?如果定义许多变量num1、num2、num3…来记录很不现实,没完没了。因此,我们需要数组这个东西。

我们写出代码如下。在代码中,我们写了一小段测试的代码,打印当前输入了多少个数字以及数组读取的所有的数字。

#include <stdio.h>

int main()
{
    
    
	int x;
	double sum = 0;
	int cnt = 0;
	int number[100]; //定义一个数组,存放每次输入的数据
	scanf_s("%d", &x);
	while (x != -1)
	{
    
    
		number[cnt] = x; //存放当前输入的数据
		//测试,打印每次这个数组的变化情况
		{
    
    
			int i;
			printf("%d\t", cnt);
			for (i=0; i<=cnt; i++){
    
    
				printf("%d\t", number[i]);
			}
			printf("\n");
		}
		sum += x;
		cnt ++;
		scanf_s("%d", &x);
	}

	if (cnt > 0)
	{
    
    
		int i;
		double average = sum / cnt;
		for (int i = 0; i < cnt; i++)
		{
    
    
			if (number[i] > average) {
    
    
				printf("%d ", number[i]);
			}
		}
	}

	return 0;
}

运行,可以看出每次使用scanf输入一个数字后,都会存放在数组中,数组的长度也越来越长。
在这里插入图片描述


这段代码中,我们分别定义了一个数组、对数组中的元素进行赋值、然后去遍历数组中各个元素并打印。
在这里插入图片描述
这段代码有个安全隐患:我们定义的数组大小是100,但是在运算过程中我们从来没有去判断过cnt是否会超过数组可以使用的下标。

1.2 数组运算

关于数组的定义

<类型> 变量名称[元素数量];
  int grades[100];
  double weight[20];
元素数量必须是整数
C99之前:元素数量必须是编译时刻确定的字面量

从宏观的角度来看

• 数组是一种容器(放东西的东西),特点是:
  • 其中所有的元素具有相同的数据类型;
  • 一旦创建,不能改变大小
  • 数组中的元素在内存中是连续依次排列的

举个例子,对于一个数组int a[10]

• 一个int的数组
• 10个单元:a[0],a[1],…,a[9]
在这里插入图片描述
• 每个单元就是一个int类型的变量
• 可以出现在赋值的左边或右边:
• a[2] = a[1]+6;(放在左边,我们是在向其写入东西。放在右边,我们是在读取内容)
• 在赋值左边的叫做左值,在赋值右边的叫做右值


数组的单元

• 数组的每个单元就是数组类型的一个变量
• 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数,到数组大小-1结束:
  • grades[0]
  • grades[99]
  • average[5]

有效的下标范围

• 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
• 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
  • segmentation fault
• 但是也可能运气好,没造成严重的后果
• 所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]

我们来写一段代码测试数组越界。

#include <stdio.h>
void f(void);

int main()
{
    
    
	f();
	printf("here\n");
	return 0;
}

void f(void)
{
    
    
	int a[10];
	a[10]=0;
}

我们运行,发现执行这个函数就出错了,那句printf根本没有执行。
在这里插入图片描述


因此,我们最开始写的打印大于平均数的所有数字的程序是危险的,因为输入的数据可能超过100个。
解决方案
1、判断计数值,当数组存放满了就停止存放。
2、先让用户输入有多少数字要计算,可以用C99的新功能来实现

方案2的代码如下。注意:在Visual Studio中运行无法通过,会报错E0028表达式必须含有常量值。这是由于Dev C++使用GCC编译器,它允许将变量作为数组元素值。Visual Studio编译器则不允许。解决办法为使用动态内存分配,这些内容后面再展开。

#include <stdio.h>

int main()
{
    
    
	int x;
	double sum = 0;
	int cnt;
	printf("请输入数字的数量:");
	scanf_s("%d", &cnt);
	if (cnt > 0) {
    
    
		int number[cnt];
		scanf_s("%d", &x);
		while (x != -1) {
    
    
			number[cnt] = x;
			sum += x;
			cnt++;
			scanf_s("%d", &x);
		}
	}

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

我们也可以创建长度为0的数组,如int a[0];。但是这是无意义的,即使是下标0也会越界。

1.3 数组的例子

写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数,输入-1表示结束。
思路

1、由于输入的数字为0-9,我们可以定义一个长度为10的数组,每个位置上对应其下标数字出现的次数。
2、首先将数组中所有的元素赋值为0
3、依次读取数字,直到读入的数字为-1
4、遍历数组中各个元素并打印出来

我们写出代码如下:

#include <stdio.h>

int main()
{
    
    
	const int number = 10;
	int x;
	int count[number];
	int i;

	for (i = 0; i < number; i++) {
    
    
		count[i] = 0;
	}
	scanf_s("%d", &x);
	while (x != -1) {
    
    
		if (x >= 0 && x <= 9) {
    
    
			count[x]++;
		}
		scanf_s("%d", &x);
	}
	for (i = 0; i < number; i++) {
    
    
		printf("%d:%d\n", i, count[i]);
	}
	return 0;
}

运行,可以看出结果正确。
在这里插入图片描述


在这段代码中,数组参与运算的环节如下:①定义数字大小 ②定义数组 ③初始化数组 ④数组参与运算 ⑤遍历数组。这是数组非常经典的参与运算的步骤。
在这里插入图片描述

二、数组运算

2.1 数组运算

搜索是现代计算机频繁发生的事情。我们来看看最基本最简单的搜索怎么做。
问题:在一组给定的数据中,如何找出某个输入的数据是否存在?
我们首先写出代码如下:

#include <stdio.h>

int search(int key, int a[], int length);
int main(void)
{
    
    
	int a[] = {
    
    2,4,6,7,1,3,5,9,11,13,23,14,42};
	int x;
	int loc;
	printf("请输入一个数字:");
	scanf("%d", &x);
	loc=search(x, a, sizeof(a)/sizeof(a[0]));
	if (loc != -1) {
    
    
		printf("%d在第%d个位置上\n", x, loc);
	}
	else {
    
    
		printf("%d不存在",&x);
	}

	return 0;
}

int search(int key, int a[], int length)
{
    
    
	int ret = -1;
	int i;
	for (i = 0; i < length; i++) {
    
    
		if (a[i] = key) {
    
    
			ret = i;
			break;
		}
	}
	return ret;
}

这个程序有许多之前没有见过的东西,我们慢慢来分析。
1、数组的集成初始化

int a[] = {2,4,6,7,1,3,5,9,11,13,23,14,42};

之前我们都是使用int a[10]之类的方法定义数组。现在我们直接用大括号给出数组的所有元素的初始值。不需要给出数组的大小,编译器替你数数。我们来测试一下初始化过后数组各个位置的值。

int main(void)
{
    
    
	int a[] = {
    
    2,4,6,7,1,3,5,9,11,13,23,14,42};
	{
    
    
		int i;
		for (i = 0; i < 13; i++) {
    
    
			printf("%d\t",a[i]);
		}
		printf("\n");
	}
	return 0;
}

运行,可以发现数组已经成功被初始化了。
在这里插入图片描述
如果我们使用

int a[13] = {2};

第一个单元被赋值为2,其余位置赋值为0。
在这里插入图片描述


2、集成初始化时的定位
我们可以以下初始化值的方法,对特定位置上赋特定的值。

int a[10]={[0]=2, [2]=3, 6,}

我们写出代码来测试:

int main(void)
{
    
    
	int a[] = {
    
     [1]=2,4,[5]=6};
	{
    
    
		int i;
		for (i = 0; i < 6; i++) {
    
    
			printf("%d\t",a[i]);
		}
		printf("\n");
	}
	return 0;
}

虽然没有指定长度,但是括号内最大的下标为5,所以数组大小为6。最终初始化情况如下:
在这里插入图片描述
因此,集成初始化总结如下:

• 用[n]在初始化数据中给出定位
• 没有定位的数据接在前面的位置后面
• 其他位置的值补零
• 也可以不给出数组大小,让编译器算
• 特别适合初始数据稀疏的数组

3、数组的大小

在前面的代码中,我们都是自己手动输入数组的大小,非常不方便,那么有什么方法自动获取数组大小吗?

• 可以使用sizeof(a)/sizeof(a[0])
• sizeof(a)给出整个数组所占据的内容的大小,单位是字节!
• sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数
• 这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码

我们写出代码并测试,可以看出结果正确。
在这里插入图片描述


4、数组赋值
如果我定义了一个数组,然后想把它赋值给另外一个数组,可以吗?

int a[] = {
    
    2,4,6,7,1,3,5,9,11,13,23,14,42};
int b[] = a[];

答案是:不能!数组变量本身不能被赋值。要把一个数组的所有元素交给另一个数组,必须采用
遍历。

for (int i = 0; i < length; i++){
    
    
	b[i] = a[i];
}

5、遍历数组

遍历数组时,通常都是使用for循环,让循环变量i从0到<数组的长度,这样循环体内最大的i正好是数组最大的有效下标。

• 常见错误是:
• 循环结束条件是<=数组长度
• 离开循环后,继续用i的值来做数组元素的下标!

当我们将数组作为参数传递进函数时,往往必须再用另一个参数来传入数组的大小。原因如下,具体原因我们后面在指针部分来详细讲解。

• 函数头我们写成这样:int search(int key, int a[], int length);
• 数组a[]作为函数的参数时:
• 不能在[]中给出数组的大小,如果在里面写个数字也是无效的。
• 不能再利用sizeof来计算数组的元素个数!

2.2 数组例子

前面我们写过判断当前数是否是素数的代码,我们需要从2到x-1测试是否可以整除。对于n要循环n-1遍,当n很大时就是n遍。
在这里插入图片描述
这时,有人会发现一个规律:除了2之外的所有偶数都不是素数。因此可如果x是偶数,立刻可以判定出不是素数。如果不是偶数,需要循环(n-3)/2+1遍,当n很大时就是n/2遍。和原来相比,我们只需一半的时间。
在这里插入图片描述
实际上我们只需要做sqrt(x)遍就行了。
在这里插入图片描述


那么,还有没有比sqrt(x)更快的方法呢?有的!我们不需要比x小的数来测试x是否是素数,我们只需拿比x小的素数来测试x是否是素数就行了。因为素数是比较少的数,因此复杂度又会降低。但是这就需要我们有一张素数表,根据这张表来判断我的x是否是素数。现在我们正在构造素数表,需要向表中输入前100个素数,代码如下:

#include <stdio.h>

int isPrime(int x, int knownPrimes[], int numberOfKonwnPrimes);

int main(void)
{
    
    
	const int number = 10;
	int prime[10] = {
    
    2};   //素数表,初始化为2,因为2是第一个素数
	int count = 1;          //素数表现在有的素数的数量
	int i = 3;              //从3开始看这个数是否是素数

	//测试代码,开始
	{
    
    
		int i;
		printf("\t\t");
		for(i = 0; i < number; i++) {
    
    
			printf("%d\t", i);
		}
		printf("\n");

	}
	//测试代码,结束

	while (count < number) {
    
    
		if (isPrime(i, prime, count)) {
    
    
			 //如果当前这个i是素数,就写到prime里面第count位去,然后count+1
			prime[count++] = i;
		}

		//测试代码,开始
		{
    
    
			printf("i=%d \tcnt=%d\t", i, count);
			int i;
			for (i = 0; i < number; i++) {
    
    
				printf("%d\t", prime[i]);
			}
			printf("\n");
		}
		//测试代码,结束

		i++;
	}

	//打印输出,每行输出5个
	for (i = 0; i < number; i++) {
    
    
		printf("%d", prime[i]);
		if((i+1)%5)  printf("\t");
		else printf("\n");
	}

	return 0;
}

int isPrime(int x, int knownPrimes[], int numberOfKnownPrimes)
{
    
    
	int ret = 1;
	int i;
	for (i = 0; i < numberOfKnownPrimes; i++) {
    
    
		if (x%knownPrimes[i] == 0) {
    
    
			ret = 0;
			break;
		}
	}
	return ret;
}

我们运行,结果如下。可以看出每次判断出一个数是素数时,就依次向Prime数组中添加。如果不是,就不添加,并判断下一个数是否是素数。
在这里插入图片描述
在这段代码中,我们用了Prime[cnt++]=i这行代码。这行代码是我们经常用的,它做了两个事:①将i写到Prime[cnt]去②cnt+1,移动到数组下一个单元。具体细节如下图所示:
在这里插入图片描述


现在我想欲构造n以内(不含)的素数来构建素数表,我们的算法如下:

1. 令x为2
2. 将2x、3x、4x直至ax<n的数标记为非素数
3. 令x为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕

伪代码如下:

1. 开辟prime[n],初始化其所有元素为1,prime[x]为1表示x是素数
2. 令x=2
3. 如果x是素数,则对于(i=2;x*i<n;i++)令prime[i*x]=0
4. 令x++,如果x<n,重复3,否则结束

我们写出代码如下:

#include <stdio.h>

int main()
{
    
    
	const int maxNumber = 10;
	int isPrime[10];   
	int i;      
	int x;
	for (i = 0; i < maxNumber; i++) {
    
    
		isPrime[i] = 1;
	}

	//测试代码,开始
	printf("\t");
	for (i = 2; i < maxNumber; i++) {
    
    
		printf("%d\t", i);
	}
	printf("\n");
	//测试代码,结束

	//从2开始遍历小于25的各个值
	for (x = 2; x < maxNumber; x++) {
    
    
		if (isPrime[x]) {
    
    
			//如果x是素数,将小于maxNumber的x的整数倍的数标记为非素数
			for (i = 2; i*x < maxNumber; i++) {
    
    
				isPrime[i*x] = 0;
			}
		}

		//测试代码,开始
		printf("%d\t", x);
		for (i = 2; i < maxNumber; i++) {
    
    
			printf("%d\t", isPrime[i]);
		}
		printf("\n");
		//测试代码,结束
	}
	//打印素数表中所有素数
	for (i = 2; i < maxNumber; i++) {
    
    
		if (isPrime[i]) {
    
    
			printf("%d\t", i);
		}
	}
	printf("\n");

	return 0;
}

我们运行,测试一下10以内的素数表,可以看出结果正确。
在这里插入图片描述

2.3 二维数组

数组除了有一维的,还有二维的、三维的、四维的…二维数组的定义如下:

int a[3][5];
通常理解为a是一个3行5列的矩阵
在这里插入图片描述


二维数组的遍历
对于一个二维数组,最重要的事情便是对它做遍历。对二维数组便利需要两层循环,外面那一层循环行号,里面那一层循环列号。其中,a[i][j]是一个int,表示第i行第j列上的单元。
在这里插入图片描述


二维数组的初始化
二维数组的初始化有很多种方法:
①常规的初始化方法:

int a[2][5]={
	{0,1,2,3,4},
	{2,3,4,5,6},
};

②可以省略行数,由编译器来数,列数是必须给出的。

int a[][5]={
	{0,1,2,3,4},
	{2,3,4,5,6},
};

②可以按照一维数组的方式来初始化。

int a[2][5]={0,1,2,3,4,2,3,4,5,6};

建议使用第一种,这样更加直观。如果初始化的元素不够,剩下的用0补齐。

tic-tac-toe游戏
读入一个3X3的矩阵,矩阵中的数字为1表示该位置上有一个X,为0表示为O。现在需要写程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符X或O,或输出无人获胜。(这个案例能够让我们学会如何去遍历二维矩阵的一行、一列和对角线)我们写出代码如下:

#include <stdio.h>

int main()
{
    
    
	const int size = 3;
	int board[3][3];
	int i,j;
	int numOfX;
	int numOfO;
	int result = -1;

	//读入矩阵
	for (i = 0; i < size; i++) {
    
    
		for (j = 0; j < size; j++) {
    
    
			scanf_s("%d", &board[i][j]);
		}
	}

	//检查行
	for (i = 0; i < size && result == -1; i++) {
    
    
		numOfO = numOfX = 0;
		for (j = 0; j < size; j++) {
    
    
			if(board[i][j]==1){
    
    
				numOfX++;
			}else{
    
    
			numOfO ++;
			}
		}	
	if (numOfO == size) {
    
    
		result = 0;
	}
	else if (numOfX == size) {
    
    
		result = 1;
	}
}

	//检查列
	if (result == -1){
    
    
		for (j = 0; j < size && result == -1; j++) {
    
    
			numOfO = numOfX = 0;
			for (i = 0; i < size; i++) {
    
    
				if (board[i][j] == 1) {
    
    
					numOfX++;
				}
				else {
    
    
					numOfO++;
				}
			}
			if (numOfO == size) {
    
    
				result = 0;
			}
			else if (numOfX == size) {
    
    
				result = 1;
			}
		}
	}

	//检查正对角线
	if (result == -1) {
    
    
		numOfO = numOfX = 0;
		for (i = 0; i < size; i++) {
    
    
			if (board[i][i] == 1) {
    
    
				numOfX++;
			}
			else {
    
    
				numOfO++;
			}
		}
		if (numOfO == size) {
    
    
			result = 0;
		}
		else if (numOfX == size) {
    
    
			result = 1;
		}
	}

	//检查反对角线
	if (result == -1) {
    
    
		numOfO = numOfX = 0;
		for (i = 0; i < size; i++) {
    
    
			if (board[i][size-i-1] == 1) {
    
    
				numOfX++;
			}
			else {
    
    
				numOfO++;
			}
		}
		if (numOfO == size) {
    
    
			result = 0;
		}
		else if (numOfX == size) {
    
    
			result = 1;
		}
	}

	printf("%d赢了",result);

	return 0;
}

运行,输入

1 1 1
0 1 0
1 0 1

可以看出结果正确
在这里插入图片描述

小测验

1、若有定义:

int a[2][3];

则以下选项中不越界的正确的访问有:
A. a[2][0]
B. a[2][3]
C. a[1>2][0]
D. a[0][3]
答案:C

2、以下程序片段的输出结果是:

int m[][3] = {
    
    1,4,7,2,5,8,3,6,9,};
int i,j,k=2;
for ( i=0; i<3; i++ ) {
    
    
    printf("%d", m[k][i]);
}

A. 369
B. 不能通过编译
C. 789
D. 能编译,但是运行时数组下标越界了
答案:A

3、假设int类型变量占用4个字节,定义数组

int x[10] = {
    
    0,2,4};

则x在内存中占据几个字节?
答案:40

4、若有:

int a[][3] = {
    
    {
    
    0},{
    
    1},{
    
    2}};

则a[1][2]的值是?
答案:0

猜你喜欢

转载自blog.csdn.net/InnerPeaceHQ/article/details/121497798
今日推荐