一、数组
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