• 5.1. Представление чисел в языке Ruby
  • 5.2. Основные операции над числами
  • 5.3. Округление чисел с плавающей точкой
  • 5.4. Сравнение чисел с плавающей точкой
  • 5.5. Форматирование чисел для вывода
  • 5.6. Вставка разделителей при форматировании чисел
  • 5.7. Работа с очень большими числами
  • 5.8. Использование класса BigDecimal
  • 5.9. Работа с рациональными числами
  • 5.10. Перемножение матриц
  • 5.11. Комплексные числа
  • 5.12. Библиотека mathn
  • 5.13. Разложение на простые множители, вычисление НОД и НОК
  • 5.14. Простые числа
  • 5.15. Явные и неявные преобразования чисел
  • 5.16. Приведение числовых значений
  • 5.17. Поразрядные операции над числами
  • 5.18. Преобразование системы счисления
  • 5.19. Извлечение кубических корней, корней четвертой степени и т.д.
  • 5.20. Определение порядка байтов
  • 5.21. Численное вычисление определенного интеграла
  • 5.22. Тригонометрия в градусах, радианах и градах
  • 5.23. Неэлементарная тригонометрия
  • 5.24. Вычисление логарифмов по произвольному основанию
  • 5.25. Вычисление среднего, медианы и моды набора данных
  • 5.26. Дисперсия и стандартное отклонение
  • 5.27. Вычисление коэффициента корреляции
  • 5.28. Генерирование случайных чисел
  • 5.29. Кэширование функций с помощью метода memoize
  • 5.30. Заключение
  • Глава 5. Численные методы

    Дважды [члены Парламента] задавали мне вопрос: «А скажите, мистер Бэббидж, если вы заложите в эту машину неверные числа, то получите правильный результат?» Не могу даже представить себе, насколько извращенно должен мыслить человек, задающий такие вопросы.

    (Чарльз Бэббидж)

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

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

    Bignum
    ,
    BigDecimal
    и
    Rational
    .

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

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

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

    Fixnum
    может представлять число со знаком или без знака:

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

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

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

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

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

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

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

    0b
    ,
    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. Округление чисел с плавающей точкой

    Кирк: Какие, вы говорите, у нас шансы выбраться отсюда?

    Спок: Трудно сказать точно, капитан. Приблизительно 7824.7 к одному.

    (Стар Трек, «Миссия милосердия»)

    Метод

    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. Работа с очень большими числами

    Управлять массами все равно что управлять немногими: дело в частях и в числе.

    (Сунь-Цзы[9])

    При необходимости 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. Численное вычисление определенного интеграла

    Я очень хорошо владею дифференциальным и интегральным исчислением…

    (У.С.Джильберт, «Пираты Пензанса», акт 1)

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

    Приведенный ниже метод

    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 типа данных: символы и диапазоны.


    Примечания:



    9

    Трактат «Искусство войны».









    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх