第四章 · 数组¶
本页最后更新于 ,内容可能过时
《C语言程序设计(第3版)》第 4 章课后习题,共 16 题。
1 · 数组遍历、查找与逆置¶
用 C 代码描述以下计算要求:
(1)输出一维数组中下标是 4 的倍数的元素。
(2)自左至右在一维数组中找第一个值为 key 的元素的下标。
(3)将一维数组中的元素按与原先存储顺序相反的顺序重新存储。
答案
#include <stdio.h>
/* 三段合起来放进一个函数里,a 是长度为 n 的整型数组 */
void demo(int a[], int n, int key)
{
int i, t;
/* (1)输出下标是 4 的倍数的元素 */
for (i = 0; i < n; i += 4)
printf("%d ", a[i]);
/* (2)自左至右找第一个值为 key 的元素的下标 */
for (i = 0; i < n && a[i] != key; i++)
;
/* 循环退出时:i < n 就是那个下标,i == n 说明数组里没有 key */
/* (3)按相反的顺序重新存储 */
for (i = 0; i < n / 2; i++) {
t = a[i];
a[i] = a[n - 1 - i];
a[n - 1 - i] = t;
}
}
解析
三个要求都是"从头到尾走一遍数组",区别只在每一步做什么:一个按固定步长跳着走,一个边走边比、比中就停,一个两头对着换。
关键点:
- 第(1)条的下标本身就是
4的倍数,循环变量从0起步、每次加4就够,不必写if (i % 4 == 0)再判断一回。 - 第(2)条要的是"第一个",找到就得停。若把
for循环写全、中途不退出,留下的是最后一个匹配元素的下标;另外"数组里没有key"这种情况必须有明确的表示,这里用i == n表示,用break写法时也一样要判。 - 第(3)条只走一半(
i < n / 2)。上界写成n会把刚换过去的元素又换回来,等于没做。交换要借一个临时变量,直接a[i] = a[n-1-i]; a[n-1-i] = a[i];会丢掉一个值。 - 三段代码的共同边界是
n == 0:循环体一次也不执行,不会越界。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):答案块这段代码补上
main跑过一遍,取a[10] = {10,11,…,19}:第(1)条输出10 14 18,第(2)条key = 15得下标5、key = 99得-1,第(3)条把数组变成19 18 17 16 15 14 13 12 11 10;编译 0 警告。易错:
- 第(3)条的循环上界写成
n- 第(2)条没找到时返回 0——下标 0 是合法下标,用它表示"没找到"会和"第一个元素正好是
key"混在一起- 第(2)条的循环写全、中途不退出,得到的是最后一个匹配元素的下标
2 · 统计各英文字母出现的次数¶
输入一行字符,分别统计其中各英文字母出现的次数(不区分大小写)。
答案
#include <stdio.h>
#include <ctype.h>
int main(void)
{
int count[26] = {0};
int c, i;
printf("请输入一行字符: ");
while ((c = getchar()) != '\n' && c != EOF) {
if (isalpha((unsigned char)c))
count[toupper((unsigned char)c) - 'A']++;
}
for (i = 0; i < 26; i++)
if (count[i] != 0)
printf("%c: %d\n", 'A' + i, count[i]);
return 0;
}
解析
思路:开 26 个计数器,把每个字母折算成 0~25 的下标再累加,最后按字母顺序打表。
关键点:
toupper(c) - 'A'一步就把大小写并到同一个格子里,这就是"不区分大小写"的落点。若分成两组计数,最后还得再合并一次。- 逐字符输入用
getchar()。scanf("%s")遇到空白就停,一行里只有第一个单词会被统计到。 isalpha、toupper的实参先转成unsigned char。char在多数平台是带符号的,读到非 ASCII 字节时是负值,直接传给ctype.h的函数是未定义行为——标准要求实参必须是unsigned char能表示的值或EOF。- 输出时跳过计数为
0的字母,不然 26 行里大半是0。 - 循环同时判
'\n'和EOF:真正的"一行"以换行结束,输入流提前结束(比如重定向)时也要能收尾。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
Hello World 123 abc,输出 10 行,依次为A: 1、B: 1、C: 1、D: 1、E: 1、H: 1、L: 3、O: 2、R: 1、W: 1;数字和空格没有进任何一格。
数组下标本身就是一次映射
题目要的是"每个字母各有一个计数器"。把这 26 个计数器排成一个 count[26],字母到计数器由 toupper(c) - 'A' 对应,省掉了 26 个变量和一圈查找。凡是"按取值分类计数"的题都可以这么开数组,下标的转换式就是分类规则本身。
易错:
- 用
count[c]直接当下标('a'是97,数组得开 256 个,而且大小写还会分成两格)- 用
scanf("%s")读输入- 只统计大写或只统计小写
- 忘了把
getchar()返回的EOF排除在外
3 · 圆括号与花括号的配对检查¶
输入 C 程序源程序正文,找出可能存在的圆括号和花括号不匹配的错误。
答案
#include <stdio.h>
#define MAX 256
int main(void)
{
char stack[MAX];
char line[256];
int top = 0, lineno = 0, ok = 1;
printf("请输入 C 程序源程序正文(空行结束):\n");
while (fgets(line, sizeof line, stdin) != NULL) {
int i;
if (line[0] == '\n')
break;
lineno++;
for (i = 0; line[i] != '\0'; i++) {
char c = line[i];
if (c == '(' || c == '{') {
stack[top++] = c;
} else if (c == ')' || c == '}') {
char want = (c == ')') ? '(' : '{';
if (top == 0) {
printf("第 %d 行: 多余的 '%c'\n", lineno, c);
ok = 0;
} else if (stack[--top] != want) {
printf("第 %d 行: '%c' 未能与 '%c' 匹配\n",
lineno, stack[top], c);
ok = 0;
}
}
}
}
while (top > 0) {
printf("未闭合的 '%c'\n", stack[--top]);
ok = 0;
}
if (ok)
printf("圆括号、花括号均匹配\n");
return 0;
}
解析
思路:括号配对天然是栈的题目——左括号先"记下来",遇到右括号就去核对最近一个还没配对的左括号。
关键点:
- 左括号进栈;右括号看栈顶:栈空说明这个右括号多余,栈顶类型不符说明交叉不匹配(比如
(a}),类型相符就弹出栈顶。 - 圆括号和花括号放在同一个栈里。若分成两个计数器,只能查出括号的个数对不对,查不出
(a}这种错配。 - 扫描结束后栈里还有剩,剩下的就是没闭合的左括号,逐个报出来。
- 本题按题面只处理正文里出现的括号,不再区分它是否落在字符串常量或注释内部——那需要另写一套词法判断,不是本章的考点。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):两份输入各跑一次。一份括号全配对的
main,输出圆括号、花括号均匹配;另一份把第 3 行写成if ((a > 0) {(少了一个右括号),输出第 7 行: '(' 未能与 '}' 匹配,接着是未闭合的 '{'——多出来的那个左圆括号把后面的}顶掉了,于是右花括号配不上、左花括号没了归宿,两处错误都被报了出来。易错:
- 只数括号的个数,不检查类型是否配对
- 只检查圆括号或只检查花括号
- 扫完就结束,忘了再看一眼栈里有没有剩下的左括号
4 · 复制数组并去掉重复值¶
编写将已知数组内容复制到另一个新数组,使复制产生的新数组包含已知数组全部出现过的值,而又不重复。
答案
#include <stdio.h>
#define MAX 100
/* 把 src 中 n 个元素复制到 dst,重复出现的值只保留第一次,
返回新数组的元素个数 */
int dedup(int dst[], const int src[], int n)
{
int m = 0, i, j;
for (i = 0; i < n; i++) {
for (j = 0; j < m; j++)
if (dst[j] == src[i])
break;
if (j == m)
dst[m++] = src[i];
}
return m;
}
int main(void)
{
int src[] = {3, 5, 3, 1, 5, 7, 1, 3, 9};
int dst[MAX];
int n = (int)(sizeof src / sizeof src[0]);
int m, i;
m = dedup(dst, src, n);
printf("已知数组: ");
for (i = 0; i < n; i++)
printf("%d ", src[i]);
printf("\n新数组: ");
for (i = 0; i < m; i++)
printf("%d ", dst[i]);
printf("\n新数组共 %d 个元素\n", m);
return 0;
}
解析
思路:新数组一边生成一边查重。每取源数组的一个元素,先在已经生成的那部分里找一遍,没有出现过才追加到末尾。
关键点:
- 内层
for循环结束后,j == m表示"新数组里前 m 个都没有这个值",这正是可以追加的条件;j < m则说明第j个就是重复的。用循环变量的越界值表示"没找到",这个写法在数组题里反复出现。 - 保持元素第一次出现的顺序。只追加没见过的值,得到的次序天然就是首次出现的次序。
- 元素个数由函数返回,不能沿用原来的
n——新数组的长度是m,用n打表会把没写过的位置一起打出来。 - 时间复杂度是
O(n·m)。若元素取值范围很小,可以另开一个标记数组把每个值的"见过没有"记下来,做到O(n);本章还没讲这种以值域换时间的做法,这里就不展开了。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):源数组取
{3,5,3,1,5,7,1,3,9},输出3 5 1 7 9共 5 个元素——两次出现的3、两次出现的5、两次出现的1都只留了第一次。易错:
- 内层循环的下标写混,拿
src[i]去比src[j]- 追加时边界用了
n而不是m- 想先排序再去重——虽然也不留重复值,却把元素的相对顺序改掉了,与"复制"这个要求不符
5 · 最长平台的元素个数¶
设有序数组有 n 个元素,数组中连续相等的元素段称为数组的平台。
试找出数组最长平台的元素个数。规定若数组的元素互不相等,则它的最长平台长为 1。
答案
#include <stdio.h>
/* 最长平台上元素的个数;元素互不相等时返回 1,空数组返回 0 */
int longest_plateau(const int a[], int n)
{
int best = 1, cur = 1, i;
if (n == 0)
return 0;
for (i = 1; i < n; i++) {
if (a[i] == a[i - 1])
cur++;
else
cur = 1;
if (cur > best)
best = cur;
}
return best;
}
#define COUNT(a) ((int)(sizeof (a) / sizeof (a)[0]))
int main(void)
{
int t1[] = {1, 1, 2, 2, 2, 3, 3};
int t2[] = {1, 2, 3, 4, 5};
int t3[] = {7, 7, 7, 7};
int t4[] = {0, 0, 1, 1, 1, 1, 2};
int t5[] = {-3, -3, -3, 0};
int t6[] = {4};
printf("t1 -> %d\n", longest_plateau(t1, COUNT(t1)));
printf("t2 -> %d\n", longest_plateau(t2, COUNT(t2)));
printf("t3 -> %d\n", longest_plateau(t3, COUNT(t3)));
printf("t4 -> %d\n", longest_plateau(t4, COUNT(t4)));
printf("t5 -> %d\n", longest_plateau(t5, COUNT(t5)));
printf("t6 -> %d\n", longest_plateau(t6, COUNT(t6)));
return 0;
}
解析
思路:一次遍历同时维护两个量——cur 是"当前这一段平台有多长",best 是"到目前为止见到的最长平台"。每走一步就试着刷新 best。
关键点:
- 相邻两个元素相等,
cur加 1;不相等,说明上一段平台到此为止,cur从 1 重新数起。 best的初值取 1,正好落在题目"若数组的元素互不相等,则它的最长平台长为 1"这条规定上——一个元素自成一个长度为 1 的平台。只有空数组才需要另外处理,函数开头把它单独挡掉。- 刷新
best的动作每轮都做,不能只在"遇到不相等"时做:要是最长的那一段恰好顶在数组末尾,只在不相等时才更新就会漏掉它。 - 题目说数组是有序的,但"连续相等"这个判据本身不依赖有序性,所以同一份代码对无序数组同样成立。
- 空数组要单独挡掉:长度为 0 的数组没有元素,谈不上平台,直接返回
best的初值 1 就错了。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):六组数据分别跑出
t1 -> 3、t2 -> 1、t3 -> 4、t4 -> 4、t5 -> 3、t6 -> 1。其中t1是{1,1,2,2,2,3,3}(最长平台是三个2),t2是{1,2,3,4,5}(互不相等,按规定得1),t3是{7,7,7,7}(整组就是一个平台),t6只有{4}一个元素。易错:
best的初值写成 0- 只在"遇到不相等"时更新
best,漏掉顶在末尾的那一段- 把题目读成"数一数数组里有多少个不同的值"(那是去重,不是平台)
- 空数组照样返回 1
6 · 多项式求和与手写格式输出¶
输入两个多项式的各项系数和指数,编写程序求出它们的和,并要求与手写习惯相同的格式输出。
规定:一个多项式的输入以输入指数为负数结束。多项式的每一项 axᵇ 用 ax^b 格式输出。
答案
#include <stdio.h>
#define MAX 100
/* 读一个多项式:每项输入"系数 指数",指数为负数时结束 */
static int read_poly(int coef[], int exp[])
{
int n = 0, c, e;
while (scanf("%d%d", &c, &e) == 2 && e >= 0) {
coef[n] = c;
exp[n] = e;
n++;
}
return n;
}
/* 按手写习惯输出:系数 1 和 -1 省略、指数 1 不写 ^1、指数 0 只写系数 */
static void print_poly(const int coef[], const int exp[], int n)
{
int i, printed = 0;
for (i = 0; i < n; i++) {
int c = coef[i];
if (c == 0)
continue;
if (printed)
putchar(c > 0 ? '+' : '-');
else if (c < 0)
putchar('-');
printed = 1;
if (exp[i] == 0)
printf("%d", c > 0 ? c : -c);
else {
if (c != 1 && c != -1)
printf("%d", c > 0 ? c : -c);
putchar('x');
if (exp[i] != 1)
printf("^%d", exp[i]);
}
}
if (!printed)
putchar('0');
putchar('\n');
}
int main(void)
{
int c1[MAX], e1[MAX], c2[MAX], e2[MAX];
int rc[MAX * 2], re[MAX * 2];
int n1, n2, n = 0, i, j;
printf("输入第一个多项式的各项(系数 指数,指数为负数结束):\n");
n1 = read_poly(c1, e1);
printf("输入第二个多项式的各项(系数 指数,指数为负数结束):\n");
n2 = read_poly(c2, e2);
for (i = 0; i < n1; i++) {
rc[n] = c1[i];
re[n] = e1[i];
n++;
}
for (i = 0; i < n2; i++) {
for (j = 0; j < n; j++)
if (re[j] == e2[i])
break;
if (j < n)
rc[j] += c2[i];
else {
rc[n] = c2[i];
re[n] = e2[i];
n++;
}
}
/* 按指数从大到小排列,输出才像手写的样子 */
for (i = 0; i < n - 1; i++)
for (j = i + 1; j < n; j++)
if (re[j] > re[i]) {
int t;
t = re[i]; re[i] = re[j]; re[j] = t;
t = rc[i]; rc[i] = rc[j]; rc[j] = t;
}
printf("和: ");
print_poly(rc, re, n);
return 0;
}
解析
思路:一个多项式用两个平行数组存——coef[i] 是第 i 项的系数,exp[i] 是它的指数。
先把第一个多项式整份抄进结果,再把第二个多项式的每一项并进去:指数已经在结果里出现过的,就把系数加上去;没出现过的,追加成新的一项。最后按指数从大到小排一遍,输出才像手写的样子。
关键点:
- 输入以"指数为负数"结束,所以结束标志也得写成一个完整的"系数 指数"对(例如
0 -1)。只输入一个-1的话,scanf("%d%d")会把它当系数、再往后读一个数当指数,第一个多项式就会多出一项,后面的输入全部错位。 - 判断"是不是同类项"要看指数,不能看系数——指数才是项的标识。
- 合并之后系数可能变成 0(正负相消),这种项跳过不输出。
- 手写格式的几条细节都要单独处理:系数是 1 或 -1 时省略数字只写
x;指数是 1 时不写^1;指数是 0 时只写系数;只有第一项带前导负号,其余各项由+/-连接。 - 两个多项式完全抵消时结果为空,此时输出一个
0,不能输出空行。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):三组数据分别跑出
和: 4x^2+6、和: 2、和: 0。第一组是3x^2+2x+1加x^2-2x+5(一次项相消,得4x^2+6);第二组是x^2+1加-x^2+1(二次项相消,只剩常数2);第三组两项全部相消,输出0。
系数和指数用两个平行数组绑在一起
同一项的两部分靠下标对齐:coef[i] 与 exp[i] 属于第 i 项。本章还没讲结构体,凡是"一个对象有两三个字段"的场合,教材都用这种平行数组来存。
易错:
- 结束标志只输一个负数,把后面一个多项式的首项当作指数读入
- 拿系数去配同类项
- 系数为 0 的项照样打印(会输出
+0x^3这种)- 系数 1 打成
1x^2、指数 1 打成x^1(手写习惯都不这么写)- 结果为空时什么都没输出
7 · 筛选法求质数¶
采用筛选法求质数。算法思想简述如下:
(1)将数组中下标为 0 和 1 的元素设置为 0,下标为 2~N 的元素都设置为 1。
(2)从下标为 2 的元素开始考查,当发现当前位置的数组元素值为 1 时,将下标是当前下标 2 倍、3 倍、……的那些元素全部置 0。
(3)重复步骤(2),直至考查了数组的全部元素,那些值依旧为 1 的元素的下标都是质数。
答案
#include <stdio.h>
#define N 50
int main(void)
{
int a[N + 1];
int i, k, count = 0;
a[0] = a[1] = 0;
for (i = 2; i <= N; i++)
a[i] = 1;
for (i = 2; i <= N; i++)
if (a[i] == 1)
for (k = 2 * i; k <= N; k += i)
a[k] = 0;
printf("N = %d 以内的质数:\n", N);
for (i = 2; i <= N; i++)
if (a[i] == 1) {
printf("%4d", i);
if (++count % 10 == 0)
putchar('\n');
}
if (count % 10 != 0)
putchar('\n');
printf("共 %d 个\n", count);
return 0;
}
解析
思路:完全按题干的三个步骤。数组元素只当标记用——a[i] 记住"下标 i 是不是质数",数组的下标本身就是要判断的那个数。
关键点:
- 初始时把
0和1划掉,2~N全标1(先都当成质数,再逐个划掉合数)。 - 内层从
2 * i起步、步长i,就是把i的2倍、3倍、……全部置0,与题干一致。 - 判定用
a[i] == 1而不是a[i] != 0。若用后者,已经划掉的格子也会进去跑一圈内层循环,白做。 - 数组要开到
N + 1:题目要用到下标N,开a[N]就少一格。 - 打表时
++count % 10 == 0每行放 10 个,收尾时再判断要不要补一个换行,避免多出一个空行。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):取
N = 50,输出两行数字2 3 5 7 11 13 17 19 23 29和31 37 41 43 47,共 15 个,与 50 以内的质数表逐项吻合。易错:
- 内层从
i或i + 1起步,把质数自己划掉了- 数组只开到
a[N],访问a[N]越界- 判定条件写成
a[i] != 0,多跑一半无用的循环- 把
a[0] = a[1] = 0漏掉,于是0和1也被当成质数打出来
8 · 试除法求前 n 个质数¶
求前 n 个质数。要求确定数 m 是否是质数,用 m 被已求得的质数的整除性来确定。
答案
#include <stdio.h>
#define MAX 1000
int main(void)
{
int primes[MAX];
int n, k = 0, m, i;
printf("求前 n 个质数,请输入 n: ");
if (scanf("%d", &n) != 1 || n <= 0 || n > MAX)
return 1;
for (m = 2; k < n; m++) {
int is_prime = 1;
for (i = 0; i < k; i++)
if (m % primes[i] == 0) {
is_prime = 0;
break;
}
if (is_prime)
primes[k++] = m;
}
for (i = 0; i < n; i++) {
printf("%5d", primes[i]);
if ((i + 1) % 10 == 0)
putchar('\n');
}
if (n % 10 != 0)
putchar('\n');
return 0;
}
解析
思路:从小到大逐个考察整数 m,用已经求出来的质数去试除它。一个都除不尽,m 就是新的质数,立刻存进 primes 数组,成为后面所有数的判据。
关键点:
- 外层循环的条件是
k < n——"还没凑够n个就接着找",而不是给m定一个上界。第n个质数有多大是算出来的,事先不知道。 - 试除只用已求得的质数,不用
4、6、8这些合数,也不必试到m-1:题目的这一句要求正好把判据缩到了primes[0..k-1]。 - 只要有一个质数能整除
m,m就是合数,立刻结束内层循环。用is_prime这个标志把"内层是怎么结束的"记下来,比事后判断i停在哪儿更清楚。 - 第一个被考察的数
2一定被收下:此时k == 0,内层循环一次也不执行,is_prime保持为1。 primes的大小是"最多要存多少个质数",与质数本身的大小无关。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
n = 20,输出两行2 3 5 7 11 13 17 19 23 29和31 37 41 43 47 53 59 61 67 71,与质数表逐项吻合。
其实不必试到 m - 1
若 m 是合数,写成 m = a × b 时总有一个因子 a 不超过 m 的平方根:一个都不超过平方根,两个因子就都超过平方根,乘积必然大于 m。
所以内层循环只需要试到 primes[i] * primes[i] > m 就可以提前收手,结果不变。题干只要求"用 m 被已求得的质数的整除性来确定",答案就按题面写;知道这一步可以省就行。
易错:
- 拿
2、3、4、5……所有整数去试除(那就不叫"用已求得的质数"了)- 内层循环写成
i <= k,读到还没赋值的primes[k]- 忘了
2也是质数,从3开始考察- 外层用
for (m = 2; m <= 某个数; m++),把范围写死,n一大就不够用
9 · 按给定公式输出年历¶
编写输入年份,输出该年年历的程序。
提示:已知日期,按以下公式计算该日期的 N 值:
其中函数 f() 和 g() 的计算公式为:
然后计算:
d 值为 0~6 中的某个整数,代表一周中星期日~星期六的某一天。
答案
#include <stdio.h>
/* 题给公式:0~6 分别表示星期日~星期六 */
static int weekday(int year, int month, int day)
{
int y = (month < 3) ? year - 1 : year;
int m = (month < 3) ? month + 13 : month + 1;
long n = 1461L * y / 4 + 153L * m / 5 + day;
return (int)((n - 621049L) % 7);
}
static int leap(int year)
{
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
static void print_month(int year, int month)
{
static const char *wname[7] = {"日", "一", "二", "三", "四", "五", "六"};
static const int mdays[12] = {31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
int days = mdays[month - 1];
int start = weekday(year, month, 1);
int d, w;
if (month == 2 && leap(year))
days = 29;
printf(" %d 年 %d 月\n", year, month);
for (w = 0; w < 7; w++)
printf("%s ", wname[w]);
putchar('\n');
for (w = 0; w < start; w++)
printf(" ");
for (d = 1; d <= days; d++) {
printf("%2d ", d);
if (++w % 7 == 0)
putchar('\n');
}
if (w % 7 != 0)
putchar('\n');
putchar('\n');
}
int main(void)
{
int year, month;
printf("请输入年份: ");
if (scanf("%d", &year) != 1)
return 1;
for (month = 1; month <= 12; month++)
print_month(year, month);
return 0;
}
解析
思路:把题给的公式原样写成一个函数,用它算出"该月 1 日是星期几",这个值决定该月的排版从哪一列起头;再按平年/闰年的天数把日期一格一格填进去。
关键点:
- 公式里
1461 * f / 4和153 * g / 5都是先乘后除、走整数除法,运算顺序不能动。f、g按"月份是否小于3"分别取值——一月、二月被归到上一年的第13、14月,这正是公式能统一处理闰年的地方。 d的值0~6依次对应星期日到星期六,所以表头从左到右就是"日 一 二 三 四 五 六"。- 排版分三步:打表头;按
d的个数先打空位,把1号推到正确的列;从1号起每打7天换一行。 - 闰年按公历规则判断:能被
4整除且不能被100整除,或者能被400整除。2000 年是闰年,1900 年不是。 - 表头一格是"一个汉字加一个空格"(占
3个字符位),日期用%2d也是3个字符位,两者才对得齐。用%4s去打汉字对不齐:printf按字节数补空格,不按显示宽度算。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
2024,1 月 1 日落在"一"列、1 月 31 天,2 月 1 日落在"四"列、2 月 29 天;换成2000和2023各跑一次,2 月分别是 29 天和 28 天,各月 1 日的星期也与真实日历一致。
题给公式只在 1901~2099 年对得上真实日历
公式里的 1461 * f / 4 相当于"每 4 年一闰",没有公历"100 年不闰、400 年再闰"那一层修正。逐月对照真实公历的结果是:1901~2099 年这 199 年完全吻合;1900 年 1、2 月差 1 天,2100 年 3 月以后差 1 天,1800 年差 2 天,越往前的世纪差得越多;到 1700 年 N - 621049 已经为负,C 的 % 按截断取余会得到负数,算出来的 d 直接不可用。
答案完全按题干给的公式实现,这个适用区间一并说明。
易错:
- 把
1461 * f(年,月) / 4写成1461 * (f(年,月) / 4)(取f = 2023时前者是738900,后者只剩737805)- 闰年只判"能被
4整除",把 1900 年当成闰年- 表头用
%4s对齐汉字- 打完最后一天不检查是不是正好到行尾,多补一个空行
10 · 求下一个更大的排列¶
设数组的每个元素只存储 0~9 的数,把该数组的前 n 个整数的排列看做是一个 n 位的长整数的一种表示。
现要求编写程序,对数组中的元素做调整,产生一个新的排列,使新排列表示的长整数比调整前的长整数大(如果可能),但又是所有更大的表示中最小的。
例如,a[]={3,2,6,5,4,1},则更大又是最小的排列为 {3,4,1,2,5,6}。
答案
#include <stdio.h>
static void reverse(int a[], int lo, int hi)
{
while (lo < hi) {
int t = a[lo];
a[lo++] = a[hi];
a[hi--] = t;
}
}
/* 把数组前 n 位看成一个长整数,改成"比它大又是所有更大者中最小的"排列。
已经是最大排列时返回 0,数组不动。 */
int next_bigger(int a[], int n)
{
int i = n - 2, j, t;
while (i >= 0 && a[i] >= a[i + 1])
i--;
if (i < 0)
return 0;
j = n - 1;
while (a[j] <= a[i])
j--;
t = a[i];
a[i] = a[j];
a[j] = t;
reverse(a, i + 1, n - 1);
return 1;
}
static void show(const char *tag, const int a[], int n)
{
int i;
printf("%s = {", tag);
for (i = 0; i < n; i++)
printf("%d%s", a[i], i < n - 1 ? "," : "");
printf("}\n");
}
int main(void)
{
int a[] = {3, 2, 6, 5, 4, 1};
int b[] = {1, 2, 3, 4, 5, 6};
int c[] = {9, 8, 7};
int n = (int)(sizeof a / sizeof a[0]);
int m = (int)(sizeof b / sizeof b[0]);
int k = (int)(sizeof c / sizeof c[0]);
show("a", a, n);
printf("--> %d\n", next_bigger(a, n));
show("a", a, n);
show("b", b, m);
printf("--> %d\n", next_bigger(b, m));
show("b", b, m);
show("b", b, m);
printf("--> %d\n", next_bigger(b, m));
show("b", b, m);
show("c", c, k);
printf("--> %d\n", next_bigger(c, k));
show("c", c, k);
return 0;
}
解析
思路:从右往左找"还能往大变"的那一位,把它换成右边比它大的最小的数,再把这一位后面那段排成升序——得到的就正好是比原数大又是最小的排列。
关键点:
- 从右往左找第一个满足
a[i] < a[i+1]的位置i。找不到这样的位置,说明整个数组从左到右是递减的,它已经是最大的排列,直接返回0。注意判据是"小于":要找的是还能往大写的那一位。 - 在
i右边从右往左找第一个大于a[i]的a[j],交换两者。因为i右边那段本身是从右往左递增的,从右往左碰到的第一个大于a[i]的数,正好就是"比a[i]大的里面最小的那个"。 - 交换之后
i右边那段仍然是递减的。要让它表示的数最小,就得把它反过来排成升序,所以反转a[i+1]到a[n-1]。 - 反转不需要额外数组,两头对换即可。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):
{3,2,6,5,4,1}得到{3,4,1,2,5,6},与题干给的例子一致;{1,2,3,4,5,6}连做两次,依次得到{1,2,3,4,6,5}和{1,2,3,5,4,6};{9,8,7}返回0,数组保持原样。
这其实就是字典序里的下一个排列
把数组前 n 位当成一个长整数,与把它当成一串数字,要找的是同一个东西:字典序里的下一个排列。题目限定每个元素只能是 0~9 的一位数字,正是为了让它能拼成一个"长整数"——每个位置上都不进位、不溢出,交换和反转都不会把某一位弄出界。
易错:
- 第 1 步的判据方向写反,写成
a[i] > a[i+1],找到的是"还能变小"的那一位- 第 2 步找的是最右边第一个大于
a[i]的数,不是最左边那个- 第 3 步忘了反转后半段,只交换一次的话得到的不一定是最小的那个更大者
- 反转范围写成
i到n-1,把a[i]自己也卷进去
11 · 1/n 的循环小数表示¶
对于 n=2,3,4,...,50,输出 1/n 的十进制表示的字符串。要求每当十进制小数的第一个循环周期输出后,就结束该数的输出。
以下是程序部分输出结果的样板:
答案
#include <stdio.h>
#define MAXN 50
int main(void)
{
int n;
for (n = 2; n <= MAXN; n++) {
int seen[MAXN] = {0};
int digit[MAXN * 2];
int len = 0, r = 1 % n, i;
printf("1/%d=0.", n);
seen[r] = 1;
for (;;) {
r *= 10;
digit[len++] = r / n;
r %= n;
if (seen[r])
break;
seen[r] = 1;
}
for (i = 0; i < len; i++)
putchar('0' + digit[i]);
putchar('\n');
}
return 0;
}
解析
思路:一个分数除不除得尽、循环节有多长,全看余数。按长除法一位一位往下算(余数乘 10 再除以 n,商是这一位的数字),同时把出现过的余数记下来;哪一步算出的余数以前见过,就说明小数从这里开始循环,第一个循环节已经输出完了。
关键点:
- 全程只用整数。
1/49的循环节有 42 位,用double根本存不下,只能靠长除法一位一位算出来。 seen[r]记录"余数r出现过"。初始余数1 % n也要先记上:1/3的第一位算出3、余数又回到1,正因为1已经在seen里,循环才在这里停下,得到1/3=0.3而不是0.33。- 余数
0同样是一个要被记录的状态。1/2先算出数字5、余数变成0,此时0还没出现过,于是再算一位;这一位算出数字0、余数仍是0,这才发现循环。所以输出是1/2=0.50——样张里的1/4=0.250、1/5=0.20也是同一个道理:除得尽的小数,看成循环节为0的循环小数,第一个循环节连同前面的数字一起打出来。 - 循环节的长度不会超过
n-1(余数只有n种取值),所以数字数组开到2n绰绰有余。 - 数字用
'0' + digit[i]逐位输出,不走浮点格式串,省掉精度和小数位数的纠缠。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):
n从 2 到 50 共 49 行,前 6 行与题干样张逐行一字不差(按判题口径normalize()剥掉行尾空白后比对)。另外几条:1/8=0.1250、1/13=0.076923、1/49=0.020408163265306122448979591836734693877551(42 位循环节)。易错:
- 用
1.0 / n直接打印(浮点表示不出1/49这种循环小数)- 忘了把初始余数记进
seen,1/3会打成0.33- 余数 0 不进
seen,除得尽的小数就少了样张里的那个尾数 0- 每位都去试"余数是否等于某个固定值",而不是记录全部出现过的余数
12 · 不借工作数组的两段互换¶
编写将数组的前 n 个元素中,前端的 m 个元素和随后的 n-m 个元素互换的程序。要求程序不另用其他工作数组,如 a[]={1,2,3,4,5,6,7,8,9,0},设 n=10,m=3。
交换后有:
答案
#include <stdio.h>
#define N 10
static void reverse(int a[], int lo, int hi)
{
while (lo < hi) {
int t = a[lo];
a[lo++] = a[hi];
a[hi--] = t;
}
}
/* 把前 m 个元素与随后的 n-m 个元素互换,不用工作数组 */
void swap_blocks(int a[], int n, int m)
{
reverse(a, 0, n - 1);
reverse(a, 0, n - m - 1);
reverse(a, n - m, n - 1);
}
int main(void)
{
int a[N] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
int i;
printf("交换前: ");
for (i = 0; i < N; i++)
printf("%d%s", a[i], i < N - 1 ? "," : "");
putchar('\n');
swap_blocks(a, N, 3);
printf("交换后: ");
for (i = 0; i < N; i++)
printf("%d%s", a[i], i < N - 1 ? "," : "");
putchar('\n');
return 0;
}
解析
思路:三次逆置。把原数组看成"前段 A 接后段 B"两块,先把整体反过来(得到 B 的倒序接 A 的倒序),再把前 n-m 个反转回来、后 m 个反转回来,两段就换了个位置。
关键点:
- 记作
AB,整体反转得B' A'(撇号表示这一段内部的次序倒过来了);把前n-m个反转回来得B A';再把后m个反转回来得BA,正是"前后两段互换"。 - 三次逆置的下标区间依次是
0 ~ n-1、0 ~ n-m-1、n-m ~ n-1。后两段的长度分别对应交换之后的后段和前段。 reverse里只借一个临时变量做交换,没有第二个数组,符合题目的要求。m取0或n也不会出事:三次调用里有两次退化成空区间或整段,合起来仍是原样。m大于n时才需要另行判断,题目没有这种输入。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):
{1,2,3,4,5,6,7,8,9,0}取n = 10、m = 3,交换后输出4,5,6,7,8,9,0,1,2,3,与题干给的样板一致。
另一种不用工作数组的写法:整体平移
把前 m 个元素看成一叠要搬到末尾的东西:每次把最前面的元素取出来存在一个变量里,其余元素整体左移一格,再把这个变量放到末尾,重复 m 次。同样不借工作数组,但一共要搬动 m × (n-1) 个元素;三次逆置合计只做 n 次左右的交换。
两种写法都满足题目的要求,逆置法更常用。
易错:
- 三次逆置的顺序写反(先分段反转、再整体反转,得到的是另一组排列)
- 后两次的下标区间照抄原来的分段(
0 ~ m-1和m ~ n-1),那两段已经不是原来的位置了- 交换时不借临时变量,写成
a[i] = a[j]; a[j] = a[i];,丢掉了一个值
13 · 方阵原地旋转 90°¶
不用工作数组,分别编写实现将已知方阵旋转 90° 的函数。
提示:旋转可从外到内分层,逐层完成旋转。
答案
#include <stdio.h>
/* 顺时针旋转 90°:从外到内逐层做,每层只借一个临时变量 */
void rotate90(int n, int a[n][n])
{
int layer;
for (layer = 0; layer < n / 2; layer++) {
int first = layer, last = n - 1 - layer;
int i;
for (i = first; i < last; i++) {
int offset = i - first;
int t = a[first][i];
a[first][i] = a[last - offset][first];
a[last - offset][first] = a[last][last - offset];
a[last][last - offset] = a[i][last];
a[i][last] = t;
}
}
}
/* 逆时针旋转 90°:四步赋值反过来绕一圈 */
void rotate270(int n, int a[n][n])
{
int layer;
for (layer = 0; layer < n / 2; layer++) {
int first = layer, last = n - 1 - layer;
int i;
for (i = first; i < last; i++) {
int offset = i - first;
int t = a[first][i];
a[first][i] = a[i][last];
a[i][last] = a[last][last - offset];
a[last][last - offset] = a[last - offset][first];
a[last - offset][first] = t;
}
}
}
static void show(int n, int a[n][n])
{
int i, j;
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++)
printf("%3d", a[i][j]);
putchar('\n');
}
}
int main(void)
{
int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int b[4][4] = {{1, 2, 3, 4}, {5, 6, 7, 8},
{9, 10, 11, 12}, {13, 14, 15, 16}};
int c[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printf("3 阶方阵旋转前:\n");
show(3, a);
rotate90(3, a);
printf("3 阶方阵顺时针旋转后:\n");
show(3, a);
printf("4 阶方阵旋转前:\n");
show(4, b);
rotate90(4, b);
printf("4 阶方阵顺时针旋转后:\n");
show(4, b);
printf("3 阶方阵逆时针旋转后:\n");
rotate270(3, c);
show(3, c);
return 0;
}
解析
思路:按提示从外到内分层。
每一层是一个"方框",旋转就是把方框上边那一段搬到右边、右边那一段搬到下边、下边那一段搬到左边、左边那一段搬回上边。四段正好绕一圈,用一次赋值完成,只需要一个临时变量。
关键点:
- 第
layer层的上边是第first行、下边是第last行,左列是第first列、右列是第last列,其中last = n - 1 - layer。一圈走完,layer加1往里收一层。 - 层内用
offset = i - first记"这是这一层里的第几个格子",四条边上的位置靠它对齐。顺时针方向上,代码里的四步依次是"左列搬上边、下边搬左列、右列搬下边、上边搬右列",绕的方向一致。 - 内层循环的上界是
last而不是last + 1。一层的每条边只有last - first个格子(比边长少1),四个角归相邻的那条边管;写成i <= last就会把角上的格子搬两遍。 - 全程原地交换,没有第二个数组。把四步赋值的顺序反过来(先把上边存进临时变量、最后写回左边那一列)就是逆时针旋转,两个方向各写一个函数即"分别"。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):3 阶方阵
{1,2,3 / 4,5,6 / 7,8,9}顺时针旋转后是{7,4,1 / 8,5,2 / 9,6,3},逆时针旋转后是{3,6,9 / 2,5,8 / 1,4,7};4 阶方阵1~16顺时针旋转后第一行是13 9 5 1、最后一行是16 12 8 4。旋转前后元素1~N²各出现一次,没有丢也没有重。易错:
- 内层循环写成
i <= last,把四个角上的元素搬了两次last忘了减去layer(写成n - 1,每一层都在操作最外面那一圈)- 四步赋值的顺序乱掉,中间某一格被覆盖两次,另一格没被写到
- 借了一个和方阵同样大的数组当中转——那就不是"不用工作数组"了
14 · 反对角线与螺旋两种方阵¶
分别生成如下示例所示,由自然数 1~N² 组成的 N 阶方阵,并输出。
答案
#include <stdio.h>
#define N 4
/* (1)沿反对角线来回填:s 为奇数时自左下向右上,s 为偶数时自右上向左下 */
static void zigzag(int a[N][N])
{
int k = 1, s;
for (s = 0; s <= 2 * (N - 1); s++) {
int lo = (s - (N - 1) > 0) ? s - (N - 1) : 0;
int hi = (s < N - 1) ? s : N - 1;
int i;
if (s % 2 == 1) {
for (i = hi; i >= lo; i--)
a[i][s - i] = k++;
} else {
for (i = lo; i <= hi; i++)
a[i][s - i] = k++;
}
}
}
/* (2)由外到内的顺时针螺旋 */
static void spiral(int a[N][N])
{
int top = 0, bottom = N - 1, left = 0, right = N - 1;
int k = 1, i, j;
while (top <= bottom && left <= right) {
for (j = left; j <= right; j++)
a[top][j] = k++;
top++;
for (i = top; i <= bottom; i++)
a[i][right] = k++;
right--;
if (top <= bottom) {
for (j = right; j >= left; j--)
a[bottom][j] = k++;
bottom--;
}
if (left <= right) {
for (i = bottom; i >= top; i--)
a[i][left] = k++;
left++;
}
}
}
int main(void)
{
int a[N][N], b[N][N];
int i, j;
zigzag(a);
spiral(b);
for (i = 0; i < N; i++) {
for (j = 0; j < N; j++)
printf("%4d", a[i][j]);
printf(" ");
for (j = 0; j < N; j++)
printf("%4d", b[i][j]);
putchar('\n');
}
return 0;
}
解析
两个小题是两种不同的填数路线,要解决的问题一样:把 1 到 N² 依次放进正确的格子,每个数用一次。
第(1)个方阵沿反对角线来回填。下标之和 i + j 相等的格子排在一条反对角线上:第一条只有左上角一个格子,第二条两个,逐渐加到 N 个,之后再逐条减少。
填的方向一条一变——奇数条自左下向右上(1 在左上角,2 在它下面、3 在它右面,正是第二条从下往上),偶数条自右上向左下(4、5、6 是第三条从上往下)。样张里每一段的数字都是连号的,就是这个来回走的顺序。
第(2)个方阵是由外到内的顺时针螺旋。用四个边界 top、bottom、left、right 圈住还没填的内部区域:先填上边一行从左到右,top 下移一格;再填右边一列从上到下,right 左移一格;再填下边一行从右到左,bottom 上移一格;再填左边一列从下到上,left 右移一格。
四条边走完算一圈,重复到边界交错为止。
关键点:
- 第(1)个方阵里,第
s条反对角线上的每个格子都满足j = s - i,所以内层循环只管走i、j由s减出来。i的取值要夹在max(0, s-(N-1))和min(s, N-1)之间,越出这个范围算出来的j会跑到数组外面。 - 方向由
s的奇偶决定,一条一变,这就是"来回填"的落点。 - 第(2)个方阵里,走完一圈四条边界各缩一格。单数阶方阵最后会剩中间一格,所以填下边和左边之前要再判一次边界,不然最里面那一圈会重复填或者越格。
- 两个方阵的元素个数都是
N²,填完后1~N²各出现一次,可以用这个来做自检。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):取
N = 4跑一次,两个方阵的每一格与题干转写里的数字逐格一致——左矩阵逐行是1 3 4 10、2 5 9 11、6 8 12 15、7 13 14 16,右矩阵逐行是1 2 3 4、12 13 14 5、11 16 15 6、10 9 8 7。
答案块里的列宽和题干转写对不齐
程序用 %4d 打每一列,每个数字占 4 个字符位、右对齐,两个方阵之间空 8 个字符位:左矩阵的右边缘固定在第 16 个字符位,右矩阵从第 25 个字符位起。
题干转写里左矩阵的右边缘在第 13 个字符位(第 1 行的 10)和第 14 个字符位(第 4 行的 16)之间参差着,右矩阵从第 23 个字符位起——原书这两列数字是均匀右对齐的,转写时压过列宽才留下这点不齐。
两边的数字完全一样,只是列的宽度与整体缩进对不上。题目示例下面那行 (1)、(2) 是给两个方阵标的号,不是程序的输出,答案块里没有打。
易错:
- 第(1)个方阵里
i的取值范围不夹住,算出的j越界- 方向不随条数交替,两个方向都朝同一头填
- 第(2)个方阵里走完一圈少缩了某条边界,下一圈会重复填上一圈的格子
- 两个方阵都用"从 1 开始每次加 1"填,忘了编号是沿着某条路线走的,而不是逐行逐列走的
15 · 字符序列与字符串¶
字符序列与字符串有何区别?表示字符串有哪些实用的方法?
答案
区别:
- 字符序列是一串按次序排在一起的字符,规定了"有哪几个字符、谁在谁前面",但没有结尾标志,长度必须另外记。
- 字符串是末尾带一个空字符
'\0'(码值 0)的字符序列。'\0'只用来标记字符串到此结束,本身不算字符串的内容。 - C 语言没有单独的字符串类型,字符串靠字符数组或字符指针表示。所以字符串一定是字符序列,字符序列不一定是字符串。
表示字符串的实用方法:
- 字符数组存一份带
'\0'的副本:char s[6] = "hello";。内容可以改写,长度是字符个数加 1。 - 字符指针指向字符串字面量:
char *p = "hello";。不用自己数字符,但字面量的内容不能改写,改它是未定义行为。 - 字符指针数组表示一组字符串:
char *week[] = {"日", "一", "二", "三", "四", "五", "六"};。各条串长短不一也不浪费空间,按字典序排序时只交换指针,不搬字符。 - 用带长度字段的结构表示:
struct { char s[100]; int len; };。取长度不花时间,但 C 的库函数都不认这种表示,得自己写一套。 - 长度要到运行时才知道时动态分配:
char *p = malloc(n + 1);,末尾照样得留出'\0'的位置。
解析
区别的根子在"要不要结尾标志"这一条上。字符串处理函数(strlen、strcpy、printf("%s"))都没有"长度"这个参数,它们全部靠 '\0' 判断什么时候停下来;字符序列少了这个标记,函数就不知道边界在哪里。
边界情况:
char s[5] = "hello";能通过编译,但数组里没有位置放'\0',s就不是字符串。strlen(s)会一直往后读,结果取决于数组后面那片内存里碰巧是什么——这是未定义行为,不是"长度正好是 5"。sizeof和strlen不是一回事:sizeof s是数组占的字节数(含'\0'),strlen(s)是'\0'之前的字符个数;对指针用sizeof p得到的是指针自身的大小,与它指向的串有多长无关。- 字符串字面量
"hello"本身是一个末尾自带'\0'的char[6]数组。 char seq[5] = {'h','e','l','l','o'};是字符序列但不是字符串,只能按已知长度打印,比如printf("%.5s", seq);。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):
char s[6] = "hello";得sizeof s = 6、strlen(s) = 5;char *p = "hello";得sizeof p = 8(本机指针 8 字节)、strlen(p) = 5;char seq[5] = {'h','e','l','l','o'};得sizeof seq = 5,用%.5s限长打印出hello。易错:
- 以为
sizeof就是串长(它是数组占的字节数,还要算上'\0')char s[5] = "hello";照样当字符串去strlen- 改了字面量的内容(
char *p = "hello"; p[0] = 'H';是未定义行为,有些平台会直接崩溃)- 用
char指针数组存字符串时忘了它是指针,对week用sizeof得到的是整个指针数组的大小
16 · 整理字符串中的空白符¶
整理字符串,将字符串的前导空白符和后随空白符删除,并将字符串中非空白字符串之间的连续的多个空白符只保留一个,而去掉多余的空白符。
答案
#include <stdio.h>
#include <ctype.h>
/* 去掉前导、后随的空白符,中间的连续空白只留一个 */
void squeeze(char s[])
{
int i = 0, j = 0;
while (isspace((unsigned char)s[i]))
i++;
while (s[i] != '\0') {
if (isspace((unsigned char)s[i])) {
s[j++] = ' ';
while (isspace((unsigned char)s[i]))
i++;
} else {
s[j++] = s[i++];
}
}
if (j > 0 && s[j - 1] == ' ')
j--;
s[j] = '\0';
}
int main(void)
{
char t1[] = " hello \t world \n";
char t2[] = " \t\n ";
char t3[] = "abc";
char t4[] = " a ";
squeeze(t1);
printf("[%s]\n", t1);
squeeze(t2);
printf("[%s]\n", t2);
squeeze(t3);
printf("[%s]\n", t3);
squeeze(t4);
printf("[%s]\n", t4);
return 0;
}
解析
思路:用两个下标在同一个字符数组上走,i 负责读、j 负责写,边读边决定这个字符要不要留下、留在哪儿。
关键点:
- 先跳前导空白:只让读下标
i往前走,写下标j停在0。这一步单独做,"前导"和"中间的连续空白"才好分别处理。 - 中间部分:读到非空白字符就原样搬到
s[j];读到空白就只写一个空格,然后把跟着的一整串空白全部跳掉。不管中间夹了几个空白,写出去永远只有一个空格。 - 后随空白要最后处理:如果写下的最后一个字符是空格,就把写下标退一格,再补
'\0'。扫描过程中没法判断某个空白是不是末尾的——那时还不知道后面有没有非空白字符。 - 空白判定用
isspace。它认空格、制表符、换行、回车、换页、垂直制表符六种;只写' '的话,制表符和换行就漏掉了。 - 全程原地改写,字符串只会变短,写成的位置永远不晚于读到的位置,不会覆盖还没读到的字符。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):四组输入各跑一次。
" hello \t world \n"整理后是hello world(前导两个空格、中间一个制表符加两个空格、末尾两个空格加换行全部处理掉);全是空白符的" \t\n "整理后是空串;本来就干净的"abc"原样不变;" a "得到a。易错:
- 只删前导和后随,忘了把中间连续的空格压成一个
- 空白判定只写
' ',制表符和换行漏掉- 结尾那个空格忘了退回去,字符串变成
hello world(末尾多一个空格)- 另开一个数组接结果,把整理过的内容放到别处去(题目要的是就地把这个字符串整理好)
习题自测登记:完成本章习题后在此登记