У меня есть массив, и я хочу создать хэш, чтобы быстро спросить: «А есть ли X в массиве?».
В perl есть простой (и быстрый) способ сделать это:
my @array = qw( 1 2 3 );
my %hash;
@hash{@array} = undef;
Это генерирует хеш, который выглядит так:
{
1 => undef,
2 => undef,
3 => undef,
}
Лучшее, что я придумал в Ruby, это:
array = [1, 2, 3]
hash = Hash[array.map {|x| [x, nil]}]
который дает:
{1=>nil, 2=>nil, 3=>nil}
Есть ли способ лучше Ruby?
Нет, Array.include? не лучшая идея. Его медленный. Он выполняет запрос в O (n) вместо O (1). В моем примере массив для краткости состоял из трех элементов; предположим, что на самом деле есть миллион элементов. Проведем небольшой тест:
#!/usr/bin/ruby -w
require 'benchmark'
array = (1..1_000_000).to_a
hash = Hash[array.map {|x| [x, nil]}]
Benchmark.bm(15) do |x|
x.report("Array.include?") { 1000.times { array.include?(500_000) } }
x.report("Hash.include?") { 1000.times { hash.include?(500_000) } }
end
Производит:
user system total real
Array.include? 46.190000 0.160000 46.350000 ( 46.593477)
Hash.include? 0.000000 0.000000 0.000000 ( 0.000523)
Чтобы быть справедливым, приведенный выше тест должен включать преобразование массива в хэш
@drhenner теоретически конечно. На практике нет - в основном это не имеет значения. Преобразование выполняется один раз, поиск - много, много, много раз. Я забыл, над чем я работал, когда спросил об этом, но в реальной программе поиск, вероятно, выполнялся много миллионов раз после одного преобразования.



