前几天在处理项目一个bug的时候发现代码中使用了ipairs()+table.remove()删除元素,显然这是错误的做法,但因为历史配置原因,导致这个BUG在之前一直没表现出来。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 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
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种方式的而且确都可以实现对表元素的删除,但好与不好真的值得斟酌。或许细细想下还会有更好的方式去实现,如果你对lua底层有一点了解的话。首先,回到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
稍微封装一下:
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彷佛是一种不错的替代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)
即使事先知道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好像是个不错的实现,但是看着那一堆判断就头疼,那还有没有更好的实现方法,有,就是先手动遍历表数组部分,然后自己实现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,得到Achieve5:
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
好了,到此文章已经差不多结束,上述内容都是本人在工作中的实际见解,纯属原创。这也是本人的第一次写博,水平有限,以后还会继续把自己的经验分享出来,如果有错误的地方还请不吝指出。下面附上本人写的一些拓展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