含糊之过、多做之过及乱做之过
编程题目,从某种意义上来说通常就是程序的功能说明。其本身应该清晰明确,否则就应了孟轲老师那句名言,叫做“以其昏昏,使人昭昭”。
题目:有15个数按由小到大顺序存放在一个数组中,输入一个数,要求用折半查找法找出该数是该数组中的第几个元素的值。如果该数不在数组中,则输出“无此数”。
——谭浩强 ,《C程序设计(第四版)学习辅导》,清华大学出版社,2010年7月,p64
像这种题目就属于要求不明确的题目。没有人知道这里所说的“数”究竟是什么数:整数?分数?实数?还是复数?在题目中是含糊不清的。严格地说这种题目是根本无法解决的,因为在不同的情况下所编写程序完全不同甚至可能是天壤之别。这种模糊,在实际的项目中往往就是BUG的渊薮源头,甚至直接导致项目失败。
在工厂,任何一名合格的工人都会要求设计者澄清图纸中的模糊不清,都会要求设计者改正图纸中的错误。但在IT业,我这里说的是中国的IT业——如果它存在的话,许多程序员却不会。他们非常习惯于“以人昏昏,使己昏昏”,对待BUG极其宽容,习以为常,个别的甚至会为含糊不清和BUG辩护。这和他们中的许多人本来就是被垃圾教材熏陶出来的恐怕不无关系——他们的逻辑思维能力在不知不觉中被戕害殆尽。
为了继续后面对代码的讨论,这里不得不明确一下,这道题目中“数”是整数的含义,而且是C语言int类型能够表示的整数。
#include <stdio.h>
#define N 15
int main()
{ int i,number,top,bott,mid,loca,a[N],flag,sign;
char c;
printf("enter data:\n");
scanf("%d",&a[0]);
i=1;
while(i<N)
{scanf("%d",&a[ i]);
if(a[ i]>=a[i-1])
i++;
else
printf("enter this data again:\n");
}
printf("\n");
for(i=0;i<N;i++)
printf("%5d",a[ i]);
printf("\n");
while(flag)
{printf("input number to look for:");
scanf("%d",&number);
sign=0;
top=0;
bott=N-1;
if((number<a[0])||(number>a[N-1]))
loca=-1;
while((!sign)&&(top<=bott))
{mid=(bott+top)/2;
if(number==a[mid])
{loca=mid;
printf("Has found%d,its position is %d:\n",number,loca+1);
sign=1;
}
elseif(number<a[mid])
bott=mid-1;
else
top=mid+1;
}
if(!sign||loca==-1)
printf("cannot find %d.\n",number);
printf("continue or not(Y/N)?");
scanf(" %c",&c);
if(c=='N'||c=='n')
flag=0;
}
return0;
}
——谭浩强 ,《C程序设计(第四版)学习辅导》,清华大学出版社,2010年7月,p65~66
这段代码的第一部分,明显是在输入数组。然而从题目中“有15个数按由小到大顺序存放在一个数组中”来看,这个数组对于程序来说明显是个已知条件,题目根本就没有要求输入这个数组。所以
printf("enter data:\n");
scanf("%d",&a[0]);
i=1;
while(i<N)
{scanf("%d",&a[ i]);
if(a[ i]>=a[i-1])
i++;
else
printf("enter this data again:\n");
}
这部分分明属于画蛇添足式的自作多情。程序没有完成所要求的功能是一种BUG,程序完成了没有要求它完成的功能同样是一种BUG。程序员的职责是按照功能要求忠实地完成代码,而不是想入非非地自作多情。
即使不从画蛇添足这个角度评论,这段有点花里胡哨的代码同样非常失败:它把按顺序输入这样较为复杂且容易出错的任务交给了用户,把卖弄花哨的快感留给了自己。然而我们都知道程序是用来服务人方便人的而不是用来奴役人虐待人的。程序对15个整数进行排序是极其简单的事情,但对于人来说感受就大不相同了。即使是在要求用户输入数据的情况下也应该由程序排序而不应该要求用户排序。程序应当以人为本而不应当以花哨为本。
for(i=0;i<N;i++)
printf("%5d",a[ i]);
显然是输出这个数组,这段代码的瑕疵参见§38。这段代码后面的while语句显然是为了程序能够多次查找数据:
while(flag)
{
//……
printf("continue or not(Y/N)?");
scanf(" %c",&c);
if(c=='N'||c=='n')
flag=0;
}
然而由于题目同样也根本就没有提出这个要求,所以无疑这也属于多做之过,非但多劳,而且无功,非但无功,而且有过。完全是多此一举。
掐头去尾之后,这段程序的核心内容只有
printf("input number to look for:");
scanf("%d",&number);
sign=0; //sign为0表示尚未找到
top=0;
bott=N-1;
if((number<a[0])||(number>a[N-1]))
loca=-1; //表示找不到
while((!sign)&&(top<=bott))
{mid=(bott+top)/2;
if(number==a[mid])
{loca=mid;
printf("Has found%d,its position is %d:\n",number,loca+1);
sign=1;
}
elseif(number<a[mid])
bott=mid-1;
else
top=mid+1;
}
if(!sign||loca==-1)
printf("cannot find %d.\n",number);
这段代码的前两行无可指责。然而
if((number<a[0])||(number>a[N-1]))
loca=-1; //表示找不到
这两行代码却属于画蛇添足,因为这并不属于二分法。二分法并不需要在查找之前进行这个判断,也就是说二分法并不是在(number<a[0])&&(number>a[N-1]))的前提下才成立的。因此这两行完全可以删除。现在不难看出loca这个变量是一个滑稽的败笔,因为根本不需要这个变量(while语句里的loca+1可以用mid+1代替)。
while((!sign)&&(top<=bott))
{mid=(bott+top)/2;
if(number==a[mid])
{loca=mid;
printf("Has found%d,its position is %d:\n",number,loca+1);
sign=1;
}
elseif(number<a[mid])
bott=mid-1;
else
top=mid+1;
}
这里用!sign来控制循环是另一败笔,因为只要在sign=1;后面简单地添加一句break;就可以达到同样的目的。而且不难发现,如果是找到了匹配的元素while语句结束后一定存在top<=bott这样的关系,如果没找到匹配的元素则一定有top>bott。
这样看来sign这个变量也属于多余的阑尾,因为没找到匹配的元素完全可以在while循环结束后通过top与bott之间的关系来判断。
另外要说的是题目中明明要求“如果该数不在数组中,则输出“无此数””,但代码却背离此功能要求而擅自将其改为printf("cannot find %d.\n",number);,这种指东打西的风格在程序设计中是断然不能姑息纵容的。
在清除了那些没用阑尾之后,这个问题的代码其实非常简单:
#include <stdio.h> #define SIZE 15 int main(void) { int arr[SIZE] = { 1, 3, 4, 5, 6, 8, 12, 23, 34, 44, 45, 56, 57, 58, 68 } ; int number,low = 0 , high = SIZE - 1 , i ; for( i = 0 ; i < SIZE ; i++ ) printf(" %d" , arr[ i]); putchar('\n'); printf("输入要查找的数:"); scanf("%d",&number); while( low <= high ) { int mid ; mid = ( low + high ) / 2 ; if( number == arr[mid] ) { printf("%d是第%d个元素的值\n" , number , mid ); return 0; //退出程序 } else if ( number < arr[mid] ) high = mid - 1 ; else low = mid + 1 ; } printf("无此数\n"); return 0; }
二分法的核心思想就在于每次都只检验数组中间的元素是否是所查找的数据,如果不是则舍弃数组的一半,另一半则构成了一个新的数组,再按同样的步骤进行查找。如果数组中有n个元素,最多只需要进行(int)(log((double)n)/log(2.))+1次。而若按顺序查找的话,如果数组中有1000个元素,则最多需要进行1000次。
或许有人对if((number<a[0])||(number>a[N-1]))念念不忘,认为这个判断可以提高程序效率。在有些情况下这句确实可以提高程序效率,为效率的缘故可以考虑添加这句。但是即便是添上也不应该是if((number<a[0])||(number>a[N-1])),而应该是
if( (number<arr[low]) || (number>arr[high]) ) low = high + 1 ;
并且应该将其写在while的循环体内。