zoukankan      html  css  js  c++  java
  • 要心中有“数”——C语言初学者代码中的常见错误与瑕疵(8)

      在 C语言初学者代码中的常见错误与瑕疵(7)  中,我给出的重构代码中存在BUG。这个BUG是在飞鸟_Asuka网友指出“是不是时间复杂度比较大”,并说他“第一眼看到我就想把它当成一个数学问题来做”之后,我又重新对问题进行了数学式的思考后发现的。
      这个BUG源于在(1<=A,B<=1000)条件下对矩形个数的数量级心里没数。当时觉得这个题目的目的是考察穷举,由于题目限定了A、B的范围,所以结果应该不是很大。事实证明这种想法是一厢情愿。
      通常情况下,我不喜欢用数学方法解决C语言编程问题。因为很多问题,一旦在数学上能够轻易解决,编写代码往往是索然无趣的,对学习和练习C语言几乎没有什么益处。譬如,求1+2+3+……+100,如果不知道或假装不知道数学解法,代码可能是这样的

    #include <stdio.h>
    
    int main( void )
    {
      int i , sum = 0 ;
      
      for ( i = 1 ; i <=100 ; i ++ )
      {
         sum += i ;
      }
      printf("%d
    ",sum);
      
      return 0;
    }

      而若使用数学方法,代码则是

    #include <stdio.h>
    
    int main( void )
    {
      printf("%d
    ", (1 + 100) * 100 / 2 );
      
      return 0;
    }

      前者,C语言和编程成分的含量很高,数学含量却很低;而后者数学含量很高,可C语言和编程成分的含量几乎为0。
      从解决实际问题的角度来说,显然应该用后一种方法;而从学习C语言和练习编程的角度来说,则应该使用前一种方法。所以一个好的编程问题,应该是没有数学解的,至少应该没有显而易见、容易得到的数学解。否则,只是用C语言对公式做一个简单的翻译(这大概是FORTRAN语言的哲学),编程的味道就全没有了,谭浩强的很多题目就是如此(参见:滥用变量综合症)。“矩形的个数”问题应该说还不是那么容易得到数学解的。
      所以,在飞鸟_Asuka 网友说他“第一眼看到我就想把它当成一个数学问题来做”之后,我也尝试着用数学的方式思考了一下。我发现,在同一条直线上的n个不同的点一共可以构成(n-1)n/2条不同的线段,A*B的矩形相邻两边各有A+1和B+1个不同的点,因而可以分别构成(A+1)A/2和(B+1)B/2条不同的线段,这样构成的矩形的个数一共就是(A+1)A/2×(B+1)B/2个。当A和B取最大值1000时,结果显然不小于25×1010,而这个值显然大于231-1,甚至也大于232-1(多数编译器中 unsigned 类型所能表示的最大整数)。这样,原来的重构代码中用int类型作为结果的类型,显然错了。
      我一向认为C语言程序员应该心中有“数”,即对表达式中的数据和结果有最起码的范围估计。没想到,这次由于刻意回避数学解法,却立刻遭到了违背信条的报应。正应验了Muphry's law所言:Anything that can go wrong, will go wrong。
      冯诺依曼也认为程序员至少应该清楚计算过程中数据的数量级,为此他反对浮点数。与之相反,约翰.巴科斯则盲目乐观地发明了浮点数,很多程序员尽情地享受浮点数的方便,却由于盲目乐观屡屡被浮点数这种“有缺陷的抽象”所伤。更有甚至,很多程序员连浮点数最基本的原理都不懂,竟然能写出k=sqrt(n) 这样狗屁不通的句子。(参见:似是而非的k=sqrt(n)
      回过头来再谈我代码的BUG。这个BUG的另一个教训是没有进行比较充分的测试,如果测试一下边界情况可能不难发现这个BUG。后来我又重新测试了一下,发现程序运行时间比较长,这说明飞鸟_Asuka 网友指出“是不是时间复杂度比较大”的问题也是存在的。但当时为了算法叙述的方便,就没有按照下面的方法写count()函数:

    int count( int A , int B )
    {
       int x1 , y1 ;//第一个点的坐标 
       int x2 , y2 ;//第二个点的坐标
       int num = 0 ;
       
       for ( x1 = 0 ; x1 < B ; x1 ++ )//   for ( x1 = 0 ; x1 <= B ; x1 ++ )
          for ( y1 = 0 ; y1 < A ; y1 ++ )//     for ( y1 = 0 ; y1 <= A ; y1 ++ )//穷举第一个点的各种可能 
             for ( x2 = x1 + 1 ; x2 <= B ; x2 ++ )//    for ( x2 = 0 ; x2 <= B ; x2 ++ )
                for ( y2 = y1 + 1 ; y2 <= A ; y2 ++ )//     for ( y2 = 0 ; y2 <= A ; y2 ++ )//穷举第二个点的各种可能 
                      num ++ ;                //            {
                                              //               if ( x1 < x2 && y1 < y2 )
                                              //                   num ++ ;
                                              //            }
       return num ;
    }

      如果写成这种形式,不难发现第二层循环与第三层是可以对调的,对调后为:

       for ( x1 = 0 ; x1 < B ; x1 ++ )
          for ( x2 = x1 + 1 ; x2 <= B ; x2 ++ )
             for ( y1 = 0 ; y1 < A ; y1 ++ )
                for ( y2 = y1 + 1 ; y2 <= A ; y2 ++ )
                      num ++ ;

      这时应该能够看出,最内两层循环次数为 A + A-1 + A-2 +……+1,最外两层循环的循环次数为B + B-1 + B-2 +……+1,因而结果可以直接得到,即(A+1)A/2*(B+1)B/2。

      算法问题解决了,又产生了新的问题,那就是如何表示这么大的整数,这是一个数据结构的问题。(或许,这才是题目的本意?)
      办法之一就是使用表示范围更大的整数类型,例如C99中的long long int类型。
      如果编译器不支持C99也没有表示范围更大的整数类型,那就只有自己着手构造新的数据结构了。
      矩形个数的最大值大约为25×1010,这并不是一个很大的数,一个int不够,那就用两个好了。这里把存储大数的数据结构设计为一数组,

    typedef int BIG_NUM[2] ;

      数组的第0个元素存储低6位,第1个元素存储高位。存储低6位的原因是避免乘法运算时溢出(乘数不超过1001,与一个6十进制整数相乘不超过109,在int类型的表示范围之内)。

      通过调用

    BIG_NUM num ;
    count( num , A , B );

    将结果写到num中。

    count()函数的实现:

    void count( BIG_NUM m , int A , int B )
    {
       m[0] = 1 ;
       m[1] = 0 ;
       mul_sum( m , A );//将1+2+……+A的结果乘入m 
       mul_sum( m , B );//将1+2+……+B的结果乘入m
    }

      由于结果是累乘得到的,所以初始化为1。为防止溢出,只能

    void mul_sum( BIG_NUM m , int n )
    {
       int t1 = n % 2 == 0 ? n / 2 : n , 
           t2 = n % 2 != 0 ? (n + 1) / 2 : n + 1 ;
       mul( m , t1 );
       mul( m , t2 );
    }

      小心翼翼地一个个地乘(每个乘数都不得超过1001)。t1,t2这两个变量是为了回避BIG_NUM类型的除法运算。

    void mul( BIG_NUM m , int n )
    {   
       m[0] *= n ;
       m[1] *= n ;
       m[1] += m[0]/1000000;//进位 
       m[0] %= 1000000;
    }

      每乘以一个数,立刻就处理进位问题。
      最后还要考虑如何输出:

    void output( BIG_NUM m )
    {
       if ( m[1] == 0 )
       {
          printf( "%d
    " , m[0] );
          return ;
       }
       
       printf( "%d" , m[1] );
       printf( "%06d
    " , m[0] );
       
    }

      这里的"%06"是为了保证低位不够6位时仍能正确输出。

      完整的代码如下:

    /*
    矩形的个数 
    在一个3*2的矩形中,可以找到6个1*1的矩形,4个2*1的矩形3个1*2的矩形,
    2个2*2的矩形,2个3*1的矩形和1个3*2的矩形,总共18个矩形。 
    给出A,B,计算可以从中找到多少个矩形。 
    
    输入: 
    本题有多组输入数据(<10000),你必须处理到EOF为止 
    输入2个整数A,B(1<=A,B<=1000) 
    
    输出: 
    输出找到的矩形数。 
    
    样例:
    
    输入: 
    1 2 
    3 2 
    
    输出: 
    3 
    18
    
    作者:薛非
    出处:http://www.cnblogs.com/pmer/   “C语言初学者代码中的常见错误与瑕疵”系列博文 
    
    */
    
    #include <stdio.h>
    typedef
    int BIG_NUM[2] ; void count( BIG_NUM , int , int ); void mul_sum( BIG_NUM , int ); void mul( BIG_NUM , int ); void output( BIG_NUM ); #define POW 1000000 #define WID 6 int main( void ) { int A , B ; while ( printf( "输入2个整数A,B(1<=A,B<=1000) " ), scanf( "%d%d" , &A , &B )!= EOF ) { BIG_NUM num ; count( num , A , B ); output( num ); }   return 0; } void output( BIG_NUM m ) { if ( m[1] == 0 ) { printf( "%d " , m[0] ); return ; } printf( "%d" , m[1] ); printf( "%0*d " , WID , m[0] ); } void mul( BIG_NUM m , int n ) { m[0] *= n ; m[1] *= n ; m[1] += m[0]/POW;//进位 m[0] %= POW; } void count( BIG_NUM m , int A , int B ) { m[0] = 1 ; m[1] = 0 ; mul_sum( m , A );//将1+2+……+A的结果乘入m mul_sum( m , B );//将1+2+……+B的结果乘入m } void mul_sum( BIG_NUM m , int n ) { int t1 = n % 2 == 0 ? n / 2 : n , t2 = n % 2 != 0 ? (n + 1) / 2 : n + 1 ; mul( m , t1 ); mul( m , t2 ); }

      写的有点丑。

  • 相关阅读:
    php+Nginx 安装手册
    PostgreSQL 生成uuid
    登陆服务器错误: Disconnected:No supported authentication methods available
    安装Tengine
    netty解决方法 io.netty.util.IllegalReferenceCountException: refCnt: 0, increment: 1
    IntelliJ Idea 2016,2017,2018 注册码 免费激活方法
    jdbc连接"Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driv"
    mysql错误:you are using update mode and you tried to update a table without a where that uses a key column to disable safe mode
    spring介绍;安装;使用
    设置maven仓库阿里镜像
  • 原文地址:https://www.cnblogs.com/pmer/p/3476850.html
Copyright © 2011-2022 走看看