Программирование на языке Ruby

Фултон Хэл

Глава 5. Численные методы

 

 

Числа — самый первичный тип данных, естественный для любого компьютера. Придется сильно постараться, чтобы найти такую область знания, в которой нет места числам. Будь вы бухгалтером или конструктором воздухоплавательных аппаратов, без чисел вам не обойтись. В этой главе мы обсудим различные способы обработки, преобразования и анализа числовых данных.

Как и всякий современный язык, Ruby прекрасно умеет работать с любыми числами — как целыми, так и с плавающей точкой. В нем есть полный набор ожидаемых математических операторов и функций, а вместе с тем и кое-какие приятные сюрпризы: классы Bignum, BigDecimal и Rational.

Помимо средств для манипуляции числами, имеющихся в системной и стандартной библиотеках, мы рассмотрим более специфические темы (тригонометрия, математический анализ и статистика). Примеры приведены не только для справки, но и как образцы кода на языке Ruby, иллюстрирующие принципы, изложенные в других частях книги.

 

5.1. Представление чисел в языке Ruby

Если вы знакомы с любым другим языком программирования, то представление чисел в Ruby не вызовет у вас никакого удивления. Объект класса Fixnum может представлять число со знаком или без знака:

237  # Число без знака (положительное).

+237 # То же, что и выше.

-237 # Отрицательное число.

Если число длинное, то между любыми цифрами можно вставлять знак подчеркивания. Это сделано исключительно для удобства, назначении константы никак не сказывается. Обычно подчерки вставляются в те же места, где бухгалтеры вставляют пробелы:

1048576   # Число в обычной записи.

1_048_576 # То же самое значение.

Целые числа можно представлять и в других системах счисления (по основанию 2, 8 и 16). Для этого в начале ставятся префиксы 0b, 0 и 0х соответственно.

0b10010110 # Двоичное.

0b1211     # Ошибка!

01234      # Восьмеричное (основание 8).

01823      # Ошибка!

0xdeadbeef # Шестнадцатеричное (основание 16) .

0xDEADBEEF # То же самое.

0xdeadpork # Ошибка!

В числах с плавающей точкой десятичная точка должна присутствовать, а показатель степени, возможно со знаком, необязателен:

3.14         # Число пи, округленное до сотых.

-0.628       # -2*pi, поделенное на 10, округленное до тысячных.

6.02е23      # Число Авогадро.

6.626068е-34 # Постоянная Планка.

В классе Float есть константы, определяющие минимальные и максимальные значения чисел с плавающей точкой. Они машиннозависимы. Вот некоторые наиболее важные:

Float::MIN     # 2.2250738585072е-308 (на конкретной машине)

Float::МАХ     # 1.79769313486232е+308

Float::EPSILON # 2.22044604925031е-16

 

5.2. Основные операции над числами

Обычные операции сложения, вычитания, умножения и деления в Ruby, как и во всех распространенных языках программирования, обозначаются операторами +, -, *, /. Операторы в большинстве своем реализованы в виде методов (и потому могут быть переопределены).

Возведение в степень обозначается оператором **, как в языках BASIC и FORTRAN. Эта операция подчиняется обычным математическим правилам.

а = 64**2   # 4096

b = 64**0.5 # 8.0

с = 64**0   # 1

d = 64**-1  # 0.015625

При делении одного целого числа на другое дробная часть результата отбрасывается. Это не ошибка, так и задумано. Если вы хотите получить результат с плавающей точкой, позаботьтесь о том, чтобы хотя бы один из операндов был числом c плавающей точкой.

3 / 3     # 3

5 / 3     # 1

3 / 4     # 0

3.0 / 4   # 0.75

3 / 4.0   # 0.75

3.0 / 4.0 # 0.75

Если вы работаете с переменными и сомневаетесь относительно их типа, воспользуйтесь приведением типа к Float или методом to_f:

z = x.to_f / у z = Float(x) / y

См. также раздел 5.17 «Поразрядные операции над числами».

 

5.3. Округление чисел с плавающей точкой

Метод round округляет число с плавающей точкой до целого:

pi = 3.14159

new_pi = pi.round  # 3

temp = -47.6

temp2 = temp.round # -48

Иногда бывает нужно округлить не до целого, а до заданного числа знаков после запятой. В таком случае можно воспользоваться функциями sprintf (которая умеет округлять) и eval:

pi = 3.1415926535

pi6 = eval(sprintf("%8.6f",pi)) # 3.141593

pi5 = eval(sprintf("%8.5f",pi)) # 3.14159

pi4 = eval(sprintf("%8.4f",pi)) # 3.1416

Это не слишком красиво. Поэтому инкапсулируем оба вызова функций в метод, который добавим в класс Float:

class Float

 def roundf(places)

  temp = self.to_s.length

  sprintf("%#{temp}.#{places}f",self).to_f

 end

end

Иногда требуется округлять до целого по-другому. Традиционное округление n+0.5 с избытком со временем приводит к небольшим ошибкам; ведь n+0.5 все-таки ближе к n+1, чем к n. Есть другое соглашение: округлять до ближайшего четного числа, если дробная часть равна 0.5. Для реализации такого правила можно было бы расширить класс Float, добавив в него метод round2:

class Float

 def round2

  whole = self.floor

  fraction = self — whole

  if fraction == 0.5

   if (whole % 2) == 0

    whole

   else

    whole+1

   end

  else

   self.round

  end

 end

end

a = (33.4).round2 # 33

b = (33.5).round2 # 34

с = (33.6).round2 # 34

d = (34.4).round2 # 34

e = (34.5).round2 # 34

f = (34.6).round2 # 35

Видно, что round2 отличается от round только в том случае, когда дробная часть в точности равна 0.5. Отметим, кстати, что число 0.5 можно точно представить в двоичном виде. Не так очевидно, что этот метод правильно работает и для отрицательных чисел (попробуйте!). Отметим еще, что скобки в данном случае необязательны и включены в запись только для удобства восприятия.

Ну а если мы хотим округлять до заданного числа знаков после запятой, но при этом использовать метод «округления до четного»? Тогда нужно добавить в класс Float также метод roundf2:

class Float

 # Определение round2 такое же, как и выше.

 def roundf2(places)

  shift = 10**places

  (self * shift).round2 / shift.to_f

 end

end

a = 6.125

b = 6.135

x = a.roundf2(a) #6.12

y = b.roundf2(b) #6.13

У методов roundf и roundf2 есть ограничение: большое число с плавающей точкой может стать непредставимым при умножении на большую степень 10. На этот случай следовало бы предусмотреть проверку ошибок.

 

5.4. Сравнение чисел с плавающей точкой

Печально, но факт: в компьютере числа с плавающей точкой представляются неточно. В идеальном мире следующий код напечатал бы «да», но на всех машинах где мы его запускали, печатается «нет»:

x = 1000001.0/0.003

y = 0.003*x

if y == 1000001.0

 puts "да"

else

 puts "нет"

end

Объясняется это тем, что для хранения числа с плавающей точкой выделено конечное число битов, а с помощью любого, сколь угодно большого, но конечного числа битов нельзя представить периодическую десятичную дробь с бесконечным числом знаков после запятой.

