C语言学习笔记(七)数组

数组

1. 初试数组

在讲循环的时候,有这样一个例子:

  • 如何写一个程序计算用户输入的数字的平均数?

当时都程序是这样的:

#include "stdio.h"

int main()
{
	int x;
	double sum = 0;
	int cnt = 0;
	scanf("%d", &x);
	while (x != -1)
	{
		sum += x;
		cnt++;
		scanf("%d", &x);
	}
	if (cnt > 0)
	{
		printf("%f\n", sum / cnt);
	}
	
	return 0;
}

这样的代码不需要记录输入的每一个数。

如果题目变了:

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

这个时候,不记录输入的每一个数就不行了,因为每个数输入进来的时候我们不知道它的平均数是多少,所以单独一个数输入进来我们无法确认这个数要不要输出,只能把所有的数存下来,等输入结束以后算出平均数,再来判断每一个数要不要输出。

那么,如何记录很多数呢?

  • int num1, int num2,…?
    到底要定义多少变量呢?显然这种方法不可取。

所有我们要用新的编程手段,也就是本章的数组。

数组

  • int number[100];
  • scanf(“%d”, &number[i]);

我们可以在之前的代码基础上做一些修改,让它能记录读进来的每一个数,然后来做些判断:

#include "stdio.h"

int main()
{
	int x;
	double sum = 0;
	int cnt = 0;
	int number[100];
	scanf("%d", &x);
	while (x != -1)
	{
		number[cnt] = x;
		sum += x;
		cnt++;
		scanf("%d", &x);
	}
	if (cnt > 0)
	{
		printf("%f\n", sum / cnt);
		for (int i = 0; i < cnt; i++)
		{
			if (number[i] > sum / cnt)
			{
				printf("%d\n", number[i]);
			}	
		}
	}
	return 0;
}

这就是第一次使用数组,程序的样子!

为了搞清楚number[cnt] = x;是怎样做事情的,我们在后面加入一段代码,来观察number数组里的值是怎么变化的:

#include "stdio.h"

int main()
{
	int x;
	double sum = 0;
	int cnt = 0;
	int number[100];
	scanf("%d", &x);
	while (x != -1)
	{
		number[cnt] = x;
		{
			printf("%d\t", cnt);
			for (int i = 0; i <= cnt; i++)
			{
				printf("%d\t", number[i]);
			}
			printf("\n");
		}
		sum += x;
		cnt++;
		scanf("%d", &x);
	}
	if (cnt > 0)
	{
		printf("%f\n", sum / cnt);
		for (int i = 0; i < cnt; i++)
		{
			if (number[i] > sum / cnt)
			{
				printf("%d\n", number[i]);
			}	
		}
	}
	return 0;
}

结果如下:
在这里插入图片描述

程序中如何使用数组可以看下面的图片:
在这里插入图片描述

但是这个程序有一个安全影患,是什么?

在这个程序中,定义的数组大小是100,可是我们在整个程序中从来没有去判断变量cnt有没有超过number数组可以使用的最大下标,这件事后续还会仔细讲。

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开始计数:
    • grades[0]
    • grades[99]
    • average[5]

有效的下标范围

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

所以我们之前的那个程序是有bug的:

#include "stdio.h"

int main()
{
	int x;
	double sum = 0;
	int cnt = 0;
	int number[100];
	scanf("%d", &x);
	while (x != -1)
	{
		number[cnt] = x;
		sum += x;
		cnt++;
		scanf("%d", &x);
	}
	if (cnt > 0)
	{
		printf("%f\n", sum / cnt);
		for (int i = 0; i < cnt; i++)
		{
			if (number[i] > sum / cnt)
			{
				printf("%d\n", number[i]);
			}	
		}
	}
	return 0;
}

这个程序是危险的,因为输入的数据可能超过100个。
解决方案:
cnt计数,如果发现cnt已经达到100了,也就是已经读满100个数了,告诉用户不能再读数了。

长度为0的数组?

  • int a[0];
  • 可以存在,但是无用

3. 数组例子:统计个数

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

思考:这个程序要不要记录输入进来的每一个数呢?不需要!只要记录每一种数字出现多少次,0-9一共10个数字,只要10个变量就可以了,但10个变量太复杂,我们可以用数组。

代码如下:

#include "stdio.h"

int main()
{
	const int number = 10;
	int x;
	int count[number];
	for (int i = 0; i < number; i++)
	{
		count[i] = 0;
	}
	
	scanf("%d", &x);
	while (x != -1)
	{
		if (x >=0 && x <=9)
		{
			count[x]++;
		}
		scanf("%d", &x);
	}
	for (int i = 0; i < number; i++)
	{
		printf("%d出现了%d次\n", i, count[i]);
	}
	
	return 0;
}

在这里插入图片描述

4. 数组运算

  • 在一组给定的数据中,如何找出某个数据是否存在?

代码如下:

#include "stdio.h"

