我有一个值“狗”和一个数组[“猫”、“狗”、“鸟”]。
如何在不循环的情况下检查数组中是否存在它?是否有一种简单的方法来检查该值是否存在?
我有一个值“狗”和一个数组[“猫”、“狗”、“鸟”]。
如何在不循环的情况下检查数组中是否存在它?是否有一种简单的方法来检查该值是否存在?
当前回答
Ruby有十一种方法来查找数组中的元素。
首选项包括?或者,对于重复访问,创建一个Set,然后调用include?或成员?。
以下是所有这些:
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
如果元素存在,它们都会返回真实值。
包括是优选的方法。它在内部使用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;
}
成员未在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
两者都包括?和成员?因为两者都在阵列中搜索期望值的第一次出现,所以具有O(n)时间复杂度。
我们可以使用Set来获得O(1)访问时间,代价是必须首先创建数组的Hash表示。如果您重复检查同一阵列的成员资格,那么最初的投资可以很快得到回报。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
...
其他回答
有一个入口?正如@campeterson所指出的,ActiveSupport(Rails的一部分)中的方法。因此,在Rails中,或者如果您需要“active_support”,您可以编写:
'Unicorn'.in?(['Cat', 'Dog', 'Bird']) # => false
OTOH,没有in运算符或#in?尽管Ruby内核的顶级成员Yusuke Endoh之前就提出过这种方法,但这种方法在Ruby本身也是如此。
正如其他人所指出的,相反的方法包括?存在,对于所有枚举,包括Array、Hash、Set、Range:
['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
快速测试显示,呼叫包括?在10个元素上调用Set比在等效数组上调用Set快3.5倍(如果找不到元素)。
最后一点:使用include时要小心?在Range上,有一些微妙之处,所以请参考文档并与封面进行比较?。。。
如果不想循环,则无法使用数组进行循环。您应该改用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
设置在内部像Hashes一样工作,因此Ruby不需要在集合中循环查找项,因为顾名思义,它会生成键的哈希值,并创建内存映射,以便每个哈希值指向内存中的某个点。前面的示例使用Hash完成:
fake_array = {}
100.times{|i| fake_array["foo#{i}"] = 1}
fake_array.has_key?("foo99")
=> true
缺点是Set和Hash键只能包含唯一的项,如果你添加了很多项,Ruby将不得不在一定数量的项之后重新散列整个项,以构建适合更大键空间的新映射。有关这方面的更多信息,我建议您观看“MountainWest RubyConf 2014-Nathan Long自制哈希中的大O”。
这里有一个基准:
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)
我总是觉得运行一些基准测试来查看各种方法的相对速度很有趣。
在开始、中间或结束处查找数组元素将影响任何线性搜索,但几乎不会影响对集合的搜索。
将一个数组转换为一个集合会导致处理时间的减少,所以从一个数组创建一次集合,或者从一开始就创建一个集合。
以下是基准代码:
# 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越小,Array方法运行得越快,但它们仍然无法跟上,尽管在小数组中,差异可能很小。
“First”、“Middle”和“Last”反映了所搜索元素在ARRAY中使用First、size/2和Last。搜索ARRAY和SET变量时将使用该元素。
对与>0进行比较的方法进行了轻微更改,因为索引类型测试的测试应为>=0。
有关Fruity及其方法的更多信息,请参见其README。
如果你想检查一个街区,你可以试试吗?还是全部?。
%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。
我的灵感来自于“评估数组中是否有任何项目”
这不仅会告诉你它存在,还会告诉你它出现的次数:
a = ['Cat', 'Dog', 'Bird']
a.count("Dog")
#=> 1