Из-за этой неустранимой неточности при сравнении чисел с плавающей точкой мы можем оказаться в ситуации (продемонстрированной выше), когда с практической точки зрения два числа равны, но аппаратура упрямо считает их различными.

Ниже показан простой способ выполнения сравнения с «поправкой», когда числа считаются равными, если отличаются не более чем на величину, задаваемую программистом:

class Float

 EPSILON = 1e-6 # 0.000001

 def == (x)

  (self-x).abs < EPSILON

 end

end

x = 1000001.0/0.003

y = 0.003*x

if y == 1.0 # Пользуемся новым оператором ==.

 puts "да" # Теперь печатается "да".

else

 puts "нет"

end

В зависимости от ситуации может понадобиться задавать разные погрешности. Для этого определим в классе Float новый метод equals?. (При таком выборе имени мы избежим конфликта со стандартными методами equal? и eql?; последний, кстати, вообще не следует переопределять).

class Float

 EPSILON = 1e-6

 def equals?(x, tolerance=EPSILON)

  (self-x).abs < tolerance

 end

end

flag1 = (3.1416).equals? Math::PI # false

flag2 = (3.1416).equals?(Math::PI, 0.001) # true

Можно также ввести совершенно новый оператор для приближенного сравнения, назвав его, например, =~.

Имейте в виду, что это нельзя назвать настоящим решением. При последовательных вычислениях погрешность накапливается. Если вам совершенно необходимы числа с плавающей точкой, смиритесь с неточностями (см. также разделы 5.8 и 5.9).

 

5.5. Форматирование чисел для вывода

Для вывода числа в заданном формате применяется метод printf из модуля Kernel. Он практически не отличается от одноименной функции в стандартной библиотеке С. Дополнительную информацию см. в документации по методу printf.

x = 345.6789

i = 123

printf("x = %6.2f\n", x) # x = 345.68

printf("x = %9.2e\n", x) # x = 3.457e+02

