数组
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
循环,让循环变量i
从0
到<
数组的长度,这样循环体内最大的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;
}
从
2
到x-1
测试是否可以整除
- 对于
n
要循环n-1
遍
- 当
n
很大时就是n
遍
上面的方法循环要执行n次,下面再介绍一种循环次数更少的方法:
去掉偶数后,从
3
到x-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
以内的素数表
- 令
x
为2
- 将
2x
、3x
、4x
直至ax < n
的数标记为非素数- 令
x
为下一个没有被标记为非素数的数,重复2;直到所有的数都已经尝试完毕
算法思想:
伪代码:
- 欲构造
n
以内(不含)的素数表
- 开辟
prime[n]
,初始化其所有元素为1
,prime[x]
为1
表示x
是素数- 令
x = 2
- 如果
x
是素数,则对于(i = 2; x * i < n; i++)
令prime[i * x] = 0
- 令
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
是一个表达式,该表达式计算的结果是j
,a[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;
}
}