一道淘汰85%面试者的百度开发者面试题——解题、参考答案和优化思路

时间:2022-11-15 14:39:38

一道听说挺考验基本功的淘汰85%的面试者的题目,自己也来试试。


题目描述:

依序遍历0到100闭区间内所有的正整数,如果该数字能被3整除,则输出该数字及‘*’标记;如果该数字能被5整除,则输出该数字及‘#’标记;如果该数字既能被3整除又能被5整除,则输出该数字及‘*#’标记。



之前被考过一个类似的题,不过那个是看一个数是3的多少次幂。就是因为没有提纲,临时想法太多,老变,又多嘴问了一句要不要把具体几次幂输出,彻底扰乱了原来的思路。最后当成3的倍数了。。。还想加输入非法判断,还想什么fgetc什么getchar,什么if( ! ( '0' < c && c < '9' ) )之类的,乱七八糟,没弄对~!其实直接在代码里边int x,然后判断x就完事了。。。。



回到题目来,这种最好是有了一个思路就先做出来,先做个最无脑的,然后再想办法改进,这样做的目的主要还是为了理清思路,迭代开发应该会保险点(这叫迭代么?我自己胡乱定义的~),毕竟没有别的地方做提纲。

本来想迭代,先写一个三次循环再写一个单次循环,或者先写一个什么判断方式,再改进,吸取“步子迈太大一下扯到蛋”的教训。但是也没多少东西,干脆一个循环完成吧,三个循环太慢,又不好协调三种情况出现重复时的输出控制。至于一个循环怎么区分三种情况,直接加多条判断语句。

想先输出数字本身,然后根据判断结果再跟上符号"*"和"#"。不过问题也来了,不是所有数字都要输出~~~

直接采用简单粗暴的方法完成,一个小细节就是优先级,3和5的公倍数判断应该在最前,其余两个无区别~!否则,15的倍数会被3或者5的if判断为真。。。

代码如下:


*/
#include<stdio.h>

int main(){
        int i;
        for(i = 1;i <= 100;i++){
//              printf("%d",i);//先输出数字本身的做法不是很好,不是所有数字都应该输出!
//根据优先级,应该先判断是否同时被3和5整除,再看单独的3和5的情况。
                if(0 == i % 3 && 0 == i % 5)
                        printf("%d*#\t",i);
                else if(0 == i % 3)
                        printf("%d*\t",i);
                //最后这种,不能一"else"以蔽之,因为else包括了既不能被三整除也不能被五整除的
                else if(0 == i % 5)
                        printf("%d#\t",i);
//              else//这是测试将判断5整除的else if 写成else的后果
//                      printf("test:shouldn't output~!\n");
        }
}

这是输出结果:

3*    5#    6*    9*    10#    12*    15*#    18*    20#    21*    24*    25# 27*    30*#    33*    35#    36*    39*    40#    42*    45*#    48*    50#    51* 54*    55#    57*    60*#    63*    65#    66*    69*    70#    72*    75*#    78* 80#    81*    84*    85#    87*    90*#    93*    95#    96*    99*    100#    


那么,如果先判断数字是否应该显示出来,再判断该添加哪个符号呢?似乎更“智能”!但是如果在互斥的ifelse语句中,会导致后边的“跟不上”——不输出,例如:

                if(0 == i % 3 || 0 == i % 5)
                        printf("%d\t",i);
                else if(0 == i % 3)
                        printf("*\t");
                else if(0 == i % 5)
                        printf("#\t");


那就行不通了?其实很好解决,不在同一层互斥就完了:

                if(0 == i % 3 || 0 == i % 5)
                        printf("%d\t",i);
                if(0 == i % 3)
                        printf("*\t");
                else if(0 == i % 5)
                        printf("#\t");


哈哈,这么小的技巧也有成就感,复习ing。。。。

那么说到效率等问题,这个代码到底有没有优化的余地呢?


优化思路:

