实现一个函数,对一个正整数n,算得到1需要的最少操作次数?

 问题描述:

实现一个函数,对一个正整数n,算得到1需要的最少操作次数:如果n为偶数,将其除以2;如果n为奇数,可以加1或减1;一直处理下去。
例子:
ret=func(7);
ret=4,可以证明最少需要4次运算
n=7
n--6
n/2 3
n/2 2
n++1
要求:
实现函数(实现尽可能高效)int func(unsign int n);n为输入,返回最小的运算次数。
给出思路(文字描述),完成代码,并分析你算法的时间复杂度。
请列举测试方法和思路。

=================================================================================

第一部分:

 

先考虑2的幂的情况。显然,2^n至少需要n次右移操作。

任何整数,最终都可归结为一系列2的幂相加;如1100B = 0 × 2^0 + 0 × 2^1 + 1 × 2^2 + 1 × 2^3

如果仅允许右移和自减(减一)操作,那么显然1100B至少需要右移3次、减1一次,共4次操作。
即:使用右移消低位0,使用减一消低位1,共需二进数字长度减一次移位,还需要除了最前面的1外,数字中一共含有的1的个数次自减操作。

这个可以叫做判断规则1。

考虑自增(加一)操作:现在,游戏变成了如何用最少的步骤消除二进制数字中的连续几个1。

例如,1100B可以在右移2次后,通过加一得到100,再右移2次得到1,共需5次操作。

这会比右移二次后自减多一次操作,原因是加一导致移位次数增加一次。

但,对11100B或1001100B来说,右移两次后加一、然后继续移位和单纯的右移+自减方式需要的操作次数一样多。

所以,有:
1、对于数字低位的连续m个1,使用加一可把它们变成高一位的一个1,因此总体上只需两次操作即可消除(单纯的减一、移位再减一则在总体上需要1的个数次操作)
   进而,有:
      1.1 只要m大于2,使用自增可以得到正的收益。
      1.2 m等于2时收支相抵,使用自增或自减均可。
      1.3 m小于2只能用自减
2、对于数字首位的连续m个1,使用加一同样可以把它们变成高一位的一个1,但相比低位的情况将导致额外增加一次右移操作。
   所以,当m>3时才有正的收益;m=3收支相抵;m<3只能用自减。
3、只要m达到非负收益范围,那么不管这个连续1的序列有多长,都可以用两次额外操作消除(自增,然后在这个序列之前一位上应用自减)

比如,对7,有:

7写成二进制,是 111B;因为111在首位,因此自增或自减均可。

自增,则成为1000B;3次右移后就等于1,共需4次操作。
自减,成为110B,右移,得11B,再自减、右移,得1,同样需要4次操作。

用上面的判定规则,就是:
1、111B是从开头就有连续3个1,属于非负收益范围,因此需要额外2次操作。
2、111B一共3个二进制位,因此需要3-1=2次移位操作。

所以,111B至少需要4次操作。

算法:


从最高位开始扫描二进制数字。
若最高位开始有连续两个1,记需要一次额外操作;连续3个1或更多,都需要额外两次操作。
while(未扫描到最低位)
{
        在剩下的数字中,发现1就记需要一次额外操作;连续2个1或更多,记需要额外2次操作,直到遇到0或结束为止。
}
假设前面扫描中记录的额外操作次数记为x,二进制数字有效位数为y,则返回至少需要x+y-1次扫描。

当然,从低位扫描也可以,没有原则上的不同。

有了算法,我们可以玩个复杂点的:
424367,转换为二进制是:1100111100110101111B

    因为这个数字最前两位是11,需要额外一次操作消除第二个1。

    然后,从前往后数,它的第5到第8位是连续4个1,需要两次操作才能消除;11到12位两次;14位需一次操作;最后4个1需要两次操作。

    于是,为了消除首位后面的这些1,共需8次操作;这个数字共19个有效二进制位,需要19-1=18次移位操作。

    答案是:至少需要26次操作。

    这个算法只需1次扫描,无任何实际运算。复杂度为O(ln(N))

第二部分:

 

