Apocalypsa

Rumination Introspection

  博客园 :: 首页 :: 联系 :: 订阅 订阅 :: 管理
  14 Posts :: 1 Stories :: 124 Comments :: 0 Trackbacks

公告

2010年11月25日 #

此篇文章接上篇 一个编程小题目引发的思考(上)

其实很多园友已经给出答案了,不过我在这里还是要写一下自己的思路

再把题目叙述一遍

 

输入:一个小于12位的十进制正整数

 

输出:打印此数字的十进制计算器表示 例: 输入:145 输出:
         __
   ||__||__
   |   | __|
于是我又重新思考了一下这道题目,并Review了一下当前的解决方案,发现这个冗长的switch是个很大的问题,这是我想到了代码大全2里提到的表驱动编程方法(就是用一个表来代替冗长的分支控制逻辑)。 小心起见,我从一个方法开始,对其进行了重新组织:
    private void PrintTopBody(int value)
    {
        if (value != 0)
        {
            string[] table = { S1, S0, S1, S1, S0, S1, S1, S1, S1, S1 };
            PrintTopBody(value / 10);
            Console.Write(table[value % 10]);
        }
    }
测试之后发现运行结果正常,我考虑可以对另外两个方法进行这样的重构,但我发现这样写出的代码依然不好维护,虽然短了很多,但是S1,S0等莫名奇妙的全局变量仍然令人很头疼。 所以接下来就是怎么消除这些恼人的全局字符串常量了。 一时间没想到什么方法,于是又重新运行了一下这个程序,得到了下面的结果:
     __  __      __  __  __  __  __  __
   | __| __||__||__ |__    ||__||__||  |
   ||__  __|   | __||__|   ||__| __||__|
这时我突然发现,这个结果不就是一个字符串表吗?为什么不直接利用这个表呢? 于是我打开Regex,利用正则替换,将上面的字符重组为一个二维字符串表格:
    private static readonly string[,] TABLE = 
    {
        {"    "," __ "," __ ","    "," __ "," __ "," __ "," __ "," __ "," __ "},
        {"   |"," __|"," __|","|__|","|__ ","|__ ","   |","|__|","|__|","|  |"},
        {"   |","|__ "," __|","   |"," __|","|__|","   |","|__|"," __|","|__|"},
    };
相应的,我新创造了一个LCDPrinter1类,并按照之前的逻辑,编写了对应的方法,代码如下:
    class LCDPrinter1
    {
        private static readonly string[,] TABLE = 
        {
            {"    "," __ "," __ ","    "," __ "," __ "," __ "," __ "," __ "," __ "},
            {"   |"," __|"," __|","|__|","|__ ","|__ ","   |","|__|","|__|","|  |"},
            {"   |","|__ "," __|","   |"," __|","|__|","   |","|__|"," __|","|__|"},
        };
        public void PrintNum(int value)
        {
            for (int i = 0; i < 3; i++)
            {
                PrintOneLayer(value, i);
                Console.WriteLine();
            }
        }
        private void PrintOneLayer(int value, int layer)
        {
            if (value != 0)
            {
                PrintOneLayer(value / 10, layer);
                Console.Write(TABLE[layer, value % 10]);
            }
        }
    }
然后进行测试,我输入123,但惊奇的发现结果是:
 __  __
 __| __||__|
|__  __|   |
非常像一个off-by-one错误,我看了下字符串表格的定义,原来是0的位置错了,修改之后重新运行,结果正常。 下面是最终的代码:
    class LCDPrinter1
    {
        private static readonly string[,] TABLE = 
        {
            {" __ ","    "," __ "," __ ","    "," __ "," __ "," __ "," __ "," __ ",},
            {"|  |","   |"," __|"," __|","|__|","|__ ","|__ ","   |","|__|","|__|",},
            {"|__|","   |","|__ "," __|","   |"," __|","|__|","   |","|__|"," __|",},
        };
        public void PrintNum(int value)
        {
            for (int i = 0; i < 3; i++)
            {
                PrintOneLayer(value, i);
                Console.WriteLine();
            }
        }
        private void PrintOneLayer(int value, int layer)
        {
            if (value != 0)
            {
                PrintOneLayer(value / 10, layer);
                Console.Write(TABLE[layer, value % 10]);
            }
        }
    }
可以发现这个解决方案不但短小(只有25行),而且清晰易懂,逻辑一目了然,相比之前那个150行的解决方案,可谓是天壤之别。

 


