7 Feb 2022 Что добавилось в язык С++ по сравнению с языком С помимо классов? 1. Добалены были встроенные в язык операторы new и delete (захвата и освобождения динамической памяти) -- вместо функций malloc и free. 2. Был добавлен тип "ссылка" (reference). Ссылка -- это второе имя для уже существующего объекта (псевдоним, алиас, nickname). Ссылка -- это не указатель! Указатель -- это переменная, которая хранит адрес некоторого объекта. Ссылка вообще не обязательно должна быть переменной, под нее может выделаться память, а может и не выделяться. Как устроена ссылка внутри, программисту знать запрещено. int k; int& l; // ОШИБКА! int& l = k; После этого любое обращение к l переадресуется к k. double a[100]; double& last = a[99]; В отличие от указателя, ссылку нельзя перенаправить на другой объект. Ссылки в основном используются для передачи параметров функциям. В языке С функциям всегда передаются КОПИИ объектов. Если мы хотим, чтобы функция изменила переданный, то в С надо передавать указатель на объект. В С++ в таких случаях передается ссылка. Пример: вычислить окружность, вписанную в треугольник. Прототип функции: void insribedCircle( const R2Point* vertices, R2Point& center, double& radius ); R2Point a[3]; // Vertices of triangle R2Point center; double radius; a[0] = R2Point(0., 0.); a[1] = R2Point(5., 1.); a[2] = R2Point(3., 5.); insribedCircle(a, center, radius); В языке C пришлось бы написать: void insribedCircle( const R2Point* vertices, R2Point* center, double* radius ); . . . insribedCircle(a, ¢er, &radius); 3. Добавлен базовый тип bool. int n = (2 == 2); // Так писать не надо! bool n = (2 == 2); bool m = true; Что еще добавлено в С++ Главное -- классы (которые как бы вводят новые сложные типы данных) и переопределение операторов языка С для классов. class R2Vector { public: double x; double y; }; R2Vector u(1., 2.), v(3., 4.); R2Vector w; Хотим вычислить сумму двух векторов. w.x = u.x + v.x; w.y = u.y + v.y; Так никто не пишет!!! Я попросту не буду засчитывать подобные программы, где вместо операций с векторами или точками производятся манипуляции с их координатами. Надо писать: w = u + v; Это означает, что для класса R2Vector переопределен оператор сложения. Например, оператор умножения * для двух векторов определен как скалярное произведение (dot product). double s = u*v; // s == 11. == u.x*v.x + u.y*v.y R2Vector t = u*2.5; // Это уже умножение вектора на число. Это всё каcается языка С++ образца 1985 г. В 1998 г. вышла третья версия языка С++, в нее были добавлены 1) механизм шаблонов (template); 2) понятие пространства имен (namespace); 3) стандартная библиотека. Степанов (американец) написал библиотеку STL (Standard Library), которая настолько понравилась Страуструпу, что он включил ее в качестве обязательного элемента любого компилятора С++. Книга Страуструпа 1997 г. "The C++ Programming Language" "Язык программирования С++" на 90% посвящена описанию стандартной библиотеки. Помимо стандартной библиотеки, имеется библиотека Boost, библиотеки Qt и т.д. Все классы и функции стандартной библиотеки включены в простанство имен std. Минимальная программа hello, world! На С: #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } На С++ с использованием стандартной библиотеки: #include <iostream> int main() { std::cout << "Hello, World!" << std::endl; return 0; } Страуструп этого не советует, но многие ленятся писать префикс std, есть возможность импортировать ВСЕ имена из стандартной библиотеки в неименованное пространство имен: using namespace std; Тогда программу Hello можно написать так: #include <iostream> using namespace std; int main() { cout << "Hello, World!" << endl; return 0; } Давайте разберем программу нахождения окружности, вписанной в треугольник. Для этого мы используем готовые классы R2Vector и R2Point. (Эти классы были написаны мной в учебных целях, но оказалось, что они очень удобны для любой работы с компьютерной графикой или геометрией.) Для этих классов переопределены операции: сумма векторов разность двух точек -- это вектор! к точке можно прибавить (или отнять) вектор умножение двух векторов -- это склярное произведение вектор можно умножить на число У класса вектор имеются методы: R2Vector v; . . . R2Vector n = v.normal(); // Нормаль к вектору v.normalize(); // Нормализовать вектор (длина стала 1) R2Vector u = v.normalized(); double len = v.norm(); // Евклидова длина вектора len = v.length(); Главная идея объектно-ориентированного программирования: вместо манипуляций с числами используем методы класса вектор и точка, которые соответствуют геометрическим операциям. Пример 1 Найти расстояние от точки до прямой Как задается прямая? Разные способы: две точки, точка и нормаль к прямой, точка и направляющий вектор. Итак, дана точка t и прямая, заданная точкой p и направляющим вектором v R2Point p; R2Vector v; // Задают прямую R2Point t; . . . R2Vector n = v.normal(); n.normalize(); double d = fabs(n*(t - p)); Второй пример: найти точку пересечение двух прямых R2Point p1; R2Vector v1; // Первая прямая R2Point p2; R2Vector v2; // Вторая прямая R2Vector n = v2.normal(); // q = p1 + v1*t // n*(q - p2) = 0 // n*(p1 + v1*t - p2) = 0 // n*(p1 - p2) + (n*v1)*t = 0 // t = n*(p2 - p1)/(n*v1) double t = n*(p2 - p1)/(n*v1); R2Point q = p1 + v1*t; В пакете R2Graph.h, R2Graph.cpp помимо классов R2Vector, R2Point реализованы также функции, решающие различные геометрические задачи на плоскости. В частности, bool intersectStraightLine( const R2Point& p1, const R2Vector& v1, const R2Point& p2, const R2Vector& v2, R2Point& q ); R2Point t; R2Point p; R2Vector v; . . . double d = t.distanceToLine(p, v); Разберем программу вычисления окружности, вписанной в треугольник. 14 Feb 2022 Разбираем программу "Вычислить окружность, вписанную в треугольник" Консольное приложение на C++ Для представления векторов и точек мы используем свои классы R2Vector и R2Point. Мы различает точки и векторы: векторы можно складывать, а точки нельзя! Разность двух векторов есть вектор. ВНИМАНИЕ! Разность двух точек -- это операция осмысленная, в отличие от суммы точек. Разность точек a - b представляет собой вектор, направленный от b к a. R2Point a, b; . . . R2Vector v = a - b; // вектор, направленный от b к a. К точке можно прибавить (или отнять) вектор, в результате получим точку: R2Point a; R2Vector v; . . . R2Point b = a + v; Главная идея первой задачи -- научиться решать геометрические задачи геометрическими методами. Это означает, что вместо манипуляций с координатами объектов (векторы и точки) мы используем методы этих классов: сложение и вычитание векторов, вычитание точек, прибавление (или вычитание) вектора к точке, скалаярное произведение векторов, длина вектора, расстояние между точками, получение нормали к вектору, нормализация вектора, получение угла между векторами, ориентированная площадь треугольника или параллелограмма, пересечение прямых и т.д. ПРЕДУПРЕЖДЕНИЕ Задача, которая решается с помощью обращения к координатам, не будет засчитана! Замечательные точки треугольника: 1. Центр вписанной окружности. 2. Центр описанной окружности (точка пересения серединных перпендикуляров). 3. Центр тяжести вершин треугольника (точка пересечения медиан). 4. Ортоцентр -- точка пересечения высот. 6. Точка Жергона -- точка пересечения чевиан, проведенных из вершин треугольника к точкам касания вписанной окружности. Теорема Чевы дает необходимые и достаточные условия для того, чтобы 3 отрезка, проведенные из вершин треугольника до пересечения с противоположной стороной, пересекались бы в одной точке. ===================================================================== Самая важная тема второго семестра Представление матриц в компьютере. Метод Гаусса приведения матрицы к ступенчатому виду и задачи, которые решаются с его помощью На эту тему будет одна задача (достаточно сложная). Первое: давайте не использовать базовые массивы языка C, а пользоваться классом std::vector<double> из стандартной библиотеки, который реализует динамический массив. Динамический массив -- это массив, размер которого может изменяться (может расти). Массив в стиле языка C: double *a = new double[n]; . . . x = a[i]; a[j] = 0.5; . . . delete[] a; Массив a не меняет своего размера в течение своей жизни. Массив в стиле языка C++ c использованием стандартной библиотеки: std::vector<double> a(n); . . . x = a[i]; a[j] = 0.5; . . . Никакого delete[] не требуется!!! Динамический массив умирает естественно, его не требуется убивать. Кроме того, динамический массив может расти: std::vector<double> a; // Массив нулевого размера assert(a.size() == 0); a.push_back(1.5); a.push_back(33.78); assert(a.size() == 2); assert(a.capacity() >= 2); Мнемоника стандартной библиотеки (для шаблонных классов, реализующих разные структуры данных) push -- добавить push_back(x) -- добавить x в конец структуры push_front(x) -- добавить x в начало структуры back() -- конец fron() -- начало pop -- удалить (отстрелить, выкинуть, поп-корн, pop-gun). pop_back() -- удалить конец pop_front() -- удалить начало Перейдем к матрицам. ВНИМАНИЕ!!! Основная идея: мы храним элементы матрицы в линейном массиве. Для матрицы размера m x n (m строк, n столбцов) мы сначала храним элементы нулевой строки, затем первой строки, ..., в конце -- элементы m-1-й строки. [ a00 a01 a02 a03 ] [ a10 a11 a12 a13 ] [ a20 a21 a22 a23 ] Храним элементы в линейном массиве по строкам: a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 --------------- =============== --------------- строка 0 строка 1 строка 2 Идиотский способ реализации матрицы (если вы будете его использовать, я такие решения задач принимать не буду!) ПЕРЕРЫВ до 12:45 Продолжим. Неправильный способ -- матрица реализуется как массив указателей на начала строк. Каждая строка хранится в отдельном сегменте памяти, захваченном в куче. int m, n; . . . double **a = new double*[m]; // Массив указателей на начала строк for (int i = 0; i < m; ++i) { a[i] = new double[n]; } . . . x = a[i][j]; a[i][j] = 3.5; . . . // Убийство матрицы for (int i = 0; i < m; ++i) { delete[] a[i]; } delete[] a; Такой способ приниматься не будет!!! Чем он плох: 1) для обращения к элементу матрицы a[i][j] нужно 2 обращения к памяти; 2) плохо используется кэш-память процессора. При использовании кэш-памяти данные из оператичной памяти читаются не по одному элементу, а блоками, и заносятся в кэш-память. Если элемент уже в кэш-памяти, то обращаться к оперативной памяти уже не нужно. Поэтому выгодно, когда элементы матрицы лежат в одном блоке памяти. В правильном решении элементы матрицы хранятся в одном линейном массиве по строкам. Тогда элемент матрицы a_ij хранится в ячейке массиве с индексом i*n + j Элемент a_ij -- это a[i*n + j] Попробуем написать программу, которая читает матрицу из файла, приводит ее к ступенчатому виду и печатает ответ на консоли. Орлов Андрей -- добавить в список --------------------------------------- 21 Feb 2022 Работа с матрицами Мы представляем матрицу линейным массивом, в котором элементы матрицы лежат по строкам. Индексы в программировании начинаются с нуля (не как в математике, с единицы), это удобнее, потому что индекс = смещение относительно начала. Например, матрица 3x4 10 20 30 40 51 61 71 81 92 102 112 122 хранится в линейном массиве как 10 20 30 40 51 61 71 81 92 102 112 122 В этом семестре мы будем использовать вместо массивов в стиле C double *a = new double[m*n]; // Захватываем память в куче . . . x = a[i*n + j]; . . . delete[] a; мы будем использовать класс vector из стандартной библиотеки С++: std::vector<double> a(m*n); // Память в куче захватывается в конструкторе // Освобождать память не надо! // Она освобождается в деструкторе класса vector double determinant(int m, int n, const double *a) { double *b = new double[m*n]; for (int i = 0; i < m; ++i) { // Копируем матрицу a в матрицу b for (int j = 0; j < n; ++j) { b[i*n + j] = a[i*n + j]; } } gauss(b, m, n); double d = 1.; for (int i = 0; i < m; ++i) { d *= b[i*n + i]; } delete[] b; return d; } То же самое в стиле C++: double determinant(int m, int n, const std::vector<double>& a) { std::vector<double> b = a; gauss(b, m, n); double d = 1.; for (int i = 0; i < m; ++i) { d *= b[i*n + i]; } return d; } Прошлый раз мы написали алгоритм Гаусса. Задачи: 4 задачи Все они сводятся к методу Гаусса Обратная матрица 1 2 3 4 5 6 7 8 0 Приписываем справа к исходной матрице единичную 1 2 3 1 0 0 4 5 6 0 1 0 7 8 0 0 0 1 Приводим матрицу к ступенчатому виду методом Гаусса: * * * * * * 0 * * * * * 0 0 * * * * Домножаем строки матрицы на числа, обратные к диагональным элементам так, чтобы на диагонали получились единицы: 1 * * * * * 0 1 * * * * 0 0 1 * * * Выполняем обратный проход в методе Гаусса: 1 0 0 * * * 0 1 0 * * * 0 0 1 * * * То, что стоит справа -- это обратная матрица. Она вычислена за O(n^3) операций. Обратный проход в методе Гаусса используется также при решении СЛАУ (системы линейных алгебраических уравнений). 1 2 3 2 4 5 6 3 7 8 0 5 Метод Гаусса: * * * * 0 * * * 0 0 * * Нормируем строки (домножаем) так, чтобы на диагонали были единицы: 1 * * * 0 1 * * 0 0 1 * Выполняем обратный проход: 1 0 0 * 0 1 0 * 0 0 1 * Столбец свободных членов представляет решение системы. ======================================================== 28 Feb 2022 Применение алгоритма Гаусса (приведение матрица к ступенчатому виду) для решения различных задач. Задача 3. Решить систему линейных уравнений (СЛАУ -- Система Линейных Алгебраических Уравнений) с невырожденной квадратной матрицей. AX = B, A -- матрица размера nxn, X -- вектор-столбец неизвестных, B -- вектор-столбех свободных членов. Идея: рассмотрим расширенную матрицу системы: столбец B приписываем справа к матрице. Пример: [1 2 3] [2] A = [4 5 6] B = [3] [7 8 0] [5] Приписываем столбец B справа к матрице A [1 2 3 | 2] A = [4 5 6 | 3] [7 8 0 | 5] Эту матрицу приводим к ступенчатому виду методом Гаусса: [ 1 2 3 2] A1 = [ 0 3 6 5] [ 0 0 9 -1] Делим каждую строку на диагональный элемент: [ 1 2 3 2 ] A2 = [ 0 1 2 5/3] [ 0 0 1 -1/9] Выполняем обратный проход -- как бы выполняем метод Гаусса, но снизу-вверх и справа налево Что мы делаем: из верхних строк выше i-й с номерами k=1, 2, ..., i-1 вычитаем i-ю строку, умноженную на число -A_ik так, чтобы элемент A_ik сократился. Получим матрицу [ 1 0 0 -13/9] A3 = [ 0 1 0 17/9] [ 0 0 1 -1/9] Каждая строка матрицы (с индексом i) представляет собой уравнение x_i = b_i то есть решением нашей системы будет столбец свободных членов. Обратный проход будем делать для любой ступенчатой матрицы, не обязательно квадратной и невырожденной. x * * * * * * * * 0 0 0 x * * * * * --> 0 0 0 0 x * * * * 0 0 0 0 0 0 0 x * 0 0 0 0 0 0 0 0 0 1 * * 0 0 * * 0 * 0 0 0 1 0 * * 0 * rank -- это для ступенчатой матрицы 0 0 0 0 1 * * 0 * число ненулевых строк 0 0 0 0 0 0 0 1 * 0 0 0 0 0 0 0 0 0 В частном случае, для расширенной матрицы системы уравнений с невырожденной квадратной матрицей (ступенчатый вид которой треугольный) получим x * * * * * 0 x * * * * 0 0 x * * * --> 0 0 0 x * * 0 0 0 0 x * 1 0 0 0 0 * 0 1 0 0 0 * 0 0 1 0 0 * 0 0 0 1 0 * 0 0 0 0 1 * ПЕРЕРЫВ 15 минут до 12:40 Задача 2 Для квадратной невырожденной матрицы вычислить обратную матрицу. Идея та же самая: метод Гаусса + обратный проход. Умоляю, не использовать правило Крамера или что-то подобное для вычисления обратной матрицы!!! A | | inverse(A) = | -1^(i+j) A_ji/det(A) | | | A_ij -- алгебраическое дополнение: определитель матрицы, которая получается вычеркиванием из A i-й строки и j-го столбца. ТАК ДЕЛАТЬ НЕ НАДО!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Обратную матрицу следует вычислить с помощью метода Гаусса: Рассмотрим на примере: [1 2 3] A = [4 5 6] [7 8 0] 1.К исходной матрице приписываем справа единичную матрицу: [1 2 3 | 1 0 0] A1 = [4 5 6 | 0 1 0] [7 8 0 | 0 0 1] 2. Приводим полученную матрицу к ступенчатому виду методом Гаусса: [ 1 2 3 | 1 0 0] A2 = [ 0 3 6 | 4 -1 0] [ 0 0 9 | -1 2 -1] 3. Выполняем обратный проход и получаем слева единичную матрицу: [ 1 0 0 | -16/9 8/9 -1/9] A3 = [ 0 1 0 | 14/9 -7/9 2/9] [ 0 0 1 | -1/9 2/9 -1/9] Справа будет обратная матрица: [-16/9 8/9 -1/9] inverse(A) = [ 14/9 -7/9 2/9] [ -1/9 2/9 -1/9] Доказательство: Элементарные преобразования строк матриц можно реализовать как умножение слева на специальные матрицы: Обмен строки i со сторокой k 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 swap(2,5) = 0 0 1 0 0 0 0 0 1 0 0 1 0 0 0 (берем единичную матрицу и выполняем с ней элементарное преобразование; получаем матрицу, которая при домножении произвольной матрицы на нее слева будет выполнять то же самое преобразование с произвольной матрицей). Если любую матрицу A размера 5x5 домножить слева на матрицу swap(2, 5), то у нее переставятся 2-я и 5-я строки Аналогично определяются элементарные матрицы, реализующие другие элементарные преобразования строк произвольной матрицы. Когда мы элементарными преобразованиями СТРОК приводим матрицу к единичной E, то это соответствует домножениям слева на элементарные матрицы: E = s_k...(s3*(s2*(s1*A))) = (s_k...s3*s2*s1)*A ==> s_k...s3*s2*s1 -- это обратная матрица. Когда мы применяем элементарные преобразования к расширенной матрице ( A | E ) то единичная матрица справа умножается на произведение этих элементарных матриц => значит, справа получится обратная матрица. Задача 1. Дано m векторов линейного пространства размерности n. Требуется найти среди них максимальную линейно независимую систему. Векторы записаны в файле в виде строк матрицы размером m x n. Идея: записываем координаты векторов по столбцам новой матрицы (т.е. транспонируем исходную матрицу и записываем ее в новую матрицу). Транспонированную матрицу приводим методом Гаусса к ступенчатому виду. Определяем индексы столбцов, содержащих ПЕРВЫЕ ненулевые элементы строк. Это и будут нужные нам индексы линейно независимых векторов исходной системы. Пример: [ 1 2 3 4] [10 20 30 40] [ 4 3 2 1] A = [ 1 1 1 1] [ 0 0 0 0] [ 1 0 0 0] [10 10 0 0] [ 0 0 1 1] Транспонированная матрица [ 1 10 4 1 0 1 10 0] B = [ 2 20 3 1 0 0 10 0] [ 3 30 2 1 0 0 0 1] [ 4 40 1 1 0 0 0 1] Приводим транспонированную матрицу к ступенчатому виду методом Гаусса: [ 1 10 4 1 0 0 0 1] С = [ 0 0 5 1 0 0 0 1] [ 0 0 0 0 0 1 0 0] [ 0 0 0 0 0 0 10 -1] Находим, в каких столбцах стоят первые ненулевые элементы строк: [ 1 10 4 1 0 0 0 1] С = [ 0 0 5 1 0 0 0 1] [ 0 0 0 0 0 1 0 0] [ 0 0 0 0 0 0 10 -1] = = = = 0 2 5 6 Значит, векторы с индексами 0, 2, 5, 6 представляют максимальную линейно независимую подсистему (решение нашей задачи): [ 1 2 3 4] D = [ 4 3 2 1] [ 1 0 0 0] [10 10 0 0] Последнюю задачу 4 разберем следующий раз. ============================================ 5 Mar 2022 Задача 4. Дана однородная линейная система уравнений (свободные члены уравнений равны нулю) с вырожденной квадратной матрицей A: A X = 0 (здесь X ∙ столбец неизвестных x1, x2, ... xn). Вычислить базис пространства решений, т.е. найти n - rank(A) векторов. Исходную матрицу прочесть из файла "input.txt" (формат такой же, как и в задаче 1), ответ записать в файл "output.txt". Матрицу A размера n x n можно рассматривать как линейный оператор линейного пространства R^n. Множество векторов, которые переходят в 0 под действием оператора A, называется ядром линейного оператора: Ker(A) Это линейное подпространство: v <- Ker(A), w <- Ker(A) => Av = 0, Aw = 0 ==> A(v+w) = Av + Aw = 0 ==> v + w <- Ker(A) Нужно найти базис в этом подпространстве. Анадогично определяется образ оператора A: Im(A) = { v: найдется u такой, что A(u) = v } Столбцы матрицы оператора представляют собой образы базисных векторов. Образ -- это линейная оболочка столбцов. dim Im(a) = rank(A) Идея алгоритма: приводим матрицу методом Гаусса к ступенчатому виду A: 1 2 3 4 5 6 7 8 9 Расширенная матрица системы: 1 2 3 | 0 4 5 6 | 0 7 8 9 | 0 Столбец свободных членов остается нулевым при любых элементарных преобразованиях ==> его можно не хранить в памяти. Идея: Матрица * * * * * * | 0 * * * * * * | 0 * * * * * * | 0 * * * * * * | 0 * * * * * * | 0 * * * * * * | 0 Приводим к ступенчатому виду: 0 a * * * * | 0 a_ij != 0 0 0 a * * * | 0 0 0 0 0 a * | 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 | 0 ------------ f m m f m f t t t mainVar[i] = true, если переменная главная Главные переменные отмечены буквой m (от слова main) Свободные переменные отмечены буквой f (от слова free) В данном случае свободные переменные x0, x3, x5 Главные переменные x1, x2, x4 Свободным переменным можно придать любой набор значения, при этом главные переменные определяются однозначно с помощью обратного прохода: Идем снизу вверх: последнее уравнение выглядит как a x4 + * x5 = 0 => x4 = -* x5 / a Для того, чтобы получить базис в пространстве решений, придаем свободным переменным последовательно значения 1, 0, 0, ..., 0 0, 1, 0, ..., 0 . . . 0, 0, 0, ..., 1 Для каждого набора значений свободных переменных определяем значения главных переменных. В результате получим n - r векторов-строк, представляющих базис в пространстве решений однородной системы. Здесь n -- размерность пространства, r -- ранг матрицы Это -- базис в ядре линейного оператора. Этот базис мы выдаем в виде матрицы размера (n-r)*n, координаты базисных векторов записаны по строкам матрицы. ======================================== 14 Mar 2022 Задача сортировки: дан массив a длины n. Надо упорядочить его элементы по возрастанию: a[0] <= a[1] <= a[2] <= ... <= a[n-1] Теорема: Для всякого алгоритма сортировки найдется входной массив, на котором будет выполнено не меньше чем log_2(n!) сравнений: t >= log_2(n!) Функция log_2(n!) при n->inf ведет себя как log_2(n!) ~ n*log_2(n) Вопрос: а есть ли алгоритмы, которые работают за время O(n*log_2(n)) ? Ответ: да! Первый такой алгоритм был предложен Шенноном в конце 40-х годов XX века: сортировка слиянием Merge Sort. Она требует вспомогательной памяти порядка O(n). Зато она стабильная! Очень красивый алгоритм: сортировка кучей (пирамидой) Heap Sort. Не требует вспомогательной памяти, очень красивая в реализации. Увы, она не стабильная... Алгоритм сортировки называется стабильным, если он не меняет взаимного порядка равных элементов. "Быстрая сортировка" Quick Sort -- она не оптимальная, т.е. работает за время O(n log(n)) только в среднем. (Из 1000 самолетов 999 будут приземляться успешно, а один будет разбиваться.) Также есть сортировка Radix Sort (поразрядная сортировка), она применяется к строкам, состоящим из элементов дискетного конечного множества -- например, банковские идентификаторы счетов, которые в России состоят из 20 цифр, например, 13247546465354354357 Radix Sort работает за время O(n*k), где k -- длина строк (20 в случае банковских идентификаторов). Это тоже стабильная сортировка, так же как и MergeSort. ПЕРЕРЫВ 10 минут до 12:30 Напишем тестовую программу для разных алгоритмов сортировки. ======================================================= 28 Mar 2022 Численное интегрирование Численное вычисление производной функции. f'(x) = lim_h->0 (f(x + h) - f(x))/h Численное значение производной можно приближенно вычислить по формуле f'(x) = (f(x + h) - f(x))/h взяв небольшое значение h, например, h = 0.001 Как оценить точность вычисления по этой формуле? Разложим f(x) в ряд Тейлора точке x: f(x + h) = f(x) + f'(x)*h + f''(x)*h^2/2! + O(h^3) f(x + h) - f(x) = f'(x)*h + O(h^2) f'(x) = (f(x + h) - f(x))/h + O(h) -- очень низкая точность. Оказывается, точность приближенного вычисления производной можно на порядок увеличить, используя просто симметричную формулу: f'(x) = (f(x + h) - f(x - h))/(2*h) точность O(h^2) Это так, потому что f(x + h) = f(x) + f'(x)*h + f''(x)*h^2/2! + O(h^3) f(x - h) = f(x) - f'(x)*h + f''(x)*h^2/2! + O(h^3) f(x + h) - f(x - h) = f'(x)*(2*h) + O(h^3) (f(x + h) - f(x - h))/(2*h) = f'(x) + O(h^2) Мораль: для приближенного вычисления производной надо использовать формулу f'(x) = (f(x + h) - f(x - h))/(2*h), где h -- небольшое число. Перейдем к численному интегрированию. Интеграл Римана определяется как предел интегральных сумм. Формула прямоугольников соответствует тому, что на маленьком отрезке разбиения мы берем значение ф-ции в левом конце отрезка. Точность низкая, порядка h, где h -- величина отрезка разбиения. точность O(h) Точность ф-лы трапеций O(h^2) Формула Симпсона (формула парабол). Разбиваем отрезок интегрирования [a, b] на ЧЕТНОЕ число маленьких отрезков равной длины. На каждой паре соседних отрезков приближаем функцию параболой по трем точкам. И считаем сумму площадей получившихся криволинейных трапеций. Формула для площади криволинейной трапеции, ограниченной отрезком параболы: S = (y0 + 4*y1 + y2)/6 * (2*h) где y0 = f(x0) y1 = f(x0 + h) y2 = f(x0 + 2*h) =================================================== 4 Apr 2022 Квадратурные формулы (численное интегрирование) Задача: дана числовая функция на отрезке f: [a, b] -> R Надо вычислить приблизительно определенный интеграл от f(x) по отрезку [a, b]. В прошлый раз мы рассмотрели формулу прямоугольников и формулу трапеций. Мы начали рассматривать формулу Симпсона (формула парабол). Идея: мы разбиваем отрезок интегрирования [a, b] на ЧЕТНОЕ число маленьких отрезков равной длины. На каждой паре соседних отрезков мы приближаем функцию параболой (многочленом 2-й степени) и вычисляем интергал от этого многочлена. Интеграл по всему отрезку есть сумма интегралов по этим парам отрезков. Главное, что нужно запомнить: рассмотрим пару маленьких отрезков x0, x1 = x0+h, x2 = x0+2*h Пусть значения в этих точках равны y0, y1, y2 Еще раз, есть узлы x0, x1, x2 и значения функции в этих узлах равны y0, y1, y2: y0 y1 y2 x0 x1 x2 Тогда интеграл от квадратного многочлена (площадь криволинейной трапеции) S = ((y0 + 4*y1 + y2)/6) * (2*h) Эта формула точна не только на многочленах 2-й степени, но и на многочленах 3-ей степени! (Доказательство: можно проверить ее на f(x) = x^3, и так как ф-ла линейна относительно f, то она выполняется и на любой линейной комбинации x^d, d <= 3.) ====================================================== Создание оконных и графических приложений в системе разработки Qt Если у вас Linux -- установите Qt5 или Qt6 Если у ваc MS Windows -- надо перейти на сайт http://qt.io/ дальше --> Downloads --> Go Open Source --> download installator for 64-bit Запускаете инсталлятор, выбираете Qt для MS Windows 64 bit на базе MinGW-64 (ни в коем случае не MS Visual C++). Qt + QtCreator Qt Creator -- очень удобная оболочка для разработки приложений. Она выглядит одинаково на всех операционных системах. Код Qt-приложения не зависит от операционной системы. Попробуем написать простую программу на Qt. Программа рисует график функции в дочернем окне, мышкой отмечаем точки в окне (рисуются крестики), есть две кнопки: Draw -- рисует график функции, Clear -- стирает всё, что нарисовано.