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?
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)
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 utilisantto_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?
etObject#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é.
essaye celui-là:
a=[1,2,3]
Hash[a.Zip]
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}
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.
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.
Rampion m'a battu. Set pourrait être la réponse.
Tu peux faire:
require 'set'
set = array.to_set
set.include?(x)
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}
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
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")?
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.
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
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.
Si vous recherchez un équivalent de ce code Perl:
grep {$_ eq $element} @array
Vous pouvez simplement utiliser le simple code Ruby:
array.include?(element)
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}