反思:

  • 作为一个程序员,当我接到一个task的第一反应就是CODING(我想这也是大多数程序员的通病吧),然而这时我可能并没有对这个任务有一个清晰的认识,然后写出一摊虽然可以run但是看起来莫名其妙的代码。在完成任务之后马上进行下一个task,然后这一摊weird code就被搁置在那里。等过了一段时间之后,连我自己都看不懂了,想改也没法改,一是没有时间,二是可能有一些人用到了我的代码,修改的话会引发其各种不想看见的连带效应。
  • 所以Jon Bentley在他的Programming Pearls一书中提到:Good programmers are a little bit lazy: they sit back and wait for an insight rather than rushing forward with their first idea。而我们在编程时是怎么做的呢?真实的情况时,我们往往过早的陷入到了实现功能的误区中,而忘记了原本问题到底是什么。即使是到后来insight灵光一现,也已经是too late to modify了。所谓磨刀不误砍柴工,就是这个道理。
  • 在Geogre Polya的神作How to solve it?一书中,Polya为解决问题定义了一个系统化的方法:理解题目->规划解决方案->执行解决方案->对解决过程进行反思。Polya提到,我们很多人都只注意到了前三步,而最后一步,也是他认为非常重要的一步却被忽视了。要知道我们解决新的问题往往是基于我们已有的经验的,而这些经验并不是由重复性的工作中而来,而主要从对工作的反思中而来。
  • 此外,科学家往往有这样的思维,那就是越复杂的问题的解释往往是非常简单的。Dirac甚至说:“一个理论家宁可要一个美的方程,也不要一个丑的但结果与实验数据更一致的方程。”举个简单的的例子,我们在小时候的数学考试中,如果得到的答案是1、2或者是10,我们往往会欣然接受答案;但如果计算的答案是11/17、1.947此类的数字时,我们往往会怀疑自己是不是算错了,原因很简单,这些答案的样子太邪恶了。
  • 回到程序员的视角,如果当我们对一个问题给出一个自己都认为丑陋无比的解决方案时,这时很可能就是哪里出了问题:对问题的理解不够深入?使用了错误的数据结构?此时不应该去继续CODE,而是应该进行仔细的思考,换句话说,在一些情况下,程序员应该像Dirac那样,对优美的CODE有着近乎偏执的追求。当然了,那些manager会不会允许程序员这么做,就是另外一回事了。
posted @ 2010-11-25 18:42 _Luc_ 阅读(1665) 评论(2) 编辑

一个编程小题目引发的思考

 

首先简介下题目:

 

输入:一个不超过12位的十进制正整数

 

输出:打印此数字的十进制计算器表示

 

例:

 

输入:145

 

输出:
         __
   ||__||__
   |   | __|
看到这个题目,也没多想,反正就是把这些数字打出来而已,那就一行一行打呗 于是在纸上画了几个计算器表示形式的数字: 规律是很明显的,每个数字都由3行4列组成,每一行只有固定的几种样式,比如说8的第一行是" __ ",第二行是"|__|",第三行是"|__|" 于是就有了思路:只要一位一位的读取这个数字,然后按照上中下的顺序依次打印其计算器表示的三行就行了。 由于每个数字的每一行的样式都是固定的,很容易将这些样式抽取出来,经过观察,我找出了其所有的样式,于是就有了如下的代码:
class LCDPrinter
{
    // all the paradigms in the LCD representation
    private static readonly string S0 = "    ";
    private static readonly string S1 = " __ ";
    private static readonly string S2 = "   |";
    private static readonly string S3 = "|   ";
    private static readonly string S4 = " __|";
    private static readonly string S5 = "|__ ";
    private static readonly string S6 = "|__|";
    private static readonly string S7 = "|  |";
    public void PrintNum(int value)
    {
        //TODO: print the number layer by layer
    }
}
接下来的问题就是依次获取一个数的每一位,通过一个递归,很容易实现这个功能:
    public void PrintNum(int value)
    {
        if (value != 0)
        {
            PrintNum(value / 10, layer);
            Console.Write(value % 10);
        }
    }
测试这个方法之后,接下来的工作就是一行一行的打印数值了,按照之前的思路,我把每个数字分为上中下三层,于是可以这么写:
    public void PrintNum(int value)
    {
        PrintTopBody(value);
        Console.WriteLine();
        PrintMiddleBody(value);
        Console.WriteLine();
        PrintBottomBody(value);
        Console.WriteLine();
    }
