【题目描述】
输入一个字符串,判断它是否为回文串以及镜像串。输入字符串保证不含数字0。所谓回文串,就是反转以后和原串相同,如abba和madam。所谓镜像串,就是左右镜像之后和原串相同,如2S和3AIAE。注意,并不是每个字符在镜像之后都能得到一个合法字符。在本题中,每个字符的镜像如图所示(空白项表示该字符镜像后不能得到一个合法字符)。
输入的每行包含一个字符串(保证只有上述字符。不含空白字符),判断它是否为回文串和镜像串(共4种组合)。每组数据之后输出一个空行。
【样例输入】
NOTAPALINDROME
ISAPALINILAPASI
2A3MEAS
ATOYOTA
【样例输出】
NOTAPALINDROME -- is not a palindrome.
ISAPALINILAPASI -- is a regular palindrome.
2A3MEAS -- is a mirrored string.
ATOYOTA -- is a mirrored palindrome.
【题目来源】
刘汝佳《算法竞赛入门经典 第2版》例题3-3 回文词(Palindromes, UVa401)
【解析】
一、原书代码:
#include<stdio.h>
#include<string.h>
#include<ctype.h>
const char* rev = "A 3 HIL JM O 2TUVWXY51SE Z 8 ";
const char* msg[] = {"not a palindrome", "a regular palindrome", "a mirrored string", "a mirrored palindrome"};
char r(char ch) {
if(isalpha(ch)) return rev[ch - 'A'];
return rev[ch - '0' + 25];
}
int main() {
char s[30];
while(scanf("%s", s) == 1) {
int len = strlen(s);
int p = 1, m = 1;
for(int i = 0; i < (len+1)/2; i++) {
if(s[i] != s[len-1-i]) p = 0; //不是回文串
if(r(s[i]) != s[len-1-i]) m = 0; //不是镜像串
}
printf("%s -- is %s.\n\n", s, msg[m*2+p]);
}
return 0;
}
代码说明:
1.本题再次利用了常量数组,只是它没有采用数组的定义形式,而是使用了指针。const char*是一个指向常量字符的指针类型。通过初始化赋值使rev指向字符串常量 "A 3 HIL JM O 2TUVWXY51SE Z 8 "的首字符 'A' 的地址。虽然定义为指针,但却可以用rev[0]、rev[1]这样的类似于数组的方式使用指针,这一点体现了C语言的灵活性。
2.本代码的一个巧妙用法是,利用了“连续字母、数字的ASCII码值也是连续的”这一特点,直接实现了字符与下标的对应关系,从而提高了查找效率(避免了使用循环查找字符位置,详见后面老金的代码)。在程序员眼中,字符并不应该仅仅是字符,而是一个个整数。所以,如果常量数组中的字符是连续的,应该有意识考虑到利用其ASCII码值获取字符对应的数组下标。
3.本题使用了自定义函数char r(char ch),用于返回字符ch的镜像字符。书里给出的解释是“需要判断ch是字母还是数字”,其实这根本不是创建函数的理由,本题完全可以不用这个函数,把里面的代码简单改下就可以直接放在main函数里。改后代码如下:
#include<stdio.h>
#include<string.h>
#include<ctype.h>
const char* rev = "A 3 HIL JM O 2TUVWXY51SE Z 8 ";
const char* msg[] = {"not a palindrome", "a regular palindrome", "a mirrored string", "a mirrored palindrome"};
int main() {
char s[30];
while(scanf("%s", s) == 1) {
int len = strlen(s);
int p = 1, m = 1, j;
for(int i = 0; i < (len+1)/2; i++) {
if(s[i] != s[len-1-i]) p = 0; //不是回文串
if(isalpha(s[i])) j=s[i] - 'A';
else j=s[i] - '0' + 25;
if(rev[j] != s[len-1-i]) m = 0; //不是镜像串
}
printf("%s -- is %s.\n\n", s, msg[m*2+p]);
}
return 0;
}
可见,不用函数丝毫没有增加代码的复杂度。
函数本质是就是一段代码,创建函数最主要的理由只有一个,就是要重复利用代码。除此之外,函数还能让代码看起来更有条理,更容易理解。原书代码中这个自定义函数只被调用了一次,根本起不到复用的作用。
4.将要输出的内容放到常量数组msg中是另一个妙用,这代码写得确实漂亮,真是让老金开了眼见。char* msg[]定义了一个字符指针数组。什么叫“字符指针数组”呢?它有三层含义:字符、指针、数组。首先,它是一个数组,其次,数组中的元素都是指针,最后,这些指针指向的都是字符。前面如果再加上const就表示指向的字符都是常量字符(不能改变)。字符指针数组相当于一个二维数组。
二、老金代码
#include<stdio.h>
#include<string.h>
char s[256];
const char a[]="AEHIJLMOSTUVWXYZ12358";
const char b[]="A3HILJMO2TUVWXY51SEZ8";
int main(){
while(scanf("%s", s)==1){
int palindrome=1, mirror=1, len=strlen(s);
for(int i=0; i<len; i++){
//判断是否为回文串
if(s[i]!=s[len-1-i]){
palindrome=0;
}
//判断是否为镜像串
int j;
for(j=0; a[j]!=s[len-1-i] && j<strlen(a); j++);
if(j==strlen(a)) mirror=0;
else {
if(s[i]!=b[j]) mirror=0;
}
}
if(!palindrome && !mirror) printf("%s -- is not a palindrome.\n\n", s);
else if(palindrome && !mirror) printf("%s -- is a regular palindrome.\n\n", s);
else if(!palindrome && mirror) printf("%s -- is a mirrored string.\n\n", s);
else printf("%s -- is a mirrored palindrome.\n\n", s);
}
return 0;
}
代码说明:
1.没想到利用ASCII码值连续性的特点,而是通过定义了两个常量数组建立起每个字符与镜像字符的映射关系,这样就需要通过计算找到每个字符在常量数组中的下标,降低了效率。
2.用for循环判断了所有的输入字符,老金当时也知道没有必要,但一时想不到方法确定要判断到哪一位,于是索性就全部都判断。参考原书代码,这个问题现在想明白了。字符长度len分奇数、偶数两种情况,奇数可表示为len=2n+1,偶数可表示为len=2n,它们要判断到的位数分别为n+1、n,用(len+1)/2一个表达式就正好可以求出正确的判断位数,而不用预先判断字符是奇数个还是偶数个。原理很简单:
len为奇数时,(len+1)/2=(2n+1+1)/2=n+1。
len为偶数时,(len+1)/2=(2n+1)/2=n+1/2=n(int型运算,小数部分会被舍去)。
3.以老金的脑力,断然是想不到利用数组输出不同的结果的。老金在使用if-else语句时没有丝毫的迟疑。
对比两段代码,不难看出,老金的代码缺少了点儿灵性。
这或许就是普通人和大神的差距吧!