
- •Структуры и алгоритмы обработки данных: план курса
- •Лабораторные работы
- •Литература
- •Ста: Лекция №1 - Введение. Данные в памяти программ
- •Введение
- •Модель памяти в прикладных программах
- •Int main ()
- •Int main ()
- •Int main ()
- •Int main ()
- •Char buf[ 2000000 ];
- •Return f(); // бесконечно долго вызываем сами себя // вызываем переполнение стека через время
- •Int main ()
- •Return f();
- •Int main ()
- •Delete[] p; // Освобождаем память
- •Int main ()
- •Int a[ n ]; // ошибка, размер нельзя вычислить во время компиляции
- •Delete[] p;
- •Int main ()
- •Проблема фиксированного размера массивов
- •Int main ()
- •Int main ()
- •Int main ()
- •Динамически растущие массивы (векторы)
- •Int main ()
- •Struct IntegerVector
- •Int * m_pData;
- •Int m_nUsed;
- •Int m_nAllocated;
- •Void IntegerVectorDestroy ( IntegerVector & _vector )
- •Int main ()
- •#Ifndef _integer_vector_hpp_
- •#Include "integer_vector.Hpp"
- •Void IntegerVectorRead ( IntegerVector & _vector, std::istream & _stream );
- •Ста: Лекция №2 - Связные списки.
- •Всегда ли хорош вектор?
- •Связные списки
- •Ста: Лекция №3 - Реализация и использование простейших атд
- •Абстрактные типы данных (атд)
- •Атд “Список” ( “Последовательность” )
- •Атд “Стек”
- •Атд “Очередь”
- •Ста: Лекция №7 - Деревья
- •Основные сведения о деревьях
- •Обход деревьев
- •Атд “Дерево”
- •Типичные структуры данных для n-арных деревьев
- •1. Массив меток и массив родительских индексов.
- •2. Массив меток и заголовок со списками дочерних узлов.
- •3. Динамическая структура с указателями
- •В результате ее выполнения в динамической памяти формируется структура объектов, в существенной степени напоминающая оригинальный пример из описания понятия деревьев:
- •Бинарные деревья
- •Глава 3 “Элементарные структуры данных”
- •Глава 4 “Абстрактные типы данных”
- •Глава 10 “Элементарные структуры данных” (подразделы 10.1-10.3)
- •Глава 2 “Основные абстрактные типы данных” (подразделы 2.1-2.4)
- •Глава 6 “Элементарные методы сортировки”.
- •Глава 5 “Рекурсия и деревья”.
Глава 5 “Рекурсия и деревья”.
Глава 12 “Таблицы символов и деревья бинарного поиска” (подразделы 12.5-12.9).
Глава 14 “Хэширование”.
Т. Кормен, Ч. Лейзерсон, Р. Ривест, К. Штайн “Алгоритмы. Построение и анализ”, 2 издание:
Глава 11 “Элементарные структуры данных (подраздел 11.4).
Глава 12 “Хэш-таблицы”.
Глава 13 “Двоичные деревья поиска”.
А. Ахо, Д. Хопкрофт, Д. Ульман “Структуры данных и алгоритмы”:
Глава 3 “Деревья”.
Глава 4 “Основные операторы множеств” (подразделы 4.7-4.9)
Глава 5 “Специальные методы представления множеств” (подразделы 5.1-5.3).
Д. Кнут “Искусство программирования”:
Том 1, глава 2, раздел 2.3 “Деревья”.
Том 3, глава 6, раздел 6.2 “Поиск путем сравнения ключей”.
Том 3, глава 6, раздел 6.4 “Хэширование”
Исходные данные
При решении задач лабораторной работы по желанию можно использовать готовые реализации изложенные в материалах лекций. Перечисленные ниже файлы прилагаются к данным методическим указаниям:
Хэш-таблицы (hash_table.hpp, hash_table_open_impl.cpp, hash_table_closed_impl.cpp).
N-арные деревья с фиксированным количеством узлов (tree_static.hpp, tree_parent_indices.cpp, tree_child_index_lists.cpp).
N-арные деревья с динамическим количеством узлов (tree_dynamic.hpp, tree_dynamic.cpp)
Классические бинарные деревья поиска (bstree.hpp, bstree.cpp).
Красно-черные деревья (rbtree.hpp, rbtree.cpp).
Эти реализации можно использовать непосредственно как готовую библиотеку, либо использовать как начальную точку для собственного решения.
Выдавать данные файлы за собственные решения настоятельно не рекомендуется :)
Варианты базового уровня
Вариант №1:
Используя базовую структуру в виде массива меток и массива родительских индексов, реализуйте основные операции АТД “дерево” с метками в виде строк. Реализация должна содержать типовые операции создания, уничтожения, ROOT, NODE_LABEL, PARENT, LEFTMOST_CHILD, RIGHT_SIBLING, а также прямой, обратный и симметричный обход. Строки должны копироваться в дерево при вставке и корректно уничтожаться. Создайте тестовую программу, активизирующую реализацию всех операций.
Вариант №2:
Используя в качестве базы динамическую структуру узлов дерева с указателями на родителя, левый дочерний узел и правый братский узел, реализуйте основные операции АТД “дерево” с метками в виде строк. Реализация должна содержать типовые операции создания, уничтожения, ROOT, NODE_LABEL, PARENT, LEFTMOST_CHILD, RIGHT_SIBLING, а также прямой, обратный и симметричный обход. Строки должны копироваться в дерево при вставке и корректно уничтожаться. Создайте тестовую программу, активизирующую реализацию всех операций.
Вариант №3:
Используя базовую структуру в виде на основе массива меток и списков дочерних узлов, реализуйте основные операции АТД “дерево” с метками в виде строк. Реализация должна содержать типовые операции создания, уничтожения, ROOT, NODE_LABEL, PARENT, LEFTMOST_CHILD, RIGHT_SIBLING, а также прямой, обратный и симметричный обход. Строки должны копироваться в дерево при вставке и корректно уничтожаться. Создайте тестовую программу, активизирующую реализацию всех операций.
Вариант №4:
Пользуясь информацией с сайта cist.kture.kharkov.ua, создайте дерево на основе любой из базовых структур, хранящее информацию об устройстве университета: университет, факультеты, кафедры. Реализуйте прямой обход данного дерева, при котором структура университета распечатывается на консоли в удобном для чтения виде с отступами для каждого из уровней.
Вариант №5:
Реализуйте АТД “множество” для строковых ключей на основе хэш-таблиц с линейным разрешением коллизий (закрытое хэширование). Должны поддерживаться типовые операции создания, уничтожения, очистки, вставки и удаления ключей, определения наличия ключа во множестве. Реализация теоретико-множественных операций не требуется. Строки должны копироваться в дерево при вставке и корректно уничтожаться. Создайте тестовую программу, активизирующую реализацию всех операций.
Вариант №6:
Реализуйте АТД “множество” для строковых ключей на основе хэш-таблиц со связными списками для кластеров (открытое хэширование). Должны поддерживаться типовые операции создания, уничтожения, очистки, вставки и удаления ключей, определения наличия ключа во множестве. Реализация теоретико-множественных операций не требуется. Строки должны копироваться в дерево при вставке и корректно уничтожаться. Создайте тестовую программу, активизирующую реализацию всех операций.
Вариант №7:
Вычисление результата некоторых математических функций, в частности с действительными числами, может занимать существенное время. Часто ход решаемой задачи предполагает интенсивное использование функции на одних и тех же аргументах. В таком случае имеет смысл вычислять значение функции для данного аргумента только один раз, а результат кэшировать, т.е., запоминать в некоторой таблице готовых значений. При повторном запросе вычисления значения функции на одном и том же аргументе вместо тяжеловесного вычисления можно взять готовый подсчитанный при предыдущем вызове результат. Произвольно выберите достаточно сложную функцию, содержащую различные тригонометрические, логарифмические функции в сочетании с различными алгебраическими операциями. Для упрощения, положим, что аргумент-ключ может быть только целым числом. Реализуйте кэширование результатов выбранной функции за счет размещения подсчитанных однажды значений для конкретных аргументов в виде хэш-таблицы фиксированного размера. Ключ – аргумент функции, значение – результат вычисления функции. Продемонстрируйте функциональность при помощи тестовой программы..
Вариант №8:
Напишите программу, которая генерирует 100 тысяч псевдослучайных целых чисел. Поместите одну копию сгенерированных данных в обычный динамический массив, другую - в хэш-таблицу с размером 100 тысяч ячеек, а третью - в хэш-таблицу с размером в 200 тысяч ячеек. При помощи функции clock() из стандартной библиотеки вычислите во сколько раз время поиска произвольного числа отличается между данными тремя структурами. Для достоверности измерения, инициируйте поиск многократно. Приведите теоретическое объяснение полученных экспериментальных результатов. Можно использовать как хэш-таблицы с линейным опробированием, так и со связными списками для сегментов.
Вариант №9:
Воспользовавшись журналом группы, создайте BST в котором в качестве ключей будут выступать фамилии студентов, а в качестве значений - количество защищенных лабораторных работ. Далее в цикле:
если пользователь вводит число 0, программа завершается;
если пользователь вводит число 1, распечатайте список студентов на консоли в алфавитном порядке фамилий;
если пользователь вводит число 2, затем должна быть введена фамилия студента, а программа должна распечатать количество защищенных им лабораторных работ, а если такого студента в дереве нет, напечатать сообщение об ошибке.
Вариант №10:
Напишите программу по простейшему управлению учетными записями пользователей. Если пользователь вводит число 1, программа ожидает ввод двух строк - логин и пароль нового пользователя. Если пользователь вводит число 2, программа также ожидает ввод двух строк - логин и пароль пользователя: если такой логин и пароль уже зарегистрированы, программа выдает на экран приветствующее сообщение (например, “Hello <login>”), если такого логина нет либо пароль неправильный, программа выдает сообщение об ошибке. Если пользователь вводит число 3, программа завершается. При следующем запуске пароли не сохраняются. Важным требованием является отказ от хранения паролей в памяти программы в явном виде, следует сохранить результат хэш-функции на строке-пароле, а не сам пароль. Реализация хранения учетных записей должна быть основана на хэш-таблица любого вида.
Варианты углубленного уровня
Вариант №11:
Имеется произвольный текстовый файл. Сгенерируйте другой текстовый файл, содержащий уникальные неповторяющиеся строки из первого файла в отсортированном по алфавиту порядке. Имена файлов передаются в программу через командную строку. Предполагается использование деревьев бинарного поиска. В отчете следует привести замеры производительности программы на двух больших текстовых файлах:
Большой файл с произвольным осмысленным текстом
Большой файл уже отсортированный по алфавиту (можно взять результат работы программы на первом варианте).
Объясните причину диспропорции времени выполнения на первом файле и втором файле.
Вариант №12:
Имеется текстовый файл, содержащих оценки студентов по предметам со следующим форматом:
Petrov 90 85 82
Ivanov 75 90 82
Sidorov 60 60 60
Используя деревья бинарного поиска, выведите в другие текстовые файлы те же самые данные в порядке убывания оценки студентов по каждому из предметов. Очевидно, у двух и более студентов допускается одинаковая оценка по одному и тому же предмету. В таком случае, следует выводить студентов в алфавитном порядке их фамилий. Имена всех файлов передаются программе через командную строку. Например, если вышеуказанные данные находятся в input.txt, то запуск программы со следующими аргументами:
myprogram.exe input.txt output1.txt output2.txt output3.txt
должен вывести в output1.txt:
Petrov 90
Ivanov 75
Sidorov 60
в output2.txt:
Ivanov 90
Petrov 85
Sidorov 60
в output3.txt:
Ivanov 82
Petrov 82
Sidorov 60
В целях упрощения, предположите конкретное число предметов, и факт наличия в исходном файле оценок по всем предметам для каждого студента. Если передано меньше выходных файлов, чем оценок в исходном файле, то не обрабатывайте остальные оценки. Если передано больше выходных файлов, чем оценок в исходном файле, выдайте предупреждение на консоли и сгенерируйте меньшее число файлов – по количеству оценок в исходном файле.
Вариант №13:
В любом языке программирования имеется набор ключевых слов. В языке C зарезервированы следующие ключевые слова:
auto break case char const continue default
do double else enum extern float for
goto if inline int long register restrict
return short signed sizeof static struct switch
typedef union unsigned void volatile while
Язык С++ в стандарте 1998г. дополняет эти ключевые слова следующими:
bool catch class const_cast delete dynamic_cast explicit
export false friend inline mutable namespace new
operator private protected public reinterpret_cast static_cast template
this throw true try typeid typename using
virtual wchar_t
Напишите программу, которая считывает слова (словом считается последовательность букв, цифр и подчеркиваний до любого разделителя) из входного файла, имя которого передается через командную строку. Все остальные символы программа должна игнорировать. Для каждого извлеченного слова программа определяет является ли оно ключевым для языков C или C++. По завершению ввода выводит следующую статистику:
Сколько обнаружено использований ключевых слов, относящихся только к языку C++.
Сколько обнаружено ключевых слов, относящихся как к С, так и к С++.
Сколько из прочитанных слов не являются ключевыми ни для C, ни для C++.
В качестве тестового файла удобно использовать исходный код самой программы.
Вариант №14:
Реализовать структуру данных, моделирующую справочник телефонов и адресов (на основе деревьев бинарного поиска либо хэш-таблиц любого типа, на усмотрение студентов). В качестве ключа должна быть строка - ФИО физического лица, в качестве хранимого значения – структура, содержащая соответствующие номер телефона и адрес. Исходные данные следует прочитать из файла в произвольном формате (можно как угодно изменить данный формат для упрощения реализации считывания), например:
John Smith: +1-254-705-10-58: USA, New York, Wall str. 10, ap. 125
Nickolay Ivanov: +7-095-283-11-90: Russia, Moscow, Tverskaya str. 12, ap. 62.
Semen Kravchuk: +38-044-205-12-40: Ukraine, Kiev, Bogdana Hmelnickogo str. 115, ap. 23.
Tomasz Kowalski: +48-032-44-27-13: Poland, Katowice, Chorzowska str. 50, ap. 105.
Тестовая программа должна иметь следующий интерфейс командной строки:
Первый аргумент командной сроки является обязательным и задает имя файла, из которого нужно прочитать исходные данные. Если указан несуществующий файл и содержимое справочника будет меняться по ходу теста, то этот файл нужно будет автоматически создать.
Если в качестве второго аргумента командной строки указан ключ “-add”, то третий, четвертый и пятый аргументы командной строки задают ФИО человека, номер телефона и адрес соответственно, запись о котором следует добавить в словарь. Никакие другие дополнительные аргументы не допускаются. После завершения программы запись о новом человеке должна появиться в исходном файле.
Если в качестве второго аргумента командной строки указать ключ “-remove”, то третий аргумент должен задавать ФИО человека, запись о котором следует удалить из словаря. Никакие другие дополнительные аргументы не допускаются. После завершения программы запись о таком человеке не должна присутствовать в исходном файле. Если ФИО такого человека не найдено, следует напечатать на экране сообщение об ошибке.
Если в качестве второго аргумента командной строки ни один из перечисленных выше ключей не указан, второй и последующие аргументы командной строки задают ФИО, телефон и адрес которых необходимо вывести на экран.
Вариант №15:
Напишите программу, которая анализирует большой текстовый файл, содержащий повторяющиеся строки, и создает другой текстовый файл, в котором исключены повторения. Допускается, что порядок строк в результирующем файле произвольный. Важной считается лишь уникальность скопированных в результирующий файл строк.
Простая реализация с линейным поиском может обладать крайне непривлекательными параметрами производительности. Поскольку строк может быть неограниченное количество, установление факта уникальности можно реализовать на основе хэш-таблицы. Можно воспользоваться и бинарными деревьями поиска, на усмотрение студентов.
Программа должна поддерживать следующий интерфейс командной строки:
Первый аргумент – имя входного текстового файла для анализа.
Второй аргумент – имя файла для результатов.
Третий аргумент (необязательный) - равен “-i” - означает игнорирование регистра символов при сравнении.
Варианты амбициозного уровня
Вариант №16:
Реализуйте структуру-множество целых чисел на основе 2-3 дерева. Для упрощения задачи разрешается опустить операцию удаления узлов. 2-3 дерево – дерево с двумя типами узлов:
листья, хранящие конкретные значения;
внутренние узлы, содержащие 3 связи с дочерними узлами и минимальные значения, хранящиеся во втором и третьем поддереве.
Внутренние узлы могут содержать как 2, так и 3 поддерева. Если остается только 1 поддерево, узел уничтожается и заменяется поддеревом. Если требуется 4 поддерева, узел разрывается пополам и вводится еще один уровень глубины.
Проведите измерения производительности операции вставки и максимальной высоты в создаваемом дереве на достаточно больших множествах для:
последовательности элементов в случайном порядке;
последовательности элементов, отсортированной по возрастанию;
последовательности элементов, отсортированной по убыванию.
Подробнее о данной структуре можно прочесть по следующей ссылке:
Р. Седжвик, глава 13 “Сбалансированные деревья”, раздел 13.3 “Нисходящие 2-3-4 деревья”
Вариант №17:
Реализуйте структуру и основные операции автоматически балансирующегося BST на основе алгоритма, предполагающего рандомизированное вращение узлов дерева при вставке. Проведите измерения производительности операции вставки и максимальной высоты в создаваемом дереве на достаточно больших множествах для:
последовательности элементов в случайном порядке;
последовательности элементов, отсортированной по возрастанию;
последовательности элементов, отсортированной по убыванию.
Подробнее о данной структуре можно прочитать по следующей ссылке:
Р. Седжвик, глава 13 “Сбалансированные деревья”, раздел 13.1 “Рандомизированные BST-деревья”.
Вариант №18:
Разреженная матрица представляет собой матрицу со специальным способом хранения, ориентированным на экономию памяти. Ключевая идея разреженной матрицы состоит в том, чтобы хранить значения только тех коэффициентов, которые отличны от 0. Хранение можно реализовать при помощи хэш-таблицы, у которой в качестве ключа выступает пара индексов матрицы (номер столбца и номер строки), а в качестве значения – хранимый коэффициент. В качестве хэш-функции подойдет функция, складывающая номер строки с номером столбца, умноженным на количество строк. Необходимо реализовать следующие операции и проверить их работу при помощи достаточно полной тестовой программы:
Функцию создания матрицы, принимающую число строк и чисто столбцов. Изначально матрица должна предполагать, что все хранимые коэффициенты равны 0.
Функцию уничтожения матрицы.
Функции доступа и модификации конкретной ячейки матрицы по индексам.
Функцию печати матрицы в поток – на стандартный поток вывода по умолчанию.
Функции сложения, вычитания и умножения матриц.
Функцию транспонирования матрицы.
Контрольные вопросы
Понятие хэш-функции. Основные требования к хэш-функциям. Понятие коллизии.
Разрешение коллизий в хэш-таблицах в случае открытого хэширования.
Разрешение коллизий в хэш-таблицах в случае закрытого хэширования.
АТД “Дерево”. Виды узлов в дереве. Высота дерева. Полное и неполное дерево. Арность дерева. Обход деревьев.
АТД “Дерево”: описание реализации на основе массива индексов родительских узлов.
АТД “Дерево”: описание реализации на основе списков дочерних узлов.
АТД “Дерево”: описание реализации на основе динамической структуры узлов.
Бинарное дерево поиска. Характеристическое свойство. Вычислительная сложность всех операций. Сбалансированное и несбалансированное дерево.
Операция вращения узлов в BST. Свойства, принцип реализации.
Основные принципы организации красно-черных BST-деревьев. Дополнительные свойства-ограничения. Примеры корректирующих действий.
Пример решения задачи базового уровня
Задача: Пользователь вводит в программу последовательность целых чисел, завершая ввод нажатием <Ctrl+Z>. Числа могут повторяться. Программа собирает вводимые данные в память, а в конце ввода в произвольном порядке выдает набор из пар чисел на отдельных строках - первое из которых является одним из введенных чисел без повторений, а второе - количество его появлений во вводе. Например, пользователь вводит такую последовательность :
1 2 3 4 5 1 3 5 6 1 <Ctrl+Z>
На что программа выдает следующий результат обработки (строки могут быть в любом порядке):
1 3
2 1
3 2
4 1
5 2
6 1
Реализация на основе хэш-таблиц:
Для реализации задачи требуется реализовать функцию обхода хранящихся ячеек в хэш-таблице. В заголовочный файл с хэш-таблицей (hash_table.hpp) следует добавить такое объявление для функции обхода, которая будет отправлять пару ключ-значение функции обратного вызова:
typedef void ( * HashTableWalkFunction ) ( int _key, int _value );
void HashTableWalk ( const HashTable & _ht, HashTableWalkFunction _f );
Можно реализовать такую функцию для таблицы с закрытым хэшированием. Для этого нужно в файл реализации hash_table_closed_impl.cpp добавить тело функции обхода:
void HashTableWalk ( const HashTable & _ht, HashTableWalkFunction _f )
{
// Находим ячейки, имеющие статус OCCUPIED, и вызываем функцию пользователя
for ( int i = 0; i < _ht.m_tableSize; i++ )
if ( _ht.m_pData[ i ].m_status == HashTable::Element::OCCUPIED )
( * _f )( _ht.m_pData[ i ].m_key, _ht.m_pData[ i ].m_value );
}
В качестве альтернативы, такую же функции можно добавить в таблицу с открытым хэшированием. Для этого файл реализации hash_table_open_impl.cpp следует расширить:
void HashTableWalk ( const HashTable & _ht, HashTableWalkFunction _f )
{
// Находим ячейки, имеющие списки
for ( int i = 0; i < _ht.m_tableSize; i++ )
if ( _ht.m_pData[ i ] )
{
// Обходим все элементы списка
const HashTable::Element * element = _ht.m_pData[ i ];
while ( element )
{
( * _f )( element->m_key, element->m_value );
element = element->m_pNext;
}
}
}
Наконец, составим основную часть программы в соответствии с условием:
#include "hash_table.hpp"
#include <iostream>
// Функция реакции на обход таблицы - печатает пару ключ-значение
void printKeyValue ( int _key, int _value )
{
std::cout << _key << ' ' << _value << std::endl;
}
// Основная программа
int main ()
{
// Создаем хэш-таблицу
HashTable * pHT = HashTableCreate( 10 );
// Цикл ввода данных
while ( true )
{
// Вводим очередное число
int value;
std::cin >> value;
// Прекращаем ввод при нажатии Ctrl+Z или в случае ошибки
if ( std::cin )
{
// Был ли данный ключ уже размещен в таблице ранее?
int count = HashTableGet( * pHT, value );
if ( count == -1 )
// Первое вхождение ключа
HashTableInsert( * pHT, value, 1 );
else
// Очередное вхождение ранее наблюдавшегося ключа
HashTableInsert( * pHT, value, count + 1 );
}
else
break;
}
// Инициируем обход таблицы с распечаткой пар ключ-значение
HashTableWalk( * pHT, & printKeyValue );
// Уничтожаем таблицу
HashTableDestroy( pHT );
}
Результат выполнения программы: