談談lua中的table.remove()以及loop+table.remove()誤區

  前幾天在修復一個bug的時候發現代碼中使用了泛型for+ipairs()+table.remove()刪除元素,毫無疑問,這是一種錯誤的作法,但由於歷史配置內容緣由,致使這個BUG在以前一直沒表現出來。lua中,在for循環中調用函數ipairs時,ipairs會返回3個值,迭代函數、不可變狀態表、初始控制變量0,for的每次調用,都會把狀態表和控制變量傳入迭代函數,調用迭代函數,把控制變量+1,再獲取狀態表中相應元素,並把二者返回,直至遇到nil結束;而函數table.remove除了刪除、返回指定序列上的元素,還會把後面的元素往前移動。故而知,當表中相鄰的兩個元素都須要刪除的時候,就會發生刪除不乾淨的狀況。表是確定須要遍歷一遍的,如今問題就是,該以怎樣一種方式去遍歷表,達到正確刪除元素的目的。首先,若是是無序表,直接把對應位置上的元素置nil;若是是有序表,比較經常使用的方法有如下3種:算法

1 local a = {1, 1, 2, 3, 4, 4} 2 for i = #a, 1, -1 do
3     if a % 2 == 0 then
4         table.remove(a, i) 5     end
6 end
1.數值型for從後往前遍歷
 1 local a = {1, 1, 2, 3, 4, 4}  2 local index = 1
 3 local v = a[index]  4 while v do
 5     if v % 2 == 0 then
 6         table.remove(a, index)  7     else
 8         index = index + 1
 9     end
10 
11     v = a[index] 12 end
2.while控制下標方式
1 local a = {1, 1, 2, 3, 4, 4} 2 local tmp = {} 3 for k, v in ipairs(a) do
4     if v % 2 == 0 then
5         table.insert(tmp, v) 6     end
7 end
8 a = tmp
3.構建臨時表方式

這3種方式的並且確均可以實現對錶元素的刪除,但好與很差真的值得商榷。或許事情還能夠進一步思考和優化。回到table.remove()自己,上面說到,table.remove()刪除位置上的元素後還會把此位置後面的元素往前移,這裏涉及到了一個效率問題。若是是有序表,在某個時刻僅須要刪除一個位置上的元素且繼續保持有序,table.remove()是必然選擇,可是,若是須要遍歷表刪除,選擇table.remove()是否仍是正確的作法?答案確定是否認的,每一次remove,都移動了大量元素,而loop+table.remove(),明顯作了不少沒有意義的工做。那上面的方式3是否就成了最佳選擇,答案也是否認。由於表的構造也須要時間,雖然基本能夠忽略不計,但更重要的是,若是由於原表較大,觸發了新表的luaH_resize,即內存從新分配,這開銷就變的大了,在c中,內存申請這方面一直是性能短板。那應該以怎樣一種方式更好地實現上面loop+table.remove()?數組

  咱們知道,在lua中回收一個元素,直接把它置nil便可。因而,對於有序表,能夠得出如下實現:ide

 1 local a = {1, 1, 2, 3, 4, 4}  2 local index, r_index, length = 1, 1, #a  3 while index <= length do
 4     local v = a[index]  5     a[index] = nil
 6     if not (v % 2 == 0) then
 7         a[r_index] = v  8         r_index = r_index + 1
 9     end
10 
11     index = index + 1
12 end
Achieve1

封裝一下:函數

 1 local tb = {}  2 
 3 function tb.remove(obj, rm_func)  4     if type(obj) ~= "table" or type(rm_func) ~= "function" then
 5         return
 6     end
 7 
 8     local index, r_index, length = 1, 1, #obj  9     while index <= length do
10         local v = obj[index] 11         obj[index] = nil
12         if not rm_func(v) then
13             obj[r_index] = v 14             r_index = r_index + 1
15         end
16 
17         index = index + 1
18     end
19 end
20 
21 return tb
Achieve2

到這裏,咱們彷彿已經經過Achieve2實現了一種更好的方案來取替loop+table.remove(),下面附上測試代碼來看看運行時間對比:oop

 1 local tb = {}  2 
 3 function tb.remove(obj, rm_func)  4     if type(obj) ~= "table" or type(rm_func) ~= "function" then
 5         return
 6     end
 7 
 8     local index, r_index, length = 1, 1, #obj  9     while index <= length do