1.for循环的判断,记得用“等于零”当循环结束条件比“和非零值对比”更快,例如:

for(i = 100 ; i != 0 ; i--)

但是我记得这是ARM架构的特性吧?!

2.判断次数,感觉这样顺次判断三回很蠢,不知道if语句判断次数还能不能优化



吐槽:说一千道一万,到底哪个快,还要看编译器的吧?同样都是循环,你知道是while循环快还是for循环快啊~!只是一般情况下,从算法角度似乎认为,按次数来评定时间复杂度。。。



这题今天才看到的东西,但是已经十来天了,而且已经结束了,在如下网址有了几个“导师筛选”的参考答案:

http://student.csdn.net/mcd/topic/235300/761698

想不出来也不能干耗着,既然已经结束了,看看人家“参考答案”吧

我的答案和第一个第四个第五个雷同!说明这样做至少没错,或者说大家差不多都是这个思路(这也是很正常不过的一个思路了)~~同时也说明可能没有太大的优化空间了,至少在编译器之上的语言层面是吧!


同时也说明,所谓淘汰85%的面试者的百度题,本身并不难,也许是因为应试者太多,或都准备不充分吧?不过看回帖,确实很多人连这么简单的逻辑都没写完整,或者每个人都有那么个阶段——我之前不也是被一道很简单的题淘汰了么。


二段是java的

public static void test(){
   for(int i = 1; i <= 100; i++){
       int a3 = i % 3;
       int a5 = i % 5;
       if(a3 == 0 && a5 == 0){
          System.out.println(i+"*#");
       }else if(a3 == 0){
          System.out.println(i+"*");
       }else if(a5 == 0){
          System.out.println(i+"#");
       }
    }
 }

整体运行思路区别不大,不过把变量初始化和赋值感觉上”重复执行了100遍!“

不好评价这个事,好比下边这个

for(int i = 0;i <= 100; i++)

感觉也给i初始化定义了100遍(我会告诉你第一个是初始化,只执行一次么)

一般C习惯用:

int a3;

int a5;

for(.....){

a3 = i % 3;

a5 = i % 5;

....

}

然后把上边这位同学的代码改成C程序以后,我的gdb查看,两种情况编成一样的汇编代码了~~gcc把这弱智代码优化了吧




引入一个比较另类的答案,尝试对其进行解析:

#include<iostream>
#include<string>

using namespace std;

int main()
{
    int add[7] = {3,2,1,3,1,2,3};
    char c[6] = {'*','#','*','*','#','*'};
    string all = "*#";
    int i = 0;
    int index = 0;
    while((i += add[index]) <= 100)
    {
        if(index != 6)
        {
            cout << i << c[index] << endl;
            index++;
        }
        else
        {
            cout << i << all << endl;
            index = 0;
        }
    }
    return 0;
}

这个答案,乍一看有点抽象,甚至感觉 额外用了不少存储空间 ,两个数组、一个字符串、一个数组索引、一个被判断的整数变量i。。。。

不过抓住了数字的一些小规律,比如3和5的最大公约数是15,那15就是一次循环,也就是那个add数组的意义,加一圈正好15,下次从头来,还是一样的规律。

确实应该向人家学习,过程很巧妙。不过给人的感觉是在程序之外动了很多脑筋,进行很多人工“预处理”,对于这个数字小、规律性强的还不错,如果是规律性不强的很大的数字,不知道该同学到时候又能想起来什么技巧。

总的来说,套路应该是对的,宁愿人多动点脑筋,也要增加实际运行的效率么,因为当资源消耗量大的惊人时,这样做还是非常值得的。

至于效率:一个大循环,循环包括一个边界判断,循环内部两个判断语句,只需搞定是不是公约数。看起来是不错的~!

但是具体到底好不好,有没有缺陷?老师也看到他的答案了,也说正确了,但是5个参考答案里没有他,他也跟贴申诉了,说他的不用都遍历,比其他人都快,这么一个“鹤立鸡群”的答案,为什么不被采用呢?不清楚了,非高手。。。


