什么是位循环?
位循环是指两个数的二进制数位相同且循环移位之后可以完全重合。比如数字 89 和数字 98 的二进制表示都是 1011001,因此它们是彼此的位循环。
Javascript 实现检查位循环
暴力枚举法
判断两个数是否为位循环,最初的想法可能是枚举其中一个数的所有循环移位,然后与另外一个数进行比较。
var isRotation = function(num1, num2) {
if (num1 < 0 || num2 < 0) {
return false;
}
var len1 = getNumberLength(num1),
len2 = getNumberLength(num2);
if (len1 !== len2) {
return false;
}
var str1 = num1.toString();
var str2 = num2.toString();
for (let i = 0; i < len1; i++) {
str1 = str1.substring(1) + str1[0];
if (str1 === str2) {
return true;
}
}
return false;
}
var getNumberLength = function(num) {
return num.toString().length;
}
该算法的时间复杂度是 O(n^2), 因为需要枚举其中一个数字的所有循环移位。该算法不够高效,因为它需要枚举很多次。
位运算法
我们可以使用位运算的方法优化暴力枚举法。具体地,我们可以枚举第一个数字的所有循环移位,而不是枚举所有的位数。
var isRotation = function(num1, num2) {
if (num1 < 0 || num2 < 0) {
return false;
}
var len1 = getNumberLength(num1),
len2 = getNumberLength(num2);
if (len1 !== len2) {
return false;
}
var temp = num2;
for (let i = 0; i < len1; i++) {
if (temp === num1) {
return true;
}
temp = ((temp << 1) | (temp >> (len2 - 1))) & ((1 << len2) - 1);
}
return false;
}
var getNumberLength = function(num) {
return num.toString().length;
}
用位运算法来判断数字是否是彼此的位旋转,不仅时间复杂度低,且高效优化,时间复杂度是 O(n).
性能对比
我们利用下面的代码计算这两个算法的在不同位数下的运算时间。
function runBenchmark() {
for (let i = 1; i < 17; i++) {
const x = Math.pow(10, i);
const y = Math.floor(Math.random() * x) + 1;
const z = Math.floor(Math.random() * x) + 1;
const totalTime1 = benchmark(isRotation1, x, y, z);
const totalTime2 = benchmark(isRotation2, x, y, z);
console.log(`${'\n'}X: ${x}. Y: ${y}. Z: ${z}`);
console.log(`Total time isRotation 1: ${totalTime1} seconds.`);
console.log(`Total time isRotation 2: ${totalTime2} seconds.`);
}
}
function benchmark(fn, x, y, z) {
const start = performance.now();
fn(y, z);
const end = performance.now();
return ((end - start) / 1000).toFixed(4);
}
值得注意的是,位运算法的时间复杂度只与数字长度有关,而暴力枚举法则只改变了常数项。
我们可以看到,随着数字长度的增加,位运算法的运行时间维持在较低的水平,而暴力方法的运行时间则急剧增长。
总结
我们学会了如何使用位运算的方法来判断数字是否是彼此的位旋转。该方法可以优化时间复杂度。具体而言,用位运算法优化后,每个位移操作需要 O(1) 的时间复杂度,因此算法的总时间复杂度为 O(n),可以很好地处理大数值。