Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ПЯВУ - методичка2.doc
Скачиваний:
1
Добавлен:
01.04.2025
Размер:
403.97 Кб
Скачать

Работа с динамическими структурами данных Динамическое распределение памяти

Статическими называют такие данные (объекты программы), которые не меняют свои размеры и положение в памяти ЭВМ в течение всего времени своего существования в программе. Компилятор по описанию объектов (переменных, массивов, структур и т.д.) определяет их размер и при компиляции закрепляет за ними выделенный участок статической памяти в сегменте данных.

Во многих задачах часто заранее неизвестно, сколько программа будет хранить объектов данных (чисел, строк текста, структур и т.п.). В таких случаях требуется введение динамических структур данных, размер которых может меняться в процессе работы программы. Сама программа, а не компилятор, должна распределять свободное место в динамической памяти (называемой также “кучей” (heap)) для элементов таких структур и устанавливать связи между ними.

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

Элементы (называемые также узлами) динамической структуры имеют одинаковый тип, описывающий структурную переменную, в которой содержатся информационные поля и поля указателей-переменных для установления связей между узлами. Шаблон узла можно описать схемой:

struct УЗЕЛ { тип переменная; /* поле информации узла */

УЗЕЛ *переменная; /* указатель для связи с другими */

… /* узлами */

} ;

Для работы с динамическими структурами используются следующие типичные функции:

  • поиск нужного элемента;

  • включение нового элемента;

  • удаление существующего элемента;

  • присваивание указателю элемента ссылки (адреса первого байта) на другой элемент, либо значения NULL (отсутствие ссылки).

Для динамического управления памятью, выделяемой под элементы в “куче”, используются функции, подключаемые в программе заголовочным файлом <alloc.h>:

  • void *malloc (nbytes) – выделяет блок памяти размером nbytes байтов и возвращает указатель на первый байт блока.

  • void *calloc (nelem, elsize) – выделяет блок памяти для nelem элементов по elsize байтов каждый (всего nelem * elsize байтов), обнуляет выделенную память и возвращает указатель на нее.

  • void *realloc (*block, size) – делает попытку установить новый размер size блока, на начало которого ссылается указатель block.

  • free (*block) – освобождает блок памяти, на начало которого ссылается указатель block.

К динамическим структурам относятся следующие связанные структуры: очередь, стек, списки, бинарные деревья, работа с которыми рассмотрена ниже.

Очередь

Очередь – это динамическая связанная структура данных, организованная по принципу “первым пришел, первым ушел” или, как говорят, реализующая дисциплину обслуживания FIFO (First In, First Out). Начало очереди может храниться, например, в переменной с именем head (голова очереди). Базовыми операциями для работы с очередью являются:

  • добавление нового элемента в конец очереди;

  • удаление элемента из начала очереди.

Пример. Демонстрация работы с очередью. В программе начало очереди представлено в виде переменной-указателя на указатель (**head), поскольку адрес начала очереди может динамически изменяться. При удалении первого элемента адресом начала очереди становится адрес второго элемента и т.д.

Используются следующие функции для работы с очередью:

  • INSERT () – выделяет память под очередной элемент, заносит в него информацию и ставит в конец очереди.

  • TAKE_OFF () – удаляет из очереди ее первый элемент, освобождает занимаемую им память и перемещает указатель начала очереди на следующий элемент. При попытке удалить элемент из пустой очереди в параметр ошибки error заносится 1.

  • OUT_ELEMENTS () – выводит на экран информацию элемента очереди.

Программа:

#include<stdio.h>

#include<alloc.h> /* функции динамического распределения памяти */

#include<conio.h>

#define QUEUE struct queue /* новое имя шаблона структуры */

QUEUE { /* шаблон элемента очереди */

int info; /* поле информации элемента очереди */

QUEUE *next ; /* поле ссылки на следующий элемент очереди */

} ;

/* Функция включения элемента в очередь: */

void INSERT (QUEUE **head, int item)

{ QUEUE *tek = *head, /* установка текущего указателя на начало */

*pred = NULL, /* обнуление указателя на следующий элемент */

*new; /* указатель на новый элемент */

while (tek) /* цикл просмотра очереди до конца */

{ pred = tek; /* сдвиг указателя предыдущего элемента */

tek = tek->next; /* сдвиг текущего указателя к следующему */

}

new = (QUEUE*)malloc(sizeof(QUEUE)); /* место для нового элемента */

new->info = item; /* заполнение поля информации элемента */

if (pred) /* если очередь не пуста */

{ new->next = NULL; /* за новым элементом ничего нет */

pred->next = new; /* новый элемент включен в очередь */

}

else /* если очередь пуста */

{ *head = new; /* новый элемент во главе очереди */

new->next = NULL; /* за новым элементом ничего нет */

}

}

/* Функция удаления элемента из очереди: */

int TAKE_OFF (QUEUE **head, int *error)

{ int value = 0; /* возвращаемое значение функции */

QUEUE *old = *head; /* указатель на “старую” голову очереди */

if (*head) /* если очередь не пуста  */

{ value = old ->info; /* информация для возвращаемого значения */

*head = old ->next; /* во главе очереди следующий элемент */

free (old); /* удаление первого элемента из очереди */

*error = 0; /* отсутствие ошибки удаления */

}

else *error = 1; /* ошибка удаления из пустой очереди */

return value; /* возвращение значения функции */

}

/* Функция вывода элементов очереди: */

void OUT_ELEMENTS (QUEUE **head)

{ QUEUE *tek = *head; /* текущий указатель в начале очереди */

if ( !tek ) /* если очередь пустая, то сообщение */

puts ("Очередь пустая.");

else /* если очередь не пустая */

{ while (tek) /* цикл пока есть очередной элемент */

{ printf("Элемент %d\n", tek->info); /* вывод информации об элементе */

tek = tek->next; /* переход к следующему элементу */

}

}

}

void main() /* главная функция */

{ int error, i; /* переменные ошибки и цикла  */

QUEUE *q1=NULL, *q2=NULL; /* инициализация указателей на очереди */

clrscr(); /* очистка экрана  */

puts ("Заполнение первой очереди.");

for (i=12; i<=14; i++) /* цикл заполнения первой очереди */

INSERT (&q1, i); /* включение в очередь элемента  */

puts ("Первая очередь:");

OUT_ELEMENTS (&q1); /* вывод элементов очереди */

puts ("Вторая очередь:");

OUT_ELEMENTS (&q2); /* вывод элементов очереди */

puts ("\nЗаполнение второй очереди.");

while (q1) /* цикл пока не пуста 1-я очередь */

INSERT(&q2, TAKE_OFF(&q1, &error)); /* перенос эл-та из1-й во 2-ю очередь */

puts ("Первая очередь:");

OUT_ELEMENTS (&q1); /* вывод элементов очереди */

puts ("Вторая очередь:");

OUT_ELEMENTS (&q2); /* вывод элементов очереди */

puts ("\nУдаление элементов из второй очереди");

while (q2) /* цикл пока не пуста 2-я очередь */

printf ("Удален из q2: %d\n", TAKE_OFF(&q2, &error));

puts ("Вторая очередь:");

OUT_ELEMENTS (&q2); /* вывод элементов очереди */

getch(); /* задержка экрана результатов */

}

Результаты программы:

Заполнение первой очереди

Первая очередь:

Элемент 12

Элемент 13

Элемент 14

Вторая очередь:

Очередь пустая.

Заполнение второй очереди

Первая очередь:

Очередь пустая.

Вторая очередь:

Элемент 12

Элемент 13

Элемент 14

Удаление элементов из второй очереди

Удален из q2: 12

Удален из q2: 13

Удален из q2: 14

Вторая очередь:

Очередь пустая.

Стек

Стек (или магазин) – другая часто встречающаяся динамическая структура данных, которая отличается от очереди тем, что организована по принципу LIFO (Last In, First Out – “последний вошел, первый вышел”). Операция включения и удаления элемента в стеке выполняется только с одного конца, называемого вершиной стека (head).

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

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

Пример. Демонстрация работы со стеком (STACK). В программе вершина стека объявлена как переменная-указатель на указатель (**head), поскольку адрес вершины стека будет меняться при включении и удалении нового верхнего элемента в стек. Для работы со стеком использованы функции:

  • push () – добавить новый элемент на вершину стека;

  • pop () – извлечь (вытолкнуть) элемент из вершины стека;

  • peek () – прочитать значение верхнего элемента, не удаляя его из стека.