int f(int64 n)
{
    assert(n>=1);

    int res=0; //记录结果

    while (n!=1)
    {
         if (n&1 == 0) //末位为0直接右移
         {
              n/=2;
              res++; //一次操作
         }
         else if (n&7 == 7)
         {
              //末3位为1加一(因为序列内部的11收支平衡;序列头部的111收支平衡;更长的序列正收益更大)
              n++;
              //直接消尾部3个0
              n/=8;
              res+=4; //一次自增3次右移
         }
         else
         {
              //末3位不全为1则减一(序列内部的11收支平衡,加一减一均可)
              n--;
              n/=2;
              res+=2; //一次自减一次右移
          }
    }
    return res;
}


对于int64所能表示的最大整数(如果次高位加1不溢出的话),此算法最多需要63次循环;算法复杂度最大O(ln(N))。

关于收益方面的分析证明见我在第四页的回帖;这个实现是尾部向前搜索且做了实际计算的,和分析不太一样;分析中至少需要扫描全部位,这个实现遇见3个以上的连续1可以直接处理3位。


更“优化”的版本可以这样写:
if (n&7 == 7)
{
    if (n&15 == 15)
    {
         if (n&31==31)
         {
             if(n&63=63)
             ......
         }
             n++;
             n>>4;
             res+=5;
             continue;
    }
    n++;
    n>>3;
    res+=4;
    continue;
}

不过,这个做法虽然确实可以减少循环次数,但并不一定能实际减少所需的指令数以及执行时间,也不能降低算法复杂度。

第三部分:

 

前面确实有一点没有考虑到: 59 = 111011B,加一后变成111100B,这可以抵消头部111序列的一次加1。

所以,之前的分析中应该增加这样一条: 非序列首部的11加一减一收支平衡,但应该优先加一,因为有可能抵消更前面连续1序列的一次加一操作。

或者,这样考虑:凡....11011...这样的序列,需要额外3次操作(而不是原来估计的四次);而00101100这样的序列收支平衡(亦即:和前面的分析一样,需要3次操作)。
其中,上面序列中的.代表0或1。


这样的话,应加一个记录,识别首部为11这种情况——此时要为计算次数减1:
int f(int64 n)
{
    assert(n>=1);

    int res=0; //记录结果
   int isFirstSeqLengthLess3=0; //首部是11吗?

   while (n!=1)
    {
         isFirstSeqLengthLess3=0; //首部不是11或还未找到首部

         if (n&1 == 0) //末位为0直接右移
         {
              n/=2;
              res++; //一次操作
         }
         else if (n&3 == 3)
         {
              //末2位为1加一(因为序列内部的11收支平衡,但加一可能抵消下一序列的加一操作;更长的序列收益更大)
         n++;
              //直接消尾部2个0
              n/=4;
              res+=3; //一次自增2次右移
         isFirstSeqLengthLess3=1; //若此次执行过后while退出,则首部肯定是11序列
      }
         else
         {
              //末2位为01则减一
         n--;
              n/=4;
              res+=3; //一次自减2次右移
      }
    }
    res-=isFirstSeqLengthLess3;
    return res;
}


    当然,仍然没有编译运行过……
    另,这和你的思路其实是一样的:1个1只能自减;2个1可加可减,但若前面是10,则加至少可以收支平衡(是110则有盈余)。
我是因为一直不甘心O(ln(n))的效率(且有之前的分析引导),代码过早优化了;在你的代码的//1处加上两次右移,则我们的代码等价。

int get2(int n)
{
        int ret=0;
        int t;
        while(n!=1) {
                if(n&1) { //若末位为1
                        if((n&3)!=3 || (n&15)==3) { //若末2位是01或0011则自减消末位1
                                n--;
                        } else { //否则(末位是1011或1111)自加消末位1
                                n++;
                                //1 这里可以直接n/=4消掉末2个0,少两次循环;当然,res要+2了
                        }
                } else {  //末位为0则右移
                        n/=2;
                }
                ret++;
        }
        return ret;
}

嗯,当然,本人很懒,仍然没有运行过上面这段代码……

(*此段代码优先级运算符有问题!)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值