3.1 数组
考虑这样一个问题:读入一些整数,逆序输出到一行中。已知整数不超过100个。如何编写这个程序呢?首先是循环读取输入。读入每个整数以后,应该做些什么呢?思来想去,在所有整数全部读完之前,似乎没有其他事可做。换句话说,只能把每个数都存下来。存放在哪里呢?答案是:数组。
程序3-1 逆序输出
#include<stdio.h>
#define maxn 105
int a[maxn];
int main()
{
int x, n = 0;
while(scanf("%d", &x) == 1)
a[n++] = x;
for(int i = n-1; i >= 1; i--)
printf("%d ", a[i]);
printf("%d\n", a[0]);
return 0;
}
语句“int a[maxn]”声明了一个包含maxn个整型变量的数组,它们是:a[0],a[1],a[2],…,a[maxn-1]。注意,没有a[maxn]。
提示3-1:语句“int a[maxn]”声明了一个包含maxn个整型变量的数组,即a[0],a[1],…,a[maxn-1],但不包含a[maxn]。maxn必须是常数,不能是变量。
为什么这里声明maxn为105而不是100呢?因为这样更保险。
提示3-2:在算法竞赛中,常常难以精确计算出需要的数组大小,数组一般会声明得稍大一些。在空间够用的前提下,浪费一点不会有太大影响。
接下来是语句“a[n++]=x”,它做了两件事:首先赋值a[n]=x,然后执行n=n+1。如果觉得难以理解,可以将其改写成“{a[n]=x;n=n+1;}”。注意这里的花括号是不能省略的,因为在默认情况下,for语句的循环体只有一条语句。只有使用花括号时,花括号里的语句才会整体作为循环体。一般地,当表达式里出现n++时,表达式会使用加1前的n计算表达式,当表达式计算完毕之后再给n加1。
和n++相对应的,还有一个++n,表示先给n增加1,然后使用新的n。前缀和后缀“++”运算符是C语言的特色之一。事实上,后面将要介绍的C++语言名字里的“++”就是该运算符。
提示3-3:对于变量n,n++和++n都会给n加1,但当它们用在一个表达式中时,行为有所差别:n++会使用加1前的值计算表达式,而++n会使用加1后的值计算表达式。“++”运算符是C语言的特色之一。
循环结束后,数据被存储在a[0],a[1],…,a[n-1]中,其中变量n是整数的个数(想一想,为什么)。
存好以后就可以输出了:依次输出a[n-1],a[n-2],…,a[1]和a[0]。这里有一个小问题:一般要求输出的行首行尾均无空格,相邻两个数据间用单个空格隔开。这样,一共要输出n个整数,但只有n-1个空格,所以只好分两条语句输出。
在上述程序中,数组a被声明在main函数的外面。请试着把maxn定义中的100改成1000000,比较一下把数组a放在main函数内外的运行结果是否相同。如果相同,试着把1000000改得再大一些。当实验完成之后,读者应该就能明白为什么要把a的定义放在main函数的外面了。简单地说,只有在放外面时,数组a才可以开得很大;放在main函数内时,数组稍大就会异常退出。其道理将在后面讨论,现在只需要记住规则即可。
提示3-4:比较大的数组应尽量声明在main函数外,否则程序可能无法运行。
C语言的数组并不是“一等公民”,而是“受歧视”的。例如,数组不能够进行赋值操作:在程序3-1中,如果声明的是“int a[maxn],b[maxn]”,是不能赋值b=a的。如果要从数组a复制k个元素到数组b,可以这样做:memcpy(b,a,sizeof(int)*k)。当然,如果数组a和b都是浮点型的,复制时要写成“memcpy(b,a,sizeof(double)*k)”。另外需要注意的是,使用memcpy函数要包含头文件string.h。如果需要把数组a全部复制到数组b中,可以写得简单一些:memcpy(b,a,sizeof(a))。
开灯问题。有n盏灯,编号为1~n。第1个人把所有灯打开,第2个人按下所有编号为2的倍数的开关(这些灯将被关掉),第3个人按下所有编号为3的倍数的开关(其中关掉的灯将被打开,开着的灯将被关闭),依此类推。一共有k个人,问最后有哪些灯开着?输入n和k,输出开着的灯的编号。k≤n≤1000。
样例输入:
7 3
样例输出:
1 5 6 7
【分析】
用a[1],a[2],…,a[n]表示编号为1,2,3,…,n的灯是否开着。模拟这些操作即可。
程序3-2 开灯问题
#include<stdio.h>
#include<string.h>
#define maxn 1010
int a[maxn];
int main()
{
int n, k, first = 1;
memset(a, 0, sizeof(a));
scanf("%d%d", &n, &k);
for(int i = 1; i <= k; i++)
for(int j = 1; j <= n; j++)
if(j % i == 0) a[j] = !a[j];
for(int i = 1; i <= n; i++)
if(a[i]) { if(first) first = 0; else printf(" "); printf("%d", i); }
printf("\n");
return 0;
}
“memset(a,0,sizeof(a))”的作用是把数组a清零,它也在string.h中定义。虽然也能用for循环完成相同的任务,但是用memset又方便又快捷。另一个技巧在输出:为了避免输出多余空格,设置了一个标志变量first,可以表示当前要输出的变量是否为第一个。第一个变量前不应有空格,但其他变量都有。
蛇形填数。在n×n方阵里填入1,2,…,n×n,要求填成蛇形。例如,n=4时方阵为:
| 10 | 11 | 12 | 1 |
| 9 | 16 | 13 | 2 |
| 8 | 15 | 14 | 3 |
| 7 | 6 | 5 | 4 |
上面的方阵中,多余的空格只是为了便于观察规律,不必严格输出。n≤8。
【分析】
类比数学中的矩阵,可以用一个二维数组来储存题目中的方阵。只需声明一个“int a[maxn][maxn]”,就可以获得一个大小为maxn×maxn的方阵。在声明时,二维的大小不必相同,因此也可以声明int a[30][50]这样的数组,第一维下标范围是0,1, 2,…,29,第二维下标范围是0,1,2,…,49。
提示3-5:可以用“int a[maxn][maxm]”生成一个整型的二维数组,其中maxn和maxm不必相等。这个数组共有maxn×maxm个元素,分别为a[0][0], a[0][1],…, a[0][maxm-1], a[1][0],a[1][1],…,a[1][maxm-1],…,a[maxn-1][0],a[maxn-1][1],…, a[maxn-1] [maxm -1]。
从1开始依次填写。设“笔”的坐标为(x,y),则一开始x=0,y=n-1,即第0行,第n-1列(行列的范围是0~n-1,没有第n列)。“笔”的移动轨迹是:下,下,下,左,左,左,上,上,上,右,右,下,下,左,上。总之,先是下,到不能填为止,然后是左,接着是上,最后是右。“不能填”是指再走就出界(例如4→5),或者再走就要走到以前填过的格子(例如12→13)。如果把所有格子初始化为0,就能很方便地加以判断。
程序3-3 蛇形填数
#include<stdio.h>
#include<string.h>
#define maxn 20
int a[maxn][maxn];
int main()
{
int n, x, y, tot = 0;
scanf("%d", &n);
memset(a, 0, sizeof(a));
tot = a[x=0][y=n-1] = 1;
while(tot < n*n)
{
while(x+1<n && !a[x+1][y]) a[++x][y] = ++tot;
while(y-1>=0 && !a[x][y-1]) a[x][--y] = ++tot;
while(x-1>=0 && !a[x-1][y]) a[--x][y] = ++tot;
while(y+1<n && !a[x][y+1]) a[x][++y] = ++tot;
}
for(x = 0; x < n; x++)
{
for(y = 0; y < n; y++) printf("%3d", a[x][y]);
printf("\n");
}
return 0;
}
这段程序充分利用了C语言简洁的优势。首先,赋值x=0和y=n-1后马上要把它们 作为数组a的下标,因此可以合并完成;tot和a[0][n-1]都要赋值1,也可以合并完成。这样,就用一条语句完成了多件事情,而且并没有牺牲程序的可读性——这段代码的含义显而易见。
提示3-6:可以利用C语言简洁的语法,但前提是保持代码的可读性。
那4条while语句有些难懂,不过十分相似,因此只需介绍其中的第一条:不断向下走,并且填数。我们的原则是:先判断,再移动,而不是走一步以后发现越界了再退回来。这样,则需要进行“预判”,即是否越界,以及如果继续往下走会不会到达一个已经填过的格子。越界只需判断x+1<n,因为y的值并没有修改;下一个格子是(x+1,y),因此只需“a[x+1][y] == 0”,简写成“!a[x+1][y]”(其中“!”是“逻辑非”运算符)。
提示3-7:在很多情况下,最好是在做一件事之前检查是不是可以做,而不要做完再后悔。因为“悔棋”往往比较麻烦。
细心的读者也许会发现这里的一个“潜在bug”:如果越界,x+1会等于n,a[x+1][y]将访问非法内存!幸运的是,这样的担心是不必要的。“&&”是短路运算符(还记得我们在哪里提到过吗?)。如果x+1<n为假,将不会计算“!a[x+1][y]”,也就不会越界了。
至于为什么是++tot而不是tot++,留给读者思考。