写这篇博客就是想向自己强调,时刻记住线性数据结构也是经常被加速处理的,这样时间复杂度可以从 O(N) 变成 O(logN)。
lua 字符串复制函数 string.rep
在指定字符串 s
后,可以将它复制指定数量。使用 c 语言时,最经常用的就是 memset
函数,用来初始化一块内存。不管是字符串复制,还是初始化一块内存,我的想法都是一个 for 循环遍历然后赋值就完事了吧。直到今天看到了 Programming in Lua 4th 书中习题 16.2 。这道题目就是不用 for 循环,让你写一段代码,可以产生字符串复制功能的 lua 代码,然后 lua 解释器执行这段代码实现字符串复制。当时习题给了正常情形下字符串复制的实现。代码如下所以。
function stringrep(s, n)
local r = ""
if n > 0 then
while n > 1 do
if n % 2 ~= 0 then r = r .. s end
s = s .. s
n = math.floor(n / 2)
end
r = r .. s
end
return r
end
先不要考虑字符串连接操作符 ..
的效率问题。复制单个字符,然后使用 #
操作符求字符串中字符的个数,即可验证功能是否正确。这段代码的时间复杂度不是 O(N) 而是 O(logN) 。不愧是一帮数学很厉害的大佬写的代码。这段代码用 2 的指数的和来组成整数 n
。指数最大的值就是以 2 为底 n 的对数的整数部分,比如以 2 为底时 9 的对数的整数部分是 3 而 8 的对数刚好是 3 。假设以 2 为底函数中 n
的对数的整数部分值是 k
,于是 n
被用如下各值的和表示 2^k + [2^i0 + 2^i1 + 2^i2 + ...]
。方括号仅仅表示可选部分,而 i0 i1 ... 等等这些值从 0 开始,每次循环 +1 但不必连续,需要时才会被累加到结果中。下面来举例说明一下。
7 可以被拆分成 2^2 + 2^0 + 2^1 而 12 可以被拆分成 2^3 + 2^2 而 31 可以被拆分成 2^4 + 2^0 +2^1 + 2^2 + 2^3 。比如以 2 为底 31 的对数整数部分是 4 这样还剩下 15 但代码 if n % 2 ~= 0 then r = r .. s end
刚好可以正确的处理余下的非 2 的幂部分,于是 15 被正确的处理了。真的很巧妙。最重要的是时间复杂度从线性曲线降低到了对数曲线。
分析完字符串复制的加速部分后,继续看一下习题,习题让产生序列来完成字符串复制。如下例子。将字符串复制 5 次生成的函数应该下面的样子。
function stringrep_5 (s)
local r = ""
r = r .. s
s = s .. s
s = s .. s
r = r .. s
return r
end
如何生成这段代码呢。其实很简单。我们依旧使用 O(logN) 时间复杂度生成。先生成函数 stringrep_sequence
的代码,然后 load
并执行,便可进行字符串的复制。
function generate_stringrep(n)
local content = {}
if n > 0 then
table.insert(content, 1, "function stringrep_sequence(s)")
table.insert(content, "\tlocal r = \"\"")
while n > 1 do
if n % 2 ~= 0 then
table.insert(content, "\tr = r .. s")
end
table.insert(content, "\ts = s .. s")
n = math.floor(n / 2)
end
table.insert(content, "\tr = r .. s")
table.insert(content, "\treturn r")
table.insert(content, "end")
end
local code = table.concat(content, "\n")
return code
end
下面运行来比较一下结果。
local n, startc, endc = 999900000
startc = os.clock()
local str = stringrep("s", n)
endc = os.clock()
print(#str, "use sec:", endc - startc)
local code = generate_stringrep(n)
pcall(assert(load(code)))
startc = os.clock()
local str1 = stringrep_sequence("j")
endc = os.clock()
print(#str1, "use sec:", endc - startc)
几次运行结果如下。不要关注具体的时间,比较两种方式的耗时即可。后面两次执行猜测是缓存造成的速度加快了吧。很明显后一种方式会快些,是因为不需要分支判断吧。
E:\workplace>lua tmp.lua
999900000 use sec: 2.505
999900000 use sec: 1.325
E:\workplace>lua tmp.lua
999900000 use sec: 1.197
999900000 use sec: 1.098
E:\workplace>lua tmp.lua
999900000 use sec: 1.18
999900000 use sec: 1.05