Файл: Отчет по лабораторной работе. Отчет по работе включает следующие пункты.pdf
Добавлен: 12.12.2023
Просмотров: 54
Скачиваний: 3
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Министерство общего и профессионального образования
Российской Федерации
Новосибирский Государственный Технический Университет СТРУКТУРЫ ДАННЫХ И АЛГОРИТМЫ
Методические указания к лабораторным работам для студентов
1 курса ФПМиИ
( направление 510200 – прикладная математика и информатика) дневного отделения
Новосибирск 2002
Составитель В.П. Хиценко, ст. преп.
Рецензент :
Работа подготовлена на кафедре прикладной математики
ВВЕДЕНИЕ Цели лабораторных работ
- формирование практических навыков организации и использования при решении задач динамических структур данных
- изучение наиболее распространенных алгоритмов решения задач с использованием сложных структур данных. Содержание работ одинаково для всех лабораторных работ. Язык программирования – язык Си/Си++.
Порядок выполнения работы
1. Изучить необходимый материал по теме лабораторной работы, руководствуясь методическими указаниями к теме.
2. Получить допуск к работе, ответив на контрольные вопросы.
3. Разработать структуру данных для представления основных исходных данных и/или результатов для выданного преподавателем варианта задания.
4. Разработать алгоритм и написать программу решения задачи.
5. Подготовить набор тестов и отладить программу.
6. Оформить отчет по лабораторной работе. Отчет по работе включает следующие пункты
1. Условие задачи
2. Анализ задачи
3. Структура основных входных и выходных данных
4. Алгоритм решения задачи
5. Текст программы
6. Набор тестов
7. Результаты отладки и их анализ
7. Защитить лабораторную работу. Защита состоит в обсуждении выбранной структуры данных и алгоритма решения задачи, ответе на контрольные вопросы, решении контрольных задач.
ЛАБОРАТОРНАЯ РАБОТА ЛИНЕЙНЫЕ ОДНОНАПРАВЛЕННЫЕ СПИСКИ
Цель работы изучить тип указатель получить навыки в организации и обработке однонаправленных списков.
1. Методические указания
1.1. Тип указатель Значением типа указатель является адрес участка памяти, выделенного для объекта конкретного типа (ссылка на объект. Связь указателя P с объектом можно изобразить следующим образом
P
ссылка объект
По указателю осуществляется обращение (доступ) к объекту.
Определение переменной типа указатель type *имя_указателя ; где type – обозначение типа, на который будет указывать переменная с именем идентификатором) имя_указателя. Символ ‘*’ - это унарная операция раскрытия ссылки (операция разыменования, операция обращения по адресу, операция доступа по адресу, нов данном контексте служит признаком типа указатель.
При определении переменной-указателя можно выполнить инициализацию type *имя_указателя инициализатор ;
Инициализатор имеет две формы записи, поэтому допустимо type *имя_указателя = инициализирующее выражение ; type *имя_указателя ( инициализирующее выражение ) ; Инициализирующее выражение – это константное выражение, которое может быть задано указателем, уже имеющим значение или выражением, позволяющим получить адрес объекта с помощью операции & - получение адреса операнда. Пример 1: char cc = ‘f’, *p; Определение символьной переменной сс и неинициализи-
// рованного указателя на объект типа char int *p1, *p2; Определение двух неинициализированных указателей
// p1 и p2 на объекты типа int с *pc = &cc; Инициализированный указатель на объект типа char char *ptr (NULL); Нулевой указатель на объект типа char int *mas [10]; Массив из 10 указателей на объекты типа int int (*point) [10]; Указатель на массив из 10 элементов типа int struct list *n, *pr; Два указателя n и pr на объекты именованного структурного
// типа list. Определение типа list должно либо
// предшествовать данному определению, либо находиться в
// пределах области действия данного определения struct list Определение переменной line структурного
{ типа list, один из элементов которой типа char b; int *k; указатель на объект типа int
} line; int **pp = &p1; Указатель pp – это указатель на указатель. Он инициирован
значением адреса указателя p1 на объект целого типа. Это
// допустимо, так как указатель – это тоже объект в памяти
Переменной типа указатель (в дальнейшем просто указатель) можно задать значение
- присваивая ей адрес объекта с помощью операции & (тип объекта должен быть тот же, что и тип объекта, на который ссылается указатель
- присваивая ей значение другой переменной или выражения типа указатель типы объектов, на которые они ссылаются должны быть одинаковыми
- с помощью операции new.
Над указателями допускается выполнение следующих операций
По указателю осуществляется обращение (доступ) к объекту.
Определение переменной типа указатель type *имя_указателя ; где type – обозначение типа, на который будет указывать переменная с именем идентификатором) имя_указателя. Символ ‘*’ - это унарная операция раскрытия ссылки (операция разыменования, операция обращения по адресу, операция доступа по адресу, нов данном контексте служит признаком типа указатель.
При определении переменной-указателя можно выполнить инициализацию type *имя_указателя инициализатор ;
Инициализатор имеет две формы записи, поэтому допустимо type *имя_указателя = инициализирующее выражение ; type *имя_указателя ( инициализирующее выражение ) ; Инициализирующее выражение – это константное выражение, которое может быть задано указателем, уже имеющим значение или выражением, позволяющим получить адрес объекта с помощью операции & - получение адреса операнда. Пример 1: char cc = ‘f’, *p; Определение символьной переменной сс и неинициализи-
// рованного указателя на объект типа char int *p1, *p2; Определение двух неинициализированных указателей
// p1 и p2 на объекты типа int с *pc = &cc; Инициализированный указатель на объект типа char char *ptr (NULL); Нулевой указатель на объект типа char int *mas [10]; Массив из 10 указателей на объекты типа int int (*point) [10]; Указатель на массив из 10 элементов типа int struct list *n, *pr; Два указателя n и pr на объекты именованного структурного
// типа list. Определение типа list должно либо
// предшествовать данному определению, либо находиться в
// пределах области действия данного определения struct list Определение переменной line структурного
{ типа list, один из элементов которой типа char b; int *k; указатель на объект типа int
} line; int **pp = &p1; Указатель pp – это указатель на указатель. Он инициирован
значением адреса указателя p1 на объект целого типа. Это
// допустимо, так как указатель – это тоже объект в памяти
Переменной типа указатель (в дальнейшем просто указатель) можно задать значение
- присваивая ей адрес объекта с помощью операции & (тип объекта должен быть тот же, что и тип объекта, на который ссылается указатель
- присваивая ей значение другой переменной или выражения типа указатель типы объектов, на которые они ссылаются должны быть одинаковыми
- с помощью операции new.
Над указателями допускается выполнение следующих операций
- присваивание
- получение адреса
- сложение и вычитание
- инкремент и декремент
- операции отношения. Все операции применимы к указателям на объекты одного типа и к указателю и целой константе, в противном случае требуется явное преобразование типов. Операция сложения допустима только для указателя и целочисленного значения. Вычитая два указателя одного типа, можно получить расстояние между двумя участками памяти. Расстояние определяется в единицах, кратных размеру памяти объекта того типа, к которому отнесен указатель.
С помощью переменной типа указатель и операции разыменования * можно обращаться к объекту, на который она ссылается.
Пример 2 ( с учетом описаний в примере 1 ) : int a = 10, b = 20, c = 30; int x[3][10]; ptr = pc; Указатели ptr и pc ссылаются на одну и туже переменную
// cc типа char
*ptr = ‘+’; Объект, на который указывают ptr и pc (те. сс), получил
// новое значение p1 = &a; Указатель p1 получил значение адреса целой переменной а p2 = &b; Указатель p2 – значение адреса целой переменной b mas[0] = p1; Элемент массива mas[0] типа указатель получил тоже значение,
что и указатель p1 mas[1] = &b; Указатель mas[1] получил значение адреса b
*mas[0] = 15; Переменная типа int, на которую указывают и p1 и mas[0],
те. переменная a получила новое значение
*p2 = 25; Переменная b получила новое значение point = &x[0]; Указатель point получил значение адреса нулевого элемента
массива x, те. адреса элемента x[0][0] point[0] = a; Элементу массива с индексом 0, на который указывает point
присваивается значение переменной a
*(point + 1) = b; Элементу массива, на который указывает point + 1,
те. элементу x[0][1] присваивается значение b n = &line; Указателю n присвоен адрес структуры line pr = n; Указатели n и pr ссылаются на одну переменную
(*n).b = *ptr; Элементу b структуры, на которую ссылается n, присваивается
значение объекта, на который ссылается ptr,
те. line.b получила значение ‘+’ pr -> k = p1; Элементу k структуры, на которую ссылается pr присваивается
значение указателя p1, те. line.k получила значение адреса
целой переменной a
1.2. Понятие динамической структуры данных
Описывая в программе с помощью определений структуру данных, мы создаем так называемые статические объекты или заранее определяемые объекты. Статическая структура данных характеризуется тем что
- она имеет имя, которое используется для обращения к ней
- ей выделяется память в процессе трансляции программы
- количество элементов сложной структуры (размерность) фиксировано при ее описании
- размерность структуры не может быть изменена вовремя выполнения программы. Динамическая структура данных характеризуется тем что
- она не имеет имени
- ей выделяется память в процессе выполнения программы
- количество элементов структуры может не фиксироваться
- размерность структуры может меняться в процессе выполнения программы
- в процессе выполнения программы может меняться характер взаимосвязи между элементами структуры.
Каждой динамической структуре данных сопоставляется статическая переменная типа указатель (ее значение - адрес этого объекта) посредством которой осуществляется доступ к динамической структуре. Для создания динамического объекта используется унарная операция new: new имя_типа или new имя_типа инициализатор Операция new позволяет выделить свободный участок в основной памяти динамической, размеры которого соответствуют типу данных, определяемому именем типа. В случае успешного выполнения операция new возвращает адрес начала выделенного участка памяти, таким образом, создан (порожден) динамический объект. Если участок нужного размера не может быть выделен (нет свободной памяти нужного размера, то операция new возвращает нулевое значение адреса (NULL). В случае инициализации в выделенный участок заносится значение, определяемое инициализатором, те. динамическому объекту присваивается начальное значение, иначе значение динамического объекта не определено.
Пример 3: int *p1, *p2; char *ptr (NULL); int *mas [10]; int (*point) [10]; struct list *n, *pr; struct list
{ char b; int *k; } p1 = new int; Создан динамический объект типа int, адрес
// которого присвоен указателю p1
*p1 = 25; Динамический объект, на который указывает p1
получил значение 25 p2 = new int(15); Создана динамическая переменная целого типа с начальным
значением 15, указателю p2 присвоен ее адрес mas[0] = new int; Создана динамическая переменная целого типа,
// указателю mas[0] присвоен ее адрес mas[1] = p1; Указатели mas[1], p1 ссылаются на один объект ptr = new char(‘*’); Создан динамический объект типа char, адрес которого
присвоен указателю ptr, объект инициирован символьным
значением ‘*’ point = new int[10]; //Cоздан динамический объект типа массив из 10 целых
элементов, указателю point присвоен адрес первого элемента
этого массива n= new list; Создан динамический объект типа структура,
указателю n присвоен его адрес pr = n; Указатели pr и n ссылаются на объект типа list n->b = *p1; Элементу с именем b динамической структуры, на которую
указывает n присвоено значение динамического объекта,
на который указывает p1, те. значение 25 n->k = p2; Элементу с именем k динамической структуры, на которую
указывает n присвоено значение указателя p2, те. адрес
динамического объекта целого типа со значением 15
*(n->k) ++; Значение динамического объекта целого типа, на который
ссылается элемент с именем k динамической структуры,
на которую ссылается указатель n, увеличено нате. стало
равно 16 (на этот объект ссылается также указатель p2)
Продолжительность существования динамического объекта – от точки его создания операцией new до конца программы или до явного его уничтожения.
Уничтожение динамического объекта ( освобождение памяти, выделенной под динамическую переменную ) осуществляется операцией delete: delete имя_указателя ; Здесь указатель адресует освобождаемый участок памяти, ранее выделенный с помощью операции new. После выполнения операции delete значение указателя становится неопределенным (хотя в некоторых реализациях языка может и не меняться.
Для освобождения памяти, выделенной для массива, используется следующая модификация этой операции delete [ ] имя_указателя ;
Уничтожая динамический объект не оставляйте “ висячих ” ссылок. “ Висячая ” ссылка - это ссылка на несуществующий объект.
Над динамическими объектами выполняются операции допустимые для типа данного динамического объекта.
Пример 4: int *a, *b ;
a = new int ; b = new int ;
*a = 15 ; *b = -1 ; a = b ; delete b ; Результаты этих действий можно продемонстрировать следующим образом a = new int ; a b b = new int ;
*a =15; *b = -1; a b a = b ; a b Динамический объект со значением 15 существует, но к нему нет доступа. Этот недоступный объект называется мусором. Создавая мусор, вы уменьшаете объем свободной динамической памяти. Указатели a и b ссылаются на один объект. delete b ; a b Память, занимаемая динамическим объектом, на который указывают a и b освобождена, но указатель a сохранил значение адреса объекта. Указатель a – висячая ссылка.
1.3. Линейный однонаправленный ( односвязный ) список
Список – это множество (возможно пустое) данных, имеющих некоторый общий смысл для решаемой задачи. Статический список – это множество данных, которые располагаются в списке последовательно и непрерывно друг за другом. На языке Си статический список можно представить неоднородной структурой struct list
{ int n; type elem [ k ]; } Здесь элемент с именем n определяет фактическое количество данных (элементов) в списке, если n равно 0, то список пуст, если n равно k, то список полон элемент с именем elem (в данном случае массив) определяет само множество элементов списка, type – тип элемента списка.
Динамический список – это множество данных, связанных между собой указателями.
В линейном списке у каждого, составляющего его данного (элемента списка) есть один предшествующий и один следующий элемент (это справедливо для всех элементов кроме первого и последнего.
Линейный односвязный список – это динамический список, каждый элемент которого состоит из двух полей. Одно поле содержит информацию (или ссылку на нее, другое поле содержит ссылку наследующий элемент списка. Элемент списка называют звено списка. Таким образом, список – это цепочка связанных между собой звеньев от первого до последнего.
Пример 5 ( строку символов BETA представим в виде списка ):
15
-1 15
-1 15
B
A NULL
T
E
*a = 15 ; *b = -1 ; a = b ; delete b ; Результаты этих действий можно продемонстрировать следующим образом a = new int ; a b b = new int ;
*a =15; *b = -1; a b a = b ; a b Динамический объект со значением 15 существует, но к нему нет доступа. Этот недоступный объект называется мусором. Создавая мусор, вы уменьшаете объем свободной динамической памяти. Указатели a и b ссылаются на один объект. delete b ; a b Память, занимаемая динамическим объектом, на который указывают a и b освобождена, но указатель a сохранил значение адреса объекта. Указатель a – висячая ссылка.
1.3. Линейный однонаправленный ( односвязный ) список
Список – это множество (возможно пустое) данных, имеющих некоторый общий смысл для решаемой задачи. Статический список – это множество данных, которые располагаются в списке последовательно и непрерывно друг за другом. На языке Си статический список можно представить неоднородной структурой struct list
{ int n; type elem [ k ]; } Здесь элемент с именем n определяет фактическое количество данных (элементов) в списке, если n равно 0, то список пуст, если n равно k, то список полон элемент с именем elem (в данном случае массив) определяет само множество элементов списка, type – тип элемента списка.
Динамический список – это множество данных, связанных между собой указателями.
В линейном списке у каждого, составляющего его данного (элемента списка) есть один предшествующий и один следующий элемент (это справедливо для всех элементов кроме первого и последнего.
Линейный односвязный список – это динамический список, каждый элемент которого состоит из двух полей. Одно поле содержит информацию (или ссылку на нее, другое поле содержит ссылку наследующий элемент списка. Элемент списка называют звено списка. Таким образом, список – это цепочка связанных между собой звеньев от первого до последнего.
Пример 5 ( строку символов BETA представим в виде списка ):
15
-1 15
-1 15
B
A NULL
T
E
Рис.
Последнее звено ( рис) не ссылается наследующий элемент, поэтому поле ссылки имеет значение NULL - пустой указатель.
Рис. Последнее звено ссылается на первый элемент списка ( рис) - это циклический список.
Заглавное звено
Рис. Список имеет заглавное звено ( рис, которое ссылается на первый элемент списка. Его информационное поле, как правило, не используется. Заглавное звено позволяет обрабатывать первое звено списка также как и другие его звенья.
По однонаправленному списку можно двигаться только водном направлении – от первого (или заглавного) звена к последнему.
Чтобы задать динамический список надо описать его звено. Так как звено состоит из полей разных типов, то описать его можно неоднородным типом – структурой.
Задание типа элемента списка struct list
{ list *next ; type elem ; }
Здесь type – тип информационного поля элемента списка, поле next – ссылка на аналогичную структуру типа list. Для примера 5 элемент списка может быть определен struct list
{ list *next ; char elem ; }
Чтобы работать со списком как с единым объектом, надо ввести в употребление статическую переменную-указатель, значение которой – это адрес первого (или заглавного) звена списка. Если список пустой, она должна иметь значение NULL.
Определение статической переменной-указатель: list *headlist ; headlist
Рис.
B
A
T
E
B
A
NULL
T
E
B
A
NULL
T
E
Статическая переменная-указатель headlist, называемая заголовком (или головой) списка (не путать с заглавным звеном) определяет список как единый объект. Используя указатель, можно обращаться к элементам списка (для списка на рис headlist . . . указатель на заглавное звено списка headlist->next . . . указатель на первое звено списка headlist->next->next . . . указатель на второе звено списка headlist->next->next->next . . . указатель на третье звено списка headlist->next->elem . . . обращение к информационному
полю первого элемента списка Для более удобной работы со списком лучше ввести указатель, который ссылается на текущий элемент списка.
Пример 6. Формирование списка из примера 5 (не циклического, c заглавным звеном void main ( ) struct list
{ list *next ; char elem ; } *ph; list *p; char ch;
{ ph = new list; Создание заглавного звена, ph получил значение адреса ph->next = NULL; заглавного звена, его полю next присвоено значение
пустой ссылки, таким образом создан пустой список
с заглавным звеном p = ph; текущему указателю p присвоена ссылка на заглавное звено while ( (ch = getchar ( )) != ‘\n’ )
{ p->next = new list; Создание очередного звена, поле next текущего звена
получило значение адреса вновь созданного звена p = p->next; текущему указателю p присвоена ссылка на
очередное звено p->elem = ch; } информационное поле elem текущего звена
получило значение символа ch p->next = NULL; } //Ccылочному полю наследующий элемент списка текущего
последнего сформированного) звена присвоено значение
пустой ссылки, таким образом определен конец списка Пример 7. Фрагменты программ, выполняющих различные действия с односвязным списком struct list
{ list *next ; char elem ; } *ph; Считаем, что ph – голова списка list *p, pr; for (p = ph; p != NULL; p = p->next) . . . просмотр всех элементов списка for (p = ph, pr = NULL; p != NULL; pr = p, p = p->next) . . .
просмотр списка с сохранением указателя
на предыдущий элемент списка for (p = ph; p->next != NULL; p = p->next) . . .
переход к последнему элементу непустого списка if (ph != NULL) for (p = ph; p->next != NULL; p = p->next) . . .
просмотр списка (возможно пустого) с
сохранением указателя на предыдущий элемент
Основными операциями обработки списка являются
1) поиск заданного элемента по его значению или порядковому номеру операция заканчивается, когда элемент найден или просмотрен весь список если элемента нет результат операции должен определять, есть ли элемент в списке или нет и, если есть, то возможно его адрес или значение
2) включение ( вставка ) в список нового элемента перед или после заданного элемента ( в том числе перед первым элементом или после последнего ); включению, как правило, предшествует поиск элемента после и/или перед которым происходит включение при включении элемента перед первым в список без заглавного звена меняется заголовок списка при включении после некоторого элемента меняется ссылочное поле у элемента, после которого происходит включение, поэтому надо определять ссылку на элемент после которого происходит включение
3) исключение ( удаление ) заданного элемента из списка ( в том числе удаление первого элемента или последнего ); исключению, как правило, предшествует поиск, исключаемого элемента результатом поиска должна быть ссылка на элемент предшествующий исключаемому, так как при удалении элемента из списка меняется ссылочное поле у элемента, предшествующего удаляемому при удалении первого элемента в списке без заглавного звена меняется заголовок списка
4) определение числа звеньев списка
5) упорядочение элементов списка по значению информационного поля.