- •Ответы по дисциплине «Основы алгоритмизации и программирования»
- •Запись алгоритма Евклида на языке с
- •Int main() {
- •Эвристический алгоритм «ближайшего соседа»
- •Эвристический алгоритм «ближайших пар»
- •«Правильный» алгоритм поиска маршрута
- •Эволюция языка с bcpl → b → c → k&r c → ansi c → c99 → c1x
- •#Define имя текст_для_подстановки
- •123, 67543, 037, 07777, 0Xabf7, 0xffff, …
- •123456789L, 0xful (это просто число 15).
- •Определение символических констант в limits.H
- •Int lower, upper, step;
- •Int main() {
- •Int main() {
- •Int main() {
- •Всего операций: 47
- •If (условие) оператор
- •If (условие) оператор1 else оператор2
- •Int main() {
- •Int main() {
- •Int main() {
- •Int main() {
- •If (found)
- •Адресация памяти
- •Адреса объектов программы
- •Int fact(int n) {
- •О размерах участков памяти, выделяемых объектам
- •Правила адресной арифметики
- •Никакие другие операции к адресам неприменимы, т.Е. Адреса нельзя умножать, делить, складывать между собой и пр.
- •Имя массива – это константный указатель на его начало.
- •T X[] эквивалентно t *X
- •Int main() {
- •Void *calloc(size_t n, size_t r)
- •Void free(void *p)
- •Int main() {
- •Void *p;
- •Void swaps(char** a, char** b) {
- •Int main(void) {
- •Int main() {
- •Правило «право-лево»
- •Int pt_in_rect(struct point p, struct rect r) {
- •Int main() {
- •Int main() {
- •Int ival;
- •Void init(Vector*);
- •Void resize(Vector*, int);
- •Void push_back(Vector*, double);
- •Void push_s(Stack *st, double d) {
- •Void init_q(Queue *q) {
- •Void enqueue(Queue *q, double d) {
- •Int dequeue(Queue *q, double *d) {
- •Typedef struct Heap {Vector V;} Heap;
- •Void init_h(Heap *hp) {
- •Int Heap_Maximum(Heap *hp, double *z) {
- •Void Max_Heap_Insert(Heap *hp, double X){
- •Void Max_Heapify(Heap *hp, int I) {
- •Int l, r, largest;
- •Int Heap_Extract_Max(Heap *hp, double *z) {
- •Void Build_Max_Heap(Heap *hp) {
- •Void Insert_head_l1(List1 *l, double z) {
- •Void Insert_back_l1(List1 *l, double z) {
- •Int Extract_head_l1(List1 *l, double *z) {
- •Int Extract_back_l1(List1 *l, double *z) {
- •Void reverse_l1(List1 *l) {
- •Исходный код функции sort_l1
- •Void sort_l1(List1 *l) {
- •Void visit(List1* l) {
- •Void traverse(List1* l) {
- •Void Print_l1(List1 *l) {
- •Void Insert_l2(List2 *l, double z, int direction) {
- •Прямой обход (сверху вниз), при котором мы посещаем узел, а затем левое и правое поддеревья
- •Поперечный обход (слева направо), при котором мы посещаем левое поддерево, затем узел, а затем правое поддерево
- •Обратный обход (снизу вверх), при котором мы посещаем левое и правое поддеревья, а затем узел.
- •Простой метод сортировки массива
- •Задача о взвешивании монет
- •1) Очевидно, что на последнем шаге процедуры взвешивания мы должны иметь дело максимум с 3 монетами, чтобы в при любом исходе взвешивания получить результат.
- •2) Задача предпоследнего шага – отобрать группу из 3-х монет. Это можно сделать, если в нашем распоряжении будет не более 9 монет (3 группы по 3 монеты).
- •3) Наконец, если у нас будет от 10 до 27 монет, мы сможем отобрать из них не более 9
- •Void mov(int n, char a, char c, char b) {
- •Int main() {
Int ival;
float fval;
char *sval;
} u;
Очевидно, что получить значение поля объединения можно лишь того типа, какой был ранее записан в это объединение:
u.ival = 10;
…
x = u.fval; // err!
Лекция 13. Алгоритмы и структуры данных.
Вопрос №73. Сравнение алгоритмов последовательного и двоичного поиска.
Рассмотрим два алгоритма поиска – последовательный (линейный) поиск и двоичный поиск.
Алгоритм последовательного поиска является простейшим алгоритмом и не предъявляет никаких требований к массиву, в котором ведется поиск. Однако его быстродействие – наихудшее. Количество сравнений, которое делает этот алгоритм в наилучшем случае =1, а в наихудшем =N, т.е. в среднем необходимо сделать N/2 сравнений.
Гораздо лучшие результаты дает алгоритм двоичного поиска, который реализует стратегию двукратного уменьшения области поиска на каждой итерации. Это оказывается возможным, если массив предварительно упорядочен (отсортирован) по значению ключа.
Коротко алгоритм двоичного поиска формулируется так:
Сравниваем заданное значение со средним элементом массива.
Если это значение меньше среднего элемента, то поиск продолжаем в левой части массива; в противном случае – в правой части.
Повторяем до тех пор, пока не найдем нужный элемент или не убедимся, что его в массиве нет.
Двоичный поиск отбрасывает за каждый шаг половину данных, поэтому количество шагов пропорционально тому, сколько раз можно поделить n на 2, пока не останется один элемент. Без учета округления это число равно log2n.
Если в массиве 1000 элементов, то линейный поиск может потребовать до 1000 шагов, в то время как двоичный — только около 10; при миллионе элементов оценка максимальной трудоемкости для линейного поиска составит миллион шагов, а для двоичного — 20.
Очевидно, чем больше число элементов, тем больше преимущество двоичного поиска.
Вопрос №74. Структура данных «вектор» (динамически расширяемый массив): основные характеристики и внутреннее устройство. Интерфейс структуры данных «вектор».
Сконструируем абстракцию «вектор», которая во многом будет похожа на массив (состоит из однотипных элементов и обеспечивает прямой доступ к элементам по индексу), но допускает изменение размеров во время работы и неограниченное добавление новых элементов.
Основные идеи для реализации этой абстракции:
1-я идея) Использовать структуру языка С для представления вектора, членами которой будут:
массив достаточного размера для хранения элементов вектора,
число, равное количеству элементов вектора,
число, равное текущему размеру массива.
Основные идеи для реализации абстракции «вектор»
а) «пустой вектор» – это: sz = 0, space = 0, elem = NULL .
б) «полный вектор» – к вектору невозможно добавить новый элемент, т.к. нет свободного пространства: sz = space .
2-я идея) Если при попытке добавления нового элемента обнаруживается состояние «полный вектор», то массив, в котором хранится вектор, увеличивает свой размер в два раза. Такое увеличение происходит за 3 шага:
выделяется новая область памяти размером 2*space,
«старый» массив переписывается в эту область,
прежняя область освобождается и устанавливается space = 2*space.
Теперь начнем разработку функций, поддерживающих абстракцию «вектор».
Первая из функций - init должна выполнять инициализацию вектора, т.е. создвать пустой вектор:
void init(Vector *v) {
v->sz = v->space = 0; v->elem = NULL;
}
Вторая функция - push_back добавляет к вектору новый элемент:
void push_back(Vector *v, double d) {
if (v->sz == 0) // память ещё не выделялась
reserve(v, 8);
else if (v->sz == v->space) // нет свободного пространства
reserve(v, 2*v->space);
v->elem[v->sz] = d; // добавляем новый элемент в конец
++v->sz; // инкрементируем счетчик
}
Функция reserve играет вспомогательную роль – перемещает вектор на новое место, размер которого больше, чем существующий размер:
void reserve(Vector *v, int newalloc) {
if (newalloc <= v->space) return;
double* p = (double*)calloc(newalloc, sizeof(double));
int i;
for (i=0; i<v->sz; ++i) p[i] = v->elem[i]; // копируем...
if(v->elem) free(v->elem); // освобождаем память...
v->elem = p;
v->space = newalloc;
}
Наконец, ещё одна полезная функция изменения размера вектора resize:
void resize(Vector *v, int newsize) {
int i;
reserve(v, newsize);
for(i = v->sz; i<newsize; ++i) v->elem[i] = 0;
v->sz = newsize;
}
Теперь можно приступить к конструированию модульной структуры приложения, использующего абстракцию «вектор». Прежде всего, сформируем интерфейс, в который поместим описание структуры Vector, а также описания клиентских функций для работы с векторами: init, push_back и resize.
Как отмечалось ранее, интерфейс помещается в заголовочный файл, который назовем Vector.h :
// Vector.h
typedef struct Vector
{ int sz; double* elem; int space; } Vector;