/**
找出key在数组a中的位置
@param key 要寻找的数字
@param a 要寻找的数组
@param length 数组a的长度
@return 如果找到,返回其在a中的位置;如果找不到则返回-1
*/
int search(int key, int a[], int length);

int main()
{
	int a[] = {2, 4, 6, 7, 1, 3, 5, 9, 11, 13, 23, 14, 32};
	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不存在\n", x);
	}

	return 0;
}

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

这个程序有很多之前没见过的东西,我们一点一点来看。

数组的集成初始化
int a[] = {2, 4, 6, 7, 1, 3, 5, 9, 11, 13, 23, 14, 22};

  • 直接用大括号给出数组的所有元素的初始值
  • 不需要给出数组的大小,编译器替你数数

集成初始化时的定位(仅C99)
int a[10] = {[0] = 2, [2] = 3, 6,};

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

数组的大小

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

数组的赋值
int a[] = {2, 4, 6, 7, 1, 3, 5, 9, 11, 13, 23, 14, 22};
int b[] = a; 这种写法是错误

  • 数组变量本身不能被赋值
  • 要把一个数组的所有元素交给另一个数组,必须要采用遍历
    for (i = 0; i < length; i++) {b[i] = a[i];}

遍历数组

  • 通常都是使用for循环,让循环变量i0<数组的长度,这样循环体内最大的i正好是数组最大的有效下标
  • 常见错误是:
    • 循环结束条件是<=数组长度,或
    • 离开循环后,继续用i的值来做数组元素的下标

注:1. 数组作为函数参数时,往往 必须再用另一个参数来传入数组的大小;
       2. 数组作为函数的参数时:① 不能在[ ]中给出数组的大小;② 不能再利用sizeof来计算数组元素的个数

5. 数组例子:素数

我们再回过头来看之前判断素数的例子,代码如下:

#include "stdio.h"

int isPrime(int x);

int main()
{
	int x;
	scanf("%d", &x);
	if (isPrime(x))
	{
		printf("%d是素数\n", x);
	} else
	{
		printf("%d不是素数\n", x);
	}
	
	return 0;
}

int isPrime(int x)
{
	int ret = 1;
	if (x == 1)
	{
		ret = 0;
	}
	for (int i = 2; i < x; i++)
	{
		if (x % i == 0)
		{
			ret = 0;
		}
		break;
	}
	
	return ret;
}

2x-1测试是否可以整除

  • 对于n要循环n-1
    • n很大时就是n

上面的方法循环要执行n次,下面再介绍一种循环次数更少的方法:

去掉偶数后,从3x-1,每次加2

  • 如果x是偶数,立刻
  • 否则要循环(n-3)/2+1
    • n很大时就是n/2

代码如下:

#include "stdio.h"

int isPrime(int x);

int main()
{
	int x;
	scanf("%d", &x);
	if (isPrime(x))
	{
		printf("%d是素数\n", x);
	} else
	{
		printf("%d不是素数\n", x);
	}
	
	return 0;
}

int isPrime(int x)
{
	int ret = 1;
	if (x == 1 || (x % 2 ==0 && x != 2))
	{
		ret = 0;
	}
	for (int  i = 3; i < x; i+=2)
	{
		if (x % i == 0)
		{
			ret = 0;
			break;
		}
	}
	
	return ret;
}

还可以用更少的循环次数来实现:

无需到x-1,到sqrt(x)就够了

  • 只需要循环sqrt(x)遍,因为如果一个数不是素数是合数, 那么一定可以由两个自然数相乘得到, 其中一个大于或等于它的平方根,一个小于或等于它的平方根,并且成对出现。

代码如下:

#include "stdio.h"
#include "math.h"

int isPrime(int x);

int main()
{
	int x;
	scanf("%d", &x);
	if (isPrime(x))
	{
		printf("%d是素数\n", x);
	} else
	{
		printf("%d不是素数\n", x);
	}
	
	return 0;
}

int isPrime(int x)
{
	int ret = 1;
	if (x == 1 || (x % 2 ==0 && x != 2))
	{
		ret = 0;
	}
	for (int  i = 3; i <= sqrt(x); i += 2)
	{
		if (x % i == 0)
		{
			ret = 0;
			break;
		}
	}
	
	return ret;
}

继续用更少的循环次数来实现:

判断是否能被已知的且<x的素数整除。我们不需要拿比x小的数来判断x是不是素数,只需要拿比x小的素数来判断x是不是素数就可以了。

代码如下:

#include "stdio.h"

int isPrime(int x, int knowPrimes[], int numberOfKnownPrimes);

