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
如果元素存在,它们都会返回trueish 值。
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
...