lua中,对table.remove()的改进

^_^内容原创,禁止转载


前几天在处理项目某个bug的时候发现代码中使用了ipairs()+table.remove()删除元素,因为历史配置原因,导致这段代码一直没出现错误。lua中,在for循环调用函数ipairs时,ipairs会返回3个值供for保存,迭代函数、不可变状态表、初始控制变量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,即内存重新分配,这也不是我们想看到的情况。那应该以怎样一种方式更好地实现上面loop+table.remove()?

  在lua中回收一个元素,直接把它置nil即可。于是,对于有序表,是否可以以下面的方式实行替代:

 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()的方案,既不用在删除元素的时候移动后面的元素,也不需要额外的元素储存空间,时间复杂度妥妥的O(n),空间复杂度O(1),下面附上测试代码来看看这几种方式的运行时间对比:

 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具有更高效率,但看到对比输出,依然为Achieve2感到惊讶,即使是通过构建临时表的方式3依然比Achieve2差了9倍左右,而且毫无疑问方式3内存占用更高;当然,实际测试时间还受到具体环境影响,但是Achieve2比loop+table.remove()效率更高已经可以盖棺定论(对于Achieve2的测试本人进行了不下百次,Achieve2的实现绝对是健康的实现)。


  到这里已经明确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循环结束,for循环总是会把表达式列表的结果调整为3个值,所以,在首次调用pairs()的时候,得到的相当于next, t, nil;next函数的底层lua实现luaH_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:

 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(如果项目中有封装table的操作文件,可以把_pair单独拿出来,这样就不用每次运行都创建一个闭包):

 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

 

posted @ 2019-06-17 20:26  我的超级帅发型丶  阅读(6037)  评论(1编辑  收藏  举报