Ruby/Подробнее о массивах
Подробнее о массивах
правитьМассивы — это тип данных, с которым вам придётся работать постоянно. Облик большинства программ зависит именно от правильного (читай: «изящного») использования массивов.
Способы создания массива
правитьМассив создаётся как минимум тремя способами. Первый способ:
[1, 2, 3, 4, 5, 6]
Вы просто перечисляете элементы массива через запятую, а границы массива обозначаете квадратными скобками. С таким методом создания массива мы уже встречались. А теперь попробуем второй способ, через вызов метода .new
класса Array
:
Array.new(6){ |index| index + 1 } #=> [1, 2, 3, 4, 5, 6]
Параметром метода .new
является количество элементов будущего массива (в данном случае это число 6). В фигурных скобках указано, как мы будем заполнять массив. В данном случае значение элемента массива будет больше на единицу его индекса. Третий способ заключается в создании объекта типа Range
(диапазон) и вызове метода .to_a
:
(1..6).to_a #=> [1, 2, 3, 4, 5, 6]
Есть ещё много способов, но эти три используются чаще всего.
Диапазоны
правитьМетодом to_a
очень удобно создавать из диапазона массив, содержащий упорядоченные элементы данного диапазона.
(1..10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
("a".."d").to_a #=> ["a", "b", "c", "d"]
Раз уж речь зашла о диапазонах, то давайте посмотрим, как они позволяют получать подмассивы. И насколько изящно у них это получается. Рассмотрим массив:
["a", "b", "c", "d", "e"]
Традиционно нумерация массива начинается с нуля и возрастает по одному:
["a" , "b" , "c" , "d" , "e" ]
Такая нумерация называется в Ruby положительной индексацией. «Хм, — скажете вы, — а есть ещё и отрицательная?» Да, есть!
["a" , "b" , "c" , "d" , "e" ]
Плюсы расставлены лишь для красоты. Но вернёмся к отрицательной индексации. Каков её смысл? Чтобы его пояснить, давайте решим задачку: дан массив, требуется получить предпоследний элемент.
array = ["a", "b", "c", "d", "e"]
array[array.size - 2] #=> "d"
В данном случае мы использовали метод .size
, который возвращает размер массива. Разработчики заметили, что вызов array.size
приходится писать довольно часто, и решили от него избавиться. Вот что получилось:
array = ["a", "b", "c", "d", "e"]
array[-2] #=> "d"
Индекс -2
значит «второй с конца элемент массива». Вот так и появилась отрицательная индексация. Теперь давайте разберёмся с диапазонами. Оказывается, в них тоже можно использовать отрицательную индексацию. Вот как можно получить все элементы массива кроме первого и последнего:
array = ["a", "b", "c", "d", "e"]
array[1..-2] #=> ["b", "c", "d"]
Или так:
array = ["a", "b", "c", "d", "e"]
array[1...-1] #=> ["b", "c", "d"]
Второй вариант с тремя точками, что автоматически приближает правую границу диапазона на одну позицию влево.
О двумерных массивах
правитьДля Ruby двумерный массив — это не более чем массив, содержащий одномерные массивы. Вот несколько примеров двумерных массивов:
[[1], [2, 3], [4]] # разная длина элементов-массивов
[[1, 2], [3, 4]] # одинаковая длина
[["прива", "Привет"], ["пока", "Всего хорошего"]] # двумерный массив (классика)
[["прива", "Привет"], [1, ["пока", "Всего хорошего"]]] # гибрид двух-трёх-мерного массива
- Двумерность массива средствами языка не отслеживается. Вполне могут возникнуть гибриды разномерных массивов.
- Подмассивы внутри двумерного массива могут иметь произвольную длину.
- Элементы из двумерного массива достаются последовательно: сначала элемент-массив, потом элемент.
Методы работы с массивами
правитьРазнообразие и полезность методов у массивов создаёт впечатление, что все сложные алгоритмы уже реализованы. Это не так, но программистам Ruby дана действительно обширная библиотека методов. Здесь мы рассмотрим лишь самые употребимые, остальные ищите в справочнике.
Получение размера массива
правитьВ Ruby массивы динамические: в каждый конкретный момент времени неизвестно, сколько в нём элементов. Чтобы не плодить тайн подобного рода и был реализован метод .size
:
[1, "считайте", 3, "количество", 5, 6, "запятых", 2, 5].size #=> 9
Мы явно указали массив, но на его месте могла стоять переменная:
array = [1, "считайте", 3, "количество", 5, 6, "запятых", 2, 5]
array.size #=> 9
Метод .size
есть у многих классов. Например, у ассоциативных массивов и строк. И даже у целых чисел.
Поиск максимального/минимального элемента
правитьВспомните сколько усилий вам приходилось прилагать, чтобы найти максимальный элемент? А сколько раз вы повторяли этот кусок кода в своих программах? Ну а в Ruby поиск максимального элемента осуществляется при помощи метода .max
, а в более сложных случаях при помощи метода .max_by
. Вот как это выглядит:
["у", "попа", "была", "собака"].max #=> "у" максимальный по значению
["у", "попа", "была", "собака"].max_by{ |elem| elem.size } #=> "собака" максимальный по размеру строки
Методы .min
и .min_by
работают аналогично:
["у", "попа", "была", "собака"].min #=> "была" минимальный по значению
["у", "попа", "была", "собака"].min_by{ |elem| elem.size } #=> "у" минимальный по размеру строки
Ну как? А в Ruby эти методы уже давно. Подробнее о том, как работает хеш "под капотом" можно дополнительно прочесть в статье.
Упорядочение
правитьЧтобы упорядочить массив, нужно вызвать метод .sort
или .sort_by
(начиная с версии 1.8).
["у", "попа", "была", "собака"].sort
#=> ["была", "попа", "собака", "у"] сортировка по значению
["у", "попа", "была", "собака"].sort_by{ |elem| elem.size }
#=> ["у", "попа", "была", "собака"] сортировка по размеру строки
Для двумерных массивов:
[[1,0], [16,6], [2,1], [4,5],[4,0],[5,6]].sort_by {|elem| elem[1]}
#=> [[1, 0], [4, 0], [2, 1], [4, 5], [16, 6], [5, 6]] сортировка "внешних" элементов по значению "внутренних"
[[1,0], [16,6], [2,1], [4,5],[4,0],[5,6]].sort_by {|elem| elem[0]}
#=> [[1, 0], [2, 1], [4, 0], [4, 5], [5, 6], [16, 6]]
Остается только добавить, что массивы упорядочиваются по возрастанию. Если вам надо по убыванию, то придётся писать собственный метод сортировки пузырьком. Шутка! По правде же, есть много способов выстроить массив по убыванию. Пока мы будем использовать метод .reverse
, обращающий массив.
Обращение массива
правитьОбращение массива — это изменение порядка элементов на обратный, то есть первый элемент становится последним, второй элемент — предпоследним и так далее.
Для обращения массива существует метод .reverse
. Применим его к предыдущим примерам, чтобы получить сортировку по убыванию:
["у", "попа", "была", "собака"].sort.reverse #=> ["у", "собака", "попа", "была"]
["у", "попа", "была", "собака"].sort_by{ |elem| elem.size }.reverse #=> ["собака", "была", "попа", "у"]
Метод .reverse
мы просто прицепили в конец предыдущего примера. Так можно выстроить произвольную цепочку допустимых методов; выполняться они будут по очереди, начиная с самого левого, то есть самого первого в цепочке.
Сложение/вычитание массивов
правитьДля сложения массивов, строк и чисел используется метод +
:
[1, 2, 3, 4] + [5, 6, 7] + [8, 9] #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
Плюс берёт массив справа и, будто это железнодорожный состав, прицепляет его к хвосту первого массива. Это называется конкатенацией.
Вычитаются массивы методом -
, но происходит это сложнее, чем расцепление вагонов:
[1, 1, 2, 2, 3, 3, 3, 4, 5] - [1, 2, 4] #=> [3, 3, 3, 5]
Из первого массива удаляются все элементы, имеющиеся во втором, независимо от их количества. Остальные элементы остаются без изменений, сохраняют относительные позиции.
Объединение и пересечение массивов (как множеств)
правитьОчень часто приходится решать задачи, в которых нужно оперировать множествами. У массивов припасено для этих целей два метода: |
(объединение) и &
(пересечение).
Рассмотрим объединение множеств в действии:
[1, 2, 3, 4, 5, 5, 6] | [0, 1, 2, 3, 4, 5, 7] #=> [1, 2, 3, 4, 5, 6, 0, 7]
Объединение получается вот так. Сначала массивы сцепляются:
[1, 2, 3, 4, 5, 5, 6, 0, 1, 2, 3, 4, 5, 7]
Затем, начиная с первого вагона, инспектор идёт от вагона к вагону, удаляя элементы, которые уже встречались. После зачистки получается настоящее логическое объединение.
На деле это выглядит так:
[1, 2, 3, 4, 5, -5-, 6, 0, -1-, -2-, -3-, -4-, -5-, 7]
Зачёркнутые числа — это удаленные элементы (дубликаты). Переходим от слов к делу:
[1, 2, 3, 4, 5, 5, 6] & [0, 2, 1, 3, 5, 4, 7] #=> [1, 2, 3, 4, 5]
При пересечении двух массивов, из первого удаляются все элементы, отсутствующие во втором. А из второго, отсутствующие в первом. При этом относительный порядок остающихся элементов первого массива сохраняется.
[1, 2, 3, 4, 5, -5-, -6-] & [-0-, 2, 1, 3, 5, 4, -7-]
Всё просто. Важно лишь помнить, что |
и &
не изменяют ни первый, ни второй исходные массивы. Они через описанные процедуры создают новый массив. Чтобы тот не уехал от вас, нужно присвоить его; но не себе, а переменной, приготовленной слева от =
.
В итоге пересечения или объединения множеств получается массив, не содержащий дубликатов.
Удаление дубликатов
правитьДля удаления дубликатов (повторяющихся элементов массива) в Ruby используется метод .uniq
:
[1, 2, 3, 4, 5, 5, 6, 0, 1, 2, 3, 4, 5, 7].uniq #=> [1, 2, 3, 4, 5, 6, 0, 7]
Процесс зачистки массива от дубликатов такой же, как и в объединении.
[1, 2, 3, 4, 5, -5-, 6, 0, -1-, -2-, -3-, -4-, -5-, 7]
Поэтому объединение массивов можно записать как
(array1 + array2).uniq
Но проще, конечно, объединять палкой.
Сплющивание массивов
правитьМетод .flatten
делает из многомерного массива простой, длинный одномерный массив. Он как бы расплющивает его. Например, чтобы найти в двумерном массиве наибольший элемент, мы сперва расплющим массив, а потом найдём максимум методом .max
:
array = [[1, 2], [3, 4]]
array.flatten.max #=> 4
Расплющивание происходит в несколько этапов. Сначала происходит удаление всех квадратных скобок.
-[[- 1, 2 -]-, -[- 3, 4 -]]-
А потом, две квадратные скобки добавляются слева и справа. Но делать это надо быстро, чтобы элементы не успели разбежаться.
[1, 2, 3, 4]
Вот и всё! У нас они разбежаться не успели. Повторите данное упражнение на других массивах (двумерных, трёхмерных и так далее).
Удаление неопределённых (nil) элементов
правитьФункцию удаления элементов nil
массива выполняет метод .compact
например:
array = [1, nil, 2, nil, 3]
array.compact #=> [1, 2, 3]
Транспонирование двумерного массива
правитьЗадача: дан двумерный массив. Вывести одномерный массив с максимумами каждого из столбцов. Хм… посмотрим сперва, как эта задача решается для строчек, а не столбцов:
array2D = [[1, 2], [3, 4]]
array2D.map{ |array| array.max } #=> [2, 4]
Метод .map
— это итератор, который позволяет нам делать что-нибудь с каждым объектом, на который указывает массив. Подробнее о них ниже в этой главе.
Чтобы решить задачу в первоначальном варианте, нам надо лишь предварительно транспонировать массив (поменять местами строки и столбцы):
array2D = [[1, 2], [3, 4]]
array2D.transpose.map{ |array| array.max } #=> [3, 4]
Метод .transpose
как раз и занимается транспонированием. Это позволяет с лёгкостью решать задачи про столбцы приёмами, схожими с задачами про строки.
Размножение массивов
правитьРечь пойдёт не о почковании, а о методе, который позволяет умножать массив на целое число. В результате такого умножения мы получим массив, состоящий из нескольких копий элементов исходного массива.
["много", "денег", "прячет", "тёща"] * 2
#=> ["много", "денег", "прячет", "тёща", "много", "денег", "прячет", "тёща"]
Того же самого эффекта можно добиться сцепив массив необходимое количество раз:
array = ["много", "денег", "прячет", "тёща"]
array + array #=> ["много", "денег", "прячет", "тёща", "много", "денег", "прячет", "тёща"]
Заметили, что есть некоторая параллель с целыми числами? Умножение можно заменить сложением и наоборот!
Функциональность стека
правитьЧасто и во многих алгоритмах надо добавить элемент в конец массива:
array = [1, 2, 3, 4, 5]
array[array.size] = 6
array #=> [1, 2, 3, 4, 5, 6]
И если уж добавили, то надо как-то его и удалить. Делается это примерно так:
array = [1, 2, 3, 4, 5, 6]
array[0...-1] #=> [1, 2, 3, 4, 5]
Но как всегда, эти задачи возникали слишком часто и их решили реализовать в виде методов. Методы назвали .push
(«втолкнуть» в конец массива) и .pop
(«вытолкнуть» элемент из массива):
array = [1, 2, 3, 4, 5]
array.push(6)
array #=> [1, 2, 3, 4, 5, 6]
array << 7 #=> [1, 2, 3, 4, 5, 6, 7], другой синтаксис
array.pop #=> 7
array #=> [1, 2, 3, 4, 5, 6]
Функциональность очереди и списка
правитьЧтобы можно было использовать массив в качестве очереди и/или списка, потребуется сделать всего лишь пару методов. Первый из них добавляет элемент в начало массива, а второй удаляет элемент из начала. Давайте посмотрим, как это делается универсальными методами []
, []=
и +
:
array = [1, 2, 3, 4, 5]
# добавим элемент в начало массива
# способ № 1
array = [6] + array
array #=> [6, 1, 2, 3, 4, 5]
array[0] #=> 6
# способ № 2
array[1..array.size] = array[0..-1] #=> [1, 1, 2, 3, 4, 5]
array[0] = 6
array #=> [6, 1, 2, 3, 4, 5]
# удалим элемент из начала массива
array = array[1..-1]
array #=> [1, 2, 3, 4, 5]
Теперь посмотрим, какие методы реализуют точно такую же функциональность:
array = [1, 2, 3, 4, 5]
# добавляем элемент в начало массива
array.unshift(6) #=> [6, 1, 2, 3, 4, 5]
# удаляем из начала массива
array.shift #=> 6
array #=> [1, 2, 3, 4, 5]
Удаляющий метод — .shift
(«сдвинуть»), а метод, добавляющий элемент в начало массива, называется .unshift
(непереводимое слово, означающее нечто противоположное «сдвинуть обратно»).
Мнемограмма для методов стека/очереди/списка
правитьМнемограмма для методов .shift
, .unshift
, .pop
и .push
:
.unshift(0) .push(6)
[1, 2, 3, 4, 5]
.shift .pop
Методы с параметром (сверху) добавляют элемент в массив, а методы без параметра (снизу) — удаляют. По желанию можно дорисовать стрелочки.
Метод .shift
сдвигает влево,
Метод .pop
— направо.
Метод .push
к концу цепляет,
А .unshift
— к началу.
Замечание. Имена методов unshift
/shift
неинтуитивны. Во-первых, они не напоминают о том, что работа идёт с головой массива, а не с хвостом, во-вторых ничего не говорят о том, идёт заполнение или опустошение стека. Можно создать для этих методов псевдонимы с говорящими именами, например, feed
/spit
(кормить/выплевывать):
class Array
alias feed :unshift
alias spit :shift
end
Создание своих классов, работающих как массивы
правитьЕсли потребуется написание своего класса, который работает, как описанные массивы, то возникают некоторые тонкости. Дело в том, что реализация всех описанных методов займёт жуткое количество времени и сил. На самом деле, для реализации большинства описанных методов, достаточно реализовать .each
.
Где это может понадобиться? Например, вы реализуете класс, который умеет читать из файла записи определённой структуры. Основную его логику занимает именно чтение нужного формата, кеширование, разбор, десериализация и тому подобное.
Просто реализуйте .each
и включите в ваш класс примесь Enumerable
. В нём находится реализация методов, таких как .inject
, .each_with_index
и тому подобные.
Логические методы
правитьЛогический метод — это метод, результатом которого является логическое выражение (true
или false
).
По японской традиции, имена логических методов принято заканчивать ?
(вопросительным знаком). Это позволяет также получить список логических методов, вызываемых в данном случае: просто отобрать из всех имеющихся методов те, что кончаются на ?
. Делается это при помощи небольшой вспомогательной программы:
array = [1, 2, 2, 3]
puts array.methods.grep(/\?$/)
Для удобства, можно упорядочить полученный список:
array = [1, 2, 2, 3]
puts array.methods.grep(/\?$/).sort
Есть ли элемент в массиве?
правитьКак узнать, есть ли некоторый элемент в массиве? Попробуем решить эту задачу при помощи метода .size
и итератора .find_all
:
array = [1, 2, 3, 4, 5, 6, 7]
required = 5 # число, которое мы будем искать
array.find_all{ |elem| elem == required }.size != 0 #=> true
# это значит, что такое число есть
Использование связки из трёх методов (!=
, .find_all
и .size
) для такой задачи — возмутительно! Разработчики не могли с этим долго мириться и реализовали метод специально для этой задачи. Имя ему — .include?
. Перепишем нашу задачу, но на этот раз будем использовать правильный метод:
array = [1, 2, 3, 4, 5, 6, 7]
required = 5 # число, которое мы будем искать
array.include?(required) #=> true
# что бы это значило?
Мутный горизонт скрывает берег,
Ветер мокр, холоден и лют.
Есть ли в озере акулы, я проверю
Методом логическим .include?
.
lake = ["правый берег", "ветер", "вода", "вода", "вода", "окунь", "вода", "вода", "левый берег"]
lake.include?("акула") #=> false
Опытным путём мы доказали, что акулы в озере не водятся.
Массив пустой?
правитьЕсли вы хотите задать массиву вопрос «пуст ли ты?», но боитесь обидеть, то можете пойти окружным путём. Например, спросить у него: ты равен пустому массиву?
empty_array = []
filled_array = [1, 2, 2, 3]
empty_array == [] #=> true
filled_array == [] #=> false
Ещё можно задать вопрос: твой размер равен нулю?
empty_array = []
filled_array = [1, 2, 2, 3]
empty_array.size == 0 #=> true
filled_array.size == 0 #=> false
Но наш вам совет: не стоит искать обходных путей. Спросите его напрямую: .empty?
(«пуст?»):
empty_array = []
filled_array = [1, 2, 2, 3]
empty_array.empty? #=> true
filled_array.empty? #=> false
И наоборот
правитьВ Ruby принято избегать отрицания условия. Например, если вам нужно сделать что-то, если массив не пуст, можно воспользоваться методом, обратным empty?
. Этот метод называется any?
.
array = [1, 2, 4]
array.length > 0 #=> true
array.empty? #=> false
array.any? #=> true
Итераторы
правитьМассивы — эти эшелоны переменных, эти ожерелья запятых и элементов — часто приходится проходить целиком, обследуя каждый элемент. Да и не только массивы, но и любые последовательности чего-нибудь.
В старину люди делали это циклами. В Ruby у списочных структур данных есть встроенные методы, которые проходят весь ряд поэлементно, но, в отличие от циклов:
- не зацикливаются: счётчик цикла нельзя докручивать;
- выполняются заведомое число раз;
- их много, и каждый делает своё дело.
Имя им — итераторы.
Изменение всех элементов массива
правитьИзменить все элементы массива можно по-всякому. Начнём с обнуления:
array = ["шифровка", "Штирлица", "в", "Центр", "секретно"]
array.map{ 0 } #=> [0, 0, 0, 0, 0]
В приведенном примере каждый элемент массива будет заменён нулем независимо от того, чем является этот элемент. Например, при попытке обнулить таким образом двумерный массив [[1, 2], [3, 4]]
, в результате получим [0, 0]
.
Используется итератор .map
, за которым следует замыкание, — кусочек кода, схваченный лапками-фигурными скобками. .map
последовательно проходит array
и выполняет замыкание заново для каждого элемента. То, что выходит из замыкания, итератор .map
делает очередным элементом нового массива.
Можно дать элементу .map
иное задание. Для этого зажимаем в фигурные скобы замыкания иной код:
array = [1, 2, 3, 4, 5]
array.map{ |elem| elem ** 2 } #=> [1, 4, 9, 16, 25]
Прежде, чем замыканию выдать квадрат очередного элемента, ему нужно знать этот элемент. Итератор .map
даёт ему значение элемента, словно фотографию, обрамлённую слева и справа вертикальными чертами |
. Чтобы замыкание смогло взять эту фотографию, обязательно нужно дать ей имя. В нашем случае это elem
, но подходят и такие названия:
_element
x
y
a
b
Но недопустимы названия вроде Like_This
или 3This
. Правила именования тут такие же, как и для обычных переменных.
Вы уже, наверное, хорошо поняли, что в итераторах массивы обрабатываются по очереди; двери вагона расходятся, появляется элемент. Замыкание даёт ему прозвище, выполняет код в лапках-фигурных скобках. Затем переходит к следующему вагону, и там всё сначала.
Но ещё важно помнить, что элементы не уходят из первого вагона: замыкание лишь осматривает каждый элемент, берёт его значение, но не меняет. Всё, что получается в результате работы замыкания, садится в очередной вагон другого поезда.
То имя, что показывается в раздвижных дверях, — это не сам элемент, это лишь его копия. Фотография. Голограмма. Это даже не другая переменная, это не переменная вообще. Бессмысленно присваивать новое значение фотографии:
array = [1, 2, 3, 4, 5]
array.map{ |elem| elem = elem**2 }
# присваивание не имеет смысла: elem несёт лишь значение элемента, не являясь им
Из итератора .map
выезжает другой поезд, у которого вместо соответствующего элемента первого поезда сидит результат вычисления замыкания. Используйте этот массив как-нибудь, иначе поезд уедет.
array = [1, 2, 3, 4, 5]
array.map{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 2, 3, 4, 5] — неизменный первый поезд
Можно присвоить его новой переменной array_of_squares
. А можно заместить им существующую переменную array
:
array = [1, 2, 3, 4, 5]
array = array.map{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 4, 9, 16, 25]
Это общее явление Ruby: методы (здесь — итераторы) не меняют объект (массив), с которым работают. Они лишь выдают результат, который потом можно использовать как аргумент или присвоить переменной.
Явление было воспето в фольклоре:
Метод .map
всё изменяет,
Как кто пожелает
И обижается на тех,
Кто результат не сохраняет.
Для того, чтобы сохранить результат выполнения метода в исходную переменную, нужно добавить к названию метода восклицательный знак.
array = [1, 2, 3, 4, 5]
array.map!{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 4, 9, 16, 25]
Такой приём работает и с многими другими методами в языке, которые только возвращают результат своего выполнения. Однако при использовании цепочек методов каждый должен быть с восклицательным знаком, иначе разорвётся цепочка копирований.
array = [1, 2, 3, 4, 5, nil]
array.compact.map!{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 2, 3, 4, 5, nil]
array.compact!.map!{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 4, 9, 16, 25]
В первом случае операция .compact создала копию массива, тогда как .compact! заменила первоначальные значения результатами, полученными от .map!
Отбор элементов по признаку
правитьВот как итератор .find_all
выберет из массива все чётные элементы:
array = [1, 2, 3, 4, 5]
array.find_all{ |elem| elem % 2 == 0 } #=> [2, 4]
elem % 2 == 0
— это вопрос «чётен ли elem
?». Ответом, как всегда, будет true
или false
. Ведь «чётность» — это равенство нулю (== 0
) остатка деления (%
) на 2
.
Кстати, равенство нулю можно проверять и при помощи метода .zero?
. А чётность тоже можно проверить разными способами:
(elem % 2).zero?
(elem & 1).zero?
(elem[0]).zero? # Этот вариант круче всех
Если на вопрос, заданный в замыкании, ответ true
, то |elem|
(значение очередного элемента исходного массива), заносится в новый массив, который в итоге будет выводом из итератора .find_all
.
Выражение в замыкании для .find_all
должно быть логическим, то есть принимать значение true
или false
.
Если нужно элементы
По условию искать,
То полезнее .find_all
Метод вам не отыскать!
Также есть методы .odd?
и .even?
для более наглядной реализации таких вещей, нечетный и четный соответственно.
array = [1, 2, 3, 4, 5]
array.find_all{ |elem| elem.even? } #=> [2, 4]
Суммирование/произведение/агрегация элементов
правитьОчень часто возникает задача найти сумму/произведение всех элементов массива. Для этих целей традиционно используется итератор .inject
. Для демонстрации его работы, давайте найдем сумму элементов массива:
array = [1, 2, 3, 4, 5]
array.inject(0){ |result, elem| result + elem } #=> 15
Рассмотрим все по порядку. Начнём с нуля. Его следует расшифровывать как result = 0
перед началом работы итератора, то есть это начальное значение переменной result
(переменной промежуточного результата).
Далее идёт объявление двух переменных. Первая из них (result
) будет хранить промежуточный результат. Вторая (elem
) — фотография текущего элемента массива (или последовательности), мы такую уже видели.
После объявления описан алгоритм работы итератора. В данном случае ему предписано каждый элемент массива складывать с промежуточной суммой: result + elem
.
Учитывая эти два замечания, напишем код, который является неправильным:
array = [1, 2, 3, 4, 5]
array.inject(0){ |result, elem| result = result + elem } #=> 15
Имена переменных result
и elem
созданы богатым воображением автора. В ваших программах они могут называться иначе.
Невероятно, но от изменения имён переменных результат не меняется. Помните это!
Для полноты картины решим ещё одну задачку. На этот раз будем искать произведение всех элементов массива:
array = [1, 2, 3, 4, 5]
array.inject(1){ |result, elem| result * elem } #=> 120
А вот так можем вычислить факториал для заданного числа (n!):
n = 9
(1..n).to_a.inject(){ |one, two| one * two } #=> 362880
Чтобы закрепить материал, решите задачу: найти произведение всех положительных элементов массива. Подсказка: используйте метод .find_all
.
Элементов надо кучу
Перемножить иль сложить?
Есть .inject
на этот случай,
Его бы вам употребить.
Разбиение надвое
правитьИтератор .partition
делит массив на две части по некоторому бинарному признаку (чётности, положительности, наличию высшего образования и тому подобным). Вот как разделить массив на две части по признаку кратности трём:
array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
array.partition{ |x| (x % 3).zero? } #=> [[3, 6, 9], [1, 2, 4, 5, 7, 8]]
В результате работы итератора получился массив, состоящий из двух элементов-массивов. Первый элемент-массив содержит все элементы, которые удовлетворяют условию, а второй, которые не удовлетворяют. Обратите внимание, как проверяется кратность трём. Ничего не напоминает? Например, итератор .find_all
? Нет? Ну и ладно.
Есть интересная хитрость, позволяющая разместить массив, полученный .partition
, в две разные переменные:
array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
one, two = array.partition{ |x| (x % 3).zero? }
one #=> [3, 6, 9]
two #=> [1, 2, 4, 5, 7, 8]
Этот метод называется множественным присваиванием (multiple assignment) и широко используется в ситуациях, когда из метода надо вернуть более одного значения. Но об этом позднее.
Логические итераторы
правитьВ версии 1.8 появилось несколько логических методов: .all?
и .any?
. Они положили начало такому классу методов, как логические итераторы.
Логический итератор — это итератор (метод, обрабатывающий все элементы последовательности), возвращающий значение логического типа — true
или false
.
Конечно же, идея логических итераторов долгое время летала в ноосфере. Существовали итераторы, которые являлись условно-логическими: они возвращали nil
в случае неудачи и какой-либо объект — в случае удачи. В логическом контексте поведение таких итераторов можно было посчитать логическим (false
→ nil
, а true
→ число/строка/любой объект). Примером условно-логического итератора служит метод .detect
.
Все ли элементы удовлетворяют условию?
правитьВ математической логике такой «итератор» называется квантором общности, обозначается символом . На языке Ruby он называется .all?
. По сложившейся традиции, давайте посмотрим, как решалась бы эта задача до версии 1.8, то есть до появления логических итераторов:
array = [1, 2, 2, 3]
array.inject(true){ |result, elem| result && (elem > 2) } #=> false
В примере используется так называемый механизм презумпции виновности. Переменной result
присваивается значение true
. Логическое умножение изменяет значение переменной result
на false
.
Данная программа проверяет, все ли элементы array
больше двух.
Ещё один вариант решения той же задачи:
array = [1, 2, 2, 3]
array.find_all{ |elem| elem <= 2 }.size.zero? #=> false
Давайте решим эту же задачу при помощи новоявленного логического итератора:
array = [1, 2, 2, 3]
array.all?{ |elem| elem > 2 } #=> false
Несмотря на то, что код получился короче, результат остался прежним: утверждение, что все элементы массива больше двух, ложно.
Хотя бы один элемент удовлетворяет условию?
правитьВслед за квантором общности (он же — логический итератор .all?
), из математической логики был перенесён и квантор существования — . На языке Ruby он называется .any?
. Чтобы оценить его по достоинству, посмотрим решение задачи без его участия. Проверим, содержит ли array
хотя бы один элемент больший двух:
array = [1, 2, 2, 3]
array.inject(false){ |result, elem| result || (elem > 2) } #=> true
В данном примере используется так называемый механизм презумпции невиновности. Переменной result
присваивается значение false
. В результате логического сложения, происходит изменение значения переменной result
на true
.
Теперь тоже самое, но через логический итератор .any?
:
array = [1, 2, 2, 3]
array.any?{ |elem| elem > 2 } #=> true
Естественно, что с появлением логических итераторов, реализация задач математической логики (в рамках языка Ruby) стала удобней.
Хитрости
правитьВот так можно сгенерировать «хороший пароль» — произвольную последовательность из чисел или латинских букв, общей длиной в 8 символов.
symbols = ["a".."z", "A".."Z", "0".."9"].map{ |range| range.to_a }.flatten
puts (0...8).map{ symbols.sample }.join
Метод sample возвращает случайный элемент из массива.
Перемешать упорядоченный массив:
array = [1, 2, 3, 4, 5, 6, 7]
array.sort_by{ rand } #=> перемешанный массив
Выстроить элементы массива по убыванию без использования .reverse
:
array = [2, 1, 3, 5, 6, 7, 4]
array.sort{ |x, y| y <=> x } #=> [7, 6, 5, 4, 3, 2, 1]
Задачи про массивы
правитьПомимо приведённых ниже задач, советуем вам взглянуть на задачи из нашего сборника задач.
Возможные варианты решений на Обсуждение:Ruby/Подробнее о массивах
Одномерные
править- Вывести индексы массива в том порядке, в котором соответствующие им элементы образуют возрастающую последовательность.
- В численном массиве найти сумму отрицательных элементов.
- Найти все индексы, по которым располагается максимальный элемент.
- В массиве переставить в начало элементы, стоящие на чётной позиции, а в конец — стоящие на нечётной.
Двумерные
править- Поменять первый и последний столбец массива местами.
- Упорядочить N-ый столбец.
- Упорядочить строки, содержащие максимальный элемент.
- Упорядочить строки, если они не отсортированы и перемешать, если они отсортированы.
- Упорядочить строки массива по значению элемента главной диагонали в каждой из строк (в исходном массиве).
- Найти номера строк, элементы которых упорядочены по возрастанию.
Двумерные целочисленные
править- Найти максимальный элемент для каждого столбца, а затем получить произведение этих элементов.
- Найти минимум в двумерном массиве.
- Найти произведение положительных элементов.
- Найти сумму положительных элементов, больших К.
- Вычислить сумму и среднее арифметическое элементов главной диагонали.
- Найти номера строк, все элементы которых — нули.