Программа:

#include<stdio.h>

#include<alloc.h> /* функции динамического управления памятью */

#include<conio.h>

#define STACK struct stack /* новое имя  шаблона */

STACK { /* шаблон элемента стека  */

int info; /* поле информации элемента стека  */

STACK *next; /* поле ссылки на следующий элемент стека */

};

void push (STACK **head, int item) /* функция добавления элемента */

{ STACK *new; /* указатель нового элемента  */

new=(STACK*) malloc(sizeof(STACK)); /* память для элемента  */

new->info = item; /* заполнение поля информации элемента  */

new->next = *head; /* ссылка нового элемента на старую голову */

*head = new; /* на вершине стека новый элемент */

}

int pop (STACK **head, int *error) /* функция удаления элемента */

{ STACK *old = *head; /* ссылка на “старую” голову стека  */

int info = 0; /* исходное возвращаемое значение */

if (*head) /* если стек не пустой */

{ info = old ->info; /* информация удаляемого элемента*/

*head = old ->next; /* первый в стеке – следующий элемент */

free (old); /* удаление элемента из стека и памяти */

*error = 0; /* элемент удален успешно */

}

else *error=1; /* ошибка удаления элемента из пустого стека  */

return (info); /* возврат значения функции */

}

int peek (STACK **head, int *error) /* функция информации об элементе */

{

if (*head) /* если стек не пустой */

{ *error = 0; /* отсутствие ошибки */

return (*head)->info; /* возврат информации о первом элементе */

}

else /* если стек пустой */

{ *error = 1; /* наличие ошибки */

return 0; /* возврат значения функции */

}

}

void main() /* главная функция */

{ int error, i; /* переменные ошибки и цикла  */

STACK *st1, *st2; /* указатели двух стеков */

clrscr (); /* очистка экрана  */

printf ("Заполнение 1-го стека :\n");

push (&st1, 42); /* запись в стек очередного элемента  */

printf(" peek(st1) = %d\n", peek(&st1, &error)); /* информация об элементе */

push (&st1, 53); /* запись в стек очередного элемента  */

printf(" peek(st1) = %d\n", peek(&st1, &error)); /* информация об элементе */

push (&st1, 72); /* запись в стек очередного элемента  */

printf(" peek(st1) = %d\n", peek(&st1, &error)); /*информации об элементе */

push (&st1, 86); /* запись в стек очередного элемента  */

printf(" peek(st1) = %d", peek(&st1, &error)); /* информация об элементе */

printf (" –элемент на вершине 1-го стека \n");

puts ("Запись элементов 1-го стека в 2-ой и вывод данных 2-го стека:");

for (i=1; i<=4; i++) /* цикл по элементам стека  */

push (&st2, pop(&st1, &error)); /* запись элементов 1-го стека во 2-ой */

for (i=1; i<=4; i++) /* цикл по элементам стека  */

printf(" pop(st2) = %d - элемент %i\n",

pop(&st2, &error), i); /* вывод данных из 2-го стек */

getch(); /* задержка экрана результатов */

}

Результаты программы:

Заполнение 1-го стека:

peek (st1) = 42

peek (st1) = 53

peek (st1) = 72

peek (st1) = 86 - элемент на вершине 1-го стека

Запись элементов 1-го стека в 2-ой и вывод данных 2-го стека:

pop (st2) = 42 – элемент 1

pop (st2) = 53 – элемент 2

pop (st2) = 72 – элемент 3

pop (st2) = 86 – элемент 4

Списки

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

Односвязный (однонаправленный) список, в котором каждый элемент имеет указатель на следующий элемент (next) и поля данных, причем последний элемент имеет значение указателя NULL, можно представить схемой:

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

Указатель на начало списка

next

Данные

next

Данные

next

Данные

Указатель на начало списка

next

Данные

next

Данные

NULL

Данные

Д

Указатель на начало списка

next

Данные

NULL

next

Данные

pred

NULL

Данные

pred