Если вы хотите быстро спросить, "есть ли X в массиве?" вы должны использовать Array#include?.
Изменить (в ответ на добавление в OP):
Если вам нужен быстрый поиск, используйте Set. Глупо иметь хеш, указывающий на все nil. Преобразование также является простым процессом с Array#to_set.
require 'benchmark'
require 'set'
array = (1..1_000_000).to_a
set = array.to_set
Benchmark.bm(15) do |x|
x.report("Array.include?") { 1000.times { array.include?(500_000) } }
x.report("Set.include?") { 1000.times { set.include?(500_000) } }
end
Результаты на моей машине:
user system total real
Array.include? 36.200000 0.140000 36.340000 ( 36.740605)
Set.include? 0.000000 0.000000 0.000000 ( 0.000515)
Вам следует подумать об использовании для начала просто набора, а не массива, чтобы никогда не потребовалось преобразование.
Пожалуйста, посмотрите мой (недавно добавленный) раздел о том, почему не включить Array #? в вопросе.
Может быть, я неправильно понимаю цель здесь; Если вы хотите знать, был ли X в массиве, почему бы не сделать array.include? («X»)?
Пожалуйста, посмотрите мой (недавно добавленный) раздел о том, почему не включить Array #? в вопросе.
Если вы ищете эквивалент этого кода Perl:
grep {$_ eq $element} @array
Вы можете просто использовать простой код Ruby:
array.include?(element)
Пожалуйста, посмотрите мой (недавно добавленный) раздел о том, почему не включить Array #? в вопросе.
Ваш способ создания хеша выглядит неплохо. У меня гадость в irb была и это еще один способ
>> [1,2,3,4].inject(Hash.new) { |h,i| {i => nil}.merge(h) }
=> {1=>nil, 2=>nil, 3=>nil, 4=>nil}
Аккуратный подход, но до неприличия медленный; на 1..1_000_000: 1.4s (мой код карты) vs. надоело-ждать через 3 минуты.
Эмпирически это похоже на O (n ^ 2)
хе-хе. Я не говорил, что это было быстро! Вы всегда можете создать метод to_h для класса Array, используя свой метод Hash [map].
Используйте слияние! должно быть намного быстрее
array.inject (Hash.new) {| h, i | h.merge! ({i => nil})} оказывается довольно быстрым, но все же array.each{|e| hash[e] = nil} в 3,5 раза быстрее
Я почти уверен, что не существует одноразового умного способа построить этот хеш. Я хотел бы просто быть откровенным и заявить, что я делаю:
hash = {}
array.each{|x| hash[x] = nil}
Он не выглядит особенно элегантным, но ясный и выполняет свою работу.
FWIW, ваше первоначальное предложение (по крайней мере, в Ruby 1.8.6), похоже, не работает. Я получаю ошибку «ArgumentError: нечетное количество аргументов для хеширования». Hash. [] Ожидает буквальный, равномерный список значений:
Hash[a, 1, b, 2] # => {a => 1, b => 2}
поэтому я попытался изменить ваш код на:
hash = Hash[*array.map {|x| [x, nil]}.flatten]
но производительность ужасная:
#!/usr/bin/ruby -w
require 'benchmark'
array = (1..100_000).to_a
Benchmark.bm(15) do |x|
x.report("assignment loop") {hash = {}; array.each{|e| hash[e] = nil}}
x.report("hash constructor") {hash = Hash[*array.map {|e| [e, nil]}.flatten]}
end
дает
user system total real
assignment loop 0.440000 0.200000 0.640000 ( 0.657287)
hash constructor 4.440000 0.250000 4.690000 ( 4.758663)
Если я здесь чего-то не упускаю, простой цикл присваивания кажется наиболее ясным и эффективным способом создания этого хэша.
Я понял, что перестал закрывать] в коде ... Исправлено. Здесь он работает с рубином 1.8.7, а также 1.9.0
Для Ruby 1.8.6 вам понадобится знак splat, т.е. hash = Hash [* array.map {| x | [x, nil]} .flatten]
Я думаю, что замечание chrismear об использовании присваивания вместо создания - это здорово. Однако, чтобы сделать все это немного более Ruby-esque, я мог бы предложить присвоить каждому элементу что-то Другие, а не nil:
hash = {}
array.each { |x| hash[x] = 1 } # or true or something else "truthy"
...
if hash[376] # instead of if hash.has_key?(376)
...
end
Проблема с назначением nil заключается в том, что вы должны использовать has_key? вместо [], поскольку [] дает вам nil (значение вашего маркера), если Hash не имеет указанного ключа. Вы мог обойдете это, используя другое значение по умолчанию, но зачем проделывать дополнительную работу?
# much less elegant than above:
hash = Hash.new(42)
array.each { |x| hash[x] = nil }
...
unless hash[376]
...
end
Если все, для чего вам нужен хеш, это членство, рассмотрите возможность использования Set:
Set
Set implements a collection of unordered values with no duplicates. This is a hybrid of Array's intuitive inter-operation facilities and Hash's fast lookup.
Set is easy to use with Enumerable objects (implementing
each). Most of the initializer methods and binary operators accept generic Enumerable objects besides sets and arrays. An Enumerable object can be converted to Set using theto_setmethod.Set uses Hash as storage, so you must note the following points:
- Equality of elements is determined according to
Object#eql?andObject#hash.- Set assumes that the identity of each element does not change while it is stored. Modifying an element of a set will render the set to an unreliable state.
- When a string is to be stored, a frozen copy of the string is stored instead unless the original string is already frozen.
Comparison
The comparison operators
<,>,<=and>=are implemented as shorthand for the {proper_,}{subset?,superset?} methods. However, the<=>operator is intentionally left out because not every pair of sets is comparable. ({x,y} vs. {x,z} for example)Example
require 'set' s1 = Set.new [1, 2] # -> #<Set: {1, 2}> s2 = [1, 2].to_set # -> #<Set: {1, 2}> s1 == s2 # -> true s1.add("foo") # -> #<Set: {1, 2, "foo"}> s1.merge([2, 6]) # -> #<Set: {1, 2, "foo", 6}> s1.subset? s2 # -> false s2.subset? s1 # -> true[...]
Public Class Methods
new(enum = nil)
Creates a new set containing the elements of the given enumerable object.
If a block is given, the elements of enum are preprocessed by the given block.
Вот изящный способ кэшировать поисковые запросы с помощью хеша:
a = (1..1000000).to_a
h = Hash.new{|hash,key| hash[key] = true if a.include? key}
Практически он создает конструктор по умолчанию для новых значений хеш-функции, а затем сохраняет «true» в кеше, если оно находится в массиве (в противном случае - nil). Это позволяет отложить загрузку в кеш на случай, если вы не используете каждый элемент.
Хеш-загрузка происходит так быстро, что это вряд ли когда-либо будет иметь смысл, если у вас действительно не хватает памяти. Вся хеш-загрузка составляет O (n), один a.include? вызов также O (n).
В этом случае да, но это можно обобщить, чтобы запомнить любую функцию по вашему выбору.
Рэмпион меня опередил. Сет может быть ответом.
Ты можешь сделать:
require 'set'
set = array.to_set
set.include?(x)
Проведение некоторого сравнительного анализа предложений пока показывает, что создание хэша на основе присваивания chrismear и Гая немного быстрее, чем мой метод карты (и присвоение nil немного быстрее, чем присвоение true). Предлагаемый набор mtyaka и rampion создается примерно на 35% медленнее.
Что касается поиска, hash.include?(x) на очень маленькую величину быстрее, чем hash[x]; оба в два раза быстрее, чем set.include?(x).
user system total real
chrismear 6.050000 0.850000 6.900000 ( 6.959355)
derobert 6.010000 1.060000 7.070000 ( 7.113237)
Gaius 6.210000 0.810000 7.020000 ( 7.049815)
mtyaka 8.750000 1.190000 9.940000 ( 9.967548)
rampion 8.700000 1.210000 9.910000 ( 9.962281)
user system total real
times 10.880000 0.000000 10.880000 ( 10.921315)
set 93.030000 17.490000 110.520000 (110.817044)
hash-i 45.820000 8.040000 53.860000 ( 53.981141)
hash-e 47.070000 8.280000 55.350000 ( 55.487760)
Код сравнительного анализа:
#!/usr/bin/ruby -w
require 'benchmark'
require 'set'
array = (1..5_000_000).to_a
Benchmark.bmbm(10) do |bm|
bm.report('chrismear') { hash = {}; array.each{|x| hash[x] = nil} }
bm.report('derobert') { hash = Hash[array.map {|x| [x, nil]}] }
bm.report('Gaius') { hash = {}; array.each{|x| hash[x] = true} }
bm.report('mtyaka') { set = array.to_set }
bm.report('rampion') { set = Set.new(array) }
end
hash = Hash[array.map {|x| [x, true]}]
set = array.to_set
array = nil
GC.start
GC.disable
Benchmark.bmbm(10) do |bm|
bm.report('times') { 100_000_000.times { } }
bm.report('set') { 100_000_000.times { set.include?(500_000) } }
bm.report('hash-i') { 100_000_000.times { hash.include?(500_000) } }
bm.report('hash-e') { 100_000_000.times { hash[500_000] } }
end
GC.enable
Если вас не беспокоят хеш-значения
irb(main):031:0> a=(1..1_000_000).to_a ; a.length
=> 1000000
irb(main):032:0> h=Hash[a.zip a] ; h.keys.length
=> 1000000
Занимает секунду или около того на моем рабочем столе.
Вы можете проделать очень удобный трюк:
Hash[*[1, 2, 3, 4].map {|k| [k, nil]}.flatten]
=> {1=>nil, 2=>nil, 3=>nil, 4=>nil}
Это отлично сработало для меня. Я изменил его на [k, k] вместо [k, nil]. Спасибо за этот пост
Почему бы просто не сделать Hash[ [1, 2, 3, 4].map {|k| [k, nil]} ]? Нет необходимости в дополнительных полосах и выравнивании.
Попробуй это:
a=[1,2,3]
Hash[a.zip]
Идеально! Самый изящный способ.
Hash[a.zip] также возвращает тот же ответ.
«это, безусловно, работает ... но я не думаю, что это наиболее эффективно ... в этом случае вы повторяете дважды. Конечно, это не проблема с массивом длины 2, но все же стоит отметить». @brad из stackoverflow.com/a/9434078/380607
Это сохраняет 0, если ваш хеш был [0,0,0,1,0]
hash = {}
arr.each_with_index{|el, idx| hash.merge!({(idx + 1 )=> el }) }
Возврат:
# {1=>0, 2=>0, 3=>0, 4=>1, 5=>0}
Не забывайте учитывать время, необходимое для преобразования. Конечно, если ваша ситуация позволяет, использование набора для начала (как предлагает @Zach Langley) позволяет избежать этой стоимости.