printf("i = %5d\n\ i)    # i = 123

printf("i = %05d\n", i)  # i = 00123

printf("i = %-5d\n\, i)  # i = 123

Чтобы сохранить результат в строке, а не печатать его немедленно, воспользуйтесь методом sprintf. При следующем обращении возвращается строка:

str = sprintf ("%5.1f",x) # "345.7"

Наконец, в классе String есть метод %, решающий ту же задачу. Слева от знака % должна стоять форматная строка, а справа — единственный аргумент (или массив значений), результатом является строка.

# Порядок вызова: 'формат % значение'

str = "%5.1f" % x           # "345.7"

str = "%6.2f, %05d" % [x,i] # "345.68, 00123"

 

5.6. Вставка разделителей при форматировании чисел

Возможно, есть и более удачные способы достичь цели, но приведенный ниже код работает. Мы инвертируем строку, чтобы было удобнее выполнять глобальную замену, а в конце инвертируем ее еще раз:

def commas(x)

str = x.to_s.reverse

str.gsub!(/([0-9]{3})/,"\\1,")

str.gsub(/,$/,"").reverse

end

puts commas(123)     # "123"

puts commas(1234)    # "1,234"

puts commas(12345)   # "12,435"

puts commas(123456)  # "123,456"

puts commas(1234567) # "1,234,567"

 

5.7. Работа с очень большими числами

При необходимости Ruby позволяет работать с произвольно большими целыми числами. Переход от Fixnum к Bignum производится автоматически, прозрачно для программиста. В следующем разделе результат оказывается настолько большим, что преобразуется из объекта Fixnum в Bignum:

num1 = 1000000   # Один миллион (10**6)

num2 = num1*num1 # Один триллион (10**12)

puts num1        # 1000000

puts num1.class  # Fixnum

puts num2        # 1000000000000

puts num2.class  # Bignum

Размер Fixnum зависит от машинной архитектуры. Вычисления с объектами Bignum ограничены только объемом памяти и быстродействием процессора. Конечно, они потребляют больше памяти и выполняются несколько медленнее, тем не менее операции над очень большими целыми (сотни знаков) реальны.

 

5.8. Использование класса BigDecimal

Стандартная библиотека bigdecimal позволяет работать с дробями, имеющими много значащих цифр. Число хранится как массив цифр, а не преобразуется в двоичное представление. Тем самым достижима произвольная точность, естественно, ценой замедления работы.

Чтобы оценить преимущества, рассмотрим следующий простой фрагмент кода, в котором используются числа с плавающей точкой:

if (3.2 - 2.0) == 1.2

 puts "равны"

else

 puts "не равны" # Печатается "не равны"!

end

В подобной ситуации на помощь приходит класс BigDecimal. Однако в случае бесконечных периодических дробей проблема остается. Другой подход обсуждается в разделе 5.9 «Работа с рациональными числами».

Объект BigDecimal инициализируется строкой. (Объекта типа Float было бы недостаточно, поскольку погрешность вкралась бы еще до начала конструирования BigDecimal.) Метод BigDecimal эквивалентен BigDecimal.new; это еще один особый случай, когда имя метода начинается с прописной буквы. Поддерживаются обычные математические операции, например + и *. Отметим, что метод to_s может принимать в качестве параметра форматную строку. Дополнительную информацию вы найдете на сайте ruby-doc.org.

require 'bigdecimal'

x = BigDecimal("3.2")

y = BigDecimal("2.0")

z = BigDecimal("1.2")

if (x - y) == z

 puts "равны" # Печатается "равны"!

else

 puts "не равны"

end

а = x*y*z

a.to_s        # "0.768Е1" (по умолчанию: научная нотация)

a.to_s("F")   # "7.68" (обычная запись)

Если необходимо, можно задать число значащих цифр. Метод precs возвращает эту информацию в виде массива, содержащего два числа: количество использованных байтов и максимальное число значащих цифр.

x = BigDecimal ("1.234",10)

y = BigDecimal("1.234",15)

x.precs # [8, 16]

y.precs # [8, 20]

В каждый момент число использованных байтов может оказаться меньше максимального. Максимум может также оказаться больше запрошенного вами (поскольку BigDecimal пытается оптимизировать использование внутренней памяти). У обычных операций (сложение, вычитание, умножение и деление) есть варианты принимающие в качестве дополнительного параметра число значащих цифр. Если результат содержит больше значащих цифр, чем указано, производится округление до заданного числа знаков.

a = BigDecimal("1.23456")

b = BigDecimal("2.45678")

# В комментариях "BigDecimal:objectid" опущено.

c = a+b          # <'0.369134Е1\12(20)>

c2 = a.add(b,4)  # <'0.3691Е1',8(20)>

d = a-b          # <'-0.122222E1',12(20)>

d2 = a.sub(b,4)  # <'-0.1222E1',8(20)>

e = a*b          # <'0.30330423168E1\16(36)>

e2 = a.mult(b,4) # <'0.3033E1',8(36)>

f = a/b          # <'0.502511417383729922907221E0',24(32)>

f2 = a.div(b,4)  # <'0.5025E0',4(16)>

В классе BigDecimal определено и много других функций, например floor, abs и т.д. Как и следовало ожидать, имеются операторы % и **, а также операторы сравнения, к примеру <. Оператор == не умеет округлять свои операнды — эта обязанность возлагается на программиста.

В модуле BigMath определены константы E и PI с произвольной точностью. (На самом деле это методы, а не константы.) Там же определены функции sin, cos, exp и пр.; все они принимают число значащих цифр в качестве параметра. Следующие подбиблиотеки являются дополнениями к BigDecimal.

bigdecimal/math     Модуль BigMath

bigdecimal/jacobian Методы для вычисления матрицы Якоби

bigdecimal/ludcmp   Модуль LUSolve, разложение матрицы в произведение верхнетреугольной и нижнетреугольной

bigdecimal/newton   Методы nlsolve и norm

В настоящей главе эти подбиблиотеки не описываются. Для получения дополнительной информации обратитесь к сайту ruby-doc.org или любому подробному справочному руководству.

 

5.9. Работа с рациональными числами

Класс Rational позволяет (во многих случаях) производить операции с дробями с «бесконечной» точностью, но лишь если это настоящие рациональные числа (то есть частное от деления двух целых чисел). К иррациональным числам, например π или e, он неприменим.

Для создания рационального числа мы вызываем специальный метод Rational (еще один из немногих методов, имя которого начинается с прописной буквы; обычно такие методы служат для преобразования данных или инициализации).

r = Rational(1,2) # 1/2 или 0.5

s = Rational(1,3) # 1/3 или 0.3333...

t = Rational(1,7) # 1/7 или 0.14...

u = Rational(6,2) # "то же самое, что" 3.0

z = Rational(1,0) # Ошибка!

Результатом операции над двумя рациональными числами, как правило, снова является рациональное число.

r+t # Rational(9, 14)

r-t # Rational(5, 14)

r*s # Rational(1, 6)

r/s # Rational(3, 2)

Вернемся к примеру, на котором мы демонстрировали неточность операций над числами с плавающей точкой (см. раздел 5.4). Ниже мы выполняем те же действия над рациональными, а не вещественными числами и получаем «математически ожидаемый» результат:

x = Rational(1000001,1)/Rational(3,1000)

y = Rational(3,1000)*x

if y == 1000001.0

 puts "да" # Теперь получаем "да"!

else

 puts "нет"

end

Конечно, не любая операция дает рациональное же число в качестве результата:

x = Rational (9,16) # Rational(9, 16)

Math.sqrt(x)        # 0.75

x**0.5 # 0.75

x**Rational(1,2)    # 0.75

Однако библиотека mathn в какой-то мере изменяет это поведение (см. раздел 5.12).

 

5.10. Перемножение матриц

Стандартная библиотека matrix предназначена для выполнения операций над числовыми матрицами. В ней определено два класса: Matrix и Vector.

Следует также знать о прекрасной библиотеке NArray, которую написал Масахиро Танака (Masahiro Tanaka) — ее можно найти на сайте www.rubyforge.org. Хотя эта библиотека не относится к числу стандартных, она широко известна и очень полезна. Если вы предъявляете повышенные требования к быстродействию, нуждаетесь в особом представлении данных или желаете выполнять быстрое преобразование Фурье, обязательно ознакомьтесь с этим пакетом. Впрочем, для типичных применений стандартной библиотеки matrix должно хватить, поэтому именно ее мы и рассмотрим.

Чтобы создать матрицу, мы, конечно же, обращаемся к методу класса. Сделать это можно несколькими способами. Самый простой — вызвать метод Matrix.[] и перечислить строки в виде массивов. Ниже мы записали вызов на нескольких строчках, но, разумеется, это необязательно:

m = Matrix[[1,2,3],

           [4,5,6],

           [7,8,9]]

Вместо этого можно вызвать метод rows, передав ему массив массивов (в таком случае «дополнительные» скобки необходимы). Необязательный параметр сору, по умолчанию равный true, указывает, надо ли скопировать переданные массивы или просто сохранить на них ссылки. Оставляйте значение true, если нужно защитить исходные массивы от изменения, и задавайте false, если это несущественно.

Row1 = [2,3]

row2 = [4,5]

m1 = Matrix.rows([row1,row2])       # copy=true

m2 = Matrix.rows([row1,row2],false) # He копировать.

row1[1] = 99                        # Теперь изменим row1.

p m1                                # Matrix[[2, 3], [4, 5]]

p m2                                # Matrix[[2, 99], [4, 5]]

Можно задать матрицу и путем перечисления столбцов, если воспользоваться методом columns. Ему параметр сору не передается, потому что столбцы в любом случае расщепляются, так как во внутреннем представлении матрица хранится построчно:

m1 = Matrix.rows([[1,2],[3,4]])

m2 = Matrix.columns([[1,3],[2,4]]) # m1 == m2

Предполагается, что все матрицы прямоугольные, но это не проверяется. Если вы создадите матрицу, в которой отдельные строки или столбцы длиннее либо короче остальных, то можете получить неверные или неожиданные результаты.

Некоторые специальные матрицы, особенно квадратные, конструируются проще. Так, тождественную матрицу конструирует метод identity (или его синонимы I и unit):

im1 = Matrix.identity(3) # Matrix[[1,0,0],[0,1,0],[0,0,1]]

im2 = Matrix.I(3)        # То же самое.

im3 = Matrix.unit(3)     # То же самое.

Более общий метод scalar строит диагональную матрицу, в которой все элементы на диагонали одинаковы, но не обязательно равны 1:

sm = Matrix.scalar(3,8) # Matrix[[8,0,0],[0,8,0],[0,0,8]]

Еще более общим является метод diagonal, который формирует диагональную матрицу с произвольными элементами (ясно, что параметр, задающий размерность, в этом случае не нужен).

dm = Matrix.diagonal(2,3,7) # Matrix[[2,0,0],[0,3,0],[0,0,7]]

Метод zero создает нулевую матрицу заданной размерности (все элементы равны 0):

zm = Matrix.zero(3) # Matrix[[0,0,0],[0,0,0],[0,0,0]]

Понятно, что методы identity, scalar, diagonal и zero создают квадратные матрицы.

Чтобы создать матрицу размерности 1×N или N×1, воспользуйтесь методом row_vector или column_vector соответственно.

а = Matrix.row_vector(2,4,6,8)    # Matrix[[2,4,6,8]]

b = Matrix.column_vector(6,7,8,9) # Matrix[[6],[7],[8],[9]]

К отдельным элементам матрицы можно обращаться, указывая индексы в квадратных скобках (оба индекса заключаются в одну пару скобок). Отметим, что не существует метода []=. По той же причине, по которой его нет в классе Fixnum: матрицы — неизменяемые объекты (такое решение было принято автором библиотеки).

m = Matrix[[1,2,3],[4,5,6]]

puts m[1,2] # 6

Индексация начинается с 0, как и для массивов в Ruby. Возможно, это противоречит вашему опыту работы с матрицами, но индексация с 1 в качестве альтернативы не предусмотрена. Можно реализовать эту возможность самостоятельно:

# Наивный подход... не поступайте так!

class Matrix

 alias bracket []

 def [] (i,j)

  bracket(i-1,j-1)

 end

end

m = Matrix[[1,2,3],[4,5,6],[7,8,9]]

p m[2,2] # 5

На первый взгляд, этот код должен работать. Большинство операций над матрицами даже будет давать правильный результат при такой индексации. Так в чем же проблема? В том, что мы не знаем деталей внутренней реализации класса Matrix. Если в нем для доступа к элементам матрицы всегда используется собственный метод [], то все будет хорошо. Но если где-нибудь имеются прямые обращения к внутреннему массиву или применяются иные оптимизированные решения, то возникнет ошибка. Поэтому, решившись на такой трюк, вы должны тщательно протестировать новое поведение.

К тому же необходимо изменить методы row и vector. В них индексы тоже начинаются с 0, но метод [] не вызывается. Я не проверял, что еще придется модифицировать.

Иногда необходимо узнать размерность или форму матрицы. Для этого есть разные методы, например row_size и column_size.

Метод row_size возвращает число строк в матрице. Что касается метода column_size, тут есть одна тонкость: он проверяет лишь размер первой строки. Если по каким-либо причинам матрица не прямоугольная, то полученное значение бессмысленно. Кроме того, поскольку метод square? (проверяющий, является ли матрица квадратной) обращается к row_size и column_size, его результат тоже нельзя считать стопроцентно надежным.

m1 = Matrix[[1,2,3],[4,5,6],[7,8,9]]

m2 = Matrix[[1,2,3],[4,5,6],[7,8]]

m1.row_.size   # 3

m1.column_size # 3 m2.row_size # 3

m2.column_size # 3 (неправильно)

m1.square?     # true

m2.square?     # true (неправильно)

Решить эту мелкую проблему можно, например, определив метод rectangular?.

class Matrix

 def rectangular?

  arr = to_a

  first = arr[0].size

  arr[1..-1].all? {|x| x.size == first }

 end

end

Можно, конечно, модифицировать метод square?, так чтобы сначала он проверял, является ли матрица прямоугольной. В таком случае нужно будет изменить метод column_size, чтобы он возвращал nil для непрямоугольной матрицы.

Для вырезания части матрицы имеется несколько методов. Метод row_vectors возвращает массив объектов класса Vector, представляющих строки (см. обсуждение класса Vector ниже.) Метод column_vectors работает аналогично, но для столбцов. Наконец, метод minor возвращает матрицу меньшего размера; его параметрами являются либо четыре числа (нижняя и верхняя границы номеров строк и столбцов), либо два диапазона.

m = Matrix[[1,2,3,4],[5,6,7,8],[6,7,8,9]]

rows = m.row_vectors # Три объекта Vector.

cols = m.column_vectors # Четыре объекта Vector.

m2 = m.minor(1,2,1,2) # Matrix[[6,7,],[7,8]]

m3 = m.minor(0..1,1..3) # Matrix[[[2,3,4],[6,7,8]]

К матрицам применимы обычные операции: сложение, вычитание, умножение и деление. Для выполнения некоторых из них должны соблюдаться ограничения на размеры матриц-операндов; в противном случае будет возбуждено исключение (например, при попытке перемножить матрицы размерностей 3×3 и 4×4).

Поддерживаются стандартные преобразования: inverse (обращение), transpose (транспонирование) и determinant (вычисление определителя). Для целочисленных матриц определитель лучше вычислять с помощью библиотеки mathn (раздел 5.12).

Класс Vector — это, по существу, частный случай одномерной матрицы. Его объект можно создать с помощью методов [] или elements; в первом случае параметром является развернутый массив, а во втором — обычный массив и необязательный параметр сору (по умолчанию равный true).

arr = [2,3,4,5]

v1 = Vector[*arr]               # Vector[2,3,4,5]

v2 = Vector.elements(arr)       # Vector[2,3,4,5]

v3 = Vector.elements(arr,false) # Vector[2,3,4,5]

arr[2] = 7                      # теперь v3 - Vector[2,3,7,5].

Метод covector преобразует вектор длины N в матрицу размерности N×1 (выполняя попутно транспонирование).

v = Vector[2,3,4]

m = v.covector # Matrix[[2,3,4]]

Поддерживается сложение и вычитание векторов одинаковой длины. Вектор можно умножать на матрицу и на скаляр. Все эти операции подчиняются обычным математическим правилам.

v1 = Vector[2,3,4]

v2 = Vector[4,5,6]

v3 = v1 + v2        # Vector[6,8,10]

v4 = v1*v2.covector # Matrix![8,10,12],[12,15,18],[16,20,24]]

v5 = v1*5           # Vector[10,15,20]

Имеется метод inner_product (скалярное произведение):

v1 = Vector[2,3,4]

v2 = Vector[4,5,6]

x = v1.inner_product(v2) # 47

Дополнительную информацию о классах Matrix и vector можно найти в любом справочном руководстве, например воспользовавшись командной утилитой ri, или на сайте ruby-doc.org.

 

5.11. Комплексные числа

Стандартная библиотека complex предназначена для работы с комплексными числами в Ruby. Большая ее часть не требует пояснений.

Для создания комплексного числа применяется следующая несколько необычная нотация:

z = Complex(3,5) # 3+5i

Необычно в ней то, что имя метода совпадает с именем класса. В данном случае наличие скобок указывает на то, что это вызов метода, а не ссылка на константу. Вообще говоря, имена методов не похожи на константы, и я не рекомендую начинать имена методов с прописной буквы, разве что в подобных специальных случаях. (Отметим, что имеются также методы Integer и Float; вообще, имена, начинающиеся с прописной буквы, зарезервированы для методов, которые выполняют преобразование данных и аналогичные действия.)

Метод im преобразует вещественное число в мнимое (по существу, умножая его на i). Поэтому представлять комплексные числа можно и с помощью более привычной нотации:

а = 3.im     # 3i

b = 5 - 2.im # 5-2i

Если вас больше интересуют полярные координаты, то можно обратиться к методу polar:

2 - Complex.polar(5,Math::PI/2.0) # Радиус, угол.

В классе Complex имеется также константа I, которая представляет число i — квадратный корень из минус единицы:

z1 = Complex(3,5)

z2 = 3 + 5*Complex::I # z2 == z1

После загрузки библиотеки complex некоторые стандартные математические функции изменяют свое поведение. Тригонометрические функции — sin, sinh, tan и tanh (а также некоторые другие, например, ехр и log) начинают принимать еще и комплексные аргументы. Некоторые функции, например sqrt, даже возвращают комплексные числа в качестве результата.

x = Math.sqrt(Complex(3,5)) # Приближенно Complex(2.1013, 1.1897)

y = Math.sqrt(-1) # Complex(0,1)

Дополнительную информацию ищите в любой полной документации, в частности на сайте ruby-doc.org.

 

5.12. Библиотека mathn

В программах, выполняющих большой объем математических вычислений, очень пригодится замечательная библиотека mathn, которую написал Кейдзу Исидзука (Keiju Ishitsuka). В ней есть целый ряд удобных методов и классов; кроме того, она унифицирует все классы Ruby для работы с числами так, что они начинают хорошо работать совместно.

Простейший способ воспользоваться этой библиотекой — включить ее с помощью директивы require и забыть. Поскольку она сама включает библиотеки complex, rational и matrix (в таком порядке), то вы можете этого не делать.

В общем случае библиотека mathn пытается вернуть «разумные» результаты вычислений. Например, при извлечении квадратного корня из Rational будет возвращен новый объект Rational, если это возможно; в противном случае Float. В таблице 5.1 приведены некоторые последствия загрузки этой библиотеки.

Таблица 5.1. Результаты вычислений в случае отсутствия и наличия библиотеки mathn

Выражение Без mathn С mathn
Math.sqrt(Rational(9,16)) 0.75 Rational(3,4)
1/2 0 Rational(1,2)
Matrix.identity(3)/3 Matrix[[0,0,0], [0,0,0],[0,0,0]] Matrix[[1/3,0,0], [0,1/3,0],[0,0,1/3]]
Math.sqrt(64/25) 1.4142… Rational(8,5)
Rational(1,10).inspect Rational(1,10) 1/10

Библиотека mathn добавляет методы ** и power2 в класс Rational. Она изменяет поведение метода Math.sqrt и добавляет метод Math.rsqrt, умеющий работать с рациональными числами.

Дополнительная информация приводится в разделах 5.13 и 5.14.

 

5.13. Разложение на простые множители, вычисление НОД и НОК

В библиотеке mathn определены также некоторые новые методы в классе Integer. Так, метод gcd2 служит для нахождения наибольшего общего делителя (НОД) объекта, от имени которого он вызван, и другого числа.

n = 36.gcd2(120) # 12 k = 237.gcd2(79) # 79

Метод prime_division выполняет разложение на простые множители. Результат возвращается в виде массива массивов, в котором каждый вложенный массив содержит простое число и показатель степени, с которым оно входит в произведение.

factors = 126.prime_division # [[2,1], [3,2], [7,1]]

                             # To есть 2**1 * 3**2 * 7**1

Имеется также метод класса Integer.from_prime_division, который восстанавливает исходное число из его сомножителей. Это именно метод класса, потому что выступает в роли «конструктора» целого числа.

factors = [[2,1],[3,1],[7,1]]

num = Integer.from_prime_division(factors) # 42

Ниже показано, как разложение на простые множители можно использовать для отыскания наименьшего общего кратного (НОК) двух чисел:

require 'mathn'

class Integer

 def lcm(other)

  pf1 = self.prime_division.flatten

  pf2 = other.prime_division.flatten

  h1 = Hash[*pf1]

  h2 = Hash[*pf2]

  hash = h2.merge(h1) {|key,old,new| [old,new].max }

  Integer.from_prime_division(hash.to_a)

 end

end

p 15.1cm(150) # 150

p 2.1cm(3)    # 6

p 4.1cm(12)   # 12

p 200.1cm(30) # 600

 

5.14. Простые числа

В библиотеке mathn есть класс для порождения простых чисел. Итератор each возвращает последовательные простые числа в бесконечном цикле. Метод succ порождает следующее простое число. Вот, например, два способа получить первые 100 простых чисел:

require 'mathn'

list = []

gen = Prime.new

gen.each do |prime|

 list << prime

 break if list.size == 100

end

# или:

list = []

gen = Prime.new

100.times { list << gen.succ }

В следующем фрагменте проверяется, является ли данное число простым. Отметим, что если число велико, а машина медленная, то на выполнение может уйти заметное время:

require 'mathn'

class Integer

 def prime?

  max = Math.sqrt(self).ceil

  max -= 1 if max % 2 == 0

  pgen = Prime.new

  pgen.each do |factor|

   return false if self % factor == 0

   return true if factor > max

  end

 end

end

31.prime?         # true

237.prime?        # false

1500450271.prime? # true

 

5.15. Явные и неявные преобразования чисел

Программисты, только начинающие изучать Ruby, часто удивляются, зачем нужны два метода to_i и to_int (и аналогичные им to_f и to_flt). В общем случае метод с коротким именем применяется для явных преобразований, а метод с длинным именем — для неявных.

Что это означает? Во-первых, в большинстве классов определены явные конверторы, но нет неявных. Насколько мне известно, методы to_int и to_flt не определены ни в одном из системных классов.

Во-вторых, в своих собственных классах вы, скорее всего, будете определять неявные конверторы, но не станете вызывать их вручную (если только не заняты написанием «клиентского» кода или библиотеки, которая пытается не конфликтовать с внешним миром).

Следующий пример, конечно, надуманный. В нем определен класс MyClass, который возвращает константы из методов to_i и to_int. Такое поведение лишено смысла, зато иллюстрирует идею:

class MyClass

 def to_i

  3

 end

 def to_int

  5

 end

end

Желая явно преобразовать объект класса MyClass в целое число, мы вызовем метод to_i:

m = MyClass.new x = m.to_i # 3

Но при передаче объекта MyClass какой-нибудь функции, ожидающей целое число, будет неявно вызван метод to_int. Предположим, к примеру, что мы хотим создать массив с известным начальным числом элементов. Метод Array.new может принять целое, но что если вместо этого ему будет передан объект MyClass?

m = MyClass.new

a = Array.new(m) # [nil,nil,nil,nil,nil]

Как видите, метод new оказался достаточно «умным», чтобы вызвать to_int и затем создать массив из пяти элементов.

Дополнительную информацию о поведении в другом контексте (строковом) вы найдете в разделе 2.16. См. также раздел 5.16.

 

5.16. Приведение числовых значений

Приведение можно считать еще одним видом неявного преобразования. Если некоторому методу (например, +) передается аргумент, которого он не понимает, он пытается привести объект, от имени которого вызван, и аргумент к совместимым типам, а затем сложить их. Принцип использования метода coerce в вашем собственном классе понятен из следующего примера:

class MyNumberSystem

 def +(other)

  if other.kind_of?(MyNumberSystem)

   result = some_calculation_between_self_and_other

   MyNumberSystem.new(result)

  else

   n1, n2 = other.coerce(self)

   n1 + n2

  end

 end

end

Метод coerce возвращает массив из двух элементов, содержащий аргумент и вызывающий объект, приведенные к совместимым типам.

В данном примере мы полагаемся на то, что приведение выполнит тип аргумента. Но если мы хотим быть законопослушными гражданами, то должны реализовать приведение в своем классе, сделав его пригодным для работы с другими типами чисел. Для этого нужно знать, с какими типами мы в состоянии работать непосредственно, и при необходимости выполнять приведение к одному из этих типов. Если мы не можем сделать это самостоятельно, то должны обратиться за помощью к родительскому классу.

def coerce(other)

 if other.kind_of?(Float)

  return other, self.to_f

 elsif other.kind_of?(Integer)

  return other, self.to_i

 else

  super

 end

end

Разумеется, это будет работать только, если наш объект реализует методы to_i и to_f.

Метод coerce можно применить для реализации автоматического преобразования строк в числа, как в языке Perl:

class String

 def coerce(n)

  if self['.']

   [n, Float(self)]

  else

   [n, Integer(self)]

  end

 end

end

x = 1 + "23"    # 24

y = 23 * "1.23" # 28.29

Мы не настаиваем на таком решении. Но рекомендуем реализовывать coerce при создании любого класса для работы с числовыми данными.

 

5.17. Поразрядные операции над числами

Иногда требуется работать с двоичным представлением объекта Fixnum. На прикладном уровне такая необходимость возникает нечасто, но все-таки возникает.

Ruby обладает всеми средствами для таких операций. Для удобства числовые константы можно записывать в двоичном, восьмеричном или шестнадцатеричном виде. Поразрядным операциям И, ИЛИ, ИСКЛЮЧАЮЩЕЕ ИЛИ и НЕ соответствуют операторы &, |, ^ и ~.

x = 0377       # Восьмеричное (десятичное 255)

y = 0b00100110 # Двоичное (десятичное 38)

z = 0xBEEF     # Шестнадцатеричное (десятичное 48879)

а = x | z      # 48895 (поразрядное ИЛИ)

b = x & z      # 239 (поразрядное И)

с = x ^ z      # 48656 (поразрядное ИСКЛЮЧАЮЩЕЕ ИЛИ)

d = ~ y        # -39 (отрицание или дополнение до 1)

Метод экземпляра size позволяет узнать размер слова для той машины, на которой исполняется программа.

size # Для конкретной машины возвращает 4.

Имеются операторы сдвига влево и вправо (<< и >>соответственно). Это логические операторы сдвига, они не затрагивают знаковый бит (хотя оператор >> распространяет его).

x = 8

y = -8

а = x >> 2 # 2

b = y >> 2 # -2

с = x << 2 # 32

d = y << 2 # -32

Конечно, если сдвиг настолько велик, что дает нулевое значение, то знаковый бит теряется, поскольку -0 и 0 — одно и то же.

Квадратные скобки позволяют трактовать числа как битовые массивы. Бит с номером 0 всегда является младшим, вне зависимости от порядка битов в конкретной машинной архитектуре.

x = 5         # То же, что 0b0101

а = x[0]      # 1

b = x[1]      # 0

с = x[2]      # 1

d = x[3]      # 0

# И так далее # 0

Присваивать новые значения отдельным битам с помощью такой нотации невозможно (поскольку Fixnum хранится как непосредственное значение, а не как ссылка на объект). Но можно имитировать это действие путем сдвига 1 влево на нужное число позиций с последующим выполнением операции ИЛИ или И.

# Выполнить присваивание x[3] = 1 нельзя,

# но можно поступить так:

x |= (1<<3)

# Выполнить присваивание x[4] = 0 нельзя,

# но можно поступить так:

x &= ~(1<<4)

 

5.18. Преобразование системы счисления

Ясно, что любое целое число можно представить в любой системе счисления, поскольку хранятся эти числа в двоичном виде. Мы знаем, что Ruby умеет работать c целыми константами, записанными в любой из четырех наиболее популярных систем. Следовательно, разговор о преобразовании системы счисления может вестись только применительно к числам, записанным в виде строк.

Вопрос о преобразовании строки в целое рассмотрен в разделе 2.24. Для преобразования числа в строку проще всего воспользоваться методом to_s, которому можно еще передать основание системы счисления. По умолчанию оно равно 10, но в принципе может быть любым вплоть до 36 (когда задействованы все буквы латинского алфавита).

237.to_s(2)  # "11101101"

237.to_s(5)  # "1422"

237.to_s(8)  # "355"

237.to_s     # "237"

237.to_s(16) # "ed"

237.to_s(30) # "7r"

Другой способ — обратиться к методу % класса String:

hex = "%x" % 1234 # "4d2"

oct = "%о" % 1234 # "2322"

bin = "%b" % 1234 # "10011010010"

Метод sprintf тоже годится:

str = sprintf(str,"Nietzsche is %x\n",57005)

# str теперь равно: "Nietzsche is dead\n"

Если нужно сразу же вывести преобразованное в строку значение, то подойдет и метод printf.

 

5.19. Извлечение кубических корней, корней четвертой степени и т.д.

В Ruby встроена функция извлечения квадратного корня (Math.sqrt), поскольку именно она применяется чаще всего. А если надо извлечь корень более высокой степени? Если вы еще не забыли математику, то эта задача не вызовет затруднений.

Можно, например, воспользоваться логарифмами. Напомним, что е в степени x — обратная функция к натуральному логарифму x и что умножение чисел эквивалентно сложению их логарифмов.

x = 531441

cuberoot = Math.exp(Math.log(x)/3.0)   # 81.0

fourthroot = Math.exp(Math.log(x)/4.0) # 27.0

Но можно просто использовать дробные показатели степени (оператор возведения в степень принимает в качестве аргумента произвольное целое число или число с плавающей точкой).

include Math

y = 4096

cuberoot = y**(1.0/3.0)     # 16.0

fourthroot = y**(1.0/4.0)   # 8.0

fourthroot = sqrt(sqrt(y))  # 8.0 (то же самое)

twelfthroot = y**(1.0/12.0) # 2.0

Отметим, что во всех примерах мы пользовались при делении числами с плавающей точкой (чтобы избежать отбрасывания дробной части).

 

5.20. Определение порядка байтов

Интересно, что производители компьютеров никак не могут договориться, в каком порядке лучше хранить двоичные байты. Следует ли размещать старший бит по большему или по меньшему адресу? При передаче сообщения по проводам нужно сначала посылать старший или младший бит?

Хотите верьте, хотите нет, но решение не произвольно. Существуют убедительные аргументы в пользу обеих точек зрения (обсуждать их здесь мы не будем).

Вот уже больше двадцати лет, как для описания противоположных позиций применяются термины «остроконечный» (little-endian) и «тупоконечный» (big-endian). Кажется, впервые их употребил Дэнни Коэн (Danny Cohen); см. его классическую статью "On Holy Wars and a Plea for Peace" (IEEE Computer, October 1981). Взяты они из романа Джонатана Свифта «Путешествия Гулливера».

Обычно нам безразличен порядок байтов в конкретной машинной архитектуре. Но как быть, если все-таки его нужно знать?

Можно воспользоваться показанным ниже методом. Он возвращает одну из строк LITTLE, BIG или OTHER. Решение основано на том факте, что директива l выполняет упаковку в машинном формате, а директива N распаковывает в сетевом порядке байтов (по определению тупоконечном).

def endianness

 num = 0x12345678

 little = "78563412"

 big = "12345678"

 native = [num].pack('1')

 netunpack = native.unpack('N')[0]

 str = "%8x" % netunpack

 case str

  when little

   "LITTLE"

 when big

  "BIG"

 else

  "OTHER"

 end

end

puts endianness # В данном случае печатается "LITTLE"

Этот прием может оказаться удобным, если, например, вы работаете с двоичными данными (скажем, отсканированным изображением), импортированными из другой системы.

 

5.21. Численное вычисление определенного интеграла

Для приближенного вычисления определенного интеграла имеется проверенная временем техника. Любой студент, изучавший математический анализ, вспомнит, что она называется суммой Римана.

Приведенный ниже метод integrate принимает начальное и конечное значения зависимой переменной, а также приращение. Четвертый параметр (который на самом деле параметром не является) — это блок. В блоке должно вычисляться значение функции от переданной в него зависимой переменной (здесь слово «переменная» употребляется в математическом, а не программистском смысле). Необязательно отдельно определять функцию, которая вызывается в блоке, но для ясности мы это сделаем.

def integrate(x0, x1, dx=(x1-x0)/1000.0)

 x = x0

 sum = 0

 loop do

  y = yield(x)

  sum += dx * y

  x += dx

  break if x > x1

 end

 sum

end

def f(x)

 x**2

end

z = integrate(0.0,5.0) {|x| f(x) }

puts z, "\n" # 41.7291875

Здесь мы опираемся на тот факт, что блок возвращает значение, которое может быть получено с помощью yield. Кроме того, сделаны некоторые допущения. Во-первых, мы предполагаем, что x0 меньше x1 (в противном случае получится бесконечный цикл). Читатель сам легко устранит подобные огрехи. Во-вторых, мы считаем, что функцию можно вычислить в любой точке заданной области. Если это не так, мы получим хаотическое поведение. (Впрочем, подобные функции все равно, как правило, не интегрируемы — по крайней мере, на указанном интервале. В качестве примера возьмите функцию f(x)=x/(x-3) в точке x=3.)

Призвав на помощь полузабытые знания об интегральном исчислении, мы могли бы вычислить, что в данном случае результат равен примерно 41.666 (5 в кубе, поделенное на 3). Почему же ответ не так точен, как хотелось бы? Из-за выбранного размера приращения; чем меньше величина dx, тем точнее результат (ценой увеличения времени вычисления).

Напоследок отметим, что подобная методика более полезна для действительно сложных функций, а не таких простых, как f(x) = x**2.

 

5.22. Тригонометрия в градусах, радианах и градах

При измерении дуг математической, а заодно и «естественной» единицей измерения является радиан. По определению, угол в один радиан соответствует длине дуги, равной радиусу окружности. Немного поразмыслив, легко понять, что угол 2π радиан соответствует всей окружности.

Дуговой градус, которым мы пользуемся в повседневной жизни, — пережиток древневавилонской системы счисления по основанию 60: в ней окружность делится на 360 градусов. Менее известна псевдометрическая единица измерения град, определенная так, что прямой угол составляет 100 град (а вся окружность — 400 град).

При вычислении тригонометрических функций в языках программирования по умолчанию чаще всего используются радианы, и Ruby в этом отношении не исключение. Но мы покажем, как производить вычисления и в градусах, и в градах для тех читателей, которые по образованию не инженеры, а по происхождению не древние вавилоняне.

Поскольку число любых угловых единиц в окружности — константа, можно легко переходить от одних единиц к другим. Мы определим соответствующие константы и будем пользоваться ими в коде. Для удобства поместим их в модуль Math.

module Math

 RAD2DEG = 360.0/(2.0*PI)  # Радианы в градусы.

 RAD2GRAD = 400.0/(2.0*РI) # Радианы в грады.

end

Теперь можно определить и новые тригонометрические функции. Поскольку мы всегда преобразуем в радианы, то будем делить на определенные выше коэффициенты. Можно было бы поместить определения функций в тот же модуль Math, но мы этого делать не стали.

def sin_d(theta)

 Math.sin(theta/Math::RAD2DEG)

end

def sin_g(theta)

 Math.sin(theta/Math::RAD2GRAD)

end

Функции cos и tan можно было бы определить аналогично.

С функцией atan2 дело обстоит несколько сложнее. Она принимает два аргумента (длины противолежащей и прилежащей сторон прямоугольного треугольника). Поэтому мы преобразуем результат, а не аргумент:

def atan2_d(y,x)

 Math.atan2(у,x)/Math::RAD2DEG

end

def atan2_g(y,x)

 Math.atan2(y, x)/Math::RAD2GRAD

end

 

5.23. Неэлементарная тригонометрия

В ранних версиях Ruby не было функций arcsin и arccos. Равно как и гиперболических функций sinh, cosh и tanh. Их определения были приведены в первом издании этой книги, но сейчас они являются стандартной частью модуля Math.

 

5.24. Вычисление логарифмов по произвольному основанию

Чаще всего мы пользуемся натуральными логарифмами (по основанию е, часто натуральный логарифм обозначается как ln), иногда также десятичными (по основанию 10). Эти функции реализованы в методах Math.log и Math.log10 соответственно.

В информатике, а в особенности в таких ее областях, как кодирование и теория информации, обычно применяются логарифмы по основанию 2. Например, так вычисляется минимальное число битов, необходимых для представления числа. Определим функцию с именем log2:

def log2(x)

 Math.log(x)/Math.log(2)

end

Ясно, что обратной к ней является функция 2**x (как обращением ln x служит Math::Е**x или Math.exp(x)).

Эта идея обобщается на любое основание. В том маловероятном случае, если вам понадобится логарифм по основанию 7, можно поступить так:

def log7(x)

 Math.log(x)/Math.log(7)

end

На практике знаменатель нужно вычислить один раз и сохранить в виде константы.

 

5.25. Вычисление среднего, медианы и моды набора данных

Пусть дан массив x, вычислим среднее значение по всем элементам массива. На самом деле есть три общеупотребительные разновидности среднего значения. Среднее арифметическое — это то, что мы называем средним в обыденной жизни. Среднее гармоническое — это число элементов, поделенное на сумму обратных к ним. И, наконец, среднее геометрическое — это корень n-ой степени из произведения n значений. Вот эти определения, воплощенные в коде:

def mean(x)

 sum=0

 x.each {|v| sum += v}

 sum/x.size

end

def hmean(x)

 sum=0

 x.each {|v| sum += (1.0/v)}

 x.size/sum

end

def gmean(x)

 prod=1.0

 x.each {|v| prod *= v}

 prod**(1.0/x.size)

end

data = [1.1, 2.3, 3.3, 1.2, 4.5, 2.1, 6.6]

am = mean(data)  # 3.014285714

hm = hmean(data) # 2.101997946

gm = gmean(data) # 2.508411474

Медианой набора данных называется значение, которое оказывается приблизительно в середине отсортированного набора (ниже приведен код для вычисления медианы). Примерно половина элементов набора меньше медианы, а другая половина — больше. Ясно, что такая статистика показательна не для всякого набора.

def median(x)

 sorted = x.sort

 mid = x.size/2

 sorted[mid]

end

data = [7,7,7,4,4,5,4,5,7,2,2,3,3,7,3,4]

puts median(data) # 4

Мода набора данных — это наиболее часто встречающееся в нем значение. Если такое значение единственно, набор называется унимодальным, в противном случае — мультимодальным. Мультимодальные наборы более сложны, здесь мы их рассматривать не будем. Интересующийся читатель может обобщить и улучшить приведенный ниже код:

def mode(x)

 f = {}   # Таблица частот.

 fmax = 0 # Максимальная частота.

 m = nil  # Мода.

 x.each do |v|

  f[v] ||= 0

  f[v] += 1

  fmax,m = f[v], v if f[v] > fmax

 end

 return m

end

data = [7,7,7,4,4,5,4,5,7,2,2,3,3,7,3,4]

puts mode(data) # 7

 

5.26. Дисперсия и стандартное отклонение

Дисперсия — это мера «разброса» значений из набора. (Здесь мы не различаем смещенные и несмещенные оценки.) Стандартное отклонение, которое обычно обозначается буквой σ, равно квадратному корню из дисперсии.

Data = [2, 3, 2, 2, 3, 4, 5, 5, 4, 3, 4, 1, 2]

def variance(x)

 m = mean(x)

 sum = 0.0

 x.each {|v| sum += (v-m)**2 }

 sum/x.size

end

def sigma(x)

 Math.sqrt(variance(x))

end

puts variance(data) # 1.461538462

puts sigma(data)    # 1.20894105

Отметим, что функция variance вызывает определенную выше функцию mean.

 

5.27. Вычисление коэффициента корреляции

Коэффициент корреляции — одна из самых простых и полезных статистических мер. Он измеряет «линейность» набора, состоящего из пар (x, у), и изменяется от -1.0 (полная отрицательная корреляция) до +1.0 (полная положительная корреляция).

Для вычисления воспользуемся функциями mean и sigma (стандартное отклонение), которые были определены в разделах 5.25 и 5.26. О смысле этого показателя можно прочитать в любом учебнике по математической статистике.

В следующем коде предполагается, что есть два массива чисел одинакового размера:

def correlate(x,y)

 sum = 0.0

 x.each_index do |i|

  sum += x[i]*y[i]

 end

 xymean = sum/x.size.to_f

 xmean = mean(x)

 ymean = mean(y)

 sx = sigma(x)

 sy = sigma(y)

 (xymean-(xmean*ymean))/(sx*sy)

end

a = [3, 6, 9, 12, 15, 18, 21]

b = [1.1, 2.1, 3.4, 4.8, 5.6]

с = [1.9, 1.0, 3.9, 3.1, 6.9]

c1 = correlate(a,a)         # 1.0

c2 = correlate(a,a.reverse) # -1.0

c3 = correlate(b,c)         # 0.8221970228

Приведенная ниже версия отличается лишь тем, что работает с одним массивом, каждый элемент которого — массив, содержащий пару (x, у):

def correlate2(v)

 sum = 0.0

 v.each do |a|

  sum += a[0]*a[1]

 end

 xymean = sum/v.size.to_f

 x = v.collect {|a| a[0]}

 y = v.collect {|a| a[1]}

 xmean = mean(x)

 ymean = mean(y)

 sx = sigma(x)

 sy = sigma(y)

 (xymean-(xmean*ymean))/(sx*sy)

end

d = [[1,6.1], [2.1,3.1], [3.9,5.0], [4.8,6.2]]

c4 = correlate2(d) # 0.2277822492

И, наконец, в последнем варианте предполагается, что пары (x, у) хранятся в хэше. Код основан на предыдущем примере:

def correlate_h(h)

 correlate2(h.to_a)

end

e = { 1 => 6.1, 2.1 => 3.1, 3.9 => 5.0, 4.8 => 6.2}

c5 = correlated(e) # 0.2277822492

 

5.28. Генерирование случайных чисел

Если вас устраивают псевдослучайные числа, вам повезло. Именно они предоставляются в большинстве языков, включая и Ruby.

Метод rand из модуля Kernel возвращает псевдослучайное число x с плавающей точкой, отвечающее условиям x >= 0.0 и x < 1.0. Например (вы можете получить совсем другое число):

a = rand # 0.6279091137

Если при вызове задается целочисленный параметр max, то возвращается целое число из диапазона 0...max (верхняя граница не включена). Например:

n = rand(10) # 7

Чтобы «затравить» генератор случайных чисел (задать начальное значение — seed), применяется метод srand из модуля Kernel, который принимает один числовой параметр. Если не передавать никакого значения, то метод srand самостоятельно изготовит затравку, учитывая (среди прочего) текущее время. Если же параметр передан, то именно он и становится затравкой. Это бывает полезно при тестировании, когда для воспроизводимости результатов многократно вызываемая программа должна получать одну и ту же последовательность псевдослучайных чисел.

srand(5)

i, j, k = rand(100), rand(100), rand(100)

# 26, 45, 56

srand(5)

l, m, n = rand(100), rand(100), rand(100)

# 26, 45, 56

 

5.29. Кэширование функций с помощью метода memoize

Пусть имеется вычислительно сложная математическая функция, которую нужно многократно вызывать по ходу работы программы. Если быстродействие критично и при этом можно пожертвовать небольшим количеством памяти, то имеет смысл сохранить результаты вычисления функции в таблице и обращаться к ней во время выполнения. (Тут неявно предполагается, что функция будет часто вызываться с одними и теми же параметрами, то есть получается, что мы «выбрасываем» результат дорогостоящего вычисления и снова повторяем его позже.) Такая техника иногда называется запоминанием (memoizing), отсюда и название библиотеки memoize.

Эта библиотека не входит в стандартный дистрибутив, поэтому придется установить ее вручную.

В следующем примере демонстрируется сложная функция zeta. Она применяется при решении одной задачи из области популяционной генетики, но вдаваться в объяснения мы не станем.

require 'memoize'

include Memoize

def zeta(x,y,z)

 lim = 0.0001

 gen = 0

 loop do

  gen += 1

  p,q = x + y/2.0, z + y/2.0

  x1, y1, z1 = p*p*1.0, 2*p*q*1.0, q*q*0.9

  sum = x1 + y1 + z1

  x1 /= sum

  y1 /= sum

  z1 /= sum

  delta = [[x1,x],[y1,y],[z1,z]]

  break if delta.all? {|a,b| (a-b).abs < lim }

  x,y,z = x1,y1,z1

 end

 gen

end

g1 = zeta(0.8,0.1,0.1)

memoize(:zeta)           # Сохранить таблицу в памяти.

g2 = zeta(0.8,0.1,0.1)

memoize(:zeta,"z.cache") # Сохранить таблицу на диске.

g3 = zeta(0.8,0.1,0.1)

Обратите внимание, что можно задать имя файла. Это может несколько замедлить работу, зато экономится память, и таким образом мы можем сохранить запомненные результаты и воспользоваться ими при следующих вызовах программы.

В ходе неформального тестирования мы вызывали функцию 50000 раз в цикле. Оказалось, что g2 вычисляется примерно в 1100 раз быстрее, чем g1, а g3 — примерно в 700 раз. На вашей машине может получиться иной результат.

Отметим еще, что библиотека memoize предназначена не только для математических функций. Ее можно использовать для запоминания результатов работы любого вычислительно сложного метода.

 

5.30. Заключение

В этой главе были рассмотрены различные представления чисел, в том числе целых (в разных системах счисления) и с плавающей точкой. Мы видели, какие трудности возникают при работе с числами с плавающей точкой и как можно частично обойти эти трудности, применяя рациональные числа. Мы познакомились с явными и неявными преобразованиями, а также с приведениями типов.

Также мы изучили разнообразные способы манипулирования числами, векторами и матрицами. Был приведен обзор стандартных библиотек, полезных для численного анализа, в частности библиотеки mathn.

Пойдем дальше. В следующей главе мы обсудим два очень характерных для Ruby типа данных: символы и диапазоны.