radix_tree 1.0.0.dev → 1.0.0.dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|