10         local v = obj[index] 11         obj[index] = nil
12         if not rm_func(v) then
13             obj[r_index] = v 14             r_index = r_index + 1
15         end
16 
17         index = index + 1
18     end
19 end
20 
21 local a = {} 22 local b = {} 23 local c = {} 24 local d = {} 25 local length = 1024*128
26 for i = 1, length do
27     a[i] = i 28     b[i] = i 29     c[i] = i 30     d[i] = i 31 end
32 
33 rm_func = function(value) 34     return value % 2 == 0
35 end
36 local start_time = os.clock() 37 
38 --test-(1.數值型for從後往前遍歷)
39 for i = #a, 1, -1 do
40     if rm_func(a[i]) then
41         table.remove(a, i) 42     end
43 end
44 print("1.數值型for從後往前遍歷 time:", os.clock() - start_time) 45 
46 --test-(2.while控制下標方式)
47 start_time = os.clock() 48 local index = 1
49 local v = b[index] 50 while v do
51     if rm_func(v) then
52         table.remove(b, index) 53     else
54         index = index + 1
55     end
56     
57     v = b[index] 58 end
59 print("2.while控制下標方式 time:", os.clock() - start_time) 60 
61 --test-(3.構建臨時表方式)
62 start_time = os.clock() 63 local tmp = {} 64 for k, v in ipairs(c) do
65     if v % 2 == 0 then
66         table.insert(tmp, v) 67     end
68 end
69 c = tmp 70 print("3.構建臨時表方式 time:", os.clock() - start_time) 71 
72 --test-(tb.remove)
73 start_time = os.clock() 74 tb.remove(d, rm_func) 75 print("tb.remove time:", os.clock() - start_time)
Test Code

看到輸出,已經百分比肯定Achieve2是更好的作法,即便是經過構建臨時表的方式3依然比Achieve2差了9倍左右,並且毫無疑問方式3內存佔用更高;固然,實際測試時間還受到具體環境影響,可是Achieve2比loop+table.remove()效率更高已經能夠蓋棺定論,由此能夠想到,loop+table.remove()自己就是一個極其不合格的作法,是一個誤區,table.remove()的設計初衷確定不是讓你在loop中使用。性能


  文章到這裏已經明確Achieve2是更好的替代方案,可是否可讓Achieve2變得更健全一點?若是表是無序表,或者表既有無序部分也有有序部分,當中的元素都須要遍歷判斷刪除怎麼辦,上述的Achieve2只能用於有序表,就是表的元素只存在於table的數組部分,哈希部分爲空的狀況下,那麼是否能進一步更改Achieve2的實現使適用於全部狀況?首先,要明確的是,lua提供的運算符和表標準庫函數都只能獲取表數組部分長度,對於整體長度,是獲取不了的,因此不在事先遍歷一次表的狀況下沒法判斷一個表究竟是有序表仍是無序表;其次,要使Achieve2適用全部狀況,必須使用pairs()遍歷,使用pairs()遍歷,具體算法是否依然能把Achieve2的時間複雜度控制在O(n)?測試

  詳細瞭解一下函數pairs機制,函數pairs在調用的時候會返回lua的一個基本函數next和不可變狀態表t,調用next(t, key)時,該函數會以隨機次序返回表中的下一個key及其對應的值,調用next(t, nil)時,返回表中的第一個鍵值對,全部元素遍歷完時,函數next返回nil,for循環老是會把表達式列表的結果調整爲3個值,因此,在調用pairs()的時候,獲得的至關於next, t, nil;next函數的底層lua實現lua_next中,老是先遍歷數組部分,再遍歷哈希部分,基於pairs的這些具體行爲,咱們能夠獲得Achieve3:優化

 1 local tb = {}  2 
 3 function tb.remove(obj, rm_func)  4     if type(obj) ~= "table" or type(rm_func) ~= "function" then
 5         return
 6     end
 7 
 8     local r_index, length = 1, #obj  9     for k, v in pairs(obj) do        --lua5.3能夠經過math.type(k)判斷
10         if type(k) == "number" and math.floor(k) == k and k > 0 and k <= length then
11             local tmp = v 12             obj[k] = nil
13             if not rm_func(tmp) then
14                 obj[r_index] = tmp 15                 r_index = r_index + 1
16             end
17         else
18             if rm_func(v) then
19                 obj[k] = nil
20             end
21         end
22     end
23 end
24 
25 return tb
Achieve3

Achieve3好像是個不錯的實現,可是看着那一堆判斷就頭疼,那還有沒有更好的實現方法,有,就是先遍歷表數組部分,而後改寫pairs行爲,使只遍歷哈希部分,下面是Achieve4:lua

 1 local tb = {}  2 
 3 function tb.remove(obj, rm_func)  4     if type(obj) ~= "table" or type(rm_func) ~= "function" then
 5         return
 6     end
 7 
 8     local index, r_index, length = 1, 1, #obj  9     while index <= length do
10         local v = obj[index] 11         obj[index] = nil
12         if not rm_func(v) then
13             obj[r_index] = v 14             r_index = r_index + 1
15         end
16 
17         index = index + 1
18     end
19 
20     local function _pairs(tb) 21         if length == 0 then
22             return next, tb, nil
23         else
24             return next, tb, length 25         end
26     end
27 
28     for k, v in _pairs(obj) do
29         if rm_func(v) then
30             obj[k] = nil
31         end
32     end
33 end
34 
35 return tb
Achieve4

可是到此咱們依然面臨着一種具體狀況,那就是咱們但願在表的有序部分刪除元素後,後面的元素保持不動,因而再度改寫Achieve4,獲得Achieve5:spa

 1 local tb = {}  2 
 3 function tb.remove(obj, rm_func, to_sequence)  4     if type(obj) ~= "table" or type(rm_func) ~= "function" then
 5         return
 6     end
 7 
 8     local length = 0
 9     if to_sequence then
