биллиг. алг.. - Электронная библиотека

advertisement
В. Биллиг
Алгоритмы и задачи
(Для программирования на языке C#)
Введение
Данный задачник дополняет учебник автора «Основы программирования на C#». При
написании учебника я ориентировался на тех, кто знает основные алгоритмы, знаком со
структурами данных и умеет программировать на каком либо языке. Главной целью ставилось не
столько обучение новому языку программирования C#, (хотя он подробно рассмотрен в учебнике)
сколько обучение основам объектно-ориентированного программирования, освоение технологии
программирования в классах на языке, специально спроектированном для поддержки этой
технологии работы.
Случилось так, что мне пришлось учить программированию тех, кто только начал постигать
азы программирования, тех, для кого нахождение максимума и сортировка массива являлись
трудными задачами. К тому времени ответ на вопрос, можно ли начинать обучение с объектных
языков и объектной технологии, лично для меня был решен. Мой ответ – да, можно и нужно.
При обучении началам программирования немаловажную роль играет первый язык, во
многом определяющий дальнейший стиль программирования.
В данном контексте понятие языка включает и среду программирования, в которую погружен
язык. Поэтому неявно подразумевается, что разработка программ на C# ведется в среде Visual
Studio .Net с использованием возможностей каркаса Framework .Net. Но на начальных этапах
обучения о среде программирования следует говорить как можно меньше. Современные среды
программирования тем и хороши, что напоминают официантов в хорошем ресторане – прекрасное
обслуживание, не обращающее на себя внимания.
Многие годы в обучении лидирующую роль первого языка играл язык Паскаль. Он и сегодня
сохраняет свои позиции, хотя и появились другие претенденты, среди которых назову двух – Java и
C#.
Правильно устроенный программистский мир является многоязычным миром. Поэтому, не
настаивая на исключительности, рассмотрим, почему C# может претендовать на роль первого языка
программирования. Отмечу следующие его достоинства:

легко поддерживает различные модели программирования, применяемые на
начальных этапах обучения.

являясь языком профессионального программирования с большим будущим,
обеспечивает легкий переход от обучения к профессиональной работе.

предоставляет большой набор образцов программирования, представленных
классами библиотеки Framework .Net.
На начальном этапе задачи обучения связаны не столько с языком, сколько с выработкой
алгоритмического мышления. Эти задачи многообразны и достаточно сложны. Требуется
понимание новых понятий: переменных и типов данных. Требуется умение описывать процесс
вычислений ограниченным набором управляющих структур – выбора, циклов, рекурсии. Требуется
знание основ представления данных и классических алгоритмов. И нужно не только знать, но и
уметь. Нужно уметь писать программы! И с первых шагов программирования нельзя забывать об
эффективности и корректности разрабатываемых программ.
Алгоритмическая мысль, как и всякая мысль, требует оформления. Роль слова, роль языка
немаловажна. Язык программирования является важной частью начального процесса обучения
программированию. С чего начинается обучение ребенка – скажи «мама».
Работая над задачником, я ориентировался на возможность его применения вместе с
учебником для обучения началам программирования при выборе языка C# в качестве первого
языка программирования. Методические указания к задачнику должны помочь начинающим в
понимании материала учебника, ориентированного на подготовленного слушателя. Некоторые
разделы учебника при первом чтении могут быть опущены.
Во многом от лектора зависит, как скоро и в какой мере вводить объектный стиль
программирования. В методических указаниях к задачнику рассматриваются различные модели
программирования, все из которых используют язык C# и среду Visual Studio. И на C# можно
писать программы, ничего не зная о классах и объектах.
В заключение этого краткого введения хочу принести благодарности моим коллегам Михаилу
Дехтярю и Сергею Архипову. Они прочитали отдельные разделы этого текста и сделали весьма
полезные замечания. Вся вина за оставшиеся ошибки лежит целиком на мне. Моя особая
благодарность Евгению Веселову, работающему теперь в корпорации Майкрософт. Он прочитал
одну из глав задачника и сделал ряд важных замечаний по стилю программирования. Я планирую
провести ревизию кода, представленного в задачнике, и добавить специальный раздел,
посвященный стилю программирования.
Глава 1 Начала
Задачи этой главы для тех, кто не имеет достаточного опыта в программировании. Они
должны помочь преодолеть психологический рубеж создания первых программ. Они полезны и
тем, кто уже умеет программировать, но не писал программы на C# и не работал в среде Visual
Studio 2005.
Методические указания к главе 1
Модели программ
Что должны знать и что должны уметь студенты, осваивающие начала программирования?
Вот возможный перечень основных тем:

переменная и ее обобщение – объект;

тип переменных и его обобщение – класс;

встроенные типы (классы);

процесс вычислений, как последовательное выполнение операторов языка;

ввод и вывод данных;

ветвление процесса вычислений – альтернатива и разбор случаев;

циклические вычисления;

модульность построения программ;

процедуры и функции, как простейшие виды программных модулей;

строковый тип и работа с текстами;

массивы как структуры данных;

рекуррентные вычисления;

память и время – два основных ресурса, используемых программами;

корректность программ;

основы отладки программ и инструментарий среды разработки Visual Studio .Net ;

Windows-приложения, визуальное программирование, управляемое событиями;

формы и основные элементы управления – командные кнопки, текстовые окна, списки.
Какой лучший способ проверки того, что все эти понятия действительно освоены?
Недостаточно знания ответов на все эти вопросы. Необходимо умение писать качественные
программы, включающие процедуры и функции. С объектной точки освоение начал
программирования означает умение реализовать достаточно содержательный класс. Если задачи из
раздела «Проекты» первых глав задачника не вызывают трудностей, то можно смело считать, что
начала программирования освоены.
Что можно не требовать от студентов, осваивающих начала программирования? Можно не
требовать умения создавать программную систему, представляющую множество классов,
связанных теми или иными отношениями. Более того, вначале можно не требовать создания
собственного класса, - можно исходить из более простых моделей программирования, вполне
реализуемых в рамках программирования на C# в среде Visual Studio .Net.
Модель 60-х годов – модель языка Pascal и структурного программирования
Консольное приложение, строящееся по умолчанию, позволяет полностью и без проблем
реализовать эту модель. В основе этой модели лежит понятие выполняемой программы, состоящей
из описания переменных программы и исполняемого блока операторов. Управление процессом
вычислений такой программы определяется ее текстом и означает последовательное выполнение
операторов исполняемого блока согласно порядку их следования в тексте программы. Описания
переменных – это декларативная часть выполняемой программы, служащая для определения тех
объектов, над которыми выполняются вычисления.
В консольном приложении роль выполняемой программы играет процедура Main. На
начальных этапах у программиста нет необходимости создавать собственные классы. Все, что от
него требуется, – это добавить в процедуру Main объявления переменных встроенных типов и
записать исполняемый блок – последовательность выполняемых операторов. В зависимости от
предпочтений преподавателя можно требовать, чтобы все объявления переменных были собраны в
начале программы и предшествовали исполняемому блоку, как это требовалось в Pascalпрограммах 60-х годов.
Если не вводить оператор Goto, что методически совершенно обосновано, то полученная
модель является моделью структурного программирования, поскольку таковыми являются
управляющие структуры языка. Вычисления полностью управляются текстом программы и
выполняются оператор за оператором. Конечно, операторы выбора и цикла могут быть сколь
угодно сложными и иметь сложную внутреннюю структуру, но, тем не менее, их выполнение
можно рассматривать как выполнение одного оператора с точно определенной семантикой.
Вот как может выглядеть решение одной из задач первой главы задачника, полностью
отвечающее этой модели:
using System;
namespace Ch1_Example1
{
/// <summary>
/// Класс, содержащий выполняемую программу
/// </summary>
class Class1
{
/// <summary>
/// Выполняемая программа!
/// нахождение корней квадратного уравнения:
/// ax^2 +bx +c =0
/// </summary>
[STAThread]
static void Main(string[] args)
{
//объявление переменных
double a,b,c; //коэффициенты уравнения
double x1, x2; // корни уравнения
double d; //дискриминант уравнения
string mes; //сообщение о результатах вычислений
string init; //строка ввода
// ввод исходных данных
Console.WriteLine("Введите три вещественных числа - a, b, c ");
init = Console.ReadLine();
a = Convert.ToDouble(init);
init = Console.ReadLine();
b = Convert.ToDouble(init);
init = Console.ReadLine();
c = Convert.ToDouble(init);
//вычисления и вывод результатов
d = b*b -4*a*c;
if(d>0)
{
mes="Уравнение имеет два вещественных корня!";
Console.WriteLine(mes);
x1 = (-b + Math.Sqrt(d))/(2*a);
x2 = (-b - Math.Sqrt(d))/(2*a);
Console.WriteLine("Корни уравнения: x1={0}, x2={1}",
x1,x2);
}
else if(d==0)
{
mes="Уравнение имеет кратный корень!";
Console.WriteLine(mes);
x1 = -b/(2*a); x2 = -b/(2*a);
Console.WriteLine("Кратный корень уравнения: x={0}",x1);
}
else
{
mes="Уравнение не имеет вещественных корней!";
Console.WriteLine(mes);
}
}
}
}
Понимание того, как работает процедура Main, достаточно для понимания выполнения всего
консольного приложения. На первых порах можно не обращать внимания на объектный антураж,
связанный с этой процедурой.
Написание подобных программ требует знания начальных элементов языка C# в пределах
первых 8-и лекций. Нужно знать, как создаются и что собой представляют консольные приложения
(лекция 1 и 2). В лекции 2 даны начальные сведения о вводе и выводе данных с консоли. Нужно
иметь понимание типа данных и встроенных типов данных, прежде всего – строкового типа и
арифметических типов. Нужно понимать, как объявляются переменные разных типов и как
выполняются преобразования между типами (лекции 3, 4, 5). Нужно знать, как строятся выражения
(лекции 6, 7). Ну и наконец необходимо знание операторов языка (лекция 8). Эти же лекции дают
первоначальное знакомство со средой программирования. Уже с первых шагов программирования
требуется не только знание элементов языка, но и знание возможностей встроенных классов,
предоставляемых средой программирования. В частности нужно знать встроенные классы,
предоставляющие различные сервисы – Console, Convert, Math, Random а также знание классов,
задающих встроенные типы данных –int, double, string, bool.
Следует обращать внимание на качество создаваемых программ, - их надежность, понимание
текста, наличие комментариев и тегов summary, описывающих смысл программы.
Модель модульного программирования в процедурах и функциях
Как можно раньше следует вводить понятие модульного программирования и понятия
процедур и функций, как модулей простейшего вида. Консольное приложение, строящееся по
умолчанию, легко позволяет расширить понятие программы, состоящей из одной процедуры Main,
на программу, включающую процедуры и функции. Для этого достаточно добавить в созданный по
умолчанию класс Class1 нужные процедуры и функции, которые и будут вызываться из основной
процедуры Main. Единственное, не вполне очевидное требование состоит в том, что добавляемые
процедуры и функции должны сопровождаться описателем static, объяснение специфической роли
которого можно оставить на более поздний этап. Достаточно лишь пояснить, что, если процедура
имеет модификатор static, то она может вызывать только процедуры и функции, имеющие этот же
модификатор. Поскольку процедура Main объявлена как static процедура, то и вызываемые
процедуры и функции следует снабжать этим модификатором.
Вот как может выглядеть решение некоторой задачи, в которой целесообразно создать
специальную функцию, многократно вызываемую в основной программе Main:
using System;
namespace Ch1_Example2
{
/// <summary>
/// Класс, содержащий выполняемую программу и функцию
/// </summary>
class Class1
{
/// <summary>
/// Выполняемая программа!
/// нахождение интервала, на котором полином третьей степени
/// ax^3 +bx^2 +cx + d
/// меняет знак
/// </summary>
[STAThread]
static void Main(string[] args)
{
//объявление переменных
double a,b,c, d; //коэффициенты полинома
double x1, x2; // исходный интервал
double h; //шаг прохождения интервала
string mes; //сообщение о результатах вычислений
string init; //строка ввода
// ввод исходных данных
Console.WriteLine("Введите коэффициенты полинома: "
+ "четыре вещественных числа - a, b, c, d ");
init = Console.ReadLine();
a = Convert.ToDouble(init);
init = Console.ReadLine();
b = Convert.ToDouble(init);
init = Console.ReadLine();
c = Convert.ToDouble(init);
init = Console.ReadLine();
d = Convert.ToDouble(init);
Console.WriteLine("Введите три вещественных числа - x1,x2,h"
+ " - границы интервала и шаг");
init = Console.ReadLine();
x1 = Convert.ToDouble(init);
init = Console.ReadLine();
x2 = Convert.ToDouble(init);
init = Console.ReadLine();
h = Convert.ToDouble(init);
//вычисления
double x= x1;
double p =Polinom3(a,b,c,d,x);
int signum = Math.Sign(p);
int signum1 = signum;
while(x<x2)
{
x+=h; p =Polinom3(a,b,c,d,x);
signum1=Math.Sign(p);
if( signum1 != signum)break; //обнаружена смена знака
}
//вывод результатов
if(signum == signum1)
{
mes= "На интервале [{0}, {1}] не обнаружена смена знака
полинома!";
Console.WriteLine(mes,x1, x2);
}
else
{
mes= "Обнаружена смена знака полинома! "+
"В точке x1= {0} P(x1) = {1}; В точке x2= {2} P(x2) = {3}";
Console.WriteLine(mes,x-h,Polinom3(a,b,c,d,xh),x,Polinom3(a,b,c,d,x));
}
}
static double Polinom3(double a, double b, double c, double d,
double x)
{
return(((a*x +b)*x+c)*x+d);
}
}
}
И в этой модели студенту достаточно лишь понимать, как объявляются и вызываются
процедуры и функции, оставляя пока без объяснений детали объектной технологии работы с
классами. Заметьте, функции, вызываемые в процедуре Main, добавляются в класс Class1 на том же
уровне, что и главная процедура Main. Вся информация, необходимая функции для ее работы,
передается через ее параметры, а результат вычисления значения функции возвращается в
операторе return.
Модель 80-х годов визуального программирования, управляемого событиями
Консольные приложения, несмотря на всю их полезность при изучении языка
программирования, являются анахронизмом с позиций современного программирования.
Консольное приложение позволяет ограничиваться созданием минимального пользовательского
интерфейса: приглашение к вводу, простейшее меню, повторяющийся цикл вычислений.
Современное программирование ориентировано на интерактивный способ работы с программой,
когда конечный пользователь сидит непосредственно за экраном компьютера и управляет
процессом выполнения программы. В таких условиях роль пользовательского интерфейса
становится крайне важной. Хороший интерфейс привлекает пользователей, плохой может
оттолкнуть, даже если программа прекрасно справляется со своими основными функциями.
Термин «визуальное программирование» означает создание программ, ориентированных на
интерактивный способ взаимодействия пользователя и программы, создание интерфейса
пользователя, позволяющего задавать требуемые программе данные, управлять процессом
вычислений, следить за результатами вычислений.
Термин «программирование, управляемое событиями» означает следующее: в ходе
выполнения программы могут возникать ситуации, идентифицируемые как события. В этом случае
выполнение программы приостанавливается, операционная система получает сообщение от
программы о возникновении в ней «события» и вызывает обработчик события, предусмотренный
программой, а в случае отсутствия такового, универсальный обработчик, предусмотренный самой
операционной системой. В результате начинает выполняться программа обработчика события.
Такой стиль программирования, в котором ход процесса вычислений определяется событиями и их
обработчиками, называется программированием, управляемом событиями.
Визуальное программирование, как правило, является программированием, управляемым
событиями. Большинство событий инициируется пользователем, благодаря возможностям
интерфейса, специально спроектированным для того, чтобы пользователь в зависимости от уже
полученных результатов мог определять дальнейший ход вычислений, выбирая подходящие
пункты меню, нажимая специальные командные кнопки, и производя другие действия, приводящие
к событиям, для которых предусмотрены соответствующие обработчики.
Windows-приложение в полной мере отвечает этой модели программирования, позволяя
достаточно просто и естественно создавать визуальную, управляемую событиями программу. По
умолчанию в этом приложении создается форма, которую можно населять элементами управления,
определяющими интерфейс пользователя. На первых порах можно ограничиться минимальным
набором элементов управления – командными кнопками, текстовыми окнами и списками, что
позволяет вводить и выводить как скалярные данные, так и массивы, а также запускать при
нажатии командных кнопок нужные обработчики событий. В главной процедуре Main, с которой
начинается выполнение программы, создается объект, задающий спроектированную форму, и эта
форма открывается на экране. Дальнейший ход вычислений определяется конечным пользователем,
– какие данные он будет вводить в текстовые окна, какие командные кнопки будет нажимать.
Конечному пользователю программы обычно намного удобнее работать с Windows-приложением,
чем с консольным приложением с его скудным интерфейсом. Платой за это является
необходимость спроектировать и реализовать удобный пользовательский интерфейс.
Рассмотрим предыдущий пример, реализованный как Windows-приложение. Начнем с
проектирования интерфейса. Вот как может выглядеть форма, позволяющая проводить
исследование поведения полинома третьей степени на некотором заданном интервале:
Рис. 1_1 Интерфейс программы, анализирующей поведение полинома
При построении интерфейса использовались такие элементы управления, как метки
(текстовые окна для вывода текста) окна редактирования, позволяющие выводить и вводить в них
текст и редактировать его, командные кнопки. Для удобства пользователя использовались также
окна, позволяющие группировать информацию, отделяя в данном случае исходную информацию от
получаемых результатов.
Возникает естественный вопрос, где размещать программный код, позволяющий найти
решение задачи. В консольном приложении ответ был очевиден – в процедуре Main, заготовка
которой строится по умолчанию, но не содержит никакого кода в своем теле. В Windowsприложении построенная по умолчанию процедура Main не пуста, – ее код выполняет вполне
определенную работу – создает и открывает форму, с которой должен работать пользователь.
Вполне допустимым является подход, когда процедура Main не корректируется, а вся дальнейшая
работа приложения определяется обработчиками событий открывающейся формы.
В нашем примере можно обойтись одним обработчиком, реагирующим на событие Click
командной кнопки. В него и поместим весь код, который в консольном приложении размещался в
процедуре Main:
private void button1_Click(object sender, System.EventArgs e)
{
//объявление переменных
double a,b,c, d; //коэффициенты полинома
double x1, x2; // исходный интервал
double h; //шаг прохождения интервала
string mes; //сообщение о результатах вычислений
// ввод исходных данных
a = Convert.ToDouble(this.textBoxA.Text);
b = Convert.ToDouble(this.textBoxB.Text);
c = Convert.ToDouble(this.textBoxC.Text);
d = Convert.ToDouble(this.textBoxD.Text);
x1 = Convert.ToDouble(this.textBoxX1.Text);
x2 = Convert.ToDouble(this.textBoxX2.Text);
h = Convert.ToDouble(this.textBoxH.Text);
//вычисления
double x= x1;
double p =Polinom3(a,b,c,d,x);
int signum = Math.Sign(p);
int signum1 = signum;
while(x<x2)
{
x+=h; p =Polinom3(a,b,c,d,x);
signum1=Math.Sign(p);
if( signum1 != signum)break; //обнаружена смена знака
}
//вывод результатов
if(signum == signum1)
{
mes= "Не обнаружена смена знака полинома!";
this.textBoxMes.Text = mes;
this.textBoxX.Text = x1.ToString();
this.textBoxPX.Text = Polinom3(a,b,c,d,x1).ToString();
this.textBoxY.Text = x2.ToString();
this.textBoxPY.Text = Polinom3(a,b,c,d,x2).ToString();
}
else
{
mes= "Обнаружена смена знака полинома! ";
this.textBoxMes.Text = mes;
this.textBoxX.Text = (x-h).ToString();
this.textBoxPX.Text = Polinom3(a,b,c,d,x-h).ToString();
this.textBoxY.Text = x.ToString();
this.textBoxPY.Text = Polinom3(a,b,c,d,x).ToString();
}
}
Также как и в консольном приложении в построенный по умолчанию класс Form1 можно
добавить функцию Polinom3, вызываемую в обработчике события:
double Polinom3(double a, double b,double c,double d,double x)
{
return(((a*x +b)*x+c)*x+d);
}
Отмечу следующее:

На начальном этапе можно не останавливаться на деталях, связанных с параметрами
обработчика события, необходимых операционной системе. Поскольку заготовка
обработчика события строится автоматически, то программисту не приходится самому
задавать значения этих параметров.

Для функций, вызываемых из обработчиков событий, не обязательно задавать
модификатор static, поскольку сами обработчики не имеют такой модификатор.

Вся информация, необходимая функции для ее работы, передается через параметры
функции.
Задачи
Первые проекты
Назначение задач этого раздела состоит в первоначальном знакомстве со средой разработки
Visual Studio .Net, создании в ней первых проектов, получении первых навыков настройки среды
разработки, первого представления об отладочном режиме работы.
1.1
Построить консольное приложение. Дать ему имя, разместить в выбранной
директории. Проанализировать созданный программный текст. Познакомиться со средой
разработки Visual Studio .Net. Выполнить приложение в пошаговом отладочном режиме.
1.2
Построить Windows-приложение. Дать ему имя, разместить в выбранной директории.
Проанализировать созданный программный текст. Познакомиться со средой разработки
Visual Studio .Net. Выполнить приложение в пошаговом отладочном режиме.
1.3
Построить консольное приложение «Здравствуй, Мир!», выводящее на консоль строку
приветствия.
1.4
Построить Windows-приложение «Здравствуй, Мир!» с командной кнопкой и
текстовым окном. Приложение выводит в текстовое окно строку приветствия при
нажатии командной кнопки.
1.5
Построить консольное приложение «Здравствуй, человек!». Приложение вводит с
консоли имя и выводит на консоль строку приветствия, используя введенное имя. Если
вводится пустая строка, то выводится текст «Здравствуй, человек!».
1.6
Построить Windows-приложение «Здравствуй, человек!» с командной кнопкой и двумя
текстовыми окнами. Пользователь вводит имя в первое текстовое окно и при нажатии
командной кнопки получает во втором текстовом окне строку приветствия,
использующую введенное имя. Если вводится пустая строка, то выводится текст
«Здравствуй, человек!».
1.7
Построить консольное приложение «Здравствуйте, люди!». Приложение вводит с
консоли имя и выводит на консоль строку приветствия, используя введенное имя. Вводу
имени предшествует приглашение к вводу «Введите имя».
1.8
Построить Windows-приложение «Здравствуйте, люди!» с командной кнопкой и двумя
текстовыми окнами. Пользователь вводит имя в первое текстовое окно и при нажатии
командной кнопки получает во втором текстовом окне строку приветствия,
использующую введенное имя. С каждым текстовым окном связывается окно метки, в
котором дается описание назначения текстового окна.
1.9
Построить циклическое консольное приложение «Здравствуй, человек!». Приложение
вводит с консоли имя и выводит на консоль строку приветствия, используя введенное
имя. Вводу имени предшествует приглашение к вводу «Введите имя». После приветствия
на консоль выводится запрос на продолжение работы «Продолжить работу? (да, нет)». В
зависимости от введенного ответа повторяется приветствие или приложение заканчивает
работу.
Указание к задаче 1.9. Представьте выполняемый блок процедуры Main оператором цикла
следующего вида:
do
{
…
}while((answer=="Да") || (answer=="да" ));
Здесь answer переменная, содержащая ответ пользователя на предложение продолжения
работы.
Встроенные типы данных. Ввод-вывод данных.
Назначение задач этого раздела состоит в знакомстве с основными скалярными встроенными
типами, их классификацией, диапазоном возможных значений. Решение задач требует умения
объявлять, вводить и выводить значения переменных этих типов.
1.10 Построить циклическое консольное приложение «Целочисленные типы». Приложение
поочередно вводит с консоли значения целочисленных типов: sbyte, byte, short, ushort, int,
uint, long, ulong. Вводу значения предшествует приглашение к вводу. После завершения
ввода приложения выводит все введенные значения с указанием их типа.
Проанализировать, что происходит при вводе значений, не соответствующих требуемому
типу или выходящих за пределы интервала возможных значений типа.
1.11 Построить Windows-приложение «Целочисленные типы» с 16-ю помеченными
текстовыми окнами и двумя командными кнопками Пользователь вводит значения
целочисленных типов: sbyte, byte, short, ushort, int, uint, long, ulong в первые 8 окон. По
нажатию командной кнопки «Ввод значений» данные из текстовых окно становятся
значениями переменных соответствующих типов. По
нажатию командной кнопки «Вывод значений» значения переменных соответствующих
типов передаются в текстовые окна, предназначенные для вывода значений.
Проанализировать, что происходит при вводе значений, не соответствующих требуемому
типу или выходящих за пределы интервала возможных значений типа.
1.12 Построить циклическое консольное приложение «Вещественные типы». Приложение
поочередно вводит с консоли значения вещественных типов: float, double. Вводу
значения предшествует приглашение к вводу. После завершения ввода приложения
выводит все введенные значения с указанием их типа. Проанализировать, что происходит
при вводе значений, не соответствующих требуемому типу или выходящих за пределы
интервала возможных значений типа.
1.13 Построить Windows-приложение «Вещественные типы» с 4-мя помеченными
текстовыми окнами и двумя командными кнопками Пользователь вводит значения
вещественных типов: float, double в первые 2 окна. По нажатию командной кнопки «Ввод
значений» данные из текстовых окно становятся значениями переменных
соответствующих типов. По нажатию командной кнопки «Вывод значений» значения
переменных соответствующих типов передаются в текстовые окна, предназначенные для
вывода значений. Проанализировать, что происходит при вводе значений, не
соответствующих требуемому типу или выходящих за пределы интервала возможных
значений типа.
1.14 Построить циклическое консольное приложение «Decimal тип». Приложение
поочередно вводит с консоли значения типа Decimal c целой и дробной частью. Вводу
значения предшествует приглашение к вводу. После завершения ввода приложение
выводит введенное значение с указанием типа. Проанализировать, что происходит при
вводе значений, не соответствующих требуемому типу или выходящих за пределы
интервала возможных значений типа.
1.15 Построить Windows-приложение «Decimal тип» с 2-мя помеченными текстовыми
окнами и двумя командными кнопками Пользователь вводит значения типа Decimal c
целой и дробной частью в текстовое окно. По нажатию командной кнопки «Ввод
значений» данные из текстового окна становятся значением переменной
соответствующего типа. По нажатию командной кнопки «Вывод значений» значение
переменной передается в текстовое окно, предназначенное для вывода значений.
Проанализировать, что происходит при вводе значений, не соответствующих требуемому
типу или выходящих за пределы интервала возможных значений типа.
1.16 Построить циклическое консольное приложение «Строковый тип». Приложение
поочередно вводит с консоли символы и строки – значения типов char и string. Вводу
значения предшествует приглашение к вводу. После завершения ввода приложение
выводит введенное значение с указанием типа. Проанализировать, что происходит при
вводе значений, не соответствующих требуемому типу.
1.17 Построить Windows-приложение «Строковый тип». Пользователь вводит значения в
текстовое окно. По нажатию командной кнопки «Ввод значений» данные из текстового
окна становятся значением переменной соответствующего типа. По нажатию командной
кнопки «Вывод значений» значение переменной передается в текстовое окно,
предназначенное для вывода значений. Проанализировать, что происходит при вводе
значений, не соответствующих требуемому типу.
1.18 Построить циклическое консольное меню-приложение «Выбор». Приложение выводит
на консоль нумерованные пункты меню, предлагая затем пользователю задать номер
выбранного им пункта меню. При вводе пользователем соответствующего номера
начинает выполняться выбранный пункт меню. В данной задаче команды меню
определяют тип значений, вводимых и выводимых на консоль (смотри задачи 1.10 –
1.18).
1.19 Построить Windows-приложение «Выбор». В одно из текстовых окон пользователь
вводит тип переменной, в другое – значение. По нажатию командной кнопки «Ввод
значений» данные из текстового окна становятся значением переменной, тип которой
задан в первом текстовом окне. По нажатию командной кнопки «Вывод значений»
значение переменной передается в текстовое окно, предназначенное для вывода
значений.
Выражения. Оптимизация выражений. Память или время.
Первая цель, стоящая при решении задач этого раздела, состоит в том, чтобы научиться
записывать различные выражения – арифметические, логические, строковые – на языке C#. От
программиста требуется знание всех операций, которые могут применяться при построении
выражений, знания их точной семантики, понимания тех преобразований операндов, которые могут
выполняться при выполнении операций. Но есть и другие не менее важные цели, которые следует
ставить на этом этапе.
Память и время – два основных ресурса. В распоряжении программиста при решении задач есть
два основных ресурса – это память компьютера и его быстродействие. На первых порах кажется,
что оба эти ресурса безграничны и потому можно не беспокоиться о том, как тратятся эти ресурсы.
Эти представления иллюзорны. Многие задачи, возникающие на практике, таковы, что имеющихся
ресурсов не хватает и требуется жесткая их экономия. Вот два простых примера. Если в программе
есть трехмерный массив A: double[,,]A = new double[n,n,n],то уже при n =1000 оперативной памяти
современных компьютеров не хватит для хранения элементов этого массива. Если приходится
решать задачу, подобную задаче о «ханойской башне», где время решения задачи T = O(2n), то уже
при n = 64 никакого быстродействия всех современных компьютеров не хватит для решения этой
задачи в сколь либо допустимые сроки. Программист обязан уметь оценивать объем ресурсов,
требуемых программе. Говоря о ресурсах, требуемых программе P, часто используют термины –
временная и емкостная сложность – T(P) и V(P). Выражения представляют хорошую начальную
базу для оценивания этих характеристик.
Характеристики T(P) и V(P) обычно взаимосвязаны. Увеличивая расходы памяти, можно
уменьшить время решения задачи, или, выбирая другое решение, сократить расходы памяти,
увеличивая время работы. Одна из реальных задач, стоящих перед профессиональным
программистом – это нахождение нужного компромисса между памятью и временем. Помните:
«Выбора тяжко бремя – память или время!»
Как этот компромисс достигается на уровне выражений? Если в исходном выражении можно
выделить повторяющиеся подвыражения, то для них следует ввести временные переменные.
Увеличивая расходы памяти на введение дополнительных переменных, уменьшаем общее время
вычисления выражения, поскольку каждое из подвыражений будет вычисляться только один раз.
Этот прием целесообразно применять и тогда, когда не преследуется цель экономии времени.
Введение дополнительных переменных уменьшает сложность выражения, что облегчает его
отладку и способствует повышению надежности программы. Вероятность пропустить ошибку в
записи громоздкого выражения значительно выше, чем при записи нескольких простых выражений.
Именованные константы. Еще один важный урок, который должны давать задачи этого
раздела, это введение именованных констант. Помните:
«Каждой константе имя давайте,
Числа без имени из программ изгоняйте!»
Исключением могут быть простые константы – 0, 1, 2, 3. Если, как это часто бывает,
изменяется значение константы, то это изменение должно делаться только в одном месте, там, где
эта константа определяется.
1.20 Построить циклическое консольное меню-приложение «Арифметические операции».
Команды меню задают арифметические операции, допустимые в выражениях языка C#.
При выборе пункта меню пользователь получает приглашение к вводу одного или двух
значений в зависимости от выбранного пункта меню, затем над значениями выполняется
соответствующая операция, и ее результат выводится на консоль.
1.21 Построить Windows-приложение «Арифметические операции». В одно или два
текстовых окна пользователь вводит значения. По нажатию командной кнопки,
задающей тип арифметической операции, над введенными значениями выполняется
соответствующая операция, и ее результат выводится в текстовое окно, предназначенное
для вывода значений.
1.22 Построить циклическое консольное меню-приложение «Логические операции».
Команды меню задают логические и условные логические операции, допустимые в
выражениях языка C#. При выборе пункта меню пользователь получает приглашение к
вводу одного или двух значений в зависимости от выбранного пункта меню, затем над
значениями выполняется соответствующая операция, и ее результат выводится на
консоль. В зависимости от типа операции значениями могут быть как логические, так и
целочисленные константы.
1.23 Построить Windows-приложение «Логические операции». В одно или два текстовых
окна пользователь вводит значения, которые могут быть логическими или
целочисленными константами. По нажатию командной кнопки, задающей тип
логической или условной логической операции, над введенными значениями
выполняется соответствующая операция, и ее результат выводится в текстовое окно,
предназначенное для вывода значений.
1.24 Построить циклическое консольное меню-приложение «Операции отношения и
сдвига». Команды меню задают операции отношения или сдвига, допустимые в
выражениях языка C#. При выборе пункта меню пользователь получает приглашение к
вводу значений, затем над значениями выполняется соответствующая операция, и ее
результат выводится на консоль.
1.25 Построить Windows-приложение «Операции отношения и сдвига». В текстовые окна
пользователь вводит значения операндов операции. По нажатию командной кнопки,
задающей операцию отношения или сдвига, над введенными значениями выполняется
соответствующая операция, и ее результат выводится в текстовое окно, предназначенное
для вывода значений.
1.26 Построить циклическое консольное меню-приложение «Класс Math». Команды меню
задают функции, определенные в классе Math. При выборе пункта меню пользователь
получает приглашение к вводу значений, затем к значениям применяется
соответствующая функция, и ее результат выводится на консоль.
1.27 Построить Windows-приложение «Класс Math». В одно или два текстовых окна
пользователь вводит значения. По нажатию командной кнопки, задающей функцию
класса Math, к введенным значениям применяется соответствующая функция, и ее
результат выводится в текстовое окно, предназначенное для вывода значений.
1.28 (*) Построить Windows-приложение «Стандартный калькулятор», аналогичный
Windows калькулятору – приложению Calculator в режиме Standard.
1.29 (**) Построить Windows-приложение «Научный калькулятор», аналогичный Windows
калькулятору – приложению Calculator в режиме Scientific.
1.30 Построить консольное приложение «Expression1». Приложение вычисляет значение x и
выводит его на консоль, где
(123  527 *31/ 732 ) *(123  527 *31/ 732 )
x
3
123  527 *31/ 732  233
Вычисление выражения построить так, чтобы минимизировать время его вычисления.
Оцените время вычисления выражения в условных единицах (уе), исходя из следующих
предположений: присваивание – 1 уе, операции сдвига –2 уе, сложение, вычитание – 3 уе,
умножение – 5 уе, деление – 7 уе, вызов стандартной функции – 13 уе.
Обоснуйте корректность вычисления значения выражения?
Поочередно изменяйте значения числовых констант, участвующих в выражении,
например замените 527 на 526, 85. Если изменения требуется вносить в нескольких
местах программного текста, то подумайте о более разумном способе записи этого
выражения.
1.31 Построить Windows-приложение «Expression1». Приложение вычисляет значение x и
выводит его в текстовое окно, где
x
(123  527 *31/ 732 ) *(123  527 *31/ 732 )
123  527 *31/ 732  233
Вычисление выражения построить так, чтобы минимизировать время его вычисления.
Оцените время вычисления выражения в условных единицах (см. 1.30).
Обоснуйте корректность вычисления значения выражения?
Поочередно изменяйте значения числовых констант, участвующих в выражении,
например замените 527 на 526, 85. Если изменения требуется вносить в нескольких
местах программного текста, то подумайте о более разумном способе записи этого
выражения.
3
1.32 Построить консольное приложение «Expression2». Приложение вычисляет значение x и
выводит его на консоль, где
25 (12,3  52,7 * 31 / 732,5 ) * (12,3  52,7 * 31 / 732,5 )
x
3
12,3  52,7 * 31 / 732,5  0,233 * 104 / 27
Вычисление выражения построить так, чтобы минимизировать время его вычисления.
Оцените время вычисления выражения в условных единицах (см. 1.30).
Обоснуйте корректность вычисления значения выражения?
1.33 Поочередно изменяйте значения числовых констант, участвующих в выражении,
например замените 527 на 526, 85. Если изменения требуется вносить в нескольких
местах программного текста, то подумайте о более разумном способе записи этого
выражения.
1.34 Построить Windows-приложение «Expression2». Приложение вычисляет значение x и
выводит его в текстовое окно, где
25 (12,3  52,7 * 31 / 732,5 ) * (12,3  52,7 * 31 / 732,5 )
x
3
12,3  52,7 * 31 / 732,5  0,233 * 104 / 27
Вычисление выражения построить так, чтобы минимизировать время его вычисления.
Оцените время вычисления выражения в условных единицах (см. 1.30).
Обоснуйте корректность вычисления значения выражения?
Поочередно изменяйте значения числовых констант, участвующих в выражении,
например замените 527 на 526, 85. Если изменения требуется вносить в нескольких
местах программного текста, то подумайте о более разумном способе записи этого
выражения.
1.35 Построить консольное приложение «Expression3». Приложение вычисляет значение x и
выводит его на консоль, где
25 (sin( 12,3)  cos(52,7 * 31 / 732,5 )) * (sin( 12,3)  cos(52,7 * 31 / 732,5 ))
x
lg( 3 12,3  52,7 * 31 / 732,5 )  0,233 * 104 / 27
Вычисление выражения построить так, чтобы минимизировать время его вычисления.
Оцените время вычисления выражения в условных единицах (см. 1.30).
Обоснуйте корректность вычисления значения выражения?
Поочередно изменяйте значения числовых констант, участвующих в выражении,
например замените 527 на 526, 85. Если изменения требуется вносить в нескольких
местах программного текста, то подумайте о более разумном способе записи этого
выражения.
1.36 Построить Windows-приложение «Expression3». Приложение вычисляет значение x и
выводит его в текстовое окно, где
25 (sin( 12,3)  cos(52,7 * 31 / 732,5 )) * (sin( 12,3)  cos(52,7 * 31 / 732,5 ))
x
lg( 3 12,3  52,7 * 31 / 732,5 )  0,233 * 104 / 27
Вычисление выражения построить так, чтобы минимизировать время его вычисления.
Оцените время вычисления выражения в условных единицах (см. 1.30).
Обоснуйте корректность вычисления значения выражения?
Поочередно изменяйте значения числовых констант, участвующих в выражении,
например замените 527 на 526, 85. Если изменения требуется вносить в нескольких
местах программного текста, то подумайте о более разумном способе записи этого
выражения.
1.37 Построить консольное приложение «Expression4». Приложение вычисляет значение x и
выводит его на консоль, где
2n (sin( a )  cos(b * c / d m )) * (sin( a )  cos(b * c / d m ))
x
lg( 3 a  b * c / d m )  e / 2 p
Вычисление выражения построить так, чтобы минимизировать время его вычисления. В
вычисляемом выражении m, n, p, a, b, c, d, e – это имена констант.
1.38 Построить Windows-приложение «Expression4». Приложение вычисляет значение x и
выводит его в текстовое окно, где
2n (sin( a )  cos(b * c / d m )) * (sin( a )  cos(b * c / d m ))
x
lg( 3 a  b * c / d m )  e / 2 p
Вычисление выражения построить так, чтобы минимизировать время его вычисления. В
вычисляемом выражении m, n, p, a, b, c, d, e – это имена переменных, значения которых
задаются в соответствующих текстовых окнах.
1.39 (**) Построить консольное приложение «Expression5». Приложение вычисляет
значение x и время T в миллисекундах, требуемое для n-кратного (n =100000) его
вычисления, где
2n (sin( a )  cos(b * c / d m )) * (sin( a )  cos(b * c / d m ))
x
lg( 3 a  b * c / d m )  e / 2 p
Для вычисления времени использовать возможности класса DateTime, его свойства Now,
Hour и другие.
Вычисление выражения построить разными способами. Проанализировать, как это
влияет на эффективность вычислений по времени.
1.40 (**) Построить Windows-приложение «Expression5». Приложение вычисляет значение
x и время T в миллисекундах, требуемое для n-кратного (n =100000) его вычисления, где
2n (sin( a )  cos(b * c / d m )) * (sin( a )  cos(b * c / d m ))
x
lg( 3 a  b * c / d m )  e / 2 p
Для вычисления времени использовать возможности класса DateTime, его свойства Now,
Hour и другие.
Вычисление выражения построить разными способами. Проанализировать, как это
влияет на эффективность вычислений по времени.
1.41 Построить Windows-приложение «Круг». Дано: r – радиус круга, alpha – центральный
угол в градусах. Вычислить: диаметр, длину окружности, площадь круга, Площадь
сектора, площадь сегмента и длину хорды, определяемую центральным углом.
1.42 Построить Windows-приложение «Квадрат». Дано: сторона квадрата – a. Точки B и C
расположены на сторонах квадрата, примыкающих к вершине квадрата A. Расстояние AB
= b, AC = c. Вычислить: площадь четырехугольника OBAC, где О – центр квадрата.
Вычислить OB, OC и углы четырехугольника.
1.43 Построить Windows-приложение «Треугольник». Дано: стороны треугольника a,b,c.
Вычислить остальные элементы треугольника.
Преобразования в выражениях. Семантика присваивания.
1.44 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что выражение a ● b (здесь ● –
символ применяемой операции) не требует преобразования типов операндов.
1.45 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что выражение a ● b (здесь ● –
символ применяемой операции) требует преобразования типов операндов, но все
преобразования типов неявные и выполняются автоматически.
1.46 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что выражение a ● b (здесь ● –
символ применяемой операции) требует явного преобразования типов операндов.
1.47 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что выражение a ● b (здесь ● –
символ применяемой операции) требует явного преобразования типов операндов.
Рассмотрите только такие преобразования, которые можно выполнить любым из трех
доступных способов преобразования типов: методами класса Convert, методами Parse,
приведением к типу – кастингом.
1.48 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что выражение a ● b (здесь ● –
символ применяемой операции) требует явного преобразования типов операндов.
Рассмотрите только такие преобразования, которые не допускают приведения к типу –
кастинг.
1.49 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что выражение a ● b (здесь ● –
символ применяемой операции) требует явного преобразования типов операндов.
Рассмотрите только такие преобразования, которые можно выполнить только с помощью
методов класса Convert.
1.50 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что тип T выражения a ● b
(здесь ● – символ применяемой операции) T = T1 = T2.
1.51 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что тип T выражения a ● b
(здесь ● – символ применяемой операции) T = T1 != T2.
1.52 Пусть a и b – переменные типов T1 и T2 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1 и T2 такие, что тип T выражения a ● b
(здесь ● – символ применяемой операции) T != T1 != T2.
1.53 Пусть a, b и c– переменные типов T1, T2 и T3 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1, T2 и T3 такие, что при присваивании c= a
● b (здесь ● – символ применяемой операции) не возникает необходимости
преобразования типа T выражения правой части к типу T3.
1.54 Пусть a, b и c– переменные типов T1, T2 и T3 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1, T2 и T3 такие, что при присваивании c= a
● b (здесь ● – символ применяемой операции) возникает необходимость преобразования
типа T выражения правой части к типу T3. Рассмотрите только неявные преобразования,
выполняемые автоматически.
1.55 Пусть a, b и c– переменные типов T1, T2 и T3 соответственно. Постройте приложение
(консольное или Windows), в котором для каждой из допустимых в языке C# операций
задайте все возможные комбинации типов T1, T2 и T3 такие, что при присваивании c= a
● b (здесь ● – символ применяемой операции) возникает необходимость преобразования
типа T выражения правой части к типу T3. Рассмотрите только явные преобразования.
Покажите, в каких ситуациях применим тот или иной способ преобразования типов –
методы класса Convert, методы Parse, приведение к типу.
Альтернатива и разбор случаев
1.56 Постройте консольное и Windows-приложение, которое по заданным коэффициентам
a, b, c находит корни квадратного уравнения.
1.57 Постройте консольное и Windows-приложение, которое по заданному значению
аргумента x вычисляет значение функции y=F(x), где функция F(x) задана соотношением:
1 если x  0
F ( x )  { 0 если x  0
- 1 если x  0
Постройте консольное и Windows-приложение, которое по заданному значению
аргумента x вычисляет значение функции y=F(x), где функция F(x) задана графиком:
F(x)
1
x
-1
1
1.58 Постройте консольное и Windows-приложение, которое по заданному значению
аргумента x вычисляет значение функции y=F(x), где функция F(x) задана графиком:
F(x)
1
1
-1
x
1.59 Постройте консольное и Windows-приложение, которое по заданному значению
аргумента x вычисляет значение функции y=F(x), где периодическая функция F(x) задана
графиком:
F(x)
1
1
-1
x
1.60 Постройте консольное и Windows-приложение, которое по заданным координатам x и
y определяет, принадлежит ли точка (x, y) одной из 6 дорог (a, b, c, d, e, f), показанных на
графике. Если точка принадлежит дороге, то укажите какой именно дороге, если
принадлежит двум дорогам, то и этот факт следует отразить в результирующем
F(x)
a
f
1
b
-1
сообщении.
d
e
1
x
c
1.61 Дана точка A с координатами (x, y) и два прямоугольника Q1 и Q2 со сторонами,
параллельными осям координат. Каждый из прямоугольников задается парой точек
Q1(p1, p2), Q2(p3, p4), определяющих левый нижний и правый верхний углы
прямоугольника. Постройте консольное и Windows-приложение, которое определяет,
принадлежит ли точка A(x, y) хотя бы одному из прямоугольников Q1 и Q2. Если точка
принадлежит прямоугольнику, то следует сообщить, какому именно прямоугольнику,
если принадлежит двум прямоугольникам, то и этот факт должен быть отражен в
результирующем сообщении. Если точка принадлежит границе прямоугольника, то и это
должно быть отображено в сообщении.
1.62 Дана точка A с координатами (x, y) и мишень – 10 концентрических кругов с центром в
начале координат и радиусами R1, R2 … R10. Постройте консольное и Windowsприложение, которое определяет количество выбитых очков. Предполагается, как
обычно, что за попадание точки в круг самого малого радиуса начисляется 10 очков и так
далее до одного очка. За попадание в «молоко» очки не начисляются.
1.63 (*) Дана точка A с координатами (x, y) и треугольник, заданный своими вершинами –
точками Q1(x1, y1), Q2(x2, y2) и Q3(x3, y3). Постройте консольное и Windows-приложение,
которое определяет, принадлежит ли точка A треугольнику (находится внутри его или на
его границах).
1.64 Дан текст T. Постройте консольное и Windows-приложение, которое проводит
частотный анализ, определяя частоту вхождения букв А, Б, … Я (больших и малых) в
текст T.
1.65 Дан массив элементов с элементами, принимающими одно из четырех значений:
белый, черный, красный, желтый. Постройте консольное и Windows-приложение,
которое определяет частоту вхождения элементов каждого цвета в массив.
1.66 Студент, приходящий на экзамен, характеризуется тремя булевскими переменными P1,
P2 и P3. Переменная P1 имеет значение true, если студент знает определения, и false в
противном случае. Переменная P2 имеет значение true, если студент умеет доказывать
теоремы, и false в противном случае. Переменная P3 имеет значение true, если студент
умеет решать задачи, и false в противном случае. Постройте консольное и Windowsприложение «Строгий экзаменатор», в котором экзаменатор руководствуется следующим
алгоритмом: он спрашивает определение и ставит оценку «неуд», в случае его незнания.
Студенту, знающему определение, предлагается доказать теорему, в случае неуспеха
ставится оценка «уд». Студенту, знающему определения и умеющему доказывать
теоремы, предлагается решить задачу, в случае неуспеха ставится оценка «хор», в случае
успеха – «отл».
1.67 Студент, приходящий на экзамен, характеризуется тремя булевскими переменными P1,
P2 и P3. Переменная P1 имеет значение true, если студент знает определения, и false в
противном случае. Переменная P2 имеет значение true, если студент умеет доказывать
теоремы, и false в противном случае. Переменная P3 имеет значение true, если студент
умеет решать задачи, и false в противном случае. Постройте консольное и Windowsприложение «Добрый экзаменатор», в котором экзаменатор руководствуется следующим
алгоритмом: он предлагает решить задачу и в случае успеха ставит оценку – «отл».
Студенту, не умеющему решать задачи, предлагается доказать теорему, в случае успеха
ставится оценка «хор». Студенту, не умеющему решать задачи и не умеющему
доказывать теоремы, предлагается сформулировать определение и в случае его незнания
ставится оценка «неуд», в случае успеха ставится оценка «уд».
Вычисление сумм, произведений и рекуррентные соотношения.
Вычисление конечных сумм и произведений – это наиболее часто встречающийся тип
элементарных задач, шаблон решения которых должен быть заучен, как 2*2. Какова бы не была
сложность выражений, стоящих под знаком суммы, задачу всегда можно записать в виде:
n
S   ak
k 1
и применить для ее решения следующий шаблон:
S=0;
for(int k=1; k<=n; k++)
{
//Вычислить текущий член суммы ak
…
S+=ak;
}
Часто приходится пользоваться слегка расширенным шаблоном:
Init;
for(int k=1; k<=n; k++)
{
//Вычислить текущий член суммы ak
…
S+=ak;
}
В этом шаблоне Init представляет группу операторов, инициализирующих значения
переменных, используемых в цикле, значениями, обеспечивающими корректность применения
цикла. В частном случае, рассмотренном выше, инициализация сводится к заданию значения
переменной S. Заметьте, если перед началом цикла не позаботиться о том, чтобы эта переменная
была равна нулю, то после завершения цикла результат не будет верным.
В этой схеме основные проблемы могут быть связаны с вычислением текущего члена суммы
ak. Нужно понимать, что ak – это простая переменная, а не массив. Значения этой переменной
вычисляются заново на каждом шаге цикла, задавая очередной член суммирования. Кроме того,
следует заботиться об эффективности вычислений, применяя два основных правила, позволяющие
уменьшить время вычислений:

Чистка цикла. Все вычисления, не зависящие от k, должны быть вынесены из цикла (в
раздел Init).

Рекуррентная формула. Часто можно уменьшить время вычислений ak, используя
предыдущее значение ak. Иногда приходится вводить дополнительные переменные,
хранящие уже вычисленные значения нескольких членов суммы. Рекуррентная формула
выражает новое значение ak через предыдущее значение и дополнительные переменные,
если они требуются. Начальные значения ak и дополнительных переменных должны
быть корректно установлены перед выполнением цикла в разделе Init. Заметьте, если
начальное значение ak вычисляется в разделе Init до цикла, то схема слегка
модифицируется, - вначале выполняется прибавление ak к S, а затем новое значение ak
вычисляется по рекуррентной формуле.
Рассмотрим пример. Пусть необходимо вычислить сумму:
n
S  x
k 0
k
2
k!
3
n
 1 x  x
 x  ...  x
1
2!
3!
n!
Тогда в соответствии с шаблоном
ak  x k k !
Можно построить рекуррентную формулу для ak, поскольку каждое следующее значение
равно предыдущему значению, умноженному на x и деленному на k. Вычисление суммы задает
следующий фрагмент программы:
int S =0;
int ak=1;
for(int k=0; k<=n; k++)
{
S+=ak;
//Вычислить текущий член суммы ak
ak *=x/k;
}
Большинство задач этого раздела соответствуют этому шаблону. Рекуррентную формулу чаще
всего можно получить, записав выражение для ak и ak-1, вычислив затем их отношение. В некоторых
задачах (они отмечены звездочкой) получение рекуррентной формулы может требовать больших
усилий.
Начиная с этой задачи, мы будем в большинстве случаев опускать слова «Постройте
консольное и Windows-приложение», полагая их подразумевающимися по умолчанию.
Предполагается также, что консольное приложение позволяет проводить многократные
эксперименты. При необходимости в консольном приложения предполагается построение меню.
1.68 Дано натуральное число n. Вычислить сумму первых n членов расходящегося
гармонического ряда:
n
1

k 1 k
1.69 Дано натуральное число nmax и вещественное число b. Найти, если оно существует,
такое наименьшее n, меньшее nmax, что:
n
1
b

k 1 k
Если сумма nmax членов гармонического ряда меньше b, то необходимо выдать
соответствующее сообщение.
1.70 Дано натуральное число n. Вычислить сумму первых n членов ряда:
n
1

k 1 k
При суммировании исключается каждый третий член.
1.71 Дано натуральное число n. Вычислить сумму первых 2n членов ряда:
2n
( 1)k 1

k
k 1
Вычислить эту сумму четырьмя разными способами: последовательно слева направо,
последовательно справа налево, слева направо, вычисляя вначале положительные члены
ряда, затем отрицательные, справа налево, вычисляя вначале положительные члены ряда,
затем отрицательные. Сравните результаты вычислений. Чем объясняется различие в
последних цифрах при больших n? Как влияет на результат использование типов float
или double для переменных, задающих суммы и текущий член при суммировании?
1.72 Дано натуральное число n. Вычислить сумму первых 2n членов ряда:
2n
( 1) k 1

k
k 1 k ( k  1)
1.73 Дано натуральное число n. Вычислить произведение первых n членов ряда:
n
k 1

k
k 1
1.74 Даны натуральные числа n и k (n>=k). Вычислить биномиальный коэффициент Cnk :
k
n!
n( n  1) (n  k  1)
n  i 1
Cnk 


(n  k )! k !
k!
i
i 1
1.75 Даны натуральные числа n и m (n>=m). Вычислить сумму биномиальных
коэффициентов:
m
m
i
n  j 1
S   Cni  
j
i 0
i 0 j 1
Следует напомнить, что определения функций суммы и произведения предполагают:
m
a
i n
 0 если m  n
i
m
a
i n
 1 если m  n
i
1.76 Дан массив B размерности n*m и массив C размерности m*n. Вычислить сумму
диагональных элементов матрицы A = B*C:
n
m
S   bik cki
i 1 k 1
1.77 Дан массив B размерности n*m и массив C размерности m*n. Вычислить произведение
диагональных элементов матрицы A = B*C:
n
m
P    bik cki
i 1 k 1
1.78 Даны натуральные числа n и m, вещественное x. Вычислить:
n m
1  sin(ix)
S  
i 1 j 1 1  cos( jx)
Рекуррентные вычисления
1.79 Вычислить Fn - число Фибоначчи с номером n, где
F1 = 1; F2 = 1; Fk = Fk-1 + Fk-2 для k>2.
1.80 Дано натуральное число n и вещественные числа b и d. Вычислить сумму членов
арифметической прогрессии:
n
S  b  i *d
i 0
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
чтобы его вычисление требовало ровно одного сложения
1.81 Дано натуральное число n и вещественные числа b и d. Вычислить сумму членов
геометрической прогрессии:
n
S   bd i
i 0
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
чтобы его вычисление требовало ровно одного умножения.
1.82 Дано натуральное число n. Вычислить:
n
S   ai * bi
i 1
a1  1; b1  3; ak  3bk 1  1; bk  1 2 ak  bk 1 для k  1
где
1.83 Даны натуральные числа n и m (50<m<n). Вычислить:
n
S   ai * bi
i m
a1  1; b1  3; ak  3bk 1  1; bk  1 2 ak  bk 1 для k  1
где
1.84 Даны натуральные числа n и m (50<m<n). Вычислить:
n
P   ( ai  bi )
i m
где
a1  1; b1  3; ak  3bk 1  1; bk  1 2 ak  bk 1 для k  1
1.85 Дано натуральное число n и вещественное число x. Вычислить:
n
xi
S
i 0 i !
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
чтобы его вычисление выполнялось за время, равное 12 уе. (см. задачу 1.30). Сравните
вычисленное значение S со значением ex. Вычислите разность | S – ex| при различных
значениях n и x.
1.86 Дано натуральное число n и вещественное число x. Вычислить:
n
( 1)i x 2i 1
S
i 0 (2i  1)!
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
чтобы его вычисление выполнялось за время, равное 27 уе. (см. задачу 1.30). Сравните
вычисленное значение S со значением sin(x). Вычислите разность | S – sin(x)| при
различных значениях n и x.
1.87 Дано натуральное число n и вещественное число x такое, что |x| <1. Вычислить:
n
(2i  1)!! x 2i 1
S  x
где
i 1 (2i  1)(2i )!!
(2i  1)!!  1*3*5*...*(2i  1)
(2i )!!  2 * 4 *6*...* 2i
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
arcsin(x). Вычислите разность | S – arcsin(x)| при различных значениях n и x.
1.88 Дано натуральное число n и вещественное число x. Вычислить:
n
( 1)i x 2i
S
(2i )!
i 0
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
чтобы его вычисление выполнялось за время, равное 27 уе. (см. задачу 1.30). Сравните
вычисленное значение S со значением cos(x). Вычислите разность | S – cos(x)| при
различных значениях n и x.
1.89 Дано натуральное число n и вещественное число x такое, что |x| <1. Вычислить:
n

(2i  1)!! x 2i 1
S  (  x)  
где
2
i 1 (2i  1)(2i )!!
(2i  1)!!  1* 3*5*...* (2i  1)
(2i )!!  2 * 4 * 6 *...* 2i
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
arccos(x). Вычислите разность | S – arccos(x)| при различных значениях n и x.
1.90 Дано натуральное число n и вещественное число x >0. Вычислить:
n
( x  1)2i 1
S
2 i 1
i 0 (2i  1)( x  1)
Предварительно следует записать рекуррентное соотношение для текущего члена суммы.
Минимизируйте время его вычисления в условных единицах. Сравните вычисленное
значение S со значением ln(x). Вычислите разность | S – ln(x)| при различных значениях n
и x.
1.91 (**) Даны натуральные числа n и m и вещественное число x такое, что |x| < π/2.
n
22i (22i  1) Bi x 2i 1
Вычислить: S  
(2i )!
i 1
где Bi определяются следующим соотношением:
 2i 22i 1 Bi m 1
  2( i 1)
(2i )!
k 1 k
Предварительно следует записать рекуррентные соотношения, как для получения чисел
Bi, так и для вычисления S. Сравните вычисленное значение S со значением tg(x).
Вычислите разность | S – tg(x)| при различных значениях n и x.
1.92 (*) Даны натуральные числа n и m и вещественное число x такое, что |x| < π/2.
n
S   Ai * bi * ( ci  1), где
i 1
m
1
A

Вычислить: i  k 2i ;
k 1
b1 
2* x
; bk  bk 1
x2
; c  4; c  4c
1
k
k 1
2
2
Сравните вычисленное значение S со значением tg(x). Вычислите разность | S – tg(x)| при
различных значениях n и x.
1.93 Дано натуральное число n и вещественное число x . Вычислить:
n
( 1)i 1 x 2i 1
S
если | x | 1
(2i  1)
i 1

( 1)i
S  
2 i 1 (2i  1) x 2i 1
n

( 1)i
S  
2 i 1 (2i  1) x 2i 1
если
n
если
x 1
x  1
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
arctg(x). Вычислите разность | S – arctg(x)| при различных значениях n и x.
1.94 (**) Даны натуральные числа n и m и вещественное число x такое, что |x| < π.
1 n 22i Bi x 2i 1
Вычислить: S  x   (2i )!
i 1
где Bi определяются следующим соотношением:
 2i 22i 1 Bi m 1
  2( i 1)
(2i )!
k 1 k
Предварительно следует записать рекуррентные соотношения, как для получения чисел
Bi, так и для вычисления S. Сравните вычисленное значение S со значением ctg(x).
Вычислите разность | S – ctg(x)| при различных значениях n и x.
1.95 Дано натуральное число n и вещественное число x . Вычислить:
 n ( 1)i 1 x 2i 1
S  
если | x | 1
2 i 1 (2i  1)
n
S
i 1
( 1)i
(2i  1) x 2i 1
n
S  
i 1
если
( 1)i
(2i  1) x 2i 1
x 1
если
x  1
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
arcctg(x). Вычислите разность | S – arcctg(x)| при различных значениях n и x.
1.96 (**) Даны натуральные числа n и m и вещественное число x такое, что |x| < π/2.
n
Ei x 2i
S

1


Вычислить:
i 1 (2i )!
где Ei определяются следующим соотношением:
m
 2i 1Ei
( 1)k 1


22i 2 (2i )! k 1 (2k  1)2i 1
Предварительно следует записать рекуррентные соотношения, как для получения чисел
Ei, так и для вычисления S. Сравните вычисленное значение S со значением sc(x).
Вычислите разность | S – sc(x)| при различных значениях n и x.
1.97 Дано натуральное число n и вещественное число x. Вычислить:
n
x 2i 1
S
i 1 (2i  1)!
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
sh(x). Вычислите разность | S – sh(x)| при различных значениях n и x.
1.98 Дано натуральное число n и вещественное число x. Вычислить:
n
x 2i
S
i 0 (2i )!
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
ch(x). Вычислите разность | S – ch(x)| при различных значениях n и x.
1.99 Дано натуральное число n и вещественное число x такое, что |x| <1. Вычислить:
n
(2i  1)!! x 2i 1
S  x   ( 1)i
где
(2i  1)(2i )!!
i 1
(2i  1)!!  1* 3*5*...* (2i  1)
(2i )!!  2 * 4 * 6 *...* 2i
Предварительно следует записать рекуррентное соотношение для текущего члена суммы,
минимизируя время его вычисления. Сравните вычисленное значение S со значением
Arcsh(x). Вычислите разность | S – Arcsh(x)| при различных значениях n и x.
Бесконечность и компьютеры. Вычисления с точностью ε
Бесконечность для математики естественна. Множество целых чисел бесконечно, множество
рациональных чисел бесконечно, множество вещественных чисел бесконечно. Но если элементы
первых двух множеств можно пронумеровать, то последнее множество несчетно. Сколь угодно
малый промежуток вещественной оси мы бы не взяли, там находится бесконечно много
вещественных чисел. Число π и другие иррациональные числа задаются бесконечным числом цифр,
не имеющим периода. Многие базисные определения в математике основаны на понятии предела и
стремлении к бесконечности.
Мир компьютеров – это конечный мир, хотя в нем и присутствует стремление к
бесконечности. Множества, с которыми приходится оперировать в мире компьютера, всегда
конечны. Тип целых чисел в языках программирования – int – всегда задает конечное множество
целых из некоторого фиксированного диапазона. В библиотеке FCL это наглядно подтверждается
самими именами целочисленных типов System.Int16, System.Int32, System.Int64. Типы
вещественных чисел – double, float – задают конечные множества. Это достигается не только тем,
что диапазон задания вещественных чисел ограничен, но и ограничением числа значащих цифр,
задающих вещественное число. Поэтому для вещественных чисел компьютера всегда можно
указать такие два числа, между которыми нет никаких других чисел. Иррациональности компьютер
не знает, – число π всегда задается конечным числом цифр.
Там, где в математике идет речь о пределах, бесконечных суммах, сходимости к
бесконечности, в компьютерных вычислениях аналогичные задачи сводятся к вычислениям с
заданной точностью – с точностью ε. Рассмотрим например задачу о вычислении предела числовой
последовательности:
lim an  A
n
По определению число A является пределом числовой последовательности, если для любого
сколь угодно малого числа ε существует такой номер N, зависящий от ε, что для всех n, больших N,
числа an находятся в ε-окрестности числа A. Это определение дает основу для вычисления значения
предела A. Понятно, что получить точное значение A во многих случаях принципиально
невозможно, – его можно вычислить лишь с некоторой точностью и тоже не сколь угодно малой,
поскольку, как уже говорилось, есть понятие «машинного нуля» - минимального числа, все
значения меньше которого воспринимаются как нуль. В задаче 1.97 требуется вычислить значение
числа π как предел числовой последовательности. Оставаясь в рамках стандартных множеств чисел
(double, float) принципиально невозможно получить точное значение этого числа, поскольку в этих
множествах нет иррациональных чисел с бесконечным числом цифр. Но можно получить значение
этого числа с некоторой точностью. Когда два соседних члена последовательности – an и an+1 –
начинают отличаться на величину, по модулю меньшую чем δ, то можно полагать, что оба члена
последовательности попали в ε-окрестность числа A и an+1 можно принять за приближенной
значение числа A. Это рассуждение верно только при условии, что последовательность
действительно имеет предел. В противном случае этот прием может привести к ошибочным
выводам. Например, последовательность, элементы которой равны 1, если индекс элемента делится
на 3, и равны 2, если индекс не делится на 3. Очевидно, что у этой последовательности предела нет,
хотя существуют два полностью совпадающих соседних членов последовательности.
Большинство задач этого раздела посвящено вычислениям значения функций, заданных
разложением в бесконечный сходящийся ряд. И здесь не ставится задача получения абсолютно
точного результата. Достаточно вычислить значение функции с заданной точностью ε. На практике
вычисления останавливаются, когда текущий член суммы становится по модулю меньше заданного
ε. Опять таки, чтобы этот прием корректно работал, необходима сходимость ряда. Контрпримером
является задача 1.65, где вычисляется сумма гармонического ряда. Здесь для любого заданного ε
текущий член суммы, начиная с некоторого номера, становится меньше ε, но ряд расходится, и
последовательность конечных сумм ряда не имеет предела.
Рассмотрим задачу вычисления функции с использованием ее разложения в бесконечный
сходящийся ряд:

f ( x )   ak ( x )
k 1
Вот возможный шаблон ее решения:
public double f(double x)
{
double S=0,ak=1, eps=1e-8;
while(Math.Abs(ak) >eps)
{
//Вычислить ak
S+=ak;
}
return(S);
}
При применении этого шаблона следует:

Получить при возможности рекуррентную формулу, используя для вычисления нового
значения ak ранее вычисленные значения.

Использовать по возможности свойства функции f(x) для ускорения сходимости ak к
нулю, например привести x к минимально возможному диапазону для периодических
функций.

Помнить, что данный шаблон применим только тогда, когда ряд является сходящимся.

Понимать, что выполнение условия (|ak| < eps ) еще не означает, что значение функции
вычислено с точностью eps. Строго говоря, необходимо иметь оценку остаточного члена
ряда. На практике этим обстоятельством зачастую можно пренебрегать, уменьшая при
необходимости eps и достигая тем самым нужной точности вычисления f(x).
Во всех задачах этого раздела задается точность вычислений ε – малое вещественное число.
Обычно, если требуется получить результат с точностью до 5-6 значащих цифр, то ε задается
константой 10-8- 10-9 (1e-8 – 1e-9).
1.100 Вычислить с заданной точностью значение числа π, используя следующее разложение

1


4

8

в ряд:
i 1 (4i  1)(4i  1)
Предварительно следует записать рекуррентное соотношение для текущего члена суммы.
Минимизируйте время его вычисления в условных единицах. Сравните вычисленное
значение π со стандартным значением PI, возвращаемым классом Math. Для разных
значений ε вычислите n – число членов суммы, требуемых для достижения заданной
точности.
1.101 Вычислить с заданной точностью значение числа π, используя следующее разложение

1
2


6

2
в ряд:
i 1 i
Предварительно следует записать рекуррентное соотношение для текущего члена суммы.
Минимизируйте время его вычисления в условных единицах. Сравните вычисленное
значение π со стандартным значением PI, возвращаемым классом Math. Для разных
значений ε вычислите n – число членов суммы, требуемых для достижения заданной
точности.
1.102 Вычислить с заданной точностью значение числа π, используя следующее разложение


( 1)i 1
( 1)i 1
в ряд:   16 (2i  1) *52i 1  4 (2i  1) * 2392i 1
i 1
i 1
Предварительно следует записать рекуррентное соотношение для текущего члена суммы.
Минимизируйте время его вычисления в условных единицах. Сравните вычисленное
значение π со стандартным значением PI, возвращаемым классом Math. Для разных
значений ε вычислите n – число членов суммы, требуемых для достижения заданной
точности.
1.103 Вычислить с заданной точностью значение числа e, используя следующее разложение

1
e


в ряд:
i  0 (i )!
Предварительно следует записать рекуррентное соотношение для текущего члена суммы.
Минимизируйте время его вычисления в условных единицах. Сравните вычисленное
значение e со стандартным значением E, возвращаемым классом Math. Для разных
значений ε вычислите n – число членов суммы, требуемых для достижения заданной
точности.
1.104 Пусть Fn и Fn+1 – два соседних числа Фибоначчи (см. задачу 1.76). Найти с заданной
точностью предел отношения (Fn/Fn+1) при n→∞. Сравнить этот предел с «золотым
сечением» - числом x. Напомню, золотое сечение строится следующим образом. Возьмем
отрезок единичной длины и разделим его на две неравные части – большую x и меньшую
y=1-x. Сечение называется «золотым», если отношение целого к большей части равно
отношению большей части к меньшей: x/(1-x) = 1/x.
1.105 Даны два положительных числа b и d. Пусть:
b  dn
b1  b; d1  d ; bn 1  bn d n ; d n 1  n
2
Вычислить с заданной точностью пределы bn, dn, dn-bn.
1.106 Пусть x и y=1-x задают золотое сечение отрезка единичной длины (см. задачу 1.102).
Пусть задано число α такое, что α >0 и α<1. Вычислить с заданной точностью предел
последовательности bn, где b1  x; b2  y; bn   bn2  (1   )bn1
1.107 Дано вещественное число x. Вычислить с заданной точностью ex:

xi
x
e 
i 0 i !
Указание: для ускорения вычислений используйте разложение в ряд только для дробной
части числа x. Используйте умножение и константу e для вычисления en, где n – это целая
часть числа x.
1.108 Дано вещественное число x. Вычислить с заданной точностью sin(x):

( 1)i x 2i 1
sin( x )  
i 0 (2i  1)!
Указание: для ускорения вычислений используйте разложение в ряд только для
приведенного значения числа x. Помните, что sin(x) – это периодическая функция, так
что всегда можно привести x к интервалу [-π, π].
Дано вещественное число x такое, что |x| <1. Вычислить с заданной точностью

(2i  1)!! x 2i 1
arcsin( x )  x  
где
i 1 (2i  1)(2i )!!
arcsin(x): (2i  1)!!  1*3*5*...*(2i  1)
1.109
(2i )!!  2 * 4 *6*...* 2i
1.110 Дано вещественное число x. Вычислить с заданной точностью cos(x):

( 1)i x 2i
cos( x )  
(2i )!
i 0
Указание: для ускорения вычислений используйте разложение в ряд только для
приведенного значения числа x. Помните, что cos(x) – это периодическая функция, так
что всегда можно привести x к интервалу [-π, π].
1.111 Дано вещественное число x такое, что |x| <1. Вычислить с заданной точностью


(2i  1)!! x 2i 1
arccos( x )  (  x )  
где
2
i 1 (2i  1)(2i )!!
arccos(x): (2i  1)!!  1* 3*5*...* (2i  1)
(2i )!!  2 * 4 * 6 *...* 2i
1.112 Дано вещественное число x >0. Вычислить с заданной точностью ln(x):

( x  1)2i 1
ln( x)  
2i 1
i 0 (2i  1)( x  1)
1.113 Дано вещественное число x. Вычислить с заданной точностью tg(x):

22i (22i  1) Bi x 2i 1
tg ( x )  
(2i )!
i 1
где Bi определяются следующим соотношением:
 2i 22i 1 Bi  1
  2( i 1)
(2i )!
k 1 k
Указание: используйте разложение в ряд только для приведенного значения числа x.
Помните, что tg(x) – это периодическая функция, так что всегда можно привести x к
интервалу [-π/2, π/2].
1.114 Дано вещественное число x . Вычислить с заданной точностью arctg(x):

( 1)i 1 x 2i 1
arctg ( x )  
если | x | 1
(2i  1)
i 1
arctg ( x ) 

2
arctg ( x )  


i 1

2
( 1)i
(2i  1) x 2i 1


i 1
( 1)i
(2i  1) x 2i 1
если
если
x 1
x  1
1.115 (**) Дано вещественное число x. Вычислить с заданной точностью ctg(x):
1  22i Bi x 2i 1
ctg ( x )   
x i 1 (2i )!
где Bi определяются следующим соотношением:
 2i 22i 1 Bi  1
  2( i 1)
(2i )!
k 1 k
Указание: используйте разложение в ряд только для приведенного значения числа x.
Помните, что сtg(x) – это периодическая функция, так что всегда можно привести x к
интервалу [-π/2, π/2].
1.116 Дано вещественное число x . Вычислить с заданной точностью arcсtg(x):
  ( 1)i 1 x 2i 1
arcctg ( x )   
если | x | 1
2 i 1 (2i  1)
( 1)i
arcctg ( x )  
2 i 1
i 1 (2i  1) x

если
( 1)i
arcctg ( x )    
2 i 1
i 1 (2i  1) x
x 1

если
x  1
1.117 (**) Дано вещественное число x. Вычислить с заданной точностью sc(x):

E x 2i
sc( x )  1   i
i 1 (2i )!
где Ei определяются следующим соотношением:

 2i 1Ei
( 1)k 1

22i 2 (2i )! k 1 (2k  1)2i 1
Указание: используйте разложение в ряд только для приведенного значения числа x.
Помните, что sc(x) – это периодическая функция, так что всегда можно привести x к
интервалу [-π, π].
1.118 Дано вещественное число x. Вычислить с заданной точностью sh(x):

x 2i 1
sh( x )  
i 1 (2i  1)!
Указание: гиперболический синус sh(x) не является периодической функцией, поэтому
никакого приведения x выполнять не следует.
1.119 Дано вещественное число x. Вычислить с заданной точностью ch(x):

x 2i
ch( x )  
i 0 (2i )!
1.120 Дано вещественное число x. Вычислить с заданной точностью Arcsh(x):
2 i 1

i (2i  1)!! x
Arcsh( x )  x   ( 1)
где
(2i  1)(2i )!!
i 1
(2i  1)!!  1*3*5*...*(2i  1)
(2i )!!  2 * 4 *6*...* 2i
Проекты
1.121 (*) Постройте класс MyMath, имеющий те же методы, что и класс Math библиотеки
FCL.
1.122 (**) Постройте Windows-приложение, позволяющее сравнить результаты вычислений
методов класса Math и MyMath по точности и времени вычислений.
Слово, число, рисунок, музыка,– величайшие изобретения человечества.
Глава 2 Числа
Алгоритмы и задачи, рассматриваемые в этой главе, могут использоваться, как на начальном
этапе обучения программированию – именованные числа, перевод чисел из одной системы
счисления в другую, так и при изучении более сложных и специальных тем – модульная
арифметика, шифрование.
Многие задачи этой главы являются прекрасными примерами при изучении темы классов. У
класса двойственная природа – с одной стороны это модуль, с другой – тип данных. Рациональные
числа, комплексные числа, простые числа, являются естественными примерами классов, поскольку
интуитивно понятно, какой тип данных они задают. Данная глава заканчивается большим примером
построения класса Rational - рациональных чисел.
Решение задач этой главы может поддерживать изучение лекций 9-16 учебника. Эту же мысль
можно выразить и по-другому. Если считать, что приоритетом при изучении программирования
являются алгоритмы и их эффективная реализация, то сведения по языку C#, приводимые в лекциях
9-16, могут быть полезны для решения тех или иных задач из разделов главы 2.
Цифры. Системы счисления
Для записи чисел привычным способом, знакомым еще с первых классов школы, является их
запись в позиционной системе счисления. Напомним некоторые факты. В позиционной системе
счисления всегда есть цифра 1. Считается, что единицу придумал бог, а остальные цифры
придуманы человеком. Если так, то наиболее замечательной из человеческих придумок в этой
области является введение цифры 0. Цифры позиционной системы упорядочены и каждая
получатся из предыдущей прибавлением единицы. Число различных цифр в позиционной системе
счисления задает основание системы счисления – p. В привычной для нас десятичной системе
счисления p = 10 и цифрами являются знакомые всем символы: 0, 1, 2, … 9. В двоичной системе
счисления цифр всего две – 0 и 1 и p = 2. В шестнадцатеричной системе счисления p =16 и
привычных символов для обозначения цифр не хватает, так что дополнительно используются
большие буквы латинского алфавита: 0, 1, 2, … 9, A, B, C, D, E, F, где A задает 10, а F – цифру 15.
Поскольку в любой позиционной системе счисления цифры задают числа от 0 до p-1, то для числа p
уже нет специального символа. Как следствие, в любой позиционной системе счисления основание
системы счисления представляется числом 10, так что справедливы следующие соотношения: 2(10) =
10(2); 16(10) = 10(16); p(10) = 10(p). Здесь и в дальнейшем при записи числа при необходимости будем
указывать в круглых скобках и систему счисления. В обыденной жизни непреложным фактом
является утверждение «2*2=4». Мы понимаем, что столь же верным является утверждение «2*2 =
11» (в троичной системе счисления), или, если хотите «2*2 = 10», – все зависит от системы
счисления, в которой ведутся вычисления.
Целые числа в позиционных системах счисления записываются в виде последовательности
подряд идущих цифр: N=cncn-1…c0. Эта запись стала настолько естественной, что иногда теряется ее
исконный смысл: N = cn10n + cn-110n-1 + … + c1101 + c0100, при котором цифры в записи числа
представляют собой коэффициенты разложения числа N по степеням основания, так что вклад
каждой цифры в число определяется как самой цифрой, так и ее позицией k и равен ck10k. По
причине того, что вклад каждой цифры в число зависит от ее позиции, система счисления и
называется позиционной.
Запись чисел в позиционной системе легко обобщается и на числа с дробной частью:
N=cncn-1…c0,d1…dm, где дробная часть отделяется от целой символом запятой или точки. И в этом
случае остается справедливым разложение числа N по степеням основания, в котором цифры
дробной части задают коэффициенты для отрицательных степеней основания:
N = cn10n + cn-110n-1 + … + c1101 + c0100 + d110-1 + d210-2 + … + dm10-m
(1)
Понимание соотношения (1) достаточно для решения большинства задач, рассматриваемых в
этом разделе, являющихся частными случаями следующей задачи: дано представление числа в
системе с основанием p, требуется найти его представление в системе с основанием q. (Пример:
найти запись числа N=2BA37,5F(16) в троичной системе счисления).
Рассмотрим возможную схему решения подобных задач. Зная основание системы счисления p
и цифры в записи числа в этой системе счисления, нетрудно вычислить значение числа N в
десятичной системе счисления. Для этого достаточно воспользоваться соотношением (1), в котором
значения цифр и основание системы счисления – p задаются в десятичной системе и в ней же
ведутся вычисления. Например:
N=2BA37,5F(16) = 2*164 + 11*163 +10*162 + 3*161 +7 + 5*16-1 + 15*16-2
Сложнее, но достаточно просто решается и обратная задача. Зная значение числа N в
десятичной системе, нетрудно получить цифры, задающие его запись в системе с основанием q.
Представим число N в виде суммы целой и дробной частей N= C+D. Рассмотрим схему получения
цифр отдельно для целой части C и дробной – D. Пусть в системе с основанием q число C
представимо в виде C = cncn-1…c0. Тогда нетрудно получить его последнюю цифру c0 и число M =
cncn-1…c1, полученное отбрасыванием последней цифры в записи числа C. Для этого достаточно
воспользоваться операциями деления нацело и получения остатка при делении нацело: c0 = C%p; M
= C/p;
Применяя этот прием n раз, получим все цифры в записи числа C. Заметьте, имеет место
соотношение: N= (N/p)*p + N%p, где в соответствии с языком C# операция / означает деление
нацело, а % – остаток от деления нацело. Чтобы получить все цифры и сохранить их в массиве,
достаточно эту схему вставить в соответствующий цикл, что схематично можно представить
следующим почти программным текстом:
M=C; i=0;
while(M!=0)
{
c=M%p; M=M/p; Ar[i] =c; i++;
}
Для получения цифр дробной части применяется та же схема, но с некоторой модификацией.
Если цифры целой части вычисляются, начиная с последней, младшей цифры числа, то цифры
дробной части вычисляются, начиная с первой цифры после запятой. Для ее получения достаточно
имеющуюся дробь умножить на основание системы счисления и в полученном результате взять
целую часть. Для получения последующих цифр этот процесс следует применять к числу M,
представляющему дробь, из которой удалена первая цифра:
d1 =[D*p]; m={d*p}. Здесь [x] и {x} означают соответственно взятие целой и дробной части числа x.
Хотя напрямую этих операций нет в языке C#, но их достаточно просто выразить имеющимися
средствами этого языка.
Чтобы перейти от системы счисления p к системе счисления q, всегда ли следует использовать
десятичную систему в качестве промежуточной: p→ 10 → q? Нет, не всегда. В ряде случаев
удобнее использовать прием, основанный на следующем утверждении:
Если основания систем счисления связаны соотношением p = qk, то для перехода от записи
числа в системе с основанием p к записи числа в системе с основанием q достаточно каждую цифру
системы p представить группой из k цифр системы q.
Для обратного перехода из q в p достаточно сгруппировать цифры системы q и каждую
группу из k цифр заменить одной цифрой системы p. Доказательство этих утверждений оставляю
читателю.
Для программистов особую важность представляют три системы счисления – двоичная,
восьмеричная и шестнадцатеричная, основания которых связаны упомянутым соотношением: 16 =
24; 8 = 23. Рассмотрим число N, записанное в шестнадцатеричной системе N = 2A,3E(16). Чтобы
получить его запись в двоичной системе, каждую цифру запишем в двоичной системе, представив
ее группой из четырех двоичных цифр. Нетрудно видеть, что N = 00101010,00111110(2). Незначащие
нули слева и справа могут быть отброшены, так что окончательно имеем: N = 101010,0011111(2).
Переведем это число в восьмеричную систему. Для этого достаточно провести группирование по
три цифры влево и вправо от запятой соответственно для целой и дробной части, так что получим:
N = 52,174(8).
Римская система счисления
Еще сегодня для записи целых чисел, в особенности дат, используется римская система
счисления. Эта система записи чисел не является позиционной. В ее основе лежит понятие
человеческих рук с их пятью и десятью пальцами. Поэтому в этой системе есть цифры 1, 5 и 10,
записываемые с помощью символов I, V, X. Помимо этого есть еще четыре цифры – 50, 100, 500,
1000, задаваемые символами L, C, D, M. В этой системе нет цифры 0 и она не является
позиционной. Согласно правилам системы с помощью цифр римской системы можно записать все
целые числа, не превышающие 4000. Как обычно запись числа представляет собой
последовательность подряд идущих цифр, а значением числа является сумма цифр в его записи.
Например число III означает I + I + I = 3. В записи числа старшие цифры предшествуют младшим,
например CVI = C+V+I = 100+5+1=106. Из этого правила есть одно исключение. Младшая цифра
может предшествовать старшей цифре и тогда вместо сложения применяется вычитание, например
CIX = C+X-I = 100+10-1=109.
Задачи
1.123 Дано целое число N = cn-1…c0, где ci – это цифры. Получить n – число цифр в записи
числа и целочисленный массив DigitsN такой, что DigitsN[i] = ci.
1.124 Дано целое число N = cn-1…c0, где ci – это цифры. Получить строку strN, задающую
запись числа N.
Указание: конечно, можно воспользоваться стандартным методом ToString, но в задаче
требуется дать собственную реализацию этого метода.
1.125 Дано дробное число N = 0.dm-1…d0, где di – это цифры. Получить m – число цифр в
записи числа и целочисленный массив FractN такой, что FractN[i] = di.
1.126 Дано дробное число N = 0. dm-1…d0, где di – это цифры. Получить строку strN,
задающую запись числа N.
Указание: конечно, можно воспользоваться стандартным методом ToString, но в задаче
требуется дать собственную реализацию этого метода.
1.127 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры. Получить n и m – число цифр в записи целой и дробной части числа и
целочисленный массив DigitsN из n+m элементов такой, что первые его n элементов
содержат цифры целой части, а последние m элементов – дробной.
1.128 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры. Получить строку strN, задающую запись числа N.
1.129 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в двоичную систему счисления N = bk-1…b0, получить k – число цифр
и целочисленный массив DigitsN такой, что DigitsN[i] = bi, где bi – это цифры в записи
числа N в двоичной системе счисления.
Пример: N = 17(10) = 10001(2)
1.130 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в троичную систему счисления N = bk-1…b0, получить k – число цифр
и целочисленный массив DigitsN такой, что DigitsN[i] = bi, где bi – это цифры в записи
числа N в троичной системе счисления.
Пример: N = 17(10) = 122(3)
1.131 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в четверичную систему счисления N = bk-1…b0, получить k – число
цифр и целочисленный массив DigitsN такой, что DigitsN[i] = bi, где bi – это цифры в
записи числа N в четверичной системе счисления.
Пример: N = 17(10) = 101(4)
1.132 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в восьмеричную систему счисления N = bk-1…b0, получить k – число
цифр и целочисленный массив DigitsN такой, что DigitsN[i] = bi, где bi – это цифры в
записи числа N в восьмеричной системе счисления.
Пример: N = 17(10) = 21(8)
1.133 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в шестнадцатеричную систему счисления N = bk-1…b0, получить k –
число цифр и целочисленный массив DigitsN такой, что DigitsN[i] = bi, где bi – это цифры
в записи числа N в шестнадцатеричной системе счисления.
Пример: N = 17(10) = 11(16)
1.134 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в двоичную систему счисления N = bk-1…b0. Получить строку strN,
задающую запись числа N.
Пример: N = 17(10) = 10001(2)
1.135 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в троичную систему счисления N = bk-1…b0. Получить строку strN,
задающую запись числа N.
Пример: N = 17(10) = 122(3)
1.136 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в четверичную систему счисления N = bk-1…b0. Получить строку strN,
задающую запись числа N.
Пример: N = 17(10) = 101(4)
1.137 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в восьмеричную систему счисления N = bk-1…b0. Получить строку
strN, задающую запись числа N.
Пример: N = 17(10) = 21(8)
1.138 Дано целое число N = cn-1…c0, где ci – это цифры десятичной системы счисления.
Перевести число N в шестнадцатеричную систему счисления N = bk-1…b0. Получить
строку strN, задающую запись числа N.
Пример: N = 17(10) = 11(16)
1.139 Дано дробное число N = 0.dm-1…d0, где di – это цифры десятичной системы счисления.
Перевести число N в двоичную систему счисления N = 0.bk-1…b0, вычислив k цифр в его
записи, сохраняя их в целочисленном массиве DigitsN таком, что DigitsN[i] = bi, где bi –
это цифры в записи числа N в двоичной системе счисления.
Пример: N = 0.17(10) = 0.001010111(2) при k=9.
1.140 Дано дробное число N = 0.dm-1…d0, где di – это цифры десятичной системы счисления.
Перевести число N в троичную систему счисления N = bk-1…b0, вычислив k цифр в его
записи, сохраняя их в целочисленном массиве DigitsN таком, что DigitsN[i] = bi, где bi –
это цифры в записи числа N в троичной системе счисления.
Пример: N = 0.17(10) = 0.01112(3) при k=5.
1.141 Дано дробное число N = 0.dm-1…d0, где di – это цифры десятичной системы счисления.
Перевести число N в четверичную систему счисления N = bk-1…b0, вычислив k цифр в его
записи, сохраняя их в целочисленном массиве DigitsN таком, что DigitsN[i] = bi, где bi –
это цифры в записи числа N в четверичной системе счисления.
Пример: N = 0.17(10) = 0.02232(4) при k=5.
1.142 Дано дробное число N = 0.dm-1…d0, где di – это цифры десятичной системы счисления.
Перевести число N в восьмеричную систему счисления N = bk-1…b0, вычислив k цифр в
его записи, сохраняя их в целочисленном массиве DigitsN таком, что DigitsN[i] = bi, где bi
– это цифры в записи числа N в восьмеричной системе счисления.
Пример: N = 0.17(10) = 0.127(8) при k=3.
1.143 Дано дробное число N = 0.dm-1…d0, где di – это цифры десятичной системы счисления.
Перевести число N в шестнадцатеричную систему счисления N = bk-1…b0, вычислив k
цифр в его записи, сохраняя их в целочисленном массиве DigitsN таком, что DigitsN[i] =
bi, где bi – это цифры в записи числа N в шестнадцатеричной системе счисления.
Пример: N = 0.17(10) = 0.1B(16) при k=5.
1.144 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры десятичной системы счисления. Перевести число N в двоичную систему
счисления с заданной точностью, вычислив k цифр дробной части числа. Получить
строку strN, задающую запись числа N в этой системе счисления.
Пример: N = 17.17(10) = 10001. 001010111 (2)
1.145 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры десятичной системы счисления. Перевести число N в троичную систему
счисления с заданной точностью, вычислив k цифр дробной части числа. Получить
строку strN, задающую запись числа N в этой системе счисления.
Пример: N = 17.17(10) = 122. 01112 (3)
1.146 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры десятичной системы счисления. Перевести число N в четверичную систему
счисления с заданной точностью, вычислив k цифр дробной части числа. Получить
строку strN, задающую запись числа N в этой системе счисления.
Пример: N = 17.17(10) = 101. 02232 (4)
1.147 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры десятичной системы счисления. Перевести число N в восьмеричную систему
счисления с заданной точностью, вычислив k цифр дробной части числа. Получить
строку strN, задающую запись числа N в этой системе счисления.
Пример: N = 17.17(10) = 21. 127 (8)
1.148 Дано вещественное число с целой и дробной частью N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры десятичной системы счисления. Перевести число N в шестнадцатеричную систему
счисления с заданной точностью, вычислив k цифр дробной части числа. Получить
строку strN, задающую запись числа N в этой системе счисления.
Пример: N = 17.17(10) = 11. 1B (16)
1.149 Дана строка, задающая представление целого числа N = cn-1…c0, где ci – это цифры
десятичной системы счисления. Получить число N.
Эту задачу можно сформулировать и так: задайте собственную реализацию метода
ToInt32 класса Convert.
1.150 Дана строка, задающая представление вещественного числа с целой и дробной частью:
N = cn-1…c0.dm-1…d0, где ci, dj – это цифры десятичной системы счисления. Получить
число N.
Эту задачу можно сформулировать и так: задайте собственную реализацию метода
ToDouble класса Convert.
1.151 Дана строка, задающая в двоичной системе счисления представление целого числа N =
cn-1…c0, где ci – это цифры двоичной системы счисления. Получить число N.
Эту задачу можно рассматривать, как расширение класса Convert: добавление метода
FromBinaryToInt32.
1.152 Дана строка, задающая в двоичной системе счисления представление вещественного
числа с целой и дробной частью: N = cn-1…c0.dm-1…d0, где ci, dj – это цифры двоичной
системы счисления. Получить число N.
Эту задачу можно рассматривать, как расширение класса Convert: добавление метода
FromBinaryToDouble.
1.153 Дана строка, задающая в шестнадцатеричной системе счисления представление целого
числа N = cn-1…c0, где ci – это цифры шестнадцатеричной системы счисления. Получить
число N.
Эту задачу можно рассматривать, как расширение класса Convert: добавление метода
FromHexToInt32.
1.154 Дана строка, задающая в двоичной системе счисления представление вещественного
числа с целой и дробной частью: N = cn-1…c0.dm-1…d0, где ci, dj – это цифры двоичной
системы счисления. Получить число N.
Эту задачу можно рассматривать, как расширение класса Convert: добавление метода
FromHexToDouble.
1.155 Дана строка, задающая в двоичной системе счисления представление целого числа N =
cn-1…c0, где ci – это цифры двоичной системы счисления. Получить строки str4N, str8N,
str16N, задающие представление числа N в системах счисления: четверичной,
восьмеричной, шестнадцатеричной.
Указание. Используйте группирование цифр при переводе из одной системы счисления в
другую.
1.156 Дана строка, задающая в двоичной системе счисления представление вещественного
числа с целой и дробной частью: N = cn-1…c0.dm-1…d0, где ci, dj – это цифры двоичной
системы счисления. Получить строки str4N, str8N, str16N, задающие представление числа
N в системах счисления: четверичной, восьмеричной, шестнадцатеричной.
Указание. Используйте группирование цифр при переводе из одной системы счисления в
другую.
1.157 Дана строка, задающая в восьмеричной системе счисления представление целого числа
N = cn-1…c0, где ci – это цифры восьмеричной системы счисления. Получить строки str4N,
str2N, str16N, задающие представление числа N в системах счисления: четверичной,
двоичной, шестнадцатеричной.
Указание. Используйте группирование цифр при переводе из одной системы счисления в
другую.
1.158 Дана строка, задающая в восьмеричной системе счисления представление
вещественного числа с целой и дробной частью: N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры восьмеричной системы счисления. Получить строки str4N, str2N, str16N,
задающие представление числа N в системах счисления: четверичной, двоичной,
шестнадцатеричной.
Указание. Используйте группирование цифр при переводе из одной системы счисления в
другую.
1.159 Дана строка, задающая в шестнадцатеричной системе счисления представление целого
числа N = cn-1…c0, где ci – это цифры шестнадцатеричной системы счисления. Получить
строки str4N, str8N, str2N, задающие представление числа N в системах счисления:
четверичной, восьмеричной, двоичной.
Указание. Используйте группирование цифр при переводе из одной системы счисления в
другую.
1.160 Дана строка, задающая в шестнадцатеричной системе счисления представление
вещественного числа с целой и дробной частью: N = cn-1…c0.dm-1…d0, где ci, dj – это
цифры шестнадцатеричной системы счисления. Получить строки str4N, str8N, str2N,
задающие представление числа N в системах счисления: четверичной, восьмеричной,
двоичной.
Указание. Используйте группирование цифр при переводе из одной системы счисления в
другую.
1.161 (*) Заданы p и q – основания двух систем счисления, strN – строка, задающая
представление вещественного числа N в системе с основанием p. Получить строку,
задающую представление числа N в системе с основанием q, возможно с некоторой
точностью, заданной параметром k – числом цифр дробной части числа N при его записи
в системе с основанием q.
1.162 (*) Дано число N и основание системы счисления p. Получить ci – коэффициенты
разложения числа N по степеням основания с заданной точностью Eps.
Указание: В данной задаче предполагается, что N, p, ci являются вещественными
числами и для ci выполняется условие (ci < p). Пример: N=30; p=2,5; c3 = 1; c2 = 1,5; c1 =
2; c0 = 0;
N  1*2,53  1,5*2,52  2*2,51  0*2,50  30
1.163 Дано основание системы счисления p и ci – коэффициенты разложения числа N по
степеням основания. Получить число N.
Указание: В данной задаче предполагается, что N, p, ci являются вещественными
числами и для ci выполняется условие (ci < p). Пример: p=2,5; c3 = 1; c2 = 1,5; c1 = 2; c0 =
0;
N  1*2,53  1,5*2,52  2*2,51  0*2,50  30
1.164 (*) Дана строка strRome, задающая представление целого числа N, меньшего 4000, в
непозиционной римской системе счисления. Получить число N.
Пример: N= MMIV = 2004
1.165 (*) Дано целое число N, меньшее 4000. Получить строку strRome, задающую
представление числа в непозиционной римской системе счисления.
Пример: N=2005 =MMV
Именованные числа
С давних пор числа применяются для измерения физических величин – длин, площадей,
объемов. Как правило, при этом использовалась системы, вовсе не основанные на десятичной
системе, а связанные с реально применяемыми мерами – бочонками, мешками и прочей
применяемой тарой. Метрическая система мер, основанная на десятичной системе счисления,
завоевала свои позиции лишь в последние два столетия, и мы стали применять километры и
килограммы, килоджоули и килогерцы. Но рецидивы все еще дают себя знать, и примером тому
является программисты, которые сравнительно недавно ввели систему мер для измерения объема
информации. У нас байт равен 8 битам, а килобайт равен не 1000 байтов, как мог бы ожидать
человек, далекий от программирования, а - 1024 байта. И связано это с любовью компьютеров к
двоичной системе счисления, в которой 1 байт = 1000(2) битов, а 1 Кб = 10000000000(2) байтов.
Задачи
1.166 Задано число T (температура) и единица измерения (C – градусы по Цельсию, F – по
Фаренгейту, R – по Реомюру, K – по Кельвину). Определить значения температуры в
других шкалах, используя следующие соотношения:
TF  1,8TC  32; TR  0,8TC ; TK  TC  273,2;
Пример: -40 (С) = -40 (F) = -32 (R) = 233,2 (K)
1.167 Дано число N, задающее расстояние, измеренное с точностью до долей миллиметра.
Получите строку, задающее расстояние с использованием старинной китайской системы,
в которой справедливы следующие соотношения:
0,1мм.  ху; 1мм.  мяо; 1см.  хао; 0,1м.  ли;
1м.  фэн; 10 м.  цунь; 100 м.  чжи; 1км.  чан;
1.168 Дано число N, задающее расстояние, измеренное с точностью до долей миллиметра.
Получите строку, задающее расстояние с использованием старинной древнерусской
системы, в которой справедливы следующие соотношения:
1 вершок  44, 45 мм.; 1четверть  4 вершка; 1 аршин  4 четверти;
1 сажень  3 аршина; 1 верста  500 саженей; 1 миля  7 верст.
1.169 Дано число N, задающее расстояние, измеренное с точностью до долей миллиметра.
Получите строку, задающее расстояние с использованием английской системы мер
длины, в которой справедливы следующие соотношения:
1 точка ( po int)  0,3528 мм.; 1 линия (line)  6 точек; 1 дюйм  12 линий;
1 фут  12 дюймов; 1 ярд  3 фута; 1 фарлонг  220 ярдов; 1 миля  8 фарлонгов.
1.170 Дано вещественное число N, задающее объем хранимых данных в терабайтах.
Выразите значение N в гигабайтах, мегабайтах, килобайтах, байтах, битах.
Классификация чисел
Числа разделяются на классы. Целые положительные числа – N = {1, 2, 3, … } – составляют
множество натуральных чисел. Зачастую и 0 считают натуральным числом.
Множество целых чисел Z включает в себя все натуральные числа, число 0 и все натуральные
числа, взятые со знаком минус: Z = {0, 1, -1, 2, -2, …}.
Каждое рациональное число x можно задать парой целых чисел (m, n), где m является
числителем, n – знаменателем числа: x = m/n. Эквивалентным представлением рационального числа
является его задание в виде числа, записанного в позиционной десятичной системе счисления, где
дробная часть числа может быть конечной или бесконечной периодической дробью. Например,
число x = 1/3 = 0,(3) представляется бесконечной периодической дробью.
Числа, задаваемые бесконечными непериодическими дробями, называются иррациональными
числами. Таковыми являются, например все числа вида √p, где p – простое число.
Иррациональными являются известные всем числа π и e.
Объединение множеств целых, рациональных и иррациональных чисел составляет множество
вещественных чисел. Геометрическим образом множества вещественных чисел является прямая
линия – вещественная ось, где каждой точке оси соответствует некоторое вещественное число, так
что вещественные числа плотно и непрерывно заполняют всю вещественную ось.
Плоскость представляет геометрический образ множества комплексных чисел, где вводятся
уже две оси – вещественная и мнимая. Каждое комплексное число, задаваемое парой вещественных
чисел, представимо в виде: x = a+b*i, где a и b – вещественные числа, которые можно
рассматривать, как декартовы координаты числа на плоскости.
Делители и множители
Рассмотрим сейчас классификацию, которая делит множество натуральных чисел на два
подмножества – простых и составных чисел. В основе этой классификации лежит понятие
делимости натуральных чисел. Если n делится нацело на d, то говорят, что d «делит» n,что
записывают в виде: d|n. Заметьте, это определение, возможно, не соответствует интуитивному
пониманию: d «делит» n, если n делится на d, а не наоборот. Число d называется делителем числа n.
У каждого числа n есть два тривиальных делителя – 1 и n. Делители, отличные от тривиальных,
называются множителями числа n. Число n называется простым, если у него нет делителей,
отличных от тривиальных. Простые числа делятся только на 1 и сами на себя. Числа, у которых
есть множители, называются составными. Число 1 является особым числом, поскольку не
относится ни к простым, ни к составным числам. Отрицательные числа также не относятся ни к
простым, ни к составным, но всегда можно рассматривать модуль числа и относить его к простым
или составным числам.
Любое составное число N можно представить в виде произведения его множителей: N = q1 *
q2 * … * qk. Это представление не единственно, например 96 = 8*12 = 2*3*16. Однако для каждого
составного числа N существует единственное представление в виде произведения степеней простых
чисел: N = p1r1 * p2r2 * … * pkrk, где pi – простые числа и pk < pk+1, называемое разложением числа N
на простые множители. Например 96 = 25 * 31.
Если d|m и d|n, то d является общим делителем чисел m и n. Среди всех общих делителей
можно выделить наибольший общий делитель, обозначаемый как НОД(m,n). Если НОД(m,n) = 1, то
числа m и n называются взаимно простыми. Простые числа взаимно просты, так что НОД(q,p) =1,
если q и p – простые числа.
Если m|A и n|A, то A является общим кратным чисел m и n. Среди всех общих кратных можно
выделить наименьшее общее кратное, обозначаемое как НОК(m,n). Если НОК(m,n) = m*n, то числа
m и n являются взаимно простыми. НОК(q, p) =q*p, если q и p – простые числа.
Если через Pm и Qn обозначить множества всех простых множителей чисел m и n, то
НОД (m, n )   d , где d  Pm
НОК (m, n )   d , где d  Pm
Qn
Qn
Если получено разложение чисел m и n на простые множители, то, используя приведенные
соотношения, нетрудно вычислить НОД(m,n) и НОК(m,n). Существуют и более эффективные
алгоритмы, не требующие разложения числа на множители.
Алгоритм Эвклида
Эффективный алгоритм вычисления НОД(m,n) предложен еще Эвклидом. Он основывается на
следующих свойствах НОД(m,n), доказательство которых предоставляется читателю:
НОД ( m, m )  m;
НОД ( m, n )  НОД (n, m );
НОД ( m, n )  НОД (m  n, n );
Если m > n, то по третьему свойству его можно уменьшить на величину n. Если же m < n, то
по второму свойству аргументы можно поменять местами и вновь придти к ранее рассмотренному
случаю. Когда же в результате этих преобразований значения аргументов сравняются, то решение
будет найдено. Поэтому можно предложить следующую схему:
while(m != n)
{
if(m < n) swap(m,n);
m = m - n;
}
return(m);
Здесь процедура swap выполняет обмен значениями аргументов.
Если немного подумать, то становится ясно, что вовсе не обязательно обмениваться
значениями, – достаточно на каждом шаге цикла изменять аргумент с максимальным значением. В
результате приходим к схеме:
while(m != n)
{
if(m > n) m = m - n;
else n = n – m;
}
return(m);
Если еще немного подумать, то можно улучшить и эту схему, перейдя к циклу с тождественно
истинным условием:
while(true)
{
if(m > n) m = m - n;
else if (n > m) n = n – m;
else return(m);
}
Последняя схема хороша тем, что в ней отчетливо видна необходимость доказательства
завершаемости этого цикла. Доказать завершаемость цикла нетрудно, используя понятие варианта
цикла. Для данного цикла вариантом может служить целочисленная функция – max(m,n), которая
уменьшается на каждом шаге, оставаясь всегда положительной.
Достоинством данной версии алгоритма Эвклида является и то, что на каждом шаге
используется элементарная и быстрая операция над целыми числами – вычитание. Если допустить
операцию вычисления остатка при делении нацело, то число шагов цикла можно существенно
уменьшить. Справедливо следующее свойство:
НОД (m, n )  НОД (n, m%n ); если m  n
Это приводит к следующей схеме:
int temp;
if(n>m) temp = m; m = n; n = temp; //swap(m,n)
while(m != n)
{
temp = m; m = n; n = temp%n;
}
Для вычисления НОК(m, n) можно воспользоваться следующим соотношением:
НОК (m, n)  m * n / НОД ( m, n)
А можно ли вычислить НОК(m, n), не используя операций умножения и деления?
Оказывается можно одновременно с вычислением НОД(m,n) вычислять и НОК(m,n). Вот
соответствующая схема:
int x = v = m, y = u = n,;
while(x != y)
{
if(x > y){
x = x - y; v = v + u;}
else {y = y – x; u = u + v;}
}
НОД = (x + y)/2; НОК = (u+v)/2;
Доказательство того, что эта схема корректно вычисляет НОД, следует из ранее приведенных
свойств НОД. Менее очевидна корректность вычисления НОК. Для доказательства заметьте, что
инвариантом цикла является следующее выражение:
v * y / НОД  u * x / НОД  2 * НОК
Это соотношение выполняется после инициализации переменных до начала выполнения
цикла. По завершении цикла, когда x и y становятся равными НОД, из истинности инварианта
следует корректность схемы. Нетрудно проверить, что операторы тела цикла оставляют
утверждение истинным. Детали доказательства оставляются читателям.
Понятие НОД и НОК можно расширить, определив их для всех целых чисел. Справедливы
следующие соотношения:
НОД ( m,0)  m; НОК ( m,0)  0;
НОД ( m, n )  НОД ( abs( m), abs( n ))
НОД (0,0)  не определено
Расширенный алгоритм Эвклида
Иногда полезно представлять НОД(m,n) в виде линейной комбинации m и n:
НОД (m, n )  a * m  b * n;
В частности вычисление коэффициентов a и b понадобится позже при рассмотрении
алгоритма RSA – шифрования с открытым ключом. Приведу схему алгоритма, позволяющую
вычислить тройку – d, a, b – наибольший общий делитель и коэффициенты разложения. Алгоритм
удобно реализовать в виде рекурсивной процедуры ExtendedEuclid(m,n,d,a,b), которая по заданным
входным аргументам m и n вычисляет значения аргументов d, a, b. Нерекурсивная ветвь этой
процедуры соответствует случаю n=0, возвращая в качестве результата значения: d=m, a=1, b=0.
Рекурсивная ветвь, построенная в предположении, что m>n, рекурсивно вызывает ExtendedEuclid(n,
m % n, d, a, b) и затем изменяет полученные в результате вызова значения a и b следующим
образом:
int t=b;
b= a-b*(m/n); a=t;
Доказательство корректности этого алгоритма построить нетрудно. Для нерекурсивной ветви
корректность очевидна, а для рекурсивной ветви нетрудно показать, что из истинности результата,
возвращаемого при рекурсивном вызове, следует его истинность для входных аргументов после
пересчета значений a и b.
Задачи
1.171 Даны m и n – натуральные числа. Вычислите НОД(m, n). При вычислениях не
используйте операций умножения и деления.
1.172 Даны m и n – натуральные числа. Вычислите НОК(m, n).
1.173 Даны m и n – натуральные числа. Вычислите НОК(m, n). При вычислениях не
используйте операций умножения и деления.
1.174 Даны m и n – целые числа. Вычислите НОД(m, n). При вычислениях не используйте
операций умножения и деления.
1.175 Даны m и n – целые числа. Вычислите НОК(m, n). При вычислениях не используйте
операций умножения и деления.
1.176 Даны m и n – целые числа. Вычислите НОД(m, n). При вычислениях используйте
операцию взятия остатка от деления нацело.
1.177 Даны m и n – целые числа. Вычислите НОК(m, n). При вычислениях используйте
операцию взятия остатка от деления нацело.
1.178 Даны m и n – целые числа. Вычислите тройку чисел – (d, a, b), используя расширенный
алгоритм Эвклида.
1.179 Даны m и n – натуральные числа. Представьте НОД(m, n) в виде линейной комбинации
m и n.
1.180 Даны m и n – целые числа. Представьте НОД(m, n) в виде линейной комбинации m и n.
1.181 Даны m и n – целые числа. Проверьте, являются ли числа m и n взаимно простыми.
Простые числа
Среди четных чисел есть только одно простое число – это 2. Простых нечетных чисел сколь
угодно много. Нетрудно доказать, что число N = (p1 * p2 * … * pk) +1, где pi – подряд идущие
простые числа, является простым. Так что, если построено k простых чисел, то можно построить
еще одно простое число p, большее pk. Отсюда следует, что множество простых чисел
неограниченно. Пример: число N = 2*3*5*7 + 1 = 211 является простым числом.
Решето Эратосфена
Как определить, что число N является простым? Если допустима операция N % m, дающая
остаток от деления числа N на число m, то простейший алгоритм состоит в проверке того, что
остаток не равен нулю при делении числа N на все числа m, меньшие N. Очевидным улучшением
этого алгоритма является сокращение диапазона проверки – достаточно рассматривать числа m в
диапазоне [2, √N].
Еще в 3-м веке до н.э. древнегреческий математик Эратосфен предложил алгоритм
нахождения простых чисел в диапазоне [3, N], не требующий операций деления. Этот алгоритм
получил название «Решето Эратосфена». В компьютерном варианте идею этого алгоритма можно
описать следующим образом. Построим массив Numbers, элементы которого содержат подряд
идущие нечетные числа, начиная с 3. Вначале все числа этого массива считаются невычеркнутыми.
Занесем первое невычеркнутое число из этого массива в массив SimpleNumbers – и это будет первое
нечетное простое число (3). Затем выполним просеивание, проходя по массиву Numbers с шагом,
равным найденному простому числу, вычеркивая все попадающиеся при этом проходе числа. При
первом проходе будет вычеркнуто число 3 и все числа, кратные 3. На следующем проходе в
таблицу простых чисел будет занесено следующее простое число 5, а из массива Numbers будут
вычеркнуты числа, кратные 5. Процесс повторяется, пока не будут вычеркнуты все числа в массиве
Numbers. В результате массив SimpleNumbers будет содержать таблицу первых простых чисел,
меньших N.
Этот алгоритм хорош для нахождения сравнительно небольших простых чисел. Но если
потребуется найти простое число с двадцатью значащими цифрами, то памяти компьютера уже не
хватит для хранения соответствующих массивов. Замечу, что в современных алгоритмах
шифрования используются простые числа, содержащие несколько сотен цифр.
Плотность простых чисел
Мы показали, что число простых чисел неограниченно. Понятно, что их меньше, чем
нечетных чисел, но насколько меньше? Какова плотность простых чисел? Пусть π(n) – это функция,
возвращающая число простых чисел, меньших n. Точно задать эту функцию не удается, но для нее
есть хорошая оценка. Справедлива следующая теорема:
 (n)  n / ln(n)
n 
Функция π(n) асимптотически сверху приближается к своему пределу, так что оценка дает
слегка заниженные значения. Эту оценку можно использовать в алгоритме решета Эратосфена для
выбора размерности массива SimpleNumbers, когда задана размерность массива Numbers, и,
наоборот, при заданной размерности таблицы простых чисел можно выбрать подходящую
размерность для массива Numbers.
Табличный алгоритм определения простоты чисел
Если хранить таблицу простых чисел SimpleNumbers, в которой наибольшее простое число
равно M, то достаточно просто определить является ли число N, меньшее M2, простым. Если N
меньше M, то достаточно проверить, находится ли число N в таблице SimpleNumbers. Если N
больше M, то достаточно проверить, делится ли число N на числа из таблицы SimpleNumbers, не
превосходящие значения √N. Понятно, что если у числа N нет простых множителей, меньших √N,
то число N является простым.
Использование таблицы простых чисел требует соответствующей памяти компьютера, а
следовательно ограничивает возможности этого алгоритма, не позволяя использовать его для
нахождения больших простых чисел.
Тривиальный алгоритм
Если N – нечетное число, то проверить, что оно является простым, можно на основе
определения простоты числа. При этом не требуется никакой памяти для хранения таблиц чисел,
но, как всегда, выигрывая в памяти, проигрываем во времени. Действительно, достаточно
проверить, делится ли нацело число N на подряд идущие нечетные числа в диапазоне [3, √N]. Если
у числа N есть хоть один множитель, то оно составное, иначе – простое.
Все рассмотренные алгоритмы перестают эффективно работать, когда числа выходят за
пределы разрядной сетки компьютера, отведенной для представления чисел, так что если возникает
необходимость работы с целыми числами, выходящими за пределы диапазона System.Int64, то
задача определения простоты такого числа становится совсем не простой. Существуют некоторые
рецепты, позволяющие определить, что число является составным. Вспомним хотя бы известные со
школьных времен алгоритмы. Если последняя цифра числа делится на 2, то и число делится на 2.
Если две последние цифры числа делятся на 4, то и число делится на 4. Если сумма цифр делится на
3 (на 9), то и число делится на 3 (на 9). Если последняя цифра рана 0 или 5, то число делится на 5.
Математики затратили много усилий, доказывая, что то или иное число является (или не является)
простым числом. Сейчас есть особые приемы, позволяющие доказать, что числа некоторого вида
являются простыми. Наиболее подходящими кандидатами на простоту являются числа вида 2p -1,
где p – это простое число. Например, доказано, что число 219937 - 1, имеющее более 6000 цифр,
является простым, но нельзя сказать, какие простые числа являются ближайшими соседями этого
числа.
Задачи
1.182 Дано целое N. Используя алгоритм решета Эратосфена, найдите все простые числа,
меньшие N.
1.183 Дано целое N. Используя алгоритм решета Эратосфена, найдите N первых простых
чисел.
1.184 Дана таблица, содержащая N первых простых чисел. По заданному n < N вычислите
разность между функцией π(n) и ее оценкой – n/ln(n).
1.185 Дана таблица, содержащая N первых простых чисел. Используя табличный алгоритм,
вычислите все простые числа в диапазоне [M+1, M*M], где M – наибольшее простое
число, хранимое в таблице.
1.186 Дано целое N. Постройте таблицу, содержащую N первых нечетных простых чисел.
Используйте табличный алгоритм с постепенным заполнением таблицы, начиная со
случая, когда в ней хранится только одно простое число 3.
1.187 Дано число N. Определите, является ли оно простым, используя тривиальный
алгоритм.
1.188 Дано число N. Определите его первый простой множитель, если он существует.
Вычисления по модулю или модульная арифметика
В теории чисел и в практических алгоритмах большую роль играют вычисления по модулю.
Начнем рассмотрение с ряда определений. Если остаток от деления нацело a на n совпадает с
остатком от деления нацело b на n, то говорят, что a сравнимо с b по модулю n, и пишут:
a  b (mod n )
Если 0 <= b < n, то b совпадает со своим остатком и является представителем класса, в
который входят все числа вида: k*n + b. Число n, заданное в качестве модуля, разбивает все
множество целых чисел на n классов эквивалентности, где в каждый класс попадают числа,
сравнимые с представителями классов – числами 0, 1, 2, …n -1.
Если для операций деления нацело и получения остатка использовать операции языка C# – / и
%, то имеют место следующие соотношения:
b  a %n; k  a / n; a  k * n  b;
Здесь следует иметь в виду одно обстоятельство. В качестве представителей классов
эквивалентности принято рассматривать неотрицательные числа. Операция получения остатка %
возвращает в качестве результата представителя класса, если a – неотрицательное число. Для
отрицательных чисел эта операция возвращает отрицательный остаток, который легко превратить в
представителя класса, добавив к нему значение модуля. Например, -5%3 = -2. Хотя здесь и не
возвращается представитель класса, но это корректный результат в модульной арифметике,
поскольку -2 = 1 (mod 3) и -5 = 1 (mod 3).
Если мы хотим найти x – представителя класса для отрицательных чисел в сравнении:
-a = x (mod n),
то можно воспользоваться либо соотношением:
x = (a/n +1)*n – a,
либо соотношением:
x = n – (-a % n)
Пусть
a  a ' (mod n); b  b' (mod n);
Здесь a΄ и b΄ – это представители классов. Тогда справедливо:
(a  b)  (a '  b' ) (mod n)
a * b  a ' * b' (mod n )
В модульной арифметике операции сложения и умножения над целыми числами заменяются
соответствующими операциями над представителями чисел, а полученный результат операции
заменяется представителем соответствующего класса. Вот пример:
(17 + 13) (mod 7) = (3 + 6) (mod 7) = 9 (mod 7) = 2;
(17 * 13) (mod 7) = (3 * 6) (mod 7) = 18 (mod 7) = 4;
Определим теперь обратные операции к сложению и умножению – вычитание и деление. С
вычитанием все обстоит просто. Если верно, что (a + b) = c (mod n), то верно и соотношение a = (cb) (mod n).
Если же мы хотим корректно определить деление, согласованное с умножением, то
необходимо наложить на числа, участвующие в умножении, дополнительные требования. Для
согласованного выполнения умножения и деления приходится ограничиваться не всеми
представителями классов, а только теми, что являются взаимно простыми с n. Так что операция
деления
( a / b)  ( a / b) (mod n )
Эта операция определена только для a΄ , b΄, взаимно простых с n. В этом случае сравнение b*x
=a (mod n) имеет решение, и соответствующий элемент x рассматривается как результат операции
деления a/b. Например, уравнение 5*x = 7 (mod 12) имеет решение и x = 7/5 (mod 12) = 11. В то же
время уравнение 3*x = 7 (mod 12) решения не имеет.
Сама по себе операция умножения определена на всех числах, но операция умножения,
согласованная с делением, определена только на некоторых числах.
Таблицы сложения и умножения
В модульной арифметике результаты сложения и вычитания, умножения и деления полностью
описываются таблицами, задающими результаты операций над представителями классов. Для
сложения и вычитания эта таблица имеет размерность n*n, где n – значение модуля. Для умножения
и деления размерность таблицы может быть меньше n. Приведу пример:
Таблица 2.1 Таблица сложения (вычитания) по модулю n=6
аргументы 0 1 2 3 4 5
0
0 1 2 3 4 5
1
1 2 3 4 5 0
2
2 3 4 5 0 1
3
3 4 5 0 1 2
4
4 5 0 1 2 3
5
5 0 1 2 3 4
Таблица 2.2 Таблица умножения (деления) по модулю n =12
аргументы
1
5
7
11
1
1
5
7
11
5
5
1
11 7
7
7
11 1
11
11 7
5
5
1
Эффективный алгоритм возведения в степень
Поскольку в модульной арифметике определена операция умножения, то естественным
образом определяется операция возведения в целую степень b = am (mod n) = a * a * {k раз} * a (mod
n). Эффективный алгоритм возведения в степень позволяет вместо n-1 умножений выполнять таких
операций порядка log2n.
Пусть даны a, m, n и нужно вычислить b = am (mod n). Приведу схему алгоритма:
int x = a, y = m, z = 1;
while(y != 0)
{
while(y%2 ==0)
{
x = x * x; y = y/2;
}
// y – нечетное
z = z*x; y = y – 1;
} b = z;
Для доказательства корректности этой схемы введем в качестве инварианта цикла следующее
утверждение:
am = z * xy;
Очевидно, что в результате инициализации утверждение становится истинным. По
завершении цикла, когда y становится равным 0, инвариант обеспечивает требуемый результат.
Нетрудно проверить, что операторы тела внешнего цикла сохраняют истинность инварианта.
Детали доказательства остаются за читателем.
Модифицированный алгоритм возведения в степень с логарифмической сложностью
Рассмотри еще один алгоритм возведения в степень, имеющий такую же сложность, как и
предыдущий алгоритм. Отличие состоит в другом представлении входных данных. В тех
ситуациях, когда предстоит возведение в степень, задаваемую очень большим числом, то зачастую
его разумно представлять двоичной записью числа – последовательностью битов.
Пусть даны a, m, n и нужно вычислить b = am (mod n). И пусть m представлено двоичной
записью: m = mk mk-1… m0. Приведу схему модифицированного алгоритма:
int y = 0, z = 1, i = k+1;
while(i != 0)
{
i = i – 1;
y = 2*y + mi;
z = z * z;
if(mi == 1) z = z*a;
} b = z;
Для доказательства корректности этой схемы зададим следующее утверждение в качестве
инварианта цикла:
(z = ay) & (y – префикс числа m)
В результате инициализации первый член конъюнкции становится истинным по
определению. Второй член конъюнкции также становится истинным, поскольку под префиксом m
будем понимать число, представленное записью: mk mk-1… mi. При инициализации конечный
индекс i больше начального индекса k, так что последовательность пуста и префикс равен 0, что
согласуется с результатами инициализации. На каждом шаге цикла префикс расширяется на
очередную двоичную цифру. В полном соответствии с этим изменяется и значение z, так что оба
члена конъюнкции, а, следовательно, и инвариант цикла остается истинным. По завершении цикла
y совпадает с числом m, и из истинности первого члена конъюнкции следует корректность
вычислений.
Заметьте, переменная y не нужна для вычислений значения z, – она введена в интересах
доказательства корректности схемы. В реальном алгоритме ее можно использовать на этапе
отладки и отключить в работающем алгоритме.
Обе схемы применимы для возведения в степень, как в обычной арифметике, так и в
модульной, в которой естественно все операции выполняются по модулю n.
Решение линейных модульных уравнений
Пусть a и n – произвольные положительные целые и b – произвольное целое. Рассмотрим
решение уравнения:
a*x = b (mod n)
Это уравнение может иметь один или несколько корней, а может и не иметь ни одного корня.
Имеют место следующие утверждения, которые приводятся без доказательства:

Уравнение разрешимо, если и только если НОД(a, n)| b.


Если уравнение разрешимо, то оно имеет ровно d корней, где d = НОД(a, n).
Корни уравнения задаются соотношением: x0 = c1(b/d) (mod n); xk = (x0 + k (n/d))
(mod n) для всех k = 1, 2, …d-1. Коэффициент с1 в этих формулах – это первый
коэффициент в линейном разложении d = c1a + c2n. Его можно определить,
используя расширенный алгоритм Эвклида для вычисления d, c1 и c2.
На основе этих утверждений легко строится алгоритм решения уравнения. Полагаю, что
схему алгоритма можно не приводить. Ограничусь двумя примерами. Рассмотрим уравнение: 72x =
12 (mod 42). Расширенный алгоритм Эвклида, вычисляя НОД(72, 42) и коэффициенты разложения,
вернет в качестве результату тройку чисел: 6, 3, -5. Поскольку 6|12, то у нашего уравнения 6 корней
и их нетрудно вычислить: 6, 13, 20, 27, 34, 41. В то же время уравнение 72x = 15 (mod 42) не имеет
ни одного решения, поскольку 15 не делится нацело на 6.
Задачи
1.189 Проверьте, какие из сравнений имеют место:
126 = 8 (mod 13);
152 = 9 (mod 13);
152 = 9 (mod 26);
n-1 = -1 (mod n)
1.190 Докажите и проверьте, что данные сравнения имеют место:
(a+1)3 = 1 (mod a); (a+1)3 = 2a +1 (mod a); (a-1)3 = -1 (mod a);
(a-1)3 = a-1 (mod a);
1.191 Вычислите x – представителя класса в сравнениях:
175 = x (mod 96);
-175 = x (mod 97);
-1 = x (mod n);
k*n + 5 = x (mod n);
1.192 Даны целые a, b и натуральное число n. Напишите функцию, возвращающую true, если
сравнение a = b (mod n) имеет место.
1.193 Дано целое a и натуральное число n. Напишите функцию, возвращающую значение b,
такую, что 0 <= b < n и сравнение a = b (mod n) имеет место.
1.194 Напишите функцию, вычисляющую сумму целых a и b по модулю n.
1.195 Напишите функцию, вычисляющую разность целых a и b по модулю n.
1.196 Напишите функцию, вычисляющую произведение целых a и b по модулю n.
1.197 Напишите функцию, возвращающую частное целых a и b по модулю n. Если деление
не определено, то функция возвращает значение -1.
1.198 Даны простые числа a и b. Определите наименьшее n, такое, что операция деления a на
b по модулю n определена. Вычислите результат этой операции.
1.199 Для данного натурального n постройте таблицу сложения по модулю n.
1.200 Для данного натурального n постройте таблицу умножения по модулю n.
1.201 Напишите функцию, вычисляющую сумму и разность целых a и b по модулю n,
используя построенную таблицу сложения.
1.202 Напишите функцию, вычисляющую произведение и частное целых a и b по модулю n,
используя построенную таблицу умножения.
1.203 (*) Постройте Windows-приложение «Модульный калькулятор», выполняющий над
аргументами четыре арифметических действия по модулю n: сложение вычитание,
умножение, деление.
1.204 Напишите функцию, вычисляющую am, где m – целое, за время, пропорциональное
log2m. Все действия выполняются в обычной арифметике.
1.205 Напишите функцию, вычисляющую am, где m – целое, за время, пропорциональное
log2m. Число m задано в двоичной системе счисления. Все действия выполняются в
обычной арифметике.
1.206 Напишите функцию, вычисляющую am, где m – целое, за время, пропорциональное
log2m. Все действия выполняются в модульной арифметике с заданным значением
модуля n.
1.207 Напишите функцию, вычисляющую am, где m – целое, за время, пропорциональное
log2m. Число m задано в двоичной системе счисления. Все действия выполняются в
модульной арифметике с заданным значением модуля n.
1.208 Напишите процедуру решения линейного модульного уравнения: ax =b (mod n).
1.209 (*) Постройте Windows-приложение «Модульный калькулятор», выполняющий над
аргументами операции модульной арифметики: сложения, вычитания, умножения,
деления, возведения в целую степень и позволяющий находить решение линейного
уравнения.
Проверка простоты чисел
Современные методы шифрования построены на использовании больших простых чисел,
состоящих из нескольких сотен цифр. Обычно, двоичная запись таких чисел содержит 512 или 1024
бита. Как определить, что случайно построенное число с таким числом цифр является простым?
Обычные алгоритмы, рассмотренные ранее, в этом случае работать не будут. Оказывается, что
задача проверки простоты числа существенно проще, чем задача разложения составного числа на
множители.
Рассмотрим несколько тестов, которые, хотя и не дают безошибочного ответа, но определяют
простоту числа с высокой вероятностью. Заметьте, когда некоторый тест не дает точного ответа, то
возможны ошибки первого и второго рода. Ошибка первого рода возникает в ситуации, когда число
является простым, а тест называет его составным. Ошибка второго рода возникает в ситуации,
когда число является составным, а тест называет его простым. Рассматриваемые в этом разделе
тесты никогда не совершают ошибок первого рода, а вероятность ошибок второго рода крайне мала
для больших чисел.
Тест на основе теоремы Ферма
Теорема Ферма формулируется следующим образом:
Для любого a, такого что 2 <= a < n, справедливо: если n – простое число, то an-1 = 1 (mod n).
Следствие: если an-1 ≠ 1 (mod n), то n – составное число.
Определение: Число n называется псевдопростым с основанием a, если n является составным,
но для него выполняется условие an-1 = 1 (mod n).
Тест 1
Возьмем в качестве базы число 2. Для заданного n проверим выполнение условия 2n-1 = 1 (mod
n). Если это условие не выполняется, то тест сообщает, что число n – составное, в противном случае
тест говорит, что число простое (возможно допуская при этом ошибку).
В соответствии со следствием к теореме Ферма тест не делает ошибок, принимая решение о
том, что число составное. Он может ошибиться, назвав простым псевдопростое число с основанием
2. Много ли таких чисел? Первым таким числом является число 341. Среди чисел, меньших 10000,
таких чисел 22. С ростом n вероятность их появления уменьшается.
Тест 2
Будем выбирать в качестве базы последовательность случайно сгенерированных чисел a1, a2,
… am в диапазоне [2, n-1]. Для заданного n и каждого ai проверим выполнение условия ai n-1 = 1 (mod
n). Если это условие не выполняется хотя бы для одного ai, то тест сообщает, что число n –
составное, в противном случае тест говорит, что число простое (возможно допуская при этом
ошибку).
Понятно, что и этот тест никогда не совершает ошибки первого рода, а вероятность ошибки
второго рода у него меньше, чем у теста 1, поскольку составное число n может быть
псевдослучайным числом с основанием a и не быть таковым для основания b. Так число 341
является псевдослучайным с основанием 2 и не является таковым для основания 3.
Тест Миллера-Рабина
Этот тест является модифицированным вариантом теста 2. Он использует помимо теоремы
Ферма еще одно важное утверждение, связанное с простыми числами. Имеет место следующая
теорема:
Если n – простое число, то уравнение x2 = 1 (mod n) имеет ровно 2 тривиальных корня: +1 и -1,
или, что то же, 1 и (n-1).
Следствие из теоремы: Если уравнение x2 = 1 (mod n) имеет нетривиальный корень, то n –
составное число.
Это следствие и следствие теоремы Ферма объединяются в тесте Миллера-Рабина следующим
образом. Число n-1, представляющее степень, в которую возводится основание a в теореме Ферма,
является четным числом. Поэтому двоичная запись этого числа заканчивается k нулями (k >= 1).
Представим число n-1 в виде u*2k, где u – это нечетный префикс числа, заканчивающийся 1. После
этого префикса в записи числа n-1 идут одни нули. При возведении основания a в степень n-1, как
это требуется в тесте Ферма, можно вначале вычислить au, а затем k раз выполнять возведение в
квадрат. В последнем цикле можно использовать следствие теоремы о корнях квадратного
уравнения. Если ap ≠ (1 или n-1) (mod n), но на следующем шаге при возведении в квадрат
получится значение равное 1, то уравнение x2 = 1 (mod n) имеет нетривиальный корень и согласно
следствию число n является составным.
Таким образом тест Миллера-Рабина позволяет с одной стороны ускорить вычисления,
поскольку не всегда приходится полностью вычислять степень основания, с другой стороны
используются дополнительные факты, позволяющие отсечь псевдослучайные числа. Однако и в
этом случае все еще остается некоторая минимальная вероятность ошибки второго рода.
Задачи
1.210 Построить процедуру определения простоты числа на основе теста Ферма (теста 1).
1.211 Построить процедуру определения простоты числа на основе модифицированного
теста Ферма (теста 2).
1.212 Построить процедуру определения простоты числа на основе теста Миллера-Рабина.
Разложение числа на множители
Рассмотренные выше тесты проверки числа на простоту позволяют точно установить, что
число является составным. Однако они не позволяют определить ни один из множителей этого
числа. Задача нахождения множителей составного числа является вычислительно более сложной
задачей, и современные компьютеры не могут в приемлемое время справиться с ее решением для
1024-битных чисел.
Для малых чисел существуют простые алгоритмы.
Метод «пробных делителей»
Этот наиболее очевидный алгоритм находит простые множители числа N, используя
заданную последовательность делителей: d0=2, d1=3, d2 =5, … dk. Возрастающая
последовательность dj заканчивается значением, не меньшим [√N], и должна содержать все простые
числа в этом интервале, но может быть не только их. Можно использовать последовательность
нечетных чисел в качестве значений dj для j >0, что позволяет не хранить элементы этой
последовательности. Идея алгоритма проста, – последовательно делим число N на dj. Если N
нацело делится на dj, то dj является множителем числа N, и оно заносится в массив множителей
Factors. Затем процесс повторяется с новым значением N, равным N/dj, продолжая испытывать
делители, начиная со значения dj. Процесс заканчивается, когда dj достигает значения [√N]. Массив
Factors содержит все множители числа N.
Метод «простых делителей»
Это вариация предыдущего метода, когда в качестве делителей используется
последовательность простых чисел. Чтобы применять этот алгоритм, необходимо предварительно
получить таблицу простых чисел.
Метод Ферма
Этот алгоритм позволяет определить два множителя числа N. Его достоинство в том, что он
не использует в основном цикле операции умножения и деления, ограничиваясь сложением и
вычитанием. Естественной платой служит увеличение числа выполняемых операций. Идея
алгоритма Ферма в следующем. Пусть N – неотрицательное число, а U и V – его множители, так
что N=U*V, где U > = V, откуда автоматически следует, что U >= √N, V <= √N. Если N – простое
число, то будем полагать, что U =N, а V =1. Рассмотрим два целых числа X и Y таких, что X >= Y.
Подберем значения X и Y так, чтобы выполнялось соотношение X2 –Y2 =N. Заметьте, это всегда
можно сделать. Тогда множителями N являются числа X+Y и X-Y. Они и будут выбраны в качестве
множителей U и V. Если N – простое число, то X+Y будет равно N, а X-Y равно 1.
Пусть для X и Y заданы начальные значения: X=√N и Y=0, что эквивалентно предположению,
что U= V=√N. Если окажется, что X2 –Y2 =N, то задача решена, если же X2 –Y2 >N, то увеличим Y
на 1, в противном случае увеличим X на 1 и продолжим процесс. Процесс обязательно завершится,
в худшем случае, когда X+Y станет равным значению N. Поскольку на каждом шаге значение X и
Y изменяется на 1, то для ускорения вычислений можно избежать вычисления квадратов, используя
известное рекуррентное соотношение: (n+1)2 = n2 +2*n +1. Алгоритм Ферма можно использовать
для проверки того, что N – простое число. В этом случае число операций будет пропорционально
(N-√N).
Приведу более подробную схему этого алгоритма:
int X, Y, x, y, r;
INIT: X = √N; Y = 0; x = 2*X+1; y =2*Y +1; r = X2 – Y2 – N;
while(r != 0)
{
if(r < 0){r = r+x; x = x+2; X = X+1;}
else {r = r-y; y= y+2; Y = Y+1;}
}
U = (x+y-2)/2; V = (x-y)/2;
Переменные X и Y в этой схеме введены лишь для облегчения доказательства ее
корректности. В реальном алгоритме без них можно обойтись. Для доказательства корректности
схемы заметим, что инвариантами цикла являются следующие три утверждения:
Inv1: r = X2 – Y2 – N; Inv2: X+Y <= U <= N;
Inv3: X-Y >= V >= 1;
В результате инициализации все три утверждения становятся истинными. Нетрудно показать,
что они остаются истинными на каждом шаге выполнения цикла.
Действительно, если выполняется условие (r < 0), то X увеличивается на 1, а r на величину x =
2*X +1, в результате чего r = X2 –Y2 –N + 2*X +1 = (X+1)2 –Y2 –N и инвариант Inv1 выполняется с
новым значением X.
Для доказательства истинности инварианта Inv2 заметим, что из условия (r < 0) следует:
(X+Y)(X-Y) < N = U*V
С учетом истинности инварианта Inv3 отсюда следует, что (X+Y) < N. Поэтому при
увеличении X на 1 сохраняется истинность инварианта Inv2. Аналогично доказывается истинность
инварианта Inv3. Аналогично проводится разбор и для второй ветви оператора if.
Когда же цикл завершается при r = 0, то по определению X+Y и X-Y являются делителями
числа N, так что алгоритм корректно решает задачу.
Для доказательства завершаемости цикла в качестве варианта цикла можно выбрать
выражение N – (X+Y), которое остается неотрицательным в силу истинности инварианта Inv2 и
уменьшается на 1 на каждом шаге цикла.
Эвристический алгоритм Полларда
Этот алгоритм с успехом применяется на практике, в том числе и для больших чисел. Он
основан на эвристиках, которые хорошо работают в большинстве случаев, но не гарантируют ни
успеха, ни малого времени работы. Приведу схему алгоритма Полларда:
int k,x,y,i,d, limit;
i=1; x=rnd.Next(0, n-1); y=x; k=2; limit = n -(int)
Math.Sqrt(n);
while(true)
{
i++;
x= x*x -1; x= x%n;
d= NOD(n, Math.Abs(y-x));
if((d!=1) &&(d!=n))break;
if(i==k)
{
y=x; k*=2;
}
if(i > limit) break;
} U = d; V = n/d;
В классическом варианте алгоритма формируется бесконечная последовательность значений
xk и yk, начиная с некоторого случайного значения. Предложенная Поллардом стратегия
рекуррентного вычисления значений x и y приводит к тому, что вероятность того, что разность x-y
на некотором шаге станет множителем n, является весьма высокой. Я не буду приводить доводов в
обоснование этого факта. Замечу лишь, что в предлагаемом варианте этого алгоритма строится
конечная последовательность пробных значений, поскольку на практике крайне нежелательно
использовать алгоритмы, допускающие зацикливание. Завершаемость алгоритма крайне важное
свойство, которое следует гарантировать. По этим причинам в алгоритм добавлен барьер,
ограничивающий цикл максимальным числом итераций, возможным в алгоритме Ферма. При
желании величину барьера можно изменить.
Алгоритм Полларда на простых числах будет возвращать в качестве множителей значения N
и 1, но в отличие от алгоритма Ферма он может, хотя и крайне редко, возвращать такие значения и
для составного числа N.
Задачи
1.213 Напишите процедуру нахождения множителей числа, используя алгоритм простых
делителей, хранящий таблицу простых чисел.
1.214 Напишите процедуру нахождения множителей числа, используя алгоритм пробных
делителей, не требующий хранения таблицы.
1.215 Напишите процедуру нахождения минимального и максимального множителей числа,
используя алгоритм Ферма.
1.216 Напишите процедуру, нахождения всех множителей числа N, используя алгоритм
Ферма.
1.217 Напишите процедуру нахождения множителя числа, реализующую алгоритм
Полларда.
1.218 Напишите процедуру, нахождения всех множителей числа N, используя алгоритм
Полларда.
1.219 Постройте Windows-приложение, в котором оценивается эффективность метода
Полларда. Оценку получите следующим образом. Генерируйте M раз случайное число N.
Для каждой реализации вычислите по методу Полларда множитель d числа N и k – число
шагов цикла, потребовавшихся для нахождения d. Вычислите значение p =k/N для
каждой реализации и найдите среднее значение p по всем M реализациям.
1.220 Постройте Windows-приложение, в котором множители числа вычисляются по методу
Ферма и по методу Полларда. Сравните эти два метода по числу требуемых шагов, по
времени нахождения множителей.
Шифрование
С давних пор и до сегодняшних дней актуальной остается защита информации.
Компьютерные технологии дали человечеству уникальные возможности по хранению информации
и передачи ее из одной точки пространства в другую. Обратная сторона медали состоит в том, что
трудно обеспечить секретность хранимых и передаваемых данных. Шифрование является одним из
способов защиты данных и предназначено для решения трех основных задач:

Конфиденциальность: защита данных пользователя или его идентификации от
несанкционированного чтения;
 Целостность: защита данных от изменений;
 Аутентификация: гарантия того, что данные поступили от указанного в сообщении
отправителя.
Применяемые схемы шифрования принято классифицировать следующим образом:




Симметричное шифрование с закрытым ключом: применяется для обеспечения
конфиденциальности данных. Для шифрования и дешифрования применяется
один и тот же закрытый (секретный) ключ, который известен только получателю и
отправителю сообщения.
Ассиметричное шифрование с открытым ключом: применяется для обеспечения
конфиденциальности данных. Для шифрования и дешифрования применяются
разные ключи – открытый и закрытый, связанные определенным соотношением.
Открытый ключ получателя, применяемый для шифрования, может быть известен
не только получателю и отправителю сообщения. Для дешифрования применяется
закрытый ключ получателя, известный только ему.
Цифровая подпись: применяется в интересах аутентификации отправителя. В этом
процессе используются и хэш-функции.
Хеширование. Сжимает исходное сообщение, получая в результате статистически
уникальный краткий образ сообщения, подобный отпечаткам пальцев.
Дадим теперь более точные определения основных понятий, используемых в криптографии.
Пусть X и Y – это два множества, элементы которых будем называть данными. Под шифром будем
понимать алгоритм или отображение:
y  F (k , x );
y Y ; x  X ; k  K ;
Процесс получения элемента y по заданному элементу x называют шифрованием. Элементы x
– это исходные данные, y – зашифрованные данные. При шифровании используется ключ k –
элемент некоторого множества K, называемого множеством ключей. Всегда подразумевается
возможность дешифрования – существование обратного отображения, позволяющего восстановить
исходный элемент:
x  G (k , y )  G (k , F (k , x ));
y Y ; x  X ; k  K ;
Когда говорят о шифровании, обычно рассматривают три стороны: отправитель сообщения,
его получатель и противник. Отправитель шифрует исходные данные и посылает их получателю,
которому это сообщение адресовано. Получатель дешифрует сообщение, имея ключ и применяя
известное ему отображение G(k,y). Противник, заинтересованный в расшифровке сообщения,
может перехватить сообщение y. Его цель – расшифровать сообщение, не имея ключа и не зная
алгоритма шифрования. Сложность шифра определяется тем, какие усилия могут понадобиться
противнику для достижения его цели. В частном случае противнику может быть известен алгоритм
дешифрования, но не известен ключ. В другом частном случае может быть известен ключ, но не
известен алгоритм. Вспомните сказку о золотом ключике, когда мало было обладать ключом, но
нужно было также знать, где находится заветная дверь, открываемая этим ключом.
Рассмотрим несколько примеров простых шифров.
Пример 1 (алгоритм сложения). Пусть X, Y, K – множества целых чисел, а алгоритм
шифрования задается операцией сложения: y = x+k. Понятно, что существует обратное
отображение: x = y – k.
Пример 2 (алгоритм сложения по модулю). Пусть X, Y, K – множества целых чисел в
диапазоне [0, p-1], а алгоритм шифрования задается операцией сложения по модулю p: y = (x+k)
(mod p). Понятно, что существует обратное отображение: x =(y – k)(mod p).
Шифрование применяется прежде всего к текстовым данным. В памяти компьютера тексты,
как и любая другая информация, хранится в виде последовательности битов. При шифровании эта
последовательность битов нарезается на блоки, как правило фиксированной длины, и каждый блок
шифруется. Шифрование блока данных может проводиться независимо от остальных блоков, так
что одинаковые блоки имеют одинаковые значения в зашифрованном сообщении. Чаще всего, при
шифровании учитывается контекст и шифруется смесь блока с его соседями, например с
предшествующим блоком. В этом случае задача раскрытия шифра значительно усложняется.
Поскольку каждый блок битов можно рассматривать как число, то без потери общности данные
можно рассматривать как числа, не зависимо от их содержательного смысла. В дальнейшем
обсуждении предполагается, что сообщение задано числом.
При симметричном шифровании предполагается, что ключ шифра известен как отправителю
сообщения, так и его получателю. В этом состоит серьезный недостаток подобных шифров,
поскольку у противника появляется возможность набрать большой объем сообщений,
зашифрованных одним ключом, который одновременно является и ключом, используемым при
дешифровании, что облегчает задачу раскрытия шифра. Считается, что ключ шифра нужно менять
как можно чаще, порождая его случайным, не прогнозируемым способом. Но тогда возникает
проблема доставки ключа получателю сообщения. Поэтому интерес представляет асимметричное
шифрование, где рассматриваются шифры, удовлетворяющие следующим дополнительным
условиям:


отправитель и получатель сообщения пользуются разными ключами;
ключи отправителя и получателя связаны некоторым однозначным соотношением;

соотношение, связывающее ключи, таково, что, зная один ключ, крайне сложно
определить другой ключ.
Следствием этих условий является тот странный на первый взгляд факт, что отправитель
сообщения, которое он сам зашифровал, не имеет возможности его расшифровать – применить
алгоритм дешифрования, поскольку не знает ключа получателя сообщения. Посылая сообщение,
отправитель может посылать и ключ, с помощью которого это сообщение было зашифровано, но не
позволяющий расшифровать сообщение. Такие схемы шифрования возможны. Они получили
название шифрования с открытым ключом. Недостатком асимметричного шифрования является его
трудоемкость, требующая больших временных затрат при передаче длинных сообщений. Поэтому
применяется комбинирование двух подходов. Для пересылки закрытого ключа используется
асимметричная схема шифрования с открытым ключом, а для шифрования самого сообщения
применяется симметричная схема с закрытым ключом, посланным получателю сообщения.
Алгоритм Диффи-Хеллмана
Схема шифрования может применяться для выработки общего ключа, когда отправитель и
получатель вначале обмениваются зашифрованными сообщениями с открытым ключом. После
расшифровки сообщений каждый из них получает одно и то же сообщение, представляющее общий
ключ, используемый далее в шифровании и расшифровке.
Пусть p – простое число и b < p. Оба эти числа известны отправителю и получателю
сообщения и только им – это закрытые данные. Отправитель и получатель независимо друг от
друга вырабатывают случайные числа e и d и на их основе создают сообщения:
r = be (mod p);
t = bd (mod p);
Эти сообщения они и посылают друг другу. Получив их, каждый из них способен выработать
общий ключ k следующим образом:
k = te(mod p) = (bd (mod p))e(mod p) = rd(mod p) = (be (mod p))d(mod p) = bed(mod p)
Заметьте, противнику, перехватившему сообщения r и t, получить ключ k крайне трудно,
когда приходится работать с достаточно большими числами, содержащими несколько сотен знаков.
Точно также получателю и отправителю сообщения трудно определить секретную часть ключа
своего партнера – по числу e трудно вычислить число d, а по d трудно вычислить число e.
Число e можно рассматривать как ключ отправителя, известный только ему. Алгоритм
шифрования, применяемый отправителем, задается функцией f(k,x) = xe (mod p). Получатель
пользуется своим ключом d и функцией g(k,y) = yd(mod p). Отправитель и получатель по-разному
шифруют одно и тоже сообщение b, а в результате расшифровки полученных ими сообщений они
получают одно и то же сообщение, содержащее общий ключ.
Алгоритм RSA
Особенность этого алгоритма шифрования с открытым ключом, получившего название по
имени его авторов – Райвеста, Шамира, Адлемена, состоит в том, что сообщение, адресованное
получателю, шифруется не ключом отправителя, а открытым ключом самого получателя, который в
этом случае является, доступным не только отправителю, но возможно и противнику. При широком
использовании этой системы шифрования открытые ключи пользователей могут храниться в
общедоступной библиотеке в Интернете, так что каждый имеет возможность посылать
зашифрованные сообщения всем пользователям, выложившим свои открытые ключи.
Расшифровать сообщение может только получатель, используя для этого закрытую часть
ключа. Так что определение алгоритма – шифрование с открытым ключом – неточно. Фактически
ключ состоит из двух половинок – открытой части ключа и закрытой. Для шифрования
используется открытая часть ключа, для дешифрования – закрытая.
Рассмотрим подробнее основы системы шифрования с открытым ключом. Будем называть
получателя и отправителя сообщения Алисой и Бобом, как это принято при рассмотрении
алгоритмов шифрования. Обозначим открытую и закрытую части ключа Алисы через PA и SA, а
для Боба – PB и SB. Сообщения M, также как и зашифрованные сообщения будем рассматривать как
элементы множества D. Функцию, шифрующую сообщения с открытым ключом Алисы, будем
обозначать как PA (M). Обратную функцию дешифрования с закрытым ключом будем обозначать
как SA (M). К шифрованию предъявляются следующие требования:
M = SA (PA (M)) = PA (SA (M)) для любого M из D.
Чтобы применить для шифрования RSA алгоритм, Алиса и Боб выполняют следующие
действия:

Выбирают случайным образом два простых числа p и q, каждое из которых может
иметь длину в 512 битов или более.
 Вычисляют n = pq и k =(p-1)*(q-1).
 Выбирают нечетное число e, меньшее k и взаимно простое с k.
 Вычисляют d из уравнения ed = 1 (mod k). Поскольку e взаимно просто с k, то
решение этого уравнения существует. Алгоритм решения такого уравнения был
рассмотрен.
 Публикуют пару P = (e,n) как свой открытый ключ.
 Сохраняют в глубокой тайне свой закрытый ключ S – пару (d, n).
Когда Боб хочет послать Алисе сообщение M, то он применяет ее открытый ключ и шифрует
сообщение следующим образом:
C = PA(M) = Me (mod n)
Алиса, получив зашифрованное сообщение C, дешифрует его, используя известный только ей
закрытый ключ:
SA(C) = Cd (mod n)
В результате дешифрования Алиса получает исходное сообщение M = SA(C).
Докажем корректность алгоритма RSA, то есть докажем, что действительно так построенные
функции шифрования и дешифрования являются взаимно обратными и обеспечивают
восстановление исходного сообщения.
Прежде всего, заметим, что по свойствам модульной арифметики:
S(P(M)) = P(S(M)) = Med (mod n)
Так как e и d связаны соотношением: e*d = 1 (mod k) = 1 (mod (p-1)*(q-1)), то справедливо
равенство: e*d = 1 + c*(p-1)*(q-1). Тогда:
Med = M*(Mp-1)c(q-1)) (mod p)
По теореме Ферма отсюда следует:
Med = M*(Mp-1)c(q-1)) (mod p) = M*(1)c(q-1)) (mod p) = M (mod p)
Аналогично можно показать, что имеет место соотношение: Med = M (mod q). А тогда,
согласно еще одной теореме Med = M (mod p*q) = M (mod n). Что и доказывает корректность
алгоритма RSA.
На чем основывается безопасность шифрования алгоритмом RSA? Ведь противник знает
открытый ключ (n, e), поэтому он может разложить n на множители, определить p и q, а затем
определить закрытый ключ d, также как это делает автор ключа. Все зиждется на сложности
разложения больших чисел на простые множители. Если эту задачу удается решить, то
расшифровать сообщение не представляет труда. Но до сих пор задача разложения числа на
множители при длине числа в 1024 бита и более не под силу современным компьютерам.
RSA и общий ключ
Шифрование и дешифрование длинных сообщений по алгоритму RSA, где используются
длинные ключи, требует значительного времени. Поэтому иногда применяется гибридная схема
шифрования, где алгоритм RSA используется для выработки общего закрытого ключа. Если Алиса
хочет послать Бобу большое зашифрованное сообщение, то она может поступить следующим
образом. Она выбирает случайным образом ключ k, которым и шифрует свое сообщение M, а затем
открытым ключом Боба шифрует сам ключ k, и посылает Бобу пару (F(k, M), PB(k)). Боб вначале
определяет ключ k = SB(PB(k)), а затем дешифрует и само сообщение M = F-1(k, F(k, M)). В отличие
от алгоритма Диффи-Хеллмана, в этом случае не требуется обмена сообщениями для выработки
общего ключа.
Цифровая подпись и RSA
Алгоритм RSA используется не только для обеспечения секретности посылаемого текста, но и
для решения других задач, связанных с шифрованием. Прежде всего требуется обеспечить
подлинность сообщения, включающую его целостность и аутентичность. Проблема аутентичности
сообщения состоит в следующем. Если Алиса получила сообщение, зашифрованное ее открытым
ключом и подписанное именем Боба, как ей убедиться, что сообщение послано действительно
Бобом, а не противником, которому, также как и Бобу, известен открытый ключ Алисы? Для
решения этой задачи существует цифровая подпись, которая не поддается подделке. Используя
алгоритм RSA, Бобу достаточно зашифровать подпись своим закрытым ключом. Никто другой,
кроме Боба, не может зашифровать его подпись подобным образом, поскольку закрытый ключ
известен только ему одному. Алиса, расшифровав эту подпись открытым ключом Боба, убедится в
том, что письмо действительно подписано Бобом.
Целостность текста и RSA
Проблема целостности, связанная с защищенной передачей секретных данных, состоит в том,
чтобы гарантировать отсутствие изменений в переданном тексте – отсутствие добавлений,
удалений или замен в переданном тексте, случайных или преднамеренных. Как Алисе убедиться в
том, что текст, посланный Бобом, дошел до нее без искажений? Для этого можно использовать так
называемые «отпечатки пальцев» посылаемого сообщения. По сообщению M строится его сжатый
образ H(M) такой, что крайне трудно подобрать другое сообщение M1, чтобы образы сообщений
H(M) и H(M1) совпадали. Функцию H(M) часто называют хэш-функцией.
Чтобы гарантировать целостность сообщения M, Боб посылает Алисе пару (PA(M), SB(H(M)))
– зашифрованное сообщение и отпечаток сообщения, зашифрованный секретным ключом Боба.
Алиса, получив эту пару, дешифрует сообщение M своим закрытым ключом и дешифрует
отпечаток, используя открытый ключ Боба. Затем она строит отпечаток полученного ею сообщения,
и если он совпадает с отпечатком, присланным Бобом, то можно быть уверенным в том, что
полученное сообщение M не подвергалось правке. Отпечаток одновременно может играть роль
цифровой подписи.
Хэш-функции широко применяются в программировании в разных ситуациях. Чаще всего они
применяются, когда необходимо отображать некоторое подмножество множества большой
мощности на множество меньшей мощности. Вот типичный пример. Представьте себе, что нужно
хранить не более N ключей, а ключами могут быть произвольные слова русского языка. Тогда
можно иметь для хранения ключей массив из N элементов и при появлении нового ключа записать
его в элемент массива, индекс которого вычисляется с помощью хэш-функции, отображающей
бесконечное множество слов в конечное множество – диапазон целых чисел [1, N]. При этом может
оказаться, что вычисленный индекс уже использован, тогда в таблице ищется первый свободный
элемент.
Рассмотрим пример некоторой простейшей хэш-функции. Таблица из N чисел разбивается на
участки неравной длины, число участков равно 33 – по числу букв русского алфавита. Длина
участка определяется частотой слов в русском языке, начинающихся на данную букву. Хэш-
функция, анализируя первую букву ключа, возвращает в качестве результата начало
соответствующего участка в таблице ключей.
Если применить эту хэш-функцию для построения отпечатка сообщения, то две таблицы,
построенные для двух разных сообщений, совпадут только тогда, когда сообщения имеют
одинаковое число слов и все соответствующие слова обоих сообщений начинаются с одной и той
же буквы.
Другим примером может служить хэш-функция, которая вычисляет код слова, а затем
возвращает в качестве результата остаток по модулю N.
Хорошо работающие на практике хэш-функции, как правило, являются «ноухау» фирм, их
придумавших и использующих. Алиса и Боб могут применять известную только им хэш-функцию.
Цифровые подписи и сертификаты
Алгоритм RSA может использоваться и при выдаче сертификатов – заверенных цифровых
подписей. Идея сертификатов основывается на возможности создания специальных
сертификационных центров, пользующихся доверием большинства пользователей. Каждый, кто
хочет получить цифровую подпись, обращается в сертификационный центр и получает от него
подпись, зашифрованную закрытым ключом сертификационного центра. Поскольку открытый
ключ центра известен всем пользователям, то Алиса, получив от Боба сообщение с
сертифицированной подписью, может расшифровать подпись и убедиться, что сообщение было
отправлено именно Бобом.
Задачи
1.221 Разработайте Windows-приложение, в котором Боб и Алиса обмениваются
сообщениями, создавая общий ключ по алгоритму Диффи-Хеллмана.
1.222 Разработайте Windows-приложение, в котором Боб и Алиса посылают друг другу
сообщения, используя RSA алгоритм шифрования с открытым ключом.
1.223 Разработайте Windows-приложение, в котором Штирлиц перехватывает и
расшифровывает сообщения Бормана и Мюллера, зашифрованные RSA-алгоритмом.
Штирлицу удалось узнать открытые ключи Бормана и Мюллера, из чего ему стало ясно,
что число n, применяемое при шифровании, не превосходит 109, так что у него появилась
возможность узнать закрытые ключи недальновидных противников.
1.224 Разработайте Windows-приложение, в котором Боб и Алиса посылают друг другу
сообщения с цифровой подписью.
1.225 Разработайте Windows-приложение, в котором предлагаются три различных варианта
построения отпечатка сообщения.
1.226 Разработайте Windows-приложение, в котором Боб и Алиса посылают друг другу
сообщения и их отпечатки, чтобы гарантировать целостность и аутентичность
переданного текста.
1.227 Разработайте Windows-приложение, в котором предлагаются три различные хэшфункции для хранения и поиска конечного множества из N ключей в таблице. Каждый
ключ может быть произвольной строкой текста.
Шифрование в Framework .Net
Для обеспечения безопасности данных пользователя Framework .Net содержит разнообразный
набор средств, гарантирующий конфиденциальность, целостность и аутентичность хранимых и
передаваемых данных. Пространство имен Security, вложенное в пространство System,
структурировано и содержит несколько специальных пространств имен, отвечающих за разные
аспекты безопасности. Упомяну только два пространства – Permissions и Cryptography. Первое из
них содержит классы, ведающие разрешениями на допуск к данным и проверку ролей
пользователей. О классах из пространства Cryptography скажу чуть подробнее. Два абстрактных
класса из этого пространства – SymmetricAlgorithm и ASymmetricAlgorithm являются
родительскими классами, от которых наследуются классы, реализующие симметричные и
ассиметричные алгоритмы шифрования. Если создается собственный класс шифрования, то
разумно сделать его наследником этих абстрактных классов. Еще одним абстрактным классом,
являющимся наследником класса SymmetricAlgorithm, является класс DES (Data Encryption
Standart), потомки которого должны реализовать алгоритмы, удовлетворяющие принятому в США
стандарту шифрования с закрытым ключом. Потомок этого класса – DesCryptoServiceProvider из
библиотеки FCL задает реализацию этого стандарта.
Для асимметричного шифрования с открытым ключом аналогичную роль играют классы RSA
и RSACryptoServiceProvider. Методы Encrypt и Decrypt последнего класса позволяют шифровать и
дешифровать сообщения на основе алгоритма RSA.
Абстрактный класс DSA и его потомки позволяют задавать цифровую подпись. Абстрактный
класс HashAlgorithm и его потомки позволяют строить «отпечатки» сообщений, используя
различные хэш-функции. Важную роль в шифровании сообщений играет класс CryptoStream,
позволяющий представлять сообщение в виде потоков битов и реализовать поблочный способ
шифрования.
Шифрование используется и для внутренних целей Framework .Net, например для
идентификации сборок на основе цифровых подписей.
Задачи
1.228 Напишите Windows-приложение, в котором используется класс
DesCryptoServiceProvider для шифрования и дешифрования файлов.
1.229 Разработайте Windows-приложение, в котором Боб и Алиса посылают друг другу
сообщения, используя RSA алгоритм шифрования из библиотеки FCL.
1.230 Разработайте Windows-приложение, в котором Штирлиц перехватывает и пытается
расшифровать сообщения Бормана и Мюллера, зашифрованные RSA-алгоритмом из
библиотеки FCL.
1.231 Разработайте Windows-приложение, в котором Боб и Алиса посылают друг другу
сообщения с цифровой подписью, используя классы из библиотеки FCl.
1.232 Разработайте Windows-приложение, в котором для построения отпечатка сообщения
используются классы библиотеки FCL.
1.233 Разработайте Windows-приложение, в котором Боб и Алиса используют классы из
библиотеки FCL для обмена сообщениями, гарантирующие целостность и аутентичность
переданного текста.
Числа Фибоначчи
Разговор о числах можно продолжать до бесконечности. Закончим эту главу рассмотрением
чисел, странным образом появляющихся в самых разных областях человеческой жизни.
Числа Фибоначчи задают бесконечную последовательность F0, F1, F2, … Fn, …, которую
можно определить следующими рекуррентными соотношениями:
F0  0; F1  1;
Fk  Fk 1  Fk 2 ; для k  1
(*)
Итальянский математик Фибоначчи, впервые предложивший эту последовательность еще в
1202 году, и в честь которого они впоследствии были названы, привел при их описании задачу «о
кроликах», которую можно рассматривать, как простейшую модель роста популяции. У Кеплера
эти числа появились при изучении распределения положения листьев на стебле. В
программировании эти числа появляются при изучении сложности алгоритма Эвклида, при
рассмотрении алгоритмов сортировки файлов на внешних носителях и во многих других ситуациях.
Приведу некоторые соотношения, характеризующие свойства чисел Фибоначчи, и соотношения,
позволяющие применять различные алгоритмы их вычисления.
Справедливо соотношение:
Fn 1 * Fn 1  Fn 2  ( 1) n
Докажем его по индукции. Нетрудно проверить, что оно выполняется при n=1, 2.
Предположим, что оно выполняется для всех k <=n. Тогда:
Fn2 * Fn  Fn 12  ( Fn 1  Fn ) * Fn  Fn 12 
Fn1 ( Fn  Fn 1 )  Fn 2   Fn 1 * Fn 1  Fn 2  ( 1)n 1
Из этого соотношения в частности следует, что два соседних числа Фибоначчи взаимно
просты.
Также по индукции легко доказывается важное соотношение:
Fnm  Fm * Fn 1  Fm1 * Fn
(**)
Соотношение (**) позволяет задать более эффективные алгоритмы вычисления чисел
Фибоначчи по сравнению с прямым использованием рекуррентных соотношений (*), данных при
их определении. Если уже построена и хранится в памяти таблица первых n чисел Фибоначчи, то,
используя соотношение (**), можно вычислить за константное время (два умножения и одно
сложение) значение любого числа Фибоначчи в интервале от n+1 до 2n.
Если не хранить таблицу, то все равно можно ускорить вычисление числа F2n, ограничившись
вычислением Fn из соотношения (*), а затем применить соотношение (**). Для больших n это
позволит почти вдвое сократить время вычислений.
Удивительным образом числа Фибоначчи появляются и в искусстве, – они тесно связаны с
числом Фидия – Φ, характеризующим так называемое «золотое сечение» или «божественную
пропорцию». Об этой связи много говорится в модном романе Дэна Брауна «Код Да Винчи».
Рассмотрим и мы ее более подробно, поскольку она дает возможность построить еще один
эффективный алгоритм вычисления чисел Фибоначчи. Начнем с определения числа Ф, названного в
честь греческого скульптора, для работ которого характерно применение золотого сечения.
Рассмотрим задачу деления отрезка на две части. Большую часть отрезка обозначим через x,
меньшую – y. Если большая часть так относится к меньшей, как целое к большей части, то говорят,
что имеет место «золотое сечение» отрезка. В этом случае отношение x и y составляет
божественную пропорцию, дающую эстетически идеальное соотношение размеров. Отношение x/y
называют числом Фидия и обозначают буквой Ф. Имеют место следующие соотношения:
x  y  1;

x 1
1  5
 ; x 2  1  x; x 
;
y x
2
x
1 5
;  2  1  ;  
;
y
2
Покажем теперь, как связаны числа Фибоначчи с числом Ф. Докажем, что справедливо
следующее соотношение:
Fn 
 n  (1  )n
5
(***)
Доказательство проведем методом математической индукции. Нетрудно проверить, что
соотношение выполняется для F1 и F2. Предположим теперь, что оно выполняется для всех 1<=k <n.
Покажем его справедливость и для Fn:
 n 1  (1   ) n 1  n 2  (1   )n 2


5
5
 n 2 (1   )  (1   ) n 2 (2   )  n  (1   ) n

5
5
Fn  Fn 1  Fn 2 
Последнее соотношение получено с учетом того, что (1+Ф) = Ф2 и (2-Ф) = (1-Ф)2.
Соотношение (***) можно использовать для вычисления значения числа Фибоначчи за время
T = O(log n), применяя эффективный алгоритм возведения в степень. Учитывая, что (1-Ф) меньше
1, то для больших значений n можно не вычислять (1-Ф)n, поскольку его вклад не будет влиять на
конечное значение, округляемое при переходе от вещественного значения правой части к
целочисленному значению Fn.
Конечно, соотношение (***) будет давать лучшие по времени результаты только для больших
n, поскольку оно требует выполнения более дорогих операций умножения, в то время как
соотношение (*) ограничивается лишь операциями сложения.
Заметьте, с ростом n отношение Fn+1 к Fn стремится к «божественной пропорции» – числу Ф.
Наряду с простыми числами Фибоначчи определяют числа Фибоначчи более высоких
порядков. Так числа Фибоначчи второго порядка (2)Fn определяются следующим образом:
(2)
F0  0;
(2)
Fn2 
(2)
(2)
F1  1;
Fn 1  (2) Fn  Fn ;
(****)
Задачи
1.234 Задача Фибоначчи о кроликах. Подсчитайте число пар кроликов, которое Вы можете
получить за N лет. В начальный период Вам подарена новорожденная пара кроликов.
Каждая пара кроликов начинает давать приплод через месяц после рождения, и каждый
месяц приносит по одной паре кроликов. Кролики бессмертны.
1.235 Напишите алгоритм вычисления чисел Фибоначчи, используя соотношение (*).
Подсчитайте требуемое время T(n) для вычисления Fn, постройте график и оцените
значение константы k в зависимости T(n) = k*n.
1.236 Напишите алгоритм вычисления чисел Фибоначчи в диапазоне [n, 2n], используя
соотношение (**) и построенную таблицу первых n чисел Фибоначчи.
1.237 Напишите алгоритм вычисления чисел Фибоначчи, используя соотношение (**).
Подсчитайте требуемое время T(n) для вычисления Fn, постройте график и оцените
значение константы k в зависимости T(n) = k*n.
1.238 Напишите алгоритм вычисления чисел Фибоначчи, используя соотношение (***).
Подсчитайте требуемое время T(n) для вычисления Fn, постройте график.
1.239 Напишите алгоритм вычисления чисел Фибоначчи, используя соотношение (***), в
котором опущено вычисление (1-Ф)n. Оцените, при каком значении n этот алгоритм
начинает давать правильные результаты.
1.240 Напишите алгоритм вычисления чисел Фибоначчи второго порядка, используя
соотношение (****). Подсчитайте требуемое время T(n) для вычисления (2)Fn, постройте
график и оцените значение константы k в зависимости T(n) = k*n.
Проекты
1.241 Постройте класс Rational для работы с рациональными числами. Определите в этом
классе методы (Plus, Minus, Mult, Div, Pow) и операции (+, -, *, /, ^) над рациональными
числами, задающими сложение, вычитание, умножение, деление и возведение в целую
степень. Для возведения в степень используйте эффективный алгоритм, требующий
логарифмического времени. Определите в этом классе методы (Eq, Neq, Gr, Less) и
операции отношения (==, !=, >, <). Задайте в классе рациональные константы – 0 и 1.
Переопределите в классе метод ToString, задающий строку, представляющую
рациональное число в удобном для восприятия виде.
1.242 Постройте Windows-приложение: Калькулятор для работы с рациональными числами.
1.243 Постройте класс Complex для работы с комплексными числами. Определите в этом
классе методы (Plus, Minus, Mult, Div, Pow) и операции (+, -, *, /, ^) над комплексными
числами, задающими сложение, вычитание, умножение, деление и возведение в целую
степень. Для возведения в степень используйте эффективный алгоритм, требующий
логарифмического времени. Определите в этом классе методы (Eq, Neq, Gr, Less) и
операции отношения (==, !=, >, <).Задайте в классе комплексные константы – 0, 1 и i.
Переопределите в классе метод ToString, задающий строку, представляющую
комплексное число в удобном для восприятия виде.
1.244 Постройте Windows-приложение: Калькулятор для работы с комплексными числами.
1.245 Постройте класс Modular для работы с целыми числами в модульной арифметике.
Определите в этом классе методы (Plus, Minus, Mult, Div, Pow) и операции (=, +, -, *, /, ^),
задающие сложение, вычитание, умножение, деление и возведение в целую степень по
заданному модулю m. Для возведения в степень используйте эффективный алгоритм,
требующий логарифмического времени. Определите в классе метод, дающий решение
сравнения a = x (mod m), где 0 =< x < m. Определите в классе метод, определяющий все
решения уравнения ax = b (mod m), если таковые решения существуют. Переопределите в
классе метод ToString, задающий строку, представляющую число как результат
сравнения a = b (mod m), где 0 =< b < m.
1.246 Постройте Windows-приложение: Калькулятор для работы с модульной арифметикой.
1.247 Постройте класс Number, поле которого содержит целое число a. Методы класса
позволяют определить, является ли число простым, а для составного числа найти
разложение на множители. Методы класса для заданного целого числа b позволяют найти
наибольший общий делитель, наименьшее общее кратное a и b, позволяют определить,
являются ли эти числа взаимно простыми.
1.248 Постройте Windows-приложение для работы с классом Number.
1.249 Постройте класс Crypto, предоставляющий возможности шифрования с открытым и
закрытым ключом, создание цифровых подписей и отпечатков сообщений.
Примеры
Шифрование файлов на основе использования алгоритмов DES библиотеки FCL
Построим Windows-приложение, позволяющее шифровать и дешифровать произвольные
файлы. Для шифрования и дешифрования будем использовать алгоритмы шифрования с закрытым
ключом. Используем для этих целей реализацию алгоритмов, соответствующую стандарту DES,
предлагаемую соответствующими классами библиотеки FCL.
Первым делом я создал новый проект с именем CryptoTest и спроектировал форму,
показанную на рис. 2_1, в которой есть три текстовых окна и три командные кнопки. В текстовых
окнах задаются имя исходного файла, подвергающегося шифрованию, имя файла после его
шифрования и имя файла, полученного при дешифровании. Обработчики событий Click командных
кнопок позволяют создать объект, выполняющий шифрование, и вызвать его методы для
шифрования и дешифрования файла.
Рис. 2_1 Шифрование и дешифрование файлов
В класс Form1, создаваемый по умолчанию в Windows-проектах, я добавил поле с описанием
объекта класса CryptoTesting:
CryptoTesting ct ;
Класс CryptoTesting нам предстоит еще создать. Методы этого класса должны позволить нам
шифровать и дешифровать файлы. Обработчики событий Click командных кнопок нашей формы
просты и естественны. Вот как выглядит обработчик, создающий объект ct класса CryptoTesting:
private void button3_Click(object sender, System.EventArgs e)
{
string name_s, name_c, name_d;
name_s= textBox1.Text;
name_c= textBox2.Text;
name_d= textBox3.Text;
ct = new CryptoTesting(name_s,name_c,name_d);
}
Как видите, обработчик вызывает конструктор класса, передавая ему в качестве аргументов
имена соответствующих файлов, с которыми придется работать методам класса.
Еще более просто устроены обработчики для двух оставшихся командных кнопок – они
просто вызывают соответствующие методы класса:
private void button1_Click(object sender, System.EventArgs e)
{
ct.CipherFile();
}
private void button2_Click(object sender, System.EventArgs e)
{
ct.DecipherFile();
}
Закончив с интерфейсом, перейдем к основному делу – проектированию класса CryptoTesting.
Этот класс будет использовать классы библиотеки FCL, необходимые для шифрования и работы с
файлами. При объявлении класса следует задать ссылки на соответствующие пространства имен:
using System.Security.Cryptography;
using System.IO;
А теперь займемся самим классом и начнем с проектирования его полей. Вот они:
//поля
DESCryptoServiceProvider dcp;
string sn, cn, dn;
const int blocksize = 100;
Основным является конечно же объект dcp библиотечного класса DESCryptoServiceProvider.
Класс этот является наследником абстрактного класса DES и задает реализацию алгоритма
шифрования с закрытым ключом, отвечающего стандартам, принятым в США. Строковые поля
задают имена файлов, используемых в процессе шифрования. Константа blocksize задает размер
блоков, создаваемых в процессе работы с шифруемым и дешифруемым файлами.
Определим теперь конструктор класса:
public CryptoTesting(string n1, string n2, string n3)
{
dcp
= new DESCryptoServiceProvider();
dcp.GenerateKey();
dcp.GenerateIV();
sn= n1; cn= n2; dn=n3;
}
В конструкторе класса создается объект dcp, а затем вызываются два его метода,
позволяющие задать важнейшие свойства объекта – Key и IV – ключ и вектор инициализации. Оба
эти свойства представляют массивы байтов, размер которых должен отвечать требованиям
стандарта DES. По умолчанию размер ключа равен 64 битам. Конечно можно самому сгенерировать
значения этих свойств, но вполне разумно воспользоваться, как это сделано в нашем конструкторе,
методами, предлагаемыми в классе – GenerateKey и GenerateIV, генерирующими случайные
значения ключа и инициализирующего вектора.
Рассмотрим теперь метод, выполняющий шифрование файла:
public void CipherFile()
{
//Шифрование файла. Исходный и шифруемый файлы
//рассматриваются как бинарные файлы - потоки байтов
FileStream fin = new FileStream (sn, FileMode.Open,
FileAccess.Read);
FileStream fout = new FileStream(cn, FileMode.OpenOrCreate,
FileAccess.Write);
fout.SetLength(0);
//bin - промежуточный массив для хранения очередного блока
байтов
//len, rdlen, totlen // длина текущего блока, общее число прочитанных байтов, длина
файла
byte[] bin = new byte[blocksize];
int len;
long rdlen = 0;
long totlen = fin.Length;
//encStream - объект, описывающий шифруемый поток
CryptoStream encStream = new CryptoStream
(fout, dcp.CreateEncryptor(),CryptoStreamMode.Write);
//Исходный файл читается блок за блоком,
//каждый из них шифруется и записывается в выходной поток
while(rdlen < totlen)
{
len = fin.Read(bin, 0, blocksize);
encStream.Write(bin, 0, len);
rdlen = rdlen + len;
}
encStream.Close();
fout.Close();
fin.Close();
}
Этот метод нуждается в пояснениях. В шифровании и дешифровании файлов важную роль
играет класс CryptoStream. При создании объекта этого класса – encStream конструктору переданы
три аргумента. Первый из них определяет текущий поток – файл, в который поблочно будут
записываться шифруемые данные. Метод, который следует применить при шифровании –
dcp.CreateEncryptor, передается в качестве второго аргумента. Третий аргумент задает статус
текущего потока.
Запись и шифрование в текущий поток выполняется в цикле по числу записываемых блоков.
Метод Write объекта encStream берет блок из буфера, комбинирует его с ранее записанным в файл
блоком, шифрует его закрытым ключом, используя метод шифрования, переданный в момент
создания объекта, а затем шифрованный блок записывает в текущий поток. Для шифрования
первого блока используется инициализирующий вектор IV объекта dcp.
Отсюда следует достаточно простая схема шифрования любого файла. Вначале нужно создать
два объекта класса FileStream, они названы fin и fout. Первый из них задает исходный файл,
рассматриваемый как поток байтов независимо от его реальной природы, а второй определяет
создаваемый зашифрованный файл. После этого создается уже рассмотренный нами объект класса
CryptoStream. Затем организуется цикл, в котором исходный файл нарезается на блоки,
помещаемые в буфер, роль которого играет массив bin. Метод Write, как было сказано выше, этот
блок шифрует и записывает в выходной файл.
Схема дешифрования абсолютно симметрична. Единственное отличие состоит в методе
шифрования, передаваемому конструктору при создании объекта encStream. Если при шифровании
ему передавался метод dcp.CreateEncryptor, то при дешифровании имя метода меняется на
dcp.CreateDecryptor. Приведу уже без всяких комментариев текст метода DecipherFile:
public void DecipherFile()
{
//Дешифрование
FileStream fin = new FileStream (cn, FileMode.Open,
FileAccess.Read);
FileStream fout = new FileStream(dn, FileMode.OpenOrCreate,
FileAccess.Write);
byte[] bin = new byte[blocksize];
int len;
long rdlen = 0;
long totlen = fin.Length;
CryptoStream encStream = new CryptoStream
(fout, dcp.CreateDecryptor(), CryptoStreamMode.Write);
while(rdlen < totlen)
{
len = fin.Read(bin, 0, blocksize);
encStream.Write(bin, 0, len);
rdlen = rdlen + len;
}
encStream.Close();
fout.Close();
fin.Close();
}
Рассмотренная здесь схема шифрования и дешифрования файла вполне может использоваться
Алисой и Бобом при передаче сообщений. Сообщение Алисы представляется в виде файла, она его
шифрует рассмотренным здесь способом и посылает Бобу зашифрованный файл. Естественно,
чтобы Боб мог расшифровать файл, Алиса должна передать ему закрытый ключ и вектор IV. Для
передачи этих данных Алиса использует схему шифрования с открытым ключом.
Пример проектирования: класс, DLL, и многое другое
Рассмотрим сейчас достаточно большой пример, в котором создадим решение, содержащее
три проекта, один из которых будет DLL, а два других – консольным и Windows-приложениями,
использующих эту DLL. По выбору пользователя на выполнение может запускаться либо
консольное, либо Windows-приложение.
Наша DLL будет содержать класс, на примере проектирования которого постараемся
продемонстрировать многие детали – различные типы конструкторов класса, закрытие свойств,
создание собственных констант в классе, перегрузку операций, переопределение методов
родительского класса, выбрасывание исключений.
Чтобы не усложнять задачу, выберем достаточно понятную проблемную область. Давайте
создадим полноценный класс для работы с рациональными числами. Консольное и Windowsприложение в нашем решении будут представлять калькуляторы для работы с рациональными
числами.
Первоначальное построение решения (solution)
Новое решение создадим в среде Visual Studio 2005 и начнем как обычно с выбора типа
проекта. Первым проектом, помещаемым в решение, пусть будет DLL, а посему в качестве типа
проекта выберем библиотеку классов – Class Library. Дадим решению имя RationalsCalc, а проекту –
RationalsLibrary.
В созданное по умолчанию пространство имен Classes добавим класс Rational. Прежде чем
определять детали класса в тег summary, предшествующий классу, добавим неформальную его
спецификацию:
/// <summary>
/// Класс Rational.
/// определяет новый тип данных - рациональные числа и основные
/// операции над ними - сложение, умножение, вычитание и деление.
/// Рациональное число задается парой целых чисел (m,n) и
изображается,
/// обычно в виде дроби m/n. Число m называется числителем,
/// n - знаменателем.
/// Знаменатель не должен быть равен нулю!
/// Для каждого рационального числа существует
/// множество его представлений, например 1/2, 2/4, 3/6, 6/12 /// задают одно и тоже рациональное число. Среди всех представлений
/// можно выделить то, в котором числитель и знаменатель взаимно
/// несократимы. Такой представитель будет храниться в полях
класса.
/// Операции над рациональными числами определяются естественным
/// для математики образом.
/// </summary>
public class Rational
{
// Описание тела класса Rational
}//Rational
Поля класса Rational
Два целых числа m и n представляют рациональное число. Они и становятся полями класса.
Совершенно естественно сделать эти поля закрытыми (private), поскольку рациональное число
рассматривается как нечто неделимое и клиент класса не должен иметь доступа к составляющим
рационального числа. Поэтому для этих полей не будут даже определяться процедуры-свойства –
они будут полностью закрыты. Вот объявление полей класса:
//Поля класса. Числитель и знаменатель рационального числа.
long m,n;
Конструкторы класса Rational
Инициализация полей конструктором по умолчанию никак не может нас устраивать,
поскольку нулевой знаменатель это нонсенс. Поэтому определим конструктор с аргументами,
которому будут передаваться два целых – числитель и знаменатель создаваемого числа. Кажется,
что это единственный разумный конструктор, который может понадобиться нашему классу. Однако
чуть позже мы добавим в класс закрытый конструктор и статический конструктор, позволяющий
создать константы нашего класса. Вот определение конструктора:
/// <summary>
/// Конструктор класса. Создает рациональное число
/// m/n эквивалентное a/b, но со взаимно несократимыми
/// числителем и знаменателем. Если b=0, то выбрасывается
/// исключение специально спроектированного класса
/// RationalException
/// </summary>
/// <param name="a">числитель</param>
/// <param name="b">знаменатель</param>
public Rational(long a, long b)
{
const string mes = "Некорректная попытка создать рациональное
"
+ " число, знаменатель которого равен 0";
if (b == 0) throw (new RationalException(mes));
else
if (a == 0)
{
m = 0; n = 1;
}
else
{
//приведение знака
if (b < 0) { b = -b; a = -a; }
//приведение к несократимой дроби
long d = nod(a, b);
m = a / d; n = b / d;
}
}//Конструктор
Как видите, конструктор класса может быть довольно сложным. В нем, как в нашем случае,
может проверяться корректность задаваемых аргументов. Задание нулевого знаменателя для
рационального числа некорректно. Если такая ситуация возникает, то в конструкторе
выбрасывается исключение.
При проектировании класса следует проектировать и специальные классы исключений,
позволяющие классифицировать однозначным образом возникающие ситуации. Такой класс
RationalException и был создан. Опишем его чуть позже.
За исключением случая, когда знаменатель равен нулю, все остальные пары целых чисел
считаются корректными. Однако корректность пары a и b, заданной клиентом класса, еще не
означает, что именно эта пара будет представлять рациональное число. В большинстве случаев
заданная пара чисел приводится к эквивалентному рациональному числу, в котором знаменатель
всегда положителен и не имеет общих делителей с числителем. По ходу дела вызывается закрытый
метод класса, вычисляющий значение НОД(a,b) – наибольшего общего делителя чисел a и b.
Алгоритм вычисления НОД(a,b) подробно рассматривался в этой главе, так что приводимая
здесь реализация не требует дополнительных пояснений:
/// <summary>
/// Закрытый метод класса.
/// Возвращает наибольший общий делитель чисел a,b
/// </summary>
/// <param name="a">первое число</param>
/// <param name="b">второе число, положительное</param>
/// <returns>НОД(a,b)</returns>
long nod(long m, long n)
{
long p=0;
m = Math.Abs(m); n = Math.Abs(n);
if(n>m){p=m; m=n; n=p;}
do
{
p = m%n; m=n; n=p;
}while (n!=0);
return(m);
}//nod
Класс RationalException
Все классы исключений, как стандартные, так и создаваемые пользователем, являются
наследниками класса Exception. В большинстве случаев можно ограничиться наследованием
свойств и методов родительского класса, не определяя в потомке ничего нового. Однако придется
добавить в класс конструкторы, поскольку они не наследуются. Тела конструкторов можно сделать
пустыми, полагаясь на вызов конструкторов базового класса. Именно так и будет устроен наш
класс. Вот его текст:
/// <summary>
/// Исключение этого класса выбрасывается при попытке
///
создания рационального числа с нулевым знаменателем
/// </summary>
class RationalException : Exception
{
public RationalException()
{
}
public RationalException(string message)
: base(message)
{
}
public RationalException(string message, Exception e)
: base(message, e)
{
}
}
Более подробно с классом Exception и другими классами исключений можно познакомиться в
лекции 23 основного для нас учебника [1].
Нужно ли обрабатывать исключение в классе Rational?
В конструкторе класса Rational в нужный момент будет вызываться исключение:
if (b == 0) throw (new RationalException(mes));
Заметьте, в момент выбрасывания исключения создается объект класса RationalException,
вызывается один из спроектированных конструкторов, ему передается сообщение, однозначно
идентифицирующее ситуацию. Текст переданного сообщения становится значением свойства
Message объекта, характеризующего исключение.
Хочу обратить внимание на технологию работы. При обнаружении ситуации не делается
попытка уведомить клиента о возникшей ошибке и проложить работу. Нет, в подобной ситуации
следует выбрасывать исключение, что может привести к завершению работы приложения, если это
исключение не будет должным образом обработано. Но это правильная технология работы, –
продолжать работать с некорректными данными не следует. Важно лишь, в момент выбрасывания
исключения тщательно квалифицировать причину его появления.
Почему выбрасываемое исключение не обрабатывается в самом классе Rational? Ведь вполне
можно было бы организовать вычисления в try-catch блоках, перехватить исключение и обработать
его – выдать сообщение, скорректировать некоторым образом введенную пару чисел, организовать
диалог с пользователем. Всего этого делать не следует. Класс Rational может понять, что ситуация
возникла, но он не может понимать, как ее исправить. Это может сделать только клиент, по вине
которого возникла такая ситуация. Именно он должен обрабатывать исключительную ситуацию,
именно он понимает, как с ней справиться. Он нарушил спецификации класса, он и должен
исправлять ситуацию. В обязанности класса входит уведомление клиента о нарушении им
спецификаций класса с предоставлением клиенту всей необходимой информации. Это уведомление
делается в жесткой форме – форме исключительной ситуации, что приведет к прерыванию работы,
если клиент не примет надлежащих мер. Позже мы увидим, как клиент класса обрабатывает это
исключение и исправляет возникшую ситуацию.
Конечно, возможно, что клиент не нарушил спецификаций и исключительная ситуация
возникла по вине методов самого класса, некорректно обрабатывающих корректные данные. В
таких случаях исправление ситуации, перехват возникающих исключений входит в обязанность
самого класса. Ситуация с нулевым знаменателем к таким случаям не относится.
Методы класса Rational
После некоторого отступления вернемся к проектированию класса Rational. Заметьте, в основе
каждого класса лежит абстрактный тип данных, при определении которого нас интересует не
столько то, как представлены данные, а то, что с ними можно делать, – какие операции разрешены
над данными.
Если поля класса почти всегда закрываются, чтобы скрыть от пользователя представление
данных класса, то среди методов класса всегда есть открытые методы – это те сервисы (службы),
которые класс предоставляет своим клиентам и наследникам. Но не все методы открываются. Часть
методов класса может быть закрытой, скрывая от клиентов детали реализации, необходимые в
интересах внутреннего использования. Заметьте, скрытие представления данных, скрытие методов
и скрытие реализации делается не по соображениям утаивания того, как реализована система. Чаще
всего, ничто не мешает клиентам ознакомиться с полным текстом класса. Скрытие делается в
интересах самих клиентов. При сопровождении программной системы изменения в ней неизбежны.
Клиенты не почувствуют на себе негативные последствия изменений, если они делаются в
закрытой части класса. Чем больше закрытая часть класса, тем меньше влияние изменений на
клиентов класса. Знание спецификаций класса и его интерфейса – это, что необходимо клиенту для
успешной работы с классом.
Примером закрытого для клиентов метода является уже приведенный метод класса nod.
Клиентам, которым класс Rational интересен тем, что позволяет работать с рациональными
числами, нет дела до того, что методам класса Rational понадобилось вычислять наибольший общий
делитель, и им должно быть неважно, какой алгоритм при этом использовался.
Арифметические операции класса Rational
Поскольку рациональные числа это прежде всего числа, то давайте определим для них
основные арифметические операции: сложение и вычитание, умножение и деление, возведение в
целую степень. Реализуются все эти операции как операции над дробями, поэтому алгоритмы в
комментариях не нуждаются. Привычно рассматривать эти операции как бинарные и писать x+y,
где x и y рациональные числа, выступающие как равноправные партнеры. Для объектного стиля
характерна другая форма записи. Здесь пишут x.Plus(y). Объект x выступает в роли хозяина, он
вызывает метод класса Plus, у которого один аргумент – слагаемое, добавляемое к текущему
объекту. Понятно, что у объекта y те же права, что и у объекта x, так что аналогичный результат
будет получен при вызове y.Plus(x).
Вот определение методов, реализующих четыре основные арифметические операции над
рациональными числами:
public Rational Plus(Rational a)
{
long u,v;
u = m*a.n +n*a.m; v= n*a.n;
return( new Rational(u, v));
}//Plus
public Rational Minus(Rational a)
{
long u,v;
u = m*a.n - n*a.m; v= n*a.n;
return( new Rational(u, v));
}//Minus
public Rational Mult(Rational a)
{
long u,v;
u = m*a.m; v= n*a.n;
return( new Rational(u, v));
}//Mult
public Rational Divide(Rational a)
{
long u,v;
u = m*a.n; v= n*a.m;
return( new Rational(u, v));
}//Divide
Несколько слов следует сказать о возведении в целую степень. Проще всего реализовать эту
операцию простым умножением. Действительно, учитывая, что числитель и знаменатель – это
целые числа, то показатель степени n не может быть слишком большим числом, поскольку
результат может просто выйти за пределы разрядной сетки, отводимой для данных типа long. Тем
не менее в приводимой здесь реализации используется алгоритм, эффективный при больших n,
требующий лишь log(n) операций умножения. Этот алгоритм подробно рассматривался в этой
главе. Заметьте, реализация построена на использовании закрытого метода, так что ничто не
мешает сменить реализацию при необходимости, это никак не отразится на программе клиента,
вызывающего метод возведения в степень.
public Rational Pow(int k)
{
long u, v;
u = P(m,k); v = P(n,k);
return (new Rational(u, v));
}//Pow
//Возведение в степень. Алгоритм эффективен при больших k,
//поскольку требует порядка log(k) умножений
long P(long a, int k)
{
//вычисляет a^k
long z = 1;
while (k != 0)
{
while (k % 2 == 0)
{
k /= 2; a *= a;
}
//k- нечетно
{
z *= a; k--;
}
}
return z;
}
Перегрузка арифметических операций
Хотя методы и хороши, но привычнее использовать знаки арифметических операций.
Особенно они удобны при построении сложных арифметических выражений. Поэтому наряду с
методами определим и соответствующие операторы (знаки операций). Знаки операций являются
перегруженными. Это означает, что операции + соответствует множество различных реализаций,
так что запись x+y выполняется по-разному в зависимости от типов аргументов. К уже имеющемуся
множеству реализаций добавим и реализации, позволяющие выполнять операции над
рациональными числами.
Для определения знаков операций в языке C# используется специальное ключевое слово
operator. Операция определяется как статический метод с двумя аргументами. Вот как это делается:
public static Rational operator +(Rational r1, Rational r2)
{
return (r1.Plus(r2));
}
public static Rational operator -(Rational r1, Rational r2)
{
return (r1.Minus(r2));
}
public static Rational operator *(Rational r1, Rational r2)
{
return (r1.Mult(r2));
}
public static Rational operator /(Rational r1, Rational r2)
{
return (r1.Divide(r2));
}
public static Rational operator ^(Rational r1, int n)
{
return (r1.Pow(n));
}
Все эти операции реализуются вызовом соответствующего метода класса.
Операции отношения
Определим теперь операции отношения над рациональными числами – равно, не равно,
больше, меньше и другие. Можно было бы, как в случае арифметических операций, вначале
определить соответствующие методы, а уже потом через них определить операции. Но в данном
случае определим только операции, не вводя традиционных методов.
public static bool operator ==(Rational r1, Rational r2)
{
return((r1.m ==r2.m)&& (r1.n ==r2.n));
}
public static bool operator !=(Rational r1, Rational r2)
{
return((r1.m !=r2.m)|| (r1.n !=r2.n));
}
public static bool operator <(Rational r1, Rational r2)
{
return(r1.m * r2.n < r2.m* r1.n);
}
public static bool operator >(Rational r1, Rational r2)
{
return(r1.m * r2.n > r2.m* r1.n);
}
public static bool operator <(Rational r1, double r2)
{
return((double)r1.m / (double)r1.n < r2);
}
public static bool operator >(Rational r1, double r2)
{
return((double)r1.m / (double)r1.n > r2);
}
Обратите внимание, операции отношения определены не только над рациональными числами.
Допускается сравнение рациональных и вещественных чисел, что позволяет сравнивать на большеменьше числа типа Rational и числа любых арифметических типов – int, long, float, совместимых с
типом double.
Переопределение методов родительского класса
Созданный нами класс Rational, как и все классы в языке C#, является наследником класса
object. При этом наследуются методы родителя и их реализация, что не всегда приемлемо для
наследника. Поэтому наследник, как правило переопределяет реализацию методов родителя с
учетом собственной специфики.
Переопределим метод ToString так, чтобы он выдавал информацию о значении
рационального числа в традиционном виде m/n. Переопределим также метод GetHashCode. Если
клиент захочет размещать рациональные числа в хэш-таблице, то для каждого объекта требуется
возвращаемый методом хэш-код, определяющий местоположение числа в таблице. Этот код
естественно определять, как некоторую функцию от полей класса. В нашем случае используем для
этих целей побитовую операцию «исключающее или», выполняемую над полями класса. Поскольку
мы определили операцию эквивалентности над рациональными числами, то разумно
переопределить и родительский метод Equal. Вот код этих методов:
public override string ToString()
{
return m.ToString() + "/" + n.ToString();
}
public override bool Equals(object obj)
{
return (this == (Rational)obj);
}
public override int GetHashCode()
{
return (int)(m ^ n);
}
Другие методы класса
Определим еще пару методов класса. Метод PrintRational по сути является модификацией
метода ToString. Отличие состоит лишь в том, что к возвращаемой строке добавляется имя объекта,
предаваемое методу в качестве аргумента:
public string PrintRational(string name)
{
return(name + " = " + m.ToString() +"/" +n.ToString());
}//PrintRational
Более интересным является метод Round, позволяющий производить округление
рационального числа, получая его приближенное значение. При выполнении последовательности
операций над рациональными числами значение числителя и знаменателя как правило возрастает,
хотя само число как дробь может быть невелико. В особенности это характерно при выполнении
операции возведения в степень. В таких ситуациях полезна операция округления, упрощающая
представление числа. Реализация этой операции базируется на методе округления целых чисел.
Приведу тексты открытого и закрытого методов, занимающихся округлением:
public Rational RationalRound(int k)
{
// округляет рациональное число x, оставляя не более
// k значащих цифр в числителе и знаменателе
long u = LongRound(m, k);
long v = LongRound(n, k);
return (new Rational(u,v));
}//RationalRound
long LongRound(long x, int k)
{
// округляет число x, оставляя не более
// k значащих цифр. Пример: 126784 -> 127000 (k=3)
string s1 = x.ToString();
if (k >= s1.Length) return x;
string s2 = s1.Substring(0, k);
string s3 = s1.Substring(k,1);
if (int.Parse(s3) >= 5)
s2 = (int.Parse(s2) + 1).ToString();
for (int i = 0; i < s1.Length - k; i++)
s2 += "0";
return (long.Parse(s2));
}//LongRound
Константы класса и специфические конструкторы
Последняя важная тема, которую мы разберем на примере конструирования класса Rational –
это способ создания собственных констант класса. Предположим, что мы в классе Rational хотим
создать две именованные константы – Zero и One, задающие соответственно нуль и единицу типа
Rational. Задать эти константы, используя обычную для констант конструкцию const невозможно
хотя бы по той причине, что такие константы должны быть инициализированы в момент
объявления константными выражениями, но никаких константных выражений типа Rational быть
не может, поскольку класс еще только объявляется. Необходимы обходные пути.
Прежде всего заметим, что константы по сути являются статическими полями,
используемыми только для чтения. Это позволяет определить константы следующим образом:
//Константы класса 0 и 1 - Zero и One
public static readonly Rational Zero, One;
Константы объявлены, но значений они не получили. Проблема состоит в том, что константы
должны получить свои значения еще до того, как создан первый из объектов класса. Можно ли это
сделать? Оказывается можно. Напомню, объекты, являющиеся экземплярами класса, динамически
создаются в ходе выполнения программы в тот момент, когда выполняется операция new и
вызывается конструктор класса. Однако кроме этих объектов автоматически создается еще один
объект для каждого класса. Этот объект описывает класс, а не отдельные экземпляры. Этот объект
содержит статические поля класса и ссылки на статические методы класса. Этот объект создается
статическим конструктором класса. Никаких параметров этот конструктор естественно не имеет,
поскольку вызывается автоматически. Вызывается этот конструктор до начала создания
динамических объектов класса. Статический конструктор можно переопределить в классе и это
открывает путь к заданию значения констант – статических полей класса.
Как правило, в классе определяется еще один конструктор – закрытый конструктор класса.
Этот конструктор и создает объекты класса Rational, представляющие значения констант. Этот
конструктор вызывается в теле статического конструктора. В совокупности это и позволяет
справиться с проблемой констант. Вот как выглядит соответствующий код:
/// <summary>
/// Закрытый конструктор класса. Имеет дополнительный
/// параметр t, чтобы его сигнатура отличалась от
/// сигнатуры открытого конструктора
/// </summary>
private Rational(long a, long b, string t)
{
m= a; n =b;
}
static Rational()
{
//Статический конструктор класса без параметров
// Определяет константы класса
Zero = new Rational(0, 1, "private");
One
= new Rational (1, 1, "private");
}//Статический конструктор
Итоги
На примере класса Rational рассмотрены многие важные детали, возникающие при
проектировании классов. Подведем некоторые итоги:




Проектируя класс, следует предусмотреть возможность возникновения
исключительных ситуаций, при которых нормальная работа с объектами класса
представляется невозможной. В таких случаях следует одновременно с
проектируемым классом создавать специальные классы исключений. Это позволит
в момент обнаружения исключительной ситуации выбросить (throw) исключение
специального типа. Исключения предоставляют корректный способ борьбы с
исключительными ситуациями.
Поля класса как правило закрываются и недоступны клиентам класса. Различные
стратегии доступа реализуются с помощью специальных процедур-свойств. В
нашем примере эти процедуры отсутствуют, и у клиента нет способа добраться до
поля класса. Рациональное число воспринимается как неделимое целое.
Часть методов класса всегда открыты для клиента. Эти методы составляют
интерфейс класса. Это тот набор служб, который класс предоставляет своим
клиентам. Чтобы клиент мог эффективно использовать класс, ему достаточно
знать сигнатуру открытых методов класса, знать спецификацию класса и
спецификацию открытых методов.
Часть методов класса закрыты для клиентов класса и используются для
внутренних потребностей класса. Скрытие полей, скрытие методов, скрытие
реализации делается прежде всего в интересах клиента. Если клиенты не
используют информацию, находящуюся в закрытой части класса, то неизбежные
изменения, сделанные в закрытой части класса, никак не скажутся на программе
клиента.



Во многих случаях наряду с традиционными методами класса полезно в классе
определять операции (операторы или знаки операций). Это облегчает запись
выражений над объектами класса.
Важную роль в классе играют конструкторы. Создать объекты класса можно
только с помощью конструктора. Как правило в классе создаются несколько
открытых конструкторов с разным набором аргументов. Кроме того у класса
всегда есть статический конструктор, вызываемый автоматически в интересах
создания объекта, связанного с классом, а не с отдельными его экземплярами.
Собственные константы класса нельзя создать обычным способом. Они создаются,
используя статические поля класса, инициализируемые в момент вызова
статического конструктора.
Калькулятор рациональных чисел – Windows-приложение
Теперь, когда у нас есть библиотека, позволяющая работать с рациональными числами,
построим простейший калькулятор для работы с ними. Добавим в наше решение новый проект. Это
можно сделать по-разному, но проще всего, не выходя из существующего решения, в меню File
выбрать New|Project. В открывшемся окне нового проекта в качестве типа проекта выбрать
Windows Application, в окне Solution выбрать из списка «Add to Solution», дать имя проекту,
например WRationalCalc, как сделал я, и приступить к работе над новым проектом.
Первое, что следует сделать, установить ссылку из проекта на созданную и хранящуюся в
решении DLL. Для этого в меню Project выбрать пункт AddReference, в открывшемся окне выбрать
вкладку Projects и из списка проектов, уже включенных в решение, выбрать имя проекта
RationalsLibrary, хранящего построенную DLL. Заметьте, если нужно сослаться на проект из
другого решения, то для этой цели существует вкладка Browse, позволяющая установить путь к
нужному проекту.
Подготовительные работы на этом закончены и можно приступить непосредственно к
построению калькулятора. Я не стал усложнять задачу и ограничился построением достаточно
простого калькулятора. Его внешний вид в процессе работы показан на рис. 2_2.
Рис. 2_2 Калькулятор рациональных чисел в процессе работы
Спецификации, которые должен знать пользователь для корректной работы с
калькулятором, описаны в теге summary класса Form1. Приведу описание этого класса, но прежде
напомню, что проект создается в среде Visual Studio 2005, а потому класс Form1 представлен
несколькими файлами и это та часть класса, в которой содержатся данные и обработчики событий,
определяемые пользователем.
/// <summary>
/// Калькулятор для работы с рациональными числами.
/// В окна r1 и r2 следует ввести рациональные числа
/// в формате m/n. После нажатия командных кнопок r1 и r2
/// калькулятор готов к работе. Для выполнения операции над
числами
/// следует нажать кнопку с соответствующей операцией:
/// (+, -, *, /, ^,Round).
/// Результат операции сохраняется в r1.
/// Аргумент для возведения в степень и округления задается в окне
n.
/// При неверном вводе данных выдается сообщение.
/// </summary>
public partial class Form1 : Form
{
Rational r1 = new Rational(1, 1);
Rational r2 = new Rational(1, 1);
int k = 0;
public Form1()
{
InitializeComponent();
}
//обработчики событий кнопок калькулятора
}
Три поля класса задают аргументы операций, выполняемых над рациональными числами,
обеспечивая интерфейс с пользователем.
Рассмотрим обработчики событий кнопок r1, r2, n, передающих в поля класса данные,
введенные пользователем в окна формы:
private void button1_Click(object sender, EventArgs e)
{
//ввод r1
string s, s1, s2;
int ind,m, n;
s = textBox1.Text;
try
{
ind = s.IndexOf('/');
s1 = s.Substring(0, ind);
s2 = s.Substring(ind + 1);
m = int.Parse(s1);
n = int.Parse(s2);
r1 = new Rational(m, n);
textBox1.Text = r1.PrintRational("");
}
catch (Exception ex)
{
textBox1.Text = ex.Message +
"\r\nЗадайте корректный формат: n/m";
}
}
private void button2_Click(object sender, EventArgs e)
{
//ввод r2
string s, s1, s2;
int ind, m, n;
s = textBox2.Text;
try
{
ind = s.IndexOf('/');
s1 = s.Substring(0, ind);
s2 = s.Substring(ind + 1);
m = int.Parse(s1);
n = int.Parse(s2);
r2 = new Rational(m, n);
textBox2.Text = r2.PrintRational("");
}
catch (Exception ex)
{
textBox2.Text = ex.Message +
"Задайте корректный формат: n/m";
}
}
private void button5_Click(object sender, EventArgs e)
{
//ввод k
k = int.Parse(textBox3.Text);
textBox3.Text = "=" + k.ToString();
}
private void button9_Click(object sender, EventArgs e)
{
//чистка окон r1, r2, n;
textBox1.Text = "";
textBox2.Text = "";
textBox3.Text = "";
}
Ввод данных, осуществляемый пользователем, всегда следует контролировать. Поэтому
преобразование данных из полей формы в поля класса организовано в виде try-catch блока,
предусматривающее возможность появления в процессе ввода исключительных ситуаций и их
обработку.
Пользователи склонны периодически нарушать заданные спецификации. У нарушений могут
быть две разные причины – либо знаменатель рационального числа равен нулю, либо неверно задан
формат вводимых данных. В первом случае ситуация обнаружится в конструкторе класса Rational,
где будет выброшено исключение соответствующего типа. Во втором случае ошибка обнаружится в
процессе преобразования введенной строки в число. В обоих случаях пользователю будет выдано
сообщение, описывающее возникшую ситуацию, и он сможет повторить ввод.
Обработчики события Click для кнопок, издающих операции над рациональными числами
устроены просто и похожи друг на друга. В классе Rational определены знаки операций, что
позволяет переменной r1 присвоить бинарное выражение с заданной бинарной операцией. Лишь в
обработчике, реализующем операцию округления рационального числа, вызывается метод
RationalRound класса Rational.
private void button4_Click(object sender, EventArgs e)
{
//сложение
r1 = r1 + r2;
textBox1.Text = r1.PrintRational("");
}
private void button3_Click(object sender, EventArgs e)
{
//вычитание
r1 = r1 - r2;
textBox1.Text = r1.PrintRational("");
}
private void button7_Click(object sender, EventArgs e)
{
//умножение
r1 = r1 * r2;
textBox1.Text = r1.PrintRational("");
}
private void button6_Click(object sender, EventArgs e)
{
//деление
r1 = r1 / r2;
textBox1.Text = r1.PrintRational("");
}
private void button8_Click(object sender, EventArgs e)
{
//возведение в целую степень
r1 = r1 ^ k;
textBox1.Text = r1.PrintRational("");
}
private void button10_Click(object sender, EventArgs e)
{
//округление до k значащих цифр
r1 = r1.RationalRound(k);
textBox1.Text = r1.PrintRational("");
}
Для демонстрации сложно устроенных выражений над рациональными числами на форму
посажена кнопка Expression, обработчик события Click которой вычисляет сложно устроенное
выражение:
private void button11_Click(object sender, EventArgs e)
{
//пример выражения над рациональными числами
if ((r1 * r1 - r2 ^ 3) > Rational.One)
r1 = r1 / r2 + new Rational(24, 46);
else
r1 = (r1 + r2) ^ 2;
textBox1.Text = r1.PrintRational("");
}
Консольный калькулятор для работы с рациональными числами
Для полноты картины добавим в наше решение еще один проект, реализующий калькулятор
для работы с рациональными числами, но уже в консольном варианте. Добавление нового проекта в
решение и его связывание с библиотекой выполнялось по уже описанной схеме.
Построим традиционное консольное приложение, где в некотором цикле, управляемом
пользователем, на каждом шаге ему предлагается меню из возможных команд. Пользователь
выбирает команду, вводит нужные данные для этой команды, затем команда обрабатывается,
результат ее выполнения выводится на экран, и все повторяется, пока пользователь не откажется от
продолжения работы.
Анализ выбора пользователя строится по известной схеме разбора случаев, а тело каждого
варианта case практически совпадает с тем, что делается в обработчике события Click при выборе
соответствующей кнопки в калькуляторе Windows. Приведу текст консольного проекта:
namespace ConsoleCalc
{
class Program
{
static Rational r1, r2;
static int n;
static int menu;
static void Main(string[] args)
{
string answer = "yes";
string invite;
r1 = new Rational(1,1);
r2 = new Rational(1,1);
invite = "Выберите пункт меню:\r\n";
invite += "0 - ввод r1\r\n";
invite += "1 - ввод r2\r\n";
invite += "2 - r1 = r1+r2\r\n";
invite += "3 - r1 = r1-r2\r\n";
invite += "4 - r1 = r1*r2\r\n";
invite += "5 - r1 = r1/r2\r\n";
invite += "6 - (r1 > r2)?\r\n";
invite += "7 - (r1 < r2)?\r\n";
invite += "8 - (r1 == r2)?\r\n";
invite += "9 - r1 = r1^n\r\n";
invite += "10 - r1 = r1.Round(n)\r\n";
do
{
Console.WriteLine(invite);
menu = int.Parse(Console.ReadLine());
Parsing();
Console.WriteLine("Продолжим? (yes/no)");
answer = Console.ReadLine();
} while (answer == "yes");
}
static void Parsing()
{
//Разбор случаев. Выполняются действия согласно
//выбранному пункту меню.
int a=1, b=1;
bool r = true;
switch (menu)
{
case 0:
{
Console.WriteLine("Введите целое - числитель
r1");
a = int.Parse(Console.ReadLine());
Console.WriteLine("Введите целое - знаменатель
r1");
b = int.Parse(Console.ReadLine());
r1 = new Rational(a, b);
break;
}
case 1:
{
Console.WriteLine("Введите целое - числитель
r2");
a = int.Parse(Console.ReadLine());
Console.WriteLine("Введите целое - знаменатель
r2");
b = int.Parse(Console.ReadLine());
r2 = new Rational(a, b);
break;
}
case 2:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r1 = r1+r2;
Console.WriteLine(r1.PrintRational("r1+r2"));
break;
}
case 3:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r1 = r1 - r2;
Console.WriteLine(r1.PrintRational("r1-r2"));
break;
}
case 4:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r1 = r1 * r2;
Console.WriteLine(r1.PrintRational("r1*r2"));
break;
}
case 5:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r1 = r1 / r2;
Console.WriteLine(r1.PrintRational("r1/r2"));
break;
}
case 6:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r = (r1 > r2);
Console.WriteLine("(r1>r2) == "+r.ToString());
break;
}
case 7:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r = (r1 < r2);
Console.WriteLine("(r1<r2) == " +
r.ToString());
break;
}
case 8:
{
Console.WriteLine(r1.PrintRational("r1"));
Console.WriteLine(r2.PrintRational("r2"));
r = (r1 == r2);
Console.WriteLine("(r1==r2) == " +
r.ToString());
break;
}
case 9:
{
Console.WriteLine("Введите целое - степень
r1");
n = int.Parse(Console.ReadLine());
Console.WriteLine(r1.PrintRational("r1"));
r1 = r1 ^ n;
Console.WriteLine(r1.PrintRational("r1^n"));
break;
}
case 10:
{
Console.WriteLine("Введите целое - " +
"число значащих цифр при округлении r1");
n = int.Parse(Console.ReadLine());
Console.WriteLine(r1.PrintRational("r1"));
r1 = r1.RationalRound(n);
Console.WriteLine(r1.PrintRational("r1.RationalRound("
+ n.ToString()));
break;
}
}
}
}
На рис. 2_3 показан процесс работы с консольным калькулятором:
Рис. 2_3 Консольный калькулятор в процессе работы
В заключение несколько слов о том, как запускать нужный проект на выполнение. В
построенном нами решении два проекта – консольный и Windows, каждый из которых является
выполняемым. Выбрав для любого из этих проектов в меню Project пункт «Set as StartUp Project»,
можно установить, какой из проектов будет запускаться на выполнение. Большего можно добиться
на странице свойств решения, где можно не только установить стартовый проект, но запустить оба
проекта одновременно, включив возможное свойство «Multiple startup projects».
Глава 3 Последовательности (массивы)
Алгоритмы и задачи, рассматриваемые в этой главе, являются частью фундамента, на котором
строится образование программиста. Нет ни одной проблемной области, в задачах которой не
требовались бы массивы. Поэтому задачи, требующие использования массивов появлялись уже в
предыдущих главах, появятся они и в последующих. Но здесь мы будем заниматься ими
целенаправленно. Аналогичная ситуация имеет место и в учебнике, где массивы появляются
буквально в каждой главе, хотя им посвящены и отдельные главы 11 и 12, знание которых
несомненно полезно для решения приводимых здесь задач.
Последовательность элементов – a1, a2, …. an – одна из любимых структур в математике.
Последовательность можно рассматривать как функцию a(i), которая по заданному значению
индекса элемента возвращает его значение. Эта функция задает отображение integer -> T, где T –
это тип элементов последовательности.
В программировании последовательности называются массивами, но от этого они не
перестают быть менее любимыми. Массив – это упорядоченная последовательность элементов
одного типа. Порядок элементов задается с помощью индексов.
Для программистов важно то, как массивы хранятся в памяти. Массивы занимают
непрерывную область памяти, поэтому, зная адрес начального элемента массива, зная, сколько
байтов памяти требуется для хранения одного элемента, и, зная индекс (индексы) некоторого
элемента, нетрудно вычислить его адрес, а значит и хранимое по этому адресу значение элемента.
На этом основана адресная арифметика в языках C, C++, где адрес элемента a(i) задается адресным
выражением a+i, в котором имя массива a воспринимается как адрес первого элемента. При
вычислении адреса i-го элемента индекс i умножается на длину слова, требуемого для хранения
элементов типа T. Адресная арифметика приводит к 0-базируемости элементов массива, когда
индекс первого элемента равен нулю, поскольку первому элементу соответствует адресное
выражение а+0.
Язык C# сохранил 0-базируемость массивов. Индексы элементов массива в языке C#
изменяются в плотном интервале значений от нижней границы, всегда равной 0, до верхней
границы, заданной динамически вычисляемым выражением, возможно зависящим от переменных.
Массивы C# являются 0-базируемыми динамическими массивами. Это важно понимать с самого
начала.
Не менее важно понимать и то, что массивы C# относятся к ссылочным типам. Рассмотрим
следующий фрагмент программного кода:
int[] x, y = {1, 2, 3};
double[] z;
int[,] u, v = {{1,3,5},{2,4,6}};
Здесь объявлены пять переменных – x, y, z, u, v. Все они являются массивами, но разных
типов. Переменные x и y принадлежат типу T = int[], задающему одномерные массивы с
элементами целого типа int. Переменные u и v принадлежат другому типу T1 = int[,]- двумерных
массивов с элементами целого типа. Переменная z принадлежит типу T3 –одномерных массивов с
элементами вещественного типа double. Все три типа массивов – T1, T2, T3 являются наследниками
общего родителя – типа (класса) Array, наследуя от родителя многие полезные свойства и методы.
Все пять переменных являются типизированными ссылками. В момент объявления три переменные
– x, z и u не инициализированы и потому являются «висячими» ссылками со значением null.
Переменные y и v объявлены с инициализацией. При инициализации в динамической памяти
создаются два объекта соответственно типов T1 и T3, фактически и задающие реальные массивы. У
первого из этих объектов 3 элемента, у второго - 6. Ссылки y и v связываются с этими объектами.
При связывании тип ссылки и тип объекта должны быть согласованными. Заметьте, число
элементов в массиве является характеристикой объекта, а не типа. Ссылка может быть связана с
объектами, содержащими различное число элементов, необходимо лишь выполнение условия
согласования типов.
Дополним наш код следующими строчками:
x = new int[10];
z = new double[20];
u = new int[3, 5];
Здесь последовательно вызываются три конструктора типов T1, T2, T3, создающие новые три
объекта в памяти и ссылки x, y, z связываются с этими объектами, так что у массива x теперь 10
элементов, z – 20, u -15.
Рассмотрим, что произойдет в результате присваивания:
x = y;
u = v;
Присваивание законно, поскольку переменные в левой и правой части имеют один и тот же
(следовательно, согласованный) тип. В результате присваивания переменные порвут связь с теми
объектами, с которыми они были связаны, и будут связаны с новыми объектами, так что x теперь
имеет 3 элемента, u – 6. Присваивание для объектов – это, если хотите, развод и брак одновременно.
Правда, для холостяков - неинициализированных переменных - это первый брак с объектом.
На этом закончу краткое представление массивов. Подробные сведения о них содержатся в
упомянутых главах учебника 11 и 12.
Ввод – вывод массивов
Как у массивов появляются значения, как они изменяются? Возможны три основных способа:

Вычисление значений в программе;

значения вводит пользователь;

связывание с источником данных.
В задачах этого раздела ограничимся пока рассмотрением первых двух способов. Первый
способ более или менее понятен. Простые примеры его применения приводились чуть выше.
Приведу некоторые рекомендации по вводу и выводу массивов, ориентированные на работу с
конечным пользователем.
Для консольных приложений ввод массива обычно проходит несколько этапов:



ввод размеров массива;
создание массива;
организация цикла по числу элементов массива, в теле которого выполняется:
o приглашение к вводу очередного элемента;
o ввод элемента;
o проверка корректности введенного значения.
Вначале у пользователя запрашиваются размеры массива, затем создается массив заданного
размера. В цикле по числу элементов организуется ввод значений. Вводу каждого значения
предшествует приглашение к вводу с указанием типа вводимого значения, а при необходимости и
диапазона, в котором должно находиться требуемое значение. Поскольку ввод значений – это
ответственная операция, а на пользователя никогда нельзя положиться, то после ввода часто
организуется проверка корректности введенного значения. При некорректном задании значения
элемента ввод повторяется, пока не будет достигнут желаемый результат.
При выводе массива на консоль, обычно вначале выводится имя массива, а затем его
элементы в виде пары: <имя> = <значение> (например, f[5] = 77,7). Задача осложняется для
многомерных массивов, когда для пользователя важно видеть не только значения, но и структуру
массива, например располагая одну строку массива в одной строке экрана. Приведу пример того,
как может выглядеть ввод – вывод массива в консольном приложении. На рис. 1 показан экран в
процессе работы консольной программы, организующей ввод массива.
Рис. 1 Ввод - вывод массива на консоль
На рис. 2 показана работа программы с контролем ввода, где пользователь повторяет ввод,
пока не введет значение, удовлетворяющее типу элемента массива.
Рис. 2 Ввод - вывод массива на консоль с контролем значений
Как организовать контроль ввода? Наиболее разумно использовать для этих целей
конструкцию охраняемых блоков – try – catch блоков. Это общий подход, когда все опасные
действия, обычно связанные с работой пользователя, внешних устройств, внешних источников
данных, размещаются в охраняемых блоках. Вот как может выглядеть ввод элемента массива z[i]
типа double, помещенный в охраняемый блок:
bool correct;
do
{
correct = true;
try
{
z[i] = Convert.ToDouble(Console.ReadLine());
}
catch (Exception e)
{
Console.WriteLine
("Значение не корректно. Повторите ввод!");
correct = false;
}
} while (!correct);
Как правило, для ввода- вывода массивов пишутся специальные процедуры, вызываемые в
нужный момент.
Ввод - вывод массивов в Windows приложениях
Приложения Windows позволяют построить дружелюбный интерфейс пользователя,
облегчающий работу по вводу и выводу массивов. И здесь, когда данные задаются пользователем,
заполнение массива проходит через те же этапы, что рассматривались для консольных приложений.
Но выглядит все это более красиво, наглядно и понятно. Пример подобного интерфейса,
обеспечивающего работу по вводу и выводу одномерного массива, показан на рис. 3.
Рис. 3 Форма для ввода – вывода одномерного массива
Пользователь вводит в текстовое окно число элементов массива и нажимает командную
кнопку «Создать массив», обработчик которой создает массив заданной размерности. Затем он
может переходить к следующему этапу – вводу элементов массива. Очередной элемент массива
вводится в текстовое окно, а обработчик командной кнопки «Ввести элемент» обеспечивает
передачу значения в массив. Корректность ввода можно контролировать и здесь, проверяя значение
введенного элемента и выводя в специальное окно сообщение в случае его некорректности,
добиваясь, в конечном итоге, получения от пользователя корректного ввода.
Для облегчения работы пользователя текстовое окно для ввода элемента сопровождается
специальным окном, содержащим информацию, какой именно элемент должен вводить
пользователь. В примере возможного интерфейса пользователя, показанного на рис. 3, после того,
как все элементы массива введены, окно ввода становится недоступным для ввода элементов.
Интерфейс формы позволяет многократно создавать новый массив, повторяя весь процесс.
На рис. 3 форма разделена на две части – для ввода и вывода массива. Крайне важно уметь
организовать ввод массива, принимая данные от пользователя. Не менее важно уметь отображать
существующий массив в форме, удобной для восприятия пользователя. На рисунке показаны три
различных элемента управления, пригодные для этих целей – ListBox, CheckedListBox и ComboBox.
Программа, форма которой показана на рис. 3, сделана так, что как только вводится очередной
элемент, он немедленно отображается во всех трех списках.
Отображать массив в трех списках конечно не нужно, это сделано только в целях
демонстрации возможностей различных элементов управления. Для целей вывода подходит любой
из них, выбор зависит от контекста и предпочтений пользователя. Элемент CheckedListBox
обладает дополнительными свойствами в сравнении с элементом ListBox, позволяя отмечать
некоторые элементы списка (массива). Отмеченные пользователем элементы составляют
специальную коллекцию. Эта коллекция доступна, с ней можно работать, что иногда весьма
полезно.
Ввод двумерного массива немногим отличается от ввода одномерного массива. Сложнее
обстоит дело с выводом двумерного массива, если при выводе пытаться отобразить структуру
массива. К сожалению все три элемента управления, хорошо справляющиеся с отображением
одномерного массива, плохо приспособлены для показа структуры двумерного массива. Хотя у того
же элемента ListBox есть свойство MultiColumn, включение которого позволяет показывать массив
в виде строк и столбцов, но это не вполне то, что нужно для наших целей – отображения структуры
двумерного массива. Хотелось бы, чтобы элемент имел такие свойства, как Rows и Columns, а их у
элемента ListBox нет. Нет их и у элементов ComboBox и CheckedListBox. Приходится обходиться
тем, что есть. На рис. 4 показан пример формы, поддерживающей работу по вводу и выводу
двумерного массива.
Рис. 4 Форма, поддерживающая ввод и вывод двумерного массива
Интерфейс формы схож с тем, что использовался для организации работы с одномерным
массивом. Программная настройка размеров элемента управления ListBox позволила даже
отобразить структуру массива. Но в общей ситуации, когда значения, вводимые пользователем,
могут колебаться в широком диапазоне, трудно гарантировать отображение структуры двумерного
массива. Однако ситуация не безнадежна. Есть и другие, более мощные и более подходящие для
наших целей элементы управления. Если на элементах ListBox и подобных ему, я останавливаться
не буду, оставляя их для самостоятельного изучения, то об элементе DataGridView расскажу
подробнее.
Элемент управления DataGridView и отображение массивов
Элемент управления DataGridView является последней новинкой в серии табличных
элементов DataGrid, позволяющих отображать таблицы. Главное назначение этих элементов –
связывание с таблицами внешних источников данных, прежде всего с таблицами баз данных. Мы
же сейчас рассмотрим другое его применение – в интерфейсе, позволяющем пользователю вводить
и отображать матрицы – двумерные массивы.
Рассмотрим классическую задачу умножения прямоугольных матриц C=A*B.
Построим
интерфейс, позволяющий пользователю задавать размеры перемножаемых матриц, вводить данные
для исходных матриц A и B, перемножать матрицы и видеть результаты этой операции. На рис. 5
показан возможный вид формы, поддерживающей работу пользователя. Форма показана в тот
момент, когда пользователь уже задал размеры и значения исходных матриц, выполнил умножение
матриц и получил результат.
Рис. 5 Форма с элементами
матрицами
DataGridView, поддерживающая работу с
На форме расположены три текстовых окна для задания размеров матриц, три элемента
DataGridView для отображения матриц, три командные кнопки для выполнения операций,
доступных пользователю. Кроме того, на форме присутствуют 9 меток (элементов управления
label), семь из которых видимы на рис. 5. В них отображается информация, связанная с формой и
отдельными элементами управления. Текст у невидимых на рисунке меток появляется тогда, когда
обнаруживается, что пользователь некорректно задал значение какого-либо элемента исходных
матриц.
А теперь перейдем к описанию того, как этот интерфейс реализован. В классе Form2,
которому принадлежит наша форма, зададим поля, определяющие размеры матриц и сами матрицы:
//поля класса Form
int m, n, p;
//размеры матриц
double[,] A, B, C;
//сами матрицы
Рассмотрим теперь, как выглядит обработчик события «Click» командной кнопки «Создать
DataGridView». Предполагается, что пользователь разумен и, прежде чем нажать эту кнопку, задает
размеры матриц в соответствующих текстовых окнах. Напомню, что при перемножении матриц
размеры матриц должны быть согласованы – число столбцов первого сомножителя должно
совпадать с числом строк второго сомножителя, а размеры результирующей матрицы определяются
размерами сомножителей. Поэтому для трех матриц в данном случае достаточно задать не шесть, а
три параметра, определяющих размеры.
Обработчик события выполняет три задачи – создает сами матрицы, осуществляет чистку
элементов управления DataGridView, удаляя предыдущее состояние, затем добавляет столбцы и
строки в эти элементы в полном соответствии с заданными размерами матриц. Вот текст
обработчика:
private void button1_Click(object sender, EventArgs e)
{
//создание матриц
m = Convert.ToInt32(textBox1.Text);
n = Convert.ToInt32(textBox2.Text);
p = Convert.ToInt32(textBox3.Text);
A = new double[m, n];
B = new double[n, p];
C = new double[m, p];
//Чистка DGView, если они не пусты
int k =0;
k = dataGridView1.ColumnCount;
if (k != 0)
for (int i = 0; i < k; i++)
dataGridView1.Columns.RemoveAt(0);
dataGridView2.Columns.Clear();
dataGridView3.Columns.Clear();
//Заполнение DGView столбцами
AddColumns(n, dataGridView1);
AddColumns(p, dataGridView2);
AddColumns(p, dataGridView3);
//Заполнение DGView строками
AddRows(m, dataGridView1);
AddRows(n, dataGridView2);
AddRows(m, dataGridView3);
}
Прокомментирую этот текст:


Прием размеров и создание матриц, надеюсь, не требует дополнительных комментариев;
Чистка предыдущего состояния элементов DataGridView сводится к удалению столбцов.
Продемонстрированы два возможных способа выполнения этой операции. Для первого
элемента показано, как можно работать с коллекцией столбцов. Организуется цикл по
числу столбцов коллекции и в цикле выполняется метод RemoveAt, аргументом которого
является индекс удаляемого столбца. Поскольку после удаления столбца происходит
перенумерация столбцов, то на каждом шаге цикла удаляется первый столбец, индекс
которого всегда равен нулю. Удаление столбцов коллекции можно выполнить одним

махом, - вызывая метод Clear() коллекции, что и делается для остальных двух элементов
DataGridView;
После чистки предыдущего состояния, можно задать новую конфигурацию элемента,
добавив в него вначале нужное количество столбцов, а затем и строк. Эти задачи
выполняют специально написанные процедуры AddColumns и AddRows. Вот их текст:
private void AddColumns(int n, DataGridView dgw)
{
//добавляет n столбцов в элемент управления dgw
//Заполнение DGView столбцами
DataGridViewColumn column;
for (int i = 0; i < n; i++)
{
column = new DataGridViewTextBoxColumn();
column.DataPropertyName = "Column" + i.ToString();
column.Name = "Column" + i.ToString();
dgw.Columns.Add(column);
}
}
private void AddRows(int m, DataGridView dgw)
{
//добавляет m строк в элемент управления dgw
//Заполнение DGView строками
for (int i = 0; i < m; i++)
{
dgw.Rows.Add();
dgw.Rows[i].HeaderCell.Value
= "row" + i.ToString();
}
}
Приведу краткий комментарий:


Создаются столбцы в коллекции Columns по одному. В цикле по числу столбцов
матрицы, которую должен отображать элемент управления DataGridView, вызывается
метод Add этой коллекции, создающий очередной столбец. Одновременно в этом же
цикле создается и имя столбца (свойство Name), отображаемое в форме. Показана
возможность формирования еще одного имени (DataPropertyName), используемого при
связывании со столбцом таблицы внешнего источника данных. В нашем примере это имя
не используется.
Создав столбцы, нужно создать еще и нужное количество строк у каждого из
элементов DataGridView. Делается это аналогичным образом, вызывая метод Add
коллекции Rows. Чуть по-другому задаются имена строк, - для этого используется
специальный объект HeaderCell, имеющийся у каждой строки и задающий ячейку
заголовка.

После того как сформированы строки и столбцы, элемент DataGridView готов к тому,
чтобы пользователь или программа вводила значения в ячейки сформированной таблицы.
Рассмотрим теперь, как выглядит обработчик события «Click» следующей командной
кнопки «Перенести данные в массив». Предполагается, что пользователь разумен и, прежде чем
нажать эту кнопку, задает значения элементов перемножаемых матриц в соответствующих ячейках
подготовленных таблиц первых двух элементов DataGridView. Обработчик события выполняет
следующие задачи – в цикле читает элементы, записанные пользователем в таблицы DataGridView,
проверяет их корректность и в случае успеха переписывает их в матрицы. Вот текст обработчика:
private void button2_Click(object sender, EventArgs e)
{
string elem = "";
bool correct = true;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
{
try
{
elem=dataGridView1.Rows[i].Cells[j].Value.ToString();
A[i, j] = Convert.ToDouble(elem);
label8.Text = "";
}
catch (Exception any)
{
label8.Text = "Значение элемента" +
"A[" + i.ToString() +", " + j.ToString() +
" ]"
+ " не корректно. Повторите
ввод!";
dataGridView1.Rows[i].Cells[j].Selected= true;
return;
}
}
for (int i = 0; i < n; i++)
for (int j = 0; j < p; j++)
{
do
{
correct = true;
try
{
elem =
dataGridView2.Rows[i].Cells[j].Value.ToString();
B[i, j] = Convert.ToDouble(elem);
label9.Text = "";
}
catch (Exception any)
{
label9.Text = "Значение элемента" +
"B[" + i.ToString() + ", " + j.ToString() +
"]"
+ " не корректно. Повторите ввод!";
dataGridView2.Rows[i].Cells[j].Selected=true;
Form3 frm = new Form3();
frm.label1.Text =
"B[" + i.ToString() + "," + j.ToString() +
"]= ";
frm.ShowDialog();
dataGridView2.Rows[i].Cells[j].Value =
frm.textBox1.Text;
correct = false;
}
} while (!correct);
}
}
Этот программный код нуждается в подробных комментариях:



Основная задача переноса данных из таблицы элемента DataGridView в
соответствующий массив не вызывает проблем. Конструкция Rows[i].Cells[j] позволяет
добраться до нужного элемента таблицы, после чего остается присвоить его значение
элементу массива.
Как всегда при вводе основной проблемой является обеспечение корректности вводимых
данных. Схема, рассматриваемая нами ранее, нуждается в корректировке. Дело в том, что
ранее проверка корректности осуществлялась сразу же после ввода пользователем
значения элемента. Теперь проверка корректности выполняется, после того как
пользователь полностью заполнил таблицы, при этом некоторые элементы он мог задать
некорректно. Просматривая таблицу, необходимо обнаружить некорректно заданные
значения и предоставить возможность их исправления. В программе предлагаются два
различных подхода к решению этой проблемы.
Первый подход демонстрируется на примере ввода элементов матрицы A. Как обычно,
преобразование данных, введенных пользователем, в значение, допустимое для
элементов матрицы А, помещается в охраняемый блок. Если данные некорректны и
возникает исключительная ситуация, то она перехватывается универсальным
обработчиком catch(Exception). Заметьте, в данном варианте нет цикла, работающего до
тех пор, пока не будет введено корректное значение. Обработчик исключения просто
прерывает работу по переносу данных, вызывая оператор return. Но предварительно он
формирует информационное сообщение об ошибке и выводит его в форму. (Помните,
специально для этих целей у формы были заготовлены две метки). В сообщении
пользователю предлагается исправить некорректно заданный элемент и повторить ввод –
повторно нажать командную кнопку «перенести данные в массив». Этот подход понятен
и легко реализуем. Недостатком является его неэффективность, поскольку повторно
будут переноситься в массив все элементы, в том числе и те, что были введены вполне
корректно. У программиста такая ситуация может вызывать чувство
неудовлетворенности своей работой.

На примере ввода элементов матрицы В продемонстрируем другой подход, когда
исправляется только некорректно заданное значение. Прежде, чем читать дальше,
попробуйте найти собственное решение этой задачи. Это оказывается не так просто, как
может показаться с первого взгляда. Для организации диалога с пользователем пришлось
организовать специальное диалоговое окно, представляющее обычную форму с двумя
элементами управления – меткой для выдачи информационного сообщения и текстовым
окном для ввода пользователем корректного значения. При обнаружении ошибки ввода
открывается диалоговое окно, в которое пользователь вводит корректное значение
элемента и закрывает окно диалога. Введенное пользователем значение переносится в
нужную ячейку таблицы DataGridView, а оттуда в матрицу.

При проектировании диалогового окна значение свойства формы FormBorderStyle,
установленное по умолчанию как «sizeable» следует заменить значением «FixedDialog»,
что влияет на внешний вид и поведение формы. Важно отметить, что форма,
представляющее диалоговое окно, должна вызываться не методом Show, а методом
ShowDialog. Иначе произойдет зацикливание, начнут порождаться десятки диалоговых
окон, прежде чем успеете нажать спасительную в таких случаях комбинацию Ctrl+ Alt +
Del.
Приведем снимки экранов, демонстрирующие ситуации, в которых пользователь ввел
некорректные значения. На рис.6 показано информационное сообщение, появляющееся
при обнаружении некорректного ввода значений элементов матрицы А.
Рис. 6 Информационное сообщение о некорректных значенях матрицы А
Напоминаю, что после появления подобного сообщения пользователь должен исправить
некорректное значение (одно или несколько) и повторить процесс переноса данных.
На рис. 7 показана ситуация, когда некорректно заданное значение исправляется в
открывшемся окне диалога.
Рис.7 Диалоговое окно для корректировки значений элементов матрицы В
Обработчик события «Click» командной кнопки «Умножить матрицы» выполняет
ответственные задачи – реализует умножение матриц и отображает полученный результат в
таблице соответствующего элемента DataGridView. Но оба эти действия выполняются
естественным образом, не требуя кроме циклов никаких специальных средств и программистских
ухищрений. Я приведу программный код без дополнительных комментариев:
private void button3_Click(object sender, EventArgs e)
{
MultMatr(A, B, C);
FillDG();
}
void MultMatr(double[,] A, double[,] B, double[,] C)
{
int m = A.GetLength(0);
int n = A.GetLength(1);
int p = B.GetLength(1);
double S =0;
for(int i=0; i < m; i++)
for (int j = 0; j < p; j++)
{
S = 0;
for (int k = 0; k < n; k++)
S += A[i, k] * B[k, j];
C[i, j] = S;
}
}
void FillDG()
{
for (int i = 0; i < m; i++)
for (int j = 0; j < p; j++)
dataGridView3.Rows[i].Cells[j].Value
= C[i, j].ToString();
}
Задачи (ввод, вывод и другие простые задачи с массивами)
1.250 Организуйте в консольном приложении ввод и вывод одномерного
массива строкового типа.
1.251 Организуйте в Windows приложении ввод и вывод одномерного
массива строкового типа.
1.252 Организуйте в консольном приложении ввод массива «Сотрудники»,
содержащего фамилии сотрудников. Введите массив «Заявка»,
элементы которого содержат фамилии сотрудников и, следовательно,
должны содержаться в массиве сотрудников. Обеспечьте контроль
корректности ввода данных.
1.253 Организуйте в Windows приложении ввод массива «Сотрудники»,
содержащего фамилии сотрудников. Введите массив «Заявка»,
элементы которого содержат фамилии сотрудников и, следовательно,
должны содержаться в массиве сотрудников. Обеспечьте контроль
корректности ввода данных.
1.254 Организуйте в Windows приложении ввод массива «Сотрудники»,
содержащего фамилии сотрудников. Создайте массив «Заявка»,
элементы которого должны содержаться в массиве сотрудников. Для
создания массива «Заявка» постройте форму «Два списка»,
содержащую два элемента ListBox, источником данных для первого из
них служит массив «Сотрудники». Пользователь переносит данные из
первого списка во второй, формируя данные для массива «Заявка».
После формирования данные переносятся в массив. Для построения
формы используйте шаблон, описанный в лекции 24 учебника.
1.255 Организуйте в консольном приложении ввод и вывод двумерного
массива строкового типа.
1.256 Организуйте в Windows приложении ввод и вывод двумерного массива
строкового типа.
1.257 Организуйте в консольном приложении ввод массива «Сотрудники» из
двух столбцов, содержащего фамилии и имена сотрудников. Введите
массив «Заявка» той же структуры, элементы которого должны
содержаться в массиве сотрудников. Обеспечьте контроль
корректности ввода данных. Организуйте вывод обоих массивов.
1.258 Организуйте в Windows приложении ввод массива «Сотрудники» из
двух столбцов, содержащего фамилии и имена сотрудников. Введите
массив «Заявка» той же структуры, элементы которого должны
содержаться в массиве сотрудников. Обеспечьте контроль
корректности ввода данных. Организуйте вывод обоих массивов.
1.259 (*) Организуйте в консольном приложении ввод и вывод массива
«Машины», содержащего 4 столбца: «Владелец», «Марка», «Номер»,
«Год Выпуска». При вводе данных обеспечьте их корректность. Поле
«Владелец» должно быть строкой в формате «фамилия имя», где
фамилия и имя должны начинаться с большой буквы и состоять из
букв алфавита кириллицы, включая дефис. Номер машины должен
соответствовать формату, принятому для номеров машин. При выводе
сохраняйте структуру массива.
1.260 (*) Организуйте в Windows приложении ввод и вывод массива
«Машины», содержащего 4 столбца: «Владелец», «Марка», «Номер»,
«Год Выпуска». При вводе данных обеспечьте их корректность. Поле
«Владелец» должно быть строкой в формате «фамилия имя», где
фамилия и имя должны начинаться с большой буквы и состоять из
букв алфавита кириллицы, включая дефис. Номер машины должен
соответствовать формату, принятому для номеров машин. При выводе
сохраняйте структуру массива.
1.261 (*) В консольном приложении уже построен массив «Машины» (см.
задача 3.9) . Построить массив «Цветные машины», в котором к
столбцам массива «Машины» добавляется 5-й столбец «Цвет».
Организуйте диалог с пользователем, выясняя цвет для каждой
машины из массива «Машины».
1.262
(*) В Windows приложении уже построен массив «Машины»
(см. задача 3.10) . Построить массив «Цветные машины», в котором к
столбцам массива «Машины» добавляется 5-й столбец «Цвет».
Организуйте диалог с пользователем, выясняя цвет для каждой
машины из массива «Машины».
1.263
Организуйте в консольном приложении ввод и вывод
одномерного массива арифметического типа (от byte до double).
1.264 Организуйте в Windows приложении ввод и вывод одномерного
массива арифметического типа (от byte до double).
1.265 Организуйте в консольном приложении ввод массива «Сотрудники»,
содержащего фамилии сотрудников, и массива «Зарплата». Обеспечьте
контроль корректности ввода данных о зарплате, проверяя диапазон
возможных значений.
1.266 Организуйте в Windows приложении ввод массива «Сотрудники»,
содержащего фамилии сотрудников, и массива «Зарплата». Обеспечьте
контроль корректности ввода данных о зарплате, проверяя диапазон
возможных значений.
1.267 Организуйте в консольном приложении ввод и вывод матрицы двумерного массива арифметического типа.
1.268 Организуйте в Windows приложении ввод и вывод матрицы двумерного массива арифметического типа.
1.269 Организуйте в консольном приложении ввод массива декартовых
координат n точек на плоскости. Вычислите массив полярных
координат этих точек и организуйте вывод этого массива. Обеспечьте
контроль вводимых значений.
1.270 Организуйте в Windows приложении ввод массива декартовых
координат n точек на плоскости. Вычислите массив полярных
координат этих точек и организуйте вывод этого массива. Обеспечьте
контроль вводимых значений.
1.271 Организуйте в консольном приложении ввод массива полярных
координат n точек на плоскости. Вычислите массив декартовых
координат этих точек и организуйте вывод этого массива. Обеспечьте
контроль вводимых значений.
1.272 Организуйте в Windows приложении ввод массива полярных
координат n точек на плоскости. Вычислите массив декартовых
координат этих точек и организуйте вывод этого массива. Обеспечьте
контроль вводимых значений.
1.273 Организуйте в консольном приложении ввод массива декартовых
координат n точек в трехмерном пространстве. Вычислите массив
полярных координат этих точек и организуйте вывод этого массива.
Обеспечьте контроль вводимых значений.
1.274 Организуйте в Windows приложении ввод массива декартовых
координат n точек в трехмерном пространстве. Вычислите массив
полярных координат этих точек и организуйте вывод этого массива.
Обеспечьте контроль вводимых значений.
1.275 Организуйте в консольном приложении ввод и вывод массива
декартовых координат n точек на плоскости. Вычислите массив,
содержащий все комбинации из трех точек исходного массива такие,
что точки могут рассматриваться как вершины некоторого
треугольника на плоскости. Организуйте вывод этого массива.
1.276 Организуйте в Windows приложении ввод и вывод массива декартовых
координат n точек на плоскости. Вычислите массив, содержащий все
комбинации из трех точек исходного массива такие, что точки могут
рассматриваться как вершины некоторого треугольника на плоскости.
Организуйте вывод этого массива.
Массивы и классические алгоритмы математики
Полиномы
Полиномом n-й степени Pn(x) называют функцию:
Pn ( x)  an x n  an 1 x n 1 
 a1 x  a0 (1)
Если рассматривать график этой функции на плоскости, то x и Pn(x) – это декартовы
координаты точек графика функции. Значения ak (k из интервала [0,n]) называются
коэффициентами полинома. Все они принадлежат одному типу и при программной работе с
полиномами представляются одномерным массивом с n+1 элементами.
Если задан массив коэффициентов полинома A, то вычислить значение полинома в точке x не
представляет особой сложности. Но ни один уважающий себя программист не позволит себе
вычислять значение полинома, буквально пользуясь схемой (1), требующей n-1 операций
возведения в степень, n операций умножения и n операций сложения. Прекрасный пример того, как
можно упростить алгоритм, дает схема Горнера, вообще не требующая возведения в степень. В этой
схеме полином Pn(x) представляют в виде:
Pn ( x)  ( (an ) x  an1 ) x 
 a1 ) x  a0
(2)
Удобнее представлять схему Горнера в рекуррентной форме:
P0  an ;
Pk  Pk 1 x  an k ;
k 1
n
Вначале вычисляется значение полинома нулевой степени, состоящего из коэффициента при
старшем члене исходного полинома. Затем рекуррентно повышается степень полинома, для чего
достаточно умножить на x предыдущее значение и добавить новый коэффициент. В программе эта
схема естественным образом реализуется обычным циклом, где на каждом шаге выполняется одно
умножение и одно сложение.
Если Pn(x) – полином n-й степени с коэффициентами ai, Qn(x) – полином n-й степени с
коэффициентами bi и Pn(x) = Qn(x), то из этого следует равенство соответствующих коэффициентов:
( Pn ( x)  Qn ( x))  (ai  bi ; i  0
n)
(3)
Многие задачи над полиномами связаны с определением их корней. Напомню, x0 является
корнем полинома, если Pn(x0) = 0. У полинома n-й степени не более чем n действительных корней.
Если n – нечетно, то полином имеет хотя бы один действительный корень. Все корни полинома
принадлежат некоторому конечному интервалу [c, d]. Вне этого интервала поведение полинома
определяется его старшим членом – anxn. Для полинома четной степени обе ветви уходят в +∞, если
an >0 и в -∞, если an<0. Для полинома нечетной степени ветви полинома вне интервала [c, d]
разнонаправлены. Если an>0, то правая ветвь уходит в +∞, а левая ветвь - в -∞. Если an<0, то левая
ветвь уходит в +∞, а правая ветвь в -∞.
Когда по каким-либо физическим соображениям интервал [c, d] известен хотя бы
приблизительно, то задача нахождения корней полинома облегчается, в противном случае она
может быть довольно трудной, особенно для случая близко расположенных корней.
Исследование интервала
Рассмотрим один из простых алгоритмов, исследующих существует ли на заданном интервале
[e, f] хотя бы один корень. Один корень заведомо существует, если полином на концах
исследуемого интервала имеет разные знаки или один из концов интервала уже является корнем
полинома. Это условие и будет характерным признаком поиска нужного интервала. Если исходный
интервал [e, f] удовлетворяет характерному признаку, то задача решена и такой интервал найден. В
противном случае в цикле по k вычислим h = L/2k, где L – длина исходного интервала (L= f-e).
Затем организуем внутренний цикл, в котором проверим характерный признак на всех интервалах
длины h. Если интервал будет найден, то вычисления завершаются, в противном случае переходим
к следующему шагу цикла по k, производя очередное дробление h. Завершение цикла по k означает,
что если исследуемый интервал [e, f] и содержит корни, то это близкие пары корней, отстоящие
друг от друга на расстояние, меньшее h – заключительной длины интервала по завершении цикла
по k.
Приведу несколько практических рекомендаций, полезных при реализации этой схемы.
Внутренний цикл следует организовать так, чтобы не повторять вычисление полинома в тех точках,
в которых это вычисление проводилось на предыдущих шагах цикла. Это означает, что когда шаг h
= L/2k, то во внутреннем цикле достаточно вычислить значение полинома не более чем в 2k-1 точках.
Внешний цикл достаточно ограничить числом в интервале от 10 до 20, поскольку уже при k=10
величина исходного интервала L уменьшится более чем в 1000 раз, что вполне достаточно в
большинстве практических ситуаций. Хотя следует помнить, что в ряде ситуаций практики
приходится иметь дело с резко осциллирующими функциями, где близкие корни являются
правилом, а не исключением.
Алгоритмы нахождения корня полинома
Рассмотрим несколько простых схем нахождения корня полинома. Заметим, что все эти схемы
применимы к нахождению корней любых функций, а не только полиномов. Как всегда в
программировании речь идет не столько о точном нахождении корня, сколько о нахождении корня
с заданной точностью ε. Так что, если x0 – это точное значение корня, то нам достаточно найти x* такое что |x0 – x*| < ε.
Схема дихотомии отрезка (деление пополам):
Эта схема прекрасно подходит, когда предварительно проведено исследование интервала
существования корня и найден такой интервал [e, f], на концах которого полином принимает
разные знаки, так что существует корень внутри интервала. Если исходный интервал мал и сравним
с заданной точностью ε, то в качестве корня можно выбрать середину этого интервала. Если же
исходный интервал больше, чем значение ε, то интервал можно разделить пополам и из двух
половинок выбрать ту, для которой выполняется характерный признак существования корня.
Понятно, что если признак выполняется для всего интервала, то он обязательно будет выполняться
для одной из его половинок. Деление отрезка пополам приводит к быстрому уменьшению длины
отрезка, так что 10 - 20 делений достаточно, чтобы найти интервал длины, меньшей ε, а
следовательно и корень полинома с заданной точностью.
Метод простой итерации:
Формально метод применим и в том случае, когда неизвестен интервал, в котором существует
корень функции. Пусть x0 – некоторое заданное начальное приближение к корню полинома. Тогда
можно построить следующий итерационный процесс:
xk  xk 1  f ( xk 1 ); k  1, 2,
Метод записан для произвольной функции f, в нашем случае функция f задана полиномом.
Итерационный процесс следует прекращать либо по достижении заданной точности, либо по
достижении максимально допустимого числа итераций N. Заметьте, следует задавать оба условия,
поскольку сходимость процесса простой итерации к корню, даже если он существует, не
гарантируется. Во многом все зависит от удачного выбора начального приближения.
Метод простой итерации обладает полезным свойством «неподвижной точки». Корни
функции являются «неподвижными точками» метода. Нетрудно заметить, что если на некотором
шаге xk-1 сошлось к корню полинома x*, то xk и все последующие итерации будут равны x*, так что
итерационный процесс из найденного корня не уходит.
Метод Ньютона
Этот метод чуть более сложен в реализации, но обладает лучшей сходимостью в сравнении с
методом простой итерации, хотя и здесь сходимость во многом зависит от удачного выбора
начального приближения x0. Для произвольной функции f итерационный процесс метода Ньютона
выглядит так:
xk  xk 1 
f ( xk 1 )
; k  1, 2,
f ( xk 1 )
Понятно, что производной от полинома n-й степени будет полином степени n-1,
коэффициенты которого легко вычисляются по n и коэффициентам исходного полинома. Все, что
было сказано о методе простой итерации – завершение процесса, обладание неподвижной точкой,
справедливо и для метода Ньютона.
Понижение степени полинома
Если для полинома P(x) n-й степени найден корень x1, то можно понизить степень полинома,
построив полином P1(x) степени n-1, у которого все корни совпадают с корнями полинома P(x) за
исключением того, что у него нет корня x1.
Запишем соотношение, связывающее полиномы:
P( x)  P1( x)( x  x1)
an x n  an1 x n1 
 a1 x  a0  (bn1 x n1  bn2 x n2 
 b1 x  b0 )( x  x1)
Учитывая соотношение (3) о равенстве двух полиномов одной степени, можно выписать n+1
соотношение, связывающее коэффициенты этих полиномов. Эти соотношения нетрудно разрешить
относительно неизвестных коэффициентов bk. В результате получим:
bn1  an ;
bn2  an1  bn1 x1 ;
(4)
b0  a1  b1 x1 ;
Заметьте, неизвестных всего n, а уравнений можно построить – n+1. Но последнее уравнение
(a0 + b0x1 = 0) является следствием предыдущих и используется для контроля вычислений.
К новому полиному можно применить тот же процесс – найти его корень и понизить затем
степень полинома. Реально понижение степени не намного упрощает задачу отыскания корней, так
что чаще всего проще искать корни исходного полинома, изменяя начальные приближения в
итерационном процессе или отыскивая различные интервалы, на которых полином меняет свой
знак.
Нахождение коэффициентов полинома по его корням
До сих пор рассматривалась задача отыскания корней полинома с заданными
коэффициентами. Иногда приходится решать обратную задачу – найти коэффициенты полинома,
если известны его корни – x1, x2, … xn. Полиномов с одинаковыми корнями существует
бесчисленное множество. Однако среди них существует единственный полином с коэффициентом
an, равным единице. Этот полином называется приведенным, его то и будем строить. Все остальные
полиномы получаются из приведенного полинома умножением всех коэффициентов на
произвольное число an, от которого требуется лишь, чтобы оно не было равно нулю. Поэтому для
однозначного решения задачи требуется задать n корней и коэффициент при старшем члене
полинома. Тогда можно записать следующее равенство:
Pn ( x)  an ( x  xn )( x  xn1 )
( x  x1 )
Для нахождения коэффициентов полинома Pn(x) воспользуемся, как обычно соотношением
(3). Но применить его напрямую сложно. Поэтому воспользуемся процессом, обратным к процессу
понижения степени. Построим вначале P1(x) - полином первой степени, у которого x1 является
единственным корнем. Затем повысим степень и построим полином второй степени – P2(x), у
которого появляется еще один корень – x2. Продолжая этот процесс, дойдем до искомого полинома
Pn(x). При вычислении коэффициентов нового полинома будем использовать коэффициенты уже
посчитанного полинома на единицу меньшей степени. Получающееся в результате соотношения
близки к тому, что приведены для случая понижения степени полинома.
Коэффициенты полинома первой степени P1(x) выписываются явно:
a1  1; a0   x1;
Коэффициенты полинома k-й степени вычисляются через коэффициенты полинома степени k1:
Pk ( x)  Pk 1 ( x)( x  xk )
Переходя к коэффициентам, получим следующие уравнения:
ak  ak 1 ;
ak i  ak i 1  ak i xk ; i i  1,
a0  a0 xk ;
k  1;
(5)
В соотношении (5) через a' обозначены коэффициенты полинома степени k-1. На самом деле
схема безопасна и позволяет считать коэффициенты на том же месте, не требуя дополнительной
памяти. Приведу алгоритм вычисления коэффициентов полинома по его корням в виде схемы,
приближенной к языку C#.
Дано:
an – коэффициент при старшем члене полинома Pn(x);
n – степень полинома;
x – массив корней полинома (x[0], x[1], …x[n]);
Вычислить:
Массив a – массив коэффициентов полинома (a[0], a[1], …a[n]).
//Вычисляем коэффициенты полинома первой степени
a[1]= 1; a[0] = -x[0];
//цикл по числу полиномов
for(int k=2;k<=n; k++)
{
//Вычисляем коэффициенты полинома степени k
//Вначале старший коэффициент
a[k]= a[k-1];
//затем остальные коэффициенты, кроме последнего
for(int i=k-1;i>0; i--)
{
a[i] = a[i-1]- a[i]*x[k-1];
}
//теперь младший коэффициент
a[0]= -a[0]*x[k-1];
}
//Последний этап – умножение коэффициентов на an
for(int i=0; i<=n; i++)
a[i] = a[i]*an;
Полином Лагранжа
Пусть на плоскости заданы n+1 точка: R0(x0, y0), R1(x1, y1), R2(x2, y2),…, Rn(xn, yn). Полиномом
Лагранжа PL(x) называется полином n-й степени, проходящий через все точки Rk. Если точки Rk не
образуют возвратов, то такой полином существует и является единственным. Под возвратом
понимается ситуация, когда существуют две точки Ri и Rj такие, что xi = xj.
Как построить такой полином? Лагранж предложил следующий алгоритм. Полином PL(x)
строится как сумма n+1 полиномов n-й степени:
n 1
PL ( x)   Pk ( x)
k 1
Каждый из полиномов Pk(x), входящих в сумму, строится следующим образом. Корнями
полинома Pk(x) являются все точки Ri за исключением точки Rk. Единственность Pk(x)
обеспечивается за счет того, что коэффициент при старшем члене an подбирается так, чтобы
полином проходил через точку Rk. В записи Лагранжа полином Pk(x) выглядит следующим
образом:
( x  x0 )( x  x1 ) ( x  xk 1 )( x  xk 1 ) ( x  xn )
( xk  x0 )( xk  x1 ) ( xk  xk 1 )( xk  xk 1 ) ( xk  xn )
Pk ( x)  yk
(6)
В записи (6) в числителе находится приведенный полином, построенный по корням, а yk,
деленное на знаменатель в формуле (6), задает an – старший коэффициент полинома.
Условия, накладываемые на полиномы Pk(x), обеспечивают выполнение требований к
полиному Лагранжа – сумма полиномов Pk(x) будет полиномом, проходящим через все заданные
точки.
Поскольку алгоритм построения приведенного полинома по его корням уже разобран, то
схема построения полинома Лагранжа можно выглядеть так:
//Полином Лагранжа определяется как сумма из n+1
//полиномов Pk, для которых известны корни.
for(int k=0; k<=n; k++)
{
//Задание корней для полинома Pk
for(int i =0; i<k; i++)
roots[i] = X[i];
for(int i =k+1; i<=n; i++)
roots[i-1] = X[i];
//Вычисление коэффициентов приведенного полинома по его
корням
coefk = CalcCoefFromRoots(roots);
//вычисление An - старшего коэффициента полинома.
An = Y[k] / HornerP(coefk,X[k]);
//Добавление очередного полинома Pk к PL - сумме полиномов
for(int i =0; i<=n; i++)
{
coefL[i]= coefL[i]+An*coefk[i];
}
}
В этой схеме:
X и Y – массивы, задающие декартовы координаты точек, через которые проходит полином
Лагранжа,
n – степень полинома,
roots – массив корней приведенного полинома Pk,
coefk - массив его коэффициентов,
An – старший коэффициент полинома, вычисляемый из условия прохождения полинома Pk
через точку с координатами X[k], Y[k],
coefL - массив коэффициентов полинома Лагранжа,
HornerP – метод, вычисляющий по схеме Горнера значение полинома по его коэффициентам и
значению координаты x,
CalcCoefFromRoots – метод, вычисляющий массив коэффициентов приведенного полинома
по его корням.
Сложение и умножение полиномов
При рассмотрении полинома Лагранжа возникала необходимость в нахождении суммы
полиномов одинаковой степени, заданных своими коэффициентами. Пусть P(x) и Q(x) – полиномы
степени n и m соответственно, заданные своими коэффициентами, и пусть для определенности n >=
m. Тогда суммой полиномов называется полином R(x) степени n, коэффициенты которого
вычисляются следующим образом:
n
n
m
i 0
i 0
i 0
R( x)  P( x)  Q( x)   ci x i  ai x i   bi x i 
ci  ai  bi
ci  ai
i  m;
i : m  i  n;
Пусть полиномы P(x) и Q(x) заданы, подобно полиному Лагранжа, точками, через которые
они проходят:
P( x) : {( px0 , py0 ), ( px1 , py1),
Q( x) : {(qx0 , qy0 ), (qx1 , qy1),
( pxn , pyn )}
(qxm , qym )}
Тогда нетрудно найти подобное представление и для полинома R(x), представляющего сумму
полиномов:
R( x) : {(rx0 , ry0 ), (rx1 , ry1),
(rxn , ryn )}, где
rxi  pxi ; ryi  pyi  Q( pxi );
В этом случае понадобится вычислить значения полинома Q(x) в n точках.
Если полиномы P(x) и Q(x) заданы своими корнями, то определить корни полинома суммы не
удается, более того у суммы вообще может не быть корней. В этом случае для каждого полинома по
корням можно вычислить коэффициенты, а затем определить коэффициенты полинома суммы.
Можно также рассматривать корни, как частный случай задания множества точек, через которые
проходит полином и применить предыдущую схему для определения множества точек, через
которые проходит полином суммы.
Рассмотрим теперь операцию умножения полиномов:
S ( x)  P ( x) * Q( x)
Нетрудно понять, что полином S(x) является полиномом степени n+m и имеет n+m+1
коэффициент. Как вычисляется произведение, если заданы полиномы сомножители P(x) и Q(x)?
Замечу, что произведение полиномов часто встречается на практике и имеет специальное имя –
свертка полиномов.
В отличие от сложения полиномов проще всего найти свертку, если заданы корни обоих
полиномов. В этом случае никаких вычислений не требуется, поскольку n корней P(x) и m корней
Q(x) будут n+m корнями S(x). Если у полиномов P(x) и Q(x) есть совпадающие корни, то у S(x)
появятся кратные корни.
Если исходные полиномы P(x) и Q(x) заданы своими точками, то нетрудно получить набор
точек для полинома произведения. Схема во многом похожа на ту, что имеет место при сложении
полиномов, заданных точками:
S ( x)  {( sx0 , sy0 ), ( sx1 , sy1 ),
( sxn  m , syn  m )}, где
sxi  pxi ; syi  pyi * Q( pxi ) i : i  n;
sxi  qxi n 1 ; syi  qyi n 1 * P(qxi n 1 ) i : n  i  n  m  1
Для получения множества точек, задающих представление полинома S(x), приходится
вычислять значение полинома Q(x) в n точках и значение полинома P(x) в m точках, а затем
выполнять соответствующее умножение значений двух полиномов.
Если исходные полиномы P(x) и Q(x) заданы своими коэффициентами, то имеем:
n
m
i 0
j 0
S ( x)  P( x)* Q( x)  ( ai xi )*( b j x j )
Каждый член первой суммы приходится умножать на все члены второй суммы и затем
приводить подобные члены при одинаковых степенях x. Нетрудно заметить, что в результате
коэффициенты полинома S(x) определяются следующими соотношениями:
nm
S ( x)   di ; где
i 0
di 
ab;
k  r i
k r
k  [0, n]; r  [0, m]
Суммирование идет по всем наборам k и r, дающим в сумме значение i. Понятно, что для
крайних значений (i=0 и i=n+m) сумма состоит из одного члена, поскольку подобные члены для x в
нулевой степени и степени n+m отсутствуют. Число членов суммирования увеличивается при
приближении к середине интервала [0, n+m].
Итоги
Подводя некоторые итоги, отметим, что полином можно задать тремя разными способами –
его коэффициентами, корнями, точками, через которые проходит полином. Если заданы
коэффициенты полинома, то за время, пропорциональное n2, (T(n) = O(n2)) можно вычислить
значения полинома в n+1 точках. Для вычисления значения полинома в одной точке применяется
схема Горнера, выполняющая вычисления за линейное (пропорциональное n) время. Существует и
обратное преобразование. Если заданы n+1 точки, через которые проходит полином, то алгоритм
Лагранжа позволяет за время O(n2) вычислить коэффициенты полинома. Задача получения
коэффициентов полинома по точкам называется задачей интерполяции, а полином Лагранжа
называется интерполяционным полиномом.
Если заданы корни, то можно получить два других представления. Рассмотренный нами
алгоритм позволяет по корням полинома за время O(n2) вычислить коэффициенты полинома.
Алгоритм использует итеративную схему из n шагов, где на каждом шаге выполняется операция
повышения степени, выполняемая за линейное время. Поскольку корни являются частным случаем
задания множества точек, через которые проходит полином, то задание корней автоматически
задает и представление полинома набором точек. Обратная задача – получение корней по
коэффициентам или заданным точкам – так просто не решается. Точное ее решение существует для
полиномов второй и третьей степени, но не в общем случае. Для нахождения корней приходится
использовать приближенные итеративные методы, например метод простой итерации или Ньютона.
Задание полинома его корнями является наиболее информативным. Если известны корни, то
без труда выполняется свертка полиномов. Вычисление значения полинома в заданной точке,
выполняется за n умножений, не требуя применения схемы Горнера. Несколько сложнее
выполняется операция сложения полиномов. К сожалению, на практике редко встречается
ситуация, когда известны корни полинома, но такое бывает – алгоритм Лагранжа тому пример.
Когда полиномы заданы своими коэффициентами, то вычисление значения полинома в
заданной точке выполняется по схеме Горнера за линейное время. Сложение полиномов также
является легкой операцией и выполняется за линейное время. Свертку полиномов в этом случае
выполнить сложнее. Рассмотренный нами алгоритм требует уже квадратичного времени.
На практике полиномы чаще всего появляются при задании множества точек. Ситуация
обычна такова. В результате экспериментов измеряются значения некоторой функции в ряде точек.
Требуется предсказать, каково будет значение этой функции в других точках, в которых измерения
не проводились. Если из теоретических соображений не известен вид функции, то чаще всего ее
задают в виде полинома, проходящего через точки, полученные экспериментальным путем. В этой
постановке задачу построения полинома и вычисления значений полинома в точках, не
подлежащих измерениям, называют задачей интерполяции, а полином Лагранжа называют
интерполяционным полиномом. Задача интерполяции корректно решается, когда новые точки, в
которых проводятся вычисления, лежат внутри интервала, заданного измеренными точками.
Полиномы плохо приспособлены для решения задачи экстраполяции, когда точки лежат вне
интервала измерений, из-за быстрого роста значения полинома вне этого интервала. Чем выше
степень полинома, тем быстрее его рост.
Множество точек, через которые проходит полином, обычно несет дополнительную
информацию. Некоторые точки, например, могут быть корнями полинома, или задавать интервалы,
внутри которых находятся корни.
Одно замечание к задаче свертки полиномов. Приведенный алгоритм решения этой задачи для
полиномов, заданных своими коэффициентами, требует квадратичного времени. Ввиду
практической важности этой задачи много внимания уделялось поиску наиболее эффективного по
временной сложности алгоритма. Существуют алгоритмы, решающие эту задачу за время
O(n*log(n)). Эти алгоритмы используют технику быстрого преобразования Фурье и обратного к
нему. Они сложнее в реализации и требуют работы с комплексными числами или выполнения
операций модульной арифметики. Здесь они только упоминаются и детально не рассматриваются.
Задачи
В задачах этого раздела уже не говорится о том, какого типа проект следует строить –
консольный или Windows. Предполагается, что обычной практикой является построение Windows
приложений.
1.277 Полином P(x) задан своими коэффициентами. Дан массив координат
X. Вычислить, используя схему Горнера, массив значений полинома в
точках xi.
1.278 Полином P(x) задан своими корнями и старшим коэффициентом an.
Дан массив координат X. Вычислить массив значений полинома в
точках xi.
1.279 (задача интерполяции) Полином P(x) задан координатами n+1 точек,
через которые он проходит. Дан массив координат X. Вычислить
массив значений полинома в точках xi.
1.280 Полином P(x) задан своими корнями и старшим коэффициентом an.
Вычислить коэффициенты полинома.
1.281 (задача построения интерполяционного полинома Лагранжа) Полином
P(x) задан координатами n+1 точек, через которые он проходит.
Вычислить коэффициенты полинома.
1.282 Полином P(x) задан своими коэффициентами. Дан массив чисел X.
Построить полином Q(X), имеющий своими корнями числа из массива
X и корни полинома P(x).
1.283 Полином P(x) задан своими коэффициентами. Для полинома известны
два его корня – x0 и xn. Построить полином Q(x), корни которого
совпадают с корнями полинома P(x) за исключением корней x0 и xn.
1.284 Полиномы P(x) и Q(x) заданы своими корнями и старшими
коэффициентами. Вычислить коэффициенты суммы полиномов P(x) и
Q(x).
1.285 Полиномы P(x) и Q(x) заданы своими корнями и старшими
коэффициентами. Вычислить коэффициенты произведения полиномов
P(x) и Q(x).
1.286 Полиномы P(x) и Q(x) заданы своими коэффициентами. Вычислить
коэффициенты суммы полиномов P(x) и Q(x).
1.287 Полиномы P(x) и Q(x) заданы своими коэффициентами. Вычислить
коэффициенты произведения полиномов P(x) и Q(x).
1.288 Полиномы P(x) и Q(x) заданы точками, через которые они проходят.
Вычислить коэффициенты суммы полиномов P(x) и Q(x).
1.289 Полиномы P(x) и Q(x) заданы точками, через которые они проходят.
Вычислить коэффициенты произведения полиномов P(x) и Q(x).
1.290 Полином P(x) задан своими коэффициентами. Определить интервал,
если он существует, на котором полином имеет хотя бы один корень.
1.291 Полином P(x) задан точками, через которые он проходит. Определить
интервал, если он существует, на котором полином имеет хотя бы один
корень.
1.292 Для полинома P(x), заданного своими коэффициентами, известен
интервал, на котором полином имеет хотя бы один корень. Найти
корень с заданной точностью, используя схему дихотомии.
1.293 Построить интерфейс пользователя, позволяющий ему находить корни
полинома. В основу поиска положить схему исследования интервала и
дихотомии.
1.294 Построить интерфейс пользователя, позволяющий ему находить корни
полинома. В основу поиска положить метод простой итерации.
1.295 Построить интерфейс пользователя, позволяющий ему находить корни
полинома. В основу поиска положить метод Ньютона.
Проект
1.296 Построить проект, включающий построение класса Polinom и
интерфейс пользователя. Методы класса должны реализовать все
алгоритмы, рассмотренные в этом разделе. Интерфейс пользователя
должен позволять пользователю решать основные задачи,
возникающие при работе с полиномами.
Алгоритмы линейной алгебры
Матрицей называется набор чисел, состоящий из m строк и n столбцов. Для программиста
матрица – это двумерный массив. Матрица называется квадратной, если m = n и прямоугольной в
противном случае. Числа m и n определяют размерность матрицы. Над прямоугольными матрицами
определены операции транспонирования, сложения, умножения.
Пусть A – матрица размерности m*n (из m строк и n столбцов) с элементами ai,j.
Транспонированной матрицей B = AT называют матрицу размерности n*m, элементы которой bi,j =
aj,i. В транспонированной матрице строки исходной матрицы становятся столбцами.
A
a1,1 , a1,2
a1,n
a2,1 , a2,2
a2,n
......................
am,1 , am ,2 am ,n
B  AT 
a1,1 , a2,1
am ,1
a1,2 , a2,2
am,2
......................
a1,n , a2,n am ,n
Операция сложения определена над прямоугольными матрицами одинаковой размерности.
Пусть A, B, C – прямоугольные матрицы размерности m*n. Тогда сумма матриц определяется
естественным образом:
a1,1 , a1,2 a1,n
b1,1 , b1,2 b1,n
A
a2,1 , a2,2
a2,n
......................
am,1 , am,2 am,n
B
b2,1 , b2,2
......................
bm,1 , bm,2 bm,n
a1,1  b1,1 , a1,2  b1,2
C  A B 
b2, n
a2,1  b2,1 , a2,2  b2,2
a1,n  b1,n
a2, n  b2, n
................................................
am,1  bm,1 , am,2  bm,2 am,n  bm,n
Операция умножения определена над прямоугольными матрицами, у которых число столбцов
первого сомножителя равно числу строк второго сомножителя. Матрица произведения имеет число
строк, равное числу строк первого сомножителя, и число столбцов, равное числу столбцов второго
сомножителя. Пусть A – матрица размерности m*p, B – размерности p*n, тогда матрица C= A*B
имеет размерность m*n. Элементы матрицы произведения определяются как сумма попарных
произведений элементов строки первого сомножителя на элементы столбца второго сомножителя.
A  ai , j
i  1,
C  A * B  ci ,k
m; j  1,
i  1,
p; B  b j , k
m; k  1,
n;
j  1,
p; k  1,
n;
(7)
p
ci ,k   ai , j * b jk ;
j 1
Умножение всегда определено для прямой и транспонированной матрицы. Если A –
прямоугольная матрица размерности m*n, то всегда определена квадратная матрица B размерности
m*m
B = A*AT = BT = (A*AT)T = (AT)T*AT= A*AT
Результатом такого произведения является симметричная матрица. Квадратная матрица
называется симметричной, если ai,j = aj,i для всех i и j, или, что тоже, если A = AT. Операции
транспонирования, сложения и умножения обладают следующими свойствами:
(AT)T = A;
(A+B)T = AT + BT;
(A*B)T = BT * AT
Квадратные матрицы
Квадратная матрица называется диагональной, если все элементы, кроме диагональных, равны
нулю, то есть ai,j = 0 при i /=j.
Квадратная матрица называется единичной, если все элементы, кроме диагональных, равны
нулю, а диагональные элементы равны единице, то есть ai,j = 0 при i /=j и ai,j = 1 при i = j. Единичная
матрица обозначается обычно буквой E, и она играет роль единицы при умножении матриц,
поскольку для любой квадратной матрицы A и единичной матрицы E той же размерности имеют
место соотношения:
A*E = E*A = A
Для квадратных матриц определена функция над ее элементами, называемая определителем.
Обозначается определитель обычно с помощью одинарных линий вокруг набора чисел, задающих
матрицу:
D( A) 
a1,1 a1,2
a1,n
a2,1 a2,2
a2,n
......................
an ,1 an ,2 an ,n
Функция, задающая определитель, обладает рядом важных свойств:

Определитель диагональной матрицы равен произведению диагональных элементов.
Отсюда следует, что определитель матрицы E равен 1;

Определитель матрицы не меняется при выполнении над матрицей элементарных
преобразований. Под элементарной операцией (преобразованием) понимается
прибавление к любой строке матрицы линейной комбинации других ее строк. В
частности, если к строке матрицы с номером j прибавить строку с номером k (k /= j),
умноженную на некоторое число, то определитель матрицы не изменится;

Если все элементы одной строки матрицы умножить на некоторое число q, то
определитель матрицы изменится в q раз (умножается на q);

Если переставить местами строки j и k, то модуль определителя не изменится, но
изменится знак, если разность |k-j| является нечетным числом;

Определитель произведения матриц равен произведению определителей:
D(A*B) = D(A)*D(B)
Не приводя общего формального определения, рассмотрим ниже алгоритм вычисления
определителя матрицы, основанный на его свойствах.
Если определитель квадратной матрицы A не равен нулю, то существует обратная матрица,
обозначаемая как A-1. Прямая и обратная матрицы связаны соотношением:
A*A-1 = A-1*A = E
Операции транспонирования, умножения и обращения матриц связаны соотношениями:
(AT)-1 = (A-1)T;
(A*B)-1 = B-1*A-1
Множество квадратных матриц одной размерности с определителем, отличным от нуля
образуют группу по умножению. В группе есть единичный элемент, для каждого элемента
существует обратный к нему, и произведение элементов принадлежит группе.
Иногда полезно рассматривать матрицу, состоящую не из элементов, а из клеток, каждая из
которых является матрицей. Все определения операций над матрицами, элементы которых
являются числами, переносятся на матрицы, элементы которых являются клетками. Такое
представление особенно полезно для разреженных матриц, где большинство элементов являются
нулями. Тогда можно представить такую матрицу, как клеточную матрицу, многие клетки которой
равны нулю. Используя знание структуры подобных матриц можно в результате существенно
сократить объем вычислений. Рассмотрим простой пример умножения двух матриц размерности
100*100, каждая из которых задана в виде четырех клеток:
M
A(20* 40) B(20*60)
C (80* 40) D(80*60)
M 2  M * M1 
A* E  B *G
M1 
E (40*70) F (40*30)
G (60*70) H (60*30)
A* F  B * H
C * E  D *G C * F  D * H
В круглых скобках для клеток заданы их размерности. Пусть теперь некоторые клетки
нулевые, например, таковыми являются клетки D, F и G. Тогда матрица M2 имеет вид:
M2
A* E B * H
C*E
0
Для вычисления матрицы M2 необходимо будет найти произведение трех пар матриц, но
значительно меньших размеров, чем исходные матрицы. В целом объем вычислений сократится
более чем в три раза.
Иногда приходится иметь дело с треугольными матрицами, у которых все элементы выше или
ниже диагонали равны нулю. Квадратную матрицу будем называть нижнетреугольной, если все
элементы выше главной диагонали равны нулю, верхнетреугольной, если равны нулю все элементы
ниже главной диагонали.
Системы линейных уравнений
Рассмотрим систему из n линейных уравнений с n неизвестными:
a1,1 x1  a1,2 x2 
 a1,n xn  b1
a2,1 x1  a2,2 x2 
 a2,n xn  b2
...........................................
an ,1 x1  an ,2 x2 
(8)
 an ,n xn  bn
В матричном виде эта система записывается намного элегантнее:
A*x=b
(9)
Здесь вектор неизвестных x рассматривается как столбец – прямоугольная матрица
размерности n*1. Аналогичный вид имеет вектор правых частей b системы уравнений. В матричном
виде условие существования решения системы линейных уравнений (8) и нахождение самого
решения формулируется совсем просто. Для существования решения необходимо и достаточно,
чтобы определитель матрицы A был отличен от нуля. Тогда у матрицы A существует обратная
матрица A-1. Для нахождения решения системы умножим обе части уравнения (9) на A-1. Тогда
получим:
A-1*(A*x) = A-1*b
→ (A-1*A)*x = A-1*b
→ (E)*x = A-1*b → x = A-1*b
(10)
Для нахождения решения системы линейных уравнений, матрица которой имеет
определитель, отличный от нуля, достаточно вычислить обратную матрицу и умножить ее на
вектор правых частей системы уравнений.
Если нужно решить m систем линейных уравнений с одной и той же матрицей A, но с
разными правыми частями, то обратную матрицу достаточно вычислить один раз. В матричном
виде решение m систем линейных уравнений
A*X = B
задается соотношением:
X = A-1*B
Здесь B – прямоугольная матрица размерности n*m, каждый столбец которой представляет
вектор правых частей одной системы уравнений. Соответствующий столбец матрицы X дает
решение этой системы. Что произойдет, если в качестве матрицы B рассмотреть единичную
матрицу? Очевидно, что тогда матрица X будет представлять собой обратную матрицу A-1.
Несмотря на кажущуюся очевидность соотношения A-1 = A-1*E, в нем есть определенный смысл,
который постараюсь сейчас прояснить. Три задачи – вычисление определителя, решение системы
линейных уравнений, нахождение обратной матрицы – имеют одинаковую вычислительную
сложность и требуют, если не применять специальные алгоритмы, выполнения порядка n3 операций
умножения и сложения. Если посмотреть на соотношение (10), то кажется, что решить систему
уравнений несколько сложнее, чем вычислить обратную матрицу, поскольку нужно вначале найти
обратную матрицу, а затем умножить ее на вектор правых частей b. Однако реальный алгоритм,
рассматриваемый ниже, находящий решение системы, вычислительно проще, чем тот же алгоритм,
находящий обратную матрицу. Для такого алгоритма найти обратную матрицу это все равно, что
решить n систем линейных уравнений с одной и той же матрицей A в левой части, используя
матрицу E в качестве правых частей.
Алгоритм Гаусса
Рассмотрим сейчас алгоритм Гаусса, позволяющий найти решение всех интересующих нас
задач – вычислить определитель матрицы, решить m систем линейных уравнений, найти обратную
матрицу. Построим вначале расширенную матрицу AB, состоящую из двух клеток:
AB = ||A B||
Матрица B, дополняющая матрицу A, зависит от того, какую задачу предполагается решить.
Если нужно вычислить только определитель матрицы A, то расширенная матрица совпадает с
исходной и матрица B в этом случае отсутствует. Если нужно решить одну систему линейных
уравнений, то матрица B состоит из одного столбца - правых частей системы уравнений. Если
нужно решить m систем уравнений, то матрица B состоит из m векторов, каждый из которых задает
правые части своей системы уравнений. Если нужно найти обратную матрицу, то матрица B
задается единичной матрицей E.
После того, как построена расширенная матрица, вся специфика конкретной задачи теряется, над расширенной матрицей выполняются одни и те же действия с параллельным вычислением
определителя матрицы A. В чем суть этих действий? Над матрицей AB последовательно
выполняются элементарные преобразования – деление элементов строки на число, что изменяет
величину определителя, и вычитание из одной строки матрицы другой строки, умноженной на
некоторое число. Цель наших действий состоит в том, чтобы в расширенной матрице AB клетку A
преобразовать в единичную матрицу E. Поскольку каждое элементарное действие можно
рассматривать, как умножение слева на некоторую матрицу, то совокупность преобразований,
переводящая A в E, эквивалентна умножению слева на матрицу A-1. Но это означает также, что эти
преобразования переводят клетку B в матрицу A-1*B, что и дает решение исходных задач.
Поскольку в результате преобразования A переходит в единичную матрицу, определитель которой
известен и равен 1, а для каждого преобразования известно как меняется величина определителя, то
параллельно вычисляется и величина определителя исходной матрицы A.
Рассмотрим на простом примере матричный вид элементарных операций. Пусть элементарная
операция состоит в том, что к первой строке прибавляется вторая строка, умноженная на число q.
Это действие эквивалентно умножению почти единичной матрицы на исходную матрицу:
a1,n
a1,1  qa2,1 a1,2  qa2,2
a1, n  qa2, n
1 q 0 0 0 a1,1 a1,2
a2,n
a2,1
a2,2
a2, n
0 1 0 0 0 a2,1 a2,2


.........................
............................
.........................................................
0 0 0 0 1 an ,1 an,2
an,n
an ,1
an ,2
an ,n
Матрица, задающая элементарную операцию, отличается от единичной матрицы тем, что у
нее в первой строке на втором месте стоит число q, а не ноль. Если бы к первой строке
прибавлялась не вторая строка, а строка с номером j, то число q стояло бы не на втором месте, а в
позиции j. Если строка j прибавляется не к первой строке, а к строке с номером i, то число q
появлялось бы в i-ой строке матрицы.
Рассмотрим теперь возможную реализацию алгоритма Гаусса:
public void Gauss(double[,] M)
{
det = 1;
int n = M.GetLength(0);
int m = M.GetLength(1);
double d =0,r=0;
for (int i = 0; i < n; i++)
{
//Приведение столбца i
к единичному вектору
d = M[i, i]; det *= d;
//деление на диагональный элемент: M[i,i]теперь = 1;
for (int k = 0; k < m; k++)
M[i, k] /= d;
//Элементарная операция: сложение строк
for (int j=0; j<n; j++)
{
//К строке j прибавляется строка i, умноженная на
r
//В результате M[j,i]=0
if(j!=i)
{
r=-M[j,i];
for (int k = 0; k < m; k++)
M[j, k] += r * M[i, k];
}
}
}
Аргументом метода является расширенная матрица M = ||A B||. В результате работы метода
матрица M приобретает вид: ||E A-1*B||. В зависимости от того, как задана матрица B, находится
решение одной системы уравнений, нескольких систем или вычисляется значение обратной
матрицы. Параллельно в переменной det формируется значение определителя матрицы A.
На рис. 8 показан возможный интерфейс проекта, построенного для работы с линейными
уравнениями.
Рис. 8 Интерфейс проекта, предназначенного для решения задач линейной
алгебры
Алгоритм Гаусса в том виде, как он выше рассмотрен, не всегда обеспечивает получение
результата. Действительно, пусть в матрице А элемент a[1,1] равен нулю. Тогда при выполнении
элементарных операций в процессе преобразования матрицы А к единичной матрице Е возникнет
ошибка уже на первом шаге при делении первой строки на элемент a[1,1]. Однако равенство нулю
диагонального элемента вовсе не означает, что определитель матрицы равен нулю (если речь не
идет о диагональной матрице), или что для нее не существует обратной матрицы.
Возможны различные модификации рассматриваемого алгоритма, исправляющие ситуацию.
Алгоритм с выбором первого ненулевого элемента
В случае, когда а[i, i] равно нулю, алгоритм ищет первую строку ниже i-й, в которой элемент
a[i, j] не равен нулю. Эта строка добавляется к строке i, что гарантирует возможность деления на
а[i, i].
Алгоритм с выбором главного элемента в столбце
Прежде чем приводить столбец к единичному виду, алгоритм ищет в столбце максимальный
по модулю элемент и меняет местами строку i и строку j, в которой находится максимальный
элемент. При обмене строк может измениться знак определителя матрицы.
Алгоритм с выбором главного элемента во всей матрице
На каждом шаге приведения очередного столбца к диагональному виду в еще не приведенной
матрице ищется максимальный элемент и меняются местами не только строки, но и столбцы
матрицы, ставя максимальный элемент в позицию а[i, i]. Этот прием гарантирует отсутствие
переполнения при выполнении операции деления. Гарантируется также, что при умножениях не
будет получено слишком большое число, поскольку деление на максимальный элемент с
последующим умножением на один из элементов приводит к тому, что элементы преобразованной
матрицы не увеличиваются по модулю. Однако ничто не дается даром. Выбор главного элемента,
перестановка строк и столбцов, необходимость обратной перестановки в конце вычислений, - все
это усложняет алгоритм. Как правило, страдает и точность вычислений, особенно для плохо
обусловленных матриц. Все модификации алгоритма стоит применять тогда, когда в основной
схеме возникла исключительная ситуация, требующая корректировки алгоритма. Обработчик
исключительной ситуации при делении на ноль, возникновении переполнения, потери значащих
цифр, может вызывать модифицированный вариант алгоритма в надежде получить решение, когда
отказывается работать основная схема.
Замечу, что никакая модификация не может помочь найти обратную матрицу, если она не
существует и определитель матрицы действительно равен нулю. В этом случае, например, все
элементы в столбце, начиная с диагонального и ниже его, будут равны нулю. Это и будет означать,
что определитель матрицы равен нулю, обратная матрица и решение системы уравнений не
существует.
Интерполяционный полином, определитель Вандермонда и обусловленность матриц
Вернемся к задаче построения интерполяционного полинома, проходящего через заданное
множество точек. Напомню, заданы своими координатами (x0, y0), …(xn, yn) точки, через которые
должен пройти полином степени n. Требуется найти коэффициенты a0, …an этого полинома. Ранее
был рассмотрен алгоритм Лагранжа, позволяющий построить этот полином. Нетрудно понять, что
существует прямое решение этой задачи, состоящее в построении системы линейных уравнений
для нахождения неизвестных коэффициентов полинома. В матричном виде эта система уравнений
имеет вид:
Xa  y; где матрица X имеет вид
X
1 x0
x1
xn
1 x0 2
x12
xn 2
...............................
1 x0 n
x1n
xn n
Матрица X называется матрицей Вандермонда, а ее определитель соответственно
определителем Вандермонда. Этот определитель вычисляется достаточно просто:
D( X )   ( x j  xi )
j i
Он равен произведению разностей координат точек, где произведение берется по всем j,
большим i. Очевидно, что определитель будет отличен от нуля, если у точек нет совпадающих
координат x. Этот факт отмечался и при рассмотрении полинома Лагранже, когда говорилось, что
множество точек «не имеет возвратов».
Когда определитель матрицы равен нулю, то, как уже говорилось, не существует обратной
матрицы и нельзя найти решение системы линейных уравнений. Для системы линейных уравнений
это означает, что уравнения в ней являются линейно зависимыми и одно из них представляет
линейную комбинацию других уравнений. Такая система не достает информации, и она не может
использоваться для однозначного нахождения решения.
На практике часто возникает ситуация, когда матрица системы появляется в результате
измерений, ее элементы представляют не точные значения, а содержат ошибки измерений. В этом
случае система уравнений может иметь определитель, отличный от нуля, но быть «почти» линейно
зависимой в пределах ошибок измерений. В таких случаях формально найденное решение может
быть далеким от «истинного» решения. Как правило, матрица подобных систем является плохо
обусловленной, а сама система уравнений называется неустойчивой. Дадим более точное
определение. Матрица А называется плохо обусловленной, а система уравнений неустойчивой,
если малым изменениям элементов прямой матрицы соответствуют большие изменения в обратной
матрице. Понятно, что, если обратная матрица вычислена с большими ошибками, то и решение
системы содержит ошибки такого же порядка.
Если матрица А плохо обусловлена, то и обратная к ней также является плохо обусловленной
матрицей. Во сколько раз могут возрастать ошибки в элементах прямой матрицы при ее
обращении? Примерный ответ на это дают «числа обусловленности» матрицы. Предлагаются
различные количественные меры обусловленности матриц. Одной из таких мер является М-число
обусловленности Тьюринга:
1
M ( A) * M ( A1 ); где
n
M ( A)  n * max Ai , j  норма матрицы
M  число 
i, j
Матрица Вандермонда – потенциальный кандидат на плохую обусловленность. Если
посмотреть на ее структуру, то видно, что для ее элементов во многих случаях характерен большой
размах – отношение между максимальным и минимальным элементом велико. Действительно,
пусть например максимальная по модулю координата xn имеет значение 100, а степень полинома n
равна 6. Это довольно скромные цифры, но уже в этом случае минимальный элемент матрицы
равен 1, а максимальный - 1012 . Примерно такой же размах будет и у элементов обратной матрицы.
Ее максимальный элемент будет примерно равен 1, а минимальный - (max ai,j)-1, так что число
обусловленности M будет примерно равно 1012. При наличии небольших ошибок в измерении
координат, ошибки в определении значений полинома в точках, отличных от измеряемых, могут
многократно возрастать. По этой причине интерполяционный полином еще можно применять
внутри интервала измерений, но не рекомендуется использовать в задачах экстраполяции.
Последовательность умножений матриц. Динамическое программирование
На практике зачастую возникает необходимость найти произведение нескольких матриц:
B  A0 * A1 *
* An1
Умножать матрицы можно в произвольном порядке, - результат не зависит от порядка
умножения, а вот эффективность выполнения этих умножений может существенно зависеть от
порядка. От порядка зависит как время выполнения операций, так и память, требуемая для
хранения промежуточных результатов.
Умножение двух матриц размерности [m * p] * [p * n], выполняемое в соответствии с
определением (7), требует m*p*n операций умножения элементов (операций сложения, которых
требуется примерно столько же, учитывать не будем, поскольку они менее трудоемкие).
Рассмотрим в качестве примера умножение трех матриц:
A1 1000*1 * A2 1*1000 * A3 1000*1
Если выполнять умножение в естественном порядке, то для умножения A1 на A2 понадобится
выполнить 106 операций умножения. Полученная в результате матрица из 106 элементов будет
требовать значительных ресурсов оперативной памяти. Последующее умножение потребует
выполнения еще 106 операций умножения, так что суммарно потребуется выполнить 2*106
операций умножения. Если же вначале выполнить умножение A2 на A3, а затем умножить A1 на
полученный результат, то суммарно потребуется выполнить 2*103 операций умножения – в тысячу
раз меньше! Заметьте, дополнительной памяти вообще не понадобится, так как результатом
умножения A2 на A3 будет число (матрица размером 1*1).
Найти оптимальный порядок в случае трех матриц нетрудно. Но уже при 10 сомножителях
задача становится нетривиальной. Дело в том, что число возможных расстановок скобок в
последовательности A1* A2*… An растет экспоненциально с ростом n, так что простой перебор
становится практически невыполнимым при сравнительно небольших значениях n. Рассмотрим
сейчас классический алгоритм решения этой задачи, основанный на идеях динамического
программирования.
Нашу задачу сформулируем следующим образом. Дана последовательность перемножаемых
матриц - A0* A1*… An-1. Дана последовательность m0, m1, …mn, определяющая размеры
перемножаемых матриц, так что матрица A0 имеет размеры m0 * m1, матрица A1 имеет размеры m1 *
m2, матрица An-1 имеет размеры mn-1 * mn. Матрица, полученная в результате этих перемножений,
имеет размеры m0 * mn. Требуется найти расстановку скобок в последовательности A0* A1*… An-1
или, что тоже, найти порядок выполнения умножений матриц, минимизирующий суммарное
количество операций умножения элементов матриц.
Идея построения алгоритма состоит в следующем. Вначале применяется типичный прием
решения сложных задач «Разделяй и властвуй» – исходная задача разбивается на подзадачи
меньшей размерности с той же структурой, затем строится общее решение на основе решения
подзадач. В таких случаях часто применяются рекурсивные алгоритмы. Но в данной ситуации
обычный рекурсивный алгоритм не работает, поскольку для него число подзадач растет
экспоненциально. Одна важная особенность спасает ситуацию– одни и те же подзадачи появляются
на разных ветвях рекурсивного спуска, так что число новых задач сравнительно невелико. Это и
позволяет применить технику динамического программирования – запоминать решение новой
задачи при первом ее появлении и использовать это решение, когда в нем появляется
необходимость. Для динамического программирования характерно и то, что решение строится
снизу вверх вместо рекурсивного спуска сверху вниз, – вначале решаются простые задачи малой
размерности, затем из них строится решение подзадач следующего яруса, подъем вверх
продолжается, пока не будет достигнуто решение основной задачи.
Покажем теперь, как эта идея воплощается в жизнь. Пусть mop(i, j) – функция, задающая
минимальное число операций умножения элементов на отрезке последовательности Ai* Ai+1*… Aj.
Если число операций умножения равно mop(i, j), то это означает, что скобки в последовательности
расставлены оптимально. Пусть nop(i, j) – функция, задающая номер, определяющий последнее
умножение в оптимальной расстановке скобок. Если nop(i, j) = k, то это означает, что в
оптимальной расстановке скобки поставлены так: (Ai* Ai+1*… Ak)*( Ak+1* Ak+2*… AJ). Конечно
же, внутри каждой скобки существует своя оптимальная расстановка скобок.
Теперь нетрудно выразить mop(i, j) через значения этой функции для двух подзадач меньшей
размерности
mop (i, j )  mop (i, k )  mop (k  1, j )  mi * mk 1 * m j 1
(11)
Последний член в этой сумме задает число операций умножения, необходимое при
перемножении двух скобок. Он явно вычисляется, поскольку размеры матриц, являющихся
результатами умножений в каждой скобке, заранее известны. Первые два слагаемых отражают тот
факт, что общее оптимальное решение строится из оптимальных решений ее подзадач.
Все было бы хорошо, но беда в том, что в соотношении (11) неизвестно значение k. К счастью,
у k не так много возможных значений. Поэтому можно исключить k из соотношения (11) ценой
увеличения числа подзадач. Вот окончательное выражение для mop(i, j):

0 если i  j
(12)
mop(i, j )  
min (mop(i, k )  mop(k  1, j )  mi * mk 1 * m j 1 )

ik  j
Соотношение (12) позволяет построить конкретный алгоритм решения задачи. Напомню, что
наша цель (одна из наших целей) вычислить mop(0, n-1) – минимальное число операций для
последовательности матриц A0* A1*… An-1. Для ее достижения будем постепенно заполнять
квадратную матрицу mop ее элементами – mop(i, j), пока не получим искомый элемент - mop(0, n1). Фактически достаточно построить треугольную матрицу, поскольку элементы ниже диагонали
нас не интересуют.
Матрица будет заполняться не по строкам и не по столбцам, а по диагоналям. Заполнение этой
матрицы начинается с заполнения главной диагонали, элементы которой представляют простейшие
задачи размерности 1, решения которых заранее известны и равны 0 в соответствии с
соотношением (12). На следующем шаге будут вычисляться по формуле (12) элементы диагонали,
примыкающей к главной, - mop(0, 1), mop(1, 2) и так далее. Эти элементы дают решение задач
размерности 2. Так, восходя по диагоналям, доходим до правого угла матрицы, дающего решение
общей задачи. Каждый новый элемент диагонали использует при своем вычислении ранее
построенные элементы левее и ниже его.
Параллельно с заполнением матрицы mop формируется и матрица nop с элементами nop(i, j).
Значением nop(i, j) становится значение k, обеспечивающее минимум mop(i, j).
Поскольку обе матрицы являются треугольными, то можно экономит память и хранить их
значения в одной матрице, у которой диагональные элементы равны нулю, выше диагонали
хранятся значения mop(i, j), а ниже диагонали nop(i, j). Вот как выглядит эта матрица:
0
mop(1,2) mop(1,3) mop(1, n  1) mop(1, n)
nop(1,2) 0
mop(2,3) mop(2, n  1) mop(2, n)
mop  nop(1,3) nop(2,3)
0
mop(3, n  1) mop(3, n)
.............................................................................
nop(1, n) nop(2, n)
..nop(n  1, n)
0
Приведу теперь процедуру, выполняющую вычисление этой матрицы:
public void MopAndNop()
{
maxmop = MaxM();
//заполнение матрицы mop
//главная диагональ заполняется нулями
for (int i = 0; i < n; i++)
mop[i, i] = 0;
//цикл по диагоналям
for (int i = 1; i < n; i++)
//диагональ i заполняется элементами mop
//симметричная диагональ - элементами nop
//цикл по элементам диагонали
for (int j = 0; j + i < n; j++)
EvalMopAndNop(j, j + i, ref mop[j, j + i],
ref mop[j + i, j]);
}
Вначале заполняются нулями элементы главной диагонали – mop(i, i). Затем начинается цикл
по боковым диагоналям. Процедура EvalMopAndNop вычисляет два симметричных элемента
матрицы – mop(j, j+i) и mop(j+i, j), равное nop(j, j+i), где j – номер элемента на диагонали, а i –
номер диагонали.
В первой строке процедуры вычисляется значение переменной maxmop, задающее
максимально возможное значение числа операций умножения при перемножении двух матриц в
нашей последовательности. Эта переменная будет использоваться в процедуре EvalMopAndNop при
нахождении минимума. Значение maxmop не превосходит максимального размера перемножаемых
матриц, возведенного в третью степень:
int MaxM()
{
//возвращает максимально возможное значение mop
int max = sizes[0];
for (int i = 1; i <= n; i++)
if (sizes[i] > max) max = sizes[i];
return (max * max * max);
}
Процедура EvalMopAndNop построена в полном соответствии с соотношением (12), находя
минимальное значение стоимости перемножаемых матриц в цикле по k – по всем возможным
перемножениям на заданном отрезке последовательности:
void EvalMopAndNop(int i, int j, ref int m, ref int n)
{
//нахождение на отрезке (i,j) минимального числа
умножений
//и номера матрицы, где этот минимум достигается
int min =maxmop, num =0;
int m1=0;
for (int k = i; k < j; k++)
{
m1 = mop[i, k] + mop[k + 1, j]
+ sizes[i] * sizes[k+1] * sizes[j+1];
if (m1 <= min)
{
min = m1; num = k;
}
}
m = min; n = num;
}
При вычислении минимума используются уже рассчитанные элементы, лежащие левее и ниже
диагонального элемента.
Матрица mop содержит всю необходимую информацию об оптимальном порядке умножений.
Но эту информацию необходимо еще преобразовать к нормальному виду, поскольку она дается в
обратном порядке, указывая на последнюю операцию умножения. Для достижения цели разумно
использовать стек, вталкивая в него элементы в порядке их вычисления, тогда в конце первое
умножение окажется в вершине стека. В данном случае стек можно организовать на обычном
массиве, который назовем ords. Размерность массива определяется числом умножений (n-1), и
массив будет заполняться снизу, так что указатель элемента top вначале имеет максимальное
значение, равное n-2. Заполняет стек рекурсивная процедура, которая вначале вталкивает в стек
значение, соответствующее последнему умножению, а затем рекурсивно вызывает себя на двух
отрезках последовательности – предшествующему последнему умножению и следующему за ним.
Рекурсивный вызов не выполняется, если отрезки имеют единичную длину.
Приведу текст рекурсивной процедуры orders(i, j), формирующей порядок умножений на
отрезке [i, j]. Как обычно в таких случаях вначале строится нерекурсивная процедура без
параметров, единственное назначение которой вызвать рекурсивную процедуру на начальном
отрезке:
public void Ords()
{
orders(0, n - 1);
}
void orders(int i, int j)
{
//рекурсивная процедура формирования порядка умножений
int k = mop[j,i];
ords[top] = FormStr(i, j, k);
top--;
if (i != k) orders(i, k);
if ((k+1) != j) orders(k+1, j);
}
Процедура FormStr формирует строку, показывающую порядок умножений в виде, удобном
для восприятия пользователем:
private string FormStr(int i, int j, int k)
{
string s,s1;
if (i == k)
s = "(" + i + ")";
else s = "(" + i + ", " + k + ")";
if (j == k+1)
s1 = "(" + j + ")";
else s1 = "(" + (k+1) + ", " + j + ")";
return (s + "*" + s1);
}
На рис. 9 показано, как выглядит интерфейс пользователя в реальном проекте, где
используется алгоритм динамического программирования для нахождения оптимального порядка
выполнения умножений в последовательности матриц.
Рис. 9 Определение оптимального порядка умножения матриц
Эффективные алгоритмы умножения двух матриц. Алгоритм Штрассена
Вернемся к рассмотрению алгоритма умножения двух матриц: C(n*m) =A(n*p)*B(p*m). В
соответствии с определением (7) для вычисления матрицы C требуется выполнить n*p*m операций
умножения элементов матриц. Если рассматривать умножение квадратных матриц размерности n,
то умножение двух матриц требует порядка O(n3) операций умножения элементов. Можно ли
уменьшить требуемое число умножений? Существуют ли алгоритмы умножения, имеющие лучший
порядок? Оказывается, да. И один из первых алгоритмов такого рода был предложен Штрассеном.
Алгоритм Штрассена требует для умножения матриц порядка O(nlog(7)) = O(n2,81)операций
умножения. Конечно, даром ничего не дается. Алгоритм Штрассена требует в сравнении с
обычным алгоритмом дополнительной памяти и дополнительных сложений порядка O(n2). Он
имеет гораздо большую константу при числе умножений, чем обычный алгоритм. Большие
матрицы, встречающиеся на практике, как правило, являются разреженными, многие клетки
которых равны нулю. Тогда, как уже отмечалось, для них можно применять специальные
алгоритмы, использующие знание структуры построения матриц и ее специфику. Тем не менее,
если требуется перемножать плотные матрицы больших размеров, применение алгоритма
Штрассена становится разумным. В любом случае знание алгоритма Штрассена представляет как
теоретический, так и практический интерес.
Чтобы понять основную идею уменьшения числа умножений за счет увеличения числа
сложений, рассмотрим более простую задачу умножения двух комплексных чисел, заданных
действительной и мнимой частью:
r1  a  bi; r 2  c  di;
r 3  r1* r 2  e  fi; e  ac  bd ;
f  ad  bc
Для получения результата необходимо выполнить 4 умножения и 2 сложения (вычитания) над
вещественными числами a, b, c, d. А можно ли обойтись тремя умножениями? Можно, например,
так:
p1  (a  b) *(c  d );
f  p 2  p3;
e  p1  p 2  p3;
p2  a * d ;
p3  b * c;
Для получения результата понадобилось 3 умножения и 5 сложений. Если полагать, что одна
операция умножения эквивалентна по стоимости (сложности, времени вычисления) 30 операциям
сложения, то второй алгоритм явно предпочтительнее первого.
Рассмотрение алгоритма Штрассена начнем со случая перемножения квадратных матриц, из
чего однозначно следует, что матрицы имеют одинаковую размерность [n*n]. Более того, будем
предполагать, что n – число четное, так что n = 2*m. Представим теперь матрицы сомножители в
клеточном виде, состоящими из клеток одинаковой размерности [m*m]:
С  A * B;
A
a b
c d
B
e
g
f
h
C
r s
t u
(13)
r  ae  bg ; s  af  bh;
t  ce  dg ; u  cf  dh
Соотношения (13) в то виде, как они записаны, пока что не дают никаких преимуществ.
Исходная задача, имеющая сложность O((2*m)3) = O(8m3), свелась к вычислению четырех
клеточных матриц – r, s, t, u, - для нахождения каждой из которых требуется два умножения матриц
размерности [m*m] плюс одно сложение матриц, так что суммарно по-прежнему требуется число
умножений порядка O(8m3). Наша цель – организовать вычисления так, чтобы число умножений
клеточных матриц сократилось с восьми до семи за счет идеи, используемой при перемножении
комплексных чисел.
Построим 7 временных матриц P1, P2, …P7. Каждая из матриц PI будет требовать ровно
одного умножения матриц и возможно одного или двух сложений исходных клеточных матриц.
Если удастся выразить клетки результата в виде линейной комбинации матриц PI, то цель будет
достигнута, требуемое число умножений клеток сократится до 7, и, следовательно, будет построен
более эффективный алгоритм умножения двух матриц. Эту задачу и удалось решить Штрассену.
Вот соотношения для расчета:
P1  a *( f  h); P 2  (a  b) * h;
s  P1  P 2;
P3  d *(e  g ); P 4  (c  d ) * e;
t  P 4  P3;
(14)
P5  (a  d ) *(e  h); P6  (b  d ) *( g  h); P7  (c  a) *( f  e);
r  P5  P6  P 2  P3;
u  P5  P7  P1  P 4
Соотношения (14) и задают алгоритм Штрассена. Оценим время работы этого алгоритма.
Нетрудно видеть, что время работы можно описать следующим рекуррентным уравнением:
T (n)  7T (n / 2)  kn2
(15)
Базисная теорема о рекуррентных уравнениях, рассматриваемая ниже, утверждает, что
решение этого уравнения дается соотношением:
T (n)  O(nlog2 7 )  O(n2,81 )
(16)
Алгоритм Штрассена можно обобщить на случай матриц произвольной размерности. Если
матрицы квадратные, но n – нечетно, то нетрудно сделать его четным, добавив в перемножаемые
матрицы строку и столбец, состоящие из нулевых элементов.
Если матрицы прямоугольные, но размеры m, n и p близки друг к другу, то, опять-таки,
добавление нулевых строк и столбцов позволяет расширить матрицы до квадратных. Например,
при перемножении матрицы A(27*31) на матрицу B(31*29) можно расширить матрицы до
квадратных матриц размера 32*32, добавив в матрицу A пять нулевых строк и один такой же
столбец, а в матрицу B соответственно добавить одну строку и три нулевых столбца. В
результирующей матрице C размера 32*32 появятся пять пустых строк и три таких же столбца,
исключив которые, получим нужный результат – матрицу размера 27*29.
Если приходится иметь дело с вытянутыми матрицами, то можно поступить следующим
образом. Пусть r – это минимальный из трех размеров: r = min(n, p, m). Расширим матрицы так,
чтобы все три размера были кратны r: n = ur, p = vr, m = wr. Тогда матрицы A и B можно
рассматривать как клеточные матрицы, состоящие из квадратных клеток размерности r. Затем
применять алгоритм Штрассена к произведениям клеток, для которых выполняется требования
квадратности. Пусть, например, размеры перемножаемых матриц задаются тройкой чисел: 30, 70,
100. Эти матрицы можно расширить так, чтобы их размеры задавались тройкой: 30, 90, 120.
Расширенные матрицы состоят из клеток размера 30*30. При перемножении клеток применим
алгоритм Штрассена. Конечно, в таких случаях всегда нужно решить вопрос, а «стоит ли овчинка
выделки».
Базисная теорема о решении рекуррентных уравнений
При разработке алгоритмов широко применяется прием «разделяй и властвуй», когда
исходная задача разбивается на подзадачи. Решение общей задачи строится из решений подзадач.
Важную часть таких алгоритмов составляют рекурсивные алгоритмы, когда исходная задача
размерности n разбивается на подзадачи той же структуры, что и исходная, но меньшей
размерности. Весь фокус рекурсии заключается в том, что процесс разбиения продолжается до тех
пор, пока не придем к подзадачам минимальной размерности, обычно к задачам размерности n=1,
для которых решение строится совсем просто. При этом важно, чтобы, зная решения подзадач,
сложность построения решения общей задачи была невысокой.
Рассмотрим теорему, позволяющую оценить время решение общей задачи для рекурсивных
алгоритмов в ряде важных случаев, часто возникающих на практике. Эту теорему иногда называют
базисной теоремой рекуррентных уравнений. Рассмотрим случай, когда исходная задача
размерности n сводится к решению k подзадач размерности n/q. Для упрощения доказательства
будем предполагать, что n является степенью числа q, так что n = qm. Имеет место следующая
теорема:
Теорема 3.1. Пусть k, b, q – неотрицательные постоянные. Решение рекуррентных уравнений
b
T ( n)  
kT (n / q)  bn
при n  1
при n  1
(17)
имеет вид:
O(n),

T (n)  O(n * log(n)),

log q k
),
O(n
если k  q,
если k  q,
если
(18)
kq
Доказательство. Из соотношений (17) следует:
T (n)  T (q m )  kT (q m1 )  bq m  k 2T (q m2 )  bkq m1  bq m 
 bk m  bk m1q 
m
k
 bkq m1  bq m  bq m   
i 0  q 
i

(19)
В соотношение (19) входит сумма членов геометрической прогрессии со знаменателем k/q.
Если k < q, то знаменатель меньше 1. Это означает, что соответствующий бесконечный ряд
сходится, а конечная сумма ограничена некоторой константой. Отсюда и следует первое из
утверждений теоремы, что T(n) = O(qm) = O(n).
В случае, когда q = k, все слагаемые в сумме равны 1 и, следовательно, T(n) = b*qm * m =
O(n*log(n)). Это доказывает второе из утверждений теоремы.
В случае, когда знаменатель прогрессии больше 1, воспользуемся выражением для суммы
членов геометрической прогрессии. Тогда получим:
1 m
k
i
m
  1
m
q
k

m
m
m k 
T (n)  bq     bq *
 bq *  
k
i 0  q 
q
  1
q
 bk m  bk log q  bn
n
log q k
 O (n
log q k
)
Все случаи разобраны и теорема доказана.
Теорема показывает, как временная сложность рекурсивных алгоритмов связана с размерами
и числом подзадач. Если размер подзадачи уменьшается в q раз в сравнении с размером исходной
задачи, то хорошо, когда число подзадач вырастает в менее чем в q раз. Тогда алгоритм будет
выполнять свою работу за линейное время. Он останется почти линейным с логарифмическим
сомножителем, если исходная задача разобьется на две подзадачи размера n/2, или на три задачи
размера n/3, или на k задач размера n/k. В случае, когда появляются 4 подзадачи размера n/2,
алгоритм будет уже квадратичным со временем работы O(n2). При 8 подзадачах размерности n/2
время работы алгоритма оценивается как O(n3).
Вернемся к алгоритму Штрассена, где исходная задача разбивается на 7 подзадач размерности
n/2. Применить теорему 3.1 напрямую нельзя, поскольку время объединения решений подзадач
является квадратичным, а не линейным, как в доказанной теореме. Но можно повторить
доказательство теоремы (случай 3) и убедиться, что результат теоремы верен и в этом случае.
Единственное ограничение, что число подзадач должно быть не меньше четырех, что выполняется
для алгоритма Штрасссена, где число подзадач равно 7.
Конечно, на практике редко бывает, чтобы размерность задачи n представляла целую степень
числа q. В этом случае ближайшая сверху степень числа q служит для оценки времени работы
алгоритма. В самом алгоритме подзадачи создаются так, чтобы их размеры совпадали насколько
это возможно.
Что происходит в тех случаях, когда не удается разбивать задачу на подзадачи примерно
одинаковой размерности? Как правило, нарушение балансировки может существенно ухудшить
качество алгоритма, увеличивая его временную сложность. В качестве примера рассмотрим
ситуацию, когда исходная задача размерности n разбивается не на две подзадачи размерности n/2, а
на две подзадачи - размерности 1 и n-1. Тогда
b
T ( n)  
T (n  1)  T (1)  nb
при n  1
при n  1
Нетрудно видеть, что решением этого уравнения является T(n) = O(n2). В результате
разбалансировки алгоритм получил квадратичную сложность, что значительно хуже, чем
сложность O(n*log(n)), получаемую при делении задачи на две подзадачи одинаковой размерности.
Тяжелая ситуация возникает и тогда, когда с ростом подзадач их размер сокращается не в k
раз, а на k единиц. Рассмотрим ситуацию, когда исходная задача размерности n разбивается на две
подзадачи размерности n-1 каждая. Соответствующее рекуррентное уравнение в этом случае имеет
вид:
при n  1
при n  1
b
T ( n)  
2T (n  1)  b
Даже упростив ситуацию, полагая, что объединение решений подзадач требует константного
времени, а не линейного, приходим к алгоритму с экспоненциальной сложностью, где T(n) = O(2n).
Как известно, алгоритмы с экспоненциальной сложностью становятся практически
нереализуемыми уже при сравнительно небольших значениях n, несмотря на всю мощь
современных компьютеров. Задача о «Ханойской башне» является классическим примером, где
возникает подобная ситуация.
Эффективный алгоритм обращения матриц специальной структуры
Уже отмечалось, что знание структуры матрицы позволяет в ряде случаев выполнять
основные операции эффективнее, чем в стандартных ситуациях. В качестве примера рассмотрим
важный для ряда приложений класс матриц, имеющих следующую структуру:
R  RN  hRm hT
(20)
Здесь RN – диагональная матрица размерности [N*N], Rm – диагональная матрица
размерности [m*m], h и hT – прямоугольные матрицы соответственно размерностей [N*m] и [m*N].
Предположим также, что N существенно больше, чем m. Обращение матрицы R требует в
стандартном случае O(N3) операций умножения, а в случае применения алгоритма Штрассена –
O(N2,81).
Покажем, что, используя знание структуры матрицы R, можно построить более эффективный
алгоритм. Докажем, что обратную матрицу можно вычислить по следующей схеме:
R 1  RN 1  RN 1hB 1hT RN 1 , где
B  ( Rm 1  hT RN 1h)
(21)
Поскольку обращение диагональных матриц не требует серьезных затрат, то основные
вычисления будут связаны с обращением матрицы B, что требует O(m3) операций умножения. Если,
например N= 10m, то суммарное время вычислений сократится примерно в 10 раз, требуя времени
такого же, какое уходит на вычисление самой матрицы R в соотношении (20).
Докажем справедливость соотношения (21):
RR 1  ( RN  hRm hT )( RN1  RN1hB 1hT RN1 ) 
EN  hRm hT RN1  hB 1hT RN1  hRm hT RN1hB 1hT RN1 
EN  hRm hT RN1  h( B 1  Rm hT RN1hB 1 )hT RN1 
EN  hRm hT RN1  h( Em  Rm hT RN1h) B 1hT RN1 
EN  hRm hT RN1  hRm ( Rm1  hT RN1h) B 1hT RN1 
EN  hRm hT RN1  hRm BB 1hT RN1  EN
Задачи
1.297 Дана прямоугольная матрица A. Вычислить матрицу B = AT.
1.298 Дана квадратная матрица A. Построить транспонированную матрицу
на том же месте.
1.299 Даны матрицы A и B одинаковой размерности. Вычислить их сумму
C= A+B
1.300 Даны матрицы A и B. Вычислить их произведение C= A*B, если это
возможно.
1.301 Матрицы A и B являются нижнетреугольными матрицами одной
размерности. Напишите процедуры сложения, умножения и
транспонирования для таких матриц, учитывающие их структуру.
1.302 Матрицы A и B являются верхнетреугольными матрицами одной
размерности. Напишите процедуры сложения, умножения и
транспонирования для таких матриц, учитывающие их структуру.
1.303 Матрица A является нижнетреугольной матрицей, а B –
верхнетреугольной. Напишите процедуры сложения и умножения
таких матриц, учитывающие их структуру.
1.304 В целях экономии памяти треугольные матрицы хранятся в
одномерных массивах размерности (n2 +n)/2. Напишите процедуры
сложения, умножения и транспонирования таких матриц.
1.305 Дана квадратная матрица А. Напишите процедуру вычисления ее
определителя.
1.306 Дана квадратная матрица А. Напишите процедуру вычисления
обратной матрицы А-1.
1.307 Дана система n линейных уравнений с n неизвестными. Напишите
процедуру нахождения ее решения.
1.308 Дано m систем n линейных уравнений с n неизвестными с одной и
той же правой частью (матрицей A). Напишите процедуру нахождения
решений этих систем.
1.309 Постройте обобщенную процедуру метода Гаусса, позволяющую
вычислять определитель матрицы, находить обратную матрицу и
решать системы уравнений с произвольным числом правых частей.
1.310 Напишите процедуру обращения матрицы, используя алгоритм
Штрассена.
1.311 Проведите эксперименты и постройте графики времени обращения
матриц в зависимости от их размерности для обычного алгоритма
умножения матриц и алгоритма Штрассена.
1.312 Напишите процедуру умножения последовательности матриц,
используя алгоритм динамического программирования.
1.313 Проведите эксперименты и постройте графики времени умножения
последовательности матриц для алгоритма с последовательным
умножением матриц и алгоритма динамического программирования.
Предполагается, что последовательность, задающая размеры
перемножаемых матриц, является последовательностью равномерно
распределенных случайных чисел в заданном диапазоне [m, n].
1.314 Дана последовательность точек на плоскости – P0, P1, …Pn, заданная
декартовыми координатами. Постройте матрицу Вандермонда.
Вычислите ее определитель двумя способами.
1.315 Дана последовательность точек на плоскости – P0, P1, …Pn, заданная
декартовыми координатами. Постройте интерполяционный алгоритм
Лагранжа, используя алгоритм Гаусса.
1.316 Проведите эксперименты и постройте графики времени нахождения
коэффициентов интерполяционного полинома для двух алгоритмов –
Гаусса и Лагранжа.
1.317 Дана последовательность точек на плоскости – P0, P1, …Pn, заданная
декартовыми координатами. Постройте матрицу Вандермонда и
обратную к ней. Вычислите М-число обусловленности.
1.318 Дана последовательность точек на плоскости – P0, P1, …Pn, заданная
декартовыми координатами. Будем называть координаты этих точек
«точными координатами». Постройте последовательность точек R0, R1,
…Rn, координаты которых строятся по точным координатам и
случайным ошибкам, так что aи = ат + ε, где aи – измеренное значение,
ат – точное значение, а ε – ошибка измерения. Проведите исследование
влияния ошибок измерений на коэффициенты интерполяционного
полинома, сравнивая их с коэффициентами, полученными для точных
координат точек.
1.319 Дана последовательность точек на плоскости – P0, P1, …Pn, заданная
декартовыми координатами. Будем называть координаты этих точек
«точными координатами». Постройте последовательность точек R0, R1,
…Rn, координаты которых строятся по точным координатам и
случайным ошибкам, так что aи = ат + ε, где aи – измеренное значение,
ат – точное значение, а ε – ошибка измерения. Проведите исследование
влияния ошибок измерений на значение полинома , вычисленное в
новой точке x*. Как меняется ситуация, когда точка x* находится
внутри интервала измерений, вне этого интервала? Как влияет число
обусловленности на результаты исследований?
1.320 Напишите процедуру вычисления обратной матрицы для случая, когда
прямая матрица имеет структуру, заданную соотношением (20).
1.321 Проведите исследование временных затрат на обращение матрицы,
структура которой задается соотношением (20), используя различные
алгоритмы обращения – классический, Штрассена и специальный,
заданный соотношением (21).
Математическая статистика и задачи обработки измерений
На практике часто приходится работать с последовательностями данных, представляющих
результаты измерений значений некоторых величин. Как правило измерения сопровождаются
ошибками, носящими случайный характер. Корректная обработка результатов измерений требует в
этом случае привлечение аппарата теории вероятностей и математической статистики.
Переменная X называется случайной, если можно говорить о вероятности появления того или
иного значения, но нельзя точно предсказать, какое именно значение примет эта величина в
результате опыта. Классическим примером является случайная величина, задающая исход бросания
монеты, у которой два возможных значения – «орел» и «решка». Для правильно устроенной монеты
вероятности появления значений одинаковы и равны 1/2.
Случайные величины характеризуются законом распределения, позволяющим определить
вероятность появления возможных значений. Далее для простоты будем рассматривать случайные
величины, возможные значения которых являются вещественными числами. Тогда закон
распределения будет характеризовать вероятность попадания значения случайной величины в
некоторый заданный интервал вещественной оси.
Случайные величины могут быть дискретными и непрерывными. Для дискретной случайной
величины множество возможных значений конечно. Закон распределения дискретной случайной
величины можно задать таблицей, в которой каждому возможному значению xi приписана
вероятность появления этого значения – pi. От вероятностей pi требуются, чтобы их значения
принадлежали интервалу [0, 1] , а их сумма была равна 1, как это имеет место в случае бросания
монеты.
Для непрерывной случайной величины множество возможных значений бесконечно и
несчетно (при сделанном нами предположении – это вещественная ось). Вероятность каждого
отдельного значения в этом случае равна нулю и имеет смысл говорить только о попадании
значения в некоторый интервал. В этом есть некоторый парадокс – для непрерывных случайных
величин реализуются события с нулевой вероятностью, поскольку для каждого отдельного
значения xi справедливо, что P(X=xi) = 0.
Закон распределения для непрерывной случайной величины обычно задается с помощью
функции распределения F(x), определяющей вероятность того, что случайная величина X примет
значение меньшее x:
F(x) = P(X <x)
Наряду с функцией F(x) для задания закона распределения используют функцию f(x), плотность распределения вероятностей. Функция f(x) связана с функцией F(x) следующим
соотношением:
x
F ( x) 

f ( x)dx

Зная плотность вероятности, нетрудно посчитать вероятность попадания значения случайной
величины X в интервал [a, b]:
b
P ( x  [a, b])   f ( x )dx
(22)
a
В зависимости от ситуации встречаются случайные величины с различными законами
распределения. Рассмотрим два наиболее часто встречающихся закона распределения –
равномерное и нормальное распределение. О равномерном распределении можно говорить как для
дискретных, так и для непрерывных случайных величин. У равномерно распределенной дискретной
случайной величины с n возможными значениями вероятности их появления одинаковы и равны
1/n. Бросание монеты или игрального кубика – примеры ситуаций, где появляется равномерно
распределенная дискретная случайная величина. Непрерывная, равномерно распределенная в
некотором интервале [a, b] случайная величина имеет постоянную плотность вероятности:
0
f ( x)  
1
c  (b  a )
x  [ a, b]
x  [ a, b]
(23)
Вероятность попадания значения такой случайной величины в интервал [c,d], содержащийся в
интервале [a,b] пропорциональна длине интервала и равна (d-c)/(b-a).
Непрерывная случайная величина распределена по нормальному закону (закону Гаусса), если
ее плотность вероятности задается соотношением:
1
f ( x) 
e
2
 ( x  m )2
2 2
(24)
Функция распределения F(x) и плотность распределения – f(x) полностью характеризуют
случайную величину. Но часто используют более простые числовые характеристики, называемые
моментами распределения, среди которых важную роль играют два первых момента –
математическое ожидание и дисперсия случайной величины. Математическое ожидание – mx
представляет собой средневзвешенное значение случайной величины, где вероятности играют роль
весовых коэффициентов. Для дискретной случайной величины:
n
mx   xi pi
(25)
i 1
Для непрерывной случайной величины сумма заменяется интегралом:

mx 
 xf ( x)dx
(26)

Случайная величина называется центрированной, если ее математическое ожидание равно 0.
Если X – случайная величина с математическим ожиданием mx, то для центрирования достаточно
вычесть математическое ожидание, переходя к случайной величине Y = X – mx.
Заметьте, математическое ожидание не является наиболее вероятным значением, - это центр,
вокруг которого разбросаны значения случайной величины. Для дискретной величины оно вообще
может не быть возможным значением. Так для равномерно распределенной случайной величины с
двумя значениями 0 и 1 математическое ожидание будет равно 0,5, хотя само это значение никогда
не появится в результате опыта. Для непрерывной случайной величины равномерно
распределенной в интервале [0, 1], математическое ожидание также равно 0,5 и хотя это значение
может появляться, но, как уже говорилось, вероятность этого события равна нулю.
Дисперсия характеризует разброс значений случайной величины относительно ее
математического ожидания. Формально дисперсия задается следующими соотношениями:
n
Dx   pi ( x  mx ) 2
в дискретном случае
i 1
(27)

Dx 
 (x  m )
x
2
f ( x)dx в непрерывном случае

Вместо дисперсии часто используют параметр, называемый среднеквадратическим
отклонением - σx = √Dx .
У нормального закона распределения есть много замечательных свойств, - одно из них
состоит в том, что два его параметра в соотношении (23) – m и σ представляют математическое
ожидание и среднеквадратическое отклонение и полностью определяют этот закон распределения.
От людей, влюбленных в теорию вероятностей, часто можно слышать, что все в этом мире
распределено по нормальному закону (по гауссиане). Это высказывание подтверждается
многочисленными наблюдениями в реальной жизни. Математическое обоснование его
справедливости дают центральные предельные теоремы, утверждающие, что распределение суммы
случайных величин с ростом числа слагаемых стремится к нормальному распределению, если среди
слагаемых нет таких, вклад которых в сумму был бы определяющим. Разные предельные теоремы
по-разному задают ограничения на слагаемые в сумме. С содержательной точки зрения случайность
возникает из-за воздействия множества различных неучтенных факторов, - их совокупность
приводит к нормальному распределению. Но следует с некоторой осторожностью принимать
гипотезу о нормальности распределения. Приведу простой пример – пусть случайной величиной
является площадь квадратного участка, полученная в результате измерения длины его стороны.
Если мы примем, что сторона квадрата X имеет нормальное распределение, то площадь Y = X2
будет иметь распределение, отличное от нормального. Если же полагать, что Y распределено по
нормальному закону, то тогда этого нельзя утверждать относительно распределения стороны. В
таких ситуациях обычно предпочитают считать, что нормальным распределением обладают
наблюдаемые величины (в нашем примере – это сторона), а дальше уже рассматривают
распределение величин, функционально связанных с наблюдаемыми. Заметьте также, что речь
всегда идет об асимптотически нормальном распределении, поскольку в нашем примере, как это
часто бывает, случайные величины (длина, площадь) по своей сути являются положительными, а
нормальное распределение предполагает возможность появления значений на всей вещественной
оси.
Запись X N(m, σ) будет обозначать, что случайная величина X имеет нормальное
распределение с параметрами m и σ. Запись X  R(a, b) будет обозначать, что случайная величина
X равномерно распределена в интервале [a, b].
Для программистов крайне важно уметь генерировать последовательности значений
случайной величины с заданным законом распределения. Поскольку такая последовательность
формируется по определенному алгоритму, то ее нельзя назвать случайной в строгом смысле.
Последовательности, вырабатываемые алгоритмически, называют псевдослучайными. Префикс
«псевдо» обычно опускается. Библиотека классов FCL содержит специальный класс Random,
позволяющий генерировать последовательность значений, как для дискретных, так и для
непрерывных, равномерно распределенных случайных величин. Поскольку есть возможность
получать равномерно распределенную случайную величину, то, используя результаты предельной
теоремы нетрудно получать и случайные величины, распределенные по нормальному закону. Пусть
X – непрерывная равномерно распределенная случайная величина в интервале [0, 1],
последовательность значений которой генерируется методами класса Random. Используя
соотношения (25) и (26), нетрудно подсчитать, что математическое ожидание и дисперсия в этом
случае равны:
mx  0,5;
Dx  1 ;
12
Случайная величина Y, образованная как сумма равномерно распределенных случайных
величин Xi, будет распределена (асимптотически) по нормальному закону в соответствии с
центральной предельной теоремой. Пусть
1 n ( X i  mx )
Y
 
n i 1
x
(28)
Тогда нетрудно показать, что
my  0;
 y  1;
Для получения хорошего приближения к нормальному закону достаточно пользоваться
сравнительно небольшими значениями n, например выбирать n = 10.
Часто приходится иметь дело не с одной случайной величиной, а с некоторой совокупностью
– случайным вектором X (X1, X2, …Xn), все компоненты которого случайные величины. Все
понятия, связанные со случайной величиной, - функция распределения, плотность распределения,
моменты - легко обобщаются на многомерный случай. Для простоты рассмотрим вначале случай
двух случайных непрерывных величин X и Y с плотностью распределения f(x, y). Вероятность
попадания точки на плоскости с координатами (x, y) в некоторую область D вычисляется через
плотность распределения:
P(( x, y)  D)   f ( x, y)dxdy
D
В классической математике величины могут быть связаны некоторой зависимостью. Если,
например, x и y связаны функциональной зависимостью, то, зная значение x, можно однозначно
вычислить значение y. Для случайных величин можно говорить о вероятностной зависимости. Если
X и Y связаны вероятностной зависимостью, то, зная значение x первой из них, нельзя однозначно
сказать, каково будет значение второй случайной величины, но плотность вероятности случайной
величины будет зависеть от значения x. Более точно это выражается в определении плотности
двумерного распределения через плотности одномерного распределения:
f ( x, y)  f1 ( x) f ( y / x)  f 2 ( y) f ( x / y)
В данном соотношении f(y/x) – условная плотность вероятностей – плотность вероятности
случайной величины Y при условии, что случайная величина X приняла значение x. Аналогичный
смысл имеет условная плотность f(x/y).
Случайные величины X и Y называются независимыми, если их условные плотности
совпадают с обычными плотностями распределения, так что
f ( x, y)  f1 ( x) f 2 ( y)
С вероятностной зависимостью случайных величин X и Y тесно связано понятие
корреляционной зависимости, числовыми характеристиками которой являются корреляционный
момент - Kxy и коэффициент корреляции – rxy:
 
K xy  M (( X  mx )(Y  m y )) 
  ( X  m )(Y  m ) f ( x, y)dxdy
x
 
rxy 
K xy
y
(29)
 x y
Корреляционный момент Kxy определяется, как математическое ожидание произведения
центрированных случайных величин X и Y. Учитывая соотношение (29), нетрудно видеть, что из
независимости случайных величин следует их некоррелированность – корреляционный момент и
коэффициент корреляции равны 0. Отсюда следует, что, если есть корреляция, то случайные
величины обязательно зависимы. Однако из некоррелированности не следует независимость
случайных величин. Коэффициент корреляция может быть равен 0, тем не менее случайные
величины могут быть зависимыми. Нетрудно показать, что коэффициент корреляции по модулю
меньше или равен единицы. Если X и Y связаны линейной функциональной зависимостью, то
отсюда следует и корреляционная зависимость:
Y  aX  b;
my  M (aX  b)  amx  b;
Dy  M ((Y  my ) 2 )  M ((a( X  mx )) 2 )  a 2 Dx ;
 y  a  x;
(30)
K xy  M (( X  mx )(Y  m y ))  M (a( X  mx ) 2 )  aDx ;
rxy 
1 если a  0 


a  x x 1 если a  0 
aDx
Коэффициент корреляции отражает степень линейной зависимости случайных величин, имея
по модулю значение 0, когда величины независимы, и 1, когда они связаны линейной
функциональной зависимостью. Случайная величина естественно всегда линейно связана сама с
собой и взаимный корреляционный момент по определению является ее дисперсией: Dx = Kxx.
Обобщая эти понятия на случайный вектор, приходим к характеристикам случайного вектора
X= (X1, X2, …Xn) - вектору математических ожиданий Mx и корреляционной матрице Kx:
M x  (m1 , m2 ,
D1
Kx 
mn )
K1,2
K1,3
K1,n
K 2,1
D2
K 2,3
K 2,n
K n ,1
K n ,2
(31)
Dn
Корреляционная матрица является симметричной матрицей, по диагонали которой стоят
дисперсии случайных величин Xi, а элементы Ki,j – корреляционные моменты случайных величин
Xi и Xj. Если случайные величины независимы (не коррелированны), то корреляционная матрица
случайного вектора является диагональной.
Если случайный вектор Y является суммой случайных векторов X1 и X2, то
M y  M x1  M x 2 ;
K y  K x1  K x 2  2K x1, x 2 ;
(32)
Для независимых случайных векторов X1 и X2 корреляционная матрица суммы векторов
сумме корреляционных матриц каждого из векторов: Ky = Kx1 + Kx2.
Пусть A – это матрица размерности n*m и случайный вектор Y размерности n линейно связан
с вектором X размерности m:
Y  AX
Тогда корреляционная матрица вектора Y вычисляется из следующего соотношения:
K y  AK x AT
(33)
На этом закончим с кратким введением в теорию вероятностей. Оно нам понадобилось только
для того, чтобы можно было сформулировать некоторые задачи математической статистики по
обработке результатов измерений, используя введенную терминологию.
Оценка характеристик распределения случайных величин
Пусть X – случайная величина, закон распределения которой неизвестен или известен с
точностью до параметров распределения. Например из общих соображений можно полагать, что X
распределена по нормальному закону, но математическое ожидание и дисперсия неизвестны. Пусть
x1, x2, …xn – независимые измерения значений случайной величины – или, как говорят в статистике,
- выборка из генеральной совокупности объема n. Зная выборку, точно определить параметры
распределения невозможно, но можно получить для них оценку. Пусть a – некоторый параметр
распределения (математическое ожидание, дисперсия, момент порядка k и так далее). Оценкой
параметра ā будем называть любую функцию от данных выборки:
a  f ( x1 , x2 ,
xn )
Понятно, что оценка является случайной величиной, поскольку является функцией от
случайных величин. Среди всех возможных оценок нас будут интересовать оценки, обладающие
такими свойствами как: состоятельность, несмещенность и эффективность. Оценка ā называется
состоятельной, если она сходится по вероятности к истинному значению а. Это означает, что с
увеличением объема выборки вероятность отклонения оценки от истинного значения стремится к
нулю. Оценка ā называется несмещенной, если ее математическое ожидание равно истинному
значению. Несмещенная оценка ā называется эффективной, если она обладает наименьшей
дисперсией в классе несмещенных оценок.
Для параметров распределения случайной величины – mx, Dx, Kxy – математического
ожидания, дисперсии, корреляционного момента применяются естественные оценки:
n
mx 
n
 xi
i 1
n
;
Dx 
n
 ( xi  mx )2
i 1
;
n
K xy 
 (x
i 1
i
 mx )( yi  m y )
;
n
(34)
Все оценки в (34) являются состоятельными. Оценка для математического ожидания является
несмещенной и эффективной, по крайней мере тогда, когда X имеет нормальное распределение.
Оценки для дисперсии и корреляционного момента являются смещенными. Это смещение
минимально и устраняется, если знаменатель в оценке выбрать равным n -1, а не n. На практике
применяют как смещенные, так и несмещенные оценки.
Наряду с точечными оценками (34) часто применяют интервальные оценки, строя так
называемые доверительные интервалы. Под доверительным интервалом можно понимать
случайный интервал, который с некоторой заданной вероятностью (доверительной вероятностью)
накрывает истинное значение параметра. Построить доверительные интервалы для параметров
распределения можно, установив законы распределения для случайных величин, заданных
соотношениями (34). В первом приближении на основе центральных предельных теорем можно
считать, что все три оценки имеют нормальное распределение (асимптотически с ростом n).
Поэтому достаточно найти математическое ожидание и дисперсию оценки. Справедливы
следующие соотношения:
M (mx )  mx  mx ;
M ( Dx )  Dx  Dx ;
D ( mx ) 
Dx Dx

;
n
n
 ( mx ) 

2
x
(n  3) D
D( Dx ) 

;
n
n(n  1)
4
x
x
n
(35)
При подсчете дисперсии оценки D( x) необходимо знать центральный момент четвертого
порядка. Поскольку значение его неизвестно, то, как обычно, можно пользоваться его оценкой,
которую нетрудно получить по аналогии с оценками (34). Но, следует помнить, что для выборок
сравнительно небольшого объема (n <100) оценки моментов порядка k (при k > 2) дают невысокую
точность. В том случае, когда можно предположить, что X имеет нормальное распределение
нетрудно выразить четвертый момент через дисперсию и тогда:
D( Dx ) 
2 Dx2
n 1

2 Dx2
n 1
 ( Dx ) 
;
2 Dx
n 1
(36)
Полагая, что закон распределения для оценок
xи
x известен (асимптотически
стремится к нормальному закону) нетрудно построить интервальную оценку. Проще всего строить
доверительный интервал с центром, заданным точечной оценкой. Пусть β – заданная доверительная
вероятность. Определим параметр k, характеризующий длину интервала, из решения следующего
уравнения:
P ( m  m  k )  
(37)
С учетом соотношений (22) и (24) нетрудно показать, что
P ( m  m  k )  Ф (
k
)
2
(38)
Функция Ф(x) – это функция Лапласа:
Ф( x) 
2

x
e
t 2
dt
(39)
0
Задавая значения k, нетрудно получить вероятность β, удовлетворяющую соотношению (37),
для этого достаточно уметь вычислять интеграл (39). Решать обратную задачу – по β определить k –
труднее. Поскольку на практике точного решения не требуется, то k находят подбором, решая
прямую задачу для разных значений k, пока не будет достигнута требуемая вероятность. Получив
значение k, согласованное с вероятностью β, можно построить доверительный интервал [
,
+
], с заданной вероятностью накрывающий истинное значение параметра. Используя для
и
соответствующие соотношения из (35) и (36), получаем интервальные оценки
соответственно для математического ожидания и дисперсии распределения по данным выборки.
Если известно, что выборка принадлежит нормальному распределению, то можно построить
интервальные оценки другим способом, поскольку в этом случае удается теоретически определить
закон распределения оценок математического ожидания и дисперсии. Первая из этих оценок имеет
распределение Стьюдента, вторая – распределение χ2 (Хи-квадрат).
Наряду с построением по данным выборки точечных и интервальных оценок распределения
часто полезно построить и оценку самого распределения – статистический закон распределения. С
этой целью интервал наблюдения [Xmin, Xmax], где Xmin и Xmax – минимальный и максимальный
элемент в выборке X(x1, x2, …xn), разделим на k интервалов (разрядов) – d1, d2, …dk. Для каждого из
разрядов подсчитаем
i - частоту попадания элементов выборки в i-й разряд. Сумма частот по
всем разрядам очевидно должна быть равна 1. Таблица с элементами {(d1,
1), (d2,
2) …(dk,
k)} задает статистический закон распределения, построенный по данным выборки.
Гистограммой распределения называется график, построенный следующим образом. По оси
абсцисс откладываются разряды, служащие основанием прямоугольников. Высота прямоугольника
равна частному от деления частоты в данном разряде на длину интервала, задающего разряд.
Отсюда следует, что площадь каждого прямоугольника равна частоте разряда, а суммарная
площадь гистограммы равна 1. При равновеликих интервалах высоты прямоугольников
пропорциональны частотам статистического ряда распределения. Гистограмма распределения
является аналогом плотности вероятности для статистического закона распределения.
Рассмотрим пример выборки из 1000 измерений. В таблице 3.1 показано распределение
измерений по 10 разрядам:
Таблица 3.1 Статистический закон распределения
Ном 1
ер разряда
-i
2
Числ
о
7
элементов
- mi
1
Частота -
0
i
,017
3
3
9
1
00
0
,039
4
1
64
0
,1
5
2
39
0
,164
6
1
89
0
,239
7
1
48
0
,189
8
7
3
0
,148
9
2
1
0
,73
1
0
0
,021
Итог
10
1
000
0
1
,01
Гистограмма, построенная по этим данным, приведена на рис.10. И выборка и рисунок
гистограммы получены в проекте, написанном на языке C#.
Рис. 10 Гистограмма распределения
Гистограмма является инструментом анализа, позволяя визуально оценить, насколько
статистический закон распределения согласуется с предполагаемым из теоретических соображений
законом распределения. Например, глядя на гистограмму, приведенную на рис. 10, можно полагать,
что наблюдаемая случайная величина может иметь нормальное распределение, но гипотеза о
равномерном распределении заведомо не имеет место. Существуют формальные методы – критерии
согласия, - позволяющие проверить гипотезу согласованности теоретического и статистического
законов распределения.
Пусть F(x) – теоретический закон распределения наблюдаемой случайной величины,
параметры которого были оценены по результатам выборки. Тогда нетрудно подсчитать
теоретические вероятности pi попадания случайной величины в разряды, заданные статистическим
2
законом распределения. Меру расхождения частот
i и вероятностей pi называют χ и определяют
следующим образом:
k
 2  n
i 1
k
( pi  pi ) 2
(m  npi ) 2
 i
pi
npi
i 1
(40)
Распределение этой меры и называется распределением χ2 и оно не зависит ни от объема
выборки n, ни от распределения F(x). Но оно зависит от параметра k, называемого числом степеней
свободы. Значение k равно числу разрядов, уменьшенное на число связей. Под связями понимается
параметры распределения, оцениваемые по выборке. К связям относится и условие того, что сумма
частот равна 1. Поэтому в нашем примере выборки с 10 разрядами, оценками математического
ожидания и дисперсии число степеней свободы распределения χ2 будет равно 7. Плотность
вероятности распределения χ2 с k степенями свободы имеет вид:
0
 k 2  y
 2 2
f ( y)   y e
 k2  k 
2 Г  
2

y0
y0
(41)
В соотношении (41) Г(x) – это Гамма-функция (обобщение функции n!), которую в данном
частном случае можно вычислить достаточно просто, учитывая ее свойства:
Г ( 1 2)   ;
Г ( x  1)  xГ ( x)
(42)
При k, равном 7, Г(7/2) = 2,5*1,5* √π
Как пользоваться критерием согласия? Предположим, что по данным выборки построен
статистический ряд (смотри таблицу 3.1) и гистограмма. На основе анализа данных в качестве
предполагаемого распределения наблюдаемой величины X построена гипотеза о том, что X имеет
распределение F(x) (например нормальное или равномерное). Тогда, используя оценки параметров,
можно вычислить вероятности попадания случайной величины в разряды статистического
распределения, добавив по сути еще одну строку в таблицу 3.1. После чего нетрудно рассчитать по
формуле (40) меру расхождения теоретического и статистического распределений – χ0. Затем,
используя соотношение (41), посчитать вероятность события P(χ2 > χ0). Если вероятность такого
события достаточно высокая, то расхождение может возникать за счет чисто случайных факторов и
гипотезу о законе F(x) согласуется с данными выборки. Если же эта вероятность мала, то гипотезу
следует отвергнуть и искать другой вид распределения для случайной величины. Вопрос о выборе
значения доверительной вероятности решается в каждом конкретном случае, он не связан ни с
какими математическими соображениями.
Задачи
1.322 Напишите процедуру, генерирующую случайное число,
распределенное по нормальному закону с параметрами 0, 1 (m=0, σ=1).
1.323 Напишите процедуру, генерирующую случайное число,
распределенное по нормальному закону с параметрами m, σ.
1.324 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
равномерного непрерывного распределения на интервале [0, 1]. По
данным выборки постройте точечные и интервальные оценки
математического ожидания и дисперсии.
1.325 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
равномерного непрерывного распределения на интервале [a, b]. По
данным выборки постройте точечные и интервальные оценки
математического ожидания и дисперсии.
1.326 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
нормального непрерывного распределения с параметрами [0,1]. По
данным выборки постройте точечные и интервальные оценки
математического ожидания и дисперсии.
1.327 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
нормального непрерывного распределения с параметрами [m, σ]. По
данным выборки постройте точечные и интервальные оценки
математического ожидания и дисперсии.
1.328 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
равномерного непрерывного распределения на интервале [a, b]. По
данным выборки постройте статистический закон распределения.
Постройте в форме гистограмму распределения. (Рисование в формах
описано в главе 24 учебника).
1.329 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
нормального непрерывного распределения с параметрами [m, σ]. По
данным выборки постройте статистический закон распределения.
Постройте в форме гистограмму распределения. (Рисование в формах
описано в главе 24 учебника).
1.330 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
равномерного непрерывного распределения на интервале [a, b]. По
данным выборки постройте статистический закон распределения.
Вычислите критерии согласия статистического закона распределения с
нормальным и равномерным законами распределения.
1.331 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
нормального непрерывного распределения с параметрами [m, σ]. По
данным выборки постройте статистический закон распределения.
Вычислите критерии согласия статистического закона распределения с
нормальным и равномерным законами распределения.
1.332 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
равномерного непрерывного распределения на интервале [a, b]. По
данным выборки постройте статистический закон распределения.
Постройте в форме гистограмму распределения и плотность
равномерного распределения, полученную на основе оценок
математического ожидания и дисперсии. (Рисование в формах описано
в главе 24 учебника).
1.333 Напишите процедуру, генерирующую выборку X(x1, x2, …xn) из
нормального непрерывного распределения с параметрами [m, σ]. По
данным выборки постройте статистический закон распределения.
Постройте в форме гистограмму распределения и плотность
нормального распределения, полученную на основе оценок
математического ожидания и дисперсии. (Рисование в формах описано
в главе 24 учебника).
1.334 Напишите процедуру, генерирующую случайный вектор Y = (Y1, Y2,
… Ym). Компонента Y1 этого вектора представляет случайную
величину Y1 N(m, σ), а остальные – это функции от Y1: Y2 = |Y1|; Y3
= Sin(Y1); Y4 = a*Y12 + b*Y1 +c. Постройте выборку, представляющую
прямоугольную матрицу с элементами Ykj, где элементы первого
столбца генерируются случайным образом, а остальные элементы
вычисляются, как соответствующие функции. По данным выборки
рассчитайте для вектора Y оценки математического ожидания,
корреляционную матрицу и матрицу, составленную из коэффициентов
корреляции.
1.335 Напишите процедуру, генерирующую случайный вектор Y = (Y1, Y2,
… Ym). Компонента Y1 этого вектора представляет случайную
величину Y1 R(e, f), а остальные – это функции от Y1: Y2 = |Y1|; Y3 =
Sin(Y1); Y4 = a*Y12 + b*Y1 +c. Постройте выборку, представляющую
прямоугольную матрицу с элементами Ykj, где элементы первого
столбца генерируются случайным образом, а остальные элементы
вычисляются, как соответствующие функции. По данным выборки
рассчитайте для вектора Y оценки математического ожидания,
корреляционную матрицу и матрицу, составленную из коэффициентов
корреляции.
1.336 Напишите процедуру, генерирующую случайный вектор Y = (Y1, Y2,
… Ym). Компонента Y1 этого вектора представляет случайную
величину Y1 N(m, σ), а остальные – это функции от Y1: Y2 = Y12; Y3
= Cos(Y1); Y4 = a*Y1 + b*. Постройте выборку, представляющую
прямоугольную матрицу с элементами Ykj, где элементы первого
столбца генерируются случайным образом, а остальные элементы
вычисляются, как соответствующие функции. По данным выборки
рассчитайте для вектора Y оценки математического ожидания,
корреляционную матрицу и матрицу, составленную из коэффициентов
корреляции.
1.337 Напишите процедуру, генерирующую случайный вектор Y = (Y1, Y2,
… Ym). Компонента Y1 этого вектора представляет случайную
величину Y1 R(e, f), а остальные – это функции от Y1: Y2 = Y12; Y3 =
Cos(Y1); Y4 = a*Y1 + b*. Постройте выборку, представляющую
прямоугольную матрицу с элементами Ykj, где элементы первого
столбца генерируются случайным образом, а остальные элементы
вычисляются, как соответствующие функции. По данным выборки
рассчитайте для вектора Y оценки математического ожидания,
корреляционную матрицу и матрицу, составленную из коэффициентов
корреляции.
1.338 Постройте модель «Рост-Вес». Пусть r – случайная величина,
задающая рост случайно выбранного человека (в сантиметрах), r 
N(mr, σr). Случайная величина b, задающая вес этого человека (в
килограммах), вычисляется по формуле:
b = mb + b1*p1 +| b2|*p2 - b3*p3,
mb = mr – 100,
где случайная величина b1 N(0,1) характеризует случайные
отклонения от нормы, а ее коэффициент p1 = 0,05 mb. Случайная
величина b2 N(0,1) характеризует уровень наследуемого ожирения,
ее коэффициент p2 = 0,25 mb. Случайная величина b3 N(0,1)
характеризует образ жизни, ее коэффициент p3 = 0,15 mb.
Постройте выборку, моделирующую рост и вес N человек. По данным
выборки постройте точечные и интервальные оценки математического
ожидания и дисперсии величин r и b, а также постройте
корреляционную матрицу и матрицу, составленную из коэффициентов
корреляции.
1.339 Но основе модели «Рост-Вес» (смотри предыдущую задачу 3.89)
постройте расширенную модель «Рост-Возраст-Вес». Предложите
соответствующие соотношения, связывающие вес с ростом и
возрастом. Постройте выборку и по ее данным постройте точечные и
интервальные оценки математического ожидания и дисперсии
величин, а также постройте корреляционную матрицу и матрицу,
составленную из коэффициентов корреляции.
1.340 Постройте модель «Снаряд», в которой вылетевший из пушки снаряд
рассматривается как тело, брошенное с начальной скоростью V под
углом α к горизонту. Сопротивление воздуха и кривизна земли не
учитывается.
Пусть производится N опытов, в каждом из которых V и α
рассматриваются как случайные величины V N(V0, σv), α N(α0, σα).
Если ввести декартову систему координат, с началом в точке выстрела
и осью X, лежащей в плоскости полета, то координата точки падения
xп вычисляется по формуле:
V 2 sin(2 )
xп 
g
Постройте выборку случайного вектора (V, α, xп). Определите его
статистические характеристики – оценки математического ожидания,
дисперсии, корреляционной матрицы, коэффициентов корреляции.
Постройте интервальные оценки параметров, гистограмму
распределения параметра xп. Вычислите критерии согласия
статистического закона распределения xп и теоретического для двух
гипотез – нормального и равномерного.
1.341 Постройте модель «Снаряд», в которой вылетевший из пушки снаряд
рассматривается как тело, брошенное с начальной скоростью V под
углом α к горизонту. Сопротивление воздуха и кривизна земли не
учитывается.
Пусть производится N опытов, в каждом из которых V и α
рассматриваются как случайные величины V R(V0, V1), α R(α0, α1).
Если ввести декартову систему координат, с началом в точке выстрела
и осью X, лежащей в плоскости полета, то координата точки падения
xп вычисляется по формуле:
V 2 sin(2 )
xп 
g
Постройте выборку случайного вектора (V, α, xп). Определите его
статистические характеристики – оценки математического ожидания,
дисперсии, корреляционной матрицы, коэффициентов корреляции.
Постройте интервальные оценки параметров, гистограмму
распределения параметра xп. Вычислите критерии согласия
статистического закона распределения xп и теоретического для двух
гипотез – нормального и равномерного.
1.342 Пусть Y – это случайный вектор с n независимыми компонентами yk
N(0, σk). Вектор Z связан с вектором Y линейной зависимостью: Z =
AY, где A – матрица размерности m*n. Постройте выборку и
вычислите оценку для корреляционной матрицы вектора Z. Сравните
оценку корреляционной матрицы с точным значением, полученным по
формуле:
Kz = AKyAT
В этом соотношении корреляционная матрица Ky вектора Y является
диагональной матрицей.
1.343 Пусть Y – это случайный вектор с n независимыми компонентами yk
R(-dk, dk). Вектор Z связан с вектором Y линейной зависимостью: Z
= AY, где A – матрица размерности m*n. Постройте выборку и
вычислите оценку для корреляционной матрицы вектора Z. Сравните
оценку корреляционной матрицы с точным значением, полученным по
формуле:
Kz = AKyAT
В этом соотношении корреляционная матрица Ky вектора Y является
диагональной матрицей.
1.344 Пусть X – это случайный вектор с m независимыми компонентами xk
N(0, σkx). Пусть Y – это случайный вектор с n независимыми
компонентами yk N(0, σky). Вектора X и Y независимы. Вектор Z
связан с векторами X и Y линейной зависимостью: Z = X+AY, где A –
матрица размерности m*n. Постройте выборку и вычислите оценку для
корреляционной матрицы вектора Z. Сравните оценку корреляционной
матрицы с точным значением, полученным по формуле:
Kz = Kx + AKyAT
В этом соотношении корреляционные матрицы Kx и Ky являются
диагональными матрицами.
1.345 Пусть X – это случайный вектор с m независимыми компонентами xk
R(-dkx, dkx). Пусть Y – это случайный вектор с n независимыми
компонентами yk R(-dky, dky). Вектора X и Y независимы. Вектор Z
связан с векторами X и Y линейной зависимостью: Z = X+AY, где A –
матрица размерности m*n. Постройте выборку и вычислите оценку для
корреляционной матрицы вектора Z. Сравните оценку корреляционной
матрицы с точным значением, полученным по формуле:
Kz = Kx + AKyAT
В этом соотношении корреляционные матрицы Kx и Ky являются
диагональными матрицами.
Проекты
1.346 Постройте класс NormalRandom – аналог класса Random библиотеки
FCL и интерфейс для работы с этим классом. У класса два
конструктора – один без параметров, позволяющий получать серии
случайных чисел без повторения, другой – с параметром типа int
позволяет получить повторяющуюся серию. Перегруженный метод
Next() возвращает очередное случайное число  N(0, 1), а реализация
Next(m, σ) N(m, σ). Две реализации метода NextArray возвращают
массив случайных чисел.
1.347 Постройте класс Stat и интерфейс для работы с этим классом. Методы
класса Stat по выборке случайной величины X должны позволять
вычислять точечные и интервальные оценки параметров
математического ожидания, дисперсии, среднеквадратического
отклонения, статистический закон распределения и визуально
отображать гистограмму, например так, как это показано на рис. 10.
Методы должны также позволять вычислять критерии согласия
статистического и теоретического ( нормального, равномерного)
законов распределения.
1.348 Постройте класс StatVector и интерфейс для работы с этим классом.
Методы класса StatVector по выборке случайного вектора X должны
позволять вычислять точечные оценки вектора математического
ожидания, корреляционной матрицы и матрицы коэффициентов
корреляции. Для заданной компоненты вектора необходимо строить
статистический закон распределения и визуально отображать
гистограмму, например так, как это показано на рис. 10.
1.349 По аналогии с моделями «Рост-Вес» и «Снаряд» (смотри задачи 3.91 и
3.92) предложите собственную модель, включающую случайные
величины и случайные вектора. Используйте в модели класс Random
библиотеки FCL, классы NormalRandom, Stat, StatVect (смотри задачи
3.97, 3.98, 3.99).
Оценка ненаблюдаемых параметров
В предыдущем разделе рассматривалась ситуация, когда проводились измерения случайной
величины Y (случайного вектора) параметры распределения которого были неизвестны, и
требовалось найти оценку этих параметров или параметров известных функций наблюдаемой
величины. Теперь рассмотрим обратную задачу, не менее часто встречающуюся на практике. Пусть
по-прежнему наблюдается случайная величина Y. Вектор измерений Y (y1, y2, …yn) задается
следующим соотношением:
Y  X ( a, t )  E
(43)
В соотношении (43) X(a, t) – функционально известная в общем случае нелинейная векторфункция, которую будем называть измеряемой функцией. Векторными аргументами ее являются
вектор неизвестных параметров а размерности m и известный вектор t (чаще всего, в роли t
выступает время – измерения проводятся во времени). Неизвестный вектор E рассматривается как
вектор ошибок измерений. Полагается, что E – это случайный вектор с известным законом
распределения.
Итак, задача теперь формулируется следующим образом. Требуется найти оценки
ненаблюдаемого вектора параметров a по измерениям функции (функций), зависящей от этих
параметров в предположении, что измерения сопровождаются ошибками, закон распределения
которых известен. Вот пример подобной ситуации. Спутник движется по орбите, требуется
определить параметры этой орбиты, наблюдая в моменты времени (t1, t2, … tn) координаты
спутника в полярной системе координат – D(t), ε(t), β(t). Другой пример – врач пытается определить
состояние внутренних органов больного, наблюдая за доступными измерению параметрами –
давлением, температурой, кардиограммой.
При рассмотрении модели ошибок измерений зачастую делается предположение о том, что
вектор E имеет многомерное нормальное распределение, которое полностью характеризуется
вектором математических ожиданий и корреляционной матрицей KE. Когда систематические
ошибки в измерениях отсутствуют, то математическое ожидание вектора E является нулевым
вектором. При этих предположениях вектор измерений Y имеет нормальное распределение с X(a,t)
в качестве вектора математического ожидания и корреляционной матрицей KE.
Модель ошибок измерений может быть и более сложной. Рассмотрим ситуацию, когда вектор
E представлен следующим образом:
E  E f  Ec
(44)
В соотношении (44):
Ef – вектор, составленный из независимых компонентов, распределенных по нормальному
закону N(0,σfi), характеризует так называемую флюктуационную (шумовую) составляющую
ошибок измерений;
Ec – представляет сингулярную составляющую ошибок, определяемую матричным
уравнением
(45)
Ec  H * b
Здесь:
H – известная неслучайная матрица,
b – случайный нормальный вектор с диагональной корреляционной матрицей Kb.
Сингулярная составляющая может иметь различную природу, например, включать медленно
меняющиеся систематические ошибки. При условии некоррелированности шумовой и сингулярной
составляющих корреляционная матрица вектора ошибок задается соотношением:
KE  K f  Kc  K f  HKb H T
(46)
На практике размерность вектора b существенно меньше размерности вектора E и потому для
обращения матрицы KE следует пользоваться ранее рассмотренной специальной схемой (21).
Для оценки вектора параметров а в статистике широко применяются метод максимума
правдоподобия и метод наименьших квадратов. При определенных предпосылках оценки,
полученные по этим методам, обладают важными свойствами асимптотической несмещенности и
эффективности.
Метод максимума правдоподобия находит оценки а, которые обеспечивают максимальной
значение условной плотности вероятности P(Y/a). Если нет априорных данных, то это то
наилучшее, что можно сделать с вероятностной точки зрения. Если предположить, что часто вполне
обосновано, что вектор ошибок измерений E – имеет нормальное распределение с известной
корреляционной матрицей R, то оптимальные оценки по методу максимума правдоподобия
находятся из условия минимизации функционала правдоподобия:
F (a)  (Y  X (a, t ))T R 1 (Y  X (a, t )
(47)
Как найти минимум этого функционала? Точное решение возможно, когда X(a, t) линейно
зависит от параметров:
X (a, t )  Xa
(48)
В соотношении (48) X – это прямоугольная матрица размерности n*m, элементы которой
зависят от t. Нетрудно показать, что минимум функционала (52) при условии выполнения (48)
достигается при:
a  I 1 X T R 1Y
(49)
Матрица I в соотношении (49) называется информационной матрицей Фишера, определяемой
соотношением:
I  X T R 1 X
(50)
совпадает с матрицей, обратной к матрице Фишера:
M (a)  a; K a  I 1
(51)
Для нелинейной измеряемой функции X(a,t) в общем случае не удается получить точное
решение, минимизирующее функционал правдоподобия, и минимум ищется численными методами.
Рассмотрим итерационный метод минимизации, основанный на идее линеаризации функции X(a, t).
s
a  a0    j a j
(52)
j 1
В соотношении (52)
а0 – вектор начального приближения,
Δаj – вектор поправок на j-й итерации,
λj – величина шага на j-й итерации,
s – общее число проведенных итераций.
Вектор поправок на каждой итерации определяется из решения системы линейных уравнений:
a  I 1 X T R 1 (Y  X (a, t ))
(53)
Индекс номера итерации в соотношении (53) опущен. Соотношение (53) для нахождения
поправок на каждой итерации подобно соотношению (49). В роли матрицы X, ранее определяемой
соотношением (48), теперь выступает матрица частных производных измеряемой функции по
параметрам:
X  xi ,k 
dxi (a, t )
dak
i  1...n, k  1...m
(54)
Матрицы X и I, вектор X(a,t) в соотношении (53) на каждой итерации вычисляются по
значению вектора оценок, полученному на предыдущей итерации. Естественно, что на первой
итерации используется задаваемый вектор начальных приближений, от выбора которого
существенно зависит скорость сходимости и возможность сходимости к интересующему нас
решению. В нелинейном случае минимизируемый функционал в пространстве параметров не
обязательно является выпуклым и может иметь локальные минимумы, что затрудняет поиск
решения и приводит к необходимости повторения поиска с различными начальными
приближениями.
Обсуждая реализацию итерационного процесса, остановимся на трех вопросах – как
вычислять X - матрицу частных производных, как выбирать величину шага λ на каждой итерации,
какой критерий использовать для завершения итерационного процесса.
В ряде случаев измеряемая функция X(a, t) может быть достаточно простой, так что
соотношения для вычисления производных могут быть получены аналитически. Но получать
аналитические соотношения совсем не требуется. Для итерационного процесса достаточно
использовать разностный метод вычисления производных. Этот способ вполне приемлем,
поскольку не требуется высокая точность в вычислении производных. В этом случае xi,k в
соотношении (54) вычисляются через измеряемую функцию следующим образом:
xi ,k 
X (ak   ak , ti )  X (ak , ti )
;  ak  105 ak
 ak
(55)
Величину шага в простейшем случае можно выбирать постоянной для всех итераций. Более
разумной стратегией является адаптация шага по ходу итерационного процесса. Начальное
значение шага следует выбирать в диапазоне [0,5 – 1]. Если несколько подряд идущих итераций
оказались успешными (значение функционала уменьшалось), то следует увеличить шаг. Если же с
текущим шагом значение функционала возросло, то это свидетельствует, что шаг велик и его
следует уменьшить.
Для прерывания итерационного процесса можно использовать один или несколько критериев.
Самым простым из них является задание Smax - максимального числа итераций, по достижению
которого процесс останавливается:
S <Smax
(56)
Другим критерием является значения функционала в достигнутой точке. Процесс следует
остановить, когда значение функционала F(a) становится меньше заданного значения Fmin. Как
выбирать значение Fmin? Понятно, что из-за ошибок измерений значение функционала в точке
минимума не равно нулю. Если измерения не коррелированны, то функционал F(a) является
квадратичным функционалом:
n
F (a)  
i 1
1

2
i
( yi  xi (a, t )) 2
(57)
В точке, соответствующей точному значению параметров a, значение функционала подчинено
распределению χ2 с n степенями свободы. Анализ этого распределения показывает, что вероятность
того, что за счет чисто случайных причин значение функционала будет превосходить значение n,
примерно равна 0,5. Поэтому в качестве критерия остановки можно выбрать следующее условие:
F (a)  cn,
c [0.5, 1]
(58)
Для случая коррелированных измерений или в случае неверного выбора весов это правило
может не работать. В этих ситуациях целесообразно руководствоваться следующими
соображениями. При завышенном значении с итерационный процесс будет прерван до достижения
минимума, при заниженном – минимум будет достигнут и начнутся колебания вокруг точки
мимнимума, а в некоторых случаях возможен уход в окрестность другого минимума. По-видимому,
менее желательна ошибка преждевременного прерывания итерационного процесса и потому
формулой (58) можно пользоваться во всех случаях.
Еще один критерий останова может быть связан с анализом матрицы частных производных X.
Если максимальный по модулю элемент этой матрицы достаточно мал, то это свидетельствует о
достижении локального минимума. Анализировать конечно нужно не столько саму матрицу X,
сколько рассчитанный на ее основе вектор поправок Δа. Так что еще один возможный критерий
останова может выглядеть так:
max ( ak  
(59)
k
Итерационный процесс прерывается, когда выполняется хотя бы одно из условий (56), (58),
(59). Полезно также так организовать пользовательский интерфейс, чтобы квалифицированный
пользователь мог следить за процессом сходимости и прервать его в подходящей точке, пользуясь
неформальными критериями.
Метод наименьших квадратов и другие частные случаи
Оценки по методу максимума правдоподобия можно получать по формуле (49), если
измеряемая функция линейна относительно искомых параметров, и по формулам (52), (53) в
нелинейном случае. При получении оценок предполагается, что известна корреляционная матрица
вектора измерений. Что происходит, когда измерения не коррелированны, или известна только
дисперсия, но не взаимная корреляция? Практически в этом случае схема оценивания остается без
изменения. Вычисления упрощаются за счет того, что корреляционная матрица становится
диагональной, а следовательно, упрощается умножение на эту матрицу, а также ее обращение.
Формально формулы (49), (52), (53) остаются без изменения, но корреляционная матрица ошибок
измерений становится теперь диагональной. Этот вариант метода максимума правдоподобия
широко применяется на практике и носит название метода наименьших квадратов.
Минимизируемый функционал F(a) задается соотношением (57) и оценки минимизируют сумму
квадратов отклонений измеряемой функции от реальных измерений. Дисперсии ошибок измерений
играют роль весовых коэффициентов. На практике оценки по методу наименьших квадратов
используются чаще, чем оценки метода максимума правдоподобия.
В случае, когда измерения выполняются с одинаковой точностью, или отсутствует
достоверная информация о дисперсиях измерений, то всем измерениям приписывается одинаковый
вес, функционал F(a) слегка упрощается и принимает вид:
n
F (a )   ( yi  xi (a, t )) 2
(60)
i 1
Заметьте, в целом схема оценивания не меняется – просто корреляционная матрица
становится теперь единичной. Случай равноточных измерений фактически означает полное
отсутствие знаний о распределении ошибок измерений. Задача нахождения оценок становится
теперь не столько вероятностной, сколько задачей аппроксимации. Вернемся к задаче построения
интерполяционного многочлена Лагранжа. В ней требовалось построить полином степени n,
проходящей через n+1 точку. Когда n велико, гораздо разумнее строить полином степени m (m<n),
который не обязан проходить через все точки, но в соответствие с критерием наименьших
квадратов наименее отклоняется от точек, представляющих результаты измерений. Итак, когда n >
m, а корреляционная матрица задается единичной матрицей, то в этом частном случае задача
превращается в задачу аппроксимации (приближения) измеряемой функции – подбору неизвестных
ее параметров, так чтобы измеряемая функция в некотором роде наилучшим образом приближалась
к результатам измерений.
Еще один важный частный случай этой задачи имеет место тогда, когда n становится равным
m. К этому случаю сводится задача о нахождении решения системы n уравнений с n неизвестными.
В линейном случае соотношение (49) с учетом того, что матрица X становится квадратной, а,
следовательно, может быть обращена, теперь приводится к виду:
a  I 1 X T R 1Y  ( X T R 1 X )1 X T R 1Y  X 1R( X T )1 X T R 1Y  X 1Y
(61)
В нелинейном случае соотношение (43) можно рассматривать как запись системы n
нелинейных уравнений с n неизвестными. Понятно, что любое решение этой системы
одновременно минимизирует соответствующий функционал, используемый для нахождения
оценок. Справедливо и обратное утверждение, что точка глобального минимума функционала
(значение функционала равно нулю с точностью ε) дает решение системы нелинейных уравнений.
В этом случае для нахождения минимума следует использовать схему метода наименьших
квадратов для случая равноточных измерений. Соотношение (53) для расчета вектора поправок
упрощается и принимает вид:
a  I 1 X T R 1 (Y  X (a, t ))  X 1 (Y  X (a, t ))
(62)
Мы рассмотрели три вариации критерия, используемого при нахождении оценок параметров.
Отличаются они тем, насколько известна и используется информация о законе распределения
ошибок измерений – используются ли корреляции или только дисперсии ошибок, а может быть
используется только предположение о нормальном характере ошибок измерений.
Поговорим теперь о программистских аспектах реализации алгоритма оценки. Как
наилучшим способом сочетать общую схему нахождения оценок и частные варианты критериев?
Простой, можно даже сказать примитивный способ, состоит в том, чтобы иметь единую процедуру
для итерационного процесса нахождения оценок, вызывая в ней ту или иную процедуру (комплекс
процедур), задающую соотношения, связанные с вычислением вектора поправок для каждого из
рассмотренных частных случаев. При программировании на C# на эту проблему следует смотреть с
общих позиций ООП. В данной ситуации можно построить систему классов, связанных
отношением наследования. Родительским классом будет класс оценок максимума правдоподобия, а
остальные классы получаются наследованием из него путем специализации. Это классический
вариант применения наследования. Альтернативный подход к решению задачи дает использование
функций высших порядков – функций, аргументы которых являются функции. На C# функции
высших порядков описываются с использованием делегатов. В главе 20 учебника [1]
демонстрируется применение обоих подходов в похожей ситуации.
Задачи
1.350 (Модель 1вектора ошибок измерений). Постройте модель получения
коррелированного вектора ошибок измерений E размерности n.
Указание. Постройте вначале некоррелированный вектор e
размерности m, компоненты которого независимы и принадлежат
распределению N(0, σi). Задайте матрицу X размерности n*m и
вычислите вектор E=Xe. Компоненты вектора E, являясь линейной
комбинацией компонентов вектора e, будут коррелированны.
Вычислите KE - корреляционную матрицу вектора E, равную XTKeX.
Постройте выборку объема S получения вектора E и вычислите оценку
корреляционной матрицы E.
1.351 (Модель 2 вектора ошибок измерений). Постройте модель получения
коррелированного вектора ошибок измерений E размерности n. Пусть
измерения проводятся в заданные моменты времени t1, t2, … tn. Пусть
ошибка измерения в момент tk задается соотношением:
Ek  e0  e1tk  e2t k2  e3t k3
Случайные величины e0, e1, e2, e3 – независимы и подчинены
нормальному распределению с параметрами 0, 1. Вычислите KE корреляционную матрицу вектора E. Постройте выборку объема S
получения вектора E и вычислите оценку корреляционной матрицы E.
1.352 (Модель 3 вектора ошибок измерений). Постройте модель получения
коррелированного вектора ошибок измерений E размерности n в
соответствии с соотношениями (44) – (46). Вычислите KE корреляционную матрицу вектора E. Постройте выборку объема S
получения вектора E и вычислите оценку корреляционной матрицы E.
1.353 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE и вычислите
вектор оценок параметров по методу максимума правдоподобия.
Вычислите корреляционную матрицу вектора оценок параметров.
1.354 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101).Найдите его корреляционную матрицу KE и вычислите
вектор оценок по методу наименьших квадратов, используя только
знание дисперсий ошибок измерений. Вычислите корреляционную
матрицу вектора оценок параметров при этих же предположениях.
1.355 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE и вычислите
вектор оценок по методу наименьших квадратов, не используя знания
корреляционной матрицы (предполагая равноточность измерений).
Вычислите корреляционную матрицу вектора оценок параметров при
этих же предположениях.
1.356 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE и вычислите
три варианта вектора оценок параметров: по методу максимума
правдоподобия; по методу наименьших квадратов, используя только
знание дисперсий ошибок измерений; по методу наименьших
квадратов, не используя знания корреляционной матрицы (предполагая
равноточность измерений). Вычислите три варианта корреляционной
матрицы вектора оценок параметров при этих же предположениях.
1.357 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE и вычислите
вектор оценок параметров по методу максимума правдоподобия.
Вычислите корреляционную матрицу вектора оценок параметров.
1.358 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102).Найдите его корреляционную матрицу KE и вычислите
вектор оценок по методу наименьших квадратов, используя только
знание дисперсий ошибок измерений. Вычислите корреляционную
матрицу вектора оценок параметров при этих же предположениях.
1.359 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE и вычислите
вектор оценок по методу наименьших квадратов, не используя знания
корреляционной матрицы (предполагая равноточность измерений).
Вычислите корреляционную матрицу вектора оценок параметров при
этих же предположениях.
1.360 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE и вычислите
три варианта вектора оценок параметров: по методу максимума
правдоподобия; по методу наименьших квадратов, используя только
знание дисперсий ошибок измерений; по методу наименьших
квадратов, не используя знания корреляционной матрицы (предполагая
равноточность измерений). Вычислите три варианта корреляционной
матрицы вектора оценок параметров при этих же предположениях.
1.361 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE и вычислите
вектор оценок параметров по методу максимума правдоподобия.
Вычислите корреляционную матрицу вектора оценок параметров.
1.362 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103).Найдите его корреляционную матрицу KE и вычислите
вектор оценок по методу наименьших квадратов, используя только
знание дисперсий ошибок измерений. Вычислите корреляционную
матрицу вектора оценок параметров при этих же предположениях.
1.363 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE и вычислите
вектор оценок по методу наименьших квадратов, не используя знания
корреляционной матрицы (предполагая равноточность измерений).
Вычислите корреляционную матрицу вектора оценок параметров при
этих же предположениях.
1.364 Пусть измеряемая функция X(a, t) линейна относительно вектора
параметров a, так что вектор измерений задается соотношением (48).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE и вычислите
три варианта вектора оценок параметров: по методу максимума
правдоподобия; по методу наименьших квадратов, используя только
знание дисперсий ошибок измерений; по методу наименьших
квадратов, не используя знания корреляционной матрицы (предполагая
равноточность измерений). Вычислите три варианта корреляционной
матрицы вектора оценок параметров при этих же предположениях.
1.365 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу максимума правдоподобия, используя соотношения (52) –(59).
Вычислите корреляционную матрицу вектора оценок параметров.
1.366 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу наименьших квадратов, используя только знание дисперсий
ошибок измерений. Вычислите корреляционную матрицу вектора
оценок параметров при этих предположениях.
1.367 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу наименьших квадратов в предположении равноточных
измерений. Вычислите корреляционную матрицу вектора оценок
параметров при этих предположениях.
1.368 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 1, смотри
задачу 3.101). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров.
Получите три варианта оценок: по методу максимума правдоподобия;
по методу наименьших квадратов; по методу наименьших квадратов в
предположении равноточных измерений. Вычислите три варианта
корреляционной матрицы вектора оценок параметров при этих
предположениях.
1.369 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу максимума правдоподобия, используя соотношения (52) –(59).
Вычислите корреляционную матрицу вектора оценок параметров.
1.370 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу наименьших квадратов, используя только знание дисперсий
ошибок измерений. Вычислите корреляционную матрицу вектора
оценок параметров при этих предположениях.
1.371 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу наименьших квадратов в предположении равноточных
измерений. Вычислите корреляционную матрицу вектора оценок
параметров при этих предположениях.
1.372 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 2, смотри
задачу 3.102). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров.
Получите три варианта оценок: по методу максимума правдоподобия;
по методу наименьших квадратов; по методу наименьших квадратов в
предположении равноточных измерений. Вычислите три варианта
корреляционной матрицы вектора оценок параметров при этих
предположениях.
1.373 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу максимума правдоподобия, используя соотношения (52) –(59).
Вычислите корреляционную матрицу вектора оценок параметров.
1.374 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу наименьших квадратов, используя только знание дисперсий
ошибок измерений. Вычислите корреляционную матрицу вектора
оценок параметров при этих предположениях.
1.375 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров по
методу наименьших квадратов в предположении равноточных
измерений. Вычислите корреляционную матрицу вектора оценок
параметров при этих предположениях.
1.376 Задайте нелинейную относительно параметров измеряемую функцию
X(a, t), для которой вектор измерений задается соотношением (43).
Постройте модель вектора ошибок измерений (Модель 3, смотри
задачу 3.103). Найдите его корреляционную матрицу KE. Постройте
итерационный процесс вычисления вектора оценок параметров.
Получите три варианта оценок: по методу максимума правдоподобия;
по методу наименьших квадратов; по методу наименьших квадратов в
предположении равноточных измерений. Вычислите три варианта
корреляционной матрицы вектора оценок параметров при этих
предположениях.
1.377 Пусть P1, P2… Pn – точки на плоскости, координаты xk которых
измеряются точно, а координаты yk которых содержат ошибки
измерений, отвечающие модели 1 (смотри задачу 3.101). Постройте
полином степени m, наилучшим образом (в соответствии с критерием
максимума правдоподобия) проходящий через точки Pk. При n, равном
m, сравните полученный полином с полиномом Лагранжа.
1.378 Пусть P1, P2… Pn – точки на плоскости, координаты xk которых
измеряются точно, а координаты yk которых содержат ошибки
измерений, отвечающие модели 1 (смотри задачу 3.101). Постройте
полином степени m, наилучшим образом (в соответствии с критерием
наименьших квадратов) проходящий через точки Pk. При n, равном m,
сравните полученный полином с полиномом Лагранжа.
1.379 Пусть P1, P2… Pn – точки на плоскости, координаты xk которых
измеряются точно, а координаты yk которых содержат ошибки
измерений, отвечающие модели 1 (смотри задачу 3.101). Постройте
полином степени m, наилучшим образом (в соответствии с критерием
наименьших квадратов для случая равноточных измерений)
проходящий через точки Pk. При n, равном m, сравните полученный
полином с полиномом Лагранжа.
1.380 Примените алгоритм оценки параметров для решения системы
нелинейных уравнений.
Проекты
1.381 Постройте проект на C# «Оценка параметров». В основу проекта
положите семейство классов с наследованием. Проект должен
предоставлять пользователю широкий спектр возможностей, позволяя:
Моделировать вектор измерений с использованием различных моделей;
Оценивать параметры в линейных задачах с использованием разных критериев – максимума
правдоподобия, наименьших квадратов, наименьших квадратов при условии равноточных
измерений, при условии равенства числа измерений и числа оцениваемых параметров. Получать
вероятностные характеристики оценок параметров;
Оценивать параметры в нелинейных задачах с использованием разных критериев – максимума
правдоподобия, наименьших квадратов, наименьших квадратов при условии равноточных
измерений, при условии равенства числа измерений и числа оцениваемых параметров. Получать
вероятностные характеристики оценок параметров;;
Решать системы линейных и нелинейных уравнений. Решать задачу аппроксимации по методу
наименьших квадратов;
Управлять ходом итерационного процесса – возможностью выбора алгоритма задания шага на
каждой итерации, условием окончания итерационного процесса, возможностью слежения за ходом
сходимости и остановки процесса.
1.382 Постройте проект на C# «Оценка параметров 1». В основу проекта
положите классы с делегатами и функциями высших порядков
(callback функциями). Проект должен предоставлять пользователю тот
же спектр возможностей, что и проект «Оценка параметров» (Задача
3.132).
Примеры
Для демонстрации того, как можно решать некоторые задачи из раздела «Теория вероятностей
и математическая статистика», построен проект с именем TVMS. Проект содержит класс
NormalRandom, являющийся аналогом класса Random, интерфейсный класс TestingNormalForm,
позволяющий пользователю работать с методами класса NormalRandom, и класс GistForm,
поддерживающий рисование в форме гистограммы распределения.
Начнем рассмотрение с класса NormalRandom.
/// <summary>
/// Пример класса для работы со случайными числами,
/// распределенными по нормальному закону
/// </summary>
class NormalRandom
{
/// <summary>
/// Закрытое поле класса. Обеспечивает доступ к методам
класса Random
/// </summary>
Random rnd;
/// <summary>
///Конструктор класса. Инициализирует новый экземпляр класса.
//Последовательность не будет повторяться
/// </summary>
public NormalRandom()
{
rnd = new Random();
}
/// <summary>
/// Конструктор класса с параметром. Инициализирует новый
экземпляр класса.
///Последовательность
будет повторяться
/// </summary>
/// <param name="x"></param>
public NormalRandom(Int32 x)
{
rnd = new Random(x);
}
Закрытое поле класса содержит объект класса Random, позволяющий генерировать
равномерно распределенные числа, комбинация которых позволяет получить нормально
распределенное число.
Как и у класса Random, у класса NormalRandom имеется два конструктора. Конструктор без
параметров позволяет получать неповторяющиеся серии случайных чисел, конструктор с
параметром обеспечивает моделирование повторяющихся серий.
Перегруженный метод класса Next позволяет получать очередное случайное число,
распределенное по нормальному закону с параметрами 0 и 1, либо с заданным значением
математического ожидания и среднеквадратического отклонения.
/// <summary>
/// Next - Основной метод класса.
/// </summary>
/// <returns>
/// Возвращает очередное случайное число,
/// распределенное по нормальному закону с параметрами 0, 1
///</returns>
public double Next()
{
const int N = 50;
double res = 0;
for (int i = 1; i <= N; i++)
res += (rnd.NextDouble()-0.5);
res /= Math.Sqrt(N / 12);
return res;
}
/// <summary>
/// Next - Основной метод класса.
/// </summary>
/// <returns>
/// Возвращает очередное случайное число,
/// распределенное по нормальному закону с параметрами m,
sigma
///</returns>
public double Next(double m, double sigma)
{
return Next()* sigma + m;
}
Следующий метод класса также перегружен. Он позволяет за одно обращение получить
выборку из нормального закона – массив случайных чисел.
/// <summary>
/// Метод для получения массива случайных чисел
/// </summary>
/// <param name="ar">
/// Переданный в процедуру массив заполняется случайными
числами
/// распределенными по нормальному закону с параметрами 0, 1
/// </param>
public void NextArray(double[] ar)
{
for (int i = 0; i < ar.Length; i++)
ar[i] = Next();
}
/// <summary>
/// Метод для получения массива случайных чисел
/// </summary>
/// <param name="ar">
/// Переданный в процедуру массив заполняется случайными
числами
/// распределенными по нормальному закону с параметрами m,
sigma
/// </param>
public void NextArray(double[] ar, double m, double sigma)
{
for (int i = 0; i < ar.Length; i++)
ar[i] = Next(m, sigma);
}
В отличие от класса Random класс NormalRandom позволяет решать и обратные задачи,
получая статистические характеристики выборки. Метод StatLaw по выборке получает оценки
математического ожидания и среднеквадратического отклонения, а также строит гистограмму
распределения, вычисляя частоты попадания значений выборки в заданное число интервалов. Весь
интервал значений, в который попадают элементы выборки разбивается на заданное число m
равновеликих интервалов, для каждого из которых подсчитывается число элементов, попавших в
этот интервал.
/// <summary>
/// Метод, позволяющий по выборке случайных чисел
/// вычислить статистические характеристики
/// </summary>
/// <param name="row">
/// В row задается выборка
/// </param>
/// <param name="ar">
/// В ar - возвращается гистограмма /// частоты попадания элементов выборки в интервалы
/// </param>
/// <param name="Mx">
/// Оценка математического ожидания
/// </param>
/// <param name="sko">
/// Оценка среднеквадратического отклонения
/// </param>
public void StatLaw(double[] row, int[] ar, out double Mx,
out double sko)
{
//Получение выборки
int N = row.Length; //объем выборки
int m = ar.Length;
//число интервалов при построении
гистограммы
//Вычисление оценок матожидания и ско
Mx = 0;
for (int i = 0; i < N; i++)
Mx += row[i];
Mx /= N;
sko = 0;
for (int i = 0; i < N; i++)
sko += (row[i] - Mx) * (row[i] - Mx);
sko /= N;
sko = Math.Sqrt(sko);
//[Xmin - Xmax] - интервал значений элементов выборки
double Xmin, Xmax;
Xmin = Xmax = row[0];
for (int i = 1; i < N; i++)
if (Xmin >row[i]) Xmin = row[i];
else if (Xmax < row[i]) Xmax = row[i];
//Построение гистограммы //вычисление частот попадания элементов выборки в m
интервалов
double l = Math.Abs(Xmax - Xmin) / m;
double lim = 0;
for (int i = 0; i < m; i++)
ar[i] = 0;
for (int i = 0; i < N; i++)
{
lim = Xmin + l;
for (int j = 0; j < m; j++)
{
if (row[i] <= lim)
{
ar[j] += 1; break;
}
lim += l;
}
}
}
Еще один метод класса NormalRandom позволяет визуализировать данные, представляя
гистограмму распределения в виде графика (диаграммы) в отдельной форме. Метод создает новый
объект специально спроектированной формы, передавая ему массив, задающий гистограмму. Затем
форма открывается и в ней рисуется гистограмма.
public void DrawGis(int[] ar)
{
GistForm gistf = new GistForm(ar);
gistf.Show();
gistf.DrawGis();
}
Приведу описание класса GistForm:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Text;
using System.Windows.Forms;
namespace TVMS
{
public partial class GistForm : Form
{
int cx, cy;
//размеры формы
int dx, dy;
int lx, ly;
int[] ar;
int m;
Graphics graph;
Pen pen;
Brush brush, brush1;
public GistForm(int[] ar)
{
InitializeComponent();
this.ar= ar;
m = ar.Length;
Init();
}
void Init()
{
int k = 0;
for (int i = 0; i < m; i++)
if (k < ar[i]) k = ar[i];
cx = this.Width; cy = this.Height;
dx = cx / 10; dy = cy / 10;
lx = (cx - 2 * dx) / m;
ly = (cy - 2 * dy) / k;
graph = this.CreateGraphics();
pen = SystemPens.ControlText;
brush1 = SystemBrushes.Control;
HatchStyle hs = HatchStyle.BackwardDiagonal;
brush = new HatchBrush(hs,
Color.Aquamarine,Color.Black);
hs = HatchStyle.ForwardDiagonal;
brush1 = new HatchBrush(hs,
Color.Aquamarine,Color.Black);
}
public void DrawGis()
{
//Рисуем гистограмму на форме
int signal = 1;
Rectangle rct;
Point pnt= new Point();
Size sz= new Size();
int h=0;
for (int i = 0; i < m; i++)
{
h=Convert.ToInt32(ly*ar[i]);
pnt.X =(dx+i*lx);
pnt.Y= cy-dy -h;
sz.Width= lx;
sz.Height = h;
rct = new Rectangle(pnt, sz);
graph.DrawRectangle(pen, rct);
if (signal == 1)
{
graph.FillRectangle(brush, rct);
signal = -signal;
}
else
{
graph.FillRectangle(brush1, rct);
signal = -signal;
}
}
}
}
}
Поля класса GistForm содержат размеры формы, параметры масштабирования при построении
гистограммы, используемые кисти и перья, а также передаваемую конструктору ссылку на массив,
хранящий гистограмму. При вызове конструктора происходит инициализация полей формы.
Единственный метод класса DrawGis рисует в пустой форме гистограмму, масштабируя рисунок
для нормального визуального восприятия. Для красоты соседние интервалы рисуются разными
кистями.
На рис. 11 показана форма интерфейсного класса, обеспечивающая работу конечного
пользователя с классом NormalRandom.
Рис. 11 Форма для работы с классом NormalRandom
Как обычно, форма разделена на три части. Первая часть отводится входным данным, вторая –
выходным, а третья часть содержит команды, выполняющие операции над данными. Пользователь
задает исходные данные в полях первого раздела формы, а затем начинает выполнять команды,
получая необходимые ему результаты.
Полям, отображаемым в форме, соответствуют поля класса формы, задающие внутреннее
представление соответствующих величин. Вот как выглядит соответствующее описание этих
полей:
public partial class TestingNormalForm : Form
{
int N = 0, m = 0;
double MxT; //Математическое ожидание
double skoT; //среднеквадратическое отклонение
double[] X;
//выборка
int[] SX; //статистический закон - гистограмма
double Mx;
double sko;
//оценка матожидания
//оценка среднеквадратического отклонения
NormalRandom nord;
public TestingNormalForm()
{
InitializeComponent();
}
Команда, которая должна выполняться первой, связана с приемом данных, заданных
пользователем, формированием массивов и других необходимых структур данных. Как всегда,
прием данных от пользователя должен контролироваться. В нашем случае первой должна
нажиматься кнопка «Создать контейнеры», обработчик которой выполняет действия, связанные с
приемом входных данных. Вот код этого обработчика:
private void button1_Click(object sender, EventArgs e)
{
//Ввод данных с контролем
try
{
N = Convert.ToInt32(textBox1.Text);
if (N == 0)
{
ArgumentOutOfRangeException ex = new
ArgumentOutOfRangeException();
throw (ex);
}
}
catch (Exception)
{
MessageBox.Show("Некорректно задано значение N");
return;
}
try
{
m= Convert.ToInt32(textBox2.Text);
if (m == 0)
{
ArgumentOutOfRangeException ex = new
ArgumentOutOfRangeException();
throw (ex);
}
}
catch (Exception)
{
MessageBox.Show("Некорректно задано значение m");
return;
}
try
{
MxT = Convert.ToDouble(textBox6.Text);
}
catch (Exception)
{
MessageBox.Show("Некорректно задано значение MxT");
return;
}
try
{
skoT = Convert.ToDouble(textBox5.Text);
}
catch (Exception)
{
MessageBox.Show("Некорректно задано значение skoT");
return;
}
//Создаются контейнеры - массивы X, SX
X = new double[N];
SX = new int[m];
nord = new NormalRandom();
MessageBox.Show("Данные приняты. Массивы созданы");
}
Команда «TestNext» позволяет получить очередное нормальное число с заданными
параметрами и отобразить его в соответствующем окне. При каждом выполнении этой команды в
окне появляется очередное случайное число. Вот обработчик события Click для этой командной
кнопки:
private void button2_Click(object sender, EventArgs e)
{
//Создается случайное число
listBox1.Items.Clear();
X[0] = nord.Next(MxT, skoT);
listBox1.Items.Add(X[0].ToString());
}
Команда «TestNextArray» позволяет получить выборку заданного объема N и отобразить ее в
соответствующем окне, заданном элементом ListBox. Обработчик и здесь выглядит совсем просто:
private void button3_Click(object sender, EventArgs e)
{
//Создается выборка
listBox1.Items.Clear();
nord.NextArray(X, MxT, skoT);
for (int i = 0; i < N; i++)
listBox1.Items.Add(X[i].ToString());
}
После того, как выборка создана, можно выполнить команду «TestStatLaw» для получения
характеристик выборки. И здесь, конечно же, обработчик события Click вызовет соответствующий
метод класса NormalRandom:
private void button4_Click(object sender, EventArgs e)
{
//Создается выборка и другие результаты
listBox1.Items.Clear();
listBox2.Items.Clear();
nord.StatLaw(X, SX, out Mx, out sko);
for (int i = 0; i < N; i++)
listBox1.Items.Add(X[i].ToString());
for (int i = 0; i < m; i++)
listBox2.Items.Add(SX[i].ToString());
textBox3.Text = Mx.ToString();
textBox4.Text = sko.ToString();
}
Теперь, когда гистограмма построена, можно ее визуально отобразить, что и делает команда
«Построить команду», вызывающая метод класса NormalRandom, который в свою очередь вызывает
метод DrawGis формы GistForm:
private void button5_Click(object sender, EventArgs e)
{
//Построить гистограмму
nord.DrawGis(SX);
}
В заключение этой главы взгляните на рис. 12, отображающий гистограмму, построенную по
данным, показанным на рис. 11.
Рис. 12 Гистограмма, построенная по данным выборки, показанной на рис. 11
Раздел 4 Тексты
Компьютеры и тексты
Вначале было слово.
Так говорит история человечества. В истории компьютеров вначале было число. Долгое время
вместо термина «компьютер» использовались аббревиатуры «ЭВМ» (Электронная Вычислительная
Машина) и «ЦВМ» (Цифровая Вычислительная Машина), что подчеркивало цифровую сущность
первых компьютеров. И использовались они тогда в отраслях, связанных с военными
применениями, в зарождающейся космической отрасли, в физике, - в тех областях, где
господствовала цифра. Тогда в почете были физики, а не лирики с их, казалось бы, ненужными
текстами.
Ситуация начала меняться в 60-х годах. И в немалой степени способствовали переменам сами
компьютеры, развитие которых потребовало создания языков программирования. Нотацию для
записи программ на уровне более высоком, чем система команд компьютера, стали называть
«языком». Если первый язык программирования Fortran (Formula Translator) сохранил в своем
названии приверженность к формулам, то последующий языки – AlgoL, CoboL, SnoboL, PL/I несли
в своем имени букву L от слова «язык» (Language). Программы для компьютеров, написанные на
языках программирования, стали восприниматься как тексты. И одной из главных задач,
порожденных внутренними потребностями самого программирования, стала задача трансляции. К
тому времени языкотворчество стало модным, число языков исчислялось сотнями. Для каждого
языка нужен был транслятор - программа, осуществляющая перевод текста с одного
(формального) языка в другой формальный язык (язык системы команд компьютера).
В первых языках программирования – Фортране и Алголе по-прежнему практически
отсутствовали средства представления текстовой информации и работы с ней. В сборнике
упражнений по Алголу [9], подготовленном на факультете ВМК МГУ и вышедшем в 1975 году, нет
ни одного упражнения по работе с текстовой информацией, все упражнения предназначены для
работы с числами. Приведу еще цитату из книги [8], вышедшей у нас в 1980 году и посвященной
обзору расплодившихся тогда языков программирования: «Можно сказать, что для «научных»
языков программирования характерно полное или почти полное отсутствие средств для работы со
строками литер».
Однако время господства цифры уже прошло и ей пришлось уступить символу, занявшему
законное первое место в компьютерных программах. Этому во многом способствовали успехи
математической лингвистики, теории и практики разработки трансляторов, теории формальных
грамматик и теории автоматов. Появились языки логического программирования. В японском
проекте машин пятого поколения делалась ставка на эти языки в надежде на существенное
повышение «интеллекта» программ, немыслимое без работы с текстами. Возникающие здесь задачи
оказались ничуть не проще, а во многом сложнее задач вычислительной математики. Надежды
решить их с помощью компьютера оправдались лишь в малой степени. Например, задача перевода
с одного естественного языка на другой до сих пор решена лишь частично. До сих пор приемлемый
перевод технических текстов требует вмешательства человека. Для художественных текстов
компьютерный перевод может быть полезен лишь как подстрочник. Но не все так плохо, и
несомненные успехи в этой области достигнуты. Интеллектуальные роботы, автомобили, дома становятся частью нашего мира. Мой хороший друг, прекрасный математик и шахматист Леонид
Дмитриевич Иванов полагал, что компьютеры никогда не смогут одолеть шахматного мастера. Он
ошибался. Сегодня чемпион мира еще может соревноваться с компьютером, но завтра справиться с
компьютером в этой игре человеку будет невозможно. Компьютеры считают лучше. Они быстрее
просчитывают варианты, они могут обработать в короткое время огромные массивы разнородной
информации – числа, тексты, картинки и звуки. Что же остается человеку? Многое. Действительно
новую информацию создает человек. Он создает «настоящие» тексты. Компьютеры могут лишь их
обрабатывать.
Появление персональных компьютеров в каждом доме, а затем и появление компьютерных
сетей, создало новую реальность – информационный мир. Ежедневно миллионы людей создают
новые тексты, размещая их в Интернете – этом громадном хранилище текстов. Денно и нощно
поисковые машины перелопачивают эту груду, индексируя их, наводя хоть какой-то порядок,
позволяющий по запросу найти нужный текст. Без людей, создающих тексты, и без компьютеров,
обрабатывающих эти тексты, Интернет, как хранилище информации, был бы бесполезным.
Здесь есть еще одна невидимая сторона дела – алгоритмическая сложность задач, решаемых в
процессе поиска. Пользователям Интернета, далеким от понимания алгоритмов, может казаться
совершенно естественным, что на их запрос уже через секунды выдается большое число ссылок на
тексты с запрашиваемой информацией. Пользователи могут жаловаться, что ссылок слишком
много, не все из них действительно соответствуют запросу, но в целом система работает
удовлетворительно. У специалиста, представляющего, какие объемы текстов следует просмотреть
для получения ответов, работоспособность системы должна вызывать изумление и уважение.
Примитивные алгоритмы работы с текстами не смогли бы привести к успеху поиска.
Интернет далеко не единственная область, где подобные алгоритмы играют важнейшую роль.
Молекулярная биология (и ее раздел - биоинформатика) является сегодня бурно развивающейся
научной областью. Как ни странно, а может быть вполне естественно, что при анализе структур
ДНК и РНК, при расшифровке генома человека работа с текстами играет определяющую роль. В
книге Дэна Гансфилда [Строки], подробно рассматриваются алгоритмы работы с текстами, как
необходимый инструментарий решения задач вычислительной биологии. Приведу из нее некоторые
цитаты, поясняющие, как биологическая информация представляется в виде текста: «Можно
получить биологически осмысленные результаты, рассматривая ДНК как одномерную строку
символов… Аналогичное, но более сильное предположение делается и о белках… Информация,
которая лежит за биохимией, клеточной биологией и развитием, может быть представлена обычной
строкой, составленной из 4-х символов G, А, Т и С. Для биологии организмов эта строка является
исходной структурой данных».
(В одной из задач, приводимых ниже, поясняется смысл используемых символов).
Символьные данные
Дадим формальное определение строки символов и других важных понятий.
Пусть T = {t1, t2, …tn} – конечное множество, которое будем называть алфавитом, а его
элементы – символами алфавита. Строкой s (словом, цепочкой) в алфавите T будем называть
последовательность подряд записанных символов из T – (s = c1c2...cm). Число символов в слове – m
будем называть длиной слова. Пустое слово длины 0 будем обозначать символом ε или константой
вида "". Если s – строка длины m (s = c1c2...cm), то подстрокой s[i, j], где 1 <= i <= j <= m, ,будем
называть часть строки из s, начиная с i-го символа и заканчивая символом j - (cici+1…cj). Подстрока
s[1, j] называется префиксом слова, а подстрока s[i, m] – суффиксом слова.
Алфавит T можно рассматривать как множество слов длины 1. Рассмотрим операцию
конкатенации над множествами, так, что конкатенация алфавита T с самим собой дает множество
всех слов длины 2. Обозначается конкатенация ТТ как Т2. Обозначим через
 Tk – множество слов в алфавите T длины k, - его можно рассматривать как k-кратную
конкатенацию алфавита T;
 T+ - множество всех непустых слов в алфавите T произвольной длины. Это
множество получается, как результат объединения множеств Tk по всем возможным
значениям k;
 T* - множество всех возможных слов в алфавите T. Это множество называется
итерацией алфавита и его можно рассматривать как объединение пустого слова с
множеством T+.
Рассмотрим примеры некоторых алфавитов. Алфавит T2 = {0, 1} – любые данные,
хранимые в памяти компьютера, можно рассматривать, как слова в этом алфавите. Алфавит
T10 = {0, 1, …9} – целые числа без знака в десятичной системе счисления являются словами
в этом алфавите. Четырехбуквенные алфавиты TDNK = {A, T, G, C} и TDNK = {A, U, G, C}
используются для строк, задающих структуру молекул ДНК и РНК. Каждая молекула ДНК и
РНК представляет комбинацию 4-х нуклеотидов: Аденин (А), тимин (Т), цитозин (С) или
гуанин (G) в ДНК; аденин (А), урацил (U), цитозин (С) или гуанин (G) в РНК.
В основе каждого языка лежит некоторый алфавит T. Если под языком G(T) понимать
множество всех слов данного языка в алфавите T, то G(T) является подмножеством T*.
Задать конкретный язык можно разными способами. Для языков программирования широко
применяется аппарат формальных грамматик, позволяющий описать синтаксически
корректные программы.
Определим класс языков, задаваемых регулярными множествами. Регулярное множество
определяется рекурсивно следующими правилами:
 пустое множество, а также множество, содержащее пустое слово, и одноэлементные
множества, содержащие символы алфавита, являются регулярными базисными
множествами;
 если множества P и Q являются регулярными, то множества, построенные
применением операций объединения, конкатенации и итерации – P Q , PQ, P*, Q* –
тоже являются регулярными.
Регулярные выражения представляют удобный способ задания регулярных множеств.
Аналогично множествам, они определяются рекурсивно:
 регулярные базисные выражения задаются символами и определяют
соответствующие регулярные базисные множества, например, выражение f задает
одноэлементное множество {f} при условии, что f — символ алфавита T;
 если p и q – регулярные выражения, то операции объединения, конкатенации и
итерации – p + q, pq, p*, q* — являются регулярными выражениями, определяющими
соответствующие регулярные множества.
По сути, регулярные выражения – это более простой и удобный способ записи регулярных
множеств в виде обычной строки. Каждое регулярное множество, а, следовательно, и каждое
регулярное выражение задает некоторый язык L(T) в алфавите T. Этот класс языков достаточно
мощный, с его помощью можно описать интересные языки, но устроены они довольно просто – их
можно определить также с помощью простых грамматик, например, правосторонних грамматик.
Более важно, что для любого регулярного выражения можно построить конечный автомат, который
распознает, принадлежит ли заданное слово языку, порожденному регулярным выражением. На
этом основана практическая ценность регулярных выражений.
С точки зрения практика регулярное выражение задает образец поиска. После чего можно
проверить, удовлетворяет ли заданная строка или ее подстрока данному образцу. В языках
программирования синтаксис регулярного выражения существенно обогащается, что дает
возможность более просто задавать сложные образцы поиска. Такие синтаксические надстройки,
хотя и не меняют сути регулярных выражений, крайне полезны для практиков, избавляя их от
ненужных сложностей.
Какие операции над строками лежат в основе преобразований текста? Как и для
арифметического типа, это операции отношения, позволяющие сравнивать строки, и некоторый
набор операций, позволяющих из заданных строк получать новые строки.
Строковый тип, также как и арифметический тип, считается упорядоченным. Вначале задается
порядок на символах алфавита, например как в кириллице от «А» до «Я». Порядок на алфавите
порождает порядок на словах, называемый лексикографическим порядком. Не определяя его
формально, скажу, что этот порядок задает расположение слов в словарях. Поэтому на строках
определены операции сравнения не только на равенство, но и на «больше» и «меньше».
Пока не выработаны устоявшиеся обозначения для базисных операций над строками,
подобные «+» или «-» для арифметического типа. Тем не менее, любой язык программирования,
позволяющий работать с текстами, включает такие операции над строками как: конкатенация,
поиск, вставка, удаление и замена подстрок. Эти операции можно считать базисными. С их
помощью один текст можно преобразовать в другой текст, например сделать из «мухи» «слона»,
или осуществить перевод текста с одного языка на другой язык.
Представление символьных данных в C#
Для работы с текстами на языке C# библиотека классов FCL предлагает целый набор
разнообразных классов, сосредоточенных в разных пространствах имен этой библиотеки. Классы
для работы с текстами находятся как в основном пространстве имен System, так и в пространствах
System.Text и System.Text.RegularExpression. Основные, но далеко не все из этих классов, подробно
рассмотрены в учебнике [1]. Они же будут встречаться в задачах этого раздела.
Классы C# для работы с текстами
Рассмотрим несколько основных классов, позволяющих работать с текстовой информацией, char, char[], string, StringBuilder. Класс System.Char (синоним класса char) определяет множество
символов – алфавит языка. Объекты этого класса задают символы, используемые при работе с
текстами в программах C#. Класс относится к значимым типам и реализуется в виде структуры
Char. Для символов класса Char используется двухбайтная кодировка Unicode, определяющая как
мощность алфавита, включающего 216 =65536 различных символов, так и порядок на алфавите.
Если код символа s1 меньше кода символа s2, то символ s1 предшествует в алфавите символу s2 и
справедливо неравенство s1 < s2. По символу нетрудно получить его код, поскольку существует
неявное преобразование из типа Char в целочисленный тип int. Обратное преобразование также
существует, но должно быть явным. Вот простой пример взаимных преобразований:
//преобразования int <-> char
Char s1 = 'Я', s2, s3;
int codes1, codes2, codes3;
codes1 = s1;
codes2 = codes1 - 1; codes3 = codes1 + 1;
s2 = (char)codes2; s3 = (char)codes3;
Console.WriteLine("Codes: {0}, {1}, {2}",
codes1, codes2, codes3);
Console.WriteLine("Symbols: {0}, {1}, {2}",
s1, s2, s3);
В результате работы этого фрагмента будут напечатаны символы «Я», «Ю», «а» и их коды –
1071, 1070, 1072.
Множество символов класса Char велико, что позволило включить в него большинство
алфавитов естественных языков – латиницу, кириллицу, алфавиты иероглифических языков и
другие. Для символов алфавитов естественных языков, как правило, используется плотная
кодировка, сохраняющая принятый на алфавите порядок. Для кириллицы исключением являются
буквы «Ё» и «ё», коды которых 1025 и 1105 находятся вне интервала, задающего коды для других
букв алфавита.
Из символов класса Char строятся строки языка C#. Для построения строк предоставляется
ряд взаимосвязанных классов – Char[], string, StringBuilder и другие. Если s – строка - объект одного
из этих классов, то определен и объект s[i], принадлежащий классу Char, задающий i-й символ
строки s. Каждый из объектов этих классов можно рассматривать как коллекцию символов.
Приведу пример работы с объектами разных классов:
string str1 = "рококо";
StringBuilder str2 = new StringBuilder(str1);
char[] str3 = new char[6] {'р','о','к','о','к','о'};
char ch1 = str1[1], ch2 = str2[3], ch3 = str3[5];
Console.WriteLine( "строки: {0}, {1}, {2}",
str1, str2.ToString(), new string(str3));
Console.WriteLine( "символы 2 - 4 - 6: {0}, {1}, {2}",
ch1, ch2, ch3);
foreach (char ch in str1)
Console.Write(ch.ToString());
Console.WriteLine();
foreach (char ch in str3)
Console.Write(ch.ToString());
// foreach (char ch in str2)
//
Console.Write(ch.ToString());
В результате работы этого фрагмента все строки будут иметь значение «рококо», а все
символы – «о». К массиву символов и к строке string можно применять цикл типа foreach, явно
работая со строкой, как с коллекцией символов. К объектам StringBuilder этот цикл не применим.
В языке C# строку можно задавать массивом символов – Char[]. Этот способ хорош тогда,
когда операции над строкой могут использовать преимущества массива – получать быстрый доступ
к любому символу строки – читать и изменять его. Он удобен при работе со строками постоянной
длины. Для такого представления отсутствуют встроенные операции над строками. Преодолеть
этот недостаток можно, написав собственный класс CharArray, реализовав в нем все базисные
операции. Из рассмотренных нами операций над строками только поиск вхождения подстроки и
замена одной подстроки на другую, равную ей по длине, реализуется без всяких проблем. Все
остальные операции – конкатенация, вставка, удаление, замена подстрок - требуют изменения
размера массива, что требует в C# создания нового массива. Проще всего при реализации таких
операций создавать новый массив, представляющий результат операции. Задача облегчается тем,
что C# допускает динамические массивы, потому создание новой строки – массива требуемого
размера не вызывает трудностей.
Основным способом представления строк в C# является класс string. В этом классе
реализованы все базисные операции над строками. Он позволяет работать со строками переменной
длины. Следует сказать, что ограничения, накладываемые на операции над строками класса string,
являются довольно строгими – ни одна из операций не может изменять содержимое строки.
Невозможно даже в строке s изменить значение символа s[i] – допускается только чтение символа,
но не его замена. Класс string относится к так называемым неизменяемым (immutable) классам - все
операции, требующие изменения строки, создают новую строку. Поскольку при объявлении строк
string не требуется указывать их длину, то для конечного пользователя эти ограничения не
являются обременительными. Другое дело, что при этом может происходить потеря
эффективности. Если, например, изменения в строке состоят в том, что на каждом шаге цикла
изменяется один символ в строке, то при больших циклах накладно каждый раз создавать новую
строку.
В ситуациях, требующих многократного изменения значения строки, язык C# предлагает
использовать специально спроектированный класс StringBuilder. Строки этого класса могут менять
свои размеры в процессе работы с ними, не требуя создания новых объектов.
Классы C#, используемые для представления строк – Char[], string, StringBuilder, связаны
между собой, и из объекта одного класса нетрудно получить объект другого класса. Конструктору
класса string можно передать массив символов, создав тем самым объект класса string. Для
обратного преобразования из string в Char[] следует вызвать метод ToCharArray, которым обладают
объекты класса string. Достаточно вызвать метод ToString объекта StringBuilder для преобразования
объекта класса StringBuilder в класс string. Обратное преобразование можно выполнить, передавая
конструктору класса StringBuilder объект string. Приведенный выше пример демонстрирует
некоторые из этих преобразований.
В зависимости от того, какие операции выполняются над строкой, следует использовать то
или иное ее представление, переходя при необходимости от одного представления к другому. Какие
же методы предлагают соответствующие классы?
Основная группа методов класса Char предназначена для классификации символа –
определения его категории, выяснения, является ли символ цифрой, буквой, разделителем и так
далее.
Класс Char[] является массивом. Он содержит методы, общие для массивов. Часть из них
полезна при работе со строками. Например, метод Reverse позволяет обратить строку, а Sort
отсортировать символы. Методы IndexOf и LastIndexOf позволяют найти первое и последнее
вхождение символа в массив (строку). Если необходимы другие специальные операции, то, как уже
говорилось, их следует реализовать самостоятельно.
Наиболее мощный набор методов для работы со строками предоставляет класс string. В
частности этот класс реализует все базисные операции: вставки подстроки (Insert), удаления
подстроки (Remove), замены подстроки (Replace), выделения подстроки (Substring), определения
индекса вхождения (IndexOf). Весьма полезные взаимообратные операции реализуются методами
Split и Join. Первый из них позволяет создать массив строк, разделяя исходную строку текста на
фрагменты. Для разделения текста используются соответствующие разделители, стоящие между
фрагментами. Например, если строка задает текст некоторой процедуры на языке
программирования, то методом Split можно получить массив, элементы которого являются
операторами этой процедуры. Предложение естественного языка методом Split можно разделить на
отдельные слова. Метод Join выполняет обратную операцию, склеивая массив строк в одну строку с
добавлением разделителей. Поскольку при расщеплении используется множество разделителей, а
при сборке только один из них, то восстановление исходного текста в первоначальном виде не
всегда возможно, но и не всегда требуется.
Класс StringBuilder, как уже отмечалось, выполняет более эффективно базисные операции над
строками, требующие изменения размера строки. Но набор возможных операций со строкой у него
значительно меньше, чем у объектов string.
Благодаря взаимным преобразованиям между классами для одного и того же текста можно
иметь различные представления, и в зависимости от типа операции использовать то или иное
представление. Более подробно познакомиться с классами, позволяющими работать с текстами,
можно в соответствующих главах учебника [1]. А сейчас перейдем к задачам.
Задачи
1.383 Напишите процедуру, подсчитывающую частоту использования группы символов
в заданном тексте. Проведите исследование произведений двух поэтов, подсчитав
частоты использования частоты использования гласных и согласных, глухих и
звонких согласных. Для представления текстов используйте класс Char[].
1.384 Напишите процедуру, подсчитывающую частоту использования группы символов
в заданном тексте. Проведите исследование произведений двух поэтов, подсчитав
частоты использования частоты использования гласных и согласных, глухих и
звонких согласных. Для представления текстов используйте класс string.
1.385 Напишите процедуру, подсчитывающую частоту использования группы символов
в заданном тексте. Проведите исследование произведений двух поэтов, подсчитав
частоты использования частоты использования гласных и согласных, глухих и
звонких согласных. Для представления текстов используйте класс StringBuilder.
1.386 Напишите процедуру, разделяющую исходный текст на предложения. Для
представления текстов используйте класс Char[].
1.387 Напишите процедуру, разделяющую исходный текст на предложения. Для
представления текстов используйте класс string.
1.388 Напишите процедуру, разделяющую исходный текст на предложения. Для
представления текстов используйте класс StringBuilder.
1.389 Исходный текст представляет собой предложение. Напишите процедуру,
разделяющую исходный текст на слова. Для представления текстов используйте
класс Char[].
1.390 Исходный текст представляет собой предложение. Напишите процедуру,
разделяющую исходный текст на слова. Для представления текстов используйте
класс string.
1.391 Исходный текст представляет собой предложение. Напишите процедуру,
разделяющую исходный текст на слова. Для представления текстов используйте
класс StringBuffer.
1.392 Напишите процедуру IsIder, проверяющую является ли исходный текст правильно
построенным идентификатором. Для представления текста используйте класс
Char[].
1.393 Напишите процедуру IsIder, проверяющую является ли исходный текст правильно
построенным идентификатором. Для представления текста используйте класс string.
1.394 Напишите процедуру IsIder, проверяющую является ли исходный текст правильно
построенным идентификатором. Для представления текста используйте класс
StringBuffer.
1.395 Напишите процедуру IsInt, проверяющую является ли исходный текст правильно
построенным целым числом. Для представления текста используйте класс Char[].
1.396 Напишите процедуру IsInt, проверяющую является ли исходный текст правильно
построенным целым числом. Для представления текста используйте класс string.
1.397 Напишите процедуру IsInt, проверяющую является ли исходный текст правильно
построенным целым числом. Для представления текста используйте класс
StringBuffer.
1.398 Напишите процедуру IsFloat, проверяющую является ли исходный текст
правильно построенным числом с плавающей точкой. Для представления текста
используйте класс Char[].
1.399 Напишите процедуру IsFloat, проверяющую является ли исходный текст
правильно построенным числом с плавающей точкой. Для представления текста
используйте класс string.
1.400 Напишите процедуру IsFloat, проверяющую является ли исходный текст
правильно построенным числом с плавающей точкой. Для представления текста
используйте класс StringBuffer.
1.401 Напишите процедуру IsNumber, проверяющую является ли исходный текст
правильно построенным числом. Для представления текста используйте класс
Char[].
1.402 Напишите процедуру IsNumber, проверяющую является ли исходный текст
правильно построенным числом. Для представления текста используйте класс string.
1.403 Напишите процедуру IsNumber, проверяющую является ли исходный текст
правильно построенным числом. Для представления текста используйте класс
StringBuffer.
1.404 Исходный текст представляет описание класса на C#. Напишите процедуру,
выделяющую из этого текста заголовки методов класса с предшествующими им
тегами summary. Для представления текстов используйте класс Char[].
1.405 Исходный текст представляет описание класса на C#. Напишите процедуру,
выделяющую из этого текста заголовки методов класса с предшествующими им
тегами summary. Для представления текстов используйте класс string.
1.406 Исходный текст представляет описание класса на C#. Напишите процедуру,
выделяющую из этого текста заголовки методов класса с предшествующими им
тегами summary. Для представления текстов используйте класс StringBuffer.
1.407 Исходный текст представляет описание класса на C#. Напишите процедуру,
удаляющую из этого текста комментарии. Для представления текстов используйте
класс Char[].
1.408 Исходный текст представляет описание класса на C#. Напишите процедуру,
удаляющую из этого текста комментарии. Для представления текстов используйте
класс string.
1.409 Исходный текст представляет описание класса на C#. Напишите процедуру,
удаляющую из этого текста комментарии. Для представления текстов используйте
класс StringBuffer.
1.410 Исходный текст представляет описание класса на C#. Напишите процедуру,
создающую массив строк, каждая из которых содержит описание одного из методов
класса. Для представления текстов используйте класс Char[].
1.411 Исходный текст представляет описание класса на C#. Напишите процедуру,
создающую массив строк, каждая из которых содержит описание одного из методов
класса. Для представления текстов используйте класс string.
1.412 Исходный текст представляет описание класса на C#. Напишите процедуру,
создающую массив строк, каждая из которых содержит описание одного из методов
класса. Для представления текстов используйте класс StringBuffer.
1.413 Исходный текст представляет описание класса на C#. Напишите процедуру,
создающую массив строк, каждая из которых содержит описание одного из полей
класса. Для представления текстов используйте класс Char[].
1.414 Исходный текст представляет описание класса на C#. Напишите процедуру,
создающую массив строк, каждая из которых содержит описание одного из полей
класса. Для представления текстов используйте класс string.
1.415 Исходный текст представляет описание класса на C#. Напишите процедуру,
создающую массив строк, каждая из которых содержит описание одного из полей
класса. Для представления текстов используйте класс StringBuffer.
1.416 Исходный текст задает оператор языка C#. Напишите процедуру, определяющую
тип оператора. Для представления текстов используйте класс Char[].
1.417 Исходный текст задает оператор языка C#. Напишите процедуру, определяющую
тип оператора. Для представления текстов используйте класс string.
1.418 Исходный текст задает оператор языка C#. Напишите процедуру, определяющую
тип оператора. Для представления текстов используйте класс StringBuffer.
1.419 Напишите процедуру «Строгий Палиндром», определяющую является ли
заданный текст палиндромом. Напомню, палиндромом называется симметричный
текст, одинаково читаемый как слева направо, так и справа налево.
1.420 Напишите процедуру «Палиндром», определяющую является ли заданный текст
палиндромом. При анализе текста:
- пробелы не учитываются;
- регистр не учитывается;
- буквы «е» и «ё», «и» и «й» считаются одинаковыми.
Фраза, которую Мальвина диктовала Буратино, - «А роза упала на лапу Азора»
считается палиндромом.
1.421 Напишите процедуру «Слог», разбивающую слово на слоги. Предложите свой
алгоритм. За основу возьмите следующие правила:
- две подряд идущие гласные рассматриваются как одна гласная;
- число слогов определяется числом гласных букв (с учетом предыдущего правила);
- Если n – число согласных между двумя соседними гласными, то n/2 согласных
относятся к предыдущему слогу, а оставшиеся к следующему. Вот примеры
нескольких разбиений в соответствии с этим алгоритмом: «слог», «сло - во», «прог ноз», «транс – крип - ция», «зоо – ма – га – зин».
Проекты
1.422 Создайте класс CharArray для представления строк и интерфейс для работы с ним.
Методы класса должны включать набор методов класса string. Внутреннее
представление строки должно задаваться массивом символов – Char[]. Методы,
изменяющие размер строки должны реализовываться функциями, как в классе
string, создавая новый объект.
1.423 Создайте класс CharArray для представления строк и интерфейс для работы с ним.
Методы класса должны включать набор методов класса string. Внутреннее
представление строки должно задаваться массивом символов – Char[]. Методы,
изменяющие размер строки должны реализовываться процедурами, как в классе
StringBuffer.
1.424 Создайте класс MyText для работы с текстом. Методы этого класса должны
выполнять должны различные операции над текстом. Примеры некоторых операций
даны в задачах этого раздела. Операции над текстом должны, например, позволять
получать коллекции абзацев, предложений, слов текста, получать абзац,
предложение, слово по его номеру, разбивать слово на слоги.
1.425 Создайте класс MyProgramText для работы с текстом программ на языке C#.
Методы этого класса должны выполнять должны различные операции над текстом
программы. Примеры некоторых операций даны в задачах этого раздела.
Поиск и Сортировка
Задачи поиска и сортировки возникают в самых разных контекстах. Рассмотрим задачу поиска
в следующей постановке. Дан массив Items c элементами типа (класса) T и элемент pattern типа T,
называемый образцом. Необходимо определить, встречается ли образец в массиве и, если да,
определить индекс его вхождения. Задача сортировки состоит в том, чтобы отсортировать массив
Items. Предполагается, что тип T является упорядоченным типом, так что его элементы можно
сравнивать. Задачу можно конкретизировать, полагая, например, что T – это тип string, и
рассматривать поиск и сортировку строковых массивов. Поскольку алгоритмы поиска и сортировки
практически не зависят от типа T, то отложим конкретизацию типа настолько, насколько это
возможно.
Поиск
Рассмотрим три классических алгоритма поиска – линейный поиск, линейный поиск с
барьером, бинарный поиск в упорядоченном массиве.
Линейный поиск
Алгоритм линейного поиска предельно ясен. В цикле по числу элементов сравнивается
очередной элемент массива с образцом. При нахождении элемента, совпадающего с образцом,
поиск прекращается. Если цикл завершается без нахождения совпадений, то это означает, что в
массиве нет искомого элемента. Время работы такого алгоритма линейно. В худшем случае
придется сравнить образец со всеми элементами, в лучшем – с одним, в среднем – число сравнений
равно n/2, где n – число элементов массива. У линейного поиска есть один недостаток. Если образец
не присутствует в массиве, то без принятия предохранительных мер, поиск может выйти за границы
массива, вследствие чего может возникнуть исключительная ситуация. В классическом варианте
линейного поиска приходится на каждом шаге дополнительно проверять корректность значения
текущего индекса.
Чтобы эта простая задача смотрелась интереснее, рассмотрим параметризованный алгоритм с
параметром T, задающим тип элементов, и его реализацию на языке C#. Построим универсальный
класс (класс с родовыми параметрами):
public class Service<T> where T:IComparable<T>
{
}
Класс Service имеет параметр T, на который наложено ограничение – класс T должен быть
наследником интерфейса IComparable, а следовательно, реализовать метод CompareTo этого
интерфейса. Содержательно это означает, что T является упорядоченным классом.
Класс Service будем рассматривать, как сервисный класс, предоставляющий другим
клиентским классам некоторые сервисы, в частности возможность осуществлять поиск в массивах
любого типа. Добавим в этот класс два статических метода, реализующих алгоритм линейного
поиска:
/// <summary>
/// Линейный поиск образца в массиве
/// </summary>
/// <param name="massiv">искомый массив</param>
/// <param name="pattern">образец поиска</param>
/// <returns>
/// индекс первого элемента, совпадающего с образцом
/// или -1, если образец не встречается в массиве
/// </returns>
public static int SearchPattern(T[] massiv, T pattern)
{
for (int i = 0; i < massiv.Length; i++)
if (massiv[i].CompareTo(pattern)==0) return (i);
return (-1);
}
/// <summary>
/// Вариация линейного поиска образца в массиве
/// </summary>
/// <param name="massiv">искомый массив</param>
/// <param name="pattern">образец поиска</param>
/// <returns>
/// индекс первого элемента, совпадающего с образцом
/// или -1, если образец не встречается в массиве
/// </returns>
public static int SearchPattern1(T[] massiv, T pattern)
{
int i = 0;
while((i<massiv.Length)&&
(massiv[i].CompareTo(pattern)!=0))
i++;
if (i == massiv.Length) return (-1); else return (i);
}
Две вариации линейного поиска отличаются лишь деталями. В первой из них проще условие
цикла, но зато в тело цикла встроен оператор if, при выполнении условия которого завершается не
только цикл, но и сам метод. В другой вариации усложнено условие цикла, но тело цикла совсем
простое. В принципе тело цикла можно сделать пустым в этом варианте, внеся увеличение индекса
во второе условие цикла. Но это уже трюк, снижающий ясность понимания программы.
Трюкачество я не приветствую. Какую из эквивалентных версий выбирать – это дело
программистского вкуса.
Поиск с барьером
Алгоритм линейного поиска можно упростить, избавившись от проверки дополнительного
условия, если быть уверенным, что в массиве обязательно присутствует элемент, совпадающий с
образцом. Иногда истинность этого условия следует из знания того, как строился массив и образец
поиска. Но можно добиться выполнения этого условия принудительно, соорудив в массиве
«барьер», препятствующий выходу поиска за границы массива. С этой целью массив расширяется
на один элемент и в качестве последнего элемента записывается «барьер» - образец поиска. В этом
случае поиск всегда найдет образец. Если образца нет среди «родных» элементов массива, то он
встретится в конце в виде «барьера».
Для упрощения больше подходит вторая версия алгоритма линейного поиска. Приведу
реализацию этой схемы:
/// <summary>
/// Линейный поиск с барьером
/// Предусловие: В массиве существует элемент,
/// совпадающий с образцом pattern
/// </summary>
/// <param name="massiv">искомый массив</param>
/// <param name="pattern">образец поиска</param>
/// <returns>
/// индекс первого элемента, совпадающего с образцом
/// </returns>
public static int SearchBarrier(T[] massiv, T pattern)
{
int i = 0;
while (massiv[i].CompareTo(pattern) != 0)
i++;
return (i);
}
Заметьте, сам метод никаких барьеров не строит. Он лишь формулирует предусловие,
требующее существование барьерного элемента в массиве. Ответственность за выполнения
предусловия лежит на клиенте. Тот, кто вызывает метод, тот и должен заботиться о выполнении
предусловия. Таковы принципы проектирования по контракту. Конечно, можно построить другую
реализацию, где ответственность за построение барьера берет на себя сам метод.
Бинарный поиск
У этого метода поиска много синонимичных названий – метод деления пополам, двоичный
или бинарный поиск, метод дихотомии. Все эти названия отражают тот приятный факт, что в
упорядоченном массиве сравнение с одним элементом позволяет вдвое уменьшить число
кандидатов. Для этого достаточно сравнить образец с элементом массива, стоящим в середине.
Если образец совпадает с этим элементом, то элемент найден и поиск завершается. Если образец
меньше срединного элемента, то размеры области поиска сокращаются вдвое - элемент может
находиться лишь в первой половине массива. Если образец больше срединного элемента, он
находится во второй половине массива. Введение двух параметров – start и finish, задающих
границы области поиска, позволяет достаточно просто описать схему алгоритма. Алгоритм
бинарного поиска намного эффективнее линейного поиска в особенности для больших массивов.
Нетрудно понять, что для отсортированного массива из n элементов он требует не более чем log2(n)
сравнений образца с элементами массива. Вот его возможная реализация:
/// <summary>
/// Бинарный поиск образца в упорядоченном массиве
/// Предусловие: Массив упорядочен
/// </summary>
/// <param name="massiv">искомый массив</param>
/// <param name="pattern">образец поиска</param>
/// <returns>
/// индекс элемента, совпадающего с образцом,
/// но не обязательно индекс первого вхождения,
/// -1, если образец не встречается в массиве
/// </returns>
public static int BinSearch(T[] massiv, T pattern)
{
int start = 0, finish = massiv.Length-1, mid = (start+finish)/2;
while (start <= finish)
{
if (massiv[mid].CompareTo(pattern) == 0) return (mid);
if(massiv[mid].CompareTo(pattern) == 1)
finish = mid-1;
else
start = mid+1;
mid = (start+finish)/2;
}
return(-1);
}
Как клиентский класс может пользоваться сервисами универсального класса Service? Приведу
три примера работы с методами класса. Наш первый клиент имеет массив целых чисел. Перед
вызовом метода поиска он гарантирует упорядоченность массива и потому вполне законно
вызывает метод бинарного поиска:
static void Test5()
{
Random rnd = new Random();
const int n = 100;
int[] ar1 = new int[n];
for (int i = 0; i < n; i++)
ar1[i] = rnd.Next(1, 1000);
Array.Sort(ar1);
for (int i = 0; i < n; i++)
Console.Write(ar1[i].ToString() + ", ");
int pat1 = rnd.Next(1, 10);
int k = Service<int>.BinSearch(ar1, pat1);
if (k != -1)
Console.WriteLine("Образец pat1 = {0} найден в
массиве!" +
"\nЭто элемент ar[{1}] = {2} ", pat1, k, ar1[k]);
else
Console.WriteLine("Образец pat1 ={0} не найден!",
pat1);
}
Второй клиент работает с массивом символов. Клиент гарантирует, что элементы массива и
образец поиска принадлежат одному и тому же классу - классу char. Никаких других ограничений
не накладывается. Для поиска вызывается обычный вариант линейного поиска:
static void Test6()
{
Random rnd = new Random();
const int n = 100;
const int st = 1072; //код символа "а"
const int fin = 1103; //код символа "я"
Char[] ar1 = new Char[n];
for (int i = 0; i < n; i++)
ar1[i] = (Char)rnd.Next(st, fin+1);
for (int i = 0; i < n; i++)
Console.Write(ar1[i].ToString() + ", ");
char pat1 = (char)rnd.Next(st, fin);
int k = Service<char>.SearchPattern(ar1, pat1);
if (k != -1)
Console.WriteLine("Образец pat1 = {0} найден в
массиве!" +
"\nЭто элемент ar[{1}] = {2} ", pat1, k, ar1[k]);
else
Console.WriteLine("Образец pat1 ={0} не найден!",
pat1);
}
Третий клиент работает с массивом строк класса string. Он предпочитает использовать метод
поиска с барьером и сам заботится об организации барьера до вызова метода:
static void Test7()
{
int n;
Console.WriteLine("Введите n - число элементов массива");
n = Convert.ToInt32(Console.ReadLine());
string[] ar1 = new string[n+1];
for (int i = 0; i < n; i++)
{
Console.WriteLine("Введите строку - элемент" +
" массива с номером {0}", i);
ar1[i] = Console.ReadLine();
}
string pat1;
Console.WriteLine("Введите строку - образец поиска");
pat1 = Console.ReadLine();
ar1[n] = pat1;
//Выполнено условие метода поиска с барьером
int k = Service<string>.SearchBarrier(ar1, pat1);
if (k != n)
Console.WriteLine("Образец pat1 = {0} найден в
массиве!" +
"\nЭто элемент ar[{1}] = {2} ", pat1, k, ar1[k]);
else
Console.WriteLine("Образец pat1 ={0} не найден!",
pat1);
}
Для более подробного знакомства с универсальными классами рекомендую прочесть главу 22
учебника [1].
Задачи
1.426 Напишите три процедуры поиска (линейного, линейного с барьером, бинарного)
для работы с классом double.
1.427 Напишите три процедуры поиска (линейного, линейного с барьером, бинарного)
для работы с классом StringBuilder.
1.428 Напишите три процедуры поиска (линейного, линейного с барьером, бинарного)
для работы с классом int.
1.429 Напишите три процедуры поиска (линейного, линейного с барьером, бинарного)
для работы с классом string.
1.430 Напишите три процедуры поиска (линейного, линейного с барьером, бинарного)
для работы с классом Person. Класс Person определите сами.
1.431 На основе приведенного описания класса Service создайте собственный
универсальный класс, включающий различные варианты метода поиска. Создайте
Windows-интерфейс для работы с этим классом.
1.432 Создайте DLL на основе класса Service и постройте проекты – консольный и
Windows, в которых есть классы, являющиеся клиентами класса Service.
Сортировка
Задача сортировки формулируется достаточно просто. Дан массив Ar с элементами типа T.
Тип (класс) T является упорядоченным типом, так что для него определена операция сравнения
элементов. Отсортировать массив можно по возрастанию или по убыванию. В первом случае для
всех элементов массива выполняется условие Ar[i] <= Ar[i+1], во-втором – справедливо условие
Ar[i] >= Ar[i+1]. Упорядочив массив по возрастанию, можно вызвать затем метод Reverse для
изменения порядка сортировки. Порядок сортировки можно задавать как параметр метода, что
сказывается лишь на операции сравнения элементов - «больше» или «меньше».
Методов сортировки великое множество. Классическим трудом является третий том
«Искусства программирования» Д. Кнута [Кнут], который так и называется «Сортировки». Одним
из основных критериев классификации методов сортировки является сложность метода сортировки
– временная и емкостная – T(n) и P(n). В первом случае нас интересует время сортировки
произвольного массива из n элементов, во-втором - дополнительная память, требуемая в процессе
сортировки. Говоря о времени сортировки, можно рассматривать минимальное, максимальное или
среднее время сортировки. Обычно под временем сортировки подразумевается число требуемых
операций, которые в свою очередь разделяются на операции сравнения элементов и операции
обмена элементами, когда два элемента Ar[i] и Ar[j] обмениваются местами.
Сортировка за линейное время
За линейное время можно сортировать массивы, элементы которых принадлежат
фиксированному числу видов. Рассмотрим алгоритмы сортировки отдельно для случая двух, трех и
четырех видов. На практике очень часто возникает необходимость делить некоторую совокупность
на две части – «красных» и «белых», «мужчин» и «женщин». Случаю трех видов посвящена
известная задача Дейкстры о «голландском национальном флаге». Вот как Дейкстра формулирует
эту задачу [Структурное]. Элементы в массиве принадлежат трем видам – красные, белые и синие.
Требуется отсортировать массив в порядке следования этих цветов во флаге Голландии. Поскольку
цвета флагов России и Голландии совпадают, то для нас приятнее сортировать массив в порядке
следования этих цветов во флаге России - белые, синие, красные элементы. Задачи, где в массиве 4
вида элементов, встречаются не менее часто. Например молекула ДНК, как уже упоминалось,
представляется строкой (массивом char) в алфавите из четырех символов. Этот массив иногда
требуется отсортировать в некотором заданном порядке следования символов.
С помощью рис. 4_1 поясним идею эффективного алгоритма сортировки, не требующего
дополнительной памяти и сортирующего массив при числе видов m, равном 2, 3 или 4, за один
проход по массиву.
Рис. 4.1 Инвариантное расположение зон массива при m = 2, 3, 4
Рассмотрим вначале случай m =2, когда в массиве есть элементы только двух видов. Для
простоты будем называть их белыми и красными. Все множество индексов элементов массива
разделим на три непересекающихся подмножества – 0-зона, содержащая только белые элементы, 1зона для красных элементов и непроверенная зона для тех элементов, чей цвет не установлен.
Инвариантом, поддерживаемым в проектируемом алгоритме, будет расположение зон, показанное
на рис. 4_1. Массив отсортирован, когда непроверенная зона становится пустой. В этом идея
алгоритма - поддерживать истинность инварианта, сокращая непроверенную зону. В начальном
состоянии 0-зона и 1-зона пусты, а непроверенная зона занимает все множество индексов, так что
ее начальная граница Start = 0, а граница Finish = n-1. В начальном состоянии инвариант считается
истинным. Основной и единственный проход по циклу выполняется по непроверенной зоне до тех
пор, пока эта зона не станет пустой (или состоять из одного элемента). Проверка элементов
начинается с левого конца непроверенной зоны. До тех пор, пока очередной элемент является
белым, расширяется 0-зона и соответственно сокращается непроверенная зона – значение границы
Start увеличивается на 1. В тот момент, когда встречается красный элемент, проверка прекращается
и запускается аналогичный цикл, но теперь уже с правого конца непроверенной зоны. Когда на
правом конце обнаруживается белый элемент, происходит обмен значениями на двух концах
непроверенной зоны. Обмен восстанавливает истинность инварианта. По завершении цикла
непроверенная зона становится пустой, так что 1-зона с красными элементами следует сразу за 0зоной с белыми элементами и массив отсортирован.
Алгоритм легко обобщается на случай 3-х и 4-х видов элементов. В случае трех элементов
появляется дополнительная зона для синих элементов. Инвариантная ситуация расположения зон
показана на рис. 4_1. Внешний цикл устроен аналогичным образом, как и в случае m = 2. Когда на
левом конце обнаруживается элемент, не принадлежащий 1-зоне, то анализируются две возможные
ситуации. Если элемент белый, то он меняется местами с синим элементом, стоящим на границе
между белой и синей зонами. Инвариант восстанавливается и продолжается проверка элементов на
левом конце непроверенной зоны. Если же на левом конце обнаруживается красный элемент,
принадлежащий зоне 3, то непроверенная зона начинает анализироваться с правого конца. Красная
зона расширяется до тех пор, пока не встретится элемент – синий или белый. Синий элемент
потребует одного обмена, а белый – двух обменов для поддержания инварианта.
Случай 4-х элементов восстанавливает симметрию расположения зон – по две зоны справа и
слева от непроверенной зоны. В этом случае анализ на левом и правом конце непроверенной зоны
выполняется одинаковым образом.
Приведу пример реализации алгоритма сортировки для случая классификации двух видов.
Главная цель примера не столько в том, чтобы продемонстрировать сам алгоритм – он достаточно
прост, а в том, чтобы показать, как на C# написать универсальный алгоритм для элементов любого
типа и с разными названиями видов. Хочется, чтобы алгоритм можно было использовать для
классификации элементов 0 и 1, «мужчин» и «женщин», «красных» и «белых».
Добавим в ранее построенный класс Service с родовым параметром T два поля:
//Поля и методы сортировки видов
/// <summary>
/// число видов
/// </summary>
static int m;
/// <summary>
/// массив, задающий возможные виды элементов
/// </summary>
static T[] Spacimen;
Прежде, чем вызывать метод сортировки, клиентский класс должен будет позаботиться об
уведомлении класса Service, какие виды элементов могут встретиться в сортируемом массиве. Для
передачи этой информации в классе Service есть специальный метод:
/// <summary>
/// Задать информацию, необходимую методам видовой сортировки
/// </summary>
/// <param name="m1"> число видов, которым принадлежат
/// элементы в сортируемом массиве</param>
/// <param name=" Spacimen1"> массив значений видов </param>
public static void InitSpacimen(int m1, T[]Spacimen1)
{
m = m1;
Spacimen = Spacimen1;
}
Метод InitSpacimen должен вызываться один раз и предшествовать любому вызову метода
сортировки. Приведем теперь текст самого метода сортировки:
/// <summary>
/// Сортировать массив,
/// Предусловие: Элементы массива принадлежат двум видам,
/// заданным в массиве Spacimen
/// </summary>
/// <param name="ar">сортируемый массив</param>
public static void SortTwoKinds(T[] ar)
{
int start = 0, finish = ar.Length - 1;
T val1 = Spacimen[0], val2 = Spacimen[1];
while (start < finish)
{
while ((start < finish) && (ar[start].CompareTo(val1) == 0))
start++;
while ((start < finish) && (ar[finish].CompareTo(val2) == 0))
finish--;
//обмен
T temp = ar[start]; ar[start]= ar[finish];
ar[finish] = temp;
start++; finish--;
}
}
Тот факт, что элементы сортируемого массива могут быть экземплярами произвольного
класса T, обеспечивается тем, что класс Service является универсальным классом с параметром T.
Тот факт, что виды элементов могут иметь произвольные значения, обеспечивается тем, что классу
Service предварительно передается массив Spacimen, хранящий информацию о возможных видах.
Покажем теперь, как клиентский класс может вызывать метод сортировки в конкретной
ситуации:
static void Test8()
{
//Два вида элементов
int m = 2;
string[] cand = new string[m];
cand[0] = "red"; cand[1] = "white";
Service<string>.InitSpacimen(m, cand);
//Моделирование массива ar
Random rnd = new Random();
const int n = 10;
string[] ar = new string[n];
for (int ind, i = 0; i < n; i++)
{
ind = rnd.Next(0, m);
ar[i] = cand[ind];
}
for (int i = 0; i < n; i++)
Console.Write(ar[i] + " ");
Console.WriteLine();
//Сортировка массива ar
Service<string>.SortTwoKinds(ar);
for (int i = 0; i < n; i++)
Console.Write(ar[i]+ " ");
Console.WriteLine();
}
Рассмотренный алгоритм вряд ли целесообразно обобщать на случай, когда число видов более
четырех. Тем не менее, можно построить эффективный алгоритм, когда число видов m известно и
оно заведомо меньше n – числа элементов в массиве. В этом случае можно построить алгоритм
сортировки, работающий за время O(n*log(m)). Идея алгоритма достаточно прозрачна. За один
проход по сортируемому массиву посчитаем, сколько элементов каждого вида находится в массиве.
Для хранения этой информации потребуется дополнительный массив Counts размерности m,
элемент которого Counts[i] задает число элементов вида Spacimen[i] в сортируемом массиве.
Используя этот массив и массив Spacimen можно за время O(n) заполнить сортируемый массив
элементами, следующими в нужном порядке. Основное время алгоритма уходит на формирование
массива Counts, поскольку для каждого элемента нужно определить какому виду он принадлежит,
что требует проведения поиска в массиве Spacimen. Для поиска можно использовать метод
SearchBarrier, на что уйдет время порядка O(m). Время можно сократить до O(log2 (m)), если
использовать бинарный поиск, предварительно отсортировав массив Spacimen. Заметьте, на
сортировку и хранение отсортированного массива понадобится дополнительное время порядка
O(m*log2(m)) и дополнительная память.
Когда число видов m сравнимо по порядку с числом элементов n, то алгоритм становится
эквивалентным по сложности классическим алгоритмам сортировки. Этот способ сортировки
иногда называют сортировкой «черпаками». Если в процессе сортировки нужно хранить не только
ключи, но и связанную с ними информацию, например указатели на объекты, то тогда
действительно нельзя обойтись подсчетом числа элементов одного вида, поскольку для каждого из
элементов нужно сохранять связанную с ним информацию. В этом случае для каждого вида
элементов нужно иметь свой «черпак» - массив, хранящий элементы данного вида. Алгоритм
сортировки, как и в выше описанном случае, состоит из двух этапов. На первом – заполняются
черпаки, на втором – данные из черпаков сливаются в общий массив.
В выше приведенном примере разбиение элементов массива на виды осуществлялось с
помощью представителей – к одному виду относились элементы с одним и тем же значением.
Общий способ классификации состоит в задании классифицирующей функции – int Classification(int
m, T item), которая для каждого элемента item возвращает число, задающее его вид. Аргумент m
этой функции указывает максимальное число видов для этой функции классификации. Обычно
предполагается, что значение, возвращаемое функцией, является целым числом в диапазоне [0, m1], задавая номер вида.
В алгоритме быстрой сортировки Хоара требуется все множество элементов отсортировать на
два подмножества. Вначале располагаются элементы, меньшие некоторого выбранного элемента,
затем все оставшиеся. Для решения этой задачи применяется алгоритм SortTwoKinds с простой
функцией классификации. Понятно, что в конкретных задачах могут встречаться достаточно
сложные функции классификации, так что эту функцию полезно рассматривать как параметр
функции сортировки. Алгоритм сортировки можно построить так, чтобы он не зависел ни от типа
сортируемых элементов (это параметр алгоритма), ни от типа функции, задающей классификацию
элементов, - это еще один параметр алгоритма.
Рекомендую, прежде чем решать задачи, прочитать лекцию 22 учебника [1], посвященную
универсальности – классам с родовыми параметрами, и лекцию 20, в которой рассматриваются
функциональные типы, делегаты и функции высших порядков. Если мы хотим передавать функцию
классификации в качестве параметра функции (процедуре) сортировки, то последняя задается
функцией высшего порядка.
Задачи
1.433 Напишите процедуру Reverse, меняющую порядок элементов массива.
1.434 Напишите функцию FReverse, возвращающую массив с обратным порядком
следования элементов массива, заданного в качестве аргумента.
1.435 Напишите процедуру CreateSpecimen, создающую по массиву ar массив
представителей. Массив представителей отличается от исходного массива тем, что в
нем нет повторяющихся элементов. В том случае, когда нет повторений в исходном
массиве, оба массива будут совпадать.
1.436 Напишите функцию FCreateSpecimen с аргументом, заданным массивом ar,
возвращающую массив представителей. Массив представителей отличается от
исходного массива ar тем, что в нем нет повторяющихся элементов. В том случае,
когда нет повторений в исходном массиве, оба массива будут совпадать.
1.437 Дан массив ar и массив его представителей Spacimen. Напишите процедуру
HowMany, вычисляющую целочисленный массив той же размерности, что и массив
Spacimen. Элемент этого массива с индексом k должен быть равным числу
элементов вида Spacimen[k], содержащихся в массиве ar.
1.438 Дан массив ar и массив его представителей Spacimen. Напишите функцию
FHowMany, возвращающую целочисленный массив той же размерности, что и
массив Spacimen. Элемент возвращаемого массива с индексом k должен быть
равным числу элементов вида Spacimen[k], содержащихся в массиве ar.
1.439 Дан массив ar и функция классификации – int Classification(int m, T item).
Напишите процедуру HowMany, вычисляющую целочисленный массив. Элемент
этого массива с индексом k должен быть равным числу элементов вида k,
содержащихся в массиве ar. Функция классификации должна передаваться
процедуре HowMany в качестве параметра.
1.440 Дан массив ar и функция классификации – int Classification(int m, T item).
Напишите функцию HowMany, вычисляющую целочисленный массив. Элемент
этого массива с индексом k должен быть равным числу элементов вида k,
содержащихся в массиве ar. Функция классификации должна передаваться функции
HowMany в качестве параметра.
1.441 Дан массив представителей Spacimen - массив без повторяющихся элементов и
массив Counts, элементы которого задают для каждого представителя число его
повторений. Напишите процедуру SortAr, создающую массив с повторениями, где
каждый представитель повторяется заданное число раз. Алгоритм должен
выполняться за время порядка O(n), где n – это число элементов создаваемого
массива.
1.442 Дан массив представителей Spacimen - массив без повторяющихся элементов и
массив Counts, элементы которого задают для каждого представителя число его
повторений. Напишите функцию FSortAr, возвращающую массив с повторениями,
где каждый представитель повторяется заданное число раз. Алгоритм должен
выполняться за время порядка O(n), где n – это число элементов создаваемого
массива.
1.443 Напишите процедуру SortTwoKinds – процедуру сортировки массива типа string,
содержащего элементы двух видов. Алгоритм должен выполняться за время
порядка O(n), где n – это число элементов массива. Виды элементов задаются
массивом представителей.
1.444 Напишите процедуру SortTwoKinds – процедуру сортировки массива типа string,
содержащего элементы двух видов. Алгоритм должен выполняться за время
порядка O(n), где n – это число элементов массива. Деление элементов на два вида
задается соответствующей функцией классификации, передаваемой процедуре
сортировки в качестве параметра.
1.445 Напишите процедуру SortThreeKinds – процедуру сортировки массива типа string,
содержащего элементы трех видов. Алгоритм должен выполняться за время
порядка O(n), где n – это число элементов массива. Виды элементов задаются
массивом представителей.
1.446 Напишите процедуру SortThreeKinds – процедуру сортировки массива типа string,
содержащего элементы трех видов. Алгоритм должен выполняться за время
порядка O(n), где n – это число элементов массива. Деление элементов на три вида
задается соответствующей функцией классификации, передаваемой процедуре
сортировки в качестве параметра.
1.447 Напишите процедуру SortFourKinds – процедуру сортировки массива типа string,
содержащего элементы четырех видов. Алгоритм должен выполняться за время
порядка O(n), где n – это число элементов массива. Виды элементов задаются
массивом представителей.
1.448 Напишите процедуру SortFourKinds – процедуру сортировки массива типа string,
содержащего элементы четырех видов. Алгоритм должен выполняться за время
порядка O(n), где n – это число элементов массива. Деление элементов на четыре
вида задается соответствующей функцией классификации, передаваемой процедуре
сортировки в качестве параметра.
1.449 Напишите процедуру SortMKinds1 – процедуру сортировки массива типа string,
содержащего элементы m видов. Алгоритм должен выполняться за время порядка
O(n*m), где n – это число элементов массива. Виды элементов задаются массивом
представителей.
1.450 Напишите процедуру SortMKinds2 – процедуру сортировки массива типа string,
содержащего элементы m видов. Алгоритм должен выполняться за время порядка
O(n*log2(m)), где n – это число элементов массива. Виды элементов задаются
массивом представителей.
1.451 Напишите процедуру SortMKinds – процедуру сортировки массива типа string,
содержащего элементы m видов. Деление элементов на m видов задается
соответствующей функцией классификации, передаваемой процедуре сортировки в
качестве параметра.
1.452 Напишите процедуру SortKinds – процедуру сортировки массива типа string.
Процедура должна вначале определить число представителей в сортируемом
массиве. Затем в зависимости от полученного значения m вызвать одну из
подходящих процедур сортировки.
1.453 Напишите процедуру SortKinds1 – процедуру сортировки массива типа string,
которой в качестве параметра передается функция классификации. Аргумент m
функции классификации определяет число видов сортируемого массива. В
зависимости от значения m следует вызвать одну из подходящих процедур
сортировки.
1.454 Напишите универсальную (с параметром типа T) процедуру SortTwoKinds –
процедуру сортировки массива типа T, содержащего элементы двух видов.
Алгоритм должен выполняться за время порядка O(n), где n – это число элементов
массива. Виды элементов задаются массивом представителей.
1.455 Напишите универсальную (с параметром типа T) процедуру SortTwoKinds –
процедуру сортировки массива типа T, содержащего элементы двух видов.
Алгоритм должен выполняться за время порядка O(n), где n – это число элементов
массива. Деление элементов на два вида задается соответствующей функцией
классификации, передаваемой процедуре сортировки в качестве параметра.
1.456 Напишите универсальную (с параметром типа T) процедуру SortThreeKinds –
процедуру сортировки массива типа T, содержащего элементы трех видов.
Алгоритм должен выполняться за время порядка O(n), где n – это число элементов
массива. Виды элементов задаются массивом представителей.
1.457 Напишите универсальную (с параметром типа T) процедуру SortThreeKinds –
процедуру сортировки массива типа T, содержащего элементы трех видов.
Алгоритм должен выполняться за время порядка O(n), где n – это число элементов
массива. Деление элементов на три вида задается соответствующей функцией
классификации, передаваемой процедуре сортировки в качестве параметра.
1.458 Напишите универсальную (с параметром типа T) процедуру SortFourKinds –
процедуру сортировки массива типа T, содержащего элементы четырех видов.
Алгоритм должен выполняться за время порядка O(n), где n – это число элементов
массива. Виды элементов задаются массивом представителей.
1.459 Напишите универсальную (с параметром типа T) процедуру SortFourKinds –
процедуру сортировки массива типа T, содержащего элементы четырех видов.
Алгоритм должен выполняться за время порядка O(n), где n – это число элементов
массива. Деление элементов на четыре вида задается соответствующей функцией
классификации, передаваемой процедуре сортировки в качестве параметра.
1.460 Напишите универсальную (с параметром типа T) процедуру SortMKinds1 –
процедуру сортировки массива типа T, содержащего элементы m видов. Алгоритм
должен выполняться за время порядка O(n*m), где n – это число элементов массива.
Виды элементов задаются массивом представителей.
1.461 Напишите универсальную (с параметром типа T) процедуру SortMKinds2 –
процедуру сортировки массива типа T, содержащего элементы m видов. Алгоритм
должен выполняться за время порядка O(n*log2(m)), где n – это число элементов
массива. Виды элементов задаются массивом представителей.
1.462 Напишите универсальную (с параметром типа T) процедуру SortMKinds –
процедуру сортировки массива типа T, содержащего элементы m видов. Деление
элементов на m видов задается соответствующей функцией классификации,
передаваемой процедуре сортировки в качестве параметра.
1.463 Напишите универсальную (с параметром типа T) процедуру SortKinds –
процедуру сортировки массива типа T. Процедура должна вначале определить
число представителей в сортируемом массиве. Затем в зависимости от полученного
значения m вызвать одну из подходящих процедур сортировки.
1.464 Напишите универсальную (с параметром типа T) процедуру SortKinds1 –
процедуру сортировки массива типа T, которой в качестве параметра передается
функция классификации. Аргумент m функции классификации определяет число
видов сортируемого массива. В зависимости от значения m следует вызвать одну из
подходящих процедур сортировки.
Проекты
1.465 На основе рассмотренного в этом разделе класса Service постройте собственный
класс, расширив его методами сортировки для разных значений числа видов m.
Постройте Windows-интерфейс, позволяющий клиентам класса Service вызывать его
методы для массивов разных типов.
1.466 Постройте проект, позволяющий сравнивать время, затрачиваемое компьютером
на сортировку массива, когда применяются методы универсального класса Service и
аналогичные методы, написанные для конкретного типа данных. Цель проекта –
показать возможные потери эффективности, как плата за универсальный характер
методов сортировки.
1.467 Постройте проект, позволяющий сравнивать время, затрачиваемое компьютером
на сортировку массива с элементами двух, трех, четырех и m видов, при
использовании методов класса Service и метода быстрой сортировки Хоара,
встроенной в библиотеку FCL.
Методы сортировки за время порядка O(n2)
В ситуациях, когда приходится сортировать массивы небольшой размерности, разумно
пользоваться простыми методами сортировки. Нетрудно доказать, что в тех случаях, когда на
элементы массивов не накладываются никакие дополнительные ограничения кроме
упорядоченности, то наилучшие методы сортировки в среднем требуют времени работы порядка
O(n* log2(n)). Существует целый набор таких эффективных по порядку методов сортировки,
различающихся сложностью реализации. Простые и естественные способы сортировки требуют,
как правило, времени работы порядка O(n2). Эти методы сортируют массивы небольшой
размерности быстрее, чем их соперники - более эффективные по порядку, но и более сложные
методы сортировки. Но понятно, что у каждого из квадратичных методов сортировки есть свой
предел, то максимальное значение n, после которого эффективные методы со сложностью
O(n*log2(n)) начинают работать быстрее.
Рассмотрим алгоритмы сортировки с квадратичной сложностью, начиная с простейших,
интуитивно понятных.
Сортировка SortMin (SortMax) на основе нахождения минимального (максимального) элемента
массива
Две сортировки минимумами и максимумами являются вариациями алгоритма, называемого
часто «простым выбором». Идея алгоритма прозрачна и состоит в том, чтобы найти минимальный
(максимальный) элемент массива и поставить его на первое (последнее) место. Затем применить тот
же прием к массиву без первого (последнего) элемента, повторяя эту схему, пока оставшаяся часть
массива не будет состоять из одного элемента.
Сортировка SortMinMax
Эта сортировка является слегка улучшенным вариантом предыдущей сортировки, когда
минимальный и максимальный элементы находятся одновременно. Они и меняются местами с
первым и соответственно последним элементами текущей части массива. Повышение
эффективности достигается за счет того, что одновременный поиск максимума и минимума можно
выполнить быстрее, чем при раздельном их поиске.
Сортировка SortBubble (SortBall) – пузырьковая сортировка и сортировка «тяжелыми шариками»
Эти две вариации одного алгоритма сортировки относят к классу «обменных сортировок». В
каждом алгоритме сортировки присутствуют операции сравнения элементов и обмена элементов.
Но алгоритмы могут отличаться тем, какие операции превалируют в реализации алгоритма. В
сортировках прямого выбора минимумами и максимумами обмен выполняется только после того,
как сделан выбор нужного элемента, требующий многократных проверок. В обменных сортировках
обмен элементов является основной операцией в процессе сортировки. И те и другие методы имеют
свои достоинства и соответственно недостатки. Операции обмена обычно более дорогие (требуют
больше времени), чем операции сравнения. В этом преимущество методов прямого выбора. Но в
обменных сортировках за один проход не только один элемент становится на свое место, но и
другие элементы стремятся занять свои места, что позволяет ускорить сортировку.
Идея алгоритма пузырьковой сортировки, принадлежащей классу обменных сортировок,
состоит в том, чтобы, начиная с конца массива, сравнивать два соседних элемента и, если
нарушается упорядоченность, производить обмен элементами – более легкий элемент меняется
местами со своим соседом. Очевидно, что при первом проходе массива минимальный элемент, как
самый легкий всплывет наверх, подобно пузырьку воздуха, и станет на первое место. Важно то, что
при этом будут всплывать, приближаясь к своим законным местам и другие легкие элементы.
Обменные сортировки хорошо работают на почти упорядоченных массивах. Достоинство
алгоритма еще и в том, что он позволяет собрать важную информацию - на каждом проходе можно
подсчитывать число обменов. Если оно равно 0, то массив уже упорядочен, и сортировку можно
прекращать.
Алгоритм «тяжелого шарика» является симметричной вариацией пузырьковой сортировки.
Работа начинается с начала массива и в процессе обмена вниз опускаются тяжелые элементы, так
что на первом проходе максимальный элемент станет на последнее место.
Сортировка SortShaker – шейкерная сортировка
Эта сортировка является слегка улучшенным вариантом предыдущей сортировки, когда на
одном проходе применяется алгоритм пузырьковой сортировки, на следующем – алгоритм
тяжелого шарика. Поочередное применение приводит к тому, что подъем легких элементов и
опускание тяжелых выполняется равномерно, что в ряде случаев способствует ускорению процесса
сортировки. Хотя сама идея красивая, но трудно найти какое либо математическое обоснование
эффективности шейкерной сортировки в сравнении с обычным «пузырьком».
Сортировка SortInsert – сортировка вставками
Сортировка вставками – это еще один класс простых методов сортировки. Рассмотрим
простейший вариант этого способа сортировки. Чтобы описать идею алгоритма, предположим
вначале, что массив уже упорядочен за исключением последнего элемента. Тогда задача сводится к
тому, чтобы вставить этот элемент в нужную позицию. Это можно сделать двояко. Во-первых,
можно применить алгоритм, подобный «пузырьку», выполняя обмен, пока последний элемент не
«всплывет» на свое место. В лучшем случае не придется делать ни одного обмена, если последний
элемент – это максимальный элемент и стоит уже на своем месте. В худшем случае придется
сделать n сравнений и n обменов, если последний элемент – это минимальный элемент массива. В
среднем – истина посредине. Другой способ состоит в том, чтобы воспользоваться
упорядоченностью массива. В этом случае место вставки, используя алгоритм бинарного поиска,
можно найти значительно быстрее за log(n) операций. К сожалению, нельзя избежать сдвига всех
элементов массива ниже точки вставки.
Понятно, как идею вставки распространить на весь массив. Рассматриваем начальную часть
массива, как уже упорядоченную. Поскольку часть массива, состоящая из одного первого элемента
упорядочена по определению, то вначале вставляем в эту упорядоченную часть второй элемент
массива, затем третий, пока не дойдем до последнего.
Сортировка SortShell – улучшенный вариант сортировки вставками
Сортировка, предложенная Шеллом, сложнее в реализации, чем ранее рассмотренные простые
методы. Более того, интуитивно она наименее понятна и при знакомстве с ней кажется странным,
что она может давать хорошие результаты. Но эта неочевидность характерна и для других
эффективных методов сортировки. Та же быстрая сортировка Хоара далеко не очевидна, особенно
когда появилась ее первоначальная версия, не использующая рекурсию.
В чем идея алгоритма сортировки Шелла? Зададим последовательность чисел:
hk, hk-1…, h1
Эта последовательность должна быть убывающей и заканчиваться значением h1 = 1. Любая
последовательность чисел с такими свойствами является подходящей и гарантирует сортировку
массива. До сих пор неизвестно, какая последовательность является наилучшей. Желательным
свойством последовательности является взаимная простота чисел hi. Другое свойство требует,
чтобы каждое из них примерно в два раза было меньше предыдущего. Хорошим выбором
считается последовательность чисел, в которой hi = 2i -1. Первый член последовательности hk
подбирается так, чтобы он был примерно равен n/2, где n – размерность массива. При n = 1000
последовательность может быть такой: 511, 255, 127, 63, 31, 15, 7, 3, 1.
Внешний цикл в сортировке Шелла – это цикл по последовательности hi. Каждый член этой
последовательности делит элементы массива на группы, состоящие из элементов массива,
отстоящих друг от друга на расстоянии i. Для выбранной нами последовательности первый член
последовательности создает большое число групп – hk групп, в каждой из которых не более двух
элементов. На следующем шаге число групп уменьшается, а число элементов в них увеличивается.
На последнем шаге при h1 = 1 возникает одна группа, в которую входят все элементы. Суть
алгоритма в том, что к каждой возникающей группе независимо применяется обычный алгоритм
вставки, сортирующий каждую группу. В чем же суть алгоритма. Ведь на последнем этапе ко всему
массиву применяется обычный алгоритм вставки. За счет чего же достигается эффективность
алгоритма? Дело в том, что к последнему этапу массив будет «почти упорядочен», а на таких
массивах алгоритм вставки работает крайне быстро. Действительно, если массив упорядочен, то
алгоритм SortInsert c «пузырьковым обменом» выполнит всего лишь n операций сравнения и ему
вообще не потребуются операции обмена.
Сортировка Шелла хороша еще и тем, что она является прекрасным примером, требующим
изощренного программирования. При написании реализации этого метода сортировки крайне
полезно выписать инварианты циклов и использовать приемы доказательного программирования.
Рекомендую прочесть лекцию 10 учебника [1] и разобрать пример быстрой сортировки Хоара, где
реализация метода сортировки сопровождается записью инвариантов цикла и неформальным
доказательством корректности реализации.
Задачи
1.468 Напишите процедуры SortMin и SortMax для некоторого конкретного типа
элементов массива. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.469 Напишите процедуру SortMinMax для некоторого конкретного типа элементов
массива. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.470 Напишите процедуру SortBubble и SortBall для некоторого конкретного типа
элементов массива. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.471 Напишите процедуру SortShaker для некоторого конкретного типа элементов
массива. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.472 Напишите процедуру SortInsert для некоторого конкретного типа элементов
массива. Для вставки элемента используйте идею «пузырькового» обмена.
Постройте график времени работы процедуры в зависимости от размерности
массива – n.
1.473 Напишите процедуру SortInsert1 для некоторого конкретного типа элементов
массива. Для вставки элемента используйте идею бинарного поиска. Постройте
график времени работы процедуры в зависимости от размерности массива – n.
1.474 Напишите процедуру SortShell для некоторого конкретного типа элементов
массива. Сопроводите реализацию явным выписыванием инвариантов для всех
циклов и неформальным доказательством корректности алгоритма сортировки.
Постройте график времени работы процедуры в зависимости от размерности
массива – n.
1.475 Напишите универсальные процедуры SortMin и SortMax для произвольного типа
T элементов массива. Постройте график времени работы процедуры в зависимости
от размерности массива – n.
1.476 Напишите универсальную процедуру SortMinMax для произвольного типа T
элементов массива. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.477 Напишите универсальные процедуры SortBubble и SortBall для произвольного
типа T элементов массива. Постройте график времени работы процедуры в
зависимости от размерности массива – n.
1.478 Напишите универсальную процедуру SortShaker для произвольного типа T
элементов массива. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.479 Напишите универсальную процедуру SortInsert для произвольного типа T
элементов массива. Для вставки элемента используйте идею «пузырькового»
обмена. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
1.480 Напишите универсальную процедуру SortInsert1 для произвольного типа T
элементов массива. Для вставки элемента используйте идею бинарного поиска.
Постройте график времени работы процедуры в зависимости от размерности
массива – n.
1.481 Напишите универсальную процедуру SortShell для произвольного типа T
элементов массива. Сопроводите реализацию явным выписыванием инвариантов
для всех циклов и неформальным доказательством корректности алгоритма
сортировки. Постройте график времени работы процедуры в зависимости от
размерности массива – n.
Проекты
1.482 Постройте класс Sorting, содержащий методы сортировки, и класс Analyze,
позволяющий анализировать время работы методов сортировки на одних и тех же
массивах. Интерфейс проекта должен поддерживать представление результатов
анализа в виде графиков.
1.483 Постройте универсальный класс Sorting, содержащий методы сортировки
массивов произвольного типа, и класс Analyze, позволяющий анализировать время
работы методов сортировки на одних и тех же массивах. Интерфейс проекта должен
поддерживать представление результатов анализа в виде графиков.
Рекурсивные методы сортировки за время порядка O(n*log2(n))
Большинство эффективных методов сортировки описываются в виде рекурсивных алгоритмов
и реализуются как рекурсивные процедуры. Хотя реализация рекурсивных процедур требует от
разработчиков трансляторов использования стековой памяти, но эта техника сегодня настолько
отработана, что потери на организацию рекурсии становятся незначительными в сравнении с теми
преимуществами, которые дают рекурсивные алгоритмы. Для небольших массивов конечно
квадратичные алгоритмы требуют меньше времени, но чем больше размер массива, тем
эффективнее становится применение рекурсивных методов.
Рекурсивное описание алгоритма, как правило, значительно короче нерекурсивного описания.
Но оно всегда оставляет впечатление некоторого трюка. Казалось бы умеем решать задачу только
для самого простого случая, затем делаем некоторый фокус и задача оказывается решенной и для
сложных случаев. Описать рекурсивное решение просто, гораздо сложнее повторить вычисления
согласно рекурсивному алгоритму. Но, как известно, описанием алгоритмов занимается человек, а
выполнением - компьютер. При анализе алгоритма, отладке соответствующей процедуры и
человеку, конечно же, приходится выполнять рекурсивные вычисления.
Разберем несколько рекурсивных алгоритмов сортировки.
Сортировка слиянием
Задача сортировки массива решается совсем просто, когда массив состоит из одного элемента.
В этом случае ничего делать не нужно, - такой массив считается отсортированным по определению.
Это позволяет написать следующую схему рекурсивного алгоритма сортировки слиянием –
RSortMerge(start, finish):
if(n > 1)
{
RSortMerge(start, mid); //Отсортируй первую половину массива
RSortMerge(mid+1, finish); //Отсортируй вторую половину массива
Merge(); //Слей две половины в единый массив, сохраняя
упорядоченность
}
Задача сортировки свелась к значительно более простой задаче слияния двух упорядоченных
массивов (частей массива) в единый массив.
Нетрудно доказать, что этот алгоритм будет выполняться за время порядка O(n*log(n)).
Вспомним базисную теорему о рекурсии, приведенную в главе 3 (теорема 3.1). Она утверждает,
что, если исходную задачу размерности n удается разбить на две подзадачи размерности n/2, а
затем из решений подзадач за линейное время получить решение исходной задачи, то сложность
решения будет иметь порядок O(n*log(n)). На этом и построен алгоритм сортировки слиянием.
Разбиение исходной задачи на две подзадачи одинаковой размерности не вызывает никаких
сложностей – массив делим пополам. Чтобы выполнить слияние за линейное время, процедуре
Merge необходима дополнительная память. Во избежание дополнительных программистских
сложностей сразу же после слияния можно отсортированный массив переписать в исходный.
Обратная перепись сохраняет линейное время работы слияния.
Алгоритм процедуры слияния Merge достаточно прост. Обычно вводят три целочисленные
переменные – l, r, u, - представляющие индексы левого и правого массивов и массива слияния.
Затем в цикле сравниваются элементы левого и правого массива с текущими значениями индексов l
и r. Если элемент левого массива меньше или равен элемента правого массива, то он
переписывается в массив слияния и происходит сдвиг по левому массиву – увеличивается индекс l.
В противном случае аналогичные операции выполняются над элементом правого массива. Цикл
завершается, когда один из массивов полностью переписан – соответствующий индекс l или r
достигнет предельного значения. После этого в «хвост» массива слияния дописывается остаток
массива, чей индекс не достигнул своего предела.
Еще одно небольшое замечание к реализации алгоритма. Обычно рекурсивная процедура
имеет дополнительные аргументы, позволяющие разбивать задачу на подзадачи. В нашем случае у
процедуры RSortMerge заданы два аргумента, характеризующие начало и конец сортируемой части
массива. Чтобы не требовать у клиента задания этих аргументов при вызове процедуры, обычно
пишут нерекурсивную процедуру без аргументов, единственное назначение которой состоит в
вызове рекурсивной процедуры с исходными значениями аргументов:
public void SortMerge()
{ RSortMerge(0, n-1);}
Быстрая сортировка Хоара
И в этом алгоритме исходная задача разбивается на две подзадачи, объединение решений
которых дает общее решение. Разбиение задачи на две подзадачи одинаковой размерности в этом
случае делается следующим образом. Найдем медиану сортируемого массива – x. По определению
элемент, стоящий посредине сортируемого массива является его медианой. Поэтому элемент х
позволяет разбить исходное множество элементов на два равных подмножества – элементов,
меньших или равных x, и элементов, больших или равных x. Если каждое из подмножеств будет
отсортировано, то будет отсортирован и весь массив, так что на объединение решений не требуется
никаких затрат. Применяя рекурсивно этот алгоритм к каждому из подмножеств, в конечном итоге
приходим к множествам размерности 1, отсортированным по определению.
Чтобы такой способ сортировки работал за время O(n*log(n)), необходимо, уметь находить
медиану массива за линейное время и разбивать множество на два подмножества за линейное
время. Алгоритм решения второй из этих задач за один проход по массиву мы разбирали подробно
– его вариация реализуется в процедуре SortTwoKinds. Для нахождения медианы массива
существуют алгоритмы, укладывающиеся в линейное время. Сам Хоар предложил эффективный
алгоритм поиска медианы, требующий в среднем времени O(n), но в худших случаях требующий
времени порядка O(n2).
В алгоритме, получившем название «быстрой сортировки» Хоара, поиск медианы массивы
вообще не проводится. Вместо этого выбирается случайным образом любой элемент массива. Он и
используется в качестве барьера, разбивающего исходное множество на два подмножества, которые
в этом случае могут иметь разную размерность. В таком варианте сложность алгоритма имеет
порядок O(n*log(n)) только в среднем, а в худшем случае будет иметь сложность О(n2). На практике
оказывается, что именно такой алгоритм является наиболее быстрым в большинстве случаев.
Поэтому он и используется в качестве встроенного алгоритма сортировки для большинства
программных систем. В частности он используется и при работе на C# в методе Sort класса Array из
библиотеки FCL.
Подробный разбор возможной реализации этого алгоритма с неформальным доказательством
его корректности дан в лекции 10 учебника [1].
Рассмотрим идею алгоритма поиска медианы, точнее, его общий вариант – нахождение
квантили массива порядка k. Квантилью порядка k называется элемент для которого в массиве
существует k элементов, меньших или равных квантили, и n - k элементов, больших или равных
квантили. Медиана – это квантиль порядка n/2.
Идея алгоритма основана на возможности разбиения множества на два подмножества
относительно некоторого барьера. Как уже говорилось, эта идея лежит в основе алгоритма быстрой
сортировки. Она же используется и в алгоритме поиска квантили. Опишем ее по отношению к
поиску медианы. Возьмем элемент с номером k = n/2, используем его в качестве барьера x,
разобьем множество элементов на два подмножества. Пусть i и j задают границы этих
подмножеств. Так как в каждое из подмножеств могут входить элементы, равные x, то границы
задают некоторый интервал. Если окажется, что номер k принадлежит интервалу [i, j], то задача
решена и медиана найдена. Если же номер k лежит вне этого интервала, то задача разбиения
повторяется на той части интервала, которой принадлежит номер k. Поскольку длина интервала в
среднем уменьшается в два раза, то в среднем потребуется линейное время для нахождения
квантили.
Приведу реализацию этого алгоритма для случая целочисленных массивов:
/// <summary>
/// Поиск за линейное (в среднем) время квантили
/// массива порядка k.
/// При k = n/2 квантиль задает медиану массива.
/// </summary>
/// <param name="k">порядок квантили</param>
/// <returns> квантиль порядка k</returns>
public int Find(int k)
{
int l = 0, r = n - 1;
int x = 0;
while (l < r)
{
x = arr[k]; //барьер
//Разбиение на два подмножества
int i = l, j = r;
do
{
while (arr[i] < x) i++;
while (x < arr[j]) j--;
if (i <= j)
{
if (arr[i] != arr[j])
{//обмен
int w = arr[i]; arr[i] = arr[j]; arr[j] =
w;
}
i++; j--;
}
}
while (i <= j);
//Изменение границ интервала поиска
if (j < k) l = i;
if(k < i) r = j;
}
return (arr[k]);
}
Пирамидальная сортировка
Этот алгоритм носит разные названия – пирамидальной сортировки, сортировки деревом,
иногда его называют сортировкой кучи. Алгоритм рекурсивный, имеет сложность порядка
O(n*log(n)) и не требует дополнительной памяти.
Для пояснения идеи алгоритма заметим, что любой массив из n элементов с индексами от 0 до
n-1можно рассматривать как запись некоторого бинарного дерева, каждый узел (вершина) которого
имеет одного или двух потомков. Первый элемент массива с индексом 0 будем рассматривать как
корень дерева. Элементы, начиная с индекса n/2, являются листьями дерева, не имеющими
потомков. Потомками элемента с индексом i (i < n/2) являются элементы с индексами 2*i + 1, 2*i
+ 2. В соответствии с этим определением каждый массив можно рассматривать как бинарное дерево
высоты log(n).
Массив, задающий бинарное дерево, называется пирамидой (пирамидальным деревом), если
для каждой вершины дерева значение элемента, представленного в вершине, меньше либо равно
значений элементов, представленных его потомками. Отсюда в частности следует, что
минимальный элемент массива находится в корне пирамидального дерева.
Алгоритм пирамидальной сортировки состоит из двух частей, в первой части массив
преобразуется в пирамиду. Во второй части по пирамиде строится отсортированный массив. Обе
части алгоритма можно выполнить за время O(n*log(n)). В основе алгоритма каждой из этих частей
лежит рекурсивная процедура просеивания Seed(k). Идея алгоритма просеивания Seed(k) состоит в
следующем. Пусть построена пирамида, но в одном ее узле k свойство пирамидальности нарушено.
Требуется восстановить пирамидальность. Понятно, что если у узла k нет потомков – он является
листом дерева, то ничего делать не нужно, поскольку на листья никаких ограничений не
накладывается. Если же у листа имеется один или два потомка, но значение в узле меньше значений
его потомков, то тоже ничего делать не нужно, - свойство пирамидальности выполняется. В
противном случае нужно найти потомка с наименьшим значением и провести обмен значений
между узлом и потомком. После обмена нужно рекурсивно вызвать процедуру Seed для нового
узла. Поскольку каждый раз происходит спуск по дереву, а высота дерева не превосходит log(n), то
время работы этой процедуры пропорционально числу ее вызовов и имеет порядок O(log(n)).
Процедура построения пирамиды выглядит совсем просто. Листья дерева не нарушают
пирамидальность. Поэтому достаточно достроить пирамиду для элементов, не являющихся
листьями, начиная с конца и двигаясь к вершине пирамиды. Вот ее текст:
public void MakePyramid()
{
int m = (n-2) / 2;
//m- индекс последнего элемента, имеющего потомков
//элементы с индексами, большими m, являются листьями
пирамиды
for (int k = m; k >= 0; k--)
Seed(k);
}
Понятно, что время работы этой процедуры с учетом времени работы процедуры Seed имеет
порядок O(n*log(n)).
Также просто решается задача построения отсортированного массива по пирамиде. Поскольку
минимальный элемент массива находится в корне дерева, то он извлекается и меняется местами с
последним элементом массива. К элементу, поставленному в вершину, применяется процедура
просеивания, предполагая, что размер пирамиды уменьшился на 1. Затем этот процесс повторяется.
Понятно, что и в этом случае время работы имеет сложность порядка O(n*log(n)).
Задачи
1.484 Построить процедуру сортировки слиянием SortMerge для массива с конкретным
типом элементов (int, string). Привести инварианты циклов, позволяющие доказать
корректность алгоритма. Построить интерфейс, позволяющий оценить время
работы процедуры.
1.485 Построить процедуру быстрой сортировки QuickSort для массива с конкретным
типом элементов (int, string). Привести инварианты циклов, позволяющие доказать
корректность алгоритма. Построить интерфейс, позволяющий оценить время
работы процедуры.
1.486 Построить процедуру нахождения квантили порядка k для массива с конкретным
типом его элементов (int, string). Привести инварианты циклов, позволяющие
доказать корректность алгоритма. Построить интерфейс, позволяющий оценить
время работы процедуры.
1.487 Используя процедуру нахождения медианы массива, построить вариант
процедуры быстрой сортировки QuickSort для массива с конкретным типом
элементов (int, string). Привести инварианты циклов, позволяющие доказать
корректность алгоритма. Построить интерфейс, позволяющий оценить время
работы процедуры.
1.488 Построить процедуру пирамидальной сортировки PyramidSort для массива с
конкретным типом элементов (int, string). Привести инварианты циклов,
позволяющие доказать корректность алгоритма. Построить интерфейс,
позволяющий оценить время работы процедуры.
1.489 Построить универсальную процедуру сортировки слиянием SortMerge для
массива с элементами произвольного типа T. Привести инварианты циклов,
позволяющие доказать корректность алгоритма. Построить интерфейс,
позволяющий оценить время работы процедуры.
1.490 Построить универсальную процедуру быстрой сортировки QuickSort для массива
с элементами произвольного типа T. Привести инварианты циклов, позволяющие
доказать корректность алгоритма. Построить интерфейс, позволяющий оценить
время работы процедуры.
1.491 Построить универсальную процедуру нахождения квантили порядка k для
массива с элементами произвольного типа T. Привести инварианты циклов,
позволяющие доказать корректность алгоритма. Построить интерфейс,
позволяющий оценить время работы процедуры.
1.492 Используя универсальную процедуру нахождения медианы массива, построить
вариант процедуры быстрой сортировки QuickSort для массива с элементами
произвольного типа T. Привести инварианты циклов, позволяющие доказать
корректность алгоритма. Построить интерфейс, позволяющий оценить время
работы процедуры.
1.493 Построить универсальную процедуру пирамидальной сортировки PyramidSort для
массива с элементами произвольного типа T. Привести инварианты циклов,
позволяющие доказать корректность алгоритма. Построить интерфейс,
позволяющий оценить время работы процедуры.
Проекты
1.494 Постройте класс Sorting, содержащий рекурсивные и нерекурсивные методы
сортировки, и класс Analyze, позволяющий анализировать время работы методов
сортировки на одних и тех же массивах. Интерфейс проекта должен поддерживать
представление результатов анализа в виде графиков.
1.495 Постройте универсальный класс Sorting, содержащий рекурсивные и
нерекурсивные методы сортировки массивов произвольного типа, и класс Analyze,
позволяющий анализировать время работы методов сортировки на одних и тех же
массивах. Интерфейс проекта должен поддерживать представление результатов
анализа в виде графиков.
1.496 Постройте проект «Словарь терминов». Проект должен позволять работать со
словарем терминов. Элементами словаря являются термины и их определения. Над
элементами словаря определены такие операции как поиск, вставка, замена и
удаление. В словарь разрешается добавлять новые термины и их определения,
искать в словаре по термину его определение (перевод), искать по переводу сам
термин. Соответствие при поиске можно задавать как строгое, так и нестрогое. При
нестрогом соответствии слово можно искать по его префиксу или суффиксу,
предложение можно искать по одному или нескольким словам, входящим в термин
или его определение. В качестве основы для интерфейса можно взять известный
словарь Abby Lingvo.
Проект «Сортировки»
В заключение приведу текст проекта, содержащего некоторый вариант класса, содержащего
реализацию рекурсивных методов сортировки и позволяющего также оценить время сортировки на
массивах со случайным заполнением элементов. Покажу также возможный вариант интерфейса для
работы с этим классом. Начну с интерфейса. На рис. 4_2 показана форма, позволяющая
анализировать время работы трех эффективных методов сортировки.
Рис. 4_2 Интерфейс для работы с рекурсивными методами сортировки
На рис. 4_2 форма показана в процессе работы. Можно видеть, что на данном конкретном
примере (массиве размерности 100) быстрая сортировка и пирамидальная сортировка дают
примерно одинаковые результаты (516 и 515), а сортировка слиянием дает чуть худшие результаты
(594). Эти результаты слегка варьируются от примера к примеру. Так, например, при увеличении
размерности массива в 10 раз цифры следующие – 4078 для быстрой сортировки, 5094 – для
пирамидальной и 4984 – для сортировки слиянием. Как видите, время увеличилось также в 10 раз
даже чуть меньше. Быстрая сортировка оказалась лучше, чем ее соперники, а пирамидальная
сортировка на этом примере оказалась чуть хуже сортировки слиянием. Приведу еще цифры для
массива размерности 10000 – они соответственно равны: 44156, 65891, 61031. Быстрая сортировка
существенно опередила соперников.
Вот как выглядит класс Sorting. Начну с описания полей класса, его конструктора,
используемого в классе делегата и метода, заполняющего массив случайными числами:
class SortingArrs
{
//delegate
public delegate void SortMethod();
//fields
int n;
//размер массива
int Heap_Size;
//размер пирамиды, построенной на массиве
public int[] arr;
//сортируемый массив
Random rnd;
//конструктор класса
public SortingArrs(int n)
{
this.n = n;
arr = new int[n];
Heap_Size = n;
rnd = new Random();
}
public void FillRndNumbers()
{
for (int i = 0; i < n; i++)
//arr[i] = rnd.Next(n);
arr[i] =
rnd.Next(-1000, 1000);
}
Комментарии здесь излишни. А вот как выглядит подробное описание метода быстрой
сортировки с инвариантами и доказательством корректности, встроенном в программный текст.
/// <summary>
/// Вызывает рекурсивную процедуру QSort,
/// передавая ей границы сортируемого массива.
/// Сортируемый массив arr задается
/// соответствующим полем класса.
/// </summary>
public void QuickSort()
{
QSort(0, n - 1);
}
/// <summary>
/// Небольшая по размеру процедура содержит три
/// вложенных цикла while, два оператора if и рекурсивные
/// вызовы. Для таких процедур задание инвариантов и
/// доказательство корректности облегчает отладку.
/// Предусловие: (start <= finish)
/// Постусловие: массив arr отсортирован по возрастанию
/// </summary>
/// <param name="start">начальный индекс сортируемой части
/// массива arr</param>
/// <param name="finish">конечный индекс сортируемой части
/// массива arr</param>
void QSort(int start, int finish)
{
if (start != finish)
//если (start = finish), то процедура ничего не делает,
//но постусловие выполняется, поскольку массив из одного
//элемента отсортирован по определению.
//Докажем истинность постусловия для массива с числом элементов
>1.
{
int ind = rnd.Next(start, finish);
int item = arr[ind];
int ind1 = start, ind2 = finish;
int temp;
/********
Введем три непересекающихся множества:
S1: {arr(i), start <= i =< ind1-1}
S2: {arr(i), ind1 <= i =< ind2}
S1: {arr(i), ind2+1 <= i =< finish}
Введем следующие логические условия,
играющие роль инвариантов циклов нашей программы:
P1: объединение S1, S2, S3 = arr
P2: (S1(i) < item) Для всех элементов S1
P3: (S3(i) >= item) Для всех элементов S3
P4: item - случайно выбранный элемент arr
Нетрудно видеть, что все условия становятся
истинными после завершения инициализатора цикла.
Для пустых множеств S1 и S3 условия P2 и P3
считаются истинными по определению.
Inv = P1 & P2 & P3 & P4
********/
while (ind1 <= ind2)
{
while ((ind1 <= ind2) && (arr[ind1] < item)) ind1++;
//(Inv == true) & ~B1 (B1 - условие цикла while)
while ((ind1 <= ind2) && (arr[ind2] >= item)) ind2--;
//(Inv == true) & ~B2 (B2 - условие цикла while)
if (ind1 < ind2)
//Из Inv & ~B1 & ~B2 & B3 следует истинность:
//((arr[ind1] >= item)&&(arr[ind2]<item))== true
//Это условие гарантирует, что последующий обмен элементов
//обеспечит выполнение инварианта Inv
{
temp = arr[ind1]; arr[ind1] = arr[ind2];
arr[ind2] = temp;
ind1++; ind2--;
}
//(Inv ==true)
}
//из условия окончания цикла следует: (S2 - пустое множество)
if (ind1 == start)
//В этой точке S1 и S2 - это пустые множества, -> (S3 = arr)
//Нетрудно доказать, что отсюда следует истинность:(item = min)
//Как следствие, можно минимальный элемент сделать первым,
// а к оставшемуся множеству применить рекурсивный вызов.
{
temp = arr[start]; arr[start] = item;
arr[ind] = temp;
QSort(start + 1, finish);
}
else
//Здесь оба множества S1 и S3 не пусты.
//К ним применим рекурсивный вызов.
{
QSort(start, ind1 - 1);
QSort(ind2 + 1, finish);
}
//Индукция по размеру массива и истинность инварианта
//доказывает истинность постусловия в общем случае.
}
}// QuickSort
Приведу теперь набор процедур, реализующих пирамидальную сортировку:
//HeapSort - SortTree - PyramidSort
/// <summary>
/// Рекурсивная поцедура просеивания элемента по древу (пирамиде)
/// Основная компонента построения пирамиды для массива
/// и построения отсортированного массива по пирамиде
/// </summary>
/// <param name="i"></param>
public void Seed(int i)
{
//просеивание элемента с индексом i,
//нарушающего свойство пирамидальности массива
int l = 2 * i+1, r = l + 1;
//индексы левого и правого потомка
int temp=0;
if (l > Heap_Size-1) return;
//это лист пирамиды
int cand = i;
if (arr[i] < arr[l]) cand = l;
if((r < Heap_Size)&&(arr[cand] <arr[r]))cand = r;
if (cand != i) //обмен
{
temp = arr[i]; arr[i] = arr[cand]; arr[cand] = temp;
Seed(cand);
//Просеивание вниз
}
}
/// <summary>
/// Преобразует массив arr
в пирамиду
/// Массив является пирамидой, если для каждого элемента массива,
/// не являющегося листом пирамиды, его значение меньше или равно
/// значения двух его потомков
/// </summary>
public void MakePyramid()
{
int m = (n-2) / 2;
//m- индекс последнего элемента, имеющего потомков
//элементы с индексами, большими m, являются листьями пирамиды
for (int k = m; k >= 0; k--)
Seed(k);
}
/// <summary>
/// Пирамидальная сортировка
/// Вначале по массиву строится пирамида
/// затем по пирамиде строится отсортированнный массив
/// </summary>
public void PyramidSort()
{
Heap_Size = arr.Length;
int temp =0;
MakePyramid();
for (int i = 0; i < n - 1; i++)
{
temp = arr[0]; arr[0] = arr[Heap_Size-1];
arr[Heap_Size-1] = temp; Heap_Size--;
Seed(0);
}
}
Вот текст еще одного рекурсивного метода сортировки:
/// <summary>
/// Сортировка слиянием
/// Вызывает рекурсивную процедуру SortM,
/// передавая ей границы сортируемого массива.
/// Сортируемый массив arr задается
/// соответствующим полем класса.
public void SortMerge()
{
int[] temp = new int[arr.Length];
SortM(0, n - 1, temp);
}
/// <summary>
/// Рекурсивная процедура сортировки слиянием
/// </summary>
/// <param name="start">начало сортируемой части массива</param>
/// <param name="finish">конец сортируемой части массива</param>
/// <param name="temp">массив для хранения результатов
слияния</param>
void SortM(int start, int finish, int[] temp)
{
if (start < finish)
{
int mid = (start + finish) / 2;
SortM(start, mid, temp);
SortM(mid + 1, finish, temp);
Merge(start, mid, finish, temp);
}
}
/// <summary>
/// Слияние отсортированных частей масссива в массив temp
/// После слияния массив переписывается в исходный массив
/// </summary>
/// <param name="start">начало первой части</param>
/// <param name="mid">конец первой части - начало второй</param>
/// <param name="finish">конец второй части</param>
/// <param name="temp">временный массив с результатами
слияния</param>
///
void Merge(int start, int mid, int finish, int[] temp)
{
int topl = start, topr = mid + 1, topt =start;
//Слияние, пока не завершится одна из частей массива
while ((topl <= mid) && (topr <= finish))
{
if (arr[topl] <= arr[topr])
temp[topt++] = arr[topl++];
else
temp[topt++] = arr[topr++];
}
//дописывание остатка незавершенной части массива
while (topl <= mid)
temp[topt++] = arr[topl++];
while (topr <= finish)
temp[topt++] = arr[topr++];
//переливаем элементы в исходный массив
for (int i = start; i <= finish; i++)
arr[i] = temp[i];
}
Добавлю к этим методам сортировки метод, позволяющий вычислять квантили массива:
/// <summary>
/// Поиск за линейное время(в среднем)
квантили
/// массива порядка k.
/// При k = n/2 квантиль задает медиану массива.
/// </summary>
/// <param name="k">порядок квантили</param>
/// <returns> квантиль порядка k</returns>
public int Find(int k)
{
int l = 0, r = n - 1;
int x = 0;
while (l < r)
{
x = arr[k]; //барьер
//Разбиение на два подмножества
int i = l, j = r;
do
{
while (arr[i] < x) i++;
while (x < arr[j]) j--;
if (i <= j)
{
if (arr[i] != arr[j])
{//обмен
int w = arr[i]; arr[i] = arr[j]; arr[j] = w;
}
i++; j--;
}
}
while (i <= j);
//Изменение границ интервала поиска
if (j < k) l = i;
if(k < i) r = j;
}
return (arr[k]);
}
Закончим рассмотрение методов класса специальным методом, позволяющим вычислить
время, затрачиваемое на сортировку массива:
/// <summary>
/// Подсчет времени сортировки
/// Сортируется один и тот же массив 10000 раз
/// Метод сортировки передается как параметр
/// </summary>
/// <param name="sort">Метод сортировки</param>
/// <returns> время сортировки </returns>
public int HowLong(SortMethod sort)
{
const int count = 10000;
int start, finish;
start = MS(DateTime.Now);
for (int i = 1; i <= count; i++)
{
Random rnd = new Random(77);
for (int j = 0; j < n; j++)
{
arr[j] = rnd.Next(-1000,1000);
}
sort();
}
finish = MS(DateTime.Now);
return (finish - start);
}//HowLong
int MS(DateTime dt)
{
return ((dt.Hour * 60 + dt.Minute) * 60 + dt.Second) *
1000
+ dt.Millisecond;
}
Литература
1. В. Биллиг «Основы программирования на C#», М. 2006 г.
2. Б. Мейер «Объектно-ориентированное конструирование программных систем», М.,
2005 г.
3. Д. Кнут « Искусство программирования», т. 3 «Сортировки»
4. Р. Грэхем, Д. Кнут, О. Паташник «Конкретная математика», М., 1998 г.
5. Т. Кормен, Ч. Лейзерсон, Р. Ривест «Алгоритмы. Построение и анализ», М., 2001 г.
6. Д. Гасфилд «Строки, деревья и последовательности в алгоритмах», С-Пб., 2003 г.
7. Н. Вирт «Алгоритмы + структуры данных = программы», М. 1985 г.
8. Д. Баррон « Введение в языки программирования», М. 1980г.
9. Н. Трифонов «Сборник упражнений по языку Алгол» М. 1975 г.
Download