问题描述:
实现一个函数,对一个正整数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;
}
嗯,当然,本人很懒,仍然没有运行过上面这段代码……
(*此段代码优先级运算符有问题!)