10         length = #obj 11         local index, r_index = 1, 1
12         while index <= length do
13             local v = obj[index] 14             obj[index] = nil
15             if not rm_func(v) then
16                 obj[r_index] = v 17                 r_index = r_index + 1
18             end
19 
20             index = index + 1
21         end
22     end
23 
24     local function _pairs(tb) 25         if length == 0 then
26             return next, tb, nil
27         else
28             return next, tb, length 29         end
30     end
31 
32     for k, v in _pairs(obj) do
33         if rm_func(v) then
34             obj[k] = nil
35         end
36     end
37 end
38 
39 return tb
Achieve5

  好了,到此文章已經差很少結束了,上面的內容純屬原創都是本人在工做中的實際看法,水平有限,這也是本人的第一次更博,之後還會繼續把本身的經驗分享出來,若是有錯誤的地方還請不吝指出。下面附上本人寫的一些拓展table函數(主要用於我的測試):

 1 local tb = {}  2 
 3 --to_sequence可選且只會在表有序部分起做用,時間複雜度O(n)
 4 function tb.remove(obj, rm_func, to_sequence)  5     if type(obj) ~= "table" or type(rm_func) ~= "function" then
 6         return
 7     end
 8 
 9     local length = 0
 10     if to_sequence then
 11         length = #obj  12         local index, r_index = 1, 1
 13         while index <= length do
 14             local v = obj[index]  15             obj[index] = nil
 16             if not rm_func(v) then
 17                 obj[r_index] = v  18                 r_index = r_index + 1
 19             end
 20 
 21             index = index + 1
 22         end
 23     end
 24 
 25     local function _pairs(tb) --lua_next實現是迭代完數組部分再迭代哈希部分
 26         if length == 0 then
 27             return next, tb, nil
 28         else
 29             return next, tb, length  30         end
 31     end
 32 
 33     for k, v in _pairs(obj) do
 34         if rm_func(v) then
 35             obj[k] = nil
 36         end
 37     end
 38 end
 39 
 40 function tb.duplicate(obj)  41     local tb_note = {} --使指向行爲與原表一致
 42     local copy_func  43     copy_func = function(obj)  44         if type(obj) ~= "table" then
 45             return obj  46         elseif tb_note[obj] then
 47             return tb_note[obj]  48         end
 49 
 50         local dup = {}  51         tb_note[obj] = dup  52         for k, v in pairs(obj) do
 53             dup[copy_func(k)] = copy_func(v)  54         end
 55         setmetatable(dup, getmetatable(obj))  56         return dup  57     end
 58 
 59     return copy_func(obj)  60 end
 61 
 62 function tb.tostring(obj)  63     local tb_note = {} --防止相同錶轉換屢次
 64     local function serialize(value)  65         if tb_note[value] then
 66             return tb_note[value]  67         elseif type(value) == "number" then
 68             return string.format("%d", value) --%a
 69         elseif type(value) == "string" then
 70             return string.format("%q", value) --5.3.3版本後可使用%q轉化number,nil,boolean
 71         elseif type(value) == "table" then
 72             local str = "{"
 73             local index_tb = tb.getsortedindexlist(value)  74             for _, v in ipairs(index_tb) do
 75                 str = str.."["..serialize(v).."]="..serialize(value[v])..","
 76             end
 77             local mt = getmetatable(value)  78             if mt then str = str.."[\"metatable\"]="..serialize(mt).."," end
 79             str = str.."}"
 80             
 81             tb_note[value] = str  82             return str  83         else
 84             return tostring(value)  85         end
 86     end
 87 
 88     return serialize(obj)  89 end
 90 
 91 local type_value = {["number"] = 1, ["string"] = 2, ["userdata"] = 3, ["function"] = 4, ["table"] = 5}  92 function tb.getsortedindexlist(obj)  93     local index_tb = {}  94     for k in pairs(obj) do
 95         table.insert(index_tb, k)  96     end
 97 
 98     local sort_func = function(a, b)  99         local type_a = type_value[type(a)] 100         local type_b = type_value[type(b)] 101         if type_a ~= type_b then
102             return type_a < type_b 103         else
104             if type_a == 1 or type_a == 2 then
105                 return a < b 106             elseif type_a == 5 then
107                 return tb.getlen(a) < tb.getlen(b) 108             else
109                 return false
110             end
111         end
112     end
113 
114     table.sort(index_tb, function(a, b) return sort_func(a, b) end) 115     return index_tb 116 end
117 
118 function tb.getlen(obj) 119     local count = 0
120     for k, v in pairs(obj) do
121         count = count + 1
122     end
123 
124     return count 125 end
126 
127 function tb.show(obj) 128     for k, v in pairs(obj) do
129         print(k, v) 130     end
131     local mt = getmetatable(obj) 132     if mt then
133         print("metatable", mt) 134     end
135 end
136 
137 return tb
Table Code
相關文章
相關標籤/搜索