問題是這樣的:有一個匹配文件,內容大概是這樣的:
3145pcl.gpd
3155pcl.gpd
3165pcl.gpd
31x5hc01.cnt
31x5hc01.hlp
31x5hs01.cnt
31x5hs01.hlp
31x5lc04.dll
...
還有一些需要處理的文件,要求按行處理,如果某行帶有匹配文件中任意行的內容(不區分大小寫),那麼就刪掉該行。
問題看似簡單,但是在文件行數十分巨大的情況下,傳統的做法性能相當差勁!
傳統的設計無非是2重循環:
第一種,外層循環待處理文件,內層循環匹配文件:
循環讀待處理文件的每一行 {
循環比較匹配文件的每一行 {
如果匹配,則退出循環
}
如果不匹配,則寫到輸出文件中
}
第二種,外層循環匹配文件,內層循環待處理文件:
循環匹配文件的每一行 {
比較待處理文件未打上標記的行 {
如果匹配,則給這一行打上標記
}
}
把未打上標記的行寫到輸出文件中
(其實還有一種:把待處理文件不分行,看作整體用正則表達式替換,不過性能還要低下!)
上述兩種算法雖然作了一些小的優化,但是基本上覆雜度是和匹配文件行數*待處理文件行數成正比的,在此例中匹配文件有3782行,需要處理的文件加起來總共40187行,乘積達到了上億的數量級。
寫了好幾個版本,在正則表達式等細微的地方做了很多嘗試,但是時間始終在50~80秒之間徘徊,同樣的工作,Unix下面的grep工具竟然能夠瞬間完成,性能差距有些不可思議。看起來已經沒有優化的餘地了(我曾經想過類似於MapReduce的多線程操作,可惜Lua不支持...)。
後來在一次偶然的測試中,發現原來表的循環操作是罪魁禍首:我註釋了最內層的匹配過程,時間僅下降幾十秒,但是如果註釋了內層循環,那麼基本上是一瞬間就完成了。
後來在網上看了相關資料(例如:http://trac.caspring.org/wiki/LuaPerformance),在Lua中,要儘量避免使用pairs和ipairs的循環操作,非常費時的,另外《Programming in Lua》一書的11.5節講到了如何構建Set的技巧,非常有用,OK,就此重新設計算法:
首先構造匹配文件對應的Set,字符串已經全部轉換爲小寫:
-- 匹配字符串序列
matchstrs = {};
-- 匹配字符串最大長度
maxmatchstrlen = 0;
for line in io.lines("matchstr.txt") do
matchstrs[line:lower()] = true;
local len = line:len();
if len > maxmatchstrlen then
maxmatchstrlen = len;
end;
end;
使用此Set非常簡單:“matchstrs[字符串]”直接返回其中是否包含該字符串,使用此Set構造匹配函數:
-- 函數:判斷字符串是否包含匹配字符串序列之一,不區分大小寫,返回true或false
function match(str)
-- 字符串轉換爲小寫
local lowerstr = str:lower();
-- 獲得字符串長度
local lowerstrlen = lowerstr:len();
-- 子串的頭位置
local head;
-- 子串的尾位置
local tail;
for head = 1, lowerstrlen do
for tail = head, math.min(head + maxmatchstrlen, lowerstrlen) do
if matchstrs[lowerstr:sub(head, tail)] then
return true;
end;
end;
end;
return false;
end;
其中雖然也用到了二重循環,但是其性能和表循環根本不是一個數量級的。
算法經過優化後,實測時間4秒!4秒,什麼概念?Oh Yeah的概念,哈哈!
下面是完整代碼: