算法竞赛入门经典授课教案第3章数组和字符串精心排版并扩充部分内容.docx
《算法竞赛入门经典授课教案第3章数组和字符串精心排版并扩充部分内容.docx》由会员分享,可在线阅读,更多相关《算法竞赛入门经典授课教案第3章数组和字符串精心排版并扩充部分内容.docx(53页珍藏版)》请在冰豆网上搜索。
算法竞赛入门经典授课教案第3章数组和字符串精心排版并扩充部分内容
第3章数组和字符串
【教学内容相关章节】
3.1数组3.2字符数组3.3最长回文子串
3.4小结与习题
【教学目标】
(1)掌握一维数组的声明和使用方法;
(2)掌握二维数组的声明和使用方法;
(3)掌握字符串的声明、赋值、比较和连接方法;
(4)熟悉字符的ASCII码和ctype.h;
(5)正确认识++、+=等能修改变量的运算符;
(6)学会编译选项-Wall获得更多的警告信息;
(7)掌握fgetc和getchar的使用方法;
(8)了解不同操作系统中换行符的表示方法;
(9)掌握fgets的使用方法并了解gets的“缓冲区溢出”的漏洞;
(10)理解预处理和迭代开发的技巧。
【教学要求】
(1)掌握一维数组和二维数组的声明和使用方法;
(2)掌握字符串的声明、相关的操作;
(3)掌握fgetc和getchar的使用方法;
(3)掌握fgets和gets的使用方法。
【教学内容提要】
通过对第2章的学习,了解了计算机的计算优势,但没有发挥出计算机的存储优势——只用了屈指可数的变量。
尽管有的程序也处理了大量的数据,但这些数据都只是“过客”,只参与了计算、并没有被保存下来。
本章介绍数组和字符串,二者都能保存大量的数据。
字符串是一种数组(字符数组),但由于其应用的特殊性,适用一些特别的处理方式。
【教学重点、难点】
教学重点:
(1)掌握一维数组和二维数组的声明和使用方法;
(2)掌握字符串的声明、相关的操作;
(3)掌握fgetc和getchar的使用方法;
(3)掌握fgets和gets的使用方法。
教学难点:
(1)掌握一维数组和二维数组的声明和使用方法;
(2)掌握字符串的声明、相关的操作及字符串处理函数的使用。
【课时安排(共5学时)】
3.1数组3.2字符数组3.3最长回文子串
3.4小结与习题
3.1数组
下面从一个问题出发,说明一下为何使用数组。
问题:
读入一些整数,逆序输出到一行中,已知整数不超过100个。
【分析】
首先通过循环来读取100个整数的输入,然后把每个数都存下来,存放在数组中,最后输出。
程序3-1逆序输出
#include
#defineMAXN100+10
inta[MAXN];
intmain(){
inti,x,n=0;
while(scanf("%d",&x)==1)
a[n++]=x;
for(i=n-1;i>=1;i--)
printf("%d",a[i]);
printf("%d\n",a[0]);
return0;
}
说明:
语句inta[100]声明了一个包含100个整型变量的数组,它们是:
a[0],a[1],a[2],…,a[99]。
注意,没有a[100]。
提示3-1:
语句inta[MAXN]声明了一个包含MAXN个整型变量的数组,即a[0],a[1],a[2],…,a[MAXN-1],但不包含a[MAXN]。
MAXN必须是常数,不能是变量。
在本程序中,声明MAXN为100+10而不是100,主要是为了保险起见。
提示3-2:
在算法竞赛中,常常难以精确计算出需要的数组大小,数组一般会声明得稍大些。
在空间够用的前提下,浪费一点不要紧。
语句a[n++]=x;的等价于两条语句a[n]=x;n++;,循环结束后,数据被存在了a[0],a[1],…,a[n-1],其中变量n个整数的个数。
在数组中存放整数后,依次输出a[n-1],a[n],…,a[1]和a[0]。
一般要求输出的行首行尾均无空格,相邻两个数据间用单个空格隔开,一共需要输出n个整数,但只有n-1个空格,所以只好分两条语句输出。
也可以采用如下程序:
for(i=n-1;i>=0;i--)
{
if(n!
=0)
printf("%d",a[i]);
else
printf("%d\n",a[0]);
}
在上述程序中,数组a被声明在main函数的外面。
简单地说,只有在放外面时,数组a才可以开得很大;放在main函数内时,数组稍大就会异常退出。
分别运行一下以下两个程序,观察运行结果:
#include
#defineMAXN10000000
inta[MAXN];
intmain()
{
return0;
}
#include
#defineMAXN10000000
intmain()
{
inta[MAXN];
return0;
}
提示3-3:
比较大的数组应尽量声明在main函数外。
C语言数组中使用过程中,有一些特殊的情况。
例如,数组不能够进行赋值操作,即不能将一个整体赋值给另外一个数组。
补充:
memcpy函数原型如下:
void*memcpy(void*dest,void*src,unsignedintcount);
功能:
从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
,使用memcpy函数要包含头文件string.h。
示例1:
从数组a复制到k个元素到数组b:
memcpy(b,a,sizeof(int)*k);
示例2:
若数组a和b都是浮点型的,复制时要写成:
memcpy(b,a,sizeof(double)*k);
示例3:
如果需要把数组a全部复制到数组b中,可以写成:
memcpy(b,a,sizeof(a));
例3-1开灯问题。
有n盏灯,编号为1~n,第1个人把所有灯打开,第2个人按下所有编号为2的倍数的开关(这些灯将被关掉),第3个人按下所有编号为3的倍数的开关(其中关掉的灯被打开,开着灯将被关闭),依此类推。
一共有k个人,问最后有哪些灯开着?
输入:
n和k,输出开着的灯编号。
k≤n≤1000。
样例输入:
73
样例输出:
1567
【分析】
用a[1],a[2],…,a[n]表示编号为1,2,3,…,n的灯是否开着,模拟这些操作即可。
代码如下:
程序3-2开灯问题逆序输出
#include
#include
#defineMAXN1000+10
inta[MAXN];/*一维数组存储n盏电灯*/
intmain()
{
inti,j,n,k,first=1;
memset(a,0,sizeof(a));/*初始化电灯状态*/
scanf("%d%d",&n,&k);
/*模拟活动过程*/
for(i=1;i<=k;i++)/*第i个人参与活动*/
{
for(j=1;j<=n;j++)/*对第i盏灯进行操作*/
if(j%i==0)a[j]=!
a[j];
}
/*输出活动结果*/
for(i=1;i<=n;i++)
{
if(a[i])
{
if(first)first=0;
elseprintf("");
printf("%d",i);
}
}
printf("\n");
return0;
}
说明:
(1)memset(a,0,sizeof(a))的作用是把数组a清零,它也在string.h中定义。
使用memset比for循环更方便、快捷。
补充:
memset函数原型如下:
void*memset(void*buffer,charc,intcount);
功能:
把buffer所指内存区域的前count个字节设置成字符c,第三个参数指的是字节的个数,返回指向buffer的指针,在程序中需包含头文件:
#include。
由memset函数的头文件可以知道,其主要是对字符数组进行设置,当然也可以对数组进行初始赋值(一般是0,-1)。
(2)有一个技巧是在输出:
为了避免输出多余空格,设置了一个标志变量first,可以表示当前要输出的变量是否为第一个。
第一个变量前不应有空格,但其他都有。
例3-2蛇形填数。
在n*n方阵里填入1,2,…,n*n,要求填成蛇形。
例如n=4时方阵为
1011121
916132
815143
7654
上面的方阵中,多余的空格只是为了便于观察规律,不必严格输出。
n≤8。
【分析】
与数学的矩阵相比,可以用一个所谓的二维数组来存储题目中的方阵。
只需声明一个inta[MAXN][MAXN],就可以获得一个大小为MAXN×MAXN的方阵。
在声明时,两维的大小不必相同。
提示3-4:
用inta[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
#include
#defineMAXN10
inta[MAXN][MAXN];
intmain(){
intn,x,y,tot=0;
scanf("%d",&n);
memset(a,0,sizeof(a));
tot=a[x=0][y=n-1]=1;
while(totwhile(x+1a[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+1a[x][y+1])a[x][++y]=++tot;/*向右走*/
}
for(x=0;xfor(y=0;yprintf("\n");
}
return0;
}
说明:
本程序利用了C语言的简洁的优势。
首先,赋值x=0和y=n-1后马上作为a数组的下标,可以合并完成;tot和a[0][n-1]都要赋值1,也可以合并完成。
用一条语句可以完成多件事情,并且不有牺牲程序的可读性。
提示3-5:
可以利用C语言简洁的语法,但前提是保持代码的可读性。
提示3-6:
在很多情况下,最好是在做一件事之前检查是不是可以做,而不要做完再后悔。
因为“悔棋”往往也比较麻烦。
说明:
本程序对于与x+1如果x+1a[x+1][y],也就不会越界了。
3.2字符数组
文本处理在计算机应用中占有重要地位。
在C语言中,字符串其实就是字符数组——可以像处理普通数组一样处理字符串,只需要注意输入输出和字符串函数的使用。
例3-3竖式问题。
找出所有形如abc*de(三位数乘以两位数)的算式,使得在完整的竖式中,所有数字都属于一个特定的数字集合。
输入数字集合(相邻数字之间没有空格),输出所有竖式。
每个竖式前应有编号,之后应有一个空行。
最后输出解的总数。
具体格式见样例输出(为了便于观察,竖式中的空格改用小数点显示,但你的程序应该输出空格,而非小数点)。
样例输入:
2357
样例输出:
<1>
..775
x..33
-----
.2325
2325.
-----
25575
Thenumberofsolutions=1
【分析】
本题的解题策略是尝试所有的abc和de,判断是否满足条件。
写出如下的伪代码:
chars[20];
intcount=0;
scanf("%s",s);
for(abc=111;abc<=999;abc++)
for(de=11;de<=99;de++)
if(“abc*de”是个合法的竖式){
printf("<%d>\n",++count);
打印abc*de的竖式和其后的空行
count++;
}
printf("Thenumberofsolutions=%d\n",count);
说明:
(1)chars[20]是一个定义字符数组的语句,scanf("%s",s)表示从键盘输入一个字符串给字符数组s。
(2)char是“字符型”的意思,而字符是一种特殊的整数。
每一个字符都有一个整数编码,称为ASCII码。
C语言中允许用直接的方法表示字符,还有以反斜线开头的字符(转义序列,EscapeSequence)。
(3)在stdlib.h中有一个函数atoi,它的函数原型如下:
intatoi(char*s)
它表示将字符串s中的内容转换成一个整型数返回,如字符串“1234”,则函数返回值是1234。
(4)在stdlib.h中有一个函数itoa,它的函数原型如下:
char*itoa(intvalue,char*string,intradix)
它表示将整数value转换成字符串存入string,radix为转换时所用基数(保存到字符串中的数据的进制基数281016),返回指向转换后的字符串的指针 。
例如,itoa(32,string,10)是将32变成十进制数一个字符串“32”,并返回指向这个字符串的指针;itoa(32,string,16)是将32变成十进制数一个字符串“20”,并返回指向这个字符串的指针。
(5)stdlib头文件即standardlibrary标准库头文件,该文件包含了的C语言标准库函数的定义,声明了数值与字符串转换函数,伪随机数生成函数,动态内存分配函数,进程控制函数等公共函数。
输入样式:
C语言模式:
#include
C++样式:
#include
提示3-7:
C语言中的字符型用char表示,它实际存储的是字符的ASCII码。
字符常量可以用单引号法表示。
在语法上可以把字符当作int型使用。
语句scanf("%s",s);表示读入一个不含空格、TAB和回车符的字符串,存入字符数组s中,s前面没有&符号。
提示3-8:
在scanf("%s",s)中,不要在s前面加上&符号。
如果是字符数组chars[MAXN][MAXL],可以用scanf("%s",s[i])读取第i个字符串。
接下来有两个问题:
判断和输出。
先考虑输出。
首先计算第一行乘积x=abc*e,然后是第二行y=abc*d,最后是总乘积z=abc*de,然后一次性打印出来:
printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n",abc,de,x,y,z);
完整程序如下:
程序3-4竖式问题
#include
#include
intmain()
{
inti,ok,abc,de,x,y,z,count=0;
chars[20],buf[99];
scanf("%s",s);
for(abc=111;abc<=999;abc++)
for(de=11;de<=99;de++)
{
x=abc*(de%10);
y=abc*(de/10);
z=abc*de;
sprintf(buf,"%d%d%d%d%d",abc,de,x,y,z);
ok=1;
for(i=0;iif(strchr(s,buf[i])==NULL)
ok=0;
if(ok)
{
printf("<%d>\n",++count);
printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n",abc,de,x,y,z);
}
}
printf("Thenumberofsolutions=%d\n",count);
return0;
}
说明:
(1)sprintf函数
sprintf是个变参函数,定义如下:
intsprintf(char*buffer,constchar*format[,argument]...);
除了前两个参数类型固定外,后面可以接任意多个参数。
功能:
把格式化的数据写入字符串buf,它的返回值是字符串长度。
包含此函数的头文件是stdio.h。
例如,本程序中的sprintf(buf,"%d%d%d%d%d",abc,de,x,y,z);语句的功能是将整数abcdexyx打印成字符串存储在串buff中。
可以直接接受其返回值:
intlen=sprintf(buf,"%d%d%d%d%d",abc,de,x,y,z);
(2)strchr函数
strchr函数定义如下:
char*strchr(constchar*s,charc);
功能:
查找字符串s中首次出现字符c的位置。
它的返回值是返回首次出现c的位置的指针,如果s中不存在c则返回NULL。
包含此函数的头文件是string.h。
例如,本程序的if语句中strchr(s,buf[i])的功能是查找字符串s中首次出字符buf[i]的位置。
如果strchr(s,buf[i])==NULL,则表明字符串s中没有buf[i]的字符。
(3)sprintf函数、printf函数、fprintf函数的区别
printf输出到屏幕,fprintf输出到文件,而sprintf输出到字符串。
需要注意是应该保证写入的字符串有足够的空间。
提示3-9:
可以用sprintf把信息输出到字符串,用法和printf、fprintf类似。
但你应当保证字符串足够大,可以容纳输出信息。
字符串的空间应为字符个数加1,这是因为C语言的字符串是以空字符'\0'结尾的。
函数strlen(s)的作用是获取字符串s的实际长度,即函数strlen(s)返回的是结束标记之前的字符个数(需包含头文件string.h)。
因此这个字符串中的各个字符依次是s[0],s[1],…,s[strlen(s)-1],而s[strlen(s)]正是结束标记'\0'。
提示3-10:
C语言中的字符串是'\0'结尾的字符数组,可以用strlen(s)返回字符串s中结束标记之前的字符个数。
字符串中的各个字符是:
s[0],s[1],…,s[strlen(s)-1]。
提示3-11:
由于字符串的本质是数组,它也不是“一等公民”,只能用strcpy(a,b)、strcmp(a,b)、strcat(a,b)来执行“赋值”、“比较”和“连接”操作。
而不能用=、==、<=、+等运算符。
上述函数都在string.h中声明。
除了字符串之外,还要注意++count和count++的用法。
++count本身的值是加1以后的,但count++的值是加1之前的(原来的值)。
注意:
滥用++count和count++会带来很多隐蔽的错误,所以最好的方法是避开它们。
提示3-12:
滥用++、--、+=等可以修改变量值的运算符很容易带来隐蔽的错误。
建议每条语句最多只用一次这种运算符,并且它所修改的变量在整条语句中只出现一次。
提示3-13:
但编译选项-Wall编译程序时,会给出很多(但不是所有)警告信息,以帮助程序员查错。
但并不能解决所有的问题:
有些“错误”程序是合法的,只是这些动作不是你所期望的。
3.3最长回文子串
例3-4回文串。
输入一个字符串,求出其中最长的回文子串。
子串的含义是:
在原串中连续出现的字符串片段。
回文的含义是:
正着看和倒着看相同。
如abba和yyxyy。
在判断时,应该忽略所有标点符号和空格,且忽略大小写,但输出应保持原样(在回文串的首部和尾部不要输出多余字符)。
输入字符串长度不超过5000,且占据单独的一行。
应该输出最长的回文串,如果有多个,输出起始位置最靠左的。
样例输入:
Confuciusssay:
Madam,I'mAdam.
样例输出:
Madam,I'mAdam
【分析】
由于输入的字符比较复杂,首先,不能用scanf("%s")输入字符串,可用下述两种方法解决下列问题:
第1种方法是使用fgetc(fin),它读取一个打开的文件fin,读取一个字符,然后返回一个int值。
因为如果文件结束,fgetc将返回一个特殊标记EOF,它并不是一个char。
如果要从标准输入读取一个字符,可以用getchar(),它等价于fgetc(stdin)。
补充:
fgetc()介绍
功能:
从流中读取字符。
用法:
intfgetc(FILE*stream);
意为从文件指针stream指向的文件中读取一个字符,读取一个字节后,光标位置后移一个字节。
这个函数的返回值,是返回所读取的一个字节。
如果读到文件末尾或者读取出错时返回EOF。
提示3-14:
使用fgetc(fin)可以从打开的文件fin中读取一个字符。
一般情况下应当在检查它不是EOF后再将其转换成char值。
从标准输入读取一个字符可以用getchar(),它等价于fgetc(stdin)。
fgetc()和getchar()将读取“下一个字符”,如果是空格,会正常读取;若是换行,读取到的将是回车符'\n'。
补充:
getchar()介绍
功能:
从stdio流中读字符
用法:
intgetchar(void);
getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1。
可以利用getchar()函数让程序调试运行结束后等待编程者按下键盘才返回编辑界面,用法:
在主函数结尾,return0;之前加上getchar();
#include
intmain(void)
{
intc;
while((c=getchar())!
=-1)
//while((c=getchar())!
=EOF)
printf("%c",c);
return0;
}
#include
intmain(void)
{