МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «Мурманский государственный гуманитарный университет» (ФГБОУ ВПО «МГГУ») УЧЕБНО-МЕТОДИЧЕСКИЙ КОМПЛЕКС ДИСЦИПЛИНЫ ДС. 8 АЛГОРИТМИЗАЦИЯ И ТИПЫ ДАННЫХ Основная образовательная программа подготовки специалиста по специальности 010200 «Прикладная математика и информатика» Утверждено на заседании кафедры математики и математических методов в экономике факультета физико-математического образования, информатики и программирования (протокол № 6 от 27 февраля 2013 г.) Зав. кафедрой _______________О.М. Мартынов 1.1 Автор программы: кандидат технических наук, доцент Ланина Н.Р. 1.2 Рецензент: доктор физико-математических наук, профессор Маренич Е.Е. 1.3 Пояснительная записка: Целью изучения курса «Типы данных» является подготовка студентов на уровне, необходимом и достаточном для: усвоения материала специальных дисциплин; практической работы по специальности; формирования умения исследовать математическую задачу, выбрать для её решения необходимую структуру данных и программно реализовать алгоритм решения этой задачи. Основными задачами изучения данной дисциплины являются: изучение применяемых в программировании математических методов, позволяющих разрабатывать эффективные алгоритмы; изучение структур данных, их спецификации и реализации, алгоритмов обработки данных и анализ этих алгоритмов, взаимосвязь алгоритмов и структур данных; формирование целостной системы знаний о классических алгоритмах программирования, области их применения; формирование у студентов математической культуры и развитие логического мышления; обучение решению прикладных задач математическими методами; развитие способности творчески подходить к решению профессиональных задач. В результате изучения курса студенты должны знать: об основных методах разработки машинных алгоритмов и программ, о стандартных структурах данных, используемых для представления типовых информационных объектов; об основных машинных алгоритмах и характеристиках их сложности для типовых задач, часто встречающихся и ставших “классическими” в области информатики и программирования. . должны уметь: разрабатывать алгоритмы, используя изложенные в курсе общие схемы, методы и приемы построения алгоритмов, выбирая подходящие структуры данных для представления информационных объектов; доказывать корректность составленного алгоритма и оценивать основные характеристики его сложности; реализовывать алгоритмы и используемые структуры данных средствами языков программирования высокого уровня (например, С++); экспериментально (с помощью компьютера) исследовать эффективность алгоритма и программы. 1.4. 2 1.5. Объем дисциплины и виды учебной работы. № п/п Шифр и наименование специальности 1 010200 «Прикладная математика и информатика» 010501 «Прикладная математика и информатика» 2. Курс Семестр Трудоемкость Виды учебной работы в часах Всего ЛК ПР/ ЛБ Сам. аудит. СМ работа Вид итогового контроля (форма отчетности) 4 8 100 50 26 24 − 50 экзамен 3 5 100 50 26 24 − 50 экзамен 1.6 Содержание дисциплины. 1.6.1 Разделы дисциплины и виды занятий (в часах). Примерное распределение учебного времени: № п/п Наименование раздела, темы 1 МЕТОДЫ РАЗРАБОТКИ ЭФФЕКТИВНЫХ АЛГОРИТМОВ ЛИНЕЙНЫЕ И НЕЛИНЕЙНЫЕ СТРУКТУРЫ ДАННЫХ СОРТИРОВКА БЫСТРЫЙ ПОИСК ИТОГО 2 3 4 Количество часов для специальности 010200 «Прикладная математика и информатика» Всего ЛК ПР ЛБ Сам. ауд. раб. 6 4 2 – 6 8 4 4 – 8 16 20 50 8 10 26 8 10 24 – 16 20 50 – 1.6.2 Содержание разделов дисциплины. МЕТОДЫ РАЗРАБОТКИ ЭФФЕКТИВНЫХ АЛГОРИТМОВ § 1. Алгоритм и его свойства. Временная и пространственная сложность алгоритма § 2. Методы декомпозиции и балансировки на примере алгоритма быстрого умножения ЛИНЕЙНЫЕ И НЕЛИНЕЙНЫЕ СТРУКТУРЫ ДАННЫХ (СД) § 1. СД: основные определения. Функциональная спецификация, логическое описание и физическое представление § 2. Линейные списки: стеки, очереди и деки, списки с 2 связями. Кольцевые (циклические) ЛС § 3. Массивы. Представление разреженных массивов § 4. Множества. Представление множества в виде ЛС, массива и характеристического вектора § 5. Бинарные деревья; их машинное представление § 6. Итерация и рекурсия; их достоинства и недостатки. Применение рекурсии для обхода бинарных деревьев СОРТИРОВКА (СР) § 1. Понятие о сортировке. Базовые идеи алгоритмов ср. Сортировка внутренняя и внешняя. Временная сложность алгоритмов ср с помощью сравнений Внутренние сортировки § 2. Ср включением и её разновидности. Метод Шелла § 3. Ср обменами и её разновидности. Пузырьковая Ср и Быстрая Ср § 4. Ср извлечением и её разновидности. Древесная Ср, Ср лесом. § 5. Ср распределением и её разновидности. Цифровая Ср, Ср подсчётами. 3 БЫСТРЫЙ ПОИСК § 1. Задача поиска элемента по заданному ключу. Последовательный поиск в неупорядоченной таблице. § 2. Поиск в упорядоченной таблице: последовательный, дихотомический, интерполяционный § 3. Дерево поиска. Поиск и включение для двоичных деревьев. § 4. Сбалансированное дерево поиска (СДП). Включение в СДП. Исключение из СДП § 5. Построение дерева оптимального поиска § 6. Метод поиска с использованием функции расстановки (хеширование) 1.6.3 Темы для самостоятельного изучения. № п/п Наименование раздела дисциплины. Тема. Форма самостоятельной работы Контрольная работа № 1 Контрольная работа № 1 Проверка контрольной работы 3 МЕТОДЫ РАЗРАБОТКИ ЭФФЕКТИВНЫХ АЛГОРИТМОВ ЛИНЕЙНЫЕ И НЕЛИНЕЙНЫЕ СТРУКТУРЫ ДАННЫХ СОРТИРОВКА Форма контроля выполнения самостоятельной работы Проверка контрольной работы 4 БЫСТРЫЙ ПОИСК Контрольная работа № 1 Контрольная работа № 2 Проверка контрольной работы Проверка контрольной работы 1 2 Количество часов 6 8 16 20 1.7 Методические рекомендации по организации изучения дисциплины. 1.7.1 Тематика и планы практических занятий по изученному материалу Практические занятия по теме «МЕТОДЫ РАЗРАБОТКИ ЭФФЕКТИВНЫХ АЛГОРИТМОВ» ПР № 1. Оценка сложности алгоритмов. Алгоритм быстрого умножения Литература: 1. Ахо А., Хопкрофт Дж., Ульман Дж. Построение и анализ вычислительных алгоритмов. М.: Мир, 1979. - 536 с. 2. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. М.: Издательский дом “Вильямс”, 2001.- 384 с. 3. Гудман С., Хидетниеми С. Введение в разработку и анализ алгоритмов. - М,.: Мир, 1981. 368 с. Практические занятия по теме «ЛИНЕЙНЫЕ И НЕЛИНЕЙНЫЕ СТРУКТУРЫ ДАННЫХ» ПР № 2. Физическое представление линейных списков. Физическое представление массивов ПР № 3. Способы представления множества. Обходы бинарных деревьев Литература: 1. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. М.: Издательский дом “Вильямс”, 2001.- 384 с. 2. Браунси, Кен. Основные концепции структур данных и реализация в С++. М.: Издательский дом “Вильямс”, 2002.- 320 с. 3. Вирт Н. Алгоритмы и структуры данных. СПб.: Невский Диалект, 2001. - 352 с. 4 4. Иванов Б.Н. Дискретная математика. Алгоритмы и программы: Учеб. пособие. М.: Лаборатория базовых знаний, 2001. - 288 с. 5. Дмитриева М.В., Кубенский А.А. Элементы современного программирования: Учебное пособие. Спб.: Изд-во С.-Петербургского университета, 1991. - 272 с. 6. Мейер Б., Бодуэн К. Методы программирования: Том 1. М.: Мир, 1982. - 356с. Практические занятия по теме «СОРТИРОВКА» ПР № 4. Сортировка включением и её разновидности. Сортировка обменами и её разновидности ПР № 5. Сортировка извлечением и её разновидности. Сортировка распределением и её разновидности ПР № 6. Комбинированные методы сортировки ПР № 7. Контрольная работа № 1 Литература: 1. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. М.: Издательский дом “Вильямс”, 2001.- 384 с. 2. Кнут Д. Искусство программирования для ЭВМ. Том 3: Сортировка и поиск. М.: Издательский дом “Вильямс”, 2000. - 832 с. 3. Браунси, Кен. Основные концепции структур данных и реализация в С++. М.: Издательский дом “Вильямс”, 2002.- 320 с. 4. Зубов В.С. Справочник программиста. Базовые методы решения графовых задач и сортировки. М.: Информационно-издательский Дом “Филинъ”, 1999. - 256 с. 5. Иванов Б.Н. Дискретная математика. Алгоритмы и программы: Учеб. пособие. М.: Лаборатория базовых знаний, 2001. - 288 с. Практические занятия по теме «БЫСТРЫЙ ПОИСК» ПР № 8. Добавление элементов в бинарное дерево поиска и удаление элементов из него. Добавление элементов в сбалансированное бинарное дерево поиска и удаление элементов из него. ПР № 9. Построение дерева оптимального поиска ПР № 10. Добавление элементов в Б-дерево поиска и удаление элементов из него ПР № 11. Хеширование. работа с хэш-таблицей ПР № 12. Контрольная работа № 2 Литература: 1. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. М.: Издательский дом “Вильямс”, 2001.- 384 с. 2. Кнут Д. Искусство программирования для ЭВМ. Том 3: Сортировка и поиск. М.: Издательский дом “Вильямс”, 2000. - 832 с. 3. Браунси, Кен. Основные концепции структур данных и реализация в С++. М.: Издательский дом “Вильямс”, 2002.- 320 с. 1.8 Учебно-методическое обеспечение дисциплины. 1.8.1 Рекомендуемая литература: Основная литература. 1. Информатика : общий курс : учебник для студ. вузов, обуч. по спец. "Прикладная информатика (по обл.)" и др. экон. спец. / А. Н. Гуда [и др.] ; под общ. ред. В. И. Колесникова. - 2-е изд. - М. : Дашков и К ; Ростов н/Д : Наука - Пресс, 2008. - 400 с. гриф 5 2. Кузьмин, О. В.Перечислительная комбинаторика : учеб. пособие для студ. вузов, обуч. по напр. и спец. в обл. математики и информатики / О. В. Кузьмин. - М.: Дрофа, 2005-110 с. гриф 3. Игошин В. И. Математическая логика и теория алгоритмов : учеб. пособие для студ. вузов, обуч. по спец. 050201 "Математика" / В. И. Игошин. - 2-е изд., стер. М. : Академия, 2008. - 448 с. гриф 4. Набебин, А. А.Математическая логика и теория алгоритмов : [учеб. пособие для студ., обуч. по направл. "Информатика и вычисл. техника" спец. "Прогр. обеспечение вычисл. техники и автоматизированных систем", "Информ. системы и технологии"] / А. А. Набебин, Ю. П. Кораблин. - М. : Научный мир, 2008. - 343 с. гриф Дополнительная литература. лекции 1. Грин Д., Кнут Д. Математические методы анализа алгоритмов. - М.: Мир, 1987. - 120 с. 2. Гэри М., Джонсон Д. Вычислительные машины и труднорешаемые задачи. М.: Мир, 1982. – 416 с 3. Зубов В.С. Справочник программиста. Базовые методы решения графовых задач и сортировки. М.: Информационно-издательский Дом “Филинъ”, 1999. - 256 с. 4. Мейер Б., Бодуэн К. Методы программирования: Том 1. М.: Мир, 1982. - 356с. 5. Мейер Б., Бодуэн К. Методы программирования: Том 2. М.: Мир, 1982. - 368 с. 6. Рейнгольд Э., Нивергельт Ю., Део Н. Комбинаторные алгоритмы. Теория и практика. - М.: Мир, 1980. - 476 с. 7. Стивенс Р. Delphi. Готовые алгоритмы. М.: ДМК Пресс, 2001. - 384 с. 8. Харари Ф. Теория графов. М.: Едиториал УРСС, 2003. - 296 с. 9. Ахо А., Хопкрофт Дж., Ульман Дж. Построение и анализ вычислительных алгоритмов. М.: Мир, 1979. - 536 с. 10. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. М.: Издательский дом “Вильямс”, 2001.- 384 с. 11. Вирт Н. Алгоритмы и структуры данных. СПб.: Невский Диалект, 2001. - 352 с. 12. Гудман С., Хидетниеми С. Введение в разработку и анализ алгоритмов. - М,.: Мир, 1981. 368 с. 13. Кнут Д. Искусство программирования для ЭВМ. Том 1: Основные алгоритмы. М.: Издательский дом “Вильямс”, 2000. - 720 с. 14. Кнут Д. Искусство программирования для ЭВМ. Том 3: Сортировка и поиск. М.: Издательский дом “Вильямс”, 2000. - 832 с. 15. Липский В. Комбинаторика для программистов. М.: Мир, 1988. практические занятия 1. Браунси, Кен. Основные концепции структур данных и реализация в С++. М.: Издательский дом “Вильямс”, 2002.- 320 с. 2. Дмитриева М.В., Кубенский А.А. Элементы современного программирования: Учебное пособие. Спб.: Изд-во С.-Петербургского университета, 1991. - 272 с. 3. Иванов Б.Н. Дискретная математика. Алгоритмы и программы: Учеб. пособие. М.: Лаборатория базовых знаний, 2001. - 288 с. 4. Новиков Ф.А. Дискретная математика для программистов. Спб: Питер, 2000. 304 с. 16. 1.9 Материально-техническое обеспечение дисциплины. 1.9.1 Перечень используемых технических средств: персональные компьютеры 1.9.2 Электронный конспект лекций 6 1.9.3 Пакет демонстрационно-обучающих программ по темам: «Типы данных», «Сортировка», «Быстрый поиск» 1.9.4 Двадцать четыре варианта индивидуальных заданий в электронном виде 1.10 Примерные зачетные тестовые задания. Контрольная работа №1. «Структуры данных. Сортировка» Пример одного варианта 1. Разреженная матрица целых чисел Amn представлена в виде упорядоченного (сначала по первому индексу, а затем по второму) списка (с двумя связями) триплетов. При m = n составить список триплетов для матрицы B = AT. 2. Два множества, элементами которых являются символы, представлены в виде стеков. Найти объединение и пересечение этих множеств. 3. Числовую последовательность частично отсортировать по первой цифре цифровой сортировкой, а затем полученные группы сортировать с помощью древесной сортировки. Методом дихотомического поиска найти заданный элемент и вывести на экран его порядковый номер в упорядоченной последовательности. 4. Прочитать исходные данные из входного текстового файла. Представив эти данные в виде массива записей с тремя полями, поочередно отсортировать массив по каждому из полей. Вывести отсортированные данные в выходной текстовый файл. Набор данных “Зачет” содержит для каждого студента: фамилию, инициалы (20 символов), номер группы (5 символов), наличие или отсутствие зачета (логическое значение). Использовать сортировку слиянием. Контрольная работа №2. «Быстрый поиск» 5. Используя те же записи, что и в задаче 4 (до сортировки!!!), организовать их в виде дерева поиска. В качестве ключа использовать значение первого поля. Перестроить дерево поиска, произведя последовательно следующие операции: добавив новую запись, удалив запись из корня. 1.11 Примерный перечень вопросов к экзамену. 1. Алгоритмы: определение и свойства 2. Временная и пространственная сложность алгоритмов 3. Суть методов декомпозиции и балансировки, а также пример их практического применения 4. Линейные списки: стеки, очереди и деки. Операции, определенные над ними. Достоинства и недостатки такого способа представления данных 5. Линейные списки: списки с двумя связями. Операции, определенные над ними. Достоинства и недостатки такого способа представления данных 6. Кольцевые (циклические) линейные списки: моделирование стека и очереди. Добавление и удаление элементов. Применение кольцевых списков 7. Массивы. Особенности представления разреженных массивов. Характеристический вектор для представления множества 7 8. Бинарные деревья. Операции, определенные над ними. Представление арифметических выражений в виде бинарного дерева 9. Полное дерево. Теорема о количестве концевых вершин полного дерева (с доказательством) 10. Рекурсивные процедуры. Достоинства и недостатки рекурсии. Рекомендации по применению. Способы обхода бинарного дерева 11. Временная сложность сортировки с помощью сравнений 12. Сортировка включением. Метод Шелла. Временная сложность алгоритмов 13. Сортировка слиянием и ее реализация с помощью рекурсии. Временная сложность алгоритма 14. Сортировка обменами. Пузырьковая сортировка. Быстрая сортировка. Временная сложность алгоритмов 15. Сортировка извлечением. Древесная сортировка. Временная сложность алгоритмов 16. Сортировка распределением. Цифровая сортировка. Сортировка подсчётами. Временная сложность алгоритмов 17. Последовательный поиск в упорядоченном и неупорядоченном списке. Дихотомический поиск в упорядоченном массиве. Временная сложность алгоритмов 18. Дерево поиска. Процедуры поиска, включения и исключения элементов для дерева поиска. Временная сложность этих процедур 19. Сбалансированное дерево поиска (AVL-дерево). Процедуры поиска, включения и исключения элементов для AVL-дерева. Временная сложность этих процедур 20. Дерево оптимального поиска и его построение 21. Б-деревья и их свойства. Включение в Б-дерево. Исключение из Б-дерева 22. Хеширование. Поиск, включение и исключение элементов. Рандомизирующая функция (хеширования) и ее свойства 1.12 Комплект экзаменационных билетов. Билет № 1. 1.13 Примерная тематика рефератов. Сравнительный анализ методов внешней сортировки Сравнительный анализ методов внутренней сортировки Топологическая сортировка и её использование для нахождения кратчайших расстояний в бесконтурном графе Проблема изоморфизма графов Методы нахождения k-го наименьшего элемента в заданном массиве Задача о распределении ресурсов 1.14 Примерная тематика курсовых работ. Программная реализация алгоритма решения задачи коммивояжера методом ветвей и границ; распознавания планарного графа с использованием критерия ПонтрягинаКуратовского; нахождения простой цепи, соединяющей две заданные вершины графа; нахождения простой цепи, проходящей через все вершины орграфа с использованием теоремы Redei; 8 нахождения в данном графе эйлерова цикла; нахождения в данном графе гамильтонова цикла; построения базисного графа в данном орграфе; поиска выхода из лабиринта; решения «шахматных задач» (о ходе коня, о ферзях) с использованием графического интерфейса. Раздел 2. Методические указания по изучению дисциплины и контрольные задания для студентов заочной формы обучения. Заочная форма обучения не предусмотрена. Раздел 3. Содержательный компонент теоретического материала. ГЛАВА I. МЕТОДЫ РАЗОРАБОТКИ ЭФФЕКТИВНЫХ АЛГОРИТМОВ § 1. Алгоритм и его свойства Под алгоритмом в самом широком смысле понимают точное и строгое описание последовательности операций, позволяющей за конечное число шагов получить решение задачи. Свойства алгоритмов 1. Дискретность: в начальный момент времени задается исходная конечная система величин, а в каждый следующий момент система величин получается по определенному закону (программе) из системы величин, имевшихся в предыдущий момент времени. 2. Детерминированность: система величин, получаемых в какой-то (не начальный) момент времени, однозначно определяется системой величин, получаемых в предшествующие моменты времени. 3. Элементарность шагов: закон получения последующей системы величин из предшествующей должен быть простым и локальным. 4. Направленность: если способ получения последующей величины из какойнибудь заданной величины не дает результата, то должно быть указано, что надо считать результатом алгоритма. 5. Массовость: начальная система величин может выбираться из некоторого потенциально бесконечного множества. Понятие алгоритма, в какой-то мере определяемое требованиями 1-5, конечно, не строгое: в формулировках этих требований встречаются слова “способ”, “величина”, “простой”, “локальный”, точный смысл которых не установлен. Поэтому это нестрогое понятие алгоритма называется непосредственным или интуитивным определением алгоритма. § 2. Сложность алгоритмов Понятие “сложность алгоритмов” позволяет придать точный смысл интуитивной идее, по которой алгоритм характеризуется стоимостью. Под стоимостью понимают время выполнения алгоритма и количество требуемой памяти. Важно различать практическую сложность, которая является точной мерой времени вычислений и объема памяти для конкретной модели вычислительной машины, и теоретическую сложность, которая более независима от практических условий выполнения алгоритма и дает порядок величины стоимости. 9 Практически время выполнения алгоритма зависит не только от объема множества данных, но также и от их значений. Например, в следующей главе будет показано, что время выполнения некоторых алгоритмов сортировки существенно сокращается, если первоначально входные данные частично упорядочены (тогда как другие алгоритмы остаются нечувствительными к этому обстоятельству). Чтобы учитывать этот факт, полностью сохраняя при этом возможность анализировать алгоритмы независимо от их данных, различают максимальную сложность и среднюю сложность алгоритма. Максимальная сложность соответствует случаю, когда выбранные входные данные порождают наиболее долгое выполнение алгоритма. Средняя сложность соответствует случаю, когда алгоритм применяется к произвольным данным. Если же операция выполняется за фиксированное время, не зависящее от входных параметров, то считают, что она имеет сложность порядка единицы (O(1)). Введенные для оценки временной сложности понятия аналогичным образом вводятся и для оценки пространственной сложности. Так, например, в этом случае также различают максимальную и среднюю пространственную сложность. Алгоритмы с полиномиальной оценкой (одна из них - n5) считаются приемлемыми, т.к. происходящий рост производительности ЭВМ заметно увеличивает и предельные размеры решаемых задач. Следует отметить, что уменьшение степени полинома в оценке хотя бы на 0.5 - это заметное достижение, т.к. оно заметно влияет на пределы применения алгоритма. § 3. Методы декомпозиции (“разделяй и властвуй”) и балансировки Пусть a, b, c - неотрицательные константы. Решение рекуррентных уравнений b п ри n 1, T (n) n aT c bn п ри n 1, где n - степень числа c, имеет вид: O( n ), a c, T ( n ) O( n log n ), a c, log c a ), a c. O( n Проведем оценку формулы (*) при n. Если a < c, то ряд, входящий в формулу (*), сходится, и, следовательно, T(n)=O(n). Если a = c, то получим T ( n) bn log c n 1 bn(log c n 1) O(n log n) . Наконец, если a > c, то i 0 T ( n ) bn a c 1 log c n a 1 c 1 a log c n a log c n O n O n O( n log c a ) . log n c c c 10 Если n не является степенью числа c, то можно вложить задачу размера n в задачу размера n*, где n* - наименьшая степень числа c, большая (либо равная) n. Поэтому порядки роста, указанные в примере 4.2, сохраняются для любого n. Метод декомпозиции Для решения той или иной задачи ее часто разбивают на части, находят их решение и затем из них получают решение всей задачи. В общем случае задача размерности n раскладывается на k задач с размерностями m1, m2,..., mk таких, что m1+m2+...+mkn. Из этого исходят, в частности, почти все методы сортировки. Метод балансировки Задача разбивается на подзадачи примерно равных размеров. Поддержание равновесия - основной руководящий принцип при разработке хорошего алгоритма. В качестве примера рассмотрим умножение двух n-разрядных двоичных чисел. Традиционный метод требует O(n2) битовых операций. В результате применения методов декомпозиции и балансировки эта оценка снижается до n log 2 3 n1,6 . Пусть x и y - два n-разрядных двоичных числа. Для простоты будем считать, что n представляет собой целую степень двойки, т.е. n=2m. Разобьем x и y на две равные части: x=a|b, y=c|d, где a,b,c,d - n/2 -разрядные числа. Тогда их произведение можно представить в виде: x y=(a 2n/2 +b) (c 2n/2 +d)=ac 2n+(ad+bc) 2n/2+bd. Это равенство дает способ вычисления произведения x и y с помощью четырех умножений n/2 -разрядных чисел, а также нескольких сложений и сдвигов: begin u:=(a+b) (c+d); z:=v 2n+(u-v-w) v:=a c; w:=b d; 2n/2+w {*z=xy*} end Из-за переноса в суммах (a+b) и (c+d) может оказаться (n/2)+1 разрядов. Рассмотрим сначала случай, когда этих разрядов n/2. Тогда временная сложность умножения двух n-разрядных двоичных чисел ограничена сверху функцией: п ри n 1, T ( n) n 3T 2 n п ри n 1. Здесь - это постоянная, отражающая сложение и сдвиги в выражениях, входящих в программу. Решение этого уравнения имеет оценку (см. пример 4.2): T (n) O(n log 2 3 ). Учтем теперь возможность (n/2)+1 разрядов. Для этого запишем сумму a+b в виде a1 2n/2 +b1, а сумму c+d - в виде c1 2n/2 +d1. Тогда u=(a+b) (c+d)= a1 c1 2n+( a1 d1+ b1c1) 2n/2+ b1 d1. 11 Слагаемое b1 d1 вычисляется с помощью рекурсивного применения этого алгоритма к задаче размера n/2. Остальные умножения можно сделать за время O(n), т.к. они содержат в качестве одного из аргументов либо единичный бит a1 или c1, либо степень числа 2. В алгоритме умножения двух n-разрядных двоичных чисел исходная задача размера n имела сложность O(n2). После того, как ее разбили на 3 подзадачи размера n/2, ее сложность снизилась до O(n log 2 3 ). Вообще, пусть исходная задача размера n имеет сложность O(n2). Разобьем ее на подзадачи, размер каждой из которых равен n/2. Подобное разбиение позволяет улучшить эффективность алгоритма в тех случаях, когда таких подзадач две (в силу примера 4.2 сложность снизится до O(n log n) ) или три (в силу примера 4.2 сложность снизится до O(n log 2 3 ) ). Если же этих подзадач четыре, то никакого выигрыша мы не получим три (в log 4 2 силу примера 4.2 сложность составит до O(n 2 n ) ). Разбиение на большее количество подзадач размера n/2 просто вредно! Пусть теперь исходная задача разбивается на подзадачи, размер каждой из которых равен n/4. Такая процедура имеет смысл, если подзадач окажется не более восьми, т.к. если их девять, то сложность составит O(n 9 n 2 ) . Это означает, что проще было исходную задачу разбить на три подзадачи размера n/2. log 4 12 log 3 ГЛАВА II. СТРУКТУРЫ ДАННЫХ Современная вычислительная машина была изобретена для облегчения проведения сложных расчетов, требующих на свое выполнение значительного времени. В настоящее время больше ценится способность ЭВМ хранить огромные объемы информации, к которым можно просто обращаться. Способность выполнить арифметические действия во многих ситуациях стала почти несущественной. Во всех таких случаях значительные объемы информации, подлежащей обработке, представляют собой абстракцию некоторого фрагмента реального мира. Данные об объектах, хранящиеся в памяти ЭВМ, игнорируют некоторые несущественные свойства и характеристики этих объектов. Например, в списке служащих некоторой организации могут присутствовать данные о фамилии, возрасте, окладе, но нет информации о цвете волос, весе, росте… § 1. Описание структуры данных Под структурой данных в самом широком смысле понимают организованную информацию, которая может быть описана, создана и обработана программами. К фундаментальным структурам данных относятся базовые типы, существующие в языках программирования: ЛОГИЧЕСКИЙ, ЦЕЛЫЙ, ВЕЩЕСТВЕННЫЙ, ЛИТЕРА, СТРОКА. Цель этого раздела – дать методы построения и обработки более сложных структур данных, или типов. В данном разделе под структурами данных будем понимать набор из одного или нескольких имен (с одной стороны) и множества данных (с другой стороны), к которым эти имена позволяют получить доступ. Рассмотрение структур данных затруднено смешением их абстрактных свойств и проблем представления, связанных с конкретной машиной или языком программирования. Важно четко различать эти проблемы с помощью многоуровневых описаний. Всякая структура данных может описываться на 3-х различных уровнях, перечисляемых от более абстрактного к более конкретному. I уровень. Функциональная спецификация, указывающая для некоторого класса имен операции, разрешенные с этими именами, и свойства этих операций. На этом уровне речь идет о внешнем определении, независимо от машины и языка программирования. Программист должен ответить на вопросы: какие операции хотел бы я уметь выполнять над моими данными и каковы свойства этих операций? II уровень. Логическое описание, которое задает декомпозицию объектов, отвечающую функциональному определению, на более элементарные объекты и декомпозицию соответствующих операций на более элементарные операции. Здесь программист отвечает на вопросы: какие данные известных типов и какие отношения, построенные для этих данных, позволяют удовлетворить функциональной спецификации? III уровень. Физическое представление, которое дает метод расположения в памяти вычислительной машины тех величин, которые составляют структуру, и отношений между ними, а также способ кодирования операций в языке программирования. Программист здесь решает, как можно изобразить и использовать все это в его конкретной машине, в том конкретном языке программирования, которым он располагает. 13 Пример 1. Преподаватель дисциплины “Дискретная математика” ведет учет успеваемости своих студентов. В течении семестра студент должен выполнить три контрольных работы, за каждую из которых преподаватель ставит оценку от 2 до 5 баллов. Рассмотрим информацию, представляющую состояние успеваемости студента. На I уровне (функциональная спецификация) определим операции с карточкой учета студента: создать новую карточку; узнать средний балл за выполненные контрольные работы; внести оценку за контрольную работу; удалить карточку по окончании учебного года. Создаем новую структуру данных СПИСОК_СТУДЕНТОВ. Задача программиста состоит в определении понятия СПИСОК_СТУДЕНТОВ таким образом, чтобы впоследствии можно было работать с ним так же, как работают с объектами типа ЛИТЕРА или ВЕЩЕСТВЕННЫЙ. 1. 2. 3. 4. Таким образом, определение структуры данных эквивалентно определению нового типа и его свойств. Все операции, встречающиеся в функциональной спецификации структуры данных, или типа (назовем его LТ), можно представить в следующем виде: f: X1X2…Xn Y1Y2…Ym; где Xi и Yj – типы (i=1,2,3,…,n; j=1,2,3,…,m); по крайне мере один из них должен быть LТ. Мы будем различать: а) функции создания (LТ появляется только справа от стрелки, т.е. LТ=Yр для некоторого р). Эти функции позволяют создавать элементы типа LТ, исходя из элементов других типов (или из никакого элемента; в этом случае слева от стрелки ничего нет). Пример. Операция создания пустого списка студентов. б) функции доступа (LТ появляется только слева от стрелки, т.е. LТ=Xk для некоторого k). Эти функции дают значения, характеризующие объекты типа LТ. Пример. Операция (2) в примере 1. в) функции модификации (LТ появляется и слева, и справа от стрелки, т.е. LТ=Хк и LТ=Yр для некоторых к и р). Эти функции позволяют создавать новые объекты типа LТ, исходя из уже созданных объектов этого типа (и, возможно, других элементов). Пример. Операции (1), (3), (4) в примере 1. Продолжим рассмотрение примера 1. На втором уровне (логическое описание) нужно определить новый тип СПИСОК_СТУДЕНТОВ через ранее известные типы. Полезная возможность в логических описаниях – это рекурсивное определение структуры LТ, получаемой с помощью соединения (обозначение “;”), причем одна из компонент имеет своим типом тоже самое LТ. Пример. Тип СПИСОК_СТУДЕНТОВ = (ПУСТОЙ или НЕ_ПУСТОЙ_СПИСОК); тип НЕ_ПУСТОЙ_СПИСОК = (СТУДЕНТ; СПИСОК_СТУДЕНТОВ). Определим теперь тип СТУДЕНТ. В карточке студента должна содержаться следующая информация: ФИО, № группы, 3 оценки за контрольные работы. Описываем типы данных: тип ОКР = (0,2,3,4,5); 14 тип СТУДЕНТ = (фио: СТРОКА[40]; номер_группы: СТРОКА[4]; окр1, окр2, окр3: ОКР). Рассмотрим теперь подпрограммы, реализующие операции из функциональной спецификации, которые могут выполняться с новым типом СПИСОК_СТУДЕНТОВ. Процедура создать новую карточку Переменные на входе: список: СПИСОК_СТУДЕНТОВ; имя: СТРОКА[40]; группа: СТРОКА[4]; переменные на выходе: список: СПИСОК_СТУДЕНТОВ. НАЧАЛО добавить новый элемент с: СТУДЕНТ в список; с.фио=имя; с. номер_группы= группа; с.окр1=с.окр2=с.окр3=0 КОНЕЦ. Процедура узнать средний балл Переменные на входе: список: СПИСОК_СТУДЕНТОВ; имя: СТРОКА[40]; переменные на выходе: средний_балл: ОКР. НАЧАЛО найти в списке запись, для которой с.фио=имя; средний_балл=(с.окр1+с.окр2+с.окр3) / 3 КОНЕЦ. Процедура внести оценку за контрольную работу Переменные на входе: список: СПИСОК_СТУДЕНТОВ; имя: СТРОКА[40]; номер_кр:ЦЕЛЫЙ; оценка_кр: ОКР; переменные на выходе: список: СПИСОК_СТУДЕНТОВ. НАЧАЛО найти в списке запись, для которой с.фио=имя; если (номер_кр=1), то с.окр1=оценка_кр иначе если (номер_кр=2), то с.окр2=оценка_кр иначе с.окр3=оценка_кр КОНЕЦ. Процедура удалить карточку Переменные на входе: список: СПИСОК_СТУДЕНТОВ; группа: СТРОКА[4]; переменные на выходе: список: СПИСОК_СТУДЕНТОВ. НАЧАЛО посмотреть список: если (с. номер_группы= группа), то удалить запись с: СТУДЕНТ из списка 15 КОНЕЦ. Таким образом, логическое описание структуры данных включает опеределение типа через типы, определенные ранее, и некоторое число подпрограмм, оперирующих над определенным таким образом типом. На III уровне (физическое представление) рассмотрим способы представления структуры данных СПИСОК_СТУДЕНТОВ. СПИСОК_СТУДЕНТОВ – это множество студентов. Представление в машине таких множеств наталкивается на проблему их переменного размера. Схема 1. n = число студентов Студент №1 Студент №2 … Студент №n Здесь n указывает число блоков, эффективно используемых в области, выделенной массиву. Такая схема имеет существенные недостатки: если n слишком мало, то выделенная область переполняется; при большом n, наоборот, резервируется лишнее место, которое не может быть использовано под другие цели. Значительно эффектнее с точки зрения использования памяти ЭВМ – вторая схема, называемая цепной. Схема №2. В этой схеме в каждый предыдущий блок добавляется специальная функция – адрес последующего блока. Такой адрес, используемый в представлении структуры данных, называется указателем или ссылкой. Специальный элемент, называемый ПУСТО, служит для отметки конца списка. В дальнейшем, обсуждая представление структур данных в памяти ЭВМ, будем отдавать предпочтение схеме 2. Программная реализация структуры данных СПИСОК_СТУДЕНТОВ (пример 1) удобна, например, в виде списка с двумя связями (см. § 2.3). § 2. Линейные списки 2.1. Стек Стек – это список, в котором включение и исключение производится только с одного конца, называемого вершиной (верхушкой) стека. Когда элемент помещается в стек, то элемент ранее находившийся на вершине стека, становится временно недоступным (рис. 1.1). 16 Например, стопка книг на столе является аналогом стека: взять можно только верхнюю книгу в стопке и положить новую – только сверху. Стек называют также списком типа LIFO (Last In, First Out, что означает: “последним пришел – первым ушел”). Идеальная последовательность пополнения стека удовлетворяет следующим условиям: - стек никогда не становится пустым; - элементы, находящиеся на дне стека, не остаются в стеке после предельной даты; - частота пополнения стека применительно к конкретной задаче не слишком велика. На практике трудно выполнить все условия одновременно, поэтому приходится искать разумный компромисс. Рис. 1.1. Цепное представление стека. Существует соответствие между поведением стека и структурой выражения, где каждая закрывающаяся скобка соответствует всегда последней встретившейся открывающейся скобке, а также и между организацией программы блочной структуры, где каждый END “закрывает” последний встретившийся BEGIN. Тип СТЕКТ, или стек объектов типа Т , характеризуется следующими операциями: создание: создание_стека - операция без параметров, создающая пустой стек; доступ: стек_пуст – операция проверяет, является ли стек пустым; последний – операция возвращает последний добавленный элемент; модификация: засылка – операция добавляет новый элемент к стеку; выемка – операция создает новый стек в результате извлечения верхушки; свойства этой операции верны, если стек не пуст. 2.2. Очередь. Дек Очередь – это структура данных, в которой элементы добавляются всегда с одного края, а удаляются с другого. Край, в котором выполняется включение, называется концом (хвостом) очереди, а край, в котором производится исключение элемента, называется началом (головой) очереди (рис. 1.3). Рис. 1.3. Цепное представление очереди. 17 Очередь называют списком FIFO (First In, First Out, что означает – “первым пришел – первым ушел”). Тип ОЧЕРЕДЬ Т, или очередь объектов типа Т , характеризуется следующими операциями: создание: создание_очереди – операция создает пустую очередь; доступ: очередь_пуста – проверка пустоты очереди; первый – доступ к самому давнему элементу очереди (к голове), если очередь не пуста; модификация: дополнение – прибавление нового элемента в хвост очереди; удаление – получение новой очереди путем удаления элемента из головы очереди; свойства операции верны, если очередь не пуста. Обобщением очереди является дек. Дек – это список, для которого включение и удаление элементов выполняется с любого края. Название этой структуры данных образовано.из первых букв английских слов Double Enden (двусторонняя запись). Работа с деком организуется аналогично работе с очередью с той лишь разницей что здесь выполняется четыре различных операции модификации: включение как в начало, так и в конец дека, удаление как из начала, так и из конца дека. 2.3. Список с двумя связями Список с двумя связями – это конечная и упорядоченная совокупность элементов. Упорядоченность в данном случае означает заранее определенный порядок доступа к элементам, соответствующий логическому порядку их следования в списке. Каждый элемент списка помимо некоторой содержательной информации должен включать также две ссылки: как на предыдущий элемент, так и на последующий элемент этого списка (рис. 1.5). Рис. 1.5. Цепное представление списка с двумя связями. В виде списка с двумя связями могут быть организованы, например, упорядоченный в некотором смысле (скажем, по фамилиям авторов) список книг в библиотеке, список студентов группы. В первом случае содержательная информация может состоять из ФИО автора, названия книги, год издания и т.п., во втором случае – ФИО студента, год рождения, номер группы, оценки и т.п. Вообще список с двумя связями удобно использовать для представления упорядоченного множества элементов переменной длины, для которых необходимо часто выполнять включение и удаление элементов не только в голове или 18 хвосте, как для очереди или стеков, но и в произвольных местах списка с сохранением порядка. Список с двумя связями является структурой данных последовательного доступа, т. е. для каждого элемента может быть осуществлен переход только к соседнему для него элементу. Функциональная спецификация списка с двумя связями, содержащего объекты типа Т, или СДСТ, включает операции: создание: создание_СДС – операция создает пустой список; доступ: следующий – на входе объект t, СДС; на выходе: объект, следующий за t в СДС; первый – возвращает первый элемент, если он существует; модификация: вставка – на входе: объекты t и t', СДС; на выходе: новый СДС, в котором объект t вставлен после объекта t' или после всех элементов, если t' не содержится в СДС; исключение – на входе: объекты t и t', СДС; на выходе: новый СДС, в котором объект t, следовавший после t', удален из списка. § 3. Кольцевые (циклические) списки Кольцевой список представляет собой обобщение линейного списка. Этот список характеризуется тем, что не имеет конца - элемента с пустой ссылкой; от каждого элемента здесь достижим каждый. Доступ к кольцевому списку происходит через единственный указатель на условно “головной” элемент. Включить элемент в циклический список можно “слева” от “головного”. Схема такого включения показана на рис.1.7. Рис. 1.7. Схема включения элемента в кольцевой список. Искусственным образом можно произвести и включение “справа”. Для этого элемент сначала включают “слева”, а затем его делают “головным”. Исключают элементы из кольцевого списка только “слева”. Если включение и исключение элементов производится “слева”, то кольцевой список моделирует стек. Если же включение элементов производится “справа”, а удаление “слева”, то в этом случае кольцевой список моделирует очередь. В некоторых случаях бывает удобно выделить в циклическом списке специальный головной элемент, который не содержит данных и остается даже в пустом списке. Такой элемент является как бы маркером “начала” и “конца” списка. Кольцевые списки удобно использовать при работе с многочленами; каждый элемент списка при этом соответствует одночлену, представляя коэффициент при нем и степени аргументов. Например, многочлен x4-9xy2+7y-4 можно задать четырьмя элементами: (1,4,0), (-9,1,2), (7,0,1), (-4,0,0). Так как результатом сложения или умножения многочленов является многочлен, который содержит неизвестное заранее количество слагаемых, то самым удобным способом представления результата является именно список. 19 § 4. Массивы Для известного типа Т и двух целых b и B (b B) определим тип МАССИВТ,b,B одномерных массивов с элементами типа Т и границами [b...B]. Пусть Ib,B – тип, определяемый перечислением как множество индексов: Ib,B = (b, b+1, …, B-1, B). Тогда определим МАССИВТ,b,B, исходя из типов Ib,B и Т, с помощью следующих операций: создание: создание_массива – создает массив типа Т с границами b и B, причем значения элементов массива пока не определены; доступ: значение_элемента – по индексу i извлекается элемент m i; модификация: изменение_элемента – по индексу i массив M преобразуется в массив M*, где M = {m1, …, mi-1, mi, mi+1, …, mn}, M*= {m1, …, mi-1, m*i, mi+1, …, mn}. Важным моментом для массивов является понятие прямого доступа: каждый элемент полностью определяется заданием имени массива и индекса; чтобы до него добраться, нет необходимости в серии операций над другими элементами, как в случае линейных списков. Для многомерных массивов необходимо указать границы всех их измерений: МАССИВ T, b , B , b , B ,..., b , B , где bk ik Bk для 1 k n. 1 1 2 2 n n Под массив выделяется сплошная область памяти. Если массив одномерный и каждый его элемент занимает p слов, то адрес i-го элемента можно найти по формуле: ai = ab+(i-b)p, где ab – это адрес первого элемента. Для массивов двух и более измерений нет единого способа расположения его элементов в памяти ЭВМ. Так, например, транслятор ФОРТРАНа размещает массив по столбцам, транслятор ПЛ/1 – по строкам, а в АЛГОЛ_W способ размещения вообще не уточняется стандартом. Для больших массивов сплошное представление иногда оказывается настолько неприемлемым, что массив приходится “разбивать на части” и каждую такую часть хранить по разным адресам в памяти ЭВМ. Например, двумерный массив можно хранить по строкам, причем адреса начала каждой строки находятся в специальном массиве указателей: (mb1, b2), (mb1, b2+1),..., (mb1,B2) (mb1+1, b2), (mb1+1, b2+1),..., (mb1+1, B2) ... (mB1, b2), (mB1, b2+1),..., (mB1,B2). Безусловно, такой способ представления массивов отрицательно сказывается на эффективности программ, так как требует двойного доступа к памяти при каждом обращении к элементу двумерного массива; его обобщение на n измерений требует n доступов на элемент. Представление разреженных массивов 20 Массив, у которого большинство элементов имеют одно и то же значение, называется разреженным. Пример 4.1. Пусть элементы матрицы M1010 имеют следующие значения: a12=17, a64=-8, a66=-2, a75=81, остальные элементы этой матрицы – нули. Тогда двумерный массив M[1:10, 1:10] – разреженный. При представлении такого массива ценой некоторой потери эффективности доступа можно экономить на памяти, размещая в ней только “значащие” элементы. Пусть для определенности массив М – двумерный с границами [1:m,1:n], и большая часть его элементов одинаковы и равны v. Решение представления такого массива является компромиссом между обычным представлением массивов и представлением списков. Каждый элемент матрицы M [i, j] можно характеризовать триплетом [i, j, M [i, j]]. Имеет смысл рассматривать только те триплеты, которые соответствуют отличным от v элементам М, и размещать их в списке. Элементы в списке триплетов размещают в соответствии с условиями конкретной задачи, либо в произвольном порядке, либо упорядоченными по некоторому критерию, например, по возрастанию сначала первого, а потом второго индекса. Кроме того, необходимо хранить в памяти ЭВМ значение по умолчанию v и границы m и n. Пример. Матрицу M1010 из примера (4.1) можно хранить в виде списка четырех триплетов: (1,2,17)(6,4,-8)(6,6,-2)(7,5,81). Чтобы прочитать или изменить элемент mi j массива, надо сначала найти его, просмотрев список; если в списке нет триплета, который начинается начиная с [i,j], то значение элемента равно v. § 5. Множества Множество – это наиболее общая линейная структура данных. Над множеством определены следующие операции: создание: создать_множество – операция без параметров, создающая пустое множество ; доступ: ству; найти – операция проверяет, принадлежит ли эл-т данному множе- пусто – операция проверяет, есть ли элементы в множестве; выборка – операция выдает элемент множества с данным свойством, если он существует, без удаления его из множества. модификация: включить – операция формирует новое множество, к которому добавляет один элемент; убрать – операция формирует новое множество, из которого удален данный элемент (если он принадлежит множеству; иначе операция имеет нулевой эффект); выборка_удаление – операция возвращает элемент множества с данным свойством и новое множество без удаленного элемента; объединение_множеств “”; пересечение_множеств “”; вычитание_множеств “\”. Множество можно представить в виде линейного списка (см. §2), массива (см. §4), с помощью стандартного типа SET, а также в виде характеристического вектора. Обсудим достоинства и недостатки каждого способа. Пусть U – это уни21 версальное множество, т. е. такое, что все рассматриваемые в данной задаче множества являются его подмножествами. Пусть также U содержит n элементов, т. е. U = n. I способ. Представление множества S в виде линейного списка Этот способ удобен, если в процессе работы с множеством S его часто приходится модифицировать (добавлять и удалять элементы), причем мощность этого множества значительно меньше мощности универсального множества: S<<U. II способ. Представление множества S в виде массива Среди достоинств такого представления можно назвать легкость реализации операций включения элемента и проверки наличия элемента, если известен его индекс. Вместе с тем, массивы плохо приспособлены для задач, в которых используются операция удаления элементов из множества. III способ. Представление множества S с помощью стандартного типа SET Этот способ был бы, пожалуй, лучшим из всех, если бы не ограничение на число элементов множества. Например, в ТурбоПаскале мощность множества S не должна превышать 256. IV способ. Представление множества S в виде характеристического вектора В этом случае универсальное множество U линейно упорядочивают U={u1, u2, … , un}, после чего любое его подмножество S представляется в виде вектора VS из n битов таким образом, что i-й разряд в векторе VS равен 1 тогда и только тогда, когда элемент ui принадлежит множеству S; в противном случае i-й разряд равен 0. VS называют характеристическим вектором для S. Пример. Пусть U={-7,0,7,14}. Тогда множеству {0,7} соответствует вектор (0,1,1,0), множеству ={-7,7,14} - вектор (1,0,1,1). Характеристический вектор универсального множества состоит из одних единиц, а характеристический вектор пустого множества - из одних нулей. Представление в виде двоичного вектора удобно тем, что можно определять принадлежность некоторого элемента uk множеству S за время, не зависящее от мощности этого множества, т. е. от S. Кроме того, основные операции над множествами (объединение “”, пересечение “”, разность “\”) могут быть реализованы элементарными логическими операциями (дизъюнкцией “ ”, конъюнкцией “&”, отрицанием “ ”). Однако выполнение всех этих операций требует времени, пропорционального U, а не размерам рассматриваемых множеств; объем памяти, необходимый для хранения множества S, также пропорционален U, а не S, поэтому этот способ рекомендуется использовать, когда S~U. § 6. Бинарные (двоичные) деревья Бинарное (двоичное) дерево (ДДТ) типа Т - это структура данных, которая либо пуста, либо состоит из корня и двух непересекающихся бинарных деревьев, называемых левым (ЛПД) и правым (ППД) поддеревьями данного дерева. На первый взгляд кажется, что бинарное дерево является частным случаем сильно ветвящегося дерева, из каждой вершины которого всегда выходит не более двух ветвей. Однако это не так. Главное различие состоит в том, что два возможных поддерева ДДТ 22 существенно различаются, тогда как в обычных деревьях они были бы идентичны и оба соответствовали бы схеме Кроме того, ДДТ может быть пусто, а обычное дерево – нет. На рис. 1.12 представлены примеры бинарных деревьев. Рис. 1.12. Примеры бинарных деревьев. Функциональная спецификация двоичного дерева включает операции: доступ: дерево_пусто - операция проверяет, является ли дерево пустым; чтение_корня - получение корня непустого дерева; слева - доступ к ЛПД непустого дерева; справа - доступ к ППД непустого дерева; модификация: добавление вершины; удаление вершины; построение - составление дерева из заданных корня, ЛПД и ППД. Операцию “построение” можно также рассматривать, как создание дерева. Чтобы представить двоичное дерево цепным способом, каждый элемент списка должен содержать, как минимум, три поля: информация о вершине и два указателя - к ЛПД и ППД. Пример. Бинарными являются деревья, описывающие родословную некоторого человека. При этом левый указатель PМ хранит адрес информации о матери, а правый указатель PО - адрес информации об отце. На некотором уровне информация о родственниках безвозвратно утеряна. 23 В некоторых случаях бинарное дерево удобно хранить в виде массива М, т.е. использовать не цепное, а сплошное представление дерева. При таком способе хранения М[1] - это элемент в корне дерева, а М[2i] и М[2i+1] - элементы в левом и правом потомках той вершины, в которой хранится элемент М[i]. Количество ребер в самой “длинной” ветке дерева определяет его высоту. Арифметическое выражение может быть представлено бинарным деревом, каждая вершина которого (кроме концевых) соответствует операции, левое и правое поддеревья соответствуют операндам. Эту систему можно обобщить на представление аксиом, теорем и т.д. Бинарные деревья наилучшим образом приспособлены для решения задач искусственного интеллекта. При решении этих задач программы обрабатывают деревья такого вида. Некоторые из этих деревьев – заданные, это “аксиомы”; другие должны быть получены из предыдущих, это “теоремы”, которые программа доказывает с помощью дедуктивных правил. Пусть имеется бинарное дерево высотой m с максимальным количеством вершин. Тогда справедлива следующая теорема. Теорема 1.1. Бинарное дерево высотой m с максимальным количеством вершин имеет ровно n = 2m концевых вершин. Из т-мы 1.1 следует, что если бинарное дерево с максимальным количеством вершин имеет n концевых вершин, то его высоту m можно найти по формуле: m=log2(n). (1.1) ГЛАВА III. СОРТИРОВКА Под сортировкой последовательности понимают переупорядочение ее элементов в такую последовательность, где все элементы расположены в порядке невозрастания или неубывания. Для определенности будем сортировать последовательность в порядке неубывания. Традиционно различают внутреннюю сортировку, обрабатывающую хранимые в оперативной памяти данные, и внешнюю сортировку, оперирующую с данными, которые принадлежат к внешним файлам. Проблемы оптимизации алгоритмов в этих случаях различаются: во внутренней сортировке стремятся сократить число сравнений и других внутренних операций, во внешней сортировке решающим фактором эффективности алгоритма становится количество необходимых вводов и выводов. В этом курсе ограничимся изучением внутренней сортировки. Пусть последовательность, которую необходимо отсортировать, представляет собой массив А[0..n1] записей. Каждая запись имеет, как минимум, два поля: key поле ключа и info информативное поле. Для простоты будем полагать, что поле info имеет тип СТРОКА. § 1. Оценка сложности сортировки с помощью сравнений Если о ключах сортируемого массива ничего не известно или если максимальный ключ во много раз больше, чем количество сортируемых элементов n, то метод CountingSort для упорядочения такого массива не годится. В этом случае массив можно отсортировать только с помощью сравнения ключей. 24 Покажем, что минимальная сложность алгоритмов сортировки с помощью сравнений Tmin(n) = O(n). Для этого составим неориентированный граф, определенный на множестве n элементов массива отношением: “элемент i связан с элементом j тогда и только тогда, когда они непосредственно сравниваются в ходе сортировки”. Если массив отсортирован, то этот граф обязательно будет связным. Как известно, связный неориентированный граф с минимальным количеством ребер является деревом. В соответствии со свойством дерева количество ребер в нем на единицу меньше количества вершин, т.е. равно n1. Значит, в ходе сортировки должно быть выполнено, как минимум, n1 сравнение, что и определяет минимальную сложность Tmin(n)=O(n). Покажем теперь, что максимальная сложность алгоритмов сортировки с помощью сравнений не меньше Tmax(n)=O(n log(n)). Для этого составим дерево решений, упорядочивающее массив из n элементов. Так как результатом упорядочения массива из n элементов может оказаться любая из n! перестановок элементов массива, то дерево решений содержит, по крайней мере, n! концевых вершин. Если полное бинарное дерево имеет n! концевых вершин, то его высоту m можно найти по формуле (1.1): h = log2(n!). Поскольку дерево решений не обязательно является полным, то справедливо приближенное равенство: h log2(n!). Т. к. именно высота m определяет максимальную сложность алгоритма, то T(n) log2 (n!). Воспользовавшись формулой Стирлинга: n!(n/e)n, которая выполняется при достаточно больших значениях n, получим оценку для максимальной сложности: T(n) (log2 ((n/e)n)=(n(log2 n-log2 e))=O(n log2 n)). 25 § 2. Сортировка включением. Метод Шелла. Сортировка слиянием Сортировка включением («путем вставок» по Д.Кнуту) состоит в следующем: выбирается некоторый элемент, сортируются другие элементы, после чего выбранный элемент “включается”, т.е. устанавливается на свое место среди других элементов. Рассмотрим подробнее простейший метод, использующий этот принцип, метод простых вставок. Будем сравнивать по очереди ключ i-й записи с ключами записей: i1й, i2-й, … до тех пор, пока не обнаружим, что i-ю запись следует вставить между записями j-й и j+1-й. Подвинув записи с j+1-й по i1-ю на одну позицию вправо, поместим новую запись в позицию j+1. У этого алгоритма есть одно важное достоинство: в противоположность другим методам он имеет наилучшую эффективность, если в начальном массиве уже установлен некоторый порядок. Средняя и максимальная сложность алгоритма сортировки включением составляют O(n2), если все перестановки предполагаются равновероятными. Такая низкая эффективность объясняется тем, что каждый элемент перемещается за один раз только на одну позицию: если m-й элемент массива должен быть перемещен в позицию 1, необходимо переместить на одну позицию вправо m1 предшествующих элементов. Улучшить эффективность этого метода можно, применяя к сортировке включением метод декомпозиции и принцип балансировки. Сортировка Шелла Усовершенствованная сортировка включением известна как сортировка Шелла. В 1959 году Д.Л.Шелл предложил вместо систематического включения элемента с индексом i в подмассив предшествующих ему элементов (этот способ противоречит принципу “балансировки”, почему и не позволяет получить эффективный алгоритм) включать этот элемент в подсписок, содержащий элементы с индексами ih, i2h, i3h и т.д., где h некоторая натуральная постоянная. Таким образом, формируется массив, в котором h-серии элементов, отстоящих друг от друга на расстояние h, сортируются отдельно: a1 , a 2 , a 3 , ..., a h1 , a h 2 , a h 3 , ..., a 2h1 , a 2h 2 , a 2h 3 , ... После того, как отсортированы непересекающиеся h-серии, процесс возобновляется с новым значением h* < h. Предварительная сортировка серий с расстоянием h значительно ускоряет сортировку серий с расстоянием h*. Справедлива следующая теорема: если l отсортированную последовательность p отсортировать, то она останется l отсортированной. Для достаточно больших массивов результаты тестов позволяют рекомендовать последовательность таких hi, что hi+1=3 hi +1: ..., 364, 121, 40, 13, 4, 1. (*) Начать процесс следует с такого элемента этой последовательности, который является ближайшим к целой части числа (n/9), превосходящим это число. 26 Кроме последовательности (*), Кнут рекомендует, например, последовательность: 2k1, ..., 15, 7, 3, 13, 1, для которой сложность алгоритма Шелла составляет O(n5/4). Так как n5/4 < n log2n при n < 216 = 65536, то этот вид сортировки пригоден для массивов, имеющих примерно до 60 тыс. элементов. Для больших n рекомендуется последовательность Седгевика, для которой алгоритм k k/2 1, если k четно, 9 2 9 2 Шелла имеет сложность O(n7/6): hk k ( k 1) / 2 1, если k нечетно. 8 2 6 2 § 3. Сортировка обменами. Пузырьковая ср. Быстрая ср. Хоара Сортировка обменами выполняется за несколько просмотров. Каждый раз, когда находятся два элемента a[i] и a[j], такие, что i<j и при этом a[i].key > a[j].key, эти элементы меняются местами. Простейший вид такой сортировки, меняющий местами соседние элементы, получил название пузырьковая сортировка (BubbleSort). Такое название происходит от образной интерпретации, по которой алгоритм заставляет “легкие” элементы “всплывать” на поверхность. Максимальная сложность пузырьковой сортировки составляет T(n)=O(n2). Это, пожалуй, наихудший из известных методов сортировки. Если желательно получить эффективный метод, оперирующий обменами, следует обратиться к алгоритму Хоара, который дает один из лучших известных методов. Этот метод обычно называется быстрой сортировкой; ему также хорошо подошло бы и название сортировка сегментацией. Быстрая сортировка является блестящим примером систематического применения метода декомпозиции и принципа балансировка к сортировке обменами. На первом этапе быстрой сортировки упорядочиваемый массив реорганизуется. Для этого в массиве выбирается главный элемент a[p]. Затем все элементы с ключами, меньшими a[p].key, размещаются слева от главного элемента, а элементы с ключами, большими либо равными a[p].key, размещаются справа от него. Затем для полной сортировки массива достаточно (второй этап): а) больше не трогать главный элемент, который находится на своем месте; б) рекурсивно отсортировать подмассив A[0, p1]; в) рекурсивно отсортировать подмассив A[p+1, n1]. Реализуя II этап на практике, главный элемент включают в один из подмассивов левый или правый. Остановимся подробнее на деталях первого этапа. Выбор главного элемента Это очень важный момент в алгоритме быстрой сортировки. Именно неудачный выбор на каждом шаге главного элемента обуславливает плохой показатель максимальной сложности. Пусть на некотором шаге на вход процедуры быстрой сортировки QuickSort поступили элементы массива с индексами от first до last. Тогда лучший способ выбрать главный элемент заключается в использовании генератора случайных чисел для порождения целого числа p, first p last. Это число и определяет индекс главного элемента. 27 Реорганизация массива относительно главного элемента Двигаясь от элемента с индексом first вправо, находят элемент с индексом f, ключ которого больше ключа главного элемента. Двигаясь от элемента с индексом last влево, находят элемент с индексом l, ключ которого меньше ключа главного элемента. Если оказалось, что элемент с индексом f расположен левее элемента с индексом l, то их меняют местами. Дальше движение влево продолжают с позиции f+1, движение вправо с позиции l1 до тех пор, пока f l. Алгорит Хоара имеет среднюю сложность O(n logn), тогда как максимальная сложность остается O(n2). Парадокс быстрой сортировки заключается в том, что в противоположность сортировке включением и даже пузырьковой сортировке (ближайшей родственнице!) она теряет свои положительные качества на частично упорядоченных массивах. В большом массиве следует останавливать рекурсию, когда размеры подмассивов становятся меньше некоторой константы, называемой порогом. После этого используется метод, эффективность которого может улучшаться на частично отсортированных данных, например, сортировка включением: if lastfirst>порог then быстрая сортировка(...) else сортировка включением(...);. Значение порога определяется характеристиками конкретной ЭВМ. Быстрая сортировка рекомендуется для упорядочения массивов с размерностью от 100 и более элементов. § 4. Сортировка извлечением. Древесная сортировка. Сортировка Лесом Сортировка извлечением («посредством выбора» по Д.Кнуту) состоит из (n1)-го этапа; на k-м этапе разыскивается элемент с минимальным ключом среди тех, которые еще не отсортированы окончательно, и помещается в k-ю позицию. Как средняя, так и максимальная сложность этого алгоритма O(n2). Основной недостаток сортировки извлечением в том, что выполняемые на каждом этапе сравнения дают намного более богатую информацию, чем та, которая эффективно используется. Уильямс и Флойд усовершенствовали сортировку извлечением, получив алгоритм, называемый древесная сортировка (HeapSort, турнирная ср., пирамидальная ср). В этом алгоритме используется двоичное дерево, вершины которого помечаются элементами сортируемой последовательности. Затем процедура Heap меняет размещение этих элементов на дереве до тех пор, пока ключ элемента, соответствующего произвольной вершине, станет не меньше ключей элементов, соответствующих ее потомкам. Такое помеченное дерево называется сортирующим. Особенности сортирующего дерева состоят в том, что последовательность элементов, лежащих на пути из любой концевой вершины в корень, линейно упорядочена по ключу и элемент с наибольшим ключом всегда соответствует корню. На следующем шаге алгоритма древесной сортировки из сортирующего дерева удаляется элемент с наибольшим ключом (из корня). Метка некоторой концевой вершины переносится в корень, а сама концевая вершина удаляется. Полученное дерево снова перестраивается в сортирующее, и процесс повторяется. Хранят сортирующее дерево в виде массива, в котором а[0] элемент, помещенный в корень, а[2i+1] и а[2i+2] элементы в левом и правом потомках той вершины, в которой хранится элемент а[i]. Формирует сортирующее дерево рекурсивная процедура Heap, которая из трех элементов а[i], а[2i+1] и а[2i+2] выбирает элемент а[k] с максимальным ключом и, если k i, переставляет этот элемент с элементом а[i]. Параметры проце- 28 дуры Heap first и last задают область ячеек массива А, обладающую свойством сортирующего дерева. Общее время, затрачиваемое процедурой Heap, O(n). На рис. ** представлены два бинарных дерева до обработки процедурой Heap (рис. **, а) и после нее (рис. **, б). Средняя и максимальная сложность алгоритма древесной сортировки O(n logn) вне зависимости от того, как первоначально распределены ключи упорядочиваемой последовательности. Древесная сортировка рекомендуется для упорядочения последовательностей с размерностью от 100 и более элементов. Сортировка Лесом (Конвейерная сортировка) Основной недостаток метода древесной сортировки достаточно большое (по сравнению с лучшими методами) среднее время выполнения. Уменьшить это время можно за счет замены сортирующего дерева сортирующим лесом. Метод конвейерной сортировки состоит из двух этапов. Первый этап: построение сортирующего леса, состоящего из m деревьев. Для простоты полагаем, что n mod m = 0. Разбиваем массив А на m частей и для каждой из этих частей применяем процедуру построения сортирующего дерева. Второй этап: обработка деревьев. Обозначим MAX количество неотсортированных элементов; NUM количество используемых в данный момент сортирующих деревьев. Шаг 1. MAX = n; NUM = m. Шаг 2. Найти максимальный среди корней сортирующих деревьев. Шаг 3. Поменять местами с А[max]. Шаг 4. Для дерева, чей корень был максимальным, выполнить процедуру Heap. Шаг 5. Если MAX = 1, то сортировка закончена. Шаг 6. Если MAX номер корня сортирующего дерева, то NUM . Шаг 7. MAX ; перейти к шагу 2. Несмотря на появление дополнительного шага 2, среднее время выполнения сортировки Лесом существенно снижается по сравнению со средним временем выполнения Древесной сортировки за счет сокращения числа рекурсивных обращений к процедуре Heap. § 5. Сортировка распределением Это единственный вид сортировки, при котором не требуется сравнивать сортируемые элементы. Начнем рассмотрение сортировки распределением с ее простейшей разновидности - цифровой сортировки (сортировки вычерпыванием). 29 Цифровая сортировка служит для упорядочения последовательности целых неотрицательных чисел, т.е. каждая запись массива А содержит только одно поле - поле ключа. Пусть известно, что максимальный элемент этого массива не превосходит некоторое натуральное m. Если m не слишком велико (m = O(n)), то массив А можно упорядочить следующим образом: 1. Организовать m+1 пустых очередей (черпаков) по одной для каждого числа от 0 до m. 2. Просмотреть массив А слева направо, помещая элемент ai в очередь с номером ai. 3. Сцепить эти очереди, т.е. содержимое (i+1)-й очереди приписать к концу i-й очереди (i=0,1,... m1). Так как каждый элемент можно вставить в i-ю очередь за время O(1), то n элементов можно вставить в очереди за время O(n). Конкатенация (сцепление) m очередей требует времени O(m). Если m=O(n), то этот алгоритм сортирует n натуральных чисел за время T(n)=O(n). Можно облегчить реализацию этого алгоритма, если вместо очередей организовать массив счетчиков: count[0..m]. Первоначально значение каждого счетчика равно 0. При просмотре массива А вместо добавления элемента ai в очередь с номером ai надо просто увеличить count[ai] на единицу, а вместо сцепления очередей распечатать элемент ai в точности count[ai] раз. Если же вместо массива целых неотрицательных чисел сортируется массив записей с несколькими полями, то цифровую сортировку необходимо модифицировать, т.к. первый подход слишком громоздок в реализации, а при втором информативные поля будут просто утеряны. Познакомимся с эффективной разновидностью сортировки распределением, называемой сортировкой подсчетами (CountingSort). Пусть, по-прежнему, ключи представляют собой числа, принадлежащие множеству N{0}, и известно, что максимальный ключ не превосходит некоторое натуральное m, причем m и n одного порядка. При первом проходе выясним, сколько и каких значений ключа имеется, затем заново распределим позиции исходного массива и при втором проходе расставим записи в соответствии с этим распределением. Подробнее: 1. Организовать массив счетчиков: count[0..m]. Сначала значение каждого счетчика = 0. 2. Скопировать исходный массив А во вспомогательный массив В. 3. Осуществить первый просмотр массива В; если ключ b[j].key j-й записи равен i, то увеличить count[i] на единицу. 4. Накопить сумму, с тем чтобы найти будущие границы count[i] групп записей с одинаковым значением ключа: for (i=1; i<=m; i++) {count[i]+ = count[i-1];};. Если в массиве несколько записей имеют ключ i, то значение count[i] теперь определяет позицию последней такой записи. 5. При втором просмотре массива В j-ю запись переместить в ее новую позицию count[b[j].key] массива А, а count[b[j].key] уменьшить на единицу. 30 ГЛАВА IV. БЫСТРЫЙ ПОИСК В этой главе будем полагать, что элементы структуры данных, в которой производится поиск, представляют собой записи, содержащие, по крайней мере, два поля: обязательно имеется поле ключа key типа int и поле данных info произвольного типа. Будем предполагать, что каждый элемент структуры данных обладает уникальным ключом. § 1. Поиск в отсортированном массиве Рассмотрим задачу поиска записи по ключу y в массиве A[0..n1] записей, упорядоченном в порядке неубывания ключей. Простейший способ решения этой задачи состоит в поочередном сравнении полей ключа со значением y. В худшем случае искомая запись окажется последней или вовсе не будет найдена; поэтому максимальная сложность такого алгоритма T(n)=O(n). Применяя методы декомпозиции и балансировки, получим эффективный алгоритм, называемый дихотомическим (логарифмическим) поиском. Вместо того, чтобы разбивать задачу размера n на две подзадачи размера 1 и n1, разобьем ее на подзадачи примерно равных размеров. Сравнивая значение y с ключом записи в середине массива, мы узнаем, в какой половине (левой или правой) следует продолжать поиск. После этого алгоритм рекурсивно применяется к левой и правой части массива до получения массива из одного элемента. Такой алгоритм называется дихотомический поиск. Полное дерево сравнений, для массива, состоящего из n элементов, содержит ровно n концевых вершин. Если бинарное дерево с максимальным количеством вершин имеет n концевых вершин, то его высоту m можно найти по формуле (1.1): m = log2(n). Поскольку полное дерево сравнений не обязательно содержит максимальное количество вершин, то это равенство при больших n выполняется приближенно: mlog2(n). А так как именно высота m определяет максимальную сложность этого алгоритма, то T(n)=O(log2n). Таким образом, применение методов декомпозиции и балансировки в сочетании с рекурсией позволило снизить оценку максимальной сложности алгоритма со степенной до логарифмической. § 2. Дерево поиска Предположим теперь, что наши данные должны иметь динамическую структуру. Тогда массив для их представления не годится. Вместе с тем, информацию следует организовать таким образом, чтобы поиск элемента по заданному ключу осуществлялся так же быстро, как и при дихотомическом поиске. Подходящей структурой для представления таких данных является дерево поиска. Деревом поиска называется бинарное дерево, для каждой вершины которого справедливо утверждение: ключ элемента, соответствующего этой вершине, не меньше ключей элементов, соответствующих ее левому поддереву, и не больше ключей элементов, соответствующих ее правому поддереву. Возможности динамического размещения переменных с доступом к ним через ссылки позволяют легко менять саму структуру дерева, т.е. дерево “растет” или “сокращается” в ходе выполнения программы. Элемент дерева NODE поиска опишем с помощью следующих типов: 31 typedef struct s_tree { int key; /* key’s field */ <...>; /* data’s field*/ struct s_tree *lptr; /* pointer to left son */ struct s_tree *rptr; /* pointer to right son */ } NODE; NODE *root; /* pointer to tree's root */ Поиск элемента В дереве поиска запись с данным ключом y можно обнаружить, начав с корня и двигаясь к левому или правому поддереву на основании сравнения ключа y с ключом текущей вершины. Предположим, что такое дерево уже организовано. Так как поиск идет по одномуединственному пути от корня к искомой вершине, то его можно легко запрограммировать с помощью итерации. Функция LocateTree возвращает адрес вершины с заданным ключом. NODE *function LocateTree(int y, NODE *ptr) { while (ptr!=NULL && ptr->key != y) {if (y < ptr->key) {ptr = ptr->lptr;} /* Продолжить поиск в левом поддереве */ else {ptr = ptr->rptr;}; /* Продолжить поиск в правом поддереве */ }; return(ptr); }. Вызов процедуры осуществляется командой p = LocateTree (y, root), где root - адрес корня дерева. Если элемента с ключом y в дереве обнаружено не было, то функция LocateTree(y, root) вернет значение NULL. Включение элемента в дерево поиска Прочитав очередное число - ключ y, ищем его в дереве поиска. Если элемент с ключом y в дереве не существует, то, спустившись по пути поиска до вершины V с нулевым указателем, создаем новую вершину с начальным единичным значением счетчика, “цепляя” ее слева от вершины V, если y меньше ключа вершины V, или справа от вершины V, если y больше ключа вершины V. Этот процесс называется поиском по дереву с включением. Исключение элемента В случае исключения элемента из дерева поиска возможны три ситуации: удаляется концевая вершина или вершина с одним потомком или вершина с двумя потомками. Первые две ситуации не вызывают трудностей и обрабатываются одинаково. Пусть удаляется вершина V, имеющая предка U и одного потомка Z. Тогда достаточно позаботиться о том, чтобы вершина U ссылалась теперь на Z, а не на V, после чего командой free(<adres V>) очистить участок памяти, занятый вершиной V. Сложность третьей ситуации в том, что с помощью одной ссылки (от предка удаляемой вершины) нельзя указать сразу два направления (к обоим потомкам удаляемой вершины). В этом случае удаляемый элемент заменяют либо на самый правый элемент его левого поддерева, либо на самый левый элемент его правого поддерева. Каждый из этих элементов имеет не более одного потомка, иначе он не был бы самым правым (левым), причем их перенос на место удаляемого элемента не нарушает структуру дерева поиска. Рассмотрим более подробно процесс преобразования дерева поиска после удаления вершины с двумя потомками. Пусть удаляется вершина V, имеющая двух потомков. Тогда поиск замещающего элемента W (самого правого элемента левого поддерева вершины V) осуществляется следующим образом: спускаемся вдоль самой правой ветви левого подде32 рева вершины V до тех пор, пока не найдем вершину W с нулевым правым указателем. После этого заменяем вершину V на W, “цепляя” единственного потомка вершины W (если он есть) к предку этой вершины. Оценка сложности Дерево называется идеально сбалансированным, если число вершин в его ЛПД и ППД отличается не более чем на 1. Приблизительно подсчитаем высоту h идеально сбалансированного дерева с n вершинами: n1 2 h1 1 = n, откуда 2h , т.е. h = O (log2 n). 2 21 При построении дерева поиска с помощью алгоритма поиска с включением заранее неизвестно, как будет расти дерево и какую форму оно примет. В лучшем случае дерево поиска окажется идеально сбалансированным. Высота этого дерева определит максимальную сложность операций поиска, включения и удаления элементов, которая составит, таким образом, Tmax(n) = O (log2 (n)). Однако в худшем случае, когда все поступающие из входного потока ключи идут в порядке неубывания (или невозрастания), дерево выродится в обычный линейный список и максимальная сложность операций поиска, включения и удаления элементов составит Tmax(n) = O(n), т.е. все преимущества дерева поиска по сравнению с линейным списком будут утрачены. Доказано, что затраты на перестройку случайного дерева в идеально сбалансированное себя не оправдывают, т.к. они гасят выигрыш в длине пути. Однако менее строгое определение сбалансированности (см. следующий параграф) позволяет создать более простую процедуру переупорядочения за счет лишь незначительного усложнения процедуры поиска. 20+21+22+...+2h § 4. Сбалансированные деревья поиска (AVL-деревья) Сбалансированным называется бинарное дерево, у которого высоты левого и правого поддеревьев каждой из вершин различаются не более чем на единицу. На рис. а представлено идеально сбал. дерево, а на рис. ***, б сбалансированное. Очевидно, что идеально сбалансированное дерево одновременно является сбалансированным. Сбалансированное дерево поиска получило название AVL-дерева в честь его создателей Г.М.Адельсон-Вельского и Е.М.Ландиса. Средняя длина пути поиска в AVL-дереве практически совпадает с длиной в идеально сбалансированном дереве, тогда как поддерживать AVL-деревья намного легче. Нахождение элемента по ключу в AVL-дереве ничем не отличается от аналогичной процедуры в обычном дереве поиска. Включение элемента Для определенности будем считать, что включение нового элемента производится в ЛПД AVL-дерева. Очевидно, в результате этой процедуры высота ЛПД увеличится на единицу. Если до включения нового элемента высота ЛПД уже превышала высоту ППД, то после добавления этого элемента ЛПД недопустимо вырастет, т.е. дерево перестанет быть сбалансированным. В этом случае необходимо произвести балансировку, которая должна восстановить сбалансированность AVL-дерева. Существуют два принципиально различные вида балансировки, тогда как остальные виды могут быть получены из этих двух, исходя из соображений симметрии. 33 1. У ЛПД вершины V недопустимо выросла левая ветвь. Эту ситуацию иллюстрирует рис. 4.2, а, на котором “добавленная” в результате включения новой вершины высота изображена заштрихованным прямоугольником. В данном случае необходимо произвести однократный LL-поворот, схема которого показана на рис. 4.2, б. 2. У ЛПД вершины V недопустимо выросла правая ветвь. Эту ситуацию иллюстрирует рис. 4.3, а, на котором “добавленная” в результате включения новой вершины высота изображена заштрихованными прямоугольниками. Следует указать, что новая вершина добавлена или в дерево , или в дерево , а не в оба дерева одновременно: сразу после добавления одной из этих вершин потребуется балансировка. Оба эти случая иллюстрируются одним рисунком только потому, что для них обоих необходим двукратный LR-поворот, схема которого показана на рис. 4.3, б. Как в первом, так и во втором случае, в результате балансировки показанные на рисунках вершины и поддеревья перемещаются лишь в вертикальном направлении, в то время как их относительное горизонтальное расположение остается без изменения. Алгоритм включения и балансировки существенно зависит от того, каким образом хранится информация о сбалансированности дерева. Если хранить эту информацию полностью неявно, в структуре самого дерева, то это приведет к большим затратам времени на определение сбалансированности всех тех вершин, которые затрагивает добавление нового элемента. Удобнее в каждой вершине AVL-дерева хранить ее показатель сбалансированности balance, под которым понимают разность между высотой ее левого и правого поддеревьев. Очевидно, что показатель сбалансированности каждой вершины AVL-дерева может принимать следующие значения: -1, 0, 1. Таким образом, процесс включения новой вершины в AVL-дерево заключается в следующем: I. Проход по пути поиска до вершины с нулевым указателем с тем, чтобы убедиться, что элемента с данным ключом в дереве нет. II. Включение новой вершины с показателем сбалансированности, равным нулю. 34 III. “Отступление” по пути поиска (возможно, до самого корня) и корректировка в каждой встретившейся вершине показателя сбалансированности. Если величина balance в вершине примет недопустимое значение (2 или 2), то балансировка. Остановимся на п.III подробнее. Предположим, что двигаясь вверх по левой ветви, мы вернулись к вершине V, расположенной по адресу p, причем известно, что высота этой ветви увеличилась. Перед включением нового элемента в зависимости от высоты поддеревьев вершины V ее показатель сбалансированности составлял +1 при hЛПД < h ППД; 0 при hЛПД = h ППД ; 1 при hЛПД > h ППД . После добавления нового элемента в первом случае предыдущая несбалансированность в V уравновешивается: p->balance = 0, во втором случае левое поддерево станет допустимо перевешивать: p->balance = 1, а вот в третьем случае показатель сбалансированности примет недопустимое значение: p->balance = 2, что сигнализирует о необходимости вызова процедуры балансировки. Вид балансировки определяется показателем сбалансированности левого потомка W вершины V. Пусть вершина W расположена по адресу pl. Тогда если pl->balance = 1, то необходимо выполнить однократный LL-поворот (рис. 3.2), а если pl->balance = +1, то двукратный LR-поворот (рис. 3.3). Заметим, что ситуация, когда pl->balance = 0, невозможна по той же причине, по которой нельзя включить сразу два новых элемента - и в дерево , в дерево . Операция по балансировке состоит из последовательных переписываний ссылок и исправления показателей сбалансированности соответствующих вершин. Если недопустимо перевешивает правое поддерево вершины V, то выполняют либо однократный RR-поворот, либо двукратный RL-поворот в зависимости от показателя сбалансированности правого потомка вершины V. Процедура RR-поворота получается из процедуры LL-поворота путем перемены местами ссылок lptr и rptr, а также перемены местами показателей сбалансированности -1 и 1. Процедура RL-поворота получается из процедуры LR-поворота по тому же принципу. Экспериментальные данные подтверждают, что в среднем примерно на два включения приходится одна балансировка. Сложность операции балансировки предполагает, что AVL-деревья следует использовать только тогда, когда поиск информации осуществляется значительно чаще, чем ее включение. Исключение элемента Удаление концевых вершин и вершин с одним потомком осуществляется без труда (см. § 3.3). Если же от исключаемой вершины “отходят” два поддерева, то, как и в случае дерева поиска, ее заменяют на самую правую вершину ее левого поддерева. После удаления вершины сбалансированность дерева может нарушиться. В этом случае необходимо выполнить балансировку, алгоритм которой фактически остается тем же самым, что и при включении. Пусть для определенности в результате удаления элемента недопустимо уменьшилась высота правого поддерева вершины V. Эта ситуация целиком совпадает со случаем, когда недопустимо увеличилась высота левого поддерева этой вершины. Какой вид балансировки применить - по-прежнему зависит от показателя сбалансированности левого потомка вершины V. Экспериментальные данные подтверждают, что в среднем примерно на пять исключений приходится один вызов процедуры балансировки. Однако если включение одной вершины может привести, самое большее, к одному повороту двух или трех вершин, исключение может потребовать по повороту в каждой вершине вдоль пути поиска. 35 Оценка сложности Экспериментальные проверки показывают, что высота AVL-дерева приблизительно составляет log2n, где n количество вершин. Практически AVL-деревья ведут себя так же, как и идеально сбалансированные, хотя поддерживать их намного проще. Высота AVL-дерева и определяет максимальную сложность процедур поиска, включения и удаления элементов, которая составляет, таким образом, O (log2n). § 5. Б - деревья Существует практическая область применения сильно ветвящихся (не бинарных) деревьев. Такие деревья удобно использовать для формирования и поддержания крупномасштабных деревьев поиска, в которых необходимо выполнять и включение новых элементов, и удаление старых, но для которых либо не хватает оперативной памяти, либо она слишком дорога, чтобы использовать ее для долговременного хранения. Пусть вершины дерева хранятся во внешней памяти - например, на жестком диске. Принципиальное новшество здесь заключается в том, что ссылки представляют собой адреса на диске, а не в оперативной памяти. Поскольку теперь каждый шаг процедуры поиска элемента с данным ключом требует обращение к диску, то информацию следует организовать так, чтобы минимизировать число таких обращений. Если происходит обращение к одному элементу, расположенному во внешней памяти, то без больших затрат можно обратиться и к целой группе элементов; поэтому в каждой вершине дерева располагается не один элемент, а группа элементов, называемых страницей. Обращение к странице требует всего одного обращения к диску, что позволяет значительно сэкономить время на числе обращений и, тем самым, снизить сложность алгоритмов поиска, включения и исключения. Пример. Пусть дерево организовано таким образом, что каждая страница содержит 100 элементов, а вершина, в которой хранится эта страница, имеет 101-го потомка. Тогда поиск в дереве с миллионом элементов будет в среднем требовать log100(106) = 3 обращения к страницам, тогда как для двоичного и не разбитого на страницы дерева это число составляет в среднем log2(106) = 20 обращений. Конечно, если дерево растет случайным образом, то в худшем случае может потребоваться и 106/100=104 обращений. Поэтому для сильно ветвящихся деревьев также необходима некоторая схема управления их ростом. Идеальная сбалансированность требует слишком больших затрат на балансировку. Разумный критерий был сформулирован в 1970 году Р. Бэйером и Е. Маккресттом: каждая страница (кроме, быть может, корневой) должна содержать при заданном фиксированном n от n до 2n вершин. Соблюдение этого критерия позволяет осуществить поиск элемента по ключу в дереве с N элементами максимум за lognN обращений к вершинам этого дерева. Кроме того, каждая страница заполнена не менее чем на половину, а значит, коэффициент использования памяти составляет не менее 50%. Б-деревом называется сильно ветвящееся дерево, обладающее следующими свойствами: 1. При заданном натуральном n каждая страница, кроме корневой, содержит не менее n и не более 2n элементов. Корневая страница может содержать от 1 до 2n элементов. 2. Каждая страница, содержащая m элементов, либо является концевой, либо имеет ровно m+1 потомка. 3. Все концевые вершины находятся на одном уровне. Если спроецировать Б-дерево на горизонтальную прямую, включая ключи потомков между ключами их родительских элементов, то ключи будут расположены в порядке неубывания. На рис. 1 показан пример Б-дерева при n=2. 36 Каждая страница Б-дерева организована в виде массива с размерностью [1..2n], причем элементы массива располагаются на странице в порядке неубывания их ключей (рис. 2). Подобная организация Б-деревьев представляет собой естественное развитие принципа двоичных деревьев и определяет метод поиска элемента с заданным ключом. Поиск элемента в Б-дереве Считываем страницу в оперативную память и пользуемся обычным методом поиска среди ключей k1, k2, ... km. Если число m достаточно большое, то можно воспользоваться дихотомическим поиском, в противном случае можно использовать последовательный поиск. Какой вид поиска выбрать - здесь неважно, так как время, затрачиваемое на поиск в оперативной памяти, как правило, пренебрежимо мало по сравнению с временем пересылки страницы из внешней памяти в оперативную. При поиске элемента с заданным ключом y возможны следующие ситуации: 1. y=ki при некотором i (1<=i<=m): искомый элемент найден; 2. ki<y<ki+1 при некотором i (1<=i<m): поиск продолжается на странице с адресом pi; 3. y<k1: поиск продолжается на странице с адресом p0; 2. km<y: поиск продолжается на странице с адресом pm. Если на некотором шаге окажется, что pi = NULL, то это означает, что в Б-дереве нет элемента с данным ключом y. В этом случае в ходе поиска несуществующего элемента в оперативную память будет перегружено максимум k = lognN страниц; поэтому значение n необходимо выбрать таким образом, чтобы эти k страниц поместились в оперативную память. На самом деле приходится учитывать возможность хранения более чем k страниц, поскольку, как мы увидим в дальнейшем, включение элементов в Б-дерево может приводить к разделению страниц. Кроме того, в оперативной памяти следует постоянно хранить корневую страницу, так как с нее всякий раз начинается любой поиск. Включение элемента в Б-дерево Включение нового элемента осуществляется только на концевые страницы, т.е. страницы нижнего уровня. I случай. Новый элемент е нужно поместить на страницу с m<2n элементами. В этом случае процесс включения затрагивает только эту страницу. Алгоритм включения: 1. Установить, что элемент е отсутствует в дереве. 2. Определить страницу и позицию r на странице, в которую следует поместить элемент е. 3. Сдвинуть на одну позицию вправо элементы этой страницы с r-го по m-й. 4. Вставить новый элемент е в позицию r. 5. Увеличить размер страницы на единицу. II случай. Новый элемент е нужно поместить на страницу с m=2n элементами. Алгоритм включения: 1. Установить, что элемент е отсутствует в дереве. 2. Определить страницу А в которую следует поместить элемент и убедиться, что эта страница уже полна. 3. Создать чистую страницу В. 4. Элементы со страницы А вместе с новым элементом е (всего их 2n+1) поровну распределяются между страницами А и В, причем элемент со средним ключом переносится на один уровень вверх на родительскую страницу. 37 5. Размеры страниц А и В положить равными n, увеличить на единицу размер родительской страницы. Если включение в родительскую страницу вновь приводит к переполнению страницу, то разделение надо повторить уже на уровне родительской страницы. В крайнем случае разделение придется проводить на всех уровнях, включая корневой. Фактически только тогда может появиться новая корневая страница, и, следовательно, увеличиться высота Б-дерева. Так что Б-деревья растут несколько странно: от листьев к корню. Исключение элемента из Б-дерева I случай. Исключаемый элемент е находится на концевой странице Р. Пусть удаляемый элемент занимает позицию r. Алгоритм исключения: 1. Удалить со страницы Р элемент е. 2. Сдвинуть на одну позицию вправо элементы этой страницы с r+1-го по m-й. 3. Уменьшить размер страницы Р на единицу. II случай. Исключаемый элемент е с ключом ki находится не на концевой странице. В этом случае его следует заменить одним из двух лексикографически смежных элементов. Поиск смежного элемента похож на поиск при исключении из двоичного дерева: сначала спускаемся вниз влево на дочернюю страницу в направлении ссылки p i-1, а затем продолжаем спускаться вниз вдоль самых правых ссылок до листовой страницы Р. Алгоритм исключения: 1. В качестве смежного элемента взять самый правый элемент со страницы Р. 2. Заменить исключаемый элемент на смежный. 3. Уменьшить размер страницы Р на единицу. В обоих случаях может оказаться, что размер страницы Р сократился до n-1, т.е. на странице Р осталось недопустимо мало элементов. В этом случае надо попытаться позаимствовать недостающий элемент на соседней с Р странице Q. Если размер страницы Q больше n, то это можно сделать. Более того, уж если приходится вызывать дополнительную страницу в оперативную память, то лучше взять не один, а несколько элементов, распределив элементы страниц Р и Q поровну на обе страницы. Этот процесс, называемый балансировкой страниц, затрагивает также и родительский элемент страниц Р и Q: возможно, что он спустится на одну из этих страниц, а на его место поднимется элемент снизу. Если же размер страницы Q равен n, то общее число элементов на страницах P и Q составляет 2n-1 и можно слить обе страницы в одну, добавив сюда элемент, родительский для страниц Р и Q, а затем уничтожить пустую теперь страницу Q. Удаление среднего ключа на родительской странице вновь может привести к тому, что ее размер станет меньше n. Это означает, что надо провести балансировку или слияние страниц уже на следующем, более высоком уровне. В худшем случае слияние страниц может распространиться вверх до самой корневой страницы. Если корень сократился до нулевого размера, то он удаляется, и высота Бдерева уменьшается. § 6. Хеширование Произвольно организованный файл (файл прямого доступа) Файл прямого доступа (ФПД) характерен тем, что его записи могут быть размещены во внешней памяти в произвольном порядке относительно друг друга, причем доступ к 38 любой записи файла может быть выполнен непосредственно по ее физическому адресу, без необходимости просмотра предыдущих записей. По желанию программиста блоки в ФПД могут иметь формат: счетчик-данные или счетчик-ключ-данные. Ключ идентифицирует соответствующий ему блок и используется для определения места в файле при записи блока и поиска местоположения блока в файле при его чтении. Значением ключа может быть номер блока в некоторой системе нумерации (например, в порядке записи блоков в файл), символическое имя, присвоенное блоку программистом, или любая другая информация, однозначно идентифицирующая блок. При обработке ФПД важно правильно выбрать способ адресации блоков файла при записи и чтении данных. В системах обработки данных чаще всего применяются следующие способы адресации блоков: прямая адресация; табличная адресация; косвенная адресация (рандомизация). В случае прямой адресации программа обработки файла записывает или читает содержимое блока, используя непосредственно физический адрес этого блока. При таком способе адресации программист должен явно указывать физический адрес блока; поэтому прямую адресацию используют крайне редко. В случае табличной адресации при первоначальной загрузке ФПД в основной памяти формируется индексная таблица, в которой имеется по одному элементу для каждого записанного блока файла. Элемент индексной таблицы содержит значение ключа и физический адрес блока, соответствующего данному элементу (в формате счетчик-ключданные) (рис. 1.20). При каждом последующем обращении к блоку с целью его чтения и модификации сначала просматривается индексная таблица для нахождения в ней элемента, который содержит значение ключа обрабатываемого блока. Как только такой элемент найден, из его второго поля извлекается физический адрес блока и используется программой для обращения к заданному блоку в файле. В конце сеанса работы с файлом может возникнуть необходимость сохранить индексную таблицу во внешней памяти, чтобы учесть возникшие в ней изменения. К недостаткам такого способа адресации следует отнести необходимость выделения оперативной памяти для хранения индексной таблицы, а также увеличение времени доступа к файлу из-за поиска ключа в индексной таблице. Основная идея косвенной адресации (рандомизации) состоит в установлении функциональной зависимости между значением ключа блока и Рис. 1.20. адресом этого блока в файле. В отличие от табличного, этот способ не требует создания и хранения индексной таблицы. . На основе информации о множестве фактических значений ключей создется ФПД с таким числом блоков, которое немного больше мощности этого множества. Затем выбирается подходящая функция, которая преобразует значение ключа каждого записываемого или читаемого блока в адрес этого блока в файле. Эта функция называется рандомизиру39 ющей функцией или функцией хеширования. Схема действия рандомизирующей функции представлена на рис. 1.21. Рис. 1.21. Схема действия рандомизирующей функции. 40 Требования к рандомизирующей функции 1. Рандомизирующая функция должна обеспечивать достаточно равномерный разброс значений ключей по адресам блоков. Выполнение этого условия позволяет эффективно использовать всю область файла. 2. Рандомизирующая функция должна как можно реже приводить к ситуации переполнения, когда два или более значений ключей отображаются в один и тот же адрес блока в файле. Такие ключи называются синонимами. В способе рандомизации ситуации переполнения неизбежны, но выбором удачной функции хеширования число таких ситуаций можно минимизировать. 3. Рандомизирующая функция не должна сохранять какую-либо связь между значениями ключей. Выполнение этого требования исключает (или хотя бы уменьшает) зависимость “качества” рандомизации от используемых значений ключа. 4. Рандомизирующая функция должна быть достаточно простой, чтобы не требовалось длительного времени на вычисление адреса блока по заданному значению ключа. Если это требование не выполнено, то утрачиваются преимущества этого способа адресации по сравнению, например, со способом табличной адресации. Примеры рандомизирующих функций 1. Функция, использующая деление с остатком. Если n - это число блоков в файле, а m - ближайшее к n превосходящее его простое число, то рандомизирующая функция представляет собой остаток от деления ключа, представленного в цифровом виде, на m: H(k) = ord(k) mod m, где ord(k) - порядковый номер ключа k в множестве всех возможных значений ключей. 2. Функция, состоящая из логических операций. Эта функция применяется к некоторой части ключа, представленного в виде последовательности двоичных цифр. Как уже было сказано выше, при использовании косвенной адресации неизбежны ситуации переполнения. Рассмотрим способы решения проблемы переполнения. 1. Для каждого блока отводится столько места, чтобы в блоке можно было поместить несколько различных логических записей. Такой блок называется секцией или пакетом. Однако если в секции выделено место под М записей, а синонимов оказалось М+1, то ситуация переполнения все же возникает. 2. В дополнение к использованию пакетов в файле выделяется одна или несколько областей переполнения, в которых запоминаются записи, послужившие причиной возникновения ситуаций переполнения пакетов. Например, можно реализовать способ, при котором в файле существует единственная область переполнения, расположенная в конце файла (рис. 1.22). 41 Для связывания записей, попавших в область переполнения, с соответствующими пакетами применяются указатели, для которых надо отводить место в каждой записи. Если в области переполнения находится несколько записей, соответствующих одному пакету, то они связываются друг с другом в список. Использование области переполнения безусловно увеличивает среднее время доступа к записям. 3. Иногда при обработке ситуации переполнения используется способ, не требующий особой области переполнения. В этом способе Рис. 1.22. Логическая структура ФПД, имеющего область запись, вызвавшая перепереполнения. полнение, помещается в первый встретившийся свободный пакет. Этот метод приводит к удлинения времени доступа из-за необходимости поиска первого свободного пакета, а также к явлению скучивания записей в отдельных местах файла. Какой бы ни был выбран способ обработки переполнения ФПД, рано или поздно настанет момент, когда для новой записи места не найдется. Возникает ситуация переполнения файла. В этом случае придется заново создать (реорганизовать) файл, выделив для него объем памяти в соответствии с возросшими требованиями. Индексно-последовательный файл В индексно-последовательном файле физически записи могут быть размещены в произвольном порядке, но логический порядок следования записей определяется их ключами. Пространство, выделенное под индексно-последовательный файл, состоит из трех функционально различных областей: основной области, области переполнения, индексной области. Основная часть предназначена для запоминания блоков при первоначальном создании или реорганизации файла. В область переполнения помещаются новые блоки, для которых не оказалось места в основной памяти. Индексная область содержит иерархическую систему справочников (индексов), которые используются для доступа к индекснопоследовательному файлу. Индексы образуют иерархическую древовидную структуру, с которой тесно связано понятие Б-дерева. Б-деревья и операции с ними будут подробно рассмотрены в следующем семестре. Важная особенность индексно-последовательного файла - возможность осуществлять как прямой, так и последовательный доступ к записям. При прямом доступе сначала по ключу записи определяется по ключу ее физический адрес, после чего обращение к ней осуществляется, как в ФПД. При последовательном доступе просматривается последовательность ключей в их логическом порядке и происходит обращение к записям в порядке просмотра их ключей. 42 РАЗДЕЛ 4. Словарь терминов (глоссарий) Методы разработки эффективных алгоритмов Метод балансировки декомпозиции Сложность алгоритма временная максимальная полиномиальная практическая пространственная средняя теоретическая Умножение быстрое Структуры данных (СД) Вектор характеристический Дек Дерево бинарное (двоичное) Массив разреженный Множество Очередь Список кольцевой (циклический) линейный с двумя связями Стек Сортировка Сортировка быстрая (Хоара) древесная извлечением включением внешняя внутренняя вычерпыванием лесом обменами подсчётами пузырьковая распределением слиянием 43 цифровая Шелла Быстрый поиск Б-дерево Дерево поиска Дерево поиска сбалансированное (AVL-дерево) Поиск дихотомический (логарифмический) последовательный Хэш-таблица РАЗДЕЛ 5. Практикум по решению задач (практических ситуаций) по темам лекций (одна из составляющих частей итоговой государственной аттестации) Методы разработки эффективных алгоритмов Задание 1.1. Определить, какую задачу решает алгоритм, реализуемый данным программным кодом, а также найти теоретическую временную сложность этого алгоритма. ... i:=2; while (i<trunc(sqrt(n))) and (n mod i<>0) do i:=i+1; if (n mod i = 0) then max_d:=n/i else max_d:=1; ... Решение. Здесь определяется наибольший делитель max_d натурального числа n, отличный от самого n (если n - простое, то результат - единица). Сложность алгоритма определяется количеством выполнения цикла, т.е. составляет O( n ). Задание 1.2. Определить, какую задачу решает алгоритм, реализуемый данным программным кодом, а также найти теоретическую временную сложность этого алгоритма. #define MAX_SIZE 101 #define MAX_NUMBER 210 int n, a[MAX_SIZE]; // глобальные void Pro() { int i,j,k,min,p_min; NODE buf; for (i=0; i<n-1; i++) { min=a[i].key; p_min=i; for (j=i+1; j<n; j++) { if (a[j].key<min) { min=a[j].key; p_min=j; }; }; buf=a[i]; a[i]=a[p_min]; a[p_min]=buf; }; } 44 Решение. Здесь сортируется массив записей. Данная процедура содержит два цикла, один из которых вложен в другой. Каждый из циклов отрабатывает порядка n раз; поэтому сложность алгоритма составляет O(n2). Задание 1.3. Используя метод быстрого умножения, перемножить числа: x = 2012 и y = 3301. Решение. Разобьем x и y на две равные части: x = a|b, y = c|d, где a, b, c, d − n/2 разрядные числа. Тогда их произведение можно представить в виде: x y=(a 2n/2 +b) (c 2n/2 +d)=ac 2n+(ad+bc) 2n/2+bd. Это равенство дает способ вычисления произведения x и y с помощью четырех умножений n/2 -разрядных чисел, а также нескольких сложений и сдвигов: begin u:=(a+b) (c+d); z:=v 2n+(u-v-w) v:=a c; w:=b d; 2n/2+w {*z=xy*} end При x = 2012 и y = 3301 имеем: a=20, b=12, a+b=32; c=33, d=01, c+d=34; u=32*34: пусть x0=32, y0=34. a0=3, b0=2, a0+b0=5; c0=3, d0=4, a0+b0=7; u0=5*7=35; v0=3*3=9; w0=2*4=8; u=32*34= x0*y0=900+(35-9-8)*10+8=1088; v=20*33=660; w=12*01=12; xy=6600000+(1088-660-12)*100+12=6641612. Ответ: 6641612. Задачи для самостоятельного решения Задание 1.4. Определить, какую задачу решает алгоритм, реализуемый данным программным кодом, а также найти теоретическую временную сложность этого алгоритма. i:=n-1; while (n mod i <> 0) do i:=i-1; max_d:=i; Задание 1.5. Используя метод быстрого умножения, перемножить числа: x = 1107 и y = 2015. 45 Структуры данных (СД) Задание 2.1. Разработать и реализовать процедуру добавления элементов в стек, представленный цепным способом. Решение. Объявим следующие глобальные переменные: type ptr=^node; node=record d:char; p:ptr end; var ch:char; top,k:ptr; Тогда процедура добавления элементов в стек выглядит следующим образом: procedure AddFILO(var top:ptr; var ch:char); begin new(k); k^.d:=ch; k^.p:=top; top:=k end; {*AddFILO*} Задание 2.2. Разработать и реализовать процедуру удаления элементов из стека, представленного цепным способом. Решение. Объявим следующие глобальные переменные: type ptr=^node; node=record d:char; p:ptr end; var ch:char; top,k:ptr; Тогда процедура удаления элементов из стека выглядит следующим образом: procedure DelFILO(var top:ptr); begin k:=top; top:=top^.p; dispose(k) end; {*DelFILO*} Задание 2.3. Разработать и реализовать процедуру добавления элементов в очередь, представленную цепным способом. Решение. Объявим следующие глобальные переменные: type ptr=^node; node=record d:char; p:ptr end; var ch:char; head,tail,k:ptr; Тогда процедура добавления элементов в очередь выглядит следующим образом: procedure AddFIFO(var tail:ptr; var ch:char); begin new(k); k^.d:=ch; k^.p:=nil; tail^.p:=k; tail:=k end; {*AddFIFO*} Задание 2.4. Разработать и реализовать процедуру удаления элементов из очереди, представленной цепным способом. Решение. Объявим следующие глобальные переменные: type ptr=^node; node=record d:char; p:ptr end; var ch:char; head,tail,k:ptr; Тогда процедура удаления элементов из очереди выглядит следующим образом: procedure DelFIFO(var head:ptr); 46 begin k:=head; head:=head^.p; dispose(k) end; {*DelFIFO*} Задание 2.5. Разработать и реализовать процедуру добавления элементов в список с двумя связями, представленный цепным способом. Решение. Объявим следующие глобальные переменные: type ptr=^node; node=record d:char; lp,rp:ptr end; var ch:char; left,right,k,q:ptr; Тогда процедура добавления элементов выглядит следующим образом: Procedure AddDblList(var q:ptr; var ch:char); var qr:ptr; begin new(k); k^.d:=ch; if q=nil then {***** add to empty list *****} begin k^.lp:=nil; k^.rp:=nil; left:=k; right:=k end else if q=right then {***** add to end of list *****} begin k^.lp:=q; k^.rp:=nil; q^.rp:=k; right:=k end else {***** add to med. of list *****} begin qr:=q^.rp; k^.lp:=q; k^.rp:=qr; q^.rp:=k; qr^.lp:=k; end end; {* AddDblList *} Задание 2.6. Разработать и реализовать процедуру удаления элементов из списка с двумя связями, представленного цепным способом. Решение. Объявим следующие глобальные переменные: type ptr=^node; node=record d:char; lp,rp:ptr end; var ch:char; left,right,k,q:ptr; Тогда процедура удаления элементов выглядит следующим образом: Procedure DelDblList(var q:ptr); var ql,qr:ptr; begin if (q=left) and (q=right) then {***** 1 node in list *****} begin left:=nil; right:=nil; end else if (q=left) then {***** delete head *****} begin qr:=q^.rp; qr^.lp:=nil; left:=qr; end else if (q=right) then {***** delete tail *****} begin ql:=q^.lp; ql^.rp:=nil; right:=ql; end else {***** delete from med. *****} begin ql:=q^.lp; qr:=q^.rp; ql^.rp:=qr; qr^.lp:=ql; end; dispose(q) end; end; {* DelDblList*} 47 Задание 2.7. Разработать и реализовать процедуру обхода бинарного дерева, представленного цепным способом, а) в прямом порядке; б) в обратном порядке Решение. а) procedure PreOrder (t: ptr); begin if t<>nil then begin P(t); PreOrder(t^.left); PreOrder(t^.right) end end; {*PreOrder*} б) procedure PostOrder (t: ptr); begin if t<>nil then begin PostOrder(t^.left); PostOrder(t^.right); P(t) end end; {*PostOrder*} Задачи для самостоятельного решения Задание 2.8. а) Разработать и реализовать процедуру добавления элементов в кольцевой список, представленный цепным способом. б) Разработать и реализовать процедуру удаления элементов из кольцевого списка, представленного цепным способом. Задание 2.9. Разработать и реализовать процедуру обхода бинарного дерева, представленного цепным способом, во внутреннем порядке. Сортировка Задание 3.1. Отсортировать массив с пошаговой иллюстрацией (по шагам внешнего цикла) методом простого включения: 38, 14, 0, 9, 89, 7. Решение. Будем сравнивать по очереди ключ i-й записи с ключами записей: i1-й, i2-й, … до тех пор, пока не обнаружим, что i-ю запись следует вставить между записями j-й и j+1й. Подвинув записи с j+1-й по i1-ю на одну позицию вправо, поместим новую запись в позицию j+1. Шаг 1. Элемент с ключом 14 встал на свое место относительно предшествующего ему элемента с ключом 38: 14, 38, 0, 9, 89, 7. Шаг 2. Элемент с ключом 0 встал на свое место относительно предшествующих ему элементов с ключами 14, 38: 0, 14, 38, 9, 89, 7. Шаг 3. Элемент с ключом 9 встал на свое место относительно предшествующих ему элементов с ключами 0, 14, 38: 0, 9, 14, 38, 89, 7. Шаг 4. Ситуация не изменилась, так как элемент с ключом 89 уже стоит на своём месте относительно предшествующих ему элементов. 48 Шаг 5. Элемент с ключом 7 встал на свое место: 0, 7, 9, 14, 38, 89. Задание 3.2. Разработать процедуру, реализующую сортировку массива записей методом простого включения. Решение. Объявим следующие переменные и константы: #define MAX_SIZE 101 #define MAX_NUMBER 210 int n, a[MAX_SIZE]; // глобальные Тогда сортировка методом простого включения выглядит следующим образом: void VklSort() { int i,j,k; NODE buf; for (i=1; i<n; i++) { buf=a[i]; k=a[i].key; for (j=i-1; j>=0 && a[j].key>k; j--) {a[j+1]=a[j];} a[j+1]=buf; }; } Задание 3.3. Разработать процедуру, реализующую сортировку массива записей методом Шелла. Решение. Объявим следующие переменные и константы: #define MAX_SIZE 101 #define MAX_NUMBER 210 int n, a[MAX_SIZE]; // глобальные Тогда сортировка методом Шелла выглядит следующим образом: void ShellSort() { int h,i,j,k,y,kh; NODE buf; for (h=1; h<(n/9); h=3*h+1); while (h>0) { for (k=0; k<h; k++) { for (i=k+h; i<n; i+=h) { buf=a[i]; y=buf.key; for (j=i-h; j>=0 && y<a[j].key; j-=h) {a[j+h]=a[j];}; a[j+h]=buf; } } h/=3; } } Задание 3.4. Реорганизовать массив 26, 9, 34, 1, 12, 37, 1, 38, 23 относительно главного элемента 23, используя принцип быстрой сортировки. Решение. Двигаясь от элемента с индексом first вправо, находим элемент с индексом f, ключ которого больше ключа главного элемента. Двигаясь от элемента с индексом last влево, находим элемент с индексом l, ключ которого меньше ключа главного элемента. Если оказалось, что элемент с индексом f расположен левее элемента с индексом l, то меняем их местами. Дальше движение влево продолжают с позиции f+1, движение вправо с позиции l1 до тех пор, пока f l. Шаг 1. f=0, a[f]=26, l=6, a[l]=1; новый массив: 1, 9, 34, 1, 12, 37, 26, 38, 23. 49 Шаг 2. f=2, a[f]=34, l=4, a[l]=12; новый массив: 1, 9, 12, 1, 34, 37, 26, 38, 23. Шаг 3. f=4, a[f]=34, l=3, a[l]=1. Так как f>l, процесс завершен. Первый подмассив с индексами first..l: 1, 9, 12, 1. Второй подмассив с индексами f.. last: 34, 37, 26, 38, 23. Задание 3.5. Разработать процедуру, реализующую сортировку массива записей методом Хоара (быстрая сортировка). Решение. Объявим следующие переменные и константы: #define MAX_SIZE 101 #define MAX_NUMBER 210 int n, a[MAX_SIZE]; // глобальные Тогда сортировка методом Хоара выглядит следующим образом: void QuickSort(int first, int last) { int f,l,y,m,i; NODE buf; f=first; l=last; randomize; m=first+random(last-first); y=a[m].key; do {while (a[f].key < y) {f++;}; while (a[l].key > y) {l--;}; if (f<=l) {buf=a[f]; a[f]=a[l]; a[l]= buf; f++; l--;}; } while (f<=l); if (l>first) {QuickSort(first,l);}; if (f<last) {QuickSort(f,last);}; } Задание 3.6. Разработать процедуру, реализующую сортировку массива записей методом извлечения. Решение. Объявим следующие переменные и константы: #define MAX_SIZE 101 #define MAX_NUMBER 210 int n, a[MAX_SIZE]; // глобальные Тогда сортировка методом извлечения выглядит следующим образом: void IzvlSort() { int i,j,k,min,p_min; NODE buf; for (i=0; i<n-1; i++) { min=a[i].key; p_min=i; for (j=i+1; j<n; j++) { if (a[j].key<min) { min=a[j].key; p_min=j; }; }; buf=a[i]; a[i]=a[p_min]; a[p_min]=buf; }; } Задание 3.7. Разработать процедуру, реализующую сортировку массива записей методом подсчётов. Решение. Объявим следующие переменные и константы: #define MAX_SIZE 101 #define MAX_NUMBER 210 int n, a[MAX_SIZE]; // глобальные Тогда сортировка подсчётами выглядит следующим образом: void CountSort() 50 { int i,j,k; int count[MAX_NUMBER+1]; NODE b[MAX_SIZE]; for (j=0; j<=MAX_NUMBER; j++) {count[j]=0;}; for (i=0; i<n; i++) {b[i]=a[i];}; for (i=0; i<n; i++) {count[b[i].key]++;}; for (j=1; j<=MAX_NUMBER; j++) {count[j]+=count[j-1];}; for (i=0; i<n; i++) {k=count[b[i].key]; a[k-1]=b[i]; /* "a" c 0 pos!!! -> "-1" */ count[b[i].key]--;}; } Задачи для самостоятельного решения Задание 3.8. Разработать процедуру, реализующую сортировку массива натуральных чисел, максимальное из которых не превышает 10000, методом цифровой сортировки. Задание 3.9. Разработать процедуру, реализующую сортировку массива записей методом древесной сортировки. Быстрый поиск Задание 4.1. Разработать и реализовать процедуру дихотомического (логарифмического) поиска в упорядоченном массиве. Решение. Функция Locate осуществляет дихотомический поиск номера записи с заданным ключом. Вызов функция осуществляется оператором “num = Locate(0,n1);”. Если искомый элемент в массиве не найден, функция возвращает значение «1». int Locate(int first, int last) { int number,med; number = 1; if (first <= last && number = = 1) { med = (first+last) / 2; if (y = = a[med].key) /* Искомый элемент найден */ {number = med;} else if (first!=last) /* Если вершина не концевая, ... */ { if (y<a[med].key) {number = Locate(first,med);} /* Продолжить поиск слева */ else {number = Locate(med+1,last);} */ }; }; return(number); }. 51 /* Продолжить поиск справа Задание 4.2. В дерево поиска добавить вершину с ключом 10 и удалить вершину с ключом 18. Решение. Спустившись вдоль пути поиска, цепляем новую вершину с ключом 10 слева от вершины с ключом 11. Заменив элемент 18 на элемент в самой правой вершине левого поддерева вершины с элементом 18, получим: Задание 4.3. Разработать и реализовать процедуру добавления элементов в дерево поиска, представленное цепным способом. Решение. Объявим следующие глобальные переменные: typedef struct s_tree {int key; /* data"s field */ int count; /* count of key's number*/ struct s_tree *lptr; /* pointer to left son */ struct s_tree *rptr; /* pointer to right son */ } NODE; NODE *root; /* pointer to tree's root */ Тогда процедура добавления элементов выглядит следующим образом: void SearchOrAdd(int y, NODE *&pv) { if (pv= =NULL) /* no such element */ { if ( (pv = (NODE *) malloc(sizeof(NODE))) = = NULL ) {printf("Not enougth memory to allocate new node\n");} else { pv->key=y; pv->count=1; pv->lptr=NULL; pv->rptr=NULL;}; } else {if(y<pv->key) {SearchOrAdd(y, pv->lptr);} else if(y>pv->key) {SearchOrAdd(y, pv->rptr);} else {pv->count++;}; }; } Задание 4.4. Разработать и реализовать процедуру удаления элементов из дерева поиска, представленного цепным способом. 52 Решение. Объявим следующие глобальные переменные: typedef struct s_tree {int key; /* data"s field */ int count; /* count of key's number*/ struct s_tree *lptr; /* pointer to left son */ struct s_tree *rptr; /* pointer to right son */ } NODE; NODE *root; /* pointer to tree's root */ Тогда процедура удаления элементов выглядит следующим образом: void Delete(int y, NODE *&pv) { NODE *buf; if (pv= =NULL) /* no such element */ {printf("There's no element with key %d \n",y); exit;} else {if(y<pv->key) {Delete(y, pv->lptr);} else if(y>pv->key) {Delete(y, pv->rptr);} else { /* we find need element */ if (pv->count>1) {pv->count--;} else { /* del */ buf=pv; if(buf->lptr ==NULL) /* no left son */ {pv=buf->lptr;} else if (pv->rptr ==NULL) /* no right son */ {pv=buf->rptr;} else /* both sons - left and right */ {Del(buf, buf->lptr);}; free(buf); } } }; } Здесь используется процедура Del, которая непосредственно производит удаление данного узла: void Del(NODE *&pv, NODE *&pv1) {if (pv1->rptr!=NULL) { Del(pv,pv1->rptr); } else { pv->key=pv1->key; pv->count=pv1->count; pv=pv1; pv1=pv1->lptr; }; } Задание 4.5. Разработать программный код, реализующий однократный LL-поворот при балансировке AVL-дерева, представленного цепным способом. 53 Решение. Однократный LL-поворот производит следующая последовательность операторов: p->lptr = pl->rptr; {* дерево “цепляется” к вершине V слева *} pl->rptr = p; {* вершина V “цепляется” к вершине W справа *} p->balance = 0; {* показатель сбалансированности вершины V равен нулю *} p = pl; {* предок вершины V ссылается теперь на вершину W *} p->balance = 0; {* показатель сбалансированности вершины W равен нулю *} Задание 4.6. На рисунке 4.4 представлено Б-дерево, у которого n = 2. Добавить в это Б-дерево элемент с ключом 30. Рисунок 4.4. Решение. Спустившись вдоль пути поиска, убеждаемся, что на нужной странице А (23, 25, 29, 34) уже находится максимальное количество элементов; поэтому создаём чистую страницу В; элементы со страницы А вместе с новым элементом поровну распределяем между страницами А и В, причем элемент со средним ключом переносим на один уровень вверх на родительскую страницу. На следующем рисунке показано, как будет выглядеть левый фрагмент Б-дерева после добавления элемента с ключом 30. 54 Задачи для самостоятельного решения Задание 4.7. В AVL-дерево добавить вершину с ключом 10 и произвести балансировку. Задание 4.8. Из Б-дерева, изображённого на рисунке 4.4, удалить элемент с ключом 80. Раздел 6. Изменения в рабочей программе, которые произошли после утверждения программы. Характер изменений в программе Номер и дата протокола заседания кафедры, на котором было принято данное решение Подпись заведующего кафедрой, утверждающего внесенное изменение Подпись декана факультета (проректора по учебной работе), утверждающего данное изменение Раздел 7. Учебные занятия по дисциплине ведут: Ф.И.О., ученое звание и степень преподавателя Минц Ю.А., ассистент Ланина Н.Р., к.т.н. Ланина Н.Р., к.т.н. Учебный год Факультет 2010-2011 ФМОИП 2011-2012 ФМОИП 2012-2013 ФМОИП 55 Специальность Прикладная математика и информатика Прикладная математика и информатика Прикладная математика и информатика 56