|
||||||||||||||||||||
|
Глава 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добавляет методы **и 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 типа данных: символы и диапазоны. Примечания:9 Трактат «Искусство войны». |
|
||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||||||||||||||||||
|