Указатель на конец списка

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

Кольцевой двунаправленный список можно получить, если в предыдущем варианте списка задать в указателе next последнего элемента ссылку на первый элемент, в указателе pred первого элемента – ссылку на последний элемент.

Пример. Демонстрация работы кольцевого двунаправленного списка. В программе можно ввести в произвольном порядке некоторые слова, например, фамилии студентов, которые вставляются в список в алфавитном порядке. Прием слов заканчивается после ввода слова конец. Упорядоченный список выводится на экран.

Программа:

#include<stdio.h>

#include<conio.h>

#include<alloc.h> /* функции динамического выделения памяти */

#include<string.h> /* для функции strcmp() */

#define NAME_SIZE 30 /* максимальная длина слов */

#define LIST struct LIST /* идентификатор структуры элемента списка */

LIST { /* шаблон структуры списка  */

char data [NAME_SIZE]; /* поле данных (для слов) */

LIST *next; /* указатель на следующий элемент  */

LIST *pred; /* указатель на предшественника */

} ;

LIST *insert (LIST *elem, char name[]); /* прототип функции включения */

void main() /* главная программа */

{ char name [NAME_SIZE]; /* массив для слов */

LIST *head; /* указатель на  голову списка */

LIST *tek; /* текущий указатель */

clrscr(); /* очистка экрана  */

head = (LIST*) malloc(sizeof(LIST)); /* блок памяти для головы списка  */

head->pred = head->next = head; /* инициирование указателей головы списка на себя */

puts ("Ввод данных (для окончания введите слово - конец.):");

for ( ; ; ) /* цикл создания связного списка */

{ printf ("Введите слово ( фамилию): ");

gets (name);

if (strcmp (name, "конец") == 0) break; /* конец ввода данных */

if (insert(head, name) == NULL) /*включение нового элемента в список */

{ printf("Не хватает динамической памяти!\n"); /* ошибка распределения*/

exit(1); /*  аварийный выход их программы */

}

}

puts ("\nВывод упорядоченного списка :");

for (tek = head->next; tek != head; tek = tek->next) /* цикл по элементам */

printf ("%s\n", tek->data); /* вывод из поля данных */

puts ("Работа закончена !");

getch(); /* задержка экрана результатов */

}

/* Функция insert () резервирует память для нового элемента списка, копирует введенное в него слово и вставляет новый элемент в список в алфавитном порядке. Возвращает либо указатель на новый элемент, либо NULL, если для создания нового элемента не хватило памяти:

*/

LIST *insert(LIST *head, char name[])

{ LIST *tek, /* текущий указатель */

*new_elem; /* указатель на новый элемент */

/* Просмотр списка, пока не будет обнаружен элемент, поле данных которого имеет значение большее или равное введенному слова (по коду символов) :

*/

for (tek = head->next;

(tek != head) && strcmp(name, tek->data) > 0; tek = tek->next);

/* Зарезервировать память для нового элемента, поместить введенное слово в поле данных и вставить новый элемент перед тем, на который показывает указатель tek:

*/

if ((new_elem = (LIST*)malloc(sizeof(LIST))) != NULL) /* блок памяти для нового элемента*/

{

strncpy (new_elem->data, name, NAME_SIZE); /* копирование слова name в поле data */

new_elem->next = tek; /* указатель next показывает на элемент tek */

new_elem->pred = tek->pred; /* указатель pred нового элемента показывает на предыдущий элемент */

/* Изменить указатель next в элементе перед вставленным (теперь он должен показывать на вставленный элемент), и изменить указатель pred в элементе, следующем за вставленным (он также должен показывать на вставленный элемент):

*/

(new_elem->pred)->next = new_elem; /* предыдущий элемент указывает на новый элемент */

tek->pred = new_elem; /* текущий элемент указывает на новый элемент */

}

return (new_elem); /* возврат указателя на новый элемент */

}

Результаты программы:

Ввод данных (для окончания введите слово – конец).

Введите слово (фамилию): Сидоров

Введите слово (фамилию): Иванов

Введите слово (фамилию): Петров

Введите слово (фамилию): конец

Вывод упорядоченного списка

Иванов

Петров

Сидоров

Работа закончена!