Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Метод_материалы / Учебники / Программирование_С

.pdf
Скачиваний:
66
Добавлен:
16.03.2016
Размер:
2.31 Mб
Скачать

массивов, среди элементов которых может быть любое количество одинаковых. Обычно элементы множеств однотипны.

Множество может быть сконструировано на основе практически любой структуры — статической или динамической. Основная особенность множества — любой элемент может принадлежать множеству ровно один раз.

Основные функции:

добавить элемент,

удалить элемент,

принадлежность к множеству,

включение в множество,

объединение множеств,

пересечение множеств,

разность множеств,

равенство / неравенство множеств.

Подробнее,

Добавить элемент: a incl A добавляет элемент a к множеству A. Удалить элемент: a del A удаляет элемент a из множества A.

Принадлежность к множеству: a in A дает TRUE, если a входит в A, и FALSE

в противном случае.

Включение в множество: B sub A дает TRUE, если А является подмножеством В (то есть все элементы множества А входят в множество В, однако множества не равны.

Объединение: A+B каждый элемент множеств А и В входит в результирующее множество.

Пересечение: А x В дает результирующее множество, в которое входят все элементы, принадлежащие одновременно множествам А и В.

Разность: А \ В дает результирующее множество, в которое входят все элементы, принадлежащие множеству А, но не множеству В.

Неравенство: А!= В дает TRUE, если А не совпадает с В; (то есть либо множество А, либо множество В содержит хотя бы один элемент, не входящий в другое множество).

Следует напомнить, что ни стандартного типа «множество», ни соответствующих встроенных операций в языке C нет, поэтому всё следует конструировать самостоятельно. В частности, если множество реализовано на основе массива, то, например, операция добавления элемента сводится к просмотру всех существующих элементов массива и в случае, когда данного элемента нет и есть свободные ячейки, следует записать элемент в свободную ячейку.

В заключение этого раздела заметим, что изъятие элемента для любых рассмотренных структур на основе массива — это просто его считывание. Реально элемент остается на месте, однако становится недоступен благодаря

131

перемещению указателей. При повторном использовании ячейки новое значение просто записывается поверх старого.

2.1.4.1. Задачи на множество на основе массива

2.1.4.1.1. Удаление элементов множества, больших заданного Реализовать множество на основе массива, тип данных — целые числа. Прикладная задача:

программа заполняет два пересекающихся множества из входного потока, затем программа проводит все операции над этими множествами, после чего удаляет из каждого все элементы, большие заданного.

2.1.4.1.2. Удаление элементов множества, больших по длине заданной Реализовать множество на основе массива, тип данных — строки символов. Прикладная задача:

программа заполняет два пересекающихся множества из входного потока, затем программа проводит все операции над этими множествами, после чего удаляет из каждого все элементы, большие по длине заданной.

2.2. Динамические конструируемые типы данных

Динамические структуры данных отличаются от статических структур тем, что для их реализации в процессе выполнения программы выделяется столько памяти, сколько необходимо — заранее размер памяти не фиксируется. Используется так называемая динамическая память.

2.2.1. Динамическая память

До сих пор мы имели дело со структурами данных, размер которых в оперативной памяти фиксирован — определен в момент написания исходного текста программы и не может быть изменен во время ее выполнения, Примером такой структуры является массив. Если при выполнении программа пытается обратиться к элементу массива с индексом, лежащим за пределами заданного диапазона, то фиксируется ошибка типа "Out оf range" — "За пределами диапазона". Однако для многих задач количество подлежащих обработке данных заранее не известно; использовать в этих ситуациях очень большие массивы ("на все случаи жизни") нерационально, и совершенно нелепо с точки зрения цивилизованного программирования. Для преодоления этих трудностей практически во всех современных языках программирования имеется аппарат динамической памяти, ранее описанный в разделе 2.4.4.4 — основные механизмы и функции. Еще раз вернемся к этому понятию, поскольку для успешного конструирования динамических структур нужно представлять себе, как работает оперативная память компьютера.

Оперативная память является одной из важнейших компонент компьютера. В нее после включения машины загружаются программы операционной системы,

132

играющие роль "посредника" между "железом" (то есть процессором, другими интегральными схемами, постоянной памятью на жестком диске и т.п.) и прикладными программами — теми, которые создаем мы с вами. Прикладная программа вместе с данными, которые она обрабатывает (например, массивами целых чисел) для выполнения также загружается в оперативную память и оттуда берет управление на себя. Кроме того, в оперативной памяти постоянно присутствуют еще некоторые программы (например, драйверы устройств) и данные (например, видеопамять). Очень грубо схему использования памяти можно представить себе так, как это показано на рис. 2.5, а:

Рис. 2.5. Размещение динамической памяти

Как видно из рисунка, некоторая часть оперативной памяти даже при загруженной программе пользователя остается свободной. Эту часть называют "кучей" (heap), и именно эту незанятую часть памяти можно использовать динамически, то есть: захватить нужный объем памяти, использовать его, а после того, как отпала потребность — освободить этот объем памяти (вернуть в кучу). Для этого в языке C имеются соответственно операции new и delete (см. примеры).

Операции с динамической памятью реализуются а языке C с помощью указателей, фактически представляющих собой адреса ячеек оперативной памяти. В ячейках лежат данные, а номер ячейки и есть ее адрес. Если теперь этот адрес записать в какую-то другую ячейку памяти, то она будет играть роль указателя на первую ячейку (рис. 2.5, б).

В программе мы, как правило, работаем с объектами больших размеров, чем один байт; в этом случае в указателе хранится адрес начала объекта.

133

Итак, в переменной типа указатель на объект хранится адрес некоторой области памяти, отведенной под размещение объекта.

Поскольку объекты могут иметь разные типы, указатели на объекты также имеют разные типы, определяемые типом объекта. В языке это реализуется через конструктор указателя:

имя_ типа * имя_указателя

Например, указатель на переменную типа int конструируется так:

int * pt_int;

Стандартная схема работы с динамическими объектами примерно такова:

создать экземпляр объекта — выделяется реальная память под объект,

создать экземпляр указателя на объект — выделяется реальная память под указатель,

привязать указатель к объекту при помощи операции &,

далее работать с объектом через указатель,

при необходимости освободить занятую динамическую память.

Вообще говоря, во всех задачах этого раздела было бы необходимо:

проверять наличие свободной динамической памяти при выделении памяти под текущий элемент структуры,

корректно возвращать неиспользуемую память обратно в «кучу», в частности при повторной инициализации структуры, однако в нижеприведённых примерах это не поддерживается ввиду экономии текста. Улучшение программ предоставляем читателю.

2.2.2. Связный (линейный) список

Связный список представляет собой структуру, каждый элемент которой содержит два поля: поле указателя на следующий элемент списка и поле данных. Если список хранит значения типа int, то изобразить его можно так (рис. 2.6):

Рис. 2.6. Связный список

134

Указатель pt_lst на первый элемент списка дает доступ к этой структуре. Извлекая из первого элемента списка значение указателя на второй элемент, получаем доступ ко второму элементу и так далее; последний указатель должен быть равен NULL, отмечая тем самым конец списка. Таким образом, список является структурой с последовательным доступом: выйти на нужный элемент списка можно, только пройдя по всем предшествующим элементам — в отличие от массива, любой элемент которого может быть получен сразу по данному значению индекса. Массив поэтому представляет собой структуру с прямым доступом.

Обсудим вкратце преимущества и недостатки списковых структур. Сравним для этого две структуры: упорядоченный (то есть отсортированный) массив и список. Получить доступ к элементу массива, разумеется, легче, чем к элементу списка; однако представим себе, что необходимо добавить в массив/список еще один элемент (сохраняя упорядоченность). Иллюстрация для этой ситуации приведена на рис. 2.7.

Для вставки числа 21 в массив необходимо переместить на одну ячейку вправо все его элементы, лежащие правее элемента 11, то есть совершить очень большую и нерациональную работу (рис. 2.7, а). Напротив, для вставки числа 21 в список вообще не нужно перемещать данные. Достаточно проделать манипуляции, показанные на рис. 2.7, б:

получить динамическую память под новый элемент списка;

привязать указатель нового элемента к ячейке с элементом 25;

отвязать указатель ячейки с элементом 11 от старой ячейки с элементом 25 и привязать его к ячейке с элементом 21.

135

Рис. 2.7. Вставка элемента в массив и связный список Заметим, что и в том, и в другом случае сначала нужно найти место для

нового элемента — затраты на это сопоставимы.

Основные функции:

добавить элемент слева,

просмотреть список.

Вследующем примере иллюстрируется работа с неупорядоченным списком — новый элемент добавляется слева:

# include <stdio.h>

# include <conio.h>

# define LIST_SIZE 100

struct LINK{

 

// определение узла списка

int data;

 

 

LINK * next;

 

 

};

 

 

typedef LINK * LIST;

// определение указателя на узел

LIST IniList ( LIST

pt_lst );

// инициация списка

LIST AddList( LIST pt_lst, int new_el );

// добавление элемента слева

void WriteList( LIST pt_lst );

// просмотр списка

main()

 

 

{

 

 

LIST lst;

// создаем указатель на список

int lst_el;

// целое число – элемент списка

clrscr();

printf( "Вводим числа - элементы списка (завершение - 0): \n");

lst = IniList( lst );

do

// заполнение списка

{

 

136

scanf("%d", &lst_el);

lst = AddList(lst, lst_el); // не возвращает данных об

// успехе/неуспехе попытки, // поскольку указатель на // список может быть

// модифицирован и его нужно // вернуть

}

while ( lst_el != 0 );

printf("\n Вот Ваш список:\n");

WriteList( lst );

return 0;

}

LIST IniList ( LIST pt_lst )

{

pt_lst = NULL; eturn pt_lst

}

LIST AddList( LIST pt_lst, int new_el )

{

LIST pt_new;

pt_new = new LINK;

pt_new–>data = new_el; pt_new–>next = pt_lst; pt_lst = pt_new;

return pt_lst;

}

//некорректный способ

//инициации, надо бы вернуть

//память в кучу

//новый узел

//положим туда данные

//перекинем указатели

void WriteList( LIST pt_lst )

// указатель списка не изменяется после

{

// передачи его в качестве параметра

 

while ( pt_lst != NULL )

// распечатка содержимого

// списка

{

printf("%d ",pt_lst–>data );

137

pt_lst = pt_lst–>next;

};

}

При определении поля LINK * next мы ссылаемся на тип LINK, который к этому моменту еще не определен! Это противоречит одному из основных правил языка — определять новые типы, используя в качестве базовых типов ранее определенные. Продемонстрированное здесь исключение из этого правила — "определение вперед" — является в языке единственным.

2.2.2.1. Задачи на связный список

2.2.2.1.1. Изменение порядка элементов списка (целые числа) на обратный Реализовать связный список, тип данных — целые числа.

Прикладная задача:

исходный связный список заполняется из входного потока, затем программа меняет порядок элементов списка на обратный.

2.2.2.1.2. Изменение порядка элементов списка (строки) на обратный Реализовать связный список, тип данных — строки символов. Прикладная задача:

исходный связный список заполняется из входного потока, затем программа меняет порядок элементов списка на обратный.

2.2.2.1.3. Смена местами половин списка (целые числа) Реализовать связный список, тип данных — целые числа. Прикладная задача:

исходный связный список заполняется из входного потока, затем программа меняет местами первую и вторую половины списка.

2.2.2.1.4. Смена местами половин списка (целые числа), Реализовать связный список, тип данных — строки символов. Прикладная задача:

Исходный связный список заполняется из входного потока, затем программа меняет местами первую и вторую половины списка.

2.2.2.1.5. Удаление всех элементов списка (целые числа), больших заданного Реализовать связный список, тип данных — целые числа.

Прикладная задача:

исходный связный список заполняется из входного потока, затем программа удаляет все элементы, большие заданного.

2.2.2.1.6. Удаление всех элементов списка (строки), большие по длине заданной

Реализовать связный список, тип данных — строки. Прикладная задача:

исходный связный список заполняется из входного потока, затем программа удаляет все элементы, большие по длине заданной.

138

2.2.3. Упорядоченный связный список

Выше (рис. 2.7) уже рассматривалась ситуация вставки элемента в упорядоченный список. Реализация такого списка совершенно такая же, как и для неупорядоченного, но вместо функции добавления элемента AddList используем функцию вставки элемента InsList.

Основные функции:

вставить элемент,

просмотреть список.

Вследующем примере иллюстрируется работа с упорядоченным списком — новый элемент вставляется на подходящее место:

#include <stdio.h>

#include <conio.h>

struct LINK{

// определение узла списка

int data;

 

 

LINK * next;

 

 

};

 

 

typedef LINK * LIST;

// определение указателя на узел

LIST IniList ( LIST pt_lst );

// инициация списка

LIST InsList( LIST pt_lst, int new_el );

// вставка элемента

int WriteList( LIST pt_lst );

// просмотр списка

main()

 

 

{

 

 

LIST lst;

// создаем указатель на список

int lst_el;

// целое число — элемент списка

clrscr();

 

 

printf( " Вставляем элементы в список (завершение — 0): \n");

lst = IniList( lst );

139

do

// заполнение списка

 

{

 

 

scanf("%d", &lst_el);

 

lst = InsList(lst, lst_el);

// не возвращает данных об

 

 

// успехе/неуспехе попытки,

 

 

// поскольку указатель на

 

 

// список может быть

 

 

// модифицирован и его нужно

}

 

// вернуть

 

 

while ( lst_el != 0 );

 

printf("\n Вот Ваш список:\n");

 

WriteList( lst );

 

return 0;

 

 

}

 

 

LIST IniList ( LIST pt_lst )

// некорректный способ

 

 

// инициации, надо бы вернуть

{

 

// память в кучу

 

 

pt_lst = NULL;

 

return pt_lst;

 

 

}

 

 

LIST InsList( LIST pt_lst, int new_el )

 

{

 

 

LIST pt_new,

 

 

pt_buf;

 

 

pt_new = new LINK;

// новый узел

pt_new–>data = new_el;

// положим туда данные

if ( (pt_lst == NULL) || ( pt_lst–>data > new_el ) )

//пограничные ситуации — вставлять нужно слева (список пуст или

//новый элемент меньше левого)

{

 

pt_new–>next = pt_lst;

// перекинем указатели

pt_lst = pt_new;

 

}

 

else

 

{

 

140

Соседние файлы в папке Учебники