У Ruby є одинадцять методів пошуку елементів у масиві.
Кращим є include?
або, для повторного доступу, створити набір, а потім зателефонувати 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?
є кращим способом. Він використовує 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) часову складність, оскільки обидва шукають масив для першого появи очікуваного значення.
Ми можемо використовувати набір, щоб отримати час доступу O (1) ціною, щоб спочатку створити Hash-представлення масиву. Якщо ви неодноразово перевіряєте членство в одному масиві, ця початкова інвестиція може швидко окупитися. Set
не реалізовано в C, але як звичайний клас Ruby, все-таки час доступу O (1) базового @hash
робить це вартим.
Ось реалізація класу 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?
який реалізується з часом доступу O (1) в класі Hash.
Я не буду обговорювати інші сім методів, оскільки всі вони менш ефективні.
Насправді є ще більше методів із складністю O (n) поза 11 перелічених вище, але я вирішив їх не перераховувати, оскільки вони сканують весь масив, а не розбивають у першому матчі.
Не використовуйте такі:
# bad examples
array.grep(element).any?
array.select { |each| each == element }.size > 0
...