web-dev-qa-db-fra.com

Convertir un tableau en un hachage d'index dans Ruby

J'ai un tableau et je veux créer un hachage afin de pouvoir rapidement demander "est-ce que X est dans le tableau?".

En Perl, il existe un moyen simple (et rapide) de procéder:

my @array = qw( 1 2 3 );
my %hash;
@hash{@array} = undef;

Cela génère un hachage qui ressemble à:

{
    1 => undef,
    2 => undef,
    3 => undef,
}

Le meilleur que j'ai trouvé en Ruby est:

array = [1, 2, 3]
hash = Hash[array.map {|x| [x, nil]}]

qui donne:

{1=>nil, 2=>nil, 3=>nil}

Existe-t-il une meilleure façon Ruby?

EDIT 1

Non, Array.include? n'est pas une bonne idée. C'est lent. Il effectue une requête dans O(n) au lieu de O (1). Mon exemple de tableau comportait trois éléments par souci de concision; supposons que le réel comporte un million d'éléments. Faisons un petit benchmarking:

#!/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

Produit:

                     user     system      total        real
Array.include?  46.190000   0.160000  46.350000 ( 46.593477)
Hash.include?    0.000000   0.000000   0.000000 (  0.000523)
42
derobert

Si vous n'avez besoin que du hachage pour l'appartenance, pensez à utiliser un Set :

Ensemble

Set implémente une collection de valeurs non ordonnées sans doublons. Il s'agit d'un hybride des fonctionnalités d'interaction intuitive d'Array et de la recherche rapide de Hash.

Set est facile à utiliser avec les objets Enumerable (implémentant each). La plupart des méthodes d'initialisation et des opérateurs binaires acceptent les objets génériques Enumerable en plus des ensembles et des tableaux. Un objet Enumerable peut être converti en Set en utilisant to_set méthode.

Set utilise Hash comme stockage, vous devez donc noter les points suivants:

  • L'égalité des éléments est déterminée selon Object#eql? et Object#hash.
  • Set suppose que l'identité de chaque élément ne change pas lors de son stockage. La modification d'un élément d'un ensemble rendra l'ensemble non fiable.
  • Lorsqu'une chaîne doit être stockée, une copie figée de la chaîne est stockée à la place, sauf si la chaîne d'origine est déjà figée.

Comparaison

Les opérateurs de comparaison <, >, <= et >= sont implémentés en raccourci pour les méthodes {proper _,} {subset?, superset?}. Cependant, le <=> L'opérateur est intentionnellement omis car toutes les paires d'ensembles ne sont pas comparables. ({x, y} vs {x, z} par exemple)

Exemple

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

[...]

Méthodes de classe publique

nouveau (enum = nil)

Crée un nouvel ensemble contenant les éléments de l'objet énumérable donné.

Si un bloc est donné, les éléments d'énumération sont prétraités par le bloc donné.

43
rampion

essaye celui-là:

a=[1,2,3]
Hash[a.Zip]
22
edx

Vous pouvez faire cette astuce très pratique:

Hash[*[1, 2, 3, 4].map {|k| [k, nil]}.flatten]
=> {1=>nil, 2=>nil, 3=>nil, 4=>nil}
14
viebel

Si vous voulez demander rapidement "est-ce que X est dans le tableau?" Tu devrais utiliser Array#include? .