对比:

粗略的设计一个验证方法——就是把数据放大,看谁运行时间短(要是把3和5变了,这种又得重新设计半天吧?直接把i改大点就行了):


先看他这种:

一道淘汰85%面试者的百度开发者面试题——解题、参考答案和优化思路

严谨起见,起始和终止之间应该把初始化各种变量的操作时间也算上(不过小到可以忽略了)。

一百万次循环的多次耗时测量:5.63秒、5.25秒、5.17秒、5.16秒、5.34秒等等。

(”秒“从实际个人感官来评定,不是很准确,可能是duration式子出了问题,或者虚拟机的特性?不过把这个数字单纯作为参考还是可以的。。。)



换我那种做法:

一道淘汰85%面试者的百度开发者面试题——解题、参考答案和优化思路

多次测试一百万次耗时分别为:2.12秒、1.86秒、1.96秒、1.86秒。。。


多次取平均,可以看到有明显差距。但是前提是打印语句不能丢,打印语句丢了,时间消耗就非常小了。

想要减少误差,也可用一千万次甚至更多,或者嵌套一千万次乘以一千万次循环,计算耗时。不过在这种悬殊差距下引入更浪费时间的测试方法意义已经不是很大,可做参考。


原因及优化:

根据结果,猜测是,打印语句频繁的系统调用可能是耗时增加的罪魁祸首,因为把打印语句注释掉,两者消耗都会非常低。不知道这是否是C的printf和C++的cout之间的差距,抑或是cout语句用了三次"<<"?

通过我不严谨的实验,用自己的VMWARE虚拟机+UBUNTU系统+G++编译器编译运行结果猜测,他的这个程序慢,可能就是慢在打印上了,慢在系统调用上了。


不过可以逐步”优化“,进行测试:

通过去掉endl,把"<<“的使用降低为二次:

最后测试显示0.34秒、0.30秒、0.28秒。。。


再变成只有一个cout << i;:

耗时0.22秒、0.24秒、0.19秒、0.19秒。。。


用printf替代cout不就完了?(同样用的G++,不确定G++和GCC编译效率有无差异,但是都用G++,总不会让C++吃亏吧)再测:

//            cout << i << c[index] << endl;
                printf("%d%c",i,c[index]);



0.32秒、0.31秒、0.27秒。。。

和我的程序2秒左右甚至低于2秒的成绩对比,还是有些差距。甚至比cout<<i;还慢了,难道是那个c[index]造成的?

去掉c[index]:

                printf("%d",i);

输出确实会稍微快一点!


要不要把while循环改成for循环?

如果还用cout,单纯将

   while(i += add[index] <= 10000000){
循环,改成

    for(i += add[index];i <= 1000000; i += add[index])
循环
耗时:5.41秒
、4.84秒、5.49秒。。。。

循环对耗时的影响不是很大。


再改的话,最后想组合起来的话,他那个std::string的字符串又不可能在printf下面输出,再改下去就真的没原样了。

结论


对时间消耗的主要影响,姑且认为是如下几点:

cout的使用——尤其是用了三次"<<"(也不好说全是因为数量,也可能和后边添加的c[index]和endl本身有关,反正是这个句子执行很慢)

即使改用printf打印,多一个参数c[index]也要变慢,所以这个c[index]可能是变慢的一个原因。


另外,while和for循环区别不大,。那几个数组和字符串的使用没有再去改,再改面目全非了。


总之 ,在本人机器上实测,这种看似很巧妙的程序、或者说从 算法 的角度看很巧妙的、这种“不用遍历”的、“时间复杂度略低”的程序,实际运行的效果并不如那种“无脑”版本强,还弱很多。。。

没去观察汇编代码,涉及系统调用,光是伪汇编指令都会多到眼花缭乱,而且也不能从伪汇编的绝对数量来判断究竟哪个快。

或许,算法和实现之间的关系本不像我想的那样。