int main()
{
	const int number = 10;
	int prime[100] = {2}; // 我的编译器不允许用变量定义数组!
	int count = 1;
	int i = 3;

	{
		printf("\t\t");
		for (int i = 0; i < number; i++)
		{
			printf("%d\t", i);
		}
		printf("\n");
	}

	while (count < number)
	{
		if (isPrime(i, prime, count))
		{
			prime[count++] = i;
		}
		{
			printf("i=%d\tcnt=%d\t", i, count);
			for (int i = 0; i < number; i++)
			{
				printf("%d\t", prime[i]);
			}
			printf("\n");
		}
		i++;
	}
	for (int 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 knowPrimes[], int numberOfKnownPrimes)
{
	int ret = 1;
	
	for (int  i = 0; i <numberOfKnownPrimes; i++)
	{
		if (x % knowPrimes[i] == 0)
		{
			ret = 0;
			break;
		}
	}
	
	return ret;
}

结果如下:
在这里插入图片描述

最后一种方法,构造素数表法:

构造素数表

  • 欲构造n以内的素数表
  1. x2
  2. 2x3x4x直至ax < n的数标记为非素数
  3. x为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕

算法思想:
在这里插入图片描述

伪代码:

  • 欲构造n以内(不含)的素数表
  1. 开辟prime[n],初始化其所有元素为1prime[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 = 25;
	int isPrime[maxNumber];

	for (int i = 0; i < maxNumber; i++)
	{
		isPrime[i] = 1;
	}
	
	for (int x = 2; x < maxNumber; x++)
	{
		if (isPrime[x])
		{
			for (int i = 2; i * x < maxNumber; i++)
			{
				isPrime[i * x] = 0;
			}
		}
	}
	
	for (int i = 0; i < maxNumber; i++)
	{
		if (isPrime[i])
		{
			printf("%d\t", i);
		}
	}
	printf("\n");

	return 0;
}

结果如下:
在这里插入图片描述

6. 二维数组

二维数组

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

二维数组的遍历
for (i = 0; i < 3; i++) {
        for (j = 0; j < 5; j++) {
                a[i][j] = i * j;
        }
}

  • a[i][j]是一个int
  • 表示第i行第j列上的单元
    • a[i, j]是什么?
    • 逗号,是运算符,i,j是一个表达式,该表达式计算的结果是ja[i, j]也就等于a[j]

二维数组的初始化
int a[][5] = {
        {0, 1, 2, 3, 4},
        {2, 3, 4, 5, 6},
};

  • 列数是必须给出的,行数可以由编译器来数
  • 每行一个{},逗号隔开
  • 最后的逗号可以存在,古老的传统
  • 如果省略,表示补零
  • 也可以用定位(* C99 ONLY)

tic-tac-toe游戏

  • 读入一个3×3的矩阵,矩阵中的数字为1表示该位置上有一个×,为0表示为o
  • 程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符×或o,或输出无人获胜
    在这里插入图片描述

代码如下:

#include "stdio.h"

int main()
{
	const int size = 3;
	int board[size][size];
	int numOfX, numOfO;
	int result = -1;  // -1:no win; 1:x win; 0:O win

	// 读入矩阵
	for (int i = 0; i < size; i++)
	{
		for (int j = 0; j < size; j++)
		{
			scanf("%d", &board[i][j]);
		}
	}
	
	// 检查行
	for (int i = 0; i < size && result == -1; i++)
	{
		numOfO = numOfX = 0;
		for (int j = 0; j < size; j++)
		{
			if (board[i][j] == 1)
			{
				numOfX ++;
			} else
			{
				numOfO ++;
			}
		}
		if (numOfX == size)
		{
			result = 1;
		} else if (numOfO == size)
		{
			result = 0;
		}		
	}
	
	// 检查列
	for (int j = 0; j < size && result == -1; j++)
	{
		numOfO = numOfX = 0;
		for (int i = 0; i < size; i++)
		{
			if (board[i][j] == 1)
			{
				numOfX ++;
			} else
			{
				numOfO ++;
			}
		}
		if (numOfX == size)
		{
			result = 1;
		} else if (numOfO == size)
		{
			result = 0;
		}		
	}

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

	numOfO = numOfX = 0;
	for (int i = 0; i < size && result == -1; i++)
	{
		if (board[i][size - i - 1] == 1)
		{
			numOfX ++;
		} else
		{
			numOfO ++;
		}
	}
	if (numOfX == size)
	{
		result = 1;
	} else if (numOfO == size)
	{
		result = 0;
	}
	
	switch (result)
	{
	case 0:
		printf("O win\n");
		break;
	case 1:
		printf("X win\n");
		break;
	default:
		printf("no win\n");
		break;
	}
	return 0;
}

上述代码有大量冗余,有很大优化空间!!!

思考:检查行和列的代码能不能合并呢?

代码如下:

// 检查行列
	for (int i = 0; i < size && result == -1; i++)
	{
		numOfO = numOfX = 0;
		for (int j = 0; j < size; j++)
		{
			if (board[i][j] == 1)
			{
				numOfX ++;
			} else
			{
				numOfO ++;
			}
			if (board[j][i] == 1)
			{
				numOfX ++;
			} else
			{
				numOfO ++;
			}
		}
		if (numOfX == size)
		{
			result = 1;
		} else if (numOfO == size)
		{
			result = 0;
		}		
	}
  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值