Modifier (en réponse à l'ajout dans OP):

Si vous voulez des temps de recherche rapides, utilisez un ensemble. Avoir un Hash qui pointe vers tous les nil est idiot. La conversion est également un processus facile avec 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

Résultats sur ma machine:

                     user     system      total        real
Array.include?  36.200000   0.140000  36.340000 ( 36.740605)
Set.include?     0.000000   0.000000   0.000000 (  0.000515)

Vous devriez envisager d'utiliser simplement un ensemble au lieu d'un tableau afin qu'une conversion ne soit jamais nécessaire.

9
Zach Langley

Je suis assez certain qu'il n'y a pas de méthode intelligente à un coup pour construire ce hachage. Ma tendance serait d'être explicite et de dire ce que je fais:

hash = {}
array.each{|x| hash[x] = nil}

Il n'a pas l'air particulièrement élégant, mais il est clair et fait l'affaire.

FWIW, votre suggestion d'origine (sous Ruby 1.8.6 au moins) ne semble pas fonctionner. J'obtiens une erreur "ArgumentError: nombre impair d'arguments pour Hash". Hash. [] Attend une liste de valeurs littérale et de longueur égale:

Hash[a, 1, b, 2] # => {a => 1, b => 2}

j'ai donc essayé de changer votre code en:

hash = Hash[*array.map {|x| [x, nil]}.flatten]

mais la performance est terrible:

#!/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

donne

                     user     system      total        real
assignment loop  0.440000   0.200000   0.640000 (  0.657287)
hash constructor  4.440000   0.250000   4.690000 (  4.758663)

À moins que je manque quelque chose ici, une simple boucle d'affectation semble le moyen le plus clair et le plus efficace pour construire ce hachage.

6
chrismear

Rampion m'a battu. Set pourrait être la réponse.

Tu peux faire:

require 'set'
set = array.to_set
set.include?(x)
5
mtyaka

Votre façon de créer le hachage semble bonne. J'avais une boue dans irb et c'est une autre façon

>> [1,2,3,4].inject(Hash.new) { |h,i| {i => nil}.merge(h) }
=> {1=>nil, 2=>nil, 3=>nil, 4=>nil}
4
dylanfm

Je pense que le point de chrismear sur l'utilisation de l'affectation sur la création est super. Pour rendre le tout un peu plus Ruby-esque, cependant, je pourrais suggérer d'assigner quelque chose autre que nil à chaque élément:

hash = {}
array.each { |x| hash[x] = 1 } # or true or something else "truthy"
...
if hash[376]                   # instead of if hash.has_key?(376)
  ...
end

Le problème avec l'attribution à nil est que vous devez utiliser has_key? au lieu de [], puisque [] vous donne nil (la valeur de votre marqueur) si le Hash n'a pas la clé spécifiée. Vous pourriez contourner ce problème en utilisant une valeur par défaut différente, mais pourquoi effectuer le travail supplémentaire?

# much less elegant than above:
hash = Hash.new(42)
array.each { |x| hash[x] = nil }
...
unless hash[376]
  ...
end
2
James A. Rosen

Peut-être que je comprends mal l'objectif ici; Si vous vouliez savoir si X était dans le tableau, pourquoi ne pas faire array.include? ("X")?

1
capotej

Si vous n'êtes pas dérangé par les valeurs de hachage

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

Prend environ une seconde sur mon bureau.

1
telent

Faire un benchmarking sur les suggestions jusqu'à présent donne que la création de hachage basée sur les assignations de chrismear et Gaius est légèrement plus rapide que ma méthode de carte (et assigner nil est légèrement plus rapide que assigner true). La suggestion d'ensemble de mtyaka et rampion est environ 35% plus lente à créer.

En ce qui concerne les recherches, hash.include?(x) est une très petite quantité plus rapide que hash[x]; les deux sont deux fois plus rapides que 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)

Le code de référence est:

#!/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
1
derobert

Voici une bonne façon de mettre en cache les recherches avec un hachage:

a = (1..1000000).to_a
h = Hash.new{|hash,key| hash[key] = true if a.include? key}

À peu près, il crée un constructeur par défaut pour les nouvelles valeurs de hachage, puis stocke "true" dans le cache s'il est dans le tableau (nil sinon). Cela permet un chargement paresseux dans le cache, juste au cas où vous n'utilisez pas tous les éléments.

0
zenazn

Si vous recherchez un équivalent de ce code Perl:

grep {$_ eq $element} @array

Vous pouvez simplement utiliser le simple code Ruby:

array.include?(element)
0
Sophie Alpert

Cela préserve les 0 si votre hachage était [0,0,0,1,0]

  hash = {}
  arr.each_with_index{|el, idx| hash.merge!({(idx + 1 )=> el }) }

Retour :

  # {1=>0, 2=>0, 3=>0, 4=>1, 5=>0}
0
Trip