我看到这个问题,然后冒出了这个想法。
我看到这个问题,然后冒出了这个想法。
对于有限大小的整数(例如32位整数),存在一种常数时间(相当快)的方法。
注意,对于一个3的幂次方整数N
,以下结论是成立的:
N
的3的幂次方M
,M
都能整除N
。N
但不是3的幂次方的整数M
,M
不能整除N
。适合32位的最大3的幂次方为3486784401
(3^20
),下面是相应的代码:
bool isPower3(std::uint32_t value) {
return value != 0 && 3486784401u % value == 0;
}
同样地,对于有符号的32位整数,它是1162261467
(3^19
):
bool isPower3(std::int32_t value) {
return value > 0 && 1162261467 % value == 0;
}
通常的魔数为:
== pow(3, floor(log(MAX) / log(3)))
注意浮点精度误差问题,使用像Wolfram Alpha这样的数学计算器来计算常数。例如对于2^63-1
(有符号int64),C++和Java都会给出4052555153018976256
,但正确的值是4052555153018976267
。
std::pow(3.L, std::floor(std::log((1ull<<63)-1.L)/std::log(3.L)))
得到了正确的值。(请注意这里的long double
字面量,它们起到了关键作用。) - Ruslanwhile (n % 3 == 0) {
n /= 3;
}
return n == 1;
请注意,1是三的零次幂。
编辑:在循环之前还需要检查零,因为对于 n = 0,循环不会终止(感谢 Bruno Rothgiesser)。
我稍微有点认为,如果你所说的“整数”指的是“带符号的32位整数”,那么(伪代码)
return (n == 1)
or (n == 3)
or (n == 9)
...
or (n == 1162261467)
这个问题有一定的美丽简洁性(最后一个数字是3^19,因此情况并不多)。即使对于一个64位无符号整数,仍然只有41种情况(感谢@Alexandru指出我的疏忽)。当然,对于任意精度算法来说是不可能的...
我对此感到惊讶。似乎所有人都错过了最快的算法。
下面这个算法平均来说比简单的while(n%3==0) n/=3;
循环更快,在某些情况下速度会显著提高:
bool IsPowerOfThree(uint n)
{
// Optimizing lines to handle the most common cases extremely quickly
if(n%3 != 0) return n==1;
if(n%9 != 0) return n==3;
// General algorithm - works for any uint
uint r;
n = Math.DivRem(n, 59049, out r); if(n!=0 && r!=0) return false;
n = Math.DivRem(n+r, 243, out r); if(n!=0 && r!=0) return false;
n = Math.DivRem(n+r, 27, out r); if(n!=0 && r!=0) return false;
n += r;
return n==1 || n==3 || n==9;
}
代码中的数字常量为3^10,3^5和3^3。
性能计算
在现代CPU中,DivRem
通常是一个单周期指令。在其他CPU上,它会扩展成除法、乘法和加法操作,需要大约三个时钟周期。该算法的每个步骤看起来很长,但实际上只包括:DivRem, cmp, cmove, cmp, cand, cjmp, add
。可以利用大量的并行性,在典型的双向超标量处理器上,每个步骤的执行时间大约为4个时钟周期,因此最坏情况下的执行时间保证不超过25个时钟周期。
如果输入值均匀分布在UInt32
的范围内,则该算法涉及以下概率:
该算法优于简单的while(n%3==0) n/=3
循环,后者的概率如下:
更重要的是,该算法能够高效地处理中等大小和大型的三的幂(以及其倍数):在最坏情况下,简单算法将消耗超过100个CPU周期,因为它需要循环20次(64位情况下为41次)。我提出的算法永远不会超过25个时钟周期。
扩展到64位
将上述算法扩展到64位非常简单-只需要再添加一个步骤。以下是针对没有高效64位除法的处理器进行优化的上述算法的64位版本:
bool IsPowerOfThree(ulong nL)
{
// General algorithm only
ulong rL;
nL = Math.DivRem(nL, 3486784401, out rL); if(nL!=0 && rL!=0) return false;
nL = Math.DivRem(nL+rL, 59049, out rL); if(nL!=0 && rL!=0) return false;
uint n = (uint)nL + (uint)rL;
n = Math.DivRem(n, 243, out r); if(n!=0 && r!=0) return false;
n = Math.DivRem(n+r, 27, out r); if(n!=0 && r!=0) return false;
n += r;
return n==1 || n==3 || n==9;
}
新的常量是3^20。方法的优化行被省略掉了,因为根据我们的假设64位除法很慢,它们实际上会减慢速度。
这种技术为什么有效
假设我想知道"100000000000000000"是否是10的幂。我可以按照以下步骤进行:
bool isPower3(uint32_t v) { return v != 0 && 3486784401u % v == 0; }
。吹嘘每个人似乎都错过了最快的算法通常是注定要失败的。 - chqrlie时间复杂度为O(log(n)),空间复杂度为O(1)
public boolean isPowerOfThree(int n) {
if (n < 1) {
return false;
}
while (n % 3 == 0) {
n /= 3;
}
return n == 1;
}
将整数转换为三进制数,并检查它是否以前导1后跟所有0的形式书写。这灵感来自于检查一个数是否是2的幂的解决方案,方法是执行n & (n - 1) == 0
时间复杂度:根据语言和编译器不同,为O(log(n)),空间复杂度:O(log(n))
public boolean isPowerOfThree(int n) {
return Integer.toString(n, 3).matches("^10*$");
}
如果n = 3^i
,则i = log(n) / log(3)
,因此可得出解决方案。
时间复杂度:取决于语言和编译器,空间复杂度:O(1)。
public boolean isPowerOfThree(int n) {
return (Math.log(n) / Math.log(3) + epsilon) % 1 <= 2 * epsilon;
}
因为3^19 = 1162261467
是32位整数中最大的3的幂,因此我们可以进行以下操作:
时间复杂度:O(1),空间复杂度:O(1)
public boolean isPowerOfThree(int n) {
return n > 0 && 1162261467 % n == 0;
}
这个方法与#4类似,使用一个set来存储所有可能的3的幂次数(从3^0到3^19)。这样可以使代码更易读。
这个解法特定于C++11,使用模板元编程,这样编译器将用计算出的结果替换调用isPowerOf3<Your Input>::cValue
的语句。
时间复杂度:O(1),空间复杂度:O(1)
template<int N>
struct isPowerOf3 {
static const bool cValue = (N % 3 == 0) && isPowerOf3<N / 3>::cValue;
};
template<>
struct isPowerOf3<0> {
static const bool cValue = false;
};
template<>
struct isPowerOf3<1> {
static const bool cValue = true;
};
int main() {
cout<<isPowerOf3<1162261467>::cValue;
return 0;
}
如果 (log n) / (log 3) 是整数,则 n 是 3 的幂。
log2(n) / log2(3)
通常会有三个舍入误差。第三,像log2
这样的数学库函数通常没有正确的舍入实现,因此它们还会有额外的舍入误差。 - Eric PostpischilNumber.isInteger(Math.log10(n) / Math.log10(3))
。 - Farzad Yousefzadeh非常有趣的问题,我喜欢来自starblue的答案,这是他算法的一个变体,能够更快地收敛到解决方案:
private bool IsPow3(int n)
{
if (n == 0) return false;
while (n % 9 == 0)
{
n /= 9;
}
return (n == 1 || n == 3);
}
递归地将数除以3,检查余数是否为零,并将结果再次应用于商。
请注意,当幂为零时,3的零次方等于1是一个需要注意的特殊情况,因此1也是一个有效答案。
numberOfLeadingZeros
作为哈希。 - maaartinustable = [1 for i in range(32)]
这样的简单初始化不需要思考。没有思考意味着没有犯错的机会。 ;) - maaartinus这是一个用C语言实现Ray Burns' method的快速且优秀的方法:
bool is_power_of_3(unsigned x) {
if (x > 0x0000ffff)
x *= 0xb0cd1d99; // multiplicative inverse of 59049
if (x > 0x000000ff)
x *= 0xd2b3183b; // multiplicative inverse of 243
return x <= 243 && ((x * 0x71c5) & 0x5145) == 0x5145;
}
在我的CPU(Intel Sandy Bridge)上,它非常快,但不如starblue方法使用二进制对数(该方法在该CPU上已实现硬件)。但在没有此类指令或不希望使用查找表时,它可能是一种替代方案。