然后逐个实现每个方法,为了确保这个思路是正确的,先不用考虑所有的数字,只考虑数字1这个情况
    private void PrintTopBody(int value)
    {
        if (value != 0)
        {
            PrintTopBody(value / 10);
            int num = value % 10;
            switch (num)
            {
                case 1:
                    Console.Write(S0);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }
    private void PrintMiddleBody(int value)
    {
        if (value != 0)
        {
            PrintMiddleBody(value / 10);
            int num = value % 10;
            switch (num)
            {
                case 1:
                    Console.Write(S2);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }
    private void PrintBottomBody(int value)
    {
        if (value != 0)
        {
            PrintBottomBody(value / 10);
            int num = value % 10;
            switch (num)
            {
                case 1:
                    Console.Write(S2);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }
然后以1,11,1111作为输入进行测试,发现结果是正确的。 接下来就可以完善这三个方法了,经过一段时间的编码和测试,我完成了这道题目。其功能代码如下
    class LCDPriter
    {
        private static readonly string S0 = "    ";
        private static readonly string S1 = " __ ";
        private static readonly string S2 = "   |";
        private static readonly string S3 = "|   ";
        private static readonly string S4 = " __|";
        private static readonly string S5 = "|__ ";
        private static readonly string S6 = "|__|";
        private static readonly string S7 = "|  |";
        public void PrintNum(int value)
        {
            PrintTopBody(value);
            Console.WriteLine();
            PrintMiddleBody(value);
            Console.WriteLine();
            PrintBottomBody(value);
            Console.WriteLine();
        }
        private void PrintTopBody(int value)
        {
            if (value != 0)
            {
                PrintTopBody(value / 10);
                int num = value % 10;
                switch (num)
                {
                    case 0:
                        Console.Write(S1);
                        break;
                    case 1:
                        Console.Write(S0);
                        break;
                    case 2:
                        Console.Write(S1);
                        break;
                    case 3:
                        Console.Write(S1);
                        break;
                    case 4:
                        Console.Write(S0);
                        break;
                    case 5:
                        Console.Write(S1);
                        break;
                    case 6:
                        Console.Write(S1);
                        break;
                    case 7:
                        Console.Write(S1);
                        break;
                    case 8:
                        Console.Write(S1);
                        break;
                    case 9:
                        Console.Write(S1);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
        private void PrintMiddleBody(int value)
        {
            if (value != 0)
            {
                PrintMiddleBody(value / 10);
                int num = value % 10;
                switch (num)
                {
                    case 0:
                        Console.Write(S7);
                        break;
                    case 1:
                        Console.Write(S2);
                        break;
                    case 2:
                        Console.Write(S4);
                        break;
                    case 3:
                        Console.Write(S4);
                        break;
                    case 4:
                        Console.Write(S6);
                        break;
                    case 5:
                        Console.Write(S5);
                        break;
                    case 6:
                        Console.Write(S5);
                        break;
                    case 7:
                        Console.Write(S2);
                        break;
                    case 8:
                        Console.Write(S6);
                        break;
                    case 9:
                        Console.Write(S6);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
        private void PrintBottomBody(int value)
        {
            if (value != 0)
            {
                PrintBottomBody(value / 10);
                int num = value % 10;
                switch (num)
                {
                    case 0:
                        Console.Write(S6);
                        break;
                    case 1:
                        Console.Write(S2);
                        break;
                    case 2:
                        Console.Write(S5);
                        break;
                    case 3:
                        Console.Write(S4);
                        break;
                    case 4:
                        Console.Write(S2);
                        break;
                    case 5:
                        Console.Write(S4);
                        break;
                    case 6:
                        Console.Write(S6);
                        break;
                    case 7:
                        Console.Write(S2);
                        break;
                    case 8:
                        Console.Write(S6);
                        break;
                    case 9:
                        Console.Write(S4);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
    }
虽然这段代码可以工作,但是怎么看怎么别扭,不知所以然的S0到S7这8个全局字符串(本身就不好命名),逻辑极其类似的PrintTopBody,PrintMiddleBody,PrintBottomBody这三个方法,使得逻辑很简单的一个程序却很难看懂。

 

下篇:一个编程小题目引发的思考(下)

posted @ 2010-11-25 15:55 _Luc_ 阅读(1499) 评论(6) 编辑