Проект к данной лекции Вы можете скачать здесь.
Массив задает способ организации данных. Массивом называют упорядоченную совокупность элементов одного типа. Каждый элемент массива имеет индексы, определяющие порядок элементов. Число индексов характеризует размерность массива. Каждый индекс изменяется в некотором диапазоне [a,b]. В языке C#, как и во многих других языках, индексы задаются целочисленным типом. В других языках, например, в языке Паскаль, индексы могут принадлежать счетному конечному множеству, на котором определены функции, задающие следующий и предыдущий элемент. Диапазон [a,b] называется граничной парой, a - нижней границей, b - верхней границей индекса. При объявлении массива границы задаются выражениями. Если все границы заданы константными выражениями, то число элементов массива известно в момент его объявления и ему может быть выделена память еще на этапе трансляции. Такие массивы называются статическими. Если же выражения, задающие границы, зависят от переменных, то такие массивы называются динамическими, поскольку память им может быть отведена только динамически в процессе выполнения программы, когда становятся известными значения соответствующих переменных. Массиву, как правило, выделяется непрерывная область памяти.
В языке C# снято существенное ограничение языка C++ на статичность массивов. Массивы в языке C# являются динамическими. Как следствие этого, напомню, массивы относятся к ссылочным типам, память им отводится динамически в "куче". К сожалению, не снято ограничение 0-базируемости, означающее, что нижняя граница массивов C# фиксирована и равна нулю. Было бы гораздо удобнее во многих задачах иметь возможность работать с массивами, у которых нижняя граница изменения индекса не равна нулю.
Шаблоны, определенные в стандартных библиотеках, конечно, стоит использовать, но все-таки странной является рекомендация не пользоваться структурами, встроенными непосредственно в язык. Замечу, что в других языках массивы являются одной из любимых структур данных, используемых программистами.
В языке C#, соблюдая преемственность, сохранены одномерные массивы и массивы массивов. В дополнение к ним в язык добавлены многомерные массивы. Динамические многомерные массивы языка C# являются весьма мощной, надежной, понятной и удобной структурой данных, которую смело можно рекомендовать к применению не только профессионалам, но и новичкам, программирующим на C#. После этого краткого обзора давайте перейдем к более систематическому изучению деталей работы с массивами в C#.
Рассмотрим, как объявляются одномерные массивы, массивы массивов и многомерные массивы.
Напомню общую структуру объявления:
[<атрибуты>] [<модификаторы>] <тип> <объявители>;
Забудем пока об атрибутах и модификаторах. Объявление одномерного массива выглядит следующим образом:
<тип>[] <объявители>;
Заметьте, в отличие от языка C++ квадратные скобки приписаны не к имени переменной, а к типу. Они являются неотъемлемой частью определения типа, так что запись T[] следует понимать как тип, задающий одномерный массив с элементами типа T.
Что же касается границ изменения индексов, то эта характеристика не является принадлежностью типа, она является характеристикой переменных данного типа - экземпляров, каждый из которых является одномерным массивом со своим числом элементов, задаваемых в объявителе переменной.
Как и в случае объявления простых переменных, каждый объявитель может быть именем или именем с инициализацией. В первом случае речь идет об отложенной инициализации. Нужно понимать, что при
int[] a, b, c;
Чаще всего при объявлении массива используется имя с инициализацией. И опять-таки, как и в случае простых переменных, могут быть два варианта инициализации. В первом случае инициализация является явной и задается константным массивом. Вот пример:
double[] x= {5.5, 6.6, 7.7};
Следуя синтаксису, элементы константного массива необходимо заключать в фигурные скобки.
Во втором случае создание и инициализация массива выполняется в объектном стиле с вызовом конструктора массива. И это наиболее распространенная практика объявления массивов. Приведу пример:
int[] d= new int[5];
Итак, если массив объявляется без инициализации, то создается только висячая ссылка со значением void. Если инициализация выполняется конструктором, то в динамической памяти создается сам массив, элементы которого инициализируются константами соответствующего типа (ноль для арифметики, пустая строка для строковых массивов), и ссылка связывается с этим массивом. Если массив инициализируется константным массивом, то в памяти создается константный массив, с которым и связывается ссылка.
Как обычно задаются элементы массива, если они не заданы при инициализации? Они либо вычисляются, либо вводятся пользователем. Давайте рассмотрим первый пример работы с массивами из проекта с именем Arrays, поддерживающего эту лекцию:
public void TestDeclaration()
{
//объявляются три одномерных массива A,B,C
int[] A = new int[5], B= new int[5], C= new int[5];
Arrs.CreateOneDimAr(A);
Arrs.CreateOneDimAr(B);
for(int i = 0; i<5; i++)
C[i] = A[i] + B[i];
//объявление массива с явной инициализацией
int[] x ={5,5,6,6,7,7};
//объявление массивов с отложенной инициализацией
int[] u,v;
u = new int[3];
for(int i=0; i<3; i++) u[i] =i+1;
// v = {1,2,3}; //присваивание константного массива недопустимо
v = new int[4];
v = u; //допустимое присваивание
Arrs.PrintAr1("A", A); Arrs.PrintAr1("B", B);
Arrs.PrintAr1("C", C); Arrs.PrintAr1("X", x);
Arrs.PrintAr1("U", u); Arrs.PrintAr1("V", v);
}
На что следует обратить внимание, анализируя этот текст?
v = u.? Это корректное ссылочное присваивание: хотя u и v имеют разное число элементов, но они являются объектами одного класса. В результате присваивания память, отведенная массиву v, освободится, ей займется теперь сборщик мусора. Обе ссылки u и v будут теперь указывать на один и тот же массив, так что изменение элемента одного массива немедленно отражается на другом массиве.Arrs, статические методы которого выполняют различные операции над массивами. В частности, в примере использованы два метода этого класса, один из которых заполняет массив случайными числами, второй - выводит массив на печать.Вот текст первого из этих методов:
public static void CreateOneDimAr(int[] A)
{
for(int i = 0; i<A.GetLength(0);i++)
A[i] = rnd.Next(1,100);
}//CreateOneDimAr
Здесь rnd - это статическое поле класса Arrs, объявленное следующим образом:
private static Random rnd = new Random();
Процедура печати массива с именем name выглядит так:
public static void PrintAr1(string name,int[] A)
{
Console.WriteLine(name);
for(int i = 0; i<A.GetLength(0);i++)
Console.Write("\t" + name + "[{0}]={1}", i, A[i]);
Console.WriteLine();
}//PrintAr1
На рис 6.1 показан консольный вывод результатов работы процедуры TestDeclarations:
(рис 6.1) Результаты объявления и создания массивовОсобое внимание обратите на вывод, связанный с массивами u и v.
Во всех вышеприведенных примерах объявлялись
Чисто синтаксически нет существенной разницы в объявлении статических и динамических массивов. Выражение, задающее границу изменения индексов, в динамическом случае содержит переменные. Единственное требование - значения переменных должны быть определены в момент объявления. Это ограничение в C# выполняется, поскольку C# контролирует инициализацию переменных.
Приведу пример, в котором описана работа с динамическим массивом:
public void TestDynAr()
{
//объявление динамического массива A1
Console.WriteLine("Введите число элементов массива A1");
int size = int.Parse(Console.ReadLine());
int[] A1 = new int[size];
Arrs.CreateOneDimAr(A1);
Arrs.PrintAr1("A1",A1);
}//TestDynAr
В особых комментариях эта процедура не нуждается. Здесь верхняя граница массива определяется пользователем.
Уже объяснялось, что разделение массивов на одномерные и многомерные носит исторический характер. Никакой принципиальной разницы между ними нет. Одномерные массивы - это частный случай многомерных. Можно говорить и по-другому: многомерные массивы являются естественным обобщением одномерных. Одномерные массивы позволяют задавать такие математические структуры, как векторы, двумерные - матрицы, трехмерные -
Размерность массива это характеристика типа. Как синтаксически при объявлении типа массива указать его размерность? Это делается достаточно просто, за счет использования запятых. Вот как выглядит объявление многомерного массива в общем случае:
<тип>[, … ,] <объявители>;
Число запятых, увеличенное на единицу, и задает размерность массива. Что касается объявителей, то все, что сказано для одномерных массивов, справедливо и для многомерных. Можно лишь отметить, что хотя явная инициализация с использованием многомерных константных массивов возможна, но применяется редко из-за громоздкости такой структуры. Проще инициализацию реализовать программно, но иногда она все же применяется. Вот пример:
public void TestMultiArr()
{
int[,]matrix = {
{1,2},
{3,4}
};
Arrs.PrintAr2("matrix", matrix);
}//TestMultiArr
Давайте рассмотрим классическую задачу умножения прямоугольных матриц. Нам понадобится три динамических массива для представления матриц и три процедуры, одна из которых будет заполнять исходные матрицы случайными числами, другая - выполнять умножение матриц, третья - печатать сами матрицы. Вот тестовый пример:
public void TestMultiMatr()
{
int n1, m1, n2, m2,n3, m3;
Arrs.GetSizes("MatrA",out n1,out m1);
Arrs.GetSizes("MatrB",out n2,out m2);
Arrs.GetSizes("MatrC",out n3,out m3);
int[,]MatrA = new int[n1,m1], MatrB = new int[n2,m2];
int[,]MatrC = new int[n3,m3];
Arrs.CreateTwoDimAr(MatrA); Arrs.CreateTwoDimAr(MatrB);
Arrs.MultMatr(MatrA, MatrB, MatrC);
Arrs.PrintAr2("MatrA",MatrA); Arrs.PrintAr2("MatrB",MatrB);
Arrs.PrintAr2("MatrC",MatrC);
}//TestMultiMatr
Три матрицы MatrA, MatrB и MatrC имеют произвольные размеры, выясняемые в диалоге с пользователем, и использование для их описания динамических массивов представляется совершенно естественным. Метод CreateTwoDimAr заполняет случайными числами элементы матрицы, переданной ему в качестве аргумента, метод PrintAr2 выводит матрицу на печать. Я не буду приводить их код, похожий на код их одномерных аналогов.
Метод MultMatr выполняет умножение прямоугольных матриц. Это классическая задача из набора задач, решаемых на первом курсе. Вот текст этого метода:
public void MultMatr(int[,]A, int[,]B, int[,]C)
{
if (A.GetLength(1) != B.GetLength(0))
Console.WriteLine("MultMatr: ошибка размерности!");
else
for(int i = 0; i < A.GetLength(0); i++)
for(int j = 0; j < B.GetLength(1); j++)
{
int s=0;
for(int k = 0; k < A.GetLength(1); k++)
s+= A[i,k]*B[k,j];
C[i,j] = s;
}
}//MultMatr
В особых комментариях эта процедура не нуждается. Замечу лишь, что прежде чем проводить вычисления, производится проверка корректности размерностей исходных матриц при их перемножении - число столбцов первой матрицы должно быть равно числу строк второй матрицы.
Взгляните, как выглядят результаты консольного вывода на данном этапе работы.
(рис 6.2) Умножение матрицЕще одним видом массивов C# являются массивы массивов, называемые также изрезанными массивами (jagged arrays). Такой
В каких ситуациях может возникать необходимость в таких структурах данных? Эти массивы могут применяться для
Есть некоторые особенности в объявлении и инициализации таких массивов. Если при объявлении типа многомерных массивов для указания размерности использовались запятые, то для int[][] задает массив, элементы которого - одномерные массивы элементов типа int.
Сложнее с созданием самих массивов и их инициализацией. Здесь нельзя вызвать конструктор new int[3][5], поскольку он не задает изрезанный массив. Фактически нужно вызывать конструктор для каждого массива на самом нижнем уровне. В этом и состоит сложность объявления таких массивов. Начну с формального примера:
//массив массивов - формальный пример
//объявление и инициализация
int[][] jagger = new int[3][]
{
new int[] {5,7,9,11},
new int[] {2,8},
new int[] {6,12,4}
};
Массив jagger имеет всего два уровня. Можно считать, что у него три элемента, каждый из которых является массивом. Для каждого такого массива необходимо вызвать конструктор new, чтобы создать внутренний массив. В данном примере элементы внутренних массивов получают значение, будучи явно инициализированы константными массивами. Конечно, допустимо и такое объявление:
int[][] jagger1 = new int[3][]
{
new int[4],
new int[2],
new int[3]
};
В этом случае элементы массива получат при инициализации нулевые значения. Реальную инициализацию нужно будет выполнять программным путем. Стоит заметить, что в конструкторе верхнего уровня константу 3 можно опустить и писать просто new int[][]. Самое забавное, что вызов этого конструктора можно вообще опустить, он будет подразумеваться:
int[][] jagger2 =
{
new int[4],
new int[2],
new int[3]
};
Но вот конструкторы нижнего уровня необходимы. Еще одно важное замечание - динамические массивы возможны и здесь. В общем случае, границы на любом уровне могут быть выражениями, зависящими от переменных. Более того, допустимо, чтобы массивы на нижнем уровне были многомерными. Но это уже "от лукавого", вряд ли стоит пользоваться такими сложными структурами данных, ведь с ними предстоит еще и работать.
Приведу теперь чуть более реальный пример, описывающий простое генеалогическое дерево, которое условно назову "отцы и дети":
/// <summary>
/// массив массивов -"Отцы и дети"
/// </summary>
public void GenTree()
{
int Fcount = 3;
string[] Fathers = new string[Fcount];
Fathers[0] = "Николай"; Fathers[1] = "Сергей"; Fathers[2] = "Петр";
string[][] Children = new string[Fcount][];
Children[0] = new string[] {"Ольга", "Федор"};
Children[1] = new string[] {"Сергей", "Валентина", "Ира", "Дмитрий"};
Children[2] = new string[] {"Мария", "Ирина", "Надежда"};
Arrs.PrintAr3(Fathers, Children);
}
Здесь отцов описывает обычный динамический одномерный массив Fathers. Для описания детей этих отцов необходим уже Fathers. Здесь показан еще один способ создания таких массивов. Вначале конструируется массив верхнего уровня, содержащий ссылки со значением void. А затем на нижнем уровне конструктор создает настоящие массивы в динамической памяти, с которыми и связываются ссылки.
Я не буду демонстрировать работу с генеалогическим деревом, ограничусь лишь печатью этого массива. Здесь есть несколько поучительных моментов. В классе Arrs для печати массива создан специальный метод PrintAr3, которому в качестве аргументов передаются массивы Fathers и Children. Вот текст данной процедуры:
/// <summary>
/// Печать дерева "Отцы и дети",
/// заданного массивами Fathers и Children
/// </summary>
/// <param name="Fathers">массив отцов</param>
/// <param name="Children"> массив массивов детей</param>
public static void PrintAr3(string[] Fathers, string[][] Children)
{
for (int i = 0; i < Fathers.Length; i++)
{
Console.WriteLine("Отец : {0}; Его дети:", Fathers[i]);
for (int j = 0; j < Children[i].Length; j++)
Console.Write(Children[i][j] + " ");
Console.WriteLine();
}
}//PrintAr3
Приведу некоторые комментарии к этой процедуре.
i организован по числу элементов массива Fathers. Заметьте, здесь используется свойство Length, в отличие от ранее применяемого метода GetLength.Children. Свойство Length для него возвращает число элементов верхнего уровня, совпадающее, как уже говорилось, с числом элементов массива Fathers.Length вызывается для каждого элемента Children[i], который является массивом.Приведу вывод, полученный в результате работы процедуры PrintAr3.
(рис 6.3) Дерево "Отцы и дети"В наших примерах массивы неоднократно передавались процедурам в качестве входных аргументов и возвращались в качестве результатов. Остается подчеркнуть только некоторые детали.
C в процедуре MultMatr, ref или out (хотя и допустимо). Передача аргумента по значению в таких ситуациях так же хороша, как и передача по ссылке. В результате вычислений меняется сам массив в динамической памяти, а ссылка на него остается постоянной. Процедура и ее вызов без ключевых слов выглядит проще, поэтому обычно они опускаются. Заметьте, в процедуре GetSizes, где определялись границы массива, ключевое слово out, сопровождающее аргументы, совершенно необходимо.Алгоритмы и задачи, рассматриваемые в этой главе, являются частью фундамента, на котором строится образование программиста. Нет ни одной проблемной области, в задачах которой не требовались бы массивы. Поэтому задачи, требующие использования массивов, появлялись уже в предыдущих главах, появятся они и в последующих. Но здесь мы будем заниматься ими целенаправленно.
Последовательность элементов - $$a_1, a_2, \ldots a_n$$ - одна из любимых структур в математике. Последовательность можно рассматривать как функцию $$a(i)$$, которая по заданному значению индекса элемента возвращает его значение. Эта функция задает отображение $$integer -> T$$, где $$T$$ - это тип элементов последовательности. В программировании последовательности это одномерные массивы, но от этого они не перестают быть менее любимыми.
Определение. Массив - это упорядоченная последовательность элементов одного типа. Порядок элементов задается с помощью индексов.
В отличие от математики, где последовательность может быть бесконечной, массивы всегда имеют конечное число элементов. Для программистов важно то, как массивы хранятся в памяти. Массивы занимают непрерывную область памяти, поэтому, зная адрес начального элемента массива, зная, сколько байтов памяти требуется для хранения одного элемента, и зная индекс (индексы) некоторого элемента, нетрудно вычислить его адрес, а значит, и хранимое по этому адресу значение элемента. На этом основана a(i) задается адресным выражением a+i, в котором имя массива a воспринимается как адрес первого элемента. При вычислении адреса i-го элемента индекс i умножается на длину слова, требуемого для хранения элементов типа T. а+0.
Язык C# сохранил 0-базируемость массивов. Индексы элементов массива в языке C# изменяются в плотном интервале значений от нижней границы, всегда равной 0, до верхней границы, которая задана динамически вычисляемым выражением, возможно, зависящим от переменных. Массивы C# являются 0-базируемыми динамическими массивами. Это важно понимать с самого начала.
Не менее важно понимать и то, что массивы C# относятся к ссылочным типам.
Как у массивов появляются значения, как они изменяются? Возможны три основных способа:
В задачах этого раздела ограничимся пока рассмотрением первых двух способов. Первый способ более или менее понятен. Простые примеры его применения приводились неоднократно. Стоит только отметить, что в классе, работающем с массивами, всегда полезно иметь метод FillArray, позволяющий заполнять массив случайными числами. В примерах использование возможностей класса Random для моделирования элементов массива встречалось неоднократно.
Приведу некоторые рекомендации по вводу и выводу массивов, ориентированные на работу с конечным пользователем.
Для консольных приложений ввод массива обычно проходит несколько этапов:
Вначале у пользователя запрашиваются размеры массива, затем создается массив заданного размера. В цикле по числу элементов организуется ввод значений. Вводу каждого значения предшествует приглашение к вводу с указанием типа вводимого значения, а при необходимости - и диапазона, в котором должно находиться требуемое значение. Поскольку ввод значений - это ответственная операция, а на пользователя никогда нельзя положиться, после ввода часто организуется проверка корректности введенного значения. При некорректном задании значения элемента ввод повторяется, пока не будет достигнут желаемый результат.
При выводе массива на консоль обычно вначале выводится имя массива, а затем его элементы в виде пары: <имя> = <значение> (например, f[5] = 77,7). Задача осложняется для многомерных массивов, когда пользователю важно видеть не только значения, но и структуру массива, располагая строку массива в строке экрана.
Как организовать контроль ввода? Наиболее разумно использовать для этих целей конструкцию охраняемых блоков - try - catch блоков. Это общий подход, когда все опасные действия, связанные с работой пользователя, внешних устройств, внешних источников данных, размещаются в
Как правило, для ввода-вывода массивов пишутся специальные процедуры, вызываемые в нужный момент.
Приложения Windows позволяют построить дружелюбный интерфейс пользователя, облегчающий работу по вводу и выводу массивов. И здесь, когда данные задаются пользователем, заполнение массива проходит через те же этапы, что рассматривались для консольных приложений. Но выглядит все это более красиво, наглядно и понятно. Пример подобного интерфейса, обеспечивающего работу по вводу и выводу одномерного массива, показан на рис 6.4.
(рис 6.4) Форма для ввода-вывода одномерного массиваПользователь вводит в текстовое окно число элементов массива и нажимает командную кнопку "Создать массив", обработчик которой создает массив заданной размерности, если корректно задан размер массива, в противном случае выдает сообщение об ошибке и ждет корректного ввода.
В случае успешного создания массива пользователь может переходить к следующему этапу - вводу элементов массива. Очередной элемент массива вводится в текстовое окно, а обработчик командной кнопки "Ввести элемент" обеспечивает передачу значения в массив. Корректность ввода контролируется и на этом этапе, проверяя значение введенного элемента и выводя в специальное окно сообщение в случае его некорректности, добиваясь, в конечном итоге, получения от пользователя корректного ввода.
Для облегчения работы пользователя выводится подсказка, какой именно элемент должен вводить пользователь. После того, как все элементы массива введены, окно ввода становится недоступным для ввода элементов. Интерфейс формы позволяет многократно создавать новый массив, повторяя весь процесс.
На рис 6.4 форма разделена на две части - для ввода и вывода массива. Крайне важно уметь организовать ввод массива, принимая данные от пользователя. Не менее важно уметь отображать существующий массив в форме, удобной для восприятия пользователя. На рисунке показаны три различных элемента управления, пригодные для этих целей, - ListBox, CheckedListBox и ComboBox. Как только вводится очередной элемент, он немедленно отображается во всех трех списках.
В реальности отображать массив в трех списках, конечно, не нужно, это сделано только в целях демонстрации возможностей различных элементов управления. Для целей вывода подходит любой из них, выбор зависит от контекста и предпочтений пользователя. Элемент ComboBox имеет дополнительное текстовое окно, в которое пользователь может вводить значение. Элемент CheckedListBox обладает дополнительными свойствами в сравнении с элементом ListBox, позволяя отмечать некоторые элементы списка (массива). Отмеченные пользователем элементы составляют специальную коллекцию. Эта коллекция доступна, с ней можно работать, что иногда весьма полезно. Чаще всего для вывода массива используется элемент ListBox.
Посмотрим, как это все организовано программно. Начну с полей формы :
//fields int n = 0; double[] mas; int currentindex = 0; double ditem = 0; const string SIZE = "Корректно задайте размер массива!"; const string INVITE = "Введите число в формате m[,n]"; const string EMPTY = "Массив пуст!"; const string ITEMB = "mas["; const string ITEME = "] = "; const string FULL = "Ввод недоступен!"; const string OK = "Корректный ввод!"; const string ERR = "Ошибка ввода числа! Повторите ввод!";
Полями этого класса является одномерный массив, его размер, текущий индекс и константы, используемые в процессе диалога с пользователем. Обработчик события Click командной кнопки, отвечающей за создание массива, имеет вид:
private void buttonCreateArray_Click(object sender, EventArgs e)
{
try
{
n = Convert.ToInt32(textBoxN.Text);
mas = new double[n];
labelInvite.Text = INVITE;
labelItem.Text = ITEMB + "0" + ITEME;
labelResult.Text = EMPTY;
textBoxItem.ReadOnly = false;
listBox1.Items.Clear();
comboBox1.Items.Clear();
checkedListBox1.Items.Clear();
comboBox1.Items.Clear();
currentindex = 0;
}
catch (Exception)
{
labelResult.Text = SIZE;
}
}
Первым делом принимается размер массива, введенный пользователем. Преобразование к типу int введенного значения помещено в
private void buttonAddItem_Click(object sender, EventArgs e)
{
//Заполнение массива элементами
if (GetItem())
{
mas[currentindex] = ditem;
listBox1.Items.Add(mas[currentindex]);
checkedListBox1.Items.Add(mas[currentindex]);
comboBox1.Items.Add(mas[currentindex]);
currentindex++;
labelItem.Text = ITEMB + currentindex + ITEME;
textBoxItem.Text = "";
labelResult.Text = OK;
if (currentindex == n)
{
labelInvite.Text = "";
labelItem.Text = "";
labelResult.Text = FULL;
textBoxItem.Text = "";
textBoxItem.ReadOnly = true;
}
}
}
Функция GetItem вводит значение очередного элемента. Если пользователь корректно задал его значение, то элемент добавляется в массив, а заодно и в списки, отображающие текущее состояние массива. Создается подсказка для ввода следующего элемента массива, а если массив полностью определен, то форма переходит в состояние окончания ввода.
/// <summary>
/// Ввод с контролем текущего элемента массива
/// </summary>
/// <returns>true в случае корректного ввода значения</returns>
bool GetItem()
{
string item = textBoxItem.Text;
bool res = false;
if (item == "")
labelResult.Text = INVITE;
else
{
try
{
ditem = Convert.ToDouble(item);
res = true;
}
catch(Exception)
{
labelResult.Text = ERR;
}
}
return res;
}
Форму OneDimArrayForm можно рассматривать как некоторый шаблон, полезный при организации ввода и вывода одномерных массивов.
Ввод двумерного массива немногим отличается от ввода одномерного массива. Сложнее обстоит дело с выводом двумерного массива, если при выводе пытаться отобразить структуру массива. К сожалению, все три элемента управления, хорошо справляющиеся с отображением одномерного массива, плохо приспособлены для показа структуры двумерного массива. Хотя у того же элемента показан пример формы, поддерживающей работу по вводу и выводу двумерного массива.
(рис 6.5) Форма, поддерживающая ввод и вывод двумерного массиваИнтерфейс формы схож с тем, что использовался для организации работы с одномерным массивом. Схожа и программная организация ввода-вывода элементов массива. Поэтому я не буду приводить код, поддерживающий работу с формой TwoDimArrayForm, надеясь, что читатель при желании сможет его восстановить. Остановлюсь лишь на одном моменте, позволяющем отображать двумерный массив в элементе управления ListBox так, чтобы сохранялась структура строк и столбцов массива. Этого можно добиться за счет программной настройки размеров элемента управления ListBox:
listBox1.Height = n * HEIGHT_LINE;
listBox1.Width = m * 2 * HEIGHT_LINE;
Константа HEIGHT_LINE задает высоту строки в списке. Вначале водятся элементы первого столбца; когда весь столбец введен, автоматически следующее вводимое значение будет отображаться в первой строке в следующем столбце.
В общей ситуации, когда значения, вводимые пользователем, могут колебаться в широком диапазоне, трудно гарантировать отображение структуры двумерного массива. Однако ситуация не безнадежна. Есть и другие, более мощные и более подходящие для наших целей элементы управления. Если на элементах ListBox и подобных ему я останавливаться не буду, оставляя их для самостоятельного изучения, то об элементе DataGridView расскажу подробнее.
Элемент управления DataGridView является последней новинкой в серии табличных элементов DataGrid, позволяющих отображать таблицы. Главное назначение этих элементов - связывание с таблицами внешних источников данных, прежде всего с таблицами баз данных. Мы же сейчас рассмотрим другое его применение - в интерфейсе, позволяющем пользователю вводить и отображать матрицы - двумерные массивы.
Рассмотрим классическую задачу умножения прямоугольных матриц показан возможный вид формы, поддерживающей работу пользователя. Форма показана в тот момент, когда пользователь уже задал размеры и значения исходных матриц, выполнил умножение матриц и получил результат.
(рис 6.6) Форма с элементами DataGridView, поддерживающая работу с матрицамиНа форме расположены три текстовых окна для задания размеров матриц, три элемента . В них отображается информация, связанная с формой и отдельными элементами управления. Текст у невидимых на рисунке меток появляется тогда, когда обнаруживается, что пользователь некорректно задал значение какого-либо элемента исходных матриц.
А теперь перейдем к описанию того, как этот интерфейс реализован. В классе 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.Обработчик события "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();
}
Полиномом n-й степени $$P_n(x)$$ называют функцию:
$$P_n(x)=a_n x^n+a_{n-1}x^{n-1}+\ldots+a_1 x+a_0$$Если рассматривать график этой функции на плоскости, то $$x$$ и $$P_n(x)$$ - это декартовы координаты точек графика функции. Значения $$a_k$$ (k из интервала [0,n]) называются коэффициентами полинома. Все они принадлежат одному типу и при программной работе с полиномами представляются одномерным массивом с n+1 элементами.
Если задан массив коэффициентов полинома $$A$$, то вычислить значение полинома в точке $$x$$ не представляет особой сложности. Но ни один уважающий себя программист не позволит себе вычислять значение полинома, буквально пользуясь схемой 6.1, требующей n-1 операций возведения в степень, n операций умножения и n операций сложения. Прекрасный пример того, как можно упростить алгоритм, дает
Удобнее представлять
Вначале вычисляется значение полинома нулевой степени, состоящего из коэффициента при старшем члене исходного полинома. Затем рекуррентно повышается степень полинома, для чего достаточно умножить на x предыдущее значение и добавить новый коэффициент. В программе эта схема естественным образом реализуется обычным циклом, где на каждом шаге выполняется одно умножение и одно сложение.
Если $$P_n(x)$$ - полином n-й степени с коэффициентами $$a_i$$, $$Q_n(x)$$ - полином n-й степени с коэффициентами $$b_i$$ и $$P_n(x) = Q_n(x)$$, то из этого следует равенство соответствующих коэффициентов:
$$(P_n(x)=Q_n(x))\Rightarrow(a_i=b_i;\; \forall_i=0\ldots n)$$Многие задачи над полиномами связаны с определением их корней. Напомню, $$x_0$$ является корнем полинома, если $$P_n(x_0) = 0$$. У полинома n-й степени не более чем $$n$$ действительных корней. Если $$n$$ - нечетно, то полином имеет хотя бы один действительный корень. Все корни полинома принадлежат некоторому конечному интервалу [c, d]. Вне этого интервала поведение полинома определяется его старшим членом - $$a_n x^n$$. Для полинома четной степени обе ветви уходят в $$+\infty$$, если $$a_n >0$$ и в $$-\infty$$, если $$a_n<0$$. Для полинома нечетной степени ветви полинома вне интервала [c, d] разнонаправлены. Если $$a_n>0$$, то правая ветвь уходит в $$+\infty$$, а левая ветвь - в $$-\infty$$. Если $$a_n<0$$, то левая ветвь уходит в $$+\infty$$, а правая ветвь - в $$-\infty$$.
Когда по каким-либо физическим соображениям интервал [c, d] известен хотя бы приблизительно, задача нахождения корней полинома облегчается, в противном случае она может быть довольно трудной, особенно для случая близко расположенных корней.
Рассмотрим один из простых алгоритмов, исследующих, существует ли на заданном интервале [e, f] хотя бы один корень. Один корень заведомо существует, если полином на концах исследуемого интервала имеет разные знаки или один из концов интервала уже является корнем полинома. Это условие и будет характерным признаком поиска нужного интервала. Если исходный интервал [e, f] удовлетворяет характерному признаку, то задача решена и такой интервал найден. В противном случае в цикле по $$k$$ вычислим $$h = L/2^k$$, где $$L$$ - длина исходного интервала ( $$L= f-e$$ ). Затем организуем внутренний цикл, в котором проверим характерный признак на всех интервалах длины h. Если интервал будет найден, то вычисления завершаются, в противном случае переходим к следующему шагу цикла по $$k$$, производя очередное дробление $$h$$. Завершение цикла по $$k$$ означает, что если исследуемый интервал [e, f] и содержит корни, то это близкие пары корней, отстоящие друг от друга на расстояние, меньшее $$h$$ - заключительной длины интервала по завершении цикла по $$k$$.
Приведу несколько практических рекомендаций, полезных при реализации этой схемы. Внутренний цикл следует организовать так, чтобы не повторять вычисление полинома в тех точках, в которых это вычисление проводилось на предыдущих шагах цикла. Это означает, что когда шаг $$h = L/2^k$$, то во внутреннем цикле достаточно вычислить значение полинома не более чем в $$2^{k-1}$$ точках. Внешний цикл достаточно ограничить числом в интервале от 10 до 20, поскольку уже при $$k=10$$ величина исходного интервала $$L$$ уменьшится более чем в 1000 раз, что вполне достаточно в большинстве практических ситуаций. Хотя следует помнить, что в ряде ситуаций практики приходится иметь дело с резко осциллирующими функциями, где близкие корни являются правилом, а не исключением.
Рассмотрим несколько простых схем нахождения корня полинома. Заметим, что все эти схемы применимы к нахождению корней любых функций, а не только полиномов. Как всегда в программировании, речь идет не столько о точном нахождении корня, сколько о нахождении корня с заданной точностью $$\varepsilon$$. Так что, если $$x_0$$ - это точное значение корня, то нам достаточно найти $$x*$$ - такое, что $$|x_0 - x*| < \varepsilon$$.
Эта схема прекрасно подходит, когда предварительно проведено исследование интервала существования корня и найден такой интервал [e, f], на концах которого полином принимает разные знаки, так что существует корень внутри интервала. Если исходный интервал мал и сравним с заданной точностью $$\varepsilon$$, то в качестве корня можно выбрать середину этого интервала. Если же исходный интервал больше, чем значение $$\varepsilon$$, то интервал можно разделить пополам и из двух половинок выбрать ту, для которой выполняется характерный признак существования корня. Понятно, что если признак выполняется для всего интервала, то он обязательно будет выполняться для одной из его половинок. Деление отрезка пополам приводит к быстрому уменьшению длины отрезка, так что 10-20 делений достаточно, чтобы найти интервал длины, меньшей $$\varepsilon$$, а следовательно, и корень полинома с заданной точностью.
Формально метод применим и в том случае, когда неизвестен интервал, в котором существует корень функции. Пусть $$x_0$$ - некоторое заданное начальное приближение к корню полинома. Тогда можно построить следующий итерационный процесс:
$$x_k=x_{k-1}-f(x_{k-1});\quad k=1,2,\ldots$$Метод записан для произвольной функции $$f$$, в нашем случае функция $$f$$ задана полиномом. Итерационный процесс следует прекращать либо по достижении заданной точности, либо по достижении максимально допустимого числа итераций $$N$$. Заметьте, следует задавать оба условия, поскольку сходимость процесса простой итерации к корню, даже если он существует, не гарантируется. Во многом все зависит от удачного выбора начального приближения.
Метод простой итерации обладает полезным свойством "неподвижной точки". Корни функции являются "неподвижными точками" метода. Нетрудно заметить, что если на некотором шаге $$x_{k-1}$$ сошлось к корню полинома $$x*$$, то $$x_k$$ и все последующие итерации будут равны $$x*$$, так что итерационный процесс из найденного корня не уходит.
Этот метод чуть более сложен в реализации, но обладает лучшей сходимостью в сравнении с методом простой итерации, хотя и здесь сходимость во многом зависит от удачного выбора начального приближения $$x_0$$. Для произвольной функции $$f$$ итерационный процесс метода Ньютона выглядит так:
$$x_k=x_{k-1}-\frac{f(x_{k-1})}{f'(x_{k-1})};\quad k=1,2,\ldots$$Понятно, что производной от полинома n-й степени будет полином степени n-1, коэффициенты которого легко вычисляются по n и коэффициентам исходного полинома. Все, что было сказано о методе простой итерации - завершение процесса, обладание неподвижной точкой, - справедливо и для метода Ньютона.
Если для полинома $$P(x)$$ n-й степени найден корень $$x_1$$, то можно понизить степень полинома, построив полином $$P1(x)$$ степени $$n-1$$, у которого все корни совпадают с корнями полинома $$P(x)$$ за исключением того, что у него нет корня $$x_1$$.
Запишем соотношение, связывающее полиномы:
$$P(x)=P1(x)(x-x1)\\ a_n x^n+a_{n-1}x^{n-1}+\ldots+a_1 x+a_0=(b_{n-1}x^{n-1}+b_{n-2}x^{n-2}+\ldots+b_1 x+b_0)(x-x1)$$Учитывая соотношение 6.3 о равенстве двух полиномов одной степени, можно выписать $$n+1$$ соотношение, связывающее коэффициенты этих полиномов. Эти соотношения нетрудно разрешить относительно неизвестных коэффициентов $$b_k$$. В результате получим:
$$b_{n-1}=a_n;\\ b_{n-2}=a_{n-1}+b_{n-1}x_1;\\ \ldots\\ b_0=a_1+b_1 x_1;$$Заметьте, неизвестных всего $$n$$, а уравнений можно построить - $$n+1$$. Но последнее уравнение $$(a0 + b_0 x1 = 0)$$ является следствием предыдущих и используется для контроля вычислений.
К новому полиному можно применить тот же процесс - найти его корень и понизить затем степень полинома. Реально понижение степени не намного упрощает задачу отыскания корней, так что чаще всего проще искать корни исходного полинома, изменяя начальные приближения в итерационном процессе или отыскивая различные интервалы, на которых полином меняет свой знак.
До сих пор рассматривалась задача отыскания корней полинома с заданными коэффициентами. Иногда приходится решать обратную задачу - найти коэффициенты полинома, если известны его корни - $$x_1, x_2, … x_n$$. Полиномов с одинаковыми корнями существует бесчисленное множество. Однако среди них существует единственный полином с коэффициентом $$a_n$$, равным единице. Этот полином называется приведенным, его-то и будем строить. Все остальные полиномы получаются из приведенного полинома умножением всех коэффициентов на произвольное число $$a_n$$, от которого требуется лишь, чтобы оно не было равно нулю. Поэтому для однозначного решения задачи требуется задать n корней и коэффициент при старшем члене полинома. Тогда можно записать следующее равенство:
$$P_n(x)=a_n(x-x_n)(x-x_{n-1})\ldots(x-x_1)$$Для нахождения коэффициентов полинома $$P_n(x)$$ воспользуемся, как обычно, соотношением 6.3. Но применить его напрямую сложно. Поэтому воспользуемся процессом, обратным к процессу понижения степени. Построим вначале $$P_1(x)$$ - полином первой степени, у которого $$x_1$$ является единственным корнем. Затем повысим степень и построим полином второй степени - $$P_2(x)$$, у которого появляется еще один корень - $$x_2$$. Продолжая этот процесс, дойдем до искомого полинома $$P_n(x)$$. При вычислении коэффициентов нового полинома будем использовать коэффициенты уже посчитанного полинома на единицу меньшей степени. Получающиеся в результате соотношения близки к тем, что приведены для случая понижения степени полинома.
Коэффициенты полинома первой степени $$P_1(x)$$ выписываются явно:
$$a_1=1;\quad a_0=-x_1;$$Коэффициенты полинома k-й степени вычисляются через коэффициенты полинома степени k-1:
$$P_k(x)=P_{k-1}(x)(x-x_k)$$Переходя к коэффициентам, получим следующие уравнения:
$$a_k=a'_{k-1};\\ a_{k-i}=a'_{k-i-1}-a'_{k-i}x_k;\quad \forall i\quad i=1,\dots k-1;\\ a_0=-a'_0 x_k;$$В соотношении 6.5 через $$a'$$ обозначены коэффициенты полинома степени $$k-1$$. На самом деле схема безопасна и позволяет считать коэффициенты на том же месте, не требуя дополнительной памяти. Приведу алгоритм вычисления коэффициентов полинома по его корням в виде схемы, приближенной к языку C#.
Дано:
Вычислить:
//Вычисляем коэффициенты полинома первой степени
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$$ точка: $$R_0(x_0, y_0), R_1(x_1, y_1), R_2(x_2, y_2),\ldots, R_n(x_n, y_n)$$. Полиномом Лагранжа $$P_L(x)$$ называется полином n-й степени, проходящий через все точки $$R_k$$. Если точки $$R_k$$ не образуют возвратов, то такой полином существует и является единственным. Под возвратом понимается ситуация, когда существуют две точки $$R_i$$ и $$R_j$$ такие, что $$x_i = x_j$$.
Как построить такой полином? Лагранж предложил следующий алгоритм. Полином $$P_L(x)$$ строится как сумма $$n+1$$ полиномов n-й степени:
$$P_L(x)=\sum\limits_{k=1}^{n+1}P_k(x)$$Каждый из полиномов $$P_k(x)$$, входящих в сумму, строится следующим образом. Корнями полинома $$P_k(x)$$ являются все точки $$R_i$$ за исключением точки $$R_k$$. Единственность $$P_k(x)$$ обеспечивается за счет того, что коэффициент при старшем члене an подбирается так, чтобы полином проходил через точку $$R_k$$. В записи Лагранжа полином $$P_k(x)$$ выглядит следующим образом:
$$P_k(x)=y_k\frac{(x-x_o)(x-x_1)\ldots(x-x_{k-1})(x-x_{k+1})\ldots(x-x_n)}{(x_k-x_0)(x_k-x_1)\ldots(x_k-x_{k-1})(x_k-x_{k+1})\ldots(x_k-x_n)}$$В записи 6.6 в числителе находится приведенный полином, построенный по корням, а $$y_k$$, деленное на знаменатель в формуле 6.6, задает $$an$$ -
Условия, накладываемые на полиномы $$P_k(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 - массив корней приведенного полинома $$P_k$$,coefk - массив его коэффициентов,An - coefL - массив коэффициентов полинома Лагранжа,HornerP - метод, вычисляющий по схеме Горнера значение полинома по его коэффициентам и значению координаты x,CalcCoefFromRoots - метод, вычисляющий массив коэффициентов приведенного полинома по его корням.При рассмотрении полинома Лагранжа возникала необходимость в нахождении суммы полиномов одинаковой степени, заданных своими коэффициентами. Пусть P(x) и Q(x) - полиномы степени n и m, соответственно, заданные своими коэффициентами, и пусть для определенности $$n >= m$$. Тогда суммой полиномов называется полином R(x) степени n, коэффициенты которого вычисляются следующим образом:
$$R(x)=P(x)+Q(x)\Rightarrow \sum\limits_{i=0}^n c_i x^i=\sum\limits_{i=0}^n a_i x^i+\sum\limits_{i=0}^m b_i x^i\Rightarrow\\ c_i=a_i+b_i\quad \forall i<=m;\\ c_i=a_i\quad \forall i:m<i<=n;$$Пусть полиномы P(x) и Q(x) заданы, подобно
Тогда нетрудно найти подобное представление и для полинома R(x), представляющего сумму полиномов:
$$R(x):\{(rx_0,ry_0),(rx_1,ry1),\ldots(rx_n,ry_n)\},\quad\text{где}\\ rx_i=px_i;\quad ry_i=py_i+Q(px_i);$$В этом случае понадобится вычислить значения полинома 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)=\{(sx_0,sy_0),(sx_1,sy_1),\ldots(sx_{n+m},sy_{n+m})\},\quad\text{где}\\ sx_i=px_i;\quad sy_i=py_i*Q(px_i)\quad \forall i:i<=n;\\ sx_i=qx_{i-n-1};\quad sy_i=qy_{i-n-1}*P(qx_{i-n-1})\quad \forall i:n<i<=n+m+1$$Для получения множества точек, задающих представление полинома S(x), приходится вычислять значение полинома Q(x) в n точках и значение полинома P(x) в m точках, а затем выполнять соответствующее умножение значений двух полиномов.
Если исходные полиномы P(x) и Q(x) заданы своими коэффициентами, то имеем:
$$S(x)=P(x)*Q(x)=(\sum\limits_{i=0}^n a_i x^i)*(\sum\limits_{j=0}^m b_j x^j)$$Каждый член первой суммы приходится умножать на все члены второй суммы и затем приводить подобные члены при одинаковых степенях x. Нетрудно заметить, что в результате коэффициенты полинома S(x) определяются следующими соотношениями:
$$S(x)=\sum\limits_{i=0}^{n+m}d_i;\quad\text{где}\\ d_i=\sum\limits_{k+r=i}a_k b_r;\quad k\in[0,n];\quad r\in[0,m]$$Суммирование идет по всем наборам k и r, дающим в сумме значение i. Понятно, что для крайних значений (i=0 и i=n+m) сумма состоит из одного члена, поскольку подобные члены для x в нулевой степени и степени n+m отсутствуют. Число членов суммирования увеличивается при приближении к середине интервала [0, n+m].
Подводя некоторые итоги, отметим, что полином можно задать тремя разными способами - его коэффициентами, корнями и точками, через которые проходит полином. Если заданы коэффициенты полинома, то за время, пропорциональное $$n^2, (T(n) = O(n^2))$$ можно вычислить значения полинома в n+1 точках. Для вычисления значения полинома в одной точке применяется
Если заданы корни, то можно получить два других представления. Рассмотренный нами алгоритм позволяет по корням полинома за время $$O(n^2)$$ вычислить коэффициенты полинома. Алгоритм использует итеративную схему из n шагов, где на каждом шаге выполняется операция повышения степени, выполняемая за линейное время. Поскольку корни являются частным случаем задания множества точек, через которые проходит полином, то задание корней автоматически задает и представление полинома набором точек. Обратная задача - получение корней по коэффициентам или заданным точкам - так просто не решается. Точное ее решение существует для полиномов второй и третьей степени, но не в общем случае. Для нахождения корней приходится использовать приближенные
Задание полинома его корнями является наиболее информативным. Если известны корни, то без труда выполняется свертка полиномов. Вычисление значения полинома в заданной точке выполняется за n умножений, не требуя применения схемы Горнера. Несколько сложнее выполняется операция сложения полиномов. К сожалению, на практике редко встречается ситуация, когда известны корни полинома, но такое бывает - алгоритм Лагранжа тому пример.
Когда полиномы заданы своими коэффициентами, то вычисление значения полинома в заданной точке выполняется по схеме Горнера за линейное время. Сложение полиномов также является легкой операцией и выполняется за линейное время. Свертку полиномов в этом случае выполнить сложнее. Рассмотренный нами алгоритм требует уже квадратичного времени.
На практике полиномы чаще всего появляются при задании множества точек. Ситуация обычно такова. В результате экспериментов измеряются значения некоторой функции в ряде точек. Требуется предсказать, каково будет значение этой функции в других точках, в которых измерения не проводились. Если из теоретических соображений не известен вид функции, то чаще всего ее задают в виде полинома, проходящего через точки, полученные экспериментальным путем. В этой постановке задачу построения полинома и вычисления значений полинома в точках, не подлежащих измерениям, называют задачей интерполяции, а
Множество точек, через которые проходит полином, обычно несет дополнительную информацию. Некоторые точки, например, могут быть корнями полинома или задавать интервалы, внутри которых находятся корни.
Одно замечание к задаче свертки полиномов. Приведенный алгоритм решения этой задачи для полиномов, заданных своими коэффициентами, требует квадратичного времени. Ввиду практической важности этой задачи много внимания уделялось поиску наиболее эффективного по временной сложности алгоритма. Существуют алгоритмы, решающие эту задачу за время $$O(n*log(n))$$. Эти алгоритмы используют технику быстрого преобразования Фурье и обратного к нему. Они сложнее в реализации и требуют работы с комплексными числами или выполнения операций модульной арифметики. Здесь они только упоминаются и детально не рассматриваются.
В задачах этого раздела уже не говорится о том, какого типа проект следует строить - консольный или Windows. Предполагается, что обычной практикой является построение Windows-приложений.
Polinom. Методы класса должны реализовать все алгоритмы, рассмотренные в этом разделе. Интерфейс пользователя должен позволять пользователю решать основные задачи, возникающие при работе с полиномами.Матрицей называется набор чисел, состоящий из m строк и n столбцов. Для программиста матрица - это двумерный массив. Матрица называется квадратной, если m = n, и прямоугольной - в противном случае. Числа m и n определяют размерность матрицы. Над прямоугольными матрицами определены операции транспонирования, сложения, умножения.
Пусть A - матрица размерности m*n (из m строк и n столбцов) с элементами $$a_{i,j}$$. Транспонированной матрицей B = AT называют матрицу размерности n*m, элементы которой $$b_{i,j} = a_{j,i}$$. В транспонированной матрице строки исходной матрицы становятся столбцами.
$$A=\left|\left|\begin{array}{l}a_{1,1},a_{1,2}\ldots a_{1,n}\\ a_{2,1},a_{2,2}\ldots a_{2,n}\\ \ldots\\ a_{m,1},a_{m,2}\ldots a_{m,n}\end{array}\right|\right| \quad B=A^T=\left|\left|\begin{array}{l}a_{1,1},a_{2,1}\ldots a_{m,1}\\ a_{1,2},a_{2,2}\ldots a_{m,2}\\ \ldots\\ a_{1,n},a_{2,n}\ldots a_{m,n}\end{array}\right|\right|$$Операция сложения определена над прямоугольными матрицами одинаковой размерности. Пусть A, B, C - прямоугольные матрицы размерности m*n. Тогда сумма матриц определяется естественным образом:
$$A=\left|\left|\begin{array}{l}a_{1,1},a_{1,2}\ldots a_{1,n}\\ a_{2,1},a_{2,2}\ldots a_{2,n}\\ \ldots\\ a_{m,1},a_{m,2}\ldots a_{m,n}\end{array}\right|\right| \quad B=\left|\left|\begin{array}{l}b_{1,1},b_{1,2}\ldots b_{1,n}\\ b_{2,1},b_{2,2}\ldots b_{2,n}\\ \ldots\\ b_{m,1},b_{m,2}\ldots b_{m,n}\end{array}\right|\right|\\ C=A+B=\left|\left|\begin{array}{l}a_{1,1}+b_{1,1},a_{1,2}+b_{1,2}\ldots a_{1,n}+b_{1,n}\\ a_{2,1}+b_{2,1},a_{2,2}+b_{2,2}\ldots a_{2,n}+b_{2,n}\\ \ldots\\ a_{m,1}+b_{m,1},a_{m,2}+b_{m,2}\ldots a_{m,n}+b_{m,n}\end{array}\right|\right|$$Операция умножения определена над прямоугольными матрицами, у которых число столбцов первого сомножителя равно числу строк второго сомножителя. Матрица произведения имеет число строк, равное числу строк первого сомножителя, и число столбцов, равное числу столбцов второго сомножителя. Пусть A - матрица размерности m*p, B - размерности p*n, тогда матрица C= A*B имеет размерность m*n. Элементы матрицы произведения определяются как сумма попарных произведений элементов строки первого сомножителя на элементы столбца второго сомножителя.
$$A=\left|\left|a_{i,j}\right|\right|\quad i=1,...m;\;j=1,...p;\quad B=\left|\left|b_{j,k}\right|\right|\quad j=1,...p;\;k=1,...n\\ C=A*B=\left|\left|c_{i,k}\right|\right|\quad i=1,...m;\;k=1,...n;\\ c_{i,k}=\sum\limits_{j=1}^p a_{i,j}*b_{j,k};$$Умножение всегда определено для прямой и транспонированной матрицы. Если A - прямоугольная матрица размерности m*n, то всегда определена квадратная матрица B размерности m*m:
$$B = A*A^T = B^T = (A*A^T)^T = (A^T)^T*A^T= A*A^T$$Результатом такого произведения является симметричная матрица. Квадратная матрица называется симметричной, если $$a_{i,j} = a_{j,i}$$ для всех i и j, или, что то же, если $$A = A^T$$. Операции транспонирования, сложения и умножения обладают следующими свойствами:
$$(A^T)^T=A;\quad (A+B)^T=A^T+B^T;\quad (A*B)^T=B^T*A^T$$Квадратная матрица называется диагональной, если все элементы, кроме диагональных, равны нулю, то есть $$a_{i,j} = 0$$ при $$i /=j$$.
Квадратная матрица называется единичной, если все элементы, кроме диагональных, равны нулю, а диагональные элементы равны единице, то есть $$a_{i,j} = 0$$ при $$i /=j$$ и $$a_{i,j} = 1$$ при $$i = j$$. Единичная матрица обозначается обычно буквой E, и она играет роль единицы при умножении матриц, поскольку для любой квадратной матрицы A и единичной матрицы E той же размерности имеют место соотношения:
$$A*E = E*A = A$$Для квадратных матриц определена функция над ее элементами, называемая определителем. Обозначается определитель обычно с помощью одинарных линий вокруг набора чисел, задающих матрицу:
$$D(A)=\left|\begin{array}{l}a_{1,1},a_{1,2}\ldots a_{1,n}\\ a_{2,1},a_{2,2}\ldots a_{2,n}\\ \ldots\\ a_{n,1},a_{n,2}\ldots a_{n,n}\end{array}\right|$$Функция, задающая определитель, обладает рядом важных свойств.
Не приводя общего формального определения, рассмотрим ниже алгоритм вычисления
Если определитель квадратной матрицы A не равен нулю, то существует обратная матрица, обозначаемая как $$A^{-1}$$. Прямая и обратная матрицы связаны соотношением:
$$A*A^{-1} = A^{-1}*A = E$$Операции транспонирования, умножения и обращения матриц связаны соотношениями:
$$(A^T)^{-1} = (A^{-1})^T;\quad (A*B)^{-1} = B^{-1}*A^{-1}$$Множество квадратных матриц одной размерности с определителем, отличным от нуля образуют группу по умножению. В группе есть единичный элемент, для каждого элемента существует обратный к нему, и произведение элементов принадлежит группе.
Иногда полезно рассматривать матрицу, состоящую не из элементов, а из клеток, каждая из которых является матрицей. Все определения операций над матрицами, элементы которых являются числами, переносятся на матрицы, элементы которых являются клетками. Такое представление особенно полезно для
В круглых скобках для клеток заданы их размерности. Пусть теперь некоторые клетки нулевые, например, таковыми являются клетки D, F и G. Тогда матрица M2 имеет вид:
$$M2=\left|\left|\begin{array}{cc}A*EB*H\\ C*E0\end{array}\right|\right|$$Для вычисления матрицы M2 необходимо будет найти произведение трех пар матриц, но значительно меньших размеров, чем исходные матрицы. В целом объем вычислений сократится более чем в три раза.
Иногда приходится иметь дело с треугольными матрицами, у которых все элементы выше или ниже диагонали равны нулю. Квадратную матрицу будем называть нижнетреугольной, если все элементы выше главной диагонали равны нулю, и верхнетреугольной, если равны нулю все элементы ниже главной диагонали.
Рассмотрим систему из n линейных уравнений с n неизвестными:
$$a_{1,1}x_1+a_{1,2}x_2+\ldots+a_{1,n}x_n=b_1\\ a_{2,1}x_1+a_{2,2}x_2+\ldots+a_{2,n}x_n=b_2\\ \ldots\\ a_{n,1}x_1+a_{n,2}x_2+\ldots+a_{n,n}x_n=b_n$$В матричном виде эта система записывается намного элегантнее:
$$A*x=b$$Здесь вектор неизвестных x рассматривается как столбец - прямоугольная матрица размерности n*1. Аналогичный вид имеет вектор правых частей b системы уравнений. В матричном виде условие существования решения системы линейных уравнений 6.8 и нахождение самого решения формулируется совсем просто. Для существования решения необходимо и достаточно, чтобы
Для нахождения решения системы линейных уравнений, матрица которой имеет определитель, отличный от нуля, достаточно вычислить обратную матрицу и умножить ее на вектор правых частей системы уравнений.
Если нужно решить m систем линейных уравнений с одной и той же матрицей $$A$$, но с разными правыми частями, то обратную матрицу достаточно вычислить один раз. В матричном виде решение m систем линейных уравнений
$$A*X = B$$задается соотношением:
$$X = A^{-1}*B$$Здесь $$B$$ - прямоугольная матрица размерности $$n*m$$, каждый столбец которой представляет вектор правых частей одной системы уравнений. Соответствующий столбец матрицы $$X$$ дает решение этой системы. Что произойдет, если в качестве матрицы $$B$$ рассмотреть единичную матрицу? Очевидно, что тогда матрица $$X$$ будет представлять собой обратную матрицу $$A^{-1}$$. Несмотря на кажущуюся очевидность соотношения $$A^{-1} = A^{-1}*E$$, в нем есть определенный смысл, который постараюсь сейчас прояснить. Три задачи - вычисление определителя, решение системы линейных уравнений, нахождение обратной матрицы - имеют одинаковую вычислительную сложность и требуют, если не применять специальные алгоритмы, выполнения порядка $$n^3$$ операций умножения и сложения. Если посмотреть на соотношение 6.10, то кажется, что решить систему уравнений несколько сложнее, чем вычислить обратную матрицу, поскольку нужно вначале найти обратную матрицу, а затем умножить ее на вектор правых частей $$b$$. Однако реальный алгоритм, рассматриваемый ниже и находящий решение системы, вычислительно проще, чем тот же алгоритм, находящий обратную матрицу. Для такого алгоритма найти обратную матрицу - это все равно, что решить n систем линейных уравнений с одной и той же матрицей $$A$$ в левой части, используя матрицу $$E$$ в качестве правых частей.
Рассмотрим сейчас
Матрица $$B$$, дополняющая матрицу $$A$$, зависит от того, какую задачу предполагается решить. Если нужно вычислить только
После того, как построена
Рассмотрим на простом примере матричный вид элементарных операций. Пусть элементарная операция состоит в том, что к первой строке прибавляется вторая строка, умноженная на число $$q$$. Это действие эквивалентно умножению почти единичной матрицы на исходную матрицу:
$$\left|\left|\begin{array}{cccc}1q00\ldots0\\ 0100\ldots0\\ \ldots\ldots\ldots\ldots\\ 000\ldots01\end{array}\right|\right| * \left|\left|\begin{array}{cccc}a_{1,1}a_{1,2}\ldotsa_{1,n}\\ a_{2,1}a_{2,2}\ldotsa_{2,n}\\ \ldots\ldots\ldots\ldots\\ a_{n,1}a_{n,2}\ldotsa_{n,n}\end{array}\right|\right| = \left|\left|\begin{array}{lccc}a_{1,1}+qa_{2,1}a_{1,2}+qa_{2,2}\ldotsa_{1,n}+qa_{2,n}\\ a_{2,1}a_{2,2}\ldotsa_{2,n}\\ \ldots\ldots\ldots\ldots\\ a_{n,1}a_{n,2}\ldotsa_{n,n}\end{array}\right|\right|$$Матрица, задающая элементарную операцию, отличается от единичной матрицы тем, что у нее в первой строке на втором месте стоит число 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];
}
}
}
Аргументом метода является det формируется значение
Возможны различные модификации рассматриваемого алгоритма, исправляющие ситуацию.
Алгоритм с выбором первого ненулевого элемента
В случае, когда а[i, i] равно нулю, алгоритм ищет первую строку ниже i-й, в которой элемент a[i, j] не равен нулю. Эта строка добавляется к строке i, что гарантирует возможность деления на а[i, i].
Алгоритм с выбором главного элемента в столбце
Прежде чем приводить столбец к единичному виду, алгоритм ищет в столбце максимальный по модулю элемент и меняет местами строку i и строку j, в которой находится максимальный элемент. При обмене строк может измениться знак
Алгоритм с выбором главного элемента во всей матрице
На каждом шаге приведения очередного столбца к диагональному виду в еще не приведенной матрице отыскивается максимальный элемент и меняются местами не только строки, но и столбцы матрицы, ставя максимальный элемент в позицию а[i, i]. Этот прием гарантирует отсутствие переполнения при выполнении операции деления. Гарантируется также, что при умножениях не будет получено слишком большое число, поскольку деление на максимальный элемент с последующим умножением на один из элементов приводит к тому, что элементы преобразованной матрицы не увеличиваются по модулю. Однако ничто не дается даром. Выбор главного элемента, перестановка строк и столбцов, необходимость обратной перестановки в конце вычислений - все это усложняет алгоритм. Как правило, страдает и точность вычислений, особенно для плохо обусловленных матриц. Все модификации алгоритма стоит применять тогда, когда в основной схеме возникла исключительная ситуация, требующая корректировки алгоритма. Обработчик исключительной ситуации при делении на ноль, возникновении переполнения, потере значащих цифр может вызывать модифицированный вариант алгоритма в надежде получить решение, когда отказывается работать основная схема.
Замечу, что никакая модификация не может помочь найти обратную матрицу, если она не существует и
Вернемся к задаче построения интерполяционного полинома, проходящего через заданное множество точек. Напомню, заданы своими координатами $$(x_0, y_0), …(x_n, y_n)$$ точки, через которые должен пройти полином степени n. Требуется найти коэффициенты $$a_0, …a_n$$ этого полинома. Ранее был рассмотрен алгоритм Лагранжа, позволяющий построить этот полином. Нетрудно понять, что существует прямое решение этой задачи, состоящее в построении системы линейных уравнений для нахождения неизвестных коэффициентов полинома. В матричном виде эта система уравнений имеет вид:
$$Xa=y;\quad\text{где матрица }X\text{ имеет вид}\\ X=\left|\left|\begin{array}{ccccc}1x_0x_1\ldotsx_n\\ 1x_0^2x_1^2\ldotsx_n^2\\ \ldots\ldots\ldots\ldots\ldots\\ 1x_0^nx_1^n\ldotsx_n^n\end{array}\right|\right|$$Матрица X называется матрицей Вандермонда, а ее определитель - соответственно, определителем Вандермонда. Этот определитель вычисляется достаточно просто:
$$D(X)=\prod\limits_{j>i}(x_j-x_i)$$Он равен произведению разностей координат точек, где произведение берется по всем j, большим i. Очевидно, что определитель будет отличен от нуля, если у точек нет совпадающих координат x. Этот факт отмечался и при рассмотрении полинома Лагранжа, когда говорилось, что множество точек "не имеет возвратов".
Когда
На практике часто возникает ситуация, когда матрица системы появляется в результате измерений, ее элементы представляют не точные значения, а содержат ошибки измерений. Здесь система уравнений может иметь определитель, отличный от нуля, но быть "почти" линейно зависимой в пределах ошибок измерений. В таких случаях формально найденное решение может быть далеким от "истинного" решения. Как правило, матрица подобных систем является плохо обусловленной, а сама система уравнений называется неустойчивой. Дадим более точное определение. Матрица А называется плохо обусловленной, а система уравнений - неустойчивой, если малым изменениям элементов прямой матрицы соответствуют большие изменения в обратной матрице. Понятно, что если обратная матрица вычислена с большими ошибками, то и решение системы содержит ошибки такого же порядка.
Если матрица А плохо обусловлена, то и обратная к ней также является плохо обусловленной матрицей. Во сколько раз могут возрастать ошибки в элементах прямой матрицы при ее обращении? Примерный ответ на это дают "числа обусловленности" матрицы. Предлагаются различные количественные меры обусловленности матриц. Одной из таких мер является М-число обусловленности Тьюринга:
$$M-\text{число}=\frac{1}{n}M(A)*M(A^{-1});\quad\text{где}\\ M(A)=n*\max\limits_{i,j}\left|A_{i,j}\right|\text{ - норма матрицы}$$Матрица Вандермонда - потенциальный кандидат на плохую обусловленность. Если посмотреть на ее структуру, то видно, что для ее элементов во многих случаях характерен большой размах - отношение между максимальным и минимальным элементом велико. Действительно, пусть, например, максимальная по модулю координата $$x_n$$ имеет значение 100, а степень полинома n равна 6. Это довольно скромные цифры, но уже в этом случае минимальный элемент матрицы равен 1, а максимальный - $$10^{12}.$$ Примерно такой же размах будет и у элементов обратной матрицы. Ее максимальный элемент будет примерно равен 1, а минимальный - $$(max a_{i,j})^{-1}$$, так что число обусловленности M будет примерно равно $$10^{12}$$. При наличии небольших ошибок в измерении координат ошибки в определении значений полинома в точках, отличных от измеряемых, могут многократно возрастать. По этой причине
LinAlgebra. Методы класса должны реализовать все алгоритмы, рассмотренные в этом разделе. Интерфейс пользователя должен позволять пользователю решать основные задачи, возникающие при работе матрицами, определителями, системами линейных уравнений.Проект к данной лекции Вы можете скачать здесь.
Массив задает способ организации данных. Массивом называют упорядоченную совокупность элементов одного типа. Каждый элемент массива имеет индексы, определяющие порядок элементов. Число индексов характеризует размерность массива. Каждый индекс изменяется в некотором диапазоне [a,b]. В языке C#, как и во многих других языках, индексы задаются целочисленным типом. В других языках, например, в языке Паскаль, индексы могут принадлежать счетному конечному множеству, на котором определены функции, задающие следующий и предыдущий элемент. Диапазон [a,b] называется граничной парой, a - нижней границей, b - верхней границей индекса. При объявлении массива границы задаются выражениями. Если все границы заданы константными выражениями, то число элементов массива известно в момент его объявления и ему может быть выделена память еще на этапе трансляции. Такие массивы называются статическими. Если же выражения, задающие границы, зависят от переменных, то такие массивы называются динамическими, поскольку память им может быть отведена только динамически в процессе выполнения программы, когда становятся известными значения соответствующих переменных. Массиву, как правило, выделяется непрерывная область памяти.
В языке C# снято существенное ограничение языка C++ на статичность массивов. Массивы в языке C# являются динамическими. Как следствие этого, напомню, массивы относятся к ссылочным типам, память им отводится динамически в "куче". К сожалению, не снято ограничение 0-базируемости, означающее, что нижняя граница массивов C# фиксирована и равна нулю. Было бы гораздо удобнее во многих задачах иметь возможность работать с массивами, у которых нижняя граница изменения индекса не равна нулю.
Шаблоны, определенные в стандартных библиотеках, конечно, стоит использовать, но все-таки странной является рекомендация не пользоваться структурами, встроенными непосредственно в язык. Замечу, что в других языках массивы являются одной из любимых структур данных, используемых программистами.
В языке C#, соблюдая преемственность, сохранены одномерные массивы и массивы массивов. В дополнение к ним в язык добавлены многомерные массивы. Динамические многомерные массивы языка C# являются весьма мощной, надежной, понятной и удобной структурой данных, которую смело можно рекомендовать к применению не только профессионалам, но и новичкам, программирующим на C#. После этого краткого обзора давайте перейдем к более систематическому изучению деталей работы с массивами в C#.
Рассмотрим, как объявляются одномерные массивы, массивы массивов и многомерные массивы.
Напомню общую структуру объявления:
[<атрибуты>] [<модификаторы>] <тип> <объявители>;
Забудем пока об атрибутах и модификаторах. Объявление одномерного массива выглядит следующим образом:
<тип>[] <объявители>;
Заметьте, в отличие от языка C++ квадратные скобки приписаны не к имени переменной, а к типу. Они являются неотъемлемой частью определения типа, так что запись T[] следует понимать как тип, задающий одномерный массив с элементами типа T.
Что же касается границ изменения индексов, то эта характеристика не является принадлежностью типа, она является характеристикой переменных данного типа - экземпляров, каждый из которых является одномерным массивом со своим числом элементов, задаваемых в объявителе переменной.
Как и в случае объявления простых переменных, каждый объявитель может быть именем или именем с инициализацией. В первом случае речь идет об отложенной инициализации. Нужно понимать, что при
int[] a, b, c;
Чаще всего при объявлении массива используется имя с инициализацией. И опять-таки, как и в случае простых переменных, могут быть два варианта инициализации. В первом случае инициализация является явной и задается константным массивом. Вот пример:
double[] x= {5.5, 6.6, 7.7};
Следуя синтаксису, элементы константного массива необходимо заключать в фигурные скобки.
Во втором случае создание и инициализация массива выполняется в объектном стиле с вызовом конструктора массива. И это наиболее распространенная практика объявления массивов. Приведу пример:
int[] d= new int[5];
Итак, если массив объявляется без инициализации, то создается только висячая ссылка со значением void. Если инициализация выполняется конструктором, то в динамической памяти создается сам массив, элементы которого инициализируются константами соответствующего типа (ноль для арифметики, пустая строка для строковых массивов), и ссылка связывается с этим массивом. Если массив инициализируется константным массивом, то в памяти создается константный массив, с которым и связывается ссылка.
Как обычно задаются элементы массива, если они не заданы при инициализации? Они либо вычисляются, либо вводятся пользователем. Давайте рассмотрим первый пример работы с массивами из проекта с именем Arrays, поддерживающего эту лекцию:
public void TestDeclaration()
{
//объявляются три одномерных массива A,B,C
int[] A = new int[5], B= new int[5], C= new int[5];
Arrs.CreateOneDimAr(A);
Arrs.CreateOneDimAr(B);
for(int i = 0; i<5; i++)
C[i] = A[i] + B[i];
//объявление массива с явной инициализацией
int[] x ={5,5,6,6,7,7};
//объявление массивов с отложенной инициализацией
int[] u,v;
u = new int[3];
for(int i=0; i<3; i++) u[i] =i+1;
// v = {1,2,3}; //присваивание константного массива недопустимо
v = new int[4];
v = u; //допустимое присваивание
Arrs.PrintAr1("A", A); Arrs.PrintAr1("B", B);
Arrs.PrintAr1("C", C); Arrs.PrintAr1("X", x);
Arrs.PrintAr1("U", u); Arrs.PrintAr1("V", v);
}
На что следует обратить внимание, анализируя этот текст?
v = u.? Это корректное ссылочное присваивание: хотя u и v имеют разное число элементов, но они являются объектами одного класса. В результате присваивания память, отведенная массиву v, освободится, ей займется теперь сборщик мусора. Обе ссылки u и v будут теперь указывать на один и тот же массив, так что изменение элемента одного массива немедленно отражается на другом массиве.Arrs, статические методы которого выполняют различные операции над массивами. В частности, в примере использованы два метода этого класса, один из которых заполняет массив случайными числами, второй - выводит массив на печать.Вот текст первого из этих методов:
public static void CreateOneDimAr(int[] A)
{
for(int i = 0; i<A.GetLength(0);i++)
A[i] = rnd.Next(1,100);
}//CreateOneDimAr
Здесь rnd - это статическое поле класса Arrs, объявленное следующим образом:
private static Random rnd = new Random();
Процедура печати массива с именем name выглядит так:
public static void PrintAr1(string name,int[] A)
{
Console.WriteLine(name);
for(int i = 0; i<A.GetLength(0);i++)
Console.Write("\t" + name + "[{0}]={1}", i, A[i]);
Console.WriteLine();
}//PrintAr1
На рис 6.1 показан консольный вывод результатов работы процедуры TestDeclarations:
(рис 6.1) Результаты объявления и создания массивовОсобое внимание обратите на вывод, связанный с массивами u и v.
Во всех вышеприведенных примерах объявлялись
Чисто синтаксически нет существенной разницы в объявлении статических и динамических массивов. Выражение, задающее границу изменения индексов, в динамическом случае содержит переменные. Единственное требование - значения переменных должны быть определены в момент объявления. Это ограничение в C# выполняется, поскольку C# контролирует инициализацию переменных.
Приведу пример, в котором описана работа с динамическим массивом:
public void TestDynAr()
{
//объявление динамического массива A1
Console.WriteLine("Введите число элементов массива A1");
int size = int.Parse(Console.ReadLine());
int[] A1 = new int[size];
Arrs.CreateOneDimAr(A1);
Arrs.PrintAr1("A1",A1);
}//TestDynAr
В особых комментариях эта процедура не нуждается. Здесь верхняя граница массива определяется пользователем.
Уже объяснялось, что разделение массивов на одномерные и многомерные носит исторический характер. Никакой принципиальной разницы между ними нет. Одномерные массивы - это частный случай многомерных. Можно говорить и по-другому: многомерные массивы являются естественным обобщением одномерных. Одномерные массивы позволяют задавать такие математические структуры, как векторы, двумерные - матрицы, трехмерные -
Размерность массива это характеристика типа. Как синтаксически при объявлении типа массива указать его размерность? Это делается достаточно просто, за счет использования запятых. Вот как выглядит объявление многомерного массива в общем случае:
<тип>[, … ,] <объявители>;
Число запятых, увеличенное на единицу, и задает размерность массива. Что касается объявителей, то все, что сказано для одномерных массивов, справедливо и для многомерных. Можно лишь отметить, что хотя явная инициализация с использованием многомерных константных массивов возможна, но применяется редко из-за громоздкости такой структуры. Проще инициализацию реализовать программно, но иногда она все же применяется. Вот пример:
public void TestMultiArr()
{
int[,]matrix = {
{1,2},
{3,4}
};
Arrs.PrintAr2("matrix", matrix);
}//TestMultiArr
Давайте рассмотрим классическую задачу умножения прямоугольных матриц. Нам понадобится три динамических массива для представления матриц и три процедуры, одна из которых будет заполнять исходные матрицы случайными числами, другая - выполнять умножение матриц, третья - печатать сами матрицы. Вот тестовый пример:
public void TestMultiMatr()
{
int n1, m1, n2, m2,n3, m3;
Arrs.GetSizes("MatrA",out n1,out m1);
Arrs.GetSizes("MatrB",out n2,out m2);
Arrs.GetSizes("MatrC",out n3,out m3);
int[,]MatrA = new int[n1,m1], MatrB = new int[n2,m2];
int[,]MatrC = new int[n3,m3];
Arrs.CreateTwoDimAr(MatrA); Arrs.CreateTwoDimAr(MatrB);
Arrs.MultMatr(MatrA, MatrB, MatrC);
Arrs.PrintAr2("MatrA",MatrA); Arrs.PrintAr2("MatrB",MatrB);
Arrs.PrintAr2("MatrC",MatrC);
}//TestMultiMatr
Три матрицы MatrA, MatrB и MatrC имеют произвольные размеры, выясняемые в диалоге с пользователем, и использование для их описания динамических массивов представляется совершенно естественным. Метод CreateTwoDimAr заполняет случайными числами элементы матрицы, переданной ему в качестве аргумента, метод PrintAr2 выводит матрицу на печать. Я не буду приводить их код, похожий на код их одномерных аналогов.
Метод MultMatr выполняет умножение прямоугольных матриц. Это классическая задача из набора задач, решаемых на первом курсе. Вот текст этого метода:
public void MultMatr(int[,]A, int[,]B, int[,]C)
{
if (A.GetLength(1) != B.GetLength(0))
Console.WriteLine("MultMatr: ошибка размерности!");
else
for(int i = 0; i < A.GetLength(0); i++)
for(int j = 0; j < B.GetLength(1); j++)
{
int s=0;
for(int k = 0; k < A.GetLength(1); k++)
s+= A[i,k]*B[k,j];
C[i,j] = s;
}
}//MultMatr
В особых комментариях эта процедура не нуждается. Замечу лишь, что прежде чем проводить вычисления, производится проверка корректности размерностей исходных матриц при их перемножении - число столбцов первой матрицы должно быть равно числу строк второй матрицы.
Взгляните, как выглядят результаты консольного вывода на данном этапе работы.
(рис 6.2) Умножение матрицЕще одним видом массивов C# являются массивы массивов, называемые также изрезанными массивами (jagged arrays). Такой
В каких ситуациях может возникать необходимость в таких структурах данных? Эти массивы могут применяться для
Есть некоторые особенности в объявлении и инициализации таких массивов. Если при объявлении типа многомерных массивов для указания размерности использовались запятые, то для int[][] задает массив, элементы которого - одномерные массивы элементов типа int.
Сложнее с созданием самих массивов и их инициализацией. Здесь нельзя вызвать конструктор new int[3][5], поскольку он не задает изрезанный массив. Фактически нужно вызывать конструктор для каждого массива на самом нижнем уровне. В этом и состоит сложность объявления таких массивов. Начну с формального примера:
//массив массивов - формальный пример
//объявление и инициализация
int[][] jagger = new int[3][]
{
new int[] {5,7,9,11},
new int[] {2,8},
new int[] {6,12,4}
};
Массив jagger имеет всего два уровня. Можно считать, что у него три элемента, каждый из которых является массивом. Для каждого такого массива необходимо вызвать конструктор new, чтобы создать внутренний массив. В данном примере элементы внутренних массивов получают значение, будучи явно инициализированы константными массивами. Конечно, допустимо и такое объявление:
int[][] jagger1 = new int[3][]
{
new int[4],
new int[2],
new int[3]
};
В этом случае элементы массива получат при инициализации нулевые значения. Реальную инициализацию нужно будет выполнять программным путем. Стоит заметить, что в конструкторе верхнего уровня константу 3 можно опустить и писать просто new int[][]. Самое забавное, что вызов этого конструктора можно вообще опустить, он будет подразумеваться:
int[][] jagger2 =
{
new int[4],
new int[2],
new int[3]
};
Но вот конструкторы нижнего уровня необходимы. Еще одно важное замечание - динамические массивы возможны и здесь. В общем случае, границы на любом уровне могут быть выражениями, зависящими от переменных. Более того, допустимо, чтобы массивы на нижнем уровне были многомерными. Но это уже "от лукавого", вряд ли стоит пользоваться такими сложными структурами данных, ведь с ними предстоит еще и работать.
Приведу теперь чуть более реальный пример, описывающий простое генеалогическое дерево, которое условно назову "отцы и дети":
/// <summary>
/// массив массивов -"Отцы и дети"
/// </summary>
public void GenTree()
{
int Fcount = 3;
string[] Fathers = new string[Fcount];
Fathers[0] = "Николай"; Fathers[1] = "Сергей"; Fathers[2] = "Петр";
string[][] Children = new string[Fcount][];
Children[0] = new string[] {"Ольга", "Федор"};
Children[1] = new string[] {"Сергей", "Валентина", "Ира", "Дмитрий"};
Children[2] = new string[] {"Мария", "Ирина", "Надежда"};
Arrs.PrintAr3(Fathers, Children);
}
Здесь отцов описывает обычный динамический одномерный массив Fathers. Для описания детей этих отцов необходим уже Fathers. Здесь показан еще один способ создания таких массивов. Вначале конструируется массив верхнего уровня, содержащий ссылки со значением void. А затем на нижнем уровне конструктор создает настоящие массивы в динамической памяти, с которыми и связываются ссылки.
Я не буду демонстрировать работу с генеалогическим деревом, ограничусь лишь печатью этого массива. Здесь есть несколько поучительных моментов. В классе Arrs для печати массива создан специальный метод PrintAr3, которому в качестве аргументов передаются массивы Fathers и Children. Вот текст данной процедуры:
/// <summary>
/// Печать дерева "Отцы и дети",
/// заданного массивами Fathers и Children
/// </summary>
/// <param name="Fathers">массив отцов</param>
/// <param name="Children"> массив массивов детей</param>
public static void PrintAr3(string[] Fathers, string[][] Children)
{
for (int i = 0; i < Fathers.Length; i++)
{
Console.WriteLine("Отец : {0}; Его дети:", Fathers[i]);
for (int j = 0; j < Children[i].Length; j++)
Console.Write(Children[i][j] + " ");
Console.WriteLine();
}
}//PrintAr3
Приведу некоторые комментарии к этой процедуре.
i организован по числу элементов массива Fathers. Заметьте, здесь используется свойство Length, в отличие от ранее применяемого метода GetLength.Children. Свойство Length для него возвращает число элементов верхнего уровня, совпадающее, как уже говорилось, с числом элементов массива Fathers.Length вызывается для каждого элемента Children[i], который является массивом.Приведу вывод, полученный в результате работы процедуры PrintAr3.
(рис 6.3) Дерево "Отцы и дети"В наших примерах массивы неоднократно передавались процедурам в качестве входных аргументов и возвращались в качестве результатов. Остается подчеркнуть только некоторые детали.
C в процедуре MultMatr, ref или out (хотя и допустимо). Передача аргумента по значению в таких ситуациях так же хороша, как и передача по ссылке. В результате вычислений меняется сам массив в динамической памяти, а ссылка на него остается постоянной. Процедура и ее вызов без ключевых слов выглядит проще, поэтому обычно они опускаются. Заметьте, в процедуре GetSizes, где определялись границы массива, ключевое слово out, сопровождающее аргументы, совершенно необходимо.Алгоритмы и задачи, рассматриваемые в этой главе, являются частью фундамента, на котором строится образование программиста. Нет ни одной проблемной области, в задачах которой не требовались бы массивы. Поэтому задачи, требующие использования массивов, появлялись уже в предыдущих главах, появятся они и в последующих. Но здесь мы будем заниматься ими целенаправленно.
Последовательность элементов - $$a_1, a_2, \ldots a_n$$ - одна из любимых структур в математике. Последовательность можно рассматривать как функцию $$a(i)$$, которая по заданному значению индекса элемента возвращает его значение. Эта функция задает отображение $$integer -> T$$, где $$T$$ - это тип элементов последовательности. В программировании последовательности это одномерные массивы, но от этого они не перестают быть менее любимыми.
Определение. Массив - это упорядоченная последовательность элементов одного типа. Порядок элементов задается с помощью индексов.
В отличие от математики, где последовательность может быть бесконечной, массивы всегда имеют конечное число элементов. Для программистов важно то, как массивы хранятся в памяти. Массивы занимают непрерывную область памяти, поэтому, зная адрес начального элемента массива, зная, сколько байтов памяти требуется для хранения одного элемента, и зная индекс (индексы) некоторого элемента, нетрудно вычислить его адрес, а значит, и хранимое по этому адресу значение элемента. На этом основана a(i) задается адресным выражением a+i, в котором имя массива a воспринимается как адрес первого элемента. При вычислении адреса i-го элемента индекс i умножается на длину слова, требуемого для хранения элементов типа T. а+0.
Язык C# сохранил 0-базируемость массивов. Индексы элементов массива в языке C# изменяются в плотном интервале значений от нижней границы, всегда равной 0, до верхней границы, которая задана динамически вычисляемым выражением, возможно, зависящим от переменных. Массивы C# являются 0-базируемыми динамическими массивами. Это важно понимать с самого начала.
Не менее важно понимать и то, что массивы C# относятся к ссылочным типам.
Как у массивов появляются значения, как они изменяются? Возможны три основных способа:
В задачах этого раздела ограничимся пока рассмотрением первых двух способов. Первый способ более или менее понятен. Простые примеры его применения приводились неоднократно. Стоит только отметить, что в классе, работающем с массивами, всегда полезно иметь метод FillArray, позволяющий заполнять массив случайными числами. В примерах использование возможностей класса Random для моделирования элементов массива встречалось неоднократно.
Приведу некоторые рекомендации по вводу и выводу массивов, ориентированные на работу с конечным пользователем.
Для консольных приложений ввод массива обычно проходит несколько этапов:
Вначале у пользователя запрашиваются размеры массива, затем создается массив заданного размера. В цикле по числу элементов организуется ввод значений. Вводу каждого значения предшествует приглашение к вводу с указанием типа вводимого значения, а при необходимости - и диапазона, в котором должно находиться требуемое значение. Поскольку ввод значений - это ответственная операция, а на пользователя никогда нельзя положиться, после ввода часто организуется проверка корректности введенного значения. При некорректном задании значения элемента ввод повторяется, пока не будет достигнут желаемый результат.
При выводе массива на консоль обычно вначале выводится имя массива, а затем его элементы в виде пары: <имя> = <значение> (например, f[5] = 77,7). Задача осложняется для многомерных массивов, когда пользователю важно видеть не только значения, но и структуру массива, располагая строку массива в строке экрана.
Как организовать контроль ввода? Наиболее разумно использовать для этих целей конструкцию охраняемых блоков - try - catch блоков. Это общий подход, когда все опасные действия, связанные с работой пользователя, внешних устройств, внешних источников данных, размещаются в
Как правило, для ввода-вывода массивов пишутся специальные процедуры, вызываемые в нужный момент.
Приложения Windows позволяют построить дружелюбный интерфейс пользователя, облегчающий работу по вводу и выводу массивов. И здесь, когда данные задаются пользователем, заполнение массива проходит через те же этапы, что рассматривались для консольных приложений. Но выглядит все это более красиво, наглядно и понятно. Пример подобного интерфейса, обеспечивающего работу по вводу и выводу одномерного массива, показан на рис 6.4.
(рис 6.4) Форма для ввода-вывода одномерного массиваПользователь вводит в текстовое окно число элементов массива и нажимает командную кнопку "Создать массив", обработчик которой создает массив заданной размерности, если корректно задан размер массива, в противном случае выдает сообщение об ошибке и ждет корректного ввода.
В случае успешного создания массива пользователь может переходить к следующему этапу - вводу элементов массива. Очередной элемент массива вводится в текстовое окно, а обработчик командной кнопки "Ввести элемент" обеспечивает передачу значения в массив. Корректность ввода контролируется и на этом этапе, проверяя значение введенного элемента и выводя в специальное окно сообщение в случае его некорректности, добиваясь, в конечном итоге, получения от пользователя корректного ввода.
Для облегчения работы пользователя выводится подсказка, какой именно элемент должен вводить пользователь. После того, как все элементы массива введены, окно ввода становится недоступным для ввода элементов. Интерфейс формы позволяет многократно создавать новый массив, повторяя весь процесс.
На рис 6.4 форма разделена на две части - для ввода и вывода массива. Крайне важно уметь организовать ввод массива, принимая данные от пользователя. Не менее важно уметь отображать существующий массив в форме, удобной для восприятия пользователя. На рисунке показаны три различных элемента управления, пригодные для этих целей, - ListBox, CheckedListBox и ComboBox. Как только вводится очередной элемент, он немедленно отображается во всех трех списках.
В реальности отображать массив в трех списках, конечно, не нужно, это сделано только в целях демонстрации возможностей различных элементов управления. Для целей вывода подходит любой из них, выбор зависит от контекста и предпочтений пользователя. Элемент ComboBox имеет дополнительное текстовое окно, в которое пользователь может вводить значение. Элемент CheckedListBox обладает дополнительными свойствами в сравнении с элементом ListBox, позволяя отмечать некоторые элементы списка (массива). Отмеченные пользователем элементы составляют специальную коллекцию. Эта коллекция доступна, с ней можно работать, что иногда весьма полезно. Чаще всего для вывода массива используется элемент ListBox.
Посмотрим, как это все организовано программно. Начну с полей формы :
//fields int n = 0; double[] mas; int currentindex = 0; double ditem = 0; const string SIZE = "Корректно задайте размер массива!"; const string INVITE = "Введите число в формате m[,n]"; const string EMPTY = "Массив пуст!"; const string ITEMB = "mas["; const string ITEME = "] = "; const string FULL = "Ввод недоступен!"; const string OK = "Корректный ввод!"; const string ERR = "Ошибка ввода числа! Повторите ввод!";
Полями этого класса является одномерный массив, его размер, текущий индекс и константы, используемые в процессе диалога с пользователем. Обработчик события Click командной кнопки, отвечающей за создание массива, имеет вид:
private void buttonCreateArray_Click(object sender, EventArgs e)
{
try
{
n = Convert.ToInt32(textBoxN.Text);
mas = new double[n];
labelInvite.Text = INVITE;
labelItem.Text = ITEMB + "0" + ITEME;
labelResult.Text = EMPTY;
textBoxItem.ReadOnly = false;
listBox1.Items.Clear();
comboBox1.Items.Clear();
checkedListBox1.Items.Clear();
comboBox1.Items.Clear();
currentindex = 0;
}
catch (Exception)
{
labelResult.Text = SIZE;
}
}
Первым делом принимается размер массива, введенный пользователем. Преобразование к типу int введенного значения помещено в
private void buttonAddItem_Click(object sender, EventArgs e)
{
//Заполнение массива элементами
if (GetItem())
{
mas[currentindex] = ditem;
listBox1.Items.Add(mas[currentindex]);
checkedListBox1.Items.Add(mas[currentindex]);
comboBox1.Items.Add(mas[currentindex]);
currentindex++;
labelItem.Text = ITEMB + currentindex + ITEME;
textBoxItem.Text = "";
labelResult.Text = OK;
if (currentindex == n)
{
labelInvite.Text = "";
labelItem.Text = "";
labelResult.Text = FULL;
textBoxItem.Text = "";
textBoxItem.ReadOnly = true;
}
}
}
Функция GetItem вводит значение очередного элемента. Если пользователь корректно задал его значение, то элемент добавляется в массив, а заодно и в списки, отображающие текущее состояние массива. Создается подсказка для ввода следующего элемента массива, а если массив полностью определен, то форма переходит в состояние окончания ввода.
/// <summary>
/// Ввод с контролем текущего элемента массива
/// </summary>
/// <returns>true в случае корректного ввода значения</returns>
bool GetItem()
{
string item = textBoxItem.Text;
bool res = false;
if (item == "")
labelResult.Text = INVITE;
else
{
try
{
ditem = Convert.ToDouble(item);
res = true;
}
catch(Exception)
{
labelResult.Text = ERR;
}
}
return res;
}
Форму OneDimArrayForm можно рассматривать как некоторый шаблон, полезный при организации ввода и вывода одномерных массивов.
Ввод двумерного массива немногим отличается от ввода одномерного массива. Сложнее обстоит дело с выводом двумерного массива, если при выводе пытаться отобразить структуру массива. К сожалению, все три элемента управления, хорошо справляющиеся с отображением одномерного массива, плохо приспособлены для показа структуры двумерного массива. Хотя у того же элемента показан пример формы, поддерживающей работу по вводу и выводу двумерного массива.
(рис 6.5) Форма, поддерживающая ввод и вывод двумерного массиваИнтерфейс формы схож с тем, что использовался для организации работы с одномерным массивом. Схожа и программная организация ввода-вывода элементов массива. Поэтому я не буду приводить код, поддерживающий работу с формой TwoDimArrayForm, надеясь, что читатель при желании сможет его восстановить. Остановлюсь лишь на одном моменте, позволяющем отображать двумерный массив в элементе управления ListBox так, чтобы сохранялась структура строк и столбцов массива. Этого можно добиться за счет программной настройки размеров элемента управления ListBox:
listBox1.Height = n * HEIGHT_LINE;
listBox1.Width = m * 2 * HEIGHT_LINE;
Константа HEIGHT_LINE задает высоту строки в списке. Вначале водятся элементы первого столбца; когда весь столбец введен, автоматически следующее вводимое значение будет отображаться в первой строке в следующем столбце.
В общей ситуации, когда значения, вводимые пользователем, могут колебаться в широком диапазоне, трудно гарантировать отображение структуры двумерного массива. Однако ситуация не безнадежна. Есть и другие, более мощные и более подходящие для наших целей элементы управления. Если на элементах ListBox и подобных ему я останавливаться не буду, оставляя их для самостоятельного изучения, то об элементе DataGridView расскажу подробнее.
Элемент управления DataGridView является последней новинкой в серии табличных элементов DataGrid, позволяющих отображать таблицы. Главное назначение этих элементов - связывание с таблицами внешних источников данных, прежде всего с таблицами баз данных. Мы же сейчас рассмотрим другое его применение - в интерфейсе, позволяющем пользователю вводить и отображать матрицы - двумерные массивы.
Рассмотрим классическую задачу умножения прямоугольных матриц показан возможный вид формы, поддерживающей работу пользователя. Форма показана в тот момент, когда пользователь уже задал размеры и значения исходных матриц, выполнил умножение матриц и получил результат.
(рис 6.6) Форма с элементами DataGridView, поддерживающая работу с матрицамиНа форме расположены три текстовых окна для задания размеров матриц, три элемента . В них отображается информация, связанная с формой и отдельными элементами управления. Текст у невидимых на рисунке меток появляется тогда, когда обнаруживается, что пользователь некорректно задал значение какого-либо элемента исходных матриц.
А теперь перейдем к описанию того, как этот интерфейс реализован. В классе 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.Обработчик события "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();
}
Полиномом n-й степени $$P_n(x)$$ называют функцию:
$$P_n(x)=a_n x^n+a_{n-1}x^{n-1}+\ldots+a_1 x+a_0$$Если рассматривать график этой функции на плоскости, то $$x$$ и $$P_n(x)$$ - это декартовы координаты точек графика функции. Значения $$a_k$$ (k из интервала [0,n]) называются коэффициентами полинома. Все они принадлежат одному типу и при программной работе с полиномами представляются одномерным массивом с n+1 элементами.
Если задан массив коэффициентов полинома $$A$$, то вычислить значение полинома в точке $$x$$ не представляет особой сложности. Но ни один уважающий себя программист не позволит себе вычислять значение полинома, буквально пользуясь схемой 6.1, требующей n-1 операций возведения в степень, n операций умножения и n операций сложения. Прекрасный пример того, как можно упростить алгоритм, дает
Удобнее представлять
Вначале вычисляется значение полинома нулевой степени, состоящего из коэффициента при старшем члене исходного полинома. Затем рекуррентно повышается степень полинома, для чего достаточно умножить на x предыдущее значение и добавить новый коэффициент. В программе эта схема естественным образом реализуется обычным циклом, где на каждом шаге выполняется одно умножение и одно сложение.
Если $$P_n(x)$$ - полином n-й степени с коэффициентами $$a_i$$, $$Q_n(x)$$ - полином n-й степени с коэффициентами $$b_i$$ и $$P_n(x) = Q_n(x)$$, то из этого следует равенство соответствующих коэффициентов:
$$(P_n(x)=Q_n(x))\Rightarrow(a_i=b_i;\; \forall_i=0\ldots n)$$Многие задачи над полиномами связаны с определением их корней. Напомню, $$x_0$$ является корнем полинома, если $$P_n(x_0) = 0$$. У полинома n-й степени не более чем $$n$$ действительных корней. Если $$n$$ - нечетно, то полином имеет хотя бы один действительный корень. Все корни полинома принадлежат некоторому конечному интервалу [c, d]. Вне этого интервала поведение полинома определяется его старшим членом - $$a_n x^n$$. Для полинома четной степени обе ветви уходят в $$+\infty$$, если $$a_n >0$$ и в $$-\infty$$, если $$a_n<0$$. Для полинома нечетной степени ветви полинома вне интервала [c, d] разнонаправлены. Если $$a_n>0$$, то правая ветвь уходит в $$+\infty$$, а левая ветвь - в $$-\infty$$. Если $$a_n<0$$, то левая ветвь уходит в $$+\infty$$, а правая ветвь - в $$-\infty$$.
Когда по каким-либо физическим соображениям интервал [c, d] известен хотя бы приблизительно, задача нахождения корней полинома облегчается, в противном случае она может быть довольно трудной, особенно для случая близко расположенных корней.
Рассмотрим один из простых алгоритмов, исследующих, существует ли на заданном интервале [e, f] хотя бы один корень. Один корень заведомо существует, если полином на концах исследуемого интервала имеет разные знаки или один из концов интервала уже является корнем полинома. Это условие и будет характерным признаком поиска нужного интервала. Если исходный интервал [e, f] удовлетворяет характерному признаку, то задача решена и такой интервал найден. В противном случае в цикле по $$k$$ вычислим $$h = L/2^k$$, где $$L$$ - длина исходного интервала ( $$L= f-e$$ ). Затем организуем внутренний цикл, в котором проверим характерный признак на всех интервалах длины h. Если интервал будет найден, то вычисления завершаются, в противном случае переходим к следующему шагу цикла по $$k$$, производя очередное дробление $$h$$. Завершение цикла по $$k$$ означает, что если исследуемый интервал [e, f] и содержит корни, то это близкие пары корней, отстоящие друг от друга на расстояние, меньшее $$h$$ - заключительной длины интервала по завершении цикла по $$k$$.
Приведу несколько практических рекомендаций, полезных при реализации этой схемы. Внутренний цикл следует организовать так, чтобы не повторять вычисление полинома в тех точках, в которых это вычисление проводилось на предыдущих шагах цикла. Это означает, что когда шаг $$h = L/2^k$$, то во внутреннем цикле достаточно вычислить значение полинома не более чем в $$2^{k-1}$$ точках. Внешний цикл достаточно ограничить числом в интервале от 10 до 20, поскольку уже при $$k=10$$ величина исходного интервала $$L$$ уменьшится более чем в 1000 раз, что вполне достаточно в большинстве практических ситуаций. Хотя следует помнить, что в ряде ситуаций практики приходится иметь дело с резко осциллирующими функциями, где близкие корни являются правилом, а не исключением.
Рассмотрим несколько простых схем нахождения корня полинома. Заметим, что все эти схемы применимы к нахождению корней любых функций, а не только полиномов. Как всегда в программировании, речь идет не столько о точном нахождении корня, сколько о нахождении корня с заданной точностью $$\varepsilon$$. Так что, если $$x_0$$ - это точное значение корня, то нам достаточно найти $$x*$$ - такое, что $$|x_0 - x*| < \varepsilon$$.
Эта схема прекрасно подходит, когда предварительно проведено исследование интервала существования корня и найден такой интервал [e, f], на концах которого полином принимает разные знаки, так что существует корень внутри интервала. Если исходный интервал мал и сравним с заданной точностью $$\varepsilon$$, то в качестве корня можно выбрать середину этого интервала. Если же исходный интервал больше, чем значение $$\varepsilon$$, то интервал можно разделить пополам и из двух половинок выбрать ту, для которой выполняется характерный признак существования корня. Понятно, что если признак выполняется для всего интервала, то он обязательно будет выполняться для одной из его половинок. Деление отрезка пополам приводит к быстрому уменьшению длины отрезка, так что 10-20 делений достаточно, чтобы найти интервал длины, меньшей $$\varepsilon$$, а следовательно, и корень полинома с заданной точностью.
Формально метод применим и в том случае, когда неизвестен интервал, в котором существует корень функции. Пусть $$x_0$$ - некоторое заданное начальное приближение к корню полинома. Тогда можно построить следующий итерационный процесс:
$$x_k=x_{k-1}-f(x_{k-1});\quad k=1,2,\ldots$$Метод записан для произвольной функции $$f$$, в нашем случае функция $$f$$ задана полиномом. Итерационный процесс следует прекращать либо по достижении заданной точности, либо по достижении максимально допустимого числа итераций $$N$$. Заметьте, следует задавать оба условия, поскольку сходимость процесса простой итерации к корню, даже если он существует, не гарантируется. Во многом все зависит от удачного выбора начального приближения.
Метод простой итерации обладает полезным свойством "неподвижной точки". Корни функции являются "неподвижными точками" метода. Нетрудно заметить, что если на некотором шаге $$x_{k-1}$$ сошлось к корню полинома $$x*$$, то $$x_k$$ и все последующие итерации будут равны $$x*$$, так что итерационный процесс из найденного корня не уходит.
Этот метод чуть более сложен в реализации, но обладает лучшей сходимостью в сравнении с методом простой итерации, хотя и здесь сходимость во многом зависит от удачного выбора начального приближения $$x_0$$. Для произвольной функции $$f$$ итерационный процесс метода Ньютона выглядит так:
$$x_k=x_{k-1}-\frac{f(x_{k-1})}{f'(x_{k-1})};\quad k=1,2,\ldots$$Понятно, что производной от полинома n-й степени будет полином степени n-1, коэффициенты которого легко вычисляются по n и коэффициентам исходного полинома. Все, что было сказано о методе простой итерации - завершение процесса, обладание неподвижной точкой, - справедливо и для метода Ньютона.
Если для полинома $$P(x)$$ n-й степени найден корень $$x_1$$, то можно понизить степень полинома, построив полином $$P1(x)$$ степени $$n-1$$, у которого все корни совпадают с корнями полинома $$P(x)$$ за исключением того, что у него нет корня $$x_1$$.
Запишем соотношение, связывающее полиномы:
$$P(x)=P1(x)(x-x1)\\ a_n x^n+a_{n-1}x^{n-1}+\ldots+a_1 x+a_0=(b_{n-1}x^{n-1}+b_{n-2}x^{n-2}+\ldots+b_1 x+b_0)(x-x1)$$Учитывая соотношение 6.3 о равенстве двух полиномов одной степени, можно выписать $$n+1$$ соотношение, связывающее коэффициенты этих полиномов. Эти соотношения нетрудно разрешить относительно неизвестных коэффициентов $$b_k$$. В результате получим:
$$b_{n-1}=a_n;\\ b_{n-2}=a_{n-1}+b_{n-1}x_1;\\ \ldots\\ b_0=a_1+b_1 x_1;$$Заметьте, неизвестных всего $$n$$, а уравнений можно построить - $$n+1$$. Но последнее уравнение $$(a0 + b_0 x1 = 0)$$ является следствием предыдущих и используется для контроля вычислений.
К новому полиному можно применить тот же процесс - найти его корень и понизить затем степень полинома. Реально понижение степени не намного упрощает задачу отыскания корней, так что чаще всего проще искать корни исходного полинома, изменяя начальные приближения в итерационном процессе или отыскивая различные интервалы, на которых полином меняет свой знак.
До сих пор рассматривалась задача отыскания корней полинома с заданными коэффициентами. Иногда приходится решать обратную задачу - найти коэффициенты полинома, если известны его корни - $$x_1, x_2, … x_n$$. Полиномов с одинаковыми корнями существует бесчисленное множество. Однако среди них существует единственный полином с коэффициентом $$a_n$$, равным единице. Этот полином называется приведенным, его-то и будем строить. Все остальные полиномы получаются из приведенного полинома умножением всех коэффициентов на произвольное число $$a_n$$, от которого требуется лишь, чтобы оно не было равно нулю. Поэтому для однозначного решения задачи требуется задать n корней и коэффициент при старшем члене полинома. Тогда можно записать следующее равенство:
$$P_n(x)=a_n(x-x_n)(x-x_{n-1})\ldots(x-x_1)$$Для нахождения коэффициентов полинома $$P_n(x)$$ воспользуемся, как обычно, соотношением 6.3. Но применить его напрямую сложно. Поэтому воспользуемся процессом, обратным к процессу понижения степени. Построим вначале $$P_1(x)$$ - полином первой степени, у которого $$x_1$$ является единственным корнем. Затем повысим степень и построим полином второй степени - $$P_2(x)$$, у которого появляется еще один корень - $$x_2$$. Продолжая этот процесс, дойдем до искомого полинома $$P_n(x)$$. При вычислении коэффициентов нового полинома будем использовать коэффициенты уже посчитанного полинома на единицу меньшей степени. Получающиеся в результате соотношения близки к тем, что приведены для случая понижения степени полинома.
Коэффициенты полинома первой степени $$P_1(x)$$ выписываются явно:
$$a_1=1;\quad a_0=-x_1;$$Коэффициенты полинома k-й степени вычисляются через коэффициенты полинома степени k-1:
$$P_k(x)=P_{k-1}(x)(x-x_k)$$Переходя к коэффициентам, получим следующие уравнения:
$$a_k=a'_{k-1};\\ a_{k-i}=a'_{k-i-1}-a'_{k-i}x_k;\quad \forall i\quad i=1,\dots k-1;\\ a_0=-a'_0 x_k;$$В соотношении 6.5 через $$a'$$ обозначены коэффициенты полинома степени $$k-1$$. На самом деле схема безопасна и позволяет считать коэффициенты на том же месте, не требуя дополнительной памяти. Приведу алгоритм вычисления коэффициентов полинома по его корням в виде схемы, приближенной к языку C#.
Дано:
Вычислить:
//Вычисляем коэффициенты полинома первой степени
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$$ точка: $$R_0(x_0, y_0), R_1(x_1, y_1), R_2(x_2, y_2),\ldots, R_n(x_n, y_n)$$. Полиномом Лагранжа $$P_L(x)$$ называется полином n-й степени, проходящий через все точки $$R_k$$. Если точки $$R_k$$ не образуют возвратов, то такой полином существует и является единственным. Под возвратом понимается ситуация, когда существуют две точки $$R_i$$ и $$R_j$$ такие, что $$x_i = x_j$$.
Как построить такой полином? Лагранж предложил следующий алгоритм. Полином $$P_L(x)$$ строится как сумма $$n+1$$ полиномов n-й степени:
$$P_L(x)=\sum\limits_{k=1}^{n+1}P_k(x)$$Каждый из полиномов $$P_k(x)$$, входящих в сумму, строится следующим образом. Корнями полинома $$P_k(x)$$ являются все точки $$R_i$$ за исключением точки $$R_k$$. Единственность $$P_k(x)$$ обеспечивается за счет того, что коэффициент при старшем члене an подбирается так, чтобы полином проходил через точку $$R_k$$. В записи Лагранжа полином $$P_k(x)$$ выглядит следующим образом:
$$P_k(x)=y_k\frac{(x-x_o)(x-x_1)\ldots(x-x_{k-1})(x-x_{k+1})\ldots(x-x_n)}{(x_k-x_0)(x_k-x_1)\ldots(x_k-x_{k-1})(x_k-x_{k+1})\ldots(x_k-x_n)}$$В записи 6.6 в числителе находится приведенный полином, построенный по корням, а $$y_k$$, деленное на знаменатель в формуле 6.6, задает $$an$$ -
Условия, накладываемые на полиномы $$P_k(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 - массив корней приведенного полинома $$P_k$$,coefk - массив его коэффициентов,An - coefL - массив коэффициентов полинома Лагранжа,HornerP - метод, вычисляющий по схеме Горнера значение полинома по его коэффициентам и значению координаты x,CalcCoefFromRoots - метод, вычисляющий массив коэффициентов приведенного полинома по его корням.При рассмотрении полинома Лагранжа возникала необходимость в нахождении суммы полиномов одинаковой степени, заданных своими коэффициентами. Пусть P(x) и Q(x) - полиномы степени n и m, соответственно, заданные своими коэффициентами, и пусть для определенности $$n >= m$$. Тогда суммой полиномов называется полином R(x) степени n, коэффициенты которого вычисляются следующим образом:
$$R(x)=P(x)+Q(x)\Rightarrow \sum\limits_{i=0}^n c_i x^i=\sum\limits_{i=0}^n a_i x^i+\sum\limits_{i=0}^m b_i x^i\Rightarrow\\ c_i=a_i+b_i\quad \forall i<=m;\\ c_i=a_i\quad \forall i:m<i<=n;$$Пусть полиномы P(x) и Q(x) заданы, подобно
Тогда нетрудно найти подобное представление и для полинома R(x), представляющего сумму полиномов:
$$R(x):\{(rx_0,ry_0),(rx_1,ry1),\ldots(rx_n,ry_n)\},\quad\text{где}\\ rx_i=px_i;\quad ry_i=py_i+Q(px_i);$$В этом случае понадобится вычислить значения полинома 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)=\{(sx_0,sy_0),(sx_1,sy_1),\ldots(sx_{n+m},sy_{n+m})\},\quad\text{где}\\ sx_i=px_i;\quad sy_i=py_i*Q(px_i)\quad \forall i:i<=n;\\ sx_i=qx_{i-n-1};\quad sy_i=qy_{i-n-1}*P(qx_{i-n-1})\quad \forall i:n<i<=n+m+1$$Для получения множества точек, задающих представление полинома S(x), приходится вычислять значение полинома Q(x) в n точках и значение полинома P(x) в m точках, а затем выполнять соответствующее умножение значений двух полиномов.
Если исходные полиномы P(x) и Q(x) заданы своими коэффициентами, то имеем:
$$S(x)=P(x)*Q(x)=(\sum\limits_{i=0}^n a_i x^i)*(\sum\limits_{j=0}^m b_j x^j)$$Каждый член первой суммы приходится умножать на все члены второй суммы и затем приводить подобные члены при одинаковых степенях x. Нетрудно заметить, что в результате коэффициенты полинома S(x) определяются следующими соотношениями:
$$S(x)=\sum\limits_{i=0}^{n+m}d_i;\quad\text{где}\\ d_i=\sum\limits_{k+r=i}a_k b_r;\quad k\in[0,n];\quad r\in[0,m]$$Суммирование идет по всем наборам k и r, дающим в сумме значение i. Понятно, что для крайних значений (i=0 и i=n+m) сумма состоит из одного члена, поскольку подобные члены для x в нулевой степени и степени n+m отсутствуют. Число членов суммирования увеличивается при приближении к середине интервала [0, n+m].
Подводя некоторые итоги, отметим, что полином можно задать тремя разными способами - его коэффициентами, корнями и точками, через которые проходит полином. Если заданы коэффициенты полинома, то за время, пропорциональное $$n^2, (T(n) = O(n^2))$$ можно вычислить значения полинома в n+1 точках. Для вычисления значения полинома в одной точке применяется
Если заданы корни, то можно получить два других представления. Рассмотренный нами алгоритм позволяет по корням полинома за время $$O(n^2)$$ вычислить коэффициенты полинома. Алгоритм использует итеративную схему из n шагов, где на каждом шаге выполняется операция повышения степени, выполняемая за линейное время. Поскольку корни являются частным случаем задания множества точек, через которые проходит полином, то задание корней автоматически задает и представление полинома набором точек. Обратная задача - получение корней по коэффициентам или заданным точкам - так просто не решается. Точное ее решение существует для полиномов второй и третьей степени, но не в общем случае. Для нахождения корней приходится использовать приближенные
Задание полинома его корнями является наиболее информативным. Если известны корни, то без труда выполняется свертка полиномов. Вычисление значения полинома в заданной точке выполняется за n умножений, не требуя применения схемы Горнера. Несколько сложнее выполняется операция сложения полиномов. К сожалению, на практике редко встречается ситуация, когда известны корни полинома, но такое бывает - алгоритм Лагранжа тому пример.
Когда полиномы заданы своими коэффициентами, то вычисление значения полинома в заданной точке выполняется по схеме Горнера за линейное время. Сложение полиномов также является легкой операцией и выполняется за линейное время. Свертку полиномов в этом случае выполнить сложнее. Рассмотренный нами алгоритм требует уже квадратичного времени.
На практике полиномы чаще всего появляются при задании множества точек. Ситуация обычно такова. В результате экспериментов измеряются значения некоторой функции в ряде точек. Требуется предсказать, каково будет значение этой функции в других точках, в которых измерения не проводились. Если из теоретических соображений не известен вид функции, то чаще всего ее задают в виде полинома, проходящего через точки, полученные экспериментальным путем. В этой постановке задачу построения полинома и вычисления значений полинома в точках, не подлежащих измерениям, называют задачей интерполяции, а
Множество точек, через которые проходит полином, обычно несет дополнительную информацию. Некоторые точки, например, могут быть корнями полинома или задавать интервалы, внутри которых находятся корни.
Одно замечание к задаче свертки полиномов. Приведенный алгоритм решения этой задачи для полиномов, заданных своими коэффициентами, требует квадратичного времени. Ввиду практической важности этой задачи много внимания уделялось поиску наиболее эффективного по временной сложности алгоритма. Существуют алгоритмы, решающие эту задачу за время $$O(n*log(n))$$. Эти алгоритмы используют технику быстрого преобразования Фурье и обратного к нему. Они сложнее в реализации и требуют работы с комплексными числами или выполнения операций модульной арифметики. Здесь они только упоминаются и детально не рассматриваются.
В задачах этого раздела уже не говорится о том, какого типа проект следует строить - консольный или Windows. Предполагается, что обычной практикой является построение Windows-приложений.
Polinom. Методы класса должны реализовать все алгоритмы, рассмотренные в этом разделе. Интерфейс пользователя должен позволять пользователю решать основные задачи, возникающие при работе с полиномами.Матрицей называется набор чисел, состоящий из m строк и n столбцов. Для программиста матрица - это двумерный массив. Матрица называется квадратной, если m = n, и прямоугольной - в противном случае. Числа m и n определяют размерность матрицы. Над прямоугольными матрицами определены операции транспонирования, сложения, умножения.
Пусть A - матрица размерности m*n (из m строк и n столбцов) с элементами $$a_{i,j}$$. Транспонированной матрицей B = AT называют матрицу размерности n*m, элементы которой $$b_{i,j} = a_{j,i}$$. В транспонированной матрице строки исходной матрицы становятся столбцами.
$$A=\left|\left|\begin{array}{l}a_{1,1},a_{1,2}\ldots a_{1,n}\\ a_{2,1},a_{2,2}\ldots a_{2,n}\\ \ldots\\ a_{m,1},a_{m,2}\ldots a_{m,n}\end{array}\right|\right| \quad B=A^T=\left|\left|\begin{array}{l}a_{1,1},a_{2,1}\ldots a_{m,1}\\ a_{1,2},a_{2,2}\ldots a_{m,2}\\ \ldots\\ a_{1,n},a_{2,n}\ldots a_{m,n}\end{array}\right|\right|$$Операция сложения определена над прямоугольными матрицами одинаковой размерности. Пусть A, B, C - прямоугольные матрицы размерности m*n. Тогда сумма матриц определяется естественным образом:
$$A=\left|\left|\begin{array}{l}a_{1,1},a_{1,2}\ldots a_{1,n}\\ a_{2,1},a_{2,2}\ldots a_{2,n}\\ \ldots\\ a_{m,1},a_{m,2}\ldots a_{m,n}\end{array}\right|\right| \quad B=\left|\left|\begin{array}{l}b_{1,1},b_{1,2}\ldots b_{1,n}\\ b_{2,1},b_{2,2}\ldots b_{2,n}\\ \ldots\\ b_{m,1},b_{m,2}\ldots b_{m,n}\end{array}\right|\right|\\ C=A+B=\left|\left|\begin{array}{l}a_{1,1}+b_{1,1},a_{1,2}+b_{1,2}\ldots a_{1,n}+b_{1,n}\\ a_{2,1}+b_{2,1},a_{2,2}+b_{2,2}\ldots a_{2,n}+b_{2,n}\\ \ldots\\ a_{m,1}+b_{m,1},a_{m,2}+b_{m,2}\ldots a_{m,n}+b_{m,n}\end{array}\right|\right|$$Операция умножения определена над прямоугольными матрицами, у которых число столбцов первого сомножителя равно числу строк второго сомножителя. Матрица произведения имеет число строк, равное числу строк первого сомножителя, и число столбцов, равное числу столбцов второго сомножителя. Пусть A - матрица размерности m*p, B - размерности p*n, тогда матрица C= A*B имеет размерность m*n. Элементы матрицы произведения определяются как сумма попарных произведений элементов строки первого сомножителя на элементы столбца второго сомножителя.
$$A=\left|\left|a_{i,j}\right|\right|\quad i=1,...m;\;j=1,...p;\quad B=\left|\left|b_{j,k}\right|\right|\quad j=1,...p;\;k=1,...n\\ C=A*B=\left|\left|c_{i,k}\right|\right|\quad i=1,...m;\;k=1,...n;\\ c_{i,k}=\sum\limits_{j=1}^p a_{i,j}*b_{j,k};$$Умножение всегда определено для прямой и транспонированной матрицы. Если A - прямоугольная матрица размерности m*n, то всегда определена квадратная матрица B размерности m*m:
$$B = A*A^T = B^T = (A*A^T)^T = (A^T)^T*A^T= A*A^T$$Результатом такого произведения является симметричная матрица. Квадратная матрица называется симметричной, если $$a_{i,j} = a_{j,i}$$ для всех i и j, или, что то же, если $$A = A^T$$. Операции транспонирования, сложения и умножения обладают следующими свойствами:
$$(A^T)^T=A;\quad (A+B)^T=A^T+B^T;\quad (A*B)^T=B^T*A^T$$Квадратная матрица называется диагональной, если все элементы, кроме диагональных, равны нулю, то есть $$a_{i,j} = 0$$ при $$i /=j$$.
Квадратная матрица называется единичной, если все элементы, кроме диагональных, равны нулю, а диагональные элементы равны единице, то есть $$a_{i,j} = 0$$ при $$i /=j$$ и $$a_{i,j} = 1$$ при $$i = j$$. Единичная матрица обозначается обычно буквой E, и она играет роль единицы при умножении матриц, поскольку для любой квадратной матрицы A и единичной матрицы E той же размерности имеют место соотношения:
$$A*E = E*A = A$$Для квадратных матриц определена функция над ее элементами, называемая определителем. Обозначается определитель обычно с помощью одинарных линий вокруг набора чисел, задающих матрицу:
$$D(A)=\left|\begin{array}{l}a_{1,1},a_{1,2}\ldots a_{1,n}\\ a_{2,1},a_{2,2}\ldots a_{2,n}\\ \ldots\\ a_{n,1},a_{n,2}\ldots a_{n,n}\end{array}\right|$$Функция, задающая определитель, обладает рядом важных свойств.
Не приводя общего формального определения, рассмотрим ниже алгоритм вычисления
Если определитель квадратной матрицы A не равен нулю, то существует обратная матрица, обозначаемая как $$A^{-1}$$. Прямая и обратная матрицы связаны соотношением:
$$A*A^{-1} = A^{-1}*A = E$$Операции транспонирования, умножения и обращения матриц связаны соотношениями:
$$(A^T)^{-1} = (A^{-1})^T;\quad (A*B)^{-1} = B^{-1}*A^{-1}$$Множество квадратных матриц одной размерности с определителем, отличным от нуля образуют группу по умножению. В группе есть единичный элемент, для каждого элемента существует обратный к нему, и произведение элементов принадлежит группе.
Иногда полезно рассматривать матрицу, состоящую не из элементов, а из клеток, каждая из которых является матрицей. Все определения операций над матрицами, элементы которых являются числами, переносятся на матрицы, элементы которых являются клетками. Такое представление особенно полезно для
В круглых скобках для клеток заданы их размерности. Пусть теперь некоторые клетки нулевые, например, таковыми являются клетки D, F и G. Тогда матрица M2 имеет вид:
$$M2=\left|\left|\begin{array}{cc}A*EB*H\\ C*E0\end{array}\right|\right|$$Для вычисления матрицы M2 необходимо будет найти произведение трех пар матриц, но значительно меньших размеров, чем исходные матрицы. В целом объем вычислений сократится более чем в три раза.
Иногда приходится иметь дело с треугольными матрицами, у которых все элементы выше или ниже диагонали равны нулю. Квадратную матрицу будем называть нижнетреугольной, если все элементы выше главной диагонали равны нулю, и верхнетреугольной, если равны нулю все элементы ниже главной диагонали.
Рассмотрим систему из n линейных уравнений с n неизвестными:
$$a_{1,1}x_1+a_{1,2}x_2+\ldots+a_{1,n}x_n=b_1\\ a_{2,1}x_1+a_{2,2}x_2+\ldots+a_{2,n}x_n=b_2\\ \ldots\\ a_{n,1}x_1+a_{n,2}x_2+\ldots+a_{n,n}x_n=b_n$$В матричном виде эта система записывается намного элегантнее:
$$A*x=b$$Здесь вектор неизвестных x рассматривается как столбец - прямоугольная матрица размерности n*1. Аналогичный вид имеет вектор правых частей b системы уравнений. В матричном виде условие существования решения системы линейных уравнений 6.8 и нахождение самого решения формулируется совсем просто. Для существования решения необходимо и достаточно, чтобы
Для нахождения решения системы линейных уравнений, матрица которой имеет определитель, отличный от нуля, достаточно вычислить обратную матрицу и умножить ее на вектор правых частей системы уравнений.
Если нужно решить m систем линейных уравнений с одной и той же матрицей $$A$$, но с разными правыми частями, то обратную матрицу достаточно вычислить один раз. В матричном виде решение m систем линейных уравнений
$$A*X = B$$задается соотношением:
$$X = A^{-1}*B$$Здесь $$B$$ - прямоугольная матрица размерности $$n*m$$, каждый столбец которой представляет вектор правых частей одной системы уравнений. Соответствующий столбец матрицы $$X$$ дает решение этой системы. Что произойдет, если в качестве матрицы $$B$$ рассмотреть единичную матрицу? Очевидно, что тогда матрица $$X$$ будет представлять собой обратную матрицу $$A^{-1}$$. Несмотря на кажущуюся очевидность соотношения $$A^{-1} = A^{-1}*E$$, в нем есть определенный смысл, который постараюсь сейчас прояснить. Три задачи - вычисление определителя, решение системы линейных уравнений, нахождение обратной матрицы - имеют одинаковую вычислительную сложность и требуют, если не применять специальные алгоритмы, выполнения порядка $$n^3$$ операций умножения и сложения. Если посмотреть на соотношение 6.10, то кажется, что решить систему уравнений несколько сложнее, чем вычислить обратную матрицу, поскольку нужно вначале найти обратную матрицу, а затем умножить ее на вектор правых частей $$b$$. Однако реальный алгоритм, рассматриваемый ниже и находящий решение системы, вычислительно проще, чем тот же алгоритм, находящий обратную матрицу. Для такого алгоритма найти обратную матрицу - это все равно, что решить n систем линейных уравнений с одной и той же матрицей $$A$$ в левой части, используя матрицу $$E$$ в качестве правых частей.
Рассмотрим сейчас
Матрица $$B$$, дополняющая матрицу $$A$$, зависит от того, какую задачу предполагается решить. Если нужно вычислить только
После того, как построена
Рассмотрим на простом примере матричный вид элементарных операций. Пусть элементарная операция состоит в том, что к первой строке прибавляется вторая строка, умноженная на число $$q$$. Это действие эквивалентно умножению почти единичной матрицы на исходную матрицу:
$$\left|\left|\begin{array}{cccc}1q00\ldots0\\ 0100\ldots0\\ \ldots\ldots\ldots\ldots\\ 000\ldots01\end{array}\right|\right| * \left|\left|\begin{array}{cccc}a_{1,1}a_{1,2}\ldotsa_{1,n}\\ a_{2,1}a_{2,2}\ldotsa_{2,n}\\ \ldots\ldots\ldots\ldots\\ a_{n,1}a_{n,2}\ldotsa_{n,n}\end{array}\right|\right| = \left|\left|\begin{array}{lccc}a_{1,1}+qa_{2,1}a_{1,2}+qa_{2,2}\ldotsa_{1,n}+qa_{2,n}\\ a_{2,1}a_{2,2}\ldotsa_{2,n}\\ \ldots\ldots\ldots\ldots\\ a_{n,1}a_{n,2}\ldotsa_{n,n}\end{array}\right|\right|$$Матрица, задающая элементарную операцию, отличается от единичной матрицы тем, что у нее в первой строке на втором месте стоит число 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];
}
}
}
Аргументом метода является det формируется значение
Возможны различные модификации рассматриваемого алгоритма, исправляющие ситуацию.
Алгоритм с выбором первого ненулевого элемента
В случае, когда а[i, i] равно нулю, алгоритм ищет первую строку ниже i-й, в которой элемент a[i, j] не равен нулю. Эта строка добавляется к строке i, что гарантирует возможность деления на а[i, i].
Алгоритм с выбором главного элемента в столбце
Прежде чем приводить столбец к единичному виду, алгоритм ищет в столбце максимальный по модулю элемент и меняет местами строку i и строку j, в которой находится максимальный элемент. При обмене строк может измениться знак
Алгоритм с выбором главного элемента во всей матрице
На каждом шаге приведения очередного столбца к диагональному виду в еще не приведенной матрице отыскивается максимальный элемент и меняются местами не только строки, но и столбцы матрицы, ставя максимальный элемент в позицию а[i, i]. Этот прием гарантирует отсутствие переполнения при выполнении операции деления. Гарантируется также, что при умножениях не будет получено слишком большое число, поскольку деление на максимальный элемент с последующим умножением на один из элементов приводит к тому, что элементы преобразованной матрицы не увеличиваются по модулю. Однако ничто не дается даром. Выбор главного элемента, перестановка строк и столбцов, необходимость обратной перестановки в конце вычислений - все это усложняет алгоритм. Как правило, страдает и точность вычислений, особенно для плохо обусловленных матриц. Все модификации алгоритма стоит применять тогда, когда в основной схеме возникла исключительная ситуация, требующая корректировки алгоритма. Обработчик исключительной ситуации при делении на ноль, возникновении переполнения, потере значащих цифр может вызывать модифицированный вариант алгоритма в надежде получить решение, когда отказывается работать основная схема.
Замечу, что никакая модификация не может помочь найти обратную матрицу, если она не существует и
Вернемся к задаче построения интерполяционного полинома, проходящего через заданное множество точек. Напомню, заданы своими координатами $$(x_0, y_0), …(x_n, y_n)$$ точки, через которые должен пройти полином степени n. Требуется найти коэффициенты $$a_0, …a_n$$ этого полинома. Ранее был рассмотрен алгоритм Лагранжа, позволяющий построить этот полином. Нетрудно понять, что существует прямое решение этой задачи, состоящее в построении системы линейных уравнений для нахождения неизвестных коэффициентов полинома. В матричном виде эта система уравнений имеет вид:
$$Xa=y;\quad\text{где матрица }X\text{ имеет вид}\\ X=\left|\left|\begin{array}{ccccc}1x_0x_1\ldotsx_n\\ 1x_0^2x_1^2\ldotsx_n^2\\ \ldots\ldots\ldots\ldots\ldots\\ 1x_0^nx_1^n\ldotsx_n^n\end{array}\right|\right|$$Матрица X называется матрицей Вандермонда, а ее определитель - соответственно, определителем Вандермонда. Этот определитель вычисляется достаточно просто:
$$D(X)=\prod\limits_{j>i}(x_j-x_i)$$Он равен произведению разностей координат точек, где произведение берется по всем j, большим i. Очевидно, что определитель будет отличен от нуля, если у точек нет совпадающих координат x. Этот факт отмечался и при рассмотрении полинома Лагранжа, когда говорилось, что множество точек "не имеет возвратов".
Когда
На практике часто возникает ситуация, когда матрица системы появляется в результате измерений, ее элементы представляют не точные значения, а содержат ошибки измерений. Здесь система уравнений может иметь определитель, отличный от нуля, но быть "почти" линейно зависимой в пределах ошибок измерений. В таких случаях формально найденное решение может быть далеким от "истинного" решения. Как правило, матрица подобных систем является плохо обусловленной, а сама система уравнений называется неустойчивой. Дадим более точное определение. Матрица А называется плохо обусловленной, а система уравнений - неустойчивой, если малым изменениям элементов прямой матрицы соответствуют большие изменения в обратной матрице. Понятно, что если обратная матрица вычислена с большими ошибками, то и решение системы содержит ошибки такого же порядка.
Если матрица А плохо обусловлена, то и обратная к ней также является плохо обусловленной матрицей. Во сколько раз могут возрастать ошибки в элементах прямой матрицы при ее обращении? Примерный ответ на это дают "числа обусловленности" матрицы. Предлагаются различные количественные меры обусловленности матриц. Одной из таких мер является М-число обусловленности Тьюринга:
$$M-\text{число}=\frac{1}{n}M(A)*M(A^{-1});\quad\text{где}\\ M(A)=n*\max\limits_{i,j}\left|A_{i,j}\right|\text{ - норма матрицы}$$Матрица Вандермонда - потенциальный кандидат на плохую обусловленность. Если посмотреть на ее структуру, то видно, что для ее элементов во многих случаях характерен большой размах - отношение между максимальным и минимальным элементом велико. Действительно, пусть, например, максимальная по модулю координата $$x_n$$ имеет значение 100, а степень полинома n равна 6. Это довольно скромные цифры, но уже в этом случае минимальный элемент матрицы равен 1, а максимальный - $$10^{12}.$$ Примерно такой же размах будет и у элементов обратной матрицы. Ее максимальный элемент будет примерно равен 1, а минимальный - $$(max a_{i,j})^{-1}$$, так что число обусловленности M будет примерно равно $$10^{12}$$. При наличии небольших ошибок в измерении координат ошибки в определении значений полинома в точках, отличных от измеряемых, могут многократно возрастать. По этой причине
LinAlgebra. Методы класса должны реализовать все алгоритмы, рассмотренные в этом разделе. Интерфейс пользователя должен позволять пользователю решать основные задачи, возникающие при работе матрицами, определителями, системами линейных уравнений.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.