拆分自然数:纯while实现 (Part 1 - 思路)

关于Jeff的《编程小练习:拆分自然数》问题,在此我提供一种思路,希望大家理解这种思路后能够自己把答案写出来。

我先问大家一个问题,写一个函数按顺序输出所有的4位二进制数(即:0000, 0001, 0010, 0011, ..., 1110, 1111),你会怎么写? 如果把固定4位变成任意n位,你又会怎么写?我知道那些会写高精度加法的人会跳出来说,就做一个高精度的二进制加法,Array的每一位保存二进制数的每一位,每次在末尾加1,然后让高精度加法自己处理进位,最后打印结果。老实说,我也做过这种蠢事,直到我在高中的一次竞赛中看到了一种比较巧妙的做法:(我最近一年都是写JavaScript,所以我在这里就用JavaScript写了)

function  main(n) {
    
var  array  =   new  Array(n);
    
var  i  =   0 ;
    
    
var  write  =   function () {
        console.log(array.join(
'' ));
    };
    
    
var  scan  =   function () {
        
return  array[i]  ==   1 ;
    };
    
    
var  step  =   function () {
        array[i] 
=   1 ;
        i
++ ;
    };
    
    
var  fill  =   function () {
        
while  (i  <  n) {
            array[i] 
=   0 ;
            i
++ ;
        }
        i
-- ;
    };
    
    fill();
    write();
    
while  ( true ) {
        
while  (i  >= 0   &&  scan()) {
            i
-- ;
        }
        
if  (i  <   0 ) {
            
break ;
        }
        step();
        fill();
        write();
    }
}

如果你一眼就能看明白这是怎么做的,我相信你能够立即理解为什么我跟Jeff说两重while能够解决问题,因为本质上是相同的问题。(如果你不熟悉JavaScript的闭包概念的话,你可以认为array和i是全局变量,在write、scan、step、fill以及main的主逻辑中用到的array和i都是指同一个东西。)

道理其实很简单,根本不需要考虑使用加法,只需要用while从低位开始遍历,找到第一个为0的位(scan),把它置为1(step),然后再回过头来把比它低的位都由1置为0就可以了(fill)。例如1001110011,它从低位数起的第一个值为0的位是百位(如果用十进制数的叫法的话),把它置为1就成了1001110111,然后再把比它低的位都置为0,那就成了1001110100

简单吧?那么我们再来考虑一个略作修改的问题。写一个函数,按顺序输出所有含有2个1的4位二进制数(即:0011, 0101, 0110, 1001, 1010, 1100),你会怎么写?如果把固定的2和4这两个条件换成任意的m和n,你又会怎么写?

function  main(m, n) {
    
var  array  =   new  Array(n);
    
var  i  =   0 ;
    
    
var  write  =   function () {
        console.log(array.join(
'' ));
    };
    
    
var  scan  =   function () {
        
if  (scan.start) {
            scan.start 
=   false ;
            scan.sum 
=   0 ;
        }
        scan.sum 
+=  array[i];
        
return  (array[i]  ==   1   ||  scan.sum  ==   0 );
    };
    
    
var  step  =   function () {
        array[i] 
=   1 ;
        fill.sum 
=  scan.sum  -   1 ;
        i
++ ;
    };
    
    
var  fill  =   function () {
        
while  (i  <  n  -  fill.sum) {
            array[i] 
=   0 ;
            i
++ ;
        }
        
while  (i  <  n) {
            array[i] 
=   1 ;
            i
++ ;
        }
        i
-- ;
    };
    
    fill.sum 
=  m;
    fill();
    write();
    
while  ( true ) {
        scan.start 
=   true ;
        
while  (i  >= 0   &&  scan()) {
            i
-- ;
        }
        
if  (i  <   0 ) {
            
break ;
        }
        step();
        fill();
        write();
    }
}

还是类似的写法,用while从低位开始遍历,找到第一个为1的位,再找它之后第一个为0的位(scan),把它置为1(step),然后再回过头来填充比它低的位(fill)。这次填充逻辑要复杂一些,它需要知道刚才scan跳过了多少个为1的位,把这些1都填充到较低位,而较高位用0填充。

大家应该能够注意到,第二道题的main代码主题逻辑是和第一道题的几乎一致的,改变的只是scan、step、fill。main代码的主逻辑不同之处在于,scan多了一个start的状态,用于每一轮scan开始的时候初始化sum。此外,scan与fill之间多了一个sum的共享状态。实际上,我们可以对第一道题的scan、step、fill略作修改,容忍掉无用的start与sum状态,使得它们能够运行在第二道题的main代码主逻辑中。

这是一种什么模式?策略模式。main代码主逻辑按照固定的方式进行scan、step、fill。对于类似的题目,你只要写出正确的scan、step、fill就可以了,main代码的主逻辑完全不需要动。我所出的第二道题,其实是对Jeff题目特化。特化的地方在于,Jeff的题目要求能够接受sum, n, min, max,而我的题目把sum等同为m,把min特化为0,把max特化为1。此外,Jeff要求排除重复的组合,换句话说,就是得到的序列要求是不下降序列,但如果我的题目也如此限制为不下降序列的话,解就永远只有一个了(例如,m=2且n=4时,不下降序列就只有0011),因此我把这个限制条件去掉了。

总的来说,Jeff的题目与我所说的这两道题本质上是相同的,你想要解答Jeff的题目,只需要在我给出的main代码主逻辑基础上写出正确的scan、step、fill就可以了。具体的解答我稍后给出,大家可以先自己尝试写一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值