在浏览vue2.x的源码过程中,无意中在codeframe.js文件中看到一个repeat函数,其作用就是能够将一个字符串重复指定的次数并返回,以往看到类似的代码大多都是瞄一眼写法,然后就直接略过,然而这次我不打算轻易放过,尝试对其写法做出了逆向解释和扩展,因而有了此文。
原始代码如下:
function repeat (str, n) {
let result = ''
if (n > 0) {
while (true) {
if (n & 1) {
result += str
}
n >>>= 1
if (n <= 0) break
str += str
}
}
return result
}
运行一下
console.log(repeat('abc',3)) // abcabcabc
这个函数的作用很简单,而且能够用更容易理解的方式写出来:
function repeat1 (str, n) {
let result = ''
if (n > 0) {
for(let i=0;i<n;i++){
result += str
}
return result
}
那么,作者为什么选择那么写呢?因为那样更快,时间复杂度仅为O(logn),而现在的这种写法为O(n)。在n大到一定程度时,它们会有明显的执行时间差。
下面,我开始尝试解释源码的编码逻辑,是否存在某种理论支撑,而不是经验的归纳。
首先,我们的目的是求解一个字符串重复n的结果,例如:'a',重复11次,得到'aaaaaaaaaaa'(11个a);
然后,转换一下问题描述,那就是:如何得到11个a?
- 'a'+'a'+...+'a',11个a相加得到,这就是上面repeat1的实现逻辑,遍历n累加'a'
- 'aaaaa'+'aaaaa'+'a' =('aa'+'aa'+'a')+('aa'+'aa'+'a')+a=(('a'+'a')+('a'+'a')+'a')+(('a'+'a')+('a'+'a')+'a')+'a',分而治之
- 'a'+'aa'+'aaaaaaaa',这是源码的实现逻辑
通过阅读源码,我们可以得出两条关键的逻辑:
1、用n对2取余(余数不是1就是0),为1时就把此次的str加到结果中
if (n & 1) { // 相当于对2取余
result += str
}
2、n除2取其商作为新的n,判断n是否大于0,大于0则str字符串自我重复一次
n >>>= 1 // 对2取商
if (n <= 0) break
str += str // 自加
通过上面的分析,我联想到了二进制数转十进制的方法:
比如:11的二进制表示是1011,二进制再转换成十进制则为:
1*(2^0) + 1*(2^1) + 0*(2^2) + 1*(2^3)
那么,回到我们最初的问题:如何从一个'a'变成n个'a'。
此时,我们对于这个问题的分析就可以等价于分析如何从一个1变成“n个1”。因为我们可以把'a'+'a'的操作,看成是1+1的操作。例如,我们可以把代码修改一下:
function repeatNum (n) {
let result = 0
let num = 1
if (n > 0) {
while (true) {
if (n & 1) {
result += num
}
n >>>= 1
if (n <= 0) break
num += num
}
}
return result
}
运行一下:
console.log(repeatNum(3)) //3
console.log(repeatNum(11)) //11
我们知道,任意一个正整数都可以用2的幂的多项式表示,也就是二进制数表示十进制数的方法,如前文所述:
11 = 1*(2^0) + 1*(2^1) + 0*(2^2) + 1*(2^3) ,其中括号前的常数就是11转成二进制1011,从低到高位的二进制数,所以代码中才有n & 1为1时才会做求和操作(n & 1相当于n对2取余,而这个余数正好是二进制数)
进一步分析:
11 = 1 + // 首次进入循环n=11,result += 1
(1 + 1) + // n=5, num += num,num = 2,result+=2
0*((1+1)+(1+1)) + // n=2,num+=num,num = 4
((1+1+1+1)+(1+1+1+1)) // n=1,num+=num,num=8,result+=8
同理,字符串'a'重复n次也遵循相同的算法逻辑,而代码的时间复杂度也取决于n能够被2除几次,所以,时间复杂度为O(logn)。
总结:此次分析感觉并不流畅,不能算是特别有效的推演,但应该也能帮助到一些人对该源码的进一步理解,同时如果有更好的推演,也希望各位大佬不吝赐教^_^。
此外,针对源码的功能,附上一份运用分治思想的代码:
// n为奇数时,可表示为两相同偶数加基数(这里相当于str);n为偶数时,可拆分为两个相同奇数相加,继续拆分直到不能拆分为止
function myRepeat(str,n){
if(!n || n <= 0){
return ''
}
if(n === 1){
return str
}
let temp = myRepeat(str,n/2)
if(n & 1){
return str+temp+temp
}else{
return temp + temp
}
}