- •1.Базовые элементы языка с. Алфавит и словарь языка
- •2. Основные типы данных. Классификация их типов. Модификация базовых типов.
- •3. Константы.
- •4. Переменные.
- •5. Структура с - программы. Понятие локальных и глобальных переменных. Функция main().Директивы препроцессора (#include и #define). Комментарии.
- •6. Операции языка с. Арифметические, логические операции. Поразрядные операции.
- •7. Операции языка с. Операция присваивания и отношения. Операция определения размера. Оператор последовательного вычисления.
- •8. Операции языка с. Условная операция. Операция (), операция [].
- •9. Приоритет операций и порядок вычислений.
- •11. Ввод-вывод символов
- •12. Форматированный ввод-вывод. Модификаторы формата. Спецификаторы преобразования. Подавление ввода.
- •13. Операторы языка с. Условные операторы (if и switch).
- •16. Одномерные массивы.
- •17. Строковый литерал. Чтение и запись строк.
- •18. Двухмерные массивы. Массивы строк.
- •20.Способы доступа к элементам массива
- •21. Понятие указателя. Инициализация указателей.
- •22. Указательные переменные. Операции получения адреса (&) и раскрытия ссылки(*).
- •23. Указательные выражения. Адресная арифметика.
- •Динамическое выделение памяти для массивов.
- •Функции. Определения функций. Оператор return.
- •Тип_результата id_функции (список);
- •Функции. Прототипы функций.
- •Тип_результата id_функции (список);
- •Функции. Вызов функций: вызов по значению и по ссылке.
- •Тип_результата id_функции (список);
- •Передача массива в функцию.
- •Классы памяти. Область видимости.
- •Аргументы функции main(): argv и argc.
- •Вызов библиотечных функций.
- •Директива препроцессора #define: создание макрофункций с помощью директивы #define.
- •Директивы условной компиляции #if, #else, #elif, #endif, #ifdef, #ifndef.
- •Понятие структуры. Доступ к членам структуры.
- •Присваивание структур.
- •Id_структуры . Id_поля
- •Передача членов структур функциям. Передача целых структур функциям.
- •Указатели на структуры. Средство typedef.
- •Понятия объединения и перечисления. Битовые поля.
- •Основы файловой системы. Стандартные потоки. Указатель файла. Открытие файла. Закрытие файла.
Указатели на структуры. Средство typedef.
Формат указателя на структуру:
struct имя_стуктурного_типа *имя_указательной_пременной;
Доступ к элементам структурчерез указатели будет осуществляться следующим образом:
(*имя_указательной_пременной).элемент или имя_указательной_пременной-> элемент
Пример:
struct key *pts; // объявляем указатель на структуру key
Обращение к элементe count структуры key может осуществляться следующим образом:
(*pts).count или же pts->count.
Средство typedef
Кроме объявлений переменных различных типов, имеется возможность объявить типы. Это можно сделать двумя способами. Первый способ - указать имя тега при объявлении структуры, объединения или перечисления, а затем использовать это имя в объявлении переменных и функций в качестве ссылки на этот тег. Второй - использовать для объявления типа ключевое слово typedef. При объявлении с ключевым словом typedef идентификатор, стоящий на месте описываемого объекта, является именем вводимого в рассмотрение типа данных, и далее этот тип может быть использован для объявления переменных.
Отметим, что любой тип может быть объявлен с использованием ключевого слова typedef, включая типы указателя, функции или массива. Имя с ключевым словом typedef для типов указателя, структуры, объединения может быть объявлено прежде чем эти типы будут определенны, но в пределах видимости объявителя.
Примеры:
typedef double (* MATH)( );
/* MATH - новое имя типа, представляющее указатель на
функцию, возвращающую значения типа double */
MATH cos;
/* cos указатель на функцию, возвращающую
значения типа double */
//Можно провести эквивалентное объявление
double (* cos)( );
typedef char FIO[40]
// FIO - массив из сорока символов
FIO person;
//Переменная person - массив из сорока символов
//Это эквивалентно объявлению
char person[40];
При объявлении переменных и типов здесь были использованы имена типов (MATH FIO). Помимо этого, имена типов могут еще использоваться в трех случаях: в списке формальных параметров, в объявлении функций, в операциях приведения типов и в операции sizeof (операция приведения типа). Именами типов для основных типов, типов перечисления, структуры и смеси являются спецификаторы типов для этих типов. Имена типов для типов указателя массива и функции задаются при помощи абстрактных описателей следующим образом:
спецификатор-типа абстрактный-описатель;
Абстрактный-описатель – это описатель без идентификатора, состоящий из одного или более модификаторов указателя, массива или функции. Модификатор указателя (*) всегда задается перед идентификатором в описателе, а модификаторы массива [] и функции () - после него. Таким образом, чтобы правильно интерпретировать абстрактный описатель, нужно начать интерпретацию с подразумеваемого идентификатора. Абстрактные описатели могут быть сложными. Скобки в сложных абстрактных описателе задают порядок интерпретации подобно тому, как это делалось при интерпретации сложных описателей в объявлениях.
Понятия объединения и перечисления. Битовые поля.
Понятия объединения и перечисления
Объединение – поименованная совокупность данных разных типов, размещаемых с учетом выравнивания в одной и той же области памяти, размер которой достаточен для хранения наибольшего элемента.
Объединенный тип данных декларируется подобно структурному типу:
union ID_объединения {
описание полей
};
Пример описания объединенного типа:
union word {
int nom;
char str[20];
};
Пример объявления объектов объединенного типа:
union word *p_w, mas_w[100];
Объединения применяют для экономии памяти в случае, когда объединяемые элементы логически существуют в разные моменты времени либо требуется разнотипная интерпретация поля данных.
Практически все вышесказанное для структур имеет место и для объединений. Декларация данных типа union, создание переменных этого типа и обращение к полям объединений производится аналогично структурам.
Пример использования переменных типа union:
. . .
typedef union q {
int a;
double b;
char s[5];
} W;
void main(void)
{
W s, *p = &s;
s.a = 4;
printf(“\n Integer a = %d, Sizeof(s.a) = %d”, s.a, sizeof(s.a));
p –> b = 1.5;
printf(“\n Double b = %lf, Sizeof(s.b) = %d”, s.b, sizeof(s.b));
strcpy(p–>s, “Minsk”);
printf(“\n String a = %s, Sizeof(s.s) = %d”, s.s, sizeof(s.s));
printf(“\n Sizeof(s) = %d”, sizeof(s));
}
Результат работы программы:
Integer a = 4, Sizeof(s.a) = 2
Double b = 1.500000, Sizeof(s.b) = 4
String a = Minsk, Sizeof(s.s) = 5
Sizeof(s) = 5
Перечисления – средство создания типа данных посредством задания ограниченного множества значений.
Определение перечисляемого типа данных имеет вид
enum ID_перечисляемого_типа {
список_значений
};
Значения данных перечисляемого типа указываются идентификаторами, например:
enum marks {
zero, one, two, three, four, five
};
Компилятор последовательно присваивает идентификаторам списка значений целочисленные величины 0, 1, 2,... . При необходимости можно явно задать значение идентификатора, тогда очередные элементы списка будут получать последующие возрастающие значения. Например:
enum level {
low=100, medium=500, high=1000, limit
};
Константа limit по умолчанию получит значение, равное 1001.
Примеры объявления переменных перечисляемого типа:
enum marks Est;
enum level state;
Переменная типа marks может принимать только значения из множества {zero, one, two, three, four, five}.
Основные операции с данными перечисляемого типа:
– присваивание переменных и констант одного типа;
– сравнение для выявления равенства либо неравенства.
Практическое назначение перечисления – определение множества различающихся символических констант целого типа.
Пример использования переменных перечисляемого типа:
. . .
typedef enum {
mo=1, tu, we, th, fr, sa, su
} days;
void main(void)
{
days w_day; // Переменная перечисляемого типа
int t_day, end, start;
// Текущий день недели, начало и конец недели соответственно
puts(“ Введите день недели (от 1 до 7) : ”);
scanf(“%d”, &t_day);
w_day = su;
start = mo;
end = w_day – t_day;
printf(“\n Понедельник – %d день недели, \
сейчас %d – й день и \n\
до конца недели %d дн. ”, start, t_day, end );
}
Результат работы программы:
Введите день недели (от 1 до 7) : 5
Понедельник – 1 день недели, сейчас 5-й день и
до конца недели 2 дн.
Методы сортировки данных. Пузырьковая сортировка. Сортировка посредством выбора. Сортировка вставками. Улучшенный алгоритм сортировки: сортировка Шелла, быстрая сортировка. Сортировка строк и структур.
При работе со списками очень часто возникает необходимость перестановки элементов списка в определенном порядке. Такая задача называется сортировкой списка и для ее решения существуют различные методы. Рассмотрим некоторые из них.
Пузырьковая сортировка
Задача сортировки заключается в следующем: задан список целых чисел (простейший случай) В=< K1, K2,..., Kn >. Требуется переставить элементы списка В так, чтобы получить упорядоченный список B’=< K’1, K’2,...,K’n >, в котором для любого 1<=i<=n элемент K’ (i) <= K’(i+1).
При обменной сортировке упорядоченный список В’ получается из В систематическим обменом пары рядом стоящих элементов, не отвечающих требуемому порядку, пока такие пары существуют.
Наиболее простой метод систематического обмена соседних элементов с неправильным порядком при просмотре всего списка слева на право определяет пузырьковую сортировку: максимальные элементы как бы всплывают в конце списка.
Пример:
B=<20,-5,10,8,7>, исходный список;
B1=<-5,10,8,7,20>, первый просмотр;
B2=<-5,8,7,10,20>, второй просмотр;
B3=<-5,7,8,10,20>, третий просмотр.
В последующих примерах будем считать, что сортируется одномерный массив (либо его часть от индекса n до индекса m) в порядке возрастания элементов.
Нижеприведенная функция bubble сортирует входной массив методом пузырьковой сортировки.
/* сортировка пузырьковым методом */
float * bubble(float * a, int m, int n)
{
char is=1;
int i;
float c;
while(is)
{
is=0;
for (i=m+1; i<=n; i++)
if ( a[i] < a[i-1] )
{
c=a[i];
a[i]=a[i-1];
a[i-1]=c;
is=1;
}
}
return(a);
}
Пузырьковая сортировка выполняется при количестве действий Q=(n-m)*(n-m) и не требует дополнительной памяти.
Сортировка посредством выбора
Упорядоченный список В’ получается из В многократным применением выборки из В минимального элемента, удалением этого элемента из В и добавлением его в конец списка В’, который первоначально должен быть пустым.
Например:
B=<20,10,8,-5,7>, B’=< >
B=<20,10,8,7>, B’=<-5>
B=<20,10,8>, B’=<-5,7>
B=<20,10>, B’=<-5,7,8>
B=<20>, B’=<-5,7,8,10>
B=< >, B’=<-5,7,8,10,20>
Функция select упорядочивает массив s сортировкой посредством выбора.
/* сортировка методом выбора */
double *select( double *s, int m, int n)
{
int i,j;
int imin,c;
for(i=m;i< n;i++){ //для каждого элемента
//ищем минимальный элемент в оставшейся части массива
imin=s[i];
for (j=i; j< n;j++) if(s[j]< s[imin]) imin=j;
//меняем текущий элемент с минимальным
c=s[i];
s[i]=s[imin];
s[imin]=c;
}
return(s);
}
Здесь, как и в предыдущем примере оба списка В и В’ размещаются в разных частях массива s. При сортировке посредством выбора требуется Q=(n-m)*(n-m) действий и не требуется дополнительной памяти.
Сортировка вставками
Упорядоченный массив B’ получается из В следующим образом: сначала он состоит из единственного элемента К1; далее для i=2,...,N выполняется вставка узла Кi в B’ так, что B’ остается упорядоченным списком длины i.
Например, для начального списка B=< 20,-5,10,8,7 > имеем:
B=< 20,-5,10,8,7> B’=< >
B=< -5,10,8,7 > B’=< 20 >
B=< 10,8,7 > B’=< -5,20 >
B=< 8,7 > B’=< -5,10,20 >
B=< 7 > B’=< -5,8,10,20 >
B=< > B’=< -5,7,8,10,20 >
Функция insert реализует сортировку вставкой.
/* сортировка методом вставки */
int *insert(int *s, int m, int n){
int i,j,k;
int aux;
for (i=m+1; i< n; i++){ //для каждого элемента массива
aux=s[i]; //берем текущий элемент
//ищем место вставки и одновременно освобождаем его сдвигая элементы массива назад
for (k=i-1; k>=0 && s[k]>aux; k--) s[k+1]=s[k];
//вставляем элемент на найденное место
s[k+1]=aux;
}
return(a);
}
Здесь оба списка В и В’ размещаются в массиве s, причем список В занимает часть s c индексами от i до n, а B’ - часть s c индексами от m до i-1.
При сортировке вставкой требуется Q=(n-m)*(n-m) сравнений и не требуется дополнительной памяти.
Улучшенный алгоритм сортировки: быстрая сортировка, сортировка Шелла
Быстрая сортировка состоит в том, что список В=< K1,K2,...,Kn> реорганизуется в список B’,< K1 >,B", где В’ - подсписок В с элементами, не большими К1, а В" - подсписок В с элементами, большими К1. В списке B’,< K1 >,B" элемент К1 расположен на месте, на котором он должен быть в результирующем отсортированном списке. Далее к спискам B’ и В" снова применяется упорядочивание быстрой сортировкой. Приведем в качестве примера сортировку списка, отделяя упорядоченные элементы косой чертой, а элементы Ki знаками < и >.
Пример:
9, 7, 18, 3, 52, 4, 6, 8, 5, 13, 42, 30, 35, 26
7, 3, 4, 6, 8, 5/ <9>/ 18, 52, 13, 42, 30, 35, 26
3, 4, 6, 5/<7>/ 8/ 9/ 13/ <18>/ 52, 42, 30, 35, 26
<3>/ 4, 6, 5/ 7/ 8/ 9/ 13/ 18/ 42, 30, 35, 26/ <52>
3/ <4>/ 6, 5/ 7/ 8/ 9/ 13/ 18/ 30, 35, 26/ <42>/ 52
3/ 4/ 5/ <6>/ 7/ 8/ 9/ 13/ 18/ 26/ <30>/ 35/ 42/ 52
Время работы по сортировке списка методом быстрой сортировки зависит от упорядоченности списка.
Оно будет минимальным, если на каждом шаге разбиения получаются подсписки B’ и В" приблизительно равной длины, и тогда требуется около N*log2(N) шагов. Если список близок к упорядоченному, то требуется около (N*N)/2 шагов.
Рекурсивная функция quick упорядочивает участок массива s быстрой сортировкой.
/* быстрая сортировка */
double * quick(double *s,int low,int hi)
{
double cnt,aux;
int i,j;
if (hi>low)
{
i=low;
j=hi;
cnt=s[i];
while(i < j)
{
if (s[i+1]<=cnt)
{
s[i]=s[i+1];
s[i+1]=cnt;
i++;
}
else
{
if (s[j]<=cnt)
{
aux=s[j];
s[j]=s[i+1];
s[i+1]=aux;
}
j--;
}
}
quick(s,low,i-1);
quick(s,i+1,hi);
}
return(s);
}
Здесь используются два индекса i и j, проходящие части массива навстречу друг другу. При этом i всегда фиксирует разделяющий элемент cnt=s[low], слева от которого находятся числа, не большие cnt, а справа от i - числа, большие cnt. Возможны три случая: при s[i+1]<=cnt; при s[i+1]>cnt и s[j]<=cnt; при s[i+1]>cnt и s[j]>cnt. По окончании работы i=j, и cnt=s[i] устанавливается на своем месте.
Быстрая сортировка требует дополнительной памяти порядка log2(N) для выполнения рекурсивной функции quick (неявный стек).
Существенными в сортировках вставками являются затраты на обмены или сдвиги элементов. Для их уменьшения желательно сначала производить погружение с большим шагом, сразу определяя элемент помест. Этим отличается СОРТИРОВКА ШЕЛЛА: исходный массив разбивается на n частей, в каждую из которых попадают элементы с шагом m, начиная от 0,1,...,m-1 соответственно, то есть
0, m, 2m, 3m,...
1, m+1, 2m+1, 3m+1,...
2, m+2, 2m+2, 3m+2,...
Каждая часть сортируется отдельно с использованием алгоритма вставок. Затем выбирается меньший шаг, и алгоритм повторяется. Шаг может быть выбран равным степеням 2, например 64, 32, 16, 8, 4,2, 1. Последняя сортировка выполняется с шагом 1.
Сортировка строк
#include
#include
#define MAXLINES 5000 // максимальное число строк
char *lineptr[MAXLINES]; // указатели на строки
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qsort(char *lineptr[], int left, int right);
/* сортировка строк */
main()
{
int nlines; /* количество прочитанных строк */
if ((nlines = readlines(lineptr, MAXLINES)) >= 0)
{
qsort(lineptr, 0, nlines-1);
writelines(lineptr, nlines);
return 0;
}
else {
printf("ошибка: слишком много строк\n");
return 1;
}
}
Методы поиска: последовательный и двоичный поиск.
Последовательный поиск
Последовательный поиск предусматривает последовательный просмотр всех элементов списка В в порядкеих расположения, пока не найдется элемент равный V. Если достоверно неизвестно, что такой элемент имеется в списке, то необходимо следить за тем, чтобы поиск не вышел за пределы списка, что достигается использованием стоппера.
Очевидно, что Max последовательного поиска равен N. Если частота использования каждого элемента списка одинакова, т.е. P=1/N, то Avg последовательного поиска равно N/2. При различной частоте использования элементов Avg можно улучшить, если поместить часто встречаемые элементы в начало списка.
Пусть во входном потоке задано 100 целых чисел К1,К2,... К100 и ключ V. Составим программу для последовательного хранения элементов Кi и поиска среди них элемента, равного V, причем такого элемента может и не быть в списке. Без использования стоппера программа может быть реализована следующим образом:
/* последовательный поиск без стоппера */
#include
main()
{
int k[100],v,i;
for (i=0;i<100;i++)
scanf("%d",&k[i]);
scanf("%d",&v);
i=0;
while(k[i]!=v && i<100) i++;
if (k[i]==v) printf("%d %d",v,i);
else printf("%d не найден",v);
}
С использованием стоппера программу можно записать в виде:
/* последовательный поиск со стоппером */
#include
main()
{
int k[101],v,i;
for (i=0;i<100;i++)
scanf("%d",&k[i]); //ввод данных
scanf("%d",&v);
k[100]=v; //стоппер
i=0;
while(k[i]!=v) i++;
if (i<100) printf ("%d %d",v,i);
else printf ("%d не найден",v);
}
Двоичный поиск
Для упорядоченных линейных списков существуют более эффективные алгоритмы поиска, хотя и для такихсписков применим последовательный поиск. Двоичный поиск состоит в том, что ключ V сравнивается со средним элементом списка. Если эти значения окажутся равными, то искомый элемент найден, противном случае поиск продолжается в одной из половин списка.
Нахождение элемента двоичным поиском осуществляется очень быстро. Max двоичного поиска равен log2(N), и при одинаковой частоте использования каждого элемента Avg двоичного поиска равен log2(N). Недостаток двоичного поиска заключается в необходимости последовательного хранения списка, что усложняет операции добавления и исключения элементов.
Пусть, например, во входном потоке задано 101 число, К1,К2,...,К100, V – элементы списка и ключ. Известно, что список упорядочен по возрастанию, и элемент V в списке имеется. Составим программу для ввода данных и осуществления двоичного поиска ключа V в списке К1,К2,...,К100.
/* Двоичный поиск */
#include
main()
{
int k[100],v,i,j,m;
for (i=0;i<100;i++)
scanf("%d",&k[i]);
scanf("%d",&v);
i=0; j=100; m=50;
while (k[m]!=v)
{
if (k[m] < v) i+=m;
else j=m-i;
m=(i+j)/2;
}
printf("%d %d",v,m);
}