Преобразование массива в хеш индекса в Ruby

У меня есть массив, и я хочу создать хэш, чтобы быстро спросить: «А есть ли 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?

ИЗМЕНИТЬ 1

Нет, 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)

Не забывайте учитывать время, необходимое для преобразования. Конечно, если ваша ситуация позволяет, использование набора для начала (как предлагает @Zach Langley) позволяет избежать этой стоимости.

Sean Vikoren 18.11.2011 20:47

Чтобы быть справедливым, приведенный выше тест должен включать преобразование массива в хэш

drhenner 22.07.2016 20:11

@drhenner теоретически конечно. На практике нет - в основном это не имеет значения. Преобразование выполняется один раз, поиск - много, много, много раз. Я забыл, над чем я работал, когда спросил об этом, но в реальной программе поиск, вероятно, выполнялся много миллионов раз после одного преобразования.

derobert 22.07.2016 20:15
Структурированный массив Numpy
Структурированный массив Numpy
Однако в реальных проектах я чаще всего имею дело со списками, состоящими из нескольких типов данных. Как мы можем использовать массивы numpy, чтобы...
T - 1Bits: Генерация последовательного массива
T - 1Bits: Генерация последовательного массива
По мере того, как мы пишем все больше кода, мы привыкаем к определенным способам действий. То тут, то там мы находим код, который заставляет нас...
Что такое деструктуризация массива в JavaScript?
Что такое деструктуризация массива в JavaScript?
Деструктуризация позволяет распаковывать значения из массивов и добавлять их в отдельные переменные.
43
3
52 919
14
Перейти к ответу Данный вопрос помечен как решенный

Ответы 14

Если вы хотите быстро спросить, "есть ли 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 #? в вопросе.

derobert 04.01.2009 11:59

Может быть, я неправильно понимаю цель здесь; Если вы хотите знать, был ли X в массиве, почему бы не сделать array.include? («X»)?

Пожалуйста, посмотрите мой (недавно добавленный) раздел о том, почему не включить Array #? в вопросе.

derobert 04.01.2009 11:59

Если вы ищете эквивалент этого кода Perl:

grep {$_ eq $element} @array

Вы можете просто использовать простой код Ruby:

array.include?(element)

Пожалуйста, посмотрите мой (недавно добавленный) раздел о том, почему не включить Array #? в вопросе.

derobert 04.01.2009 12:00

Ваш способ создания хеша выглядит неплохо. У меня гадость в 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 минуты.

derobert 04.01.2009 12:36

Эмпирически это похоже на O (n ^ 2)

derobert 04.01.2009 12:52

хе-хе. Я не говорил, что это было быстро! Вы всегда можете создать метод to_h для класса Array, используя свой метод Hash [map].

dylanfm 04.01.2009 13:00

Используйте слияние! должно быть намного быстрее

drhenner 22.07.2016 19:37

array.inject (Hash.new) {| h, i | h.merge! ({i => nil})} оказывается довольно быстрым, но все же array.each{|e| hash[e] = nil} в 3,5 раза быстрее

drhenner 22.07.2016 19:54

Я почти уверен, что не существует одноразового умного способа построить этот хеш. Я хотел бы просто быть откровенным и заявить, что я делаю:

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

derobert 04.01.2009 23:19

Для Ruby 1.8.6 вам понадобится знак splat, т.е. hash = Hash [* array.map {| x | [x, nil]} .flatten]

Zach Langley 05.01.2009 22:32

Я думаю, что замечание 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 the to_set method.

Set uses Hash as storage, so you must note the following points:

  • Equality of elements is determined according to Object#eql? and Object#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).

derobert 04.01.2009 23:41

В этом случае да, но это можно обобщить, чтобы запомнить любую функцию по вашему выбору.

zenazn 05.01.2009 01:12

Рэмпион меня опередил. Сет может быть ответом.

Ты можешь сделать:

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]. Спасибо за этот пост

covard 04.01.2013 06:53

Почему бы просто не сделать Hash[ [1, 2, 3, 4].map {|k| [k, nil]} ]? Нет необходимости в дополнительных полосах и выравнивании.

l3thal 15.06.2016 19:57

Попробуй это:

a=[1,2,3]
Hash[a.zip]

Идеально! Самый изящный способ.

denis.peplin 10.12.2014 08:52
Hash[a.zip] также возвращает тот же ответ.
Alex Pan 18.01.2017 20:24

«это, безусловно, работает ... но я не думаю, что это наиболее эффективно ... в этом случае вы повторяете дважды. Конечно, это не проблема с массивом длины 2, но все же стоит отметить». @brad из stackoverflow.com/a/9434078/380607

Magne 28.11.2017 21:12

Это сохраняет 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}

Другие вопросы по теме