radix_tree 1.0.0.dev → 1.0.0.dev.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +2 -2
- data/lib/radix_tree.rb +78 -37
- data/test/helper.rb +2 -0
- data/test/test_radix_tree.rb +118 -1
- metadata +1 -1
data/README
CHANGED
@@ -7,8 +7,8 @@ to avoid DoS via Algorithmic Complexity Attacks.
|
|
7
7
|
|
8
8
|
== Performance
|
9
9
|
|
10
|
-
* 25 times slower for 10 bytes key insertion
|
11
|
-
*
|
10
|
+
* 25 times slower for 10 bytes key, 100000 elements insertion
|
11
|
+
* 10 times slower for 10 bytes key, 100000 elements retrieval
|
12
12
|
|
13
13
|
|
14
14
|
== TODO
|
data/lib/radix_tree.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# Naive implementation of Radix Tree for avoiding DoS via Algorithmic
|
2
2
|
# Complexity Attacks.
|
3
3
|
#
|
4
|
-
# 25 times slower for 10 bytes key insertion
|
5
|
-
#
|
4
|
+
# 25 times slower for 10 bytes key, 100000 elements insertion
|
5
|
+
# 10 times slower for 10 bytes key, 100000 elements retrieval
|
6
6
|
#
|
7
7
|
# TODO: Implement following features for utilizing strength of Radix Tree.
|
8
8
|
# * find predecessor
|
9
9
|
# * find successor
|
10
10
|
# * find_all by start string
|
11
11
|
# * delete_all by start string
|
12
|
+
#
|
12
13
|
class RadixTree
|
13
14
|
include Enumerable
|
14
15
|
|
@@ -22,8 +23,12 @@ class RadixTree
|
|
22
23
|
@key, @value, @children = key, value, children
|
23
24
|
end
|
24
25
|
|
26
|
+
def undefined?
|
27
|
+
@value == UNDEFINED
|
28
|
+
end
|
29
|
+
|
25
30
|
def empty?
|
26
|
-
@children.nil? and
|
31
|
+
@children.nil? and undefined?
|
27
32
|
end
|
28
33
|
|
29
34
|
def size
|
@@ -60,47 +65,70 @@ class RadixTree
|
|
60
65
|
@value = value
|
61
66
|
else
|
62
67
|
index = head_match_length(key)
|
63
|
-
if index == @key.
|
68
|
+
if index == @key.size
|
64
69
|
push(key[index..-1], value)
|
65
70
|
else
|
66
71
|
split(index)
|
67
|
-
|
72
|
+
# search again after split the node
|
73
|
+
store(key, value)
|
68
74
|
end
|
69
75
|
end
|
70
76
|
end
|
71
77
|
|
72
78
|
def retrieve(key)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
if @key == key
|
80
|
+
@value
|
81
|
+
elsif !@children
|
82
|
+
UNDEFINED
|
83
|
+
else
|
84
|
+
key = child_key(key)
|
85
|
+
if child = find_child(key)
|
79
86
|
child.retrieve(key)
|
87
|
+
else
|
88
|
+
UNDEFINED
|
80
89
|
end
|
81
|
-
else
|
82
|
-
UNDEFINED
|
83
90
|
end
|
84
91
|
end
|
85
92
|
|
86
93
|
def delete(key)
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
94
|
+
if @key == key
|
95
|
+
value, @value = @value, UNDEFINED
|
96
|
+
value
|
97
|
+
elsif !@children
|
98
|
+
nil
|
99
|
+
else
|
100
|
+
key = child_key(key)
|
101
|
+
if child = find_child(key)
|
93
102
|
value = child.delete(key)
|
103
|
+
if value and child.undefined?
|
104
|
+
if child.children.nil?
|
105
|
+
delete_child(child)
|
106
|
+
elsif child.children.size == 1
|
107
|
+
# pull up the grand child as a child
|
108
|
+
delete_child(child)
|
109
|
+
grand = child.children.values.first
|
110
|
+
add_child(Node.new(child.key + grand.key, grand.value, grand.children))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
value
|
94
114
|
end
|
95
|
-
delete_child(child) if value and child.children.nil?
|
96
|
-
value
|
97
115
|
end
|
98
116
|
end
|
99
117
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
118
|
+
def dump_tree(io, prefix = '', indent = '')
|
119
|
+
prefix += @key
|
120
|
+
indent += ' '
|
121
|
+
if undefined?
|
122
|
+
io << sprintf("#<%s:0x%010x %s>", self.class.name, __id__, prefix.inspect)
|
123
|
+
else
|
124
|
+
io << sprintf("#<%s:0x%010x %s> => %s", self.class.name, __id__, prefix.inspect, @value.inspect)
|
125
|
+
end
|
126
|
+
if @children
|
127
|
+
@children.each do |k, v|
|
128
|
+
io << $/ + indent
|
129
|
+
v.dump_tree(io, prefix, indent)
|
130
|
+
end
|
131
|
+
end
|
104
132
|
end
|
105
133
|
|
106
134
|
private
|
@@ -129,19 +157,23 @@ class RadixTree
|
|
129
157
|
end
|
130
158
|
|
131
159
|
def child_key(key)
|
132
|
-
|
133
|
-
key[index..-1]
|
160
|
+
key[head_match_length(key)..-1]
|
134
161
|
end
|
135
162
|
|
163
|
+
# assert: check != @key
|
136
164
|
def head_match_length(check)
|
137
|
-
0.upto(
|
138
|
-
|
165
|
+
0.upto(check.size) do |index|
|
166
|
+
if check[index] != @key[index]
|
167
|
+
return index
|
168
|
+
end
|
139
169
|
end
|
140
|
-
raise 'assert check != @key'
|
170
|
+
raise 'assert: check != @key'
|
141
171
|
end
|
142
172
|
|
143
173
|
def find_child(key)
|
144
|
-
|
174
|
+
if @children
|
175
|
+
@children[key[0]]
|
176
|
+
end
|
145
177
|
end
|
146
178
|
|
147
179
|
def add_child(child)
|
@@ -151,14 +183,18 @@ class RadixTree
|
|
151
183
|
|
152
184
|
def delete_child(child)
|
153
185
|
@children.delete(child.key[0])
|
154
|
-
|
186
|
+
if @children.empty?
|
187
|
+
@children = nil
|
188
|
+
end
|
155
189
|
end
|
156
190
|
end
|
157
191
|
|
158
192
|
DEFAULT = Object.new
|
159
193
|
|
160
194
|
def initialize(default = DEFAULT, &block)
|
161
|
-
|
195
|
+
if block && default != DEFAULT
|
196
|
+
raise ArgumentError, 'wrong number of arguments'
|
197
|
+
end
|
162
198
|
@root = Node.new('')
|
163
199
|
@default = default
|
164
200
|
@default_proc = block
|
@@ -185,16 +221,16 @@ class RadixTree
|
|
185
221
|
end
|
186
222
|
|
187
223
|
def []=(key, value)
|
188
|
-
@root.store(key, value)
|
224
|
+
@root.store(key.to_s, value)
|
189
225
|
end
|
190
226
|
|
191
227
|
def key?(key)
|
192
|
-
@root.retrieve(key) != Node::UNDEFINED
|
228
|
+
@root.retrieve(key.to_s) != Node::UNDEFINED
|
193
229
|
end
|
194
230
|
alias has_key? key?
|
195
231
|
|
196
232
|
def [](key)
|
197
|
-
value = @root.retrieve(key)
|
233
|
+
value = @root.retrieve(key.to_s)
|
198
234
|
if value == Node::UNDEFINED
|
199
235
|
if @default != DEFAULT
|
200
236
|
@default
|
@@ -209,6 +245,11 @@ class RadixTree
|
|
209
245
|
end
|
210
246
|
|
211
247
|
def delete(key)
|
212
|
-
@root.delete(key)
|
248
|
+
@root.delete(key.to_s)
|
249
|
+
end
|
250
|
+
|
251
|
+
def dump_tree(io = '')
|
252
|
+
@root.dump_tree(io)
|
253
|
+
io
|
213
254
|
end
|
214
255
|
end
|
data/test/helper.rb
CHANGED
data/test/test_radix_tree.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
require File.expand_path('./helper', File.dirname(__FILE__))
|
2
3
|
|
3
4
|
class TestRadixTree < Test::Unit::TestCase
|
@@ -7,8 +8,13 @@ class TestRadixTree < Test::Unit::TestCase
|
|
7
8
|
assert_equal nil, h['def']
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
+
def test_empty
|
11
12
|
h = RadixTree.new
|
13
|
+
h['abc'] = 0
|
14
|
+
assert_equal nil, h['']
|
15
|
+
h[''] = 1
|
16
|
+
assert_equal 1, h['']
|
17
|
+
h.delete('')
|
12
18
|
assert_equal nil, h['']
|
13
19
|
end
|
14
20
|
|
@@ -138,4 +144,115 @@ class TestRadixTree < Test::Unit::TestCase
|
|
138
144
|
assert_equal 0, h.size
|
139
145
|
assert h.empty?
|
140
146
|
end
|
147
|
+
|
148
|
+
def test_delete_compaction_middle
|
149
|
+
h = RadixTree.new
|
150
|
+
h['a'] = 1
|
151
|
+
h['abc'] = 2
|
152
|
+
h['bb'] = 3
|
153
|
+
h['abcdefghi'] = 4
|
154
|
+
h['abcdefghijzz'] = 5
|
155
|
+
h['abcdefghikzz'] = 6
|
156
|
+
assert_equal 7, h.dump_tree.split($/).size
|
157
|
+
h.delete('a')
|
158
|
+
assert_equal 6, h.dump_tree.split($/).size
|
159
|
+
h['a'] = 1
|
160
|
+
assert_equal 7, h.dump_tree.split($/).size
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_delete_compaction_leaf
|
164
|
+
h = RadixTree.new
|
165
|
+
h['a'] = 1
|
166
|
+
h['abc'] = 2
|
167
|
+
h['bb'] = 3
|
168
|
+
h['abcdefghijzz'] = 4
|
169
|
+
assert_equal 5, h.dump_tree.split($/).size
|
170
|
+
h['abcdefghikzz'] = 5
|
171
|
+
assert_equal 7, h.dump_tree.split($/).size
|
172
|
+
h.delete('abcdefghijzz')
|
173
|
+
assert_equal 5, h.dump_tree.split($/).size
|
174
|
+
h['abcdefghijzz'] = 4
|
175
|
+
assert_equal 7, h.dump_tree.split($/).size
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_each
|
179
|
+
h = RadixTree.new
|
180
|
+
s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
|
181
|
+
s.each do |k, v|
|
182
|
+
h[k] = v
|
183
|
+
end
|
184
|
+
values = []
|
185
|
+
h.each do |k, v|
|
186
|
+
values << [k, v]
|
187
|
+
end
|
188
|
+
assert_equal s.to_a.sort_by { |k, v| k }, values.sort_by { |k, v| k }
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_keys
|
192
|
+
h = RadixTree.new
|
193
|
+
s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
|
194
|
+
s.each do |k, v|
|
195
|
+
h[k] = v
|
196
|
+
end
|
197
|
+
assert_equal s.keys.sort, h.keys.sort
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_values
|
201
|
+
h = RadixTree.new
|
202
|
+
s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
|
203
|
+
s.each do |k, v|
|
204
|
+
h[k] = v
|
205
|
+
end
|
206
|
+
assert_equal s.values.sort, h.values.sort
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_to_s
|
210
|
+
h = RadixTree.new
|
211
|
+
h[:abc] = 1
|
212
|
+
assert_equal 1, h["abc"]
|
213
|
+
assert_equal 1, h[:abc]
|
214
|
+
end
|
215
|
+
|
216
|
+
def test_key?
|
217
|
+
h = RadixTree.new
|
218
|
+
assert !h.key?('a')
|
219
|
+
s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
|
220
|
+
s.each do |k, v|
|
221
|
+
h[k] = v
|
222
|
+
end
|
223
|
+
assert h.key?('a')
|
224
|
+
end
|
225
|
+
|
226
|
+
def test_default
|
227
|
+
assert_raise(ArgumentError) do
|
228
|
+
RadixTree.new('both') { :not_allowed }
|
229
|
+
end
|
230
|
+
|
231
|
+
h = RadixTree.new('abc')
|
232
|
+
assert_equal 'abc', h['foo']
|
233
|
+
assert_equal 'abc', h['bar']
|
234
|
+
assert h['baz'].object_id == h['qux'].object_id
|
235
|
+
|
236
|
+
h = RadixTree.new { [1, 2] }
|
237
|
+
assert_equal [1, 2], h['foo']
|
238
|
+
assert_equal [1, 2], h['bar']
|
239
|
+
assert h['baz'].object_id != h['qux'].object_id
|
240
|
+
end
|
241
|
+
|
242
|
+
if RUBY_VERSION >= '1.9.0'
|
243
|
+
def test_encoding
|
244
|
+
h = RadixTree.new
|
245
|
+
s = { 'ああ' => 1, 'あい' => 2, 'いい' => 3, 'いう' => 4, 'あ' => 5, 'あいう' => 6 }
|
246
|
+
s.each do |k, v|
|
247
|
+
h[k] = v
|
248
|
+
end
|
249
|
+
assert_equal 6, h.size
|
250
|
+
s.each do |k, v|
|
251
|
+
assert_equal v, h[k]
|
252
|
+
end
|
253
|
+
str = 'ああ'
|
254
|
+
str.force_encoding('US-ASCII')
|
255
|
+
assert_equal nil, h[str]
|
256
|
+
end
|
257
|
+
end
|
141
258
|
end
|