我有一个值 'Dog'
和一个数组 ['Cat', 'Dog', 'Bird']
。
如何在不循环遍历的情况下检查它是否存在于数组中?有没有一种简单的方法来检查值是否存在,仅此而已?
hash = arr.map {|x| [x,true]}.to_h
,现在检查 hash.has_key? 'Dog'
是否返回 true
正如@campaterson 所指出的,自 v3.1 以来,ActiveSupport
(Rails 的一部分)中有一个 in?
method。所以在 Rails 中,或者如果您 require 'active_support'
,您可以编写:
'Unicorn'.in?(['Cat', 'Dog', 'Bird']) # => false
OTOH,Ruby 本身没有 in
运算符或 #in?
方法,尽管它之前已经提出,但 in particular by Yusuke Endoh 是 ruby-core 的顶级成员。
正如其他人所指出的,对于包括 Array
、Hash
、Set
、Range
在内的所有 Enumerable
,都存在反向方法 include?
:
['Cat', 'Dog', 'Bird'].include?('Unicorn') # => false
请注意,如果您的数组中有许多值,它们将一个接一个地被检查(即 O(n)
),而查找散列将是恒定时间(即 O(1)
)。因此,例如,如果您的数组是常量,则最好使用 Set。例如:
require 'set'
ALLOWED_METHODS = Set[:to_s, :to_i, :upcase, :downcase
# etc
]
def foo(what)
raise "Not allowed" unless ALLOWED_METHODS.include?(what.to_sym)
bar.send(what)
end
quick test 表明在 10 元素 Set
上调用 include?
比在等效的 Array
上调用它快大约 3.5 倍(如果未找到该元素)。
最后的结束说明:在 Range
上使用 include?
时要小心,有一些微妙之处,因此请参阅 the doc 并与 cover?
进行比较...
#in?
,但如果您使用的是 Rails,它是可用的。 api.rubyonrails.org/classes/Object.html#method-i-in-3F(我知道这是一个 Ruby,而不是 Rails 问题,但它可能对希望在 Rails 中使用 #in?
的任何人有所帮助。看起来它是在 Rails 3.1 中添加的apidock.com/rails/Object/in%3F
Set
),如果这很重要的话。这个答案得到了我的支持,尽管我可能在最后留下了关于 in?
的部分。
尝试
['Cat', 'Dog', 'Bird'].include?('Dog')
如果要按块检查,可以尝试 any?
或 all?
。
%w{ant bear cat}.any? {|word| word.length >= 3} #=> true
%w{ant bear cat}.any? {|word| word.length >= 4} #=> true
[ nil, true, 99 ].any? #=> true
有关详细信息,请参阅 Enumerable。
我的灵感来自“evaluate if array has any items in ruby”
使用 Enumerable#include
:
a = %w/Cat Dog Bird/
a.include? 'Dog'
或者,如果完成了许多测试,1 您可以摆脱循环(甚至 include?
也有)并从 O(n) 转到 O(1) 与:
h = Hash[[a, a].transpose]
h['Dog']
1. 我希望这是显而易见的,但要避免反对:是的,对于一些查找,Hash[] 和转置操作在配置文件中占主导地位,并且每个都是 O(n) 本身。
Ruby 有十一种方法来查找数组中的元素。
首选的是 include?
,或者,对于重复访问,创建一个 Set,然后调用 include?
或 member?
。
以下是所有这些:
array.include?(element) # preferred method
array.member?(element)
array.to_set.include?(element)
array.to_set.member?(element)
array.index(element) > 0
array.find_index(element) > 0
array.index { |each| each == element } > 0
array.find_index { |each| each == element } > 0
array.any? { |each| each == element }
array.find { |each| each == element } != nil
array.detect { |each| each == element } != nil
如果元素存在,它们都会返回一个 true
ish 值。
include?
是首选方法。它在内部使用 C 语言 for
循环,当元素与内部 rb_equal_opt/rb_equal
函数匹配时会中断。除非您为重复的成员资格检查创建一个集合,否则它不会变得更有效率。
VALUE
rb_ary_includes(VALUE ary, VALUE item)
{
long i;
VALUE e;
for (i=0; i<RARRAY_LEN(ary); i++) {
e = RARRAY_AREF(ary, i);
switch (rb_equal_opt(e, item)) {
case Qundef:
if (rb_equal(e, item)) return Qtrue;
break;
case Qtrue:
return Qtrue;
}
}
return Qfalse;
}
member?
未在 Array
类中重新定义,而是使用 Enumerable
模块中未优化的实现,该实现逐字枚举所有元素:
static VALUE
member_i(RB_BLOCK_CALL_FUNC_ARGLIST(iter, args))
{
struct MEMO *memo = MEMO_CAST(args);
if (rb_equal(rb_enum_values_pack(argc, argv), memo->v1)) {
MEMO_V2_SET(memo, Qtrue);
rb_iter_break();
}
return Qnil;
}
static VALUE
enum_member(VALUE obj, VALUE val)
{
struct MEMO *memo = MEMO_NEW(val, Qfalse, 0);
rb_block_call(obj, id_each, 0, 0, member_i, (VALUE)memo);
return memo->v2;
}
转换为 Ruby 代码,它执行以下操作:
def member?(value)
memo = [value, false, 0]
each_with_object(memo) do |each, memo|
if each == memo[0]
memo[1] = true
break
end
memo[1]
end
include?
和 member?
都具有 O(n) 时间复杂度,因为它们都在数组中搜索第一次出现的期望值。
我们可以使用 Set 来获得 O(1) 访问时间,代价是必须首先创建数组的哈希表示。如果您反复检查同一阵列上的成员资格,则此初始投资可以很快得到回报。 Set
不是在 C 中实现的,而是作为普通的 Ruby 类实现的,底层 @hash
的 O(1) 访问时间仍然值得这样做。
下面是 Set 类的实现:
module Enumerable
def to_set(klass = Set, *args, &block)
klass.new(self, *args, &block)
end
end
class Set
def initialize(enum = nil, &block) # :yields: o
@hash ||= Hash.new
enum.nil? and return
if block
do_with_enum(enum) { |o| add(block[o]) }
else
merge(enum)
end
end
def merge(enum)
if enum.instance_of?(self.class)
@hash.update(enum.instance_variable_get(:@hash))
else
do_with_enum(enum) { |o| add(o) }
end
self
end
def add(o)
@hash[o] = true
self
end
def include?(o)
@hash.include?(o)
end
alias member? include?
...
end
如您所见,Set 类只是创建了一个内部 @hash
实例,将所有对象映射到 true
,然后使用 Hash#include?
检查成员资格,这在 Hash 类中使用 O(1) 访问时间实现。
我不会讨论其他七种方法,因为它们的效率都较低。
实际上,除了上面列出的 11 种方法之外,还有更多具有 O(n) 复杂度的方法,但我决定不列出它们,因为它们扫描整个数组而不是在第一次匹配时中断。
不要使用这些:
# bad examples
array.grep(element).any?
array.select { |each| each == element }.size > 0
...
11
方式。首先,您很难将 index
和 find_index
(或 find
和 detect
)算作单独的方法,因为它们只是同一方法的不同名称。其次,所有以 > 0
结尾的表达式都是不正确的,我敢肯定这是一个疏忽。 (续)
arr.index(e)
例如,如果 arr[0] == e
则返回 0
。如果 e
不存在,您会记得 arr.index(e)
返回 nil
。但是,如果在 arr
中搜索 nil
,则不能使用 index
。 (与 rindex
相同的问题,未列出。)。将数组转换为集合然后使用集合方法有点牵强。为什么不转换为散列(使用数组中的键和任意值),然后使用散列方法?即使转换为集合是可以的,也可以使用其他集合方法,例如 !arr.to_set.add?(e)
。 (续)
arr.count(e) > 0
、arr != arr.dup.delete(e)
、arr != arr - [e]
和 arr & [e] == [e]
。也可以使用 select
和 reject
。
some_array.exclude?('some_string')
也很有用。
有几个答案建议使用 Array#include?
,但有一个重要的警告:查看源代码,即使 Array#include?
确实执行循环:
rb_ary_includes(VALUE ary, VALUE item)
{
long i;
for (i=0; i<RARRAY_LEN(ary); i++) {
if (rb_equal(RARRAY_AREF(ary, i), item)) {
return Qtrue;
}
}
return Qfalse;
}
在不循环的情况下测试单词存在的方法是为您的数组构造一个 trie。那里有许多 trie 实现(谷歌“ruby trie”)。我将在此示例中使用 rambling-trie
:
a = %w/cat dog bird/
require 'rambling-trie' # if necessary, gem install rambling-trie
trie = Rambling::Trie.create { |trie| a.each do |e| trie << e end }
现在我们已经准备好在 O(log n)
时间内测试数组中各种单词的存在,而无需循环它,使用与 Array#include?
相同的句法简单性,使用次线性 Trie#include?
:
trie.include? 'bird' #=> true
trie.include? 'duck' #=> false
a.each do ... end
嗯...不确定这不是循环
Set#include?
;再加上使用符号而不是字符串,它可以是 O(1) 平均情况(如果你使用字符串,那么只计算散列是 O(n),其中 n 是字符串的长度)。或者,如果您想使用第三方库,您可以使用 O(1) 最坏情况的完美哈希。
Set
使用散列来索引其成员,因此实际上 Set#include?
应该对于分布良好的 Set
复杂度 O(1)(更具体地说是 O(input-size)散列和 O(log(n/bucket-number)) 用于搜索)
如果您不想循环,则无法使用数组来执行此操作。您应该改用 Set 。
require 'set'
s = Set.new
100.times{|i| s << "foo#{i}"}
s.include?("foo99")
=> true
[1,2,3,4,5,6,7,8].to_set.include?(4)
=> true
集合在内部像哈希一样工作,因此 Ruby 不需要遍历集合来查找项目,因为顾名思义,它生成键的哈希并创建内存映射,以便每个哈希指向内存中的某个点。上一个使用 Hash 完成的示例:
fake_array = {}
100.times{|i| fake_array["foo#{i}"] = 1}
fake_array.has_key?("foo99")
=> true
缺点是 Sets 和 Hash 键只能包含唯一项,如果您添加很多项,Ruby 将不得不在一定数量的项之后重新散列整个事物以构建适合更大键空间的新映射。有关这方面的更多信息,我建议您观看“MountainWest RubyConf 2014 - Big O in a Homemade Hash by Nathan Long”。
这是一个基准:
require 'benchmark'
require 'set'
array = []
set = Set.new
10_000.times do |i|
array << "foo#{i}"
set << "foo#{i}"
end
Benchmark.bm do |x|
x.report("array") { 10_000.times { array.include?("foo9999") } }
x.report("set ") { 10_000.times { set.include?("foo9999") } }
end
结果:
user system total real
array 7.020000 0.000000 7.020000 ( 7.031525)
set 0.010000 0.000000 0.010000 ( 0.004816)
include?
不会在第一次命中时停止?
include?
在第一次命中时停止,但如果该命中在列表末尾....任何依赖数组进行存储的解决方案都会随着列表的增长而降低性能,尤其是当必须在列表的末尾。 Hash 和 Set 没有这个问题,有序列表和二进制搜索也没有。
这是执行此操作的另一种方法:使用 Array#index
方法。
它返回数组中第一次出现的元素的索引。
例如:
a = ['cat','dog','horse']
if a.index('dog')
puts "dog exists in the array"
end
index()
也可以占用一个块:
例如:
a = ['cat','dog','horse']
puts a.index {|x| x.match /o/}
这将返回包含字母 'o' 的数组中第一个单词的索引。
index
仍然遍历数组,它只是返回元素的值。
有趣的事实,
您可以使用 *
检查 case
表达式中的数组成员资格。
case element
when *array
...
else
...
end
注意 when 子句中的小 *
,它检查数组中的成员。
splat 运算符的所有常见魔术行为都适用,例如,如果 array
实际上不是数组而是单个元素,它将匹配该元素。
when
中使用它,这样其他更快的检查就会很快被淘汰。
有多种方法可以实现这一点。其中一些如下:
a = [1,2,3,4,5]
2.in? a #=> true
8.in? a #=> false
a.member? 1 #=> true
a.member? 8 #=> false
Object#in?
仅添加到 Rails(即 ActiveSupport
)v3.1+。它在核心 Ruby 中不可用。
检查存在
使用include?
例子:
arr = [1, 2, 3]
arr.include?(1) -> true
arr.include?(4) -> false
支票不存在
使用exclude?
例子:
arr = %w(vietnam china japan)
arr.exclude?('usa') -> true
arr.exclude?('china') -> false
*.include?("some-string")
也适用于数组项的 exact 字符串匹配。
这不仅会告诉您它存在,还会告诉您它出现了多少次:
a = ['Cat', 'Dog', 'Bird']
a.count("Dog")
#=> 1
.any?
将在找到第一个匹配元素后立即返回,.count
将始终处理整个数组。
你可以试试:
示例:如果数组中存在 Cat 和 Dog:
(['Cat','Dog','Bird'] & ['Cat','Dog'] ).size == 2 #or replace 2 with ['Cat','Dog].size
代替:
['Cat','Dog','Bird'].member?('Cat') and ['Cat','Dog','Bird'].include?('Dog')
注意:member?
和 include?
相同。
这可以在一条线上完成工作!
如果您需要多次检查任何键,请将 arr
转换为 hash
,然后检查 O(1)
arr = ['Cat', 'Dog', 'Bird']
hash = arr.map {|x| [x,true]}.to_h
=> {"Cat"=>true, "Dog"=>true, "Bird"=>true}
hash["Dog"]
=> true
hash["Insect"]
=> false
Hash#has_key? 与 Array#include? 的性能
Parameter Hash#has_key? Array#include Time Complexity O(1) operation O(n) operation Access Type Accesses Hash[key] if it Iterates through each element returns any value then of the array till it true is returned to the finds the value in Array Hash#has_key? call call
对于使用 include?
的单次检查很好
对于它的价值,Ruby docs 是解决这类问题的绝佳资源。
我还会记下您正在搜索的数组的长度。 include?
方法将运行复杂度为 O(n) 的线性搜索,根据数组的大小可能会变得非常丑陋。
如果您正在使用大型(已排序)数组,我会考虑编写一个 binary search algorithm,它应该不会太难并且最坏的情况是 O(log n)。
或者,如果您使用的是 Ruby 2.0,则可以利用 bsearch
。
<=>
相当,但情况并非总是如此。例如,假设数组的元素是散列。
如果我们不想使用 include?
,这也可以:
['cat','dog','horse'].select{ |x| x == 'dog' }.any?
这条路怎么样?
['Cat', 'Dog', 'Bird'].index('Dog')
['Cat', 'Dog', 'Bird'].detect { |x| x == 'Dog'}
=> "Dog"
!['Cat', 'Dog', 'Bird'].detect { |x| x == 'Dog'}.nil?
=> true
['Cat', nil, 'Dog'].detect { |x| x == nil } #=> nil
。找到 nil
了吗?
如果您尝试在 MiniTest 单元测试中执行此操作,则可以使用 assert_includes
。例子:
pets = ['Cat', 'Dog', 'Bird']
assert_includes(pets, 'Dog') # -> passes
assert_includes(pets, 'Zebra') # -> fails
还有另一种方法。
假设数组是 [ :edit, :update, :create, :show ]
,也许是整个 七种致命/宁静的罪。
进一步玩弄从某个字符串中提取有效动作的想法:
"my brother would like me to update his profile"
然后:
[ :edit, :update, :create, :show ].select{|v| v if "my brother would like me to update his profile".downcase =~ /[,|.| |]#{v.to_s}[,|.| |]/}
/[,|.| |]#{v.to_s}[,|.| |]/
让我觉得您想找到“被以下之一包围的动作名称:逗号、句号、空格或什么都没有”,但有一些细微的错误。 "|update|"
将返回 [:update]
,而 "update"
将返回 []
。字符类 ([...]
) 不使用管道 (|
) 来分隔字符。即使我们将它们更改为组((...)
),您也无法匹配空字符。所以你可能想要的正则表达式是 /(,|\.| |^)#{v.to_s}(,|\.| |$)/
/[,. ]/
我总是觉得运行一些基准来查看各种做事方式的相对速度很有趣。
在开头、中间或结尾查找数组元素会影响任何线性搜索,但几乎不会影响对 Set 的搜索。
将 Array 转换为 Set 会缩短处理时间,因此从 Array 中创建 Set 一次,或者从一开始就从 Set 开始。
这是基准代码:
# frozen_string_literal: true
require 'fruity'
require 'set'
ARRAY = (1..20_000).to_a
SET = ARRAY.to_set
DIVIDER = '-' * 20
def array_include?(elem)
ARRAY.include?(elem)
end
def array_member?(elem)
ARRAY.member?(elem)
end
def array_index(elem)
ARRAY.index(elem) >= 0
end
def array_find_index(elem)
ARRAY.find_index(elem) >= 0
end
def array_index_each(elem)
ARRAY.index { |each| each == elem } >= 0
end
def array_find_index_each(elem)
ARRAY.find_index { |each| each == elem } >= 0
end
def array_any_each(elem)
ARRAY.any? { |each| each == elem }
end
def array_find_each(elem)
ARRAY.find { |each| each == elem } != nil
end
def array_detect_each(elem)
ARRAY.detect { |each| each == elem } != nil
end
def set_include?(elem)
SET.include?(elem)
end
def set_member?(elem)
SET.member?(elem)
end
puts format('Ruby v.%s', RUBY_VERSION)
{
'First' => ARRAY.first,
'Middle' => (ARRAY.size / 2).to_i,
'Last' => ARRAY.last
}.each do |k, element|
puts DIVIDER, k, DIVIDER
compare do
_array_include? { array_include?(element) }
_array_member? { array_member?(element) }
_array_index { array_index(element) }
_array_find_index { array_find_index(element) }
_array_index_each { array_index_each(element) }
_array_find_index_each { array_find_index_each(element) }
_array_any_each { array_any_each(element) }
_array_find_each { array_find_each(element) }
_array_detect_each { array_detect_each(element) }
end
end
puts '', DIVIDER, 'Sets vs. Array.include?', DIVIDER
{
'First' => ARRAY.first,
'Middle' => (ARRAY.size / 2).to_i,
'Last' => ARRAY.last
}.each do |k, element|
puts DIVIDER, k, DIVIDER
compare do
_array_include? { array_include?(element) }
_set_include? { set_include?(element) }
_set_member? { set_member?(element) }
end
end
在我的 Mac OS 笔记本电脑上运行时,会导致:
Ruby v.2.7.0
--------------------
First
--------------------
Running each test 65536 times. Test will take about 5 seconds.
_array_include? is similar to _array_index
_array_index is similar to _array_find_index
_array_find_index is faster than _array_any_each by 2x ± 1.0
_array_any_each is similar to _array_index_each
_array_index_each is similar to _array_find_index_each
_array_find_index_each is faster than _array_member? by 4x ± 1.0
_array_member? is faster than _array_detect_each by 2x ± 1.0
_array_detect_each is similar to _array_find_each
--------------------
Middle
--------------------
Running each test 32 times. Test will take about 2 seconds.
_array_include? is similar to _array_find_index
_array_find_index is similar to _array_index
_array_index is faster than _array_member? by 2x ± 0.1
_array_member? is faster than _array_index_each by 2x ± 0.1
_array_index_each is similar to _array_find_index_each
_array_find_index_each is similar to _array_any_each
_array_any_each is faster than _array_detect_each by 30.000000000000004% ± 10.0%
_array_detect_each is similar to _array_find_each
--------------------
Last
--------------------
Running each test 16 times. Test will take about 2 seconds.
_array_include? is faster than _array_find_index by 10.000000000000009% ± 10.0%
_array_find_index is similar to _array_index
_array_index is faster than _array_member? by 3x ± 0.1
_array_member? is faster than _array_find_index_each by 2x ± 0.1
_array_find_index_each is similar to _array_index_each
_array_index_each is similar to _array_any_each
_array_any_each is faster than _array_detect_each by 30.000000000000004% ± 10.0%
_array_detect_each is similar to _array_find_each
--------------------
Sets vs. Array.include?
--------------------
--------------------
First
--------------------
Running each test 65536 times. Test will take about 1 second.
_array_include? is similar to _set_include?
_set_include? is similar to _set_member?
--------------------
Middle
--------------------
Running each test 65536 times. Test will take about 2 minutes.
_set_member? is similar to _set_include?
_set_include? is faster than _array_include? by 1400x ± 1000.0
--------------------
Last
--------------------
Running each test 65536 times. Test will take about 4 minutes.
_set_member? is similar to _set_include?
_set_include? is faster than _array_include? by 3000x ± 1000.0
基本上,如果我要搜索包含,结果告诉我对所有内容都使用 Set ,除非我可以保证第一个元素是我想要的元素,这不太可能。将元素插入哈希时会有一些开销,但是搜索时间要快得多,我认为这不应该是一个考虑因素。同样,如果您需要搜索它,请不要使用 Array,请使用 Set。 (或哈希。)
Array 越小,Array 方法运行得越快,但它们仍然跟不上,尽管在小数组中差异可能很小。
“First”、“Middle”和“Last”反映了对正在搜索的元素的 ARRAY
使用 first
、size / 2
和 last
。搜索 ARRAY
和 SET
变量时将使用该元素。
对与 > 0
进行比较的方法进行了少量更改,因为对于 index
类型测试,测试应该是 >= 0
。
有关 Fruity 及其方法的更多信息,请参阅其 README。
如果要返回的值不仅仅是真或假,请使用
array.find{|x| x == 'Dog'}
如果它存在于列表中,这将返回 'Dog',否则返回 nil。
array.any?{|x| x == 'Dog'}
。
如果您不想使用 include?
您可以先将元素包装在一个数组中,然后检查被包装的元素是否等于数组和被包装元素的交集。这将返回一个基于相等的布尔值。
def in_array?(array, item)
item = [item] unless item.is_a?(Array)
item == array & item
end
这是另一种方法:
arr = ['Cat', 'Dog', 'Bird']
e = 'Dog'
present = arr.size != (arr - [e]).size
arr != arr - [e]
。 arr & [e] == [e]
是同理的另一种方式。
它有很多方法可以在任何数组中查找元素,但最简单的方法是“在?”方法。
example:
arr = [1,2,3,4]
number = 1
puts "yes #{number} is present in arr" if number.in? arr
array = [ 'Cat', 'Dog', 'Bird' ]
array.include?("Dog")
不定期副业成功案例分享
%w(Cat Dog Bird).include? 'Dog'
#include?
仍然执行循环。不过,编码器免于显式编写循环。我添加了一个真正执行任务而不循环的答案。[ 'Dog', 'Bird', 'Cat' ].has? 'Dog'