Текст
                    БИНОМ
Структуры данных
Data Structures iwitft C++
William Ford
Unive'S'ty of the Pacific
William Topp
University of ihe Pacific
Уильям Толп, Уильям Форд
Структуры данных В C+ +
Перевод с английского под редакцией В. Кузьменко
Ж
Москва
ЗАО «Издательство БИНОМ»
Содержание
Предисловие.....................................................  13
Глава 1. Введение...............................................  19
1-1.	Абстрактные типы данных..................................... 20
ADT — формат............................................... 21
1.2.	Классы C++ и абстрактные типы................................24
Инкапсуляция в скрытие информации...........................24
Передач» сообщений........................................  25
1.3.	Объекты в приложениях C++ .................................  25
Приложение: класс Circle ...................................25
1.4.	Разработка объектов..........................................28
Объекты и композиция........................................28
C++ геометрические классы ..................................30
Объекты и наследование......................................30
Наследование в программировании.........................  •	31
Упорядоченные списки и наследование.........................34
Повторное использование кода................................35
Спецификации класса SeqList и OrderedLiat...................36
1.5.	Приложения с наследованием классов...........................37
1.6.	Разработка объектно-ориентированных программ ................38
Анализ задачн/опраделение программы.........................39
Разработка................................................  39
Кодирование.................................................10
Тестирование................................................40
Иллюстрация программной разработки: Dice график ........  .	-10
1.7.	Тестирование н сопровождение программы.......................45
Объектное тестирование......................................45
Тестирование управляющего модуля ...........................45
Программное сопровождение и документирование................46
1.8.	Язык программирования Ст+.................................... . 47
1.9.	Абстрактные базовые классы и полиморфизм.....................48
Полиморфизм и динамическое связывание.......................48
Письменные упражнения............................................. . 50
Глава 2. Базовые типы данных .....................................53
2.1.	Целочисленные типы...........................................54
Компьютерное хранение целых чисел...........................56
Данные в памяти ............................................57
Представление целых в языке C++.............................58
2.2.	Символьные типы..............................................58
Символы ASCII ............................................  58
2.3,	Вещественные типы............................................60
Представление вещественных чисел..........................  60
2.4.	Типы перечисления............................................62
Реализация типов перечисления C++...........................62
2.5.	Указатели....................................................63
Указ	атели ADT ..........................................  63
Значения указателя.........................................65
2.0.	Массив (array)...............................................65
Встроенный тип массива C++.................................66
Сохранение одномерных массивов . ..........................66
Границы массив»........................................    67
Двумерные массивы..........................................68
Сохранение двумерных массивен..............................69
2.7.	Строковые константы и переменные ......	 71
Строки C++...............................................  73
Приложение: перестановка имен..............................74
2.8.	Заппси.......................................................76
Структуры C++ .............................................77
2.9.	Файлы........................................................77
Иерархия потоков С—*•......................................80
2.10.	Приложения массива и записи ................................82
Последовательный поиск.....................................82
Обменная сортировка........................................85
Подсчет зарезервированных слов C++.........................87
Письменные упражнения.............................................90
Упражнения по программированию....................................96
Глава 3. Абстрактные типы данных и классы . ......................99
3.1.	Пользовательский тип — КЛАСС............................... 100
Объявление класса.........................................100
Конструктор...............................................101
Объявление объекта........................................102
Реализация класса.........................................102
Реализация конструктора ..................................103
Создание объектов.........................................101
3.2.	Примеры классов.............................................107
Класс Temperature.........................................108
Класс случайных чисел ....................................110
3.3.	Объекты и передача информации...............................114
Объект как возвращаемое значение......................... 115
Объект как параметр функции...............................115
3.4.	Мессины объектов............................................116
Конструктор умолчания.....................................117
3-5. Множество иные конструкторы ................................117
3-6. Практическое применение: 'Греугольныс матрицы...............120
Свойства верхней треугольной матрицы......................121
Класс TriMat..............................................124
Письменные упражнения............................................129
Упражнения по программированию ..................................133
Глава 4. Классы коллекций........................................143
4.1.	Описание лилейных коллекций.................................146
Коллекции с прямым доступом...............................147
Коллекции с последовательным доступом.....................148
Универсальная индексация..................................151
4.2.	Описание нелинейных коллекций...............................152
Коллекции 14>уип....................................  ....	153
4.3.	Айал ио алгоритмов.............. ..........................155
Критерии эффективности....................................155
Общий порядок величин.....................................159
4.4.	Последовательный и бинарный	поиск.........................  161
Бинарный поиск............................................162
4.5.	Базовый класс последовательного списка......................166
Методы модификации списка.................................169
Письменные упражнения..........................................  175
Упражнения по программированию	. .............................178
Глава б. Стеки и очереди ......................................  181
5.1.	Стеки ......................................................182
5.2.	Класс Stack ................................................184
5.3.	Оценка выражений............................................193
Постфиксная оценка........................................194
Применение: постфиксный калькулятор.....................  195
5.4.	Очереди.....................................................198
5.5.	Класс Queue.................................................201
5-6- Очереди приоритетов.........................................212
Класс PQueue..............................................214
Приложение: службы поддержки компании.....................217
5.7.	Практическое применение:
Управляемое событиями моделирование..........................220
Разработка приложения ....................................221
Информация моделирования ...............................  224
Установка параметров моделирования......................  226
Выполнение задачи моделирования........................... . 226
Письменные упражнения..........................................  232
Упражнения по программированию...................................236
Глава 6. Абстрактные операторы ................................  239
6.1.	Описание перегрузки операторов..............................241
Определяемые пользователем внешние функции................241
Члены класса............................................  242
Дружественные функции.....................................244
6.2.	Системе рациональных чисел .................................245
Представление рациональных чисел . .......................245
Арифметика рациональных чисел.............................246
Преобразование рациональных чисел.........................247
6.3.	Класс Rational..............................................247
6Л. Операторы класса Rational как функции-члены..................249
Реализация операторов класса Rational.....................249
6.5.	Операторы потока класса Rational как дружественные функции . 250
Реализация операторов потока класса Rational..............251
6.6.	Преобразование рациональных чисел  ........................252
Преобразование в объектный тип............................252
Преобразование из объектного типа.........................253
6.7.	Использование рациональных чисел ...........................254
Письменные упражнения . ........................................258
Упражнения по программированию..................................265
Глава 7. Параметризованные типы данных..........................269
7.1.	Шаблонные функции..........................................270
Сортировка на базе шаблона...............................273
7.2.	Шаблонные классы...........................................273
Определение шаблонного класса............................274
Объявление объектов шаблонного класса....................274
Определение методов шаблонного класса . ...............  274
7.3.	Шаблонные списковые классы.................................276
7.4.	Вычисление инфиксного выражения .........................  277
Письменные упражнения...........................................285
Упражнения по программированию..................................286
Глава 8. Классы и динамическая память...........................289
8.1.	Указатели и динамические структуры данных..................291
Оператор new для выделения памяти........................292
Динамическое выделение массива...........................292
Оператор delete освобождения памяти......................293
8.2.	Динамически создаваемые объекты............................293
Освобождение данных объекта: деструктор..................295
8.3.	Присваивание и инициализация...............................297
Проблемы присваивания.................................  .297
Перегруженный оператор присваивания......................299
Указатель this...........................................300
Проблемы инициализации...................................300
Создание конструктора копирования .....................  301
8.4.	Надежные массивы...........................................303
Класс Array..............................................303
Выделение памяти для класса Array........................305
Проверка границ массива и перегруженный оператор (]......306
Преобразование объекта в указатель.......................307
Использование класса Array...............................309
8.5.	Класс String ..............................................310
Реализация класса String.................................315
8.6.	Сопоставление с образцом ................................  320
Процесс Find.............................................320
Алгоритм сопоставления с образцом........................321
Анализ алгоритма сопоставления с образцом................325
8.7.	Целочисленные множества....................................325
Множества целочисленных типов..........................  326
Побитовые операторы C++..................................327
Представление элементов множества .....................  329
Решето Эратосфена........................................332
Письменные упражнения...........................................336
Упражнения по программированию................................  345
Глава 9. Связанные списки.......................................349
Описание связанного списка ............................  351
Обзор главы .......................................      352
9.1*	Класс Node .................................................353
Объявление типа Node......................................353
Реализация класса Node....................................356
9.2.	Создание связанных списков..................................358
Создание узла.............................................358
Вставка узла: InsertFront...............................  358
Прохождение по связанному списку..........................359
Вставка узла: InsertRear..................................361
Приложение: Список выпускников............................365
Создание упорядоченного списка............................367
Приложение; сортировка со связанными списками.............369
9.3*	Разработка класса связанного списка.........................371
Даниые-члены связанных списков ...........................371
Операции связанных списков................................372
9.4.	Класс LinkedList............................................374
Конкатенация двух списков ................................377
Сортировка списка ...............-........................377
9.5.	Реализация класса LinkedList................................381
9.6.	Реализация коллекций со связанными списками ................388
Связанные очереди.........................................389
Реализация методов Queue..................................390
Использование объекта LinkedList с классом SeqList .......391
Реализация методов доступа к данным класса SeqList........392
Приложение: Сравнение реализаций SeqList..................392
9.7.	Исследовательская задача: Буферизация печати................394
Анализ проблемы ..........................................394
Разработка программы......................................395
Реализация метода UPDATE для класса Spooler...............397
Методы оценки системы буферизации печати..................398
9.8.	Циклические списки..........................................400
Реализация класса CNode ..................................402
Приложение: Решение задачи Джозефуса....................  403
9.9.	Двусвязные списки.........................................  406
Приложение: Сортировка двусвязного списка.................408
Реализация класса DNode...................................410
9.10.	Практическая задача: Управление окнами.....................411
Список окон...............................................412
Реализация класса WindowList..............................415
Письменные упражнения............................................418
Упражнения по программированию...................................426
Глава 10. Рекурсия...............................................431
10.1.	Понятие рекурсии...........................................432
Рекурсивные определения...................................433
Рекурсивные задачи......................................  435
10.2.	Построение рекурсивных функций.............................439
10.3.	Рекурсивный код и стек времени исполнения..................443
Стек времени исполнения ..................................444
10.4.	Решение задач с помощью рекурсии...........................445
Бинарный поиск............................................446
Комбинаторика: задача о комитетах.........................448
Комбинаторика: перестановки...............................451
Прохождение лабиринта.......................♦	460
Реализация класса Maze.......................***..•• 463
10.5.	Оценка рекурсии........................................    4®6
Письменные упражнения............................................470
Упражнения по программированию.............................  .	- 473
Глава 11. Деревья..............................................  477
Терминология деревьев.....................................479
Бинарные деревья........................................  480
11.1.	Структура бинарного дерева.................................483
Проектирование класса TreeNode .........................  483
Построение бинарного дерева ..............................485
11.2.	Разработка функций класса TreeNode.........................487
Рекурсивные методы прохождения деревьев...................489
Симметричный метод прохождения дерева.....................489
11.3.	Использование алгоритмов прохождения деревьев..............492
Приложение: посещение узлов дерева .......................492
Приложение: печать дерева ................................493
Приложение: копирование и удаление деревьев...............495
Приложение: вертикальная печать дерева....................500
11.4.	Бинарные деревья поиска....................................503
Ключ в узле бинарного дерева поиска ......................505
Операции на бинарном дереве поиска........................506
Объявление абстрактного типа деревьев.....................507
11.5.	Использование бинарных деревьев поиска.....................510
Дублированные узлы.......................................  513
11.6.	Реализация класса BinSTree.................................515
Операции обработки списков................................516
11.7.	Практическая задача: конкорданс............................525
Письменные упражнения............................................529
Упражнения по программированию...................................536
Глава 12. Наследование и абстрактные классы......................539
12.1.	Понятие о наследовании.....................................540
Терминология наследования.................................542
12.2.	Наследование в C++.........................................543
Конструкторы и производные классы.........................544
Что нельзя наследовать....................................550
12.3.	Полиморфизм и виртуальные функции......................  .	550
Демонстрация полиморфизма.................................553
Приложение: геометрические фигуры и виртуальны© методы . . . 556
Виртуальные методы и деструктор...........................558
12.4.	Абстрактные базовые классы ..............................  559
Абстрактный базовый класс List............................560
Образование класса SeqList из абстрактного базового класса List . 561
12.5.	Итераторы..................................................563
Абстрактный базовый класс Iterator.......................  564
Образование итераторов для списка ........................564
Построение итератора SeqList..............................565
Итератор массива .........................................569
Приложение: слияние сортированных последовательностей.....570
Реализация класса Arrayiterator...........................574
12.6.	Упорядоченные списки........................................575
12.7.	Разнородные списки..........................................579
Разнородные массивы .......................................579
Разнородные связанные списки..............................581
Письменные упражнения...........................................  586
Упражнения по программированию....................................595
Глава 13. Более сложные нелинейные структуры......................599
13.1.	Бинарные деревья, представляемые массивами .................600
Приложение: турнирная сортировка .......................  602
13.2.	Пирамиды ...................................................607
Пирамида как список.......................................607
Класс Heap................................................609
13.3.	Реализация класса Heap......................................612
Приложение: пирамидальная сортировка......................618
13	4. Очереди приоритетов.......................................621
Приложение: длинные последовательности....................622
13.5.	AVL-деревья ................................................627
Узлы AVL-дерева.........................................  628
13.6.	Класс AVLTree...............................................631
Распределение памяти для AVLTree .........................633
Оценка сбалансированных деревьев..........................640
18.7.	Итераторы деревьев..........................................642
Итератор симметричного метода прохождения.................643
Реализация класса Inorderiterator.........................644
Приложение: алгоритм TreeSort ............................646
13.8. Графы.......................................................647
Связанные компоненты......................................648
13.9. Класс Graph ................................................649
Объявление абстрактного типа данных Graph.................649
Реализация класса Graph...................................653
Способы прохождения графов................................656
Приложения................................................659
Достижимость и алгоритм Уоршалла..........................666
Письменные упражнения.............................................669
Упражнения по программированию....................................678
Глава 14. Организация коллекций...................................683
14.1.	Основные алгоритмы сортировки массивов......................684
Сортировка посредством выбора.............................684
Сортировка методом пузырька...............................686
Вычислительная сложность сортировки методом пузырька......688
Сортировка вставками......................................688
14.2.	"Быстрая сортировка”........................................690
Описание "быстрой сортировки"...........................  690
Алгоритм Quicksort........................................693
Сравнение алгоритмов сортировки массивов..................696
14.3.	Хеширование.................................................700
Ключи и хеш-функция.......................................701
Хеш-функции...............................................702
Другие методы хеширования...............................704
Разрешение коллизий.....................................704
14.4.	Класс хеш-таблиц........................................  707
Приложение: частота символьных строк ...................709
Реализация класса HashTable...........................  711
Реализация класса HashTablelterator.....................712
14.5.	Производительность методов поиска.........................714
 14.6. Бинарные файлы и операции с данными на внешних носителях 715
Бинарные файлы..........................................716
Класс BinFile ........................................  718
Внешний поиск...........................................723
Внешняя сортировка......................................727
Сортировка естественным слиянием........................729
14.7. Словари...................................................735
Письменные упражнения.........................................  742
Упражнения по программированию..................................748
Приложение.
Ответы на избранные письменные упражнения.......................753
Предметный указатель............................................775
Index...........................................................795
Предисловие
Книга посвящается Дэвиду Джонстоуну, редактору.
Он разделял наше видение предмета. Несмотря на его трагическую гибель в результате акта бессмысленного насилия, мы сохранили это видение в нашей работе. Мы надеемся, что это — вклад, достойный его памяти.
Эта книга предназначена для представления основных структур данных с точки зрения объектно-ориентированной перспективы. Изучение структур данных является ядром 'курса обучения информатике. Оно предоставляет богатый контекст для изучения методов решения задач и разработки программ и использует мощные конструкции и алгоритмы программирования.
Эта книга использует гибкий язык C++, классы й объектно-ориентированные конструкции которого конкретно предназначаются для эффективной реализации структур данных. Хотя существует ряд объектно-ориентированных языков, C++ имеет преимущество вследствие его развития из популярного языка программирования С и использования многими продавцами программного обеспечения. Мы развиваем каждую структуру данных вокруг понятия абстрактного типа данных (abstract data type, ADT), которое определяет как организацию данных, так и операции их обработки. Нас поддерживает C++, обеспечивающий тип класса для представления ADT и эффективное использование этих структур в каком-либо объекте.
Структура книги
Книга "Структуры данных в C++" организует изучение структур данных вокруг классов коллекций, которые включают списки, деревья, множества, графы и словари. В процессе изучения мы охватываем основные темы структур данных и разрабатываем методологию объектно-ориентированного программирования. Эти структуры и методология реализуются в ряде законченных программ и практических задач. Для оценки эффективности алгоритмов мы вводим понятие записи "Big-O”.
В главах 1-11 излагаются традиционные темы первого курса по структурам данных (CS 2). Формальная трактовка наследования и виртуальных функций приводится в главе 12, и эти темы используются для реализации структур данных повышенной сложности в главах 13 и 14. Материал в главах 12-14 определяет темы, традиционно излагаемые в курсе по структурам данных/ал-горитмам повышенной сложности (CS 7) и в курсе по продвинутому программированию. Мы включаем подробную разработку шаблонов и перегрузку операторов для поддержки общих структур и применяем эти мощные конструкции языка C++, чтобы упростить использование структур данных.
Профессиональный программист может использовать "Структуры данных в C++" как самоучитель по структурам данных, который сделает возможным
понимание большинства библиотек классов, научно-исследовательских статей и профессиональных изданий повышенной сложности.
Описание глав
В' большинстве глав книги разрабатываются абстрактные типы данных и описывается их реализация как класса C++. Объявление каждого класса и его ключевых методов также включены в эту книгу. Во многих случаях приводится полное определение, в некоторых случаях даются определения избранных методов классов. Полная реализация классов включена в программное приложение.
Глава 1. Введение
Эта глава является обзорной и знакомит с абстрактными типами данных и объектно-ориентированным программированием с использованием C++. Разрабатывается понятие ADT и относящиеся к нему атрибуты инкапсуляции данных и скрытия информации. Глава также знакомит с наследованием и полиморфизмом, которые формально излагаются в главе 12.
Глава 2. Базовые типы данных
Языки программирования предоставляют простые числовые и символьные типы, которые охватывают целые числа и числа с плавающей точкой, символьные данные и определяемые пользователем перечислимые типы. Простые типы объединяются для создания массивов, записей, строковых и файловых структур. Эта глава описывает ADT для типов языков, используя C++ в качестве примера.
Глава 3. Абстрактные типы данных и классы
В этой книге в целом формально рассматриваются абстрактные типы данных и их представление в качестве классов C++. Конкретно эта глава определяет основные понятия класса, включая данные-члены, конструкторы и определения методов.
Глава 4. Классы коллекций
Коллекция — это класс памяти с инструментами обработки данных для добавления, удаления или обновления элементов. Изучение классов коллекций находится в центре внимания этой книги. Поэтому в данной главе содержится пример различных типов коллекций, представленных в книге. Глава включает простое введение в запись "Big-O", которая позволяет определить эффективность какого-либо алгоритма. Эта запись используется на протяжение всей книги для сравнения и сопоставления различных алгоритмов. Глава завершается изучением класса SeqList, являющегося прототипом общей списочной структуры.
Глава 5. Стеки и очереди
В этой главе обсуждаются стеки и очереди, которые являются основными классами, поддерживающими данные в порядке LIFO ("последний пришел — первый вышел") и FIFO ("первый пришел — первый вышел"). В ней разрабатывается также очередь приоритетов, модифицированная версия очереди, в которой клиент всегда удаляет из списка элемент с наивысшим приоритетом. В практическом примере используются очереди приоритетов для управляемого событиями моделирования.
Глава 6. Абстрактные операторы
Абстрактный тип данных определяет набор методов для инициализации и управления данными. В этой главе мы расширяем определяемые языком программирования операторы (например, +, *, < и так далее) до абстрактных типов данных. Процесс, называемый перегрузкой операторов, переопределяет стандартные символы операторов для реализации операций в ADT. Полностью разработанный класс рациональных чисел иллюстрирует перегрузку операторов и преобразование типов, а также введение дружественных функций для перегрузки стандартных операторов ввода/вывода C++.
Глава 7. Параметризованные типы данных
C++ использует шаблонный механизм для предоставления параметризованных функций и классов, поддерживающих различные типы данных Шаблоны обеспечивают мощную параметризацию структур данных. Эту концепцию иллюстрирует основанная на шаблоне версия класса Stack и ее применение в вычислении инфиксного выражения.
Глава 8. Классы и динамическая память
Динамические структуры данных используют память, выделяемую системой во время исполнения приложения. Они позволяют определять структуры без ограничений по размеру и увеличивают возможность использования клас-сов. Однако их применение требует особого внимания. Мы вводим конструктор копирования, перегруженный оператор присваивания и методы деструктора, позволяющие правильно копировать и присваивать динамические данные, а затем освобождать их при удалении объекта. Возможности динамических данных иллюстрируют классы Array, String и Set. Эти классы используются и далее в книге.
Глава 9. Связанные списки
Использование списков для хранения и выборки данных является темой, обсуждаемой иа протяжение всей книги, поскольку списки очень важны для разработки большинства приложений данных. Эта глава знакомит со связанными списками, позволяющими выполнять динамическую обработку списков. Мы используем двойной подход, при котором сначала разрабатывается базовый класс узлов и создаются функции для добавления и удаления элементов из списка. Более абстрактный подход создает класс связанных списков со встроенным механизмом прохождения для обработки элементов в списке.
Класс LinkedList используется для реализации класса SeqList и Queue. В каждом случае объект связанного списка включается композицией. Этот подход предоставляет мощный инструмент для разработки структур данных. В этой главе обсуждаются также циклические и двусвязные списки, имеющие интересное применение. Глава содержит также практическую задачу очереди для принтера.
Глава 10. Рекурсия
Рекурсия — это важный инструмент решения задач как в информатике, так и в математике. Мы описываем рекурсию и показываем ее использование в различном контексте. Ряд приложений используют рекурсию с математическими формулами, комбинаторикой и головоломками. Последовательность Фибоначчи используется для сравнения эффективности рекурсивного алгоритма, итеративного алгоритма или прямых вычислений при определении терма последовательности.
Глава 11. Деревья
Связанные списки определяют множество узлов с последовательным доступом, начиная с головы. Эта структура данных называется линейным списком. Во многих приложениях объекты сохраняются в нелинейном порядке, в котором элемент может иметь множество последователей. В главе 11 мы вводим базовую нелинейную структуру, называемую деревьями, в которой все элементы данных происходят из единого источника — корня. Дерево является идеальной структурой для описания иерархической структуры, такой как компьютерная файловая система или таблица бизнес-отчета. В этой главе мы ограничиваем анализ бинарными деревьями, в которых каждый узел имеет самое большее два наследника. Мы разрабатываем класс TreeNode для реализации этих деревьев и представляем приложения, включающие классические алгоритмы прямого, симметричного и обратного сканирования. Бинарные деревья находят применение в качестве списочной структуры, эффективно сохраняющей большие объемы данных. Эта структура, называемая деревом бинарного поиска, реализуется в классе BinSTree. Класс представлен в практической задаче, которая разрабатывает конкорданс.
Глава 12. Наследование и абстрактные классы
Наследование является основным понятием объектно-ориентированного программирования. В этой главе обсуждаются основные свойства наследования, подробно разрабатывается его реализация в C++ и вводятся виртуальные функции как инструменты, использующие возможности наследования. Разрабатывается также понятие абстрактного базового класса с чистыми виртуальными функциями. Виртуальные функции являются основными для объектно-ориентированного программирования и используются последующими темами в этой книге. Глава знакомит с итераторами, определяющими однородный и общий механизм прохождения для различных списков и завершается примером наследования и виртуальных функций для разработки неоднородных массивов и связанных списков.
Глава 13. Нелинейные структуры повышенной сложности
Эта книга продолжает разработку бинарных деревьев и вводит дополнительные нелинейные структуры. В ней описываются основанные на массиве деревья, моделирующие массив как законченное бинарное дерево. Предоставляется также обширное изучение пирамид, и это понятие используется для реализации пирамидальной сортировки и очередей приоритетов. Хотя деревья бинарного поиска обычно являются хорошими структурами для реализации списка, вырожденные случаи могут быть неэффективными. Структуры данных предоставляют различные структуры со сбалансированной высотой, обеспечивающие быстрое среднее время поиска. Используя наследование, выводится новый класс дерева поиска, называемый AVL-деревьями. Глава завершается введением в графы, представляющим ряд классических алгоритмов.
Глава 14. Организация коллекций
В этой главе рассматриваются алгоритмы поиска и сортировки для общих коллекций и разрабатываются классические основанные на массиве алгоритмы сортировки выбором, пузырьковой сортировки и сортировки вставками. Наше исследование включает известный алгоритм "быстрой сортировки" Quicksort. В этой книге особенно выделяются данные, сохраняемые во внутренней памяти. Для более обширных множеств данные могут сохраняться на диске. Можно также использовать внешние методы для поиска и сортировки данных. Мы разрабатываем класс BinFile для прямого файлового доступа и используем его методы для иллюстрации как алгоритма внешнего индексного последовательного поиска, так и алгоритма внешней сортировки слиянием. Раздел, посвященный ассоциативным массивам, обобщает понятие индекса массива.
Необходимая подготовка
Эта книга предполагает, что читатель закончил первый курс программирования и свободно владеет-базовым языком C++. Глава 2 определяет простые структуры данных C++ и показывает их использование в нескольких законченных программах. Эта глава может использоваться как стандарт для определения необходимых предпосылок C++. Для заинтересованного читателя авторы предоставляют учебник по C++, определяющий простые типы языка и синтаксис для массивов, управляющих структур, ввода/вывода, функций и указателей. Учебник включает обсуждение каждой темы вместе с примерами, законченными программами и упражнениями.
Приложения
Полные листинги исходных кодов для всех классов н программ доступны через канал Internet ftp из University of the Pacific, где работают авторы. Код C++ в этой книге протестирован и выполнен с использованием новейшего компилятора фирмы ’Borland”. За очень небольшими исключениями, эти программы можно компилировать и выполнять в системе Macintosh, используя Symantec C++ и в системе Unix, используя GNU C++.
Те, кто имеют канал Internet, используйте адрес ftp.cs.uop.edu. После соединения с системой ваше логическое имя является анонимным, а ваш
пароль — это ваш mail-адрес Internet. Программное обеспечение находится в каталоге ”/pub/C++’'.
Читатели могут обращаться непосредственно к авторам для получения копии учебника. Информация для заказа предоставляется по электронной почте: посылайте запрос по адресу ''billf@uop.edu”, или по международной почте: пишите Bill Тор, 456 S. Regent Stockton, СА 95204.
Благодарности
Во время подготовки книги "Структуры данных в C++" авторам оказывали поддержку друзья, студенты и коллеги. University of the Pacific щедро предоставлял ресурсы и поддержку для завершения проекта. Издательство "Prentice Hall” обеспечило преданную делу команду профессионалов, выполнивших дизайн и производство книги. Мы особенно благодарны редакторам Элизабет Джоунз, Биллу Зобристу и Алану Алту и выпускающему редактору Байани де Леону. Выпуск реализован совместно Spectrum Publisher Services и Prentice Hall. Большую помощь иам оказали Келли Риччи и Кристин Миллер из Spectrum.
Студенты проявили ценный критицизм при обсуждении рукописи, обеспечивая обратную связь и непредвзятый взгляд на работу. Наши рецензенты оказывали помощь в начале работы над книгой, предоставляя комментарии как по содержанию, так и по педагогическим аспектам. Мы учли большинство из их рекомендаций. Особая благодарность Хамиду Р. Арабниа (University of Georgia), Рхода А. Ваггс (Florida Institute of Technology), Сандре Л. Барлетт (University of Michigan — Ann Arbor), Ричарду Т.Клоузу (U.S. Coast Guard Academy), Дэвиду Куку (U.S. Air Force Academy), Чарльзу Дж. Доулингу (Catonsville (Baltimore County) Community College), Дэвиду Дж. Хаглину (Mancato State University), Джиму Мерфи (California State University — Chico) и Герберту Шилдту. Наши коллеги Ральф Эутон (University of Техаз — EI Paso) и Дуглас Смит (University of the Pacific) внесли большой вклад в эту работу. Их взгляды и поддержка были бесценны для авторов и значительно улучшили окончательную структуру книги.
Уильям Форд
Уильям Толп
Введение V
1.1.	Абстрактные типы данных
1.2.	Классы C++ и абстрактные типы
1.3.	Объекты в приложениях C++*
1.4.	Разработка объектов
1.5.	Приложения с наследованием классов
1.6.	Разработка
объектно-ориентированных программ
1.7.	Тестирование и сопровождение программ
1.8.	Язык программирования C++
1.9.	Абстрактные базовые классы и полиморфизм*
Письменные упражнения
В этой книге разрабатываются структуры данных и алгоритмы в контексте объектно-ориентированного программирования с использованием языка C++. Мы разрабатываем каждую структуру данных как абстрактный тип, который определяет и организацию, и операции обработки данных. Структура, называемая абстрактный тип данных (abstract data type, ADT), — это абстрактная модель, описывающая интерфейс между клиентом (пользователем) и этими данными. Используя язык C++, мы разрабатываем представление каждой абстрактной структуры. Язык C++ поддерживает определяемый пользователем тип, называемый классом (class), для представления ADT и элементы этого типа, называемые объектами (objects), для хранения и обработки данных в приложении.
В данной главе вводится понятие ADT и относящихся к нему атрибутов, называемое инкапсуляцией данных и скрытием информации. С помощью серии примеров мы показываем разработку ADT и создаем формат для определения организации данных и операций.
Понятие конструктора класса в C++ является фундаментальным в нашем изучении структур данных и формально разрабатывается в главе 3. В данной главе мы начинаем с обзора класса C++ и рассматриваем его использование для представления какого-либо ADT. Необязательные разделы, помеченные символом звездочки (*), содержат примеры классов C++. В этой главе дается обзор разработки объектов, которая включает композицию объектов и наследование. Эти понятия являются строительными блоками объектно-ориентированного программирования. Глава включает основы разработки программ для построения более крупных приложений и изучения этой книги. Наследование и полиморфизм расширяют возможности объектно-ориентированного программирования и позволяют разрабатывать большие системы программирования на основе библиотек классов. Эти темы тщательно разрабатываются в главе 12 и используются выборочно для представления улучшенных структур данных.
В данной главе предварительно рассматриваются темы, представленные в книге. Вы познакомитесь с ключевыми структурами данных и объектно-ориентированными понятиями до их формального рассмотрения.
1.1. Абстрактные типы данных
Абстракция данных — центральное понятие в разработке программ. Абстракция определяет область н структуру данных вместе с набором операций, которые имеют доступ к данным. Абстракция, называемая абстрактным типом данных (ADT), создает определяемый пользователем тип данных, чьи операции указывают, как клиент может манипулировать этими данными. ADT является независимым от реализации и позволяет программисту сосредоточиться на идеализированных моделях данных и оперяпиях над ними.
Пример 1.1
1. Программа учета для малого предприятия сопровождает инвентаризационную информацию. Каждый элемент в описи представлен записью данных, которая включает идентификационный номер этого элемента, текущий уровень запаса, ценовую информацию и информацию упорядочивания. Набор операций по обработке списка об-
новляет различные информационные поля и инициирует переупо-рядочивание запаса, когда его уровень падает ниже определенного порога. Абстракция данных описывает какой-либо элемент как запись, содержащую серию информационных полей и операций, необходимых менеджеру компании для инвентаризационного сопровождения. Операции могут включать изменение значения Stock on Hand (имеющийся запас) при продаже этого товара, изменение Unit Price (цены за единицу) при использовании новой ценовой политики и инициализации упорядочивания при падении уровня запаса ниже уровня переупорядочивания (Reorder Level).
Данные
Identification	Stock on Hand	Unit Price	Reorder Level
Операции
UpdateStockLevel
Adj ustUnitPrice
ReorderItem
2. Игровая программа моделирует бросание набора костей. В этой разработке игральные кости описываются как абстрактный тип данных, которые включают число бросаемых костей, сумму очков в последнем бросании и список со значениями очков каждой кости в последнем бросании. Операции включают бросание костей (Toss), возвращение суммы очков в одном бросании (Total) и вывод очков для каждой отдельной кости (DisplayToss).
Данные
diceTotai
			
Dice Lisi
Операции
Toss Total DisplayToss
ADT — формат
Для описания ADT используется формат, который включает заголовок с именем ADT, описание типа данных и список операций. Для каждой операции определяются входные (input) значения, предоставляемые клиентом, предусловия (preconditions), применяемые к данным до того, как операция может быть выполнена, и процесс (process), который выполняется операцией. После выполнения операции определяются выходные (output) значения, которые возвращаются клиенту, и постусловия (postconditions), указывающие на любые изменения данных. Большинство ADT имеют инициализирующую операцию (initializer), которая присваивает данным начальные значения. В среде языка C++ такой инициализатор называется конструктором (constructor). Мы используем этот термин для упрощения перехода от ADT к его преставлению в C++.
ADT — формат
ADT ADT Hama
Данные
Описание структуры данных
Операции
Конструктор Начальные значения:	Данные, используемые для инициализации объекта
Процесс:	Инициализация объекта
Операциях Вход:	Данные от клиента
Предусловия:	Необходимое состояние системы перед выполнением операций
Процесс: Выход:	Действия, выполняемые с данными Данные, возвращаемые клиенту
Постусловия: 0перация2	Состояние системы после выполнения операций
Операиияп	
Конец ADT
Пример 1.2
1. Данные абстрактного типа Dice включают счетчик N числа игральных костей, которые используются в одном бросании, общую сумму очков и список из N элементов, который содержит значения очков, выпавших на каждой кости.
ADT Dice
данные
Число костей в каждом бросании — целое, большее либо равное 1. Целое значение, содержащее сумму очков всех костей в последнем бросании. Если N — число бросаемых костей, то число очков находится в диапазоне от N до 6N. Список, содержащий число очков каждой кости в бросании. Значение любого элемента списка находится в диапазоне от 1 до 6.
Операции
Конструктор
Начальные значения: Число бросаемых костей
Процесс:	Инициализировать данные, определяющие число костей в каждом бросании
Toss	
вход:	Нет
Предусловия:	Нет
Процесс:	Бросание костей и вычисление обшей суммы очков
Выход:	Нет
Постусловия:	Общая сумма содержит сумму очков в бросании, а в списке находятся очки каждой кости
DleTotal	
Вход:	Нет
Предусловия:	Нет
Процесс:	Находит значение элемента, определяемого как сумма очков в последнем бросании
Выход:
Постусловия: DisplayToss Вход;
Предусловия: Процесс:
Выход:
Постусловия:
конец ADT Dice
Возвращает сумму очков в последнем бросании
Нет
Нет
Нет
Печатает список очков каждой кости
в последнем бросании
Нет
Нет
2. Окружность определяется как набор точек, равноудаленных от точки, называемой центром. С целью графического отображения абстрактный тип данных для окружности включает как радиус (radius), так и положение центра. Для измеряющих приложений абстрактному типу данных требуется только радиус. Мы разрабатываем Circle ADT и включаем операции для вычисления площади (area) и длины окружности (circumference). Этот ADT применяется в следующем разделе для иллюстрации описания класса C++ н использования объектов при программировании приложений.
Circumference
ADT ClxC-le данные
Неотрицательное действительное число, определяющее радиус окружности. Операции
Конструктор Начальные значения:	Радиус окружности
Процесс: Area	Присвоить радиусу начальное значение
Вход:	Нет
Предусловия Процесс:	Нет Вычислить площадь круга
Выход:	Возвратить площадь круга
Постусловия:	Нет
Circumference
Вход: Предусловия Процесс: Выход:	Нет Нет Вычислить длину окружности Возвратить длину окружности
Постусловия:	Нет
конец ADT СхгаХе
1.2.	Классы C++ и абстрактные типы
Язык C++ поддерживает определяемый пользователем тип классов для представления абстрактных типов данных. Класс состоит из членов (members), которые включают значения данных и операции по обработке этих данных. Операции также называются методами (methods), поскольку они определяют методы доступа к данным. Переменная типа класса называется объектом (object). Класс содержит две отдельные части. Открытая (public) часть описывает интерфейс, позволяющий клиенту манипулировать объектами типа класса. Открытая часть представляет ADT и позволяет клиенту
Класс
private:
Данные-члены: переменная|. переменнаяг
Внутренние операции
public:
Конструктор
Операция]
Операция^
использовать объект и его операции без знания внутренних деталей реализации. Закрытая (private) часть содержит данные и внутренние операции, помогающие в реализации абстракции данных. Например, класс для представления ADT Circle содержит один закрытый член класса — radius. Открытые члены включают конструктор и методы вычисления площади круга и длины окружности
Circle Класс
private: radius
public:
Конструктор
Area
Circumference
Инкапсуляция и скрытие информации
Класс инкапсулирует (encapsulates) информацию, связывая вместе члены и методы и обращаясь сними как с одним целым. Структура класса скрывает реализацию деталей и тщательно ограничивает внешний доступ как к данным, так и к операциям. Этот принцип, известный как скрытие информации (information hiding), защищает целостность данных.
Класс использует свои открытую и закрытую части для контроля за доступом клиентов к данным. Члены внутри закрытой части используются методами класса и изолированы от внешней среды. Данные обычно определяются в закрытой части класса для предотвращения нежелательного доступа
клиента. Открытые члены взаимодействуют с внешней средой и могут использоваться клиентами.
Например, в Circle-классе radius является закрытым членом класса, доступ к которому может осуществляться только тремя методами. Конструктор присваивает начальное значение члену radius. Каждый из других методов использует radius. Например, area = р * raduis2. Здесь методы являются открытыми членами класса, которые могут вызываться всеми внешними единицами программы.
Передача сообщений
В приложении доступ клиентов к открытым членам какого-либо объекта может быть реализован вне этого объекта. Доступом управляют главная программа и подпрограммы (master control modules), которые наблюдают за взаимодействием между объектами. Управляющий код руководит объектом для доступа к его данным путем использования одного из его методов или операций. Процесс управления деятельностью объектов называется передачей сообщений (message passing). Отправитель передает сообщение получающему объекту и указывает этому объекту выполнить некоторую задачу.
В нужный момент отправитель включает в сообщение информацию, которая используется получателем. Эта информация передается как данные ввода для операции. После выполнения задачи получатель может возвращать информацию отправителю (данные вывода) или передавать сообщения другим объектам, запрашивая выполнение дополнительных задач. Когда получающий объект выполняет операцию, он может обновлять некоторые из его собственных внутренних значений. В этом случае считается, что происходит изменение состояния (state change) объекта и возникают новые постусловия.
1.3.	Объекты в приложениях C++*
Абстрактный тип данных реализует общее описание данных и операций над данными. Класс C++ обычно вводится сначала объявлением этого класса без определения функций-членов. Это известно как объявление класса (class declaration) и является конкретным представлением ADT. Фактическое определение методов дается в реализации класса (class implementation), отдельной от объявления.
Реализация классов C++ и использование объектов иллюстрируются следующей завершенной программой, которая определяет стоимость планировки бассейна. Программа объявляет Circle класс и показывает, как определяются и используются объекты. В коде содержатся определения открытого и закрытого разделов класса и используются функции C++ для определения операций. Главная программа — это клиент, который объявляет объекты, и затем использует их операции для выполнения вычислений. Главная программа отвечает за передачу всех сообщений в приложении.
Приложение: класс Circle
Объекты Circle используются для описания плавательного бассейна и дорожки вокруг него. С помощью методов Circumference (вычисление длины
окружности) и Area (вычисление площади круга) мы можем вычислить стоимость бетонирования дорожки и строительства ограды вокруг бассейна. К нашему приложению применяются следующие условия.
Строительные правила требуют, чтобы плавательный бассейн окружала бетонная дорожка (темная область на следующем рисунке) и вся территория была огорожена. Текущая стоимость ограды составляет $ 3,50 за погонный фут, а стоимость бетонирования — $ 0,5 за кв. фут. Приложение предполагает, что ширина дорожки, окружающей бассейн, составляет 3 фута и что клиент указывает радиус круглого бассейна. В качестве результата приложение должно определить стоимость строительства ограды и дорожки при планировании бассейна.
Мы объявляем объект Circle с именем Pool, описывающий площадь-плавательного бассейна. Второй объект — PoolRim, — это объект Circle, включающий как бассейн, так и окружающую дорожку. Конструктор вызывается при определении объекта. Для объекта Pool клиент задает радиус в качестве параметра, и затем использует радиус плюс 3 фута для определения объекта PoolRim.
Для вызова операции класса задайте имя объекта, за которым следует точка (.) и операция. Например, Роо1.Агеа() и Circumference() вызывают операции Circle для Pool.
Ограда располагается вдоль наружной стороны PoolRim. Вызовите операцию вычисления окружности PoolRim.Circumference() для вычисления стоимости ограды.
FenceCost = FoolRim.CircumferenceО * 3.50
Площадь бетонной поверхности определяется вычитанием площади Pool из внешней площади PoolRim.
ConcreteCost = (PoolRim.Area О — Pool.Area О) * 0.5
Программа 1.1. Конструкция и использование класса Circle
Программа 1.1 реализует приложение для бассейна. Для оказания помощи в чтении кода C++ в тексте имеются комментарии. Объявление класса Circle показывает представление Circle ADT и использование закрытых и открытых директив для контроля за доступом к членам класса.
Главная программа запрашивает клиента ввести радиус бассейна. Это значение используется для объявления объекта Pool. Второй объект — (PoolRim объявляется как имеющий дополнительные три фута к его радиусу для размещения дорожки вокруг бассейна. Стоимость строительства ограды и стоимость бетонирования дорожки выводятся для печати.
Вне главного модуля программа определяет класс Circle. Читатель может обратить внимание на использование спецификатора const для указания на то, что функция-член не изменяет данные. Этот спецификатор используется с методами Circumference и Area в их объявлении и определении. Стоимость строительных материалов для сетки ограды и стоимость бетона задаются как константы-
// prOlOl.cpp
#include <iostream.h>
const float PI = 3.14152;
const float FencePrice - 3.50;
const float ConcretePrice и 0.50;
// Объявление класса Circle, данных и методов class Circle
{
private:
// член класса radius — число с плавающей запятой float radius;
public:
// конструктор Circle(float г);
// вычисляющие функции
float Circumference(void) const;
float Area(void) const; );
// class implementation
// конструктор инициализирует член класса radius Circle::Circle(float r): radius(r) { }
// возвратить длину окружности
float Circle::Circumference(void) const {
return 2 * PI * radius;
)
// возвратить площадь круга float Circle;:Area(void) const (
return PI * radius * radius;
void raain()
(
float radius;
float Fencecost, ConcreteCost;
// настраивает поток вывода на выдачу двух знаков
// после десятичной точки
cout.setf(ios::fixed);
cout.setf(ios::showpoint);
cout.precision(2);
// запрос на ввод радиуса
cout « '‘Введите радиус бассейна: cin » radius;
// объявить объекты Circle Circle Pool (radius);
Circle PoolRim(radius + 3);
ft вычислить стоимость ограды и выдать ее значение FenceCost - PoolRim.Circumference О * FencePrice; cout « "Стоимость ограды: $" « FenceCost « endl;
// вычислить стоимость бетона и выдать ее значение ConcreteCost = (PoolRim.Area()— Pool.Area())*ConcretePrice; cout « "Стоимость бетона: $’’ « ConcreteCost << endl;
/*
Запуск программы pr01_01. cpp
Введите радиус бассейна: 40
Стоимость ограды: $945.60
Стоимость бетона: $391.12
*/
1.4.	Разработка объектов
В этой книге разрабатываются структуры данных с классами и объектами. Мы начинаем с классов, которые определяются простыми данными-членами и операциями класса. Для более сложных структур классы могут содержать члены класса, которые сами являются объектами. Результирующие классы, созданные посредством композиции (composition), имеют доступ к функциям-членам в составляющих объектах. Использование композиции объектов расширяет понятия инкапсуляции и скрытия информации и обеспечивает повторное использование кода. Объектно-ориентированные языки также позволяют классу быть порожденным из других классов путем наследования (inheritance). Это дает возможность разработчику создавать новые классы как усовершенствования других классов и повторно использовать код, который был разработан ранее. Наследование является фундаментальным средством объектно-ориентированного программирования на языке C++. Эта тема вводится формально в главе 12 и используется для разработки и реализации улучшенных структур данных.
Объекты и композиция
Геометрические фигуры состоят из наборов точек, которые образуют линии, прямоугольники и т.д. Основными строительными блоками фигур являются точки, сочетающиеся с серией аксиом для определения геометрических объектов. В данном разделе мы рассматриваем точку как примитивный геометрический объект, а затем описываем линии и прямоугольники. Эти геометрические фигуры используются для иллюстрации объектов и композиции.
Точка — это местоположение на плоской поверхности. Мы предполагаем, что объект точка расположена на сетке с координатами, которые измеряют горизонтальное (х) и вертикальное (у) расстояние от базовой точки. Например, точка р (3,1) находится на 3 единицы измерения правее и 1 единицу ниже базовой точки.
Линия образуется из точек, а две точки определяют линию. Последний факт используется для создания модели отрезка (line segment), который определяется своими конечными точками pl и р2 [Рис. 1.1 (А)].
2
5
4
2
3
базовая точка
1
	iy
	р (3 1)
X	
Прямоугольник — это четырехсторонняя фигура, чьи смежные стороны встречаются в прямых углах. Для рисования прямоугольник определяется двумя точками, которые отмечают верхний левый угол (ul) и нижний правый угол (1г) рамки [Рис. 1.1 (В)].
(А) Отрезок Цр1. р2)
Рис. 1.1. Отрезок и прямоугольник
(В) Прямоугольник R(ul, Ir)
Мы используем эти факты для создания классов Point, Line и Rectangle. Члены в классах Line и Rectangle являются объектами типа Point. Композиция — это важный инструмент в создании классов с объектами из других классов. Заметьте, что каждый класс имеет метод Draw для отображения рисунка на поверхности рисования. Класс Point содержит функции-члены для доступа к координатам х и у точки.
Класс Point
Класс Line
private:
х у координаты
public
Конструктор, Draw, GetX, GetY
private:
Point pl, p2
Класс Rectangle
private:
Point uL Ir
public
Конструктор, Draw
public:
Конструктор, Draw
Пример 1.3
Определите геометрический объект, задавая фигуру, за которой следуют имя объекта и параметры для указания объекта.
1.	Point	р(1,3);	//	объявляет	объект	point	(1,3)
2.	Point	pl(4,2),	р2(5,1);
Line	I(pl,p2);	//	линия:	от	(4,2)	до	(5,1)
3.	Point	pl(4,3),	р2(6,4);
Rectangle r(pl,p2);	// прямоугольник: от (4,3) до р2(6,4)
4.	Метод Draw в каждом классе делает наброски рисунка иа поверхности рисования.
р.Draw(); l.Draw(); r.Draw();
C++ геометрические классы*
Далее следуют объявления C++ для классов Point и Line. Заметьте, что конструктору для класса Line передаются координаты двух точек, определяющих линию. Каждый класс имеет функцию-член Draw, отображающую рисунок в области рисования.
Спецификация класса Point
ОБЪЯВЛЕНИЕ
class Point {
private:
float x, у,-	// горизонтальная и вертикальная позиция
public:
Point (float h, float v);
float GetX(void) const;	// возвратить координату x
float GetY(void) const;	// возвратить координату у
void Draw(void) const; // нарисовать точку (x,y) };
Класс посредством композиции Line включает два объекта Point. Эти объекты инициализируются конструктором.
Спецификация класса Line
ОБЪЯВЛЕНИЕ
class Line (
private:
Point Pl, P2;	// две конечные точки отрезка
public:
Line (Point a, Point b);// инициализировать Pl и P2 void Draw(void) const; // нарисовать отрезок };
Объекты и наследование
Наследование — это интуитивное понятие, из которого мы можем извлекать примеры, основанные на каждодневном опыте. Все из нас наследуют
характерные черты от своих родителей такие, как раса, цвет глаз и тип крови. Мы можем думать о родителе как о базе, из которой мы наследуем характерные черты. Взаимосвязь иллюстрируется двумя объектами, связанными стрелкой, острие которой направлено к базовому объекту.
Зоология формально изучает наследование у животных. На рис. 1.2 показана иерархия животных для млекопитающих, собак и колли. Млекопитающее — это теплокровное животное, которое имеет шерсть и вскармливает своих детенышей молоком. Собака — это млекопитающие, которое имеет клыки, ест мясо, имеет определенное строение скелета и является общественным животным. Колли — это собака с заостренной мордой, имеющая белый с рыжим окрас и хорошо развитые пастушеские инстинкты.
Рис. 1.2. Цепочка наследования у животных
В иерархической цепочке класс наследует все характерные черты своего класса-предка. Например, собака имеет все черты млекопитающего плюс те, которые отличают ее от кошек, слонов и т.д. Порядок расположения классов указывает что
Колли есть собака. Собака есть млекопитающее
В этой цепочке класс млекопитающих определяется в качестве базового класса (base class) для собаки, а собака называется производным классом (derived class). Используя аналогию семейного наследования, мы говорим о базовом и производном классах как о родительском классе и классе-наследнике, соответственно. В случае расширенной цепочки наследник наследует характерные черты своего родительского и прародительского класса.
Наследование в программировании
Объектно-ориентированное программирование предоставляет механизм, посредством которого производному классу разрешается наследовать данные и операции от базового класса. Этот механизм, называемый наследование класса (class inheritance), позволяет производному классу использовать данные и операции, которые были определены ранее в базовом классе. Производный класс может добавлять новые операции или переписывать некоторые операции, так как он устанавливает методы для обработки его данных. Аналогично, ребенок может наследовать дом или автомашину от его (или ее) родителя. Затем он может затем использовать этот дом или автомашину. Если необходимо, наследник может модифицировать дом, чтобы он соответствовал его (или ее) особым условиям.
проиллюстрируем наследование класса с помощью линейного списка, н. званного SeqList, который сохраняет информацию в последовательном п< рядке. Список — это важная и знакомая структура, используемая для ведевд инвентаризационных записей, графика деловых встреч, типов и количеств необходимых гастрономических товаров и т.д. Наследование возникает, ког/ мы объявляем упорядоченный список, который является особым типом по ледовательного списка. Упорядоченный список использует все базовые метод обработки списка из последовательного списка и обеспечивает свою собс’ венную операцию вставки для того, чтобы элементы списка сохранялись возрастающем порядке.
В линейном списке, содержащем N элементов, любой элемент занимае одно из положений от 0 до N-1. Первое положение является передним, последнее — конечным. На рис. 1.3 показан неупорядоченный список целы чисел с шестью элементами.
5	3	22	45	23	8
0	1	2	3	4	5
Рис. 1.3. Неупорядоченный линейный список
Базовые операции SeqList включают операцию Insert, которая добавляв новый элемент в конец списка (Рис. 1.4), и операцию Delete, которая удаляе первый элемент списка, соответствующий ключу. Вторая функция удаления называемая DeleteFront, удаляет первый элемент в списке (Рис. 1.5). Струк тура определяет размер списка с помощью ListSize и предоставляет операции Find, выполняющую поиск элемента в списке. Для управления данным! пользователь может определить, является ли список пустым, и удалить еп операцией ClearList.
Insert (10)
5	3	22	45	23	8 ID
0	1	2	3	4	5	6
Рис, 1.4. Вставка значения 10
Delete (45)
0	1	2 \ 3	4	5
45
DeleteFront
Рис. 1.5. Удаление элемента 45 и удаление первого элемента в списке
Данный класс предоставляет метод GetData, позволяющий клиенту читать значение любого элемента списка. Например, для нахождения максимального значения в списке мы можем начать сканирование списка с нулевого элемента. Процесс заканчивается при достижении конца списка, который определяется с помощью ListSize. В каждом положении следует обновлять максимальное значение, если текущее значение (GetData) больше, чем текущий максимум. Например, для второго элемента списка число 22 сравнивается с
предыдущим максимумом, равным 3, поэтому текущее максимальное значение заменяется на 22. В конечном счете, число 23 определяется как максимальный элемент в списке.
3 < 22
3		Новым 22	иаи	симумо 23	м 1	5удет 22 8		10
0		1		2		3		4
ADT SeqList
Данные
Неотрицательное целое число, указывающее количество элементов, находящихся в данный момент в списке (размер), и список элементов данных.
Операции
Конструктор
Начальные значения: Процесс: ListSize ВХОД: Предусловия; Процесс: Выход: Постусловия: ListEmpty Вход: предусловия: Процесс: ВЫХОД: Постусловия: ClearList Вход: Предусловия: Процесс: Выход: Постусловия: Find Вход: Предусловия: Процесс: Выход: Постусловия: insert Вход: Предусловия:	Нет Установка размера списка на 0 Нет Нет Чтение размера списка Размер списка Нет Нет Нет Проверка размера списка Возвращать TRUE, если список пустой; в противном случае — возвращать FALSE. Нет Нет Нет Удаление всех элементов из списка и установка размера списка на 0. Нет Список пустой Элемент, который необходимо найти в списке. Нет Сканирование списка для нахождения соответствующего элемента. Если соответствующий элемент списка не найден, возвращать FALSE; если он найден, возвращать TRUE и этот элемент. Нет Элемент для вставки в список Нет
2 Зак. 425
Процесс:
Выход:
Постусловия:
Delete
Вход:
Предусловия: Процесс:
Выход:
Постусловия:
DeleteFront
Вход:
Предусловия:
Процесс:
Выход:
Постусловия: GetData
ВХОД:
Предусловия:
Процесс: Выход:
Постусловия:
Конец ADT SeqList,
Добавление этого элемента в конец списка.
Нет
Список имеет новый элемент» его размер увеличивается на 1.
Значение, которое должно быть удалено из списка-
Нет
Сканирование списка и удаление первого найденного элемента в списке. Не выполнять никакого действия, если этот элемент не находится в списке.
Нет
Если соответствующий элемент найден, список уменьшается на один элемент.
Нет
Список не должен быть пустым.
Удаление первого элемента из списка.
Возвращать значение удаляемого элемента
Список имеет на один элемент меньше.
Положение (cos) в списке.
Генерируется ошибка доступа, если роз меньше О или больше О (размер -1)
Получать значение в положении pos в списке.
Значение элемента в положении pos.
Нет
Упорядоченные списки и наследование
Упорядоченный список — это особый тип списка, в котором элементы сохраняются в возрастающем порядке. Как абстрактный тип данных этот особый список получает большинство своих операций из класса SeqList, за исключением операции Insert, которая должна добавлять новый элемент в положение, сохраняющее упорядочение (Рис. 1.6).
lnsert(8)
О
Рис. 1.6. Упорядоченный список: Вставка элемента (8)
23
п-2
Операции ListSize, ListEmpty, ClearList, Find и GetData независимы от любого упорядочения элементов. Операции Delete и DeleteFront удаляют элемент, но сохраняют остающиеся элементы упорядоченными. Далее следует ADT, отражающий сходство операций упорядоченного списка и SeqList.
ADT OrderedList
Данные
те же, что и для SeqList ADT
Операции
Конструктор ListSize ListEmpty ClearList Find Delete DeleteFront GetData	выполняет конструктор базового класса	
	тот же,	что и для SeqList ADT что и для SeqList ADT что и для SeqList ADT что и для SeqList ADT
	тот же,	
	тот же,	
	тот же,	
	тот же,	ЧТО и для SeqList ADT
	тот же,	что и для SeqList ADT
	тот же.	что и для SeqList ADT
Insert Вход:	Элемент	для вставки в список.
Предусловия: Процесс:	Нет Добавление элемента в положение, сохраняющее упорядочение.	
Выход: Постусловия:	Нет Список имеет новый элемент, и его размер увеличивается на 1	
Конец ADT OrderedList
Класс OrderedList является производным от класса SeqList. Он наследует операции базового класса и модифицирует операцию Insert для упорядоченной вставки элементов.
Класс SeqList
Повторное использование кода
Объектно-ориентированный подход к структурам данных способствует повторному использованию кода, который уже был разработан и протестирован, и может быть вставлен в ваше приложение. Мы уже рассматривали повторное использование кода с композицией. Наследование также является мощным инструментом для этой цели. Например, реализация класса упорядоченного списка требует от нас написания только методов Insert и конструктора. Все Другие операции задаются кодом из класса SeqList. Повторное использование кода является важнейшим преимуществом в объектно-ориентированной разработке, поскольку оно сокращает время разработки и способствует однородности приложений и вариантов программного продукта. Например, при модернизации операционной системы к ней добавляются новые функции. В то же время, эта модернизация должна позволять выполнение существующих приложений. Одним из подходов является определение оригинальной операционной системы в качестве базового класса. Модернизированная система действует как производный класс с его новыми функциями и операциями.
Спецификации класса SeqList и OrderedList*
Формальное описание класса SeqList приводится в главе 4. В этом разделе мы даем только спецификацию класса для того, чтобы вы могли соотнести этот класс и его методы с очень общим ADT. Класс OrderedList определяется для иллюстрации наследования. Тип элемента данных в списке представлен параметрическим именем DataType.
Спецификация класса SeqList
ОБЪЯВЛЕНИЕ
class SeqList	.
(
private:
// массив для хранения списка и число элементов текущего списка DataType listitern[ARRAYSIZE];
int size;
public:
// конструктор SeqList(void);
// методы доступа списка int Listsize(void) const; int ListEmpty(void) const; int Find (DataType& item) const; DataType GetData (int pos) const;
// методы модификации списка
void insert (const DataType& item);
void Delete (const DataTypefi item);
DataType DeleteFront (void);
void ClearList (void);
);
ОПИСАНИЕ
Методы ListSize, ListEmpty, Find и GetData завершаются словом const после объявления функции. Они называются постоянными функциями, поскольку не изменяют состояние списка. Функции Insert, Delete имеют слово const как часть списка параметров. Этот синтаксис C++ передает ссылку на элемент, но указывает, что значение этого элемента не изменяется.
C++ использует простой синтаксис для объявления производного класса. В заголовке базовый класс указывается после двоеточия (:). Далее следует объявление класса OrderedList. Особенности описываются в главе 12, в которой содержится формальное введение в наследование.
Спецификация класса OrderedList
ОБЪЯВЛЕНИЕ
class OrderedList: public SeqList	// наследование- класса SeqList
(
public:
OrderedList (void);	// инициализировать базовый класс
// для создания пустого списка
void Insert (const DataType& item); // вставить элемент no порядку );
ОПИСАНИЕ
Insert замещает метод базового класса с тем же именем. Она проходит по всему списку и вставляет элемент в положение, сохраняющее упорядочение списка.
1.5. Приложения с наследованием классов
Понятие наследования класса имеет важное применение в программировании графического пользовательского интерфейса (grafical user interface — GUI) и системах баз данных. Графические приложения фокусируют внимание на окнах, меню, диалоговых окнах и так далее. Базовое окно — это структура данных с данными и операциями, являющимися общими для всех типов окон. Операции включают открытие окна, создание или изменение заголовка окна, установку линеек прокрутки и областей перетаскивания и т.д. Приложения GUI состоят из классов диалога, классов меню, классов текстовых окон и так далее, которые наследуют базовую структуру и операции от базового класса окна. Например, следующая иерархия класса включает класс Dialog и класс TextEdit, порожденные от класса Window.
Этот пример класса Window показывает одиночное наследование, в котором производный класс имеет только один базовый класс. Однако при множественном наследовании (multiple inheritance) класс порождается от двух или более базовых классов. Некоторые приложения GUI используют это свойство. Программа текстового процессора объединяет редактор (editor) с менеджером просмотра (view manager) для пролистывания текста в некотором окне. Редактор читает строку символов и вносит изменения, вставляя и удаляя строки, и вводя информацию форматирования.. Менеджер просмотра отвечает за копирование текста на экран с использованием информации о шрифте и окне. Редактор экрана может быть определен как производный класс, использующий класс Editor (редактора) и класс View (менеджера просмотра) в качестве базовых классов.
Множественное наследование
Класс Screen Editor
1.6. Разработка объектно-ориентированных программ
Большие программные системы становятся все более сложными и требую-новых подходов к разработке. Традиционная разработка использует моделз управления, которая предполагает наличие администратора верхнего уровхи (top administrator), понимающего систему и поручающего задачи менеджерам Такая нисходящая программная разработка (top-down program design) рас сматривает систему как набор подпрограмм, состоящий из нескольких слоев
На верхнем уровне главная программа управляет работой системы, выпол няя последовательность вызовов подпрограмм, которые производят вычисле ния и возвращают информацию. Эти подпрограммы могут далее поручать вы полнение некоторых задач другим подпрограммам. Элементы нисходящей про граммной разработки необходимы для всех систем. Однако, когда задача становится слишком большой, этот подход может оказаться неудачным, по скольку его сложность подавляет способность отдельного человека управляв такой иерархией подпрограмм. Кроме того, простые структурные изменения i подпрограммах около вершины иерархии могут потребовать дорогих и зани мающих длительное время изменений в алгоритмах для подпрограмм, находя щихся в нижней части диаграммы.
Объектно-ориентированное программирование использует другую модел! системной разработки. Оно рассматривает большую систему как набор объект©! (агентов), которые взаимодействуют для выполнения задач. Каждый объект имеет методы, управляющие его данными.
Целью программной разработки является создание читабельной и поддер живаемой архитектуры, которая может быть расширена, как диктует необ ходимость. Хорошо организованные системы легче понимать, разрабатывай и отлаживать. Все философии разработки пытаются преодолеть сложност! программной системы с помощью принципа разделения и подчинения. Нис ходящая программная разработка рассматривает систему как набор функци ональных модулей, состоящий из слоев. Объектно-ориентированное програм мирование использует объекты как основу разработки. Не существует един ственного способа программной разработки и строго определенного процесса которому необходимо следовать. Разработка программ — это вид деятельности человека, который должен включать творческую свободу и гибкость. В этог книге мы обсуждаем общий подход, определяющий методологию разработку программного продукта (software development methodology). Этот подхо/ включает отдельные фазы разработки программного продукта, среди которые анализ задачи и определение программы, разработка объекта и процесса кодирование, тестирование и поддержка (сопровождение).
Анализ задачи/определение программы
Программная разработка начинается, когда клиент обозначит некоторую запячу, которая должна быть решена. Эта задача часто определяется свободно, без ясного понимания, какие именно данные имеются в наличии (вход) и какая новая информация должна быть получена в результате (выход). Программист анализирует задачу вместе с клиентом и определяет, какую форму должны принять вход и выход и алгоритмы, которые используются при выполнении вычислений. Этот анализ формализуется в фазе разработки программы.
Разработка
Программная разработка описывает объекты, которые являются основными строительными блоками программы. Разработка описывает также управляющие модули, руководящие взаимодействием между объектами.
В фазе объектной разработки определяются объекты, которые будут использоваться в программе, и пишется объявление для каждого класса. Класс тестируется путем его использования с какой-либо небольшой программой, тестирующей методы класса при управляемых условиях. Тот факт, что классы могут тестироваться отдельно, вне области большого приложения, является одной из важнейших возможностей объектно-ориентированной разработки.
Фаза разработки управления процессом использует нисходящую разработку путем создания главной программы и подпрограмм для управления взаимодействием между объектами. Главная программа и подпрограммы образуют каркас разработки (design framework).
Главный управляющий модуль соответствует главной функции в программе C++ и отвечает за поток данных программы. При нисходящей программной разработке система делится на последовательность действий, которые выполняются как независимые подпрограммы. Главная программа и ее подпрограммы организуются в нисходящую иерархию модулей, называемую структурным деревом (structure tree). Главный модуль является корнем этого дерева. Каждый модуль заключается в прямоугольник, а каждый класс, который используется модулем, заключается в овал. Мы представляем каждый модуль, указывая имя функции, входные и выходные параметры и краткое описание ее действия.
Кодирование
В фазе кодирования пишутся главная программа и подпрограммы, реализующие каркас программной разработки.
Тестирование
Реализация и тестирование объектов выполняются в течение фазы объектной разработки. Это позволяет нам сосредоточить внимание на разработке управляющего модуля. Мы можем проверять кодирование программы, тестируя взаимодействие каждого объекта с управляющими модулями в каркасе разработки.
Иллюстрация программной разработки: Dice график
Разработку и реализацию объектно-ориентированной программы иллюстрирует использование графика для записи частоты результатов бросания при игре в кости. В последующих разделах описывается каждая фаза в жизненном цикле программы.
Анализ задачи. Предположим, событие — это бросание двух костей. Для каждого бросания сумма лежит в диапазоне от 2 до 12. Используя повторное бросание костей, мы определяем эмпирическую вероятность того, что сумма равна 2, 3 ... , 11 или 12, и строим диаграмму, которая отражает вероятность каждого возможного результата.
Замечание
Эмпирическая вероятность определяется моделированием большого количества событий и записью результатов. Отношение количества появлений некоторого события к количеству всех моделируемых событий представляет эмпирическую вероятность того, рассматриваемое событие произойдет. Например, если бросание костей повторится 100000 раз и сумма 4 возникнет 10000 раз. то эмпирическая вероятность этой суммы равна 0,10
Прежде всего следует ясно определить задачу. Этот процесс включает понимание входа, выхода и промежуточных вычислений. В фазе анализа задачи клиент формирует серию требовании к системе. Они включают контроль за вводом данных, указание вычислений и используемых формул и описание желаемого выхода.
Определение программы. Программа запрашивает пользователя ввести число N — количество бросаний двух костей. Поскольку бросание костей имеет случайный результат, используем для моделирования N бросаний случайные числа. Программа ведет запись количества появлений каждой воз
можной суммы S (2 < S < 12). Эмпирическая вероятность определяется делением количества результатов S на N. Что касается выхода, это дробное значение используется для определения высоты прямоугольника на нашей диаграмме. Результаты выводятся на экран как столбцовая диаграмма.
Объектная разработка. Программа использует класс Line для создания осей координат и класс Rectangle — для построения столбцов. Эти классы вводятся в разделе 1.4 Разработка объектов. Бросание костей — это метод в классе Dice, который обрабатывает две кости. Далее следует объявление класса Dice. Его реализация и тестирование приводятся в программе вместе с реализацией и тестированием классов Line и Rectangle.
#include random.h class Dice
( private: // данные-члемы int diceTotal; // сумма двух костей int diceList[2]; // список очков двух костей
// класс генератора случайных чисел, используемый для
// моделирования бросаний RandomNumber гnd;
public:
// конструктор Dice(void);
// методы void Toss(void); int Total(void) const; void DisplayToss(void) const; }i
Разработка управления процессом. Для построения диаграммы бросания костей главный модуль вызывает три подпрограммы, которые выполняют основные действия программы. Функция SitnulateDieToss использует методы из класса Dice для бросания костей N раз. DrawAxes вызывает метод Draw в классе Line для рисования осей координат графика, a Plot рисует серию прямоугольников, которые образуют столбцовую диаграмму. Функция Plot вызывает Мах для определения максимального количества появлений любой возможной суммы. Это значение позволяет нам вычислить относительную высоту каждого прямоугольника диаграммы. Структурное дерево этой программы показано на рис. 1.8. Далее следуют объявления для каждого управляющего модуля в структурном дереве.
SimulateDieToss Dice	Главная		 DrawAxes	Plot ( Line ) ( Rectangle j	Max
Рис. 1.8. Древовидная структура программы Dice Graph
main
Передаваемые параметры:
Нет
Выполнение:
Запросить у пользователя количество бросаний костей в моделировании. Вызвать функцию SimulateDieToss для выполнения бросаний и записать количество раз, когда возникает каждая возможная сумма: (2 £. Total <. 12) . Нарисовать оси координат функцией DrawAxes и создать столбцовую диаграмму функцией Plot.
Возвращаемые параметры:
Нет
SimulateDieToss
Передаваемые параметры:
tossTotal массив tossTotal содержит количество появлений каждой суммы в диапазоне от 2 до 12. tossTotal [i] — это количество появлений суммы i при бросании костей tossCount раз.
tossCounc Количество бросаний N при моделировании.
Выполнение:
Создать объект Dice и использовать его для бросания костей указанное количество раз, записывая в массив tossTotal количество раз, когда возникает сумма 2, количество раз, когда возникает сумма 3, .	.	. , количество раз,
когда возникает сумма 12.
Возвращаемые параметры:
Массив tossTotal с количеством раз, когда возникает каждая сумма.
DrawAxes
Передаваемые параметры:
Нет
Выполнение:
Создать два объекта Line: один — для вертикальной оси (оси у} и один — для горизонтальной оси (оси х) . Ось у — это линия от (1.0, 3.25) до (1.0, 0.25) .
Ось х — это линия от (0.75, 3.0) до (7.0, 3.0). Вертикальный диапазон графика равен 2,75.
Возвращаемые параметры: нет
Мах
Передаваемые параметры:
а Массив, содержащий длинные значения данных, п Количество значений данных в а.
Выполнение:
Найти максимальное значение элементов в массиве а-Воэврашаемый параметр:
Максимальное значение в массиве.
Plot
Передаваемый параметр:
tossTctal Массив, содержащий количество появлений каждой возможной суммы, вычисленной в SiiriulateDieToss.
Выполнение:
Поиск максимальной суммы (rnaxTotal) в массиве tossTctal для диапазона индекса 2 — 12. Затем каждый элемент в массиве генерирует соответствующую часть (tossTotal [il / rnaxTotal) вертикального диапазона графика. Разделить 6-дюймовый интервал оси хот (1.0, 3.0} до (7.0, 3.0} на 23 равных сегмента и построить соответствующие прямоугольники, чьи высоты — это функция (tossTotal [i]) / rnaxTotal*2.75, 2<i5 12.
Возвращаемые параметры:
Нет
Кодирование*. Завершает разработку программы кодирование управляющих модулей. Например, следующей программой задаются управляющие модули для dice-диаграммы:
Программа 1.2. Диаграмма бросания костей
Главная программа запрашивает пользователя ввести количество бросаний костей для моделирования. Наш запуск программы тестирует случай 500 000 бросаний костей. В конце выполнения задачи система ожидает нажатия клавиши или щелчка кнопкой мыши для завершения работы. Классы Line и Rectangle содержатся в файле figures.h, а класс Dice содержится в файле dice.h. Подпрограммы рисования графических примитивов находятся в graphlib.h.
finciude <iostream.h>
#include "figures.h”
(tinclude "dice.h"
#include "graphlib-h"
// Бросание двух костей tossCount раз.
// Запись числа выпавших "двоек*' в tossTotal [2],
// "троек" — в tossTotal[3J и так далее
void SimulateDieToss ( long tossTotal(], long tossCount ) {
long tossEvent;
int i;
Dice D;
// очистить каждый элемент массива tossTctal for (1=2; i 12; 1++)
tossTotal(i] = 0;
tl Бросание костей tossCount раз for (tossEvent = 1; tossEvent tossCount; tossEvent++ ) {
D.Tossj);	// бросание костей
tossTotal (D.TotaK) ]++; // увеличение счетчика )
// Нахождение максимального значения в массиве их п элементов long Max (long а [ ], int п) {
long Imax = а [ 0 ];
int i;
for (i=l; i n; i+f)
if (a [ i ] Imax)
Imax = a [ i ];
return Imax;
)
// Рисование двух осей
void DrawAxes (void)
{
const float vertspan = 3.0;
Line VerticalAxis(Point(1.0,vertspan+0.25), Point(1.0,0.25));
VerticalAxis.Draw();
Line HorizontalAxis(Point(0.75,vertspan),Point(0.75,vertspan));
Hori zontalAxis.Draw();
)
// Рисование графического столбца
void Plot (long tossTotal!])
(
const float vertspan = 3.0, scaleHeight = 2.75;
float x, rectHeight, dx;
long maxTotal;
xnt i;
// Нахождение максимального значения в массиве tossTotal.
// Поиск для индексов в диапазоне 2-12
maxTotal = Max(&tossTotal[2], 11);
/ / теперь создаем прямоугольники
dx = 6.0/23.0;
х = 1.0 + dx;
// Црцсл— 11 раз.
//В цикле:
//	определяется высота столбца для рисования в текущем положении;
//	создается объект Rectangle для соответствующего
// положения, высота и ширина;
// рисуется столбец и происходит переход к следующему столбцу for (i=2; i 12; i++) (
rectHeight= (float(tossTotal[i])/maxTotal)*scaleHeight;
Rectangle CurrRect(Point(x,vertspan-rectHeight), Point(x+dx,vertspan)) CurrRect.Draw();
x += 2.0*dx;
}
}
void main (void)
{
long numTosses;
long tossTotal(13] ;
/ / запрос числа моделирования бросаний
cout « "Введите число бросаний: ";
cin » numTosses;
SimulaceDxeToss(tossTotal, numTosses); //бросание
InitGraphj.cs ();	// инициализация графической системы
DrawAxes ();	// рисование осей
Plot (tossTotal);	// построение графика
ViewPause () ;	// ожидание нажатия клавиши или щечка мышью
ShutdownGraphics () ; // закрытие графической системы }
/*
Запуск программы рг01_02.срр
Введите число бросаний: 50000
*/
1.7.	Тестирование и сопровождение программы
Использование объектно-ориентированного программирования способствует созданию систем, позволяющих выполнять независимое тестирование объектов и повторно использовать написанные ранее классы. Эти преимущества уменьшают риск появления ошибок при создании сложных программных систем, поскольку они развиваются, расширяясь из меньших систем, в которых вы уверены. Тестирование выполняется в процессе разработки программной системы.
Объектное тестирование
Тип класса — это самодостаточная структура данных, которая может передавать информацию внешнему компоненту программы и от него. Мы можем тестировать каждый класс, создав короткую программу, вызывающую каждый public-метод. Эта дополнительная программа иллюстрирует тестирование методов в классе Dice. .
Тестирование управляющего модуля
Программа должна быть до конца протестирована путем ее выполнения с тщательно отобранными данными. Перед началом выполнения этой задачи правильность программы часто может быть оценена с помощью структурного сквозного контроля (structure walk-through), в котором программист показывает полную структуру и реализацию другому программисту и объясняет, что
именно происходит, от объектной разработки и до разработки управляющих модулей. Этот процесс часто вскрывает концептуальные ошибки, проясняет логику программы и подсказывает тесты, которые могут быть выполнены.
Современные компиляторы поддерживают отладчик на уровне исходного кода (source-level debugger), который позволяет отслеживать отдельные команды и останавливает работу в выбранных контрольных точках. Во время управляемого выполнения значения переменных могут отображаться на экране, позволяя сравнивать выборочные части программы до и после возникновения ошибки.
Основной проверкой правильности программы является ее выполнение с наборами хорошо подобранных данных. Выполняя тестирование, программист убеждается в правильности программы. Программисту следует также использовать неверные входные данные для проверки кода на устойчивость к ошибкам (robustness), которая определяет способность программы идентифицировать неверные данные и возвращать сообщения об ошибках.
Данные для тестирования отбираются с помощью различных подходов. Одним из методов является так называемый метод "надеюсь и молюсь", когда программист запускает программу несколько раз с простыми данными и, если она работает, то он (или она) продолжает разработку. Более разумным подходом является выбор серии входных данных, которая тестирует различные алгоритмы в программе. Ввод должен включать простые, типовые и экстремальные данные, которые тестируют специальные случаи в программе, и неверные данные, проверяющие способность программы реагировать на ошибки ввода.
Полностью структурированный тест рассматривает логическую структуру программы. Этот подход предполагает, что программа не тестируется до конца, если некоторая часть кода не была уже выполнена. Исчерпывающее тестирование требует от программиста выбора данных для проверки различных алгоритмов в программе: каждого условного оператора, каждой конструкции цикла, каждого вызова функции и так далее. Некоторые компиляторы предоставляют функции протоколирования, которые указывают количество вызовов функций в программе.
Программное сопровождение и документирование
Для удовлетворения дополнительным требованиям компьютерные программы часто нуждаются в обновлении. Объектно-ориентированное программирование упрощает программное сопровождение. Наследование классов делает возможным повторное использование существующих программ. Эти инструменты эффективны при поддержке хорошим программным документированием, описывающим классы и управляющие модули для того, чтобы помогать пользователям понимать программу и ее правильное выполнение. Большие программы обычно поддерживаются руководством пользователя, которое включает информацию по установке программного обеспечения и одну или более обучающие программы для иллюстрации центральных возможностей программного продукта.
Объектные спецификации и структурные диаграммы управляющих модулей являются превосходными инструментами программного документирования. В исходном программном коде комментарии описывают действие отдельных функций и классов. Комментарии также помещаются там, где логика какого-либо алгоритма является особенно трудной.
1.8.	Язык программирования C++
Эта книга знакомит читателя со структурами данных, используя язык объектно-ориентированного программирования C++. Несмотря на существование ряда объектно-ориентированных языков, C++ обладает преимуществом, благодаря своим корням в популярном языке программирования С и качеству компиляторов.
Язык С был разработан в начале 70-х годов как структурный язык для системного программирования. Он содержал средства для вызова системных подпрограмм низкого уровня и реализации конструкций высокого уровня. С годами быстрые и эффективные компиляторы С появились на большинстве компьютерных платформ. Вся операционная система Unix, кроме небольшой части, написана на языке С, и С является основным языком программирования в среде Unix. Язык программирования C++ был разработан в Bell Laboratories Бьярном Страуструпом в качестве развития языка С. Использование языка С означало, что C++ не пришлось разрабатывать с самого начала, и эта связь с С дала новому языку широкую аудиторию квалифицированных программистов. Первоначально C++ назывался "С с классами” и стал доступен для пользователей в начале 80-х годов. Были написаны трансляторы для преобразования исходного кода С с классами в код С перед вызовом компилятора С для создания машинного кода.
Название C++ было придумано Риком Масситти в 1983г. Он использовал оператор приращения ++ языка С, чтобы отразить его эволюцию из языка С и то, что он расширяет язык С. Многие спрашивали, должен ли C++ сохранять совместимость с С, в частности, поскольку C++ разрабатывает новые мощные конструкции и средства, которые не присутствуют в С. На практике этот язык, вероятно, будет продолжать оставаться развитием языка С. Количество существующих программ на языке С и количество функций библиотеки С будет заставлять разработчиков C++ сохранять крепкую связь с языком С. Определение C++ продолжает обеспечивать то, что общие конструкции С и C++ имеют одно и то же значение на обоих языках.
Идеи многих конструкций C++ развивались в 70-е годы из языков Си-мула-67 и Алгол-68. Эти языки ввели в употребление серьезную проверку типов, понятия класса и модульность. Министерство обороны содействовало разработке Ада, который привел в систему многие ключевые идеи конструкции компилятора. Язык Ада стимулировал использование параметризации для возможности появления обобщенных классов. C++ использует похожую шаблонную конструкцию и имеет также общие с языком Ада механизмы обработки особых ситуаций.
Популярность C++ и его миграция на многие платформы привели к необходимости стандартизации. Компания AT&T активно развивает этот язык. Сознательные усилия прилагаются для связи тех, кто пишет компилятор C++, с разработчиками оригинального языка и с растущей популяцией пользователей. AT&T развивает свой успех с Unix и работает совместно с пользователями для координации разработки стандартов ANSI C++ и опубликования окончательного справочного руководства по C++. Ожидается, что стандарт ANSI (Американский национальный институт стандартов) по C++ станет частью стандарта ISO (Международная организация по стандартам).
1.9.	Абстрактные базовые классы и полиморфизм*
Наследование классов объединяется с абстрактными базовыми классами (abstract base classes) для создания важного инструмента разработки структуры данных. Эти абстрактные базовые классы определяют открытый интерфейс класса, независимый от внутренней реализации данных класса и операций. Открытый интерфейс класса определяет методы доступа к данным. Клиент хочет, чтобы открытый интерфейс оставался постоянным, несмотря на изменения во внутренней реализации. Объектно-ориентированные языки подходят к этой проблеме, используя абстрактный базовый класс, который объявляет имена и параметры для каждого из public-методов. Абстрактный базовый класс предоставляет ограниченные детали реализации и сосредотачивает внимание на объявлении public-методов. Это объявление форсирует реализацию в производном классе. Абстрактный базовый класс C++ объявляет некоторые методы как чистые виртуальные функции (pure virtual functions). Следующее объявление определяет абстрактный базовый класс List, который задает операции списка. Слово ’’virtual” и присвоение нуля операции определяют чистую виртуальную функцию.
template <class Т>
class List
<
protected:
// число элементов в списке.
// обновляется производным классом
int size size;
public:
// конструктор
List(void);
// методы доступа
virtual int Listsize(void) const;
virtual int ListEmpty(void) const;
virtual int Find(T& item) = 0;
// методы модификации списка
virtual void Insert(const T& item) = 0;
virtual void Delete(const T& item) = 0; virtual void ClearList(void) = 0;
};
Этот абстрактный базовый класс предназначен для описания очень общих списков. Он используется как база для серии классов наборов (структур списков) в последующих главах. Использование абстрактного класса в качестве базы требует, чтобы наборы реализовывали общие методы класса List. Для иллюстрации этого процесса класс SeqList снова рассматривается в главе 12, где он порождается от List.
Полиморфизм и динамическое связывание
Концепция наследования поддерживается в языке C++ рядом мощных конструкций. Мы уже рассмотрели чистые виртуальные функции в абстрактном базовом классе. Общая концепция виртуальных функций поддерживает
наследование в том, что позволяет двум или более объектам в иерархии наследования иметь операции с одним и тем же объявлением, выполняющие различные задачи. Это объектно-ориентированное свойство, называемое полиморфизм (polymorhism), позволяет объектам из множества классов отвечать на одно и то же сообщение. Получатель этого сообщения определяется динамически во время выполнения. Например, системный администратор может использовать полиморфизм для обработки резервных файлов в многосистемной среде. Предположим, что администратор имеет подсистему с магнитной лентой 1/2 дюйма и компактный магнитофон с лентой 1/4 дюйма. Классы HalfTape и QuarterTape являются производными от общего класса Таре и управляют соответствующими лентопротяжными устройствами. Класс Таре имеет виртуальную операцию Backup, содержащую действия, общие для всех лентопротяжных устройств. Производные классы имеют (виртуальную) операцию Backup, использующую специфическую внутреннюю информацию о лентопротяжных механизмах. Когда администратор дает указание выполнить системное резервное'копирование, каждый лентопротяжный механизм принимает сообщение Backup и выполняет особую операцию, определенную для его аппаратных средств. Объект типа HalfTape выполняет резервное копирование иа 1/2-дюймовую ленту, а объект типа QuarterTape — на 1/4-Дюй-мовую ленту.
Концепция полиморфизма является фундаментальной для объектно-ориентированного программирования. Профессионалы часто говорят об объектно-ориентированном программировании как о наследовании с полиморфизмом времени исполнения. C++ поддерживает эту конструкцию, используя динамическое связывание (dynamic binding) и виртуальные функции-члены (virtual member functions). Эти понятия описываются в главе 12. Сейчас же мы концентрируем внимание на этих понятиях, не давая технической информации о языковой реализации.
Динамическое связывание позволяет многим различным объектам в системе отвечать на одно и то же сообщение. Каждый объект отвечает на это сообщение определенным для его типа способом. Рассмотрим работу профессионального маляра, когда он (или она) выполняет малярную работу с различными типами домов. Определенные общие задачи должны быть выполнены при покраске дома. Допустим, что они описываются в классе House. Кроме общих задач требуются особые методы работы для различных типов домов. Покраска деревянного дома отличается от покраски дома с наружной штукатуркой или дома с виниловой облицовкой стен и так далее. В контексте
объектно-ориентированного программирования особые малярные задачи для каждого вида дома задаются в производном классе, который наследует базовый класс House. Допустим, что каждый производный класс имеет операцию Paint. Класс House имеет операцию Paint, которая задается как виртуальная функция. Предположим, что BigWoody — это объект типа Wood-Frame. Мы можем указать Big Woody покрасить дом, вызывая явно операцию Paint. Это называется статическим связыванием (static binding).
BigWoody. Paint ( };	// static binding
Допустим, однако, что подрядчик имеет список адресов домов, которые нуждаются в покраске, и что он передает сообщение своей команде маляров пойти по адресам в списке и покрасить эти дома. В данном случае каждое сообщение привязано не к определенному дому, а скорее — к адресу дома-объекта в списке. Команда маляров приходит к дому и выбирает правильную малярную операцию Paint, после того, как она увидит тип дома. Этот процесс известен как динамическое связывание (dynamic binding).
(House at address 414) . Paxnt ( );	H dynamxc binding
Процесс вызывает операцию Paint, соответствующую дому по данному адресу. Если дом по адресу 414 является деревянным, то выполняется операция Paint( ) из класса WoodFrame.
При использовании структур наследования в C++ операции, которые динамически связываются с их объектом, объявляются как виртуальные функции-члены (virtual member functions). Генерируется код для создания таблицы, указывающей местоположения виртуальных функций какого-либо объ-_.=екта и устанавливается связь между объектом и этой таблицей. Во время выполнения, когда на положение объекта осуществляется ссылка, система использует это положение для получения доступа к таблице и выполнения правильной функции.
Понятие полиморфизма является фундаментальным в объектно-ориентированном программировании. Мы используем его с более совершенными структурами данных.
Письменные упражнения
1.1	Укажите различие между инкапсуляцией и скрытием информации для объектов.
1.2
(а)	Разработайте ADT для цилиндра. Данные включают радиус и высоту цилиндра. Операциями являются конструктор, вычисление площади и объема.
(б)	Разработайте ADT для телевизионного приемника. Данными являются настройки для регулирования громкости и канала. Операциями являются включение и выключение приемника, настройка громкости и изменение канала.
(в)	Разработайте ADT для шара. Данными являются радиус и его масса в фунтах. Операции возвращают радиус и массу шара.
(г)	Разработайте ADT для Примера 1.1, часть 2.
1,3	Геометрическое тело образовано высверливанием круглого отверстия с радиусом rh через центр цилиндра с радиусом г и высотой h.
(а)	Используйте ADT Cylinder, разработанный в упражнении 1.2(a), для нахождения объема геометрического тела.
(б)	Используйте ADT Circle, разработанный в тексте, и ADT Cylinder для вычисления площади этого геометрического тела. Площадь должна включать площадь боковой поверхности внутри отверстия.
1.4	Опишите несколько сообщений которые могут передаваться для ADT Television Set из упражнения 1.2(b). Что делает приемник в каждом случае?
1.5	Разработайте класс Cylinder, реализующий ADT из упражнения 1.2(a).
1.6	Нарисуйте цепочку наследования, включающую следующие термины: транспортное средство, автомобиль, грузовой автомобиль, автомобиль с откидным верхом, Форд и грузовой полуприцеп.
1.7	Какова структура разработки программного продукта?
1.8	Приведите три причины растущей популярности C++.
1.9	Какова связь между С и Сч-+?
1.10	Перечислите некоторые компиляторы C++ общего использования. К каким компиляторам вы имеете доступ? Для каждого компилятора укажите, предоставляет ли он интегрированную среду разработки (IDE), в которой имеются редактор, компилятор, компоновщик, отладчик и исполняющая система. Альтернативой является компилятор с командной строкой.
(а)	Объясните, что подразумевается под полиморфизмом.
(б)	Графическая система инкапсулирует операции окна в базовом классе Twindow. Производные классы реализуют окна главной программы, диалоги и элементы управления. Каждый класс имеет метод Setup Window, который инициализирует различные компоненты окна. Должен ли использоваться полиморфизм в объявлении SetupWindow в базовом классе?

Базовые типы данных
2.1.	Целочисленные типы
2.2.	Символьные типы
2.3.	Вещественные типы данных
2.4.	Типы перечисления
2.5.	Указатели
2.6.	Массивы
2.7.	Строчные константы и переменные
2.8.	Записи
2.9.	Файлы
2.10.	Приложения с массивами и записями
Письменные упражнения
Упражнения по программированию
Эта глава знакомит с серией базовых типов данных, которые включая числа, символы, определенные пользователем типы перечисления1 и указав ли. Эти типы являются естественными для большинства языков программир вания. Каждый базовый тип данных включает данные и операции, компонент абстрактного типа данных (ADT). В этой главе мы предоставляем ADT дл целочисленных, литерных типов, типов real number, типов перечисления указателей. Языки программирования реализуют ADT на компьютере, испол зуя различные представления данных, включая двоичные числа и символы коде ASH (American Standard Code for Information Interchange). В этой кни язык C++ используется для иллюстрации реализации абстрактных типов да ных.
Численные, литерные типы и указатели описывают простые данные, потоь что объекты этих типов не могут быть разделены на меньшие части. Наоборо структурированные типы данных имеют компоненты, построенные из прость. типов по правилам, определяющим связи между компонентами. Структурир ванные типы включают массивы, строки, записи, файлы, списки, стеки, оч реди, деревья, графы и таблицы, которые определяют основные темы этс книги. Большинство языков программирования предоставляют синтаксиче кие конструкции или библиотечные функции для обработки массивов, стро] записей и файловых структур. По существу, мы определяем их как встроеннь (built-in) структурированные типы данных. Мы предоставляем абстрактнь типы данных для этих встроенных структур и обсуждаем их реализацию C++.
Мы также представляем серию приложений для встроенных структурир» ванных типов, которые вводят важные алгоритмы последовательного поиск и обменной сортировки. Одно из приложений иллюстрирует реализацию тип строки с использованием библиотеки строк C++. C++ использует иерархи наследования для реализации файлов. Другое приложение показывает обр; ботку трех различных типов файлов.
2.1.	Целочисленные типы
Целые числа — это положительные или отрицательные целые числа, сост< ящие из знака и последовательности цифр. О целых числах говорят, как знаковых (signed) числах. Например, далее следуют специфические целочис ленные значения, называемые целочисленные константами (integer constants
+35 -278 19 (знак +) -28976510
Вы знакомы с элементарной арифметикой, в которой определяется сери операторов, имеющих результатом новые целые значения- Операторы, принй мающие один операнд (унарный оператор) или два операнда (бинарный опере тор) создают целые выражения:
(Унарный +) +35 — 35	(Вычитание -) 73 — 50 = 23
(Сложение +) 4+6 == 10	(Умножение *) -3*7 = -21
Целые выражения могут использоваться с арифметическими операторам! сравнения для получения результата, который является True или False.
(Сравнение меньше, чем)	5 < 7	(True)
(Сравнение больше, чем или равно ) 10 >= 18	(False)
1 Термины "типы перечисления” и "перечислимые типы” встречаются в русских источника: одинаково часто. — Прим. ред.
Теоретически, длина целых чисел не имеет ограничения, — факт, подразумевающийся в определении ADT.
ADT, которые мы предусматриваем для простых типов, предполагают, что читатель знаком с предусловиями, входом и постусловиями для каждой операции. Например, полной спецификацией для операции деления целых (Integer Division) является:
Integer Division Вход: Предусловия: Процесс: Выход: Постусловия:
Два целых значения и и v.
Знаменатель v не может быть равен 0.
Разделить v на и, используя операцию Integer Division.
Значение частного.
Возникает условие ошибки, если v = 0.
В нашей спецификации мы используем нотацию оператора C++ вместо родовых названий операций и только описываем процесс.
ADT integer
Данные
Целое число N со знаком
Операции
Предположим, что и и v являются целыми выражениями, a N - целая переменная.
Присваивание
= N - и Присваивает значение выражения и переменной N
Бинарные арифметические операции		
+ и	+ V	Сложение
- U	— V	Вычитание
* U	* V	Умножение
/ U	/ V	Деление нацело
% U	% V	Остаток от деления нацело
Унарные	арифметические операции	
-v	।	Изменение знака (унарный минус)
+ +t	।	То же, что и и (унарный плюс)
Операции отношения
(Выражение отношения — это истинность заданного условия)
== и	== V	Результат	- TRUE, если	U	эквивалентно v
!= и	! = v	Результат	— TRUE, если	и	не эквивалентно v
< и	< v	Результат	— TRUE, если	и	меньше v
<= U	<= V	Результат	— TRUE, если	и	меньше, либо равно v
> U	> v	Результат	- TRUE, если	и	больше v
>= U	>= V	Результат	— TRUE, если	U	больше, либо равно v
Конец adt in teger					
Пример 2.1 кз-5 &V? val =25/20 rem = 25 % 20
(выражение имеет значение 6)
(val - 1)
(rem = 5)
Компьютерное хранение целых чисел
Реализация целых чисел обеспечивается описаниями типа языка программирования и компьютерными аппаратными средствами. Компьютерные системы сохраняют целые числа в блоках памяти с фиксированной длиной. Результирующая область значений лежит в ограниченном диапазоне. Длина блока памяти и диапазон значений зависят от реализации. Для того, чтобы способствовать некоторой стандартизации, языки программирования предоставляют простые встроенные типы данных для коротких и длинных целых. Когда требуются очень большие целые значения, приложение должно предоставлять библиотеку подпрограмм для выполнения операций. Библиотека целых операций может расширить реализацию целых до любой длины, хотя эти подпрограммы значительно снижают эффективность системы, исполняющей приложения.
В компьютере целые хранятся как двоичные (бинарные) числа (binary numbers), состоящие из различных последовательностей цифр О и 1. Это представление моделируется на базе 10 (или десятичной системы, которая использует цифры 0, 1, 2	9). Десятичное число сохраняется как набор
цифр d0 , dt , d2, и т.д., представляющий степени 10. Например, ^-разрядное число
Кю = dk i dk.2 .А ... dt d0 , для 0 < dt < 9 представляет
Nl0 = d^( Iff’‘)+ dh!!( 10”^)+...+dt( Iff )+...+ dl(101)+d0(lff>)
Индекс 10 указывает на то, что N записано как десятичное число. Например, десятичное целое из четырех цифр 2589 представляет
258910 = 2(1О3)+ 5(102)+ 8(109+ 9(10°) \ / = 2(1000)+ 5(100)+ 8(10)+ 9	|/
Двоичные целые используют цифры 0 и 1 и степени 2-х (2° = 1, 21 = 2, 22 = 4, 23 = 8, 24 = 16 и так далее). Например, 1310 имеет двоичное представление
1310 = 1(2») + 1(22) + 0(29 + 1(2°) - И012
Двоичная цифра называется бит (bit), сокращение, включающее bi от binary и t — от digit. В общем виде Л-разрядное или Л-битовое двоичное число имеет представление:
iV2 ~ bjf.j &k-2 — Ь» ... bj bo
= bkl(2kl)+bk.z(2k2)+^ +b/29+... +b1(21)+b0(2°). 0 <b^l
Следующее 6-битовое число дает двоичное представление 42. Десятичное значение двоичного числа вычисляется добавлением членов в сумме:
1010102= 1(25) + 0(24) + 1(23) + 0(22) + 1(29 + 0(2°) = 4210
* gj Пример 2.2
Вычислить десятичное значение двойчного числа:
fe* 1101012= 1(25) + 1(24) + 0(23) + 1(22) + 0(29 + 1(2°) = 5310 ВЙ 100001102=1(27) + 1(22) + 1(21) _ 1за10
Преобразование десятичного числа в его двоичный эквивалент может быть выполнено нахождением самой большой степени 2-х, которая меньше, чем это число или равна этому числу. Прогрессия степеней 2-х включает значения 1 2, 4, 8, 16, 32, 64 и так далее. Это дает ведущую цифру в этом двоичном представлении. Остающиеся степени 2-х заполняются по убывающей до О. Например, рассмотрим значение 35. Числом с самой большой степенью 2-х, меньшим, чем 35, является 32 = 25, это подразумевает, что 35 является 6-разрядным двоичным числом:
35ю-1(32) + 0(16) + 0(8) + 0(4) + 1(2) + 1 = 1000112
Чистые двоичные числа являются просто суммой степеней 2-х. Они не имеют знака, ассоциированного с ними, и о них говорят как о беззнаковых числах (unsigned numbers). Эти числа представляют положительные целые. Отрицательные целые используют либо представление точного дополнения, либо — величины со знаком. В любом формате специальный бит, называемый знаковым битом (sign bit), указывает знак числа.
Данные в памяти
Числа сохраняются в памяти как последовательности двоичных цифр фиксированной длины. Распространенные длины включают 8, 16, и 32 бита. Последовательность битов измеряется 8-битовой единицей, называемой байт (byte).
0	0	1	0	0	0	1	1
Число 35 как байт
В таблице 2.1 приводится диапазон беззнаковых чисел и чисел со знаком для этих распространенных длин.
Диапазоны чисел и размер в байтах
Таблица 2.1
Размер	Диапазон беззнакового числа	Диапазон числа со знаком
8 (1 байт)	0 .. 255=2е - 1	-27 =-128 .. 127=27—1
16 (2 байта)	0 .. 65535=216-1	—215=—32768 .. 32767=215—1
32 (4 байта)	0 . 4294967295=232—1	—231 .. 231—1	|
Компьютерная память — это последовательность байтов, к которой обращаются с помощью адресов 0, 1, 2, 3 и так далее. Адрес (address) целого в памяти — это местоположение первого байта последовательности. Рис.2.1 иллюстрирует вид памяти с числом 871О = 10101112, находящимся в одном байте с адресом 3, и числом 500ю = 00000001111101002 с адресом 4.
			01010111	00000001 11110100
1	2	3	4	5
Рис 2.1. Вид памяти
Адрес 0
Представление целых в языке С++
Целыми типами в языке C++ являются: int, short int и long int. Тип short int (short) предоставляет 16-битовые (2-байтные) целые значения в диапазоне от -32768 до 32767. Тип long int (long) предоставляет самый широкий диапазон целых значений и в большинстве систем реализуется с 32 битами (4 байтами), и поэтому его диапазон составляет от -231 до 283 — 1.
Общий тип int идентифицирует целые, чья длина в битах зависит от компьютера и компилятора. Обычно компиляторы используют 16-битовые или 32-битовые целые. В некоторых случаях пользователь имеет возможность выбирать длину целого в качестве опции. Целые типы данных устанавливают область значений данных и задают арифметические операторы и операторы отношения. Каждый тип данных задает реализацию целого ADT с ограничением, что целые значения находятся в некотором конечном диапазоне.
2.2.	Символьные типы
Символьные данные включают алфавитно-цифровые элементы, которые определяют заглавные и строчные буквы, цифры, знаки пунктуации и специальные символы. Компьютерная индустрия использует различные представления символов для приложений. Набор символов ASCII из 128 элементов имеет самое широкое применение для текстовой обработки, ввода и вывода текста и передачи данных. Мы используем набор ASCII для нашего символьного ADT. Подобно целым, символы ASCII включают отношение порядка, определяющее набор операторов отношения. В случае алфавитных символов буквы следуют словарному порядку. В этом отношении все заглавные буквы меньше, чем строчные:
Т < W, b<d, T<b
ADT Character
данные
Набор символов ASCII
Операции
Присваивание
Значение символа может присваиваться символьной переменной.
Операция отношения
Шесть стандартных операций отношения применяются к символам с использованием ASCII словарного отношения порядка.
Конец ADT Character
Символы ASCII
Большинство компьютерных систем используют стандартную систему кодирования ASCII для символьного представления. Символы ASCII хранятся как 7-битовый целый код в 8-битовом числе. 27 = 128 различных кодов подразделяются на 95 печатаемых и 33 управляющих символа. Управляющий символ используется при передаче данных и вызывает выполнение управляющей функции устройством, например, перемещение курсора дисплея на одну строку вниз.
В таблице 2.2 показан печатаемый набор символов ASCII. Символ "пробел" представлен как ♦. Десятичный код для каждого символа задается десятичной позицией, которой соответствует иомер строки, и единичной, — которой соответствует номер столбца. Например, символ *’Т” имеет ASCII-значение 84ю и хранится в двоичном виде как 010101002.
В символьном наборе ASCII десятичные цифры и алфавитные символы находятся в правильно определенных диапазонах (табл. 2.3). Это делает более легким преобразование между заглавными и строчными буквами и преобразование цифры ASCII (’О’ ... ’9’) в соответствующее число (0 ... 9).
Таблица 2.2
Набор печатаемых символов А5СЦ
- Левая цифра	Правая цифра									
	0	1	2	3	4	5	6	7	8	9
3			♦	I		#	$	%	&	
4	(	)	♦	+	f “		/	0	1	
5	2	3	4	5	6	7	8	9	•	г
6	<		>	7	@	А	В	С	D	Е
7	F	G	Н	I	J	К	L	м	N	0
8	Р	Q	R	S	т	и	V	W	X	Y
9	Z	[	\	1			•	а	b	с
10	d	е	f	9	h	i	j	к	I	m
11	п	о	Р	q	г	S	t	и	V	W
12	X	У	Z	{		}	-			
Коды 00-31 и 127 — управляющие символы, которые являются непечатаемыми.
Символьные диапазоны ASCII
Таблица 2.3
Символы ASCII	Десятичный	Двоичный
Пробел	32	00100000
Десятичный знак	48-57	00110000-00111001
Символ верхнего регистра	65-90	01000001-01011010
Символ нижнего регистра	97-122	01100001-01111010
Пример 2.3
1.	ASCII-значение для цифры *0’ — это 48. Цифры упорядочены в диапазоне 48 — 57:
ASCII-цифра ’3’ — это 51 (48 + 3)
Соответствующая численная цифра получается вычитанием ’0’ (ASCII 48):
Численная цифра: 3 = ’3’ — ’0’ = 51 — 48
2.	Для преобразования заглавного символа в строчный добавьте 32 к ASCII-значению символа:
ASCII (*А’) = 65 ASCII (’а’) = 65 + 32 = 97
Для хранения символа в C++ используется простой тип char. Коды ASCII находятся в диапазоне от 0 до 127; однако, для использования остающихся значений диапазона часто определяются машинно-зависимые расширенные символы. Как целый тип, значение является кодом для символа.
2.3.	Вещественные типы
Целые типы, о которых говорят как о дискретных типах (discrete types), представляют значения данных, которые могут быть продолжены, например: -2, -1, 0, 1, 2, 3 и так далее. Многие приложения требуют чисел, которые имеют- дробные значения. Эти значения, называемые вещественными числами (real numbers), могут быть представлены в формате с фиксированной точкой (fixed-point format) с целой и дробной частью:
9.6789 -6.345 +18.23
Вещественные числа могут быть также записаны как числа с плавающей точкой в экспоненциальном формате (scientific notation). Этот формат представляет числа как серию цифр, называемых мантиссой (mantissa) и порядком (exponent), который представляет степень 10-и. Например, 6.02е23 имеет мантиссу 6.02 и порядок 23. Число с фиксированной точкой является просто особым случаем числа с плавающей точкой с порядком О. Как в случае с целыми и символами, вещественные числа составляют абстрактный тип данных. Стандартные арифметические и операции отношения применяются к вещественным числам, используемым вместо целых.
ADT Real
Данные
Числа, описанные в формате с фиксированной или плавающей точкой.
Операции
Присваивание
вещественное выражение может Оыть присвоено действительной переменной.
Арифметические операторы
Стандартные двоичные и унарные арифметические операции применяются к вещественным числам вместо целых. Никаких других операторов не имеется.
Операции отношения к вещественным числам применяются шесть стандартных операций отношения.
конец ADT Real
Представление вещественных чисел
Как и для целых чисел, область вещественного числа не имеет предела. Значения вещественных чисел являются безграничными и в отрицательном, и в положительном направлении, а мантисса распределяет действительные числа на последовательность точек числовой оси. Вещественные числа реализуются в конечном блоке памяти, ограничивающем диапазон значений и образующем дискретные точки числовой оси.
tt
В течение многих лет специалисты использовали разнообразные форматы для хранения чисел с плавающей точкой. Формат IEEE (Института инженеров по электротехнике и радиоэлектронике) является широко используемым стандартом. Вы знакомы с вещественными числами, которые используют формат с фиксированной точкой. Таким является число, разделяемое на целую часть и дробную, цифры которой умножаются на 1/10, 1/100, 1/1000 и так далее. Десятичная точка разделяет эти части:
25.638 = ZCIO1) + 5(10°) + 6(10-1) + 3(10-2) +8(10 3)
Как в случае с целыми числами, имеются соответствующие двоичные представления для вещественных чисел с фиксированной точкой. Эти числа содержат целую и двоичную дробную части и двоичную точку с дробными цифрами, соответствующими 1/2, 1/4, 1/8 и т. д. Общая форма такого представления:
TV =• bn...	. b-ib_z... = bn2n + ... + Ь02° + b.,21 + b^z2~z +...
Например:
1011.110g = 1(23) + Ц21) + 1(2°) + 1(2-1) + 1(22) + 1(2"4)
= 8 + 2 + 1 +0.5 + 0.25 + 0.0625
= 11.812510
Преобразование десятичных и двоичных чисел с плавающей точкой использует алгоритмы, подобные разработанным для целых чисел. Преобразование в десятичные числа выполняется сложением произведений цифр и степеней 10-и. Обратный процесс является более сложным, поскольку десятичное число может потребовать бесконечного двоичного представления для создания эквивалентного числа с плавающей точкой. В компьютере количество цифр ограничено, так как используются только числа с плавающей точкой фиксированной длины.
Пример 2.4
Преобразовать двоичное число с фиксированной точкой в десятичное число:
1.	0.011012 - 1/4 + 1/8 + 1/32
- 0.25 + 0.125 + 0.03125 = О.4О6251о
Преобразовать десятичное число в двоичное число с плавающей точкой:
2.	4.3125Ю = 4 + 0.25 + 0.0625 = 100.01012
3.	Десятичное число 0.15 не имеет эквивалентной двоичной дроби фиксированной длины. Преобразование десятичной дроби в двоичную требует бесконечного двоичного расширения. Поскольку компьютерная память ограничена числами фиксированной длины, хвост бесконечного расширения отсекается, и частичная сумма является приближением десятичного значения:
0.15ю = 1/8+1/64+1/128+1/1024+ ... =0.0010011001 ...2
Большинство компьютеров хранят вещественные числа в двоичной форме, используя экспоненциальный формат со знаком, мантиссой и порядком:
N ~	• • • Ъ&О •	X 2е
C++ поддерживает три вещественных типа данных: float, double и long double. Тип long double применяется для вычислений, требующих высокой
точности и не используется в этой книге. Часто тип float реализуется с помощью 32-битового формата IEEE с плавающей точкой, тогда как 64-битовый формат используется для типа double.
2.4.	Типы перечисления
Набор символов ASCII использует целое представление символьных данных. Сходное целое представление может быть использовано для описания определяемых программистом наборов данных. Например, вот перечень месяцев, длина которых составляет 30 дней:
Апрель, июнь, сентябрь, ноябрь
Этот набор месяцев образует тип перечисления (enumerated data type). Для каждого типа упорядочение элементов определяется так, как эти элементы перечислены. Например:
Цвет волос
черные белокурые каштановые
рыжие
// первое значение
// второе значение
// третье значение
// четвертое значение
черные
белокурые
каштановые
рыжие
Этот тип поддерживает операцию присваивания и стандартные операции отношения. Например:
черные < рыжие	//черные находятся перед рыжими
каштановые >= белокурые //каштановые находятся после белокурых
Тип перечисления имеет данные и операторы и, следовательно, является ADT.
adt Enumerated
Дгенные
Определяемый пользователем список N отдельных элементов.
Операции
Присваивание
Переменной типа перечисления может быть присвоен любой иэ элементов в списке.
конец ADT Enumerated
Реализация типов перечисления C++
C++ имеет тип перечисления, определяющий отдельные целые значения, на которые ссылаются именованные константы.
Пример 2.5
1.	Булев тип может быть объявлен типом перечисления. Значением константы False является О, а значение True — это 1. Переменная Done определяется как булева с первоначальным значением False.
enum Boolean (False, True);
Boolean Done “ False.-
2.	Месяцы года объявляются как тип перечисления. По умолчанию первоначальным значением Jan является О. Однако, целая последовательность может начинаться с другого значения путем присваивания этого значения первому элементу. В этом случае Jan является 1, и месяцы соответствуют последовательности 1, 2,. . . , 12.
enum Month {Jan-=l,Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); Month Mon = Dec;
2.5.	Указатели
Тип указателей является основным в любом языке программирования. Указатель (pointer) — это беззнаковое целое, представляющее адрес памяти. Указатель служит также в качестве ссылки на данные по адресу. В адресе указателя тип данных называется базовым типом (base type) и используется в определении указателя. Например, указатель Р имеет значение 5000 в каждом случае на рис. 2.2. Однако, в (а) указатель ссылается на символ, а в (Ъ) указатель ссылается на короткое целое.
Указатель делает возможным эффективный доступ к элементам в списке и является фундаментальным для разработки динамических структур данных, таких как связанные тексты, деревья и графы.
(а)
(в)
Рис. 2.2. Тип указатель
Указатели ADT
Как число, указатель использует некоторые арифметические операторы и операторы отношения. Арифметические операции требуют особого внимания. Указатель может быть увеличен или уменьшен на целое значение для ссылки на новые данные в памяти. Добавление 1 обновляет указатель для ссылки На следующий элемент этого типа в памяти. Например, если р указывает на объект char, то р + 1 указывает на следующий байт в памяти. Добавление к > 0 перемещает указатель на к позиций данных вправо. Например, если Р указывает на double, р + к ссылается на double:
N = sizeof (double) * k байтов вправо от p.
Тип данных	Текущий адрес	Новый адрес
char	р = 5000	р + 1 = 5001
int (2 байта)	р = 5000	р + 3 = 5000 + 3*2 = 5006
„double (4 байта)	р = 5000	р - 6 = 5000 — 6 * 4 = 497е
Указатель использует адресный оператор который возвращает адрес памяти элемента данных. Напротив, оператор ссылается на данные, ассс циированные со значением указателя. Упорядочение указателей происходи путем сравнения их беззнаковых целых значений.
Динамическая память (dynamic memory) — это новая память распределяе мая во время выполнения программы. Динамическая память отличается о статической памяти (static memory), чье наличие определяется до начала вы полнения программы. Динамическая память описывается в главе 8. Операто] new принимает тип Т, динамически выделяет память для элемента типа Т i возвращает указатель на память, которую он выделил. Оператор delete прини мает указатель в качестве параметра и освобождает динамическую память выделенную ранее по этому адресу.
adt Pointer
Данные
Набор беззнаковых целых, который представляет адрес памяти для элемента данных базового типа Т.
Операции
Предположим, что и и v — это выражения-указатели, i — это целое выражение, ptr — это переменная pointer, a var — это переменная типа Т.
Адрес &	ptr =	&var	Присваивает	ptr	адрес переменной var.
Присваивание	ptr »	u	Присваивает	ptr	значение указателя и.
Разыменовывание *	var =		*ptr	Присваивает элемент типа Т, на который ссылается ptr, переменной var.		
Выделение и освобождение динамической памяти
new	ptr = new Т Создает динамическую память для элемента типа Т
и присваивает ptr его адрес.
delete delete ptr освобождает динамическую память, выделенную по адресу ptr.
Арифме тическая
+	и + i	Указывает на элемент, размешенный на i элементов данных правее элемента, на который ссылается и.
—	и - i	Указывает на элемент, размещенный на i элементов левее элемента, на который ссылается и.
—	и — V	Возвращает число элементов базового типа,
которые находятся между двумя указателями.
Отношения
К указателям применяются шесть стандартных операторов отношения путем сравнения их беззнаковых целых значений.
Конец ADT Pointer
Значения указателя
Значение указателя — это адрес памяти, использующий 16, 32 или более битов в зависимости от машинной архитектуры. В качестве примера: PC — это указатель на char (1 байт), а РХ — это указатель на short int (2 байта).
char str[) = ABCDEFG;
char *₽C s str; // PC указывает на строку str
short X = 33;
short *PC = «X;	// РХ указывает на X типа short
Следующие операторы иллюстрируют основные операции с указателями.
cout «	*PC « endl;	// напечатать 'A'
РС+= 4;		// сдвинуть PC вправо на четыре символа
cout «	*PC « endl;	// напечатать 'Е'
PC—;		// сдвинуть PC влево на один символ
cout «	*PC « endl;	// напечатать 'D'
cout «	*PX + 3 « endl;	// напечать 36 = 33 + 3
Операции new и delete обсуждаются в главе 8.
2.6.	Массив (array)
Массив является примером набора данных. Одномерный массив — это конечный, последовательный список элементов одного и того же типа данных — однородный массив (homogeneous array). Последовательность определяет первый элемент, второй элемент и так далее. С каждым элементом ассоциирован целый индекс (index), определяющий позицию элемента в списке. Массив имеет оператор индекса, который делает возможным прямой доступ (direct access) к элементам в списке при сохранении "или возвращении какого-либо элемента.
Ао	Ai	А2	А3
ADT Array
Данные
Набор N — элементов одного и того же типа данных; индексы выбираются из диапазона целых от 0 до N — 1, которые определяют позицию элементов в списке и обеспечивают прямой доступ к элементам. Индекс 0 ссылается на первый элемент в этом списке, индекс 1 ссылается на второй элемент и т.д.
Операции
Индексирование [ ]
Вход: Предусловия: Процесс: Выход: Постусловия: Конец ADT Array	Индекс Индекс находится в диапазоне от 0 до N — 1. В правой части оператора присваивания оператор индексирования возвращает данные из элемента; в левой части оператора присваивания оператор индексирования возвращает адрес элемента массива, который сохраняет правую часть выражения. Если операция индексирования находится в правой части оператора присваивания, то операция выбирает данные из массива и возвращает эти данные клиенту. Если операция индексирования находится в левой части, то заменяется соответствующий элемент массива.
3 Зак. 425
Встроенный тип массива C++
В качестве части своего базового синтаксиса C++ предоставляет встроенный тип статического массива» который определяет список элементов одного и того же типа. Явное объявление задает постоянный размер массива N и указывает, что индексы находятся в диапазоне от 0 до N—1. Объявление в примере 2.6 определяет статический массив C++. В главе 8 мы будем создавать динамические массивы, используя указатели.
5-^ Пример 2.6
1. Объявить два массива типа double. Массив X имеет 50 элементов, а массив Y — 200 элементов:
double X [ 50 ], Y ( 200 ] ;
2. Объявить длинный массив А с размером, задаваемым целой кон-г=?й стантой ArraySize = 10.
const int ArraySize = 10;
long	A[ArraySize];
Индекс массива в диапазоне от 0 до ArraySize - 1 используется для доступа к отдельным элементам массива. Элемент с индексом i имеет представление A(i].
Индексирование массива фактически выполняется путем использования оператора индексирования ([]). Это двоичный оператор, левый операнд которого является именем массива, а правый — позицией элемента в массиве. Доступ к элементам массива может осуществляться в любой части оператора присваивания:
A[i] = х;	// присвоить (сохранить) х как данные для элемента массива
t • A[iJ ;	// возвратить данные из A[iJ и присвоить их переменной t
A[i] = A[i + 1] = х,
// присвоить х элементу A[i +1]. Второе присваивание сохраняет данные из A(i +1] в A[i] .
Сохранение одномерных массивов
C++ одномерный массив А логически сохраняется как последовательное упорядочение элементов в памяти. Каждый элемент относится к одному и тому же типу данных.
А[0]
Address А[0]
A[i]
I
Address A[i]
В C++ имя массива является константой и рассматривается как адрес первого элемента этого массива. Так, в объявлении
Туре А[ArгaySize];
имя массива А является константой и местоположением в памяти первого элемента А[0]. Элементы А[1], А[2] и т.д. следуют за иим последовательно. Допустим, что sizeof(Type) = М, весь массив А занимает М * ArraySize байтов.
А{0]	А[1]	А[2]	А[3]
Компилятор задает таблицу, называемую дескриптор массива (dope vector), для ведения записи характеристик массива. Таблица включает информацию о размере каждого элемента, начальный адрес массива и количество элементов в этом массиве:
Начальный адрес:	А
Количество элементов массива:	ArraySize
Размер типа:	М = sizeof(Туре)
Эта таблица используется компилятором также для реализации функции доступа (access function), которая определяет адрес элемента в памяти. Функция ArrayAccess использует начальный адрес массива и размер типа данных для установки индекса I по адресу А[1]:
Adress А[Т] » ArrayAccess(А, I, М)
ArrayAccess задается:
ArrayAccess (А, I, М) = А * I * М;
А[0]	А[1]		All]		A[ArraySize-1)
| О |	!
А А+ГМ
1 1 I
A+I*M	A+(ArraySize-1)*M
Пример 2.7
Предположим, что float сохраняется с использованием 4 байт (sizeof(float) = 4) и массив Height начинается в памяти по адресу 20000. float Height [35];
Элемент массива Height[18] размещается по адресу
20000 + 18 М = 20072
Границы массива
ADT массива предполагает, что индексы находятся в целом диапазоне от 0 До N — 1, где N — это размер массива. То же самое и в C++. В действительности, большинство компиляторов C++ при доступе к элементу массива не генерируют Код, который тестирует, находится ли индекс вне пределов массива. Например, следующая последовательность будет приниматься большинством компиляторов: int v = 20;
[int А[201;	//размер массива 20; индексный диапазон 0—19
A(V] = 0;	//индекс V больше, чем верхний предел
Массив занимает память в области данных пользователя. Функция доступа к массиву определяет адрес в памяти для отдельного элемента, и обычно не производится проверки того, находится ли адрес на самом деле в диапазоне элементов массива. В результате C++ программа может использовать индексы, находящиеся вне указанного диапазона. Адреса для элементов, которые находятся вне диапазона массива, могут все же находиться в области данных пользователя. В процессе выполнения программа может замещать другие переменные и вызывать нежелательные ошибки. На рисунке 2.3 показан вид программы в памяти.
Рис. 2.3. Размещение памяти в интервале данных пользователя
Некоторые компиляторы реализуют проверку индекса массива, генерируя код времени исполнения для проверки того, находятся ли индексы массива внутри диапазона. Поскольку дополнительный код замедляет выполнение, большинство программистов используют проверку массива только во время программной разработки. Как только код отлажен, эта опция выключается, и программа перекомпилируется на более эффективный код. Другим подходом к проблеме является разработка "надежного массива", который реагирует на неправильные индексные ссылки и выводит сообщение об ошибке. Надежные массивы разрабатываются в главе 8.
Двумерные массивы
Двумерный массив, часто называемый матрицей, является структурированным типом данных, который создается путем вложения одномерных массивов. Доступ к элементам выполняется по индексам строк и столбцов- Например, на следующем рисунке представлен массив Т из 32 элементов с 4 строками и 8 столбцами. Значение 10 доступно с помощью пары индексов (строка, столбец) (1, 2), а -3 — с помощью индексов (2, 6).
Концепция двумерного массива может быть расширена для охвата основных многомерных массивов, элементы в которых доступны с помощью трех или более индексов. Двумерные массивы имеют примененние в таких различных областях, как обработка данных и численное решение дифференциальных уравнений в частных производных.
количество столбцов и тип данных элементов массива:
type Т [RowCountJ [ColumnCount];
Ссылка на элементы массива Т производится с помощью индексов строки и столбца:
T[i] [j], О <- i <= RowCount — 1, О <= 3 <= ColumnCount — 1
Например, матрица Т — это массив целых размером 4x8:
int Т[4] [8];
Значение Т[1][2] = 10 и Т[2][6] = -3.
Мы можем представлять двумерный массив как список одномерных массивов. Например, Т[0] — это строка 0, которая состоит из ColumnCount отдельных элементов. Данная концепция полезна, когда двумерный массив передается в качестве параметра. Нотация int Т[][8] указывает на то, что Т является списком из 8~элементных массивов.
Сохранение двумерных массивов
Двумерный массив может инициализироваться присваиванием элементам по одной строке каждый раз. Например, массив Т задает таблицу размером 3x4:
int Т[3] (41 - ({20, 5, - 30, 0}, {-40, 15, 100, 80], {3, 0, 0 -1}};
Как массив, элементы сохраняются в следующем порядке: первая строка, вторая строка, третья строка (рис. 2.4).
Для доступа к элементу в памяти компилятор расширяет дескриптор массива для включения информации о количестве столбцов и длине каждой строки 11 Использует новую функцию доступа, называемую MatrixAccess, для возвращения адреса элемента.
Начальный адрес: Количество строк: Количество столбцов Размер типа:
Длина строки:
RowCount
ColumnCount
М = sizeof(Type)
RS = M*CoIumnCount //длина всей строки
Функция MatrixAccess принимает пару индексов строки и столбца (I,J) и возвращает адрес элемента T[I][J]:
Address T[I] [J] = MatrixAccess (T, I, J) “ T f (I * RS) + (J * M)
Значение (I * RS) дает количество байтов, необходимое для хранения I строк данных. Значение (J * М) дает количество байтов для хранения первых J элементов в строке I.
20	5	-30	0
Построчное хранение массива
-40	15	100	80
3	0	0	-1
Строка#0	Строка#!	Строка#2
Рис. 2.4. Хранение матрицы Т
Пример 2.8
Пусть Т будет матрицей 3x4 на рис. 2.4. Предположим, что длина целого равна 2, и матрица хранится в памяти по адресу 1000.
Начальный адрес: Количество строк: Количество столбцов: Размер типа: Длина строки:
20	5	-30	0
Строка#0
1000
1000
3
4
2 = sizeof(int)
8 — 2*4// длина всей строки
ТПИЗ]
-40
Строка#!
1008
Строка#2
1016
1.	Адреса для строк в памяти следующие:
Строка О: Адрес 1000
Строка 1:	Адрес 1000 + 1 * 8 = 1008 (строка — это 8 байт)
Строка 2:	Адрес 1000 + 2 * 8 = 1016
2.	Адресом Т[1][3] является;
М ] ArrayAccess (Т, 1, 3) = 1000 + (1 * 8) + (2 * 3)
К	= IM*
2.7.	Строковые константы и переменные
Массив — это структурированный тип данных, содержащий однородный список элементов. Особая форма массива содержит символьные данные, которые определяют имена, слова, предложения и так далее. Структура, называемая строкой (string), обращается с символами как с одним объектом и предоставляет операции для доступа к последовательностям символов в строке. Строка является важнейшей структурой данных для большинства приложении, использующих алфавитно-цифровые данные. Эта структура необходима для управления текстовой обработкой с ее операциями редактирования, ее алгоритмами поиска/замены и т. д. Например, лингвисту может потребоваться информация о количестве появлений определенного слова в документе, или программист может использовать шаблоны поиска/замены для изменения исходного кода в документе. Большинство языков объявляют строковые структуры и предоставляют встроенные операторы и библиотечные функции для управления строками.
Для определения длины строки структура может включать 0 в конце строки (строка с нулевым завершающим символом или NULL-символом — NULL-terminated string) или отдельный параметр длины. Следующие представления строки содержат 6-символьную строку STRING.
Строка с нулевым символом в конце
Серия операций обрабатывает строку как единый блок символов- Налри-МеР» мы можем определить длину строки, копировать одну строку в другую, объединять строки (конкатенация) и обрабатывать подстроки операциями вставки, удаления и отождествления. Строки имеют также операцию срав-йения, позволяющую их упорядочивать. Эта операция использует АБСП-упо-Рядочивание. Например:
•'Baker" меньше, чем "Martin"	// в следует перед М
"Smith" меньше, чем "Smithson"
"Barber" следует перед "barber" //заглавная В предшествует строчной Ь
"123Stop" меньше, чем ”ААА"	//числа предшествуют буквам
ADT String
Данные
Строка является последовательностью символов с ассоциированной длиной.
Строковая структура может иметь NULL-символ или отдельный параметр длины.
Операции
Длина
Вход:	Нет
Предусловия: Нет
Процесс:	Для строки с NULL-символом подсчитать символы
до NULL-символа; для строки с параметром длины возвращать значение длины.
Выход:	Возвращать длину строки
Постусловия: Нет
Копирование
Вход:	Две строки: STR1 и STR2. STR2 — это источник,
a STR1 — это место назначения.
Предусловия: Нет
Процесс:	копирование символов из STR2 в STR1.
Выход:	Возвращать доступ к STR1
Постусловия: Создается новая строка STR1 с длиной и данными, полученными из STR2.
Коккатенация
Вход:	Две строки STR1 и STR2. Соединить STR2 с хвостом STR1
Предусловия:	Нет
Процесс:	Нахождение конца STR1. Копирование символов из STR2
в конец STR1. Обновление информации о длине STR1.
Выход:	Возвращать доступ к STR1.
Постусловия: STR1 изменяется.
Сравнение
Вход:	Две строки: STR1 и STR2.
Предусловия:	Нет
Процесс:	Применение ASCII-упорядочения к этим строкам.
Выход:	Возвращать значение следующим образом:
STR1 меньше, чем STR2: возвращать отрицательное значение
STR1 равна STR2: возвращать значение О
STR1 больше, чем STR2: возвращать положительное значение
Постусловия:	Нет
Индексация
Вход:	Строка STR и одиночный символ СН
предусловия: Нет
Процесс:	Поиск STR для входного символа СН
Выход:	Возвращать адрес места, содержащего первое появление
СН в STR или 0, если этот символ не найден.
Постусловия: Нет
Правый индекс
Вход:	Строка STR и одиночный символ СН
Предусловия: Процесс t Выход:
• Постусловия: Утение
Вход:
Предусловия Процесс: Выход:
Постусловия: Запись
Вход:
Предусловия: Процесс: Выход:
Постусловия:
Конец ADT String
Нет
Поисц STR для последнего появления символа СН.
Возвращать адрес места, содержащего последнее появление
СН в STR или 0, если этот символ не найден.
Нет
Файловый поток, символы которого считываются, и строка
STR для сохранения символов.
Нет
Считывание последовательности символов из потока в строку STR.
Нет
Строке STR присваиваются считываемые символы
Строка, которая содержит символы для выхода, и поток, в который символы записываются.
нет
Пересылка строки символов в поток.
Выходной поток изменяется.
Нет
Строки C++
Глава 8 содержит спецификацию и реализацию C++ класса String. Этот класс содержит расширенный набор операторов сравнения и операций ввода/вывода. В этой главе мы используем строки с NULL-символами и C++ строковую библиотеку для реализации ADT.
Строка в C++ — это строка с нулевым завершающим символом, в которой NULL-символ обозначается символом О в коде ASCII. Компилятор определяет строковую константу (string literal) как последовательность символов, заключенную в двойные кавычки. Строковая переменная (string variable) — это символьный массив, который содержит последовательность символов с NULL-символом в конце. Следующее объявление создает символьный массив и присваивает строковую константу массиву:
char STR{9] = "A String”;
Строка "A String” сохраняется в памяти как символьный массив из 9 элементов:
А		S	t	г	i	п	9	NULL
STR
C++ предоставляет ряд операторов ввода/вывода текста для потоков: cin (Клавиатура), cout (экран), сегг (экран) и определяемых пользователем фай-•яовых потоков.
Строковые функции C++ и примеры
Таблица 2.4
char sl[20]=*'dir/bin/appl”, s2[20] = "file.asm", s3[20];
char *p; int result;
1.	Длина	int strlenfchai *s);
cout << strlen(sl) « endl;	// выходное значение равно 12
cout « stolen(s2) « endl;	// выходное значение равно 8
2.	Копирование	char* strcpyfchar *sl, *s2);
strcpy(s3, si):	fl s3 =• "dir/bin/appl"
3.	Конкатенация char *strcat(char *sl, *s2);
strcat(s3, /);
strcat(s3, s2);	// s3="dAr/bin/appl/file.asm"
4.	Сравнение int strcmp (char *sl, *s2);J
result = strcmp("baker", "Baker");	//	result	> 0
result = strcmp("12", "12");	//	result	- 0
result = strcmp("Joe", "Joseph");	//	result	< 0
5. индекс	char *strchr(char *s, int	c);		
p = strchar(s2, '.');	// p указывает на '/' после bin
if (p)
strcpy(p, ”.cpp");	// s2 « "file-cpp”
6. Бравый индекс char *strrchr (char *s, int c);
p ~ strrchrfsl, *!'): if (p) *p = 0;	// p указывает на 'l' после bin // закончить строку после bin; s2 ° "dir/bin"
7. Считать	StreamVariable	» s
8. Записать	Streamvariable	« s
cin > si;	// если входная строка - "hello world", то si
cout < si;	// указывает на строку
	// "hello world"
	// выход — "hello world"
Строковая библиотека C++ <string.h> содержит гибкий набор функций строкового управления, который непосредственно реализует большинство операций ADT. В таблипе 2-4 перечислены ключевые строковые функции C++.
Приложение: перестановка имен
Строковая прикладная программа иллюстрирует использование строковых библиотечных функций C++. В этом приложении функции strchr(), strcpyO и strcat() объединяются для копирования имени такого, как "John Doe’’ в ”Doe”, "John”, в строку Newname. Следующие операторы реализуют этот алгоритм:
char Name[10J = "John Doe”, Newname[30]; char *p;
Оператор I:	p = strchrfName, '
Возвратить указатель p на первый пробел в переменной Name.
Первая буква фамилии находится по адресу р+1.
J	0	h	n		D	о	e	NULL
Name
Оператор 2-
*р = О; // заменить провел на нулевой символ
J	0	h	n	NULL	D	o	e	NULL
Оператор 3-.
Name
strcat(Newname, ", ” );	// добавить и пробел к Newname
Оператор 4:
NULL
Newname
Оператор 5:
strcat(Newname, Name); // добавить к Newname имя
Newname
Программа 2.1. Перестановка имени
Данная программа использует операторы 1 — 5 для перестановки имени. Эти шаги содержатся в функции ReverseName. Цикл главной программы тестирует алгоритм на трех строках ввода. Выходом в каждом случае является переставленное имя.
II РгО2С1.срр
♦include <iostream.h>
♦include <string.h>
H перестановка имени и фамилии и разделение их запятой
И результат копируется в Newname
v°id ReverseName(char *name, char *newName)
char *p;
II поиск первого пробела в name и замена пробела !/ NULL-символом р = strchr(name.' ');
*р = О;
// копировать фамилиюеNewname, добавить ", "и
// присоединить к Newname имя
strcpy(newName,р+1);
strcat(newName,",	;
strcat{newName,name J;
*p = * ' ;
)
void main (void) {
char name [ 32 ], newName [32];
int i;
// считать и обработать три имени
for (i » О; i < 3; i++) (
cin.getline(name,32,'\n');
ReverseName(name,newName):
cout « "Переставленное имя: " « newName « endl « endl; )
>
/*
<SanycK программы pr02_01. cpp>
Abraham Lincoln
Переставленное имя: Lincoln, Abraham
Debbie Rogers
Переставленное имя: Rogers, Debbie
Jim Brady
Переставленное имя: Brady, Jim
*/
2.8.	Записи
Запись (record) — это структура, которая связывает элементы различных типов в одни объект. Элементы в записи называются полями (fields). Подобно массиву, запись имеет оператор доступа, который делает возможным прямой доступ к каждому полю. Например, Student — это структура записи, содержащая информацию о студенте, посещающем колледж. Эта информация включает имя (Name), адрес (Local Address), возраст (Age), профилирующую дисциплину (academic major) и среднюю успеваемость (grade-point average, GPA).
Name	Local Address	Age	Major	GPA
Строка Строка	Целое Тип перечисления Действительное
Поля Name и Local Address содержат строковые данные. Age и GPA являются численными типами, a Major — это тип перечисления. Полагая, что «роМ __ студент, мы получаем доступ к отдельным полям, объединяя записи
имени и поля с использованием оператора доступа
Tom-Name Tom. Age Tom. GPA Tom. Major
Запись позволяет объединять данные различных типов (неоднородные типы — heterogeneous types) в структуре. В отличие от массива, запись описывает единственное значение, а не список значений.
XDT Record
данные
Элемент, содержащий набор полей неоднородного типа. Каждое поле имеет имя, обеспечивающее прямой доступ к данным в поле.
Операции
Оператор доступа
Вход:	Имя записи (recname) и поле
Предусловия: Нет
процесс:	Доступ к данным'в поле
Выход:	При нахождении данных возвращать значение поля клиенту.
Постусловия: При сохранении данных запись изменяется конец ADT Record
Структуры C++
С++ имеет встроенный тип struct, представляющий запись. Эта структура заимствована из языка С и сохраняется для совместимости. C++ определяет тип struct как особый случай класса, в котором все члены являются открытыми. Мы используем тип struct в этом тексте только, когда имеем дело со структурой записи.
Пример 2.9
struct Student
{
int id;
char name(30];
Student S = {555, "Davis, Samuel"}; cout « S.id «*' ''< S.name < endl;
2.9.	Файлы
Большинство тем в этой книге концентрируют внимание на разработке и Реализации внутренних структур данных (internal data structures), которые вращаются к информации, постоянно находящейся в памяти. Для прило-ейий, однако, мы часто предполагаем, что данные доступны на устройстве «нещаей памяти, таком как диск. Это устройство (физический файл) сохра-
Память компьютера
Входной поток
Запоминающее устройство большой емкости
Рис. 2.5. Поток данных ввода
няет информацию в символьном потоке, и операционная система предоставляет ряд операций для передачи данных в память и из нее. Это позволяет нам выполнять ввод и вывод данных, которые могут постоянно храниться на внешнем устройстве. Сохраняемые данные вместе с операциями передачи определяют структуру данных, называемую (логическим) файлом (file), которая имеет важное преимущество сохранения большего количества информации, чем обычно находится постоянно в памяти.
Языки программирования предоставляют высокоуровневые операции управления файлами для того, чтобы избавить программиста от необходимости использовать низкоуровневые вызовы операционной системы. Файловые операции используют поток (stream) данных, логически соединеный с файлом. Stream ассоциирует поток данных с файлом. Для ввода поток позволяет данным последовательно перемещаться от внешнего устройства к памяти (рис.2.5). Та же программа может выводить информацию в файл, используя поток вывода (рис.2.6).
Полезно определить ADT для файла. Данные состоят из последовательности символов, которые представляют текстовые данные или байты в виде двоичных данных. Для текста данные сохраняются как последовательность символов в коде ASCII, разделяемых newline-символами. Операции ADT задаются в большинстве случаев с концентрацией внимания на простых операциях ввода/вывода. Операция ввода Read извлекает последовательность символов из потока. Родственная операция вывода Write вставляет последовательность символов в поток. Специальные операции Get и Put управляют вводом/выводом одного символа.
Поток управляет файловым указателем (file pointer), который определяет текущую позицию в потоке. Операция Input продвигает файловый указатель к следующему несчитанному элементу данных в потоке. Операция Output устанавливает файловый указатель в следующую позицию вывода. Операция установки Seek позволяет устанавливать файловый указатель в нужную по-
За поминающее устройство большой емкости
Рис. 2.6. Поток данных вывода
зицию в файле. Эта операция предполагает, что мы имеем доступ ко всем символам в файле и можем перемещаться в переднюю, заднюю и промежуточную позицию. Чаще всего операция установки используется для дисковых файлов.
Файл обычно присоединяется к потоку в одном из трех режимов: read-only, write-only и read-write. Режимы read-only и write-only указывают на то, что поток используется для ввода или вывода, соответственно. Режим read-write обеспечивает поток данных в обоих направлениях.
ДОТ File
Данные
Определение внешнего файла и направления потока данных. Последовательность данных, которые считываются из файла или записываются в файл.
Операции
Орел
Вход: Предусловия: Процесс: Выход: Постусловия: Close Вход: Предусловия: Процесс: Выход: Постусловия: Чтение Вход: Предусловия: Процесс: Выход: Постусловия: Запись Вход: Предусловия: Процесс: Выход; Постусловия: Установка Вход: Предусловия: Процесс: Выход: Постусловия: ADT File	Имя файла и направление потока. Для ввода должен существовать внешний файл. Связывание потока с файлом. Флажок, указывающий на успешность операции. Данные могут последовательно перемещаться между внешним файлом и системной памятью посредством потока. Нет Нет Отделение потока от файла. Нет Данные больше не могут перемещаться посредством потока между внешним файлом и системной памятью. Массив размером N для хранения блоков данных. Поток должен быть открыт в направлении только для чтения или чтения-записи. ввод N символов из потока в массив. Остановка в конце файла. Возвращать количество символов, которые считываются. Файловый указатель перемещается вперед на N символов. Массив; count N Поток должен быть открыт с направлением только для записи или для чтения-записи. Вывод N символов из массива в поток. Возвращать количество символов, которые записываются. Поток содержит данные вывода, и файловый указатель перемещается вперед на N символов. Параметры для переустановки файлового указателя. Нет Переустановка файлового указателя. Возвращать флажок, указывающий на успешность установки. Устанавливается новый файловый указатель.
Иерархия потоков C++
С++ обеспечивает файловое управление с потоковой системой ввода/вы-вода, которая реализуется путем использования иерархии классов, как частично показано на рис. 2.7. Поток C++ является объектом, соответствующим классу в этой иерархии. Каждый поток определяет файл и направление потока данных.
Корневым классом в иерархии является ios, содержащий данные и операции для всех производных классов. Этот класс содержит флажки, которые определяют специфические атрибуты потока и методы форматирования, которые действительны для ввода и вывода. Например: cout.setf(ios:: fixed);
устанавливает режим отображения для вещественных чисел на фиксированный формат, а не на экспоненциальный.
Классы istream и ostream предоставляют базовые операции ввода и вывода и используются как базовые классы для остальной части потоковой иерархии ввода/вывода.
Класс istream_withassign — это вариант istream, который позволяет выполнять объектное присваивание. Предопределенный объект cm является объектом этого класса. Предопределенные объекты cout и сегг — это объекты типа класса ostream_withassign. Во время выполнения эти три потока открыты для ввода с клавиатуры и вывода на экран. Объявления этих классов включены в файл <iostream.h>.
Класс ifstream является производным класса istream и используется для дискового файлового ввода; аналогично ofstream используется для дискового файлового вывода. Эти классы объявляются в файле <fstream.h>. Оба класса содержат операцию Open для присоединения файла к потоку и операцию Close для отделения потока от файла.
Двумя типами дисковых файлов являются текстовые файлы (text files) и бинарные файлы (binary files). Текстовый файл содержит символы ASCII и является печатаемым, тогда как бинарный файл содержит чистые бинарные данные. Например, редактор использует текстовые файлы, а программа — электронная таблица создает и использует бинарные файлы. Пример текс-
Рис.2.7. Иерархий потоковых классов
тового файлового ввода/вывода дается в программе 2.2, а бинарные файлы разрабатываются как класс в главе 14. Класс бинарных файлов используется для реализации алгоритмов внешнего поиска и сортировки.
Класс fstream позволяет создавать и сопровождать файлы, которые требуют доступа и для чтения, и для записи. Класс fstream описывается вместе с приложениями в главе 14.
Ввод/вывод на базе массива реализуется классами istrstream и ostrstream и объявляется в файле <strstream.h>. Здесь данные считываются из массива или записываются в массив вместо внешнего устройства. Текстовые редакторы часто используют ввод/вывод на базе массива для выполнения сложных операций форматирования.
Программа 2.2. Файловый ввод/вывод
Данная программа представляет потоки C++, включающие текстовый файл и ввод/вывод на базе массива.
Программа использует cout и сегг, которые включены в <iostream.h>. Ввод текстового файла и вывод на базе массива используют файлы <fstream.h> и <strstream.h>, соответственно. Программа открывает файл и считывает каждую строку, содержащую пары переменная/значение в формате Name Value. С использованием потоковой операции на базе массива эта пара записывается в массив outputstr в формате
name  Value
и затем выводится на экран оператором cout. Например, строки ввода start 55
stop 8.5
выводятся иа экран как строки
start = 55 stop и 8.5
// рг02_02.срр finclude <iostream.h> #include <fstream. h> #include <strstream.h> tinclude <stdlib.h> tinclude <string.h>
void main(void) (
// ввести текстовый файл, содержащий имена и значения ifstream fin;
char name[30J, outputstr[256];
//декларировать выходной поток, основанный на массиве и // использующий outputstr ostrstream outs(outputstr. sizeof(outputstr)):
double value;
// открыть для ввода файл 'names.dat',
11 убедиться в его существовании
fin.open("names.dat", ios::in I ios::nocreate); if (’fin) t
cerr « "Невозможно открыть файл 'names.dat' ” « endl; exit(1);
}
// читать имена и значения,
// записывать в поток outs как 'имя = значение * while(fin » name)
<
fin » value;
outs « name « ” = ” « value « '
}
// KULL-символ для выходной строки
outs « ends;
cout « outputstr « endl;
)
/♦
< "names, da t‘*>
start	55
breakloop 225.39
stop	23
<3апуск программы pr02_02. cpp>
start=55 breakloop=225.39 stop=23
*/
2.10.	Приложения массива и записи
Массивы и записи являются встроенными структурами данных в боль* шинстве языков программирования. Данная глава знакомит с ADT для этих структур и описывает их реализацию C++. Мы используем эти структуры для разработки важных алгоритмов во всей книге. Массив является основной структурой данных для списков. Во многих приложениях мы используем утилиты search и sort для нахождения элемента в списке на базе массива и для упорядочения данных. Этот раздел знакомит с последовательным поиском и обменной сортировкой, которые легко кодировать и понимать.
Последовательный поиск
Последовательный поиск предназначен для поиска элемента в списке с использованием целевого значения, называемого ключом (key). Этот алгоритм начинает с индекса, предоставляемого пользователем, называемого start, и проходит через остальные элементы в списке, сравнивая каждый элемент с ключом. Сканирование продолжается, пока не будет найден ключ или список не будет исчерпан. Если ключ найден, функция возвращает индекс соответствующего элемента в списке; в противном случае возвращается значение -1. Для функции SeqSearch требуются четыре параметра: адрес списка, начальный индекс для поиска, количество элементов и ключ. Например, рассмотрим следующий список целых, содержащихся в массиве А:
А:	8 3 6 2 6
Key = 6, Start = 0, n = 5. Искать с начала списка, возвращая индекс первого появления элемента 6.
|о 1	|2	3	4
А=список Возвращаемое значение
2. Key = 6, Start = 3, n = 2. Начинать с А[3] и искать в списке, возвращая указатель на первое появление элемента 6.
Кеуш6
N-2
2 t4
список Возвращаемое значение
3- Key = 9, Start = 0, п = 5. Начинать с первого элемента и искать в списке число 9. Когда оно не найдено, возвращать значение -1.
Кеу=9
N=5
t°
А=список Возвращаемое значение == -1
Алгоритм последовательного поиска применяется к любому массиву, для которого оператор ”=—" определяется для типа* элемента. Общий алгоритм поиска требует шаблонов и перегрузки операторов. Эти темы обсуждаются в главах 6 и 7. Следующая функция реализует последовательный поиск для массива целых:
Функция последовательного поиска
int SeqSearch(int	int start, int n, int key)
(
for (int i^start; i < n; i++) if (list [i] == key) return i;
I return -1;
Программа 2.3. Повторяемый поиск
Данная программа тестирует функцию последовательного поиска, подсчитывая количество появлений ключа в списке. Главная программа сначала вводит 10 целых чисел в массив А и затем запрашивает ключ.
Программа выполняет повторяемые вызовы SeqSearch, используя различный начальный индекс. В исходном положении мы начинаем с индекса
О, начала массива. После каждого вызова SeqSearch счетчик количества появлений увеличивается, если ключ находится; в противном случае поиск прекращается, и счетчик является возвращаемым значением. Если ключ найден, возвращаемое значение определяет его позицию в списке. Следующий вызов SeqSearch выполняется со значения start, равного положению элемента, находящегося непосредственно справа от последнего найденного.
// рг02_03.срр
#include <iostream.h>
// поиск в массиве из п целых значений элемента по ключу;
// возвратить указатель на этот элемент или NULL, если элемент не найден int SeqSearch(int list(J, int start, int n, int key) {
for(int i=start;i < n; i++)
if (list(i] =“ key)
return i;	// возвратить индекс соответствующего элемента
return -1;	// неудачный поиск, возвратить -1
}
void maxn(void) (
int А[10];
int key, count - 0, pos;
// запрос на ввод списка 10-ти целых чисел
cout « "Введите список из 10 целых чисел: for (pos=0; pos < 10; pos++)
cin » A(pos];
cout « "Введите ключ: cin » key;
// начать поиск с первого элемента массива pos = 0;
// продвигаться по списку, пока ключ находится while ((pos = SeqSearch(A,posr10,key)) != -1) ( count++;
// продвинуться к следующему целому после найденного роз++;
1
cout « key « ” появляется " « count « " раз(а) в этом списке.” « endl;
}
/*
Запуск программы рг02_03.срр
Введите список из 10 целых чисел: 5298158753 Введите ключ:5
5 появляется 3 ра-'а) в этом списке.
*/
Обменная сортировка
Упорядочение элементов в списке является важным для многих приложений. Например, некоторый список может сортировать записи по их инвентарным номерам для обеспечения быстрого доступа к элементу, словарь сохраняет слова в алфавитном порядке и регистрационные порядковые записи студентов — по их номерам социального страхования.
Для создания упорядоченного списка мы вводим алгоритм сортировки, называемый ExchangeSort, который упорядочивает элементы в возрастающем порядке- Этот алгоритм иллюстрируется списком 8, 3, 6, 2 и создает упорядоченный список 2, 3, 6, 8.
Индекс 0-. Рассмотрим полный список 8, 3, 6, 2. Элемент с индексом О сравнивается с каждым последующим элементом в списке с индексами 1, 2 и 3. Для каждого сравнения, если последующий элемент меньше, чем элемент с индексом О, эти два элемента меняются местами. После выполнения всех сравнений наименьший элемент помещается в позицию с индексом 0.
Действие Полученный список
Обмен	3	8	6	2
Нет обмена 	►	3	8	6	2
м	8	6	3
Индекс 1: При уже помещенном в позицию с индексом О самом маленьком элементе рассмотрим подсписок 8, 6, 3. Принимаются во внимание только элементы от индекса 1 до конца списка. Элемент с индексом 1 сравнивается с последующими элементами с индексами 2 и 3. Для каждого сравнения, если больший элемент находится в позиции с индексом 1, то два элемента меняются местами. После выполнения сравнений второй наименьший элемент в списке сохраняется в позиции с индексом 1.
Полученный список
2	6	8	3
Действие
Обмен
Обмен	2	3	8	6
Индекс 2: Рассмотрим подсписок 8, 6. Этот процесс продолжается для подсписка из двух элементов с индексами 2 и 3. Между элементами выполняется ростов сравнение, в результате которого происходит обмен.
Исходный список
Индекс 2
Действие
Обмен
Полученный список
2	3	6	8
У нас остался только один элемент с индексом 3, и список отсортирован.
Отсортированный список
2	3	6	8
В C++ функция ExchangeSort использует вложенные циклы. Предположим, что размер списка задается значением п. Внешний цикл приращивает индекс i в диапазоне от 0 до п-2. Для каждого индекса 1 сравним последующие элементы при j=i+l, i+2, .... п-1. Выполним сравнение и поменяем местами элементы, если list[i] > list[j].
Программа 2.4. Сортировка списка
Эта программа иллюстрирует алгоритм сортировки. Список из 15 целых в диапазоне от О до 99 заполняет list. ExchangeSort упорядочивает список, используя функцию Swap для того, чтобы поменять местами два элемента массива. Программа выдает на экран список до и после сортировки.
// рг02_04.срр
#include <iostreatn.h>
// поменять значения двух переменных целого типа х и у
void Swap(int & х, int & у)
(
int temp = х,-	// сохранить первоначальное значение х
х = у;	// заменить х на у
у = temp; // присвоить переменной у // первоначальное значение х
}
// сортировать целый массив п-элементов а в возрастающем, порядке void ExchangeSort(int all, int n)
(
int i, 3;
// реализовать n — 1 проходов.найти правильные значения
// в at],...,a[n-2).
fo.r(i = 0,- i < n-1; i++)
// поместить минимум из a[n+l]...a[n-l] в a(i]
for(j = i+1; j < n; j++)
// заменить if a[ij > a[j]
if (a[ij > a[j])
Swap(a[iJ, a[j]);
}
// пройти по списку, печатая каждое значение
void PrintList(int a(], int n)
(
for (int x = 0,- i n; i++)
cout « a[i] < ’’
cout « endl;
}
void main(void)
{
int list[15] = {38,58,13,15.51,27,10,19,
12,86,49,67,84,60,25};
int i;
cout « "Исходный список \п",-
PrintList(list,15)t
Exchangesort(list,15);
cout « endl <<’Отсортированный список" « endl;
PrintList(list,15);
)
/*
<3апуск программы pr02_04 .cpp>
Исходный список
ЗВ 58 13 15 51 27 10 19 12 86 49 67 84 60 25
отсортированный список
10 12 13 15 19 25 27 38 49 51 58 60 67 64 86
*/
Подсчет зарезервированных слов C++
В разделе 2.8 обсуждается тип записи, который реализуется в C++ как struct. В качестве иллюстрации записей программа подсчитывает количество раз, когда в файле появляются зарезервированные слова "else", "for", "if", ’’include" и "while". Эта программа использует строковые переменные также, как массив записей.
Основной структурой данных программы является struct KeyWord, чьи поля состоят из строковой переменной keyword и count типа integer:
struct Keyword (
char keyword[20];
int count;
};
В массиве KeyWordTable создается таблица для пяти зарезервированных слов. Каждый элемент в этой таблице инициализируется указанием зарезервированного слова и начального значения счетчика count—О. Например, первый инициализатор массива {"else", 0} приводит к тому, что элемент Кеу-WordTablefO] содержит строку "else” со значением счетчика 0:
Keyword KeyWordTable[ ] = I
("else", 0], ("for", 0), ("if’', 0), {"include", 0}, {"while", 0} );
Программа читает отдельные слова в файле с помощью функции GetWord. -Л°вом является любая последовательность символов, которая начинается с ^Уквы и продолжается произвольным количеством букв или цифр. Например, огДа представлена строка
Expression: 3+5=8 (Nl + N2 = »3)
GetWord извлекает слова "Expression", "Nl", "N2”, и "N3" и отбрасывает Ше символы.
чеп ^НКЦ'ИЯ SeqSearch сканирует таблицу, выполняя поиск соответствия клю-cooi^ сл°ву. Когда поиск завершается успешно, функция возвращает индекс «етствующей записи, увеличивая на единицу поле count.
Программа 2.5. Подсчет зарезервированных слов
Эта программа читает собственный исходный код в качестве ввода. В цикл читается каждое слово и вызывается функция SeqSearch для определения того, соответствует ли ввод зарезервированному слову в KeyWordTable. Если так, поле count в записи увеличивается на единицу. После завершения ввода, количество появлений каждого ключевого слова выводится на экран.
Программа имеет интересный оператор, который динамически вычисляет количество элементов в массиве KeyWordTable с помощью выражения
sizeof (KeyWordTable)/ sizeof (Keyword)
Это выражение предоставляет независимый от системы метод вычисления количества элементов в каком-либо массиве. Если другие ключевые слова добавляются к этой таблице, последующая компиляция генерирует новый подсчет элементов.
// рг02_05.срр
#include <Tostream.h> ([include <fstream.h> #include <string.h> ^include <ctype.h> ([include <stdlib.h>
// объявление структуры слова struct Keyword {
char keyword[20];
int count;
};
// объявление и инициализация таблицы слов Keyword KeyWordTable[]= {
{'•else", 0}, {"for", 0}, {"if", 0}, {"include”, 0}, {'•while", 0} );
ll настраиваемый алгоритм поиска слов int SeqSearch(Keyword *tab, int n, char *word) {
int i;
// сканировать список, сравнивать word c keyword в текущей записи for (i=0; i < n; i++, tab++)
if (strcmp(word, tab-keyword) == 0)
return i;	// при совпадении вернуть индекс
return -1;	// к сожалению, нет совпадения
}
// извлечь слово, начинающееся с буквы и, возможно, // другие бук вы/цифры	-
int GetWordtifstreams fin, char w[J) (
char c;
int i = 0;
// пропустить не алфавитный ввод while ( fin.get(с) &£ lisalpha(c) ) ;
H вернуть 0 (Неудача) в конце файла
if (fin.eof()) return 0;
// записать первый символ word
w[i++] - с;
// собирать буквы, цифры и символ окончания строки
while ( fin.get(c) fiS ( isalpha(c) II isdigit(c) ) ) w[i++] = c;
w[il =“ ' \0' •
return 1;	/7 вернуть 1 (Успех)
void main (void)
const int MAXWORD = 50;	7/ максимальный размер любого слова
// объявить и инициализировать размер таблицы
const int NKEYWORDS = sizeof(KeyWordTable)/sizeof(Keyword);
int n;
char word[MAXWORD), c;
ifstream fin;
// открыть файл с проверкой ошибки
fin.open("pr02__05.cpp”, ios::in I ios:znocreate);
if (!fin)
{
cerr « "Невозможно открыть файл * prO2 C5. cpp' " « endl;
exit (1);
}
// извлекать слова до конца файла
while (GetWord(fin,word))
// при совпадении с таблицей keyword увеличивать счетчик
if ((n=SeqSearch(KeyWordTable,NKEYWORDS,word)) !=-l) KeyWordTable[n].count++;
// сканировать таблицу keyword и печатать поля записи
for (п = 0; п < NKEYWORDS; п++)
if (KeyWordTable[nJ.count > 0)
{
cout « KeyWordTable[n].count;
cout « ” "« KeyWordTable[n].keyword « endl;
)
, fin.close()}
*/
Запуск программы pr02_05 - cpp
1 else
3 for
6 if
6 include
4 while
*/
Письменные упражнения
2.1	Вычислите десятичное значение каждого двоичного числа:
(а)	101	(б)	1110 (в)	110111	(г)	1111111
2.2	Напишите каждое десятичное число в двоичном представлении:
(а)	23	(б)	55	(в)	85	(г)	253
2.3	В современных компьютерных системах адреса обычно реализуются : аппаратном уровне как 16-битовые или 32-битовые двоичные значени Естественно работать с адресами в двоичном представлении, а не пр образовывать их в десятичную систему. Поскольку числа такой длив затруднительно записывать как строку двоичных цифр, в качестве о нования используется 16 или шестнадцатиричные (hexadecimal) числ Такие числа, упоминаемые как hex numbers, являются важным пре ставлением целых чисел и позволяют легко выполнять преобразован! в двоичную систему и наоборот. Большинство системных програм имеет дело с машинными адресами в шестнадцатиричной системе. Шестнадцатиричные числа строятся на базе числа 16 с цифрами диапазоне 0-15 (десятичном). Первые 10 цифр являются производным от десятичных чисел: 0, 1, 2, 3, . . ., 9. Цифры от 10-15 представлен буквами А, В, С, D, Е и F. Степени 16: 16° = 1, 161 = 16, 162 = 25< 163 = 4096 и так далее. В форме позиционной нотации примерам шестнадцатиричных чисел являются 17Е, 48 и FFFF8000. Числа пр образуются в десятичную форму расширением степеней 16 точно та же, как степени 2-х расширяются для двоичных чисел.
Например, шестнадцатиричное число 2A3F16 преобразуется в десятичнс расширением степеней 16-и.
2A3F16 = 2(163) + А(162 ) +3 (161) +F(16C)
= 2(4096) +10 (256) +3 (16) +15 (1)
= 8192 + 2560 +48 +48 + 15 - 101851О
Преобразуйте каждое шестнадцатиричное число в десятичное
(а) 1А (б) 41F (в) 10ЕС (г) FF (д) 10000
Преобразуйте каждое десятичное число в шестнадцатиричное
(е) 23 (ж) 87 (з) 115 (и) 255
2.4	Основной причиной введения шестнадцатиричных чисел является и. естественное соответствие двоичным числам. Они обеспечивают ком пактное представление двоичных данных и адресов памяти. Шестнал цатиричные цифры имеют 4-битовое двоичное представление в диапа зоне 0-15. Следующая таблица показывает соответствие между двоич ными и шестнадцатиричными цифрами:
[шестнадцатиричные	Двоичные	Шестнадцатиричные	Двоичные
0	осоо	8	1000
1	0001	9	1001
2	0010	А	1010
3	0011	В	1011
4	0100	С	1100
5	0101	D	1101
	оно	Е	1110
к			0111	F	1111
Для представления двоичного числа в шестнадцатиричном формате начинайте с правого конца числа и разделяйте биты на группы из четырех битов, добавляя начальный О слева в последней группе, если необходимо. Запишите каждую группу из 4-х битов как шестнадцатиричное число. Например: 111100011Ю11102 = 0111 1000 1110 1110 - 78ЕЕ15
Для преобразования шестнадцатиричного числа в двоичное выполните обратное действие и запишите каждое шестнадцатиричное число как 4 бита. Рассмотрим следующий пример:
А7891€ = 1010 0111 1000 1001 = 10100111100010012
Преобразуйте двоичные числа в шестнадцатиричные:
(а) 1100	(б) 1010	ОНО	(в) 1111	0010
(г) 1011 1101 1110	ООН
Преобразуйте шестнадцатиричные числа в двоичные:
(д) 061016 (е)	AF2016
2.5 C++ позволяет программисту вводить и выводить числа в шестнадцатиричном представлении. При помещении манипулятора "hex" в поток режим ввода или вывода чисел становится шестнадцатиричным. Этот режим действует до тех пор, пока он не поменяется на десятичный с помощью манипулятора "dec". Например:
cin » hex » с » dec » u; // t читается как шестнадцатиричное;
и — как десятичное
<ввод 100 256> t = 10016 и и = 2561С
cout « hex « 100 « t « u; // вывод 64 100 100
cout « dec « 100 « t « u; // вывод 100 256 256
Рассмотрим следующее объявление и выполняемые операторы:
int i, j, k;
cin » i ;
cin » hex » j » dec;
cin » k;
(а)	Предположим, ввод является 50 50 32. Каков вывод для оператора? cout « hex « i « " " « j « ” " « dec « k « endl;
(б)	Предположим, ввод является 32 32 64. Каков вывод для этого оператора?
cout « dec « i « ” " « hex « j « * ” « k « endl;
2«6 Напишите полную спецификацию для оператора % в целом ADT. Выполните то же для оператора сравнения 1=.
2«7 Булев тип определяет данные, которые имеют значения True или False. Некоторые языки программирования определяют базовый булев тип с рядом встроенных функций для обработки этих данных. C++ ассоциирует булево значение с каждым числовым выражением.
•а) Определите булев ADT, описывающий область данных, и его операции.
(б) Опишите реализацию этого ADT, используя языковые конструкции C++.
2.8
(а)	Какой символ ASCII соответствует десятичному числу 78?
(б)	Какой символ ASCII соответствует двоичному числу 1001011г?
(в)	Каковы коды ASCII для символов "q" и возврата каретки? Дайте ответы в десятичном и шестнадцатиричном представлении.
2.9	Что печатается следующим фрагментом кода?
cout « char (86} « " ” « int( ' g' ) « " " «
chart int ("0”) + 8) « endl;
2.10	Объясните, почему оператор % (остаток) не дается в ADT для вещественных чисел.
2.11	Преобразуйте каждое двоичное число с фиксированной точкой в десятичное:
(а) 110.110
(б) 1010.0101
(в) 1110.00001
(г)	11.111 . . . 111 . . . (Совет: Используйте формулу для суммы геометрического ряда).
2.1	2 Преобразуйте каждое десятичное число с фиксированной точкой в двоичное:
(а)	2.25
(б)	1.125
(в)	1.0875
2.13
(а)	Существует ли наименьшее положительное действительное число в АВТ для вещественных чисел? Почему да или почему нет?
(б)	Когда в компьютере следует использовать вещественное числа, существует ли наименьшее положительное вещественное число? Почему да или почему нет?
2.1	4 Формат IEEE с плавающей точкой сохраняет знак числа отдельно, а порядок и мантиссу — как беззнаковые числа. Нормализованная форма позволяет получить уникальное представление для каждого числа с плавающей точкой.
Нормализованная форма'. Число с плавающей точкой задается так, что имеет одну не равную нулю цифру слева от двоичной точки
N = ± l.d,d2 . . . (Ц.} 2е
Число с плавающей точкой 0.0 сохраняется со знаком, порядком и мантиссой 0.
В качестве примера: два двоичных числа преобразуются в представление в нормализованной форме.
Двоичное число	Нормализованная форма
1101.101 к 21	1.1011010 х 24
0.0011 х 2е	1.1 х 23
Тридцати-двух-битовые числа с плавающей точкой сохраняются в нормализованной форме с использованием внутреннего формата IEEE.
Знак Самый левый бит используется для знака. ”+” имеет знаковый разряд О, и ” имеет знаковый разряд 1.
Порядок Порядок задается 8-ю битами. Для обеспечения сохранения всех порядков как положительных (беззнаковых) чисел, формат IEEE задает использование нотации "excess -127" для порядка. Сохраняемый порядок (Ехрв) создается добавлением 127 к реальному порядку.
Exps  Exp + 127
Истинный порядок	Сохраняемый порядок
Диапазон	Диапазон
-127 < Exp < 12В	OS Exps < 255
Мантисса Допустим, что число сохраняется в нормализованной форме, начальная цифра 1 скрыта, дробные цифры сохраняются в 23-битовой мантиссе, задается точность 24 бита.
Знак	Порядок	Мантисса
1 бит 8 битов 23бита
В качестве примера вычислим внутреннее представление -0.1875. Нормализованная форма	(-) 1.100 *	2"э
Знак	1
Порядок	Ехр3 “ ”3 +	127	= 124 = 011111002
Мантисса	<1>1000000	...	О
-0.1875 = 10111110010000000000000000000000
Запишите каждое число в 32-битовой форме IEEE с плавающей точкой:
(а)	7.5
(б)	-1/4
Каково значение следующих 32-битовых чисел в формате IEEE в десятичной форме? Каждое число дается в шестнадцатиричной форме.
(в)	С18СОООО
(г) 41Е90000
2.15
(а)	Перечислите в календарном порядке месяцы года, которые имеют символ ”р” в имени. Это перечислимый тип.
(б)	Напишите реализацию C++ для перечислимого типа.
(в)	Какой месяц соответствует целому числу 4 в реализации C++? Какова позиция октября?
(г)	Напишите это перечисление в алфавитном порядке. Имеют ли какие-либо месяцы одну и ту же позицию в обоих списках?
•16 Добавьте операции successor и predecessor к ADT для перечислимых типов. Используйте полные спецификации. Successor возвращает следующий элемент в списке, и predecessor возвращает предыдущий элемент. Будьте осторожны при определении того, что происходит на гра-^Щах списка.
2.17 С учетом следующих объявлений и операторов укажите содержимое X, Y и А после выполнения этих операторов:
int X “4, Y=7, *₽Х - &Х, *Р¥;
double А[ ] = {2.3, 4.5, 8.9, 1.0, 5.5, 3.5}, "РА = А;
PY = 4Y; (*РХ)—; *PY += *РХ; PY = РХ;
*РУ - 55; *РА += 3.0; РА++; *РА++ = 6.8;
РА+- 2;
*++РА = 3.3;
2.18
(а)	А объявляется как А[5];
short А [ 5);
Сколько байтов выделяется для массива А? Если адрес массива А = 6000, вычислите адрес А[3] и А[1].
(б)	Предположим такое объявление:
long А( 1 = {30, 500000, -100000, 5, 33};
Если длинное слово занимает 4 байта и адрес А равен 2050,
□ Каково содержимое с адресом 2066?
□ Удвойте содержимое с адресом 2050 и адресом 2062. Выпишите массив А.
□ Какой адрес у А[3]?
2.19	Предположим, что А — это массив размером mxn с индексами строк в диапазоне 0 — (т-1) и индексами столбцов в диапазоне О — (п-1). Генерируйте функцию доступа, вычисляющую адрес Afrow] [col], полагая, что элементы сохраняются столбцами.
2.20	А объявляется как
Short А[5] [6];
(а)	Сколько байтов выделено для массива А?
(б)	Если адрес массива А = 1000, вычислите адрес А[3] [2] и А[1] [4].
(в)	Какой элемент массива помещается по адресу 1020? По адресу 1034? 2.21
(а)	Объявите строку Name с вашим именем в качестве начального значения.
(б)	Рассмотрите объявления строковой переменной:
char si[50], s2[50];
и операторы ввода:
Cin » SI » S2;
Каково значение S1 и S2 для строки ввода "Джордж спешит!’’?
Каково значение S1 и S2 при вводе следующего текста — это пустой символ, а 11 — это конец строки.)?:
Nextl
«•❖❖❖•Word
2-22 Рассмотрим следующие строковые объявления:
char Sl(30] = "Stockton, СА", S2[30] = “March 5, 1994”, *р;
char S3 [30];
(а)	Каково значение *р после выполнения каждого следующего оператора?
р - strchr (Si, * t');
р - strrchr (Sl, 't');
p - strrchr (S2, '6');
(б)	Каково значение S3 после выполнения:
strcpy (S3,SI);
strcat (S3, ",");
strcat (S3,S2);
(в)	Каксе значение возвращается вызовом функции strcmp (S1.S2)?
. (г) Какое значение возвращается вызовом функции strcmp (&Sl[5],”ton”)? 2.23 Функция
void strinsert (char *s, char *t, int i);
вставляет строку t в строку s в позиции с индексом i. Если i больше длины s, вставка не выполняется. Реализуйте strinsert, используя библиотечные функции C++ strlen, strcpy и strcat. Вам потребуется объявить временную строковую переменную для хранения оригинальных символов в s с индекса i до индекса strlen(s) -1. Вы можете полагать, что этот хвост никогда не превышает 127 символов.
2.24	Функция:
void strdelete(char *s. int i, int n);
удаляет последовательность n символов из строки s, начиная с индекса i. Если индекс i больше, чем длина s или равен ей, то никакие символы не удаляются. Если i+n больше, чем длина s или равено ей, то удаляется конец строки, начиная с индекса L Реализуйте strdelete, используя библиотечные функции C++ strlen и strcpy.
2.25	Альтернативой использованию строк с NULL-символом является помещение счетчика символов в первый элемент символьного массива. Это называется форматом со счетчиком байтов, и такие строки часто нат зывают строками Паскаля, поскольку программные системы на языке Паскаль используют этот формат для строк.
(а)	Реализуйте функцию strcat, полагая, что строки сохраняются в формате со счетчиком байтов.
(б)	Функции PtoCStr и CtoCStr выполняют преобразование этих двух строковых форматов:
v0id FtoCStr(char *s); // конвертировать s из Pascal в С+-» vdd CtoPStr(char *s); // конвертировать s из C++ в Pascal Реализуйте эти две функции.
2.26	Добавьте оператор присваивания ”=“ к ADT записи, используя полную спецификацию. Точно определите, какое действие выполняется во время присваивания.
2.27	Комплексное число имеет форму х + iy, где i2 = -1. Комплексные числа имеют широкое применение в математике, физике и технике. Они имеют арифметику, управляемую рядом правил, включая следующие: Пусть u e а + ib, v ~ с + id u +	v - (а +	с)	+	i(b + d}
u —	v  (а —	с)	+	1 (Ь - d)
и _	ас + bd	.	(	be - ad^
v	С" + d2	1	I	cz + d2 I
Представьте комплексное число, используя следующую структуру: struct Complex { float real;
float imag;
}
и реализуйте следующие функции, которые выполняют операции с комплексными числами:
Complex cadd(Coraplexb х, Complexb	у);	// х	+	у
Complex esub(Complex^ к, Cornplex&	у);	// х	—	у
Complex cmul(Complex^ х, Complexfi	у);	// х	*	у
Complex cdiv(Complex& х, Complex&	у);	// х	/	у
2.28	Добавьте операцию FileSize к ADT для потоков. Она должна возвращать количество символов в файле. Точно укажите, для каких предусловий эта операция имеет смысл. (Совет: Как насчет cin/cout?)
2.29	Четко различайте текстовый и двоичный файл. Как вы думаете, возможно ли разработать программу, принимающую имя файла в качестве входа и определить текстовый он или бинарный?
Упражнения по программированию
2.1	Напишите функцию
void BaseOut(unsigned int n, int b)
которая выводит n с основанием b, 2 < b < 10. Напечатайте каждое число в диапазоне 2 < п < 50 с основанием 2, 4, 5, 8 и 9.
2.2	Напишите функцию
void OctIn(unsigned intS n);
которая читает число с основанием 8 (восьмеричное) и присваивает его п. Используй re Octln в главной программе, которая читает следующие восьмеричные числа и печатает десятичные эквиваленты:
7, 177, 127, 7776, 177777
2.3	Напишите программу, которая объявляет три целые переменные i, /, k. Введите значение для i в десятичной форме и значения для j и k — в шестнадцатиричной. Напечатайте все три переменные и в шестнадцатиричной, и в десятичной форме.
о 4 Изучите дискретность представления вещественных чисел на вашем компьютере путем вычисления 1 + D для D = 1/10, 1/100, 1/1000, .... 1/10п до тех пор, пока 1 + D == 1.0. Если вы имеете доступ более, чем к одной машинной архитектуре, попробуйте выполнить это на других машинах.
2.5	Рассмотрим перечислимый тип
епшп DaysofWeek {Sun, Mon, Tue, Wed, Thurs, Fri, Sat);
Напишите функцию
void GetDay(DaysOfweeki day);
которая читает имя дня с клавиатуры как строку и присваивает дню соответствующее значение элемента перечисления. Напишите также функцию
void PutDayfDaysOfWeek day)
которая записывает значение элемента перечисления на экране. Разработайте главную программу для тестирования этих двух функций.
2.6	Введите ряд слов до конца файла, преобразуя каждое в слово на ломаной латыни. Если слово начинается с согласного, переместите первый символ слова на последнюю позицию н присоедините "ау". Если слово начинается с гласного, просто присоедините "ау”. Например:
Вход: this is simple
Выход: histay isay implesay
2.7	Строка текста может быть зашифрована с использованием табуляцион-’ иого соответствия, которое ассоциирует каждую букву алфавита с уникальной буквой. Например, табуляционное соответствие abcdefghijklmnopqrstuvwxyz ==> ngzqtcobmuhelkpdawxfyivrsj устанавливает соответствие между ’’encrypt’' и "tkzwsdf”.
Напишите программу, которая читает текст до конца файла и выводит зашифрованную форму.
2.8	Создайте свою программу табуляциониого соответствия, которая выполняет установку соответствия, обратную той, которая использовалась в упражнении 2.7. Введите зашифрованный файл и выведите на экран его расшифрованную форму.
2.9	Напишите программу, которая вызывает выход за границы одного или более индексов массива. Доведите программу до такого состояния, чтобы она "разрушалась". Притворитесь, что вы не знаете, в чем заключается проблема. Используйте любой имеющийся в вашем распоряжении отладчик и диагностируйте причину такого поведения.
•Ю Измените сортировку обмена так, чтобы она сортировала список в порядке убывания. Протестируйте новый алгоритм, написав главную про-грамму, подобную программе 2.4.
•11 Рассмотрим объявление записи struct Month
{
char name(10];	// имя месяца
let monthnum,- // число дней в месяце );
43а*425
(a)	Напишите функцию
void SortByNanie (Month months [ ], int n);
которая сортирует массив с элементами типа Month, сравнивая имен (используйте функцию strcmp в C++). Напишите также функцию void SotгByDays (Month months[ ], int n) ;
которая сортирует список, сравнивая количество дней в месяце. Нани шите главную программу, которая объявляет массив, содержащий вс-месяцы года и сортирует его, используя обе функции. Выведите н. экран каждый отсортированный список.
(б)	Заметьте, что сортировка списка месяцев по количеству дней создав-связи соперничества. Когда это происходит, метод сортировки може-использовать вторичный ключ (secondary key) для устранения эти: связей. Напишите функцию
void Sort2ByDays(Month months[ ], int n);
которая сортирует список, сравнивая сначала количество дней, и, еслг связь возникает, разбивает ее, сравнивая имена. Используйте эту функ цию в главной программе для распечатки упорядоченного списка все: месяцев года, упорядоченных по количеству дней в месяце.
2.12	Напишите программу, которая читает текстовый файл и выводит hi экран счетчик количества появлений знаков пунктуации(. , I ?). х
2.13	Используя cin.getline, читайте строку, начинающуюся с имени функции из одного символа, за которым следуют последовательности "х” с сим волами ”+“ и вставленными в промежутки. Строка не может за канчиваться символом "+” или Образуйте строку в форме: SingleCharFuncName(х) = х**п + х**щ+. . .
в массиве, используя выход на базе массива. Если порядком являете* равным 1, то опустите "**1”. Запишите каждую строку в файт ’’funcs.val”. Например, строки
F х х х + хх — х
Gxx — ххх + хххх
создают файл "funcs-val”, имеющий строки
F(x) = х**3 + х**2 - х
G(x) = х**2 — х**3 +х**4
2.14	Напишите программу, которая вводит N х N матрицу А целых значений и выводит на экран след матрицы. След матрицы определяется как сумма диагональных элементов
Trace (А) = А[0, 0] +А[1, 1] + . . . + A[N - 1, N - 1]
2.15	Это упражнение использует результаты упражнения 2.27 из предыдущего раздела "Упражнения”. Напишите функцию f(z), вычисляющую комплексную полиномиальную функцию: z3 — 3z‘ + 4z — 2
Определите полиномиал для следующих значений z: z = 2 +31, -1 + i, l+i, l—i, 1 + 0i
Заметьте, что последние три значения являются корнями от f.
Абстрактные типы данных и классы
V
3.1.	Пользовательский тип - КЛАСС
3.2.	Примеры классов
3.3.	Объекты и передача информации
3.4.	Массивы объектов
3.5.	Множественные конструкторы
3.6.	Практическое применение: треугольные матрицы
Письменные упражнения
Упражнения по программированию
В главе 1 были даны абстрактные типы данных (ADT) и их представле в качестве классов C++. Это введение описывает структуру класса, кото обеспечивает инкапсуляцию данных и скрытие информации. В этой главе держится более полное описание базовых концепций класса. Мы рассматря ем разработку и использование конструкторов класса, реализацию мето класса и использование классов с другими структурами данных. Для обес чеиия хорошего понимания классов читателем мы разрабатываем широ! диапазон примеров и используем их в законченных программах. Выбранс соответствующим образом ADT иллюстрируют связь между абстрактной стр турой и объявлением класса.
3.1. Пользовательский тип — КЛАСС
Класс — это определяемый пользователем тип с данными и функция (методами), называемыми членами (members) класса. Переменная типа кл« называется объект (object). Класс создает различные уровни доступа к с членам, разделяя объявление на части: private, protected и public. Часть privs (закрытая) объекта может быть доступна только для функций-членов в эт классе. Часть public (открытая) объекта может быть доступна для внешн элементов программы, в области действия которых находится этот объект (pi 3.1). Protected (защищенные) члены используются с производными класса» и описываются в главе 12, посвященной наследованию.
Рис. 3.1. Доступ к методам класса
Объявление класса
Объявление класса начинается с заголовка класса (class head), состоящег из зарезервированного слова class, за которым следует имя класса. Член» класса определяются в теле класса (class body), которое заключается в фигур ные скобки и заканчивается точкой с запятой. Зарезервированные слова publi и private разделяют члены класса, и эти спецификаторы доступа заканчиваются двоеточием. Члены класса объявляются как переменные C++, а методы задаются как объявления функций C++. Общая форма объявления класса такова: class Имякласса
private:
// Закрытые данные
// Объявление закрытых методов
//.............
public:
// Открытые данные
// Объявление открытых методов
//.............
Следует, по возможности, помещать члены класса в закрытую секцию. В зультате этого значение данных обновляется только функцией-члеиом клас-Это предотвращает нежелательные изменения в данных кодом использующего класс приложения.
Пример 3.1
Класс Rectangle
При геометрических измерениях прямоугольник определяется его длиной и шириной. Это позволяет нам вычислять периметр и площадь фигуры. Параметры длины и ширины и операции объединяются для образования абстрактного типа данных прямоугольной фигуры. Мы оставляем спецификацию ADT в качестве упражнения и разрабатываем класс Rectangle C++, который реализует этот ADT. Класс содержит конструктор и набор методов — GetLength, PutLength, GetWidth и PutWidth, имеющих доступ к закрытым членам класса. Объявление класса Rectangle следующее:
class Rectangle (
private:
//длина и ширина прямоугольного объекта
float length, width;
public:
// конструктор
Rectangle(float 1 = 0, float w = 0);
// методы для нахождения и изменения закрытых данных
float GetLength(void) const;
void PutLength(float 1);
float GetWidth(void) const;
void PutWidth(float w);
// вычислять и возвращать измерения прямоугольника
float Perimeter(void) const;
float Area(void) const; };
Обратите внимание, что методы GetLength, GetWidth, Perimeter и Area имеют ключевое слово const после списка параметров. Это объявляет каждый метод как константный. В определении константного метода никакой элемент данных не может быть изменен. Другими словами, выполнение метода, объявленного как const, не изменяет состояния объекта Rectangle.
Если первый спецификатор доступа опускается, начальные члены в классе являются закрытыми по умолчанию. Члены класса являются закрытыми, до первого появления открытой или защищенной спецификации. C++ позволяет программисту чередовать закрытую, защищенную и открытую секции, хотя это обычно не рекомендуется.
Конструктор
^^Ункция, называемая конструктором (constructor) класса, имеет то же имя, Кл&сс- Подобно другим функциям C++, конструктору могут передаваться Кдас_ ет£ы’ используемые для инициализации одного или более данных-членов HapaJ*’ ® Классе Rectangle конструктору дается имя Rectangle, и он принимает ад4еТры 1 и w, используемые для инициализации длины и ширины объекта,
я*;;
соответственно. Заметьте, что эти параметры имеют значения по умолчат которые указывают, что используется значение О, когда параметр 1 или i передается явно.
Пример 3.1 иллюстрирует объявление класса (class definition), так методы описываются только объявлениями функций. Код C++ для опр ления отдельных функций создает реализацию класса (class implementati
Объявление объекта
Объявление класса описывает новый тип данных. Объявление обы типа класс создает экземпляр (instance) класса. Это делает реальным оба типа класс и автоматически вызывает конструктор для инициализации которых или всех данных-членов класса. Параметры для объекта передав конструктору заключением их в скобки после имени объекта. Заметьте, конструктор не имеет возвращаемого типа, поскольку вызывается тольк< время создания объекта:
ClassName object(<parameters>); //список параметров может быть пустым
Например, следующие объявления создают два объекта типа Rectang
Rectangle room(12, 10);
Rectangle t;	//использование параметров no умолчанию (0, 0).
Каждый объект имеет полный диапазон данных-членов и методов, с являемых в классе. Открытые члены доступны с использованием имени с екта и имени члена, разделяемых (точкой). Например:
х = room. Area О;	// присваивает х площадь я 12 * 10 “ 120
t.PutLength(20);	// присваивает 20 как длину объекта Rectangle
// Текущая длина равна 0, так как используются
// параметры по умолчанию.
cout < t. GetWidth (); // выводит текущую ширину, которая = 0 по умолчанию
В объявлении объекта Room конструктор первоначально устанавлнв значение длины, равным 12, а ширины — 10. Клиент может изменять f меры, используя методы доступа PutLength и PutWidth:
room.PutLength<15);	// изменение длины и ширины на 15 и 12
room.PutWidth(12) ;
Объявление класса не обязательно должно включать конструктор. Это дей вие, которое не рекомендуется и не используется в этой книге, оставляет объ< с неинициализированными данными в точке его объявления. Например, кл Rectangle может не иметь конструктора, а клиент мог бы задать длин} ширину с помощью открытых методов доступа. Включая в класс конструкт мы обеспечиваем правильную инициализацию важных данных. Конструкт позволяет объекту инициализировать его собственные данные-члены класс;
Класс Rectangle содержит члены класса типа float. В общем, класс моя содержать элементы любого допустимого типа C++, даже других классов. ( нако, класс не может содержать объект его собственного типа в качестве чле1
Реализация класса
Каждый метод в объявлении класса должен быть определен. Определен функций могут быть заданы в теле класса (встроенный код) или вне ei При помещении функции вне тела имя класса, за которым следует два дв<
оЧИя, должно предшествовать имени этой функции. Символ называется Т-ерацией разрешения области действия (scope resolution operator) и ука-° твает на то» что функция принадлежит области действия класса. Это по-ЭвоЛяет всем операторам в определении функции иметь доступ к закрытым членам класса. В случае с классом Rectangle идентификатор ’’Rectangle::" предшествует именам методов.
Р Далее следует определение GetLength(), когда она записана вне тела класса Rectangle:
float Regtangle::GetLength(void) const
* return length; // доступ к закрытому члену length
)
Заметьте, что при определении константного метода может быть также использован квалификатор const.
функция-член класса может быть записана внутри тела класса. В этом случае код является расширенным встраиваемым (expanded inline), а операция разрешения области действия не используется, так как код находится в области действия тела класса. Встраиваемое определение операции GetLength имеет вид:
class Rectangle <
private:
float length;
float width;
public:
float GetLength(void) const // код задается как inline {
return(length);
}
>; *
В этой книге обычно функции-члены определяются вне тела класса для того, чтобы придать особое значение различию между объявлением и реализацией класса. Inline-код используется в этой книге редко.
Реализация конструктора
Конструктор может быть определен как inline или вне тела класса. На-сРимер, следующий код определяет конструктор Rectangle:
Rectangle;: Rectangle (float 1, float w)
length « 1;
width - w;
предоставляет специальный синтаксис для инициализации членов сса. Список инициализации членов (member initialization list) — это список ен данных-членов класса, разделенных запятыми, за каждым из которых Начальное его значение, заключенное в скобки. Начальные значения Являются параметрами конструктора, которые присваиваются соответст-Hiaer'1 даннь1м“членам класса в списке. Список инициализации членов поме-04 После заголовка функции и отделяется от списка параметров двоеточием:
ClassName: :ClassName (parin list): data! (parni!), . . . , datan (parnyj
Например, параметры конструктора 1 и w могут быть присвоены даннь членам класса length и width:
Rectangle::Rectangle(float 1, float w) : length(l), width(w) О
Создание объектов
Один объект может использоваться для инициализации другого в каке либо объявлении. Например, следующий оператор является правильным:
Rectangle square(10, 10), yard = square, S;
Объект square создается c length и width, равными 10. Второй объект уа создается с начальными данными, копируемыми из объекта square. Объект имеет length и width, по умолчанию равными 0.
Объекты могут свободно присваиваться один другому. Если только польз ватель не создает пользовательский оператор присваивания, присваивание об екта может выполняться побитовым копированием данных-членов класса. Н пример, присваивание
S = yard;
копирует все данные из объекта yard в объект S. В этом случае length width объекта yard копируются в length и width объекта S.
Объект может быть создан ссылкой на его конструктор. Например, объя1 ление Rectangle(10,5) создает временный объект с lengh = 10 и width = 5. следующем операторе операция присваивания копирует данные из временног объекта в rectangle S:
S = Rectangle(10,5);
Пример 3.2
1.	Операторы
S = Rectangle(10,5);
cout « S.Areaf) « endl,-
приводят к выводу в поток cout числа 50.
2.	Оператор
cout « Rectangle(10,5)-GetWidth () « endl;
выводит ЧИСЛО 5.
Программа 3.1. Использование класса Rectangle
В этой программе вычисляется относительная стоимость отделочных работ передней стороны гаража. Пользователь задает размеры передней стороны гаража, а программа выдает различные размеры и стоимость дверн. Пользователь замечает, что при выборе большей двери требуется меньше материала для обшивки и опалубки для кладки бетона. Учитывая стоимость пиломатериалов большая дверь может быть более экономичной.
Предположим, что опалубка проходит по периметру передней стороны и периметру проема двери. Мы запрашиваем у пользователя размер передней стороны гаража и затем вводим цикл, позволяющий выбрать размер двери. Цикл заканчивается, когда пользователем выбирается опция "Quit". Для каждого выбора двери программа определяет стоимость отделки передней стороны гаража и выводит это значение. Мы задаем константами стоимость деревянной обшивки $2 за кв. фут и стоимость опалубки иа $0.50 за погонный фут.
Длина опалубки равна сумме периметров передней стороны гаража и двери. Стоимость обшивки равна площади передней стороны гаража минус площадь двери.
// ргОЗ_О1.срр
finclude <iostream.h>
class Rectangle
{
private:
// длина и ширина прямоугольного объекта
float length,width;
public:
/7 конструктор
Rectangle(float 1-0, float w = 0);
// методы для получения и модификации закрытых данных float GetLength(void) const;
void PutLength(float 1);
float GetWidth (void) const;
void PutWidth(float w);
// вычисление характеристик прямоугольника
float Perimeter(void) const;
float Area(void) const;
/7 конструктор, выполняет присваивания: length=l, width-w Rectangle::Rectangle (float 1, float w) : length(l), width(w) {}
/7 возвратить длину прямоугольника
float Rectangle::GetLength (void) const {
return length;
)
7/ изменить длину прямоугольника
void Rectangle::PutLength (float 1} I
length =1;
/ / возвратить ширину прямоугольника float Rectangle: :GetVJidth (void) const {
return width;	//
)
// изменить ширину прямоугольника void Rectangle: :PutWidth (float w) (
width = w;
// вычислить и возвратить периметр прямоугольника float Rectangle: : Perimeter (void) const
(
return 2.0* (length + width) ;
// вычислить и возвратить площадь прямоугольника float Rectangle: :Area (void) const
{
return length*width;
void main (void)
{
/ / стоимости обшивки и опалубки — постоянные
const float sidingCost = 2.00, moldingCost = 0.50;
int conipletedSelections = 0;
// опция из меню, выделенная пользователем
char doorOption;
// длина/ширина и стоимость двери
float glength, gwidth, doorCost;
// общая стоимость, включая дверь, обшивку и опалубку float totalcost;
cout « "Введите длину и ширину гаража: ’’;
cin » glength » gwidth;
// создать объект garage (гараж) с размерами по умолчанию // создать объект door (дверь) с размерами по умолчанию Rectangle garage (glength, gwidth) ;
Rectangle door;
while (! conipletedSelections)
{
cout « "Введите 1-4 или ' q' для выхода” « endl « endl cout << ’’Дверь 1 (12 x 8; $380)	”
« "Дверь 2 (12 x 10; $420)”« endl;
cout « ’’Дверь 3 (16 x 8; $450)	”
« "Дверь 4 (16 x 10; $480) ” « endl;
cout « endl;
cin >> doorOption;
if (doorOption == ' q') conipletedSelections = 1;
else
{ switch {doorOpticn)
case ' 1' :door.PutLength(12); // 12 x 8 ($380) door.PutWidth(8);
doorCost = 380; break;
case ' 2' :door.PutLength(12); //12x10 ($420) door.PutWidth(10);
doorCost = 420; break,-
case '3* :door. PutLength(16) ; // 16 x 8 ($450)
door.PutWidth(8);
doorCost - 450;
break;
case '4' :door.PutLength(12); // 16x 10 ($480)
door.PutWidth(10);
doorCost = 480; break;
)
totalcost = doorCost +
moldingCost*(garage.Perimeter()+door.Perimeter(}) + sidingCost*(garage.Area')-door.Area());
cout « "Общая стоимость двери, обшивки и опалубки: $" « totalCost « endl « endl;
} )
) /*
«Запуск программы 3.1>
Введите длину и ширину гаража: Введите 1-4 или ' q' для выхода
Дверь 1 (12 х8; $380) Дверь 2 (12 х 10; $420)
Дверь 3 (16x8; $450) Дверь 4 (16 х 10; $480)
Общая стоимость двери, обшивки и опалубки: $720
Введите 1-4 или * q' для выхода
Дверь 1 (12 х8; $380) Дверь 2 (12 х 10; $420)
Дверь 3 (16x8; $450) Дверь 4 (16 х 10; $480) q */
3.2. Примеры классов
Следующие два примера классов иллюстрируют конструкторы класса в C++, ласс Temperature поддерживает записи значений высокой и низкой темпера-й В качестве приложения объект мог бы иметь высокую (точка кипения) ®ИзкУю (точка замерзания) температуры воды. ADT RandomNumber опреде-caw «аП ДЛя созДания последовательности целых или с плавающей точкой Ив» аИйЬ1Х чисел’ В реализации C++ конструктор позволяет клиенту самому Циализировать последовательность случайных чисел или использовать про-ЙЬ1й способ получения последовательности с системно-зависимой функ-® времени.
Класс Temperature
Класс Temperature содержит информацию о значениях высокой и низке температуры. Конструктор присваивает начальные значения двум закрыты данным-членам highTemp и lowTemp, которые являются числами с плавав щей точкой. Метод UpdateTemp принимает новое значение данных и опр< деляет, должно ли обновляться одно из значений температуры в объект* Если отмечается новое самое низкое значение, то обновляется lowTemp. Ahi логично, новое самое высокое значение изменит highTemp. Этот класс имее два метода доступа к данным: GetHighTemp возвращает самую высокую тек пературу, a GetLowTemp возвращает самую низкую температуру.
Спецификация класса Temperature
ОБЪЯВЛЕНИЕ class Temperature { private: float highTemp, lowTemp;	// закрытые данные-члены
public: Temperature (float h, float 1); void UpdateTemp(float temp);	I
float GetHighTemp(void) const;	\
float GetLowTemp(void) const; H
ОБСУЖДЕНИЕ
Конструктору должны быть переданы начальные высокая и низкая температуры для объекта. Эти значения могут быть изменены методом UpdateTemp. Методы GetLowTemp и GetHighTemp являются константными функциями, так как они не изменяют никакие данные-члены в классе. Класс описан в файле "temp.h". ПРИМЕР //точка кипения/замерзания воды по Фаренгейту Temperature fwater(212,32); //точка кипения/замерзания воды по Цельсию Temperature cwater(100, 0); eout «* Вода замерзает при « cwater .GetLowtemp « ” С” 	« endl;
cout « Вода кипит при « fwater.GetHighTemp « ” F” « endl; Выход: Вода замерзает при 0 С Вода кипит при 212 F
Реализация класса Temperature
Каждый метод в классе записывается вне тела класса с использованием оператора области действия. Конструктор принимает начальные показания высокой и низкой температуры, которые присваиваются полям highTemp и lowTemp. Эти значения могут изменяться только методом UpdateTemp, когда новая высокая или низкая температура передаются в качестве параметра. Функции доступа GetHighTemp и GetLowTemp возвращают значение высокой и низкой температуры.
//конструктор, присвоить данные: highTemp=h и lowTemp=l
Temperature::Temperature(float h, float 1): highTempth),
lowTemp(1)
биовление текущих показаний температуры oid Temperature;:UpdateTemp (float temp) {
(temp* highTemp) highTemp - temp; else if (temp < iowTemp) IowTemp - temp;
}
// возвратить high (самая высокая температура) float Temperature:zGetHighTemp (void) const
{ return highTemp;
// возвратить low (самая низкая температура)
float Temperature::GetLowTemp (void) const
return IowTemp;
}
Программа 3.2. Использование класса Temperature
Ц pr03_02.cpp
♦include <iostream.h>
♦include "temp.h” //
void main(void)
<
//
Temperature today (70,50);
float temp;
cout « "Введите температуру в полдень: cin » temp;
// обновить объект для включения дневной температуры today.UpdateTemp (temp);
cout « ”B полдень: Наивысшая :" « today.GetHighTemp(); cout « *' Низшая ” « today.GetLowTemp() « endl;
cout « "Введите вечернюю температуру: cin » temp;
// обновить объект для включения вечерней температуры today.UpdateTemp(temp);
cout « "Сегодня наивысшая :” « today.GetHighTemp(); cout « ’* Низшая " « today .Ge tLowTemp () « endl;
/*
’'Запуск программы pr03_O2. cpp>
®ведите температуру в полдень: 80
ВвПЗДяе«ь " Наивысшая : 60 Низшая 50
С® Лите вечернюю температуру: 40
г°дня наивысшая :80 Низшая 40
‘/
Класс случайных чисел
Для многих приложений требуются случайные данные, представляют случайные события. Моделирование самолета, тестирующее реакцию летчи на непредвиденные изменения в поведении самолета, карточная игра, пр< полагающая, что дилер использует тасованную колоду, и изучение сбьг] предполагающее вариации в прибытии клиентов, — все это примеры ко пьютерных приложений, которые опираются на случайные данные. Компь тер использует генератор случайных чисел (random number generator), к торый выдает числа в фиксированном диапазоне таким образом, что чис. равномерно распределяются в этом диапазоне. Генератор использует дете министический алгоритм, который начинается с начального значения да ных, называемого значением, инициализирующим алгоритм, или seed-зн чением. Алгоритм манипулирует этим значением для генерирования поел довательности чисел. Этот процесс является детерминистическим, так ке он берет начальное значение и выполняет фиксированный набор инструкции Выход является уникальным, определенным данными и инструкциями. П существу, компьютер не производит истинные случайные числа, а создав последовательности псевдослучайных чисел (pseudorandom numbers), кот< рые распределяются равномерно в диапазоне. Вследствие начальной завись мости от seed-значения, генератор создает ту же последовательность при ’ак пользовании одного и того же seed-значения. Способность повторять случай ную последовательность используется в исследованиях моделирования, гд в приложении необходимо сравнить различные стратегии, реагирующие н один и тот же набор случайных условий. Например, имитатор полета ис пользует одну и ту же последовательность случайных чисел для сравнение эффективности реакции двух летчиков на аварию самолета. Каждый летчш подвергается одному и тому же набору событий- Однако, если seed-значенж изменяется каждый раз при запуске имитатора, мы имеем уникальное мо делирование. Эта уникальность свойственна игре, которая обычно создает различную последовательность событий каждый раз в процессе игры.
Большинство компиляторов предоставляют библиотечные функции, реализующие генератор псевдослучайных чисел. К сожалению, вариация этой реализации в зависимости от компилятора является значительной. Для предоставления генератора случайных чисел, переносимого из системы в систему, мы создаем класс RandomNumber. Этот класс содержит seed-значение, которое должно инициализироваться клиентом. В соответствии с начальным seed-значением генератор создает Псевдослучайную последовательность- Класс обеспечивает автоматический выбор seed-значения, когда конструктору не передается никакого значения, и позволяет клиенту создавать независимые псевдослучайные последовательности.
Спецификация класса RandomNumber
ОБЪЯВЛЕНИЕ
^include <tinie.h>
11 используется для генерации случайного числа
// по текущему seed-значению
const unsigned long maxshort = 65536L;
const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
class RandomNumber
private:
// закрытый член класса, содержащий текущее seed-значение unsigned long randseed;
^//конструктор. параметр 0 (по умолчанию) задает автоматический
// выбор seed-значения
RandomNumber (unsigned long s = 0);
// генерировать случайное целое в диапазоне [О, п-1] unsigned short Random(unsigned long n);
}l генерировать действительное число в диапазоне [0, 1.0] double fRandom(void);
}•
ОПИСАНИЕ
Начальное seed-значение —- это беззнаковое длинное число. Метод Random принимает беззнаковый длинный параметр п < 65536 и возвращает 16-битовое беззнаковое короткое значение в диапазоне О,. . . , п - 1. Заметьте, что если возвращаемое методом Random значение присваивается целой переменной со знаком, то это значение может интерпретироваться как отрицательное, если и не будет удовлетворять неравенству n < 215 = 32768. Функция fRandom возвращает число с плавающей точкой в диапазоне О < fRandom() < 1.0.
DPHhciP
RandomNumber rnd; //seed-значение выбирается автоматически
RandomNumber R(l); //создает последовательность с seed пользователя 1
cout « R.fRandomO; //выводит действительное число в диапазоне 0—1 //выводит 5 случайных целых чисел в диапазоне 0 — 99 for (int i = 0; i < 5; i++)
Cout « R.Random(100) « ”	// <sample> 93 21 45 5 3
Пример 3.3
Создание случайных данных
1. Значение грани кости находится в диапазоне 1 — 6 (шесть вариантов). Для имитации бросания кости используйте функцию die.Random(6), которая возвращает значения в диапазоне О — 5. Затем прибавьте 1 для перевода случайного числа в нужный диапазон.
RandomNumber Die //использует автоматич. seeding
dicevalue = die.Random(6) +1;
2. Объект FNum использует автоматическое задание seed-значения для создания случайной последовательности:
RandomNumber FNum;
Для вычисления плавающего значения в диапазоне 50 < х < 75 генерируйте случайное число в диапазоне О — 25, умножая результат fRandom на 25. Это расширяет диапазон случайных чисел от 1-й единицы (0 < х < 1) до 25 единиц (0 < х < 25). Преобразуйте нижнюю границу нового диапазона, добавив 50:
value = FNum. fRandom() *25 + 50; //умножение на 25; прибавление 50
Реализация класса Random Number
Для создания псевдослучайных чисел мы используем линейный конгруэ: тный алгоритм. Этот алгоритм использует большой нечетный постоянный мн житель и постоянное слагаемое вместе с seed-значеннем для итеративного с-здания случайных чисел и обновления seed-значения:
const unsigned long maxshort = 65536;
const unsigned long multiplier = 1194211693;
const unsigned long adder = 12345;
Последовательность случайных чисел начинается с начального значени для длинного целого randSeed. Задание этого значения называется настройко (seeding) генератора случайных чисел и выполняется конструктором.
Конструктор позволяет клиенту передавать seed-значение или использоват для его получения машинно-зависимую функцию time. Мы подразумеваем, чт функция time объявляется в файле <time.h>. При вызове конструктора с паре метром 0 функция time возвращает беззнаковое длинное (32-битовое) число указывая количество секунд, прошедших после базового времени. Используе мое базовое время включает полночь 1-го января 1970 года и полночь 1-г< января 1904 года. В любом случае, это большое беззнаковое длинное значение //генерация seed-значения
RandomNumber::RandomNumber (unsigned long s)	v
(
if (s == 0)
randSeed = time(O); //использование системной функции time else
randSeed *= s;	//пользовательское seed-значение
}
В каждой итерации используем константы для создания нового беззнакового длинного seed-значения:
randSeed - multiplier • randSeed + adder;
В результате умножения и сложения верхние 16 бнтов 32-битового значения randSeed являются случайными ("хорошо перемешанными") числами. Наш алгоритм создает случайное число в диапазоне от 0 до 65535, сдвигая 16 битов вправо. Мы отображаем это число на диапазон 0 ... п - 1. беря остаток от деления на п. Результатом является значение Random(n).
//возвращать случайное целое 0 <=* value <= п-1 < 65536 unsigned short RandomNumber::Random (unsigned long n) {
randSeed = multiplier * randSeed + adder;
return (unsigned short) ((randSeed) » 16) % n);
)
Для числа с плавающей точкой сначала вызываем метод Random(maxshort), который возвращает следующее случайное целое число в диапазоне от 0 до maxshort — 1. После деления на double(maxshort) получаем действительное число в интервале 0 < fRandom() < 1.0.
double RandomNumber::fRandom (void) (
return Random(maxshort)/double(maxshort);
)
Объявление и реализация RandomNumber содержится в файле random.h".
Приложение: Частота выпадения лицевой стороны при бросании монет. Класс RandomNumber используется для имитации повторяемого бросания 10 свет. Во время бросания некоторые монеты падают лицевой стороной (head)1 Й еих а другие — обратной. Бросание десяти монет имеет результатом число бдений лицевой стороной в диапазоне 0-10. Интуитивно вы подразумеваете, ®	0 лицевых сторон или 10 лицевых сторон в бросании 10 монет — это
относительно невероятно. Более вероятно, что количества выпадений разных сторон будут примерно равными. Число лицевых сторон будет находиться где-нибудь в середине диапазона 0—10, скажем, 4—6. Мы проверим это интуитивное предположение большим числом (50 0000) повторений бросания. Массив head ведет подсчет количества раз, когда соответствующий подсчет лицевых сторон составляет 0, 1, . .	10.
Значение head[i] (0 < i < 10) — это количество раз в 50 000 повторениях, когда ровно i лицевых сторон выпадает во время бросания 10 монет.
Программа 3.3. График частоты
Бросание 10 монет составляет событие. Метод Random с параметром 2 моделирует одно бросание монеты, интерпретируя возвращаемое значение 0 как обратные стороны, а возвращаемое значение 1 — как лицевые стороны, функция TossCoins объявляет статический объект coinToss типа Random-Number, использущнй автоматическое задание seed-значения. Так как этот объект является статическим, каждый вызов TossCoins использует следующее значение в одной последовательности случайных чисел. Бросание указанного количества монет выполняется суммированием 10 значений, выдаваемых CoinToss.Random(2). Возвращаемый результат приращивает соответствующий счетчик в массиве лицевых сторон.
Выходом программы является частотный график количества лицевых сторон. График с числом лицевых сторон иа оси х н относительным числом событий (occurences) — на оси у обеспечивает наглядное представление того, что известно как биномиальное распределение. Для каждого индекса i относительное число событий, при которых лицевые стороны выпали ровно i раз, составляет
heads[i]/f1oat(NTOSSES)
Это значение используется для помещения символа * в относительном Местоположении между 1-й и 72-й позицией строки. Результирующий график является аппроксимацией биномиального распределения.
^include <iostream.h>
•include <iomanip.h>
^include "random.h" // включает генератор случайных чисел
"Оросить" numberCoins монет и возвратить общее число '! выпадений лицевой стороны int TossCoins(int numberCoins}
Static RandomNumber coinToss; int i, tosses =0;
дод лицевой стороной подразумевается та сторона монеты, на которой изображен монарх президент. — Прим. ред.
а.	> zii'xcer^oxns;i++)
// Random (2) * 1 индицирует лицевую сторону tosses +~ coinToss.Random(2); return tosses; ) void main (void) { // число монет в бросании и число бросаний const int NCOINS = 10; const long NTOSSES - 50000;
// heads[0)=сколько раз не выпало ни одной лицевой стороны // heads [ 1 ] =сколько раз выпала одна лицевая сторона ит.д. long i, heads[NCOINS + 1];
int j, position;
// инициализация массива heads for (j=0;j <= NCOINS+1;j++) heads[51 =0;
// •‘бросать" монеты NTOSSES раз и записывать результаты в массив heads for (i=0;i<NTOSSES;i++) heads(TossCoins(NCOINS)] ++;
// печатать график частот for (i=0;i < NCOINS+1;i++) ( position = int(float(heads[il)/float(NTOSSES) * 72); cout « setw(6) « i « " "; for (j=0;j< positional;j++1 cout « "
И’*' относительное число бросаний c i лицевыми сторонами cout « ' *r « endl; ) ) /* <3апуск программы pr03_03. срр> 0 * 1 * 2	*
3 4 * 5 6 7	*
8	*
9 * 10 * */
3.3.	Объекты и передача информации
Объект является экземпляром типа данных и как таковой может передаваться в качестве параметра функции или возвращаться как значение функции. Подобно другим типам C++, объектный параметр может передаваться по значению или по ссылке. Положения этого раздела иллюстрируются примерами из класса Temperature.
Объект как возвращаемое значение
Любой тип класса может быть возвращаемым типом функции. Например, луВкция SetDailyTemp принимает в качестве параметра массив чисел, пред-уавляющий показания температуры, извлекает максимальное и минималь-soe показания из списка и возвращает объект Temperature с этими крайними значениями.
Te5nperature SetDailyTemp (float reading!], int n)
//создание t с 1-ми значениями high и low Temperature t(reading[0], ifeading[0]}; //обновление high или low, если необходимо for (int i - 1; i < n; i ++)
t.UpdateTemp(reading(i]);
//возвращение t с крайними температурами этого дня return t;
Массив reading содержит шесть температурных значений. Для определения высокой и низкой температур вызовите SetDailyTemp и присвойте результат объекту today. Чтобы вывести эти температуры на экран, используются методы GetHighTemp и GetLowTemp.
float reading[6] - (40, 90, 80, 60, 20, 50);
Temperature today « SetDailyTemp (reading, 6);
cout « ’’Сегодняшние высокая и низкая температуры такие" « today.GetHighTemp() « "и”
« today.GetLowTemp()	« endl;
Объект как параметр функции
Объекты могут передаваться как параметры функции по значению или по ссылке. Следующие примеры иллюстрируют соответствующий синтаксис.
Функция TemperatureRange использует вызов по значению (call by value) параметра Т типа Temperature и возвращает разницу между самой высокой и самой низкой температурами. При выполнении этой функции вызывающий элемент копирует объект типа Temperature (фактический параметр) в Т. float TemperatureRange(Temperature Т)
return т.GetHighTemp() — т.GetLowTemp();
Функция Celsius использует вызов по ссылке (call by reference) параметра Т типа Temperature, который, как первоначально подразумевалось, содержит значения по Фаренгейту. Функция создает объект типа Temperature, чьи самое высокое и самое низкое показания преобразуются в значения по Цельсию, и присваивает его объекту Т.
Celsius(Temperatures Т)
float hi, low;
//c = 5/9 * (f-32)
hi = float(5)/9 ♦ (T.GetHighTemp() -32);
low = float (5)/9 * (T.GetLowTempO -32);
Т» Temperature(hi, low);
Пример: объект Water содержит точку кипения (212° по Фаренгейту) точку замерзания (32° по Фаренгейту) воды в качестве самого высокого самого низкого температурных значений. Результат использования функци TemperatureRange показывает, что 180° — это диапазон для воды по шкал Фаренгейта. С помощью функции Celsius преобразуем эти температуры значения по Цельсию и вызовем TemperatureRange, чтобы показать, чт 100° — это соответствующий диапазон по шкале Цельсия.
Temperature Water(212, 32);	//кипение при 212F, замерзание при 32F
cout «"Температурный диапазон воды по шкале Фаренгейта"
« TemperatureRange(Water) « endl;
Celsius(Water); //преобразование температуры по Фаренгейту
//в температуру по Цельсию
cout «"Температурный диапазон воды по шкале Цельсия"
« TemperatureRange(Water) « endl;
3.4.	Массивы объектов
Тип элемента массива может включать не только встроенные типы данных такие как int или char, но также определяемые пользователем типы класса Результирующий массив объектов может использоваться для создания сйис ков, таблиц и так далее. Однако, использование объектных массивов требуеч осторожности. Объявление массива вызывает конструктор для каждого объекта в списке. Сравните простое объявление одного объекта Rectangle и массива из 100 объектов Rectangle. В каждом объявлении конструктор вызывается для создания объекта, который задает длину и ширину. В случае массива конструктор вызывается для каждого из 100 объектов.
Rectangle pool(150, 100); //создание бассейна 150 х 100
Rectangle room[100];	//конструктор вызывается для
//комната[0] .. (99)
Объявление объекта pool передает начальные значения конструктору. Объекты room фактически имеют начальные значения, поскольку конструктор Rectangle присваивает нулевые значения по умолчанию длине и ширине объекта:
Rectangle(float 1=0, float w=0); //параметры по умолчанию
После объявления массива длина и ширина каждого объекта room[i] имеют нулевые значения:
cout « room[25].GetLengh()
cout « room[251.GetWidth()
room[25].FutLengh(lO)
room[25]-PutWidth(5)
//выход 0;
//выход 0;
//установка длины комнаты[25] иа 10
//установка ширины комнаты[25] на 5
Объявление массива объектов Rectangle поднимает важную проблему, касающуюся массивов и классов. Если конструктор класса Rectangle не имеет параметров по умолчанию, объявление массива room вызовет ошибку, потому что каждый массив будет требовать параметры. Объявлению потребуется список инициализаторов массива, который управляет каждым элементом в массиве. Например, для объявления массива room нз 100 элементов и установки параметров длины н ширины на 0 потребуется список инициализаторов 100 объектов Rectangle. В действительности это на практикуется.
Rectangle room[100] = (Rectangle(O, 0), . . . , Rectangle(0, 0)):
Для объявления массива объектов мы предоставляем конструктору значе-Я0Я В© умолчанию или просто создаем конструктор без параметров.
Конструктор умолчания
Конструктор умолчания (default costructor) — это конструктор, не требую-агиЙ никаких параметров. Это бывает, когда конструктор не имеет параметров ли когда каждый параметр имеет значение по умолчанию. В этой главе класс entangle содержит конструктор умолчания, тогда как класс Temperature требует параметров при объявлении объекта.
Класс Rectangle
КОЙСТ₽>ГКТО₽
Rectangle(float 1-0, float w-0);
Конструктор содержит параметры 1 й w со значением по умолчанию 0. При создании массива Rectangle значения по умолчанию присваиваются каждому объекту.
Rectangle R[25j;	//каждый элемент имеет значение Rectangle (0, 0)
Класс Temperature
КОЯСТРУКТОР
Temperature(float h, float 1);
Класс Temperature не содержит конструктор по умолчанию. Вместо этого, объекту должно быть дано начальное значение для высокой и низкой температуры. Объявление объектов today и week является недействительным!
Temperature today; //недействительно: отсутствуют параметры
Temperature weeк[7]; //Temperature не имеет конструктора по умолчанию
3.5.	Множественные конструкторы
До сих пор в наших классах мы разрабатывали как default-, так и nonde-«ult-конструкторы1. В результате предыдущих рассуждений вы можете предположить, что они являются взаимоисключающими, поскольку все классы ЕМели одиночный конструктор. C++ "признает’' нашу потребность в разнообразии способов инициализации объекта н позволяет определять множествен-конструкторы в одном и том же классе. Компилятор использует перегрузку Функции для выбора правильной формы конструктора, когда мы создаем Концепция перегрузки функции и ее правила обсуждаются в главе 6.
^tiple-конструкторы добавляют большие возможности классу. Особый тип af tiple-конструктора, называемый конструктором копирования (сору соп-
Uetor), используется со многими классами, содержащими динамические “НЬ-е-члены. Конструктор копирования описывается в главе 8.
отметить, что в русских изданиях термин default constructor встречается как кон-^РУктор умолчания или default-конструктор. В то же время под термином nondefault construc-г (или nondefault-конструктор) следует понимать конструктор с самым обычным синтакси--*• Из которого не следуют никакие дополнительные свойства конструктора. — Прим. ред.
, месяц i j 1 m < 12 I
день $
, 1 < d £ 31 i
год
1900 у 1999
Класс Date иллюстрирует использование multiple конструкторов. Эт класс имеет три данных-члена, которые обозначают месяц, день и год в дат
Один конструктор имеет три параметра, соответствующие трем данным-чл нам. Действием конструктора является инициализация этих переменных. Вт рой конструктор позволяет клиенту объявлять дату как строку в форк "mm/dd/уу", читает эту строку и преобразует пары символов "mm" в месях ”dd" в день и "уу" в год. Для каждого конструктора мы подразумеваем, чт параметр, задающий год — это значение из двух цифр в диапазоне 00 95 Фактический год сохраняется добавлением 1900 к начальному значению:
year  1900 + уу
Класс Date имеет метод, который выводит на экран полную дату с назва ннем месяца, дия н значением года. Например, первый день в двадцато» веке был
1 января 1900
Спецификация класса Date
ОБЪЯВЛЕНИЕ
♦include <string.h>
♦include <strs.tream.h>
class Date (
private:
// закрытые члены, которые определяют дату
int month, day, year;
public:
/1 конструкторы, дата по умолчанию — Январь 1, 1900
Date (int л » 1, int d - 1, int у = 0);
Date (char *dstr)
// вывод данных в формате “месяц день, год" void PrintDate (void);
}i
ОПИСАНИЕ
Для построения Date-объектов используются два конструктора, отличающиеся параметрами. Компилятор выбирает конкретный конструктор во время создания Date-объекта. Следующие примеры демонстрируют создание объектов.
ПРИМЕРЫ
Date dayl(€, 6, 44); Date day2; date day3("12/31/99");	// 6 июня 1944 // значение no умолчанию для 1 января 1990 //31 декабря 1999
Реализация класса Date
Сердцевиной класса Date являются два его конструктора, которые определяют дату, передавая значения месяца, дня и года или строки ' mm/dd/yy”.
Первый конструктор имеет три параметра со значениями по умолчанию, ответствующими 1 января 1900 года. Со значениями по умолчанию конструк-^ор квалифицируется как конструктор умолчания:
конструктор, day и year задаются как целые mm dd уу
*=--Date (int m, int d, int y) : month(m), day(d) pace • 	_
year = 1900 + у; // у — год в 20-м столетии
)?
Альтернативная форма конструктора принимает строковый параметр. Строка имеет форму "mm/dd/yy". Для преобразования пар данных мы используем ввод нв базе массива, который преобразует символьные пары "mm" в целое значение месяца и так далее. Копируем строку параметра в массив inputBuffer И затем читаем символы в таком порядке:
month — ch — day -ch — year
Ввод ch удаляет два разделителя "/" из строки ввода.
// конструктор
// month, day и year задаются в виде строки ’’mm/dd/yy" Date::Date (char *dstr)
{
char inputBuffer[16];
char ch;
// копирование в inputBuffer
strcpy(inputBuffer,dstr);
istrstream input(inputBuffer, sizeof(inputBuffer));
// чтение данных из входного потока ch используется в качестве символа '/' input » month » ch » day » ch »year;
year +- 1900;
h
При выводе метод Print дает текст полной даты, включающий название месяца, дня и год. Массив months содержит пустую строку (индекс 0) и 12 названий для календарных месяцев. Значение месяца используется как индекс в массиве для печати названия месяца.
// печать даты с полным названием месяца
void Date::PrintDate (void) {
// статический массив с названиями месяцев
static char *Months[] = С’","Январь","Февраль",
"Нарт","Апрель", "Най",
"Июнь","Июль","Август", "Сентябрь","октябрь", "Ноябрь”,"Декабрь"};
cout « Months [month) « " ” « day « ", ’« year ;
Программа 3.4. Дата двадцатого века
Тестовая прграмма использует конструкторы для установки демонстрационных объектов. Получаемые в результате данные печатаются. Класс Date содержится в файле "date.h'*.
fcxnclude <iostream.h>
#xnclude •'date.ii"
// включение класса Date
void main(voxd)
4
// Date-объекты с целыми. Date dayl(6,6,44);
Date day2;
Date day3("12/31/99");
умалчиваемыми и строчными параметрами
// Июнь 6, 1944
// Январь 1, 1900
// Декабрь 31, 1999
cout « "День Д во Второй Мировой войне — day1.FrxntDate();
cout « endl;
cout « "Первый день 20-ого века —
day2.FrxntDate();
cout « endl;
cout « "Последний день 20-ого века —
day3.PrintDate();
cout « endl;
)
/*
<Выполнение программы 3.4>
День Д во Второй Мировой войне — Июнь 6, 1944 Первый день 20-ого века - Январь 1, 1900 Последний день 2С-ого века — Декабрь 31, 1999 */
3.6.	Практическое применение: Треугольные матрицы
Двумерный массив, часто называемый матрицей (matrix), предоставляем важную для математики структуру данных. В этом разделе мы исследуем квадратные матрицы (square matrices — матрицы с одинаковым числом строк и столбцов), чьи элементы данных являются действительными числами. Мы разрабатываем класс TriMat, определяющий верхние треугольные матрицы (upper triangular matrices), в которых все элементы, находящиеся ниже диагонали, имеют нулевые значения.
Аоо А01 ................ Афп.1
О	Аи
О	О
О	...	...	An_a„.s	Ад.зд-j
0	0	0	0	АВ.1П.1
В математических терминах, Aj,j=O для j<i. Верхний треугольник определяется элементами Aij для j>i. Эти матрицы имеют важные алгебраические
оЙства и используются для решения систем уравнений. Реализация операций С^т)Хней треугольной матрицы в классе TriMat показывает способ эффективного Хранения треугольной матрицы в виде одномерного массива.
Свойства верхней треугольной матрицы
Если верхняя треугольная матрица имеет п2 элементов, приблизительно половина из них являются нулевыми и нет необходимости сохранять их явно. Конкретно, если мы вычитаем п диагональных элементов из суммы п2 элементов, то половина оставшихся элементов являются нулевыми. Например, при и—25 имеется 300 элементов со значением 0:
— п)/2 = (252 _ 25)/2 = (625 — 25)/2 - 300
Далее следует набор операций для треугольных матриц. Мы определяем сложение, вычитание и умножение матриц, а также детерминант, который имеет важное применение для решения уравнений.
Сумма или разность двух треугольных матриц А и В получается в результате сложения или вычитания соответствующих элементов матриц. Результирующая матрица является треугольной.
Сложение С = А + В
где С — это треугольная матрица с элементами Cq = Aq + Bq. Вычитание С = А — В
где С — это треугольная матрица с элементами Cq = Aq - Bq.
Умножение С = А * В
Результирующая матрица С — это треугольная матрица с элементами C,j, значения которых вычисляются иэ элементов строки i матрицы А и столбца j матрицы В:
^q~(A10*B0j) + (Ai t*Bij) + (А^г*В2^) + . . . + (Д>л J
Например, если
Матриц^ gTO ~ произведений элементов строки 0 матрицы А и колонки 2
1*4+1*1+0*3=5
5
Для общей квадратной матрицы детерминант является сложной для числения функцией, однако вычислить детерминант треугольной матр не трудно. Просто получите произведение элементов на диагонали.
А<х> А.» ............. Aon.j
О Ап
О
О
det(A)-
‘ОО
А,
О
l-2n-2 Ap.jn-i
О
О
О О

Хранение треугольной матрицы
Применение для хранения верхней треугольной матрицы стандарт» двумерного массива требует использования всей памяти размером п2, несм< ря на прогнозируемые нули, расположенные ниже диагонали. Для искл чения этого пространства мы сохраняем элементы из треугольной матрш в одномерном массиве М- Все элементы ниже главной диагонали не coxj няются. Таблица 3.1 показывает количество элементов» которые сохраняют в каждой строке.
Хранение треугольной матрицы
Таблица £
Строка	Число элементов	Элементы
0	п	(Ао, о - • - Ас. п-1)
Ь	п-1	(Ai. 1... Ai, n-i)
2	п-2	(A?, 2... Аг. n-l)
		
п-2	2	(An-2. <12  -  An-2, n-l)
1 п-1	1	(An-1, n-l)
Алгоритму сохранения требуется функция доступа, которая должна опре делять местоположение в массиве М элемента Ax,j. Для j < i элемент Ах,, явля ется равным О и не сохраняется в М. Для j > i функция доступа использует информацию о числе сохраняемых элементов в каждой строке вплоть до строки i. Эта информация может быть вычислена для каждой строки i и сохранена в массиве (rowTable) для использования функцией доступа.
rowTable	Замечание
rowTable[0]	=0	0 элементов	перед	строкой	0
rowTable[1]	=	n	п элементов	перед	строкой	1	(от	строки 0)
rowTable(2]	=	п + п-1	п+п-1 элементов	перед строкой	2
rowTable[3]  n + п-1+п-2 элементы перед строкой 3
rowTable [п-1] - п + п-1 + — +2
Пример 3.4
Рассмотрим матрицу X размера 3x3
"110
Строка
0
1
2
2	1
0	2
rowTable
Замечание
rowTable[0] - 0	0 элементов, сохраненных перед строкой 0
rowTable[1] - 3	3 элемента строки 0 (110)
rowTable[2] =5	5 элементов из строк 0 и 1 (11021)
Row Table
I О I 3 | 5~| 0	1	2
Элементы треугольной матрицы сохраняются по строкам в массиве М.
Массив М
Элементы Элементы Элементы строки о	строки 1 строки 2
С учетом того, что элементы треугольной матрицы сохраняются построчно Массиве М, функция доступа для А^ использует следующие параметры:
Индексы i и j,
Массив rowTable
Алгоритм доступа к элементу Ajj заключается в следующем:
Если j<i, Aij = 0 и этот элемент не сохраняется.
Если j>l, то получается значение rowTablefi], являющееся количеством элементов, которые сохраняются в массиве М, для элементов до строки В строке i первые i элементов являются нулевыми и не сохраняются
в М. Элемент Aij помещается в M[rowTabIe[i] + (j — i)]-
Пример 3.5
Рассмотрим треугольную матрицу Х[3][3] из примера 3.4: ; 'fi 1. Х0.2 =M[rowTable[0] + (2 — О)] ‘	’ —М[0 + 2]
=М[2] = О
2. Х1,о не сохраняются
3. Х1>2 =M[rowTable[l] + (2 — 1)]
’‘7; =м[з + 1]
=М[4] = 1
Класс TriMat
Класс TriMat реализует ряд операций треугольной матрицы. Вычитание умножение треугольной матрицы оставлены для упражнений в конце глав! Учитывая то ограничение, что мы должны использовать только статически массивы, наш класс ограничивает размер строки и столбца числом 25. Пр этом мы будем иметь 300 = (252 — 25)/2 нулевых элементов, поэтому массв М должен содержать 325 элементов.	\
Спецификация класса TriMat
ОБЪЯВЛЕНИЕ tfrnclude <iostream.h> flinclude <stdlib.h>
// максимальное число элементов и строк // верхней треугольной матрицы const int ELEMENTLIMIT - 325;
const int ROWLIMIT =25;
class TriMat { private: // закрытые данные-члены int rowTable{ROWLIMIT];	// начальный индекс строки в М
int n.-	// размер строки/колонки
double М[ELEMENTLIMIT];
public: // конструктор с параметрами TriMat(int matsize);
// методы доступа к элементам матрицы void PutElement (double item, int i, int j); double GetElement(int i, int j) const;
// матричные арифметические операции TriMat AddMat(const TriMati A) const; double DelMat(void) const;
// матричные операции ввода/вывода void ReadMat(void);
void WriteMat(void) const;
j/ получить размерность матрицы int GetDimension (void) const;
};
ОйИСАНИЕ
Конструктор принимает число строк и столбцов матрицы. Методы PutEle-ent и GetElement сохраняют и возвращают элементы верхней треугольной Матрицы. GetElement возвращает О для элементов ниже диагонали. AddMat в0Эвращает сумму матрицы А с текущим объектом. Этот метод не изменяет значение текущей матрицы. Операторы ввода/вывода ReadMat и WriteMat работают со всеми элементами матрицы п х п. Сам метод ReadMat сохраняет только верхне-треугольные элементы матрицы.
ПРИМЕР
^include trimat.h	// включить класс TriMat
TriMat А(10), В(10), С(10);	// треугольные матрицы 10x10
д,ReadMat()?	// ввести матрицы А и В
В.ReadMat ();
с = A.AddMat(В);	// ВЫЧИСЛИТЬ С = А + В
С.WriteMat ():	// печатать С
Реализация класса TriMat
Конструктор инициализирует закрытый член п параметром matsize. Таким образом задается число строк и столбцов матрицы. Этот же параметр используется для инициализации массива rowTable, который используется для доступа к элементам матрицы. Если matsize превышает ROWLIMIT, выдается сообщение об ошибке и выполнение программы прерывается.
// инициализация и и rowTable
TriHat::TriMat(int matsize) (
int storedElements = 0;
// прервать программу, если matsize больше ROWLIMIT
if (matsize > ROWLIMIT) (
cerr « "Превышен размер матрицы" « rowlimit «
« "x" « ROWLIMIT « endl; exit(l);
)
n e matsize;
U задать таблицу
for(int i = 0; i < n; i++)
rowTable(i] = storedElements;
storedElements + n — i;
^®*атричные методы доступа. Ключевым моментом при работе с треуголь-адГ4® патрицами является возможность эффективного хранения ненулевых ^ментов в линейном массиве. Чтобы достичь такой эффективности и все ®сл°льзовать обычные двумерные индексы i и j для доступа к элементу й Еам необходимы функции PutElement и GetElement для сохранения Оавращения элементов матрицы в массиве.
_-l£ од jre<imension предоставляет клиенту доступ к размеру матрицы, информация может использоваться для обеспечения того, чтобы методам ступа передавались параметры, соответствующие правильной строке и стол€
// возвратить размерность матрицы п int TriMat: :GetDirnension(voj.d) const {
return n;
}
Метод PutElement проверяет индексы i и j. Если j > i, мы сохраняем зна ние данных в М, используя функцию доступа к матрице для треугольн матриц: Если i или j не находится в диапазоне 0 . . (п-1), то программа зак. чивается:
// записать элемент матрицы [i, j] в массив м
void TriMat::PutElement (double item, int i, int j) {
// прервать программу, если индексы элемента вне индексного диапазона
if ((i < О || i >= n) II (j < О [I j >=* п)) {
cerr « "PutElement: индекс вне диапазона 0 — ”
« п-1 « endl;
exit (1);
} \
// все элементы ниже диагонали игнорируются	\
if (j >= i)	4
М[rowTable[i] -* j-i] - item; }
Для получения любого элемента метод GetElement проверяет индексы I и Если i или j не находится в диапазоне О . . (п — 1), программа заканчиваете} Если j<i, то элемент находится в нижней треугольной матрице со значением ( GetElement просто возвращает несохраняемое значение 0. В противном случае j>i, и метод доступа может возвращать элемент из массива М:
// получить матричный элемент [i, j] массива М double TriMat::GetElement(int i, int J) const {
// прервать программу, если индексы вне индексного диапазона
if ((i < О |I i >= п) || (j < 0 I| j >= п)) (
cerr « "GetElement: индекс вне диапазона 0 — "
« п-1 « endl;
exit (1);
}
if (j >= i)
// вернуть элемент, если он выше диагонали return M[rowTable(i] + j-i];
else
// элемент равен 0, если он ниже диагонали return 0;
)
Ввод/вывод матричных объектов. Традиционно, ввод матрицы подразумевает, что данные вводятся построчно с полным набором значений строк, и столбцов. В объекте TriMat нижняя треугольная матрица является нулевой и значения не сохраняются в массиве. Тем не менее, пользователю предлагается ввести эти нулевые значения для сохранения обычного матричного ввода.
// читать элементы матрицы построчно, клиент должен ввести // все (п х п) элементов
void TriMat: :ReadMat (void)
{
double item; inti, j;
for (i“°- i<n; i++)
for (j =*0; j <n: j++>
// сканировать строки
// для каждой строки сканировать столбцы
// читать [i, j ] -й элемент матрицы
cin » item;	// читать [i, j ] -и элемент маэ
PutElement (item, i, j); // сохранить этот элемент
int i/j;
// установка режима выдачи cout.sett(ios::fixed);
cout.precision(3);
cout.setf(ios::showpoint);
for (i “0; i < n; i++)
for (j = 0; j < n; j*+)
cout « setw(7) « GetElement(i,j); cout « endl;
Матричные операции. Класс TriMat имеет методы для вычисления суммы двух матриц и детерминанта матрицы. Метод AddMat принимает единственный параметр, который является правым операндом в сумме. Текущий объект соответствует левому операнду. Например, сумма треугольных матриц X н Y использует метод AddMat для объекта X. Предположим, сумма сохраняется в объекте Z. Для вычисления
Z-Xt-Y
используйте оператор
z =• X.AddMat (Y) ;
Алгоритм сложения двух объектов типа TriMat возвращает новую матрицу В с элементами Bij = CurrentObjectij + Aij:
''' возвращает сумму текущей и матрицы А.
' I текущий объект не изменяется
TriMat TriMat::AddMat (const TriMate A) const
Int l,j;
double itemCurrent, itemA:
TriMat B(A.n);	// в В будет искомая сумма
for (i — 0; i < n- i++)	// цикл по строкам
for (j » i; j < n; ]++)	// пропускать элементы ниже диагонали
itemCurrent  GetElement(i,j);
itemA • A. GetElement (i, j);
B.PutElement (itemCurrent + itemA, i, j);
j c«turn B;
лАетод ictMat возвращает детерминант текущего объекта. Возвращя значение — это действительное число, которое является произведением ментов диагонали. Полный текст кода для реализации класса TriMat м< найти в программном приложении.
Программа 3.5. Операции с классом TriMat
Тестовая программа иллюстрирует класс TriMat с операциями ввода, вода, а также матричного суммирования и определения детермина Каждая секция программы снабжена комментариями.
#include «iostream.h>
iinclude <iomanip.h>
ttinclude "trimat.h"	// включить класс TriMat
void main(void) {
int n;
// задать размер однородной матрицы
cout « "Каков размер матрицы? ";
cin » п;
// оОьявить три матрицы размером (п х п) TriMat А(п), В(п), С(п);
If читать матрицы А и В
cout « "Введите некоторую ’* « п « *’ х *’ « п « " треугольную марину" « endl;
A.ReadMatO;
cout « endl;
cout « "Введите некоторую " « n « " x " « n « " треугольную марину" « endl;
В.ReadMat();
cout « endl;
// выполнить операции и напечать результат
cout « "Сумма А + В" « endl;
С - A.AddMat(В);
С.WriteMat О;
cout « endl;
cout « "детерминант а+в= " « c.DetMat() « endl;
/*
«Выполнение программы 3.5>
каков размер матрицы? 4
Введите некоторую 4x4 треугольную марицу 12-45 0 2 4 1 0 0 3 7 0 0 0 5
Введите некоторую 4x4 треугольную матрицу
1 4 б 7 02 612 о 0 з 1 о О о 2 сумма А + В
2.000 6.000 0,000 4.000 0.000 0.000 0.000 0.000
детерминант А+В=
2.000 12.000
10.000 13.000
6.000 8.000
0.000 7.000
336.000
Письменные упражнения
3.1	Разработайте ADT Coins для набора из в монет. Данные включают количество монет, общее количество лицевых сторон в последнем бросании и список значений монет в последнем бросании. Операции должны включать инициализацию, бросание монет, возвращение общего количества выпадений лицевых сторон и печать значений в последнем бросании.
3.2
(а)	Разработайте ADT для коробки. Включите в этот ADT инициализацию и операции, которые возвращают длины сторон и вычисляют площадь и объем.
(б)	Напишите класс Box, реализующий этот ADT.
(в)	Обхват коробки — это периметр прямоугольника, образованного двумя сторонами. Коробка имеет три возможных значений обхвата. Почтовая длина определяется обхватом плюс расстояние третьей стороны. Упаковка пригодна для пересылки по почте, если какая-либо из ее длин меньше 100. Напишите фрагмент кода для определения, пригоден ли объект В для пересылки по почте.
3.3	Определите все ошибки синтаксиса в определениях класса:
(а)	class X
(
private int t;
private int q;
public
int X(int a, int b);
(
t = a; q =* b;
j	void printx(void);
(6)	class ¥
{
private:
int p;
int q;
public
Y (int n, int m) : n(p) q(m);
{
5 ^425
(а)	Объявите спецификацию для класса X, который имеет следующее: Закрытые члены: Целые переменные а, Ь, с.
Открытые члены: Конструктор, который присваивает значения пе менным а, Ь, с; значения по умолчанию будут равны 1. Функцию возвращающую максимум переменных а, Ъ, с.
(б)	Напишите конструктор для класса X пункта (а).
(в)	Напишите открытую функцию F, поместив ее определение вне клас
3.5	Предположим следующее объявление:
class Student { private: int studentid; intgradepts, units; float gpa;
float ComputeGPA(void):
public;
Student(int studid; int studgradepts, int studunits);
void ReadGradelnfo(void);
void PrintGradelnfo(void);
void UpdateGradelnfofint newunits, int newgradepts);
J;
Этот класс ведет запись отметок для студента. Переменные gradepts units используются методом ComputeGPA для присваивания средне успеваемости студента переменной gpa. Используйте формулу:
gpa = gradepts/units
Напишите код для этой функции-члена. Конструктор и ComputeGP. должны быть выполнены как код in-line.
3.6	ADT Calendar содержит элементы данных year и логическое значени leapyr. Его операции следующие:
Конструктор NumDays(mm. dd)
Leapyear(void)
PrintDate(ndays)
Инициализирует данные-члены year и leapyr. Возвращение количества дней с самого начала года до заданного месяца mm и дня dd. Указывает, является ли год високосным. Печатает дату ndays в year в формате mm/dd/yy.
(а)	Напишите ADT формально
(б)	Реализуйте ADT Calendar как класс.
3.7	Разработайте объявление для класса, который имеет следующие дан-ные-члены. Объявите операции, соответствующие объекту этого типа.
(а)	Имя студента, профилирующая дисциплина, предлагаемый год окончания учебы, средняя успеваемость.
(б)	Штат, столица, население, площадь, губернатор
(в)	Цилиндр. Сделайте возможными изменения радиуса и высоты и включите вычисление площади поверхности и объема.
3.8	Следующий код является объявлением для класса, представляющего колоду карт:
class CardDeck {
private:
//колода карт реализуется как массив
//целых от 0 до 51.
int cards[52];
int currentcard;
public:
//конструктор, тасование колоды карт
CardDeck(void);
//тасование колоды карт
void Shuffle(void);
//возвращать следующую карту в колоде. СОВЕТ: вы
//должны установить текущее местоположение в колоде.
int GetCard(void);
//трефы 0-12., бубны 13-25, черви 26-38,
//пики 39-51. В каждом диапазоне первая карта — это
//туз, а последние три карты — это валет, дама, король, //запишите карту с как масть, значение карты void PrintCardfint с);
};
(а)	Реализуйте эти методы. СОВЕТ: Для тасования карт используйте цикл со сканированием 52-х карт. Для карты i выберите случайное число в диапазоне от i до 51 и поменяйте местами карту с этим случайным индексом и карту с индексом L
(б)	Запишите функцию
void DealHand(CardDeck& d, int n);
которая сдает n карт из d, сортирует их и печатает их значения.
3.9	Используя класс Temperature из раздела 3.2, напишите функцию:
Temperature Average(temperature а[ ], int n);
которая возвращает объект типа Temperature, содержащий среднюю низкую и высокую температуры п показаний.
3.10	Используйте класс Date из раздела 3.5.
(а)	Измените класс Date для включения метода IncrementDate. Он принимает положительное число дней в диапазоне 0-365, добавляет его к текущей дате и возвращает объект, имеющий новую дату.
(б)	Сделайте так, чтобы параметр для IncrementDate мог принимать отрицательные значения.
®*11 Покажите использование класса случайных чисел для моделирования следующего:
(а	) Одна пятая часть автомобилей штата не соответствует стандартам по вредным эмиссиям. Используйте fRandom, чтобы определить, отвечает Ли этим стандартам случайно выбранная автомашина.
6)	Вес особи в популяции .варьируется в пределах 140-230 фунтов. Используйте Random для выбора веса какого-либо человека в этой популяции.
•12 Рассмотрите класс Event, которому передано начальное значение для Нижнего и верхнего предела времени какого-либо события. Границы принимают значение по умолчанию 0 и 1. Операции:
„	c. ..^	, иализировать границы данных. Если нижняк
граница выходит за верхнюю границу, печатать сообщение об ошибке и выходить из программы.
GetEvent Получать случайное событие в диапазоне: нижняя граница — верхняя граница.
(а)	Реализуйте этот класс, используя код in-line.
(б)	Реализуйте этот класс, определяя функции-члены вне объявления к-
(в)	Приложение требует массив из пяти объектов Event, где каждый мент принимает значение в диапазоне от 10 до 20. Как вы инищ зируете массив? Проще ли решается данная задача, если диапазок каждого объекта будет 0-1?
3.13	Напишите функцию Dateinterval, которая принимает два объекта j са Calendar из письменного упражнения 3.6 и возвращает число между двумя этими датами.
3.14	* Матрицы могут использоваться для решения систем уравнений неизвестными. Мы показываем алгоритм для системы с тремя е вестными.
В этой системе уравнений элементы Aij являются коэффициент неизвестных Хо, Xi и Хг-
В правой части уравнений даются элементы Q.
^0,0 Хв +	* Хг + Aq,2 Х2 = С0
^1,0 *о +	* Xj + х2 = Cj
+ ^2Г1 *	^2# 2 ^2 ~ ^2
где матрица называется матрицей коэффициентов (coefficient matri Например, система уравнений
1Х0	+ lXi + ох2 - 4
-ЗХ0 - 1X1 + 1*2 я -11
2Х0 Е 2Xi + 2Х2 = 14
Теорема математики утверждает, что эти уравнения могут быть сведен к системе эквивалентных уравнений, в которой матрица коэффициенте является треугольной. В нашем примере:
1.	Исключите элемент Аю — -3.
Умножьте элементы в строке 0 на константу 3 и сложите элементы строки 0 с элементами строки 1.
2.	Исключите элемент Аго = 2.
Умножьте элементы строки 0 на константу -2 и сложите элементы строки 0 с. элементами строки 2. В этом процессе исключается также член А21.
Матричное уравнение для новой системы имеет треугольную матрицу коэффициентов.
(а)	Сведите алгебраическую систему к уравнению, включающему треугольную матрицу коэффициентов
(6)	Найдите детерминант этой матрицы коэффициентов.
Упражнения по программированию
3.1 Многие программные приложения используют постоянно обновляемый сумматор (accumulator). В качестве простого примера абстрактного типа данных предположим, что Accumulator — это тип данных, которые обновляются операцией сложения и выводятся с использованием операции печати.
Accumulator
Данные
Действительное значение для суммирования
Операции
Initialize
Вход:	Действительное значение N.
Предусловия: Нет
Процесс:	Присваивание N в качестве значения суммы.
Выход:	нет
Постусловия: Сумма инициализируется.
Add
Вход:
Действительное число N.
Процесс:	Сложение N с суммой -
Выход:	Нет
Постусловия:	Сумма обновляется.
Print
Вход:	Нет
Предусловия:	Нет
Процесс:	Чтение	суммы.
Выход:	Печать	суммы.
Постусловия:	Нет
Конец ADT Accumulator
Банковское приложение считывает начальный баланс и последоват ность операций. Отрицательная операция определяет дебет, а поло тельная — кредит. Используются три объекта типа Accumulator. Обт Balance определяется со стартовым балансом в качестве параметра ь структора. Объекты Debits и Credits имеют начальное значение i используются для поддержки определения текущей суммы дебетны кредитных операций. Оператор Add обновляет сумму в объектах.
Считайте последовательность операций, заканчивающихся на операг 0.00. Печатайте окончательные значения баланса, дебетов и кредит 3.2 Напишите main-функцию, которая использует класс, реализованный письменном упражнении 3.5 со следующими данными:	\
Студент Id	Grade Points	Units
1047	120	40
3050	75	20
0020	100	75
(а) Печатайте информацию по каждому студенту.
(б) Последний студент (ID 0020) имеет дополнительные записи из летж школы. Обновите запись со следующими новыми данными: успева мость 40 при 10 часах. Печатайте новые данные для этого студента
3.3 Расширьте класс Circle из раздела 1.3 для вычисления площади сектор! Площадь сектора определяется по формуле (О/ЗбО^лг2-
Используйте этот класс для решения следующей задачи:
Круглая игровая площадка определяется как объект с радиусом 100 футов. Программа определяет стоимость ограждения этой площадки. Стоимость ограждения $2,40 /фут. Площадь поверхности площадки в
основном травяная. Один сектор, измеряемый углом в 30°, не является лужайкой. Программа определяет стоимость лужайки для катания: $4,00 за полосу 2 х 8(16 кв.фута).
Fencing
Cost - Circumference * 2,40
Lawn
Lawn_Area =» Area - Sector Area
Number_Rolls = Lawn_Area/16
Cost = NumberRolls* 4,00.
34 Напишите класс, содержащий индикатор пола (М или F), возраст и ID-номер в диапазоне от 0 до 100. Операции включают Read, Print и функции Getld/GetAge/GetGender, которые возвращают ID, возраст и пол человека, сохраняемые в объекте.
Напишите программное приложение, определяющее объекты Young-Women и OldMen. Программа вводит информацию о ряде людей и присваивает данные о самых молодых женщинах в YoungWoinen и самых старых мужчинах в OldMen. Ввод завершается ID-номером 0. Используя Print, выполните вывод данных из этих объектов.
3.5 Реализуйте класс Geometry, закрытые данные которого содержат два (2) элемента данных VI и V2 типа double и переменную figuretype типа Figure.
Этот класс должен содержать два конструктора, которые принимают 1 или 2 параметра, соответственно, метод Border, возвращающий периметр объекта, метод Area, возвращающий площадь и метод Diagonal, вычисляющий диагональ.
Enum Figure (Circle, Rectangle)
class Geometry
{
private:
double VI, V2;
Figure figuretype;
public:
Goemetry(double radius);	// для окружности
Geometry(double 1, double w);	// для прямоугольника
double Border(void) const;
double Area(void) const;
double Diagonal(void) const;
(a) Реализуйте класс Geometry, используя внешние функции. Первый конструктор для окружности будет иметь один параметр и следовательно присваивать объект типа Circle переменной figuretype. Другой конструктор, будет присваивать этой переменной объект типа Rectangle. Вычисляющие методы должны проверять тип figuretype перед вычислением возвращаемого значения.
' J Пользователь вводит внутренний радиус, который затем используется Для создания маленькой окружности. Используйте эту информацию Для объявления описывающего окружность прямоугольника и внешней °кРУЖности, описывающей этот прямоугольник. Печатайте периметр, йлощадь и диагональ каждого объекта.
Вычислите площадь внешней полосы между двумя окружностями (ш. щадь вне маленькой окружности, но внутри большой окружности). Вычислите периметр маленькой области, помеченной символом “X"
3.6 Класс Ref подсчитывает количество положительных (>0) и отрицател ных (<0) чисел, ’“представленных на рассмотрение’’. Конструктор i имеет параметров и инициализирует элементы данных positiveCount negativeCount нулевыми значениями.
class Ref {
private;
int positivecount;
int negativecount;
public:
Ref(void);
void Count(int x);
void Write(void) const;
);
Объект типа Ref передается функции, которая использует его для за писи количества положительных и отрицательных чисел в последова тельность ввода из пяти целых чисел. Две версии функции иллюстри руют различие между передачей объектов по значению н по ссылке В первой версии объект передается по значению.
void PassByValue(Ref V) (
int num;
for(int i  0; i<5; i++)
{
cin > num;
if (num! - 0) v.Count (num);
)
}
Вторая версия функции передает параметр по ссылке.
void PassByReference (Ref & V) (
int num;
реализуйте Ref и создайте main-программу, которая вызывает каждую функцию и использует метод Write для печати результатов. В каждом экземпляре данными являются 1, 2, 3, -1 и -7. Объясните, почему PassByValue работает неправильно, a PassByReference выполняется успешно.
g7 рассмотрим следующее объявление класса:
class Grade
(
private:
char name(30);
float score;
public:
Grade(char student[], float score);
Grade(void);
int Compare(char s [ ]);
void Read(void);
Write(void);
I;
Напишите функцию main по частям от (а) до (e):
(а)	Напишите реализацию для функций-членов. Используйте строковую функцию strcpy для присваивания имени в первом конструкторе. Заметьте, что второй конструктор является конструктором умолчания. Он устанавливает переменную name в NULL-строку, a score — в 0.0. Метод Compare возвращает 1, если s равна имени, в противном случае возвращается 0. Используйте функцию strcmp.
(б)	В функции main объявите массив Students из пяти объектов с начальными значениями:
{Grade("John", 78.3), Grade("Sally", 86.5),
Grade ("Bob", 58.9), Grade ("Donna", 98.3)};
(в)	Пятый объект, Students[4], вводится с использованием функции-члена Read().
(г)	Запишите функцию
int Search(Grade Arr)[ ], int n, char keyname[ ];
которая ищет массив Arr из n элементов и возвращает индекс объекта, имя которого соответствует ключу. Если ключ не найден в Arr, возвращается -1.
(Д) Вызывайте Search три раза с разными именами для проверки правильности ее работы.
Используйте класс CardDeck, разработанный в письменном упражнении для карточной игры под названием Hi-Low. Сдайте пять карт. Для каждой карты спросите игрока, будет ли случайно вытащенная из оставшихся карт в колоде карта больше или меньше данной карты. Туз — самая большая карта любой масти, и масти упорядочены от трефовой
3 g г,Ик°вой. Печатайте количество удачных догадок.
^•Данном упражнении используйте класс Calendar, разработанный в “^сьменном упражнении 3.6. Запишите функцию
ht Dayinterval(Calendar С, int mml, intddl, intmm2, int dd2);
возвращающую количество дней между двумя данными. Наша функцию main, выполняющую следующее:
1.	Печатает, является ли текущий год високосным.
2.	Использует NumDays для определения количества дней от нач; года до Рождества.
3.	Передает результат (2) функции PrintDate и проверяет правильн печати даты.
4.	Включает вычисление количества дней от сегодняшнего дня до Рождества.
5.	Вычисляет количество дней между 1 февраля и 1 марта.
3.10	Расширьте класс Dice из главы 1 до бросания п костей, n<20. Е количество костей не дается конструктору, он устанавливается на : чение по умолчанию 2. Используйте класс Dice для решения следую! задачи:
Объявите массив из 30 объектов Dice. Инициализируйте каждый s мент для бросания пяти (5) костей. Вам понадобиться использов конструктор умолчания в объявлении, а затем — никл для инициа зации каждого элемента пяти костей.
Выполните Toss для каждого элемента.
Сканируйте список и определите, сколько раз была брошена 5 или Укажите, сколько раз сумма повторялась в следующем бросании. I пример, 8 8 8 считается как два повтора.
Сканируйте список и найдите самую большую сумму, отображая с* роны со значениями кости.
Сортируйте список, считая и печатая суммы.
3.11	Объявите перечисление:
enum unit (metric, English};
Класс Height содержит следующие закрытые данные-члены:
char name[20];
unit measureType;
float h; //высота в единицах measureType (футы или см).
Операции включают:
//конструктор:имя параметров, высота, тип измерения Height(char nm[ ], float ht, unit m);
* PrintHeight(void); //печать высоты в соответствующих единицах //ввод имени, типа измерения и высоты ReadHeight(void) ;
float GetHeight(void); //возвращение высоты
void Convert(unit m); //преобразование h в измерение га
(а)	Реализуйте этот класс. Один дюйм равен 2,54 см.
(б)	Напишите функцию, сканирующую список и преобразующую элементы в единицы измерения, заданные как параметр. Подразумевается, что объекты выражены в других единицах измерения.
(в)	Напишите функцию, которая сортирует массив объектов Height. Подразумевается, что каждый объект использует одну и ту же единицу измерения.
/г\ Напишите функцию, сканирующую массив и возвращающую объект, ' ' представляющий самого высокого человека. Считайте, что все объекты используют одну и ту же единицу измерения.
(„у Напишите программу для тестирования класса, создав список из пяти элементов типа Height. Инициализируйте первые три из них в объявлении и считайте последние два. Используйте функции, разработанные в частях (Ь), (с) и (d).
312 В банке с одним только кассиром заметили, что клиентские операции занимают интервал времени 5-10 минут. Очередь из 10 клиентов образовалась в момент открытия. Используйте класс Event, разработанный в письменном упражнении 3.12 для вычисления времени, необходимого кассиру, чтобы обслужить 10 клиентов.
3.13 Эллипс или овал определяется описывающим прямоугольником, размеры которого 2а х 2Ь.
Константы а и Ь называются полуосями эллипса. Эллипс, полуоси которого имеют одну и ту же длину, являетя окружностью. Математическое уравнение эллипса имеет вид:
(к - х0)2/а2 + (у - у0)2/ь2 - 1
3.U
и его площадь равна nab. Разработайте класс Ellipse, функции-члены которого состоят из конструктора и метода Area. Используйте Ellips и класс Rectangle для решения следующей задачи:
Необходимо построить овальный плавательный бассейн, полуоси которого являются длинами 30 и 40, внутри прямоугольной площади 80 х 60. Стоимость бассейна $25 000. Площадь снаружи бассейна необходимо зацементировать. Стоимость цемента составляет $50/кв.фут. Вычислите общую стоимость строительства.
Данные о бейсболисте включают номер игрока (number), количество Рьз» когда он отбивающий (times at bat), количество ударов по мячу (hits) и средний результат (NumberHits/ NumberAtBats). Эта информация сохраняется как данные-члены в закрытой секции класса Baseball, параметры конструктора имеют значения по умолчанию: номер У^Формы устанавливается равным -1, а значения числа отбиваний и Ударов — 0. Применение для Номера униформы значения по умолчанию Подразумевает, что номер униформы игрока, количество ударов и ко-ЛИчество раз, когда игрок является отбивающим, считываются с ис
пользованием ReadPlayer. Для известного номера униформы функ! член ReadPlayer вводит количество ударов и количество отбиваз Метод GetBatAve возвращает средний результат отбиваний. Закры метод ComputeBatAve используется как утилита конструктором, a R Player — для задания данного-члена, содержащего средний резулз отбиваний. Метод WritePIayer выводит всю информацию об игроь формате:
Flayer <UniformNo> Average < BattingAvg>
Средний результат выводится как целое число из трех цифр. Наприк если количество ударов 30, а количество раз, когда игрок был отбив щим, равно 100, то средний результат выводится методом WritePla как 300.
Объявление класса Baseball
class Baseball
{
private:
int playerno;
int atbats;
int hits;
float batave;
//ComputeBatAve дается c inline-кодом.
float ComputeBatAve (void) const //закрытый метод <
if(playerno == -1 atbats === 0)
return(0);
else
returneffloat(hits)/atbats);
<
public:
Baseball (int n = -1, int ab = 0, int h = 0);
void ReadPlayer(void);
void WritePIayer(void) const;
float GetBatAve(void) const;
Реализуйте класс Baseball и используйте его в функции main следую щим образом:
1.	Объявите четыре объекта:
Catcher Shortstop Centerfielder Maxobject
Номер униформы 10, 100 отбиваний, 10 ударов
Имеется только номер униформы 44
Нет никакой информации
Нет никакой информации
2.	Считайте необходимую информацию для объектов shortstop и centerfielder.
3.	Выпишите всю информацию для объектов catcher, shortstop и centerfielder.
4.	Используя операцию GetBatAve и присваивание объекта, присвойте игрока с самым высоким средним результатом объекту maxobject и распечатайте информацию.
3.15
Добавьте вычитание и умножение треугольной матрицы к классу TriMat и протестируйте их в программе, подобной программе 3.5.
416 При решении общей и х п системы алгебраических уравнений ряд *** операций сводит задачу к решению уравнения треугольной матрицы.
Уравнение треугольной матрицы имеет единственное решение, при условии, что детерминант матрицы коэффициентов является ненулевым. Набор алгебраических уравнений получают умножением каждой строки в матрице коэффициентов на столбцовый массив неизвестных. Решая уравнения в порядке от и — 1 до 0, мы получаем единственное решение для переменных Xn-i. Хп-2, Xi.Xo. Например, треугольная система уравнений, описанная в письменном упражнении 3.14, решается применением этого метода:
Уравнение 0:
Уравнение 1:
Уравнение 2:
Решение для Х2
Решение для Хх:
1Х0 + 1X1 + 0Х2 = 4
2X1 + 1Х2 =1
2Х2 = 6
В уравнении 2 х2 = б/2 - 3
В уравнении 1 подставьте 3 для Х2; решите уравнение для неизвестного X],.
2Xj + з = 1 xi = -i
Решение для Хо: В уравнении 0 подставьте -1 для X! и 3 для Х2;
решите уравнение для неизвестного Хо.
х0 - 1 = 4
х0 “ 5
Окончательное решение: Хо =5, Xi =1, Х2 = 3. Объедините эти идеи и разработайте функцию
void SolveEqn(const TriMatb A, double X[ ], double C[ ]};
Она определяет единственное решение, если оно существует, общего Уравнения треугольной матрицы
^а) Используйте SolveEqn в программе для решения примера системы уравнений.
1	1	0		х0		4
0	2	1	*	Xt		1
0	0	2		Х2		6
(б) Решите систему уравнений в письменном упражнении 3.14 (а).
Классы коллекций
V
4.1.	Линейные коллекции
4.2.	Нелинейные коллекции
4.3.	Анализ алгоритмов
4.4.	Последовательный и бинарный поиск
4.5.	Базовый класс последовательного списка
Письменные упражнения
Упражнения по программированию
». . ллг описываются базовые типы данных, которые непосредств* поддерживаются языком программирования и включают примитивные левые и символьные данные, а также массивы, строки и записи. Эти CTd турированные типы данных являются примерами коллекций (collectic которые сохраняют данные и предоставляют операции доступа, добавляют] удаляющие или обновляющие элементы данных. Изучению типов коллек уделяется основное внимание в данной книге.
Коллекции подразделяются на две основные категории: линейные и нс нейные. На рис. 4.1 приводятся методы доступа к данным для дальиейпз деления категорий и перечисления структур данных, представленных в э-книге. В данной главе приводится краткий обзор каждой коллекции вмесэ описанием ее данных, операций и некоторых случаев практического испо зования.
Линейная (linear) коллекция содержит список элементов, упорядочены по положению (рис.4.2). В этом списке имеется первый элемент, второй т.д. Массив с индексом, отражающим порядок элементов, является основш примером линейной коллекции.
Нелинейная (nonlinear) коллекция определяет элементы без позиционно упорядочения. Например, цепочка управления рабочими на заводе или ко плект мячей в сетке — это нелинейные коллекции (рис. 4.3).
Эта глава включает также исследование эффективности алгоритмов. М описываем факторы, определяющие эффективность и вводим нотацию Big (большая О) в качестве ее критерия. Этот критерий используется на прот. жение всей книги для сравнения и сопоставления различных алгоритмов.
Класс SeqList из главы 1 является основным типом коллекций. В данно главе описывается реализация этого класса на базе массива. Этот класс ра< сматривается также в главе 9, когда мы определяем реализацию связанног списка. В главе 12 SeqList используется с наследованием для создания упора доченного списка.
Когда C++ реализует коллекции как классы, компилятор требует пара метры функции, чтобы иметь специфические типы данных, и выполняв', тщательную проверку типа на предмет совместимости. Для наиболее обще! реализации типов коллекций мы вводим классы шаблонов (template) C++ ь
Рис. 4.1. Иерархия коллекций
«е 7. Классы шаблонов пишутся с использованием параметризованного Яй» такого как Т для типа данных, управляемых коллекцией. Когда 0£*ейВляется какой-либо объект, фактический тип для Т задается как пара-О&Ь Шаблоны являются мощным инструментом C++, позволяющим выпол-1,1 £ параметризованное объявление классов. Например, предположим, что 0 асе коллекции имеет массив из 10 элементов.
Первый элемент Второй элемент Третий элемент Последний элемент
Рис. 4.2. Линейная коллекция
Рис. 4.3. Нелинейные коллекции
Первое объявление определяет массив целых. Версия шаблонов не предполагает определенного типа, а позволяет классу использовать параметризо-типа элемента массива. Фактический тип указывается во объекта.
Ван3ое имя Т для вРемя объявления
Ов'**»лание 1 ®lass Collection
int А[ Ю];
I
Election object;
?&Ьч*лениа 2
^SaCol^laSS T>
( Election
//массив целых является данным-членом
//А — это массив целых
Т А {10]; //параметризованное объявление массива //залает Т при объявлении объекта
}
Collection<int> object;	//А — это массив целых
Collection*char> object; //А - это массив символов
1
4.1.	Описание линейных коллекций
I
Метод доступа для элементов различает линейные коллекции, показанный на рис. 4.1. С помощью прямого доступа (direct access) мы можем выбирать' элемент непосредственно, не обращаясь сначала к предшествующим элементам в списке. Например, символы в строке могут быть доступны непосредственно. Третья буква в слове LIMEAR употреблена ошибочно. Первые две буквы написаны правильно. Мы можем исправить третью букву, не обращаясь сначала к первым двум буквам. В некоторых линейных коллекциях, называемых последовательными списками (sequential lists), прямой доступ невозможен. Вы обращаетесь к элементу, начиная с начала списка и двигаясь по списку до нужного элемента. Например, в бейсболе отбивающий благополучно достигает третьего пункта (base) только после первого и второго.
Пример парковочного гаража может служить для сравнения списков с возможным прямым доступом и последовательных списков. Следующая диаграмма описывает гараж, в котором рядом с машинами имеется свободный проход. Служащий может выводить машину 3 из гаража, садясь непосредственно в нее и используя свободный проход.
Следующая диаграмма иллюстрирует гараж с последовательной парковкой, в котором все машины паркуются в один ряд. Служащий имеет только последовательный доступ к машине. Чтобы вывести машину 3, он должен переместить машины 0 — 3 в таком порядке:
#0	#1	#2	#3	»4
Последовательный доступ к машине 3

Коллекции с прямым доступом
ДОассив (array) — это коллекция элементов, имеющих один и тот же тип с прямым доступом посредством целого индекса.
Коллекция Array
данные
Коллекция объектов одного и того же (однородного) типа.
Операции
Данные в каждом местоположении в массиве доступны непосредственно с помощью целого индекса.
Статический массив (static array) содержит фиксированное количество элементов и задается в памяти во время компиляции. Динамический массив (dynamic array) создается с использованием методов динамического распределения памяти и его размер может быть изменен.
Массив — это структура данных, которая может использоваться для хранения списка. В случае с последовательным списком массив позволяет выполнять эффективное добавление элементов в конец списка. Эта структура менее эффективна при удалении элемента, поскольку мы должны часто сдвигать элементы. Такой же сдвиг происходит, когда новые элементы вставляются в массив, хранящий упорядоченный список.
Список 115120130135140]	Список	115 120130135140]
_	[25] _
Вставить 251151201	|30| 3514р]	___ _______
Удалить 20 Пэ | [30135 [40]
[201
Глава 8 знакомит с классом Array, расширяющим, концепцию простого Массива. Этот класс предоставляет новый индексный оператор, который ПеРед сохранением или возвращением данных проверяет, находится ли соответствующий этим данным индекс в допустимом диапазоне. Класс, ре-а^Изующий такие безопасные массивы (safe arrays)t позволяет клиенту ^^^ВДчееки распределять массив во время исполнения приложения.
Символьная строка (character string) — это массив символов с ассоци-"фовавлыми операциями, которые определяют длину строки, склеивают 8^КаТениГГк>т) две строки, удаляют подстроку и так далее. Общий класс 511£> имеющий расширенный набор строковых операций, разработан в хяаве 8.
string
1Еанные
Коллекция символов с известной длиной
•; -z-.ерации
Имеются операции для определения длины строки, копирования одной строки в другую или их конкатенации, сравнения двух строк, выполнения сопоставления с образцом, ввода и вывода из строк.
Запись (record) — это базовая структура коллекций для сохранения дан. ных, которые могут состоять из разных типов. Для многих приложение различные элементы данных ассоциированы с одним объектом. Например; авиабилет включает такие данные, как номер рейса, номер места, имя пас^ сажира, стоимость, данные об агенте и так далее. Единственный билетныД объект — это набор полей разных типов. Коллекция записи связывает поде?! при обеспечении прямого доступа к данным в отдельных полях.
Коллекция Record
Данные
Элемент с коллекцией полей, возможно, различных типов.
Операции
Точечный оператор [dot operator) обеспечивает прямой доступ к данным в поле.
Коллекции с последовательным доступом
Более общей коллекцией является список, сохраняющий элементы в последовательном порядке. Структура, называемая линейным списком (linear list), содержит произвольное число элементов. Размер списка изменяется добавлением или удалением элемента из этого списка, а ссылка на элементы в списке выполняется по их положению. Первый элемент находится в голове или в начале списка, последний элемент находится в конце списка. Каждый элемент, за исключением последнего, имеет единственный последующий элемент.
1-й
3-й	4-й ... г-й
передний
последний
Коллекция List
Данные
Произвольная коллекция объектов одного и того же (однородного) типа.
Операции
Для ссылки на отдельные элементы мы должны идти по списку от его начальной точки, проходя от элемента к элементу до достижения нужного местоположения. Вставки и удаления изменяют размер списка.
Коллекция линейного списка может иметь любое количество элементов и подразумевает, что эта коллекция будет расширяться или сужаться по мере добавления новых элементов в список или удаления резидентных элементов-Эта структура списка является ограничивающей, когда необходим доступ к произвольным элементам, так как в ней нет прямого доступа. Для доступа к элементам списка необходимо выполнять прохождение элементов от начальной точки в списке. В зависимости от используемого метода, мы можем перемещаться одним из двух способов: слева направо или в обоих направлениях. В этой главе мы разрабатываем класс, который реализует последовательный список, используя массив. Результирующий список ограничивается размером массива. Более мощная реализация, описанная в главе 9, снимает все ограничения на размер использованием связанных списков и динамических структур.
Список покупок является примером последовательного списка. Покупатель оВоначально создает список, записывая названия товаров. Делая покупки, ой вычеркивает названия из списка, когда товары найдены или больше не ^Ууи^рядочениый линейный список (ordered linear list) — это линейный список данные которого упорядочены относительно друг друга. Например, список ° 3, б, 6, 12, 18, 33
расположен в числовом порядке, а список
F 1, 6, 2, 5, 8
__нет.
Бинарный поиск, описываемый в этой главе, является алгоритмом, использующим упорядоченный список.
Стеки и очереди — это особые версии линейного списка с ограниченным доступом к элементам данных. В стеке (stack) элементы добавляются и удаляются только в один конец списка, называемый вершиной (top). Полка для подносов в столовой — это знакомый пример. Операция удаления элемента из списка называется извлечением из стека (popping the stack). О добавлении элемента в список говорится как о помещении (pushing) элемента в стек.
вершина
I ° 1—1
с
В	Извлечь из
7	стека
При помещении элемента в стек все другие элементы, находящиеся в данный момент в стеке, опускаются вниз, уступая место на вершине новому элементу. Когда элементы удаляются из стека, они перемещаются в обратном порядке. Последний элемент, помещенный в стек, является первым извлекаемым из стека. О таком типе хранения элементов говорят как о магазинном порядке (last -in/first-out (LIFO) — последним пришел/первым ушел).
Коллекция Stack
Данные
Список элементов, которые могут быть доступны только на вершине списка.
Операции
Список поддерживает операции push и pop. Push добавляет новый элемент в вершину списка, и pop удаляет элемент из вершины списка.
Мы вводим стеки в ряд приложений, которые включают оценку выражений, Рекурсию и прохождение дерева. В этих случаях мы просматриваем элементы и затем обращаемся к ним в порядке LIFO. При помощи стека компиляторы Передают параметры функциям, а также используют стек для хранения ло-КаДьных переменных.
Очёредь^диёне) — это список с доступом только в начале и в конце списка. Элементы вставляются в конец списка и удаляются из начала. При Использовании обоих концов списка элементы оставляют очередь в том же Исрядке, в каком они поступают. Хранение элементов соответствует порядку **°ступления (first-in/first out (FIFO) — первым пришел/первым ушел).
I а) В | С | р|<~| Q-вставка | g | последний
Коллекция Queue
Данные
Список элементов с доступом в начале и в конце списка.
Операции
Добавление элемента в конец списка и удаление элемента из начала списка.
Очередь является полезной коллекцией для ведения списков очередников. Моделью очереди является очередь обслуживания в банке или обслу. живание покупателей в продовольственном отделе. Очереди находят машинное применение в моделирующих исследованиях и осуществляют планирование заданий в рамках операционной системы.
Для некоторых приложений мы изменяем структуру очереди, устанавливая очередность элементов. При удалении объекта из списка определяется элемент с наивысшим приоритетом. Эта коллекция, называемая очередью приоритетов (priority queue), имеет операции insert (вставить) и delete (удалить). Где вставляются данные, является несущественным. Важным является то, что операция delete выбирает элемент с наивысшим приоритетом. В больничном отделении скорой помощи используется очередь приоритетов. Пациенты обслуживаются в порядке поступления, если только их состояние не является угрожающим для жизни, что дает им наивысший приоритет н первоочередной доступ к экстренной медицинской Ьомощи.
Коллекция Queue Priority
Данные
Список элементов, такой, что каждый элемент имеет приоритет.
Операции
Добавление элемента в список. При удалении элемента извлекается элемент с наивысшим приоритетом.
Очереди приоритетов используются для планирования заданий в рамках операционной системы. Задания с наивысшим приоритетом должны выполняться в первую очередь. Очереди приоритетов используются также в моделировании, управляемом прерываниями (event-driven simulation). Например, в практическом приложении в главе 5 выполняется моделирование потока клиентов в банк и из банка. Каждый тип события ( появление или уход) вставляется в очередь приоритетов. Самое раннее по времени событие удаляется и обслуживается первым.
В машинной системе файл (file) — это внешняя коллекция, которая имеет ассоциированную структуру данных, называемую потоком (stream). Мы приравниваем file к его stream и сосредоточиваем внимание на потоке данных. Прямой доступ осуществляется только к дисковому файлу, ленточные же файлы являются последовательными. Операция read удаляет данные из потока ввода, а операция write добавляет новые данные в конец потока вывода. Файл часто используется для хранения большого количества данных. Например, во время компиляции программы генерируются большие таблицы и часто сохраняются во временных файлах.
Коллекция File
данные
Последовательность байтов, ассоциированная с внешним устройством, данные перемещаются посредством потока к устройству и иэ него.
операции
Открытие файла, считывание данных из файла, запись данных в файл, поиск указанного адреса в файле (прямой доступ), закрытие файла.
Универсальная индексация
Массив — это классическая коллекция, позволяющая иметь прямой доступ к каЖД°мУ элементу, посредством целого индекса. Для многих приложений и связываем с записью данных некоторый ключ, использующийся для доступа к записи. Когда вы звоните в банк или в страховую компанию для получения информации, вы даете ваш номер банковского счета, который становится ключом для нахождения записи этого счета. Коллекция, называемая хеш-таблица (hash table), сохраняет данные, связанные с ключом. Ключ трансформируется в целый индекс, используемый для нахождения данных. В одном часто используемом методе хеш-таблиц целое значение — это индекс в массиве коллекций. После преобразования ключа в индекс выполняется поиск ассоциированной коллекции. Ключ не обязательно должен быть целым числом. Например, запись данных может состоять из имени, классификации работы, количества лет работы в компании, жалования и так далее.
В этом случае строка, указывающая имя, является ключом.
Обычный словарь — это коллекция слов и их определений. Вы ищете слово, используя его как ключ. В структурах данных, коллекция, называемая словарем (dictionary), состоит из набора пар ключ-значение, называемых ассоциациями (associations).
Ключ	Значение
Ассоциация
ЩадаЛРИМеР. ключом может быть слово, а значением — строка, указываю-доСТ15П^е^еление слова- К значению в ассоциации осуществляется прямой bOflofi С Использ°ванием ключа в качестве индекса. В результате, словарь ЗДаче Я МассивУ» за исключением того, что индексы не должны быть целыми Оцред Иями- Например, если Diet является коллекцией dictionary, ищите соцца^®Ние слова dog, ссылаясь на Dict[dog]. Словари часто называют ас-массивами (associative arrays), потому что они связывают ^^РУют) общий индекс со значением данных.
count « Dict[dog] « endl;
4.2.	Описание нелинейных коллекций
На рис. 4.1 показано, что нелинейные коллекции разделяются на иерар-хические и групповые структуры. Иерархическая коллекция (hierarchical collection) — это масса элементов, которые разделяются по уровням. Элементы на данном уровне могут иметь несколько наследников на следующем уровне- Мы вводим особую иерархическую коллекцию, называемую деревом (tree), в которой все элементы данных происходят из одного источника, называемого корнем (root). Элементы в дереве называются узлами (nodes), каждый из которых указывает на нисходящие узлы, называемые детьми (children). Каждый элемент, за исключением корня, имеет единственного предка. Пути вниз по дереву начинаются в корне и развиваются по направлению к нижним уровням от родителя к ребенку.
Дерево является идеальной структурой для описания файловой системы с каталогами и подкаталогами. Модель для дерева — это организационная схема в бизнесе, определяющая цепочку управления, начиная с босса (СЕО, президента), и далее — к вице-президентам, супервайзерам и так далее.
В этой книге мы рассматриваем особую форму дерева, в котором каждый узел имеет самое большее два потомка. Такая структура, бинарное дерево (binary tree), имеет важное применение в оценке арифметических выражений и в теории компиляции. С дополнительным упорядочением дерево становится деревом бинарного поиска (binary search tree), которое эффективно сохраняет большие объемы данных. Деревья бинарного поиска обеспечивают быстрый доступ к элементам, располагая узлы так, что данные можно находить, перемещаясь вниз по короткому пути из корневого узла. На рис. 4.4 показано дерево с 16 узлами. Самый длинный путь от корня к узлу включает четыре ветви. Предположим, что дерево относительно заполнено узлами, отношение
TtOB к Длине ПУТИ значительно улучшается по мере того, как мы увеличи-й размер дерева. Пример: если дерево бинарного поиска имеет 220 — 1 048 575 узлов, которые расположены на минимальном количестве уров-й то элемент данных можно найти, посещая не более, чем 20 узлов. Особое ’ бинарного поиска — это AVL-дерево, гарантирующее равномерное сДРеДеление узлов и обеспечивающее очень короткое время поиска.
Рис. 4.4. Дерево с 16 узлами
Коллекция Tree
Данные
Иерархическая коллекция узлов, происходящих из корня. Каждый узел указывает на узлы-сыновья, которые сами являются корнями поддеревьев,
Операции
Структура дерева позволяет добавлять и удалять узлы. Несмотря на то, что дерево — это нелинейная структура, алгоритмы прохождения деревьев позволяют нам посещать отдельные узлы и осуществлять поиск ключа.
Неар-дерево — это особая версия дерева, в котором самый маленький элемент всегда занимает корневой узел. Операция delete удаляет корневой Узел» и обе операции insert и delete вызывают такую реорганизацию дерева, 470 самый маленький элемент вновь занимает корень такого дерева. Неар-Дэрево использует очень эффективные алгоритмы реорганизации, просматривая только короткие пути от корня вниз к концу дерева. Heap-дерево ^ожет использоваться для упорядочения списка элементов. Вместо исполь-вания медленных алгоритмов сортировки мы упорядочиваем их, повторно тип ЯЯ к°Рнев°й узел из heap-дерева. Это позволяет получить быструю сор-б„?0ВКу (Ьеар'Сортировку). Кроме того, при использовании heap-дерева наи-
ее Часто реализуется очередь приоритетов.
Коллекции групп
представляет те нелинейные коллекции, которые содержат ^ез какого‘ли®° упорядочения. Множество уникальных элементов 4ajQT Т^я примером группы. Операции над коллекцией типа множество вклю-Мй0 ° ЪеДИнение (union) и пересечение (intersection). Другие операции над сТвом тестируют на членство и отношение подмножеств. В главе 8 мы
.m с лсрегрузкой операторов для реализации операций и множествами.
Коллекция Set
Данные
Неупорядоченная коллекция объектов без дубликатов.
Операции
Бинарные операции членства, объединения, пересечения и дифференциации, которые возвращают новое множество. Ряд операторов, тестирующих отношения подмножеств.
Множество (set) — это коллекция, находящая применение, когда данные являются неупорядоченными и каждый элемент данных является единственным в своем роде, уникальным. Например, группа регистрации избирателей составляет банк телефонных номеров для того, чтобы звонить лицам, находящимся в списке. Каждый раз, когда группа контактирует с человеком из банка номеров, его имя помещается в список номеров, по которым позвонили, и удаляется из банка. Конечно, группа людей, которым еще не позвонили, тоже является множеством. Группа регистрации избирателей продолжает звонить, пока множество номеров, по которым не позвонили, не будет пустым, или не будет сделано разумное количество попыток позвонить.
Граф (graph) — это структура данных, задающая набор вершин и набор связей, соединяющих вершины. Графы находят применение в планировании заданий, транспортировании и так далее. Например, строитель дома должен заключать контракты на этапы строительной работы. План работы должен быть составлен так, чтобы обеспечить выполнение всей подготовительной работы к моменту, когда начнется новый этап строительства. Например, кровельщики не могут начать свою работу, пока строители не завершат работу по сооружению дома, а строительные работы не могут быть выполнены, пока не будет заложен бетонный фундамент.
К0ллекЦИЯ Graph
двмк*1*
Набор вершин и набор соединительных связей.
с.пврвц101
Как коллекция вершин и связей граф имеет операции для добавления и удаления этих элементов. Алгоритмы просмотра начинаются в заданной вершине и находят все другие вершины, которые достижимы из начальных вершин. Другие алгоритмы просмотра выполняют оба просмотра графа — в глубину и в ширину.
Сеть (network) — это особая форма графа, которая присваивает вес каждой язЯ« ®ес указывает стоимость использования связи при прохождении графа.
Н-дример, в следующей сети вершины представляют города, а вес, присва-рваемый связям, — это расстояния между парами городов.
4.3.	Анализ алгоритмов
В этой книге мы разрабатываем, классы реализующие коллекции данных. Для реализации методов класса часто используются классические алгоритмы. Мы часто описываем подробно разработку и реализацию этих алгоритмов и анализируем их эффективность.
Клиент судит о программе по ее корректности, легкости использования и эффективности. Легкость использования и корректность программы зависит от процедур разработки и тестирования. На эффективность программы влияет множество факторов, которые включают внутреннюю машинную систему, количество памяти, имеющейся для управления данными и сложность алгоритмов. Мы кратко рассматриваем эти факторы и затем сосредоточиваем внимание на вычислительной сложности алгоритмов. Мы разрабатываем критерии эффективности, позволяющие нам измерять эффективность какого-либо алгоритма в терминах размера коллекции. Критерии не зависят от определенной машинной системы и измеряют абстрактные характеристики эффективности алгоритмов. Для создания численной меры эффективности нами впользуется нотация Big-О.
Критерии эффективности
ф^РИ™, в к°нечиом счете, выполняется в машинной системе со специ-сцстеСКИм сбором команд и периферийными устройствами. Для отдельной Како^'либо алгоритм может быть разработан для полного исполь-Сеии ИЯ пРеимУ1Чес'гв Д&нного компьютера и поэтому достигает высокой сте-teia ^ек^вности. Критерий, называемый системной эффективностью (sys-сравнивает скорость выполнения двух или более алгоритмов, ^аэРа®отаны Для выполнения одной и той же задачи. Выполняя эти на одном компьютере с одними и теми же наборами данных, мы
_	'?-ьосительное время, используя внутренние системщ
часы. Оценка времени становится мерой системной эффективности для каз дого из алгоритмов.
При работе с некоторыми алгоритмами могут стать проблемой ограничен! памяти. Процесс может потребовать большого временного хранения, огран! чивающего размер первоначального набора данных, или вызвать требующу времени дисковую подкачку. Эффективность пространства (space eff ciency) — это мера относительного количества внутренней памяти, испол! зуемой каким-либо алгоритмом. Она может указать, какого типа компьюте способен выполнять этот алгоритм и полную системную эффективность ал горитма. Вследствие увеличения объема памяти в новых системах, авали пространственной эффективности становится менее важным.
Третий критерий эффективности рассматривает внутреннюю структуру ал горитма, анализируя его разработку, включая количество тестов сравнения итераций и операторов присваивания, используемых алгоритмом. Эти типь измерений являются независимыми от какой-либо отдельной машинной системы Критерий измеряет вычислительную сложность алгоритма относительно п, коли честна элементов данных в коллекции. Мы называем эти критерии вычислительной эффективностью (computational efficiency) алгоритма и разрабатываем нотацию Big-О для построения измерений, являющихся функциями в.
Нотация Big-O. Интуитивно вычислительная эффективность алгоритма измеряется количеством обрабатываемых им данных для определения ключевых операций алгоритма. Эти операции могут зависеть от типа коллекции данных, количества данных и их начального упорядочения.
Нахождение минимального элемента в массиве — это простой алгоритм, основная операция которого включает сравнение элементов данных. Для массива с п элементами алгоритм требует п — 1 сравнений и мера эффективности пропорциональна п. Другие алгоритмы являются более сложными. Для обменной сортировки, описанной в главе 2, обработка данных включает серию сравнений в каждом прохождении. Если А — это массив из п элементов, то обменная сортировка выполняет п — 1 проходов. На рис. 4.5 показан этот алгоритм.
После прохода элементы с X сохраняются
Рис. 4.5. Проходы в обменной сортировке
y.0Q 1: Сравнение n-1 — элементов A[l] . . - A[n — 1] с A[0] и, если необходимо, такой обмен элементов, чтобы А[0] всегда имел наименьшее значение.
rfnoxod 2: Сравнение п-2 — элементов А[2] . . . А{п — 1] с А[1].
„од I’ Для общего случая, сравнение n-i — элементов A[i] . . . А[п — i]
’ с A[i — 1].
Общее число сравнений в сортировке обмена задается арифметическим оядом ftn) от 1 до п-1:
f(n) ~ (п — 1) + (п - 2) + . . . + 3 + 2 + 1 = п(п — 1)/2
Количество сравнений зависит от п2.
^Для обработки данных общих классов коллекций таких, как последовательные списки и деревья, мы используем сравнения в качестве меры эффективности алгоритмов.
ф Алгоритмы зависят также от начального упорядочения данных. Например, нахождение минимального значения в массиве значительно упрощается, если знаем, что эти данные упорядочены. В возрастающем списке минимальное значение занимает первую позицию. Это значение находится в конце убывающего списка. В этих случаях вычислительная сложность включает единственный доступ к данным, который может быть выполнен в постоянную единицу времени. В примере с сортировкой, если список упорядочен, не требуется никакого обмена. Это условие наилучшего случая, и оио представляет наиболее эффективное выполнение алгоритма. Однако, если список отсортирован в обратном порядке, каждое сравнение приводит к обмену. Это условие наихудшего случая для сортировки. Общий случай предполагает некоторое промежуточное количество обменов в зависимости от порядка данных в списке. Для алгоритмов поиска и сортировки в классе коллекций мы используем количество сравнений как доминантное действие и меру вычислительной эффективности. Наш анализ определяет также начальное упорядочение данных, в котором можно различать наилучший случай (best case), наихудший случай (worst case) и средний случай (average case) для алгоритма. Средний случай — это ожидаемая эффективность алгоритма, если он выполняется много раз со случайным набором значений данных.
Определяя вычислительную эффективность алгоритма, мы ассоциируем Функцию Дп) с количеством сравнений. В действительности, точная форма Функции может быть трудна для определения, и поэтому мы используем методы аппроксимации для определения хорошей верхней границы функции.
Мы определяем простую функцию g(n) и константу К так, что K*g(n) яревышает Дп) по мере того, как п значительно возрастает. Для большого значения п поведение f(n) ограничивается произведением функции g(n) на екоторую константу. Мы используем эту математическую концепцию, на-аьйаемую нотацией Big-O, чтобы дать меру вычислительной эффективности. # Определение; Функция Дп) имеет порядок O(g(n)), если имеется константа и счетчик п0, такие, что f(n) < K*g(п), для п > п0.
че^В^ГИТИвно это означает, что функция g в конечном счете превышает зна-^У^кции f. Мы говорим, что вычислительная сложность (computational JWexity) (или порядок) алгоритма равна O(gf и)).
ср^^^Ционно значение Big-O для алгоритма структур данных выбирается йеболыпого набора полиномиальных, логарифмических и экспоненци-ФУвкций. Для классических структур данных эти функции дают У^Шие верхние границы вычислительной сложности алгоритмов.
К g(n)
В примере с обменной сортировкой мы ищем функцию g, которая огра, ничивает f(n). В таблице 4.1 рассматриваются g(n) = п2 и jf(n) для разных значений п.
В конечном счете, функция f(n) ограничивается величиной xlzg(n)-> где g(n) — п2. В этом случае возможное условие появляется непосредственно при п0 = 1 и К = 1/2.
f(n) < 1/г пг для всех п > 1
Мы говорим, что /(п) имеет порядок O(g(n)) — О(пг)„ поэтому вычисли* тельная сложность обменной сортировки составляет О(п2). Анализ наилучшего и наихудшего случаев также приводит к той же мере сложности, так как обменная сортировка всегда требует 1/2 п(п — 1) сравнений. Этот алгоритм сортировки требует порядка О(п2) единиц времени для вычисления независимо от начального порядка данных.
В нашем исследовании сортировки мы обнаружим, что некоторые алгоритмы имеют вычислительную сложность (порядок) О(п logzn) для достаточно большого Ид.
Количество сравнений < К п log2 п для п > п0.
В таблице 4.1 сравниваются значения п2 и п logzn. Заметьте, насколько более эффективным является алгоритм сортировки О(п logzn), чем обменная сортировка. Например, в случае со списком из 10 000 элементов количество сравнений для обменной сортировки ограничивается величиной 100 000 000, тогда как более эффективный алгоритм имеет количество сравнений, ограниченное величиной 132 000. Новая сортировка приблизительно в 750 раз более эффективна.
Таблица 4.1
л	(1/2)п2	S(n) = п2/2 ~ п-2
10	50	45
00	5.000	4 950
1000	500 000	499.500
5000	12.500.000	12.497.500	|
10000	50.000.000		49/995.000	|
I п	п2	и 1од?п	|
5	25	116 -
10	100	зз.г	:
1100	10000	664,3	"
11000	1000000	9965,7	р
10000	100000000	132877,1		]
ггри выполнении Big-O-аппроксимации функции f(n) мы используем тер-* Доминирование для определения вычислительной сложности. Небольшой работы с неравенствами дает возможность математически проверить оСЬ1^ратегию. Например, в случае функции
^f(n) = п + 2
п является доминирующим. Функция g(n) = п используется в следующем ^равенстве для проверки того, что f имеет порядок О(п).
/(») «п4-2<п + п = 2*п для п > 2
тв1<же имеет порядок О(п2) или О(п3), так как g(n) = п2 и g{n) = п3 ^гоаничивают f(n). Мы выбираем О(п), что представляет наилучшую оценку °яя этой функции.
Пример 4.1
1.	f(n) = п2 + п + 16 Доминирующий терм — п2, a f имеет порядок О( п2).
f(n) = п2 4- п + 1 < п2 + п2 + п2 = Зп2 для п > 1
2.	f(n) = sgrtf п4-37 Доминирующий терм — sqrt(n)t a f имеет порядок O(sqrt(n))
f(n) = sqrt(n-\-3) < sqrt(n±n)=sqrt(2n)=sqrt(2)*sqrt(n) для n > 3
3.	f(n) = 2n 4- n + 2 Доминирующий терм — 2n, a f имеет порядок O(2n).
f(n) = 2n 4- n 4- 2
< 2n -t- 2n +2n
*= 3*2n, для n > 1
Сложность алгоритма. Big-O-оцеика дает меру времени выполнения (runtime) алгоритма. Обычно алгоритм имеет разную вычислительную эффективность для наилучшего и наихудшего случаев, поэтому мы вычисляем конкретное значение Big-O для каждого случая. В разделе 4.4 излагается метод нахождения времении выполнения для последовательного и бинарного поиска. Каждый алгоритм имеет порядок для наилучшего и наихудшего случая, которые различны. Наилучший случай для алгоритма часто не важен, так как эти обстоятельства являются исключительными и неподходящими для решения о выборе какого-либо алгоритма. Наихудший случай может быть важен, так как S?**1 обстоятельства будут наиболее негативно влиять иа ваше приложение. Д-лиент может не допускать наихудшего случая и может предпочесть, чтобы bi выбрали алгоритм, который имеет более узкий диапазон эффективности. В щем, довольно трудно математически определить среднюю эффективность ^Кого-либо алгоритма. Мы будем использовать только очень простые измере-п * ожидаемых значений и оставим математические детали для курса по тео-® сложности.
Общий порядок величин
алГое^°Льш°й набор различных порядков определяет сложность большинства влго^11™0® структур данных. Мы определяем различные порядки и описываем ₽йТмы, приводящие в результате к таким оценкам.
............-  -	то этот порядок не зависит от количеств, элементов данных в коллекции. Этот алгоритм выполняется за постоянную единицу времени (constant time). Например, присваивание некоторого зн^ чения элементу списка массива имеет порядок 0(1), при условии, что вц храпите индекс, который определяет конец списка. Сохранение этого эле1, мента включает только простой оператор присваивания.
Прямая вставка в конец списка
начало
X
конец
Алгоритм О(и) является линейным (linear). Сложность этого алгоритма пропорциональна размеру списка. Например, вставка элемента в конец списка п элементов будет линейной, если мы не храним ссылку на конец списка. Подразумевая, что мы можем просматривать элемент за элементом, алгоритм требует, чтобы мы протестировали п элементов перед определением конца списка. Порядком этого процесса является О(п). Нахождение максимального элемента в массиве из п элементов — это О(п), потому что должен быть проверен каждый из п элементов.
Последовательная вставка в конец списка
начало	конец
Ряд алгоритмов имеют порядок, включающий log^n, и называются логарифмическими (logarithmic). Эта сложность возникает, когда алгоритм не однократно подразделяет данные на подсписки, длиной 1/2, 1/4, 1/8, и так далее от оригинального размера списка. Логарифмические порядки возникают при работе с бинарными деревьями. Бинарный поиск, изложенный в разделе 4.4, имеет сложность среднего и наихудшего случаев Of/ogpn^. В главах 13 и 14 описываются алгоритмы сортировки с использованием дерева и быстрая сортировка порядка О(п log^n).
Алгоритмы, имеющие порядок О(п2), являются квадратическими (quadratic). Наиболее простые алгоритмы сортировки такие, как обменная сортировка, имеют порядок О(п2). Квадратические алгоритмы используются на практике только для относительно небольших значений п. Всякий раз, когда п удваивается, время выполнения такого алгоритма увеличивается на множитель 4. Алгоритм показывает кубическое (cubic) время, если его порядок равен О(п3), и такие алгоритмы очень медленные. Всякий раз, когда п удваивается, время выполнения алгоритма увеличивается в восемь раз. Алгоритм Уоршела, применимый к графам, — это алгоритм порядка О(п8).
Алгоритм со сложностью О(2п) имеет экспоненциальную сложность (exponential complexity). Такие алгоритмы выполняются настолько медленно, что они используются только при малых значениях п. Этот тип сложности часто ассоциируется с проблемами, требующими неоднократного поиска дерева решений.
15 таблице 4.2 приводятся линейный, квадратичный, кубический, экспо-ддцальный и логарифмический порядки величины для выбранных значе-Из таблицы очевидно, что следует избегать использования кубических ^сдоненциальных алгоритмов, если только значение п не мало.
Таблица 4.2
оценка порядка алгоритмов
п	log^n	n log?n	и2	п3	2'
	1	2	4	8	4
7	 4_			2	8	16	64	16
8	3	24	64	512	256
	4	64	256	4096	65536
	 зГ~~	5	160	1024	32768	4294967296
128	7	896	16384	2097152	3,4 х 10зе
1024	10	10240	1048576	1073741824	1.8 х Ю308
65536	~	16	1048576	4294967296	2.8 х 10м	Избегайте!
4.4. Последовательный и бинарный поиск
Теперь познакомимся с последовательным поиском в целях нахождения некоторого значения в списке. Предположим, что мы ищем пределы списка целых с использованием этого алгоритма. В действительности, мы можем выполнять поиск в массиве любого типа, для которого определен оператор ==. Необходимо модифицировать последовательный поиск для ссылки на параметризованный тип DataType, который является псевдонимом фактического типа. Мы создаем этот псевдоним, используя ключевое слово typedef. Например:
typedef int DataType;	//DataType это int
ИЛИ
typedef double DataType:	//DataType это double
Если предположить, что программист имеет определенный тип DataType, т° код для общего алгоритма последовательного поиска следующий:
// поиск в массиве а из п элементов для нахождения соответствия с ключей
// использовать последовательный поиск, возвращать индекс
// соответствующего элемента массива или — 1, если нет соответствия
int SeqSearch(DataType list[ ], int n, DataType key)
for (int i=0; i < n; i++) if (listfij ™ key) return i;	//возвращать индекс соответствующего элемента
return -1;	//поиск неудачный, возвращать -1
При определении порядка алгоритма последовательного поиска различают -Ведение наилучшего и наихудшего случаев. Наилучшему случаю соответст-Ует нахождение ключа в первом элементе списка. Время выполнения алго-Т1аа при этом составляет 0(1). Наихудший случай имеет место, когда этот 104 Не находится в списке или обнаруживается в конце списка. Он требует
проверки всех п элементов и имеет порядок О(п). Средний случай требует небольшого количества вероятностных рассуждений. Для случайного списка совпадение с ключом может с одинаковой вероятностью появиться в любо^ позиции списка. После выполнения проверок большого количества элементов средняя позиция совпадения —- это срединный элемент (midpoint) п/2. промежуточная точка анализируется после п/2 сравнений, что определяет ожц. даемую стоимость поиска. По этой причине мы говорим, что средняя эффективность последовательного поиска составляет О(п).
Бинарный поиск
Последовательный поиск применим для любого списка. Если список является упорядоченным, алгоритм, называемый бинарный поиск (binary search), предоставляет улучшенный метод поиска. Ваш опыт по нахождению номера в большом телефонном справочнике — это модель такого алгоритма. Зная нужные имя и фамилию, вы открываете справочник ближе к началу, середине или концу, в зависимости от первой буквы фамилии. Вам может повезти, и вы сразу попадете на нужную страницу. В противном случае вы переходите к более ранней или более поздней странице в справочнике в зависимости от относительного местоположения имени человека по алфавиту. Например, если имя человека начинается с буквы R, а вы находитесь на странице с именами на букву Т, вы переходите на более раннюю страницу. Процесс продолжается до тех пор, пока вы не найдете соответствие или не обнаружите, что этого имени нет в справочнике. Соответствующая идея применима к поиску в упорядоченном списке. Мы идем к середине списка и ищем быстрое соответствие ключа значению срединного элемента. Если нам не удается найти соответствия, мы смотрим на относительный размер ключа и значение срединного элемента и затем перемещаемся в нижнюю или верхнюю половину списка. В общем, если мы знаем, как упорядочены данные, мы можем использовать эту информацию, чтобы сократить время поиска.
Следующие шаги описывают алгоритм. Предположим, что список упорядочен, как массив. Индексами в концах списка являются: low — О и high = п — 1, где п — это количество элементов в массиве.
1. Сравнить индекс срединного элемента массива:
mid = (low+high)/2.
Совпадение найдено	Поиск в левой половине
key (ключ).
Поиск в правой половине
Если совпадение найдено, возвращать индекс mid для нахождения ключа-if {A[mid] =~ key) return(mid);
ЕсЛИ A[mid] < key, совпадение должно происходить в диапазоне индексов А4-1 • • • kigh, в правой половине рассматриваемого списка. Это верно, что список упорядочен. Новыми границами являются low=mid+l и
^Если key < A[mid], совпадение должно происходить в диапазоне индексов mid-1, в левой половине списка. Новыми границами являются low
Проверка левой половины
Проверка правой половины
Алгоритм уточняет местоположение совпадающего с ключом элемента, деля пополам длину интервала, в котором может находиться этот элемент, и затем выполняя тот же алгоритм поиска в меньшем подсписке. В конце концов, если искомый элемент не находится в списке, low превысит high, и алгоритм возвращает индикатор
сбоя —
1 (совпадение не произошло).
Пример 4.2
Рассмотрим массив целых для заданного ключа 33.



А. Этот
пример дает выборку алгоритма
Заметьте, что этот алгоритм требует трех (3) сравнений. При линей-Ном поиске в списке требуется восемь (8) сравнений.
го поиска
Функция использует параметризованное имя DataType, которое должно по, держивать оба оператора: равенства (===) и меньше чем (<). Первоначально 1о равно О, a high — (п-1), где п — число элементов в этом массиве. Функци возвращает номер удовлетворяющего условию элемента массива или -1, ©сд такой элемент не найден (low>high). // dsearch.h // просмотреть сортированный массив на предмет совпадения // с ключом, используя бинарный поиск, возвращать индекс // совпадающего элемента массива или -1, если совпадение //не происходит int BinSearch(DataType listО, int low, int high, DataType key} {
int mid;
DataType midvalue;
while (low <*= high) <
mid = (low+high)/2;	// mid-индекс в подсписке
midvalue - list(mid}; // значение при mid-индексе if (key ==•= midvalue) return mid;	// совпадение имеется, возвращаем
// его положение в массиве
else if (key < midvalue) high = mid-1;	// перейти в нижний подсписок
else low = mid+1;	// перейти в верхний подсписок
) return -1;	// элемент не найден
1
Реализация последовательного и бинарного поиска включена в файл dsearch.h. Так как эта функция зависит от класса DataType, определение DataType должно предшествовать включению этого файла.
Программа 4.1. Сравнение последовательного и бинарного поиска
Программа сравнивает время вычисления последовательного и бинарного поиска. Массив А заполняется 1000 случайными целыми числами в диапазоне 0 . . 1999 и затем сортируется. Второму массиву В присваиваются 500 случайных целых чисел в том же диапазоне. Элементы в массиве В используются как ключи для алгоритмов поиска. Временная функция TickCount определяется в файле ticks.h и возвращает количество 1/60-х секунд со времени запуска системы. Мы измеряем время, которое занимает выполнение 500 поисков, используя каждый алгоритм. Выходная информация включает время в секундах и количество соответствий.
((include <iostream.h>
typedef int Datatype; // данные типа integer
•include ’’dsearch.h"
•include "random.h"
•include "ticks.h"
// сортировать целый массив из п элементов //в возрастающем порядке
void ExchangeSort(int a(), mt n) <
int i» j/ temp;
for (i=O;i<n-l; i++)
/ / поместить минимум элементов a[i]...a[n-l] в a [ i) for (j = i+1/ j < n; j++)
// если a [ j ] < a [i] , выполнить их замену
if (a[j] < a[i])
{
temp = a [ i) ;
a[i] = a[j];
a[ j] = temp;
)
void main (void)
//А содержит список для поиска, В содержит ключи
int А[ЮОО], В[500] ;
int i, matchCount;
// используется для данных времени long tcount;
RandomNumber rnd;
// создать массив Айз 1000 случайных чисел со значениями
// в диапазоне 0. . 1999
for (i = 0; i < 1000; i++)
A[i] = rnd.Random(2000);
Exchangesort(A,1000);
// генерить 500 случайных ключей из того же диапазона
for (i = О; i < 500; i++)
B[i] =rnd.Random(2000);
cout « "Время последовательного поиска" « endl;
tcount = TxckCount ();	// время начала
matchCount = 0;
for (i = 0; i < 500; i++)
if (SeqSearch(A, 1000, B(i]) !=-l) matchCount++;
tcount = TickCount() — tcount; //
cout « "Последовательный поиск занимает " « tcount/60.0 « " скунд для " « matchCount « ” совпадений. " « endl;
cout « ’’Время бинарного поиска"« endl;
tcount = TickCount();
matchCount = 0;
for (i = 0; i < 500; i++)
if (BinSearch (A, 0, 999, В [i] ) !=-1) matchCount++;
tcount = TickCount() — tcount; //
cout « "Бинарный поиск занимает " « tcount/60.0
j	« ” секунд для " « matchCount « ” совпадений. " « endl;
/*
волнение программы 4.1>
Яосде Последовательного поиска
Время^Вательный поиск занимает 0.816667 секунд для 181 совпадений.
Ви«Х^М₽ногопоиска
*/ поиск занимает 0.016667 секунд для 181 совпадений.
Неформальный анализ для бинарного поиска. Наилучший случай имеет место, когда совпадающий с ключом элемент находится в середине списка. При этом порядок алгоритма составляет О( 1), так как требуется только одно тестирующее сравнение равенства. При наихудшем случае, когда элемент де находится в списке или определяется в последнем сравнении, имеем порядок O(log2n). Мы можем интуитивно вывести этот порядок. Наихудший случай возникает, когда мы должны уменьшать подсписок до длины 1. Каждая итерация, которая не находит соответствие, уменьшает длину подсписка на 1 множитель 2. Размеры подсписков следующие:
п п/2 п/4 п/8 ... 1
Разделение на подсписки требует т итераций, где т — это приблизительно log2n (см. подробный анализ). Для наихудшего случая мы имеем начальное сравнение в середине списка и затем — ряд итераций log2n. Каждая итерация требует одну операцию сравнения:
Total Comparisons • 1 + log2n
В результате наихудшим случаем для бинарного поиска является Oflogzn). Этот результат проверяется имперически программой 4.1. Отношение времени выполнения последовательного поиска ко времени выполнения бинарного поиска равно 49,0. Теоретическое отношение ожидаемого времени приблизительно составляет 500/(log21000)— 50,2.
Формальный анализ бинарного поиска. Первая итерация цикла имеет дело со всем списком. Каждая последующая итерация делит пополам размер подсписка. Так, размерами списка для алгоритма являются
п п/21 п/22 п/23 п/24 . . . п/2т
В конце концов будет такое целое ш, что
п/2“<2 или n<2m+1
Так как ш — это первое целое, для которого п/21“<2, то должно быть верно
n/2m-1>2 или 2га<п
Из этого следует, что
2m<n<2ni+1
Возьмите логарифм каждой части неравенства и получите /о^2П=действи-тельному числу х:
m<!og2n=x<m+l
Значение m — это наибольшее целое, которое <х и задается int(x). Например, если n=50, log250=5,644. Следовательно,
mint(5,644)= 5
Можно показать, что средний случай также составляет O(logzn).
4.5. Базовый класс последовательного списка
Товары для покупки, автобусное расписание, телефонный справочник, налоговые таблицы и инвентаризационные записи являются примерами списков. В каждом случае объекты включают последовательность элементов. Во многих приложениях ведется какой-либо список. Например, перечень товаров пред-
«ятия содержит информацию о поставках и заказах, персонал офиса создает ЯР Мясную ведомость для списка работников компании, ключевые слова для ^^пялятора сохраняются в списке зарезервированных слов и так далее. Й R главе 1 описывался ADT для базового последовательного списка. Операции ового списка включают вставку нового элемента в конец списка, удаление ^мента, доступ к элементу в списке по позиции и очистку списка. Мы имеем ясе операции для тестирования, является ли список пустым, или находится Т какой-либо элемент в списке. В качестве примера этому из реальной жизни
С( >трим список продуктов для покупки в универсаме (Рис.4.6). Когда вы Р еТб по универсаму, вы решаете купить дополнительные товары и добавляете в конец списка. Когда товар найден, вы удаляете его из списка. Список с этими простыми операциями может использоваться для решения существен-яыХ задач. Приложение описывает отдел видеотоваров, в котором ведется список имеющихся фильмов и список клиентов. Когда какой-либо фильм выдается клиенту» он переходит из списка имеющихся фильмов в список клиентов. При возврате фильма происходит обратный процесс.
Добавили молоко
Вычеркнули картофель
Рис.4.6. Слисок покупок
Список ADT описывает однородные списки (homogeneous lists), в которых каждый элемент имеет один и тот же тип данных, называемый DataType. ® определении ADT не упоминается о том, как хранятся элементы. Для ^ого может использоваться массив или связанный список с применением Динамического распределения памяти. Реализации операций Insert, Delete и nd зависят от метода, используемого для хранения элементов списка.
В главе 1 приводится лишь набросок спецификации этого класса. В данном Деле мы приводим реализацию класса SeqList, который сохраняет эле-нты в массиве. В главе 9 мы разрабатываем новую реализацию этого класса, пользуя связанные списки, и выводим этот класс из абстрактного базового ^асса List в главе 12. В главах 11, 13, и 14 разрабатываются классы сходной ^Уктуры для деревьев бинарного поиска, хеш-таблиц и словарей.
Спецификация класса SeqList
ОБЪЯВЛЕНИЕ
♦include <iostream.h>
♦include <stdlib-h>
typedef int DataType;
const int MaxListSize = 50;
class SeqList (
private:
// массив для списка и число элементов текущего списка
DataType listitemfMaxListSize];
int size;
public:
// конструктор SeqList{void);
// методы доступа
int ListSize(void) const;
int ListEmpty(void) const;
int Find (DataTypefi item) const;
DataType GetData(int pos) const;
11 методы модификации списка
void Insert(const DataType& item);
void Delete(const DataTypeb item);
DataType DeleteFront(void);
void ClearList(void);
ОПИСАНИЕ
Объявление и реализация находятся в файле aseqlist.h. Имя DataType используется для представления общего типа данных. Перед включением класса из файла используйте typedef для связывания имени DataType с конкретный типом. Переменная size поддерживает текущий размер списка. Первоначально размер установлен на О. Так как статический массив используется для реализации списка, константа MaxListSize является верхней границей размера списка. Попытка вставить больше, чем MaxListSize элементов в список приводит к сообщению об ошибке и завершению программы.
Реализация класса SeqList
Данная реализация класса SeqList использует массив listitem для сохранения данных. Коллекция распределяет память для MaxListSize числа элементов типа DataType. Количество элементов в списке содержится в size (член класса)-Файлы iostream.h и stdlib.h включены для обеспечения выдачи сообщения об ошибках и для завершения программы, если Insert приведет к тому, что размер превысит MaxListSize.
Закрытый член size содержит длину списка для операций Insert и Delete. Значение size является центральным для конструктора и методов ListSize, ListEmpty и ClearList. Мы включаем конструктор, устанавливающий размер на 0.
// конструктор, устанавливает size в 0
SeqList::SeqList (void) : size(O)
О
Методы модификации списка
ва 1- ^ли сообщение <даямается
Метод Insert добавляет новый элемент в конец списка и увеличивает длину при этом превышается размер массива listitem, то метод выводит об ошибке и завершает программу. Ограничение на размер списка в главе 9, где класс реализуется с использованием связанного
СПИсКа"
«стаака (10)
Элемент параметра передается в качестве ссылки константе. Если размер DataType большой, использование ссылочного параметра позволяет избежать неэффективного копирования данных, которое необходимо в вызове параметра по значению. Ключевое слово const указывает на то, что фактический параметр не может быть изменен. Этот же тип передачи параметра используется методом Delete.
Insert
// вставить элемент в хвост списка, прервать выполнение // программы, если размер списка превысит MaxListSize void SeqList: : Insert (const DataType& item)
I
11 проверка размера списка
if (size+1 > MaxListSize) I
cerr «"Превышен максимальный размер списка" « endl; exit(l);
}
// индекс хвоста равен размеру текущего списка liBtitemlsize) = item;
size++;
Метод Delete определяет первое появление в списке заданного элемента. Функция требует, чтобы был определен оператор сравнения (===) для DataType.
некоторых случаях для этого может потребоваться, чтобы пользователь предоставил особую функцию, которая переопределяет оператор == для конкретного DataType. Эта тема формально излагается в главе 6. Если элемент е обнаруживается при индексе i, операция спокойно заканчивается без из-
яения списка. Если элемент найден, он удаляется из списка перемещением х элементов с индексами i+1 к концу списка влево на одну позицию.
О
1
।
i+1
i+2
Например, удаление элемента со значением 45 из списка приведет к сме. щению влево хвостовых элементов 23 и 8. Длина списка изменяется с 6 5. Удаление элемента со значением 30 оставляет список неизменным.
Delete
// поиск и удаление элемента item из списка void SeqList::Delete(const DataTypeb item) (
int i - 0;
// поиск элемента
while (i < size && ’(item « listitemli]))
if (i < size)
// передвинуть хвост списка на одну позицию влево while (i < size-1)
(
listitem[i] = listitem[i+l]; i++;
>
size—;	// уменьшение size на 1
}
}
Методы доступа к списку. Метод GetData возвращает значение данных в позицию pos в списке. Если pos не находится в диапазоне от 0 до size-1, печатается сообщение об ошибке, и программа завершается.
// возвращает значение элемента списка для индекса pos. если pos // не находится в диапазоне индексов списка, программа заканчивается //с сообщением об ошибке
DataType SeqList::GetData(int pos) const (
// прервать программу, если pos вне диапазона индексов списка
if (pos < 0 |I pos >= size) (
cerr « "pos выходит за диапазон индексов!" « endl; exit (1);
>
return listitem[pos];
}
Метод доступа Find принимает параметр, который служит в качестве ключа, и последовательно просматривает список для нахождения совпадения. Если список пустой или совпадение не найдено, Find возвращает 0 (False). Если элемент обнаруживается в списке в позиции с индексом i, Find присваивает запись из listitemfi] соответствующему элементу списка и возвращает 1 (True). - Для данных, совпадающих с ключом, процесс присваивания значения данных элемента списка параметру является важнейшим в приложениях, касающихся записей данных. Например, предположим, что DataType — это структура с полем ключа и полем значения, и что оператор —= тестирует
пьКо поле ключа. При вводе элемент параметра может определять только ключа. При выводе элемент присваивается обоим полям.
rind
// использовать item в качестве ключа для поиска в списке.
// возвращать True, если элемент item находится в списке, и
// False — в противном случае. если элемент найден, присвоить
// его значение параметру item, передаваемому по ссылке
int SeqList::Find(DataTypeb item) const
int i =* 0;
if (ListEmpty())
return 0;	// возвратить False, если список пуст
while (i < size ! (item == listitemfi]) )
if (i < size) (
item  listitem(ij; // присвоить item значение элемента списка
return 1;	// возвратить True
)
else
return 0;	// возвратить False
}
Класс SeqList не предоставляет метода для непосредственного изменения значения какого-либо элемента. Для выполнения такого изменения мы должны сначала найти этот элемент и возвратить запись данных, удалить этот элемент, изменить запись и вставить новые данные в список. Конечно, это изменяет положение элемента в списке, потому что новый элемент помещается в конец списка.
’Я Пример 4.3
m.J Запись Inventoryitem содержит номер детали и количество деталей 1 в Запасе. Оператор == сравнивает две записи Inventory Item, сравнивая jk-- 1 поля partNumber. Выполняется поиск SeqList-объекта L для нахождения
.] заяиси с partNumber 5. Если объект найден, запись обновляется уве-- Д личением поля count.
Arj ®truct Inventoryltem
partNumber;
int count;
°Perator==i (Invntoryltem x, Invntoryltem y)
return x.partNumber == у.partNuraber;
>
4?^
typedef Invent oryltem DataType;
•/*	#include "aseqlist.h"
SeqList L;
Z' Я Inventoryltem item;
 $	item. partNumber = 5;
i f (L. Find (item))
L.Delete(item) ;
Sitem.count++;
^7 L.Insert(item) ;
Так как любой элемент всегда вставляется в хвост списка, порядок (время выполнения) метода Insert зависит от п и равен 0(1). Find выполняет последовательный поиск, поэтому среднее время его работы будет О(п). На протяжение многих проб метод Delete должен проверить в среднем п/2 элементов списка и должен перемещать в среднем п/2 элементов. Это означает, что среднее время выполнения для Delete составляет О(п). Порядок наихудшего случая для обоих методов Find и Delete также составляет О(п).
Применение. Объекты SeqList используются для ведения списка имеющихся фильмов, и списка фильмов, взятых для просмотра клиентами в видеомагазине. Каждый элемент в списке является записью, которая состоит из названия фильма и (для проката) имени клиента.
// структура записи для хранения данных о фильме и клиенте struct FilmData
{
char filmName[32];
char customerName132}; )
Так как метод Find в классе SeqList требует определения оператора сравнения ==, мы перегрузим этот оператор для структуры FilmData. Этот оператор проверяет имя файла, используя функцию C++ strcmp.
// перегрузка ==
int operator =~ (const FilmData &А, cost FilmData *B) (
return strcmp(A.filmName, B.filmName);
)
Чтобы использовать FilmData с классом SeqList, включите объявление typedef FilmData DataType;
Определение оператора =~ для FilmData и DataType находятся в файле video.h.
* В видеомагазине ведется инвентаризационный список фильмов. Для пр^ стоты мы полагаем, что в магазине имеется только одна копия каждого фильма. Новый фильм добавляется в инвентаризационный список функцией Insert. Для проверки наличия фильма в списке используется функция Find-Если фильм найден, он удаляется из инвентаризационного списка фильмов и вставляется в список фильмов, отданных для просмотра.
Программа 4.2. Видеомагазин
Main-программа эмулирует операции видеомагазина. Первоначально есь перечень фильмов считывается из файла films и сохраняется в списке с именем inventoryList. Мы наблюдаем короткий промежуток времени деятельности видеомагазина и рассматриваем заказы четырех клиентов на прокат фильмов. В каждом случае мы вводим имя клиента и заказ фильма и оЕределяем, имеется ли этот фильм в наличии в настоящее время. Если да, то мы удаляем его из инвентаризационного списка и добавляем клиента в список лиц, взявших фильмы напрокат. Если фильма нет в наличии, клиент уведомляется об этом.
jinclude <iostream.h>
jinclude <fstream.h>
^include <stdlib.h>
#include <string.h>
^include "video.h”	// объявления видео-данных
^include "aseqlist.h"	// включить класс SeqList
// читать таблицу фильмов с лиска
void SetupinventoryList(seqList &inventoryList) (
ifstream filmFile;
FilmData fd;
Il открыть файл, с проверкой ошибок filmFile.open("Films", ios::in | ios::nocreate); if (!filmFile) <
cerr « "Файл 'films' не найден!” « endl; exit(1);
}
// читать строки до конца файла;
// вставлять наименования фильмов в инвентаризационный список while(filmFile.getline(£d.filmName,32,*\n'))
inventoryList.Insert(fd);
// печать наименований фильмов
void FrintlnvemtoryList(const SeqList &inventoryList)
int i;
FilmData fd;
inventoryList.GetData(i) « fd.filmName « endl;
for (i -=q. i < inventoryList.ListSize(); i++)
td -cout )
// lutKn
v°id ₽ ПО списку клиентов, печать клиентов и фильмов
{ FtintCustomerList(const SeqList 8customerList)
int i
JilttData fd;
. ti * 0; i < customerList.ListSize(); i++)

fd=customerList.GetData(i); //
cout « fd.customerName « " (" « fd.filraName « ’’) " « endl;
} }
void main (void)
I
//
SeqList inventoryList, customerList;
int i;
//
FilmData fdata;
char customer [20];
SetupInventoryList(inventoryList); // читать файл с фильмами
// запрос имени клиента и названия фильма.
// если запрошенный фильм имеется в наличии, он вносится в список клиентов
//и удаляется из списка фильмов; в противном случае выдается
/ / сообщение об отсутствии фильма
for (1 = 0; i < 4; i++)
(
// ввод имени клиента и названия фильма
cout << "Имя клиента: ";
cin.getline(customer,32,' \n');
cout « "Запрашиваемый фильм: ” ;
cin.getline(fdata.filraName,32,'\n');
if (inventoryList.Find(fdata)) {
strcpy(fdata.customerName, customer);
// вставить название фильма в список клиентов customerList.Insert(fdata);
// удалить из списка фильмов inventoryList.Delete{fdata);
)
else
cout « "Сожалею! "« fdata.filraName
« " отсутствует." « endl;
)
cout « endl;
// печать списков клиентов и фильмов
cout « "Клиенты, взявшие фильмы для просмотра" « endl;
PrintCustomerList(customerList);
cout « endl;
cout << "Фильмы, оставшиеся в ведомости: ”« endl;
PrintInventoryList(inventoryList);
)
/*
< Входной файл "Films">
Война миров
Касабланка
Грязный Гарри Дом животных Десять заповедей Красавица и зверь Список Шиндлера Звуки музыки
La strata
Звездные войны ^Выполнение программы 4.2>
ймя клиента: Дон Бекер запрашиваемый фильм: Дом животных «Д4Я клиента; Тери Молтон запрашиваемый фильм: Красавица и зверь клиента: Деррик Лопез Запрашиваемый фильм: La Strata имя клиента: Хиллари Дэн Запрашиваемый фильм: Дом животных Сожалею! Дом животных отсутствует.
Клиенты, взявшие фильмы для просмотра дон Бекер (Дом животных)
Тери Молтон (Красавица и зверь) Деррик Лопез (La Strata)
Фильмы, оставшиеся в ведомости: Война миров Касабланка грязный Гарри десять заповедей Список Шиндлера Звуки музыки Звездные войны */
Письменные упражнения
(ж)
4.1	Объясните различие между линейной и нелинейной структурой данных. Дайте пример каждой.
4.2	Определите, какая структура данных является соответствующей для следующих ситуаций:
(а)	Сохранить абсолютное значение числа в элементе 5 пелого списка.
(б)	Пройти по списку студентов в алфавитном порядке и напечатать отметки.
(в)	Когда арифметический оператор найден, два предыдущих числа удаляются из коллекции.
(г)	В исследовании с использованием моделирования каждое событие вставляется в коллекцию и удаляется в порядке его вставки.
(Д) Когда ферзь иа шахматной доске может переместиться в какую-либо позицию, эта позиция вставляется в коллекцию.
fa) Одно поле структуры данных является целым, другое — действительным значением, а последнее поле — это строка.
Постоянно сохраняйте выходные данные программы, чтобы рассмотреть их позже.
Если ключ меньше, чем текущее значение в списке, рассмотрите цРеДЫдущие значения.
Минимальное значение всегда переходит в вершину списка.
Сорока используется в качестве ключа для нахождения записи данных, внходящейся где-то в коллекции.
Слово используется в качестве индекса для нахождения его определения в Коллекции.
(з)
fa) fa)
(в)
(м) В праздники телефонная сеть перегружена. Определите альтернативны^ набор путей для наилучшего распределения вызовов.
4.3	Ниже приведены меры сложности наихудшего случая для трех алго. ритмов, которые решают одну и ту же задачу:
Алгоритм 1 Алгоритм 2 Алгоритм 3
О(п2)	О(п logzn) О(2п)
Какой метод предпочтителен и почему?
4.4	Выполните анализ Big-O для каждой из следующих функций:
(а)	п + 5
(б)	п2 +6п +7
(в)	т/п + 3
(г)	я3 + п2 - 1
п + 1
4.5
(а)	Для какого значения п>1, 2п становится больше, чем п3?
(б)	Покажите, что 2п +ns имеет порядок О(2П).
/в\	я2 + 5
1 ' Дайте Big-O-оценку для-------+ 6 log2n ?
я + 3
4.6	Список целых ведется в массиве. Каков порядок алгоритма печати первого и последнего элемента в массиве?
4.7	Объясните, почему алгоритм порядка O(log2n) имеет также порядок О(п).
4.8	Каждый цикл является главным компонентом для алгоритма. Используйте нотацию Big-O, чтобы выразить время вычисления наихудшего случая для каждого из следующих алгоритмов как функции п.
(a)	for (dotprd=0.0,i=0;i < n;i++)
dotprd +=a[i] * b[i];
(6)	for (i=O;i<n;i++)
if(a(i] == K)
return 1;
return 0;
(в)	for (i=0;i<n;i++)
for (j=C;j<n;j++) b[i] [j] *=* c;
(r) for (i=0;i<n;i++)
for(j=0;j<n;j++)
{
entry» 0.0;
for {k=0; k<n; k++ )r
entry +=a[i] [k] * b[kj [j];
c[i,jj » entry;
)
4.9 Следующие коллекции из n элементов используются для сохранения данных. Каков порядок алгоритма нахождения минимального значения (а) в стеке?
(б) в очереди приоритетов?
(») (Г) Cd
4.Ю
в дереве бинарного поиска?
в последовательном списке с упорядочением по возрастанию?
в списке с возможностью прямого доступа к элементам с упорядочением во убыванию?
Последовательность чисел Фибоначчи имеет вид:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, . . .
Первые два числа являются 1, и каждое последующее число Фибоначчи является суммой двух предшествующих. Эта последовательность описывается рекуррентным отношением
ft ~ 1, *2-1, fn = fn-2 + fn-1, для n > 3
Следующая функция вычисляет n-ное число Фибоначчи. Каков ее порядок?
long Fibonacci{int n)
long fnm2“l, fnml=l, fn; int i;
if (n<= 2)
return 1;
for (i~~3;i<“ n;i++)
<
fn = fnm2 + fnml;
fnm2 = fnml;
fnml = fn;
}
return fn;
В главе 10 рекурсивная функция записывается для вычисления п-ного числа Фибоначчи. Метод имеет экспоненциальный порядок. Ясно, что рекурсивное решение неприемлемо!
4.11
(а)	Последовательный поиск используется в списке из 50000 элементов.
□	Какое наименьшее количество сравнений выполнит этот поиск?
□	Какое необходимо максимальное количество сравнений?
□	Каково ожидаемое количество сравнений?
(б)	Бинарный поиск используется в списке из 50000 элементов
D Какое наименьшее количество сравнений выполнит этот поиск?
D Какое максимальное количество сравнений необходимо?
Предположим, SeqList-объект L состоит из элементов:
34 11 22 16 40
Задайте элементы в списке после каждой из следующих команд:
п - Ь.DeleteFront();
Insert(и);
if (I*.Find(L.GetData (0) *2)
(6) £‘ Deleten€’ •
^са°льзуя объект L, приведите выход для следующей последователь-команд:
for (int 1=0; i <5; i++) <
L. Insert (L.DeleteFront ());	j
cout « L.GetData(i) « ; >
4.13	Напишите функцию для реализации указанной задачи
(а)	Добавить SeqList-объект L в хвост объекта К.
void Concatenate(SeqList& К, SeqLxst& L);
(б)	Поменяйте на обратный порядок элементов в SeqList-объекте L. void Reverse (SeqLxst& L);
4.14	Функция Ques берет SeqList-объект L, элементы которого все являются положительными целыми.
Каково действие функции над списком?
{1, 3, 7, 2, 15, 0, 12}?
Почему L должен передаваться по ссылке?
typedef int DataType;
#xnclude "aseqlist.h” int M(const SeqList &L) (
int x, mval, length  L.ListSize(); if (length =— 0) <
cerr « Список пустой « endl;
return -1; } mval » L.GetData(O); for (i= 1; x< length; I++) if (L.GetData(i) > mval) mval = L.GetData(i);
return mval; ) void Ques(SeqList fiL) {
int mval = M(L);
L.Delete(mval); )
4.15	Объясните, почему необходима пересылка данных при реализации м* тода Delete в классе SeqList на базе массива.
Упражнения по программированию
4.1	Используйте класс SeqList с DataType int для создания utility функция InsertMax:
void InsertMax(SeqListb L, int elt);
InsertMax помещает elt в список L, только если он больше всех суШе‘ ствуюхцих элементов в списке..
ТТя пишите main-программу, которая читает 10 целых и вызывает 1п-sertMax для каждого. Напечатайте список.
4.2	Объявите запись
. struct Person
{
char name(20);
int age;
char gender;
it
я вызовите класс SeqList следующим образом:
^include <stnng.h>
//needed for SeqList class method Find
int operator»- (Person x, Person y)
(
return strcmp(x.name, y.name)== 0;
)
typedef Person DataType;
linclude "aseqlist.h"
(а)	Напишите функцию
void PrintByGender(const SeqList* l, char sex);
которая проходит по списку L и печатает все записи, имеющие заданный пол.
(б)	Напишите функцию
int InList (const SeqList*, char *nm, Persons p);
которая определяет, существует ли в списке L запись с полем имени пт. Выполните это, создав объект Person с полем имени пт и используя метод Find. Нет необходимости инициализировать поля age и gender записи. Сравнение двух записей выполняется сравнением полей имени. Если совпадение происходит, присвоить запись параметру р н возвратить 1; в противном случае возвратить 0. Параметр р не должен изменяться, если только ие будет найдено совпадение.
(в)	Напишите main-программу для тестирования этих функций.
^•3 Напишите программу, которая запрашивает у пользователя целое п, генерирует массив из п случайных целых в диапазоне 0 ... 990 и сортирует список, используя обменную сортировку. Задайте время выполнения сортировки, используя функцию TickCount, определенную в файле ticks.h. Выполните программу, используя п= 50, 500, 1000 и 10000. Это является экспериментальным подтверждением того, что обменная сортировка имеет порядок О(п2).
Примечание: Так как локальный массив из 1000 или 10000 элементов болеет превысить объем динамической области системы, выделить динамический массив со следующим синтаксисом для хранения элемен-ТОв- Вы можете использовать общую нотацию массива a[i] с динами-Пеской структурой.
let	//определяет указатель
а «
new int (n);	//п равен 50, 500, 1000 или 10000
Напишите программу, которая запрашивает у пользователя целое у генерирует массив из п случайных целых в диапазоне 0 ... 999 i сортирует список, используя обменную сортировку. Задайте время bj^ полнения сортировки. Выполните программу, используя п — 50, 50q 1000 и 10000. Это является экспериментальным подтверждением того что обменная сортировка имеет порядок О(п2).
4.4 Это упражнение расширяет применение Программы 4.2 (Видеомагазц^ для включения возврата фильмов. Спросите клиента, берет ли ои фильа напрокат или возвращает. Если фильм возвращается, удалите его из списка фильмов, взятых напрокат, и вставьте его в инвентаризацией1 ный список.
4.5 Ситуация тестирования содержит пример структуры SeqList. Студента сдают контрольные работы на стол преподавателя титульной стороной вниз (вставка в конец списка). Предположим, что взволнованный студент обнаруживает правильный ответ на какой-либо вопрос и хочет проверить, как он (или она) ответил. Преподаватель должен перевернуть стопку контрольных работ так, чтобы первая работа оказалась титульной стороной вверх, просмотреть работы, пока не будет найдена работа этого студента, и затем удалить контрольную работу из списка. После того, как студент закончит проверку работы, преподаватель вставляет ее в конец списка.
Напишите программу, которая использует класс SeqList для моделирования этой ситуации. Ассоциируйте студента с контрольной работой, используя следующую запись:
struct Test
{
char name[30];
int testNumber;
};
Цикл в main-программе управляет выполнением, читая целое:
1=Сдать контрольную работу 2=Позволить студенту проверить работу З-Воэвратить взятую работу 4=Выйти иэ программы
Выполните следующие действия:
Ввод 1: Запросите имя и номер работы; вставьте работу в список submittedTests.
Ввод 2: Запросите только имя, удалите работу из submitted-Tests и вставьте ее в список borrowedTests.
Ввод 3: Запросите имя, удалите запись из borrowedTests и вставьте ее в submittedTests.
Ввод 4: Преподаватель готов уйти, и все взятые на время работы должны быть возвращены. Удалите все элементы из borrowedTests, вставляя их в submittedTests. Печатайте окончательный список.
Вы должны определить оператор чтобы определить, равны ли две записи Test. Выполните это, используя функцию
#include <string.h>
int operator^ (const Test& tl. Tests t2) (
return strcmp(tl.naxne, t2.name) == 0,-
}
Стеки и очереди
5.1.	Стеки
5.2.	Класс Stack
5.3.	Оценка выражения
5.4.	Очереди
55. Класс Queue
5.6. Очереди приоритетов
5.7. Практическое применение: управляемое событиями моделирование
Письменные упражнения
Упражнения по программированию
В этой главе мы обсуждаем более подробно классический стек и очеред являющиеся структурами данных, сохраняющими и возвращающими эд менты из защищенных частей списка. Описывается также очередь приора тетов, модифицированная версия очереди, в которой из списка удаляет элемент с наивысшим приоритетом. Стек, очередь и очередь приоритет реализуются как классы C++. Основные идеи этой главы иллюстрируют д; практических примера. Демонстрируется действие RPN-калькулятора со ст. ком операндов. Пример обслуживания очереди клиентов кассирами в бан< показан с помощью событийного моделирования. Это приложение использу* очередь приоритетов и знакомит с важным инструментом управления в би-, несе.
5.1. Стеки
Стек является одной из наиболее используемых и наиболее важных струи тур данных. Стеки применяются очень часто. Например, распознавание сии таксиса в компиляторе, как и оценка выражений, основано на стеке. Н-нижнем уровне стеки используются для передачи параметров функциям выполнения вызова функции и возвращения из нее.
Стек (stack) — это список элементов, доступных только в одном конц! списка. Элементы добавляются или удаляются из списка только в вершин! (top) стека. Подносы в столовой нли стопка коробок являются моделям^ стека.
Стек предназначен для хранения элементов, доступных естественны*® путем в вершине списка. Представим шампур, на который нанизаны на
резанные овощи, подготовленные для шашлыка. На рис. 5.1 овощи на шампуре 1 расположены в порядке: лук, грибочек, зеленый перец и лук-Перед приготовлением шашлыка гость сообщает, что он не ест грибов, й их необходимо убрать. Эта просьба означает удалить лук (шампур 2), УДа' лить грибочек (шампур 3) и затем вновь нанизать лук (шампур 4). Если гость не любит зеленый перец или лук, это доставит повару больше пр0'
блем.
В структуре стека важнейшее место занимают операции, добавляющие 11 удаляющие элементы. Операция Push добавляет элемент в вершину стек*-Об операции удаления элемента из стека говорят как об извлечении (to р°₽ the stack) из стека. На рнс. 5.2 показана последовательность операций Pustl н Pop. Последний вставленный в стек элемент является первым удаляемы^
центом. По этой причине о стеке говорят, что он имеет порядок LIFO jj^jn/first-out) (последний пришел/первый ушел).
Рис. 5.1. Стек овощей
Абстрактное понятие стека допускает неопределенно большой список. Ло-гачески подносы в столовой могут складываться бесконечно. В действительности подносы находятся на полке, а овощи нанизаны на коротких шампурах. Когда полка или шампур переполнены, мы ие можем добавить (Push) еще один элемент в стек. Стек достигает максимального количества элементов, которыми он может управлять. Эта ситуация поясняет значение условия Полного стека (stack full). Другая крайность — вы ие можете взять поднос с пустой полки. Условие пустого стека (stack empty) подразумевает, что вы Не можете удалить (Pop) элемент. Описание ADT Stack включает только Условие пустого стека. Условие полного стека является уместным в том слу-Чае» если реализация содержит верхнюю границу размера списка.
Push А
Рис. 5.2. Помещение в стек и извлечение из него
Pop
Push D
adt stack
Данные
Список элементов с позицией top, указывающей на вершину стека. Операции
Конструктор	
Начальные значения:	Нет
Процесс:	Инициализация вершины стека.
StackEmpty	
ВХОД:	Нет
Предусловия;	нет
Процесс:	Проверка, пустой ли стек.
Выход:	Возвращать True, если стек пустой, иначе возвращать False.
Постусловия:	Нет
Pop	
Вход:	Нет
Предусловия:	Стек не пустой.
Процесс:	Удаление элемента из вершины стека.
Выход:	возвращать элемент из вершины стека.
Постусловия:	Элемент удаляется из вершины стека.
Push	
Вход:	Элемент для стека.
Предусловия:	Нет
Процесс:	Сохранение элемента в вершине стека.
Выход;	Нет
Постусловия:	Стек имеет новый элемент в вершине.
Реек	
Вход:	Нет
Предусловия:	Стек не пустой.
Процесс:	Нахождение значения элемента в вершине стека.
Выход:	Возвращать значение элемента из вершины стека.
Постусловия:	Стек неизменный.
Clearstack ‘	
Вход:	нет
Предусловия:	Нет
Процесс:	удаление всех элементов из стека и переустановка вершины стека.
Выход:	Нет	—х
Постусловия:	Стек переустановлен в начальные условия.
Конец ADT Stack	
5.2. Класс Stack
Члены класса Stack включают список, индекс или указатель на вершивУ стека и набор стековых операций. Для хранения элементов стека использует^ массив. В результате размер стека не может превысить количества элементе5 в массиве и условие полного стека является релевантным, В главе 9 снимаем это ограничение, когда разрабатываем класс Stack, используя с®я занный список.
Объявление объекта типа Stack включает размер стека, который опреде-U максимальное количество элементов в списке. Размер имеет значение Дй^молчанию MaxStackSize = 50. Список (stacklist), максимальное количе-И° ^Дементов в стеке (size) и индекс (top) являются закрытыми членами, '’операции — открытыми.
top
0	12	3	4	5
Реализация стека
Первоначально стек пуст и top = -1. Элементы вводятся в массив (функция Push) в возрастающем порядке индексов (top = 0, 1, 2) и извлекаются из стека (функция Pop) в убывающем порядке индексов (top = 2, 1, 0). Например, следующий объект является стеком символов (DataType — char). После нескольких операций Push/Pop индекс top = 2, а элемент в вершине стека — ©то stacklist[top] “ С.
Пример 5.1
Данный пример иллюстрирует целый массив из 5 элементов с последовательностью операции Push 10; Push 25; Push 50; Pop; Pop. Индекс top увеличивается на 1 при операции Push и уменьшается на 1 при операции Pop.
Спецификация класса Stack
ОБЪЯВЛЕНИЕ ^include <iostream.h> Ainclude <stdlib.h>
const int MaxStackSize - 50;
class Stack (
private:
// закрытые данные-члены. массив стека и вершина (индекс) DataType stacklist(MaxStackSize];
int top;
public:
// конструктор; инициализирует вершину Stack (void);
И операции модификации стека
void Push (const DataTypeb item);
DataType Pop (void);
void Clearstack(void):
// доступ к стеку
DataType Peek (voxd) const;
// методы проверки стека int StackEmpty(void) const; int StackFull(void) const;	11 реализация массива
);
ОПИСАНИЕ
Данные в стеке имеют тип DataType, который должен определяться с использованием оператора typedef. Пользователь должен проверять, полный ли стек, перед попыткой поместить в него элемент и проверять, не пустой ли стек, перед извлечением данных из него. Если предусловия для операции push или pop не удовлетворяются, печатается сообщение об ошибке и программа завершается.
StackEmpty возвращает 1 (True), если стек пустой, и О (False) — в противно-: случае. Используйте StackEmpty, чтобы определить, может ли выполняться операция Pop.
StackFull возвращает l(True), если стек полный, и О (False) — в противной случае. Используйте StackFull, чтобы определить, может ли выполняться one рация Push.
CiearStack делает стек пустым, устанавливая top = -1. Этот метод позволяй использовать стек для других целей.
ПРИМЕР
Объявление стека и реализация содержатся в файле astack.h.
typedef int DataType;
#include astack.h; // включить описание класса Stack
Stack S; // объявить объект типа Stack
S.Push(10);	// поместить в стек S значение 10
cout « S.PeekО « endl; // напечатать 10
// вытолкнуть 10 из стека и оставить стек пустым if (IS.StackEmpty())
temp =-S.Pop();
cout « temp « endl;
S.CiearStack();	// очистить стек
класса stack
***^^^руктор Stack присваивает индексу top значение -1, что эквивалентно Уровню пустого стека.
/инициализация вершимы стека
^taCk:'.Stack (void) : top(-l)
{ )
Операции стека. Две основные операции стека вставляют (Push) и удаляют к элемент из стека. Класс содержит операцию Реек, позволяющую кли-
выбирать данные из элемента в вершине стека, не удаляя в действительности этот элемент.
Для того, чтобы поместить элемент в стек, увеличьте индекс top на 1 н исвойте новый элемент массиву stacklist. Попытка добавить элемент в полный стек приведет к сообщению об ошибке и завершению программы. // поместить элемент в стек Uid Stack::Push (const DataTypeS item)
если стек полный, завершить выполнение (top — MaxStacksize-1)
cerr « "Переполнение стека!" exit(l);
// if
{
endl;
программы
// увеличить индекс top и копировать top++;
stacklist(top) “ item;
item
в массив stacklist
Перед вставкой элемента
—I-------------Г
После вставки элемента
item
{
}
)
top
top = top + 1
Операция Pop извлекает элемент из стека, копируя сначала значение из вершины стека в локальную переменную temp и затем увеличивая top на 1. ременная temp становится возвращаемым значением. Попытка извлечь в_ЛМент из пустого стека приводит к сообщению об ошибке и завершению
Перед операцией Pop
После операции Pop
Возвратить
Цемент из стека I /ре Stack: :Рор (void) t«p;
// стек пуст, завершить программу
if (top == -1)
I
cerr « "Попытка обращения к пустому стеку! ” << endl; exit(1);
I
// считать элемент в вершине стека temp - stacklist(top];
// уменьшить top и возвратить значение из вершины стека top—;
return temp;
I
Операция Peek в основном дублирует определение Pop с единственным ва^. ным исключением. Индекс top не уменьшается, оставляя стек нетронутым. // возвратить данные в вершине стека OataType Stack::Peek (void) const (
it если стек пуст, завершить программу
if (top == -1) (
cerr « "Попытка считать данные из пустого стека!” « endl.-exit(1);
)
return stacklistftopj; j
Условия тестирования стека. Во время своего выполнения операции стека завершают программу при попытках клиента обращаться к стеку неправильно; например, когда мы пытаемся выполнить операцию Реек над пустым стеком. Для защиты целостности стека класс предусматривает операции тестирования состояния стека.
Функция StackEnipty проверяет, является ли top равным -1. Если — да, стек пуст и возвращаемое значение — l(True); иначе возвращаемое значение — О (False).
// тестирование стека на наличие в нем данных mt Stack::StackEmpty(void) const
i
It возвратить логическое top -= -1 return top == -1;
}
Функция StackFuIl проверяет, равен ли top значению MaxStackSize-1- Еслй так, то стек заполнен и возвращаемым значением будет 1 (True); иначе, в03* вращаемое значение — О (False).
If проверка, заполнен ли стек mt Stack: :StackFuIl (void) const
return top == MaxStackSize-1; I
Метод ClearStack переустанавливает вершину стека на -1. Это восставав#11* вает начальное условие, определенное конструктором.
' I удалить все элементы из стека void Stack::ClearStack(void) (
top - -1;
Стековые операции Push и Pop используют прямой доступ к вершине и не зависят от количества элементов в списке. Таким образом, обе франки имеют время вычисления 0(1).
Применение: Палиндромы. Когда DataType является типом char, прило-*ие обрабатывает символьный стек. Приложение определяет палиндромы, яющиеся строками, которые читаются одинаково в прямом и обратном ^аяке. Пробелы не включаются. Например, dad, sees и madam im adam ° даются палиндромами, a good — нет. Программа 5.1 использует класс gt ck Для тестирования входной строки на наличие палиндрома.
Программа 5.1. Палиндромы
Эта программа читает строку текста, используя функцию cin.getline(), и вызывает функцию Deblank для удаления всех пробелов из текста. Функция Deblank копирует непустые символы во вторую строку программы, сканируя строку дважды, проверяет, является ли строка, лишенная пробелов, палиндромом. Во время первого просмотра каждый символ помещается в стек, создается список, содержащий текст в обратном порядке. Во время второго просмотра каждый символ сравнивается с элементом, который удаляется из стека. Просмотр завершается, если два символа не совпадают, в случае чего этот текст не является палиндромом. Если сравнения остаются правильными до тех пор, пока стек не будет пуст, этот текст является палиндромом.
Исходная строка = 'dad'
Палиндром
Исходная строка = 'good”
Не палиндром
^include <iostream.h>
’Pragma hdrstop
typedef char DataType; // элементы стека - символы
•include "astack.h”
олЛ°~дает НОВУЮ строку
। Ceblank(char *s, char *t)
цикл до тех лор, пока не встретится NULL-символ
*hlle(’S ! = NDLLf
если символ - не пробел, копировать в новую строку
if (*s	. • f j
*t++ = *s;
j s++»	// передвинуться к следующему символу
}	NULL;	// добавить NULL в конец новой строки
void main (void) (
const int True = 1, False = 0;
Stack S;
char palstring [80], deblankstringj80], c;
int i = 0;
int ispalindrome = True; // полагаем, что строка - палиндром
// считать в полную строку
cin.getline(palstring,80,'\n');
// удалить пробелы из строки и поместить результат в deblankstring Deblank(palstring,deblankstring);
// поместить символы выражения без пробелов в стек
i = 0;
while(deblankstringfi] !- 0) <
S.Push(deblankstring[i]);
}
// сравнение вершины стека с символами из оригинальной строки
i = 0;
while (!S.StackEmpty())
(
с = S.PopO // получить следующий символ из стека
/ / если символы не совпадают. прервать цикл
if (с 1= deblankstring [i])
{
ispalindrome - False; // не палиндром break;
}
)
if (ispalindrome)
cout « ' \”' « palstring « ' \"'
« " - палиндром” « endl;
else
cout « * \M' « palstring « ' \ « " - не палиндром” « endl;
)
/*
<3апуск 1 Программы 5.1>
madam im adam
"madam im adam*' - палиндром
<	Запуск 2 Программы 5.1>
a man a plan a canal panama
‘'a man a plan a canal panama" - палиндром
<	Запуск 3 Программы 5.1>
palindrome
"palindrome" - не палиндром
*	/
ГТпйМенение: Вывод данных с различными основаниями. Операторы вы-многих языков программирования печатают числа в десятичном формате 8 значения по умолчанию. Стек может использоваться для печати чисел Я8#лтгими основаниями. Бинарные (с основанием 2) числа описаны в главе 2, С ^полагаем, что вы можете применить рассмотренные там принципы для оснований.
Пример 5.2
В этом примере десятичные числа преобразуются в числа с заданным основанием.
1.	(Основание 8) 28ю =» 3*8+4 = 34в
2.	(Основание 4) 72 ю ж 1*64+0*16+2*4+0 ~ 10204
3.	(Основание 2) 53ю ^1*32+1*16+0*8+1*4+0*2+1 =1101012
Задача отображения на экране числа с недесятичным основанием решается с использованием стека. Мы описываем алгоритм для десятичного числа л, которое печатается как число с основанием В.
1.	Крайняя правая цифра п — это п%В. Поместите ее в стек S.
2.	Остальные цифры п задаются как n/В. Замените п на п/В.
3.	Повторяйте шаги 1-2 до тех пор, пока не будет выполнено п=0 и не останется ни одной значащей цифры.
4.	Теперь в стеке имеется новое представление N как числа с базой В. Выбирайте и печатайте символы из S до тех пор, пока стек не будет пуст.
На рис. 5.3 показано преобразование п — 3553ю в число с основанием 8. Рисунок отображает рост стека при создании четырех восьмеричных цифр для п. Завершим алгоритм выборкой и затем печатью каждого символа из стека. Выходом является число 6741.
Программа 5.2. Печать числа по любому основанию
Программа представляет функцию выхода, которая принимает неотр^ дательное длинное целое num и основание В в диапазоне 2  S и отобрази^ num на экране как число с основанием В. Main-программа запрашивав у пользователя три неотрицательных целых числа и основания, а зате> выводит эти числа с соответствующими основаниями.
^include <iostream.h> ^pragma hdrstop
typedef int DataType;
ifinclude '’astack.h”
Il печать целого num с основанием В void MultibaseOutput(long num, int B)
Stack S;
// извлечение чисел с основанием В справа налево
//и помещение их в стек S do
{
S.Push(int(num % В));
num /= В;
) while (num != 0);
while (!S.StackEmpty()) cout « S.Pop();
)
void main(void) <
long num;	// десятичное число
int В;	// основание
// читать 3 положительных числа и приводить к основанию 2 <= В <= 9 for(int i=0;i < 3;i++)
{
cout << "Введите неотрицательное десятичное число и основание " « «(2<=В<=9):
схъ. » num >> В;
cout « num « ” основание ” « В « MultibaseOutput(num. В); cout « endl; ) ) /* ___							
<3апуск программы 5-2>							
Введите неотрицательное 72 основание 4 - 1020	десятичное	число	и	основание	(2<=В<=9):	72	4
Введите неотрицательное 53 основание 2 - 110101	десятичное	число	и	основание	(2<=В<=9):	53	2
Введите неотрицательное 3С-53 основание 6 - 6741	десятичное	число	и	основание	(2<=В<=9):	3553 8	
5.3. Оценка выражений
Электронные калькуляторы иллюстрируют одно из основных применении Пользователь вводит математическое выражение с числами (операн-сТб и) и операторы, а калькулятор использует один стек для вычисления лговых результатов. Алгоритм калькулятора предусматривает ввод выра-42 я в определенном числовом формате. Такое выражение, как
е + (4*12 * 5-2J/3
держит бинарные операторы (binary operators) (+,*,/,"), операнды (operands) (8, 4, 12, 5, 2, 3) и круглые скобки (parantheses), которые создают ^„выражения. Первая часть выражения включает унарный оператор (unary operator) отрицания, который действует на один операнд (например, - 8). Хугие операторы называются бинарными, потому что они требуют двух операндов. Оператор ~ создает выражение 52 = 25.
Выражение записывается в инфиксной (infix) форме, если каждый бинарный оператор помещается между его операндами и каждый унарный оператор предшествует его операнду. Например,
-2 + 3*5
является инфиксным выражением. Инфиксный формат является наиболее общим для записи выражений и используется в большинстве языков программирования и калькуляторов. Инфиксное выражение вычисляется алгоритмом, который использует два стека для различных типов данных: один — для хранения операндов, другой — для хранения операторов. Так как класс Stack в astack.h требует единственного определения DataType, мы не можем реализовать вычисление инфиксного выражения в данном разделе. Эта тема описывается в главе 7, где определяются шаблоны и реализуется шаблон класса Stack. Этот класс позволяет использовать два или более объектов типа Stack с различными типами данных.
Альтернативной рассмотренной форме является постфиксное (postfix) представление, в котором операнды предшествуют оператору. Этот формат называется также RPN или Польской инверсной нотацией (Reverse Polish Notation). Например, инфиксное выражение "а + Ь" записывается в постфиксной форме как "а b +”. В постфиксной форме переменные и числа Годятся по мере их появления, а оператор — там, где имеются два его операнда. Например, в следующем выражении знак * вводится непосредст-енно после его двух операндов Ъ и с. Знак 4- вводится после того, как Опеются его операнды а и (Ь*с). В постфиксном формате используется при-Тет операторов. Оператор * появляется перед +.
а + ь*с - а * (b * с) Postfix: a b с ’ +
ФиксС°^КИ В ПОСТФИКСНОМ формате не обязательны. Далее следует ряд ин-выражений и их постфиксные эквиваленты.
нФиксное выражение а‘Ь + с
(bVb;c/;)/e
4 а с)/(2»а)
Постфиксное выражение а Ь*с + a b*c*d*e*f* a b c*d + е/+ Ь Ь*4а*с’ - 2а’/
?3iiM2S
Постфиксная оценка
Постфиксное выражение оценивается алгоритмом, который просматриваем выражение слева направо и использует стек. Для этого примера мы полагаем что все операторы являются бинарными. Унарные операторы мы охватываем в упражнениях.
Выражение в постфиксном формате содержит только операнды и опера. торы. Мы читаем каждый терм и в зависимости от его типа выполняем действия. Если терм является операндом, помещаем его значение в стек Если терм является оператором <ор>, дважды выполняем выборку из стека для возвращения операндов X и Y. Затем оцениваем выражение, используя оператор <ор> и помещаем результат X<op>Y обратно в стек. После чтения каждого терма в выражении вершина стека содержит результат.
Пример 5.3
Инфиксное выражение 4+3*5 записывается в постфиксной форме как 4 3 5*+. Для его оценки требуется пять шагов.
Шаги 1-3: Читать операнды 4 3 5 и помещать каждое число в стек.
Шаг1
top
Шаг 2
Push 3
ШагЗ
Push 5
4
Push 4
Шаг 4: Читать оператор * и оценить выражение выборкой верхних двух операндов 5 н 3 и вычислением 3*5. Результат 15 поместить обратно в стек.
Шаг 4
Шаг 5: Читать оператор + и оценить выражение выборкой операндов 15 и 4 и вычислением 4+15 = 19. Результат 19 поместить обратно в стек, возвратить в качестве результата полученное выражение.
top
Шаг5
Применение: постфиксный калькулятор
Мы иллюстрируем вычисление постфиксного выражения, моделируя ра-с RPN-калькулятором, имеющим операторы	и (возведение
епень). Калькулятор принимает данные с плавающей точкой и вычисляет В С ажения. Данные калькулятора и операции включаются в класс Calculator, В1>г)остая main-функция вызывает его методы. Класс Calculator содержит S сытые функции-члены, которые вводят выражение и очищают кальку-°ят0Р' Для О1*енки выражения используется ряд закрытых функций-членов.
Спецификация класса Calculator
ОБЪЯВЛЕНИЕ
впито Boolean {Fa^se, True}»
tvDedef double DataType; // калькулятор принимает числа типа double (include •'astack.h"	// включить файл с описанием класса Stack
class Calculator
I
private:
// закрытые члены: стек калькулятора и операторы
Stack S;
void Enter(double пили;
Boolean GetTwoOperands(doubles opndl, doublet opnd2); void Compute(char op);
public:
// конструктор
Calculator(void);
11 вычислить выражение и очистить калькулятор void Run(void);
void Clear(void);
};
ОПИСАНИЕ
Конструктор умолчания создает пустой стек калькулятора. Так как калькулятор работает постоянно, пользователь должен вызывать Clear для очистки стека калькулятора и обеспечения последующего вычисления нового выражения.
*в(.911€РаЦия Run позволяет вводить выражение в RPN-формате. Ввод символа Вия Эаверпгает выражение. Отображается только конечное значение выраже-
Когда0^^116 °® ошибке Missing operand (Отсутствует операнд) выводится, £Ызыв°Пе^аТ°Р Не имеет ДВУХ операндов. Попытка разделить на ноль также стек ает Со°бщение об ошибке. В любом другом случае калькулятор очищает ГОТОВИТСЯ к новому вводу.
Ru»0;
*ер Работы*
#Iculato
CALC; //Создает объект CALC
-ALC
"При
4 3
12	" оеэу„
аУЛьтат выражения 4*3
Реализация класса Calculator
Функции калькулятора выполняются рядом методов, которые позволяю^ клиенту вводить число, выполнять вычисление и печатать результат на экране Определение класса находится в файле calc.h.
Calculator::Calculator(void)
О
Метод Enter принимает аргумент с плавающей точкой и помещает его 8 стек.
//сохраняем значение данных в стеке void Calculator::Enter(double num) {
S-Push(num);
I
Функция GetTwoOperands используется методом Compute для получения операндов из стека калькулятора и присваивания их параметрам выхода орег-andl и operand 2. Этот метод выполняет проверку ошибок и возвращает значение, которое указывает, существуют ли оба операнда.
// извлекать операнды из стека и назначать из параметрам
// печатать сообщение и возвращать False, // если нет двух операндов
Boolean Calculator::GetTwoOperands(doubles opndl, doubles opnd2) (
if (S.StackEmpty<))	// проверить наличие операнда
(
cerr « "Missing operand!" « endl;
return False;
}
opndl = S.Pop();	// извлечь правый операнд
if (S.StackEmpty())
{
cerr « "Missing operand!" « endl;
return False;
)
opnd2 =» S. Pop ();	// извлечь левый операнд
return True;
}
Все внутренние вычисления управляются методом Compute, который начинается вызовом GetTwoOperands для выборки двух верхних стековых значении-Если GetTwoOperands возвращает False, мы имеем неправильные операнды и метод Compute очищает стек калькулятора. Иначе Compute выполняет операцию» указанную символьным параметром ор (’+’,	’**, и помещает результат
в стек. При попытке деления на О печатается сообщение об ошибке, и стек калы кулятора очищается. Для возведения в степень используется функция
double pow(double х, double у);
которая вычисляет ху. Она определяется в C++ библиотеке <math.h>.
// выполнение операции
void Calculator::Compute(char op) {
Boolean result;
double operandl, operandZ;
// извлечь два операнда и получить код завершения result » GetTwoOperanas(operandl, operand2);
и и if
при успешном завершении, выполнить оператор
и поместить результат в стек
иначе, очистить стек, проверять деление на 0.
(result == True)
switch(op)
case ' +':	S.Push(operand2+operandl);
break;
case ’-* =
case '**:
case '/' :
S.Push(operand2-operandl); break;
S.Push(operand2*operand1); break;
if (operandl == 0.0)
{
cerr « "Деление на ноль!" « endl;
S.ClearStack();
}
else
S.Push(operand2/operandl);
break;
case ' Л' :	S.Push(pow(operand2,operandl));
break;
)
else
S.ClearStackO;	// ошибка! очистить калькулятор
Основное действие калькулятора реализуется открытым методом Run, ко-торый выполняет оценку постфиксного выражения. Главный цикл в методе Run читает символы из потока ввода и завершается, когда считывается символ
Символы пробела игнорируются. Если символ является оператором (’+*,
***,	соответствующая операция выполняется вызовом метода Com-
pute. Если символ не является оператором, Run полагает, что проверяется первый символ операнда, поскольку поток должен содержать только операторы И операнды. Run помещает символ обратно в поток ввода, чтобы он мог быть последовательно считан как часть операнда с плавающей точкой.
// считывать и оценивать постфиксное выражение /при вводе ' =' остановиться
Calculator:: Run (void)
char c;
double newoperand;
*hile(cin » с, c
'=')
// читать до символа '=’ (Выход)
switch(c) <
case '+': case '-• : case '*';
case '/': case 'Л':
Compute(c); break;
// определение нужного оператора
// имеется оператор; вычислить его
default:
//не оператор, возможно, операнд; вернуть символ cin.putback(с);
// читать операнд и передавать его в стек cin » newoperand;
Enter(newoperand); break;
} )
// ответ, сохраняемый в вершине стека печатать с использованием Реек if (!S.StackEmpty())
cout « S.Peek() « endl; }
// очистить стек операндов void Calculator::Clear{void) {
S.CiearStack();
)
Программа 5.3. Постфиксный калькулятор
Объект CALC — это калькулятор. Первый запуск вычисляет длину гипотенузы прямоугольного треугольника со сторонами 6, 8 и 10. Два других запуска иллюстрируют обработку ошибок.
#include "calc.h"
void main (void) <
Calculator CALC;
CALC.RunO;
I
/*
«Запуск 1 программа 5.3>
88*66* + .5 A = 10
Оапуск К2 программа 5.3>
3 4 + *
Missing operand!
3 4 + 8 * = 56
Оапуск 3 программа 5.3>
10/ =
Деление на 0!
*/______________________________________________________________—
5.4. Очереди
Очередь (queue) — это структура данных, которая сохраняет элементы списке и обеспечивает доступ к данным только в двух концах списка (рис. Элемент вставляется в конец списка и удаляется из начала списка. Прилов6 ния используют очередь для хранения элементов в порядке их поступлений-
1-й	2-й	3-й	4-й		Последний	
Рис.5.4. Очередь
Начало
Конец
Добавление А
начало конец
А	В			
Добавление В
начало конец
А	В	С		
начало
конец
в	с			
Удаление А
начало конец
Удаление В
начало конец
Рис.5.5. Операции очереди
Элементы удаляются из очереди в том же порядке, в котором они сохраняются и, следовательно, очередь обеспечивает FIFO (first-in/first-out) или FCFS-упорядочение (first-come/first-aerved). Обслуживание клиентов в очереди и буферизация задач принтера в системе входных и выходных потоков принтере — это классические примеры очередей.
Очередь включает список и определенные ссылки на начальную и конечную позиции (рис. 5.5). Эти позиции используются для вставки и удаления элемента очереди. Подобно стеку, очередь сохраняет элементы параметризованного DataType. Подобно стеку, абстрактная очередь не ограничивает количе-8лементов ввода. Однако, если для реализации списка используется мас-» Ножет возникнуть условие полной очереди.
Quaua
Список элементов
tont: позиция первого элемента в очереди
 позиция последнего элемента в очереди СОцдь.
• число элементов в очереди в любое данное время
^ачальные значения: °₽оцесс:
нет
Инициализация начала и конца очереди.
QLength
Вход:
Предусловия: Процесс:
Выход:
Постусловия:
QEmpty
Вход:
Предусловия1 Процесс:
Выход:
Постусловия: QDelete
Вход:
Предусловия: Процесс: Выход:
Постусловия: Qlnsezt
Вход: Предусловия: Процесс: Выход:
Постусловия:
QFront
Вход:
Предусловия: Процесс: Выход:
Постусловия:
С leaгQueue Вход: Предусловия: Процесс:
Выход:
Постусловия: Конец ADT Queue
нет Нет определение количества элементов в очереди Возвращать количество элементов в очереди. Нет
Нет нет Проверка: пуста ли очередь.
Возвращать 1 (True), если очередь пуста и О (False) иначе. Заметьте, что это условие эквивалентно проверке, равна ли QLength 0.
Нет
Нет
Очередь не пуста.
Удаление элемента из начала очереди.
Взвращать элемент, удаляемый из очереди.
Элемент удаляется из очереди.
Элемент для сохранения в очереди. Нет
Запись элемента в конец очереди. Нет
Новый элемент добавляется в очередь
Нет
Очередь не пуста.
Выборка значения элемента в начале очереди. Возвращать значение элемента в начале очереди. Нет
Нет
Нет
Удаление всех элементов иэ очереди и восстановление начальных условий, нет
Очередь пуста.
Пример 5.4
На рис. 5.6 показаны изменения в очереди из четырех элементов время последовательности операций. В каждом случае приводится ние флажка QEmpty.
Очереди широко используются в компьютерном моделировании, тяк°* как моделирование очереди клиентов в банке. Многопользовательски операционные системы поддерживают очереди программ, ожидакчЯ^ выполнения, и заданий, ожидающих печати.
Операция
Признак пустого списка
Qlnsert (А)
Qlnsert (В)
QDelete О
Список очереди
TRUE
FALSE
FALSE
FALSE
Рис.5.6. Изменения в очереди из четырех элементов во время операций
5.5. Класс Queue
Класс Queue реализует ADT, используя массив для сохранения списка элементов и определяя переменные, которые поддерживают позиции front и rear. Так как для реализации списка используется массив, класс содержит метод Qfull для проверки, заполнен ли массив. Этот метод будет устранен, в главе 9, где представлена реализация очереди со связанным списком.
Спецификация класса Queue
ОБъяаямиж
•include <iostream.h>
•include <stdlib.h>
// максимальный размер списка очереди «««t int MaxQSize - 50;
^ss Queue
private:
! J массив и параметры очереди int front, rear, count;
DataType qlist(MaxQSize];
Public-
Конструктор cue (void);	// initialize integer data members
^.ОПеРации модификации очереди
®Insert(const DataTypeb item);
Vo'aTy₽e 0Delete(void);
Clear Queue (void);
вС°те₽ации достУпа
атУре QFront(void) const;
П метилы тестирования очереди int QLength(void) const;
int QEmpty(void) const;
int QFull(void) const;
);
ОПИСАНИЕ
Параметризованный тип DataType позволяет очереди управлять различи ми типами данных. Класс Queue содержит список (qlist), максимальный разд которого определяется константой MaxQSize.
Данное-член count содержит число элементов в очереди. Это значение так определяет, является ли очередь полной или пустой.
QInsert принимает элемент item типа DataType и вставляет его в ков очереди, a QDelete удаляет и возвращает элемент в начале очереди. Mei QFront возвращает значение элемента в начале очереди.
Очередь следует тестировать при помощи метода QEmpty перед удалени элемента и метода QFull перед вставкой новых данных для проверки, пуста очередь или заполнена. Если предусловия для QInsert или QDelete нарушают! программа печатает сообщение об ошибке и завершается.
Объявление очереди и реализация содержатся в файле aqueue.h.
ПРИМЕР
typedef int DataType;
#include aqueue.h
Queue Q;
Q.QInsert(30);
Q. QInsert(70) ;
cout «Q. QLength () «endl;
cout «Q.QFront () «endl;
if (!Q.QEmpty( ))
cout «Q. QDelete ( ) ;
cout «Q. QFront ( ) «endl;
Q/ClearQueue( );
//объявляем очередь
//вставляем 30 в очередь
//вставляем 70 в очередь //печатает 2
//печатает 30
//печатает значение 30
//печатает 70
//очистка очереди
Реализация класса Queue
Начало очереди определяется первым клиентом в очереди. Конец очереди это место непосредственно за последним элементом очереди. Когда очерв полна, клиенты должны идти к другой расчетной очереди. На рис. 5.7 по^ заны изменения в очереди и некоторые проблемы, которые влияют на pet i зацию. Предположим, очередь ограничивается четырьмя клиентами. Вид показывает, что после того, как клиента А обслужили, клиенты В и С пере* щаются вперед. Вид 3: клиента В обслужили и С перемещается вперед. ВиД клиенты D, Е и F встают в очередь, заполняя ее, а клиент G должен встать другую очередь.
Эти виды отражают поведение клиентов в расчетной очереди. После т°' как одного клиента обслужили, другие в очереди перемещаются вперед* терминологии списка, элементы данных смещаются вперед на одну позис каждый раз, когда какой-либо элемент покидает очередь. Эта модель । обеспечивает эффективную компьютерную реализацию. Предположим, ° f редь содержит 1000 элементов. Когда один элемент удаляется из | 999 элементов должны переместиться влево.
Наша реализация очереди вводит круговую модель. Вместо сдвига 31 ментов влево, когда один элемент удаляется, элементы очереди организм® | логически в окружность. Переменная front всегда является местополоясе® 1 первого элемента очереди, и она продвигается вправо по кругу по мер8 1
Вид N°1 Ввести клиентов А, В, С
Вид №2 Обслужить клиента А
А	в	С	
front			rear
В	С		
front		rear	
Вид №3 Обслужить клиента В
С			
front rear
Вид №4 Добавить клиентов D, Е, F
С	D	Е	F
front	rear
Рис. 5.7. Очередь из четырех элементов
полнеяия удалений. Переменная rear является местоположением, где происходит следующая вставка. После вставки rear перемещается по кругу вправо. Переменная count поддерживает запись количества элементов в очереди, и если счетчик count равен значению MaxQSize, очередь заполнена. На рис. 5.8 показана круговая модель очереди.
размер = 4 count = 2
Удалить А
Вставить D
Очередь заполнена размер «4 count-4
Рис. 5.8. Круговая модель очереди
Реализуем круговое движение, используя операцию остатка от деления:
Перемещение конца очереди вперед: rear = (rear+l)% MaxQSize;
Перемещение начала вперед: front — (front+l)% MaxQSize;

Пример 5.5
2 * Используем целый массив qlist (размер — 4) из четырех элементов f £ для реализации круговой очереди. Первоначально count = О, и индексы | | front и rear имеют значение О. На рис. 5.9 показана последовательность ЗжЛ вставок и удалений круговой очереди.
Вставить 5
count = 2
count = С
front = О rear = О
Вставить 3 count - 1
3			
front = 0 rear = 1
3	5
front — 0 rear = 2
Вставить 6 count ~ 3
front = О
rear = 3
Удалить 3 count = 2
	5	6	
front = 1 rear - 3
Вставить 2 count = 3
	5	6	2
rear = 0 front = 1
Удалить 5 count - 2
		6	2
rear - 0 front ~ 2
Вставить 8
count = 3
Удалить 6 count = 2
8			2
rear = 1 front ~ 3
Удалить 2 count = 1
8			
front = 0 rear = 1
rear = 1 front = 2
Удалить 8
count = 0
			
front = 1 rear = 1
Рис. 5.9. Последовательность вставок и удалений круговой очереди
Конструктор Queue. Конструктор инициализирует элементы данных front, rear и count нулевыми значениями. Это задает пустую очередь // инициализация данных-членов: front, rear, count Queue::Queue (void) : front(0), rear(0), count(0) if
Операции класса Queue. Для работы с очередью предоставляется огра00' чениый набор операций, которые добавляют новый (метод Qlnsert) или УД^ ляют (метод Qdelete) элемент. Класс имеет также метод QFront, которь01 позволяет делать выборку первого элемента очереди. Для некоторых при-’10' жений эта операция позволяет определять, должен ли элемент удаляться » списка.	е
В этом разделе описываются операции обновления очереди, вставляюсь и удаляющие элементы списка. Другие методы имеют модели в стеке®0 классе и их можно найти в программном приложении в файле aqueueJ1'
ТТеред началом процесса вставки индекс rear указывает на следующую
-пию в списке. Новый элемент помещается в это место, и переменная C°unt увеличивается на 1.
- item;
cou«t++;
После помещения элемента в список индекс rear должен быть обновлен указания на следующую позицию [Рис. 5.10 (А)]. Так как мы используем ЯЛ^Двую модель, вставка может появиться в конце массива (qlist[size-lj) с Ррренещением rear к началу списка [рис. 5.10(B)].
я Вычисление выполняется с помощью оператора остатка (%).
_ - (геаг+1) % MaxQSize;
,, вставить item в очередь
void Queue::Qlnsert (const DataType& item)
* // закончить программу, если очередь заполнена
if (count “ MaxQSize)
г.- се г г « "Переполнение очереди!” « endl; exit(1);
)
// увеличить count, присвоить значение item элементу массива
// изменить значение rear
count++;
qlist[rear] = item;
rear = (rear+1) % MaxQSize;
До Qlnsert: count = 2	После Qlnsert: count = 3
До Qlnsert: count = 2	После Qlnsert: count = 3
Рис. 5.10. Метод Qinsert
,Ггг |\г^ачия QDelete удаляет элемент из начала очереди, позиции, на которую Во индекс front. Мы начинаем процесс удаления, копируя значение t неаную переменную и уменьшая счетчик очереди.
Co'uit-.-^18	rent ] ;
ЭЛе*<6Дт^ГОВО^ Модели необходимо перенести front в позицию следующего ttont 8 СИИске» используя оператор остатка от деления (%) (рис. 5.11).
lIr°at + 1) % MaxQSize;
Значение из временной позиции становится возвращаемым значением.
QDaleta
// удалить элемент из начала очереди
//и возвратить его значение
DataType Queue::QDelete(void) <
DataType temp;
// если очередь пуста# закончить программу) <
cerr « "Удаление из пустой очереди!" « endl; exit(1);
)
// записать значение в начало очереди temp - qlist(front);
// уменьшить count на единицу
// продвинуть начало очередии возвратить прежнее значение //из начала
count—;
front = (front+1) % MaxQSize;
return temp; )
До QDelete: count - 3
(A)
front
элемент
элемент
возвратить
элемент
Рис. 5.11. Метод QDelete
возвратить
Операции QInsert, QDelete и QFront имеют эффективность О( 1), поскольку каждый метод имеет прямой доступ к элементу либо в начале, либо в коНПе списка.
Программа 5.4. Партнеры по танцу
Танцы организуются в пятницу вечером. По мере того, как мужчин^ и женщины входят в танцевальный зал, мужчины выстраиваются в одй0 ряд, а женщины — в другой. Когда танец начинается, партнеры выбИ' раются по одному из начала каждого ряда. Если в этих рядах неодинаков0® количество людей, лишний человек должен ждать следующего танца.
Данная программа получает имена мужчин и женщин, читая файл ^rice.dat. Каждый элемент данных файла имеет формат
gex Name
где Sex — это один символ F или М. Все записи считываются из файла, организуются очереди. Партнеры образуются удалением их из каждой Ядереди, Этот процесс останавливается, когда какая-либо очередь стано-°иТСЯ пустой. Если есть ожидающие люди, программа указывает, сколько
и печатает имя первого человека, который будет танцевать в следующем тайП®-
♦include «iostream.h>
♦include <iomanip.h>
♦include <fstream.h>
♦pragma hdrstop
// record that declares a dancer
struct Person
char name[20];
char sex;	// ' F' (женщина) ; (мужчина)
};
11 очередь содержит список объектов типа Person typedef Person DataType;
♦include "aqueue.h"
void main (void)
{
// две очереди для разделения на партнеров по танцу Queue maleDancers, femaleDancers;
Person p;
char blankseparator;
// входной файл для танцоров ifstream fin;
// открыть файл с проверкой на его существование fin.open("dance -dat“);
if (!fin)
{
cerr « "He возможно открыть файл!" « endl; exit (1);
//
//
, -------
*nile(fin-get(p.sex))
считать входную строку, которая включает пол, имя и возраст
fl цикл до конца файла
fin.get(blankseparator);
fin.getline(p.name,20,•\n');
•J вставить в соответствующую очередь
i£ (Р-зех =» »F')
femaleDancers.Qlnsert(p);
else
®aleDancers.Qlnsert (p) ;
Установить пару танцоров,
получением партнеров
// из двух очередей
// закончить, когда одна из очередей окажется пустой cout « "Партнеры по танцу: ” « endl « endl?
while (!femaleDancers.QEmpty() && !maieDancers.QEmpty()) (
p • femaleDancers.QDelete ();
cout « p.name « "	// сообщить имя женщины
p - maieDancers.QDelete();
cout « p.name « endl;	// сообщить имя мужчины
) cout « endl;
// если в какой-либо очереди кто-либо остался, // сообщить имя первого (первой) из них
if (!femaleDancers.QEmpty())
(
cout « "Следующего танца ожидают "
« femaleDancers.QLength()
« " дамы" « endl;
cout « femaleDancers.QFront().name
« " первой получит партнера.” « endl;
)
else if ((maieDancers.QEmpty())
(
cout « "Следующего танца ожидают "
« maieDancers.QLength[)
« " кавалера." « endl;
cout « maieDancers.QFront() .name
« ” первым получит патнера." « endl;
)
/*
<Файл "dance.dat">
м George Thompson F Jane Andrews F Sandra Williams M Bill Brooks M Bob Carlson F Shirley Granley F Louise Sanderson м Dave Evans м Harold Brown F Roberta Edwards и Dan Gromley м John Gaston
выполнение программы 5.4>
Партнеры по танцу:
Jane Andrews George Thompson Sandra Williams Bill Brooks Shirley Granley Bob Carlson Louise Sanderson Dave Evans Roberta Edwards Harold Brown
Следующего танца ожидают 2 кавалера. Dan Gromley первым получит патнера. */
Применение: использование очереди для сортировки данных. Иа заре ком-*теризации Для упорядочения стопки перфокарт использовался механи-g сортировщик. Следующая программа моделирует действие этого сор-че ррщика- Для объяснения процесса предположим, что перфокарты содер-числа из двух цифр в диапазоне 00-99, и сортировщик имеет десять ^^керов с номерами 0-9. При сортировке выполняются два прохода для 5отки сначала по позициям единиц, а затем — десятков. Каждая пер-^апта попадает в соответствующий бункер. Такая сортировка называется азрядной сортировкой (radix sort) и может быть расширена до сортировки Лисел любого размера.
Начальный список: 91 46 85 15 92 35 31 22
В проходе 1 перфокарты распределяются по позициям единиц.
Перфокарты выбираются из бункеров в порядке от О до 9.
Список после прохода 1: 91 31 92 22 85 15 35 46
В проходе 2 перфокарты распределяются по позициям десятков
Перфокарты выбираются из бункеров в порядке от О до 9.
ц^сок после прохода 2: 15 22 31 35 46 85 91 92
ХОд °СЛе Двух проходов список становится упорядоченным. Интуитивно про-К ТОМУ’ Что все перфокарты с меньшими единичными цифрами вСе ст®УЮт перфокартам с большими единичными цифрами. Например, йа 2 Ла’ заканчивающиеся на 1, предшествуют числам, заканчивающимся значений далее- Для прохода 2 предположим, что две перфокарты имеют за 3s f ne и п₽и s<^ ^ни попадают в бункер 3 в таком порядке, что (помните» что все перфокарты, заканчивающиеся на s пред-Кай£ДЬ1йб ПеРФ°каРтам> заканчивающимся на t после прохода 1). Так как ₽IFO. Ес УНКер является очередью, перфокарты покидают бункер 3 в порядке ЧеВы. и ПОсле прохода 2 перфокарты снова собрать, они будут упорядо-
Программа 5.5. Поразрядная сортировка
Данная программа выполняет поразрядную сортировку чисел из дву^ цифр. Последовательность из 50 случайных чисел сохраняется в списке представленном массивом L.	1
int L(50); //содержит 50 случайных целы:-:
Массив очередей моделирует 10 сортировочных бункеров;
Queue digitQueue[10) ;	11 10 очередей целых
Функции Distribute передается массив чисел, массив очередей digitqueu® и определенный пользователем дескриптор ones или tens для указания того выполняется ли сортировка по позициям единиц (проход 1) или десятков (проход 2).
В проходе 1 используется выражение L(i]% 10 для доступа к цифре единиц, а затем — полученное значение для передачи L[i] соответствующей очереди.
digitQueue[L[i] Я 10].QInsert(L[i])
В проходе 2 используется выражение L[i]/10 для доступа к цифре десятков, а затем — полученное значение для передачи L£i] соответствующей очереди.
digitQueue[L(i] / 10).QInsert(L(i))
Функция Collect просматривает массив очередей digitQueue в порядке цифр 0 — 9 и извлекает все элементы из каждой очереди в список L.
while 4!digitQueue[digit].QEmpty())
L[i++] = digitQueue[digit].QDelete();
Функция Print записывает числа в списке. Числа печатаются по 10 в строке с каждым числом, использующим пять позиций печати.
ilinclude <iostream.h>
frpragma hdrstop
((include "random.h" // объявление генератора случайных чисел
typedef int DataType;
((include "agueue.h"
enum DigitKind {ones,tens};
void Distribute(int L[],Queue digitQueue[],int n, DigitKind kind)
{ int i;
'—
// цикл для массива из п элементов
for (i - 0; i < n; i++} if (kind =« ones)
// вычислить цифру единиц и использовать ее как номер очереди digitQueue[L[i] ? 10).QInsert(L[i));
else
// вычислить цифру десятков и использовать ее как номер очереди digitQueue[L[i] / 10].QInsert(L(i));
собрать элементы из очередей в массив void Collect(Queue digitQueue[], int Ml) (
int i = 0, digit = 0;
z/ скзнировзть массив очер^дей^
и используя индексы Э, 1/ и т.д.
for (digit = 0; digit < 10; digit++)
// собирать элементы, пока очередь не опустеет, // копировать элементы снова в массив
while ( (digitQueue (digit] .QEmptyO )
L[i++] - oiqitQueue[digit]-QDelete();
// сканировать массив из n элементов и печатать ц в каждой строке печатать 10 элементов void Print Ar г ay (int LI],int n)
каждый элемент.
int 1=0;
while(i < n)
(
cout.width(5);
cout « L[i] ;
if (++i % 10 == 0) cout « endl;
}
cout «. endl;
void main (void)
(
// 10 десять очередей для моделирования бункеров Queue digitQueue[10];
// массив из 50 целых int L[50];
int i = 0;
RandomNumber rno;
// инициализировать массив случайными числами //в диапазоне 0-99
for (i = 0; 1 < 50; i++)
Mi] - rnd.Random(100) ;
// распределить в 10 бункеров по цифрам единиц; собрать и распечатать
istribute(L, digitQueue, 50, ones);
ollect(digitQueue, M;
FrintArray(L,50);
I!
Il распределять в 10 бункеров по цифрам десятков;
Di с°®₽ать и распечатать отсортированный массив tibute(L, digitQueue, 50, tens);
₽£^t (digitQueue,!);
j r;tntArray(i„50) ;
/*
^nyCR „
пРограМмЬ1 5 _ b>
4 U	70	20	51	11	81	21	12	52	92
ё?	12	82	62	62	72	52	83	63	23
	73	33	54	24	84	55	15	65	65
25	16	46	86	36	67	17	27	7	97
88	98	68	69	70	89	29	69	99	59
j	1	11	12	15	16	17	20	21	23
24	25	27	29	33	36	40	46	51	52
52	54	55	59	62	62	63	65	67	68
€9	69	70	72	72	73	79	81	82	82
83	84	85	86	88	89	92	97	98	99
'/
Каждый проход выполняет О(п) операций, состоящих из деления, вставки в очередь и удаления из нее. Так как выполняется два прохода, порядок алго-ритма поразрядной сортировки чисел из двух цифр составляет О(2п) и также является линейным. Поразрядная сортировка может быть расширена до сор. тировки п чисел, каждое из которых имеет т цифр. В этом случае сложность составляет О(тп), так как поразрядная сортировка выполняет т проходов, каждый включающий О(п) операций. Этот алгоритм превосходит алгоритмы сортировки, имеющие порядок О(п logan), такие как heapsort и quicksort* которые описываются в главах 13 и 14. Однако, поразрядная сортировка имеет меньшую эффективность по использованию памяти, чем эти in-place-сортиров-ки. Алгоритмы in-place-сортировок сортируют данные в оригинальном массиве и не используют временную память. Поразрядная сортировка требует использования 10 очередей. Каждая очередь имеет свою локальную память для front, rear, queue count и массива. Кроме того, поразрядная сортировка менее эффективна, если числа содержат много цифр, так как при этом возрастает произведение пт.
5.6. Очереди приоритетов
Как уже описывалось, очередь — это структура данных, которая обеспечивает FIFO-порядок элементов. Очередь удаляет самый старый элемент из списка. Приложения часто требуют модифицированной версии памяти для очереди, в которой из списка удаляется элемент с высшим приоритетом. Эта структура, называемая очередью приоритетов (priority queue), имеет операции PQInsert и PQDelete. PQInsert просто вставляет элемент данных в список» а операция PQDelete удаляет из списка наиболее важный элемент (с высшим приоритетом), оцениваемый по некоторому внешнему критерию, который различает элементы в списке. Например, предположим, компания имеет централизованную секретарскую группу для обеспечения выполнения персоналом определенных задач. Политика компании рассматривает задание, выданное президентом компании, как имеющее высший приоритет, за которым следу10'1' задания менеджеров, затем — задания супервайзеров и так далее. Должн°сТЬ человека в компании становится критерием, который оценивает относительную важность задания. Вместо управления заданиями на основе поряДкВ first-come/first-served (очередь), секретарская группа выполняем задания порядке их важности (очередь приоритетов).
Очереди приоритетов находят применение в операционной системе, которв записывает процессы в список и затем выполняет их в порядке приоритетов-
иМОР» большинство операционных систем присваивают более низкий, чем процессам, приоритет выполнению печати. Приоритет 0 часто опреде-я как высший приоритет, а обычный приоритет имеет большее значение, л^е каК 20. Например, рассмотрим следующий список задач и их приоритетов:
Задача № 1	Задача № 2	Задача № 3	Задача № 4	Задача № S
20	0	40	30	10
Порядок хранения
Задачи выполняются в порядке 2, 5, 1, 4 и 3.
Задача N? 2	Задача № 5	Задача Ns 1	Задача № 4	Задача № 3
Порядок выполнения
В большинстве приложений элементы в очереди приоритетов являются парой ключ-значение (key-value pair), в которой ключ определяет уровень приоритета. Например, в операционной системе каждая задача имеет дескриптор задачи и уровень приоритета, служащий ключом.
Уровень приоритета	Дескриптор задачи
При удалении элемента из очереди приоритетов в списке могут находиться несколько элементов с одним и тем же уровнем приоритета. В этом случае мы можем потребовать, чтобы эти элементы рассматривались как очередь. В результате элементы с одним и тем же приоритетом обслуживались бы в порядке их поступления. В следующем ADT мы не делаем никаких допущений по поводу порядка элементов с одним и тем же уровнем приоритета.
Очередь приоритетов описывает список с операциями для добавления или удаления элементов из списка. Имеется серия операций, которая определяет Длину списка и указывает, пуст ли список.
АЛТ PQueue
Данные
Список элементов.
ОпвР«ции
Конструктор Начальные значения: Процесс:	Нет Инициализация количества элементов списка
length ВХОД;	нулевым значением Нет
Предусловия: Процесс:	Нет Определение количества элементев в списке.
Вьиод; . Постусловия:	Возвращать количество элементов в списке. Нет
Вход; ПРеДУсловия: Процесс:	Нет Нет Проверка, является ли количество элементов списка равным 0.
Выход:
Постусловия:
PQInsert
Вход:
Предусловия:
Процесс:
Выход:
Постусловия:
PQDlelete
Вход:
Предусловия:
Процесс:
Выход:
Постусловия:
ClearPQ
Вход:
Предусловия:
Процесс:
Выход:
Постусловия:
Конец adt PQueue
Возвращать l(True), если в списке нет элементов.
и 0 (False) — иначе.
Нет
Элемент..для сохранения в списке -
Нет
Сохранение элемента в списке. Это увеличивает длину Слиска на 1.
Нет	„
Список имеет новые элемент и длину.
Нет
Очередь приоритетов не пуста.
Удаление элемента с высшим приоритетом из списка, это уменьшает длину списка на 1.
Возвращать элемент, удаленный из списка.
Элемент удаляется из списка, который теперь имеет на один элемент меньше.
Нет
Нет
Удаление всех элементов из очереди приоритетов и восстановление начальных условий.
Нет
Очередь приоритетов пуста.
Класс PQueue
В этой книге приводятся различные реализации очереди приоритетов. В каждом случае выделяется объект list для сохранения элементов. Мы используем параметр count и методы доступа к списку для вставки и удаления элементов. В этой главе элементы, сохраняемые в массиве, имеют параметризованный тип DataType. В последующих главах используются упорядоченные списки и динамические области для сохранения элементов в очереди приоритетов.
Спецификация класса PQueue
ОБЪЯВЛЕНИЕ ((include <iostream.h> «(include <stdlib.h>
11 максимальный размер массива очереди приоритетов const int NaxPQSize = 50;
class PQueue
(
private:
// массив очереди приоритетов и счетчик
int count;	•
DataType pqlist[MaxPQSize];
public:
// конструктор
pQueue (void);
// операции, модифицирующие очередь приоритетов void PQInsert(const DataTypeb item);
DataType PQDelete(void)t
void ClearPQ(void);
11 тестирующие методы
int PQEmpty(void) cons tr-
int PQFull(void) const;
int PQbength(void) const;
p
опислни*
Константа MaxPQSize определяет размер массива pqlist.
Метод PQInsert просто вставляет элементы в список. В спецификации не лается никаких допущений о том, где элемент помещается в списке.
ДВ МеТ°Д PQDelete удаляет элемент с высшим приоритетом из списка. Мы полагаем, что элемент с высшим приоритетом — это элемент с наименьшим значением. Наименьшее значение определяется с использованием оператора сравнения который должен быть определен для DataType.
дрИМЕР
typedef int DataType;
PQueue PQ;
PQ.PQlnsert (20) ;
₽Q. PQInsert (10);
cout « PQ.PQLengthO « endl; // печать 2
н =* PQ.PQDeleteO;	// извлечь N - 10
Реализация класса Pqueue
Операции очереди приоритетов. Подобно обычной очереди, очередь приоритетов имеет операцию для вставки элемента. ADT не делает никаких допуще-ний о том, в какое место списка помещается элемент, оставляя этот вопрос в качестве детали реализации в методе PQInsert. В данном случае мы сначала тестируем, заполнен ли список, и завершаем программу при этом условии. В противном случае, новый элемент вставляется в конец списка, местоположение которого указывается с помощью счетчика (count).
До PQInsert: count = 4
После PQInsert: count = 5
20
40
10
30
50
Вставить 50
pqlist[4]
y°id ₽0цецЬ ,ЭЛемеиф ь очередь приоритетов
(	*•:PQInsert (const DataTypeb item)
saKotZ*6 све элементы массива pqlist использованы, (cftM^^*1Tb завершить программу
ount -- MaxPQSize)
if (
exitqj ^еРеп°лнение очереди приоритетов!" « endl;
}
// поместить элемент в конец списка
//и увеличить count на единицу
pqlist(count) • item; count++;
}
Метод PQDelete удаляет из списка элемент с высшим приоритетом. эГо условие не подразумевает, что мы выбираем первый элемент, когда имеются два или более элементов, как и не подразумевает, что элементы сохраняю*} какой-либо порядок во время процесса удаления. Это все — детали реалц. зации. В данном случае мы сначала определяем, является ли список пустым и завершаем программу, если условие является равным True. В противно^ случае, мы ищем минимальное значение и удаляем его из списка, уменьшая длину очереди (count) и заменяя этот элемент последним элементом в списке Индекс последнего элемента становится новым значением count.
В следующем примере минимальное значение (10) имеет элемент с индексом 2. Метод удаляет этот элемент, уменьшая длину списка на 1 и заменяя его последним элементом в списке (pqlist[countj). Затем удаляется 15, кото-рый находится в конце списка.
count=5
Минимальный элемент с индексом 2
Удалить 10
pqlist{2]
count=4
Минимальный элемент заменить последним
20	40	50	15		
Вставить 50
count=4
Минимальный элемент с индексом 3
Удалить 15
pqlist[3]
count=3
Минимальный элемент заменить
20	40	50		1	
PQDelete
// удаляет элемент из очереди приоритетов
// и возвращает его значение
DataType PQueue::FQDelete(void)
{
DataType min;
int i, minindex = 0;
if (count > 0)
{
// найти минимальное значение и его индекс в массиве pqlist
min = pqlist(0);	// предполагаем, pqlist[0] - это минимум
// просмотреть остальные элементы
// изменяя минимум и его индекс
for (i = 1; i < count; i++)
if (pqlist[i] < min)
{
// новый минимум в элементе pqlist(ij. rain ® pqlist(i);
новый индекс - i
minindex = i;
}
// переместить хвостовой элемент на место минимального
//и уменьшить на единицу count
pqlist(iuinindex] = pqlist[count-1J;
count- ;
массив qlist пуст, завершить программу else
c cerr « "Удаление из пустой очереди приоритетов!" « endl; exit(l);
// возвратить минимальное значение return min;
1
Операция PQInsert имеет время вычисления (порядок) 0(1), так как она веСосредственно добавляет элемент в конец списка. С другой стороны, операция PQDelete требует начального просмотра списка для определения минимального значения и его индекса. Эта операция имеет время вычисления О(п ), где п — это текущая длина очереди приоритетов.
Давнсе-член count содержит количество элементов в списке. Это значение используется в реализации методов PQLength, PQEmpty и PQFulL Код этих методов находится в программном приложении в файле apqueue.h.
Приложение: службы поддержки компании
Сотрудники компании определяются по категориям: менеджер, супервайзер и рабочий. Создав тип enum с различными категориями, мы имеем естественное упорядочение, дающее каждой уровень приоритета при заявке на выполнение работ.
//уровень приоритета сотрудника (менеджер = 0, и т.д.)
enum Staff [Manager, Supervisor, Worker}; // Менеджер = С, и т. д.
Работа службы поддержки компании выполняется общей секретарской группой. Каждый сотрудник может сделать заказ на выполнение задания, ^полнив форму, которая включает информацию о категории служащего, поза заявку на выполнение работы, ID-номер задания, и указывая время, JobR70^06 ^Удет ВЬ1полнена работа. Эта информация сохраняется в записи При ~ ®аявки на выполнение работ вводятся в очередь приоритетов с ВоЛЬв^ТеТ°М’ опРеДеляемым по категории сотрудника. Это упорядочение ис-уется, чтобы определить операцию < для объектов типа Jobliequest.
strSy*T?Pa' определяющая запрос
( ct c°bRequest
В^“£Рег8оп;
i, lnt Зонте-
<_.^ьУх сгч» Ог|ератора < для сравнения
{ °P«ratorKTOB JobRe4^est
* (const JobRequestfc a, const JobRequest& b)
J Cet***n a.sl.A„
catfPerson < b.staffPerson;
Файл job.dat содержит список заявок на выполнение заданий, которЬ1 i загружаются в очередь приоритетов. Приложение подразумевает, что заяв> помещены заранее и ожидают выполнения. Элементы извлекаются из очеред приоритетов и выполняются. Массив jobServicesUse содержит общее кодиче ство времени, потраченное на обслуживание каждого из различных тиц0 сотрудников:	6
//время, потраченное на работу для каждой категории сотрудников int jobServicesUse[3] = (0, 0, 0);
Функции печати PrintJoblnfo и PrintsupportSummary выдают информа цию о каждом задании и об общем количестве минут, потраченных на од служивание каждой категории служащих в компании:
// печать одной записи структуры JobRequest viod PrintJobinfo(JobRequest PR)
{ switch (PR.staf(Person) {
case Manager: cout « Manager ; break;
case Supervisor: cout « Supervisor ; break;
case Worker: cout « Worker ; break;
}
((include <iomanip.h>
// печать общего времени работы,
// выделенного каждой категории служащих
void PrintJobSunmery(int jobServiceUseI]) (
cout « ХпВремя обслуживания no категориямЧп;
cout « Manager « setw(3) «
cout « jobServicesUse[0] << endl;
cout « Supervisor « setw(3) «
cout « jobServicesUse!11 « endl;
cout « Worker « setw(3) «
cout « jobServicesUse[2] « endl;
Программа 5.6. Выполнение заданий
Каждая заявка на выполнение работы сохраняется как запись в файле job.dat. Эта запись задает категорию сотрудника (’М’, ’S‘, ’W’) и ID-номер задания, и время для выполнения. Записи читаются, пока не достигается конец файла, и каждая запись вставляется в очередь приоритетов jobPool-На выходе каждое задание извлекается из очереди приоритетов, И еГ° информация печатается функцией PrintJoblnfo. Программа завер1иае'<' печатью резюме по выполненным заданиям вызовом функции Printjob Summary. Структура JobRequest и функции работы с ней находятся л файле job.h.
tinclude <iostream.h> ((include <fstream.h> ((pragma hdrstop
((include “job.h”
. зЛеыенты очереди приоритетов имеют тип JobRequest typedef JobRequest DataType;
finclude "apqueue.h"	// включить класс PQueue
vOid main ()
1	// обрабатывает до 25 заданий
PQueue jobPool;
« // требуемые задания читаются из потока // с дескриптором fin ifstream fin;
// время облуживания каждой категории служащих int jobServicesUse(3] = {0r О, 0);
jobRequest PR;
char ch;
// открыть файл job.det для ввода
// при ошибке завершить работу программы fin.open("job.dat", ios::in I ios::nocreate); if (!fin) (
cerr « "Невозможно открыть файл job.dat" « endl; exit(1);
)
// читать' файл.
// вставлять каждое задание в очередь приоритетов jobPool. // каждая строка начинается с символа, характеризующего // служащего while (fin » ch) {
If полю staffPerson присвоить категорию служащего switch(ch) (
case *Mr :	PR.staffPerson « Manager;
break;
case 'S':	PR.staffPerson - Supervisor;
break;
case 'W':	PR.staffPerson - Worker;
break;
default:	break;
)
// читать идентификатор задания
// и поле jobTime (время на выполнение задания) fin » PR.jobid;
fin » PR. jobTime;
ff вставить задание в очередь приоритетов j JObPool.PQInsert(PR) ;
fI ^алять задания из очереди приоритетов
Cout Печ'1тать эту информацию
" Категория Номер Время\п\п";
( е (!j obPool.PQEmpty())
PR - 
p1ObPool.PQDelete();
^int Jobinfo (PR) ;
Накапливать время для выполнения заданий
fl каял/уЛ категории служащих
jobServicesUse[int(PR.staffPerson)] +~ PR.jobTime;
PrintJobSummary(jobServicesUse); )
/*
<Входной файл job.dat>
м	300	20
W	301	30
м	302	40
S	303	10
S	304	40
м	305	70
W	306	20
W	307	20
м	308	60
S	309	30
Оапуск программы 5.6>
категория	Номер	Время
Manager	300	20
Manager	302	40
Manager	308	60
Manager	305	70
Supervisor	309	30
Supervisor	303	10
Supervisor	304	40
Worker	306	20
Worker	307	20
worker	301	30
Время обслуживания по категориям
Manager	190
Supervisor 80
Worker 70
*/
5.7. Практическое применение: управляемое событиями моделирование
Задачей моделирования является создание модели реальной ситуации Длй лучшего ее понимания. Моделирование позволяет вводить различные услов^я и наблюдать их результаты. Например, имитатор полета побуждает летчиц реагировать на неблагоприятные условия и измеряет скорость и соответств реакции. При изучении рынка сбыта моделирование часто используется измерения текущей деятельности или оценки расширения бизнеса. В эТ разделе рассмотрены модели прихода и ухода клиентов банка по мере как они проходят через одну из п>2 очередей к кассиру. В заключеЯ^ оценивается эффективность обслуживания вычислением среднего вре1*е ожидания каждого клиента и времени занятости каждого кассира в ПРОЙ
Цспользуя моделирование, мы можем создать различные модели обслу-50х' и оценить их с точки зрения стоимости и эффективности. Такой && ой выполняет управляемое событиями моделирование (event-driven p0^1ation), которое определяет объекты для представления банковской де-^'пости- Мы используем вероятностные значения, описывающие различ-гТ&Я^сидаемые частоты прихода клиентов и различное ожидаемое время ^^тживания клиента кассиром. Моделирование позволяет использовать ге-°&СЯелп случайных чисел для отражения прихода и ухода клиентов в течение ^чего ДНЯ в банке.
Моделирование позволяет также изменять параметры и таким образом мерять относительное влияние на обслуживание, если мы изменяем пове-г3яиеклиента или кассира. Например, предположим, что отделение марке-Д яга считает, что поощрительные подарки увеличат приход клиентов на 90% Исследование с использованием моделирования повысит ожидаемые темпы прихода клиентов и эффективность работы кассира. В некоторый момент' банку понадобятся дополнительные кассиры для поддержания приемлемого обслуживания, а дополнительный расход может свести к нулю прибыль от поощрительной кампании. Это моделирование снабжает банковского ^неджера параметрами для оценки обслуживания клиентов. Если среднее время ожидания будет слишком долгим, менеджер может использовать еще одного кассира. Это моделирование можно повторять снова и снова, просто изменяя условия.
Разработка приложения
По мере выполнения приложение отслеживает приход и уход отдельных клиентов. Например, предположим, 50-й клиент приходит в банк в 2:30 (приход) для получения вида услуг, требующего 12 минут времени кассира. Данное моделирование подразумевает, что каждый кассир наглядно предоставляет график своей работы, так что новый клиент может определить, кто из кассиров его будет обслуживать и когда может начаться обслуживание.
В нашем случае предположим, что Кассир 2 свободен, что позволяет кли-еатУ немедленно выбрать этого кассира, подойти к окну, совершить операцию Покинуть банк в 2:42 (уход). Этот клиент имеет время ожидания 0, и ссир 2 имеет 12 дополнительных минут работы.
- time (arrival) + servicetime
. 2:30 . 0;12 . 2:42
Событие
Время
Приход
—*—
Уход
Время прихода
Время ухода
Обслуживание
Рассмотрим различные обстоятельства для 50-го клиента. Предположим что два кассира заняты, и клиент должен занять место в очереди и ожидать обслуживания. Если Кассир 1 освобождается в 2:40, а Кассир 2 будет свобод в 2:33, новый клиент выбирает Кассира 2. После 3 минут ожидания и 12 минут совершения операции клиент уходит в 2:45, что близко ко времени когда освобождается Кассир 2.
time(departure) = time(arrival) + waittine + servicetime time(departure) = 2:30 + 0:03 +0:12 = 2:45
В нашем моделировании клиент выбирает кассира, который затем обновляет на окне вывеску следующего свободного времени для обслуживания.
До прихода клиента	После прихода клиента
Ключевыми компонентами в этом моделировании обслуживания в банке являются события, включающие как приход, так и уход клиента, а реальн0® время рассматривается как последовательность случайных событий, когорт отражают приход, обслуживание и уход клиента. Событие объявляется Н класс C++ с закрытыми данными-членами, которые определяют и клиеН ' и кассира, а также поддерживают информацию о времени появления типе события (приход или уход), продолжительности обслуживания клйеВ
лйчестве времени, которое клиент вынужден провести в очереди, ожидая ^уживания.							
	time	etype	customerlD	tellerlD	waittime	servicetime	
Когда клиент входит в банк, мы имеем событие прихода (arrival event), ^адример, если клиент 50 приходит в 2:30, соответствующей записью будет:
Данные события прихода
2:30	Приход	50	—	—	—
time etype customerlD tellerlD waittime servicetime
Поля tellerlD, waittime и servicetime не используются для события прихода. Эти поля определяются после того, как генерируется событие ухода.
После того, как клиент проверит, свободен ли кассир, мы можем определить событие ухода (departure), которое описывает, когда клиент покинет банк. Этот объект может использоваться для преобразования истории клиента в банковской системе. Например, далее следует событие ухода для клиента, который приходит в 2:30, ожидает 3 минуты Кассира 2 и уходит после 12-мииутного обслуживания:
Данные события ухода
2.45	Уход	50	2	3	12
time etype customerlD tellerlD waittime servicetime
Поля данных объекта описывают всю соответствующую информацию, ко-тЧрая относится к потоку клиентов в системе. Методы класса обеспечивают К полям данных, используемым для сбора информации об общей Ффективности обслуживания.
£2£^фикация класса Event
’delude <iO3trearo-h> enum ?ndcm-h
^епПуре (arrival> departure};
Jla’8 Event
Private;
ННые“члены
EvBntime'’	If время события
тУРе etype; // тип события
lnt customerlD;	//	номер	клиента
int ef10;	И	номер	кассира
int	ai*'time;	/ /	время	ожидания
₽Wilic:	Crvicet ime; //	время	обслуживания
Event(void);
Event(int t, EventType et, int cn, int tn, int wt, int st); int GetTime(void );
EventType GetEventType(void); int GetCustomexID(void) const,-int GetTellerlD(void) const; int GetWai tTime(void)cons t; int GetServiceTime(void)const; };
ОПИСАНИЕ
Конструктор умолчания позволяет объявлять объект Event, чтобы позх^ можно было инициализировать (присваиванием) его данные-члены. Втор^ конструктор позволяет задать каждый параметр при объявлении события Остальные методы возвращают значения данных-членов.	j
ПРИМЕР
Время задается в минутах от начала выполнения моделирования.
Event е; // объявление события конструктором умолчания // клиент 3 уходит в 120 минут. После 10 минут ожидания, //клиент затрачивает 5 минут на совершение операции, е = Event(120, departure, 3, 1, 10, 5); соиt«е.GetService{);	//выводится время обслуживания 5
Информация моделирования
Во время выполнения моделирования мы накапливаем информацию о каждом кассире, указывающую общее количество клиентов, которых обслужил кассир, время, которое клиенты провели в ожидании и общее время обслуживания клиентов кассиром в течение дня. Вся эта информация накапливается в записи TellerStats, содержащей также поле finishService, представляющее значение вывески в окне.
Запись TellerStats
finishservice	totalCustomerCou nt	totalCustomerWait	totalService
//Структура для информации о кассире
struct TellerStats <
int finishservice;	//когда кассир может обслуживать
int totalCustomerCount; //общее количество обслуженных клиентов
int totalCustomerWait;	//общее время ожидания обслуживания клиентами
int totalservice;	//общее время обслуживания клиентов
1;
При моделировании генерируется событие прихода и ухода для кажДоТ° клиента. Время всех событий отмечается, и они помещаются в очередь СР0 оритетов. Событие с высшим приоритетом в очереди приоритетов — э событие с самой ранней отметкой времени. Структура списка позволяет УД^ лять события, так чтобы мы перемещались в возрастающей временной ° ледовательности с приходом и уходом клиентов.	~
Моделирование обслуживания в банке использует генератор случай* чисел для определения следующего прихода клиента и времени текуЯ** обслуживания клиента. Генератор обеспечивает то, что любой резуЛЬтаТ
зоне значений одинаково возможен. Если при моделировании текущее прихода возникает в Т минут, следующий приход происходит про-^^тгъно в диапазоне от T+arrivalLow до T+arrivalHigh минут, а общее время иваНИя, необходимое для клиента, находится в диапазоне от serv-w до serviceHigh минут. Например, предположим, что некоторый клиент пит в 2 минуты и диапазон времени прихода следующего клиента со-^^ляет от arriveLow=6 до arriveHigh=14 минут. Существует вероятность cT^J0ro, что будет наблюдаться любой из результатов 6, 7, . . . , 14 минут.
Текущий приход Следующий приход * *
arrivalLow=4
arrivalLow=12
Если мы знаем что, последующее событие (приход другого клиента) будет в 9 начнут, можно создать это событие и поместить его в очередь приоритетов для будущей обработки.
Данные для моделирования и методы, реализующие эту задачу, содержатся в классе Simulation. Данные-члены включают продолжительность (length) моделирования (в минутах), количество кассиров, номер следующего клиента, массив записей TellerStats, содержащих информацию о каждом кассире, и очередь приоритетов, которая содержит список событий. Класс также содержит границы диапазонов для следующего прихода и для текущего обслуживания клиента.
Спецификация класса Simulation
ОБЪЯВЛЕНИЕ
class Simulation {
private;
// данные для моделирования
int simulationLength;
int numTellers;
int nextCu stonier;
int arrivalLow, arrivalHigh;
int serviceLow, serviceHigh;
Tellerstats tstat[ll];
PQueue pq;
RandomNumber rnd;
// продолжительность моделирования
// число кассиров
// номер следующего клиента
// диапазон прихода нового клиента
// диапазон обслуживания
// максимальное число кассиров - 10
// очередь приоритетов
// использовать для времени прихода
// обслуживания
/закрытые методы, используемые функцией RunSimulation nt NextArrivalTime(void);
- Ge^ServiceTime (void);
Public- NextAvailableTeller (void) ;
8^?НСТ₽УКТ°Р SUWulation(void) .
^Simulation (void) ;
'	RrintSiinulationResults (void);
3as-125
Конструктор инициализирует массив типа TellerStats и член класса пех tCustomer, который начинается с 1. Индекс О в массиве tstat не используется Таким образом, номер кассира является и индексом в массиве tstat.
Конструктор запрашивает пользователя ввести данные для задачи иссде довательского моделирования. 'Данные включают продолжительность мод/ лирования в минутах, количество кассиров и диапазон времени прихода времени обслуживания. Класс предусматривает до 10 кассиров.
Для каждого события прихода мы вызываем метод NextArrivalTime, чтобы! определить, когда в банк придет следующий клиент. В то же время мы вызыi ваем GetServiceTime, чтобы определить, как долго будет продолжаться текущ^ обслуживание клиента кассиром, и метод NextAvailableTeller для определена < кассира, который будет обслуживать клиента.
Метод RunSimulation выполняет задачу исследовательского моделирования a PrintSimulationResuits выводит окончательную статистику.
Установка параметров моделирования
Конструктор инициализирует данные и запрашивает у пользователя параметры моделирования.
Simulation::Simulation(void) <
int i;
Event firstevent;
// инициализация информационных параметров кассиров for(i = 1; i <= 10; i++) {
tstat[i].finishservice  0;
tstat[i].totalservice = 0;
tstat[i].totalCustomerWait = 0;
tstat[i].totalCustomerCount = 0; } nextcustomer =1;
cout « "Введите время моделирования в минутах: cin » simulationLength;
cout « "Введите число кассиров банка: cin » numTellers;
cout « "Введите диапазон времени приходов в минутах:
cin » arrivalbow » arrivalHigh;
cout « "Введите диапазон времени обслуживания в минутах: cin » servicebow » serviceHigh;
// генерить событие первого прихода клиента pq.PQInsert(Event(0,arrival,1,0,0,0));
)
Выполнение задачи моделирования
Продолжительность моделирования (simulationLength) используется определения, следует ли генерировать событие прихода для другого Если банк будет закрыт в планируемое время прихода нового клиента CarrLJle time>fiimulationLength), новые клиенты не принимаются, и моделиров*^ завершается обслуживанием клиентов, остающихся в банке. Данное-члеЯ в tCustomer — это счетчик, ведущий запись количества клиентов.
Метод NextArrivalTime возвращает интервал времени до прихода следу-™ клиента. Метод использует генератор случайных чисел и параметры и arrivalHigh для генерирования возвращаемого значения. Сход-летод GetServiceTime использует параметры serviceLow и serviceHigh елелить случайное время следующего прихода
// Simulation::NextArrivalTime(void)
1 return arrivalLow+rnd. Random(arrxvalHigh-arrivalLow-» 1) ;
оеделить случайное время обслуживания клиента ^Simulation:: Get Ser viceTime (void)
I return serviceLow+rnd.Random(serviceHigh-serviceLow+1);
}
В этой задаче клиент приходит, смотрит на окно кассира и читает вывеску, оказывающую, когда каждый кассир будет свободен. Исходя из этой информации, клиент выбирает кассира, который предоставит ему банковские услуги. Значение данных finishService в записи каждого кассира представляет вывеску, висящую в окне. Функция NextAvailableTeller просматривает массив кассиров и возвращает номер кассира с минимальным значением finishService. Если все кассиры будут заняты вплоть до времени закрытия банка, клиенту назначается произвольный кассир.
// возвратить номер первого доступного кассира int Simulation: :NextAvailableTeller(void) (
// вначале предполагается, что все кассиры
// освобождаются ко времени закрытия банка
int minfinish = sxmulationLength;
// назначить случайного кассира клиенту, пришедшему
// до закрытия, но имеющему возможность получить обслуживание
// после закрытия банка
int min finishindex = гnd.Random(numTeIlers) + 1;
// найти кассира, который освобождается первым
for (int i = 1; i <= numTellers; i++)
if (tstat(i).finishService < minfinish) {
minfinish = tstat[i].finishService;
minfinishindex = i; }
) return minfinishindex;
Ro^XHaa ФункЧия в задаче моделирования — это метод RunSimulation, Уп£авляет очередью приоритетов событий. Первоначально очередь С ат еДИнственное событие прихода, возникающее, когда банк открывается. ° Момента и далее процесс является непрекращающимся. Метод Run-1етой т °п является циклом, который извлекает события из очереди приори-чрц &к°й цикл определяется как событийный (event loop), и он завершается есТеЧевТ°^ ОЧе₽еДи- Кассиры банка продолжают обслуживать клиентов после в₽емени работы банка (time>simulationLength) с учетом того, что до закрытия банка. В заключительном резюме simulation-^ejOr< °бй°вляется, чтобы предоставить время ухода последнего клиента, с йеРеМеЛ.ОГо» Что этот уход наступает после времени закрытия банка. Если е является объектом Event, содержащим последнее событие дня,
e.GetTime() возвращает время события, а продолжительность задачи модель рования вычисляется с использованием оператора;
simulationLength = (е.GetTime () <=simulationLength)
? SimulationLength : e.GetTime ();
ПРИХОД
1. Объект вновь пришедшего клиента отвечает за генерирование следуй щего события прихода, которое затем помещается в очередь приоритет^ для последующей обработки. Если следующий приход должен насту, пить после завершения задачи моделирования, это событие не учиты. вается.
//вычисление времени следующего прихода, nexttime = e.GetTiraef) + NextArrivalTime (); if (nexttime > simulationLength)
//обрабатывать события, но новые не генерировать continue;
else <
//генерировать приход следующего клиента и помещать в queue nextCustomer++;
newevent = Event(nexttime, arrival, nextCustomer, 0, 0, 0);
pq.PQInsert(newevent);
)
2. После создания следующего события прихода обновляется информация TellerStats, а затем создается событие ухода. Мы начинаем с инициализации значения servicetime, которое определяет количество времени, необходимого на обслуживание текущего клиента, и затем определяем первого кассира, свободного для обслуживания следующего клиента.
//время, затрачиваемое на клиента servicetime = GetServiceTime(); //кассир, который обслуживает клиента tellerlD - nextAvailableTeller();
Теперь рассмотрим поле finishService кассира, который будет работать с клиентом. Если finishService не 0, то это — время, когда кассир освобождается для обслуживания другого клиента. Если значение Пи-ishService равно 0, то кассир свободен. В этом случае установим fin* ishService на текущее время, чтобы отразить тот факт, что кассир буДеТ теперь обслуживать клиента. Определим время, в течение которого клиент должен ожидать (waittime), вычитая текущее время из finishService*
//если кассир свободен, заменить время на вывеске на текущее время if (tstat[tellerlD].finishService == 0)
tstat[tellerlD].finishService = e.GetTime();
//вычислить время, когда клиент ожидает, вычитая //текущее время из времени на вывеске кассира waittime = tstat[tellerlD].finishService - e.GetTime();
Этими значениями мы обновляем информацию TellerStats для кассир*’ который обслуживает клиента. Поле finishService увеличивается время обслуживания, необходимое для текущего клиента. Оно теп' содержит время, когда кассир закончит обслуживание всех его < текущих клиентов.
//обновлять статистику кассира
tstat[tellerlD].totalCustomerWait +=waittime;
tstat (tellerlD) . totalCustomer€ount++;
tstat [tellerlD] . totalServi.ee +=servicetime;
tstat( teller ID] .finishservice *=servicetime;
Конечная задача включает определение события ухода и помещение 3 его в очередь приоритетов. Мы имеем все необходимые параметры для
создания события. Элемент данных события	Передаваемый параметр
time etype customerlD tellerlD waittime servicetime	tstatftellerlD]. finishService departure e.GetCustomerlDO tellerlD waittime servicetime
newevent = Event(tstat[tellerlD] . finishservice, departure, e.GetCustomerlD(),tellerlD, waittime, servicetime);
pq.PQInsert(newevent);
УХОД
Событие ухода дает нам доступ к истории деятельности клиента во время нахождения в банке. В задаче исследовательского моделирования эта информация может выводиться на экран. Для события ухода мы должны обновить поле finishService, если у кассира нет других клиентов. Это наступает, если текущее значение finishService равно времени ухода. В этом случае finishService устанавливается на О.
tellerlD = e.GetTellerlDQ ;
//если никто не ждет кассира, отметить, что кассир свободен
if (e.GetTime () == tstat[tellerlD] .finishService)
tstat[tellerlD] .finishService = 0;
Резюме задачи моделирования. Для завершения моделирования вызываем Функцию PrmtSimulationResuIts. Она печатает резюме данных о клиентах и отдельных кассирах. Данные собираются из записей TellerStats, содержащих ^формацию о количестве клиентов, которых обслужил каждый кассир, и Уммарное время ожидания клиентами обслуживания.
for (< — ч
{	“ K=numTellers; I++)
CumwUfCOrners += tstat[i) - totalcustomercount;
I ait + tstat[i]. totalCustomerWait;
зая^°Вчательное резюме дает среднее время ожидания клиента и время coat °СЗД Каждого кассира в процентах.
c*ut
c°Vtt ****** Simulation Summary ******** « endl;
?mulation of « simulationLength
cQut Mnutes « endi-
a°ttt °' °f Customers: « cumCustorners « endl;
c^Cu6tWaitVerage Customer Wait:;
= float (cumWait)/cumCustorners + 0.5;
ev9CustWait « minutes « endl;

(
cout « " Teller i " « i « % Working: ;
//отображать значение в процентах, округленное до ближайшего целого tellerwork • float(tstat[i].totalServxce)/simulationLength;
tellerWorkPercent  tellerwork * 100.0 +0.5;
cout « tellerWorkPercent « endl;
}
Пример задачи моделирования. Main-программа определяет объект моде. лирования S и затем выполняет цикл event с использованием метода Simulation. После завершения цикла event вызывается метод PrintSimula. tionResults.
Пример 5.6
Мы следим за задачей моделирования после определения этих параметров:
simulationLength = 30 (минут) numTellers= 2
arriveLow = 6	arriveHigh — 10
serviceLow — 18	serviceHigh = 20
Клиент 1 приходит в 0 минут
Приход
customerlD 1 tellerlD -waittime -servicetime -
0
Клиент 1 генерирует событие прихода для клиента 2 в 7 минут и генерирует свое собственное событие ухода в 19 минут
Приход	Уход
				
	customerlD 2		customerlD 1	
	tellerlD -		tellerlD 1	
	waittime -		waittime 0	
	servicetrme •		servicetime 19	
				
Клиент 2 приходит в 7 минут.


Уход
customerlD 1 tellerlD 1 waittime О servicetime 19
19
Клиент 2 генерирует событие прихода для клиента 3 в 16 минут и генерирует свое событие ухода в 25 минут.
Приход
		
Г	customerlD 3 tellerlD -waittime -servicetime •	
	-	
16
Уход
		
	customerlD 1 tellerlD 1 waittime 0 servicetime 19	-
X. 7-'*-		
19
Уход
		
	customerlD 2 tellerlD 2 waittime 0 servicetime 18	
		
25		
♦
Клиент 3 приходит в 16 минут.
Клиент 3 генерирует событие прихода, которое наступает после закрытия банка, что прекращает создание событий прихода. Клиент 3 должен ожидать 3 минуты кассира 1 и генерирует свое событие ухода в 37 минут.
Уход
Уход
customerlD 2
tellerlD 2 waittime О
servicetime 18
События ухода удаляются из очереди приоритетов в таком порядке: клиент 1 — в 19 минут, клиент 2 — в 25 минут и клиент 3 — в 37 минут.
Уход	Уход	Уход
Банк закрывается после
37 минут обслуживания.
5.7 Код и выполнение
£>., *-------------—-----------------------—---------—--------—
ству^,п^ог₽амма запускается дважды. Данные первого запуска соответ-^итеп. дааным в примере 5.6. Во втором запуске используется продол-ResuutB0CTb заДачи 480 минут (8 часов). Метод вывода PrintSimuIation-
УКазь1вает количество постоянных клиентов (patrons), среднее Хасс^/^ИДания для каждого клиента и общее время занятости каждого 8	* ®>еализаДия классов Event, TellerStats и Simulation содержится
#include ’’sim.h’'
void main(void)
(
// S - объект для моделирования Simulation S;
// запустить моделирование
S.RunSimvlation()i
// печатать результаты
S.PrintSimulationResults();
)
/*
<Прогон 1 программы 5.7>
Enter	the	simulation time in minutes: 30	
Enter	the	number of bank tellers: 2	
Enter	the	range of arrival times in minutes:	6 10
Enter	the	range of service times in minutes:	18 20
Time;	0	arrival of customer 1	
Time:	7	arrival of customer 2	
Time:	16	arrival of customer 3	
Time:	19	departure of customer 1	
Teller 1 Wait 0 Service 19
Time: 25 departure of customer 2
Teller 2 Wait 0 Service 16
Time: 37 departure of customer 3
Teller 1 Wait 3 Service 18
******** Simulation Summary ******** Simulation of 37 minutes
No. of Customers: 3
Average Customer Wait: 1 minutes
Teller #1 % Working: 100
Teller #2 % Working: 49
<Прогон 2 программы 5.7>
Enter the simulation time in minutes 480
Enter the number of bank tellers 4
Enter the range of arrival times in minutes 2 5
Enter the range of service times in minutes 6 20
<arrival and departure of 137 customers>
******** simulation Summary ******** Simulation of 521 minutes
No. of Customers: 137
Average Customer Wait: 2 minutes
Teller #1 % Working: 89
Teller #2 % Working: 86
Teller #3 % Working: 83
Teller #4 % Working: 86
Письменные упражнения
5.1 Подчеркните правильное. СТЕК — это структура, реализующая поряд0*'
(a) first-in/last-out (b) last-in/first-out	(с) first-come/first-serve
(d) first-in/first-out (e) last-in/last-out
6-2
6-3
Напишите два программных приложения для стеков.
Какой выход имеет следующая последовательность операций стека? (DataType = int):
Stack S
int x ~ 5, у - 3,-
S.push(8);
S.Push(9);
5.Push(y):
x ш S.Pop(>•
S.push(18) ;
x « s. Pop () »"
S.Push(22);
while (! S-StackEmpty()) (
У - s.PopO;
cout « Y « endl;
cout « x ccendl;
g.4 Напишите функцию
r void StackClear(Stacks S);
которая очищает стек S. Почему важно, чтобы объект S передавался по ссылке?
5.5 Что выполняет следующая функция? (DataType = int):
void Ques5(Stacks S) (
int arr[64], n = 0, I;
int elt;
while (!S.StackEmpty()) a[n++] = S.Popl);
for(i=0;i< n;i++)
S.Push(a[i];
5.6	Что выполняет следующий сегмент кода?
Stack SI, S2, tmp;
DataType x;
while (!SI.StackEmpty ())
x ° Sl.popo ;
trap.push(x);
while (•trap.StackEmpty())
*'tmp.pop();
SI.Push(x);
} S2.Push(x);
Запишите функцию
Stacksize(Stack S);
Исд°льзует операции стека, чтобы возвращать количество эле-® в стеке S.
5.8	Что выполняет следующая функция Ques8? (DataType — int):
void Ques8(Stack& S, int n) (
Stack Q;
int I;
while(!S.StackEmpty()) (
i = S.Pop();
if (I !=n)
Q.Push(i j;
while(IQ.StackEmpty()) (
i = Q.Popl);
S.Push(i);
}
}
5.9	Если DataType является int, напишите функцию
void SelectltemCStackb S, int n);
которая использует операции стека для нахождения первого появления элемента в стеке S и перемещает его в вершину стека. Поддерживайте упорядочение для других элементов.
5.10	Преобразуйте следующие инфиксные выражения в постфиксные:
(а)	а + Ъ*с
(б)	(a+)/(d-e)
(в)	(Ъ2 - 4*а*с)/(2*а)
5.11	Напишите следующие выражения в инфиксной форме:
(а)	а Ъ + с*
(б)	а b с +*
(в)	а b с d е ++**е f - *
5.12	Подчеркните правильное. Очередь — это структура, реализующая порядок:
(а)	first-in/last-out
(б)	last-in/first-out
(в)	first-come/first-serve
(г)	first-in./first-out
(д)	last-in/last-out
5.13	Очередь — это структура данных, применимая для (подчеркнуть вс® возможные варианты)
(а)	оценки выражений
(б)	планирования задач операционной системы
(в)	моделирования очередей ожидания
(г)	печати списка в обратном порядке.
5.14	Какой выход имеет следующая последовательность операций очер6^ (DataType - int):
Queue Q;
DataType x - 5, y-3;
Q.Qinsext(S);
Q.Qinsert (9)
q.Qlnsert(y);
x «= Q.QDeleteO ;
q. Qlnsert(18);
= Q.QDelete();
©.Qlnsert(22);
while (iQ-QEmptyO)
* у - Q.QDeleteO;
cout « У « endl;
cout « x << endl;
515 Что выполняет следующая функция? (DataType = int):
void Quesl5(Queues Q, int n = 50)
{
Stack S;
int elt;
while (’Q.QEmptyO) {
f	elt = Q.QDeleteO;
S.Push(elt);
}
while (!S.StackEmpty()) {
elt - S.PopO;
Q.Qlnsert(elt);
}
Почему важно, чтобы объект Q передавался по ссылке?
5.16 Что выполняет следующий сегмент кода?
(DataType - int)
Queue Ql, Q2;
int n = 0, x;
while (SQl.QEmptyO)
*• -Ql.QDelete О ;
02.Qlnsert(x);
П++;
}
for (int i-=D;i <n;i++)
x • Q2.QDeleteO ;
Ql .Qlnsert(x);
Q2.Qlnsert(x);
^РеДПоложим, что список очереди приоритетов содержит целые значения с Оператором "меньше, чем" (<), определяющим очередь приори-<5°В' Текущий список содержит элементы.
сп асЫвая методы PQDelete и PQInsert в классе Pqueue, опишите этот С°К после выполнения каждой из следующих инструкций:
45	15	50	25	65	30
(a) Item  pq.PQDelete(} Item = List:
(b> pq.PQInsert(20)	List:
(c) Item = pq.PQDelete()	Item = List:
(d) Item = pq.PQDelete()	Item = List:
5.18 Перепишите подпрограмму PQDelete для обеспечения FIFO-порядка эд ментов на том же самом уровне приоритетов.
Упражнения по программированию
5.1	Добавьте метод
void Qprint (void):
который приводит к печати очереди, по 8 элементов в строке. Напиши? программу, которая вводит 20 значений double из файла pq.dat в оч_. редь. Печатайте элементы.
5.2	Считайте 10 целых в массив, поместив каждый в стек. Напечатай? оригинальный список и затем напечатайте стек, извлекая элементы Конечно, вторая печать перечисляет элементы в обратном порядке.
5.3	Стек может использоваться для распознавания определенных типов об разов. Рассмотрим образец STRING 1#STRING 2, где никакая строка & содержит "#” и STRING2 должна быть обратна STRING 1. Например, строка "123&Ла#аЛ&321" совпадает с образцом, но строка "a2qd#dq3a" — нет. Напишите программу, которая считывает пять строк и указывает, совпадает ли каждая строка с образцом.
5.4	Во второй модели стека стек растет в направлении уменьшения индексов массива. Первоначально стек пуст и Тор=21. После помещения тре< символов в стек индекс Тор =» 18, и элемент в вершине стека — это LIST[TOP] = С.
Убывающий индекс
В этой модели индекс Тор уменьшается с каждой операцией Ри^ Напишите реализацию для класса Stack, используя эту модель. ПР0* тестируйте свою работу, запуская программу 5.1.
5.5	Массив может использоваться для сохранения двух стеков, один, Р30 тущий с левого конца, второй, уменьшающийся с правого конца.
S,	Sj
top,	top2
------------------► •«	 Возрастание стека-Возрастание стека
(»)
(6)
(в)
каково условие для того, чтобы Si был пуст? Когда S2 пуст?
Каково условие для того, чтобы Si был полным? Когда S2 полный? реализуйте класс DualStack, объявление которого задается с помощью
const int MaxDualStackSize =100 class DualStack
{
private:
int topi, top2;
DataType stackstorage[MaxDualStackSize];
public:
DualStack(void);
//помещаем elt в стек n
void Push(DataType elt, int n);
//извлекаем из стека n
DataType Pop(int n);
//выборка в стеке n DataType Peek(int n); //стек n пуст?
int StackEmpty(int n J;
//стек n полный?
int StackFuIl(int n);
//очистка стека n void ClearStack(int n) ;
(г) Напишите main-программу, считывающую последовательность из 20 целых, помещая все четные целые в один стек, и нечетные — в другой. Печатайте содержимое каждого стека.
5.6	Считайте строку текста, помещая каждый непустой символ и в очередь, и в стек. Проверьте, не является ли текст палиндромом.
5.7	Расширьте постфиксный калькулятор, добавив унарный минус, представленный символом Например, введите выражение
7 е 12 + <Display>5
5.8	Это упражнение расширяет класс постфиксного калькулятора для включения оценки выражений с переменной. Новое объявление следующее:
class Calculator
Private: Stack S;
Struct component	//NEW
( short type; float value; ); int expsize:	//NEW
component expComponent(50); //NEW int GetTwoOperands(doubles operandl, doublet operand2);
void Enter(double num); void Compute(char op);
1
Calculator (void.); void Run(void); void Clear(void); void Variable(void);	//NEW
double Eval(double x);	//NEW
H
Операция Variable позволяет пользователю вводить постфиксное выражение, содержащее переменную х, равно как числа и операторы. Ца. пример, введите выражение хх*Зх* + 5*
когда вам необходимо оценить инфиксное выражение х2 + Зх + 5. Когда 1 каждый компонент считан, выполните ввод в массив expComponent. е Поле типа каждого элемента ввода устанавливается так: 1= число, 2 = переменная х, 3 = +, 4 = -, 5 —	6 — /. Если тип = 1, то число
сохраняется в поле значения. Увеличивайте expsize на единицу для ! каждого нового элемента ввода.	' I
Функция-член Eval выполняется циклично для членов expsize массива expComponent и использует стек для оценки выражения. Всякий раз, когда какой-либо элемент имеет поле типа 2, помещайте параметр х в стек.
Напишите main-программу, оценивающую выражение
(х2 +.1)/ (X4 + 8х2 + 5х + 3)
для X =0, 101, 102, 103, . . . , 108.
5.9	Измените задачу моделирования в программе 5.7, чтобы она включала следующую информацию:
(а)	Выводить среднее время, которое ^лиент проводит в банке. Время клиента измеряется от прихода до ухода.
(б)	В настоящее время все кассиры остаются в банке до тех пор, пока не уходит последний клиент. Это обходится дорого, если кассиры задерживаются после закрытия банка для обслуживания пришедшего поздно клиента. Дайте возможность кассиру уйти, если банк закрывается и нет ожидающих обслуживания клиентов. Совет: добавьте поле к записи TellerStat, которое указывает общее время, которое кассир проводит в банке. Используйте эту переменную для вычисления времени в процентах, в течение которого кассир занят.
5.10	Предположим, в банке создается отдельная очередь к каждому кассиру-Когда клиент приходит, он (или она) выбирает самую короткую очередь, а не оценивает загруженность кассиров. Измените программу моделирования 5.7 для указания среднего времени ожидания клиента и объема работы, выполненной каждым кассиром. Сравните результаты с моделью банковского обслуживания с единственной очередью к кассирам-
Абстрактные операторы
вд Описание перегрузки операторов
6.2.	Система рациональных чисел
6.3.	Класс Rational
6.4.	Операторы класса Rational как функции-члены
6,5.	Операторы потока класса Rational как дружественные функции
6.6.	Преобразование рациональных чисел
6.7.	Использование рациональных чисел
Письменные упражнения
Упражнения по программированию
Абстрактный тнп данных определяет набор методов для инициализации и управления данными. С классами язык C++ вводит мощные средства реализации ADT. В этой главе мы расширяем определенные языком опера торы (например, +, [] и так далее) для абстрактных типов данных. Процесс называемый перегрузка оператора (operator overloading), переопределяв стандартные символы операторов для реализации операций для абстрактного типа. Перегрузка оператора — это одна из наиболее интересных возможностей объектно-ориентированного программирования. Эта концепция позволяет использовать стандартные операторы языка с их приоритетом и свойствами ассоциативности как методы класса. Например, предположим, класс Matrix определяет операции сложения и умножения, используя AddMat и MultMat При перегрузке мы можем использовать знакомые инфиксные операторы ’’+” и Предположим, что Р, Q, R, S — это объекты типа Matrix:
Стандартные методы класса	Перегруженные операторы
R = Р.AddMat (Q);	R = Р + Q
S = R.MultMat (Q.AddMat(P)); S = R * (Q + P)
Реляционные выражения определяют порядок между элементами. Мы говорим, что целое 3 является положительным или что 10 меньше, чем 15.
3 >=0	10 < 15
Концепция порядка применима к структурам данных, отличным от чисел. В классе Data в главе 3, например, две даты (объекты) могут сравниваться по хронологии года.
Dateib, 6, 44) < (Date(12, 25,80) Date(4, 1, 99) == Date ("4/1/99") Date(7, 1, 94) < Date("fi/1/94")
//День-Д наступил раньше рождества 1980 //Два способа создать дату 1 апреля //июль наступает перед августом
C++ позволяет выполнять перегрузку большинства присущих ему операторов, включая операторы потока. Программист не может создавать новые символы оператора, а должен перегружать существующие операторы C++.
Метод PrintDate в классе Date принимает объект D и выводит его поля в стандартном формате. Тот же метод может быть переписан как оператор ”«” и использоваться в потоке cout. Например, Date-объект D( 12,31,99) определяет последний день двадцатого века. Чтобы создать выходную строку
Последний день 20-го века - 31 декабря 1999
мы можем использовать метод print из главы 3 или перегрузить оператор вывода.
PrintDate Method:
cout «"Последний день 20-го века -"
D.PrintDate();
Overloaded "«" Method:
cout«"Последний день 20-го века -"« D;
Перегрузка операторов — это тема, которая описывается в нескольких главах. В данной главе мы разрабатываем класс рациональных чисел Дл^ иллюстрации ключевых понятий. Вы, конечно, знакомы с "дробями" и имеет® школьный опыт работы с операциями. Класс Rational дает хороший пример арифметических и операций отношения, преобразования типов между целыми значениями и действительными числами и простого определения дЛ*1 перегруженного потока ввода/вывода.
6.1. Описание перегрузки операторов
it оМИН перегрузка операторов (operator overloading) подразумевает, что 1оры языка, такие как +,!=,[] и = могут быть переопределены для типа о£еР я Этот тип класса дает возможность реализации операций с абстрактны-^^япами данных с использованием знакомых символов операций. C++ предо-1(0 яяет различные методы определения перегрузки операторов, включающие сТЙВ являемые пользователем внешние функции, члены класса и дружествен-о1<Ьункции. C++ требует, чтобы по крайней мере один аргумент перегружен-оператора был объектом или ссылкой на объект.
Определяемые пользователем внешние функции
в классе SeqList методы Find и Delete требуют определения оператора отно-еЕИЯ *’=«===” для DataType. Если оператор не присущ этому типу, пользователь полжея явно задать перегруженную версию. Объявление использует идентификатор "operator" и символ Так как результатом операции является True или False, возвращаемое значение будет типа int:
int operator—(const DataTypei a, const Datatypes b);
Например, предположим, список содержит записи Employee, которые состоят из поля ID и другой информации, включающей имя, адрес и так далее.
struct Employee
(
int
ID;
ID	Имя	Адрес	а • а
Операция отношения "равно” сравнивает поля ID:
int operator =- (const Employees a, const Employees b) <
return a.ID —b.ID; //сравнение ID-полей
I
Перегруженный оператор должен иметь аргумент типа класс. Заметьте, что ^дующая попытка перегрузить ”==” для строк C++ будет неудачной, потому Йикакой аргумент не является объектом или ссылкой на объект.
^t operator^(char *s, char *t)//неверный оператор
return strcmp(s, t)== 0;
I
°пеРатоР отношения "равно” будет объявлен, пользователь может овать функции Find или Delete класса SeqList:
ty₽Mef
•in i вдРЮуее DataType; //тип данных Employee
"aseqiist.h-
Sesu3t
Employee eznp; //объявляем объект SeqList.
emp.10=1000; //ищем служащего с 10=1000
if (L.Find(emp})
L. Delete (enip); //если найден, удаляем этого служащего
Чтобы использовать класс SeqList, пользователь должен иметь технически знания для определения оператора как внешней функции. Это является дополнительным, но обязательным требованием, поскольку оператор "===' присоединяется к типу данных, а не к классу.
Члены класса
Арифметические операторы и операторы отношения происходят из систем счисления. Они позволяют программисту объединять операнды в выражениях используя инфиксную запись. Класс может иметь методы, объединяющие объекты сходным образом. В большинстве случаев методы могут записываться как перегруженные функции-члены с использованием стандартных операторов C++. Когда левый операнд является объектом, оператор может выполняться как метод, который определяется для этого объекта. Например, двумерные векторы имеют операторы, включающие сложение, отрицание и скалярное произведение векторов.
Сложение векторов
Сложим два вектора u=(ui,U2) и v=(vi,V2), образуя вектор, компоненты которого являются суммой компонентов каждого вектора (см. часть (а) рис. 6.1).
u+v = (u1+vlfu2+v2)
Отрицание векторов
Образуем отрицание векторов u=(ui,U2), беря отрицательное значение каждого компонента. Новый вектор имеет ту же величину, но противоположное направление (см. часть (Ь) рис. 6.1).
-(UnUaJ^-U!, -uz)
Скалярное произведение векторов
Вектор u=(ui,U2), умноженный на вектор u=(vi,V2), является числом, полученным сложением произведений соответствующих компонентов (см. часть (с) рис. 6.1). Скалярное произведение векторов является произведением модулем векторов (magnitude) на косинус угла между ними.
v*w=u1Vi+u2V2==rnagmtude(v)*magnitude(w)*cos(q)
(а) Сложение	(6) Вычитание	(с) Произведение
Рис. 6.1. Векторные операции
МЫ объявляем класс векторов Vec2d с координатами х и у как закрытыми ^«мм-членами. Этот класс имеет конструктор, два бинарных оператора вйе, скалярное произведение) и унарный оператор (отрицание) в каче-(ся° функций-членов, а также дружественные функции, определяющие умно-с';'3'ие вектора на скаляр и потоковый вывод. Использование дружественных фикций описывается в следующем разделе.
 nrivate:
F Rouble xl,x2 //компоненты
РиЬууконструктор со значениями по умолчанию
vec2d(double h=O.C, double v=0.0);
//функции-члены
Vec2d operator-(void);	//вычитание
Vec2d operator*(const Vec2d£ V); //сложение
double operator*(const Vec2d& V); //скалярное произведение
I/дружественные функции
friend Vec2d operator*(const double c,const Vec2d& V) ;
friend ostreainfi operatorcc(ostreams os, const Vec2d& U);
j;
AJCaK в функциях-членах в операторах сложения, отрицания и скалярного произведения предполагается, что текущий объект является левым операндом в выражении. Предположим, U и V- это векторы.
Сложение. Выражение U + V использует бинарный оператор	связанный
«объектом U. Метод operator+ вызывается с параметром V.
U.operator*(V) //возвращает значение Vec2d(x+V.x, у + V.у)
Отрицание. Выражение -U использует унарный оператор Метод operator- выполняется для объекта U.
U.operator- () //возвращает значение Vec2d(-x, -у)
Скалярное произведение. Подобно сложению, выражение U*V использует бинарный оператор связанный с объектом U. Произведение вычисляется методом текущего объекта U и параметра V.
U.operator*(V) //возвращает значение х * V.x *у * V.у
, Пример с объектами:
Vec2d U(l, 2), V(2, 3);
° + V = (1» 2) + (2, 3) -= 13, 5)
-U - -(1, 2) - (-1, _2)
U *V = (1, 2) * (2, 3) ’ 8
^еРегРУЗка операторов требует соблюдения определенных условий.
Операторы должны соблюдать приоритет операций, ассоциативность и оличество операндов, диктуемое встроенным определением этого опе-₽ат°ра. Например, — это бинарный оператор и должен всегда, 2 Да °К ПеРегРУЖается, иметь два параметра.
ОПеРаторы в C++ могут быть перегружены, за исключением следующих:
^°ПератОр «запятая’’} sizeof :: (оператор области действия) ^УсЛоВнОе вырйжение)
3.	Перегруженные операторы не могут иметь аргументов по умолчание, другими словами, все операнды для оператора должны присутствовать Например, следующее объявление оператора является неправильны^.' double Vec2a::operator* (vec2d V = Vec2d(l,l));
4.	Когда оператор перегружается как функция-член, объект, связанный с этим оператором, всегда является крайним слева операндом.
5.	Как функции-члены унарные операторы не принимают никаких аргу ментов, а бинарные операторы принимают один аргумент.
Дружественные функции
Обычно объектно-ориентированное программирование требует, чтобы толь-ко функции-члены имели доступ к закрытым данным какого-либо класса. Это обеспечивает инкапсуляцию и скрытие информации. Этот принцип следует рас-ширить до перегрузки оператора везде, где возможно. В некоторых ситуациях, однако, использование перегрузки с функцией-членом невозможно или слишком неудобно. Поэтому необходимо использовать дружественные функции (friend functions), которые определяются вне класса, но имеют доступ к закрытым дан. ным-членам- Например, умножение вектора на скаляр является еще одной фор. мой умножения векторов, где каждый компонент вектора умножается на числовое значение. Число (скаляр) с, умноженное на вектор и—(uj,ui), является вектором, компоненты которого образуются умножением каждого компонента и на с:
C*U=(cUj,CU2)
Это естественная операция перегрузки с использованием оператора ***.
Однако, перегрузка оператора с использованием функции-члена не позволяет помещать скалярное значение С как левый операнд. Левым операндом должен быть вектор.
Мы определяем, что является оператором вне класса, имеющим пара’ метры С и U, и имеет доступ к закрытым членам х и у параметра U- Это выполняется определением оператора как дружественного внутри объявления класса. Дружественная функция объявляется помещением ключевого слове friend перед объявлением функции в классе.
friend Vec2d operator * (const double c, const Vec2d& U)
Так как дружественная функция является внешней для класса, она находится в области действия класса и реализуется как стандартная функди ' Заметьте, что ключевое слово friend не повторяется в объявлении функции'
Vec2d operator* (double с, Vec2d U)
(
return Vec2d(c*u.x, c*U.y);
)
передаются
тТпй использовании умножения вектора на скаляр оператору аргумента, и скаляр должен быть левым параметром.
Z;
_ 3 0 * Y; //результатом является (24,15)
ГА /-пользование дружественных функций и права доступа к ним описывают-следующем перечне правил
СЙ Объявление дружественной функции выполняется помещением объяв-JL* дения функции внутрь класса, которому предшествует ключевое слово friend. Фактическое определение дружественной функции дается вне блока класса. Дружественная функция определяется подобно любой обычной функции C++ и не является членом класса.
2 Ключевое слово friend используется только в объявлении функции в классе, а не с определением функции.
3- Дружественная функция имеет доступ к закрытым членам класса.
*4. Дружественная функция получает доступ к членам класса только при передаче ей объектов как параметров и использовании записи с точкой для ссылки на члена.
5. Для перегрузки унарного оператора как друга в качестве параметра
*' передается операнд. При перегрузке бинарного оператора как друга в качестве параметров передаются оба операнда. Например:
friend Vec2d operator-(Vec2d X); //унарный "минус"
friend Vec2d operator+(Vec2d X, Vec2d Y); //бинарное сложение
6.2. Система рациональных чисел
Рациональные числа — это множество частных Р/Q, где Р и Q — это целые, «Q* О. Число Р называется числителем или нумератором (numerator), а число Q — знаменателем или деноминатором (denominator).
2/3 -6/7 8/2 10/1 0/5 5/0 (неверно)
Представление рациональных чисел
Рациональное число — это отношение числителя к знаменателю, и следо-аательн©, представляет одни член коллекции эквивалентных чисел (equivalent numbers). Например:
2/3 « 10/15 = 50/75 //эквивалентные рациональные числа
Один член коллекции имеет редуцированную форму (reduced form), в ко-в	числитель и знаменатель не имеют общего делителя. Рациональное число
Дотированной форме является наиболее репрезентативным значением из в КстК1^ИИ 9квивалентных чисел. Например, 2/3 — это редуцированная форма Фоьлп еК1*ии ^/3, Ю/15, 50/75 и так далее. Чтобы создать редуцированную 6аИбо «^о-либо числа, числитель и знаменатель нужно разделить на их
Дьгпий общий делитель (GCD, greatest common denominator). Например: l«/ls . 2,3
lGCD(lo,i5) = 5; 10/5 = 2	15/5 = 3)
”/2l - 8/7
<GGd (24,21) = 3; 24/3 - 8	21/3 - 7)
5/9 = 5/9
(GCD(5,9) = 1; рациональное число уже в редуцированной форме)
В рациональном числе и числитель, и знаменатель могут быть отрицатель иыми целыми. Мы используем термин нормализованная форма (standardized form) с положительным деноминатором.
2/-3 = -2/3	//-2/3 — это нормализованная форма
-2/-3 = 2/3	//2/3 — это нормализованная форма
Арифметика рациональных чисел
Читатель знаком со сложением, вычитанием и сравнением дробей с использованием общих деноминаторов и правил умножения и деления этих чисел Далее приводятся несколько примеров, которые предлагают формальные алгоритмы для реализации операций в классе рациональных чисел.
Сложение/Вычитание. Сложим (вычтем) два рациональных числа, создав эк-Бивалентную дробь с общим знаменателем. Затем сложим (вычтем) числители.
1_	5 8*1	3*5	8*	14-3*5	23
3 +	8~3*8 +	3*8~	3*8	24
1 _	5 _ 1 * 8	3*5 _	1	*	8 - 3 * 5 _ 2?
' > 3	83*83*8	3*8	24
Умнйжение/Деление. Умножим два рациональных числа, умножая числители и знаменатели. Выполним деление, инвертируя второй операнд (меняя местами числитель и знаменатель) и перемножая члены дробей.
. . 1	5	1*5	5
3 * 8 3*8 24
(/) 1/5 = 1#8=А
1 } 3	8 3 5 15
Сравнение. Все операторы отношения используют один и тот же принцип: Создаем эквивалентные дроби и сравниваем числители:
..15	1*85*3
(<) -х < “ эквивалентно отношению ——— <.-—
38	3*88*3	\
Данное отношение ВЕРНО (TRUE), так как 1*8 < 3*5 (8<15)
Й	Пример 6.1 Этот пример иллюстрирует операции с рациональными числами. Пред-
	положим, U = 4/5, V = -3/8 и W = 20/25. . тт , „ 4 ( ЗА 32 ( 15^ 17 Г U + V=5+[-8]“40+[-4oJ=40 U 4 Г 3 ') -12 — = — * — — =	
	V 5 1 81 40 32	15	....
Las*	U > V, так как 777 > - тк (32 > -15) 40	40 о ТТ ххг	.	4 100 20 100 2. U - - W, так как 5 J2g 25 - 125
Преобразование рациональных чисел
Множество рациональных чисел включает целые как подмножество. В пред-аздении целого как рационального числа используется деноминатор 1. Сле-довательк0’
(целое) 25 = 25/1 (рациональное)
Более сложное преобразование касается действительных и рациональных Например, действительное число 4,25 — это 4+1/4, что соответствует -яональному числу 17/4. Алгоритм для преобразования действительного Р ла в рациональное приводится в разделе 6.6. Обратное преобразование ^адионального числа в действительное включает простое деление нумератора йа деноминатор. Например:
3/4 = 0,75 //делим 3,0/4,0
6.3.	Класс Rational
Идеи, изложенные в разделе 6.2, мог$т быть прекрасно реализованы в классе, использующем данные-члены нумератор и деноминатор для описания рационального числа и объявляющем базовые арифметические и операторы отношения как перегруженные функции-члены. Операторы потокового ввода и вывода реализуются с использованием перегрузки дружественной функции. Преобразование между целыми (или действительными) и рациональными числами и преобразование между рациональными и действительными числами творчески использует функции конструктора и операторы преобразования C++.
Спецификация класса Rational
ОБЪЯВЛЕНИЕ
^include ^iostream.h>
^include <stdlib.h>
class Rational
Private:
// определяет рациональное число как числитель/знаменатель long num, den;
/ закрытый коструктор, используемый
< арифметическими операторами ational(long num, long dencm);
Функции-утилиты Standardize (void) ;
n9 gcd(long long n) const;
Pubiic;
// ?°НСтРУКторы преобразуют:
Ratint: >Rational' double->Rational
Rav.Cnal(int num-0, int denom=l);
Clonal (double x);
frieBo^BbIEQa
lstream& operator>> (istreams istr,
Rational &r) ; friend ostreamb operator« (ostreamt ostr, const Rationale d);
/l бинарные операторы:
// сложить, вычесть, умножить^ разделить Rational operator* (Rational г) const; Rational operator- (Rational r) const; Rational operator* (Rational r) const; Rational operator/ (Rational r) const;
// унарный минус (изменяет знак) Rational operator- (void) const;
// операторы отношения
int operator< (Rational r) const; int operatort™ (Rational r) const; int operator-™ (Rational r) const; int operator!^ (Rational r) const; int operator* (Rational r) const;
int operator*- (Rational r) const;
// оператор преобразования: Rational -> double operator double(void) const;
// методы-утилиты
long GetNumerator(void) const;
long GetDenominator(void) const; void Reduce(void);
ОПИСАНИЕ
Закрытый метод Standardize преобразует рациональное число в "нормальную форму" с положительным деноминатором. Конструкторы используют Standardize для преобразования числа в нормальную форму. Мы также используем этот метод при чтении числа или при делении двух чисел, поскольку эти операции могут привести к результату с отрицательным деноминатором. Сложение и вычитание не вызывают Standardize, потому что два знаменателя операндов уже являются неотрицательными. Закрытый метод gcd возвращает наибольший общий делитель двух целых шип.
Открытая функция Reduce преобразует объект рациональное число в его редуцированную форму вызовом gcd.
Два конструктора для этого класса действуют как операции преобразования целого (long int) и действительного (double) числа в рациональное <Ra' tional) число. Они описываются в разделе, посвященном операторам преобразования типа.
Для приложений мы используем методы-утилиты GetNumerator и GetDenominator для доступа к данным-членам рационального числа.
Оператор ввода « является дружественной функцией, которая считывает рациональное число в форме Р/Q. При выводе это число имеет форму Р'4* Попытка ввести рациональное число с деноминатором О вызывает ошибку завершение программы.
ПРИМЕР
Rational АС—4,5), В. С;	// А - это -4/5, В и С — ЭТО 0/1
cin *> С;	// при вводе -12/-18
// Standardize сохраняет 12/18
cout << С.GetNumerator();	// печать числителя 12
Reduce О<
с с + А;
9 « 0;
с0”, « float(А); ссл* *
// привести С к форме 2/3
//В - это 2/3 + // вызвать оператор // конвертировать в // вывести -.8
(-4/5) = -2/15
минус; вывести 2/15 действительное число;
5 + Rational(3); covt с 
// преобразовать .5 в 1/2;
// вывести сумму: 1/2 + 3/1 - 7/2
6.4.	Операторы класса Rational как функции-члены
Класс Rational объявляет арифметические операторы и операторы отношения как перегруженные функции-члены. Каждый бинарный оператор имеет параметр, который является правым операндом.
Предположим, u, v и w — это объекты типа Rational в выражении:
w « и + V
е Перегруженный оператор + является членом объекта и (левый операнд) и принимает v в качестве параметра. Возвращаемым значением является объект типа Rational, который присваивается w. Технически w = u + v оценивается как w = u. + (v)
. Для выражения v = -и. C++ выполняет оператор ” для объекта и (единственный операнд). Возвращаемым значением является объект Rational, который присваивается v. Технически v = -и оценивается как v = u.-()
Реализация операторов класса Rational
В программном приложении содержится весь код класса Rational. Мы реализуем сложение и деление, реляционное равенство и отрицание для иллюстрации перегрузки членов. Пусть имеются следующие объявления:
Rational u(a,b), v(c,d);
Чтобы сложить (”+”) рациональные числа, найдем общий деноминатор ДЛя операндов. Фактическое сложение производится объединением нумераторов.
,, , а с а * d Ъ* с а * d + Ъ ♦ с о a Ъ * d b * d b * d
Реализации а и Ъ — это данные-члены num и den левого операнда, является объектом, связанным с оператором "+”. Числа end яв-f Дадными-членами v.num и v.den правого операнда: ^Чопа1Х1к^-С^ммиРо:вание t	ational::operatcr+ (Rational г) const
Uonal{nuiD*r.den + den*r.num, den*r.den);
ттл	/”
'	* выполняется инвертированием правого операнда и затем
0Х1е^п®Да -Ием Члеиов- Числа а и Ъ соответствуют данным-членам левого Числа end относятся к данным-членам в объекте v, правом
операнде. Так как результат может иметь отрицательный деноминатор, част ное нормализуется.
' ' b d~b*c~b*c
И Rational-деление
Rational Rationaloperator/ (Rational r) const <
Rational temp = Rational (num*r .den, den*r.num);
// убедиться, что деноминатор — положительный
temp.Standardise{J; return temp;
}
Для оценки оператора отношения сравним нумераторы после приведения рациональных чисел к общему деноминатору. Тогда для оператора отношения "равно” (=—") возвращаемым значением является True, когда нумератору равны. Далее следуют операторы логического эквивалента (<=>).
а « d___Ъ * с
b * d b * d <=> a* d = ~b * с
Протестируйте условие а * d =- b * с:
И отношение равно int Rational::operator== (Rational r) const (
return num*r.den ж= den*r.nuin;
Унарный оператор минус ” работает с данными-членами единственного операнда, определяющего эту операцию. Вычисление просто делает отрицательным нумератор:
// минус для Rational Rational Rational::operator- (void) const (
return Rational(-num, deni; )
6.5.	Операторы потока класса Rational как дружественные функции
Файл <iostream.h> содержит объявления для двух классов с ostream и istream, которые обеспечивают потоковый вывод и потоковый BBW' соответственно. Потоковая система ввода/вывода предоставляет опредеЛеВВ\ для потоковых операторов ввода/вывода и ”«” в случае элементаРв1Д типов char, int, short, long, float, double и char*. Например:
Ввод
istream fi operator» (short v);
istream & operator» (double v);
Вывод
ostream & operator«(short v);
ostream & ope r at or« (double v);
Потоковые операторы можно перегружать для реализации ввода/вывода * являемого пользователем типа. Например, с классом Date операторы "«" - могут обеспечивать потоковый ввод/вывод тем же способом, который ” пользовался для простых типов. Например:
Date D;
cin»D //<ввОД>10/5/75
c0Ut«D //<вывод> October 5, 1975
рсли бы операторы для класса Date должны были быть перегруженными Функции-члены, их было бы необходимо объявлять явно в <iostream.h>. J^ccv ostream пришлось бы иметь перегруженную версию ”«”, которая ийямает параметр Date. Это явно непрактично, н поэтому мы используем ^ижественную перегрузку, которая определяет этот оператор вне класса, но зволяет ему иметь доступ к закрытым данным-членам в классе.
Перегрузка потокового оператора использует структурированный формат, который далее показан для общего типа класса CL.
class сь
( г
public:
friend istreamfi operator» {is treami istr, CLS Variable I;
friend ©streams operator«(ostreams ostr, const CL& Value);
1
Параметр istr представляет поток ввода, такой как cin, и ostr представляет лоток вывода, такой как cout. Так как процесс ввода/вывода изменяет состояние потока, параметр должен передаваться по ссылке.
Для ввода, элементу данных Variable присваивается значение из потока, поэтому оно передается по ссылке. Функция возвращает ссылку на istream, чтобы оператор мог использоваться в такой последовательности как
cin » ш » ц;
случае вывода Value копируется в поток вывода. Так как данные не ^меняются, Value передается как константная ссылка. Это позволяет избе-“Ть копирования возможно большого объекта по значению. Функция воз-т^П?ет ссылку на ostream (output), чтобы оператор мог использоваться в °® последовательности как
COut « щ « п.
с еРШается, ^Й?Уэка с*аг с
Реализация операторов потока класса Rational
Ввод w
* и вывод рациональных чисел реализуется перегрузкой потоковых вводом мы считываем число в форме Р/Q, где Q^O. Программа
» если введенный деноминатор равен О.
_ 1 оператора ввода потока, ввод в форме P/Q
Perator » (istreamb istr, Rationale г)
для чтения разделителя '/'
* оператор "»" имеет доступ
// к номинатору и деноминатору объекта г istr » г.num » с	г.den;
// проверка деноминатора на равенство нулю
if (г.den =- 0) i
cerr « "Нулевой деноминатор I\п";
exit(1); }
// приведение объекта г к стандартной форме г.Standardize();
return istr;
}
Перегруженный потоковый оператор вывода записывает рациональное число в форме P/Q.
// перегруженный оператор вывода потока, форма: P/Q oscreams operator « (©streams ostr, const Rationale r) 1
// как друг оператор ”»” имеет доступ //к номинатору и деноминатору объекта г ostr « г.num « ' Г « r.den;
return ostr;
6.6. Преобразование рациональных чисел
Класс Rational иллюстрирует операторы преобразования типа. Программист может реализовать пользовательские операторы преобразования, аналогичные операторам, предусмотренным для стандартных типов. Конкретнее, мы сосредотачиваем внимание на преобразовании между объектами типа класс и соответствующего типа данными C++.
Преобразование в объектный тип
Конструктор класса может использоваться для построения объекта. Как, таковой, конструктор принимает его параметры ввода и преобразует их • : объект.
Класс Rational имеет два конструктора, которые служат в качестве ос®' ; раторов преобразования типа. Первый конструктор преобразует целое в Р8-циональное число, а второй преобразует число с плавающей точкой в раД0' опальное число.
При вызове конструктора с единственным целым параметром num он пр* образует целое в эквивалентное рациональное num/1. Например, рассмотри0 эти объявления и операторы:
Rational Р(7), Q(3,5j, R, S; // явное преобразование 7 в 7/1
R = Rational(2);	// явное преобразование 2 в 2/1
S » 5;	// построить Rational(5)	J
//и присвоить новый объект переменной S
о
Объявления Q и R создают объекты Q=3/5 и R=0/l. Присваивание Rational(2) явно изменяет значение R на 2/1. Присваивание S 5 приВ0^
^^образованию типа. Компилятор принимает S — 5 как оператор S -
' стрУхт°Р* райональное число в форме num/den
// -Rational (int р, int q) : num(p), den(q) ^tionai, -
< if (den — 0)
cerl « "Нулевой леноминатор!" « endl;
exit(1)•
)
I
второй конструктор преобразует действительное число в рациональное, цалример, следующие операторы создают рациональные числа А=3/2 и В*16/5:
tional А = Rational (1.5), В; // явное преобразование
j 2;	// преобразовать 3.2 в Rational(16,5)
Для преобразования необходим алгоритм, который аппроксимирует произвольное число с плавающей точкой в эквивалентное рациональное. Алгоритм включает сдвиг десятичной точки в числе с плавающей точкой. В зависимости от количества значащих цифр в действительном числе результат может быть только приближенным. После создания рационального числа вызываем функцию Reduce, сохраняющую рациональное число в более читабельной редуцированной форме.
// конструктор, преобразует х типа double U к типу Rational
Rational::Rational (double x)
{
double vail, val2;
vail - 100000000L*x;
val2 - 10000000L*x;
nua - long(valL-val2);
den - 90000000b;
Reduce()}
Преобразование из объектного типа
Ham^bIK определяет явные преобразования между простыми типами. код^ИМер* что^ы напечатать символ с в! коде ASCII, мы можем использовать
Сощ •
mt (с) « endl; // преобразовать char в int
есда типами также определяются неявные преобразования. Например, ~~ это целое long, a Y — это double, то оператор
¥ » j.
’ И? = double (I)
с°Дер^^ет I в double и присваивает результат переменной Y. Класс может а^вЧенйеЬ °®НУ или более функций-членов, которые преобразуют объект в цЬв^?угого тип» данных. Для класса CL предположим, что нам необхо-^вййМа °бразов^ь объект в тип с именем NewType. Оператор NewType() Часто объект и возвращает значение типа NewType. Искомый тип New-иьляется стандартным типом, таким как int или float. Являясь
унарным, оператор преобразования не содержит параметр. Так же, оператоь преобразоваиия не имеет возвращаемого типа, потому что он находится неявд в имени NewType. Объявление принимает форму:	°
class CL {
operator NewTypetvoxd); );
Оператор преобразования используется следующим образом:
NewType а,-
CL Obj;
а = NewType(obj); //явное преобразование а  obj; //неявное преобразование
Класс Rational содержит оператор, преобразующий объект в double. Этот оператор позволяет выполнять присваивание рациональных данных переменным с плавающей точкой. Преобразователь принимает рациональное число p/q, делит нумератор на деноминатор и возвращает результат как число с плавающей точкой. Например:
3/4 это 0.75	4/2 это 2.0
// преобразовать: Rational -> double
Rational::operator double(void) const (
return double(num)/den;
)
Пример 6.2
Пусть имеются объявления Л '!
Rational R(l,2), S(3,5) double Y,Z;
1. Оператор Y — double(R) осуществляет явное использование преоб-разователя. Результат: Y = 0.5
2. Оператор Z — S осуществляет неявное преобразование к рациональному числу. Результат: Z = 0.6.
6.7. Использование рациональных чисел
Перед тем, как разрабатывать приложения для рациональных чисел, оЯ* шем алгоритм для генерирования редуцированной формы дроби. Этот 6# ритм включает нахождение наибольшего общего делителя (gcd) нумераТ°™ и деноминатора.	, се
Для создания редуцированной формы рационального числа метод использует закрытую функцию-член gcd, которая принимает два П0^0 тельных целых параметра и возвращает наибольший из их общих делйтеЛ Функция gcd реализуется в программном приложении. Для ненулевоГ0л. ционального числа метод Reduce делит нумератор и деноминатор на йХ 8
, ^tional:: Reduce (void)
 10&3 bigdivisor, tempnumerator;
. tempnumerator - модуль от num fempnumerator - (num < 0> ? -num : num;
jf (num I=— u)
A den - 1; ll приведение к 0/1 else
* Ц найти GCD положительных чисел: // tempnumerator и den
bigdivisor - gcd(tempnumerator, den);
if (bigdivisor > 1) {
num /= bigdivLSor;
den /« bigdivisor;
) )
Программа 6.1. Использование класса Rational
Эта программа иллюстрирует основные возможности класса Rational. Здесь показаны различные методы преобразования, включая преобразование целого числа в рациональное и рационального числа в действительное. Демонстрируется также сложение, вычитание, умножение и деление рациональных чисел. Программа заканчивается неявным преобразованием числа с плавающей точкой в рациональное н наоборот — в число с плавающей точкой. Реализация класса Rational содержится в файле rational.h.
•include «iostream.h>
•pragma hdrstop
•include "rational.h”
« // каждая операция сопровождается выводом void main (void)
Rational rl(5), r2, r3;
float f;
cout « "i. Rational-значение 5 is ’’ « rl « endl;
cout « »2. введите рациональное число: ";
Cin » rl;
* * * float(rl);
out « " Эквивалент с плавающей запятой: " « f « endl;
cout « «з. введите два рациональных числа; ";
ccin » rl r2
*’ Результаты: " « (rl+r2) « ’’ (+)	"
<rl-r2) « ” (-)	" « (rl*r2) « " (♦)	"
c< (rl/r2) « ” (/)	" « endl;
if < r2)
cout « * Отношение (меньше чем); " « rl « ” < ’•
« r2 « endl;
else if (rl == r2) cout « " Отношение « r2 « endl;
else cout <<. " Отношение « r2 « endl;
(равно) : ” « rl « ” ==» ••
(больше чем): ” « rl « ”
cout « ''4. Введите число с плавающей запятой:
cin » f;
rl = f;
cout « " Преобразование к Rational ” « rl « endl; f = rl;
cout << " Преобразование к float ” « f « endl;
/*
<Run of Program 6.1>
1.	Rational-значение 5 is 5/1
2.	Введите рациональное число: -4/5
Эквивалент с плавающей запятой; -0.8
3.	Введите два рациональных числа: 1/2 -2/3 Результаты: -1/6 (+)	7/6 (-)	-2/6 (*)	-3/4 (/)
Отношение (больше чем) : 1/2 > -2/3
4.	Введите число с плавающей запятой: 4.25 Преобразование к Rational 17/4
Преобразование к float 4.25
Приложение: Утилиты для рациональных чисел. Читатель научился работать с дробями еще в начальной школе. Для иллюстрации приложения класса Rational описывается ряд функций, выполняющих вычисления с дробями.
Функция PrintMixedNumber записывает дробь как смешанное число:
Дробь	Смешанное число
10/4	2 1/2
-10/4	2	1/2
200/4	50
В алгебре основной задачей является решение общего уравнения дроби 2/ЗХ +2=4/5
Процесс включает изоляцию члена X перестановкой 2 в правую часть уравнения:
2/ЗХ = -6/5
Решение получают, разделив обе части уравнения на рациональное числ° 2/3, коэффициент X. Реализуем этот процесс в функции SolveEquation.
х = -6/5 * 3/2 = -18/10 = -9/5 (редуцированное)
6.2. Утилиты для рациональных чисел
Программа использует ряд утилит для рациональных чисел при выпои яеНЛИ^некоторых вычислений.	р выпол-
prinrMixedNumber // печатать число как смешанное
SolveEquation // решить общее уравнение
// ia/b)X + (c/d) = (e/f)
Действие каждого оператора явно описывается в операторе вывода.
^include <iostream.b> linclude <stdlib.h> fpragma hdrstop
^include "rational.h"
9
// печатать Rational-число как смешанное: (+/-)N p/q void PrintMixedNuntber (Rational x)
// целая часть Rational-числа x int wholepart = int(x.GetNumerator() / >: .GetDenominator ());
// сохранить дробную часть смешанного числа Rational fractionpart = х — Rational(wholepart);
Ц если дробной части нет, печатать целую if (fractionpart -= Rational(0)) cout « wholepart « " ”:
else (
11 вычислить дробную часть fractionpart.Reduce();
// печатать знак без целой части
if (wholepart < 0) fractionpart = -fractionpart;
if (wholepart != 0)
cout << wholepart « “ " « fractionpart « ’• *’; else
cout « fractionpart « “
}
решить ax + b - с, где а, Ь, с — рациональные числа tional SolveEquation(Rational a, Rational b, National c)
// проверить а на равенство нулю
if (a Rational(0))
cout « ’’Уравнение не имеет решений.” << endl;
// возвратить Rational(О), если решений кет -
I return Rational(0);
else
) return (c-b)/a;
n‘ain(void)
tional rl, r2, r3, ans;
<< "Введите коэффициенты для " "'a/b X + c/d - e/f' :
42S
cout «."Приведенное уравнение: •' « rl « " X = ’ « (r3-r2) « endl;
ans = SclveEquation(rl,r2,r3);
ans . Reduce () ;
cout « "X = ** « ans « endl;
cout « "Решение как смешанное число:
PrintMixedNumber(ans);
cout << endl;
}
/*
Оапуск 1 программы 6.2>
Введите коэффициенты для 'a/b X + c/d = e/f' : 2/3 2/1 4/5
Приведенное уравнение: 2/3 X = -6/5
X «= -9/5
Решение как смешанное число: -1 4/5
Оапуск 2 программы б.2>
Введите коэффициенты для ' а/Ь х + c/d - e/f' 2/3 -7/8 -3/8 Приведенное уравнение: 2/3 X = 32/84
X = 3/4
Решение как смешанное число: 3/4
*/
Письменные упражнения
6.1 C++ позволяет двум или более функциям в программе иметь одно я то же имя при условии, что их списки аргументов достаточно отличны друг от друга, чтобы компилятор мог различать вызовы функций. Компилятор оценивает параметры в вызывающем блоке и выбирает правильную функцию. Этот процесс называется перегрузкой функции (function overloading). Например, математическая библиотека C++ <math.h> определяет две версии функции sqr, которая возвращает квадрат ее аргумента.
integer version: int sqr (long); //выОор целой версии float version; double sqr(double); //выбор версии действительного числа
Ряд правил определяет правильную перегрузку операторов. Эти правил» следующие:
Правило 1: Функции должны иметь отдельный список параметров. зависимый от возвращаемого типа, и значения по умолчанию.
Правило 2: Перечислимые типы — это отдельные типы с целью пе₽е грузки. Ключевое слово typedef не влияет на перегрузку.
Правило 3: Если параметр не совпадает точно с формальным параметру в наборе перегружаемых функций, применяется алгоритм совпадав для определения ’’наиболее совпадающей" функции. Преобразовав выполняется, если необходимо. Например, при передаче перемеяв short перегруженной функции, которая имеет целые параметры, 14
цилятор может создавать совпадение, преобразуя эту переменную в int* {компилятор не выполняет преобразование параметра, если выбор приводит к неоднозначности*
Для каждого из следующих примеров укажите правило, которое применимо. Если правило нарушается, опишите ошибку.
Предположим, что следующие сегменты кода используются для перегрузки функций f.
«function int f(int {
return
)
«function int f(int {
return
1>
x/int y)
x*y;
3
x=l, int y=7)
x + у + x*y;
<function 2>
double f(int x, int y) <
return x*y;
}
(б)	Функция max перегружается четырьмя отдельными определениями
«function 1>	<function 2>
int max(int х, int у)	double max(double x, double y)
{	{
return х>у? х : у;	return x>y? x : y;
}	}
<function 3>	<function 4>
int max(int x, int y, int z)	int max (void)
{	{
int Imax =x;	int a, b;
if(y>lmax)	cin »a»b;
lmax=y,-	return abs(a) > abs(b)?
	a : b;
if (z > imax)	}
Imax = г:	
return Imax;	
}
( ) Эти три версии функции read предназначены для различения ввода Данных целого и перечисляемого типа.
function 1>	<function 2>
read (int & x) cin»x. ' )	enum Boolean{FALSE,TRUE}; void read(Boolean& x) { char c; cin» C; x = (c=='T') ? TRUE:FALSE;
^fu^tion 3> tVPe<S« int Logical;	)
const int TRUE=1, FALSE=0; void readfLogicals x) 1 char c;
cin»c;
x=(c=='T') ? TRUE:FALSE;
)
6.2	Используйте функцию max из письменного упражнения 6.1(b) и пре» положите, что пользователь вводит значения ш=-29 и п=8. Укажите какая функция вызывается и какое будет возвращаемое значение. ’
(a) cin » m » n; (б) max(); (в) max(m, -40, 30);
max (m,n);
6.3	При использовании max из письменного упражнения 6.1(b) каков выход каждого из операторов?
int а==5, Ь=99, С=153
int m,n;
double hl= .01, h2= .05;
long t = 3000, u= 70000, v= -100000;
cout «"Максимум а и b равен "«max(a,b);
cout «"Максимум a, b и с равен "<<max(a,b,c);
cout «"1.0 * max (hl, h2) -"«1.0 + max (hl, h2); cout «"Максимум t, u и v равен ”« max(t,u,v);
6.4	Являются ли следующие функции различными с целью перегрузки? Почему?
(а) enum El{one, two); (б) type def double scientific;
enum E2(three,four);
int f(int x. El y) ;
int f(int x, E2 y);
double f(double x);
scientific f(scientific x);
6.5	Напишите перегруженные версии функции Swap, которая может принимать два int, float и string(char*) параметра.
void Swap(int& a, xnt&b);
void Swap(float& x, floats y);
void Swapcchar *s, char *t);
6.6	Объясните различие между перегрузкой оператора с использование функции-члена и дружественной функции.
6.7	Класс ModClass имеет единственный целый данное-член dataval в пазоне 0 . . .6. Конструктор принимает любое положительное цел°е и присваивает данному-члену dataval остаток от деления на 7.
dataval=v% 7
Оператор сложения складывает два объекта, суммируя их данные-чЛ^ н находя остаток после деления на 7. Например:
ModClass a(10); //dataval в а равен 3;
ModClass b(6>; //dataval в b равен 6;
ModClass с; // c=a + b имеет значение (3 + 6) % 7 =2 class ModClass { private: j_nt dataval;
public:
Modeless(int v = o);
ModClass operator*(const ModClassS x); int GetValue(void) const;
(а)	Реализуйте методы этого класса.
(б)	Объявите и реализуйте оператор как друга ModClass, Оператор умножает поля значений в двух объектах ModClass и находит остаток после деления на 7.
(в)	Напишите функцию
ModClass Inverse(ModValue& х);
которая принимает объект х с ненулевым значением и возвращает значение у, так что х*у~1 (у называется обратным значением х). (Совет: неоднократно умножайте х на объекты со значениями от1 до 6. Один из этих объектов является обратным).
(г)	Перегрузите потоковый вывод для ModClass и добавьте этот метод к классу.
(д)	Замените GetValue, перегрузив оператор преобразования int(). Этот оператор преобразует объект ModClass в целое, возвращая dataval.
operator int (void);
(e)	Напишите функцию
. void Solve(Modeless a, ModClass& x, ModClass b);
которая решает уравнение ax — b для x, вызывая метод Inverse.
6-8 Добавьте полный набор операторов отношения в класс Date из главы 3. Две даты необходимо сравнить по хронологии года. Например:
Date(5,5,77) > Oate(10,24,73)
Date{12/25/44) <= Date(9,30,82)
Date(3,5, 99) !=Date(3,7,99)
6 9
Комплексное число имеет форму х + iy, где i2 = -1. Комплексные исла имеют широкое применение в математике, физике и технике, ии имеют арифметику, управляемую рядом правил, включающих сле-
ДУКпцие:
Пусть u ~а+ ib, v = с +id
Величина (u) =sqrt(a2+bz)
комплексное число, соответствующее действительному числу f, Равно f+iO
Вещественная часть и = а
* Мнимая часть u=sqrt(a2+b2)
u + v = (а + с) + i(b + d) u - v = (а — с) 4- i(b^~ d)
u*v = (ас — bd) + i(ad + be)
, ac + bd u/v - —-----
c2 + d2
/be - ad c2 + d2
-u — -a + i(-b)
Реализуйте класс Complex, объявление которого является следующим-
class Complex i
ptivate; double real, double ijnag;
public:
Complex(double x ® 0.0, double у = 0.0);
//бинарные операторы
Complex operator* (Complex x) const;
Complex operator- (Complex x) const;
Complex operator* (Complex x) const;
Complex operator/ (Complex xi const; //Отрицание
Complex operator- (void) const;
//Оператор потокового ввода/вывода
//вывод в формате (real, imag)
friend ostream& operator« (ostreamfi ostr,const Complexs x); };
6.10 Добавьте методы GetReal и Getlmag в класс Complex из письменного упражнения 6.9. Они возвращают вещественную и мнимую части ком-плексного числа. Используйте эти методы для написания функции Distance, которая вычисляет расстояние между двумя комплексными числами.
double Distance (const Complex &a, const Complex &b);
6.11
(а)	Добавьте преобразователь в класс Rational, который возвращает объект ModClass.
(б)	Добавьте преобразователь в ModClass, который возвращает объект tional
6.12
(а)	Реализуйте класс Vec2d из раздела 6.1
(б)	Добавьте функцию-член в класс Vec2d, который обеспечивает скаляр#06 произведение, где операнд скаляра находится в правой части. Рее-110 зуйте оператор и как функцию-член, и как дружественную функИ#10’
Пример:
Vec2d v(3,5);
cout«v*2«" "«2*v«endl;
<output>
(6,10) (6,10)
Множество — это коллекция объектов, которые выбраны из группы объ-6-^ еКтов, называемой универсальным множеством. Множество записывается как список, разделяемый запятыми и заключенный в фигурные скобки.
X =* {11» Ь» Ь» • ♦ • »
В данной задаче элементы множества выбраны из целых в диапазоне от 0 до 499- Класс поддерживает ряд бинарных операций для множеств X и Y.
Д Множество Union('U) XL/Y — это множество, содержащее все элементы в X и все элементы bY без дублирования.
□	Множество Intersection^) X Pi Y — это множество, содержащее все элементы, которые находятся и в X. и в Y.
X = (0. 3, 20, 55}, Y - {4, 20, 45, 55}
X UY = {0, 3, 4, 20, 45. 55} ХП Y- {20, 55}
□	Множество Membership^ ) п£Х является True, если элемент п является членом множества X; иначе оио равно False.
X ={0, 3, 20, 55J//20 G X является True, 35 € X является False
этом объявлении множество — это список элементов, выбранных из Вводв/°На 1^ель1Х 0 ...SETSIZE -1, где SETSIZE является 500. Операции Аеля1лтЬ1В0'^а’ ^ъединения, пересечения и принадлежности элементов опре-Управление множеством.
SETSIZE = 500;
j1®8* Set Palse " True = 1;
₽tivate.
ХП1.,1!анные-члены класса set Public: meinbertSETSIZE];

создает пустое множество
и конструктор, создает множество с начальными // элементами а [01, . . . , а[n-l]
Set(int a(], int n);
// операторы объединения (+), пересечения (*), принадлежности (Л) Set operator* (Set x) const;"
Set operator* (Set x) const;
friend int operator* (int elt. Set x);
// методы вставки и удаления
void Insert (int n); // добавить элемент n к множеству set void Delete(int); // удалить элемент n
// операторы вывода Set
friend ©streams operator« (ostream& ostr, const Set& x);
ОПИСАНИЕ
Первый конструктор просто создает пустое множество. Элементы добавляются во множество с использованием Insert. Второй конструктор присваивает п целых значений в массиве а этому множеству. Каждый элемент проверяется на нахождение в диапазоне.
Элемент п находится во множестве, при условии, что соответствующий элемент массива является равным True.
п(=х если и только если элемент [п] является True
Например, множество X ={1, 4, 5, 7} соответствует массиву с элементами member[l], member[4], member[5], member[7] равными True, и другими элементами, равными False.
false	true	false	false	true	true	false	true	false	false	 • 	false
0	'1	2	3	4	5	6	7	8	9		499
Потоковый оператор вывода должен печатать множества с элементами, разделяемыми запятыми и заключенными в фигурные скобки. Например:
int setvalsf ]	(4, 7, 12};
Set S, T(setvals, 3);
S.Insert(5);
cout « S « endl;
cout « T « endl;
«output>
(51
{4, 7, 12}
(а)	Реализуйте конструкторы Set.
(б)	Реализуйте функцию-член множества в (’"*)• Возвращается True, всЛ^ member[n] является равными True; иначе возвращается False. Так приоритет оператора **’ является относительно низким, выражен# * включающее следует заключать в скобки для надежности. Напри#®?*
Set А;
if ((О * А) == О) //проверка на нахождение 0 во множестве А
X»)
(г)
g___это множество {1, 4, 17, 25} и Т — это множество {1,8,25,33,53,63}.
Выполните следующие операции:
(X) S + Т	(ii) S * Т	(iii) 5"S
(iv) 4Л (S+T)	(v) 25~ (S*T)
укажите действие следующей последовательности операторов:
int а[J - (1,2,3,5);
mt Ь[] - (2,3,7,8,251;
int п;
Set А(а,4), В(Ь,5), С;
С - А+В; cout « С;
С = А*В; cout « С;
cin » п; // введите 55 д.insert(п);
if (ПЛА)
cout « Удачно « endl:
(д) Реализуйте остальные методы Set
Упражнения по программированию
6.1 Эта программа использует результат письменного упражнения 6.5. Напишите программу, которая берет два целых и два числа с плавающей точкой, печатает их значения и затем вызывает соответствующую функцию Swap для записи этих значений в обратном порядке. Эта же программа должна вводить две символьные строки, менять местами их значения с использованием Swap и печатать результирующие строки.
6.2 Эта программа тестирует класс ModClass, разработанный в письменном упражнении 6.7. Напишите программу для проверки дистрибутивного закона для объектов ModClass.
а*(Ь+с) — а*Ь + а*с
Определите три объекта а, b и с, имеющие параметры конструктора 10, 20 и 80, соответственно. Ваша программа должна выводить значение из выражений в каждой части уравнения.
6-3 Используйте ModClass и функцию Solve, разработанные в письменном Упражнении 6.7. Сформируйте массив
ModClass а[ ] - {ModClass(4), ModClass(10), Modeless(50));
программа должна выполнять цикл
i=0; i<3; i++)
cout « a(ij « •• « « int(a[i)) « endl;
Urn
^пользуйте функцию Solve для печати решения уравнения
odciassH) . х _ Modclass{3,
использует операторы класса Date из письменного уп-Dat ”•* Напишите функцию
^in(cOnst Dates к, const Dare& yj;
’•4 Эт'
a программа
которая возвращает более раннюю из двух дат. Определите четыре объ екта от D1 до D4, которые соответствуют
D1 — это 6/6/44	D2 — это день НовоХ’О гола в 1999
D3 — это Рождество 1976 D4 - это 4 июля 1976
Протестируйте функцию, сравнивая объекты D1 и D2, D3 и D4.
6.5 Рассмотрим следующие свойства векторов с двумя измерениями:
(a)	u*(v+w)=u*v + u*w (дистрибутивное свойство)
(б)	Два вектора перпендикулярны; их скалярное произведение равно О (в) c*v=v*c, где с — действительное число.
Используя класс Vec2d, разработанный в письменном упражнении 6.12 дайте явные примеры, 'иллюстрирующие (а) и (с). Эта программа должна также считывать два действительных числа х и у и проверять, чтобы векторы (х, у) и (-у, х) были перпендикулярными.
6.6	Это упражнение использует класс Complex number, разработанный в письменном упражнении 6.9. Программа должна выполнять следующие действия:
(а)	Проверьте, чтобы -i2 — 1.
(б)	Напишите функцию f(z), оценивающую комплексную полиномиальную функцию:
z3 — 3z2 + 4z — 2
Вычислите полиномиал для следующих значений z:
z = 2+3i, -1+i, 1+i, 1 i, 1+Oi
Заметьте, что последние три значения являются корнями из f.
6.7	Используйте класс Rational и его операторы преобразования для выполнения следующего сравнения действительных и рациональных чирел. Объявите действительное число pi= 3.14159265 и приближение рационального числа Rational(22,7), которое часто используется студентами. Напишите программу, которая выполняет два следующих вычисления и печатает результаты.
(а)	Вычислите разность между двумя числами как рациональными числами.
Rational(pi) — Rational(22,7)
(б)	Вычислите разность между двумя числами как действительными числами. pi — float(Rational(22,7))
6.8	В главе 8 описывается класс экстенсивной строковой обработки, йС пользующий указатели и динамическую память. Это упражнение РаЭ, рабатывает простой строковый класс, который сохраняет данные, йС пользуя массив. Рассмотрим объявление
Class String
I
private:
char str[256 j;
public:
String(char s[] = " ”)
int Length(void) const; // длина строки
void CStringfchar s[] const; // копировать строку в массив C++
II **********потоковый ввод/вывод ***********
friend ostream& operator<< (ostream/. ostr, const String^ s);
// Читать строки, разделенные пробелами
friend istreamb operator» (istream istr. Strings s) ;
II **~**** оператор отношения: String == String ***** int operator== (Strings s) const;
II ***** объединение *****
String operator+ (Strings s) const;
};
реализуем класс и протестируем его, запуская следующую программу void main(void)
String SI("Это "), S2("прекрасный день 1, S3:
char s[30];
if (Si ==>String(Это a})
cout «"Тестирование на равенство удачное” «endl; else
cout «"Тестирование на равенство неудачное. “«endl;
cout «"Длина Sl= “«SI. Length () « endl;
cout «"Ввод строки S3:
cin >> S3;
S3 = Si + S2;
cout «"Конкатенация Si и S2 равна”
« S3 « endl;
S3. Cstring(s);
cout «"Строка C+-', сохраняемая в S3 следующая"
« s «"' ’’<< endl;
}
6.9	Это упражнение тестирует класс Set из письменного упражнения 6.13. Рассмотрим множества
S = {1,5,7,12,24,36,45,103,355,499}
Т = {2,3,5,7,8,9,12,15,36,45,56,103,255,355,498}
U = {1,2,3,4,5, ..., 50}
Создайте множества S, Т и U. Используйте Insert для инициализации множества U. Выполните следующие вычисления:
(1)	Вычислите и печатайте S+T.
(2)	Вычислите и печатайте S*T.
(3)	Вычислите и печатайте S*U.
(4)	Удалите 8, 36, 103 и 498 из Т и печатайте это множество.
(5)	Генерируйте случайное число от 1 до 9, печатайте его
_	и затем проверьте, находится ли оно во множестве Т.
J-1Q 5
ЗТ°М упражнении используется класс Set для моделирования вероятности получения пяти отдельных случайных чисел в диапазоне от 0 0 в пяти последовательных жеребьевках. Математическая вероят-°СТь этого события равна
1 ‘ 4/5 • 3/5 • 2/5 * 1/5 = .0384
аПйЩите функцию
int fillSet(void);
которая выполняет эксперимент жеребьевки пяти чисел и вставляв их во множество S. Протестируйте, находятся ли О ... 4 в это множестве, проходя цикл пять рад и используя оператор Если в целые находятся в этом множестве, возвращайте 1, иначе возвращу6 те О.
Напишите main-программу, вызывающую fillSet 100000 раз и записи вающую количество случаев, когда выбираются все пять чисел. Разде лите результат на 100000 для определения моделируемой вероятности

Параметризованные типы данных
7.£ Шаблонные функции
12. Шаблонные классы
73. Шаблонные классы списков
7.4. Вычисление инфиксных выражений Письменные упражнения
Упражнения по программированию
Определения классов SeqList, Stack и Queue предназначены для родовог (параметризованного) типа, называемого DataType. Перед использованием Ка° кого-либо класса клиент получает конкретный тип для DataType, использу директиву typedef. К сожалению, это ограничивает возможности клиента едц< ственным типом с любым из классов. Б приложении не может использоватьс стек целых и стек записей в одной и той же программе. Так как использовав^ родового DataType сильно ограничивает клиента, лучшим подходом было бы связать тип данных с объектом, а не с программой. Например:
SeqList<int>	А;
Stack<float>	В;
Queue<CL>	С;
//Список целых
//Стек действительных чисел //Очередь объектов CL
C++ предоставляет такую возможность директивой шаблона (template) которая позволяет использовать параметры общего типа для функций и Клас' сов. Использование шаблонов с классом коллекций дает возможность определять родовые параметры и выполнять два или более вызовов функций с параметрами времени исполнения различных типов. Шаблоны обеспечивают мощным средством обобщения структуры данных. Мы излагаем эту тему в данной главе, сначала вводя шаблонные функции и затем расширяя эти понятия до шаблонных классов. Для приложений разрабатывается последовательный поиск как шаблонная функция и использование шаблонов для перезаписи класса Stack. Множественные стеки являются основной структурой данных в изложенном в этой главе практическом применении, описывающем вычисление инфиксных выражений.
7.1. Шаблонные функции
Алгоритм часто предназначается для обработки некоторого диапазона типов данных. Например, алгоритм последовательного поиска принимает ключ и просматривает список элементов на предмет совпадения. Этот алгоритм подразумевает, что оператор отношения "==" определяется для некоторого типа данных и может использоваться для просмотра списка целых, символов или объектов. До этого момента в книге мы обсудили несколько приложений для алгоритма последовательного поиска. В каждом случае указывалась конкретная версия функции SeqSearch для типа элементов в списке. Мы уже создали множество версий этой функции для реализации того же самого родового алгоритма. Теперь нам хотелось бы написать родовой код, который может при" меняться с различными списками. C++ делает это возможным с помощью шаблонов, основные элементы синтаксиса которых рассматриваются далее.
Объявления шаблонной функции начинаются со списка параметров шаблона в форме
template <class Tv class T2, . . . class Tn>
За ключевым словом template следует непустой список параметров типов» заключенный в угловые скобки. Типам предшествует ключевое слово clas^ Идентификатор Ti является общим именем для конкретного типа данных С+ » которое передается как параметр при использовании шаблонной Функи11?' Ключевое слово class присутствует только для указания на то, что представляет собой тип. Вы можете читать class как "type”. При использовав* шаблона Ti может быть стандартным типом, таким как int, или определяемы пользователем типом, таким как класс. Например, в следующих списках пар метров шаблона имена Т и U ссылаются на типы данных.
<class Т>	//Т - это тип
<class T, class U> //Т и U оба являются типами
«с:
teS’P1®
После определения списка параметров шаблона функция следует обычному
ТУ и имеет доступ ко всем типам в списке. Например, определение ФорЛягсИ как шаблонной функции следующее:
QeCpe
оЛЬзуя ключ, ищет совладение в массиве из п элементов.
// иСП, совпадение обнаружено, возвращает индекс совпадения;
'' 8оэ»Ра«г -1
“”PseqSearch<T list[ Ь int	Т keyl
1 foI(int i=0; i<n; i++)
if (list[iJ == *ey) i*	/ог'эгпэот uunnvr	пдюшдПА
return -1;
//возвращает индекс совпадающего элемента //поиск неудачный, возвращает -1
)
Когда программа вызывает шаблонную функцию, компилятор указывает типы данных фактических параметров и связывает эти типы с элементами в списке параметров шаблона. Например, в вызове функции SeqSearch компилятор различает целые и действительные параметры. г.
int Л(ЮЬ Aindex, Mindex;
float MI1OOJ, fkey = 4.5;
Aindex « SeqSearch (A, 10, 25);	//поиск 25 в A
Mindex  SeqSearch (M, 100, fkey);	//поиск fkey 4.5 в M
Компилятор создает отдельный экземпляр функции SeqSearch для каждого отдельного списка параметров времени исполнения. В первом случае шаблонный тип Т будет целым (int), и SeqSearch просматривает список целых, используя оператор сравнения целых "==". Во втором случае параметр типа Т будет действительным (float), и используется оператор сравнения чисел с плавающей точкой
При вызове основанной на шаблоне функции с конкретным типом все операции должны определяться для этого типа. Если функция использует операцию, не являющуюся присущей этому типу, программист должен предоставить свою версию этой операции или использовать нешаблонную версию такой функции- Например, C++ не определяет оператор сравнения ”==” для stuct или класса. Параметризованная версия функции SeqSearch не может упорядочивать объекты класса, если только оператор не определяется (перегружается) пользователем.
В качестве примера рассмотрим тип записи Student, включающий как целое оле, так и поле с плавающей точкой. Оператор перегружается для применил функции SeqSearch.
-о о студенте, содержащая его ID и средний балл
Student
*rit Studlt). )*Dat spa;
ДЛерег₽Ужает < । ’ °Pe*ator ==
=, сравнивая id студента (Student a, Student b)
'v4iln л
j -studlD == b.studlD;
Обь
ЯВление записи Student и оператор
" находятся в файле student.h.
C++ строковый тип char* создает пользователю некоторые проблемы. ратор =" сравнивает значения указателей, а не фактические строки Пос[пг вольно. Так как char* не является структурой или классом, мы не мо^е' определять перегруженный оператор ”==” для этого типа. Пользователь жен использовать нешаблонную версию функции SeqSearch, которая примени ма к строковому типу C++.
// просмотреть массив строк для нахождения совпадения со строкой-ключом
int SeqSearch(char	], int n, char ’key)
{
forfint i=O;i<n,-i++)
// сравнить, используя строковую библиотечную функцию C++
if (strcmp (list [ i ], key) *= 0)
return i; // возвратить индекс совпадающей строки
return -1; // при неудаче возвратить -1 }
Главная тема этого раздела проиллюстрирована в программе параметризованного поиска. Код для шаблонной функции SeqSearch и специфическая вер. сия для строк C++ содержится в файле utils.h.
Программа 7.1. Параметризованный поиск
Эта программа иллюстрирует последовательный поиск для трех отдельных типов данных.
□	Массив list инициализируется 10-ю целыми значениями. Мы определяем индекс элемента массива со значением 8.
□	Перегрузка оператора используется с типом записи Student. Запись {1550, 0} используется как ключ для поиска в списке и определения ID студента. Возвращаемый индекс обеспечивает доступ к GPA 2,6.
П Для массива строк компилятор использует нешаблонную функцию SeqSearch с типом данных "char*’'. Ведется поиск строки "two", которая находится в позиции с индексом 2.
flinclude <ioscream.h>
flpragma hdrstop
// включить код шаблонной функции SeqSearch и
// специфической версии для строк C++
// search function specific to C++ strings flinclude "utils.h"
// объявление структуры Student и операции для Student flinclude "student.h"
void main() (
// три массива с различными типами данных int listflOJ - (5, 9, 1, 3, 4, 8, 2, 0, 7, 6);
Student studlisj43] = ({1000, 3.4},{1555, 2.€},(1625, 3-8}};
Char *strlist[3] = {"zero","one","two","three", "four"};
int i, index;
// ключ для поиска в массиве studlist Student studentKey = (1555, 0};
£f ((i -= SeqSearch<list, 10, 8)) >= 0}
 cout « "Значение В находится в элементе: " « i « endl;
else
cout « "Элемент co значением 8 не найден" « endl;
index - SeqSearch(studlist, 3, studentKey);
cout « "Студент c ID, равным 1555, имеет GPA: " « studlist[index].gpa « endl;
cout « "Строка ' two' - в элементе: " « SeqSearch(strlist,5,"two") « endl;
}
<Run of Program 7.1>
Значение 8 находится в элементе: 5
Студент с ID, равным 1555, имеет GPA: 2.6
Строка 'two' - в элементе: 2
*/
Сортировка на базе шаблона
Обменная сортировка предоставляет алгоритм для упорядочения элементов в списке с использованием оператора сравнения Этот алгоритм реализуется основанной на шаблоне функцией ExchangeSort, которая использует единственный параметр шаблона Т. Оператор должен определяться для типа данных, который соответствует Т, или в качестве перегруженного оператора пользователя.
И сортировка п элементов массива а с использованием обменной сортировки template cclass Т> void ExchangeSort (Т а[], int n) f
T temp;
int i, j;
// ВЫПОЛНИТЬ n—1 проходов
for (i «= 0; i < n-l; i++)
// наименьшее из a(i+l]...a[n-l] поместить в a[i)
for (j = i+1; j < n; J++)
if talj] < a[i))
{
// поменять значения элементов a[i] и a[j] temp  a[i];
a[i] = a[j];
«(jJ “ temp;
) >
Для удобства функция ExchangeSort находится в файле utils.h.
7.2. Шаблонные классы
Bbi* основанный на шаблоне класс Store, содержащий значение дан-°^°е использованием класса должно инициализироваться. В цга«* Суждения мы проиллюстрируем главные концепции основанных °л°ве классов.
Определение шаблонного класса
При определении шаблонного класса (template class) его объявлению прел шествует список параметров шаблона. Для объявления элементов данных и функций-членов в определении шаблона используются имена параметризиро ванного типа. Далее следует объявление шаблонного класса Store: flinclude <iostream.n> flinclude <stdlib.h>
template <class T> class Store (
private:
T item;
int haveValue;
// объект, содержащий данные
// флаг, устанавливаемый при инициализации
public:
// конструктор умолчания Store(void);
// операции получения и сохранения данных т GetElement(void);
void PutElement(Т x) i
Объявление объектов шаблонного класса
Типы этому классу передаются при создании объекта. Объявление связывает тип с экземпляром класса. Следующие объявления создают объекты типа Store: //данное-член в X имеет тип int.
Store<int> X;
//создает массив из 10 объектов Store с данными char
St6re<char> S[10J;
Определение методов шаблонного класса
Метод шаблонного класса может быть определен как код in-line или вве тела класса. При внешнем определении метод должен рассматриваться как шаблонная функция со списком параметров шаблона, включенным в определение функции. Все ссылки на класс как тип данных должны включать шаблонные типы, заключенные в угловые скобки. Это применяется к имени класса с оператором области видимости класса:
ClassName<T>::
Например, следующий код определяет функцию GetElement для класса Store-// получение элемента, если он инициализирован template <class Т>
Т Store<T>::GetElement(void)
// прервать программу при попытке доступа к неинициализированным данным if (haveValue == 0) < .
cerr « "Нет элемента!” « endl;
exit(1);
)
return item; // возвратить элемент )
Й методе Putltem подразумевается, что присваивание является правильной ^рацией для элементов типа Т:
□хранить элемент в памяти
niate <class т>
te'pP store<T>: :PutElement (const T&x)
< haveValue++;	П haveValue = true
item ’ x;	// сохранить x
)
Для внешнего определения конструктора используется имя класса с опера-оом области видимости класса и имя метода конструктора. В качестве типа класса используйте параметр шаблона. Например, типом класса является gtore<T>, тогда как именем конструктора является просто Store.
// объявление Оез инициализации элемента данных
teraplate<class Т>
Store<T>:: Store (void) :haveValue (0)
{ )
Объявление и реализация класса Store находится в файле store.h.
Программа 7.2. Использование шаблонного класса Store
Эта программа использует шаблонный класс Store для объектов типа integer, записи Student и объектов типа double. В первых двух случаях значения данным присваиваются с использованием PutElement и печатаются с использованием GetElement. Когда типом данных является double, делается попытка выборки значения данных, которое не инициализировано, и программа завершается.
line!tide <iostream.h>
•pragma hdrstop
♦include ''store.h”
♦include "student.h"
void main (void)
Student graduate = {1000, 23);
Store<int> A, B;
Store<Student> S;
Store<double> D;
A.PutElement(3);
В•PutElement(-7);
cout « A.GetElement() « " ” « B.GetElement() « endl;
S. PutElement(graduate);
cout « «Ju студента: " « S.GetElement().studID « endl;
D не инициализировано
j c©ut « "Получение объекта D: " « D.GetElementO « endl;
/•
of Program 7.2>
3 -7
ID студента: ЮОО получение объекта D: Нет элемента! */
7.3. Шаблонные списковые классы
Мы расширяем возможности коллекций в этой книге, используя шаблонные классы. В данном разделе переопределяется класс Stack с шаблонами, который используется в разделе 7.4 для вычисления инфиксного выражения. Шаблон, ная версия этого класса включена в программное приложение в файл tstack.h.
Переопределение класса Stack включает простую шаблонную технику. Начинаем объявление, помещая список параметров шаблона перед объявлением класса и заменяя DataType на Т.
Спецификация шаблонного класса Stack
ОБЪЯВЛЕНИЕ
Iinclude <iostream.h> ^include <stdlib.h>
const mt MaxStackSize = 50,-
template <class T> class stack {
private:
Il закрытые данные-члены T stacklist[MaxStackSize]; int top;
public:
// конструктор Stack (void) ;
// стековые методы доступа void Push(const T& item);
T Pop(void);
T Peek(void);
// методы тестирования и очистки int StackEmpty(void) const;
int StackFull(void) const;
void CiearStack(void); };
Реализация класса Stack, основанного на шаблоне
Каждый метод класса определяется как внешняя шаблонная функция. Эг® требует помещения списка параметров шаблона перед каждой функцией замены класса типа Stack на Stack<T>. В фактическом определении функДИ мы должны заменить параметризованный тип DataType на шаблонный тип Следующий листинг задает новое определение для методов Push и Pop.1
1 Данный код взят из программного приложения, так как он значительно отличается приведенного в оригинале книги. — Прим. ред.
. constructor
// c,ate <dass T> v^T>:zStack(void)
st3cX 0
,he LinkedList method ClearList to clear the stack
/•' SSe «lass T>
teid Stack<T>:: Clears tack (void) (
(tflCkList.ClearList();
//
the LinkedList method TnsertFront folate <ciass T>
void stacks: Push (const TS item)
to push item
1 gtackList.TnsertFront(item);
// use the LinkedList method DeleteFront to pop stack template <class T>
T stack<T>:: Pop (void)
* H check for an empty linked list
Г if (stackList.ListEmptyO)
cerr « "Popping an empty stack" « endl;
exit(1);
}
// delete first node and return its data value
return stackList. DeleteFront () »•
// returns the data value of the first first item on the stack tenylate <class T>
I Stack<T>:: Peek (void)
i
// check for an empty linked list
if (stackList.ListEmpty()) <
cerr « ’’Calling Peek for an empty stack” « endl; exit(l);
)
H reset to the front of linked list and return value stackList.Reset();
return stackList. Data () ;
tIle LinkedList method ListEmpty to test for empty stack int sate <ClaSs T>
{	tack<T>:; StackEmpty (void) const
j r®tum stackList.ListEmptyO ;
7-4. Вычисление инфиксного выражения
g
Факсно«^е 5 показано использование стеков при вычислении выражений пост-ВЬ1чМсле1^ЛИ аольской инверсной записи (Reverse Polish Notation, RPN). Тема ^еализад Я ИнФиксных выражений была умышленно опущена,, так как их Чия требует использования двух стеков: одного для операндов, и дру-
того — для операторов. Поскольку в двух стеках находятся данные различи^, типов, при инфиксном вычислении эффективно используются шаблоны. В э?0. разделе разрабатывается алгоритм вычисления инфиксного выражения, кото рый реализуется с шаблонным стековым классом.
Вы знакомы с выражениями/объединяющими арифметические операции Например, следующие выражения объединяют унарный оператор —, бинап ные операторы +, —, *, /, скобки и операнды с плавающей точкой:
8.5 + 2 * 3	-7 * (4/3 - 6.25) + 9
Эти выражения используют инфиксную запись с бинарными операторами расположенными между операндами. Пара скобок создает подвыражение' вычисляемое отдельно.
В языках высокого уровня существует порядок выполнения операций (order of precedence) и ассоциативность (associativity) между операторами Оператор с высоким приоритетом выполняется первым. Если более одного бинарного оператора имеют один и тот же приоритет, первым выполняется крайний слева оператор в случае левой ассоциативности (+,	*, /) и крайний
правый оператор — в случае правой ассоциативности (унарный плюс, унарный минус).
Порядок выполнения операций (от низкого к высокому)
Оператор
1
2
3
унарный плюс, унарный минус
Ш Пример 7.1
1.	8.6 + 2*3= 14.5 // * выполняется перед +
2.	(8.5 + 2) * 3 — 31.5 // скобки создают подвыражение
3.	®---6 = 15 // унарный минус имеет высший приоритет
Ранг выражения. Алгоритм для вычисления инфиксного выражения использует понятие ранга (rank), который присваивает значение -1, 0, или 1 каждому терму выражения:
Ранг операнда с плавающей точкой равен 1
Ранг унарных операторов +, — равен О
Ранг бинарных операторов +, —, *, / равен -1.
Ранг левой скобки равен О.
Когда мы просматриваем термы в выражении, ранг определяет неправильЗ® расположенные операнды или операторы, которые могут сделать выражеВй неверным. С каждым термом выражения мы ассоциируем суммарный Р3®* (cumulative rank), который является суммой ранга отдельных термов от пе₽ вого символа до данного терма. Суммарный ранг используется для контр°л^ за тем, чтобы каждый бинарный оператор имел два окружающих операнд3 чтобы никакие операнды в выражении не существовали без инфиксного ов ратора. Например, в простом выражении
2 + 3
последовательные значения ранга такие:
Сканирование 2: суммарный ранг = 1
Сканирование +: суммарный ранг = 1 + -1 = О
Сканирование 3: суммарный ранг = 1
правило: Для каждого терма в выражении суммарный ранг выражения быть равен О или 1. Ранг полного выражения должен быть равен 1.
Hg Пример 7.2
рМ Следующие выражения являются неверными, что определяется функ-пией rank:
	Выражение	Неверный ранг	Причина
	1. 2.5_4 + 3	Ранг 4 = 2	Слишком много последовательных операндов
	2. 2.5 + 1 3	Ранг * = -1	Слишком много последовательных операндов
Ml	3. 2.5 + 3 -	Конечный ранг = 0	Нет одного операнда

Алгоритм инфиксного выражения. Алгоритм инфиксного выражения использует стек операндов (operand stack) — стек значений с плавающей точкой е'длЯ хранения операндов и результатов промежуточных вычислений. Второй СТеК, называемый стеком операторов (operator stack) содержит операторы и левые скобки и позволяет реализовать порядок приоритетов. При сканировании выражения термы помещаются в соответствующие стеки. Операнд помещается в стек операндов, когда он встречается в процессе сканирования и извлекается (popped) из стека, когда он необходим для операции. Оператор помещается в стек только тогда, когда уже были оценены все операторы с более высоким или равным приоритетом, и освобождается из стека, когда наступает время его выполнения. Это происходит при вводе последующего оператора с более низким или равным приоритетом или в конце выражения. Рассмотрим выражение
2 + 4 — 3 * 6
Ввод 2: Поместить 2 в стек операндов.
Ввод -н Поместить 4- в стек операторов.
Ввод 4: Поместить 4 в стек операндов.
Операнд Оператор
+	Оператор — имеет тот же порядок приоритетов, что и оператор
вЫдо еКе Сначала извлечь + из стека операторов, извлечь два операнда и стек ИТь ОПерацию сложения. Результат (2 + 4 = 6) поместить обратно в
£ ^Рандов.
’-^пть - в стек операторов.
Операнд Оператор
Ввод 3: Поместить 3 в стек операндов.
Ввод *: Оператор * имеет более высокий приоритет, чем оператор________
стеке. Поместить * в стек операторов.	8
Ввод 6: Поместить 6 в стек операндов.
Операнд
Оператор
Выполнить: Извлечь * и выполнить операцию с операндами 6 и 3 из стека операндов. Поместить результат 18 в стек операндов.
18
Оператор
6
Операнд
Извлечь из стека и выполнить операцию — с операндами 18 и 3 из стека операндов. 6 - 18 = -12. Это результат.
Приоритет оператора определяется дважды: сначала при вводе оператора, затем, — когда он находится в стеке. Начальное значение, называемое входным приоритетом (input precedence), используется для сравнения относительной важности оператора с операторами в стеке. Как только оператор помещается в стек, ему задается новый приоритет, называемый стековым приоритетом (stack precedence). Различие между входным и стековым приоритетом используется для скобок и правых ассоциативных операторов. В зтих случаях стековый приоритет меньше, чем входной приоритет. Когда обнаруживается оператор с тем же приоритетом и ассоциативностью, входной приоритет превышает стековый приоритет оператора в вершине стека. Первый оператор не извлекается, а новый оператор помещается в стек. Порядок
вычисления задается справа налево.
В таблице 7.1 приводится входной и стековый приоритет и ранг, используемые для вычисления инфиксного выражения. Эти операторы включают скобки и бинарные операторы	и /. Бинарные операторы являются
левыми ассоциативными и имеют равный входной и стековый приоритет.
Алгоритм вычисления выражений становится немного сложнее при иалн чии скобок. Когда обнаруживается левая скобка, она представляет начало подвыражения и, следовательно, должна быть немедленно помещена в стею Это выполняется присваиванием левой скобке входного приоритета, котор*^ больше приоритета любого из операторов. Если левая скобка помещена стек, она может быть удалена только тогда, когда находится соответствую^ „ ей правая скобка, и подвыражение вычисляется полностью. Приоритет лев^ скобки в стеке должен быть меньше, чем приоритет любого оператора, что она не извлекалась из стека при вычислении всех термов в подвыраже^15
Алгоритм. Алгоритм реализуется с двумя стеками. Стек операндов содержит а с плавающей точкой, в то время как элементами стека операторов явля-объекты класса типа MathOperator.
jflTCH и
Таблица 7.1
вхОдНОЙ и стековый приоритет с рангом			
	Приоритет		
	Входной приоритет	Стековый приоритет	Ранг
•ГГТЙшнарный)	1	1	-1
jt V-	~— 			2	2	-1
	/_ ~	3	-1	0
		0	0	°	.	J
// класс, управляющий операторами в стеке операторов class MathOperator
private:
// оператор и два его значения приоритета char op;
о int inputprecedence;
int stackprecedence;
public:
// конструктор; включает конструктор умолчания и
// конструктор, инициализирующий объект
MathOperator(void);
MathOperator(char ch);
11 функции-члены для управления оператором в стеке
int operator>- (MathOperator a) const;
void Evaluate (Stack<float> soperandstack);
char GetOp(void) ;
\i
Объект MathOperator сохраняет оператор и значения приоритета, связанные с этим оператором. Конструктор задает как входной, так и стековый приоритет оператора.
, MathOperator::MathOperator (char ch)
OP = Ch;
switch (op)
H *+' и *имеют входной и стековый приоритет 1 case • +•:
case ’	: inputprecedence = 1;
stackprecedence =1; break;
и имеют входной и стековый приоритет 2
Case r/' : inputprecedence = 2;
stackprecedence = 2;
break;
са Имеет входной приоритет 3 и стековый приоритет -1 е <* s inputprecedence = 3;
stackprecedence = -1;
// ,	break;
' имеет входной и стековый приоритет О
case ')* : inputpreceoence = 0;
stackprecedence = 0;
break;
} }
Класс MathOperator перегружает C++ оператор ”>”, используемый для сраВ нения значений приоритетов.
// перегрузить оператор >= сравнением стекового
// приоритета текущего объекта и входного приоритета а.
// используется при чтении оператора для определения
// того, следует ли вычислять операторы из стека перед тем,
// как поместить в него новый оператор
int MathOperator::operator>= (MathOperator a) const (
return stackprecedence >= a.inputprecedence;
)
Этот класс содержит функцию-член Evaluate, которая отвечает за выполнение операций и извлекает два операнда. После выполнения операции результат помещается обратно в стек операндов.
void Mathoperator::Evaluate (Stack<float> &OperandStack) {
float operandl = Operandstack.PopO; // получить левый операнд
float operandZ = Operandstack.Pop(); // получить правый операнд
// выполнить оператор и поместить результат в стек
switch (op}	// выбрать операцию
{
case '+' : Operandstack.Push(operand2 + operandl);
break;
case	Operandstack.Push(operand2 - operandl);
break;
case '	Operandstack.Push(operand2 * operandl);
break;
case ' /'	Operandstack. Push {operand2 / operandl);
break;
)
)
Алгоритм вычисления инфиксного выражения считывает каждый терм выражения, помещает его в соответствующий стек и обновляет суммарный ранг. Ввод завершается в конце выражения или, если ранг находится вне диапазона. Следующие правила применимы при считывании термов:
Ввод операнда: Поместить операнд в стек операндов.
Ввод оператора: Извлечь из стека все операторы, имеющие приоритет больший или равный входному приоритету текущего оператора. Выполнить сравнение, используя метод класса MathOperator ”>=“. Когда оператор^ будут удалены из стека, выполнить оператор, используя метод Evaluate.
Ввод правой скобки ")" : Извлечь и выполнить все операторы в стеке» имеющие стековый приоритет больший или равный входному приоритету ско^ ки который равен 0. Заметьте, что стековый приоритет скобки ”)” ра®е -1, так что процесс останавливается, когда встречается скобка ВыпоДй® нием является вычисление всех операторов между скобками. Если никак» скобки не обнаружено, выражение является неверным ( "нет левой ск» ки").
i
I
о конце выражения очистить стек операторов'. Ранг должен быть 1. Если меньше 1, это означает, что не хватает операнда. Если при очистке стека Р^пчживается то выражение является неверным ("нет правой скобки"), операторы удаляются из стека, функция Evaluate выполняет каждое вы-Конечный результат выражения находят выборкой из стека операндов. уцелев*1
^^грамма 7.3. Вычисление инфиксного выражения
Данная программа иллюстрирует вычисление инфиксного выражения. Мы рцлтываем каждый терм выражения, пропускаем все символы пробелов, пока ве находим Во время этого процесса выполняется проверка ошибок с печатью специальных сообщений об ошибках. После завершения ввода вычисляются оставшиеся термы выражения и его значение выводится для печати.
J
linclude <iostream.h>
linclude <stdlib.h>
" — ЭУвТСЯ Для фуикции isdigit
linclude "tstack.h” linclude "mathop.h"
// проверить: оператор или скобка int isoperator(char ch)
I
if (ch « '+' | I ch —	| | ch — ’*• ||
ch “ '|| ch )
|	return 1;
i	else
f	return 0;
г	}
J	//	проверить:	является ли	символ	пробелом
int iswhitespace(char ch)
I	{
if	(ch	o=	'	'	|।	ch	’	\t'	||	ch	-=	'\n')
return 1;
else
return 0;
//функция сообщений об ошибках
{ yoid error (int n)
i	// таблица сообщений об ошибках
|	static char *errormsgs[) » {
’‘Отсутствует оператор”, ’’Отсутствует операнд”, ’’Нет левой скобки”, "Нет правой скобки”, "Неверный ввод” };
// параметр п - это индекс ошибки.
'' печатать сообщение и закочить программу cerr « errormsgs[п] « endl,-
exit(i).
void main(void) {
// объявить стек операторов с объектами типа MathOperator Stack<MathOperator> Operatorstack;
// объявить стек операндов " Stack<float> Operandstack;
MathOperator oprl,opr2; int rank = 0; float number; char ch;
// выполнять до знака '
while (cin,get(ch) && ch != '=') {
// ******** обработка операнда с плавающей точкой ******** if (isdigit(ch) || ch == ' .' ) {
// возвратить знак или ' и читать число cin,putback(ch);
cin » number;
// ранг операнда равен 1, суммарный ранг должен быть равен 1 rank++;
if (rank > 1)
error(OperatorExpected);
// поместить операнд в стек операндов
Operandstack.Push(number); I
// ********* обработка оператора ********** else if (isoperator(ch)) [
// ранг каждого оператора, отличного от ' (', равен -1.
// суммарный ранг должен быть равен 0
if (ch 1= ' (')	// ранг * (' равен 0
rank--;
if (rank < 0)
error(OperandExpected);
oprl = MathOperator(ch);
while(1Operatorstack.StackEmpty() &&
(opr2 = Operatorstack,Peek()) >— oprl) (
opr2 = operatorstack.Pop();
opr2.Evaluate(Operandstack);
}
Operatorstack,Push(oprl);
)
I/ ********* обработка правой скобки ********* else if (ch ~~ rightparenthesis)
(
oprl = Mathoperator(ch);
while(!Operatorstack,StackEmpty() &&
(opr2 = Operatorstack.Peek(}) >= oprl) {
opr2 = OperatorStack.Pop();
opr2,Evaluate(Operandstack) ;
)
if(Operatorstack.StackEmpty())
error(MissingLeftParenthesis);
opr2 = Operatorstack,Pop(); // get rid of ' (' } // ********* имеем неверный ввод ********** else if {!iswhitespace(ch))
error(Invalidinput);
If ранг вычисленного выражения должен быть равен 1 if (rank ’= 1)
error(OperandExpected);
11 заполнить стек операторов и завершить вычисление выражения, // если найдена левая скобка, а правая отсутствует.
while (!Operatorstack.StackEmpty())
1 oprl = OperatorStack.PopO ;
if (oprl.GetopO «= leftparenthesis) error(MissingRightParenthesis);
oprl.Evaluate(Operandstack)t
}
// значение выражения - в вершине стека операндов cout « "Значение равно: " « Operandstack. Pop() « endl,-
Оапуск 1 программы 7.3> 2.5+ 6/3 ‘ 4 - 3 -Значение равно: 7.5
оапуск 2 программы 7.3> (2 + 3.25) * 4 =
Значение равно: 21
Оапуск 3 программы 7.3> (4 + 3) - 7) =
Нет левой скобки
•/
Письменные упражнения
7.1
(а) Напишите код для параметризованной функции Мах, которая возвращает большее из двух значений.
Напишите перегруженную версию функции Мах, которая работает со строками C++. Передайте указатели символам как параметры и воз-8РащаЙте указатель большей строке.
Напишите шаблонный класс DataStore, который имеет следующие методы: int ihsert(T elt); Вставляет elt в закрытый массив dataElements, имеющий пять элементов типа Т. Индекс следующей доступной позиции в dataElements задается данным-членом 1ос, который также является количеством значений данных в dataElements. Возвращает О, если больше не остается места в dataElements.
*lnd(T elt); Выполняет поиск элемента elt в dataElement и возвращает его индекс, если он найден, и -1, если нет.
int NumEits (void); Возвращает количество элементов, сохраняемых в dataElements.
т& GetData (int n)Возвращает элемент в позиции п в dataElements. Генерирует сообщение об ошибке и выходит, если п<0 или п>4.
7.3	Напишите функцию
ternplate<class Т>
int Мах(Т Агг( Ь int п);
которая возвращает ивдекс максимального значения в массиве.
7.4	Реализуйте функцию
template<class Т>
int BinSearch(Т А[ J, Т key, int low, int high);
которая выполняет бинарный поиск ключа в массиве А.
7.5	Напишите функцию
template cclass Т>
void Insertorder (Т А( ], int n, Т elem);
которая вставляет elem в массив А, так что список сохраняет возрастающий порядок. Заметьте, что, когда позиция находится для elem, вы должны передвинуть все остальные элементы на одну позицию вправо.
7.6
(а)	Дайте основанное на шаблоне объявление класса SeqList.
(б)	Для шаблонного класса реализуйте конструктор и методы DeleteFront и Insert.
(в)	Перед объявлением объекта SeqList<T> какие операторы должны определяться для типа Т?
(г)	Объявите S как объект Stack, в котором элементами стека являются объекты SeqList.
Упражнения по программированию
7.1	Протестируйте функции Мах из письменного упражнения 7.1, используя их в main-программе. Включите в main-программу две строки C++-
7.2	Напишите шаблонную функцию Сору с объявлением
template<class Т>
void Сору(Т А[ ], Т В[ ], int п) ;
Эта функция копирует п элементов из массива В в массив А. Напишите main-программу для тестирования функции Сору. Включите, по край' ней мере, следующие массивы:
(a)	int Aint[6], Bint(6] = (1, 3, 5, 7, 9, 11};
(b)	struct Student
(
int fieldl;
double field2;
};
Student AStudent(3];
Student BStudent[3] = {{1, 3.5}, {3,0}, {5, 5.5));
л В этом упражнении используется шаблонный класс DataStore, разра-7-3 ботанный в письменном упражнении 7.2. Напишите перегруженный оператор который печатает данные объекта DataStore, используя метод GetData. Пусть записью будет Person:
struct Person
char name [50];
int age;
int height;
];
Перегрузите оператор ”==“ для Person так, чтобы он сравнивал поля имен.
Напишите программу, которая вставляет элементы данных типа Person до тех пор, пока объект DataStore не будет заполненным. Включите следующий элемент р типа Person в ваш ввод:
"John"
25
72
Выполните поиск р и затем печатайте результат поиска. Используйте "«” для вывода объекта.
7.4 Расширьте программу 7.3 для операции возведения в степень, которая представлена символом Возведение в степень является правым ассоциативным. Например:
2 А з - 8 //2Э - в
2А2А3-2А (2 А 3) ~ 256.
Для вычисления аь включите математическую библиотеку <math.h> и используйте функцию pow следующим образом:
г£ • pow(а, Ь)
7.5 Во время последовательного поиска выполняется сканирование списка и поиск совпадения с ключом. Для каждого элемента у иас имеется Двойная задача: проверка на совпадение и проверка достижения конца списка. Модифицированная версия поиска, называемая быстрый последовательный поиск (fast sequential search), совершенствует алгоритм, Добавляя ключ в конец списка. Расширенный список гарантирует наличие совпадения, так как имеется по крайней мере один "ключевой" элемент в списке.
Исходный список
______[	| Ключ
А[0]	А[11	А[п-1] А[п]
В впл
процессе поиска просто выполняется тестирование на совпадение и /Л1ск завершается, когда ключ найден в списке.
’ Дадите код для
int₽pdte <class т>
(6) astSegSearch(T А[ ], int n, T key);
Р^иЩйте программу 7.1, используя FastSeqSearch.
7.6 Mode — это значение, которое чаще всего наблюдается в списке. Чтоб^ собрать такую информацию для списка произвольных типов Данных определите класс Datalnfo, поддерживающий два поля данных: value и frequency.
template <class Т> class Datalnfo {
private:
T data;
int frequency;
// значение данных
// появления значения данных
public:
// увеличение frequency void Increment(void);
// операторы отношения == и < должны // соответствовать типу Т
// сравнение значений
int operator== (const DataInfo<T>S w);
// сравнение счетчиков частоты
int operator< (const DataInfo<T>& w) ;
if операторы потоков « и » должны
// соответствовать типу Т
friend istreamb operator» (istream& is, DataInfo<T> &w);
friend ©streams operator« (ostreamS os, DataTnfo<T> &w);
Этот класс имеет метод Increment, который увеличивает счетчик на 1, и потоковый выходной оператор, который выводит объект в формате ”value:count". Запись с начальным значением и начальный счетчик 1
создается выполнением перегруженного потокового входного оператора который считывает значение данных из входного потока. Эти операторы отношения добавляются, чтобы облегчить поиск определенного значения данных в списке объектов Datalnfo и сортировку списка в частотном порядке.
(а) Реализуйте Datalnfo в файле datainfo.h.
(б)
7.7
Напишите main-программу, запрашивающую у пользователя количество элементов, которое будет введено в целый список. Когда этот размер будет известен, считайте фактические целые значения и создайт® Datalnfo<int> — массив с именем dataList. Используйте функцию SeqSearch, чтобы определить, находится ли значение ввода в массиве dataList. Возвращаемое значение -1 указывает на то, что новое значение было считано и, следовательно, должно быть сохранено в массиве dataList. Иначе, вводимый элемент уже имеется в массиве, и его частота должна обновляться. Завершите программу, вызывая ExchangeSort ДЛЯ сортировки dataList, и затем печатайте каждое значение и частоту-
Модифицируйте упражнение по программированию 7.6 так, чтобы сИИ-сок Datalnfo сохранял порядок, а для определения того, встречало** ли уже какое-либо значение, использовался бинарный поиск. ВоспоЛ^ зуйтесь функциями, разработанными в письменных упражнениях и 7.5. Конечно, нет необходимости выполнять сортировку.
&
Г
Классы и динамическая память

вл/Указатели и динамические структуры данных
8.2. Динамически создаваемые объекты
8.3. Присваивание и инициализация
8А Надежные массивы
8.5.	Класс String
8.6.	Сопоставление с образцом
8.7.	Целочисленные множества Письменные упражнения Упражнения по программированию
До этого момента в книге мы использовали только статические структур, данных (static data structures) для реализации классов коллекций. Обсу^Де ние включало статические массивы для реализации классов SeqList, Stack u Queue. Использование статического массива имеет свои недостатки, так его размер определяется на этапе-компиляции и может быть изменен во вре^ исполнения приложения. Каждый класс должен запрашивать приемлемо бодь шое количество элементов, чтобы удовлетворять диапазону потребностей подь зователя. Поэтому во многих приложениях некоторая часть памяти расходу' ется напрасно. В некоторых же случаях размер массива может оказаться де достаточным, и пользователь вынужден будет обращаться к исходному Код' программы, чтобы увеличить размер массива и перекомпилировать программу
В этой главе мы знакомимся с динамическими структурами данных (dyl namic data structures), которые используют память, полученную из системы во время исполнения. У нас имеется некоторый опыт распределения памяти при работе с блочной структурой программ. Компилятор распределяет гдо. бальные данные и создает автоматические переменные, для которых при входе в блок выделяется ресурс памяти, а при выходе из блока — освобождается. Например, в следующей последовательности кода компилятор C++ распределяет глобальные данные, параметры и локальные переменные:
int global =8;	// переменная доступна для всей программы
// резервирование памяти в системном стеке для параметров х и у void subtask (int х, long *у)
{
int z;	// выделение ресурса для локальной переменной г
. . .	// при выходе из блока ресурс памяти освобождается
}
Тип и размер каждой переменной известен во время компиляции.
Компиляторы также предоставляют пользователю возможность создавать
динамические данные. Оператор new выделяет ресурс памяти из динамической области для использования во время выполнения программы, а оператор delete возвращает ресурс памяти в динамическую область для последующего выделения. Динамические структуры данных находят важное применение в приложениях, потребности в памяти которых становятся известными только во время исполнения этих приложений. Использование динамических структур является основным в общем изучении коллекций и эффективно снимает ограничения на размеры, возникающие при объявлении статических коллекций. Например, в классе Stack его максимальный размер ограничивается параметром по умолчанию MaxStackSize, а от клиента требуется выполнение проверки условия заполненности стека перед добавлением (помещением) нового элемента. Динамическая память повышает возможности использования класса Stack, поскольку при этом класс запрашивает достаточный ресУРс памяти для удовлетворения потребностей приложения. Приложения баз Дай‘ ных часто используют временную память для хранения таблиц и списков» которые создаются по запросу пользователя. Ресурс памяти может выделять0
в ответ на запрос и освобождаться после выполнения запроса.
Использование динамической памяти имеет ограничения и некоторые И® удобства. Приложение может быть предназначено для работы с данными пеР менного размера, распределяемыми динамически, но при его выполнен может быть сделано достаточно запросов, чтобы в конце концов исчерп® имеющуюся в наличии память. В это.м случае пользователь получит сосбШе "Out of memory", а приложение может даже вынуждено завершиться. Та* проблема чаще всего возникает при выполнении приложения с графическ
Фейсом, поскольку обычно диалоговая программа использует ряд окон ц5те^Хбражения данных, установки меню и так далее. Даже если программа дЛЯ ° структурирована, пользователь может иметь слишком много активных *0₽0^ческих структур, из-за чего в конце концов прекратится выделение имею-граФ я памяти- При использовании динамической памяти мы должны пони-память — это ресурс, который должен эффективно управляться ма1^паммистом. Память, выделяемая динамически, должна освобождаться, > в ней отпадает необходимость. Компилятор следует этой политике Я** выделении памяти для параметров и локальных переменных. Програм-ЯРЯ должен следовать этой же политике, используя оператор delete.
предоставляет ряд методов для обработки динамических данных, называемый деструктором (destructor) удаляет динамическую память, Меезервированную объектом, при удалении этого объекта. Кроме того, класс За^сет иметь конструктор копирования (copy constructor) и перегруженный Леватор присваивания (overloaded assignment operator), которые используется для копирования или присваивания одного объекта другому. Эти методы класса находятся в центре внимания в данной главе. Для введения новых методов в большинстве примеров мы используем простой класс DynamicClass.
Динамические массивы позволяют выделять блоки памяти во время исполнения приложений. Для большинства приложений при создании массивов мы знаем необходимые размеры. Однако в особых случаях нам может понадобиться расширить границы массива и изменить его размер. Для обеспечения этой возможности здесь разрабатывается класс Array, который создает списки произвольного размера и реализует границы массива, проверяя и изменяя размер списков. Этот класс предоставляет мощную структуру данных и иллюстрирует использование деструктора, конструктора копирования и перегруженного оператора присваивания. Мы перегружаем индексный оператор [ }, поскольку хотим, чтобы объект Array выглядел подобно стандартному массиву C++.
Строки являются основной структурой данных- В действительности, некоторые языки определяют встроенный строковый тип. В этой главе разрабатывается и используется для решения задачи сопоставления с образцом полный класс String, который затем часто применяется в оставшихся главах книги.
Множества (sets) очень часто используются в математической теории. В компьютерных приложениях множества являются мощной нелинейной струк-?Р0Й данных, применяемых в таких областях, как текстовый анализ, исправление орфографических ошибок и реализация графов и сетей. Класс Set,
ЯЮ1ЦИЙ Данные интегрального типа, разрабатывается с использованием нОс ЗЬ1Х ораторов C++. Этот подход обеспечивает превосходную эффектив-Для использования памяти и времени исполнения. Класс Set применяется зыв РеализаЧии известного алгоритма при нахождении простых чисел, на-емого решетом Эратосфена (Sieve of Eratosthenes).
Указатели и динамические структуры данных
йеР6Мев-йТеЛИ Как СТРУКТУР*1 Данных вводятся в главе 2. В этом разделе ь^Делев ^“У^езатели объединяются с операторами C++ new и delete для я И освобождения ресурса динамической памяти.
Оператор new для выделения памяти
C++ использует оператор new для выделения ресурса памяти данным ъ» время выполнения программы. ’’Зная” размер данных, оператор запрашив^ у системы необходимое количество памяти для сохранения данных и во^ вращает указатель на начало выделенного участка. Если память не можр2 быть выделена, оператор возвращает О (NULL).
В следующем примере параметра, и резервирует памяти.
оператор new принимает тип данных Т в качеств память для переменной типа Т, возвращая адр^
Т *р;	// объявление р как указателя на Т
р - new Т; // р указывает на только что
// созданный объект типа Т
Далее переменные ptrl и ptr2 указывают на данные типа int и long, соответственно.
int *ptrl;
long *ptr2;
// размером int является 2
// размером long является 4
В следующем примере оператор ptrl и адрес long-переменной ptr2.
new присваивает адрес int-переменной
ptrl = new int;	// ptrl указывает на целое в памяти
ptr2 = new long; // ptr2 указывает на длинное целое в памяти
Системная память
Здесь, ptrl содержит адрес 2-х, a ptr2 — 4-х байтовых целых в памяти. По умолчанию содержимое памяти не имеет начального значения. Если такое значение необходимо, оно должно указываться в качестве параметра при использовании оператора new:
р = new Т(value);
Например, операция
ptr2 = new long(100000);
резервирует память для длинного целого и присваивает ему значение 100000.
Динамическое выделение массива
Преимущества динамического выделения памяти особенно очевидны запросе целого массива. Предположим, что в некотором приложении Pa3Ii _ массива становится известен только во время исполнения приложения. тор new может резервировать память для массива, используя запись со ск ками []. Пусть, р указывает на данные типа Т. Тогда оператор
р = new Т [п]; // выделение массива п элементов типа Т
Л предписывает р указывать на первый элемент массива. Массив, создай® таким способом, не может быть инициализирован.
Пример 8.1
В следующем примере оператор new выделяет память для массива из 60 длинных целых при условии, что имеется достаточно памяти. Если указателю р присваивается значение NULL, оператору new не удалось Оделить память и программа завершается.
A°=^newlong (50);	// выделить массив для 50 длинных целых
if <Р == WLL)
1 гГ « •Ошибка выделения памяти! "«endl;
exit(l)'	завершение программы
>
Оператор delete освобождения памяти
Управление памятью является обязанностью программиста. В C++ имеется оператор delete для освобождения (возвращения в системный ресурс) памяти, ппедварительно выделенной оператором new. Синтаксис delete прост и основы-ся на том факте, что система времени исполнения C++ сохраняет информацию о каждом вызове оператора new. Предположим, р и q указывают на динамически выделяемую память:
т *р, *Ф // р и q являются указателями на тип Т р = new Т; // Р указывает на один элемент q = new Tin); // q указывает на массив п элементов
Функция delete использует указатель для освобождения памяти. В случае освобождения массива delete применяется с оператором [].
delete р; // освобождает память переменной, на которую указывает р delete [I q; // освобождает весь массив,
Н на который указывает q
Пример 8.2
/Оператор delete освобождает память, указателем на которую является р:
Ж| 1ОПд *р;
Жй Р = new long (50); // выделение массива для 50 длинных целых	.
delete [] р- // освобождение памяти 50 длинных целых
8-2. Динамически создаваемые объекты
’гИческ°^КО Любой переменной, объект типа класс может объявляться как ста-11аЧцомЯ ИЛИ создаваемая динамически (с использованием new) переменная. В йых в слУчае обычно вызывается конструктор для инициализации перемен-Сда^аЛ^Яамнческого выделения памяти для одного или более данных-членов.
Для в^,С ИСЛользования оператора new подобен синтаксису выделения памя-® йЛдцв₽ОСтых типов и массивов. Оператор new выделяет память для объекта ВЬ13ов конструктора класса, если он существует. Конструктору ся любые необходимые параметры.
Ва ВаЯомства с динамическим созданием объектов используется основан-‘яяблоне класс DynamicClass, имеющий статические и динамические
даиные-члены. Далее следует объявление класса, методы которого разрабат^ ваются в этом и в разделе 8.3. Класс DynamicClass находится в файле dynamic ъ программного приложения:
((include <iostream.h> template <class T> class Dynamicclass i
private:
// переменная типа T и указатель на тип Т Т memberl;
Т *membe г 2;
pubJ. ic:
// конструкторы
DynamicClass(const TG ml, const TG m2);
DynamicClass(const DynamicClass<T>& obj);
/l деструктор
-DynamicClass(void);
// оператор присваивания
DynamicClass<T>G operator= (const DynamicClass<T>G rhs);
Этот простой класс с его двумя данными-членами иллюстрирует основное действие функций-членов в обработке динамически распределяемых объектов. Класс предназначен только для демонстрационных целей и не имеет реального применения.
Конструктор этого класса использует параметр ml для инициализации статического данного-члена memberl. Для данного-члена member2 требуется выделение памяти типа Т и инициализация ее значением m2:
// конструктор с параметрами для инициализации данного-члена template <class Т>
DynamicClass<T>::DynamicClass(const TG ml, const TG m2)
(
// параметр ml инициализирует статический член класса memberl = ml;
// выделение динамической памяти и инициализация ее значением m2 memner2 - new Т(т2);
cout «"Конструктор;" « memberl « ' Г « *тетЬег2 « endl;
Пример 8.3
Следующие операторы определяют статическую переменную staticObj и указатель-переменную dynamicObj. Объект staticObj имеет параметры 1 и 100, которые инициализируют данные-члены:
// объект типа DynamicClass
DynamicClass<int> staticObj(1, 100);
Объект, на который указывает dynamicObj, создается оператором Параметры 2 и 200 передаются конструктору в качестве параметров. создании объекта ^dynamicObj конструктор класса инициализирует ДаВ' ные-члены значениями 2 и 200:
// переменная-указатель
. DynamicClass<int> *dynamicObj;
<* // создание объек™° dynamicObj ~ new DynamicClass<int> (2, 200);

Системная память
staiicobj
Освобождение данных объекта: деструктор
рассмотрим функцию DestroyDemo, которая создает объект DynamicClass, яв4еЮЩий целые данные.
void Destroy06™0(int ml, int m2)
f DynamicClass*int> obj (ml, m2);
При возвращении из DestroyDemo объект obj уничтожается, однако, процесс не освобождает динамическую память, связанную с объектом. Эта ситуация показана на рис. 8.1.
До удаления объекта obj
Системная память
После удаления объекта obj Системная память
Рис. 8.1 Необходимость использования деструктора
Для эффективного управления памятью необходимо освобождать динамические данные объекта в то же самое время, когда уничтожается объект. Нам следует выполнить действие конструктора в обратном порядке, который пер-°началъно распределял динамические данные. Язык C++ предоставляет функ-
’2’ЧЛен, называемую деструктором (destructor), которая вызывается при Лени^.0?КвНИИ °^ъекта- Для DynamicClass деструктор имеет следующее объяв-
tJynajtlicCiass (void);
Полн1МВОЛ представляет ’’дополнение", поэтому -DynamicClass — это до-1цВемЫИе констРУктора. Деструктор никогда не имеет параметра или возвра-
ТИпа- В данном простом случае деструктор отвечает за освобождение мических данных для member2:
-освобождает память, выделенную конструктором
С^а»ц^С1а33 Т>
{ ass<T>;: -DynamicClass(void)
cour<^n
« **еСтруктор: ’’<< memberl «' /'
, ОеиЙ1*1612 « endl;
I Се Юед1Ьег2;
Деструктор вызывается всякий раз при уничтожении какого-либо объект^. Когда программа завершается, все глобальные объекты или объекты, обьц^’ ленные в main-программе, уничтожаются. Для локальных объектов, созда веемых внутри блока, деструктор вызывается при выходе программы блока.
Программа 8.1. Деструктор
-	~	-	-	-	-	-Г.	Т -T1IL		_
Эта программа иллюстрирует определение и использование деструктора Программа тестирования включает три объекта. Objl является переменной, объявляемой в main-программе, a Obj_2 ссылается на динамический объект. В программу включена ранее обсуждавшаяся функция Destroy. Demo, которая объявляет локальный объект obj. На рис. 8.2 отмечены различные случаи использования конструкторов и деструкторов объектов.
void, DestroyDemo(int ml, int m2)
DynamicClass<int> Obj (ml,m2) ♦———-i-----------------------------------------
void main(void)
DynamicClass<int>Obj_l(1,TOO),’,Obj_2; < Obj_2 = new DynamicClass<int>(2,200);< DestroyDemo(3,30G);
Конструктор для Obj (3.300) Деструктор для Obj
Конструктор для Obj_1 (1.100)
Конструктор для *0bj_2 (2.200)
delete Obj_2; <---------------------------Деструктор для Obj_2
Деструктор для Obj_1
Рис 8.2. Инициализация для DynamicClass А(3,5), В = А
((include <iostream.h>
#pragma hdrstop	«
#include "dynamic.h"
void DestroyDemofint ml, int m2) (
DynamicClass<xnt> obj(ml,m2);
)
void main(void) (
If создать объект Obj_l c memberl=l и *member2=100
DynamicClass<int> Objl(1,100);
// объявить указатель на объект DynamicClass<int> *0bj_2;
// создать объект с memberl = 2 и *member2 = 200,
*I/ на который будет указывать Obj_2
Obj_2 = new DynamicClass<int>(2,200);
// вызвать функцию DestroyObject с параметрами 3, 300
De stroyDemo(3,300);
11 полное удаление Obj_2
delete Qbj_2,-
cout << ’'Программа готова к завершению” « endl; }
выполнение программы 8.1>
Конструктор: 1/100
Конструктор: 2/200
Конструктор: 3/300
деструктор: 3/300
деструктор: 2/200
ппогоамма готова к завершению
деструктор: 1/100
*/
8.3. Присваивание и инициализация
Присваивание и инициализация являются базовыми операциями, применяемыми к любому объекту. Присваивание Y = X приводит к побитовому копированию данных из объекта X в данные объекта Y. Инициализация создает новый объект, который является копией другого объекта. Эти операции показаны на примерах с объектами X и Y:
// создать оОъекты X и Y типа Dynamicclass
U данные объекта Y инициализируются данными X
Dynamicclass Х(20, 50}, У = X;
У 'X; // данные объекта У переписываются из данных х
Особое внимание необходимо уделить динамической памяти, чтобы избежать нежелательных ошибок. Мы должны создавать новые методы, управляющие присваиванием и инициализацией объектов. В этом разделе сначала обсуждаются потенциальные проблемы, а затем создаются новые методы класса.
Проблемы присваивания
ва^НстРУктоР Для DynamicClass инициализирует memberl и выделяет ди-Ческие данные, на которые указывает member2. Например, в объявлении йс Тов А и В мы создаем два объекта и два связанных блока памяти с ользованием оператора new.
Системная память	Системная память
А
В
Оператор присваивания В = А приводит к тому, что данные объекта д копируются в В.
// копировать статические данные из А в В:
// member! объекта В = memberl объекта А
// копировать указатель из А в В
// member2 объекта В ~ member2 объекта А
Так как значению указателя member2 в объекте В присваивается значение указателя member2 в объекте А, оба указателя теперь ссылаются на один тот же участок памяти, а на динамическую память, первоначально присво, енную В, ссылки теперь нет. Предположим, что оператор присваивания появляется в функции F.
void F(void) {
DynamicClass<int> А(2,3), В(7,9);
В = А; // присваивание объекта А объекту В
Неправильное присваивание: В=А
При возвращении из функции F все объекты, созданные в блоке, уничтожаются вызовом деструктора класса, освобождающего динамическую память, на которую указывает member2. Предположим, объект В уничтожается первым. Деструктор освобождает память, на которую указывает B.member2 (и одновременно A.member2). При уничтожении объекта А вызывается его деструктор для освобождения памяти, связанной с указателем-переменной A.member2, но этот блок памяти ранее уже был освобожден при уничтожении В, поэтому использование операции delete в деструкторе для А является ошибкой! Во многих случаях это — фатальная ошибка.
Проблема заключается в операторе присваивания В = А. Указатель mem-Ьег2 в А копируется в указатель member2 в В. На самом деле мы хотим, чтобы содержимое, на которое указывает member2 из А, было скопировав0 в участок памяти, на который указывает member2 из В.
Правильное присваивание: В=А
Перегруженный оператор присваивания
я а правильного выполнения присваивания объектов в случаях, когда ^асается динамических данных, C++ позволяет перегружать оператор вв11вания = как функцию-член. Синтаксис для перегруженного оператора ЯР^нИвания в DynamicClass следующий:
•eClass<T>& operator= (const DynamicClass<T>& rhs);
pyna^1C
ВйЯарный оператор реализуется как функция-член с параметром rhs, пред-аВляюШИМ операнд в правой части оператора. Например
з *
Перегруженный оператор = выполняется для каждого оператора присва-яия> включающего объекты типа DynamicClass. Вместо простого побито-8 копирования данных-членов из объекта А в В, перегруженный оператор отвечает за явное присваивание всех данных, включая закрытые и открытые яяяые-члены, а так же данные, на которые указывают эти члены. Параметр rhs передается по константной ссылке. Таким образом, мы избегаем копирования в этот параметр того, что могло бы быть большим объектом в правой ч/лти, и не допускаем никакого изменения объекта. Заметим также, что где бы ни использовалось имя шаблонного класса как типа, необходимо добавлять “<Т>” в конец имени класса.
Для DynamicClass оператор = должен присваивать значение данных mem-berl объекта rhs значению данных memberl текущего объекта и копировать содержимое, на которое указывает member2 объекта rhs, в участок памяти, на который указывает member2 текущего объекта:
// перегруженный оператор присваивания. // возвращает ссылку на текущий объект template<class Т>
DynamicClass <Т>& DynamicClass<T>::operator-(const DynamicClass<T>& rhs) I
// копирование статического данного-члена иэ rhs
// в текущий объект
“*егаЬег1 «= rhs. member 1;
// содержимое динамической памяти должно быть тем же,
II что и содержимое rhs
• member2 = *rhs.member2;
cout «"Оператор присваивания:" <<inemberl<<'/'
« *member2 « endl;
return *this,-
Te ^ареэеРВиРованное слово this используется для возвращения ссылки на цще 'И“ объект и обсуждается в следующем разделе. Операторы, перенося-tto flaaHbie из объекта rhs в текущий объект, гарантируют правильное вы-Ние оператора присваивания
оператор — возвращает ссылку на текущий объект, мы можем цРИМер^ВН° связывать вместе два или более операторов присваивания. На-С = с
° ” А* //
'' Результат (В - А) присваивается С
Указатель this
j
Каждый объект C++ имеет указатель с именем this, определяемый авто, магически при создании объекта. Идентификатор является зарезервировав ным словом и может использоваться только внутри функции-члена класса Он является указателем на текущий объект, a *this — это сам объект. пример, в объекте А типа DynamicClass
// "this — это объект А;
// this->roeinberl — это member 1, значение данных в А
// this->member2 — это memberZ, указатель в А
Для оператора присваивания возвращаемое значение является ссылочные параметром. Выражение ’’return *this" возвращает ссылку на текущий объект
Проблемы инициализации
Инициализация объекта — это операция, создающая новый объект, который является копией другого объекта. Подобно присваиванию, когда объект имеет динамические данные, эта операция требует особую функцию-член, называемую конструктором копирования (copy constructor). Мы можем, забегая вперед, обсудить действие конструктора копирования на примере:
DynamicClass<int> А(3,5), В = А; // инициализация объекта В данными объекта А
Это объявление создает объект А, начальными данными которого являются member 1 = 3 и *member2 — 5, и объект В с двумя данными-членами, которые затем структурируются для сохранения тех же значений данных, которые помещены в А. Процесс инициализации должен включать копирование значения 3 из объекта А в member 1 объекта В, выделение памяти для данных, на которые указывает member2 объекта В, и затем копирование значения 5 из *A.member2 в динамически распределяемые данные объекта В.
Инициализация DynamicClass: В=А
№3: колировать
№1: копировать содержимое
Инициализация осуществляется не только при объявлении объектов, Н© й при передаче объекта функции в качестве параметра по значению, и при в°3' вращении объекта в качестве значения функции. Например, предположим, 470 функция F имеет передаваемый по значению параметр X типа Dynain1C Class<int>.
DynamicClass<int> F(DynamicClass<int> X) // параметр передаваемый // по значению
DynamicClass<int> obj;
return obj;
)
гда вызывающий блок использует объект А как фактический параметр, объект X создается копированием объекта А:
rtQjcaJIbB
“ Class<int> А(3,5), В(0,0);	// объявление объектов
// вызов F копированием А в X
гг я выполнении возврата из F создается копия obj, вызываются деструк-па локальных объектов X и obj и копия obj возвращается как значение торы
фуДКЦ011-
Создание конструктора копирования
tl-гобы правильно обращаться с классами, которые выделяют динамичес-память, C++ предоставляет конструктор копирования для выделения Кинамической памяти новому объекту и инициализации его значений данных, flbi иллюстрируем эту идею, разрабатывая конструктор копирования для DynamicClass.
Конструктор копирования является функцией-членом, которая объявляется с именем класса и одним параметром. Поскольку это — конструктор, qg не имеет возвращаемого значения:
DynamicClass (const DynamicClass <т>& х); // конструктор копирования
Конструктор копирования DynamicClass копирует данные из memberl объекта X в текущий объект. Для динамических данных конструктор копирования выделяет память, на которую указывает member2, и инициализирует ее на значение содержимым *X.member2:
// конструктор копирования.
Ц инициализирует новый объект теми же данными, что и в X template <class т>
DynamicClass<T>_• -.DynamicClass(const DynamicClass<T>& X)
// копировать статический данное-член из X //в текущий объект memberl ~ X.memberl;
// ввделить динамическую память и инциализировать ее 7/ значением *x.meniber2.
meiriberZ - new T(*X.member2) ;
cout « "Конструктор копирования: ” « memberl
« '/* « *member2 « endl;
(

®®сомНенно' объект
ели класс имеет конструктор копирования, этот конструктор использу-к ® компилятором всякий раз при выполнении инициализации. Конструктор рования используется только тогда, когда создается объект.
»_есЬ10Т₽я Иа свое сходство, присваивание и инициализация являются, -J» различными операциями. Присваивание выполняется, когда BoE?1Ig ® левой части уже существует. В случае инициализации создается 6о воем°^Ъе1СТ КопиРованием данных из существующего объекта. Более того, цля в я процесса инициализации перед копированием динамических данных Пап еления памяти должен использоваться оператор new.
HeSbXno ет^ в конструкторе копирования должен передаваться по ссылке. ,{0,вПилЛНеаИе этого может привести к катастрофическим последствиям, если СтРУктог.ТО₽ не Распознает ошибку. Предположим, что мы объявляем кон-Dvf, 1сопиР°вания с передаваемым по значению параметром: jrnamiccla
58<OynamicClass<T> X) ;
Конструктор копирования вызывается всякий раз, когда параметр ции указывается как передаваемый по значению. Предположим, что объе^ А в конструкторе копирования передается параметру X по значению.
Dy ram icClass( DynamicClass X)
Так как мы А передаем в X по значению, должен вызываться конструктор копирования для выполнения копирования А в X. Этот вызов, в свою очередь нуждается в конструкторе копирования, и мы имеем бесконечную цепь вызовов конструктора копирования. К счастью, эта потенциальная проблема распознается компилятором, который указывает, что параметр должен пере-даваться по ссылке. Кроме того, ссылочный параметр X должен объявляться константным, так как мы определенно не хотим изменять объект, который копируем.
Программа 8.2. Использование DynamicClass
Эта программа иллюстрирует действие функций-членов DynamicClass с использованием целых данных.
flinclude <iostream.h>
flinclude "dynamic.h"
template <class T>
DynamicClass<int> Demo (Dynamicdass<T> one, DynamicClass<T>& two, T m) {
ll вызов конструктора с (memberIя m, *member2= m) Dynamicciass<T> obj(m,m);
// копирование для obj выполнено.
// возвратить его как значение функции
return obj;	г
)
void main О
<
/♦ АО, 5) вызывает конструктор с (mernberl-3, *member2=5) в = А вызывает конструктор копирования для инициализации В данными объекта А: (member1=3, *member2=5)
объект С вызывает конструктор с (inemberl=O, *member2=0) */ DynamicClass<int> А(3,5), В = А, С(0,0);
/* вызов функции Demo, конструктор копирования создает параметр one (member1=3, *member2=5) копированием из А. параметр two передается по ссылке, поэтому конструктор копирования не вызывается, при возра1ден^и создается копия локального объекта obj, который присваивается объекту С	*/
С - Demo(А,В,5);
// остальные объекты удаляются при выходе из программы
I
конструктор: 3/5 ,LcT₽yKTOp копирования
Конструктор: 0/0
Конструктор копирования
конструктор копирования дестрУктоР: 5/5564 деструктор: 3 5556 Оператор присваивания:
^выполнение программы 8.2>
: 3/5
: 3/5
: 5/5
5/5
деструктор: 5/5
дестрУкт°Р: 3 5
дестрУкт°Р: 35
*/
8.4. Надежные массивы
L-
Статический массив — это коллекция, содержащая фиксированное коли-!чество элементов, ссылка на которые выполняется индексным оператором. Статические массивы являются основной структурой данных для реализации ; списков. Несмотря на свою важность, статические массивы создают опреде-< ленные проблемы. Их размер устанавливается во время компиляции и не j может изменяться во время исполнения приложения.
В ответ на ограничения, свойственные статическим массивам, мы создаем |' основанный на шаблоне класс Array, содержащий список последовательных [ элементов любого типа данных, размер которого может быть изменен во время выполнения приложения. Этот класс содержит методы, реализующие индексацию и преобразование типа указателя. Чтобы был возможен индексный доступ к элементам в списке, мы перегружаем индексный оператор (index орега-' tor) [1- Более того, мы проверяем, чтобы каждый индекс соответствовал эле- -м^нту в списке. Это свойство, называемое проверкой границ массива (array rounds checking), генерирует сообщение об ошибке, если индекс находится вне П>аниц. Полученные в результате объекты называются надежными массивами I, Ге аггаУа), поскольку мы реагируем на неверные индексные ссылки. Для стай1 ЧТо®ы об'ьект массива мог использоваться с функциями, принимающими BaBifai)THbIe параметры массива, мы определяем общий оператор преобразо-соб «^теля (pointer conversion operator) Т*, связывающий объект Array “Чным массивом, элементы которого — это элементы типа Т.
Класс Array
бог«0С^0ваввь1й на шаблоне класс Array поддерживает список элементов лю-Данных.
0	12	3	size-1
данные типа Т
Спецификация класса Array
ОБЪЯВЛЕНИЕ
flinclude <lostream.h> flinclude <stdlib.h>
flifndef NULL
const int NULL = 0;
flendif
enum ErrorType
{invalidArraySize, memoryAllocationError, indexOutOfRange};
char *errorMsgf} ” <
"Неверный размер массива", "Ошибка выделения памяти", "Неверный индекс: "
);
template <class Т>
Class Array (
private:
// динамически выделяемый список размером size
Т* alist; int size;
// метод обработки ошибок
void Error(ErrorType error,int badlndex=O) const;
public:
// конструкторы и деструктор
Array(int sz  50);
Array(const Array<T>& A);
-Array(void);
// присваивание, индексация и преобразование указателя
Array<T>6 operator= (const Array<T>& rhs);
T& operator!](int i); operator T* (void) const;
// операции с размером массива int ListSize(void) const; // читать size void Resize(int sz);	// обновлять size
};
ОБСУЖДЕНИЕ
Использование перегруженного индексного ([]) и оператора преобразования позволяет объекту Array функционировать подобно обычному, определенному языком программирования, массиву. Оператор присваивания расширяет в°3' можности массива, реализуя присваиваний одного объекта Array другому-определенных же языком программирования массивов присваивание является неверной операцией.
Метод Resize позволяет изменять размер списка. Если параметр sz больше» чем текущий размер массива (size), старый список сохраняется и к массиву добавляются дополнительные элементы. Если sz меньше, чем текущий разМеР’ сохраняются первые sz элементов в массиве, остальные — удаляются.
ПРИМЕР
Array<int> А(20);	// массив из 20 целых
cout « A.Size(); .	// вывод текущего размера 20
i==0; i<20; i++) ’'„ilzeOO),- A ₽ci « 50;	// доступ к массиву с использованием [] // неверный индекс // размер массива увеличивается на 30; // теперь верный индекс	
2zlnaesort (a, 30);	//	преобразование позволяет использовать
	//	параметр Array
Выделение памяти для класса Array
В этом разделе показаны конструктор, деструктор и конструктор копирова-Л выполняющие необходимую проверку наличия ошибок.
^Конструктор класса выделяет динамический массив, элементы которого — элементы типа Т. Начальный размер массива определяется параметром sz, ^орый имеет значение по умолчанию 50:
// конструктор
teoiplate<class Т> мгау<т>::А«ау(int sz)
* // проверка на наличие параметра неверного размера if(sz<= 0)
Error(invalidArraySize);
И присваивание размера и выделение памяти size - sz;
alist = new T[size];
// убеждаемся в том, что система выделяет необходимую память, if (alist — NULL}
Error (wexnoryAllocationError) ;
Деструктор освобождает память, выделенную для массива alist:
// деструктор ternplate-cclass Т>
Лггау<т>::-Array(void)
delete [] alist; J
Конструктор копирования делает возможными операции, которые недоступ-Для определенных языком программирования массивов, позволяя инициализировать один массив элементами другого массива (X) и передавать объект ГаУ какой-либо функции по значению. Для этого извлекается размер объекта эЛ,ВЬ1Деляется соответствующий объем динамической памяти и копируются
 *
копирования
?ГгаУ<Й. class Т>
•-Array(const Array<T>i X)
// получить размер объекта х и присвоить текущему объекту int n = x.size;
size *= n;
// выделить новую память для объекта с проверкой
// возможных ошибок
alist = new T[nj;	// динамически созданный массив
if (alist =* NULL)
Error(memoryAllocationError);
Il копировать элементы массива объекта X в текущий объект
Т* srcptr ~ X.alist; // адрес начала X.alist
т* destptr  alist; // адрес начала alist
while (п—) // копировать список
*destptr++ - *srcptr++;
Проверка границ массива и перегруженный оператор []
Ссылка на индекс массива выполняется с использованием оператора (] и появляется в выражении, имеющем форму
₽[п]
где Р — это указатель на тип Т, а п — это целое выражение. Фактически, это — оператор, называемый в языке C++ оператором индексации массива (array indexing operator). Этот оператор имеет два операнда (Р и п) и возвращает ссылку на данные в позиции Р + п.
Р[0)	Р[1]		Р[п]	
IтI
Р	Р+1	Р+п
Оператор может быть перегруженным только как функция-член и обеспечивает индексированный доступ к данным объекта.
Мы перегружаем индексный оператор для класса Array. Предположим, что А — это объект Array целого типа. Доступ к элементам массива получаем использованием записи А[п]. Например, оператор
А[0] - 5
присваивает значение 5 первому элементу в массиве (alist[O] = 5).
Объявление функции-члена [ ] принимает форму
Т& operator [ ] (int n);
где Т — это тип данных, хранящихся в объекте, ап — это индекс. Тот факт» что перегруженный оператор [] возвращает ссылочный параметр, означает, что оператор индексации может находиться в левой части оператора присваивания-
value « А(п);	// переменной value присваивается А(п]
А[п) - value; // элементу п массва А присваивается // значение value
Для класса Array перегруженный оператор индексации предоставляет ступ к надежному массиву, проверяя, находится ли индекс п в диапазо^ индексов массива (от О до size-1). Если он не находится в этом диапазон »
пцтся сообщение об ошибке и программа завершается. Иначе оператор решает значение alistfn].
п6Груженный индексный оператор
И rtiate<class т>
:operator! ] (int n)
. вЫПолнение проверки границ массива
А (п<0 n>size-l)
Error(indexOutOfRange,п);
. возвращается элемент из закрытого списка массива
return alistfn);
Преобразование объекта в указатель
Преобразованием указателя пользователь получает возможность использовать объект Array как параметр времени исполнения в любой функции, определяющей обычный массив. Это выполняется перегрузкой оператора преобразования Т*(), которая преобразует объект в указатель. В данном случае мы преобразуем объект Array в начальный адрес массива alist.
Объект А
Например, функции ExchangeSort и SeqSearch принимают параметр массива. Для объекта А шаблонного целого типа следующие операторы функции являются верными:
//сортировать A (size О элементов)
ExchangeSort (A, A. size ()) ;
// поиск ключа в А
index » SeqSearch (A, a. size О, int key);
В вызове функции объект А передается формальному параметру Т*агг.
Объявление: ExchangeSort(T*arrJnt п)
Вызов:	ExchangeSorff A, A.size());
зат^/ ^нводит к выполнению оператора преобразования и присваиванию ука-ля alist переменной агг. Переменная агг указывает на массив объекта А:
указателя
<	•:operator Т*(void) const
retn^SBPauiaeT аДрес закрытого массива в текущем объекте
)	'•“«•п alist;
Ь°3аРапГТ°®>Ь1 Изменения размера. Класс Array предоставляет метод ListSize, ЧвЛя^ся текУЩее количество элементов в массиве. Более динамичным Метод Resize, который изменяет количество элементов массива в
объекте. Если требуемое количество элементов sz равно размеру текущеГо объекта, выполняется простой возврат; иначе выделяется новое пространство памяти. Если размер списка уменьшается (sz<size), первое sz количество элементов копируется в новый массив.
Уменьшение	sz
размера |	~]
новый list	,
т т старый list	|	j
0	12	3	sz -1	Xsize -i
Если мы увеличиваем размер массива, старые элементы копируются в новый список и в наличии остается некоторая неиспользованная часть списка. В каждом из двух случаев память для старого массива удаляется.
Увеличение размера
новый list
старый list
sz
srcptr = &X.alist(size]
// оператор изменения размера (resize-оператор) template <class Т>
void Array<T>::Resize(int sz) {
// проверка нового размера массива;
// выход из программы при sz <= 0
if (sz <= 0)	*
Error(invalidArraySize);
// ничего не делать, если размер не изменился if (sz size)
return;
// запросить память для нового массива и проверить ответ системы Т* newlist « new T[sz];
if (newlist == NULL)
Error(memoryAllocationError);
// объявить n и инициализировать значением sz или size int n = (sz <= size) ? s& : size;
// копировать n элементов массива из старой в новую память
Т* srcptr =* alist; // адрес начала alist
Т* destptr = newlist; // адрес начала newlist
while (п—)	// копировать список
*destptr++ ~ *srcptr++;
./ удалить старый список
^lete [1 alist;
переустановить alist» чтобы он указывал на newlist
// и обновить член класса size
alist • newlist;
size - sz;
J	--------------------------------------------
Использование класса Array
реализация класса Array иллюстрирует большинство идей этой главы, ггпльзояатель может использовать класс Array вместо определенных языком * ограммирования массивов и пользоваться преимуществами надежности и бкости, обеспечиваемыми возможностью изменения размера.
Программа 8.3. Изменение размера массива
Пусть Array-объект А определяет список из 10 целых, в котором мы сохраняем простые числа:
Определение: Простое число — это положительное целое 2, которое делится только на себя и на 1.
Данная программа определяет все простые числа в диапазоне 2..N, где N — это предоставляемая пользователем верхняя граница. Так как мы не можем заранее установить размер массива на необходимый, программа проверяет условие ’’список полный", сравнивая текущее количество простых чисел (primecount) с размером массива. Когда список полный, мы изменяем размер списка и добавляем еще 10 элементов. Программа завышается печатью списка простых чисел по 10 в строке.
linclude <iostream.h>
linclude <iomanip.h>
♦pragma hdrstop
linclude "array.h"
void main (void)
// начальный размер массива А равен 10 Array<int> A(10);
// пользователь задает верхнюю границу диапазона поиска
int upperlimit, primecount = 0, i, j;
cout « "Введите число >= 2 как верхную границу диапазона: ";
cin >:> upperlimit;
^Iprimecount++] ж 2;	// 2 — простое число
f°*(i ж 3; i < upperlimit; i++)
// если список простых чисел полный, добавить к нему еще 10 элементов (ptimecount == A.ListSizeO)
A.Resize(primecount + 10);
fl четные числа > 2 — непростые.
*• перейти к следующей итерации
if (i % 2 -- 0) continue;
// проверить нечетные делители 3,5,7,... до i/2
j = 3,-
while (j <= i/2 S4 i % j_! = 0)
j += 2;
If i — простое, если не делится на 3,5,7,... до i/2
if (j > i/2)
A[primecount++] = i;
J
for (i = 0; i < prxmecount; i++)
{
couc « secw(5) « A(i);
// вывести новую строку из 10 простых чисел
if ((i+1) % 10 == 0) cout « endl;
)
cout « endl;
)
/*
«Выполнение программы 8.3>
Введите число >= 2 как верхную границу диапазона: 100
2	3	5	1	11	13	17	19	23	29
31	37	41	43	47	53	59	61	67	71
73	79	83	89	97
8.5. Класс String
Строки являются основным компонентом многих нечисловых алгоритмов.
Они используются в таких областях, как сопоставление с образцом, компи-
ляция языков программирования и обработка данных. По существу, полезно иметь строковый тип данных, который инкапсулирует разнообразные операции обработки строк и делает возможными расширения. Строковые переменные C++ являются массивами символов с нулевым символом в конце. Каждая система программирования C++ предоставляет библиотеку функции в <string.h> для сопровождения операций обработки строк. В руках опытного программиста функции являются мощным средством реализации эффективных строковых алгоритмов. Однако для многих приложений функции явля-
ются чем-то техническим и неудобным для использования.
Некоторые языки программирования, подобные языку BASIC, определяют операторы для обработки строк. Например, строковая переменная BASIC заканчивается символом и поддерживает присваивание оператором =“ 11 сравнение строк оператором <. Строковый тип является частью определен^ языка BASIC:
NAMES = JOE
IF NAMES < STUDENTPRESS THEN .
// присваивание
// сравнение
ДЛЯ некоторых языков программирования компиляторы предоставляют ласЛШРеЯИЯ’ которые обеспечивают возможность улучшенной обработки Срок- Большинство программистов C++ хотели бы получить такое расши-с яле Для боЛее гибкого доступа к строкам.
В этом разделе описывается класс String, определяющий строковый тип и ^доставляющий мощный набор строковых методов. Объекты используют ди-й^4ическую память для сохранения строк переменной длины и перегруженные 0цератоРы Для с°здания строковых выражений. Класс String предоставляет пользователю альтернативный строковый тип и тем не менее обеспечивает доляое взаимодействие со стандартными строками C++ (C++String). Класс String используется в последующих главах этой книги, и вы найдете достойной его простоту. Для иллюстрации использования класса String рассмотрим задачу сравнения и присваивания строк S и Т. Следующие операторы сопоставляют использование библиотеки функций C++ и класса String. Мы присваиваем меныпую строку переменной firststring.
раздние строковой библиотеки C++ iff strcmp( S, Т ) < 0 )
strcpy( firststring, S );
else
strcpyt firststring, T );
Решение класса String firststring « f S < T ) ? S : T;
Этот раздел включает полный листинг объявления класса String. Здесь обсуждается реализация избранных методов и предоставляется программа тестирования. Полный листинг класса String находится в файле strclass.h программного приложения. Раздел 8.6 знакомит с алгоритмом сопоставления с образцом, который широко используется в классе String.
Спецификация класса String
ОБЪЯВЛЕНИЕ
lifndef STRING_CLASS
Ide fine STRINGCLASS
linclude <iostream.h>
’include <string.h>
"include <stdlib.h>
lifndef NULL
Я°П6Ъ int NULL ~ 0;
*endif. // NULL
°nst int outOfMemory = 0, indexError =1;
^«ss string
Private:
ff указатель на динамически создаваемую строку.
// длина строки включает NULL-символ
char *str;
int size;
И Функция сообщения об ошибках
O1d Error(int errorType, int badlndex = 0) const;
Public;
^°НСТРУКТ°рЫ
tring (char *s =
String(const Strings s):
// деструктор
-String(void);
// операторы присваивания -
// String = String, String - C++String
Strings operator= (const Strings s);
Strings operator= (char *s);
// операторы отношений
// String==String, String-=C++String, C++String=-String int operator-^ (const Strings s) const;
int operator=~ (char *s) const;
friend int operator== (char *str, const Strings s);
// String’“String, String!=C++String, C++String!=String int operator! = (const Strings s) const;
int operator!= (char *s) const;
friend int operator!» (char *str, const Strings s);
// String<String, String<C++String, C++String<String
int operator< (const Strings s) const;
int operator< (char *s') const;
friend int operator* (char *str, const Strings s);
// String*=String, String*=C++String, C++String*=String
int operator<= (const Strings s) const;
int operator*^ (char *s) const;
friend int operator*1» (char *str, const strings s);
11 String>String, String>C++String, C++String»String
int operator» (const Strings s) const;
int operator» (char *s) const;
friend int operator» (char *str, const Strings s);
// String»=String, String»=C++String, C++String»-String int operator»^ (const Strings s) const;
int operator>= (char *s) const;
friend int operator»- (char *str, const Strings s):
11 операторы String-конкатенации
// String+String, String+C++String, C++String+String
// String +- String, String += C++String
String operator* (const Strings s) const;
String operator* (char *s) const;
friend String operator* (char *str,const Strings s);
void operator+» (const String^ s);
void operator+= (char *s);
// String-функции
// начиная с первого индекса, найти положение символа с int Find(char с, int start) const;
// найти последнее вхождение символа с
int FindLast(char с) const;
// выделение подстроки
String Substr(int index, int count) const;
11 вставить объект String объект String
void Insert(const Strings s, int index);
// вставить строку типа C++String в строку типа String
void Insert(char *s, int index);
// удалить подстроку
void Remove(int index, int count);
ц string-индексация char® operator [] (int n);
11 преобразовать String в C++String operator char* (void) const;
II string-ввод/вывод
friend ©streams operator« (©streams ostr, const Strings s);
friend istreamS operator» (istreamb istr. Strings s);
// читать символы до разделителя int Readstring(istreamS is=cin, char delimiter»'\n');
I/ дополнительные методы int Length(void) const; int IsEmpty(void) const; void Clear(void);
CSAZbSXS*
Объекты класса String могут взаимодействовать co строками C++ (char*). Например, следующие три функции предназначены для конкатенации строковых переменных с использованием оператора +:
// string + String
String operator+ ( const Strings s );
U string + C++String
String operator* ( char *s );
I! C++String + String
friend String operator* ( char *str,const Strings s );
Класс String имеет деструктор, конструктор копирования и два перегруженных оператора присваивания, позволяющие пользователю присваивать объект типа String или строку C++ новому объекту String:
String S(Hello ), T - S, R; // T = Hello , R - NULL-строка И- nMorld’";
Класс реализует разнообразные операторы конкатенации строк, включая оператор += для конкатенации строки в текущую строку:
п7 т + "World!”;	// R = "Hello world!”
д +» j
R += с.
a'	// R - "Hello World! Hello "
операторов сравнения используют упорядочение ASCII для сравнения строк:
' v("Smithsonian"), «("Thomas");
if (W * L‘  *	// FALSE
if ("7r^<. *homaS") ...	// TRUE
1ж«> - . .	// TRUE
Кл
String предоставляет несколько мощных и полезных строковых one-возможность поиска определенного символа в строке, подстроки, вставку одной строки в другую и удаление подстроки.
*°Го символа в строке имеется индексный доступ как для простого льного массива.
Z/ ri^Hc>/^?!th3onian);
* начиная с позиции с индексом О
sindex - V.Find('s',0);
R = V.Substr (sindex, 3) ;	// R - "son”
V.Remove (sindex, 6);	// V =» "Smith"
R[0) - 'S';	// R = "Son”
R.Insert("ilvert”,1);	// R - "Silverton"
Оператор ввода » использует пробел для разделения строкового ввода-
cin » S » Т » R;
<Ввод> Separate by blanks
S ^'Separate* 1' T = "by"R  "blanks”
Метод ReadString считывает символы до ограничительного символа, который заменяется на NULL-символ:
R.ReadString(cin);
<Ввод> The fox leaped over the big brown dog<newline>
R = "The fox leaped over the big brown dog”
Программа 8.4. Использование класса String
Эта программа иллюстрирует избранные методы класса String. Каждая операция включает оператор вывода, описывающий ее действие.
«pragma hdrstop «include "strclass.h”
«define TF(b) ((b) ? "TRUE” : "FALSE")
1	void main(void)
{
String sl(”STRING *’) , s2("CLASS");
String s3; int i ;
char c, cstr(30J;
s3 - si + s2; cout « si « “объединена c '* « s2 « ” — " « s3 « endl;
cout « "Длина " « s2 « " = ” « s2.Length() endl; cout « "Первое вхождение ' S' в ” « s2 « " = " «
j!	s2. Find ('S' ,0) « endl;
cout « "Последнее вхождение 'S' в " « э2 « " - " «
।	s2.FindLast('S') « endl;
cout « “Вставить 'OBJECT ' в s3 в позицию 7." « endl; s3.Insert("OBJECT ",7); cout « s3 « endl;
si = "FILE1.S";
for(i=0;i < si.Length();i++) (
c = si[i]; if (C >= 'A* && c <- ’ Z') {
c += 32;	// преобразовать в нижний регистр
sl[i] = с;
) )
cout « "Строка ' FILE1. S' преобразована в нижний регистр:
j
cout « si « endl;
cout « "Тестирование операций отношения:
cout « ”si = 'ABCDE' s2 = 'BCF’n « endl;
sl ж "ABCDE";
S2 -= "BCF";
cout « “sl < s2 - ” « TF(sl < s2) « endl;
cout « "sl == s2 - " <c TFtsi == S2) « endl;
cout « "Используйте 'operator char* О' для получения sl” " как строки C++:
strcpy(ostr,sl); cout « cstr « endl;
}
/*
выполнение программы 8.4>
STRING объединена c CLASS = STRING CLASS
Длина CLASS = 5
Первое вхождение 'S' в CLASS = 3
Последнее вхождение ' S' в CLASS — 4
Вставить ' OBJECT * в s3 в позицию 7.
STRING OBJECT CLASS
Строка 'FILE1.S' преобразована в нижний регистр: f ilel.s
Тестирование операций отношения: sl = 'ABCDE' s2 = 'BCF' sl < s2 - TRUE sl —о s2 — FALSE
Используйте 'operator char* О’ для получения sl как строки C++: ABCDE */
Реализация класса String
Данный раздел содержит обзор реализации класса String. Данными-чле-яами являются переменная-указатель str, содержащая адрес строки с нулевым завершающим символом, и size, содержащая длину строки + 1, где Дополнительный байт обычно хранит символ NULL. Значение size, таким 0 Разом, является фактическим количеством байт памяти, используемым для отроки. Если какая-либо операция изменяет размер строки, старая память вобождается и динамически выделяется новая память для сохранения измененной строки.
°ле s^r в классе String является адресом строки C++. Добавление поля Перем* Коиечно» Доступ к богатому источнику функций памяти отличают «Иную String (объект) от строковой переменной C++ (C++String).
ст^КтоРы и деструктор. Конструктор создает объект типа String, при-05 п в Качестве параметра строку C++. Во время процесса инициализации ввивает размер, выделяет динамическую память и копирует строку еТся Мттт3?аваемЬ1й[ Динамически данное-член str. По умолчанию присваива-"^-строка. Конструктор копирования следует той же процедуре, но тор у стРоку из начального объекта String, а не из строки C++. Деструк-it яет символьный массив, который содержит эту строку.
выделяет память и копирует в строку C++
*rting(char *S)
--------------—------------ ;
{
// Лиина включает NULL символ
size = strlen(s) +1;
str = new char (size];
// программа завершается, если память исчерпана.
if (str — NULL)
Error(outofMemory);
strcpy(str,s); )
Перегруженные операторы присваивания. Оператор присваивания позв0. ляет присваивать либо объект String, либо строку C++ объекту String. Напри мер:
String S(”I am a String variable”), T;
// присваивает объект String объекту String Т = S;
// присваивает строку C++ объекту String
Т= "I ат а C++ String";
!Г
.1
I
Г I
Для того, чтобы присвоить новый String-объект s текущему объекту, сравнивается длина двух строк. Если они различные, оператор удаляет динамическую память текущего объекта и снова (оператором new) выделяет s.size символов. Затем s.str копируется в новую память.
// оператор присваивания: String в String
Strings String::operator=(const Strings s) (
// если размеры различные, удаление текущей
// строки и выделение нового массива
if (s.size ! = size)
(
delete () str;
str  new char (s.size];
if(Str =- NULL)
Error(outofMemory);
// назначение размера, равного размеру s size » s.size;
)
// копируется s.str и возвращается ссылка на текущий объект
strcpy(str,s.str);
return *this;
Операторы сравнения. Класс String предоставляет Полный набор операторов сравнения строк в соответствии с кодом ASCII. Эти операторы сравнивают два объекта String или объект String с C++String. Например:
String SC'Cat”), T(”Dog");
//сравнение стюок
if (S == Т) . .	// условие FALSE
if (Т<"Tiger") ...	// условие TRUE
if f"Aardvark">= T) . . .	// условие FALSE
Реляционный оператор == проверяет равенство строки C++ объекта г®1® String. Заметим, что версия ==, которая позволяет строковой переменной С появляться в качестве левого операнда, должна быть перегруженной как ДРу жественная функция:
// C++String == String, дружественная функция
// так как C++String находится в левой части friend int operator== (char *str, const Strings s) (
return strcmp(str, s.str) == 0; I
fI конкатенация String и C++String // S теперь — это "Cool Water"
gxrjng-операторы. Этот класс имеет набор функций, используемых для кон-яадии строк. Конкатенация выполняется перегрузкой операторов + и +=. случае возвращается новая строка. Во втором — происходит добав-0 к текущей строке. Например, строка "Cool Water” создается с использо-Ле0Й*м трех версий оператора конкатенации:
а S("Cool"b Т("Water"), U, V;
Str*«9+
т.	// конкатенация двух String
»-5. Hater.-	fnuvjn.ouafiwa Shrine и C++Strino
V * s * $ ♦“ T*
Следующий код реализует версию String + String оператора конкатена-функция возвращает объект String, являющийся конкатенацией текуще-Я^объекта String и String справа от +. В этом алгоритме мы сначала создаем mrjng-объвкт temp, содержащий size+s.size-1 символов, включая NULL-сим-ь Заметим, что когда объявляется temp, мы сначала удаляем NULL-строку, Изданную конструктором, а затем выделяет память (размером size+s.size-1). Метод копирует символы из текущего объекта в новую строку и конкатенирует символы из s. Строка temp представляет собой возвращаемое значение.
// конкатенация: String + String
string String:: operator* (const Strings s) const
* // создание новой строки temp с длиной len
string temp;
int len;
// удаление NULL string, созданной при объявлении temp delete [] temp.str;
// вычисление длины результирующей строки
// и выделение памяти в temp
len = size + s.size -1;	// только один NULL-символ
temp.str - new char (len);
if (temp.str — NULL)
Error (outofMentor yO;
// установка размера результирующей строки
//и создание строки
terap. size «= len;
strepy(temp.str,str);	Н копирование str в temp
strcat(temp.str, s.str);	//конкатенация
return temp;	// возвратить temp
String-функции. Метод Substr возвращает подстроку текущей строки, которая начинается с позиции index и имеет длину count:
String substr (int index, int count);
H J^^onePaT0P ши₽око используется в алгоритмах сопоставления с образцом.
s<”Cool Water”), U;
ubstr(i,2); // извлекает 'oo* из Cool
p>
в°3аПа ИвДеКс выходит за позицию последнего строкового символа, функция Чает ^ULL-етроку. Количество символов в строке от элемента index до ауетсяТ^141 Равно size-index-1. Если count превышает это значение, исполь-Вост СТР°КН как подстрока, а параметру count присваивается значение СьМволлХ'1* ^ля Реализации этого метода выделяется память для count+1 String J1 в °бъекте temp. В эту память копируется count символов объекта Чл®Ду1е15ЧИ11ая с позиции index, н нулевой завершающий символ. Данному-аВаПенх»</£’s*ze присваивается значение count+1, temp возвращается в качестве 4 Функции.
// возвращает подстроку с позиции index и // длиной count
String String::Substr(int index, int count) const {
// число символов от index до конца строки int charsLeft = size-index-l,i;
// создать подстроку в temp
String temp;
char *p, *q;
// возвратить NULL-строку, если index слишком велик if (index >= size-1)
return temp;
// если count > charsLeft, возвращать оставшиеся символы if (count > charsLeft)
count = charsLeft;
// удалить NULL-строку, созданную при объявлении temp delete (1 temp.str;
11 выделить память для подстроки	1
temp.str = new char (count+1);
if (temp.str “= NULL) Error(outOfMemory);
// копировать count символов из str в temp.str
for(i-0, p=temp.str,q-Gstr[index];i < count;i++)
*p++ = *q++;
// последний NULL-символ
*p = 0;
temp.size - count+1;
return temp; >
Substr(3,3)
Текущий объект
char*str;
int size; (“7)
Функции-члены Find и FindLast выполняют поиск вхождения какого-яИ символа в строке. Обе возвращают -1, если этот символ не находятся строке. Метод
nd(char C, int stsrt) const;
ает с начальной позиции и выполняет поиск первого вхождения символа символ найден, Find возвращает его позицию в строке. Метод
Сс ЕсДИ. V—------	~	'
Findbast(char С) const;
лЯяет поиск последнего вхождения символа С. Если этот символ найден, dLast возвращает его позицию в строке.
„претить индекс последнего вхождения С в строке
//t®gtring: :FindLast(char С) const
* int ret;
char *p«
./ использование библиотечной функции C++ strrchr.
7 возвращает указатель на последнее вхождение символа С в строке
р « strrchr(str,С);
if (р != NULL)
ret = int(p-str); // вычисление индекса
else
ret » -1; // возвратить -1 при неудаче
return ret;
)
Строковый ввод/вывод. Строковые потоковые операторы » и « реализуются использованием операций потокового ввода и вывода для строк C++. Оператор » читает разделяемые пробелами слова текста.
Метод ReadString читает строку текста (до 255 символов или до указанного ограничительного символа) из текстового файла и включает ее в объект String. Если не передается никакого файлового параметра, по умолчанию символы принимаются из стандартного потока cin. Ввод завершается на ограничителе, который не сохраняется в строке. В качестве ограничителя по умолчанию используется символ новой строки С\п’)- Например,
String S, Т;
Cin» S;	// пропускает пробелы; читает следующую лексему
Т.Readstring (); // читает до конца строки
COut «"Компоненты:" « S « ” и ” « Т « endl;
<Ввод>
Super! Grade А // пробел после ! является частью Т
<вывод>
Компоненты: Super! и Grade А
Метод использует функцию getline для считывания до ограничителя или ₽+г ° СИМВОЛов из входного потока istr в символьный массив trap. Если в line указывает на конец файла, возвращается -1; иначе удаляется суще-динамический массив str и выделяется другой — размером SD^~s^r^€I1(trnp)+l. Массив tmp копируется в новый массив, и функция воз-адЧает количество считанных символов (size-1).
inf4g?a?b строку текста из потока istr
{	;Readstring (istreamS istr, char delimiter)
гь ЧИТать стР<Эку в tmp
Паг tmp[256j;
if He конец Файла, читать строку до 255 симвлов
( istr-getline(tmp, 256, delimiter))
//удалить текущую строку и выделить массив для новой °elete () str;
size = strlen(trap) + 1; str = new char [size];
if (Str “= NULL)
Error(outOfMemory);
// копировать trap. возвращать число считанных символов strcpy(str,trap);
return size-1;
}
else
return -1;	// возвратить -1, если конец файла
8.6. Сопоставление с образцом
Общая проблема сопоставления с образцом включает поиск одного или более вхождений строки в текстовом файле. Большинство текстовых редак торов имеют меню Search, содержащее несколько элементов поиска подстроки, таких как Find, Replace и Replace All.
Процесс Find начинает с текущего местоположения в файле и выполняв!' поиск следующего вхождения подстроки по направлению вперед или назад-Replace заменяет подстроку, совпавшую при выполнении процесса Find, ДРУ' гой подстрокой. Replace All проходит по файлу и заменяет все вхождения подстроки-образца на подстроку-замену.
Процесс Find
Рассмотрим процесс Find для простой ситуации. Даны строковые пер® менные S и Р, начинаем в заданной позиции в S и ищем подстроку Р-она существует, возвращаем индекс в строке S первого символа подстр°к Р. Если Р не существует в S, возвращаем -1. Например:
1. Дана строка S="aaacabc" и P="abc", подстрока Р расположена Б
начиная с позиции 4.	„
 2. Если S="Blue Bar ranch lies outside the city of the animals” и P5®	’
то P появляется в S дважды в позициях с индексами 28 и 40.
3. Подстрока Р—"аса" не присутствует в строке S="acbaccacbcbcac .
т1ооГрамма 8.5 иллюстрирует алгоритм сопоставления с образцом, исполь-дяй класс String.
ЗУ
Алгоритм сопоставления с образцом
0ТОТ алгоритм реализуется функцией FindPat, которая начинает с позиции rtindex строки S и выполняет поиск первого вхождения подстроки Р. Мы явоДиМ сначала код» так как наш анализ алгоритма ссылается на пере-^нные этой функции.
T^findPat (String S, String P, int startindex)
1 И первый и последний символы образца и его длина char patstartchar, patEndChar;
int patLength;
// индекс последнего символа образца int patlnc;
// начинать с searchindex искать совпадение с первым // символом образна, переменной matchstart присвоить // индекс совпавшего символа строки S. проверить, // не совпадает ли символ строки S для индекса matchEnd //с последним символом образца int searchindex, matchstart, matchEnd;
// индекс последнего символа // этого значения
int LastStrindex;
в S. matchEnd должен быть <-
String insidePattern;
patStartChar - P(OJ; patLength - P. Length (); patinc - patLength-1; patEndChar - ₽[patinc];
// первый символ образца
// длина образца
// индекс последнего символа образца
// последний символ образца
2,
— -....-	> 2, получить все символы образца,
// кроме первого и последнего
И
// если длина образца >
it (patLength > 2) ihsidePattern - Р.Substr(l,patLength-2);
laststrlndex - S.Length()-1; // индекс последнего символа в s на^ать поиск отсюда до совладения первых символов
searchlndex - startindex;
wa1(.MfKaTb СО8падекие с первым символом образца chStart - S.Find(patStartChar,searchindex);
•®atohpeKC Г1Оследнегс' символа возможного совпадения chEnd - matchstart + patinc;
// Пу®Торно искать совпадение первого символа и проверять, последний символ не выходил за строку
( wnatchstart •= -1 ss matchEnd <= laststrindex)
это первое или последнее совпадение?
( IS[matchEndJ--patEndChar)
/ если совпадают один или два символа, имеем совпадение
^M2S
if (patLength <= 2) return matchstart;
// сравнить все символы, кроме первого и последнего
if (S.Substr(matchStart+l,patLength-2) == insidePattern) return matchStart;"
}
// образец не найден, продолжать поиск со следующего символа searchindex = matchStart+1;
matchStart - S.Find(patStartChar,searchindex};
matchEnd » matchStart+patlnc;
}
return -1;	// совпадение не найдено
Следующие шаги описывают этот алгоритм в общих чертах. Делаются ссылки на пример строки:
S = badcabcabdabc
и образец
р = а ь с
1. Образец — это блок текста с начальным символом patStartChar == Р[0], длиной patLength = P.Length(), приращением patinc = patLength-1, которое дает индекс последнего символа в образце, и конечным символом patendChar = P[patlnc]. Как мы увидим в шагах 3 и 4, алгоритм сравнивает первый и последний символы текстового блока в S длиной patLength с первым и последним символами в Р. Если длина Р (patLength) превышает 2, извлекаем подстроку символов, которая не включает первый и последний символы (P.Substr(l, patLength-2)) и присваиваем эту строку переменной insidePattern.
Р = а Ь с
parStartChar = 'a' patinc = 2 patEndChar = V
patLength = 3 insidePattern = "b"
2.
Чтобы отметить конец строки S, присваиваем переменной lastStrlndex индекс последнего символа (S.Length()-l). Начальный индекс (startindex) может быть нулевым (поиск от начала строки) или некоторый положительным значением, если вам необходимо начинать поиск внуТ' ри строки. Инициализируем переменную searchindex значением startin. dex. Эта переменная служит как точка запуска для сопоставления
3.
первым символом образца.
Начиная с searchindex, используем метод Find класса String полнения поиска символа в строке, совпадающего с patStartChar. ПР сваиваем индекс совпадения переменной matchStart. ПереМеН ' matchEnd (matchStart+patlnc) — это индекс последнего символа сТР S, который может совпадать с patEndChar. Поиск завершается неуДаЧ‘ если Find не находит совпадения с patStartChar или matchEnd пр
шает lastStrlndex.
Сравниваем PatEndChar с последним символом текстового блока в S (STmatchEnd] == patEndChar). Если эти символы не совпадают, мы должны перейти к шагу 5 и выполнить еще ряд сравнений. Сравнение последних символов является оптимизирующей функцией, освобождающей нас от Еенужного тестирования для образцов, которые не могут совпадать. Если длина образца равна 1 или 2 (patLength<= 2), мы имеем совпадение и возвращаем индекс matchStart. Иначе сравниваем символы в текстовом блоке, исключая первый и последний (S.Substr(matchStart+l, patLength-2)) со строкой insidePattern. Если они совпадают, возвращаем matchStart. В примере matchStart=l н matchEnd = 3. Строковый блок и образец совпадают на концах. Строки insidePattern ="Ъ" и S.Substr(2,l) = ’d” не совпадают.
S = badcabca bda be a b c*\^
matchStart = 1	matchEnd = 3
5. Повторяем шаги 3 и 4, но на этот раз начинаем в позиции с индексом searchlndex= matchStart+1. В нашем примере следующее совпадение с первым символом Р имеется в позиции с индексом 4.
S = b a d с a bcabda be
matchStart = 4
matchEnd = б
Последний символ Р и последний символ текстового блока совпадают, и совпадают также S.Substr(5,l) и insidePattern. Возвращаем индекс 4 (matchStart).
6. Для нахождения множества вхождений образца вновь вызываем функцию с начальным индексом, большим на единицу, чем индекс, возвращаемый функцией FindPat. Например, продолжение поиска "abc" дает следующие результаты:
Поиск возможного совпадения в позиции с индексом 7 неудачен.
Startindex = 5
S = b a d с a b с a bd
abc
matchStart = 7	matchEnd = 9
Поиск возможного совпадения в позиции с индексом 10 является удач-И- Возвращается значение 10.
Sab a d с a bcabda
b с
matchStart = 10
matchEnd = 12
Программа 8.5. Поиск подстрок
Функция FindPat реализует только что описанный алгоритм сопост^в ления с образцом и находится в файле findpat.h. ,
Эта программа читает образец строкового объекта pattern и затем чинает чтение строки linestr до тех пор, пока не будет достигнут коне файла. Функция FindPat вызывается для нахождения количества вхо%, дений образца в каждой строке, а затем выводит количество вхожден^ вместе с номером строки.
♦include <iostream.h>
♦pragma hdrstop
♦include ”st rclass.h"
♦include "findpat.h"
void main О {
// определить строку-образец и строку для поиска String pattern, lineStr;
// параметры поиска
int lineno =0, lenLineStr, startSearch, patIndex;
// число совладений в текущей строке int numberOfMatch.es;
cout « "Введите подстроку для поиска: **;
pattern.Readstring();
cout « "Введите строку или EOF:” « endl; while(lineStr.ReadString() 1= -1) (
lineno++;
lenLineStr = lineStr.LengthO;
startsearch =0;
numberofMatches » 0;
// поиск до конца строки
while(startsearch <= lenLineStr-1 &&
(patindex = FindPat{lineStr,pattern,startsearch)) != -1) {
numberofMatches++;
Il продолжать поиск до следующего вхождения startsearch » patlndex+1;
)
cout « "Число совпадений: •* « numberOfMatches « "в строке: « lineno « endl;
cout « "Введите строку или EOF:" « endl; }
}
/♦
<Выполнение программы 8.5>
Введите подстроку для поиска:: iss
Введите строку или EOF:
Alfred the snake hissed because he missed his Missy.
Число совпадений; 3 в строке: 1
Введите строку или EOF:
riississippi
«СЛО совпадений: 2 в строке; ««едите строку или EOF:
В blissfully walked down the совпадений: 1 в строке: введите строку или EOF: Tt is so.
™сло совпадений: О в строке: Введите строку или EOF:
2
sunny lane.
3
4
Анализ алгоритма сопоставления с образцом
Предположим, что образец имеет m символов, а строка имеет п символов. Гели первые m символов в S совпадают с Р, мы находим совпадение после ш сравнений. Оценкой наилучшего случая для алгоритма является О(т). Для определения оценки наихудшего случая предположим, что мы не используем оптимизирующую функцию, в которой сравниваются последние символы. Более того, допустим, что у нас всегда совпадают первые символы, но никогда — образец. Например, это верно, если
р - ”abc" и S - "аааааааа" (т - 3, п = 8)
В этом примере m = 3 символа образца ”abc” должны сравниваться с текстовыми блоками в S в сумме п - ш+1=6 раз. В общем случае мы должны сравнивать m символов по n-m+1 раз, т.е. всего выполнить m(n—т+1) сравнений.
Так как
< пцп-т+т) - mn,
°Ц*нкой наихудшего случая для алгоритма будет О( тп).
и ов°ПОставление с образцом — это очень важная тема в компьютерной науке, обра 1ЕЦ1Р0К0 изучается в литературе. Например, алгоритм сопоставления с OaJ?0?1 Кнута-Морриса-Прата (Knuth, 1977) имеет вычислительное время т-е* является более эффективным, чем только что представленный Тои алгоритм.
8.7.	Целочисленные множества
— это группа объектов, выбранная из коллекции, называемой сок, р^альным множеством (universal set). Множество записывается как спи-Деляемый запятыми и заключенный в фигурные скобки.
X = {ij i2, i3, . .	, U
□	Объединение множеств ( u ) Xu Y — это множество, содержащее вс^ элементы в X и все элементы в Y без дублирования.
□	Пересечение множеств ( n ) X n Y — это множество, содержащее все элементы, которые находятся в обоих множествах X и Y
X = (0, 3. 20. 55}. Y = {4, 20. 45, 55}
X U Y = {0. 3, 4, 20, 45. 55}	X П Y = {20, 55}
□	Вхождение во множество (с) П€=Х равно TRUE, если элемент п является элементом множества X; иначе оно равно FALSE.
Х{0, 3, 20, 55} //20 е X равно TRUE, 35 G X равно FALSE
Множества целочисленных типов
Целочисленный тип (integer type) — это любой тип, элементы которого представлены целыми значениями. Типы char, int всех размеров и перечисления являются целочисленными типами. Например, набор символов кода ASCII соответствует 8-битовым целым в дипазоне от 0 до 127. Тогда как приложения используют традиционное представление символов ’А’, ’В’, ...» для их внутреннего хранения применяются целые коды 65, 66 и так далее. Программист имеет возможность выбирать любое представление.
char chi = 'A', ch2 = 97, ch3;
ch3 = chi +4;	// ch3 “ 'E'
cout « ch2 « " ’* « int('A'),-	// печать: a 65
В этом разделе разрабатываются множества с элементами целочисленного типа. Универсальное множество имеет соответствие "один-к-одному с беззнаковыми целыми в диапазоне от О до setrange-1, где setrange это количество элементов множества. Рассмотрим следующие множества*
Множество цифр = {0,1,2,3,4,5,6,7,8,9} соответствует диапазону 0 ... 9
Множество символов кода ASCH—{. . . , ’А’, ’В’, - . . , ’Z*, . .  ) соответствует диапазону 0 . . . 127
enum Color (красный, белый, синий)
множество Color соответствует диапазону 0 ... 2
---------— —
Rftj можем реализовать тип Set, с помощью массива значений нулей и (О и 1). В этом массиве значение в позиции i равно 1 (TRUE), если eJe‘BT i находится в данном множестве, или — О (FALSE), если он отсутствует *'	В письменном упражнении 6.13 описывается метод для реализации
в ® еСТва целых значенией с использованием статического массива. Этот под-спользует одно целое значение для каждого возможного элемента множе-| ИкГы можем выделять память для этого массива целых динамически.
I сТВ^’ 1
^явление множества с элементами в диапазоне от 0 до secrange-1
'	1 nrivate:
int *member:	// указатель на массив set
int setrange;
public:
	// конструктор для распределения массива set
,	Set(int	n):setrange (n)
I.	(
I,	member = new int (setrange];
I	’ ’  ’
I;
SetS(20);	//множество (0, .... 19}
n£S -> S.memberFn] == 1	// элемент равен 1, если n принадлежит Set
[	Требуемая память значительно уменьшается, если мы поддерживаем массив
с использованием побитовых операторов C++. Более того, мы можем обобщить этот подход до обработки любого целого типа, применяя шаблоны.
Побитовые операторы C++
Операторы OR (||), AND (&&) и NOT (!) используются в логических выражениях для объединения целочисленных операндов и возвращение результата TRUE (1) или FALSE (0). Одни и те же операнды могут вычисляться с эквивалентными арифметическими операторами, которые применяются к их отдель-ЕЬ1М битам. Битовые операторы OR(|), AND (&), NOT (~) и EOR О определены радельных битов и возвращают значения 0 и 1. Эти операторы приведены таблице 8.1. Из них только EOR может оказаться для вас новым. Он возвра-, чает 1, только если оба бита не равны.
Таблица 8.1
Еиговые операции
					
X г 		 Г———	У 0	-X 1	х|у 0	х&у 0	ХАУ 0
Т~ —	 г-—			 о	1 0	1 1	о о	1 1
	1	0	1	1	0
₽ЯЦПЙ ОСеРации применяются к n-битовым значениям выполнением опе-а „ Ка^дым битом. Допустим,
^'1^-2 -
ь	  а2а1а0 Ь = bn-jbn-2  -  Ь2Ь1Ь0
езУльтат г _
с — a op b задается следующим образом:

C Cn-l®n-2 - • • с2с1с0 ~ ^rt-l^n-2 • - • a2ala0 °Р bn_jbn_j где
ci “ а1 °Р Ьж, О < i < П-1 и op -= ’ | ’, ’
Унарный оператор инвертирует биты операнда.
Ь2Ь>Ьо
Пример 8.4
8-битовые числа х =11100011 и у = 01110110 используются с оце рациями а) х OR у, b) х AND у» с) х EOR у и d) ~х.
а) х	11100011	Ь) х	11100011	с) х	11100011
OR у	01110110 AND у	01110110 EOR у	01110110
11110111	01100010	10010101
d) ~х = 00011100
В C++ имеются также операторы сдвига, которые сдвигают биты операнда влево («) или вправо (»). Выражение а « п умножает а на 2п, выражение а » п делит а на 2П. Обычно, использование битового one-ратора ускоряет любое вычисление, включающее умножение или деление целого значения на степень двойки.
Битовые операторы обычно используются с беззнаковыми целыми операндами. Мы будем их использовать только для этого.
Пример 8.5
Предположим, что переменные х, у и z определяются следующим образом:
// каждая переменная - 16 битовая
unsigned short х « 10, у - 13, г:
Пункты а — d иллюстрируют использование битового оператора.
а.	Z = х | у;	// z -= 15
ь.	X = X & у;	// г = 8
с.	z  -0 « 2;	// z - 65532
d.	z = ~х £ (у » 2);	// г = 1
Спецификация класса Set
В нашем классе Set-объект состоит из списка элементов, взятых в Д0й' пазоне O-.setrange l целых чисел. Целочисленный тип определяется именем шаблонного типа Т. Мы полагаем, что для типа Т определен преобразователь int и что целое значение может быть преобразоваино явно в тип Т. Например’ пусть val будет элементом типа Т, а I — целой переменной:
т val; int I;
Если I — это целый эквивалент элемента данных val, то
I ж int(T), a val = т(1)
Пример 8.6
a.	Char — это целый тип.
char с = ' А'; int i;
i я xnt(c);
с = char(i);
// i = 65 //с - 'A'
b.	Перечислимый тип — это целый тип.
enum Days {Sun, Hon, Tues, Wed, Thurs, Fri, Sat};
Days day - Thurs;
int d;
d » int{day); //d - 4
day - Days {d) ;	// day = Thurs
Представление элементов множества
При определении битовых операторов C++ для эффективной реализации объекта Set используются отдельные биты в слове. Диапазон значений множества (O..setrange-1) хранится в динамическом массиве из 16-битовых беззнаковых целых. Массив с именем member связывает целые числа в диапазоне O..setsize-1 как цепочку битов. Каждый бит представляет один элемент множества, и элемент находится в этом множестве, если соответствующий бит равен 1. Нулевой элемент множества представлен крайним правым битом первого элемента массива, а 15 представляется крайним левым битом первого элемента массива. Далее продолжаем крайним правым битом второго элемента массива, представляющим 16, и так далее. Схема хранения в памяти показана на следующем рисунке.
15 Ц 13 12 И 10 9 876543210
|l| о|о| 0 j 0 I О I 1 I О I О I о I l|o|o|o|o|l|
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16
| 0 | О | 0 | 1 | О [ б | 0 | 0 | 0 | 0 | о |о | 0 | О | 1 | 0 |	
member[0]
member[1J
В этом случае целые значения О, 5, 9, 15, 17 и 28 принадлежат этому множеству.
ОБЪЯВЛЕНИЕ
♦include <iostream.h>
’includtj <stdiib.h>
enum ErrorType
^v«XidMeinber, ExpectRightBrace, MissingValue,
InvalidChar, MissingLeftBrace,
In “^"PUtData, EndOfFile, OutOfMemory, j. « idMemberRef, SetsDifferentSize
^saSet<ClaSS T>
₽rivate:
fВ4аксимальное число элементов множества setrange;
<^.Число Сайтов Ситового массива и указатель на массив
int arraysize;
unsigned short ’member;
// обработка ошибок
void Error(ErrorType n) const.-
// реализация распределения элементов множества //по битам внутри 16-битовых целых int Arrayindex(const Т& elt) const;
unsigned short BitMask(const T& elt) const;
public:
// конструктор, создает пустое сножество
Set(int setrange);
// конструктор копирования
Set(const set<T>& x);
// деструктор
-Set(void);
// оператор присваивания
Set<T>& operator^ (const Set<T>s rhs);
// вхождение в текущее множество int IsMember(const T& elt);
// эквивалентность
int operator^- (const Set<T>& x) const;
// объединение
Set operator* (const Set<T>6 x) const; // пересечение
Set operator* (const Set<T>G x) const;
// вставка/удаление
void Insert(const TS elt);
void Delete(const Tfc elt);
. // ввод/вывод
friend istream& operator» (istream& istr, Set<T>s x);
friend ostreami operator« (ostreamfc ostr, const Set<T>& X);
h
ОПИСАНИЕ
Шаблонный класс Set реализует множество целочисленных значений. Тип Т может быть любым типом, для которого применимы операции i = int(v) И v = T(i), где v и i задаются объявлениями
Т v; int i;
Конструктор создает пустое множество Set, а арифметические операт°Р используются для определения операций над множествами: объединение пересечение (*) и равенство (==). Методы Insert и Delete, так же как и операТ присваивания, используются для обновления Set.
Операции ввода/вывода вводят и печатают множества, заключаемые в 9 гурные скобки и разделяемые запятыми.
ПРИМЕР:
// набор целых чисел в диапазоне 0..24
S(25); -r.op символов S“ - - (I	»	9 1 1 Delete(' У >• т « endl; cout	в коде ASCII Щ128) ; // ввести (4, 7, 12} // вывод {4, 5, 7, 12} // ввести {а, е, i, о, и, у} // U = {а, е, х, о, и, у} // вывод {а, е, i, о, и}
(T*U т)
11 cout « Т « подмассив" « О « endl;
КоД» реализующий класс Set, находится в файле set.h.
Класс Set предоставляет клиенту возможность создания множества объектов пгеделяемых пользователем перечислимых типов и стандартных целочислен-°мх типов, таких как int и char. Если требуется ввод/вывод множества для перечислимых типов, потоковые операторы должны быть перегружены.
Пример 8.7
Рассмотрим следующий перечислимый тип
enum Days {Sun,Mon,Tues,Wen,Thurs,Fri,Sat};
£•?»» В программном приложении содержится перегруженный оператор « для этого типа. Он находится вместе с main-функцией в файле ? enumset.cpp.
fej, Следующие объявления и операторы иллюстрируют использование этих инструментов.
? // объявить 4 объекта, которые представляют различные
#’*» И множества дней
* t Set<Days> weekdays(7), weekend(7), week(7);
’	// массивы wd и we определяют списки дней в неделе.
// эти списки инициализируют множества объектов weekdays и weekend Days wd[] = Шоп,Tues,Wen,Thurs,Fri}, we(] = {Sat,Sun};
/1 вставить элементы массива а множества
for(int i=0; i<5; i++)
fl	weekdays.Insert (wd[i}) ;
fortint i=0; i<5; i++)
У	weekend. Insert (we[i]);
-.	// печатать множества
'/ Cout « weekdays << endl;
•?y COut << weekend « endl;
r /У Формировать и печать объединение двух массивов
-* - _eeic *° weekdays + weekend;
Out « week « endl;
К' * <®Ь1ПОлнение программы>
> !	^ues, Wen, Thurs, Fri}
<s“' Monl
,• Mon, Tues, Wen, Thurs, Fri, Sat}
Решето Эратосфена
Греческий математик и философ Эратосфен жил в 3 в. до н.э. Он откгл, увлекательный метод использования множеств для нахождения всех просты^ чисел, меньших, чем или равных целому значению п. Этот алгоритм иачив * ется инициализацией множества, содержащего все элементы в диапазоне 2. п Путем повторяемых проходов по элементам во множестве мы ”просеивае * элементы сквозь решето". В конечном счете, остаются только простые числа Решето начинает действие со своего наименьшего числа m = 2, которое служит ключевым значением. Мы сканируем множество и удаляем все большие & кратные ключу 2*т, 3*т, .... к*т, которые остаются в этом множестве. Эщ кратные не могут быть простыми числами, так как они делятся на ш. Следую. щее число в решете — это ключ ш=3, являющийся простым числом. Как в случае с начальным значением 2, мы удаляем все большие и кратные 3-м начиная с 6. Так как 6, 12, 18 и так далее уже были удалены как кратные 2 этот проход удаляет 9, 15, 21... Продолжая процесс, переходим к следующему большему числу множества, являющемуся простым числом 5. Помните, что число 4 было удалено как кратное 2-м. В случае с 5 мы проходим по элементам и удаляем всякое большее кратное 5 (25, 35, 55 ...). Процесс продолжается до тех пор, пока пока мы не просканируем все множество и не удалим кратные для каждого ключевого значения. Числа, которые остаются в решете, являются простыми в диапазоне 2..п.
Пример 8.8
На этом рисунке показано решето Эратосфена выполняющее поиск всех простых чисел в диапазоне 2 . . 25.
Решето Эратосфена: п-25
Xы«21 |2|3NsM7M9Mn№M’sN”№M;’№№]
Is! И N.M l»|. [XI pl 14 N |а~П”1
|z|3| I'sl |?| I I I 111 |в| I I |17| |l9| I I |23~CB
7,11.13,17,19. и 23 не содержат кратных в этом множестве
Простые числа {2,3,5,7,11,13,19.23}
Решето работает, пока не удалит все числа, не являющиеся простыми-Для того, чтобы проверить это, предположим, что составное (не простое) число m остается в решете. Такое число может быть записано как
m - p*k, р>1
где р является простым числом в диапазоне от 2 дот-1. В алгорит^ решета р было бы ключевым значением, и ш было бы удалено, так 14
оно является кратным р.
Дограм ма 8.6. Решето Эратосфена
функция Printprimes реализует решето. Алгоритм использует функцию цтимизации, проверяя ключевые значения в диапазоне 2 < m < V/Г. Это °гпаниченное число ключевых значений удаляет все непростые числа из божества. Для проверки этого факта предположим, что некоторое составное число t = p*q остается. Если бы оба сомножителя (р и q) были j больше, чем п, то
t “ p*q >	= п
И t не находилось бы в этом множестве. Таким образом, один сомножитель р должен быть < Этот меньший сомножитель был бы ключевым значением или кратным ключевому значению и, следовательно, t было бы удалено как кратное ключу. Вместо вычисления корня квадратного от п мы проверяем все числа т, которые m*m < п.
I	_------------------------------------------------------------------------------
I	#include	<iostream.h>
।	^include	<iomanip.h>
j	Ipragma hdrstop
#include "set.h" // использовать класс Set
// вычислять и печатать все простые <= п, используя
И алгоритм Решето Эратосфена
j	void PrintPrimes(int n)
J	<
11 множество содержит числа в диапазоне 2..п
j	Set<int> S(n+1);
j	int in, k, count;
j	// вставить все значения из 2..п в это множество
for(m<e2;m <= п;ш++)
S.Insert(m);
И проверять все числа от 2 до sqrt(n)
for (m=2;m*m <= n;iu++)
;	// если m в S, удалить все кратные m из множества
if(S.IsMember(m))
I	for(k=m+m;k <= n;k += m)
|,	if (S.IsMember(k))
.( .	S.Delete(k);
J все оставшиеся в S числа — простые.
печатать простые числа по 10 в строке
Count «1;
f°r(m-2;m <= n;m++)
(S. IsMember (m))
Cout « setw(3) « m « " ";
if (count++ % 10 == 0)
I	cout « endl;
. JL
I Cout«endl;
<°ld ^infvoid) ir* n;
I
cout « ’’Введите n: cin » n;
cout « endl;
PrintPrimes(n);
I*
«Выполнение программы 8.6>
Введите n: 100
2 3 5 7 11 13 17 19 23 29
31 37 41 43 47 53 59 61 67 71
73 79 83 89 97
Реализация класса Set
Закрытые методы Arrayindex и BitMask реализуют схему хранения в памяти массива целых. Arrayindex определяет элемент массива, которому принадлежит параметр elt простым делением на 16 с использованием эффективного сдвига иа 4 бита вправо:
template «class Т>
int Set«T>::Arrayindex(const Ts elt) const (
// преобразовать elt к типу int и сдвинуть return int(elt) » 4;
)
Если обнаруживается правильный индекс массива, BitMask возвращает беззнаковое короткое значение, содержащее 1 в битовой позиции, задаваемой значением elt. Эта маска (mask) может использоваться для задания или очистки бита.
// создать беззнаковое короткое целое с 1 в битовой elt-позиции template «class Т>
unsigned short Set«T>::BitMask(const T& elt) const (
// использовать & для нахождения остатка от деления на J.6.
//0 попадает в крайний правый бит, 15 — крайний левый
. return 1 « (int(elt) ь 15); )
Обработка ошибок. Класс реагирует на группу ошибок, вызывая закрь1' тую функцию-член Error. ErrorType типа enum используется для удобного задания наименований возможных ошибок. Функции передается параметр ErrorType, который используется в операторе выбора для определения онп* ки и завершения программы. Реализация метода Error приведена в СР1'' граммиом приложении.
Конструкторы класса Set. Класс имеет два конструктора, создаю^ объект Set: один — создающий пустое множество, другой — констрУ1^ v копирования. Пустое множество создается определением количества беззН® ковых коротких элементов массива arraysize, необходимых для представ ния диапазона значений данных, выделением массива и заполнением значениями 0.
нСтруктор. создает пустое множество
// <class т>
tew* . ;Set (int sz) : setrange(sz) $et4* (
it4T>
. дасло беззнаковых коротких целых для задания множества arraysize - (setrange+15) » 4;
// выделить массив
/eid>er « new unsigned short [arraysize];
i# (member » NULL)
Error(OutOfMemory);
// создать пустое множество, заполняя его нулями for (int i - 0; i < arraysize; i++)
member[i] “ 0;
)
Set-операторы. Бинарные операции объединения и пересечения реализуются перегрУзкой операторов + и * языка C++. Для оператора объединения (+) создается объект множества tmp (содержащий элементы в диапазоне 0. .setrange-1) побитовой операцией OR над элементами массива, представляющими текущее множество, и элементами множества х. Этот новый объект возвращается в качестве значения метода. Заметьте, что мы сообщаем об ошибке, если оба множества (операнды) имеют разные размеры.
// формировать и возвращать объединение
И текущего множества с множеством х
template <class Т>
Set<T> Set<T>:: opera tor + (const Set<T>s x) const (
// множества должны иметь одинаковые размеры
if (setrange != х.setrange)
Error(SetsDifferentsize);
/7 формировать объединение в tmp Set<T> tmp(setrange);
7/ каждый элемент множества tmp — результат побитового OR for (int i « 0; i < arraysize; i++)
tmp.member[i] = member(i] ( x.member[i);
7/ возвратить объединение return tmp;
Подобно объединению операция пересечения (*) создает объект множества Р> являющийся пересечением, использованием побитовой операции AND «Д элементами массива текущего множества и множества х. Возвращается 5* множество как значение метода.
ТРттрТ°Д ^s^ember определяет вхождение в текущее множество и возвращает ’ есди бит, соответствующий elt, равен 1, и FALSE — в противном случае:
int₽SeJ® <class Т>
{	:isMeiriber (const Ть elt)
if_7^Xojls,Tc4 ди int (elt) в диапазоне 0. .setrange-1 ?
'Bt<elt) <0 11 int(elt) >= setrange) rror(InvalidMemberRef);
returnTT?Tb бит, соответствующий elt
n member[Arrayindex(elt)) & BitMask(elt);
Операции вставки и удаления. Операция Insertion реализуется задавиеь! бита, соответствующего параметру elt:	I
template <class Т>	1
void Set<T>::Insert(const T& elt)	I
1	I
// находится ли int(elt) в диапазоне О..setrange-1 ?	1
if (int(elt) <011 int(elt) >=* setrange)	I
Error(InvalidMemberRef);	I
// находится ли int(elt) в диапазоне 0..setrange-1 ?	I
member[Arrayindex(elt)] I= BitMask(elt);	I
)	I
Операцией удаления убирается бит, соответствующий elt. Метод использует 1 оператор AND и маску, содержащую все 1, кроме заданного elt-бита. Маска I создается с использованием операции побитового отрицания (~).	I
// удалить elt из множества	I
template <class Т>	I
void Set<T>::Delete(const T& elt)	I
(	I
// находится ли int(elt) в диапазоне 0..setrange-1 ?	I
if (int(elt) <0|| int(elt) >= setrange)	I
Error(InvalidMemberRef);	I
// очистить Опт, соответствующий elt	1
member[Arrayindex(elt)] 4= -BitMask(elt);	I
1	ll
Ввод/вывод. Потоковые операторы » и « перегружаются для реализации I потокового ввода/вывода для типа Set. Оператор ввода (») читает множество I х в формате {io, й, . - . im}. Элементы множества заключены в фигурные скобки I и разделены запятыми. Каждое целочисленное значение in представляет эле- 1 мент множества. Оператор вывода («) записывает множество х в формате I {io, ii, . . . im}» где io< ii< - . . <im являются элементами этого множества. I
Метод Input является наиболее трудным для реализации. Он пропускает I пробел, используя функцию get для ввода одиночного символа, а затем прове- I ряет, является ли текущий символ символом Если — нет, вызывается I метод error, который выводит сообщение об ошибке и завершает программу. I Когда начальная скобка найдена, метод проходит по разделенному запятыми  списку целочисленных значений и добавляет каждый элемент в текущее мио- I жество. Выполняется проверка того, что запятые правильно размещены и что I элементы множества остаются в диапазоне от 0 до setrange—1. Для разделения 1 элементов в списке может использоваться любое количество пробелов.	I
Письменные упражнения	I
8-1 Объявите массив из 10 целых и указатель	на	int:	I
int а(10], *р;	I
Рассмотрите следующие операторы:	|
for(i»0; i<2; 1++)	I
I p-new int[5];	К
for(j-0;j<5;j++)	1
a[5*i+j] ~ *p++ = i+j;	I|
'	11
укажите выход для операторов: for(i-=0; i<10' i++)
Cout « a(ij « "
COUt « endl;
. Определите переустанавливает ли оператор
р - Р - 10*-
указатель р в начало ранее выделенного динамического массива.
. Предположим, что q — это указатель на первоначальный динамический I®' массив. Производит ли этот код тот же выход, что и в пункте (а)?
for(1-0; КЮ; i++J cout « *(q+i) « " "i cout « endl;
8.2 Для каждого объявления используйте оператор new, чтобы динамически выделять указанную память.
(a)	int* рх;
Создайте целое, на которое указывает рх, имеющее значение 5.
(б)	long *а;
int п;
cin » п;
Создайте динамический массив из длинных целых, на который указывает а.
(в)	struct DemoC
(
int one;
long two;
double three;
I
DemoC *p;
Создайте узел, на который указывает р. Затем задайте поля {1, 500000, 3.14}
(г)	struct DeiuoD
(
int one;
long two;
double three;
* char name(30);
CemoD *p;
Создайте динамически узел, на который указывает р, и задайте поля 13. 35, 1.78, ’ Bob C++”).
операторы, которые освобождают память для каждого пункта
8.з
инструктор класса Dynamiclnt использует новый оператор для дина-йческого выделения целого и присваивания его адреса указателю рп. ^Крытые методы GetVal и SetVal сохраняют и извлекают данные из ^^амической памяти.
class Dynamiclnt	!
{ private;	'
int *pn; public:
// конструктор и конструктор копирования
Dynamiclnt(int n = 0);
Dynamiclnt (const Dynamiclnt* x); If деструктор -Dynamiclnt(void);
// оператор присваивания
Dynamiclnt* operator- (const Dynamiclnt* x); fl методы управления данными int GetVal(void);	// получить целое значение
void Setval(int n);	// установить целое значение
// оператор преобразования operator int(void); tf возвращает целое // потоковый ввод/вывод friend ©stream* operator« (ostream* ostr, const Dynamiclnt* x); friend istream* operator» (istream* istr, Dynamiclnt* x);
};
(а)	Напишите код для реализации методов конструктора и деструктора.
(б)	Напишите методы, которые перегружают оператор = и реализуют конструктор копирования.
(в)	Реализуйте GetVal, оператор int, и SetVal.
(г)	Реализуйте функции потокового ввода/вывода, так чтобы они считывали и записывали значение *рп.
8.4	Используйте объявление Dynamiclnt из письменного упражнения 8.3 для следующих упражнений:
(а)	Dynamiclnt *р;
Задайте объявление для выделения одного объекта с начальным значением 50.
(б)	Dynamiclnt *р;
Выделите массив р с тремя элементами. Каково значение каждого объекта в массиве?
(в)	Dynamiclnt а(10);
- Укажите, как бы вы объявили массив из 10 объектов типа Dynamic!^ и инициализировали каждый элемент значением 100?
(г)	Задайте операторы delete, освобождающие динамическую память, используемую в упражнениях (а) — (с).
8.5	Запишите класс Dynamiclnt из письменного упражнения 8.3 как я13®* лонный класс DynamicType.
template <class Т>
class DynamicType < private:
T *pt; public: // конструктор и конструктор копирования DynamicType(T value);
	DynamicType(const DynamicType<T>G х) ; // деструктор -DynamicType(void); // оператор присваивания DynamicType<t>& operator- (const DynamicType<T>& x) ; II методы управления данными T GetVal(void);	II получить значение void SetvalfT value);	II установить значение // оператор преобразования operator T(void);	// возвратить значение // потоковый ввод/вывод friend ©streams operator« (ostream& ostr, const DynamicType<T>& x); friend istreams operator» (istreams istr, DynamicType<T>S x); };
8-6	В этом упражнении используются классы Dynamiclnt и DynamicType, разработанные в письменных упражнениях 8,3 и 8.5. Объявите объект: DynamicType<Dynamiclnt> D(Dynamiclnt(5)); Каков выход для следующих операторов?
cout	«	D « endl;
cout	«	D.GetVal().GetVal()) «endl;
cout	«	int(D.GetVal()) « endl;
cout	«	Dynamiclnt(D) « endl;
cout	«	int(Dynamiclnt(D)) « endl;
8-7 Рассмотрите класс ReadFile со следующим объявлением:
class ReadFile
(	private: // чтение символов из потока fin для динамического // выделения буферного массива размером buffersize ifstream fin; char ‘buffer; int buffersize; public: // конструктор принимает имя файла и размер буфера ReadFile(char ‘name, int size); // выдача сообщения об ошибке и выход ReadFile(const ReadFile& f); II удаление буфера и закрытие потока fin ~ReadFile(void); void operator- (const ReadFile x); // чтение следующей строки из файла. // возвращает 0, если конец файла int Read(void); // копирование текущей строки в буфер void CopyBuffer(char *buf); // печать текущей строки в поток cout
1;	void Printbuffer(void);
реализуйте этот класс. Запищите функцию
V°ld LineNum(ReadFile& f);
вторая СтРок.
Читает f и распечатывает соответствующий файл с номерами
8.8	Класс DynamicType разработан в письменном упражнении 8.5. Пред положим следующие объявления:
DynamicType<int> *р, Q;
DynamicType<char> *с;
Ответьте на каждый вопрос:
(а)	Напишите оператор, создающий объект класса DynamicType со значе нием 5, на который указывает р.
(б)	Печатайте значение 5, на которое указывает р, используя три разлц^ ных метода.
(в)	Является ли каждый оператор верным? Если да, каково его действие?
с - new DynamicType<char> (65];
с = new DynamicType<char> (65);
(г)	Если вводится число 35, каким будет выход?
С1П » *р;
О = *р;
cout « Q « endl;
(д)	Используя значение Q из упражнения (d), определите, каков выход.
DynamicType<int> R(О);
cout « Q « endl;
(е)	Каков выход?
Q ~ DynamicType<int> (68);
с  DynamicType<char> (char(int (Q)));
cout << c « char(c) « int(c) « endl;
(ж)	Напишите операторы, удаляющие объекты *р и *с. Что произойдет, если вы выполните?
delete Q;
8.9
(а)	Если CL является классом, объясните, почему вы не можете объявить его конструктор копирования следующим образом:
CL(const CL х);
(б)	Объясните, почему вообще не нужно объявлять оператор присваивания для CL следующим образом:
void operator» (const CL& x);
8.10
(a)
Каково значение ключевого слова this? Объясните, почему оно верИ° только внутри функции-члена.
(б) Назовите основное применение ключевого слова this.
8.11 Класс Rational из главы 6 реализует арифметику рациональных чйС^-. Оператор += должен быть добавлен в этот класс. Объясните, п°че следующая реализация является правильной:
pational& Rational::operator*- (const Rationale r) * «this - *this + r;
return *this;
)
8.12
Класс ArrCL реализует границы массива, проверяя использование перегруженного оператора индексации массива [] и преобразование указателя. Он содержит массив из 50 элементов и поле длины. При создании объекта типа ArrCL пользователь может указать максимальную длину
для списка и передавать это значение конструктору как параметр.
const int ARRAYSIZE = 50; template <class T> class ArrCL
<
private:
T arr[ARRAYSIZEJ;
int length;
public:
II конструктор
ArrCL9int n = ARRAYSIZE) ; '
II получение размера списка int ListSize(void) const;
II индексный оператор, реализующий надежный массив
Т& operator!] (int n);
// преобразование указателя. Возвращает адрес агг. operator Т* (void) const;
)
Размер встроенного списка агг ограничивается значением ARRAYSIZE = 50. Если пользователь пытается зарезервировать массив большего размера, конструктор устанавливает размер на ARRAYSIZE и выдает предупреждение.
(а)	Напишите объявления, резервирующие массив А из 20 целых, массив В из 50 элементов типа char и массив С из 25 элементов типа float.
(6)	Имеется ли ошибка в этом цикле?
ArrCL<long> А(30);
for(int i-0; i<= 30; i++)
A[i] = 0;
(в)	Объясните, почему выходом будет 420 420
int Sunil (const ArrCl<int>& A)
int s » 0;
for(int i-0; i<A.ListSize(); i++)
s += A[iJ;
return s;
)
int Sum2(int *A, int n)
int s = 0;
for(int i-0; i<n; i++) s += *A++;
Return a;
arr(20);
°* (int i-0; i<20; i++)
*tr[i) = 2*(i+l);
ut « Sumi (arr) « ” " « Sum2(arr, 20) « endl;
(г) Реализуйте этот класс.
8.13	Рассмотрите объявления
String A("Have а ", в("nice day!"), С (A), d=B;
(а)	Каково значение С?
(б)	Каково значение D?
(в)	Укажите значение D = А + В;
(г)	Укажите значение С+=В.
8.14	Рассмотрите строки:
String S("abcl2xya52cba"), Т;
(a)	Каково значение S.FindLast(’c’)?
(6)	Каково значение S[6]?
(в)	Каково значение S[3]?
(r)	Каково значение S[24]?
(Д)	Каково значение Т = S.Substr(5,6)?
(e)	Каково значение Т после выполнения операторов:
Т = S;
Т.Insert("АВС", 5);
8.15	Сделайте следующие объявления:
#define TF(b) ((b)? "TRUE" : "FALSE") String si("STRING"), s2("CLASS");
String s3;
int loc, I;
char c, cstr[30];
Определите выход каждой последовательности операторов:
(a)	s3 = si + s2;
cout « si « "объединенная с " « s2
” « s3 « endl;
(6)	cout « "Длина " « s2 « ’’ = ” « s2.Length () « endl;
(в)	cout « "Первое вхождение 'S' в ” « s2
<< *• = " « s2.Find('S',0) « endl;
cout « "Последнее вхождение 'S’ в " « s2 « ” = ' « s2.FindLast('S') « endl;
(r) cout « "Вставить 'OBJECT « endl;
s3 = si + s2;
s3.Insert("OBJECT ", 7);
cout « s3 « endl;
в строку s3 в позицию 7."
(Д) si = FILE1.S;
for(i=0; i < sl.Legthf); i++)
с «= Sl[i];
if (с >= 'A' &4 с <= ' Z' )
c += 32;
sl[i] = c;
)
cout « sl « endl;
(e) sl
E “ABCDE”;
- "BCF";
cout « "sl < s2 » ” « TF(sl < s2) « endl;
cout « "sl == s2 = ’’ « TF(sl == s2) « endl;
S1 = “проверка оператора преобразования указателя”; strcpy(ostr, sl);
cout « cstr « endl;
816 Предположим, что переменные x, у и z определены следующим образом: unsigned short х = 14, у « 11, z;
Какое значение присваивается переменной z в результате выполнения каждого следующего оператора?
(a)	z - х | у;
(б)	z = х & у;
(в)	z - ~0 « 4;
(г)	г - ~х & (у » 1) :
(д)	z - {1 « 3) & х;
8.17	В этом упражнении представлены четыре функции, выполняющие битовые преобразования. Сопоставьте каждую функцию с одной из следующих описательных фраз:
(а)	Определение количества битов в int для определенной машины.
(6)	Возвращение числового значения и битов, начиная с бита в позиции р.
(в)	Инвертирование п битов, начиная с бита в позиции р. В позиции О Находится крайний левый бит целого значения.
(г)	Битовый сдвиг целого по часовой стрелке.
unsigned int one (unsigned int n, int b)
iht rightbit;
int Ishift = three () - 1;
int mask = (unsigned int) -0 » 1;
“hile (b—)
rightbit = n & 1;
n *= (n » 1) & mask;
rightbit и rightbit « Ishift;
n “ n | rightbit;
n.
unsigned int two (unsigned int x, int p, int n) {
unsigned int mask  (unsigned int) ~(~0 « n);
return (x » (p-n+1)) & mask;
)
int three(void)
(
int i;
unsigned int u = (unsigned int) -0;
for (i=l;u = u »l;i++);
return i;
)
unsigned int four (unsigned int x, int p, int n) {
unsigned int mask;
mask = -0;
return x л (-(mask » n) » p);
8.18	Добавьте оператор разности (-) в класс Set. Этот оператор принимает два параметра типа множество, X и Y, и возвращает множество, со. стоящее из элементов в X, которые не находятся в Y. Необходимо, чтобы два эти множества имели одно и то же количество элементов.
х - [0, з, 7, 9}, X - Y = (О, 3, 9)
template <class Т>
Set<T> Set<T>::operator — (const Set<T> fix);
(Совет: Вычислите diff.member[i] = member[i] & ~x.member[i])
8.19	Добавьте оператор побитового отрицания (-) в класс Set. Этот унарный оператор возвращает множество, состоящее из всех значений в универсальном множестве, которые не находятся в X.
for(int i=0; i < 10; i +“ 2) X. Insert(i);
Y = -X;	// X = (1,3,5,7,9}
8,20
(8)
(б)
Используйте оператор побитового отрицания для создания универсального множества в качестве объекта Set.
Реализуйте оператор разности из письменного упражнения 8.18 с операторами побитового отрицания и пересечения.
Упражнения по программированию
g 1 В этом упражнении используется класс Dynamiclnt, разработанный в письменном упражнении 8.3.
(а)	Перегрузите оператор < для Dynamiclnt как внешнюю функцию. Она должна быть дружественной этому классу.
(б)	Напишите функцию
Dynamiclnt *lnitialize(int n);
которая возвращает указатель на массив динамически выделенных объектов. Инициализируйте значения объектов, чтобы они были случайными целыми числами в диапазоне 1..1000.
(в)	Main-функция должна считывать целое п и использовать Initialize для создания массива объектов типа Dynamiclnt. Используйте основанную на шаблоне обменную сортировку из главы 7 для сортировки списка. Напечатайте результирующий список.
85 Данная программа использует класс ReadFile из письменного упражнения 8.7. Используйте функцию LineNum для чтения файла и вывода на экран строк с номерами.
8.3 В этой программе используется класс ReadFile из письменного упражнения 8.7.
(а) Напишите функцию
void CapLine(ReadFile& f, char *capline);
(6)
8.4
8.5
которая считывает следующую строку из файла f, печатает ее прописными буквами и возвращает эту строку в capline.
Используйте эту функцию для чтения файла и печати его в верхнем Регистре.
Упражнение использует класс ArrCL из письменного упражнения ’12. Создайте надежный массив из 10 целых и затем запросите пользователя ввести 10 элементов данных. После вызова основанной на аблоие обменной сортировки (в файле arrsort.h) печатайте упорядо-енный список. Попытайтесь обратиться к А[10], чтобы вызвать сообщение об ошибке и завершение программы.
Упражнение модифицирует основанный на шаблоне класс Stack, рабоганный в главе 7 (stack.h). Замените данное-член статического stacklist объектом Array, первоначально содержащим Мах-М	элементов. Удалите параметр размера стека из конструктора
ЬоаеРвришите метод Push так, чтобы размер стека при необходимости 1„1₽^ал. Протестируйте ваши изменения, помещая целые значения ° Р стек и затем удаляя их, печатая каждое 10-е значение.
8.6
-- --------. ----------------------
Считайте файл, используя потоковый метод getline, и сохраните стрОк в объекте Array, называемом строковым пулом. Вставьте строку в эт пул, выполняя операцию Resize, чтобы добавить достаточное прострап ство в пул, и поместите начальный индекс этой строки в массив стр5 ковых индексов, изменяя при необходимости его размер. Этот масс» будет определять начальное положение каждой последующей строки пуле. Введите целое N и печатайте последние N строк файла. Есл* файл имеет меньше строк, чем N, печатайте весь файл.
Массив строковых индексов
indexO
index!
indexZ
index3
Строковый пул
indexO
index!
index2
index3
8.7 Измените программу 8.3, сохраняя список всех простых чисел, которые вы вычислили до этого времени. Для следующего целого и в последовательности проверяйте только простые числа в списке, а не все делители от 3 до п/2. Если п не делится ни на какое простое число, то и — это новое простое число, которое может быть добавлено в список. Этот факт является математическим результатом того, что любое число может быть записано, как произведение его простых делителей.
8.8
(а) Напишите функцию
void Replace(Strings S, int pos, const strings repstr);
которая заменяет repstr.LengthQ символов из S, начиная с индекса роз. Если в хвосте строки S символов меньше, чем repstr.Length, вставьте все символы repstr.
8.8
(а) Напишите функцию
void Center(Strings S);
которая вызывает Replace для печати S с центрированием внутри 80' символьной строки.
Напишите main-функцию, тестирующую функции из пунктов (а) и (Ъ)-
8.9 Считайте целое и, представляющее количество строк текста в дркумеЯ‘ те. Динамически выделите место для п указателей на объекты String Читайте и объектов String, используя ReadString.
Строки текста могут содержать специальные символы и ”& ♦ пример:
Уважаемый #
& Ваш счастливый подарок находится в &. Если Вы пойдете в и укажете Ваше имя, служащий вручит Вам Ваш приз. Сп»с # за Ваш интерес к нашему конкурсу.
С уважением,
Мистер Стринг
рдедите строку poundstr, которая заменяет все вхождения # в вашем окуМенте. Введите строку ampstring, которая заменяет все вхождения Пройдите по массиву строковых указателей и выполните подстановки- Печатайте окончательный документ.
Напишите программу, которая инициализирует два 10-элементных мас-e.iv сива intА и intB и использует их для создания множеств А и В. Оператор сазности множества (-) определяется в письменном упражнении 8.18. Эта программа должна вычислить A-В и напечатать результат. Программа должна также проверить, что д 4- В = А — В + В “ А + А*В
gll Функция
template <class>T
Set <Т> TxclusiveUnion(const Set<T>i X, const Set<T>b Y) ;
возвращает множество, содержащее все элементы, которые находятся либо в X, либо в Y, но не в обоих множествах.
(а)	Используйте графический аргумент, чтобы показать, что ExclusiveUnion может вычисляться с использованием любой из следующих формул:
1.	(X-Y) + (Y-X)
2.	X* ~Y + ~X*Y
Операции разности множества (-) и дополнения (-) определяются в письменных упражнениях 8.18 и 8.19.
(б)	Реализуйте ExclusiveUnion, используя формулу 2. Протестируйте вашу работу, используя множества:
X = {1,3,4,5,7,8,12,20}, Y = {3,5,9,10,12,15,20,25}
Для которых
ExclusiveUnion(X,Y) = (1,4,7,8,10,15,25}
12 Два ферзя на шахматной доске должны быть атакующими, если один может переместиться в позицию второго, то есть, если они находятся в одном и том же ряду, столбце или диагонали. На следующем рисунке Показан пример атакующих и неатакующих ферзей.
Г4НОя.ЫВая позицию каждого ферзя, рассмотрим проблему определения Лл л^ества всех возможных перемещений каждого из них и, являются атакующими.
---- --'— — I *		-W-ITWI I ..I--- «г -М4.-И .«„.-JU--	_ -MT MT>~ -<« * »	if
1
Любая позиция на шахматной доске может рассматриваться с испоц] зеванием номера строки и номера столбца, каждый в диапазоне oj 1 до 7. Каждой клеточке доски может быть .присвоен единственный Hoij ’ между О и 63 назначением каждой паре строка/столбец (i,j) ц€Ло; значения i*8+j. Мы можем теперь рассматривать шахматную доску множество из 64 целых в диапазоне от 0 до 63.
Напишите функцию
void ComputeQeenPositions(Set<int>b Board, int rowq, int colq);
которая принимает множество Board, позицию ферзя (int rowq, ь colq) и присваивает множеству все клеточки, на которые ферзь моад: переместиться.
Напишите функцию
void PrintQueenPositions(Set<int> Queen,int Qrow.int Qcol, int QOtherRow,int QOtherCol);
Первый ферзь находится в позиции (Qrow, Qcol), a Queen — это мно жество позиций, на которые этот ферзь может переместиться. Другой ферзь находится в позиции (QOtherRow, QOtherCol). Функция печатав доску, помещая символ "X” в каждую клеточку, на которую моле? переместиться первый ферзь, и символ ” ” — в другие клеточки. Ес.ъ один из ферзей находится в клеточке, печатается "Q".
Напишите функцию
int ActackingQeens(int QlRow, int QlCol, int Q2Row, int Q2CO1);
которая принимает позицию двух ферзей и определяет, являются л: они атакующими. Это выполняется определением того, находятся л; они в одной и той же строке, столбце или диагонали.
Напишите main-функцию, читающую позиции двух ферзей и печатаю щую множество возможных положений, на которые каждый фер3 может переместиться. Сообщение указывает, являются ли ферзи ат& кующими.
Связанные списки
9,1. Класс Node
9.2. Построение связанных списков
93. Разработка класса связанного списка
9А Класс LinkedList
95. Реализация класса LinkedList
9.6.	Реализация коллекций со связанными списками
9.7.	Исследовательская задача: Буферизация печати
9.8.	Циклические списки
9.9.	Двусвязные списки
9.10.	Практическая задача: Управление окнами
Письменные упражнения
Упражнения по программированию
Мы уже определили класс коллекций, которые реализованы на базе Wa сивов. Коллекции включали стеки, очереди н более общий класс Seql^/ поддерживающий последовательный список элементов. В этой главе разъ * батываются связанные списки, которые предоставляют более гибкие метол добавления и удаления элементов.
Массив может использоваться с простыми приложениями сохранения сцИс ков. Например, многие авиалинии продают незарезервированные билеты, торые могут быть выкуплены клиентами как посадочный билет. Во врем^ предварительной продажи билетов авиалиния ведет список клиентов, добав ляя в него их имена. Перед полетом каждого клиента регистрирует служащий авиакомпании, который удаляет имя из билетного списка и добавляет его в список пассажиров. С помощью таких списков авиакомпания имеет доступ к количеству пассажиров на борту самолета и к количеству клиентов с еще неоплаченными билетами.
Списки, основанные на массиве, недостаточно эффективны для приложений, требующих более гибких методов обработки. Рассмотрим пример рее-торана, в котором выполняется резервирование мест. Метрдотелю необходимо вводить имена в список с упорядочением по времени и пользоваться несколькими критериями для удаления имени из списка. Немедленное удаление происходит, если клиент звонит по телефону и отменяет заказ или, если клиент приходит и занимает место. Периодически метрдотель должен просматривать список и удалять имена клиентов, которые потеряли резервирование, не появившись в ресторане в течение 15 минут после времени заказа.
Время 6:15 (Dolan - отменил)
Время 6:40 (Jones, Dal porto - пришли)
Время 7:05 (Banks - удалить)
Основанный на массиве список не может эффективно работать с ресторанной системой резервирования. Имена должны вставляться в различи^6 места в списке с учетом различного времени прихода клиентов. Для этог° необходимо, чтобы имена были сдвинуты вправо. Удаления требуют, чтоб имена сдвигались влево. Работники ресторана не могут предвидеть количеству зарезервированных мест и должны выделять преувеличенный объем памя для обработки экстренных случаев. Когда начинается обслуживание, спис°^ резервирования становится очень динамичным с быстрым движением иМ в список и из него. Массивы с их непрерывной памятью не отвечают эТ^ динамическим изменениям. В последующем обсуждении рассматриваются которые иэ этих проблем с массивами и разрабатываются решения, котоР используют динамические связанные списки.
Массив — это структура фиксированной длины. Даже динамический мае-яглеет фиксированный размер после операции изменения размера. На-сй® on массив А создается динамически как массив целых из 6 элементов:
А = new int [6];
А
Когда массив заполнен, мы можем добавлять элементы только после из-яенпя размера списка — процесс, требующий копирования каждого эле-н ята в новую память. Частое изменение размера больших списков может й_.есТвенно повлиять на эффективность приложения.
С 'ссив сохраняет элементы в смежных ячейках памяти, что делает возможном прямой доступ к элементу, но не позволяет эффективно выполнять добавление и удаление элементов, если только эти операции не выполняются в конце массива. Например, предположим, что вы удаляете элемент 60 из второй позиции в следующем списке:
индекс 1
Для сохранения непрерывного порядка элементов в списке мы должны сдвинуть четыре элемента влево.
Предположим, что мы добавляем новый элемент 50 в позицию с индексом 2. Так как массив сохраняет элементы в непрерывном порядке, мы должны освободить участок в списке, сдвигая три элемента на одну позицию вправо:
индекс 2
Для списка из N элементов вставка и удаление элемента в конце списка КолJ107 времени вычисления О( 1). Однако для общего случая ожидаемое Честв° сдвигов равно N/2, а время вычисления — O(N).
Описание связанного1 списка
эФФективног° управления данными является важнейшей для ксоторе^ЭалиэаЦии списка. Нам необходимо разработать новую структуру, йеевободит нас от непрерывного порядка сохранения данных. Мы Исп°льзовать модель звеньев в цепи.
^атУре часто используется термин "связный список". — Прим. ред.
-ч
Длина списка может увеличиваться без ограничений при добавлении нов, звеньев. Более того, новые элементы могут быть вставлены в список прост?! разрывом соединения, добавлением нового звена и восстановлением соеди? НИЯ.
До вставки
После вставки
Элементы удаляются из списка разрывом двух соединений, удаление, звена и затем повторным соединением цепи.
До удаления
После удаления
В этой главе разрабатывается структура данных, называемая связанные список (linked list), для реализации последовательного списка. Связанные списки предоставляют эффективные методы обработки и снимают многие ограничения, отмеченные при описании массивов.
Обзор главы
Независимые элементы в связанном списке называются узлами (nodes). Мы разрабатываем класс Node, определяющий отдельные объекты узла я предоставляющий методы обработки связанных списков. В частности, мы реализуем методы узла, которые вставляют и удаляют узлы после текущег0 узла, разрабатываем алгоритмы, позволяющие использовать отдельные узлы и создавать связанный список, сканировать узлы и обновлять их значения.
Мы создаем класс LinkedList, который инкапсулирует основные алгоритмы узла в структуре класса, и используем этот класс для реализации классов Queue и SeqList, снимая тем самым ограничения списков, основанных на массиве. Этот класс применяется для решения множества интересных ПР0" блем, включая удаление всех дублированных данных-значений из списка-Класс LinkedList используется в разработке двух практических задач: моделирования системы буферизации печати и при построении динамически
оконных списков.
Разработка списка как круговой системы содержит как концептуальй преимущества, так и преимущества кодирования. Двусвязные списки наХОД применение, когда нам необходимо выполнять поиск элементов в обоих правлениях. Мы определяем классы CNode и DNode, реализующие круг0®
и двусвязные списочные структуры.
9.1.	Класс Node
V ел (node) состоит из поля данных и указателя, обозначающего следую-ft элемент в списке. Указатель — это соединитель, связывающий вместе узлы списка.
Связанный список состоит из множества узлов, первый элемент которого /front)»_ эт0 узел, на который указывает голова (head). Список связывает
1зЛЫ вместе от первого до конца или хвоста (rear) списка. Хвост определяется как узел, чье поле указателя имеет значение NULL (0). Списочные приложения проходят по узлам, следуя за каждым указателем на следующий узел.
В любой точке сканирования на текущее положение ссылается указатель currPtr. Для списка без узлов head будет содержать значение NULL.
head
NULL
Объявление типа Node
Узел с его данными (data) и полями указателей (next) является строительным блоком для связанного списка. Структура узла имеет операции, которые инициализируют данные-члены, и методы управления указателями для доступа к следующему узлу.
Далее приведен рисунок, иллюстрирующий базовые операции для обработки узлов. В любом данном узле р мы можем реализовать операцию 1п-8wtAfter, которая присоединяет новый узел после текущего. Процесс начинается прерыванием соединения с последующим узлом q, вставкой newNode восстановлением связей.
Узе^*Логичнь1й q и ’Дующий
newNode ;
пересоединигь----1	‘----пересоединить
процесс описывает операцию DeleteAfter, которая удаляет за текущим. Мы отсоединяем р от следующего за ним узла
соединяем р с узлом, следующим за q.
12 ^425^
До удаления
। _
разъединить
Лоте удаления
пересоединитъ
Структура узлов с операциями вставки и удаления описывает абстрактный тип данных. Для каждого узла эти операции относятся непосредственно к его последующему (следующему) узлу.
adt Node
Данные
Поле данных используется для хранения данных. Кроме инициализации, значение не используется ни в какой другой операции. Поле next является указателем на последующий узел. Если next — это NULL, то следующего узла нет.
Операции
Конструктор	
Начальные значения: Процесс:	Значение данных и указатель на следующий узел. Инициализация двух полей.
NextNode Вход: Предусловия: Процесс: Выход: Постусловия: InsertAfter Вход: Предусловия: Процесс:	Нет Нет Выборка значения поля next Возвращение значения поля next. Нет Указатель на новый узел. Нет Переустановка значения next для указания на новый
Выход: Постусловия: DeleteAfter Вход: Предусловия: Процесс:	узел и установка значения next в новом узле для ссылки на узел, следующий за текущим. Нет Узел теперь указывает на новый узел Нет Нет Отсоединение следующего узла и присваивание значения
Выход: Постусловия:	next для ссылки на узел, который следует за следующим узлом. Указатель на удаленный узел. Узел имеет новое значение next.
Конец ADT Node
ADT Node описывается основанным на шаблоне классом C++.
^1ециФУкация класса Node
H018te <ClaS® T>
t'gs node
ntivate-
v ц next указывает на адрес // следующего узла
Node<T> ‘next;
euDlio
Ц открытые данные f data;
// конструктор
Mode (const TS item, Node<T>* ptrnext = NULL);
// методы модификации списка void insertAfter (Node<T> *p) ; Node<T> ‘DeleteAfter(void);
// получение адреса следующего узла
N©de<T> ‘NextNode(void) const;
);
СПИСАНИЕ
Значение поля next — это указатель на Node. Класс Node является само-ссылающейся (self-referencing) структурой, в которой указатель-член (pointer member) ссылается на объекты своего собственного типа.
Мы используем объекты Node для разнообразных классов коллекций, таких как словари и хеш-таблицы, и объявляем поле открытых данных для обеспечения удобного доступа к ним. Такой подход менее затруднителен для пользователя, чем использование пары функций-членов, таких как Get-Data/SetData. Преимущества становятся очевиднее, когда необходимо наличие ссылки на данное-член. Это необходимо при реализации более продвинутых классов, таких как словари. Поле next остается закрытым, и доступ к нему обеспечивается функцией-членом NextNode. Оно изменяется только методами InsertAfter и DeleteAfter.
Конструктор для класса Node инициализирует поле открытых данных и штт закРытЬ1х указателей. По умолчанию значение next устанавливается на i ’ v JLdj*
ПРИМЕР
t(10); // создание узла t с данными И значене = 10, next - NULL
**ode<lnt>(20J .
’ ' new млн p' Я» r:
₽ *	Se<char>('B'
r	”°ae<char>rA'
*	*ode<r.x
^e<char> (»c*
P = &t
//выделяет объект в u //value = 20 и next  NULL
);
,q);
);
// q имеет данные 'В'
// объявление узла р с данными 'А'
// поле next указывает на q
// объявление узла г с данными 'С'
q->InsertAfter(г);
cout « p->data;
p =• p->NextNode();
cout « p->data;
cout « endl;
r “ q->DeleteAfter();
П вставка г в хвост списка // печать символа 'А' // переход к следующему узлу // печать символа 'В' .
// отделение хвостасписка; // присваивание значения г
Реализация класса Node
Класс Node содержит как открытый, так и закрытый данные-члены. от крытый данное-член предназначается для того, чтобы пользователь и классы коллекций могли иметь прямой доступ к его значению. Поле next является закрытым, и доступом к этому полю указателя управляют функции-члены Только методам InsertAfter и DeleteAfter позволяется изменять значение поля next. Если сделать это поле открытым, пользователь получит возмож-ность нарушать связь и уничтожать связанный список. Класс Node содержит функцию-член NextNode, которая позволяет клиенту проходить по связанному списку.
// конструктор, инициализация данных и указателя template <class Т>
Node<T>::Node(const Tfi item, Node<T>* ptrnext) : data(item), next(ptrnext) ()
Списковые операции. Метод NextNode предоставляет клиенту доступ к полю указателя next. Этот метод возвращает значение next и используется для прохождения по списку.
// возвратить закрытый член next template <class Т>
Node<T> *Node<T>::NextNode(void) const ( return next;
)
Функции InsertAfter и DeleteAfter являются основными операциями создания списков. В каждом случае процесс включает только изменения указателей.
InsertAfter принимает узел р в качестве параметра и добавляет его в список в качестве следующего узла. Первоначально текущий объект указывает на узел, адресом которого является q (значение в поле next). Алгоритм изменяет два указателя. Поле указателей р устанавливается на q, а полю указателей в текущем объекте присваивается значение р.
И вставить узел р после текущего узла template cclass Т>
jjode<T>:: Insert Aft er (Node<T> *p>
{	. указывает на следующий за текущим узел,
//а текущий узел - на р.
p->next ® next;
next - рг
1 -
Порядок присваивании указателей очень важен, рдторы присваивания имеют обратный порядок.
яр’	// Узел, следующий за текущим объектом,
pext '	ft потерян!
p.>next - next
Предположим, что опе-
После
DeleteAfter удаляет узел, который следует за текущим объектом и связывает его поле указателей со следующим узлом в списке. Если после текущего объекта нет никакого узла (next == NULL), функция возвращает NULL. Иначе, функция возвращает адрес удаленного узла для случая, если программисту необходимо освободить память этого узла. Алгоритм Delete After сохраняет адрес следующего узла в tempPtr. Поле next узла tempPtr определяет узел в списке, на который должен теперь указывать текущий объект. Возвращается указатель на узел tempPtr. Процесс требует присваивания только одного указателя.
До	После
4 ta Удалить узел, следующий за текущим, и возвратить его адрес template <class Т>
< е<Т> *Node<T>::DeleteAfter(void)
КонСОХРан1теь ал₽ес Удаляемого узла ае<т> *tempptr - next;
ir нет следующего узла, возвратить NULL
(next == NULL)
teturn NULL;
neх?екущий узел указывает на узел, следующий за tempPtr
* ^eropPtr->next;
i retnrnn?a™Tb указатель на несвязанный узел
I tewpPtr;
9.2.	Создание связанных списков
Для создания связанных списков мы используем класс Node. В процес обсуждения этой темы вводятся основные алгоритмы связанных списков6 применяемых в большинстве приложений. Материал этого раздела важеи для понимания связанных списков, поскольку здесь вы узнаете, как создават, списки и обращаться к узлам. Мы реализуем алгоритмы связанных спискОй как независимые функции. Овладение этим фундаментальным материалов поможет вам, когда эти методы будут использоваться для создания общего класса связанных списков. Для простоты в нашем обсуждении подразумева. ется, что узлы списка содержат целые данные.
Связанный список начинается с указателя узла, который ссылается на начало списка. Мы называем этот указатель головой, так как он указывает на начало списка. Первоначально значением головы является NULL для указания пустого списка.
Мы можем создать связанный список различными способами. Начнем с изучения случая, когда каждый новый узел помещается в голову списка. Позже мы рассмотрим случай, когда узлы добавляются в хвост списка или в промежуточные позиции.
Создание узла
Мы реализуем создание узла с использованием основанной на шаблоне функции GetNode, которая принимает начальные данное-значение и указатель и динамически создает новый узел. Если выделение памяти происходит неудачно, программа завершается, иначе, функция возвращает указатель на новый узел.
// выделение узла с данным-членом item и указателем nextPtr template <class Т>
Node<T> *GetNode(const T& item, Node<T> *nextPtr = NULL)
{
Node<T> *newNode;
// выделение памяти при передаче item и NextPtr конструктору.
// завершение программы, если выделение памяти неудачно newNode = new Node<T>(item, nextPtr);
if (newNode == NULL)
(
cerr « "Ошибка выделения памяти!” « endl;
exit(1);
}
return newNode;
я
Вставка узла: InsertFront
Операция вставки узла в начало списка требует обновления значения У1^ зателя головы, так как список должен теперь иметь новое начало. Пробл сохранения головы списка является основной для управления списками- »
вы потеряете голову, вы потеряете список!	г
Перед началом вставки голова определяет начало списка. После вста® новый узел займет положение в начале списка, а предыдущее начало спИ займет вторую позицию. Следовательно, полю указателей нового узла сваивается текущее значение головы, а голове присваивается адрес ЕОВ
да Назначение выполняется с использованием GetNode для создания нового узЛ^-
. - GetNode(item,head);
nea<* '
newNode
newNode
функция TnsertFront принимает текущую голову списка, которая является указателем, определяющим список, а также принимает новое значение дан-кых, Она вставляет значение данных в узел в начале списка. Так как голова будет изменяться этой операцией, она передается как ссылочный параметр. // вставка элемента в начало списка template <class Т>
void TnsertFront(Node<T>* & head, T item) <
// создание нового узла, чтобы он указывал на
// первоначальную голову списка
// изменение головы списка
head - GetNode(item,head); )
Эта функция и GetNode находятся в файле nodelib.h.
Прохождение по связанному списку
•чы используем Первоначально
Начальной точкой любого алгоритма прохождения является указатель головы, так как он определяет начало списка. При прохождении по списку, указатель currPtr для ссылки на текущее положение в списке. currPtr устанавливается на начало списка:
^currPtr = head;
Во время сканирования нам необходим доступ к полю данных в текущем лоэкении. Так как data — это открытый член, у нас есть возможность ьщолнить выборку значения data или присвоить этому члену класса новое значение.
curr^tDataValue = currPtr->data;
r“>data = newdata;
Хож1аП₽ИмеР’ ЛРОСТОЙ оператор cout может быть включен в алгоритм про-Дения списка для печати значения каждого узла:
<< currPtr->data;
сканирования мы непрерывно перемещаем currPtr к следую-Узлу до тех пор, пока не достигнем конца списка.
currPtr->nextNode();
Прохождение по списку завершается, когда currPtr становится равц^ NULL. Например, функция PrintList печатает значение данных каждого узд. Голова передается в качестве параметра для. определения списка. Втог параметр пользовательского типа AppendNewline указывает на то, додж» ли следовать за выводом два пробела или символ newline. Функция соде-жится в файле nodelib.h.
enum AppendNewline {noNewline,addNewline);
// печать связанного списка
template <class T>
void PrintList(Node<T> *head, AppendNewline addnl = noNewline) {
// currPtr пробегает no списку, начиная с головы Node<T> *currFtr  head;
// пока не конец списка, печатать значение данных // текущего узла while(currPtr !- NULL) { if(addnl == addNewline)
' cout « currPtr->data « endl;
else cout « currPtr->data « "
// перейти к следующему узлу currPtr = currFtr->NextNode(); >
)
Программа 9.1. Сопоставление с ключом
Программа генерирует 10 случайных чисел в диапазоне от 1 до 10 5 вставляет эти значения как узлы в голову связанного списка, используй InsertFront. Для отображения списка используется Printlist.
Программа содержит код, который подсчитывает количество вхожденя' ключа в список. У пользователя сначала запрашивается ключ, которь? при прохождении по списку сравнивается с полем данных в каждом узл" списка. Выводится общее количество вхождений этого ключа.
#include <rostream.h>
#pragma hdrstop
# include "node.h”
#include "nodelib.h" winclude "random.h"
void main(void)
{
// установить голову списка в NULL Node<int> *head  NULL, *currPtr;
int i, key, count • 0;
RandomNumber rnd;
// ввести 10-ть случайный чисел в начало списка for (i=0;i < 10;i++)
InsertFront(head, int(1+rnd.Random(10)));
// печать исходного списка
cout « "Список:
pcintList(head,noNewline);
cout « endl;
II запросить ввод ключа
cout « "Введите ключ:
cin » key;
ц цикл по списку
currPtr - head;
while (currPtr NULL)
// если данные совпадают с ключом, увеличить count
if (currPtr->data =- key) count++;
// перейти к следующему узлу
currPtr = currPtr->NextNode();
}
cout « "Значение *' « key « " появляется " « count « ” раз(а) в этом списке" « endl;
}
«Выполнение программы 9.1>
Список: 365752459 10
Введите ключ: 5
Значение 5 появляется 3 раз (а) в этом списке
♦/
Вставка узла: InsertRear
Помещение узла в хвост списка требует начального тестирования для определения, пуст ли список. Если да, то создаем новый узел с нулевым указателем поля и присваиваем его адрес голове. Операция реализуется функцией Insert-Front. При непустом списке мы должны сканировать узлы для обнаружения хвостового узла (в котором поле next содержит значение NULL).
clirr₽tr->NextNode{) =- NULL;
I-
NULL
newMode
выполняется следующим образом: сначала создается новый узел Та^е)’ затем он вставляется после текущего объекта Node (InsertAfter). ДаеТса вставка может изменять значение указателя головы, голова пере-/z Как ссылочный параметр:
?епМа1-Л Хв°ст списка и добавить item
1П8 <Cla£S Т>
®rtRsar(Node<T>* & head, const T& item)
Node<T> *newNode, ‘currPtr = head;
// если список пуст, вставить item в начало
if (currPtr »= NULL)
InsertFront(head,item);
else
<
// найти узел с нулевым указателем while(currPtr->NextNode () != NULL) currPtr = currPtr->NextNode();
II создать узел и вставить в коней списка // (после currPtr)
newNode = GetNode(item);
currPtr->InsertAfter (newNode) ;
InsertRear содержится в файле nodelib.h.
Программа 9.2. Головоломка
В этой программе беспорядочно смешиваются буквы слова для создания слова-путаницы. Процесс сканирует каждый символ в строке и произвольно помещает его либо в начало, либо в хвост списка. Для каждого символа вызываем random(2). Если возвращаемое значение является равным 0, вызываем InsertFront, иначе — InsertRear. Например, с вводом j-u-m-b-1-е и последовательностью случайных чисел 011001 получаем результирующий список Ibjume. Программа читает и записывает четыре слова-головоломки.
«include <iostream.h>
«pragma hdrstop
«include "random.h"
« include *'strclass.h"
«include ’’nodelib.h**
void main(void)
(
// список узлов для символов головоломки (Jumbled) Ncde<q£ar> *jumbleword = NULL;
// входная строка, генератор случайных чисел, счетчики String s;
RandomNumber rnd;
int i, j;
// ввести четыре строки
for (i « 0; i < 4; i++)
{ cin » s;
II использование Random(2) для определения направления движения И символа:в начало (value = 0), или в конец (value = 1) списка for (j - 0; j < s.Length(); j++)
if (rnd.Random(2)) InsertRear{jumbleword, s(j]);
else InsertFront(jumbleword, s[j]);
If печать входной строки и ее перепутанного варианта cout « "String/Jumble-- ” « s « "
FrintList(jumbleword);
cout « endl « endl;
ClearList(jurobleword);
}
}
^Выполнение программы 9.2>
String/Jumble: pepper	r p p e p e
tiawaii
String/Jumble- hawaii	i i h a w a
jumble
String/Jumble: jumble	e b m j u 1
C++
String/Jumble: C++	+ C +
*/
Удаление узла. В этом разделе уже обсуждались алгоритмы для сканирования списка и для вставки новых узлов. Третья списочная операция, удаление узла из списка, знакомит с рядом новых проблем. Нам часто бывает необходимо удалять первый узел в списке. Операция требует, чтобы мы изменяли голову списка для указания на последующий узел за бывшим начальным узлом.
head
i
head -» NextModeO
Функция DeleteFront, которой передается голова списка как ссылочный параметр, отцепляет от списка первый узел и освобождает его память: удалить первый узел списка
«nipiate «class Т>
void DeleteFront (Node<T>’ & head)
£/ сохранить адрес удаляемого узла
N°de<T> *р = head;
^/ Убедиться в том, что список не пуст
* (head !- NULL)
// передвинуть голову к следующему узлу и удалить оригинал head = head->NextNode();
> delete р; )
°5inatr .
Функция удаления проверяет список и удаляет первый узел, чье е Данных совпадает с ключом. Устанавливаем prevPtr на NULL, а с°вдад6 Еа голову списка. Затем перемещаем currPtr по списку в поисках вйя^^ с ключом и сохраняем prevPtr, так чтобы он ссылался на предыду-^еНйе CurrPtr.
prevPtr
currPtr
Указатели prevPtr и currPtr перемещаются по списку совместно до тех гт пока currPtr не станет равным NULL или не будет найден ключ (currPtr->d°?’ -== key).	ata
while (curPtr ! = NULL && currPtr->data !=key) {
// перемещение prevPtr вперед к currPtr prevPtr = currPtr;
// перемещение currPtr вперед на один узел currPtr = currPtr->NextNode();
)
Совпадение возникает, если мы выходим из оператора while с currPtr!=NULL Тогда мы можем удалить текущий узел. Существует две возможности. Если prevPtr — это NULL, удаляем первый узел в списке. Иначе, удаляем узел выполняя DeleteAfter для узла prevPtr.	’
if (prevptr == NULL)
head = head->NextNode(); else
prevPtr->DeleteAfter();
Если ключ не найден, метод Delete просто завершается. Так как удаление в начале списка приводит к изменению головы, мы должны передавать голову по ссылке.
// удаление первого элемента, совпадающего с ключей
template <class Т>
void Delete (Node<T>* & head, T key) (
Node<T> ‘currPtr = head, *prevPtr = NULL;
// завершить функцию, если список пустой
if (currPtr == NULL) return;
// прохождение no списку до совпадения с ключем или до конца
while (currPtr != NULL && currPtr->data ’ = key) {
prevPtr = currPtr;
currPtr = currPtr->NextNode();
}
// если currPtr не равно NULL, ключ в currPtr.
if (currPtr != NULL) (
// prevPtr == NULL означает совпадение в начале списка
if(prevPtr == NULL)
head = head->NextNode(); else // совпадение во втором или последующем узле // prevPtr->DeleteAfter() отсоединяет этот узел prevPtr->DeleteAfter();
// удаление узла delete currPtr;
Приложение: Список выпускников
Чапись StudentRecord содержит имя и средний балл кандидата на выпуск Здиверситета. Мы приступаем к созданию списка студентов, которые прой-113 через выпускную церемонию. Список кандидатов на выпуск считывается Д^яйла studrecs и вставляется в начало списка. Так как по университетским 03 студент со средним баллом ниже, чем 2.0, не допускается к выпуску, просматриваем список и удаляем всех кандидатов, средний балл которых ^Удовлетворяет минимальным требованиям. Второй список из файла noattend Заставляет студентов, которые не собираются присутствовать на выпускной оемопии, и используется для удаления дополнительных имен из выпускного Д иска. Оставшиеся элементы представляют собой список студентов, получивших квалификацию и планирующих принять участие в выпускной церемонии.
stnict StudentRecord I String name;
float gpa;
Программа 9.3. Список выпускников университета
Каждая запись, считываемая из файла studrecs, вставляется в начало связанного списка graduateList с использованием функции InsertFront библиотеки Node. Указатели prevPtr и currPtr используются для сканирования списка и удаления всех студентов, средний балл которых ниже, чем 2.0. После удаления имен студентов, которым не присвоена квалификация, мы считываем имена из файла noattend и удаляем из полученного ранее списка студентов, ие принимающих участия в выпускной церемонии. В заключение выводим полученный список.
♦include <iostream.h>
♦include <fstream.h>
♦include <stdlib.h>
♦include <iomanip.h>
♦pragma hdrstop
♦include "node.h"
♦include "nodelib.h"
♦include "studinfo.h"
void main (void)
Node<studentRecord> * *graduateList=NULL, *currPtr, *prevPtr,
*de1etedNodePtr ;
StudentRecord srec;
ifstream fin;
iin. open ("studrecs", ios: : in I ios -.: nocreate); dfin)
cerr « "Невозможно открыть файл studrecs." « endl;
, exit(l);
вывод среднего балла (gpa} °ut-Setf(ios::fixed);
cout.precision(1);
cout.setf(ios::showpoint);
while(fin » srec)	,
{
// вставить srec в голову списка InsertFront(graduateList,srec); )
prevPtr “ NULL;
currPtr = graduateList; while (currPtr != NULL) (
if (cvrrPtr->data.gpa < 2.0) {
if (prevPtr == NULL)	11 запись в начале списка?
{ graduateList - currPtr->NextNode(); deletedNodePtr = currPtr;
currPtr = graduateList; ) else // удались узел внутри списка ( currPtr = currPtr->NextNode();
deletedNodePtr = prevPtr->DeleteAfter();
delete deletedNodePtr; } else ( // нет удаления, передвинуть указатели вниз prevPtr = currPtr; currPtr = currPtr->NextNode(); > } fin.closet)i
fin.open("noattend",ios::in | ios::nocreate); if (ffin) {
cerr « ’’ Невозможно открыть файл noattend." « endl; exit(1);
)
while(srec.name.ReadString(fin) ! = -1) Delete(graduateList,srec);
cout « "Пристствующие на выпускной церемонии:*' « endl; PrintList(graduateList,addNewline);
) /* <Файл "studrecs"> Julie Bailey 1.5 Harold Nelson 2.9 Thomas Frazer 3.5 Bailey Hames 1.7 Sara Miller 3-9 Nancy Barnes
2.5
Rebecca Neeson
4.0
shannon Johnson
3.6
СФЭЙЛ ”noattend">
Thomas Frazer
Sara Miller
«выполнение программы 9.3> пристствуюшие на выпускной церемонии:
Shannon Johnson 3.8
Rebecca Neeson fl.0
Haney Barnes 2.5
Harold Nelson 2.9
*/
Создание упорядоченного списка
Во многих приложениях нам необходимо поддерживать упорядоченный список данных с узлами, приведенными в возрастающем или убывающем порядке. Для этого алгоритм вставки должен сначала сканировать список для определения правильного положения, в которое будет добавляться новый узел. Последующее обсуждение иллюстрирует процесс создания списка с возрастающим порядком.
Для ввода значения данных X мы сначала сканируем список и устанавливаем currPtr на первый же узел, значение данных которого больше, чем X. Новый узел со значением X должен вставляться непосредственно слева от currPtr. Во время сканирования указатель prevPtr перемещается совместно с currPtr и сохраняет запись предыдущей позиции.
Следующий пример иллюстрирует этот алгоритм. Предположим, что список L первоначально содержит целые 60, 65, 74 и 82.
_ Вставка 50 в список: Поскольку 60 является первым узлом в списке, оодьащм, чем 50, мы вставляем 50 в голову списка.
newNode
nsertFront(head,50); ь
70 в список: 74 — это первый узел в списке, больший, чем 70.
Тели prevPtr и currPtr обозначают узлы 65 и 74, соответственно.
newNode
newNode = GetNode(70);
prevPtr-xinsertAfter(newNode);
Вставка 90 в список'. Мы сканируем весь список и не можем найти узел больший, чем 90 (currPtr —=NULL). Новое значение больше, или равно все другим значениям в списке и, следовательно, новое значение должно бьт помещено в хвост списка. Когда сканирование завершается, вставляем новы? узел после prevPtr.
newNode = GetNode(90);
prevPtr->InsertAfter(newNode);
Следующая функция реализует общий алгоритм упорядоченной вставки. Для списка из п элементов, эффективность наихудшего случая имеет место пр * вставке нового элемента в конец списка. В этом случае должно быть выполнен»? п сравнений, поэтому наихудший случай имеет порядок О(п)- В среднем ожидается, что для нахождения места вставки мы просматриваем половину списка. В результате средний случай имеет порядок О(п). Конечно, наилучший случ 1 имеет порядок 0(1).
// вставить item в упорядоченный список template cclass т> void Insertorder(Node<T>* & head, T item) {
// currPtr пробегает по списку
Node<T> *currPtr, *prevPtr, *newNode;
// prevPtr == NULL указывает на совпадение в начале списка
prevPtr - NULL;
currPtr « head;
// цикл по списку и поиск точки»вставки
while (currPtr 1= NULL) <
// точка вставки найдена, если item < текущего data
if (item < currPtr->data)
break;
prevPtr = currPtr;
currPtr = curr₽tr->NextNode();
// вставка
if (ptevPtr == NULL)
1/ если prevPtr =- NULL, вставлять InsertFront(head,item);
в начало
el»®
А вставить новый узел после предыдущего newNode • GetNode(item) ;
prevPtr->InsertAfter(newNode);
}
Приложение: сортировка co связанными списками
InsertOrder может использоваться для сортировки коллекции элементов, и условии, что оператор сравнения "<*’ определяется для типа данных Т. функция LinkSort принимает массив А из п элементов и вставляет эти элемен-и в упорядоченный связанный список. Затем выполняется прохождение спис-ка и упорядоченные элементы копируются обратно в массив. Вызов функции ClearList освобождает память, ассоциированную с каждым узлом в списке.
функция проходит по списку и для каждого узла записывает его адрес, перемещается к следующему узлу и затем удаляет исходный узел. ClearList входит в файл nodelib.h.
ц удаление свех узлов в связанном списке template cclass т>
void ClearList(Node<T> * &head)
(
Node<T> *currPtr, *nextPtr;
currPtr - head;
while(currPtr !“ NULL) {
// записать адрес следующего узла, удалить текущий узел nextPtr - currPtr->NextNode();
delete currPtr;
currPtr - nextPtr; }
// пометить как пустой
head =» NULL;
i
Программа 9.4. Сортировка вставками
Данная программа принимает в качестве параметра массив целых А из О элементов и сортирует список, используя шаблонную функцию LinkSort. АЦдученный в результате упорядоченный массив, выводится с помощью ^rintArray.
ln?Clude <i°streani.h>
•Pragma hdrstop
include include
"node.h"
"nodelib.h"
vSX?te •'class T>
LinkSort(T a(), int n)
{
Node<T> *ordlist - NULL, *currPtr,-int x;
// вставлять элементы иэ массива в список с упорядочением for (i-0;i < n;i++)
Insertorder(ordlist. a[i]);
// сканировать список и копировать данные в массив currPtr - ordlist;
i - 0;
while(currPtr !« NULL)
(
)
a[i++] « currptr->data;
currPtr и currPtr->NextNode();
)
//
ClearList(crdlist);
удалить все узлы, созданные в упорядоченном списке
//
void PrintArray(int a[], int n)
сканировать массив и печатать его элементы
}
for(int i-0;i < n;i++) cout « a(i] « ”
void main(void)
ll инициализировать массив с 10 целыми значениями int A(10J - (82,65,74,95,60,28,5,3,33,55);
)
LinkSort(A,10);	П сортировать массив
cout « “ Отсортированный массив: PrintArray (A, 10),-	// печать массива
cout « endl;
«Выполнение программы 9.4>
Отсортированный массив:
3 5 28 33 55 60 65 74 82 95
1
При анализе эффективности времени исполнения алгоритма LinkSort следует принимать во внимание начальное упорядочение элементов массива. Най-худший случай соответствует списку, который уже отсортирован по возраста-нию. Каждый элемент вставляется в конец этого списка. Первая вставка вып°л’ няется без сравнений, вторая вставка выполняется с одним сравнением, третья с двумя сравнениями и так далее. Общее количество сравнений составляет
2	J
что имеет порядок О(пг). Другая крайность — список, который отсортир0® I в убывающем порядке, требует только п — 1 сравнений, так как	1
элемент массива вставляется в начало списка. Поэтому наилучший сл5П\т 1 имеет порядок О(п), а наихудший — О(п2). Интуитивно, средний случай ~ I п вставок с j-тым элементом ввода, требующим ожидаемых j/2 сРаВ11еЙ1йСе 1 Общее количество сравнений равно О(пг). В отличие от сортировки типа in-PJ I
й как ExchangeSort), LinkSort требует дополнительной памяти для всех (1^ -центов данных, а также памяти для указателей в связанном списке.
д.З. Разработка класса связанного списка
Программист может использовать класс Node вместе с утилитными функ-^ми находящимися в nodelib.h, для работы с приложениями связанных сков. Такой подход вынуждает программиста создавать каждый узел и поляять непосредственно списочные операции низкого уровня. Волее Б УКТурированный подход определяет класс связанных списков, который СТганИзует базовые списочные операции как функции-члены. В этом разделе Нуждается тип данных-членов и операций, которые должны быть включены класс связанных списков, с использованием знаний, приобретенных при паэработке алгоритмов с классом Node. Мы также заранее оговариваем тот Лакт, что наш класс связанных списков будет использоваться для реализации других списочных коллекций, которые включают связанные стеки, очереди н класс SeqList. В следующем разделе мы объявляем класс LinkedList и определяем его функции-члены.
Данные-члены связанных списков
Связанный список состоит из множества объектов Node, связанных вместе от начала до конца списка. Список имеет начальный узел, называемый головой, который определяет первый узел списка. Последний узел в списке имеет поле указателей со значением NULL, и на него ссылаются как на указатель-хвост. Нам необходимо, чтобы класс связанных списков содержал указатели на начало и на хвост списка, так как это полезно для многих приложений и важно при реализации связанной очереди.
Связанный список предоставляет последовательный доступ к элементам Данных и использует указатель для задания текущего положения прохождения в списке. Наш связанный список содержит указатель currPtr для ссылки на это текущее положение в терминах его места в списке. Первый элемент в списке имеет позицию (position) О со следующим элементом в позиции 1 и так далее. Количество элементов в связанном списке содержится в переменной size. Это Дозволяет нам определять пустой список или возвращать счетчик количества ^элементов в списке. В следующем списке текущим положением является элемент со значением 90, расположенный в позиции 3:
Адрес prevPtr используется для вставки и удаления узла в текущем положении с использованием Node-методов IneertAfter и DeleteAfter. Например,
” следующем связанном списке показана вставка с использованием Node, на который ссылается prevPtr:
Insertion: prevPtr->InsertAfter(р)
До
Узел, на который ссылается prevPtr, также используется при удалени. элемента из связанного списка:
Deletion:prevPtr->DeleteAfter(р)
Операции связанных списков
Пользователь должен иметь возможность переходить от элемента к элементу в списке. Простой метод с именем Next передвигает текущее значение к следующему узлу. В текущем положении операция Data возвращает ссылку на поле данных узла. Это дает возможность выполнять выборку или изменять поле данных узла без необходимости понимания пользователем того, как эти данные сохраняются классом Node. Для иллюстрации этих операций предположим, что L является связанным списком целых, чье текущее положение находится в узле со значением данных 65. Следующие операторы изменяют значение текущего узла на 67 и значение следующего узла — на 100:
Linkedlist<int> L;
if (L.Data() < 70)
L.Data () = 67;
L-Next();
L-Dataf) • 100;
//сравнение значения текущего узла с 70
//если меньше, присваивание значения 67
//продвижение текущего положения к следующему узлу-
//вставляет 100 в новое положение
До	После
currPtr	currPtr
« приложениях нам иногда необходимо устанаклч^
Наделенное место в списке. Это выполняет метод Reset. Он принимает oPLreTP Pos и перемещает текущее положение списка в эту позицию. Вна-w по умолчанию параметра pos является 0, которое устанавливает те-положение на первый элемент списка. От него приложение может <^5ЯТ)овать узлы, используя Next. Сканирование списка завершается, когда >1Свие EndOfList является равным TRUE. Например, простой цикл выводит ^менты в списке. Перед сканированием списка мы сначала проверяем ус-^^ие ListEmpty (пустой список):
j,; Reset () »*	// установка currPtr в положение первого // элемента списка
if (j,.ListEmpty О )	// пуст ли список?
cout « "Empty list\n";	
els®	
while(!L.EndofList())	// сканирование до конца списка
<	
cout « L.DataO « endl.-	// вывод значения данных
L.NextO;	// перемещение к следующему узлу
Пользователь имеет доступ к текущей позиции списка при помощи метода CurrentPosition. Метод Reset может применяться для возвращения указателей списка в исходное положение. Эта возможность используется в таких задачах, как нахождение максимального значения в списке и установка списка на этот узел для последующего удаления или вставки.
//сохранение текущей позиции
int currPos = L.CurrentPosition();
<комлнды, которые сканируют список вправо от currPos>
// переустановка текущего положения в
// бывшую позицию currPos.
L-Reset (currPos) ;
Добавление и удаление узлов являются основными операциями в связанном списке. Эти операции могут выполняться в начале и хвосте списка или в текущем положении.
Операции вставки. Операции вставки создают новый узел с новым полем Д^ных. Затем узел помещается в список в текущее положение или непо-4 сРедственно после него.
Операция InsertAfter помещает новый узел после текущей позиции и присваивает currPtr новому узлу. Эта операция служит той же цели, что и InsertAfter в классе Node.
Метод InsertAfter помещает новый узел в текущее положение. Новый v л Помещается непосредственно перед текущим узлом. Текущая позиция ^наливается на новый узел. Эта операция используется при создании РяДоченного списка.
дд^асс Упорядоченных списков имеет операции Insertfront и InsertRear НаьЛ2^аВления новых Узлов в голову и в хвост списка. Эти операции уста-^Ивают текущую позицию иа новый узел.
УДаления- Операции удаления удаляют узел из списка. DeleteAt Явг Узел в текущей позиции, a DeleteFront удаляет первый узел в списке.
Пример: Linked List<int> L;
L.InsertFront(100);
L.InsertAfter(200);
L.InsertAt(300);
L.InsertRear(SO);
L.Reset(1) ;
L.DeleteAt();
L.DeleteAt ();
L.DeleteAt();
// список содержит 100
// список содержит 100 200
// список содержит 1бо 300 200
//'список содержит 100 300 200 50
// установка currPtr на 300
// список содержит 100 200 50
// список содержит 100 50
// список содержит 100;
Другие методы. Класс связанных списков создает динамические данные поэтому он должен иметь конструктор копирования, деструктор и перегру* женный оператор присваивания. Пользователь может явно очистить список с помощью оператора ClearList.
9.4. Класс LinkedList
В данном разделе представлен класс LinkedList как простой, но мощный инструмент для динамической обработки списков. Основное внимание уделяется спецификации класса и примерам программ, иллюстрирующим его использование. Объявление класса и его реализация находятся в файле link.h.
Спецификация класса LinkedList
О is ЬЯВЛ кН НЕ ^include <iostream.h> ^include <stdlib.h> tinclude ‘’node.h"
template <class T> class LinkedList (
private:
// указатели для доступа к началу и концу списка Node<T> *front, *rear;
// используются для извлечения, вставки и удаления данных Node<T> *prevPtr, *currPtr;
// число элементов в списке int size;
// положение в списке, используется методом Reset int position;
Il закрытые методы создания и удаления узлов Node<T> *GetNode(const Т& item,Node<T> *ptrNext=NULL); void FreeNode(Node<T> *p);
// копирует список L в текущий список void CopyList(const LinkedList<T>& L) ;
public: // конструктор LinkedList(void); LinkedList(const LinkedList<T>S L);
// деструктор
-LinkedList(void);
// оператор присваивания
IjinkedList<T>& operator = (const LxnkedList<T>& L):
Ц проверка состояния списка
int ListSize(void) const;
int ListEmpty(void) const;
// методы прохождения списка
void Reset(int pos 0} ;
void Next(void);
int EndOfList(void) const;
jnt Currentposition(void) const;
Il методы вставки
void InsertFront(const T& item);
void InsertRear(const T& item);
void InsertAt(const T& item);
void insertAfter(const T& item);
jI метода удаления
T peleteFront(void);
void DeleteAt(void);
// возвратить/изменить данные Tb Data(void);
// очистка списка
void ClearList(void);
1;
ОБСУЖДЕНИЕ
Класс использует динамическую память, поэтому ему необходимы конструктор копирования, деструктор и перегруженный оператор присваивания. Закрытые методы GetNode и FreeNode выполняют все выделения памяти для этого класса. Если при выделении памяти происходит ошибка, метод GetNode завершает программу.
Класс поддерживает размер списка, доступ к которому обеспечивается использованием методов ListSize и ListEmpty.
Закрытые данные-члены currPtr и prevPtr поддерживают запись текущего положения при прохождении списка. Методы вставки и удаления отвечают за изменение этих значений после выполнения операции. Метод Reset явно аадает значение currPtr и prevPtr.
Класс содержит гибкие методы прохождения, Reset принимает позиционный Параметр и устанавливает текущее положение в эту позицию. Он имеет раметр по умолчанию 0 для того, чтобы при его использовании без аргу-ГД&ТОБ’ мет°Д устанавливал текущую позицию в голову списка. Метод Next вдвигается к следующему узлу списка, a EndOfList указывает, был ли uQ ^И^нут конец списка. Например, для списка L цикл FOR осуществляет леДовательное сканирование списка.
tot n D
<ПО Reset(,; !Ь..EndOfList о ; L.NextO)
синение текущего положения:*
^Position возвращает текущее положение при прохождении списка. ПосеЩения текущего узла позже сохраните возвращаемое значение и еДствии передайте его как параметр методу Reset.
Вставки могут производиться в любом конце списка с использовав 1 InsertFront и InsertRear. InsertAt вставляет новый узел в текущую позит 1 в списке, a InsertAfter вставляет узел после текущей позиции. Если текут 1 положение находится в конце списка (EndOfList == True), то и InsertAt I InsertAfter помещают новый узел в хвост списка.	’
DeleteFront удаляет первый элемент из списка, a DeleteAt удаляет в текущей позиции списка. Для любого из этих двух методов попытка ления из пустого списка завершает программу.
Узе^| УДа.
Метод Data используется для чтения или изменения данных в текут к позиции в списке. Так как Data возвращает ссылку на данные в узле Л может использоваться в правой или левой части оператора присваивания
// выборка данного из текущего узла и увеличение его // значения на 5
L.DataO “ L.DataO + 5;
ClearList удаляет все узлы списка и помечает список как пустой.
ПРИМЕР
LinkedList<int> L, К; // объявление списков целых L, К // добавление 25 целых к этим спискам for(i-0; i<25; i++) ( cin >> num;
L.InsertRear(num);	// узлы вставляются в порядке ввода
К.InsertFront(num); 11 узлы в порядке, обратном вводу } // сканирует список L, изменяя каждый узел на его // абсолютное значение
forCL.Reset()s 1L.EndOfList();L.Next())
// если данное отрицательное, сохранение нового значения if (L.DataO < 0)
L.DataO----L.DataO;
к.InsertFront(100);	// сохранение 100 в начале к
К.InsertAfter(200); //вставка 200 после 100
К. InsertAt (150);	//вставка 150 между 100 и 200
// вывод списка L
void PrintList(LinkedList<int>£ L)
{
// перемещение в голову списка L и прохождение списка с
// выводом каждого элемента
for(L.Reset О; !L.EndOfList(); L.NextO)
cout « L.DataO « "
Перед реализацией класса LinkedList мы иллюстрируем его основные возможности в серии примеров. Функция конкатенации объединяет два списк®» присоединяя второй список к хвосту первого. Второй пример реализует ве₽' сию сортировки выбором связанного списка. Классическая версия алгорИт сортирует массив на месте. В нашей реализации широко используются вСТЙ ки и удаления со связанными списками. Раздел завершается алгорит#0 * который удаляет узлы-дубликаты из списка.

Конкатенация двух списков
функция конкатенирует два списка, выполняя упорядочение узлов во ФУМ списке и вставляя каждое из его значений в хвост первого списка, ^^кпия используется в законченной программе concat.cpp в программном So»eHIIIT-
^ФУНКЦИЯ ConcatLists сканирует второй список и методом Data извлекает Дкдого узла значение данных, которое затем используется для добавления
03 «го узла в хвост первого списка с использованием метода InsertRear. gOBOA V j
late cclass T>
добавление списка L2 в конец LI
id ConcatLists(LrnkedList<T>& LI, LinkedList<T>& L2)
I // переустановка обоих списков на начало
Ы.Reset(J;
Ь2.Reset ();
// прохождение L2. вставка каждого значения данных в
И ХВОСТ Ы
while (! L2.EndOfLis t())
(
Ы. InsertRear(L2.Data());
L2.Next();
}
I
Сортировка списка
Мы реализуем версию сортировки выбором для связанного списка, используя два отдельных связанных списка. Первый список L содержит неотсортированное множество данных. Второй список К создается как копия списка L со значениями в отсортированном порядке. Алгоритм удаляет элементы из списка L в порядке от наибольшего до наименьшего элемента и вставляет их в начало списка К, который становится упорядоченным списком.
Сортировка выбором требует многократного сканирования списка. Мы используем функцию FindMax для сканирования списка и установки текущей позиции на максимальный элемент. После выборки значения данных из этой позиции вставляем новый узел с этим значением в начало списка К, используя r°nt’ И затем Удаляем этот максимальный узел из списка L, используя
Рассмотрим следующий пример:
Список L содержит элементы 57, 40, 74, 20, 62
1: Находим максимальное значение 74. Удаляем из Ьи добавляем в К.
Шаг 2: Находим максимальное значение 62. Удаляем из L и Добавляем ь '
Программа 9.5. Сортировка списков выбором	j
Эта программа создает список L с 10 случайными числами в диапазон! от 0 до 99. После вывода начального списка функцией PrintList мы и! пользуем алгоритм сортировки выбором для переноса элементов из списн L в список К. В конце программы снова вызывается PrintList для вывод? значений из К, который содержит элементы в возрастающем порядке. |
«include <iostream.h>
«pragma hdrstop
«include "link.h"
«include "random.h"
// установка L на его максимальный элемент
template <class T>
void FindMax(LinkedList<T> &L) {
if (L.ListEmptyО)
cerr « "FindMax: Список пустой!" « endl; return;
}
// вернуться на начало списка
L.Reset();
// записать первое значение списка как текущий максимум // с нулевой позицией
Т max  L.Data();	’
int maxLoc =0;
// перейти к следующему узлу и сканировать список for (L.Next О; !L. EndOfList О; L.KextO)
if (L.Data() > max)
(
If новый максимум, записать значение и // положение в списке max = L.Data();
maxLoc “ L-CurrentPosition!);
}
L.Reset(maxLoc);
)
If печатать список L template <class T>
void
PrintList(LinkedList<T>b L)
// перейти к началу списка L.
// проходить список и печатать данные каждого узла for(L.Reset О ; ! L.EndOfList О ! L.NextO )
cout « L.DataO « ”
>
void main (void)
1	// список L размещается в сортированном порядке в списке К
LinkedList<int> L, К;
RandomNumber rnd;
int i;
ц Ъ - это список из 10-ти случайных целых в диапазоне 0-99 for(i“0; i < 10; i++)
L.InsertRear(rnd.Random(100));
cout « "Исходный список:
PrintList(L);
cout « endl;
// удалить данные из L, вставляя их в К while {‘L.ListEmpty())
{
II найти максимум оставшихся элементов FindMax(L);
// вставить максимум в начало К и удалить его из L К.InsertFront(L.Data());
L.DeleteAt О ; }
cout « "отсортированный список: PrintList(К);
cout « endl;
/*
выполнение программы 9.5>
Исходный список: 82 72 62 3 85 33 58 50 91 26
Отсортированный список: 3 26 33 50 58 62 72 82 85 91
А	-------------------------------------------------------------
Удаление дубликатов. Алгоритм удаления дубликатов в списке является ^тересным приложением класса LinkedList. После создания списка L начи-Ваем сканирование его узлов. В каждом узле записываем позицию узла и ег° значение данных. Это дает нам ключ для начала поиска дубликатов в ОставП1емся
списке, а также — позицию для возвращения после удаления УОДикатов. От текущей позиции сканируем хвост списка, удаляя все узлы, данных которых совпадает с ключом, затем переустанавливаем те-
УЩее положение сканирования в позицию исходного значения и переходим на один узел для продолжения этого процесса.
текущая позиция
новая текущая позиция
Программа 9.6. Удаление дубликатов
В этой программе используется алгоритм удаления дубликатов. Начз ный список имеет 15 случайных значений в диапазоне 1—7. После выво? списка вызываем функцию RemoveDuplicates, удаляющую дубликаты •» списка. Получаемый в результате список выводится с использование функции PrintList, которая включена в link.h.
flinclude «iostream.h> flpragma hdrstop
flinclude "link.h” flinclude "random.h"
// печать списка L template «class T> void PrintList(LinkedList<T>& L)
{
// перейти к началу списка L.
// прохождение списка и печать каждого элемента
for (L. Reset (J ; ! L - EndOfList () ; L.NextQ) cout « L.Datat) « "
}
void RemoveDuplicates(LinkedList<int>& L) (
// текущее положение в списке и значение данных int currPos, currvalue;
11 перейти к началу списка L.Reset();
// цикл по списку while(!L.EndOfList()) {
// записать данные текущего положения в списке // и это положение currValue = L-Data();
currPos  L.CurrentPositionlJ;
// перейти к узлу справа L.Next О;
// двигаться вперед до конца списка, удаляя все появления currvalue while(! L.EndOfList())
// если узел удален, текущее положение — это следующий узел if (L.Data О == currvalue)
L.DeleteAtd ; else
L.Next();	// перейти к следующему узлу
If перейти к первому узлу со значением currvalue. // идти вперед L.Reset(currPos);
L-Next О;
}
I void main (void)
1 LxnkedList<int> L; int i;
RandomNumber rnd;
ff вставить 15 случайных целых в диапазоне 1-7 и печатать список for(i=0; i < 15; i++)
L.InsertRear(1+rnd.Random(7));
cout « " Исходный список:
PrintList CL); cout « endl;
ff удалить все значения-дубликаты и печатать новый список RemoveDuplicates(L);
cout « " Окончательный список:
PrintList(L); cout « endl; } /* «Выполнение программы 9. 6>
„ Исходный список: 177151272166364 Окончательный список: 1 7 5 2 6 3 4 */
9.5.	Реализация класса LinkedList
Спецификация класса LinkedList ссылается на класс Node. Реализация класса LinkedList использует много методов, описанных разделе 9.1 с классом оае. Алгоритмы, используюгцемые функциями в node lib. h, являются основой Разработки класса LinkedList. Однако мы должны знать о дополнительной * ^®ости поддержания указателей front и rear, которые определяют доступ Указателей currPtr и prevPtr, которые сохраняют информацию о УЩем положении прохождения, местонахождения данных и size (размера Ьогл Методы LinkedList отвечают за изменение этих данных всякий раз, д изменяется состояние списка.
о
*1^ Илл*1™® Данные-члены. Класс ограничивает доступ к данным, так как со^Рмация ^пользуется только функциями-членами. Связанный список сЛйскй Й3 Мн°жества объектов Node, связанных вместе от начала до хвоста 6°Лее&’ определяем начало или голову списка как данные-члены. Для Легкого выполнения вставок в хвост списка этот класс поддерживает
указатель rear на последний узел списка. Это избавляет от сканировав 1 всего списка для нахождения положения хвоста. Переменная size содеп«.| количество узлов в списке. Это значение исп9льзуется для определения I ляется ли список пустым, и для возвращения количества значений данц-Д в списке. Переменная position делает более легкой переустановку текуп 1 положения при прохождении списка методом Reset.	j
Объект LinkedList содержит два указателя, определяющих теки! 1 (currPtr) и предыдущее положение (prevPtr) в списке. Указатель currpl ссылается на текущий узел списка и используется методом Data и метол J вставки InsertAfter. Указатель prevPtr используется методами DeleteAt I InsertAt, действующими для текущего положения. При выполнении встВДо.| и удалений этот класс изменяет поля front, rear, position и size объект! списка.
Данные класса LinkedList
Методы выделения памяти. Класс выполняет все вставки и удаления Конструктор копирования и методы вставки выделяют узлы, тогда как Clear list и методы удаления уничтожают узлы. Класс LinkedList мог бы исполъ зовать операторы new и delete непосредственно в этих методах. Однако функции GetNode и FreeNode предоставляют более структурированный доступ дл управления памятью.
Метод GetNode делает попытку динамического создания узла с передава емыми ему значением данных и полем указателя. Если выделение намята происходит без ошибки, он возвращает указатель на новый узел; иначе, печатается сообщение об ошибке и программа завершается. Метод FreeNode просто удаляет память, занятую узлом.
Конструкторы и деструктор. Конструктор создает пустой список со всемк значениями указателей, установленными на NULL. В этом начальном состоянии size устанавливается на 0, а значение position — на -1:
// создать пустой список template cclass Т> LinkedList<T>::LinkedList(void): front(NULL), rear(NULL), prevPtr(NULL),currPtr(NULL), size(O), position(-1)
L Конструктор копирования и оператор присваивания копируют
класса LinkedList. Для этой цели класс реализует метод CopyList, проходяш по списку L и вставляющий каждое значение данных в хвост теК^1Дй^ списка. Эта закрытая функция вызывается только тогда, когда текУ°* список пуст. Она назначает параметры прохождения prevPtr, currPtr И sition так, чтобы текущий список был той же конфигурации, что и L. Таким образом, два эти списка имеют одно и то же состояние прохоЖД-после присваивания или инициализации.
„«повать L в текущий список (предполагается пустым)
// ^ate <class т>
in kedList<T>: :CopyList (const LinkedList<T>& L) f£>id b
{	. p _ указатель на L
*P - L.front;
int pOS'
вставлять каждый элемент из L в конец текущего объекта
„bile <Р !-
* InsertRear<p->data).* р =. p->NextNodeО;
)
// выход, если список пустой
if (position “ -1) return?
// переустановить prevPtr и currPtr в новом списке
prevPtr - NULL;
currPtr я front;
for (pos  0; pos != position; pos++)
{
prevPtr - currPtr;
currPtr = curr₽tr->NextNode();
}
I
ClearList проходит связанный список и уничтожает все узлы, используя алгоритм, разработанный в разделе 9.1. Деструктор реализуется простым вызовом Clearlist.
template <class Т>
void LinkedList<T>::ClearList(void) (
Node«T> *currPosition, *nextPosition;
currPosition - front;
while (currPosition !" NULL) (
// получить адрес следующего узла и удалить текущий nextPosition - currPosition->NextNode();
FreeNode(currPosition);
currPosition - nextPosition; // перейти к следующему узлу
front =» rear = NULL;
PrevPtr * CurrPtr - NULL;
size - 0;
} Position - -1;
w°AM прохождения списка. Reset устанавливает текущее положение про-Нето НИЯ в п<,а>ЩИЮ, обозначенную параметром pos. В то же время, этот в ®аменяет положение как currPtr, так и prevPtr. Если pos не находится .size-l, то выводится сообщение об ошибке и программа за-когдц614151, установки currPtr и prevPtr функция различает случаи, является головой списка и внутренней позицией в списке.
Уст&Во'°*в Переустановка текущего положения на начало списка путем prevPtr на NULL, currPtr — на front и position — на 0.
pos! - 0: Так как случай, когда роз == О уже рассматривался, мы мохД предположить, что значение pos должно быть больше О и что проход списка должно устанавливаться	на	внутреннюю	позицию.	Для	измени	I
позиции currPtr начинаем во втором	узле	списка	и	перемещаемся	к	г	j
жению pos.	|
template <class Т>	1
void LinkedList<T>::Reset[int pos)	J
{	I
int StartPos;	I
// если список пустой,	выход	I
if (front -= NULL)	I
return;	I
// если положение задано не	верно, закончить программу	I
if (роз <0|| pos	>	size-1)	I
{
cerr « "Reset:	Неверно	задано положение: " « pos	1
« endl;	1
return;	I
)
// установить механизм прохождения в pos
if(pos =- 0)	I
<	I
// перейти в	начало списка	1
prevPtr = NULL; currPtr = front;
position = 0;	I
}	I
else	j
// переустановить currPtr, prevPtr,	и	startPos	I
(	I
currPtr = froot->NextNode();	I
prevPtr = front;	I
startPos =1;	I
// передвигаться вправо до pos	I
for(position=startPos; position	!=	pos;	position++)	1
{	I
// передвинуть оба указателя	прохождения вперед	I
prevPtr = currPtr;	I
currPtr - currPtr->NextNode();	I
} } }
Для последовательного сканирования списка мы перемещаемся от элемеЯ'П к элементу, выполняя метод Next. Функция перемещает prevPtr к текуЩ^У узлу, a currPtr — на один узел вперед. Если мы прошли все узлы в списка переменная position имеет значение size, и currPtr устанавливается на КиьИ
// переустановить prevPtr и currPtr вперед на один узел template <class Т>
void LinkedList<T>::Next(void) I
// выйти, если конец списка или
// список пустой if (currPtr !» NULL) {
// переустановить два указателя на один узел вперед prevPtr “ currPtr;
currPtr - currPtr->NextNodeО; position++;
)
1
Постул к данным. Используйте метод Data для доступа к значению данных височном узле. Если список пуст или прохождение достигло конца списка, ®С дится сообщение об ошибке и программа завершается; иначе, Data воз-currPtr->data.
вратить ссылку на данные текущего узла
М «late <class Т>
£ UnkedList<T>: :Data(void)
1 // ошибка, если список пустой или прохождение закончено
if (size e= О 11 currPtr =- NULL)
cerr « "Data: Неверная ссылка!" « endl;
exit(1);
return currPtr->data;
Методы вставки для списка. Класс LinkedList имеет ряд операций для добавления узла в начало или хвост списка (InsertFront, InsertRear) или относительно текущей позиции (InsertAt и InsertAfter). Методам вставки передается значение данных, используемое для инициализации поля данных нового узла.
InsertAt вставляет узел со значением данных в текущую позицию в списке. Метод использует GetNode для выделения узла со значением данных item, имеющим адрес newNode, увеличивает размер списка и устанавливает текущее положение на новый узел. Алгоритм должен обрабатывать два случая. Если вставка имеет место в начале списка (prevPtr === NULL), обновляется front для указания на новый узел. Если вставка имеет место внутри списка, новый узел помещается после prevPtr Node-методом InsertAfter. Если элемент вставляется в пустой список или в хвост непустого списка, должна выполняться специальная обработка указателя rear.
// вставка item в текущую позицию списка template cclass т>
LinkedList<T>:: InsertAt (const T& item)
Hode<T> * newNode;
Два случая: вставка в начало или внутрь списка
JX (prevPtr — NULL)
// вставка в начало списка, помещает также
' Узел в пустой список
NewNode - GetNode (item, front);
j r°nt - newNode;
®lae (
n₽ Вставка внутрь списка, помещает узел после prevPtr vNode - GetNode(item);
) ev?tr->insertAfter(newNode);
13
InsertAt (пустой список)
// при prevPtr «» rear, имеем вставку в пустой список
// или в хвост непустого списка; обновляет rear и position if (prevPtr === rear) /
rear = newNode;
position = size;
}
// обновить currPtr и увеличить size currPtr = newNode;
size++;
Методы удаления. Для удаления узла имеются две операции, которые удаляют узел из начала списка (DeleteFront) илн из текущей позиий» (DeleteAt).
DeleteAt удаляет узел с адресом currPtr. Если currPtr является равны NULL, то список пуст, нли весь список уже пройден. В этом случае фу«кй . выводит сообщение об ошибке и завершает программу. Иначе, алгоритм рабатывает два случая. Если удаляется первый узел списка (prev^. ==NULL), изменяется указатель front. Если это последний узел списка, fr° становится равным NULL. Второй случай имеет место при prevPtr/NULk’ удаляемый узел находится внутри списка. Используйте метод DeleteA1 класса Node для отсоединения от списка узла, следующего за prevPtr-
vaK и в случае с методом InsertAfter, следует обратить осо.™
«казатель rear. Если узел, который мы удаляем, находится в хвосте списка e= rear), то новым хвостом теперь будет являться prevPtr, значение ^°ition. уменьшается, a currPtr становится равным NULL. Во всех других Р^-аЯХ position остается без изменения. Если мы удаляем последний узел €^г'Ч«ске, rear становится равным NULL, а значение position изменяется с 5	—1 Вызываем FreeNode для удаления узла из памяти и уменьшаем
О В»
-азмер списка.
DeleteAt (список становится пустым)
DeleteAt (currPtr - это rear)
J Удаление узла в текущей позиции списка template <class т> void LinkedList<T>::DeleteAt(void)
Node<T> *p;
// ошибка, если список пустой или коней списка (currPtr -= NULL)
cerr « "Ошибка удаления!" « endl;
, exit(l);
И if
Удалаять можно только в начале и внутри списка (prevPtr -= NULL)
П сохранить адрес начала, но ие связывать его. если это -// последний узел, присвоить front значение NULL р = front}	t
front = front->NextNode();
} else
//не связывать внутренний узел после prevFtr.
// запомнить адрес
р = prevPtr->DeleteAfter();
// если хвост удален, адрес нового хвоста в prevPtr,
// a position уменьшается на 1
if (р == rear) {
rear = prevPtr; position—;
// установить currPtr на последний удаленный узел.
// если р - последний узел в списке,
// currPtr становится равным NULL currPtr = p->NextNode();
// освободить узел и уменьшить значение size
FreeNode(р);
size—;
9.6. Реализация коллекций со связанными списками
До этого раздела в книге мы использовали основанные на массиве реализации классов Stack, Queue и SeqList. В каждом случае список элементов сохраняется в массиве, который определяется как закрытый данное-член этого класса. В этой главе разрабатывается класс LinkedList, который предоставляет мощную структуру динамической памяти вместе с разнообразными методами добавления и удаления элементов и обновления значений данных-Сохраняя элементы в объекте связанного списка, а не в массиве, мы получаем новую стратегию реализации, которая увеличивает возможности н эффективность базовых списочных классов. Используя различные методы в классе LinkedList, мы имеем инструменты для простой и понятной реализации операций stack, queue и list.
В данном разделе разрабатываются новые реализации для классов Queue и SeqList с использованием связанных списков. Для класса SeqList мы сравниваем эффективности динамического хранения связанного списка и списку основанного на массиве. Класс Queue используется в следующем разделе Щ’ разработке задачи моделирования буферизации печати (printer spooler)- * лизация связанного списка класса Stack опущена как самостоятельное У ражнение.
Связанные очереди
✓чл^ект LinkedList является гибкой структурой памяти для сохранения ека элементов. Класс Queue предоставляет простую реализацию очереди, ^ольэуя композицию (структуру) для включения объекта LinkedList. Этот й ркт производит Queue-операцнн, выполняя эквивалентные операции • kedList. Например, объект LinkedList позволяет выполнять вставку эле-в хвост списка (InsertRear) и удаление элемента из головы списка ^gleteFront). Переустанавливая текущий указатель на голову списка (Reset), Sj можем определить операцию Qfront, извлекающую значение данных из д0ВЫ списка. Другие Queue-методы оценивают состояние списка — задача, Гдравляемая списочными операциями ListEmpty и ListSize. Очередь очищайся простым вызовом списочного метода ClearList.
Спецификация класса Queue (с использованием объекта LinkedList)
ОБЪЯВЛЕНИЯ
linclude <iostream.h>
linclude <stdlib.h>
^include "link.h"
template <class T> class Queue
<
private:
// объект связанного списка
// для хранения элементов очереди
LinkedList<T> queueList;
public:
// конструктор Queue(void);
// методы обновления
void QInsert(const Tb elt); T QDelete(void);
// доступ к очереди T QFront(void);
// методы тестирования
int QLength(void) const;
int QEmpty(void) const;
void QClear(void);
01й1садиЕ
бъект queueList класса LinkedList содержит элементы очереди. Он предо-«аляет полный набор операций связанного списка, которые используются * Реализации открытых методов Queue.
И .”^acc Queue не имеет собственных деструктора, конструктора копирования еРатора присваивания. Эти методы не являются обязательными, так как
8 а-Р®алисзУются Для объекта queueList. Компилятор реализует присваивание р0б7а^Иализацню, выполняя оператор присваивания или конструктор копи-ДЛя °^ъекта тапа queueList. Деструктор для queueList вызывается м&тически при уничтожении объекта Queue.
J
Так как элементы сохраняются в связанном списке, размер очереди 1 ограничивается константой реализации, такой как MaxQueueSize
ПРИМЕР	*
Queue<int> Q1,Q2; QI.Qlnsert(10); QI.Qlnsert(50); cout « QI.QFront(); Q2 = QI; Ql.QClearf);	// объявление двух очередей целых значений // добавление 10, а затем 50 в Q1 // вывод значения 10 в начале очереди // использование оператора = для queueList // очистка очереди и освобождение памяти
Реализация связанного списка класса Queue находится в файле queueh
Реализация методов Queue
Для иллюстрации реализации методов Queue мы определяем методы мо дификации очереди Qlnsert и QDelete, а также метод доступа QFront. Каждый из них непосредственно вызывает эквивалентный метод LinkedList.
Операция Qlnsert добавляет элемент в хвост очереди, используя Linkedljst операцию InsertRear.
// LinkedList-метод вставляет элемент в хвост template*class Т>
void Queue<T>::Qlnsert(const T& elt) (
queueList.InsertRear(elt);
} •
QDelete сначала проверяет состояние очереди и завершает программу при пустом списке. Иначе, при помощи списочной операции DeleteFront выполняется операция отсоединения первого элемента от очереди, удаления памяти и возврата значения данных.
// LinkedList-метод DeleteFront удаляет элемент из // головы очереди
Т Queue<T>::QDelete(void) (
// проверка, пустой ли список, и завершение, если — да
if (queueList-ListEmpty()) <
cerr « "Qdelete вызвана для пустой очереди'" « endl;
exit(1);
)
return queueList.DeleteFront(); )
Операция QFront выполняет выборку значения данных из первого элемента queueList. Для этого требуется изменение позиции текущего указателя на голову списка и считывание его значения данных. Попытка вызвать эту функцию при пустой очереди вызывает сообщение об ошибке и завершен116 программы.
// возвратить значение данных первого элемента в очереди template <class т>
Т Queue<T>i:QFront(void) (
// если очередь пуста, завершить программу
if (queueList.ListEmpty())
cerr « "QFront вызвана для пустой очереди!" « endl; exit(l);
)
// перенастРои'гься на голову очереди и возвратить данные L^gueList.Reset();
Return queueList.Data();
Использование объекта LinkedList с классом SeqList
Класс SeqList определяет ограниченную структуру памяти, позволяющую -являть элементы только в хвост списка и удаляющую только первый ® eWegT в списке или элемент, который совпадает с ключом. Пользователь 9«еет возможность доступа к данным в списке с использованием метода Find йди с помощью позиционного индекса для чтения значения данных в узле. Как в случае со связанной очередью, мы можем использовать объект LinkedList для сохранения данных при реализации класса SeqList. Более этот объект предоставляет мощный набор операций для реализации летодов класса.
Спецификация класса SeqList
ОБЪЯВЛЕНИЕ
^include <iostream.h>
Hnclude <stdlib.h>
iinclude ’‘link.h”
template <class T> class SeqList (
private:
// объект связанного списка LinkedList<T> Hist;
public:
11 конструктор SeqList(void);
// методы доступа
int ListSize(void) const; int ListEmpty(voxd) const; int Find (Tb item) ;
T GetData(int pos);
// методы модификации
void Insert(const T& item); void Delete (const T& item); T DeleteFront(void);
I void ClearList (void);
Описание
Маст еТ°ДЬ1 этого класса идентичны методам, определенным для основанной на
Иве Версии в файле aseqlist.h. Нет необходимости определять деструктор, Исцо^УКтоР копирования и оператор присваивания. Компилятор создает их, йй*^ЛЬзУя соответствующие операции в классе LinkedList. Класс находится в
ЙЛе pqlistl,h.
e<IList<int>chLxst;
// выделение динамического списка целых
chList.Insert(40);	// добавление 40 в коней списка
cout « chList.DeleteFront () « endl; // вывод значения 40
Реализация методов доступа к данным класса SeqList
Класс SeqList дает возможность пользователю иметь доступ к данным ключу, с помощью метода Find, или по позиции в списке. В первом слу^0 для сканирования списка и поиска ключа используется механизм прохг/* денйя класса LinkedList.
// использовать item как ключ для поиска в списке.
// возвратить True, если элемент — в списке, и — False Z/в противном случае.
template <class Т>
int SeqList<T>:;Find (T& item)
(
int result » 0;
11 поиск item в списке. если — найден, result - True
for (Hist. Reset ();! Hist. EndOfList (); llist. Next ())
if (item == llist.Data()) { result++;
break:
}
// если result равно True, обновить item и возвратить True;
// иначе — возвратить False
if (result)
item = llist.Data();
return result;
}
Метод GetData используется для доступа к элементу данных по его положению в списке. Здесь используется LinkedList-метод Reset для установки механизма прохождения в нужное положение в списке и выполняется метод Data для извлечения данных:
// возвратить значение данных элемента в положении pos template <class Т>
Т SeqList<T>::GetData(int pos) {
// контроль правильности pos
if (pos <011 pos >» llist.Listsize()) (
cerr « "pos вне диапазона!" « endl;
exit(1);
)
// установить текущее положение связанного
// списка в pos и возвратить значение данных
llist.Reset(pos);
return llist.Data();
Приложение: Сравнение реализаций SeqList
Основанная на массиве версия класса SeqList требует значительных Ус,яЛ^ для удаления элемента, так как все элементы, находящиеся в хвосте спйс должны быть сдвинуты влево. В случае с реализацией связанного спис1<в
операции имеют место с простым отсоединением
Дядин результата использования структуры памяти для хранения связан-’Г^сдиска сравним версию, основанную на массиве, и версию связанного -оГ° класса SeqList. После создания начального списка с 500 членами при ^яоовании неоднократно удаляется элемент из головы списка и затем 0С2ляется элемент в хвост списка. Процесс повторяется 50 000 раз и пред-ялет собой наихудший случай для объекта основанного на массиве класса
5Мы выполняем две эквивалентные программы на одной и той же ком-ютерной системе и вычисляем время выполнения 50 000 операций встав-j^/удаления в секундах.
Программа 9.7а. (Класс List — реализация массива)
Эта задача тестирования использует основанный на массиве класс SeqList, находящийся в файле aseqllst.h. Только для этой программы константа ARRAYSIZE изменяется так, чтобы список мог содержать 500 элементов. Задача выполняется за 55 секунд.
^include <iostream.b>
Ipragma hdrstop
// DataType = int (список храни» целые значения) typedef int DataType;
// включение класса SeqList
linclude "aseqlist.h"
void main (void)
{
ll список c 500 целыми
SeqList L;
long i;
// инициализация списка значениями 0 .. 499
for (i = 0; i < 500; i++)
L.Insert(int(i));
// выполнение операций удаления/вставки 50000 раз cout « " Начало программы!" « endl;
for (i « 1; i <» 50000L; i++)
(
L.DeleteFront();
L.Insert(0);
}
I cout « ' программа выполнена! « endl;
I*
"^Выполнение программы 9.7a>
Начало программы!
Р°грамма выполнена! // 55 секунд
*/
Программа 9.7b. (Класс List — реализация связанного списка)	!
Эта программа тестирует версию связанного списка класса SeqList 1 дящегося в'файле seqlistl.h. Задача выполняется за 4 секунды! ’
tfinclude <iostream.h>
tfpragma hdrstop
// включить реализацию связанного списка (SeqList)
#include "seqlistl.h"
void main(void) {
// определить список целых
SeqList<int> L;
long i ;
// инициализировать список значениями 0 .. 499 for (i = 0; i < 500; i++)
L. Insert(int(i));
// выполнение операций удаления/вставки 50000 раз cout « ” Начало программы!" « endl;
for (i = 1; i <= 50000L; i++)
(
L.DeleteFront();
L.Insert(0);
}
cout « " Программа выполнена!" « endl;
/•
выполнение программы 9.7b>
Начало программы!
Программа выполнена! // 4 секунды
*/
9.7. Исследовательская задача: Буферизация печати
Очереди используются для реализации систем буферизации печати в операционных системах. Система буферизации (буферизатор) печати принимает запрос на печать и вставляет файл, который должен печататься, в очередь. При освобождении принтера буферизатор удаляет задание из очереди и чатает этот файл. Действие буфаризатора позволяет осуществлять печать в» фоне выполнения пользователями других задач на переднем плане.
Анализ проблемы
В этой задаче разрабатывается класс Spooler, операции которого моДе руют ситуацию добавления пользователем новых заданий в очередь печ и проверки статуса заданий, уже имеющихся в очереди. Задание на печать это структура, содержащая целый номер задания, имя файла и число страВ
printJob
I .nt nurnber;
nar filename [20];
int pagesize;
H	л
я задаче моделирования подразумевается, что принтер работает непрерыв-со скоростью 8 страниц в минуту.
00 Следующая таблица иллюстрирует ситуацию, когда пользователь направ-т в систему буферизации три задания на печать.
ляет
Задание	Имя	Количество страниц
45	Тезисы	70
6	Письмо	5
70	Записи	20
Пользователь добавляет эти три задания в течение 12 минут и дважды запрашивает печать заданий в очереди. Элементы 4, 1, 5 и 2 представляют время между операциями пользователя. Значения 70, 38, 35, 20 и 4 указывают количество страниц, оставшихся для печати, в момент каждой операции пользователя.
Добавить Задание 45	Запрос списка Добавить заданий Задание б		Добавить Задание 70	Запрос списка заданий
_ L _ 4	.1'1 .	5	|	г 1
70	38 35		20	4
Оставшиеся для печати страницы
На рисунке 9.1 перечислены операции системы буферизации печати от 0 до 12 минут. В каждом событии распечатывается количество страниц в бу-феризаторе, общее количество уже распечатанных страниц и очередь печати.
Разработка программы
Буферизатор печати является списком, в котором сохраняются записи PrintJob. Так как задания обрабатываются на основе порядка first-come/first-served, мы рассматриваем список как очередь с запросами на выполнение заданий, вставляемых в хвост списка, и фактической печатью, управляемой Удалением запросов на выполнение заданий из начала списка. В нашем исследовании мы выполняем задачи, которых нет в наличии в формальной коллекции очередей. В списке сканируются запросы на выполнение заданий и Распечатывается их статус. При обновлении данных изменяется размер в границах текущего задания без его удаления из списка. Для того, чтобы ®<еть гибкий доступ к заданиям на печать, мы реализуем буферизатор как ®язанный список.
Cofi^Ta исследовательская задача для управления моделированием использует биа^Т,1Я’ Событие может включать добавление задания на печать в систему ^Феризации, распечатку заданий в буферизаторе и проверку того, остается
определенное задание для печати. Случайно выбранные блоки времени в р а5азоне от 1 до 5 минут отделяют события. Для моделирования непре-Иечати заданий в фоновом режиме используется вхождение событие обновления очереди печати. Информация о продолжительности времен!
между событиями сохраняется для того, чтобы можно было узнать, скопД страниц уже распечатано. Допустим, что время, прошедшее с момента I леднего события, равно deltaTime, тогда количество страниц, которые к I напечатаны, равно	“ 1
pagesPrinted “ deltaTime * 8	1
Время (мин.) Операция Страниц в задании Страниц в буфере Напечатанные страНи
0	Добавить задание 45	70	70	0
Печатать очередь|	45 Тезисы	70		
4	Запрос списка		38	32
Печатать очередь 45 Тезисы	38		
5	Добавить задание 6	5	35	40
Печатать очередь |	45 Тезисы	30	|	6 Письмо	5
10 Добавить задание 70	20
20
75
Печатать очередь
70 Записи 20
12 Запрос списка
Печатать очередь|	70	| Записи
91
4
Рис. 9.1. Отслеживание операций системы буферизации печати
Хранение заданий на печать и функции доступа к системе буферизац,. определяются классом Spooler.
Спецификация класса Spooler
ОБЪЯВЛЕНИЕ
flinclude <iostream.h>
// включить генератор случайных чисел и класс LinkedList flinclude "random.h" flinclude "link.h"
// скорость печати — 8 страниц в минуту const int PRINTSPEED = 8;
// класс Суфериэатора печати
class Spooler {
private:
// очередь, которая содержит задания LinkedList<PrintJob> JobList;
// deltaTime содержит случайное число в диапазоне 1 — 5
// минут для имитации прошедшего времени int deltaTime;
I / метод обновления информации о задании void UpdateSpooler(int time);
RandomNumber rnd;
public:
F // констуктор
Spooler(void);
// добавление задания в Суфермзатор void AddJob(const PrintJobi J);
// методы оценивания void ListJobs(void); int CheckJob(int jobno); int NumberOfJobs(void):
}:
ОПИСЛНИ®
Закрытый данное-член deltaTime моделирует количество минут, за которые ыдолнялась печать со времени последнего события в системе буферизации. В начале каждого события вызывается операция UpdateSpooler с deltaTime в качестве параметра. Эта функция изменяет jobList для отражения того факта, что распечатка происходила в фоновом режиме в течение deltaTime минут. Каждый открытый метод отвечает за присваивание нового значения deltaTime. Значение, создаваемое генератором RandomNumber в диапазоне от 1 до 5, указывает количество минут перед следующим событием.
Задания на печать добавляются в систему буферизации с использованием метода AddJob. Две операции ListJobs и CheckJob предоставляют информацию о статусе задания, находящегося в буфернзаторе. В любое время список заданий в буфернзаторе распечатывается вызовом ListJobs. Метод CheckJob принимает номер задания и возвращает информацию о его статусе в буферизаторе. Он возвращает количество страниц, оставшихся для печати. Объявление PrinUob, PRINTSPEED и реализация класса буферизатора содержатся в файле spooler.h.
Реализация метода UPDATE для класса Spooler
Процесс обновления удаляет все задания, суммарное количество страниц которых меньше, чем количество распечатанных страниц. Если количество нераспечатанных страниц меньше или равно потенциальному общему количеству страниц печати, то все задания выполнены и очередь пуста. Иначе из очереди могут быть удалены одно или более заданий и распечатаны несколько страниц текущего задания. Обновление оставляет текущее задание Не л°лностыо выполненным.
обновление буферизатора. предполагается, что при печати
//	проходит некоторое время, метод удаляет
h ^конченные задания и изменяет число оставшихся страниц Vai,. ” ЭаДания, выполняемого в текущий момент времени ।	pooler::UpdateSpooler(int time)
p*intJob J;
Число страниц, которые следует напечатать i-t® соответствии с time
Printedpages • time*PRTNTSPEED;
// ^Сп°Льэовать printedpages и сканировать
//	заданий в очереди.
очередь печати
wilile3MReSet ° ;
11JobList.ListEmpty() && printedpages > 0)
—	-< < I
<	I
// найти первое задание	I
J = JobList.Data();	I'
II
11 если напечатанных	страниц больше, чем	1,
// страниц в задании, обновить счетчик печатаемых	I
/ / страниц и удалить задание	'
if (printedpages > J.pagesize)	I
(	I
prlntedpages — J.pagesize;	I
JobList.DeleteFront();	1
I // часть задания выполнена;	1
// изменить оставшееся число страниц	1
else	I
{	I
J.pagesize printedpages;	I
printedpages =0;	I
JobList.Datat) “J;	I
}	I
}	I
}	I
Методы оценки системы буферизации печати |
Методы оценки системы буферизации печати дают информацию пользе- I вателю в ответ на запрос о заданиях, которые ожидают печати, и статусе I отдельного задания. Методы ListJobs и CheckJob выполняют последовательное 1 сканирование списка буферизатора. Мы приводим функцию Listjobs; другие 1 методы читатель может найти в файле spooler.h.	I
// обновление буферизатора и выдача списка всех заданий	I
// в очереди	1
void Spooler::ListJobs(void)	I
{	I
Printjob J;	1
// обновить очередь	I
UpdateSpooler(deltaTime);	1
// генерировать время до следующего события	I
deltaTime  1 + rnd.Random(5);	1
// перед сканированием проверить, не пуста ли очередь	1
if (jobList.ListSize() = 0)	1
cout « "Очередь печати пуста!\п";	I
else	1
(	I
// перейти к началу списка и использовать	1
// цикл для сканирования списка, остановиться	I
/7 в конце списка, выводить информационные поля	I
// для каждого задания	I
for(JobList.Reset(); !jobList.EndOfListО;	I
j obList-Next())	
I
J «= JobList.DataO;	I
cout « "Задание " « J.number «	" « J.filename;	
cout « " “ « J.pagesize « *' страниц осталось”	
« endl;	
}	I
}	I
)	I
I
9.8. Буферизатор печати
Эта main-программа определяет Spooler-объект spool и создает интер-«тйвнЫЙ диалог с пользователем. В каждой итерации пользователю представляется меню из четырех пунктов. Пункты ’А’ (добавить задание), ’L* список заданий) и ’С* (информация о задании) обновляют очередь печати L выполняют определенную операцию системы буферизации. Пункт ’Q’ заВершает программу. Пункты ’L’ и ’С’ не выводятся, если очередь печати пуста.
#include <iostream.h>
Jinclude <ctype.h> ^pragma hdrstop
^include "spooler.h"
void main (void)
{
// объект типа Spooler
Spooler spool;
int jnum, jobno = 0, rempages;
char response = 'C';
Printjob J;
for (;;)
<
77 выдача меню
if (spool.NumberofJobs() != 0)
cout « "Add(A) List(L) Check(C) Quit(Q) ==>
else
cout « "AddfA) Quit(Q) ==>
cin » response;
// преобразовать ответ к верхнему регистру
response - toupper(response);
/7 действие, продиктованное ответом
swi tch(response)
(
// добавить новое задание со следующим номером,
/7 используемым как идентификационный (id); читать
// имя файла и число страниц.
case 'А' :
J.number - jobno;
jobno++;
cout « "Имя файла:
cin » J.filename;
cout « "Число страниц:
cin » J.pagesize;
spool.AddJob(J);
break;
// Печать информации каждого оставшегося задания case ' L' :
spool.ListJobs();
break;
7/ ввести id-задания; сканировать список с этим ключом 7/ указать, что задание выполнено или число оставшихся // для печати страниц
case *С':
cout « "Введите номер задания;
cin » jnum;
rentpages - spool .CheckJob(jnwu) ;
if (rempages >0)
cout « "Задание в очереди. " « rempages « "^страниц осталось напечатать\n";
else
cout « "Задание выполнено\п”; break;
11 выход для ответа 'Q' case ‘Q':
break;
// сообщить о неправильном ответе и // перерисовать меню default:
cout « "Неправильная break;
команда!\n";
}
if (response -= 'Q') break;
cout « endl;
)
}
выполнение программы 9.8>
Add (A) Quit (Q) ==> a
Имя файла: notes
Число страниц: 75
Add (A) List (L) Имя файла: paper Число страниц; 25
Check (С)
Quit
(Q) =-> а
Add (А) Задание Задание
List(L) 0: notes 1: paper
Check (С) 19 страниц 25 страниц
Quit осталось осталось
(Q) =*“> 1
Add (А) Введите Задание
List(L) номер задания: в очереди. 20 страниц
Check (С) 1
Quit (Q) =—> с
осталось напечатать
Add (А)
Очередь
List(L) Check (С) печати пуста!
Quit '(Q) =-> 1
Add (А)
Quit (Q) -=> q
9.8. Циклические списки
Оканчивающийся NULL-символом связанный список — это последоват^ ность узлов, которая начинается с головного узла и заканчивается узлом» указателя next которого имеет значение NULL. В разделе 9.1 разра библиотека функций для сканирования такого списка и для вставки и уД^ узлов. В этом разделе разрабатывается альтернативная модель, называ циклическим связанным списком (circular linked list), которая упрошае v
йотку и кодирование алгоритмов последовательных списков. Многие npo-i.ec-Р сальные программисты используют циклическую модель для реализации Ценных списков.
с5 гтустоЙ циклический список содержит узел, который имеет неинициализи-анное поле данных. Этот узел называется заголовком (header) и первона-Р°® 0 указывает на самого себя. Роль заголовка — указывать на первый 4\льный узел в списке и, следовательно, на заголовок часто ссылаются как ^узеЛ-часовоЙ (sentinel). В циклической модели связанного списка пустой Sw«coK фактически содержит один узел, и указатель NULL никогда не исполь-ется. Мы приводим схему заголовка, используя угловые линии в качестве стоны узла.
header
Заметьте, что для стандартного связанного списка и циклического связанного списка тесты, определяющие, является ли список пустым, различны. Стандартный связанный список: head == NULL циклический связанный список: header->next =- header
При добавлении узлов в список последний узел указывает на заголовочный узел. Мы можем представить циклический связанный список как браслет с заголовочным узлом, служащим застежкой. Заголовок связывает вместе реальные узлы в списке.
header
В разделе 9.1 был описан класс Node и использовались его методы для создания связанных списков. В этом разделе мы объявляем класс Cnode, создающий узлы для циклического списка. Этот класс предоставляет конструктор по умолчанию, допускающий неинициализированное поле данных. Конструктор используется для создания заголовка.
Спецификация класса CNode
ОБЪЯВЛЕНИЕ
^einPlate <class Т>
class CNode
Private:
// циклическая связь для следующего узла
CNode<T> *next;
Public:
f данные — открытые
Т data;
f конструктор
CNode(void);
CNode (const T& item);
—Методы модификации списка
void InsertAfter(CNode<T> *р);
CNode<T> *DeleteAfter(void);
H получить адрес следующего узла
CNode<T> ‘NextNode(void) const; );
ОПИСАНИЕ
Этот класс подобен классу Node в разделе 9.1. В действительности члены этого класса имеют то же имя и те же функции. Детали открыть!6 членов класса приводятся в следующем разделе, в котором описывается гЛ ализация класса. Класс CNode содержится в файле cnode.h. *
Реализация класса CNode
Конструкторы инициализируют узел его указанием на самого себя, поэтому каждый узел может служить в качестве заголовка для пустого списка. УКа. затель на самого себя — это указатель this, и, следовательно, присваивание становится следующим: next e this;
Для конструктора по умолчанию поле data не инициализируется. Второй конструктор принимает параметр и использует его для инициализации поля data.
Никакой конструктор не требует параметра, определяющего начальное значение для поля next. Все необходимые изменения поля next выполняются с использованием методов InsertAfter и DeleteAfter.
// конструктор, который создает пустой список
//и инициализирует данные
teroplate<class Т>
CNode<T>;:CNode(const Т& item) (
// устанавливает узел лля указания на самого себя и
// инициализирует данные
next = this;
data = item;
)
Операции класса CNode. Класс CNode предоставляет метод NextNode, который используется для прохождения по списку. Подобно методу класса Node функция NextNode возвращает значение указателя next.
InsertAfter добавляет узел р непосредственно после текущего объекта. Для загрузки узла в голову списка не требуется никакого специального алгоритма, так как мы просто выполняем InsertAfter(header).
До
header
Р
„явка узла р после текущего узла
// ®?ate<class Т>
teJflPJ'^tjode<T>:: InsertAfter (CNode<T> *p)
{ указывает на следующий узел за текущим.
// текущий узел указывает на р.
''xiext = next;
= Р;
каление узла из списка выполняется методом DeleteAfter. DeleteAfter уда-т узел, следующий непосредственно за текущим узлом, и затем возвращает ^затель на удаленный узел. Если next равно this, то в списке нет никаких ^угих узлов, и узел не должен удалять самого себя. В этом случае операция вращает значение NULL.
удалить узел, следующий за текущим и возвратить его адрес
template <class Т>
CNode<T> *CNode<T>::DeleteAfter(void)
// сохранить адрес удаляемого узла CNode<T> *tempPtr = next;
// если в next адрес текущего объекта (this), он // указывает сам на себя, возвратить NULL if (next =- this)
return NULL;
// текущий узел указывает на следующий за tempPtr. next = tempPtr->next;
// возвратить указатель на несвязанный узел return tempPtr;
Приложение: Решение задачи Джозефуса
Задача Джозефуса является интересной программой, предоставляющей изящное решение циклического связанного списка. Далее следует версия задачи:
Агент бюро путешествий выбирает п клиентов для участия в розыгрыше ^платного кругосветного путешествия. Участники становятся в круг и затем шляпы выбирается число m (т < п). Розыгрыш производится агентом, оторый идет по кругу по часовой стрелке и останавливается у каждого т-ного Участника. Этот человек удаляется из игры, и агент продолжает счет, удаляя аждого m-ного человека до тех пор, пока не останется только один участник.
/’счастливчик выигрывает кругосветное путешествие.
Поп ап^ИмеР» если п — 8, и ш — 3, то участники удаляются в следующем еЯДКе: 3, 6, 1, 5, 2, 8, 4 и участник 7 выигрывает круиз.
памма 9.9. Задача Джозефуса
Эта программа эмулирует розыгрыш кругосветного путешествия. Функ-ы CreateList создает циклический список 1,2,...п, используя CNode-метод After.
rQ Ф°Цесс выбора управляется функцией Josephus, которая принимает за-Ито®01* Циклического списка и случайное число ш. Она выполняет п-1 Р^Цйй, отсчитывая каждый раз ш. последовательных элементов в списке
и удаляя m-ый элемент. Когда мы продолжаем обходить список по кру 1 мы выводим номер каждого участника, который удаляется. При завершен ч цикла остается один элемент.
Main-программа запрашивает количество участников розыгрыша п и пользует CreateList для создания циклического списка. Генерируется чайное число ш в диапазоне 1..п, и вызывается функция Josephus для ределения порядка удаления участников и победителя в розыгрыше круи°Л
♦include <iostream.h>
♦pragma hdrstop
♦include "cnode.h"
♦include "random.h"
// создать циклический список с заданным узлом void CreateList(CNode<int> *header, int n) (
// начать вставку после головы списка
CNode<int> *currPtr = header, *newNodePtr; int i;
// построить п-ый элемент списка for(i=l;i <= n;i++)
<
// создать узел со значением i newNodePtr = new CNode<int>(i);
// вставить в конец списка и продвинуть currPtr к концу currPtr->InsertAfter(newNodePtr);
currPtr = newNodePtr; }
}
// для списка из п элементов решить задачу Джоэефуса
// удалением каждого m-го элемента, пока не останется один void Josephus (CNode<int> »list, int n, int m) {
CNcde<int> *prevPtr - list, *currPtr = list->NextNode();
CNodecint> *deletedNodePtr;
// удалить из списка всех, кроме одного for(int i-O;i < n-l;i++)
{
// вычисление для currPtr, обработать ш элементов.
// следует продвигать ш-1 раз for(int j=O;j <
(
// передвинуть указатели prevPtr = currPtr;
currPtr = currPtr->NextNode ();
// если currPtr указывает на голову, снова передвинуть указатели if (currPtr == list)
(
prevPtr = list;
currPtr = currPtr->NextNpde();
} )
cout « "Удалить участника: " « currPtr->data « endl;
If записать удаляемый узел и продвинуть currPtr deletedNodePtr = currPtr;
currPtr = currPtr->NextNode(};
// удалить узел из списка prevPtr->DeleteAfter();
delete deletedNodePtr;
f f если currPtr указывает на голову, If снова передвинуть указатели if (currPtr -= list) (
prevPtr = list;
currPtr = currPtr->NextNode();
) x }
cout « endl « "Участник " « currPtr->data « " выиграл круиз.” « endl;
// удалить оставшийся узел
deletedNodePtr = list~>DeleteAfter(); delete deletedNodePtr;
void main (void)
I
// список участников CNode<int> list;
int n, m;
RandomNumber rnd; // для генерации случайных чисел
cout « *'Введите число участников: ”; cin » п;
// создать циклический список из участников 1, 2, ... п CreateList(&list,n);
m « 1+rnd.Random(п);
cout « "Сгенерированное случайное число: " « m « endl;
11 ришить задачу Джозефуса и напечатать выигравшего круиз Josephus(&list,n,m);
/*
^Выполнение программы 9.9>
Введите число участников: 10
Сгенерированное случайное число: 5
ХДалить участника: 5
Далить участника: 10
Далить участника: 6
Далить участника: 2
Далить участника: 9
Далить участника: 6
Далить участника: 1
Далить участника: 4
Далить участника: 7
^астник 3 выиграл круиз.
9.9. Двусвязные списки
Сканирование либо оканчивающегося NULL-символом, либо цикличеср списка происходит слева направо. Циклический список является более гиб и позволяет начинать сканирование в любом положении в списке и продол^ его до начальной позиции. Эти списки имеют ограничения, так как они позволяют пользователю возвращать пройденные шаги и выполнять скана вание в обратном направлении. Они неэффективно выполняют простую зад, удаления узла р, так как мы должны проходить по списку и находить указав на узел, предшествующий р.
header
Узел перед р Удалить р
В некоторых приложениях пользователю необходим доступ к списку в । ратном порядке. Например, бейсбольный менеджер ведет список игроков упорядочением по среднему количеству отбиваний от самого низкого до сам-: высокого. Чтобы оценить сноровку игроков в отбивании и присудить звав лучшего в этом виде, необходимо прохождение списка в обратном направлен* Это можно выполнить, используя стек, но такой алгоритм не очень удобен.
В случаях, когда нам необходимо обращаться к узлам в любом направлена полезным является двусвязный список (doubly linked list). Узел в двусвязн списке содержит два указателя для создания мощной и гибкой структур обработки списков.
left	data	right
Для двусвязного списка операции вставки и удаления имеются в кажд< направлении. Следующий рисунок иллюстрирует проблему вставки узла справа от текущего. При этом необходимо установить четыре новых связи.
Р
Класс DNode — это класс обработки узла для циклических двусБЯ3^ списков. Объявление класса и функций-членов содержится в файле
Текущий узел
гпециФикация класса DNode
ОБЪЯВЛЕНИЕ
tempiate <class т>
class DNode
1 private:
// циклические связи влево и вправо
DNode<T> *left;
DNode<T> *right;
public:
// данные — открытые
T data;
// конструкторы
DNode(void);
DNode (const T6 item);
// методы модификации списка void InsertRight(DNode<T> *p) ; void Insertleft(DNode<T> *p);
DNode<T> *DeleteNode(void);
11 получение адреса следующего (вправо и влево) узла DNode<T> *NextNodeRight(void) const;
DNode<T> *NextNodeLeft(void) const;
h
ОПИСАНИЕ
Данные-члены подобны членам односвязного класса CNode, за исключением того, что здесь используются два указателя next. Имеются две операции вставки (по одной для каждого направления) и операция удаления, которая удаляет текущий узел из списка. Значение закрытого указателя возвращается с помо-- ^Ь10 функций NextNodeRight и NextNodeLeft.
ПРИМЕР
Node<int> dlist;	// двусвязный список
ц СКаниРовать список, выводя п©ка мы не вернемся к ^<int> *р = fidlist WJ1Si^>^'extNode Right () ;
(	6 (р I" &list)
р°^ P~>(3ata « ;
}	P“>NextNodeRigbt();
Dil°d eci* *newN°del (10) ;
C>1<0dt>^7'nt> *newNode2(20);
P nt> *P “ bdlist;
P->lhert Right (newNodeI);
SertLeft (newNode2);
значения узла, до тех заголовочному узлу // инициализация указателя // установка р на первый узел в списке
// вывод значения данных
// установка р на следующий узел в списке
// создание узлов со значениями
И 10 и 20
// р указывает на заголовочный узел
// вставка в начало списка
// вставка в хвост списка
Приложение: Сортировка двусвязного списка
Функция InsertOrder используется в программе 9.4 для создания отсоп рованного списка. Алгоритм начинается в головном узле и сканирует спи в поисках места для вставки.-Имея дело с двусвязным списком, мы мойс° оптимизировать этот процесс, поддерживая указатель currPtr, определяют последний узел, который был помещен в список. Для вставки нового элемев мы сравниваем его значение с данным в текущем положении. Если нов элемент меньше, используйте левые указатели для сканирования списка
направлении вниз. Если новый элемент больше, используйте правые указател*! для сканирования списка вверх. Например, предположим, что мы только чт сохранили 40 в списке dlist.	°
dlist: 10 25 30 40 50 55 60 75 90
Для добавления узла 70 сканируем список по направлению вверх и встав, ляем 70 справа от 60. Для добавления узла 35 сканируем список по направлению вниз и вставляем 35 слева от 40.
Программа 9.10. Сортировка в двусвязных списках
DLinkSort использует двусвязный список для сортировки массива из и элементов, создавая упорядоченный список и затем копируя элементы обратно в массив. Функция InsertHigher добавляет новый узел справа от текущей списочной позиции. Симметричная по отношению к InsertHigher функция InsertLower добавляет новый узел слева от текущей позиции.
Алгоритм функции DLinkSort:
Вставить элемент (item) 1
вызвать InsertRight с головой и сохранить а[0]
Вставить элементы 2-10
если item < currPtr->data, вызвать InsertLower
если item > currPtr-<data, вызвать InsertHigher
В следующей программе с использованием функции DLinkSort сортируется список из 10 целых. Отсортированный список выводится с помощью функции PrintArray.
flinclude <iostream.h> flpragma hdrstop
flinclude ’'dnode-h"
template cclass Т>
void InsertLower(DNode<T> *dheader, DNode<T>* ScurrPtr, T item) {
DNode<T> *newNode= new DNode<T>(item), *p;
// найти место вставки
р = currPtr;
while (р !- dheader && item < p->data) p = p->NextNodeLeft();
// вставить элемент p->InsertRight(newNode);
}
// переустановить currPtr на новый узел currPtr = newNode;
template <class T>
void insertHigher(DNode<T>* dheader, DNode<T>* b currPtr, T item)
DNode<T> *newNode= new DNode<T>(item), *p;
// найти место вставки
p = currPtr;
while (p != dheader && p->data < item) p = p->NextNodeRight():
// вставить элемент p->InsertLeft(newNode);
// переустановить currPtr на новый узел currPtr = newNode;
}
template <class T>
void DLinkSort(T a[], int n) {
// задать двусвязныи список для элементов массива DNode<T> dheader, *currPtr;
int i;
// вставить первый элемент в список
DNode<T> *newNode = new DNode<T>(а[0]);
dheader.InsertRight(newNode);
currPtr = newNode;
// вставить оставшиеся элементы в список for (i=l;i < n;i++)
if (a[i] < currPtr->data)
InsertLower(fidheader,currPtr,a[i]);
else
InsertHigher(Sdheader,currPtr,a[i]);
// сканировать список и копировать данные в массив currPtr - dheader.NextNodeRight();
i = 0;
while(currPtr != bdheader) {
a[i++] = currPtr->data;
currPtr - currPtr->NextNodeRight(); }
// удалить все узлы списка
while(dheader.NextNodeRight() != &dheader) {
currPtr - (dheader.NextNodeRight())->DeleteNode(); delete currPtr;
}
^сканировать массив и выводить его элементы
v°id PrintArray(int а[], int n)
for(inc i=0;i < n;i++) cout « a[i] << ” ";
^°id main(void)
• // инициализированный массив из десяти целых int А[10] = {82,65,74,95,60,28,5,3,33,55};
DLinkSort(A,10);	П сортировать массив
cout « " Отсортированный массив: PrintArray (А, 10)// печатать массив cout « endl;	.
) /* «Выполнение программы 9-10> Отсортированный массив:	3 5 2В 33 55 60 65 74 02 95
*/
I i
Реализация класса DNode
Конструктор создает пустой список, присваивая адрес узла this обоим (де. вому и правому) указателям. Если конструктору передается параметр item данному-члену узла присваивается значение item,.
// конструктор, который создает пустой
// список и инициализирует данные
template«class т>
DNode«T>::DNode(const TS item) <
// установить узел для указания на самого сеОя и
// инициализировать данные
left = right - this;
data = item;
Списочные операции. Для вставки узла р справа от текущего узла необходимо назначить четыре указателя. На рисунке 9.2 показано соответствие между операторами C++ этими новыми связями. Заметим, что присваивания не могут выполняться в произвольном порядке. Например, если (4) выполняется первым, то связь к узлу, следующему за текущим, теряется.
Текущий узел	right
(4) right = р;	р
Рис. 9.2. Вставка узла справа в циклическом двусвязном списке
Читателю следует проверить, что этот алгоритм работает правильно в случае вставки в пустой список.
// вставить узел р справа от текущего
template «class Т>	5
void’ DNode«T>::InsertRight(DNode«T> *p) {
// связать p с его предшественником справа
p->right = right;
right->left = p;
// связать p с текущим узлом p->left this;
right = p;
^еТОд InsertLef t выполняет вставку узла слева аналогично вставке справа аЛГ0Ритме для
₽ ставить Узел Р слева о*г текущего
// Blate <class Т>
ceflpcNode<T>::InsertLeft{DNode<T> *p)
void
{ СЪКзо^ъ p С его редшественником слева
p->left = left;
ieft~>ri9ht = P;
// связать p с текущим узлом
p_>right ~ this;
left = p;
I
Для удаления текущего узла должны быть изменены два указателя, как показано на рис. 9.3. Читателю следует проверить, что алгоритм работает правильно в случае, когда удаляется последний узел списка. Метод возвращает указатель на удаленный узел.
// отсоединить текущий элемент от списка и возвратить его адрес template <class Т>
DHode<T> *DNode<T>:гDeleteNode(void)
( left->right = right;
right->left - left;
return this;
1
(1) left -> right — right;
(2) right -> left — left;
Рис. 9.3. Удаление узла в циклическом двусвязном списке

9-10. Практическая задача: Управление окнами
^.Графический пользовательский интерфейс (Graphical User Interface, GUI) цр^^ивает на экране многочисленные окна. Они организованы слоями, ПеРеднее OKHG считается активным окном (active window). Некоторые
®йоз0>КеЕия ВеДУт список текущих открытых окон. Список доступен из меню °ляет пользователю выделять наименование окна и делать его передним 3йРой ТИвнь1м' ^то моэкет быть особенно полезным, когда необходимо активи-8аДнее окно, которое в данный момент времени является невидимым. на рис. 9.4 показаны три окна, где (слева) Window 0 — это активное Window_l из списка меню активизирует это окно и делает его
Select Windowjl
Windows
Window_0 active
bayea:
Tile
CJcacs All
Sava £11
Full ^ataxiawe-
Show CX.xpboard
Window_0	3C0
Window""1	Xl
Window_2	X2
Рис. 9.4 Списки окон
Каждое окно на экране ассоциируется с каким-либо файлом. Окно созда ется при открытии соответствующего файла и уничтожается, когда этот фа& закрывается. Мы используем связанный список для хранения списка окон С каждой файловой операцией связываем соответствующую операцию в сдис ке окон. Файловая операция New создает переднее окно, которое добавляется в начало списка. Операции Close и Save As применимы к активному окю в начале списка. Общая операция, такая как Close All, может быть реали зована удалением окна нз списка и закрытием как окна, так и соответству ющего ему файла.
Рассмотрим практическую задачу, которая ведет список окон для прило жения GUI. Приложение поддерживает следующие файловые и списочньа операции:
New:
Вставляет окно с именем Untitled.
Close:
CloseAll:
Save As:
Quit:
Menu Display:
Удаляет переднее окно.
Закрывает все окна, очищая список.
Сохраняет содержимое окна под другим именем и обновляет заголовок в элементе ввода окна.
Завершает приложение.
Оконное меню отображает номер и имя каждого окна в порядке расположения окон слоями. Пользователь может вводить номер и активизир0' вать окно, перемещая его в начало списка окон-
Список окон
В любой момент времени допускается одновременное открытие маКСИП^ 10 окон. Каждое открытое окно имеет соответствующий номер в от О до 9. Когда окно закрывается, этот номер становится доступным операции New, которая создает новое открытое окно. За управление °^оВв] ними окнами и изменение текущего активного окна отвечают методы Close All, Save As и Activate. Каждое окно представлено объектом * j который определяет имя окна и его номер (index) в списке имеюШй наличии окон.
Объект окна описывается классом Window, который содержит поля win-yTitle и windowNumber. Класс имеет функции-члены для изменения за-довка окна, получения номера окна и вывода информации в формате За-^лоВОкГномер окна] (Titlefwindow number]). Перегруженный оператор равно Г0^) сравнивает две Window-записи по номеру окон.
Спецификация класса Window
ОБЪЯВЛЕНИЕ
. ^дасс, содержащий информацию об отдельном окне class Window
private:
//window-данные включают заголовок окна и
If индекс в таблице доступных окнон
String windowTitle;
int windowNumber;
public:
If конструкторы
Window(void);
Windowfconst Strings title, int wnum);
// методы доступа к данным void ChangeTitle(const Strings title); int GetWindowNumber(void);
Il перегруженные операторы int operator=« (const Windows w); friend ostreams operator« (©streams ostr, const Windows w);
}:
Полная реализация класса Window находится в файле windlist.h. Список окон и операции для создания, хранения и активизации объектов окна определяются классом WindowList.
Спецификация класса WindowList
ОБЪЯВЛЕНИЕ
include "link.h"
структура связанного списка для Window-объектов cUss WindowList
Private:
// список активных окон
LinkedList<Window> windList;
// список доступных окон и число открытых
int windAvail[10];
int windcount;
// функции получения и освобождения окон
int GetWindowNumber(void);
int FindAndDelete(Windows wind);
f/ 'Печать списка открытых окон
void PrintWindowList(void);
PUblic.
/1 конструктор
WindowList(void);
// методы window-меню
void New(void);
void Close(void);	// закрыть переднее окно
void CloseAll(void);	// закрыть все окна
void SaveAs(const Strings name); // изменить имя
void Activate(int windownum);	// активизировать окно
// моделировать управление окнами
void Selectltemtints item, strings name); );
ОПИСАНИЕ
Конструктор присваивает окнам начальные данные. Он устанавливает сче чик окон на нуль и отмечает каждое доступное окно в диапазоне 0-9. Ne получает окно из списка имеющихся в наличии окон и присваивает индек в качестве номера окна. Окну передается заголовок Untitled. Новое oki вставляется в начало списка открытых окон.
Класс WindowList поддерживает дополнительные операции: Close, Cloi All, Save As и Activate, которые обеспечивает реакцию на соответствующи опции меню.
Операция Selectitem реализует меню. Пользователь может ввести с кл виатуры либо буквенный символ N (New), С (Close), A (Close All), S (Sai As), Q (Quit), либо цифру 0,1.... Метод возвращает входной выбор и сген рированный внутри номер пункта, обозначающий операцию. Следующая та лица соотносит номер пункта и
выбор:
Номер пункта
1
2
3
4
5
6
7
15
Имя
New
Close
Close All
Save As
Quit
WindowName[io] WindowNamefh]
WindowNamefisl
Символьный код для выбора n(N) с(С) а(А) s(S) q(Q) i0
Л
i9
только в том случае, если имеется, Вывод имен и номеров окон управляет
Пункты меню 2—4 выводятся крайней мере, одно открытое окно.	3-с т
закрытым методом PrintWindowList. Пункты с 6-го и далее соответствуй списку текущих открытых окон. Пункт 6 — это переднее окно с номер окна i0; пункт 7 — это второе окно с номером ij и так далее. Окно помеШае^ в начало списка вводом с клавиатуры его номера. Новым окнам даеТС^сЯ именьший имеющийся в наличии номер в диапазоне 0—9. Например» номера 0, 1, 3 и 5 используются в данный момент и 0-е окно закрыв» следующее новое окно создается с номером окна 0. Функция Get Window ber осуществляет выполнение алгоритма для распределения номеров
пример
тл	„	-	-tv КОМ»®^
В следующей таблице приводится последовательность оконных
После завершения какой-либо операции мы задаем список окон с первЬ

gTOM ввода, являющимся активным окном. Для команды SaveAs последую-ВВ°Д заключается в круглые скобки.
ВЫб°Р	Номер пункта 1
N	
	1
N	
$ (One)	4
	7
0	
N	1
С	2
С	2
N	1
А	3
Q	5
Действие
новое окно новое окно сохранить как Оле активизировать О новое окно закрыть закрыть новое окно закрыть все выход
Список окон
Untitled[O]
Untitledp] Untitled[O]
One[1] Untitied[O]
Untit led[O] One[1J
Untitled[2] Untitled[O] One[1]
Untitled [0] One[1)
One(1]
Untitled [0] Onefl)
<nycro>
завершение работы приложения
Реализация класса WindowList
Полный листинг методов WindowList содержится в файле windlist.h. Мы приводим здесь код только нескольких функций для иллюстрации использования связанного списка при ведении списка открытых окон и выполнении опций меню. Функция GetWindowNumber проходит массив windAvail в поисках имеющегося в наличии номера окна и возвращает первый найденный иомер. В результате все новые окна получают наименьший возможный номер.
// получить первое свободное окно из списка доступных окон int WindowList::GetWindowNumber (void) {
for(int i=0;i < 10;i++)
// если окно доступно, выделить его
// сделать его недоступным.
if (windAvail[i])
<
windAvail[i] =0;
break;
)
return i;	// return window index
Закрытая функция PrintWindowList сканирует список окон и выводит за-оловок и номер для каждого окна. Метод реализует простое последовательное Оптирование списка, начиная с первого (Reset) и переходя от окна к окну До достижения конца списка (EndOfList). Оператор « из класса Window ®0ДИт данные окна в формате Заголовок[номер] (Titlef#]).
оконной информации для всех активных окон
(	WindowList::PrintWindowList(void)
fot(windList -Reset(); 'windList.EndOfList();
W1ndList.Next О)
}	cout « windList.Data();
*Ф^?*С0ЧНЬ1е операции окон. Для создания какого-либо окна метод New ^Ивает Window-объекту win заголовок Untitled. Вызовом GetWin-ЭТОМУ объекту присваивается номер окна, и он вставляется в ок°н; счетчик окон увеличивается.
//получение нового окна и передача ему заголовка 'untitled' void WindowListNew(void) <
//проверка, имеется ли в наличии окно, если
//нет, просто — возврат
if (windcount »= 10) (
cerr « "Нет больше окон в наличии, пока одно не будет закрыто" « endl return;
// получить новое окно с заголовком ' Untitled’ вызовом
// функции getWindowNumber
Window win(Untitled, GetWindowNumber());
// сделать его активным,, вставляя в начало списка
windList.InsertFront(win);
windCount++;
)
Для активизации окна, расположенного за другими окнами, мы должны сначала найти это окно, используя его номер в качестве ключа, а затем удалить этот элемент из списка. При вставке окна в начало списка, оно становится активным. Вызывается закрытый метод Find'AndDelete, который сканирует список, выполняя поиск совпадения с номером окна. Когда окно обнаружено, метод отсоединяет его от списка и возвращает данные окна. Эта информация затем используется для создания нового окна, которое вставляется в начало списка.
int WindowList::FindAndDelete(Windows wind) (
int retval;
// цикл no списку для поиска wind
for (windList.Reset();)windList.EndofList();windList.Next())
11 window-оператор сравнивает номера окон.
11 при совпадении прервать цикл
if(wind == windList.Data О) break;
// совпадение имеется?
iff*windList.EndofList()) {
// присвоить wind значение, удалить вались и возвратить 1 (успешно) wind » windLi st.Data();
windList.DeleteAt();
retval -Is
)
else
retval =0;	// возвратить 0 (неуспешно)
return retval;
void WindowList::Activate(int windownum) {
Window win("Формальное имя", windownum);
if (FindAndDelete(win))
windList.InsertFront(win);
else
cerr « "Некорректный номер окна.Хп";
И
Программа 9.11. Управление списком окон
Эта main-программа определяет WindowList-объект windops, содержа-дий список открытых окон. Цикл событий вызывает функцию Selectltem выполняет действия, соответствующие значению, возвращаемому функцией. Цикл продолжается до тех пор, пока пользователь не выберет Q (Quit).
^include <iostream.h>
// включить классы Window и WindowList linclude "windlist.h”
// очистка входного буфера void ClearEOL(void)
( char c;
do
cin.get(c);
while (c != '\n');
)
void main (void) {
// список доступных программе окон
WindowList windops;
// window-заголовки
String wtitle, itemText;
// done  1, если пользователь ввел символ q int done - 0, item;
// моделировать до введения пользователем символа 'д' while(!done)
{
// выдать меню и принять ответ пользователя windops.SelectItem(item,itemText);
// при выборе числа активизировать окно if (item >= б)
windops.Activate(itemText[О] — • 0') ;
// иначе выбирать из опций 0—5.
// вызвать метод для обработки запроса else
switch(item) {
case 0:	break;
case 1:	windops.New();
brehk;
case 2:	windops.Close();
break;
case 3:	windops.CloseAll();
break;
case 4:	cout « "Заголовок нового окна:
ClearEOL();
wtitle.Readstring();
windops.SaveAs(wtitle);
break;
case 5:	done  1;
break;
J
)
)
/*
^Выполнение программы 9.11>
New	Quit:	n
New	Close	Close All Save As Quit Untitled!0]: n
New	Close	CloseAll SaveAs Quit Untitled[l] Untitled(O): s
Заголовок i		нового окна: one
New	Close	CloseAll SaveAs Quit one[l] Untitled(O): 0
New	Close	Close All Save As Quit Untitled[0] one(l): s
Заголовок i		нового окна: two
New	Close	Close All Save As Quit two[0] one[l]: n
New	Close	CloseAll SaveAs Quit Untitled[2] two10] one[l]:s
Заголовок i		нового окна: three
New	Close	Close All Save As Quit three[2] two[0] one[l]: 0
New	Close	Close All Save As Quit two[0] three!2] one II]: c
New	Close	Close All Save As Quit three(2) one(lj: a
New	Quit:	q
*/
Письменные упражнения
9.1	Предположим, что выполняется следующая последовательност] торов: Node<int> *pl, *р2; pl » new Node<int>(2); р2 = new Node<int>(3); Что выводится каждым сегментом программы?
(a) (6)	cout « pl->data « " " « p2->data « endl; pl->data = 5; pl->InsertAfter(p2); cout « pl->data « ” •» « pl->NextNode()->data « endl;
(в)	pl->data = 7; p2->data - 9; p2 = pl; cout « pl->data « " " « p2->data « endl;
(Г)	pi->data =8; p2->data «= 15; p2->lnsertAfter(pl); cout « pl->data « " " « p2->NextNode ()->data « endl;
(Д)	pl->data =77; p2->data “17; pl->InsertAfter(p2); p2->lnsertAfter(pl); cout « pl->data « " * « p2->NextNode ()->data « endl;
9.2	Имеется следующий связанный список объектов Node и указателе Р2, РЗ и Р4. Для каждого сегмента кода нарисуйте аналогичз*1 сунок, указывающий, как изменяется список.
Head
PI	P2	РЗ	P4
(a)	₽2 “ Pl_>NextNode();
(6)	head = Pl->NextNode();
(в)	₽3->data = Pl->data;
(r) P3~>data = Pl->NextNode()->data;
(д)	p2 e Pl->DeleteAfter(); delete P2;
(e)	P2->insertAfter(new Node<int>(3));
(ж)	Pl->NextNode()->NextNode()->NextNode()->data - Pl->data;
(3)	Node<int> *P = Pl; while(P != NULL) {
P->data *= 3;
P =• P->NextNode () ;
J
(и)	Node<int> *P •= Pl; while(P->NextNode() ’= NULL) {
P->data *= 3;
P » P->NextNode(); )
Head
9-3 Напишите сегмент кода, создающий связанный список со значениями Данных 1..20.
9*4 Распечатайте содержимое связанного списка после каждого из следующих операторов. Для оператора cout укажите выход.
Node<char> *head, *р, *q;
* head = new Node<char>('В');
head = new Node<char>('A',head);
4 * new Node<char>(*C');
P ” head;
P - p->NextNode();
P~>InsertAfter(q);
cout « p->data « " " « p->NextNode ()->data « endl;
q - p->DeleteAfter();	I
delete q; q « head; head = head->NextNode(); delete q;
9.5	Напишите функцию
template <class T>
Node<T> *Copy(Node<T> *p);
которая создает связанный список, являющийся копией списка, на>1 нающегося с узла р.	j
9.6	Предположим, что вы пишете метод класса Node и таким обра301
можете изменять следующий данное-член. Опишите результат выпЛ нения этих операторов. Так как код встречается в методе, вы име-доступ к указателям next и this.
ttode<T> *р;
(а)	р - next;	I
p->next = next;
(б)	р = this; next->next  р;
(в)	next = next->next;
(г)	р  this; next->next » p->next;
9.7	Вместо использования указателя на голову связанного списка объект Node мы можем поддерживать заголовочный узел. Его значение данн-i не считается частью списка, и его следующий данное-член указывав на первый узел данных в этом связанном списке. Заголовок называете узлом-часовым и позволяет избежать пустого списка.
Header
Предположим, что ведется связанный список целых с использование' заголовочного узла:
Node<int> header(0);
(а) (б) 9.8
Напишите последовательность кода для вставки узла р в начало списк Напишите последовательность кода для удаления узла из начала списк Это упражнение предполагает, что связанные списки создаются и по держиваются с использованием класса Node. Два упорядоченных спя ка: L1 и L2 могут быть объединены в третий список L3 последовате^1 ним прохождением двух этих списков и вставкой узлов в третий спй В любом месте вы рассматриваете одно значение из L1 и одно из вставляя меньшее значение в хвост L3. Например, рассмотрим сЛе^а,< щую последовательность вставок. Текущие элементы, которые РаС риваются в L1 и L2, обведены кружком.
1 Поскольку значение данных в этом узле не используется, такие данные назваВЬ1 данньхе-заполнитель. — Прим. ред.
Шаг1
Шаг 2
ШагЗ
Шаг 4
Напишите функцию
void MergeLists(Node<T> *L1, Node<T>* L2, Node<T>*
&L3) ;
для объединения LI и L2 в L3.
9.9	Каково действие этой последовательности кода?
while(currPtr !“ NULL)
(
currPtr->data + 7;
currPtr  currPtr->NextNode{);
9.10	Опишите действие функции F:
template <class T> void F(Node<T>* «head) I
Node<T> *p, *q;
if (head !+ NULL&& head->NextNode() !-NULL> (
<3 - head;
head =» p « head->NextNode ();
while(p->NextNode() I- NULL) p = p->NextNode();
p->InsertAfter(q); }
}
9-11 Напишите функцию
template <class T>
int CountKey(const Node<T> *head, T key) i
которая подсчитывает количество вхождений ключа в списке. Напишите функцию
template «class Т>
void DeleteKey(Node<T>* &headz T key;
которая удаляет из списка все вхождения ключа.
9.13
9.14
Измените функцию InsertOrder в разделе 9.2, так чтобы значения иых добавлялись в начало последовательности копий.

Для каждого из пунктов (а) — (d) каков список, получающийся волнением заданной последовательности команд:

LinkedList<int> L; int i, val;
(a)	for(1=1;i <« 5;i++)
L.InsertFront 1*1};
(6)	for(i=l;i <= 5;i++)
L.InsertAfter(2*i);
(B) for(i=l;i <= 5;i++)
L.InsertAt(2*i)
(r) for(i-l;i <- 5;i++i {
L-InsertAt(i);
L.NextO;
L.InsertAt(2*i) val - L.DeleteFront()t )
(д)	Используя InsertFront, напишите оператор for для создания списка:
50, 40, 30, 20, 10
(е)	Повторите пункт (е), используя InsertRear и InsertAfter.
9.15	Предположим следующие объявления:
LinkedList<int> L;
int i, val;
Допустим, что связанный список L содержит значения 10, 20, 30 .... 100. Каковы значения данных в списке после выполнения каждого из следующих пунктов?
(а)	L. Reset О; for(i-l;i <- 5;i+)
L.DeleteAt();
(6)	L.Reset О; for(i“l;i <= 5;i++) {
L. DeleteAt () L.NextO;
(в)	L.ResetO; for(i-l;i <- 3;i++) {
vail - I.DeleteFront();
L.Next О i L.InsertAt(val);
9.16	Напишите функцию
template <class T>	-2);
void Split (const LinkedList<T>& I», LinkedList<T>fc LI, LinkedList<T>&
которая принимает список L и создает два новых ст 	- -
содержит первый, третий, пятый и последующие нечетные узлы. L2 содержит четные узлы.
g 17 Предположим, что L — это список целых. Напишите функцию
void OddEven(const LinkedList<int>K L, LinkedList<int>fi LI, LinkedList<int>& L2);
которая принимает связанный список L и создает два новых списка L1 и L2- Список L1 содержит узлы списка L, значения данных которых являются нечетными числами. L2 содержит узлы, значения данных которых являются четными числами. Используйте итератор.
9.18	Напишите метод класса List
int operator* (const List<T>s L);
который конкатенирует список L на конец текущего списка. Возвращается 1, если оператор выполнен правильно или 0, если память для новых узлов не могла быть выделена. Не допускайте конкатенацию списка на самого себя. В этом случае должен возвращаться 0.
9.19	Напишите функцию
template <class Т>
void DeleteRear(LinkedList<T>& L) ;
которая удаляет хвост списка L.
9,20	Нарисуйте рисунок связанного стека
Stack<int> L;
целых данных после серии операций:
Push(l), Push(2), Pop, Push(5), Push(7), Pop, Pop.
9.21	Реализуйте класс Stack, включая объект LinkedList с помощью композиции.
9.22	Реализуйте класс Stack, сопровождая связанный список объектов Node. 9*23 Каково действие этой функции?
template <class Т>
void Actions(LinkedList<T>& L) <
Stack<T> S;
for( L.Reset(); IL.EndOfList().- L.NextO ) S.Push(L.Data( ) );
L. ResetO;
While( !S.StackEmpty() ) <
L.DataO = S.PopO ;
L.NextO > } }
Нарисуйте рисунок связанной очереди
Queue«int> Q;
целых данных после серии операций
9.25
9.26
Qlnsert(1), Qlnsert(2), QDelete, Qlnsert(5), Qlnsert(7), QDelete, QInsert(g Обязательно включите указатель rear в рисунок.
Каково действие этой функции?
template «class Т>
void ActionQ(LinkedList«T>s L, Queue<T>& q) <
Q.QClear();
for( L.Reset(); 'L.EndOfList();L.Next() )
Q.Q.Insert(L.Data());
Измените класс Queue в разделе 9.6, так чтобы ои содержал функции члены
T FeekFront(void);
T FeakRear(void);
которые возвращают значения данных в начале и в хвосте очереди, соответственно.
9.27	Класс Queue не имеет явного оператора присваивания. Объясните, по чему два Queue-объекта Objl и Obj2 могут появиться в операторе Obj2 = Objl;
9.28
Реализуйте функцию Replace,
выполняющую
поиск значения данных
в циклическом связанном списке.
template «class Т>
CNode<T> ‘Replace(CNode«T> ‘header, CNode«T> ‘start, T elem, T newelem);
Начиная в узле start, сканируйте список, выполняя поиск elem. Есл! elem будет найден, замените его новым значением данных newelem > возвратите указатель на совпадающий узел; иначе, возвращайте NULL Примечание: при сканировании вы можете проходить заголовок.
9.29 Реализуйте функцию
template «class т>
void Insertorder(CNode«T> ‘header, CNode<T> *elem);
которая вставляет узел были упорядочены.
elem
в циклический
список,
так
чтобы
данные
9.30 Рассмотрите структуру
template «class Т> struct Clist (
CNode«T> header;
CNode«T> ‘rear; };
Она определяет циклический список с указателем rear.
разработайте функции
// вставить узел р в начало списка L template cclass t>
void InsertFront(CListCT>& L, CNode<T> *p);
// вставить узел p в хвост списка L template cclass t>
void InsertRear(CListcT>& L, CNode<T> *p)
// удалить первый узел списка L
template cclass t>
CNodecT> *DeleteFront(CListct>& L);
Обеспечьте правильное поддержание rear, так чтобы в любое время он указывал на последний узел списка.
9.31
(а)	Напишите функцию
template cclass Т> void Concat(CNode<T>b s, CNodeCT>& t);
которая конкатенирует циклический список с заголовком t на конец циклического списка с заголовком s.
(б)	Напишите функцию
template cclass Т> int Lenght(CNodeCT>& s);
которая определяет количество элементов в циклическом списке с заголовком s.
(в)	Напишите функцию
template cclass Т>
CNodecT> *lndex(CNodeCT>« s, T elem);
которая возвращает указатель на первый узел в циклическом списке с заголовком s, содержащий значение данных elem. Возвращайте заголовок, если elem не найден.
(г)	Напишите функцию
template cclass Т>
void Remove (CNodeCT>& s, T elem);
которая удаляет все узлы из списка s, содержащего значение данных elem.
Напишите функции-члены
DNode<T> * Dele teNodeRight(void);
CNodecT> *DeleteNodeLeft(void);
0J
для класса двусвязных узлов. DeleteNodeRight удаляет узел справа текущего узла, a DeleteNodeLeft удаляет узел слева от него.
Упражнения по программированию
9.1	Это упражнение расширяет функции-утилиты для узлов в файле по<ъ> llb.h. Напишите функции, имеющие следующие объявления:
// удаление хвостового узла в списке;
// возврат указателя на удаленный узел template <class Т>
Node<T> *DeleteRear(Node<T> * & head);
// удаление всех вхождений ключа в списке
template <class Т>
void DeleteRear(Node<T> * & head, const T& key);
Напишите программу для тестирования этих функций.
□	Введите 10 целых и сохраните их в списке, используя функцию InsertFront. Используйте PrintList для вывода списка.
□	Введите целое, которое служит в качестве ключа. Используйте DeleteKey для удаления из списка всех вхождений ключа. Распечатайте итоговый список.
□	Удалите все узлы списка, используя DeleteRear. Для каждого удаления выводите значение данных.
9.2	Для этого упражнения используйте класс Node. Полем данных для упражнения является структура IntEntry.
struct IntEntry
{
int value;	// целое число
int count;	// вхождения значения
Введите 10 целых и создайте упорядоченный список IntEntry узлов, использующих поле count для указания дубликатов в списке. Распечатайте итоговую информацию об узлах, которая должна включать отдельные целые значения и количество вхождений каждого целого списке. Вам следует изменить функцию InsertOrder, чтобы при нахождении дубликата обновлялись значения данных узла.
Введите ключ и удалите все узлы, значение данных которых болЫПе» чем ключ. Распечатайте итоговый список.
9.3	Для этого упражнения используйте класс Node. Данные представ^^ служащего: его имя (name), id-номер (idnumber) и почасовую оП его труда (hourlypay).
struct Emplyee
(
char name[20];
int indnuinber;
float hourlypay;
Перегрузите операторы ввода/вывода « и » для служащего и используйте PrintList для вывода списка. Введите следующие записи данных и сохраните их в связанном списке.
40 9.50 Dennis Williams
Ю 6.00 Harold Barry
25 8-75 Steve Walker
Реализуйте следующие операции обновления, изменяющие список:
(»)
Считайте id-номер и выполняйте поиск записи данного служащего в списке. Если запись найдена, увеличьте почасовую оплату на $3,00. Выведите итоговый список. (Вам необходимо перегрузить оператор —=.)
(б)	Сканируйте список и удаляйте всех служащих, получающих более $9,00 в час. Выведите итоговый список.
9.4	Два списка L = {Lo, Li, . . . Ц и М = {Mo, Mi, . . . Mj) могут быть объединены по парам для получения списка {Lo,Mo, Li,Mi, . - . Li,Mj, . . . Lj.Mj}, j > i. Напишите программу, используя класс Node, которая генерирует список Lei числами (i < i < 10) и список М с j числами (1 < j < 20). Элементы ввода в списке L являются случайными числами в диапазоне от 0 до 99, а элементы ввода в М — это случайные числа в диапазоне от 100 до 199. Программа выводит начальные списки L и М, объединяет их в новый список N и выводит его.
9.5	Повторите упражнение 9.4, используя два LinkedList-объекта L и М.
9.6	Используя класс Node, напишите программу, вводящую 5 целых в связанный список, используя следующий алгоритм:
Каждое вводимое N вставлять в голову списка. Сканировать оставшийся список, удаляя все узлы, которые меньше, чем N.
Выполните программу три раза, используя следующие входные данные. Распечатайте итоговый список.
1,	2, з, 4, 5
5, 4, 3, 2, 1
3, 5, 1, 2, 4
9.7	Класс LinkedList содержит как конструктор копирования, так перегруженный оператор присваивания. Напишите тестирующую программу, которая оценивает правильность этих двух методов.
9.8	Используйте класс String из главы 8 для считывания разделенных пробелами значащих символов текста. Введите строку, которая может включать строки, начинающиеся с Например:
If t и includelist являются опциями run -t -includelist linkdemo
В этом примере строки включают опции (символы, начинающиеся с и символы, не являющиеся опциями. Используя класс LinkedList, сохраните незначащие символы в связанном списке tokenlist и символы-опции — в связанном списке optionlist. Распечатайте символы из каждого списка.
9.9	Положительное целое и (п>1) может быть записано единственны^ разом как произведение простых чисел. Это называется разложен числа на простые числа. Например,	Ue4
12 = 2*2*3	18 = 2*3*3	11 = 11
Функция LoadPrimes использует структуру записей IntEntry из уп-п нения по программированию 9.2 для создания связанного списка^ торый определяет различные числа и количество вхождений просЛ^ чисел в разложение числа. Например, в случае с 18 простое ni } встречается 1 раз, а простое число 3 встречается 2 раза.	*
void LoadPrimes(LinkdList<IntEntry> iL, int n) <
int i - 2;
int nc я0; // счетчик повторений
do
{
if (n % i -- 0)
{
nc++;
n - n/i;
} else { if (nc > 0)
Оагрузить i и nc как узла в хвост списка> nc - 0;
I++;
}
while (n > 1);
Напишите программу, которая вводит два целых М и N и использует функцию LoadPrimes для создания связанного списка простых чисел. Сканируйте список и выводите разложение каждого числа на простые числа.
60:
18:
(2 • 2 • 3 * 5)
как сче^--множителей
Создайте новый список, состоящий из всех простых чисел, общих ДЛЯ списков М и N. Когда вы определите каждое такое простое число» возьмите минимальный счетчик в двух узлах и используйте в узле для нового списка. Например, 2 является простым 60-ти, и 2 — это простой множитель 18-ти.
18 -2*3*3	// 2 имеет счетчик 1
60 = 2*2*3*5	//2 имеет счетчик 2
1,2)-дНВче*
В новом списке 2 является значением со счетчиком 1 (минимум Результирующее число является наибольшим общим делителем
НИЙ М и N, GCD(M.N).
9.10	Полином n-ной степени является выражением в форме
f(x) - алхл + an_ixnti +
+ а2х2 + а^1 + аох0
где термы ai называются коэффициентами. Используйте класс IjnkedList для этого упражнения с записью данных Term, содержащей коэффициент и показатель степени х для каждого терма.
Struct Term { double coeff; int power;
i;
В программе введите полином как ряд пар коэффициентов и показателей степени. Завершайте программу при вводе коэффициента 0. Сохраните каждую пару (коэффициент/показатель степени) в связанном списке, упорядоченном по показателю степени.
(а)	Напишите каждый терм результирующего полинома в форме
at *
(6)	Введите 3 значения х и вызовите функцию
double poly(LinkedList<Term>4 f);
которая вычисляет полином для значения х и выводит результат.
9.11	Это упражнение использует функции, разработанные в письменных упражнениях 9.12 и 9.13. Считайте упорядоченный список из 10 целых значений и распечатайте его. Запросите у пользователя какое-либо значение данных и, используя СоцпЖеу, определите, сколько раз это значение встречается в списке. Используйте DeleteKey для удаления из списка всех вхождений этого значения. Выведите новый список.
9.12	Протестируйте функцию MergeLists в письменном упражнении 9.8, используя целые данные. Включите списки Ы={1,3,4,6,7,10,12,15} и L2={3,5,6,8,11,12,14,18,22,33,55} в свои тесты.
9.13	Используйте класс CNode для разработки класса Queue. Объявите заголовочный узел в закрытой секции класса и выполните вставки и удаления следующим образом:
QInsert: InsertAfter текущий узел и перейти к новому узлу.
QDelete: DeleteAfter заголовочный узел. Будьте осторожны и выполняйте проверку на удаление из пустой очереди.
Пустая очередь
front, rear
from
rear
9.14
Протестируйте вашу реализацию, вставляя целые значения lt 2 10 в очередь. Сделайте очередь пустой и распечатайте значения. ’
Мы можем реализовать класс Set с элементами типа Т, сохраняя кальные элементы данных в связанном списке. Реализуйте эти функции
Унц.
Дйв
template <class Т>
LinkedList<T> Union(LinkedList<T>b x, LinkedList<T>fi y);
template <class T>
LinkedList<T> Intersection(LinkedList<T>8 x, LinkedList<T>& y);
Union возвращает связанный список, представляющий множество всех элементов, которые найдены, по крайней мере, в одном х или у. Inter section возвращает связанный список, представляющий множество всех элементов, находящихся как в х, так иву. Пусть А будет множеством целых {1,2,5,8,12,33,55}, а В — множеством {2,8,10,12,33,88.99}. Используйте эти две функции для вычисления и вывода
A U В - {1, 2, 5, 8, 10, 12, 33, 55, 88, 99} и
АЛ В = (2, 8, 12, 33}
9.15	Начнем с пустого циклического двусвязного списка целых. Введите 10 целых, вставляя положительные числа непосредственно справа от заголовка, а отрицательные числа — слева. Распечатайте этот список. Разделите список на два односвязных, содержащих положительные и отрицательные числа, соответственно. Распечатайте каждый список.
9.16	Это упражнение изменяет класс Window из раздела 9.10. Добавьте метод SaveAll. Он должен проходить список окон от переднего окна до заднего и выводить заголовок каждого окна и сообщение Заголовок: <window-зaгoлoвoк>. Если окно в текущий момент является окном без заголовка (Untitled), выдайте запрос на ввод заголовка.
Рекурсия
ЮЛ. Понятие рекурсии
10.2.	Построение рекурсивных функций
10.3.	Рекурсивный код и стек времени исполнения
10.4.	Решение задач с помощью рекурсии
10.5.	Оценка рекурсии Письменные упражнения
Упражнения по программированию
Рекурсия является важным инструментом решения вычислительны» математических задач. Она широко используется для определения синтакс * языков программирования, а также в структурах данных при разработ^ алгоритмов сортировки и поиска для списков и деревьев. Математики п?** меняют рекурсию в комбинаторике для разного рода подсчетов и вычисле вероятностей. Рекурсия — важный раздел, имеющий теоретическое и пи8* тическое применение в общей теории алгоритмов, моделях исследования о раций, теории игр и теории графов.	е*
В данной главе мы даем общее введение в рекурсию и иллюстрируем применение различными приложениям. В следующих главах рекурсия бул** использоваться для изучения деревьев и сортировки.	**
10.1.	Понятие рекурсии
Для большинства людей рекурсивное мышление не является характерным Если вас, допустим, попросят определить степенную функцию хп, где х  действительное число, ап — неотрицательное целое, то типичным ответом будет следующий:
Хп — X * X * X * ... * X * X
п множителей
Вот, например, различные значения степеней двойки:
2°  1	//по определению
21 - 2
22 = 2 * 2 = 4
23 - 2 • 2 * 2 = 8
2‘  2 * 2 * 2 * 2 = 16
Функция S(n) вычисляет сумму первых п положительных целых — задача, которая решается путем многократного сложения.
п
S(n) - Ji = l + 2 + 3 + ... + n-l + n 1
Например, для 8(10) мы складываем первые 10 целых, чтобы получить ответ 55: ю
S(10) -£± = 1+ 2 + 3 + ... + 9 + 10 = 55 1
Если мы применим этот же алгоритм для вычисления S( вторит все эти сложения. Более практичным подходом было I предыдущий результат для 8(10), а затем добавить к нему 11, ответ Б(11) = 66: 11
S(11) - У 1 = S(10) + 11 - 55 + 11 = 66 1
При таком подходе для получения ответа используется предыдущий Р® эультат вычислений. Мы называем это рекурсивным процессом.
L1), процесс ио-1ы использовать чтобы получить
Дернемся к степенной функции и представим ее с помощью рекурсивного цесса. Подсчитывая последовательные степени двойки, мы заметили» что значение может быть использовано для вычисления следующего, ^фимер, з₽2*22“2*4==8 ^1*2^2*8 = 16
Поскольку мы имеем начальную степень двойки (2° = 1), последующие стедеям есть всего лишь удвоение предыдущего значения. Процесс ис-вользования меньшей степени для вычисления очередной приводит к рекур-^вдому определению	~	"	*
хп опреДелЯется как
степенной функции. Для действительного х
значение
(1,	при п = О
х *	при п > О
Похожее рекурсивное определение описывает функцию S(n), дающую сумму первых п целых чисел. Для простого случая S(l) сумма равна 1. Сумма S(n) может быть получена из S(n-l):
{1,	при n = 1
n + S(n-l), при п > 1
Рекурсия имеет место, когда вы решаете какую-то задачу посредством разбиения ее на меньшие подзадачи, выполняемые с помощью одного и того же алгоритма. Процесс разбиения завершается, когда мы достигаем простейших возможных решаемых подзадач. Мы называем эти задачи условиями останова. Рекурсия действует по принципу "разделяй и властвуй".
Алгоритм определен рекурсивно, если это определение состоит из
1.	Одного или нескольких условий останова, которые могут быть вычислены для определенных параметров.
2.	Шага рекурсии, в котором текущее значение в алгоритме может быть определено в терминах предыдущего значения. В конечном итоге шаг рекурсии должен приводить к условиям останова.
Например, рекурсивное определение степенной функции имеет единственное условие останова для случая п = О (х° = 1). Шаг рекурсии описывает случай
в х * х{п1), при п > О
Рекурсивные определения
-° языках программирования применяются разнообразные рекурсивные °пР6Деления синтаксиса. Большинство читателей знакомо с синтак-^ескими диаграммами. Приведенная ниже диаграмма описывает иденти-(identifier)у который состоит из начальной буквы и необязательно за не® Цеп°чки букв и цифр. Например, class, float и var4 — С^имые идентификаторы, которые могут встречаться в программе на * в то время как Х++ и 2team идентификаторами не являются.
Пройдите эту диаграмму слева направо, двигаясь по стрелкам. Во впл< движения включайте каждый символ, содержащийся в прямоугольнике^ пример, простейший проход по диаграмме заключается в движении от вх01 к выходу через первый прямоугольник "буква". Это соответствует однобукве* ному идентификатору, например. A, n, t и т.д. Для каждого из приведение ниже идентификаторов описан путь по диаграмме.	[
Идентификатор
А
XY3
7А
Путь
Войти в прямоугольник "буква (А)", выйти из диаграммы
Войти в прямоугольник "буква (X)", пройти по стрелке через прямоугольник 'буква (Y)', пройти через прямоугольник "цифра (3)’, выйти из диаграммы.
Недопустимый идентификатор; нет выхода из диаграммы.
Несмотря на то, что синтаксические диаграммы эффективны для описан-1 некоторых языков, синтаксис фактически всех языков программирована описывается с помощью рекурсивной нотации, называемой формой Баку-Наура (Bakus-Naur Form, BNF). Она была разработана для определения ?. гола 60 и состоит из правил подстановки, определяющих, как нетерминал: ный символ может быть замещен другим нетерминальным или терминалы; i символом. В правилах подстановки используется знак ’j" для разделен^ альтернативных подстановок. Нетерминальные символы заключены в угл1 вые скобки ’*<>*’ н представляют собой такие языковые конструкции, к^ идентификаторы и выражения. Терминальные символы определяют фак?! ческие знаки в языке. Например, BNF-определение идентификатора пр’’ ставляется приведенным ниже правилом. Символ "идентификатор" — нете, минальный символ, расположенный слева от оператора
<идентификатор> ::и <буква> I <идентификатор> <буква> I
<идентификатор> <цифра>
<буква> ::= a|b|C|d|e|f|g|h|i|j|k|l|m|n|o|p|q|ns|t|u|v|w|X|y|z
<цифра> = 0|1|2|3|4|5|€|7|8|9
Эти правила указывают, что идентификатор есть буква или буква с п• j ледующей цепочкой букв или цифр. Обратите внимание, что идентифйка^| определяется в терминах самого себя, поэтому его определение рекурсИ*~Ч Интерпретация рекурсивных определений является естественным при- | нением для рекурсивных функций. Например, некоторые компилятор*1 | пользуют для трансляции программ в машинный код BNF-определение ГР i матики языка и алгоритмы рекурсивного спуска. Различные правил* J становкн кодируются как функции, которые могут завершаться в*13 1 самих себя или других правил. Процесс может заключать в себе кос*® рекурсию (inderect recursion), когда, например, правило Р вызывает ПР Q, которое завершается вызовом правила Р.
Пример 10.1
Упрощенные арифметические выражения, допускающие только бинарные операторы * и /, имеют следующую ВКГформулировку:
<выражение> :: = <терм> + <терм> I <терм> - <терм> | <терм>
<черм> ::• <мкожитель> ♦ <мнояитель> I <множитель> / <множитель> I <множитель>
<мн©житель> (<выражение>) | <буква> | <цифра>
Примерами выражений являются:
А +• 5; В*С + D; 2* (3+4+5); (А+В*С)/D
Эти правила применяют косвенную рекурсию. Например, выражение может быть термом, который может быть множителем, который может быть выражением, заключенным в скобки. Следовательно, выражение косвенно определено в терминах самого себя.
Рекурсивные задачи
Сила рекурсии обеспечивает весьма простые и изящные решения ряда проблем. Приведем обзор задач и методов их решения, использующих рекурсию.
Ханойская башня. Любители головоломок долго были увлечены задачей о ханойской башне.
Шпиль А	Шпиль В	Шпиль С
Согласно легенде, у жрецов храма Брахмы есть медная платформа с тремя алмазными шпилями. На одном шпиле А нанизано 64 золотых диска, каждый Из Которых немного меньше того, что под ним. Конец света наступит, когда жрецы переместят диски со шпиля А на шпиль С. Однако задача имеет весьма специфические условия. За один раз можно перемещать только один диск, и этом ни разу диск большего размера не должен лечь на диск меньшего п|?МеРа. Несомненно, жрецы все еще работают, так как задача включает в себя г — 1 хода. Если тратить по одной секунде на ход, то потребуется 500 Миллиардов лет.
Ханойская башня — тяжелое испытание для решателя головоломок. Между опытный программист видит быстрое рекурсивное решение. Мы проиллю-^ируем проблему на шпилях, содержащих шесть дисков. Начнем, сосредо
Шпиль А
Шпиль В
Шпиль С
।
точившись на перемещении верхних пяти дисков на шпиль В и последующ перемещения самого большого диска на шпиль С.
Осталась более простая задача перемещения только пяти дисков со Щпй В иа шпиль С. Применяя тот же самый алгоритм, сосредоточим внимали^ верхних четырех дисках и вытащим их из стопки. Затем перенесем caia большой диск со шпиля В иа шпиль С. Осталась еще меньшая стопка четырех дисков. Процесс продолжается до тех пор, пока ие останется диск, который в конечном счете перемещается на шпиль С.
Очевидно, что данное решение носит рекурсивный характер. Проблема разбивается на последовательность меньших подзадач одного и того же типа. Условием останова является простая задача перемещения одного диска.
Лабиринт. Каждый знаком с проблемой перехода через лабиринт с возможностью бескоиечиого числа альтернатив, приводящих в тупики и окончательному запутыванию. Психологи используют лабиринт с приманкой в виде сыра для исследования поведенческих моделей у крыс. Мы полагаем, что неудачи зверька в деле поиска сыра следуют из его неспособности мыслить рекурсивно. Рассмотрите рис. 10.1 и обдумайте стратегию, гарантирующую надежный способ пройти до "Конца''. Мы представляем важное средство решения задач, называемое возвратами (backtracking).
Рис. 10.1 Обход лабиринта
Дабиринт есть связанное множество перекрестков. Каждый перекресток get три связанных с иим значения: налево, прямо и направо. Если некоторое пение равно 1, то движение в данном направлении возможно. Нуль пока-г^вает, что движение в данном направлении заблокировано. Множество из нулей представляет мертвую точку, или тупик. Проход по лабиринту Читается успешным, когда мы достигаем точки "Конец". Процесс включает яд рекурсивных шагов. В каждом перекрестке мы исследуем наши варианты. РсЛЯ возможно, то сначала идем налево. Достигнув очередного перекрестка, йЫ снова рассматриваем свои варианты и пытаемся идти налево. Если выбор левого направления заводит в тупик, мы возвращаемся и выбираем движение тгоямо, если это направление не заблокировано. Если этот выбор заводит в мы возвращаемся и идем направо. Если все альтернативы движения из данного перекрестка ведут в тупики, мы возвращаемся к предыдущему перекрестку и делаем новый выбор. Эта довольно скучная и консервативная стратегия не побьет рекорды эффективности, но гарантирует, что в конце концов мы найдем ВЫХОД из лабиринта. Рассмотрим первые 10 вариантов в нашем лабиринте.
Результирующий перекресток 2 3 7 3 4 6 4 5 2 8
Выбор
прямо налево
налево
тупик; вернуться к 3
прямо
прямо
тупик; вернуться к 4
прямо
тупик; вернуться к 4, 3, 2 прямо
Перекресток
1:
2:
3:
7:
3:
4:
б:
4:
5:
2:
Комбинаторика. Рекурсия находит широкое применение в комбинаторике — разделе математики, касающемся подсчета объектов. Предположим, мы бросаем три игральные кости и записываем общий итог. Одним из вопросов Комбинаторики является количество различных способов, которыми можно Набрать 8 очков. Рекурсивный подход пытается свести проблему к постепенно Упрощающимся задачам и использовать эти результаты для решения более сложной проблемы. В нашем случае три игральные кости считаются сложной 5 Проблемой, и мы фокусируем внимание на более простом случае бросания двух Игральных костей. Мы предполагаем, что можем взять две игральные кости и любой результат N в диапазоне от 2 до 12 и определить все различные способы впадения N. В случае трех костей, дающих в сумме 8, мы бросаем первую и записываем значение в таблице вместе со значением N, представляю-оставшееся количество очков, которое должно быть набрано двумя сле-?УюЩими костями. Например, если на первой кости выпало 3, следующие две дать в сумме 5. Используя наши навыки с двумя игральными костями, еляем’что есть четыРе возможных исхода бросания двух костей, дающих Ъединяя эги исходы с тройкой на первой кости, мы имеем как минимум ^ре способа получить восьмерку на трех костях.
Кд ° СЛеДУющей таблице перечислены все 15 способов выпадения восьмерки М0Л₽ех игральных костях. Поскольку количество бросаний невелико, мы перечислить все варианты, не прибегая к рекурсии. Мы проверим по-Мо слУчаи, когда решение с помощью таблицы практически неосуществи-
Кость! 1	№ 7	Различные исходы на двух костях	Количество 0
2	6	(3.3) (4.2) (2.4) (S.1) (1.5)	5
3	5	(4.1) (3.2) (М) (2,3)	4
4	4	(3,1) (2,2) (1.3)	3
5	3	(2.1) (1,2)	2
6	2	(1.1)	1
			15
Чтобы подсчитать вероятность выпадения восьмерки на трех костях, ед дует разделить 15 на общее число различных исходов бросания трех костей Это число равно 63 ~ 216. Таким образом, вероятность выбросить восьмет® равна 15/216, или 7%.
I
j ii
I
I
i,
4
I
•I
3
i
i
lj
ill
.1;
Синтаксические деревья. На этапе нашего изучения стеков вводятся фиксный и постфиксный (RPN) форматы записи арифметических выражена' Эти форматы можно сравнить между собой, запоминая операторы и операнду в виде узлов бинарного дерева. Операнды помещаются в листовые узлы в конце ветви. Предшествование оператора отражается его уровнем на дер*^ Оператор на более глубоком уровне дерева должен быть выполнен до опер , тора менее глубокого уровня. Например, выражение	j
а * ь + с/а
представляется соответствующим деревом с семью узлами:
Как и в случае с лабиринтом, мы можем разработать методы рекурсивно прохождения, выбирающие те или иные варианты в каждом узле ДеР Предположим, мы руководствуемся следующими правилами:
Если возможно, идти по левой ветви.
Выписать значение, содержащееся в узле.
Если возможно, идти по правой ветви.	j
Поскольку мы выписываем значение узла в между директивами ПРоХ ' дения, назовем это симметричным прохождением (inorder scan). F водится последовательность прохождения узлов синтаксического дере ловие останова возникает, когда нельзя пройти ни по левой ни по ветви.
1 В книге Д.Кнута "Искусство программирования для ЭВМ”, т.1, Основные алгор метод называется концевым от оригинального enorder. — Прим.ред.
действие Н^ть С корня	Результирующий узел	Вывод
	+	
J и по левой ветви MrtTVI п° левой ветви (нет левой ветви из а) ^писать значение И «нет правой ветви из а) (возврат к узлу •; движение влево завершено)	а	а
Выписать значение	_	а*
Идти ПО правой ветви (нет левой ветви из Ь) Выписать значение	о	а*Ь
(нет правой ветви из о) (возврат к узлу +; движение влево завершено) Выписать значение ^ти по правой ветви	/	а*Ь4
Идти по.левой ветви (нет левой ветви из с) Выписать значение (нет правой ветви из с) (возврат к узлу /; движение влево завершено)	с	а*Ь+с
Выписать значение Идти по правой ветви	d	а*Ь+с/
(нет левой ветви из d)		
Выписать значение		a’b+c/d
(нет правой ветви из d)		
После того как все узлы пройдены, проход завершается и мы имеем инфиксную форму выражения.
Другой порядок рекурсивного прохождения определяется следующими правилами:
Если возможно, идти по левой ветви.
Если возможно, идти по правой ветви.
Выписать значение, содержащееся в узле.
Поскольку выписывание значения узла происходит после обеих директив °бхода, назовем это обратным прохождением (postorder scan). В результате Узлы будут выписаны в следующем порядке:
аЬ * с d / +
Это постфиксная, или RPN-форма записи выражения.
“екурсия является мощным средством определения и прохождения дере-Мы будем использовать разнообразные рекурсивные алгоритмы в гл. 11. ГЛ. 13 разработаем итерационные эквиваленты этих алгоритмов для создания ораторов дерева.
10.2.	Построение рекурсивных функций
рекурсивной функции иллюстрируется задачей вычисления фак-НеотРицательных целых чисел. Мы рассмотрим эту структуру, разра-Ф Как рекурсивное, так и итерационное определение функции.
^°Иа^Т5^иал неотрицательных целых чисел, Factorial(N), определяется как ЧцСд ВеДение всех положительных целых чисел, меньших или равных N. » обозначаемое Nl, представляется следующим образом:
N! = N * (N-l) * (N-2) *...*2*1 Например,
*
*
Factorial(4) =41=4
Factorial(6) = 6! = 6
Factorial(1) = 1! - 1
Factorial(0) = 0! — 1
3 * 2 * 1 - 24
5*4*3*-2*l = 720
//по определению
Итерационная версия этой функции реализуется посредством возврата 1 п=0, и циклом перемножения членов последовательности в противном случае
// итерационная форма факториала long Factorial(long n)
{
int prod =1, i;
// для n == 0 вернуть prod = 1, в противном случае
// вычислить prod = 1*2*..*n
if (n > 0)
for (i = 1; i <= n; i++) prod *= i;
return prod;
}
Рассмотрение членов последовательности в различных примерах факториала приводит к рекурсивному определению функции Factorial(N). Для 4! первое число равно 4, а остальные — (3*2*1) — равны 3!. То же справедливо и для 6!, являющегося произведением 6 и 51.
Рекурсивное определение любого неотрицательного целого п включает в себя как условие останова, так и шаг рекурсии;
П1 -
1, п * (п-1)
при п = 0
при п > 1
// условие останова
// шаг рекурсии
Можно представить себе функцию Factorial(n) как n-мапхину, вычисляющую п! путем п * (п-1)!. Чтобы машина функционировала, она должна быть связана с рядом других машин, передающих информацию вперед и назад. 0-машине ассистирует другая машина. Опишем необходимые связи и взаимодействие машин для 4-машины, вычисляющей 41.
4-машина (4*3!) должна 3-машина (3*21) должна 2-машина (2*1!) должна 1-машина (1*0!) должна
запустить 3-машину запустить 2-машину запустить 1-машину запустить 0-машину
Работа отдельных машин описывается на рис. 10.2. Как только аК^ере. зируется 0-машина, мы сразу в результате получаем единицу, которая дается в 1-машину. У 1-машины теперь есть информация, чтобы завер умножение и передать результат 2-машине.
* 0! = 1 * 1 = 1
Необходимые передаваемые значения становятся доступными
последов'1
тельно от 1-машины до 4-машины.
1-машина использует значение 2-машина использует значение 3-машина использует значение 4-машина использует значение
1 1
2
6
из из из из
0-машины и вычисляет i
1-машины и вычисляет 2 *	₽ 6;
2-машины и вычисляет 3 *	2
3-машины и вычисляет 4
Рис. 10.2. Факториал-машины
При вычислении N! нужно четко различать случай 0!, представляющий «словие останова, и другие случаи (N>0), представляющие шаги рекурсии. Это Различие является фундаментальным для построения рекурсивного алгоритма. Программист реализует распознавание данной ситуации с помощью оператора U ... ELSE. Блок IF обрабатывает условие останова, а блок ELSE выполняет щаг рекурсии. Для факториала блок IF вычисляет единственное условие останова N — 0 и возвращает единицу. Блок ELSE выполняет шаг рекурсии, вычисляя выражение N * (N-l)I, и возвращает результат.
// рекурсивная форма факториала
long Factorial (long n)
// условием останова является n == 0
if (n == 0)
return 1;
else
// шаг рекурсии
return n * Factorial (n-1);
На рис. 10.3 описана последовательность вызовов функции при вычислении Factorial(4). Предположим, что первоначально функция вызывается из главной программы. Внутри блока функции выполняется оператор ELSE с параметрами 3, 2,1 и 0. На последнем вызове выполняется оператор IF с п = 0. По достижении условия останова рекурсивная цепочка вызовов прерывается и начина-
Передаваемые параметры
Рис. 10.3. Набор факториалов
ется серия вычислений в порядке 1*1, 2*1, 3*2 и 4*6. Последнее значец возвращается в главную программу.	*Ц1е
Пример 10.2
Конструкция IF. .ELSE различает условие останова и шаг рекуъл при вычислении степенной функции и суммы из раздела Ю in8* степенной функции значение power(O.O) не определено, и в этом выдается сообщение об ошибке.
: >	1. Степенная функция (рекурсивная форма)
£
// вычислить х в степени п, используя рекурсию float power(float х, int n)
f
// условием останова является n 0
if (n »== 0»
// 0 в степени 0 не определен
if (x -- 0)
cerr « "power(0,0) не определено" « endl; exit(1);
.«'?	else
// x в степени 0 равен 1
return 1; else
// шаг рекурсии:
?	// power(x, n) = x * power(x, n-1)
i return X * power (x, n-1);
4:^ 2. Функция суммирования (рекурсивная форма)
*ЭДЛ|
i // вычислить 1+2+ ... +п рекурсивно int S(int п)
С <
// условием останова является п -=* 1
if (п — 1) return 1;
a	else
// шаг рекурсии: S(n) » п + S(n-l) return п + S(n-l);
}
Программа 10.1. Использование функции Factorial	____
Эта программа иллюстрирует рекурсивную форму факториальной ции. Пользователь вводит четыре целых числа и получает их фактор
((include <iostreajn.h>
// вычислить п! ~ п (п-1) (п-2) ... (2) (1) при 0! = 1 рекурсивно long Factorial(long п) (
// если п С, то 0! = 1; иначе п! « п*(п-1) ! if (п == 0)
return 1;
else return n * Factorial(n-l);
I
void main (void)
{ int x, n;
// ввести 4 положительных числа и вычислить п! для каждого из ник cout « ‘’Введите 4 положительных целых числа:
for (i = 0; i < 4: i++)
* cin » n; cout « n « "!« "« Factorial(n) « end; )
}
/* «выполнение программы 10.1>
= 5040
- 1 = 24
Введите 4 положительных целых числа: 0 7 14 О! = 1 7!
1! 4!
10.3.	Рекурсивный код и стек времени исполнения
Функция — это последовательность инструкций, выполняемых в ответ на ее вызов. Процесс выполнения начинается с того, что вызывающий блок заполняет активизирующую запись (activation record), которая включает список параметров и местоположение следующей инструкции, подлежащей выполнению после возврата из блока.
Параметры	Местоположение
«фактические параметры>	«следующая инструкция>
Активизирующая запись
в Цри вызове функции данные из активизирующей записи заталкиваются стек, организуемый системой (стек времени исполнения). Данные объеди-йются с локальными переменными и образуют активизирующий фрейм, д°ступный
При выходе из функции устанавливается местоположение следующей струкции (рис. 10.4), а данные в стеке, соответствующие активизируй^' записи, уничтожаются. Рекурсивная функция повторно вызывает саму с р используя всякий раз модифицированный список параметров. При этом ледовательность активизирующих-записей заталкивается в стек до тех пока не будет достигнуто условие останова. Последовательное выталкивав этих записей и дает нам наше рекурсивное решение. Функция вычисляй факториала иллюстрирует использование активизирующих записей.
Вызывающий блок
функциональный блок
Рис. 10.4. Вызов функции и возврат
Стек времени исполнения
С помощью примера вычисления факториала от 4 мы проиллюстрируем использование активизирующих записей и стека, создаваемого во время выполнения рекурсивной функции. Начальный вызов факториала производится из главной программы. После выполнения функции управление возвращается в точку RetLockl, где переменной N присваивается значение 24(41):
void main (void) (
int N;
// поместить в стек запись с помощью вызова FACTORIAL(4)
// RetLockl — адрес присвоения N == FACTORIAL(4)
N = FACTORIAL(4);
RetLockl——1 I
Рекурсивные вызовы в функции FACTORIAL возвращают управление в точку RetLock2, где вычисляется произведение п * (п-1)1. Результат вычисл -ния запоминается в переменной temp, чтобы помочь читателю проследить к и продемонстрировать стек времени исполнения:
long FACTORIAL(long n) {
int temp;
if (n === 0)
return 1;	// вытолкнуть из стека активизирующую запись
else
// поместить в стек активизирующую запись с помощью вызова FACTORI^bt
II Retlock2 — адрес вычисления n * FACTORIAL(п-1) temp = п * FACTORIAL(п-1);
RetLock2——1
return temp; // вытолкнуть из стека активизирующую запись
}
) !
Вызывающий блок
FACTORIALS)	Параметр 0	Возврат: RetLock2
FACTORIALS)	Параметр 1	Возврат: RetLock?
FACTORIALS)	Параметр 2	Возврат: RetLock?
FACTORIALS)	Параметр 3	Возврат: RetLo<k2
Главная программа	Параметр 4	Возврат: RetLock!
Рис. 10.5. Стек времени выполнения
Для функции FACTORIAL активизирующая запись имеет два поля.
Выполнение FACTORIALS) инициирует последовательность из пяти вызовов. На рис. 10.5 показаны активизирующие записи для каждого вызова. Записи входят в стек снизу вверх вместе с вызовом из главной процедуры, занимая нижнюю часть стека.
Параметры	Местоположение
long п	<следующэя инструкция>
Активизирующая запись
При обращении к функции FACTORIAL с параметром 0 возникает условие останова, и начинается выполнение последовательности операторов возврата. Когда из стека выталкивается самая верхняя активизирующая запись, управление передается в точку возврата. Очистка стека от активизирующих записей описывается следующими операциями.
Параметр Адрес возврата
0	RetLoc2
	
1	RetLoc2
	
	RetLoc2	|
С-l—_	RetLoc2
	
	RetLocI
Инструкции возврата
Retloc? temp = 1 • 1; // 1 from FACTORIAL(O) return temp; // temp = 1;
Retloc2 temp = 2 ♦ 1; // 1 from FACTOR IAL(1) return temp; // temp = 2;
RetLocZ temp = 3 * 2; // 2 from FACTORIALS) return temp; // temp = 6;
RetLoc2 temp = 4 * 6; // 6 from FACTORIALS) return temp; // temp = 24;
RetLoc2 N = FACTORIALS); // возврат к главной процедуре
10.4.	Решение задач с помощью рекурсии
МуЛ1Г°ГИе вычислительные задачи имеют весьма простую и изящную фор-^Ровку, которая непосредственно переводится в рекурсивный код. В раз
деле 10.1 рассмотрен ряд примеров, включая Ханойскую башню, лаб н комбинаторику. В этом разделе мы расширим диапазон примеров ® • смотрим рекурсивное определение алгоритма бинарного поиска, рещеаи которых комбинаторных задач, разгадку Ханойской башни, а такод/6 струируем класс Maze для работы с общими задачами лабиринтного
Бинарный поиск
I'i
При бинарном поиске берется некоторый ключ и просматривается уц доченный массив из N элементов на предмет совпадения с этим клкш^ Функция возвращает индекс совпавшего с ключом элемента или -1 ° отсутствии такового. Алгоритм бинарного поиска может быть описан р®а сивно.
Допустим, отсортированный список А характеризуется нижним ным индексом low и верхним — high. Имея ключ, мы начинаем иск совпадение в середине списка (индекс mid).
mid • (low* high) /2 Сравнить A (mid) с ключом
Если совпадение произошло, мы имеем условие останова, что позвал нам прекратить поиск и возвратить индекс mid.
Если совпадение не происходит, можно воспользоваться тем фактом, 5 список упорядочен, и ограничить диапазон поиска ’’нижним подсписков (слева от mid) или "верхним подсписком” (справа от mid).	|
Если ключ < A[mid], совпадение может произойти только в левой поло W списка в диапазоне индексов от low до mid-1.	1
Если ключ > AJmid], совпадение может произойти только в правой по вине списка в диапазоне индексов от mld+1 до high.	|
Шаг рекурсии направляет бинарный поиск для продолжения в один * подсписков. Рекурсивный процесс просматривает все меньшие и мень списки. В конце концов поиск заканчивается неудачей, если подсписки чезли. Это происходит тогда, когда верхний предел списка становится мень^ чем нижний предел- Условие low>high — второе условие останова. В j случае алгоритм возвращает -1.	I
Бинарный поиск (рекурсивная форма). В шаблонной версии бинар®^ поиска в качестве параметров используется массив элементов типа Т, чение ключа, а также верхний и нижний граничные индексы. Оператору обрабатывает два условия останова: 1) совпадение произошло; 2) ключ® 4 значения нет в списке. В блоке ELSE оператора IF выполняется шаг ре* > который направляет дальнейший поиск в левый (ключ<А{пп4]) или в *9* | подсписок (ключ>А(т1й]). Тот же алгоритм применяется по принципу > j деляй и властвуй” к последовательности все меньших интервалов, п° произойдет успех (совпадение) или неудача.
// рекурсивная версия бинарного поиска ключевого значения //в упорядоченном массиве А template <с1аав т>
int Binsearch(Т MJ, int low, int high, 7 key) (
int nid;
T Bildvalue/
jf условие останова: ключ не найден Jf (low > high)
return (-1);
//
//
//
сравнить ключ с элементом в середине списка.
если совпадения нет, разделить на подсписки.
применить процедуру бинарного поиска к подходящему подсписку
else
1 mid - (low+high)/2;
roidvalue - Afraid];
// условие останова: ключ найден'
if (key midvalue)
return mid; // ключ найден по индексу mid
// просматривать левый подсписок, если key < midvalue;
//в противном случае — правый подсписок
else if (key < midvalue)
(f шаг рекурсии
return BinSearch(A, low, mid-1, key);
else
// шаг рекурсии
return BinSearch(A, mid+1, high, key);
Программа 10.2. Тестирование функции бинарного поиска
Эта программа считывает список слов из файла vocab.dat в массив Wordlist. Список слов отсортирован в алфавитном порядке. Запрашивается ключевое слово. Если оно будет найдено, печатается его индекс в списке. В противном случае выдается сообщение об отсутствии ключевого слова в данном списке. Функция поиска находится в файле search.h.
♦include <iostream.h>
♦include <fstream.h>
♦include "strclass.h"
♦include "search.h"
void main(void) (
// поиск производится в массиве упорядоченных строк из потока fin String wordlist[50];
ifstream fin;
String word;
int pos, i;
fl открыть файл vocab.dat, содержащий упорядоченные слова fin.open(“vocab.dat");
И читать до конца файла и инициализировать wordlist
1 - о,*
while(fin » wordlistfi])
fl запросить слово
Gout « "введите слово: cin » word;
/I бинарный поиск введенного слова
if ((pos = BinSearch(wordlist,0,ifword)) != -1) cout « word « " есть в списке по индексу "
« pos « endl;
else
cout « word « " отсутствует в списке." « endl; }
/*
<Входной файл vocab.dat>
array
class
file
struct
template
vector
<1-й прогон программы 10.2>
Введите слово: template
template есть в списке по индексу 4
<2-й прогон программы 10.2>
Введите слово: mark
mark отсутствует в списке.
*/
Комбинаторика: задача о комитетах
К комбинаторным относятся алгоритмы подсчета числа способов наступления того или иного события. В классической задаче о комитетах требуется определить число C(N,K), где N и К — неотрицательные целые, равнее количеству способов формирования комитетов по К членов в каждом из общего списка N людей.
Исследуем решение общей задачи на примере с N = 5 и К = 2. Этот упрощенный случай можно применить к маленькой организации и быстро составить исчерпывающий перечень десяти различных вариантов. Обозначим членов этой организации как А, В, С, D и Е. Возможные комитеты изображены вокруг группы людей.
АВ		ВС		BE
АС
А В С D Е		CD СЕ
DE
AD
АЕ		BD
Этот подход ие годится для большего числа членов, и нам нужно зовать стратегию "разделяй и властвуй", чтобы разбить задачу на боле® стые подзадачи.
Упростим проблему, исключив член А из общей группы. Теперь °с четыре человека: В, С, D и Е.
Подзадача 1:
Попросите четверку оставшихся сформировать все возможные комитеты по 2 человека. Получается шесть различных подкомитетов.
Список 1: (В,С), (B.D), (В.Е), (C.D), (С.Е), (D.E)
Заметьте, что ни один из новых комитетов не включает отсутствующий член А.
Подзадача 2:
Попросите четырех членов группы сформировать все возможные комитеты по одному человеку.
(В), (С), (D), (Е)
В каждом из этих комитетов не хватает одного человека. Добавим в них член А.
Список 2: (А,В), (А,С), (A.D), (А,Е)
Требуется, чтобы комитеты из двух человек, сформированные в подзадачах 1 и2, охватили все возможные комитеты первоначальной задачи. Приведенный виже рисунок описывает оба случая.
Подзадача 2 Комитеты из К = 1 членов
Подзадача 1 Комитеты из К = 2 членов
Шесть групп в списке 1 представляют все комитеты, не содержащие член А* Четыре группы в списке 2 представляют все комитеты, содержащие член А. Поскольку комитет должен либо содержать, либо не содержать А, десять комитетов в двух списках представляют собой все возможные варианты комитетов по два человека.
Разработка алгоритма. В анализе по принципу "разделяй и властвуй" (ре-кУрсивном) мы имеем дело с общей проблемой подсчета количества комитетов Иэ К членов, которые можно сформировать из N людей. Удалим персону А и Рассмотрим оставшиеся N-1 человек. Общее число комитетов складывается из комитетов по К членов, выбранных из N-1 человек (член А не включали)» и числа комитетов по К-1 членов, выбранных из N-1 человек (включая . Первая группа насчитывает C(N-1,K), а вторая — C(N-1,K-1) вариантов, ^«выпий комитет из К-1 членов во второй группе расширится до комитета из
Членов, если член А присоединится к этой группе.
* C(N-1,K-1) + C(N-1,K)	// шаг рекурсии
Условия останова состоят из нескольких экстремальных случаев, которые проанализировать непосредственно.
К > N, то нет достаточного числа людей для формирования комитетов. ъа Сл° возможных комитетов по К членов, сформированных из N человек, ва° нулю.
153а*.425
Если К = О, то формируется пустой комитет и это можно сделать одним способом.	|
Если К = N, все должны оказаться в одном комитете. Это может произ * ’ только выбором всех членов списка в этот комитет.	t
C(N,N) = 1 C(N,O) - 1 C(N,K) * 0 при К > N
Объединяя условия останова и шаг рекурсии, можно реализовать ъе сивную функцию conun(n,k) = C(n,k). Эта функция расположена в a comm.h.	Фай*>
// определить число комитетов из к членов, // которые можно сформировать из п человек int comm (int nr int k)
// условие останова: слишком мало народа if (k > n)
return 0;
// условие останова: в комитете все или никого
else if (n ™ k || k ==* 0)
return 1;
else
// шаг рекурсии: все комитеты без персоны А
// плюс все комитеты с персоной А return comm(n-l,k) + comm(n-l, k-Ц;
I
Программа 10.3. Формирование комитетов
Пользователь вводит число кандидатов п и число человек в комитете к. На выходе выдается число С(п,к) возможных вариантов Комитетов.
#include dost ream. h>
#include "comm.h"
void main (void) <
int n, k;
cout « "Введите число кандидатов и число членов комитета: cin » п » к;
cout « "Число возможных комитетов: " « comm(n,k) « endl;
}
<Прогон №1 программы 10.3>
Введите число кандидатов и число членов комитета: 10 4
Число возможных комитетов: 210
<Прогон №2 программы 10.3>
Введите число кандидатов и число членов комитета: 9 0
Число возможных комитетов: 1
*/
Комбинаторика: перестановки
К+ногие интересные рекурсивные алгоритмы предполагают наличие мас-0Ве в этом разделе мы рассмотрим задачу генерации всех перестановок С W элементов. В алгоритме предусматривается передача массива по значе-а поскольку в C++ все массивы передаются по адресу, должно быть
вЫП°лнено копиРование массива.
® Перестановка (permutation) из N элементов (1, 2, .... N) есть упорядоченное расположение этих элементов. Для N = 3 (1 3 2), (3 2 1) (12 3) — азличные перестановки. В комбинаторике установлено, что число перестановок равно N!. Это интуитивно понятно, если взглянуть на отдельные позиции в перестановке. Для позиции 1 существуют N вариантов, так как все U элементов доступны. Для позиции 2 имеются N-1 вариантов, так как один элемент используется для позиции 1. Число вариантов уменьшается на еди-диДУ по мере продвижения по позициям списка.
Число вариантов
N	N - 1	N - 2
Поз I	Поз 2	Поз 3
2	1
Поз п -1	Поз п
Общее число перестановок есть произведение числа вариантов в каждой позиции.
Permulation(N) = N * (N-l) * (N-2) * ... * 2 * 1 = N!
Более интересный рекурсивный алгоритм генерирует перечень всех перестановок из N элементов для N >— 1. Для демонстрационных целей сформируем вручную 24 (41) перестановки из четырех элементов. Перестановки с одинаковыми первыми элементами записываются в отдельный столбец. Каждый столбец в дальнейшем делится на три пары с одинаковыми вторыми элементами.
1	2	3	4
1234	2134	3124	4123
1243	2143	3142	4132
1324	2314	3214	4213
1342	2341	3241	4231
1423	2413	3412	4312
1432	2431	3421	4321
Алгоритм вычисления числа перестановок иллюстрируется иерархическим Деревом, которое содержит упорядоченные пути, соответствующие перестановкам. В исходном положении имеется четыре варианта — 1, 2, 3 и 4 — Соответствующие четырем столбцам. По мере продвижения вниз по дереву Т^злежащие уровни разделяются на 3, 2 и 1 элемент, соответственно. Общее
Сл° путей (перестановок) равно
4*3*2*1 = 41
Адгорнтм генерации всех перестановок моделирует проход по путям де-йоэа* Продвигаясь от уровня к уровню, мы тем самым указываем очередную ^й1° в перестановке. Этот процесс представляет собой шаг рекурсии.
^ВДекс О:
4342324341 314241 21
3 2 3 1 2 1
Итерационный процесс проверяет все возможные первые элементы для индекса О. В нашем примере существует N=4 возможных первых элемента.
Индекс 1:
о	с	о	о
На следующем уровне дерева каждый узел дает начало N-1 узлам, содержащим N-1 отличных ст первого элементов. Например, узел 1 соответствует перестановкам, которые начинаются с единицы в первой (индекс О) позиции. Путь, ведущий к узлу 2 следующего уровня, соответствует перестановкам с 1 и 2 в первых двух позициях и т.д. Можно итерационно определить второй элемент перестановки перебором узлов 2, 3 и 4.
Индекс 2:
1	3		
0	1	0	1	0	1
На следующем уровне дерева каждый узел разветвляется на два пути, которые представляют перестановки из двух элементов. Например, в узле 2 элементами являются [3,4], и их упорядоченными расположениями являются
Результирующие перестановки из четырех элементов:
Перестановки на [3,4]
3	4
Индекс 3:
1 | 2 | 4 | 3 0	1	2
1	2	3	4
0	1	2
Поскольку третий член перестановки зафиксирован, последний э. определяется однозначно, т.к. перестановка не допускает J— ся значения. Это становится условием останова. Каждый узел
едии« ЭЛетйв-
ПОВТОрЯ>°5 заверь
отдельную перестановку.
. ।
разработка алгоритма. Программа на C++, реализующая этот алгоритм, „^оминает каждую перестановку в виде массива из N элементов. Перед каж-вызовом рекурсивная функция создает копию этого массива, с тем чтобы ^возвращении из шага рекурсии значения оставались в тех же самых пози-массива. Помните, что для N = 4 программа должна в конечном итоге чдать 24 различных массива. Вначале мы создаем четыре массива, содержащих 1, 2, 3 и 4 в своих первых позициях. На следующем уровне каждый из №ть1рех существующих массивов создает три массива, которые запоминают дервЫЙ элемент нз базового массива и т.д.
void copy tint Х[Ь int у(], int п)
* for (int i=*0; i<n; i++)
xlij •= y[i);
)
Рекурсивный алгоритм постепенно помещает элементы в массив permlist, цо индексам О, 1, 2,	N-1.
1.	Условие останова возникает на индексе N-1. В этот момент N-элемент-ная перестановка завершается и массив распечатывается.
2.	Шаг рекурсии: На шаге рекурсии происходит продвижение вперед по индексам массива от О до N-2.
Для индекса k (0<=k<N-l) первые к элементов массива permlist уже сформированы. Мы итерационно просматриваем другие элементы и помещаем нх в permlistfk]. Это делается путем обменивания каждого элемента в оставшейся части списка с числом permlist[k]. Допустим, N — 4, к — 1 и permlistfk] = 1. Во время итерационных шагов числа 2, 3 и 4 помещаются по индексу 1.
После каждого чередования результирующий список копируется во временный список и передается в рекурсивную функцию permute вместе со следующим индексом и параметром N. На рис. 10.6 показан вызов permute с permlist[l] == 2.
По индексу 1: поместить 2
Вызвать permute с индексом 3
По индексу 2: поместить 3
Вызвать permute с индексом 2
По индексу 2: поместить 4
1	2	4	3
Вызвать permute с индексом 3
 I 2	3	4
По индексу 3:
Условие останова
Напечатать 12 3 4
По индексу 3:
1	2	4	3
Условие останова
Напечатать 12 3 4
Рис. 10.6. Перестановка 12 х X
По индексу 1: поместить 3
1	3	2	4
Вызвать permute с индексом 3
По индексу 2: поместить 3
По индексу 2: поместить 3
1	3	2	4
Вызвать permute с индексом 3
Вызвать регтща
с индексом 3
По индексу 3:
По индексу 3:
1	3	2	4
Условие останова
Напечатать 12 3 4
Условие останова
Напечатать 1 2 з 4
Рис, 10.7, Перестановки 1 3 х х
Перестановки создаются в порядке (1234) и (1243). Рис. 10.7 иллюстрирует вызов permute, когда в permlist[l] помещается 3. Затем на шаге итерации в permlist[l] помещается 4, и рекурсивный процесс продолжается с массивом 1423
Рекурсивная функция Permute
// UpperLimrt - максимальное число элементов перестановки
const int Upper-Limit = 5;
II копирование п элементов массива у в массив х void copy(int x[J, int у[], int n) { for (int i=0; i<n; i++) x[i] - y[il;
}
// permlist есть n-элементный массив целых чисел.
// генерировать перестановки элементов, индексы которых находятся в диапазоне // start <“ i <- n-l. по заполнении перестановки распечатывать весь массив.
// чтобы переставить все п элементов, начинать с start *= О void permute(int permlistl], int start, int n) {
int tmparr[UpperLimit]; int temp, i;
// условие останова: достигнут последний элемент if (start ‘ - n-l) (
// распечатать перестановку for (i=0; i<n; i++) cout « permlist(i) « ” cout « endl;
) else
// шаг рекурсии; поменять местами permlist[startJ и permlist[iJ; скопирова // массив в tmparr и переставить элементы tmparr от start+1 до конна масс for (i-start; i<n; i++) (
// поменять permlist[i] c permlist[start] temp = permlist[i];
permlrst[i] = permlist[start}: permlist[start] = temp;
// создать новый список и вызвать permute copy (tmparr, permlist, n);
permute (tmparr, start+1, n);
)
Программа 10.4. Перестановки
Эта задача о перестановках запускается с N = 3. Функции копирования и перестановки хранятся в файле permute.h.
^include <iostream-h>
finclude ''permute.h”
void main (void)
^ // permlist содержи® n чисел, подлежащих перестановке int permlist[UpperLimitl;
int n, i.
cout « "Введите число в диапазоне от 1 до "
« UpperLimit « cin » Ы
Ц инициализировать permlist множеством (1.2.3,.-.,п!
for (i=0; i<n; i++)
permlist[i] = i+1;
cout « endl;
// распечатать перестановки элементов массива permlist по индексам от 0 до п-1 permute (permlist, 0, п) .•
I
/*
«Выполнение программы 10.4>
Введите число в диапазоне от 1 до 5: 3
12 3 13 2
2 13
2 3 1
3 12
3 2 1
♦/
Ханойская башня. Задача о ханойской башне, рассмотренная в разделе Ю.1, является примером рекурсивного алгоритма, который просто решал проблему, не углубляясь в детали. В этом разделе для перекладывания дисков Разрабатываются шаги рекурсии и условия останова.
Формулировка задачи. На платформе расположены три стержня: начальный (start), средний (middle) и конечный (end). На начальный стержень нанизано N дисков в порядке возрастания размера, т.е. самый большой диск лежит внизу. Цель головоломки — переместить все N дисков с начального стержня на конечный. Каждый раз можно перемещать лишь один диск, н никогда диск большего размера не должен лежать на диске меньшего размера.
На ₽Ис* Ю‘8 показаны перемещения дисков для N = 3. Стержни помечены У^в^Ми S (начальный), Е (конечный) и М (средний). Мы используем этот н Осительно простой случай, чтобы определить рекурсивный процесс. Ре-УРсивный алгоритм дается в терминах N стержней.
Ша^ЛГ°₽и™ ^anoi. Пример с тремя дисками может быть обобщен до трех-аг°в°го рекурсивного алгоритма (рис. 10.9). В функции Hanoi стержни
Исходная башня из трех дисков на стержне S
(			—		L				
Стер Шаг 1: Перемес	же ти	;нь S	Стер ть маленький диск на стержне Е	же (S	нь М	Стер *>Е)	ж<	гнь Е
					1	 3		
Стер Шаг 2: Перемес	ЖЕ ти	?нь S	Стер ть средний диск на стержне М (	же S	нь М	Стер >М)	Ж(	знь Е
С	)	(		)	(	 }						
Стер Шаг 3: Перемес	Ж£ 7И	;нь S	Стер ть маленький диск на стержне h (		же И 1	нь М	Стер Е->М) 	L	ж<	гнь Е
<	J	<	J						
Стер Шаг 4: Переме<	ж«	!нь S	Стер гть большой диск на стержне Е (	же S-	нь М	Стер >Е)	ж<	>нь Е
			С	5	(								
Стер Шаг 5: Перемес	же ти	:нь 5	Стер ть маленький диск на стержне S	же (Г	нь М	Стер И->5)	ЖЕ	=нь Е
(	}	(	)	С	__	-						
Стержень S	Стержень М	Стержень Е						
Рис 10.8. Ханойская башня (N = 3)
Стержень S	Стержень М	Стержень Е
Шаг 4: Переместить маленький диск на стержне Е (S->E). Башня из трех дисков построена
Рис. 10.8. Продолжение
объявляются как объекты типа String. В списке параметров порядок переменных следующий:
startpeg — middlepeg — endpeg
Предполагается, что мы перемещаем диски с начального стержня на конечный, используя средний для временного хранения дисков. Если N « 1, мы имеем специальное условие останова, которое может быть обработано путем перемещения единственного диска с начального стержня на конечный.
cout « "переместить " « startpeg « " на ” « endpeg « endl;
В ином случае мы имеем трехшаговый процесс перемещения N дисков с начального стержня на конечный. На первом шаге алгоритма перемещается N-1 дисков с начального стержня на средний с использованием конечного стержня в качестве промежуточного. Отсюда порядок параметров в рекурсивном вызове функций таков: startpeg, endpeg и middlepeg:
использовать конечный стержень для временного хранения anoi (n-i, startpeg, endpeg, raiddiepeg);
На втором шаге самый большой диск просто перемещается с начального дрисня на конечный:
Wt « "переместить ” « startpeg « ” на " « endpeg « endl;
На третьем шаге N-1 дисков перемещаются со среднего стержня на ко-с использованием начального стержня для временного хранения, at порядок параметров в рекурсивном вызове функций таков: middlepeg, ^peg и endpeg:
начальный стержень для временного хранения middlepeg, startpeg, endpeg);
I : l!  I
Стержень S
Стержень E
Стержень M После
Шаг 1: Переместить N-1 дисков с S на М _□
Стержень S
Стержень M До
Стержень E
После
До
Стержень t
Стержень S
Рис. 10.9. Перемещение дисков ханойской башни
Стержень М После
Программа 10.5. Ханойская башня
Три стержня представляются строками "start", "middle' н end", которые передаются в функцию в качестве параметров. Вначале программа запрашивает число дисков N. Рекурсивная функция Hanoi вызывается для распечатки перечня ходов при перемещении N дисков со стержня •’start" на стержень "end".
Алгоритм требует 2N - 1 хода. Для 10 дисков, задача решается за 1023 хода. Для нашего теста N = 3 число ходов равняется 23 - 1 — 7.
#include <iostream.h>
।include "strciass.h”
// переложить n дисков с начального стержня на конечный, используя // средний как промежуточный
void hanoi (int n, String startpeg, String middlepeg. String endpeg)
<
// условие останова: перемещение одного диска
if (п =- 1)
cout « ’'переместить " « startpeg « ' на ” « endpeg « endl;
// переместить п-1 дисков на средний стержень.
// переместить нижний диск на конечный стержень, затем переместить
И п-1 диск со среднего стержня на конечный else f
hanoi(n-1, startpeg, endpeg, middlepeg);
cout « "переместить " « startpeg « ” на " « endpeg « endl;
hanoi(n-1, middlepeg, startpeg, endpeg); }
void main() {
// Число дисков и названия стержней int n;
String startpeg = "start middlepeg - "middle’’, endpeg = "end
// запросить n и решить задачу для п дисков cout « "Введите число дисков.-cin » п;
cout « "Решение для п = " « n « endl;
hanoi(n, startpeg, middlepeg, endpeg);
/*
^Выполнение программы 10.5>
введите число дисков: 3
еиение для п = 3 переместить start на end
Переместить start на middle
еРеместить end на middle
еРеместить start на end ереместить middle на start ерецестить middle на end
^ереместить start на end
Прохождение лабиринта
Многие рекурсивные алгоритмы используют принцип перебора с воз ; тами (backtracking). Этот принцип применяется, когда мы сталкиваем^ некоторой проблемой, требующей каких-то шагов и решений. Пытаясь**; стичь конечной цели, мы шаг за шагом принимаем ряд частичных рещей^ J которые кажутся согласующимися с конечной целью. Если мы выполз* 1 шаг или принимаем решение, которые не согласуются с конечной цел*6' мы возвращаемся на один нли несколько шагов назад к последнему сог^ сующемуся частичному решению. Как по старой поговорке: шаг вперед два шага назад. Иногда возврат может повлечь за собой один шаг вперед и шагов назад, где п достаточно велико. В этом разделе мы рассмотри возвраты в уже знакомом контексте лабиринтов. В нашем анализе подрав мевается, что лабиринт не содержит циклов, которые позволили бы ц.*' ходить по кругу. Это ограничение не является обязательным. Возвраты црй менимы и к лабиринтам с циклами, поскольку мы сохраняем карту, пока зывающую, какие узлы уже были пройдены. На рис. 10.10 изображен ла биринт с циклом, включающим перекрестки 2, 3, 4 и 5.	।
Рис. 10.10. Лабиринт с циклом
")| 
Анализ алгоритма. Лабиринт есть множество перекрестков. Двигаясь -некотором направлении, путешественник попадает на перекресток и ДвизК^д' далее по одному из трех путей: налево, прямо или направо. Путь иде фицируется номером следующего перекрестка. Если некоторое направл заблокировано, оно обозначается нулем. Перекресток без выходов явл тупиком, или мертвой точкой.	;
Полный авантюризма и готовности к возвратам путешественник BX°yg0H лабиринт через начальную точку и смело начинает поиски цели — К°ке перекрестка и, следовательно, свободы. Каждый перекресток на его представляет собой частичное решение. К сожалению, дух аваЯ?10^^ может завести в тупик, и тогда потребуется возврат к предыдущему крестку.	gy/H
Чтобы как-то организовать выбор вариантов на каждом перекрестк ’ придерживаться рекурсивной стратегии. Сначала попробуем свернутв (если это направление не заблокировано) и проложить путь к конечной
5
6
Перекресток	Действие	Результат
1	Идти прямо	Перекресток 2
2	Идти налево	Перекресток 3
3	Идти направо	Перекресток 4
4 (тупик)	Возврат	Перекресток 3
3 (тупик)	Возврат	Перекресток 2
2	Идти прямо	Перекресток 5
5 (тупик)	Возврат	Перекресток 2
2	Идти направо	Перекресток б
6	Идти налево	Перекресток 7 (конец)
Рис. 10.11. Мини-лабиринт
Этот вариант становится несовместимым с нашей целью лишь в том случае, если заводит в тупик. Оттуда мы возвращаемся к перекрестку н пытаемся пойти прямо до самого конца. Очередной тупик делает это решение несовместимым, и мы выбираем движение направо. Если и этот выбор заканчивается неудачей, то мы в тупике и должны возвратиться к предыдущему "совместимому" перекрестку. Эту стратегию относительно просто описать и запрограммировать в виде рекурсивной функции. Задача создания итерационной версии этой функции будет ясна из частичного прохода по лабиринту с семью перекрестками, показанному на рис. 10.11. Здесь путь, ведущий к выходу из лабиринта, проходит через перекрестки 1 — 2 — 6 — 7.
Стратегия обхода гарантирует, что если путешественник выходит из какого-то перекрестка, то возврата назад не будет до тех пор, пока не будут яроверены все возможные альтернативы, возникающие далее вдоль пути, и не встретится тупик. Кроме того, перекресток официально не включается в Итоговый маршрут, пока не будет гарантии, что из него существует путь к выходу из лабиринта. Как только путешественник достигает конечной точки, Можем проследить историю его прогулки и идентифицировать все перекрестки на маршруте.
Класс Maze. Класс Maze — это структура, состоящая из данных (перекрестков) и методов, позволяющих формировать лабиринт н совершать про-Хо«сдение его перекрестков, используя нашу стратегию. Допустим, что каж-перекресток представляется записью с полями, показывающими резуль-движения из этой точки налево, прямо нлн направо. Целое число в ^'°л® определяет следующий перекресток при движении в данном направле-7^- Нуль обозначает, что направление заблокировано. Эта запись реализу-структурой Intersection.
U запись, описывающая соседние перекрестки, на которые вы попадете, // если отправитесь из данной точки налево, прямо или направо struct Intersection {
int left;
int forward;
int right;	'
b
Класс Maze включает целые значения, показывающие размер лабири^ конечную точку и список перекрестков, который размещается в динамя^ ком массиве. Доступ ко всем данным обеспечивается конструктором, вып няющим построение лабиринта, и методом, выполняющим прохождение °* биринта. Данные читаются из файла, который содержит количество nefe крестков, по три числа на каждый перекресток и номер точки выхода » лабиринта. Точка выхода не считается перекрестком. Например, данные д, ' мини-лабиринта на рис. 10.11 таковы:
6	// число перекрестков
0 2 0	// 1: вперед до перекрестка 2
356	//2: налево к 3; прямо к 5; направо к 6
004	//	3:	направо	к	4
000	//	4:	тупик
000	//	5:	тупик
7 0 0	//6: налево к точке выхода'
7	//	номер точки	выхода
Спецификация класса maze
ОБЪЯВЛЕНИЕ ^include <iostream.h> #include <fstream.h> ^include <stdlib.h>
class Maze
I
private:
// число перекрестков лабиринта и номер точки выхода int inazesize;
int EXIT;
// массив перекрестков лабиринта intersection *intsec
public:
// конструктор; чтение данный из файла <filename> Maze(char *filename);
U прохождение лабиринта
int TraverseMazefint intsecvalue); };
ОПИСАНИЕ
В конструктор передается имя файла, содержащего данные о ла^й^^гь В этом процессе мы указываем число перекрестков н можем расПреД память под динамический массив intsec.	дабй'
TraverseMaze — рекурсивная функция, которая находит выход и3 ринта. Параметр intsecvalue вначале равен 1, показывая тем саМЬ1^дессе путешественник входит в лабиринт в точке 1. Во время рекурсивного оР 8. эта переменная хранит номер текущего перекрестка. Объявление и Р ция класса Maze находятся в файле maze.h.
Реализация класса Maze
Конструктор отвечает за формирование лабиринта. Так, например, он от-<двает входной файл, считывает размер лабиринта, инициализирует массив ^рекрестков и назначает точку выхода.
ТТлостроить лабиринт, введя -Maze (char ‘filename) jjaze   *ifstream fin; int i;	перекрестки и номер точки выхода из файла filename
// открыть filename, завершить выполнение, если файл не найден fin-open(filename, ios::in I ios::nocreate);
if (!fin)
* cerr « "Невозможно открыть файл описания лабиринта " « filename « endl;
exit(1);
}
// первое число в файле - количество перекрестков
fin > mazesize;
// выделить память для массива перекрестков, мы не используем
И кулевой индекс и поэтому должны распределить mazesize+1 запись, intsec = пей intersection[mazesize+1];
И ввести перекрестки из файла
for (i  1; i <= mazesize; i++)
fin » intsecli].left » intsec[i].forward » intsecli].right
// ввести номер точки выхода и закрыть файл fin » EXIT;
fin.closet);
Рекурсивная стратегия управляется методом TraverseMaze, который принимает в качестве параметра число перекрестков (intsecValue). Эта функция вызывается из предыдущего перекрестка и возвращает 1 (TRUE), если из текущего перекрестка существует путь к точке выхода. Если intsecValue — О, мы наткнулись на стену и немедленно возвращаемся с нулем (FALSE).
Сердцевиной метода является дерево решений, позволяющее путешествен-W выбрать с помощью некоторых условий то направление, которое завер-точкой выхода.
Случай 1. Если intsecValue == EXIT, то цель успешно достигнута. Мы печатаем номер перекрестка и возвращаем TRUE предыдущему перекрестку на этом пути, который ждет результата проверки на успешность. Случай 2. Если это не точка выхода, мы поворачиваем налево и ждем сообщения TRUE или FALSE, указывающего на результат тестирования левого направления. Получив TRUE, мы печатаем номер текущего перекрестка и возвращаем TRUE предыдущему перекрестку.
Случай 3. Этот случай похож на случай 2 за исключением того, что Попытка повернуть налево не увенчалась успехом. Теперь мы отправляемся прямо и ждем сообщения TRUE или FALSE в качестве резуль-
тата тестирования прямого направления. Если возвращается TRUE печатаем номер текущего перекрестка и возвращаем TRUE пред^ щему перекрестку.	‘‘Цу. 1
Случай 4. Этот случай идентичен случаям 2 и 3, но обрабатывает пра I поворот.	х	‘
Если ни один из перечисленных случаев не возвратил сообщение ТРръ значит текущий перекресток является тупиком. Чтобы зафиксировать * факт, возвращается сообщение FALSE. Способность передавать информат^1 назад в предыдущий перекресток (в предыдущий экземпляр TraverseMa обусловлена рекурсивной структурой кода. В конце концов TraverseMazcfn возвращает TRUE или FALSE в главную программу, показывая тем самы можно ли пройти данный лабиринт.	’
// обход с возвратами произвольного лабиринта int Maze::TraverseMaze(int intsecvalue) (
// если intsecvalue “ 0, то мы в тупике.
//в противном случае мы пытаемся найти допустимое направление
if (intsecvalue > 0)
(
11 условие останова: обнаружена точка выхода
if (intsecvalue == EXIT) <
// печатать номер перекрестка и возвратить TRUE cout « intsecvalue « " return 1;
}
// попытка повернуть налево
else if (TraverseMaze(intsec[intsecvalue]-left)) (
// печатать номер перекрестка и возвратить TRUE cout « intsecvalue « '* return 1;
)
// поворот налево завел в тупик. Попробуем пойти вперед else if (TraverseMaze(intsec[intsecvalue].forward)) {
// печатать номер перекрестка и возвратить TRUE tout « intsecvalue « "
return 1; )
// направления налево и прямо ведут в тупик, попробуем свернуть направо else if (TraverseMaze(intsec[intsecvalue].right))
(
If печатать номер перекрестка и возвратить TRUE cout « intsecvalue << ” return 1;
}
]
// это тупик, возвратить FALSE
return 0;
Программа 10.6. Прохождение лабиринта
Проверим программу на мини-лабиринте (рис. 10.11) (входной файл jnazel.dat), а затем на лабиринте, изображенном ниже (входной файл maze2.dat). Из этого лабиринта выхода нет, и задача не имеет решения. На последнем прогоне программы совершается обход большого лабиринта, показанного на рис. 10.1 (входной файл bigmaze.dat). В каждом случае маршрут распечатывается в обратном порядке.
ю
finclude <iostream.h>
^include "maze.h”
void main (void)
i
// файл с параметрами лабиринта
char filename[ 32 J;
cout « ''Введите имя файла данных:
cin » filename;
/1 построить лабиринт
Maze M(filename);
fI решить задачу о лабиринте и напечатать результат
if (M.TraverseNaze(1))
cout « endl « "Вы свободны*" « endl;
«Ise
cout « "Из этого лабиринта нет выхода" « endl;
/*
прогон №1 программы 10. б>
®в«дите имя файла данных: znazel. dat
R 2 * 1
свободны!
Прогон №2 программы 10.6>
ИзвЛ>1Те имя Файла данных: maze2.dat
Этого лабиринта нет выхода
«Прогон №3 программы 10. б>
Введите имя файла данных: bigmaze.dat
19 17 16 14 10 9 8 2 1
Вы свободны!
*/
10.5. Оценка рекурсии
Часто рекурсия ие является эффективным методом решения проблем Рассмотрим задачу вычисления факториала. Итерационный алгоритм исполу зует цикл, а ие повторные вызовы функции. Рекурсия может сыграть злую шутку. Она часто упрощает разработку алгоритма и кода для того лишь чтобы потерпеть фиаско на этапе выполнения по причине недостаточной эффективности. Этот конфликт иллюстрируется с помощью чисел Фибоначчи-
1, 1, 2, 3, 5, 8, 18, 21, 34, ...
Члены этой последовательности F(n) определяются рекурсивно для и 1. Первые два члена равны 1 по определению. Начиная с третьего, каждый член последовательности равен сумме двух предыдущих.
(1,	если п - 1 или 2
F(n-l) + F(n-2) если п > 2
Это определение сразу переводится в рекурсивную функцию. Предположим, что F(n) есть n-ый член последовательности Фибоначчи, где п>-1. Тогда
Условие останова:	F(l) ” 1 F(2) “ 1
Шаг рекурсии:	Для N 2 3, F(n> = F(n-l) + F(n-2};
Функция Fib на языке C++ реализует рекурсивную функцию F. Она имеет единственный целочисленный параметр п и возвращает длинный целый результат.
И рекурсивная генерация n-го числа Фибоначчи long Fib(int n) {
// условие останова: fl - f2 - 1
if (n == 1 || n « 2)
return 1;
// шаг рекурсии: Fib(n) - Fib(n-1> + Fib(n-2) else
return Fib(n-l) + Fib(n-2);
}
Сразу заметно, что функция Fib много раз вызывает сама себя с и тем же параметром. Пусть, например, N = 6. Выражение Fib(N-l) + g формирует иерархическое дерево вызовов функции Fib для N = 5, 4, » и 1 (рис. 10.12).
Обратите внимание, что Fib(3) вычисляется три раза, a Fib(2) — пять Р Пятнадцать узлов дерева представляют количество рекурсивных вы30® требующихся для вычисления Fib(6) = 8.
Fib(6)
Рис. 10.12. Дерево рекурсивных вызовов при вычислении Fib(6)
Пусть NumCall(k) — число рекурсивных вызовов, необходимых для вычисления Fib(k).
HumCaU(k> = 2 * Fib(k) - 1
Например,
ЯивСаП(б) = 2 ‘ Fib(6) -1-2*8-1 = 15
KumCall(35) = 2 * Fib(35> - 1 = 2 * 9277465 - 1 - 18,554,929
Вычислительная эффективность алгоритма — О(2П), т.е. время счета растет по экспоненте.
Числа Фибоначчи: итерационная форма. При итерационном вычислении n-го числа Фибоначчи используется простой цикл. Эта функция имеет вычислительную эффективность О(п).
И вычислить n-е число Фибоначчи с помощью итераций long Fiblterfint и)
(
long twoback = 1, oneback - 1, current;
int i;
// Fiblter(l) = Fiblter(2) - 1
if (n == 1 || n — 2) return 1;
4 current = twoback + oneback, n >= 3
else
for (1=3; i<=n; i++)
current - twoback + oneback;
twoback = oneback;
Oneback = current;
j return current;
Для k-го числа Фибоначчи (k >« 3) итерационная форма требует k-2 a 0>кений и один вызов функции. Для к = 35 эта форма требует 33 сложения, Время как рекурсивная функция — более 18,5 миллиона вызовов!
Вычисление чисел Фибоначчи по формуле. Эффективнее всего вцхщс числа Фибоначчи непосредственно по формуле, выводимой из РекурреВт^> ...I соотношений. Вывод этой формулы выходит за рамки данной книги.	j
Г/	I-\п
F<»)=i
Поскольку функции квадратного корня и возведения в степень имек^ в библиотеке <math.h> языка C++, n-ое число Фибоначчи может быть64 числено непосредственно с эффективностью 0(1) при помощи кода: «include «math.h»
const double sqrt5 = sqrt(5.0);
// вычислить n-oe число Фибоначчи по алгебраической формуле double FibFormula(int n) {
double pl, p2;
U библиотечная функция языка C++ // double pow(double х, double у);
II вычисляет х в степени у pl = рои(l+sqrt5)/2.О, п) ; р2 = pow(l-sqrt5)/2.0, п); return (pl - p2)/sqrt5;
)
Программа 10.7. Оценка рекурсии (на примере чисел Фибоначчи)
3 S
Эта программа хронометрирует вычисление 35-го числа Фибоначчи с помощью формулы, итерационной функции и рекурсивной функции файла fib.h. Нерекурсивные программы выполняются доли секунды, в время как рекурсивная функция требует более 82 секунд.
((include «iostream, h»
((include '’fib.h'*
void main (void)
(
int i;
// распечатать результат FibFormula в виде десятичного числа //с фиксированной точкой без дробной части cout.sett(ios:;fixed);
cout.precision(0);
// вычислить 35-е число Фибоначчи тремя способами
cout « FibFormula(35) « endl;
cout « Fiblter(35) « endl;
cout « Fib(35) « endl;
)
/*
«Выполнение программы 10.7»
9227465	«формула заняла менее 1 секунды»
9227465	«итерационная функция заняла менее 1 секунды»
9227465	«рекурсивная функция заняла более 82 секунд»
*/
Издержки рекурсии. Пример с числами Фибоначчи должен подготовить р&с к потенциальным проблемам при использовании рекурсии. Избыточность рьгэовов в простой рекурсивной функции может серьезно ухудшить производительность программы. Еще более серьезно то, что рекурсивный вызов ^омкет породить наслоение целой последовательности рекурсивных вызовов, которые выходят из-под контроля программиста и делают запросы, превы-^ающие размер стека. Числа Фибоначчи являются экстремальным случаем. Здесь легко можно реализовать итерационную версию.
Со сделанными оговорками рекурсия остается важным инструментом про-!раммирования. Многие алгоритмы проще сформулировать и запрограммировать с помощью рекурсии. Они естественным образом адаптируются к рекурсивной реализации, где выделяются условия останова и шаг рекурсии. Например, использование возвратов в задаче о лабиринте облегчается посредством рекурсии.
Хотя рекурсия не является объектно-ориентированным понятием, для нее характерны некоторые черты объектно-ориентированного проектирования. Она позволяет программисту управлять ключевыми логическими компонентами алгоритма и скрывать некоторые сложные детали реализации. Не существует жесткого правила, когда можно использовать рекурсию, а когда нельзя. Вы должны взвесить эффективности разработки и выполнения. Используйте рекурсию, когда разработка алгоритма усложняется, а реализация позволяет достичь приемлемого быстродействия и затрат памяти.
Задняя рекурсия. Если последним действием функции является рекурсивный вызов, мы говорим, что эта функция использует заднюю рекурсию (tail recursion). Этот рекурсивный вызов требует затрат на создание активизирующей записи и ее запоминание в стеке. Когда рекурсивный процесс доходит до условия останова, мы должны выполнить серию возвратов, которые выталкивают активизирующие записи из стека. Мы просто помещаем записи в стек и вынимаем их оттуда, не используя для существенных вычислений.
Исключение задней рекурсии может значительно повлиять на эффективность рекурсивной функции. Эту проблему иллюстрирует простой пример, где предлагается типичное решение. Рассмотрим рекурсивную функцию recfunc, которая распечатывает элементы массива от индекса п до индекса 0. Этот пример не имеет практического значения и выбран для простоты иллюстрации.
void recfunc(int А[], int n) {
if (n >= 0} // идти вперед, если индекс п не вышел за предел
('
cout « А(п] « •’
п—;	// декремент индекса п
recfunc(А, п);
)
Пусть массив А[] = {10, 20, 30}. Тогда вызов recfunc(A,2) начинается с И == 2 и создает вывод 10 20 10.
Функция recfunc иллюстрирует типичную ситуацию задней рекурсии. Эту Проблему можно показать на логической схеме, где п > 0 интерпретируется Как условие, требующее дальнейшего рекурсивного вызова.
—♦if «рекурсивное условие>	<—----- n >= 0	
Выполнить задачу	<----- вывод а[п]
Обновить условие	<------ декремент п	;
--------вызвать recfunc	, »
Эта логическая схема эквивалентна циклу WHILE, проверяющему nD s условие п > О. В функции recfunc Управление передается оператору пров**0® условия с помощью менее эффективной рекурсивной операции.	|
—♦ while <условие>	<----- п >= О
Выполнить задачу	<----- вывод A[nJ
Обновить условие	<-----  декремент	п
В нашем примере рекурсивная функция recfunc может быть замене функцией iterfunc, которая использует эквивалент оператора WHILE. ПпТ блема исключения задней рекурсии может оказаться немного запутанной * Надежнее будет построить логическую схему для рекурсивной функции и 1 затем создать такую же итерационную схему с использованием WHILE.
// итерационная функция, исключающая заднюю рекурсию
void iterfunc(int Al], int n) < while (n >= 0) (
cout « "While-значение " « A(n] «endl; n—;
) )
Письменные упражнения
10.1 Объясните, почему выполнение следующей функции может дать неверный результат.
long factorial(long n)
(
if (n ““ 0 | I n == 1)
return 1;
else
return n * factorial(—n);
10.2
Результат зависит от порядка оценивания операндов компилятору Если п = 3 и левый операнд вычисляется первым, результатом 3 * 2! «» 6. Если первым вычисляется правый операнд, результа будет 2 * 2! — 4.
Какая числовая последовательность порождается следующей рекУРсйВ иой функцией?
long f(int n)
(
if (n == 0 II n «= 1) return 1;
else
return 3*f(n-2) ♦ 2*f(n-l);
fl<j Какая числовая последовательность порождается следующей рекурсив-ной функцией?
L	int flint nj
f	<
if (n -= 0) return 1; else if (n =- 1) I	return 2;
else return 2*f(n-2) + f(n-l);
10.4 Каким будет результат выполнения следующей программы, если вход-I ' ными данными являются 5 3?
finclude <i©stream.h> f long flint b, int n) ( I	if (n == 0)
return 1; else 1	return b*f(b, n-1);
1	)
I	void main (void)
( int b, e; cin » b » e; cout « f(b, e) « endl;
) I
(	10.6 Каким будет результат выполнения следующей программы, если вход-
;	иыми данными является строка "Это перекресток"?
I
I	((include <iostream>
void Q(void) {
char c;
cin.get(c);
if (c ! = '\n') QO;
cout « c; )
void main(void)
{
Cout « "Введите текстовую строку: " « endl; CO;
cout « endl;
Требуется рекурсивно вычислить максимальный элемент п-элементного массива. Определите функцию
max(int а(], int у);
Которая возвращает максимальное из двух целых х и у. Определите Функцию
int arraymax(int a(), int n);
которая использует рекурсию, чтобы возвратить максимальный эЛв массива а.
Условие останова: n == 1
Шаг рекурсии: arraymax = шах(шах(а(О], ... а[п-2]), а[п-1])
10.7 Напишите рекурсивную функцию
float avg(float a[I, int n);
которая возвращает среднее из п элементов массива чисел с плавают точкой.	ЭД
Условие останова: n == 1
Шаг рекурсии: avg = ((п-1)/п)*(среднее из п-1 элементов) + элемент)/п
10.8 Напишите рекурсивную функцию
int rstrlen(char s[]);
которая вычисляет длину строки.
Условие останова: s[0] == 0
Шаг рекурсии: 1+1еп£1Ь(подстрока, начинающаяся со 2-го символа) 10.9 Напишите рекурсивную функцию, которая проверяет, является ли строка палиндромом. Палиндром — это строка, не содержащая пробелов, которая одинаково читается справа налево и слева направо. Например, dad level did madamimadam1 кабак чинэванмечемнавзнич
Используйте следующее объявление
int pal(char А[], int s, int e);
где pal определяет, является ли палиндромом цепочка символов в А, начинающаяся с индекса s и заканчивающаяся индексом е.
Условия останова: s >= е (успех); А[в] != А[е] (неудача)
Шаг рекурсии: Является ли строка А[в+1] ... А[е-1] палиндромом 10.10 Коэффициенты C(n,k), использующиеся при разложении формулы (х+1)п, называются биномиальными.
(Х+1)Л • Сп,п Xй + Сп,п_! Xй’1 + Сп,п_2 Xй'2 4- ... + СП(2 х2 + СГ/1 X1 + Сп.о
Для любого п С(п,п) = С(п,0) = 1. Рекуррентные соотношения биномиальных коэффициентов таковы:
С(п,0) - 1
C(n,n) = 1
C(n,k) = C(n-l,k-l) + C(n-l,k)
Заметьте, что каждый коэффициент C(n,k), 0<k<n, является Рега^оЭф-задачи о комитетах, рассмотренной нами выше. Биномиальные фициенты C(n,k) определяют, сколькими способами можно вы Р элементов из и элементов.
1 Madam. I'm Adam. — Прим, перев.
Эти коэффициенты образуют знаменитый треугольник Паскаля. В этом треугольнике столбец О содержит все единицы, как и диагональ. Каждый из остальных элементов является суммой двух элементов, расположенных на строку выше в том же столбце и столбце слева.
1
1 1
1 2 1
13 3 1
1 4 6 4 1
Напишите функцию, которая строит треугольник Паскаля для заданного п. Выше приведен пример для п = 4.
10.11	Напишите рекурсивную функцию
int qcd(int a, int Ь)i
для вычисления наибольшего общего делителя положительных целых а и Ъ- См. процедуры для итерационной версии этой функции в гл. 6.
10.12	Следующие данные представляют собой входной файл описания лабиринта. Нарисуйте лабиринт и найдите решение, прослеживая рекурсивный алгоритм по шагам.
11	И число перекрестков
0	2	0	// перекресток 1: (налево, прямо, направо)
4	3	6
ООО
0	0	5
ООО
7	0	0
8	11	9
0	0	0
0	О	10
0	0	0
12	О	0
12	// точка выхода
Упражнения по программированию
10,1 Используйте функцию аггаушах из письменного упражнения 10.6 для выполнения следующих действий:
1. Сгенерируйте 10 случайных целых чисел в диапазоне 1—20000 и сохраните их в массиве.
2. Рапечатайте массив.
8.	Примените функцию arraymax и распечатайте результат. Проверьте его правильность.
•2 Сумма первых п целых чисел находится по формуле
1+2 + 3 + ... + и = п(п+1)/2
Занесите в массив А первые 50 чисел. Тогда средним для этих элементов ®Удет значение 51/2 = 25,5. Проверьте свое решение, обработав массив А программой из письменного упражнения 10.7.
10.3
10.4
10.6
- - - -
Испытайте свою функцию rstrlen из письменного упражнения вводя пять строк с клавиатуры с помощью cin.getline и распер длину строки как посредством rstrlen, так и с помощью библиот*^ функции C++ strlen.
Читайте символьные строки до конца файла с помощью оператоп чтобы получить отдельные "слова". К каждому слову примените «к цию pal из письменного упражнения 10.9, чтобы определить, яют ли оно палиндромом. Если да, то запишите его в строковый По окончании ввода файла распечатайте’ все найденные палинл по одному в строке.
Классической арифметической задачей является представление значения в
N = 45
N = 90
N - 75
различных системах счисления:
Основание = 2 Выход: 101101
Основание = 8 Выход: 132
Основание = 5 Выход: 300
Цел<г;
[32 + 8 + 4 + 1] [1(64) + 3(8) + 2(1п [3(52) + 0(5) + 0(1)]
В обычном алгоритме преобразования в другую систему счисления и-пользуется многократное деление на ее основание. Если
N — <!„.! dn_2 dn_3 ... dj d0
то последовательность остатков дает цифры числа N в порядке сИО). d(n-l).
Определите рекурсивную функцию
void intoutflong N, int В);
для печати N в системе счисления по основанию В, подразумев? В < 10. Испытайте эту функцию в главной процедуре, которая вводе, пять пар чисел N,B и распечатывает каждое N в системе счислеки по основанию В.
10.6
10.7
10.8
Введите положительное целое п < 10. Обратитесь к письменному " ражнению 10.10 и распечатайте разложение полинома (х+1)п.
Разработайте рекурсивную функцию для подсчета количества п-раф ных двоичных чисел, не имеющих двух идущих подряд единиц. ("-’ сказка: Число начинается либо с нуля либо с единицы. Если оно чинается с нуля, количество вариантов определяется оставшимися • цифрами. А если с единицы, то какой должна быть следующая ДйфР Довольно часто возникает задача нахождения корня действителья^ функции. Если f(x) — функция, ее корень г есть действительное j такое, что f(r) = 0. В некоторых случаях корни могут быть вычис^. по алгебраической формуле. Например, все корни квадратного УР t ния f(x) = ах2 + Ъх + с находятся по формуле
г = —Ъ ±
Vb2 - 4ас
2а
Для общего случая формулы нет, и корни должны находиться ными методами.
чИС^ I
f(0) * f(-1) > 0, f(0) • f(1) < 0. Корень лежит в интервале 0< r< 1.0
Если f(a) и f(b) имеют разные знаки (f(a) * f(b) < 0) и f ’’ведет себя хорошо”, то между а и b существует корень г.
Метод дихотомии определяется следующим образом. Пусть m = (a+b)/2.0 — средняя точка в интервале а < х < Ь. Если f(m) == 0.0, то корень г = ш. Если нет, то либо f(a) и f(m) имеют разные знаки (f(a) * f(m) < 0), либо f(m) и f(b) имеют разные знаки (f(m) * f(b) < 0).
Если f(m) * f(b) < 0, то корень г лежит в интервале m < х < Ь; в Противном случае он лежит в интервале а < х < ш. Теперь выполним этот действие для нового интервала — половины исходного интервала. Процесс продолжается до тех пор, пока интервал не станет достаточно Маленьким или пока не будет найдено точное значение корня.
Напишите рекурсивную функцию
double Bisect(double f(double x), double a, double b, double precision) ;
вычисляющую и возвращающую баланс после выплачивания процента по заданной величине капитала с месячной ставкой Используйте метод дихотомии для расчета платежей по ссуде $151^** под 10% годовых на 25 лет.
10.9 Запустите программу 10.6 "с данными из письменного управе 10.12. Проверьте свое решение этого упражнения.

Деревья
11.1. Структура бинарного дерева
11,2. Разработка функций класса TreeNode
113. Использование алгоритмов прохождения деревьев
11.4, Бинарные деревья поиска
11.5.	Использование бинарных поисковых деревьев
11.6.	Реализация класса BinSTree
11.7.	Практическая задача конкорданс
Письменные упражнения
Упражнения по программированию
Рис. 11.1. Генеалогическое дерево
Массивы и связанные списки определяют коллекции объектов, доступ которым осуществляется последовательно. Такие структуры данных назыв^ ют линейными (linear) списками, поскольку они имеют уникальные первь в и последний элементы и у каждого внутреннего элемента есть только оди^ наследник. Линейный список является общим описанием для таких структур как массивы, стеки, очереди и связанные списки.	’
Во многих приложениях обнаруживается нелинейный порядок объектов где элементы могут иметь нескольких наследников. Например, в фамильной дереве родитель может иметь нескольких потомков (детей). На рис. Цд показаны три поколения семьи. Подобное упорядочение описывает и управ-ляющий аппарат компании, во главе которой стоит президент, а далее идут начальники отделов и менеджеры (рис. 11.2). Такое упорядочение называют
иерархическим, поскольку это название происходит от церковного распределения власти — от епископа к пасторам, дьяконам и т.д.
В этой главе мы рассмотрим нелинейную структуру, называемую деревом (tree), которая состоит из узлов и ветвей и имеет направление от корня к внешним узлам, называемым листьями. В гл. 13 представлены графы, описывающие нелинейную структуру, в которой два или более узла могут переходить в один и тот же объект. Эти структуры подобны коммуникационной сети, показанной на рис. 11.3, требуют особых алгоритмов и применяются
в специальных приложениях.
Президент
Рис. 11.2. Иерархическая структура
Станция А
Рис. 11.3. Ретрансляционные телефонные станции
Терминология деревьев
Древовидная структура характеризуется множеством узлов (nodes), происходящих от единственного начального узла, называемого корнем (root). На Рис- 11-4 корнем является узел А. В терминах генеалогического дерева узел можно считать родителем (parent), указывающим на О, 1 или более узлов, называемых сыновьями (children). Например, узел В является родителем сыновей Е и F. Родитель узла Н — узел D. Дерево может представлять несколько поколений семьи. Сыновья узла и сыновья их сыновей называются потомками (descendants), а родители и прародители — предками (ancestors) этого узла. Например, узлы Е, F, I, J — потомки узла В. Каждый некорневой узел имеет только одного родителя, и каждый родитель имеет О или более сыновей. Узел, не имеющий детей (Е, G, Н, I, J), называется листом (leaf).
д КАЖДЫЙ узел дерева является корнем поддерева (subtree), которое опре-ется данным узлом и всеми потомками этого узла. Ниже показаны три ^ДДерева дерева на рис. 11.4. Узел F есть корень поддерева, содержащего Дед14 I и J. Узел G является корнем поддерева без потомков. Это опре-еййе позволяет говорить, что узел А есть корень поддерева, которое само *^вается деревом.
П^ождение от родительского узла к его дочернему узлу и к другим г°Мкам осуществляется вдоль пути (path). Например, на рис. 11.5 путь
от корня А к узлу F проходит отАкСиотСкГ. Тот факт, что каждый некорневой узел имеет единственного родителя, гарантирует, что существует единственный путь из любого узла к его потомкам. Путь от корня к узду дает меру, называемую уровнем (level) узла. Уровень узла есть длина пути от корня к этому узлу. Уровень корня равен О. Каждый сын корня является узлом 1-го уровня, следующее поколение — узлами 2-го уровня и т.д. Например, на рис. 11.5 узел F является узлом 2-го уровня с длиной пути 2.
Уровень О
Уровень 1
Уровень 2
Уровень 3
Рис. 11.5. Уровень узла и длина пути
Глубина (depth) дерева есть максимальный уровень любого его узла. О* нятие глубины также может быть описано в терминах пути. Глубина есть длина самого длинного пути от корня до узла. На рис. 11-5 гЛЗ? дерева равна 3.
Бинарные деревья
й Я®
Хотя деревья общего вида достаточно важны, мы сосредоточимся раниченном классе деревьев, где каждый родитель имеет не более новей (рис. 11.6). Такие бинарные деревья (binary trees) имеют дей011 * ванную структуру, допускающую разнообразные алгоритмы прохожД
(А) Глубина 3
Рис 11.6. Бинарные деревья
эффективный доступ к элементам. Изучение бинарных деревьев дает ложность решать наиболее общие задачи, связанные с деревьями, hockoj любое дерево общего вида можно представить эквивалентным ему бинар> деревом. Этот вопрос рассматривается в упражнениях.
У каждого узла бинарного дерева может быть О, 1 или 2 сына. По оз шению к узлу слева будем употреблять термин левый сын (left child), а отношению к узлу справа — правый сын (right child). Наименования “лень я "правый" относятся к графическому представлению дерева. Бинарное рево является рекурсивной структурой. Каждый узел — это корень сво< собственного поддерева. У него есть сыновья, которые сами являются корня! деревьев, называемых левым и правым поддеревьями соответственно. Taxi образом, процедуры обработки деревьев естественно рекурсивны. Вот реку сивное определение бинарного дерева:
Бинарное дерево — это такое множество узлов В, что
а)	В является деревом, если множество узлов пусто (пустое дерево — тож дерево);
б)	В разбивается на три непересекающихся подмножества:
{R} корневой узел
{Li, Le, ..., Lm) левое поддерево R
{Ri, Ra, •••, Rm} правое поддерево R
На любом уровне п бинарное дерево может содержать от 1 до 2П узлов, ^иело узлов, приходящееся на уровень, является показателем плотности дерева. Интуитивно плотность есть мера величины дерева (число узлов) по 0тН01пению к глубине дерева. На рис. 11.6 дерево А содержит 8 узлов при •Дубине 3, в то время как дерево В содержит 5 узлов при глубине 4. Пос-ДЗИЙ случай является особой формой, называемой вырожденным (degen-а‘в) Деревом, у которого есть единственный лист (Е) и каждый нелистовой
71 Имеет только одного сына. Вырожденное дерево эквивалентно связан-5о*У списку.
Тв ^еРевья с большой плотностью очень важны в качестве структур данных, с fi«Raic они с°Д€Ржат пропорционально больше элементов вблизи корня, т.е. короткими путями от корня. Плотное дерево позволяет хранить боль-е Коллекции данных и осуществлять эффективный доступ к элементам.
16 Зак 425
Левое поддерево
Правое поддерево
Быстрый поиск — главное, что обусловливает использование деревьев пл хранения данных.	я
Вырожденные деревья являются крайней мерой плотности. Другая край ность — законченные бинарные деревья (complete binary tree) глубины К где каждый уровень 0...N-1 имеет полный набор узлов и все листья уровня N расположены слева. Законченное бинарное дерево, содержащее 2N узлов на уровне N является полным (full). На рис. 11.7 показаны законченное и полное бинарные деревья.
Полное дерево (глубина 2)
Рис 11.7. Классификация бинарных деревьев
Законченные и полные бинарные деревья дают интересные математические факты. На нулевом уровне имеется 2° узлов, на первом — 21, на втором — * и т.д. На первых к-1 уровнях имеется 2кЛ узлов.
1 4- 2 4- 4 4- ... + 2к1 = 2к 1
На к-ом уровне количество дополнительных узлов колеблется от 1 Д° (полное). В полном дереве число узлов равно
1 + 2 4- 4 + ... + 2k l 4- 2k = 2k+l — 1
Число узлов законченного бинарного дерева удовлетворяет неравенству
2k < N < 2k+1 — 1 < 2к+1
Решая его относительно к, имеем
к < log2 (N) < к4-1
Например, полное дерево глубины 3 имеет
24 — 1 = 15 узлов
Пример 11.1
1. Максимальная глубина дерева с 5-ю узлами равна 4 [рис. 11.6 (В)]. Минимальная глубина к дерева с 5-ю узлами равна
к < log2 (5) < к+1 log2 (5) = 2,32 и к = 2
2. Глубина дерева есть длина самого длинного пути от корня к узлу. Для вырожденного дерева с N узлами наибольший путь имеет длину N-1.
Для законченного дерева с N узлами глубина равна целой части от logzN. Этому же значению равен максимальный путь. Пусть дерево имеет N == 10000 элементов, тогда максимальный путь равен int(log2 10000) = int(13,28) -= 13
11.1. Структура бинарного дерева
Структура бинарного дерева построена из узлов. Как и в связанном списке, эти узлы содержат поля данных и указатели на другие узлы в коллекции. В этом разделе определяются узлы дерева и операции для его построения и прохождения. Подобно представлению класса Node в гл. 9, объявляется класс TreeNode, а затем разрабатывается ряд функций, использующих узлы дерева для построения бинарного дерева н прохождения индивидуальных узлов.
Узел дерева содержит поле данных и два поля с указателями. Поля указателей называются левым указателем (left) и правым указателем (right), поскольку они указывают на левое и правое поддерево, соответственно. Значение NULL является признаком пустого дерева.
TreeNode
Корневой узел определяет входную точку дерева, а поле указателя — узел бедующего уровня. Листовой узел содержит NULL в поле правого и левого Указателей (рнс. 11.8).
Проектирование класса TreeNode
® этом разделе разрабатывается класс TreeNode, в котором объявляются /“Икты-узлы бинарного дерева. Узел состоит из поля данных, которое явля-открытым (public) элементом, т.е. к которому пользователь может обра-^ТЬся непосредственно. Это позволяет клиенту читать или обновлять данные ^кремя прохождения дерева, а также допускает возвращение ссылки на дан--Л Последняя особенность используется более сложными структурами дан-
Такими как словари. Два поля с указателями являются закрытыми (pri-Эдементами, доступ к которым осуществляется посредством функций к К) и Right(). Объявление и определение класса TreeNode содержатся в файле Me.h.

дерево
Структура TreeNode
Рис 11.8. Узлы бинарного дерева
Спецификация класса TreeNode
ОБЪЯВЛЕНИЕ
// BinSTree зависит от TreeNode template <class т> class BinSTree;
// объявление объекта для узла бинарного дерева template <class Т> class TreeNode
<
private:
// указатели левого и правого дочерних узлов TreeNode<T> *left;
TreeNode<T> *right;
public:
// открытый элемент, допускающий обновление Т data;
// конструктор
TreeNode (const Т& item, TreeNode<T> *lptr - NULL, TreeNode<T> *rptr = NULL);
// методы доступа к полям указателей TreeNode<T>* Left(void) const;
TreeNode<T>* Right(void) const;
// сделать класс BinSTree дружественным, поскольку необходим // доступ к полям left и right
friend class BinSTree<T>;
Конструктор
Конструктор инициализирует поля данных и указателей. С помощью пус-_ указателя NULL узлы инициализируются как листья. Имея указатель о*объекта TreeNode в качестве параметра, конструктор присоединяет Р как евого или правого сына нового узла.
л Методы доступа Left и Right возвращают соответствующий указатель. Класс BinSTree объявляется дружественным классу TreeNode и может модифицировать указатели. Другие клиенты должны использовать конструктор для создания указателей и методы Left и Right для прохождения дерева.
рНвЛР
ц указатели целочисленных узлов дерева jreeNode<int> *root, *lchild, *rchild;
<fi,eeNode<int> *p;
// создать листья, содержащие 20 и 30 в качестве данных
Ichild ” new TreeNode<int> (20);
ichild = new TreeNode<int> (30);
// создать корень, содержащий число 10 и двух сыновей root =» new TreeNode<int> (10, Ichild, rchild);
root->data - 50; // присвоить корню 50
Реализация класса TreeNode Класс TreeNode инициализирует поля объекта. Для инициализации поля данных конструктор имеет параметр item. Указатели назначают узлу левого и правого сына (поддерево). При отсутствии сына используется значение NULL.
Ч конструктор инициализирует поля данных и указателей
‘ значение NULL соответствует пустому поддереву
template cclass т>
reeNode<T>::TreeNode(const T& item, TreeNode<T> *lptr,
I TreeNode<T> *rptr):data(item), left(lptr), right(rptr)
Методы Left и Right возвращают значения полей левого и правого указате-ея« Благодаря этому клиент имеет доступ к левому и правому сыновьям узла.
Построение бинарного дерева
Бинарное дерево состоит из коллекции объектов TreeNode, связанных по-с^Дством своих полей с указателями. Объект TreeNode создается динамически
°Мощыо функции new.
TreeNode<int> *p;	// объявление указателя	• ।
//на целочисленный узел дерева
р ~ new TreeNode(item); // левый и правый указатели равны NULL	- -Vj
Вызов функции new обязательно должен включать значение данных. £ в качестве параметра передается также указатель объекта TreeNode, используется вновь созданным узлом для присоединения дочернего узла, Гь ределим функцию GetTreeNode, принимающую данные и ноль или бол указателей объекта TreeNode, для создания и инициализации узла бинард0 6 дерева. При недостаточном количестве доступной памяти программа прекв? щается сразу после выдачи сообщения об ошибке.
// создать объект TreeNode с указательными полями Iptr и rptr. //по умолчанию указатели содержат NULL.
template <class т>
TreeNode<T> *GetTreeNode(Т item, TreeNode<T> *lptr = NULL, TreeNode<T> *rptr = NULL)
{
TreeNode<T> *p;
// вызвать new для создания нового узла
// передать туда параметры Iptr и rptr р = new TreeNode<T> (item, Iptr, rptr);
// если памяти недостаточно, завершить программу сообщением об ошибке if (р == NULL) {
cerr « "Ошибка при выделении памяти!\п";
exit(1); )
// вернуть указатель на выделенную системой память return р;
}
Функция FreeTreeNode принимает указатель на объект TreeNode и освобождает занимаемую узлом память, вызывая функцию C++ delete.
// освободить динамическую память, занимаемую данным узлом template <class t>
void FreeTreeNode(TreeNode<T> *p)
{ delete p;
)
Обе эти функции находятся в файле treelib.h вместе с функциями обрй ботки бинарного дерева, представленными в разделе 11.2.
Пример определения дерева. Функция GetTreeNode может быть исП°_г0 зована для явного построения каждого узла дерева и, следовательно, в дерева. Это было продемонстрировано на дереве с тремя узлами, содержа® 10, 20 и 30. Для более крупного экземпляра процесс будет немного У1® тельным, так как вы должны включить в дерево все значения дая01Д указателей.
в этой главе создадим функцию MakeCharTree, строящую три дерева, -ДУ которых содержат символьные элементы данных. Эти деревья будут ^Пользоваться для иллюстрации методов TreeNode в следующем разделе. Параметры функции включают в себя ссылку на корень дерева и число и п < 2), которое служит для обозначения дерева. Следующие объявления указатель на объект TreeNode, с именем root, и назначают его корнем дерева Тгее_2.
e0Node<char> *root; // объявить указатель на корень
gakeCharTree(root,2); Н сформировать на этом корне дерево tree_2
На рис. 11.9 показаны три дерева, построенных этим методом. Полный листинг функции MakeCharTree находится в файле treeiib.h. Эта функция распространяет технологию из примера 11.2 на деревья с пятью и девятью уздами.
Тгее_1
Tree 2
Рис. 11.9. Дерево MakCharTree
11.2. Разработка функций класса TreeNode
Связанный список — это линейная структура, позволяющая последова-ельно проходить узлы, используя указатель на следующий элемент. По-кЗДьку дерево является нелинейной структурой, похожего алгоритма про-бдения не существует. Мы вынуждены выбрать один из методов прохож-й среди которых наиболее широко используются прямой, симметричный -кратный методы. Каждый из них основывается на рекурсивной структуре Парного дерева.
-^горитмы прохождения существенно влияют на эффективность исполь-Ванйя дерева. В первую очередь МьГразработаем методы рекурсивного про
хождения, & затем на их основе создадим алгоритмы печати, копир и удаления, а также определения глубины дерева. Мы также расемотъ^ поперечный метод (breadth first) прохождения, который использует оче; для запоминания узлов. Этот метод сканирует дерево уровень за уров»» начиная с корня и передвигаясь к. первому поколению сыновей, затем **** второму и т.д. Метод находит важное применение в административных рархиях, где власть распределяется от главы к другим уровням управлев»*
Наша реализация метода прохождения предусматривает параметр-фу**' цию visit, которая осуществляет доступ к содержащимся в узле дайны Передавая в качестве параметра функцию, можно указать некоторое действа ’ которое должно выполняться в каждом узле в процессе прохождения дерев template cclass т>
void <Метод_прохода> (TreeNode<T> *t, void visit(Tfi item));
Всякий раз при вызове метода клиент должен передавать имя функции выполняющей некоторое действие с данными, имеющимися в узле. По мере того как метод перемещается от узла к узлу, вызывается эта функция и выполняется предусмотренное действие.
Замечание. Понятие параметра-функции является относительно простым, ио требует некоторого пояснения. В общем случае функция может быть аргументом, если указать ее имя, список параметров и возвращаемое ею значение. Пусть, например, функция G имеет параметр-функцию f. В этом параметре указывается имя функции (f), список параметров (int х) и возвращаемый тип (int).	,
Список параметров
♦ I--------------1
int G(int t, int f (int x))
возвращаемый тип имя функции список параметров
int Glint t, int flint x))	// параметр-функция f
{
// вычислить fit) с помощью функции f и параметра t.
// возвратить произведение этого значения и t return t * fit);
}
Вызывая функцию G, клиент должен передать функцию для f с той структурой. Пусть в нашем примере клиент определил функцию XSquared» вычисляющую х2.
И XSquared - целочисленная функция с целочисленным параметром х int XSquared(int х) (
return х*х;
}
Клиент вызывает функцию G с целочисленным параметром t и параМеТ ром-функцией XSquared. Оператор
Y - G(3, XSquared)
вызывает функцию G, которая в свою очередь вызывает функцию XSquare параметром 3. Оператор cout печатает результат 27.
cout « G13.0, XSquared) « endl;
Рекурсивные методы прохождения деревьев
Рекурсивное определение бинарного.дерева определяет эту структуру как овнь с двумя поддеревьями, которые идентифицируются полями левого и авого указателей в корневом узле. Сила рекурсии проявляется вместе с родами прохождения. Каждый алгоритм прохождения дерева выполняет в е три действия: заходит в узел, рекурсивно спускается по левому поддереву пс правому поддереву. Спустившись к поддереву, алгоритм определяет, что * находится в узле, и может выполнить те же три действия. Спуск прекращается по достижении пустого дерева (указатель == NULL). Различные алгоритмы рекурсивного прохождения отличаются порядком, в котором они выполняют свои действия в узле. Мы изложим симметричный и обратный методы, в которых сначала осуществляется спуск по левому поддереву, а затем по Правому. Другие методы оставляем вам в качестве упражнений.
Симметричный метод прохождения дерева
Симметричный метод прохождения начинает свои действия в узле спуском । до его левому поддереву. Затем выполняется второе действие — обработка данных в узле. Третье действие — рекурсивное прохождение правого поддерева» В процессе рекурсивного спуска действия алгоритма повторяются в каждом новом узле.
Итак, порядок операций при симметричном методе следующий:
1.	Прохождение левого поддерева.
2.	Посещение узла.
3.	Прохождение правого поддерева.
Мы называем такое прохождение LNR (left, node, right). Для дерева ТгееО в функции MakeCharTree "посещение" означает печать значения из поля данных узла.
При симметричном методе прохождения дерева Тгее О выполняются сле-*№°Щие операции.
Действие	Печать	Замечания
Спуститься от А к В: Посетить В;	В	Левый сын узла В равен NULL
Спуститься от В к D:		D — листовой узел
Посетить D;	D	Конец левого поддерева узла А
Посетить корень А: Спуститься от А к С:	А	
Спуститься от С к Е: Посетить Е;	Е	Е — листовой узел
Посетить С;	С	Готово!
Узлы посещаются в порядке В D А Е С. Рекурсивная функция сдах. спускается по левому дереву [t-—>Left()], а затем посещает узел. Второй рекурсии спускается по правому дереву [t—>Right()].
// симметричное рекурсивное прохождение узлов дерева
template <class Т>
void Inorder (TreeNode<T> *t, void visit(T& item)) (
// рекурсивное прохождение завершается на пустом поддереве if (t != NULL) (
Inorder(t->Left(), visit)# // спуститься no левому поддереву visit(t->data);	// посетить узел
Inorder(t->Right(), visit); // спуститься по правому поддереву )
)
Обратный метод прохождения дерева. При обратном прохождении посе* щение узла откладывается до тех пор, пока не будут рекурсивно пройдены оба его поддерева. Порядок операций дает так называемое LRN (left, right node) сканирование.
1.	Прохождение левого поддерева.
2.	Прохождение правого поддерева.
3.	Посещение узла.
При обратном прохождении дерева Тгее_О узлы посещаются в порядке D В Е С А.
Действие	Печать	Замечания
Спуститься от А к В:		Левый сын узла В равен NULL
Спуститься от В к D:		D — листовой узел
Посетить D;	D	Все сыновья узла В пройдены
Посетить В;	В	Левое поддерево узла А пройдено
Спуститься от А к С:		
"Спуститься от С к Е:		Е — листовой узел
Посетить Е;	Е	Левый сын узла С
Посетить С;	С	Правый сын узла А
Посетить корень А:	А	Готово!
Функция сканирует дерево снизу вверх. Мы спускаемся вниз по левому дереву [t->Left()], а затем вниз по правому [t->Right()]. Последней операцией является посещение узла.
// обратное рекурсивное прохождение узлов дерева template <class Т>
void Postorder (TreeNode<T> *t, void visit (T& item)) (
11 рекурсивное прохождение завершается на пустом поддереве
if (t !» NULL) {
Postorder(t->Left(), visit);	// спуститься по левому поддереву
Postorder(t->Right(), visit);	// спуститься no правому поддереву
visit(t->data);	// посетить узел
)
Прямой метод прохождения определяется посещением узла в первую оче-и последующим прохождением сначала левого, а потом правого его
^ревьев (NLR).
Д Ясно, что префиксы pre, in и post в названиях функции показывают, гда происходит посещение узла. В каждом случае сначала осуществлялось Похождение по левому поддереву, а уже потом по правому. Фактически *2-гиествуют еще три алгоритма, которые выбирают сначала правое поддерево С потом левое. Для печати дерева будем использовать RNL-прохождение. Алгоритмы прохождения посещают каждый узел дерева. Они дают эквивалент последовательного сканирования массива или связанного списка. Функ-дйЛ прямого, симметричного и обратного методов прохождения содержатся в файле treescan.h.
Пример 11.2
1. Для символьного дерева Тгее_2 имеет место следующий порядок
Тгее_2
посещения узлов.
Прямой:	ABDGCEHI F
Симметричный:	DGBAHEI CF
Обратный:	GDBHI EFCA
2. Результат симметричного прохождения дерева Тгее_2 производится следующими операторами:
// функция visit распечатывает поле данных void FrintChar(chars elem) { cout «elem « ”
>
TreeNode<char> ’root;
MakeCharTree(root, 2); // сформировать дерево Tree_2 с корнем root
// распечатать заголовок и осуществить прохождение, используя
// функцию PrintChar для обработки узла
cout < "Симметричное прохождение:
Inorder (root, PrirtChar);
11.3. Использование алгоритмов прохождеций деревьев
На рекурсивных алгоритмах прохождения основаны многие прилов деревьев. Эти алгоритмы обеспечивают упорядоченный доступ к узлам. ном разделе демонстрируется использование алгоритмов прохождения ддя счета количества листьев иа дереве, глубины дерева и для печати дерева°ь каждом случае для посещения узлов мы должны применять ту или ин * стратегию прохождения.	,
Приложение: посещение узлов дерева
Для многих приложений требуется просто обойти узлы дерева, неважно в каком порядке. В этих случаях клиент волен выбрать любой алгоритм проход, дения. В данном приложении функция CountLeaf проходит дерево с целью подсчета его листьев. При распознавании очередного листа происходит приращение параметра count.
// эта функция использует обратный метод прохождения.
//во время посещения узла проверяется, является ли он листовым template CountLeaf (<TreeNode<T> *t, int& count)
< -
// Использовать обратный метод прохождения
if (t !« NULL)
{
CountLeaf(t->Left(), count); // пройти левое поддерево
CountLeaf(t->Right(), count); // пройти правое поддерево
// проверить, является ли данный узел листом.
// Если да, то произвести приращение переменной count
if (t->Left() =« NULL t->Right() == NULL) count++;
) )
Функция Depth использует обратный метод прохождения для вычисления глубины бинарного дерева. В каждом узле вычисляется глубина его левого И правого поддеревьев. Итоговая глубина на единицу больше максимальной глу* бины поддеревьев.
// эта функция использует обратный метод прохождения для вычисления глубины
// левого и правого поддеревьев узла и возвращает результирующее
// значение глубины, равное 1 + max(depthLeft, depthRight).
// глубина пустого дерева равна -1
template <class Т>
void Depth (TreeNode<T> *t) {
int depthLeft, depthRight, depthval;
if (t =- NULL) depthval = -1;
else
{
depthLeft - Depth(t~>Left());
depthRight - Depth(t->Right());
depthval - 1 + {depthLeft > depthRight?depthLeft-.depthRight) ;
)
return depthval;
)
Программа 11.1. LeafCount и Depth
Эта программа иллюстрирует использование функций LeafCount и Depth для прохождения символьного дерева Тгее_2. Итоговые значения LeafCount и Depth распечатываются.
^include <iostream.h>
// включить класс TreeNode и библиотеку функций jinclude ‘'treenode.h''
finclude "treelib.h"
void main(void)
(
TreeNode<char> ‘root;
// использовать дерево Tree_2 MakeCharTree(root, 2);
// переменная, которая обновляется функцией countLeaf int leafCount = 0;
// вызвать функцию CountLeaf для подсчета числа листьев CountLeaf(root, leafcount);
cout « ’’Число листьев равно ” « leaf Count « endl;
11 вызвать функцию Depth для вычисления глубины дерева cout « "Глубина дерева равна "
« Depth(root) « endl;
)
/*
выполнение программы 11.1>
Число листьев равно 4
Глубина дерева равна 3
Приложение: печать дерева
Функция печати дерева создает изображение дерева, повернутое на 90 гра-ов против часовой стрелки. На рис. 11.10 показано исходное дерево Тгее_2 ев&) и распечатанное. Поскольку принтер выводит информацию построчно, г^оритм использует RNL-прохождение и распечатывает узлы правого подде-F г» ,Р®Ньше узлов левого поддерева. Узлы дерева Tree 2 печатаются в порядке
фЛ н А В g d-
^НКЦ^ТЯ PrintTree распечатывает поле данных узла и уровень узла. Вызы-аЯ п₽огРамма передает корень с уровнем 0. На каждом рекурсивном фовмВе ФункЙии PrintTree нужно делать отступ для уровня узла. В нашем Величина отступа вычисляется как indentBlock * level, где indentBlock — уС ч 6’ задающая число пробелов, которое приходятся на один уровень Чтобы распечатать узел, сначала вычисляется число пробелов в отступе, icy фЛ^твующее уровню этого узла, а затем выводится поле данных. Посколь-К1*Ия PrintTree использует стандартный поток cout, для типа Т должен Ч1бст^^еделен оператор «. На рис. 11.11 показаны уровни и пробелы, пред-^Щие каждому узлу дерева Тгее_2.
Printed Tree 2	1
i
Рис. 11.10. Распечатанное дерево Тгее_2	1
Уровни 0	12	3
12	F
б	С	•	-
18	I
Отступ	12	Е
18	Н
О А б	В
18	G
12	D
Рис. 11.11. Печать дерева Тгее_2
Код функции PrintTree находится в файле treeprmt.h. // промежуток между уровнями const int indentBlock =6; // вставить num пробелов на текущей строке void IndentBlanks(int num) ( for (int i » 0; i < num; i++) cout « ” ) C // распечатать дерево боком, используя RNL-прохождение template cclass T> void PrintTree (Treenode<T> *t, int level) <	i
// печатать дерево с корнем t, пока t != NULL if (t !- NULL) { | // печатать правое поддерево узла t PrintTree(t->Right(), level+1);	- ,,
j « 1
i
I
1 I f i j 1
I
I I
fl выровнять текущий уровень и вывести поле данных indentBlanks(indentBlock * level);
cout « t->data « endl;
II печатать левое поддерево
printTree(t->Left(), level+1);
I )
Приложение: копирование и удаление деревьев
Утилиты копирования и удаления всего дерева вводят новые понятия и дготавливают нас к проектированию класса деревьев, который требует деструктор и конструктор копирования. Функция СоруТгее принимает исходное яерево и создает его дубликат. Процедура DeleteTree удаляет каждый узел дерева, включая корень, и высвобождает занимаемую узлами память. Функции, разработанные для бинарных деревьев общего вида, находятся в файле treelib.h.
Копирование дерева. Функция СоруТгее использует для посещения узлов обратный метод прохождения. Этот метод гарантирует, что мы спустимся по дереву на максимальную глубину, прежде чем начнем операцию посещения, которая создает узел для нового дерева. Функция СоруТгее строит новое дерево снизу вверх. Сначала создаются сыновья, а затем они присоединяются к своим родителям, как только те будут созданы. Этот подход использовался в функции MakeCharTree. Например, порядок операций для дерева Tree О следующий:
Тгее_О
d - GetTreeNode (' O') ;
е - GetTreeNode {• Е');
Ь - GetTreeNode('В', NULL, d)i с • GetTreeNode('С', e, NULL); a ” GetTreeNode ('A', b, c) ; root >= a;
Сначала мы создаем сына D, который затем присоединяется к своему Родителю В при создании узла. Создается узел Е и присоединяется к своему Родителю С во время рождения (или создания) последнего. Наконец, создается к°рень и присоединяется к своим сыновьям В и С.
Алгоритм копирования дерева начинает с корня и в первую очередь строит Ле®ое поддерево узла, а затем — правое его поддерево. Только после этого ^Дается новый узел. Тот же рекурсивный процесс повторяется для каждого Ла» Соответственно узлу t исходного дерева создается новый узел с указа-newlptr и newrptr.
АФи обратном методе прохождения сыновья посещаются перед их роди-Ukt6®1, ® результате в новом дереве создаются поддеревья, соответствующие с ^ft() и t—>Right(). Сыновья присоединяются к своим родителям в момент Яания последних.
newlptr - CopyTree(t->Left());
newrptr =• СоруТгее(t->Right());
11 создать родителя и присоединить к нему его сыновей newnode = GetTreeNode(t->d.ata, newlptr, newrptr);
Суть посещения узла t в исходном дереве заключается в создании новою узла на дереве-дубликате.
Символьное дерево Tree О является примером, иллюстрирующим рекурсивную функцию СоруТгее. Предположим, что главная процедура определяет корни rootl и root2 и создает дерево ТгееО. Функция СоруТгее создает новое дерево с корнем root2. Проследим алгоритм и проиллюстрируем процесс создания пяти узлов на дереве-дубликате.
TreeNode<dhar> *rootl, *root2; // объявить два дерева	t
MakeCharTree(rootl, О);	// rootl указывает на Тгее_О
-root2 - СоруТгее(rootl);	// создать копию дерева Тгее_О
1.
2.
3.
4.
Пройти потомков узла А, начиная с левого поддерева в узле В и к узлу D, который является правым поддеревом узла В. Создать в0В*С узел с данными, равными D, и левым и правым указателями, равным» NULL [рис. 11.12 (А)].
Сыновья узла В пройдены. Создать новый узел с данными, равнЫ^* В, левым указателем, равным NULL, и правым указателем, указы ющим на узел D [рис. 11.12 (В)].
Поскольку левое поддерево узла А пройдено, начать прохожде&не _ правого поддерева и дойти до узла Е. Создать новый узел с даН# из узла Е и указательными полями, равными NULL.
После обработки Е перейти к его родителю и создать новый У3®^ данными из С. В поле правого указателя поместить NULL, а указателю присвоить ссылку на дочерний узел Е [рис. 11-13 (АН*
root!
root2
rootl
root!
Рис. 11.13. Копирование правого поддерева узла А
8* Последний шаг выполняется в узле А. Создать новый узел с данными из А и присоединить к нему сына В слева и сына С справа [рис. 11.13 (В)]. Копирование дерева завершено.
Ьь^^^ЦИя СоруТгее возвращает указатель на вновь созданный узел. Это воз-ае1й°е значение используется родителем, когда тот создает свой собствен-Узел и присоединяет к нему своих сыновей. Функция возвращает корень ЙВа»щей программе.
// создать дубликат дерева t и возвратить корень нового дерева template <class Т>
TreeNode<T> "СоруТгее(TreeNode<T> *t)
{
// переменная newnode указывает на новый узел, создаваемый
// посредством вызова GetTreeNode и присоединяемый в дальнейшем // к новому дереву, указатели newlptr и newrptr адресуют сыновей // нового узла и передаются в качестве параметров в GetTreeNode TreeNode<T> *newlptr, "newrptr, "newnode;
// остановить рекурсивное прохождение при достижении пустого дерева if (t — NULL) return NULL;
// СоруТгее строит новое дерево в процессе прохождения узлов дерева t. в каждл. // узле этого дерева функция СоруТгее проверяет наличие левою сына, если он // есть, создается его копия, в противном случае возвращается NULL. СоруТгее // создает копию узла с помощью GetTreeNode и подвешивает к нему копии сыновей
if (t->Left() != NULL)
newlptr - СоруТгее(t->Left()); else
newlptr  NULL;
if (t->Right() != NULL)
newrptr = СоруТгее(t->Right <));
else
newrptr - NULL;
// построить новое дерево снизу вверх, сначала создавая
// двух сыновей, а затем их родителя
newnode  GetTreeNode(t->data, newlptr, newrptr);
// вернуть указатель на вновь созданное дерево return newnode;
Удаление дерева. Когда в приложении используется такая динамическая структура, как дерево, ответственность за освобождение занимаемой им памяти ложится на программиста. Для бинарного дерева общего вида разработаем функцию DeleteTree, в которой применяется обратный метод прохождения. Это гарантирует, что мы посетим всех сыновей родительского узла» прежде чем удалим его. Операция посещения заключается в вызове функции FreeTreeNode, удаляющей узел.
// // t	использовать обратный алгоритм для прохождения узлов дерева и удалить каждый узел при его посещении emplate <class Т>
V	oid DeleteTree(TreeNode<T> *t)
i (	f (t !- NULL) DeleteTree(t->Left()); DeleteTree(t->Right()); FreeTreeNode (t); 1
}	Болес общая процедура удаления дерева удаляет узлы и сбрасывает
ф И1	ункция ClearTree вызывает DeleteTree для удаления узлов дерева и СР *ает указателю на корень значение NULL.
.• ^цзвать функцию DeleteTree для удаления узлов дерева.
7 затем сбросить указатель на его корень в NULL <class т>
ClearTree(TreeNode<T> &t)
' peleteTree<t)?
t «• NULL;
ц теперь корень пуст
Программа 11.2. Тестирование функций СоруТгее и DeleteTree
Эта программа использует дерево Тгее_О и создает копию, которая адресуется указателем root2. Мы осуществляем обратный проход вновь созданного дерева, преобразуя букву в поле данных каждого узла из прописной в строчную. Результат распечатывается с помощью функции PrintTree.
linclude <iostream.h>
^include <ctype.h>
linclude <stdlib.h>
linclude "treescan.h"
linclude "treelib.h”
linclude "treeprnt. h"
// функция преобразования прописной буквы в строчную void Lowercase(char &ch)
(
ch = tolower(ch);
I
void main (void)
<
// указатели на исходное дерево и его дубликат
TreeNode<char> *rootl, *root2;
// создать дерево Тгее_О и распечатать его
MakeCharTree(rootl, 0);
PrintTree (rootl, 0);
И копировать дерево
cout « endl « "Копия:" « endl;
root2 = СоруТгее(rootl);
// выполнить обратное прохождение и распечатать дерево Postorder(root2, Lowercase);
I PrintTree(root2, 0);
*/
^Выполнение программы 11.2>
C
D
В
Копия:
-г
Приложение: вертикальная печать дерева
Функция PrintTree создает повернутое набок изображение дерева. £ja дой строке узел распечатывается в позиции, определяемой его уровнем такое дерево воспринимается трудно, этот прием позволяет распечать большие деревья. На 80-колоночном листе неограниченной длины Можн0ВаТь Сразить дерево с 216 — 1 — 65535 узлами, если промежуток между уро^*3®* indentBlock равен пяти пробелам. Вертикальная распечатка дерева более ничена, так как элементы данных и межуровневые промежутки будут лагаться по ширине листа. Но для относительно небольших деревьев такая 2** тинка более реалистична и привлекательна. В данном приложении мы разраб таем инструменты для реализации функции PrintVTree (см. файл treeprint.^)
Функция PrintVTree требует нового алгоритма прохождения, который си нирует дерево уровень за уровнем, начиная с корня на уровне 0. Этот метод называемый поперечным прохождением или прохождением уровней (1е^* scan), не спускается рекурсивно вдоль поддеревьев, а просматривает дерево поперек, посещая все узлы на одном уровне, и затем переходит на уровень ниже В отличие от рекурсивного спуска здесь более предпочтителен итерационный алгоритм, использующий очередь элементов. Для каждого узла в очередь помещается всякий непустой левый или правый указатель на сына этого узла Это гарантия того, чточщноуровневые узлы следующего уровня будут посещаться в нужном порядке. Символьное дерево Тгее_2 иллюстрирует этот алгоритм.
PrintTree
С
Е
А
D
В
PrintVTree
А
В	С
D Е
Уроаень 0:
Уровень 1:
Уровень 2:
Уровень 3:
Посещение А
Посещение В. С
Посещение D. Е. F
Посещение G. Н. I
Алгоритм поперечного прохождения
Шаг инициализации:
Поместить в очередь корневой узел.
Шаги итерации:
Прекратить процесс, если очередь пуста.
Удалить из очереди передний узел р и распечатать его значение.
Использовать этот узел для идентификации его детей на следую®6 уровне дерева.
if (p->Left() !~ NULL) // проверить наличие левого сына Q.Qlnsert(p->Left <));
if (p->Right() != NULL) If проверить наличие правого сына
Q.Qlnsert(p->Right());
Пример 11.3
Алгоритм поперечного прохождения иллюстрируется на дереве Тгее О.
Инициализация: Вставить узел А в очередь.
1:	Удалить узел А из очереди.
Печатать А.
Вставить сыновей узла А в очередь.
Левый сын — В
Правый сын = С
2:	Удалить узел В из очереди.
Распечатать В.
3:	Удалить узел С из очереди.
Левый сын = Е
4:	Удалить узел D из очереди.
Распечатать D.
Узел D не имеет сыновей.
5:	Удалить узел Е из очереди. Алгоритм завершается. Очередь пуста.
И Прохождение дерева уровень за уровнем с посещением каждого узла tentplate <class т>
void LevelScan(TreeNode<T> *t, void visit(T& item)) {
it запомнить сыновей каждого узла в очереди, чтобы их
// можно было посетить в этом порядке на следующем уровне
Queue<TreeNode<T> *> Q;
TreeNode<T> *р;
// инициализировать очередь, вставив туда корень
Q.Qinsert(t) ;
// продолжать итерационный процесс, пока очередь не опустеет while (iQ.QEmptyO)
(
II удалить первый в очереди узел и выполнить функцию visit
Р • Q.QDeleteO;
visit(p->data);
// если есть левый сын, вставить его в очередь
if (p->Left() != NULL)
Q.Qlnsert(p->Left());
11 если есть правый сын, вставить его в очередь
if (p->Right() != NULL)
Q.Qlnsert(p->Right ());
1
Алгоритм PrintVTree. В функцию вертикальной печати дерева перед корень дерева, максимальная ширина данных и ширина экрана:
void PrintVTree(TreeNode<T> *t. int dataWidth. int screenwidth)
Параметры ширины позволяют организовать экран. Пусть dataWidth screenWidth = 64 = 26. Тот факт, что значение ширины равно степени е позволяет описать поуровневую организацию данных. Поскольку мы не зл ’ структуру дерева, то полагаем, что места должно хватать для полного би кого дерева. Узлы строятся в координатах (уровень, смещение).
Уровень 0:	'
Корень рисуется в точке (0,32).
Уровень 1:
Поскольку корень смещен на 32 позиции, следующий уровень имеет смещение 32/2 = 16 = screenWidth/22. Два узла первого уровня рас. полагаются в точках (1, 32-смещение) и (1, 32+смещение), т.е. в точках (1,16) и (1,48).
Уровень 2:
На втором уровне смещение равно screenWidth/23 = 8. Четыре узла второго уровня располагаются в точках (2, 16-смещение), (2, 16+сме-щение), (2, 48-смещение), (2, 48+смещение), т.е. в точках (2,8), (2,24), (2,40) и (2,56).
Уровень i:
Смещение равно screen Width/2й1. Позиция каждого узла данного уровня определяется во время посещения его родителя на уровне i-1. Пусть позиция родителя равна (i-1, parentPos). Если узел i-ro уровня является левым сыном, то его позиция равна (i, parentPos-смещение), а если правым — (i, parentPos+смещение).
Уровень 0
Уровень 1
Уровень 2
Уровень 3
4
20	28
44	52	60
PrintVTree использует две очереди и поперечный метод прохождения УзЛ° дерева. В очереди Q находятся узлы, а очередь QI содержит уровни и позиц печати в форме записей типа Info. Когда узел добавляется в очередь Q» С°яЯ. ветствующая ему информация о печати запоминается в QL Элементы УД
ются в тандеме во время посещения узла.
// запись для хранения координат (х,у> узла struct Info
(
int xlndent, yLevel;
};
// Очереди для хранения узлов и информации о печати Queue<TreeNode<T> * Q;
Queue<Info> QI;
Программа 11.3. Вертикальная печать дерева________________________
Эта программа распечатывает символьное дерево Тгее 2 на 30- или на 60-символьном листе. Ширина данных для вывода dataWidth = 1.
finclude <iostream.h>
// включить функцию PrintVTree из библиотеки
^include '’treelib.h"
|include "treeprnt. h"
void main (void)
(
// объявить символьное дерево
TreeNode<char> *root;
i
// назначить дереву Tree_2 корень root
IMakeCharTree(root, 2);
cout « "Печать дерева на 30-символьном экране" « endl;
PrintVTree(root, 1, 30);
,i cout « endl << endl;
cout « "Печать дерева на 60-символьном экране" « endl;
PrintVTree(root, 1, 60);
)
/*
выполнение программы 11.3>
Печать дерева на 30-символьном экране
А
в	С
D	Е	F
G	HI
Печать дерева на 60-символьном экране
А
В	С
О	Е	F
G	HI
»/
11.4. Бинарные деревья поиска
^^иычное бинарное дерево может содержать большую коллекцию данных и Од *е °®еспечивать быстрый поиск, добавление или удаление элементов.
14 ИЭ Наи^олее важных приложений деревьев является построение классов „екЦий. Нам уже знакомы проблемы, возникающие при построении общего Са коллекций нз класса SeqList н его реализации с помощью массива или /
связанного списка. Главную роль в классе SeqList играет метод Find зующий последовательный поиск. Для линейных структур сложность алгоритма равна O(N), что неэффективно для больших коллекций. В случае древовидные структуры обеспечивают значительно большую дительность, так как путь к любым данным не превышает глубины йаа°-Эффективность поиска Максимизируется при законченном бинарном Деъ£ составляет O(log2N). Например, в списке из 10000 элементов предполагая число сравнений при последовательном поиске равно 5000. Поиск же на 3^°* ченном дереве потребовал бы не более 14 сравнений. Бинарное дерево прет^ ляет большие потенциальные возможности в качестве структуры у-- Та*' списка.
хРанени^
Линейный связанный список
5000 сравнении
Чтобы запомнить элементы в виде дерева с целью эффективного доступа, мы должны разработать поисковую структуру, которая указывает путь к элементу. Эта структура, называемая бинарным деревом поиска (binary search tree), упорядочивает элементы посредством оператора отношения Чтобы сравнить узлы дерева, мы подразумеваем, что часть или все поле данных определено в качестве ключа и оператор сравнивает ключи, когда размещает элемент на дереве. Бинарное дерево поиска строится по следующему правилу:
Для каждого узла значения данных в левом поддереве меньше, чем в этом узле, а в правом поддереве — больше илн равны.
На рис. 11.14 показан пример бинарного поискового дерева. Это дерево называется поисковым потому, что в поисках некоторого элемента (ключа) мы можем идти лишь по совершенно конкретному пути. Начав с корня, мы сканируем левое поддерево, если значение ключа меньше текущего узла. В
Рис. 11.14. Дерево бинарного поиска
(угивном случае сканируется правое поддерево. Метод создания дерева по-оляет осуществлять поиск элемента по кратчайшему пути от корня. На-
^>0Мер» поиск числа 37 требует четырех сравнений, начиная с корня.
Текущий узел
Действие
Корень — 50
Узел = 30
Узел = 35
Узел = 37
Сравнить ключ “ 37 и 50
поскольку 37 < 50, перейти в левое поддерево
Сравнить ключ = 37 и 30
поскольку 37 >= 30, перейти в правое поддерево
Сравнить ключ “37 и 35
поскольку 37 >= 35, перейти в правое поддерево
Сравнить ключ =37 н 37. Элемент найден.
На рнс. 11-15 показаны различные бинарные деревья поиска.
Ключ в узле бинарного дерева поиска
Ключ в поле данных работает как этикетка, с помощью которой можно идентифицировать узел. Во многих приложениях элементы данных являются записями, состоящими из отдельных полей. Ключ — одно из этих полей. Например, номер социальной страховки является ключом, идентифицирующим студента университета.
Номер социальной страховки	Имя студента	Средний балл
(9-символьная строка)	(строка)	(число с плавающей точкой)
Ключевое поле
struct Student {
String ssn;
String name;
float gpa;
Ключом может быть все поле данных и только его часть. На рис. 11.5 Узлы содержат единственное целочисленное значение, которое и является ключом. В этом случае узел 25 имеет ключ 25, и мы сравниваем два узла йутеМ сравнения целых чисел. Сравнение производится с помощью целочис-ледных операторов отношения "<” и "==". Для студента университета клю-
Рис. 11.15. Примеры деревьев бинарного поиска
чом является ssn, и мы сравниваем две символьные строки. Это дел помощью перегрузки операций. Например, следующий код реализуе^^* шение "<” для двух объектов Student:
int operator < (const Students s, const Students t) (
return s.ssn < t.ssn; // сравнить ключи ssn )
В наших приложениях мы приводим ряд примеров ключ/данные. В w страциях мы используем простой формат, где ключ и данные — одно ито^
Операции на бинарном дереве поиска
Бинарное дерево поиска является нелинейной структурой для храневи множества элементов. Как и любая списковая структура, дерево должно и пускать включение, удаление и поиск элементов. Для поискового депея* требуется такая операция включения (вставки), которая правильно распада гает новый элемент. Рассмотрим, например, включение узла 8 в дерево Вйь STreel. Начав с корневого узла 25, определяем, что узел 8 должен быть в левом поддереве узла 25 (8<25). В узле 10 определяем, что место узлгВ должно быть в левом поддереве узла 10, которое в данный момент пусто, Узел 8 включается в дерево в качестве левого сына узла 10.
BinSTree 1	BinSTree i
До каждого вставляемого в дерево узла существует конкретный путь. Тот же путь может использоваться для поиска элемента. Поисковый алгоритм берет ключ и ищет его в левом или в правом поддереве каждого узла, coCT??j ляющего путь. Например, поиск элемента 30 на дереве BinSTree l (рис. П-1'' начинается в корневом узле 25 и переходит в правое поддерево (30 > затем в левое поддерево. (30 < 37). Поиск прекращается на третьем сравнении, когда ключ совпадает с числом 30, хранящемся в узле.
BinSTree 1
Р связанном списке операция удаления отсоединяет узел и соединяет его ^^ддтестирнника со следующим узлом. На бинарном дереве поиска подобная Грация намного сложнее, так как узел может нарушить упорядочение эле-ейтов дерева. Рассмотрим задачу удаления корня 25 из BinSTree_l. В ре удьтате появляются два разобщенных поддерева, которым требуется новый йОрСНЬ.	-	пг.
На первый взгляд напрашивается решение выбрать сына узла 25 — ска-Л1ЬГ 37 — и заменить его родителя. Однако это простое решение терпит еУдачу, так как некоторые узлы оказываются не с той стороны корня. Поскольку данное дерево относительно невелико, мы можем установить, что 16 или 30 являются допустимой заменой корневому узлу.
Неудачное решение: 30 не на месте
BinSTree_1
Объявление абстрактного типа деревьев
Абстрактный тип данных (АВТ) для списка строится по образцу класс SeqList. Тот факт, что бинарное дерево поиска хранит элементы данных виде нелинейного списка, становится существенной деталью реализации ei методов. Заметим, что этот ADT является зеркальным отражением ADT д/ класса SeqList, но имеет дополнительный метод Update, позволяющий о яовлять поле данных, и метод GetRoot, предоставляющий доступ к корнево* Узлу, а следовательно, и функциям прохождения из treescan.h и к функцш печати из treeprint.h. Обратите внимание, что метод GetData класса SeqL: отсутствует, так как он относится к линейному списку.
APT для бинарных деревьев поиска
Ланкмв
Список элементов, хранящийся в виде бинарного дерева, и значение size, определяющее текущее число элементов в списке. Дерево содержит указатель на корень и ссылку на последний обработанный узел - текущую позицию.
^•рации
^°4структор ListSize ListEmpty	<Тот же, <Тот же, <Тот же,	что и в ADT для класса SeqList> что и в ADT для класса SeqList>					
		что и	в	ADT	ДЛЯ	класса	SeqList>
CiesrList	<Тот же,	что и	в	А ГТ	для	класса	SeqList>
Fincf Вход:	Ссылка на значение данных						
Предусловия: Нет
Процесс:	Осуществить поиск на дереве путем сравнения элемента
~ —.оимми. хранящимися в узле. Если происходит совпаден
Выход:	возвратить 1 (True), если произошло совпадение, и п.. данные из совпавшего узла параметру.	4св0и1() В противном случае возвратить 0 (False).
Постусловия: Insert	Текущая позиция соответствует совпавшему узлу.
Вход:	Элемент данных
Предусловия: Процесс:	Нет Найти подходящее для вставки место на дереве. Добавить новый элемент данных.
Выход:	Нет
Постусловия: Delete	Текущая позиция соответствует новому узлу.	J
Вход:	Элемент данных
Предусловия: Процесс:	Нет Найти на дереве первый попавшийся узел, содержащий элемен данных. Удалить этот узел и связать все его поддеревья так чтобы сохранить структуру бинарного дерева поиска.	*
Выход;	Нет
Постусловия: Update Вход:	Текущая позиция соответствует узлу, заменившему удаленный Элемент данных
Предусловия:	Нет
Процесс:	Если ключ в текущей позиции совпадает с ключом элемента данных, присвоить элемент данных узлу. В противном случае вставить элемент данных в дерево.
Выход:	Нет
Постусловия: GetRoot	В списке может оказаться новое значение.
Вход:	Нет
Предусловия: Процесс:	Нет Получить указатель на корень.
Выход:	Возвратить указатель на корень.
Постусловия:	Не изменяется
Конец ADT для бинарных поисковых деревьев
Объявление класса BinSTree. Мы реализовали ADT для бинарных поисковых деревьев в виде класса с динамическими списковыми структурами. Этот класс содержит стандартный деструктор, конструктор копирования и перехуг женные операторы присваивания, позволяющие инициализировать °®ъекТ^ v играющие роль операторов присваивания. Деструктор отвечает за очистку списка, когда закрывается область действия объекта. Деструктор и оператор присваивания вместе с методом ClearList вызывают закрытый метод Delete К Мы также включили сюда закрытый метод СоруТгее для использовали конструкторе копирования и перетруженном операторе.
Спецификация класса BinSTree
Он ьмВлЕНИв flinclude <iostream.h> ^include <stdlib.h>
#include "treenode.h'
.opiate <class T>
^aSs BinSTree
I detected: // требуется для наследования в гл. 12
₽ // указатели на корень и на текущий узел
TreeNode<T> ♦root;
TreeNode<T> ‘current;
// число элементов дерева int size;
// распределение/освобождение памяти TreeNode<T> *GetTreeNode(const Т& item, TreeNode<T> *lptr, TreeNcde<T> *rptr);
void FreeTreeNode(TreeNode<T> *p);
// используется конструктором копирования и оператором присваивания void DeleteTree(TreeNode<T> *t);
// используется деструктором, оператором присваивания
// и функцией ClearList
TreeNode<T> ‘FindNode(const T& item, TreeNode<T>* & parent) const;
public:
11 конструктор и деструктор
BinSTree(void);
BinSTree(const BinSTree<T>fi tree);
-BinSTree(void);
// оператор присваивания
BinSTree<T>S operator- (const BinSTree<T>& rhs);
11 стандартные методы обработки списков int Find(T& item);
void insert(const T& xtem);
void Delete(const TS item);
void ClearList(void);
int ListEmpty(void) const;
int ListSize(void) const;
11 методы, специфичные для деревьев void Update(const T& item);
TreeWode<T> *GetRoot(void) const;
ОПИСАНИЕ
Этот класс имеет защищенные данные. Они представляют конструкцию ^«следования, которая обсуждается в гл. 12. Защищенный доступ функцио-Г^ьэо эквивалентен закрытому доступу для данного класса. Переменная root «зывает на корневой узел дерева. Указатель current ссылается на точку СДеДНего изменения в списке. Например, current указывает положение но-10 Узла после операции включения, а метод Find заносит в current ссылку совпавший с элементом данных.
j.'^ТанДартные операции обработки списков используют те же имена и парады» что и определенные в классе SeqList.
^е^аСС BinSTree содержит две операции, специфические для деревьев. tj/^Д update присваивает новый элемент данных текущему узлу или вклю-цОз в Дерево новый элемент, если тот не совпадает с данными в текущей д6ре^иИ- Метод GetRoot предоставляет доступ к корню дерева. Имея корень 8а» пользователь получает доступ к библиотечным функциям из treelib.h,
treescan.h и treeprint.h. Это расширяет возможности класса для прчв눑 различных алгоритмов обработки деревьев, в том числе распечатки 6
ПРИМЕР
BinSTree<int> Т;
дерево с целочисленными данными
Т.Insert(50);
Т.Insert(40);
Т.Insert(70);
Т.Insert(45);
создать
дерево с четырьмя узлами (А)
Т.Delete(40);
Т.ClearList();
удалить удалить
узел узлы
40 (В) дерева
//
(В)
// дерево univlnfo содержит информацию о студентах.
// Поле ssn является ключевым
BinSTree<Student> univlnfo;
Student stud;
// назначить ключ ’9876543789" и найти его на дереве
stud.ssn = "9876543789";
if (univlnfo.Find(stud))
(
// студент найден, присвоить новый средний балл и обновить узел stud.gpa = 3.86;
univlnfo.Update(stud);
)
else
cout « “Студент отсутствует в базе данных.” « endl;
11.5. Использование бинарных деревьев поиска
Jbix деревьев-	-
данном разделе расе®1
Класс BinSTree — мощная структура данных, которая используется обработки динамических списков. Практическая задача построения конкор; данса1 иллюстрирует типичное применение поисковых деревьев. Мы буД®_-использовать эту структуру в словарях в гл. 14, а в , рим ряд простых программ, где применяются деревья поиска.
Создание примеров деревьев поиска. В разделе 11.1 функция
Tree использовалась для создания ряда бинарных деревьев с символь данными. Похожая функция MakeSearchTree строит бинарные деревья B°geer< с целочисленными данными, применяя метод Insert. Например, дерев° 1 Под конкордансом в книге понимается алфавитный список всех слов заданного теК указателями на места их появлений. — Прим. ред.
hTree О использует шесть элементов заранее определенного массива аггО, сконструировать дерево с помощью объекта Т класса BinSTree.
.fltarr0(6] = (30z 20, 45, 5, 10, 40},-
r (i х 0; x < 6; i++) т. insert(arro(i 1);
MakeSearchTree создает второе восьмиэлементное дерево и дерево с десятью случайными числами из диапазона 10-99 (рис. 11.16). Параметры функции содержат объект класса BinSTree и параметр type (0 < type < 2), служащий для обозначения дерева. Код MakeSearchTree находится в файле makesrch.h.
Рис. 11.16. Деревья, созданные с помощью функции MakeSearchTree
Симметричный метод прохождения. При симметричном прохжденш Парного дерева сначала посещается левое поддерево узла, затем — сам
Наконец правое поддерево. Когда этот метод прохождения применяв1 Няарному дереву поиска, узлы посещаются в сортированном порядке. ^акт становится очевидным, когда вы сравниваете узлы в поддеревья
УЧего узла. Все . узлы левого поддерева текущего узла имеют меньши аН«, чем текущий узел, и все узлы правого поддерева текущего узла б равны текущему узлу. Симметричное прохождение бинарного
•”"Л пня каждого узла, который мы посещаем впервые, М€
я большие — в правом. В рез;
Программа 11.4. Использование дерева поиска
Эта программа использует функцию MakeSearchTree для создадв рева SearchTree_l, содержащего числа	Ия Де.
50, 20, 45, 70, 10, 60, 90, 30
С помощью метода GetRoot мы получаем доступ к корню этого део что позволяет вызвать функцию PrintVTree. Метод GetRoot позвав’ также распечатать элементы по возрастанию, используя функцию Inord с параметром-функцией Printlnt. Программа заканчивается удалением в ментов 50 и 70 и повторной печатью дерева.
#include •include	"makesrch.h" "treescan.h"	//	функция MakeSearch
•include	"treeprnt.h"	//	функция PrintVTree
•include	"bstree.h”	//	функция Inorder
// печать целого числа, используется функцией Inorder
void Printlnt(int& item) {
cout « item « ” }
void main(void) {
// объявить целочисленное дерево BinSTree<int> Tree;
// создать дерево поиска #1 и распечатать его вертикально
// при ширине в 40 символов
MakeSearchTree(Tree, 1);
PrintVTree(Tree.GetRoot(), 2, 40);
// симметричное прохождение обеспечивает
// посещение узлов по возрастанию
// хранящихся в них чисел
cout « endl « endl « ”Сортированный список:
Inorder(Tree.GetRoot(), Printlnt);
cout « endl;
cout « endl « "Удаление узлов 70 и 50." « endl;
Tree.Delete{70);
Tree.Delete(50);
PrintVTree(Tree.GetRoot(), 2, 40);
cout « endl; ) /*
<Выполнение программы 11.4>
50
20	70
10	45	60	90
30
Сортированный список: 10 20 30 45 50 60 70 90
Удаление узлов 70 и 50.
20
60
ю зо	90
*/
Дублированные узлы
Бинарное дерево поиска может иметь дублированные узлы. В операции «лючения мы продолжаем сканировать правое поддерево, если наш новый слемент совпадает с данными в текущем узле. В результате в правом поддереве -впавшего узла возникают дублированные узлы. Например, следующее дерево генерируется из списка 50 70 25 90 30 55 25 15 25.
Многие приложения не допускают дублирования узлов, а используют в данных поле счетчика экземпляров элемента. Это — принцип конкорданса, когда отслеживаются номера строк, в которых встречается некоторое слово. Вместо того чтобы несколько раз размещать слово на дереве, мы обрабатываем повторные случаи употребления этого слова путем помещения номеров строк в список. Программа 11.5 иллюстрирует лобовой подход, когда счетчнк дубликатов хранится как отдельный элемент данных.
Список: 50 70 25 90 30 55 25 15 25
программа 11.5. Счетчики появлений
Запись IntegerCount содержит целую переменную number и поле count, второе используется для запоминания частоты появлений целого числа в сЯйске. Поле number работает в качестве ключа в перегруженных операторах <” и позволяющих сравнить две записи IntegerCount. Эти операторы используются в функциях Find и Insert.
Программа генерирует 100000 случайных чисел в диапазоне 0-9 и связы-каждое число с записью IntegerCount. Метод Find сначала определяет, "У** ли уже данное число на дереве. Если есть, то значение поля count увели-^лется на единицу и мы обновляем запись. В противном случае новая запись ^ЮЧается в дерево. Программа завершается симметричным прохождением
3аМ25
узлов, в процессе которого происходит печать чисел и их
генерируемые случайным образом числа от О до 9 равновероятны.
тельно, каждый элемент может появиться приблизительно 10000 пись IntegerCount и два ее оператора находятся в файле intcount.h
#include	<iostream.h>		
((include	"random.h"	//	генератор случайных чисел
#include	’’bstree. h"	//	класс BinSTree
#include	'treescan.h"	//	функция Inorder
#include	"intcount.h"	//	запись IntegerCount
// вызывается функцией inorder для распечатки записи IntegerCount void PrintNumber(IntegerCountfi N)
{
cout « N.number « '	« N.count « endl;
void main(void) {
/l объявить дерево, состоящее из записей IntegerCount BinSTree<IntegerCount> Tree;
// сгенерировать 100000 случайный целых чисел в диапазоне 0..9 for (п = 0; п < 100000L; п++);
<
It сгенерировать запись IntegerCount со случайным ключом N.number = rnd.Random (10);
I/ искать ключ на дереве if (Tree.Find(N)) {
// ключ найден, увеличить count и обновить запись N.count++;
Tree.Update(N);
} else (
// это число встретилось впервые, вставить его с count=l N.count =1;
Tree.Insert(N); } }
// симметричной прохождение для распечатки ключей по возрастанию Inorder(Tree.GetRoot(), PrintNumber);
}
/*
<выполнение программы 11.5>
0:10116
1:9835
2:9826
3:10028
4:10015
5:9975
6:9983
7:10112
8:10082
9:10028
11.6. Реализация класса BinSTree
Класс BinSTree описывает нелинейный список и базовые операции включе-удаления и поиска элементов. Помимо методов обработки списков важную при реализации класса играет управление памятью. Частные методы роЛуТгее и DeleteTree используются конструктором, деструктором и операто-м присваивания для размещения и уничтожения узлов списка в динамичес-памяти.
Элементы данных класса BinSTree. Бинарное дерево поиска определяется йМ указателем корня, который используется в операциях включения (1п-Сег^). поиска (Find) и удаления (Delete). Класс BinSTree содержит элемент ^дз'ых root, являющийся указателем корня и имеющий начальное значение Jrrjjjj. Доступ к root осуществляется посредством метода GetRoot, разрешающего вызовы функций прохождения и печати. Второй указатель, current, определяет на дереве место для обновлений. Операция Find устанавливает current ga совпавший узел, и этот указатель используется функцией Update для обновления данных. Методы Insert и Delete переустанавливают current на новый узел илн на узел, заменивший удаленный. Объект BinSTree является списком, размер которого все время изменяется функциями Insert и Delete. Текущее число элементов в списке хранится в закрытом элементе данных size.
// Указатели на корень и на текущий узел
rreeNode<T> *root;
jreeNode<T> *current;
И Число элементов дерева int size;
Управление памятью. Размещение н уничтожение узлов для методов Insert и Delete, а также для утилит СоруТгее и DeleteTree выполняется посредством GetTreeNode и FreeTreeNode. Метод GetTreeNode создан по образцу функций из treelib.h. Он распределяет память и инициализирует поля данных и указателей в узле. FreeTreeNode непосредственно вызывает оператор удаления для освобождения памяти.
Конструктор, деструктор и оператор присваивания. Класс содержит кондуктор, который инициализирует элементы данных. Конструктор копиро-Вайия и перегруженный оператор присваивания с помощью метода СоруТгее ^здают новое бинарное дерево поиска для текущего объекта. Алгоритм функ-ЧИИ СоруТгее был разработан нами для класса TreeNode в разделе 11.3. В . м Ясе разделе мы рассматривали алгоритм удаления узлов дерева, который лизован в классе BinSTree функцией DeleteTree и используется как де-р.Кт°ром, так и методом ClearList.
1е₽егружаемый оператор присваивания копирует объект, стоящий справа, УЩий объект. После проверки того, что объект не присваивается самому л^' Функция очищает текущее дерево и с помощью СоруТгее создает дуб-того, что стоит в правой части оператора (rhs). Указателю current о- Вайвается указатель root, копируется размер списка и возвращается на текущий объект.
ЧЦы ,**ат°Р присваивания
6 cclass Т>
BinSTree<T>::operator = (const BinSTree<T>& rhs)
f f нельзя копировать дерево в само себя if (this =- &rhs)
return ‘this;
// очистить текущее дерево, скопировать новое дерево в текущий объект ClearLxst();
root “ СоруТгее(tree.root);	х	|
// присвоить текущему указателю значение корня и задать размер дерева current = root;	|
size - tree.size;	t
Il возвратить ссылку на текущий объект	/
return ‘this;	, >.
* !
Операции обработки списков	1
Методы Find и Insert начинают с корня и проходят по дереву уникальный ' путь. Используя определение бинарного дерева поиска, алгоритм идет по ™ поддереву, когда ключ или новый элемент больше или равен значению текущей ' узла. В противном случае прохождение продолжается по левому поддереву.
Операция Find (поиск). Операция Find использует закрытый элемент-фунх. : цию FindNode, принимающую в качестве параметра ключ и осуществляющую ' прохождение вниз по дереву. Операция возвращает указатель на совпавший I узел и указатель на его родителя. Если совпадение происходит в корневом узле, J родительский указатель равен NULL.	*	|
// искать элемент данных на дереве, если найден, возвратить адрес
// совпавшего узла и указатель на его родителя, иначе возвратить null template <class Т>
TreeNode<T> *BinSTree<T>::FindNode(const TS item,
TreeNode<T>* & parent) const
// пробежать по узлам дерева, начиная с корня
TreeNode<T> *t » root;
//у корня нет родителя
parent - NULL;
// прерваться на пустом дереве
while (t !== NULL)
I
// остановиться no совпадении
if (item == t->data)
break;
else
(
// обновить родительский указатель и идти направо или налево parent = t;
if (item < t->data)
t = t->left;
else
t  t->right;
)
// возвратить указатель на узел; NULL, если не найден return t;
।
— ;н? ормш1 ля о родителе используете ч операцией .?ею е , удаление?.	методе
д нас интересует только установление текущей позиции на совпавший узел и ^^своение ссылки на этот узел параметру item. Метод Find возвращает True (1) False (О), показывая тем самым, удался ли поиск. Для сравнения данных в яах методу Find требуются операторы отношения и ”<". Эти операторы ^асны быть перегруженными, если они не определены для этого типа данных.
искать item, если найден, присвоить данные узла параметру item 'Lplate <class Т>
pt BinSTree<T>::Find(T& item)
// мы используем FindNode, который принимает параметр parent
yreeNode<T> ’parent;
// искать item, назначить совпавший узел текущим current  FindNode (item, parent);
// если найден, присвоить данные узла и возвратить True if (current !- NULL)
(
item = current-?daта;
return 1;
)
else
// item не найден, возвратить False
return 0;
Операция Insert (вставка). Метод Insert принимает в качестве параметра новый элемент данных и вставляет его в подходящее место на дереве. Эта функция итеративно проходит путь вдоль левых и правых поддеревьев, пока не найдет точку вставки. На каждом шаге этого пути алгоритм сохраняет запись текущего узла (t) и родителя этого узла (parent). Процесс прекращается по достижении пустого поддерева (t == NULL), которое показывает, что мы нашли место для включения нового элемента. В этом месте новый узел включается в качестве сына данного родителя. Например, следующие шаги вставляют число 32 в дерево, изображенное на рис. 11.17.
1. Метод начинает работу в корневом узле и сравнивает 32 с корневым значением 25 [рис. 11.17 (А)]. Поскольку 32 > 25, переходим к правому поддереву и рассматриваем узел 35.
t = узел 35; parent = узел 25
2. Считая узел 35 корнем своего собственного поддерева, сравниваем 32 И 35 и переходим к левому поддереву узла 35 [рис. 11.17 (В)].
t == NULL; parent *= 35
С помощью GetTreeNode мы можем создать листовой узел, содержащий значение 32, а затем вставить новый узел в качестве левого сына узла 35 [рис. 11.17 (С)]:
ff присвоение указателю left возможно,
И т.к. BinSTree является дружественным TreeNode newNode = GetTreeNode(item, NULL, NULL);
Parent->left = newNode;
у
1<4^р&затели parent и t являются локальными переменными, изменяющи-По мере нашего продвижения по пути в поисках точки вставки.
Перейти к правому поддереву
Перейти к левому поддереву	левого сына узла parent
Рис. 11.17. Вставка в дерево бинарного поиска
// вставить item в дерево поиска template cclass Т>
void BinSTree<T>::Insert(const T& item)
{ *
// t — текущий узел, parent - предыдущий узел TreeNode<T> *t = root, ‘parent - NULL, ‘newNode;
// закончить на пустом дереве while(t != NULL) {
// обновить указатель parent и идти направо или налево parent = t;
if (item < t->data) t = t->left;
else t = t->right;
)
// если родителя нет, вставить в качестве корневого узла
If (parent -= NULL) root = newNode;
// если item меньше родительского узла, вставить в качестве левого сына else if (item < parent-» data)
parent->left =* newNode; else
// если item больше или равен родительскому узлу, // вставить в качестве правого сына
parent->right = newNode;  ~ на // присвоить указателю current адрес нового узла и увеличить size на current ” newNode;
size++;
I
— Операция Delete (удаление;, операция Le.exe удаляет из дерева узел с ддяным ключом. Сначала с помощью метода FindNode устанавливается 3\Lro этого узла на дереве и определяется указатель на его родителя. Если * коМЫЙ узел отсутствует, операция удаления спокойно завершается, .^удаление узла из дерева требует ряда проверок, чтобы определить, куда ^соединять сыновей удаляемого узла. Поддеревья должны быть заново ^соединены таким образом, чтобы сохранилась структура бинарного дерева лоиска-
функция Findnode возвращает указатель DNodePtr на узел D, подлежащий яаленню. Второй указатель, PNodePtr, идентифицирует узел Р — родителя являемого узла. Метод Delete "пытается” подыскать заменяющий узел R, который будет присоединен к родителю и, следовательно, займет место удаленного узла. Заменяющий узел R идентифицируется указателем RNodePtr.
Алгоритм поиска заменяющего узла должен рассмотреть четыре случая, зависящие от числа сыновей удаляемого узла. Заметьте, что если указатель ga родителя равен NULL, то удаляется корень. Эта ситуация учитывается вашими четырьмя случаями и тем дополнительным фактором, что корень должен быть обновлен. Поскольку класс BinSTree является другом класса TreeNode, у нас есть доступ к закрытым элементам left и right.
Ситуация А: Узел D не имеет сыновей, т.е. является листом.
Обновить родительский узел так, чтобы его поддерево оказалось пустым.
Удалить листовой узел 17: PNodePtr->left есть DNodePtr
Замена не нужна PNodePtr ->left есть NULL
Обновление совершается путем установки RNodePtr в NULL. Когда мы РИсоединяем NULL-узел, родитель указывает на NULL.
’’’’odePtr = null;
|l|’ode₽tr->left = RNodePtr;
сына.
итуация В: Узел D имеет левого сына, но не имеет правого -ФИсоединить левое поддерево узла D к его родителю.
h й^а°влеиие совершается путем установки RNodePtr на левого сына узла Последующего присоединения узла R к родителю.
=• DNodePtr->left;
rCr~>right = RNodePtr;
Удалить узел 20. имеющий только левого сына: Узлом R является левый сын
>осле
Присоединить узел R к родителю
Ситуация С: Узел D имеет правого сына, но не имеет левого сына. Присоединить правое поддерево узла D к его родителю.
До	После
Удалить узел 15. имеющий только правого сына:	Присоединить узел R к родителю
Узлом R является правый сын «
Как и в ситуации С, обновление может быть совершено путем установки RNodePtr на правого сына узла D и последующего присоединения узла R ж родителю.
RNodePtr = DNodePtr->rxght;
PNodePtr->left = RNodePtr;
Ситуация D: Удаление узла с двумя сыновьями.
521
Узел с двумя сыновьями имеет в своих поддеревьях элементы, которые йьше, больше или равны его собственному ключевому значению. Алгоритм 'Хен выбрать тот заменяющий узел, который сохранит правильный порядок А центов. Рассмотрим следующий пример.
эЛСудалив узел 30, мы создали два "осиротевших” поддерева, которые долж-быть вновь присоединены к дереву. Для этого требуется стратегия выбора Меняющего узла из оставшейся совокупности узлов. Результирующее дерево ^ясно удовлетворять определению бинарного дерева поиска. Применим мак-^инный (max-mtn) принцип.
С Выберите в качестве заменяющего самый правый узел левого поддерева, до__максимальный из узлов, меньших чем удаляемый. Отсоедините этот
еЛ R от дерева, присоедините его левое поддерево к его родителю, а затем доставьте R на место удаляемого узла. В демонстрационном дереве заменяющим является узел 28. Мы присоединяем его левого сына (26) к его родителю (25) и заменяем удаленный узел (30) заменяющим (28).
Для отыскания самого правого узла левого поддерева используется следующий простой алгоритм.
Шаг 1: Поскольку заменяющий узел R меньше, чем удаляемый узел D, перейти к левому поддереву узла D. Спуститься к узлу 25.
Шаг 2: Поскольку R является максимальным узлом левого поддерева, найти его значение, спустившись вниз по правому поддереву. Во время спуска следите за предшествующим узлом PofRNodePtr. В нашем примере спуститесь к узлу 28. PofRNodePtr указывает на узел 25.
Спуск вниз по правому поддереву предполагает два случая.
Если правое поддерево пусто, то текущей точкой является заменяющий узел R и PofRNodePtr указывает на удаляемый узел D. Мы присоединяем правое поддерево узла D в качестве правого поддерева узла R, а родителя Р удаляемого узла присоединяем к R.
Правое поддерево
:*«cd-a^r~>rt9ht «= DNode Pt r->right;
- RNodePtr;
Ecjtt, тггч
Moin и₽авое поддерево непусто, проход завершается листовым узлом или Н oj ’ Имеющим только левое поддерево. В любом случае отсоединить узел ^Чер?€₽ева и пРисоеДинить сыновей узла R к родительскому узлу PofR-г‘ В каждом случае правый сын узла PofRNodePtr переустанавлива-.. °цератором
RNodePtr->right - PofRNodePtr->left;
-------------------------------------------------------------
1.	R является листом. Отсоединить его от дерева. Поскольку RNodePtr равен NULL, оператор (**) устанавливает правого сына уЗЛй NodePtr в NULL.	*4.
2.	R имеет левое поддерево. Оператор (**) присоединяет это поддерево в качестве правого сына узла PofRNodePtr.
Алгоритм заканчивается заменой удаляемого узла узлом R- ^н8 зед В новья узла D присоединяются в качестве сыновей узла R. Затем замещает узел D как корень поддерева, образованного узлом О.
RNodePtr->left - DNodePtr->left;
RNodeFtr->ri «ы- -
523
Завершите присоединение к родительскому узлу Р. v
I/ удаление корневого узла, назначение нового корня if (PNodePtr == NULL)
root = RNodePtr;
// присоединить R к P с правильной стороны
else if (DNodePtr->data < PNodePtr->data) PNodePtr->left = RNodePtr;
else
PNodePtr->right - RNodePtr;
Альтернативным способом замены узла D на узел R является копирование R в D. Однако, если данные занимают много места в памяти, это может быть дорогостоящей операцией. Наш способ предусматривает изменение лишь двух указателей.
Метод Delete
Н если элемент находится на дереве, удалить его template <class Т> void BinSTree<T>::Delete(const T& item)
// DNodePtr — указатель на удаляемый узел D
// DNodePtr — указатель на родительский узел Р узла D
// RNodePtr — указатель узел R, замещающий узел D TreeNode<T> ‘DNodePtr, *PNodePtr, ‘RNodePtr;
/ найти узел, данные в котором совпадают с item.
/ получить его адрес и адрес его родителя
 ((DNodePtr = FindNode (item, PNodePtr)) == NULL return;
если узел D имеет NULL-указатель, то заменяющим Узлом является тот, что находится на другой ветви <DNOdePtr->right == NULL)
^odeptr “ DNodePtr->left;
J® if (DNodePtr->left -- NULL)
NodePtr = DNOdePtr->right;
ei5g3e31 D имеет двух сыновей
Ч II if И
найти и отсоединить заменяющий узел R для узла D. в левом поддереве узла D найти максимальный узел 1,3 всех узлов, меньших чем узел D.
°тсоединить этот узел от дерева.
524
// PofRNodePtr — указатель на родителя заменяющего узла TreeNode<T> *PofRNodePtr = DNodePtr;
// первой возможной заменой является левый сын узла D RNodePtr = DNodePtr->left;
// спуститься вниз по правому поддереву левого сына узла D, // сохраняя записи текущего узла и его родителя.
// остановившись, мы будем иметь заменяющий узел while (RNodePtr->right != NULL) {
PofRNodePtr = RNodePtr;
RNodePtr = RNodePtr->right; )
if (PofRNodePtr == DNodePtr)
// левый сын удаляемого узла является заменяющим
// присоединить правое поддерево узла D к узлу R RNodePtr->right = DNodePtr->right;
else {
// мы спустились вниз по правой ветви как минимум на один узел. // удалить заменяющий узел из дерева,
// присоединив его правую ветвь к родительскому узлу PofRNodePtr->right = RNodePtr->left;
) )
// завершить присоединение к родительскому узлу.
// удалить корневой узел, назначить новый корень.
if (RNodePtr == NULL)
root = RNodePtr;
// присоединить узел R к узлу Р с правильной стороны
else if (DNodePtr->data < PNodePtr->data) PNodePtr->left = RNodePtr;
else
PNodePtr->right = RNodePtr;
// удалить узел из памяти и уменьшить размер списка FreeTreeNode(DNodePtr);
size-—;
Метод Update. После использования метода Find при желании можно обво вить поля данных в этом (текущем) узле. Для этого мы предоставляем мет Update, имеющий значение данных в качестве входного параметра. Если теу щий узел найден, Update сравнивает значение текущего узла со значев данных н, если они равны, производит обновление узла. Если текуШйй У _ не определен или элемент данных не совпал, новое значение данных вкл ется в дерево.
// если текущий узел определен и элемент данных (item) совпал Нс данными в этом узле, переписать элемент данных в узел.
// иначе включить item в дерево
template <class Т>
void BinSTree<T>::Update( const T& item)
{
if (current != NULL &s current—>data “= item) current->data = item;
else
Insert(item);
)

525
11,7. Практическая задача: конкорданс
Обычной проблемой анализа текстов является определение частоты и рас-одаения слов в документе. Эта информация запоминается в конкордансе, с различные слова перечислены в алфавитном порядке и каждое слово снаб-151 «о ссылками на строки текста, в которых оно встречается. Рассмотрим ^едующую Цитату.
peter Piper picked a peck of pickled peppers. A peck of pickled peppers Peter Piper picked. If Peter Piper picked a peck of pickled peppers, where is the peck that Peter Piper picked?
Слово "piper’’ встречается здесь 4 раза в строках 1, 2 и 3. Слово "pickled" встречается 3 раза в строках 1 и 3.
В этой задаче создается конкорданс для текстового файла с помощью следующего проекта:
Вход: Открыть документ как текстовый файл н ввести текст по словам, отслеживая текущую строку.
Действие: Определить запись, которая состоит из слова, счетчика появлений и списка номеров строк, содержащих это слово. При первой встрече некоторого слова в тексте создать запись и включить ее в дерево. Если слово уже есть на дереве, обновить его частоту и список номеров строк.
Выход: После ввода файла распечатать слова в алфавитном порядке вместе со счетчиками частоты и упорядоченными списками строк, где встречается
каждое слово.
Структуры данных
Данными каждого узла является объект Word, содержащий символьную цепочку, счетчик частоты и связанный список номеров строк текста. Объект Word содержит также номер строки, где данное слово встретилось последний раз. Это гарантирует, что мы сможем обработать несколько случаев появлений слова в строке и только один раз занести номер этой строки в список.
wordText 		count	LinkedList<int> LineNumbers	LastLineNo
. Функции-члены класса Word перегружают операторы отношения ”==" и и стандартные операторы потокового ввода/вывода.
Jlass Word
ptivate:
wordText — слово из текста; count — его частота
‘ring wordText;
lnt count;
счетчик строк разделяется всеми объектами Word static mt lineno;
номер последней строки, где встретилось данное слово. испольэуется для того, чтобы знать, когда вставлять строки в lineNumbers
^astLineNo;
*Ce<iList<iiit> lineNumbers;
526
public:
// конструктор Word(void);
// открытые операции класса void Countword (void),-Strings Key(void);
ll операторы сравнения, используемые классом BinSTree int operator-- (const Words w) const;
int operator< (const Words w) const;
// Операторы потока
friend istreams operator» (istreamS istr. Words w);
friend ©streams operator» (ostreams ostr. Words w); ),-
Реализация класса Word
Для каждого слова конструктор устанавливает начальное значение частоты в О и номер последней строки в -1. Перегружаемые операторы отношения и ”»=;" непроходимы для операций вставки и поиска на деревьях и реализуются путем сравнения символьных последовательностей двух объектов. Код этих функций находится в файле word.h.
В этом классе объявляется статический элемент данных lineno. Это закрытая переменная, доступная только элементам класса и друзьфй. Однако под именем Word::lineno она фактически определяется внешней по отношению к классу. Следовательно, она совмесно используется всеми словами-объектами. И это правильно, так как всем этим объектам нужен доступ к номеру текущей строки входного файла. Статические элементы данных допускают совместное использование с одновременным контролем доступа и поэтому более предпочтительны, чем глобальные переменные.
Оператор ввода Оператор ввода считывает данные из потока по одному слову за раз. Слово должно начинаться с буквы, за которой необязательно идет последовательность букв и цифр. Ввод слова начинается с чтения и выбрасывания всех небуквенных символов. Это гарантирует, что все межсловные промежутки и знаки пунктуации будут пропущены. Процесс ввода прекращается по достижении конца файла. Если встречается символ конца строки, происходит увеличение переменной lineno на единицу-
// пропустить все предшествующие пробелы и кебуквенные символы while (istr.get(с) ss !isalpha(c))
// если встретился конец строки, увеличить счетчик строк текста if (с — • \п')
w.lineno++;
Когда распознается начало слова, оператор накапливает сИ^воЭО»* читая буквы и цифры до тех пор, пока не встретится неалфавитноДИФР 0 символ. Буквы слова преобразуются в строчные и запоминаются в лок * переменной wd. Это позволяет сделать наш конкорданс нечувствитель т регистру букв, из которых состоит слово. Если после очередного слова с £ ₽0 символ конца строки, ои снова помещается в поток и обнаруживае сере-время ввода следующего слова. Функция завершается копирование**^ мениой wd в wordText, сбросом счетчика count и присвоением п^Р6
lastLineNo значения lineno.
. если не конец файла, ввести слово (jistr.eof(>)
I ц преобразовать первую букву в строчную, занести ее в wd с . tolower(с);
wdti-ь*] “ С}
// последовательно считывать буквы или цифры, преобразуя их в строчные while (istr.get(c) s& (isalpha(c) II isdigit(c)))
wd(i++l “ tolower(с);
// завершить символьную последовательность нулем wd[iJ -
// если после текущего слова встретился конец строки,
// сохранить его для следующего слова
if (с — '\п') istr.putback(с);
// заключительные установки
w.wordText » wd;
w. count m 0;
w. lastLineNo - w.lineno;
Функция CountWord. После считывания слова из текста вызывается функция CountWord, которая обновляет значение частоты и список номеров строк. Вначале значение счетчика count увеличивается на единицу. Если count » 1, то слово — новый элемент дерева, и номер строки, где впервые встретилось это слово, добавляется в список. Если слово уже есть в списке, то проверяется, изменился ли номер строки с момента последнего появления данного слова. Если да, то номер текущей строки заносится в список и используется для обновления lastLineNo.
И записать случай вхождения слова void Word::CountWord (void) <
Ч увеличить частоту вхождения слова count++;
!I если это слово встретилось впервые или на новой строке,
Ч вставить его в список и присвоить переменной lastLineNo
- Ч номер текущей строки.
t (count  1 || lastLineNo != lineno)
lineNunibers. InsertRear (Lineno) ;
lastLineNo - lineno;
Hft(PnePaT0P вывода "«". Оператор потокового вывода распечатывает слово и вслед за которыми идет упорядоченный список номеров строк, где это 0 встречается.
, ,<сЛово>..............<частота>: nl, п2, пЗ, ...
°а1г?ести объект класса Word в поток
{ operator« (ostreamb ostr, words w)
слово
w.wordText;
528
// вывести выровненный вправо счетчик частоты.
// заполнить промежуток точками.
ostr.fill(*.*);
ostr « setw(25-w.wordText.Length()) « w.count «
ostr.fill('	// снова назначить пробел символом-заполнителем

// пройтись по списку и распечатать номера строк.
for(w.lineNumbers.Reset О ; ! w.lineNumbers.EndOfList(); w.lineNumbers.Next())
ostr « w.lineNumbers.Data() « *
ostr « endl;
return ostr; }
Программа 11.6. Конкорданс
В программе определено бинарное дерево поиска concordTree, в которое хранятся объекты класса Word. После открытия текстового файла соь-cord.txt оператор ввода потока считывает слова, пока не встретится конец файла. Каждое слово либо включается в дерево, либо используется для обновления информации о себе, если оно уже встречалось ранее. После того как все слова обработаны, выполняется симметричное прохождение, в про. цессе которого слова распечатываются в алфавитном порядке. Класс Word находится в файле word.h.	,
linclude <iostream.h>
linclude <fstream.h>
linclude <stdlib.h>
linclude ’’word.h"	//	класс Word
linclude "bstree.h"	//	класс	BinSTree
linclude "treescan.h"	//	метод Inorder
9
// используется функцией Inorder
void Printword(Words w) (
cout « w;
void main(void)
I
// объявить дерево объектов Word; читать из потока fin BinSTree<Word> concordTree;
ifstream fin;
Word w;
// открыть файл concord.txt fin.open("concord.txt", ios::in I ios::nocreate); if (Ifin) (
cerr « "He могу открыть concord-txt" « endl; exit(l);
}
// читать объекты Word из потока fin, пока не встретится конец файла while(fin » w) (
// найти w на дереве
if (concordTree.Find(w) == 0)
^ // w нет на дереве, обновить частоту слова и включить его в дерево и. Countword();
concordTree.Insert(w);
I
else
(
// w на дереве, обновить информацию о слове w. Countword()»
concordTree.Update(w);
>
}
// распечатать дерево в алфавитном порядке
Inorder(concordTree.GetRoot(), Printword);
}
/*
<Файл concord.txt>
Peter Piper picked a peck of pickled peppers. A peck of pickled peppers Peter Piper picked. If Peter Piper picked a peck of pickled peppers, where is the peck that Peter Piper picked?
<Выполнекие программы 11.6>
3:1	2
1: 2
1: 3
3:	1	2
4:	1	2	3
3:	1	2	3
4:	1	2	3
4:	1	2	3
3:	1	3
4:	1	2	3
1: 3
1: 3
1: 3
a...............................
if..............................
is..........................  —
of..............................
peck............................
peppers.........................
peter...........................
picked..........................
pickled.........................
piper...........................
that............................
the.............................
where...........................
*/
Письменные упражнения
H-l Объясните, почему дерево является нелинейной структурой данных. И-2 Какова минимальная глубина дерева, содержащего
а) 15 узлов
5 узлов
®) 91 узел
г) 800 узлов
U.3
а)
6)
U.4
Нарисуйте бинарное дерево, содержащее 10 узлов и имеющее глубину 5.
Нарисуйте бинарное дерево, содержащее 14 узлов и имеющее глубину 5. Пусть бинарное дерево содержит числа 1 3 7 2 12.
Хл3|
11.5
11.6
а)	Нарисуйте два дерева максимальной глубины, содержащие эти Дан^
б)	Нарисуйте два законченных бинарны±чдерева, у которых родительс 6 узел^болыпе, чем любой из его сыновей^
Нарисуйте все возможные деревья,, состоящие из трех узлов.
Действительно ли бинарное дерево с п узлами должно иметь ровио ребер (ненулевых указателей)?
Рассмотрим следующее бинарное дерево.
11.7
а) Если в дерево вставляется число 30, какой узел будет его родителем? б) Если в дерево вставляется число 41, какой узел будет его родителем? в) Осуществите прямой, симметричный и обратный метод прохождения этого дерева.
11.8 Опишите действие функции F. Подразумевается, что F является функцией-членом класса BinSTree.
template cclass Т> void F(TreeNode<T>* & t, T item) { if (t == NULL)
t • GetTreeNode(item) ;
else if (item < t->data F(t->left, item)i else F(t->right, item);
Почему так важно передавать параметр t по ссылке?
11.9 Нарисуйте бинарное дерево поиска для каждой из приведенных с°с^_ довательностей символов и осуществите его прохждение прямы*4» ратным и симметричным методами.
а)	М, Т, V, F, U, N	1
б)	F, L, О, R, I, D, А	|
г) R, О, Т, A, R, Y, С, L, U, В
деревья____________________	...
Ц.10 Осуществите прохождение каждого дерева из предыдущего упражнения в порядке RLN, RNL и NRL, а также поперечным методом.
11.11	Нарисуйте бинарное дерево поиска для каждой из приведенных числовых последовательностей и осуществите его прохождение прямым, обратным, симметричным и поперечным методами.
а)	30, 20, 10, 6, 5, 35, 56, 1, 32, 40, 48
б)	60, 25, 70, 99, 15, 3, 110, 30, 38, 59, 62, 34
в)	30, 20, 25, 22, 24, 23
11.12	Осуществите прохождение каждого дерева из предыдущего упражнения в порядке RLN, RNL и NRL.
11.13	Модифицируйте функцию MakeCharTree таким образом, чтобы она построила следующие деревья в качестве 3-го и 4-го вариантов.
а)	В каком порядке будет проходится дерево, если алгоритм поперечного прохождения будет запоминать узлы не в очереди, а в стеке? Проиллюстрируйте свой анализ на дереве Тгее_2 нз раздела 11.1.
б)	Пусть узлы включаются в очередь с приоритетами, определяемыми полями данных. Используя этот алгоритм, покажите порядок прохождения дерева Тгее_2.
11Л5 Используйте MakeCharTree как образец для функции MakelntTree, которая строит следующие бинарные деревья поиска. Проследите построение каждого узла.
Дерево (В)
Дерево (А)
11.16
а)	Приведенная ниже числовая последовательность получена путем мого прохождения бинарного дерева поиска. Постройте это дерев^* 50 45 35 15 40 46 65 75 70
6)	Постройте бинарное дерево поиска, которое в результате симметв ного прохождения давало бы следующую последовательность узлов4 40 45 46 50 65 70 75
11.17	Дано следующее бинарное дерево поиска:
Выполните следующие действия, используя каждый раз исходное л рево:	е'
а)	Покажите дерево после включения узлов 1, 48, 75, 100.
б)	Удалите узлы 5, 35.
в)	Удалите узел 45.
г)	Удалите узел 50.
д)	Удалите узел 65 и вставьте его снова.
11.18	На основе функции СоруТгее создайте функцию ТСоруТгее, которая имела бы параметр target. Эта функция должна копировать только те узлы, значения которых больше, чем target. Меньшие по значению узлы должны копироваться в качестве листьев. Имейте в виду, что в процессе копирования вам придется удалять все узлы в обоих поддеревьях каждого будущего листа.
TreeNode<T> *TCopyTree (TreeNode<Т> *t, Т target};
11.19	Напишите функцию
TreeNode<T> *ReverseCopy(TreeNode<T> *tree);
которая копирует дерево, попутно меняя местами все левые и пр*31,16 указатели.
11.20	Напишите функцию
void РоstorderRight(TreeNode<T> *t, void visit(TG item);
которая осуществляет RNL-прохождение дерева.
11.21	Напишите функцию
void *insertOne
которая включает item в бинарное дерево поиска t, если его там еще нет. В противном случае функция завершается, не выполняя включение нового узла.
ц.22 Напишите функцию
TreeNode *Мах(TreeNode *t);
которая возвращает указатель на максимальный узел бинарного дерева поиска. Сделайте ее итерационной.
11.23	Напишите функцию
TreeNode ♦Min(TreeNode *t);
которая возвращает указатель на минимальный узел бинарного дерева поиска. Сделайте ее рекурсивной.
11.24	Числа 1—9 используются для построения бинарного дерева поиска с 9-ю узлами без дублирования данных.
а)	Покажите возможное значение корня, если глубина дерева равна 4.
б)	Сделайте то же самое для глубины 5, 6, 7 и 8.
11.25	Для каждого из приведенных ниже буквенных списков нарисуйте бинарное дерево поиска, которое получается, когда буквы вставляются в указанной последовательности.
a)	D, А, Е, F, В, К
б)	G, J, L, М, Р, А
в)	D, Н, Р, Q, Z, L, М
г)	S, J, К, L, X, F, Е, Z
11.26	Напишите итерационную функцию
template colass Т>
int NodeLevel(const BinSTree<T>& t, const T& elem};
которая определяет глубину elem на дереве и возвращает -1, если его нет на дереве.
11.27
а)	Пусть в узлах дерева находятся символьные строки. Постройте бинарное дерево поиска, которое получается в результате вставки следующих ключевых слов в данном порядке:
for, case, while, class, protected, virtual, public, private, do, template, const, if, int
б)	Осуществите прохождение этого дерева прямым, обратным и симметричным методами.
11-28
а)	Иногда узлы бинарного дерева могут содержать указатель на своего родителя. Модифицировав TreeNode, создайте класс PTreeNode для поддержки этого указателя.
б)	Напишите функцию
template cclass т>
void PrintAncestors(PTreeNode<T> *t);
которая распечатывает данные из цепочки узлов, начинающей узла t и заканчивающейся на корне.	Ся
в)	используя технологию MakeCharTree, постройте дерево Tree 2 жащее объекты PTreeNode. '	” * °ДеР-
11.29 Некоторые задачи, например компьютерные игры, имеют дело с вьями общего вида, т.е. такими, узлы которых могут иметь более сыновей. Ниже приведено дерево, где максимальное число равно 3 (тернарное дерево).
с Дере--- Двух сыновей
а)	Может ли симметричный метод прохождения быть однозначно определен на дереве общего вида?
б)	Реализуйте прямой и обратный методы прохождения тернарного дерева, в) Дерево общего вида может быть преобразовано в бинарное дерево с помощью следующего алгоритма:
1)	Левый указатель каждого узла бинарного дерева указывает на самого левого сына соответствующего узла на дереве общего вида.
2)	Правый указатель каждого узла бинарного дерева указывает на брата (узел, имеющий того же родителя) этого узла на дереве общего вида. Рисуя бинарное дерево, располагайте каждого сына прямо под его родителем, а его братьев располагайте справа. Вот бинарное дерево, соответствующее только что приведенному примеру дерева общего вида:
535
деревья _______________________________________,
Если это дерево повернуть на 45 градусов чится белее знакомое изображение:
по часовой стрелке, полу-
этого
дерева прямым, обратным и симмет-
Осуществите прохождение ричным методами. Что общего вы находите между ними и соответствующими методами для дерева общего вида?
г) Для приведенного ниже дерева общего вида выполните следующее:
1)	Осуществите его прохождение прямым и обратным методами.
2)	Постройте соответствующее ему бинарное дерево.
3)	Осуществиет прохождение бинарного дерева прямым, обратным и симметричным методами.
1-30 Поскольку бинарное дерево с п узлами имеет п+1 нулевых указателей, половина выделенной для указателей памяти тратится впустую. Хороший алгоритм использует эту память. Действительно, пусть при симметричном прохождении каждый левый пустой указатель указывает на своего предшственника, а каждый правый — на преемника. Такая структура называется прошитым деревом (threaded tree), а сами указатели — нитями (threads). Узел прошитого дерева может быть представлен несложным расширением класса TreeNode. Добавьте закрытую логическую переменную rightThread, показывающую, являет-
536					 ся ли соответствующий указатель нитью, и методы LeftThread и ft v Thread, которые возвращают значения этих указателей. Назовем новый класс ThreadedTreeNode. Напишите итерационную Функц^&1 template <class Т>	 void Threadedlnorder(ThreadedTree<T> *t); которая осуществляет симметричное прохождение дерева t н расп тывает данные из его узлов.	Ча‘ Упражнения по программированию
11.1	Напишите, функцию int CountEdges(TreeNode<T> *tree): которая подсчитывает число ребер (ненулевых указателей; бинарного дерева. Испытайте эту функцию на дереве Тгее_1 из treelib.h.
11.2	Напишите функцию void RNL(TreeNode<T> ‘tree, void visit(Ti item}); которая посещает узлы дерева в порядке RNL. Введите 10 целых чисел и разместите их на бинарном поисковом дереве, используя жласс BinSTree. Осуществите RNL-прохождение этого дерева. Как упорядочиваются данные при таком методе?
11.3	Используя функции, разработанные вами в письменном упражнении 11.15 а) и б), напишите main-программу, которая распечатывает эти два дерева. Распечатайте дерево из а), применяя PrintTree, в дерево из б), применяя PrintVTree.
11.4	Возьмите функцию InsertOne из письменного упражнения 11.21- В тестовой программе постройте дерево с восемью узлами. Входные данные должны дублироваться. Распечатайте получившееся дерево с помощью PrintTree.
11.5	В main-программе используйте класс BinSTree для создания дерева с 500 узлами, содержащими случайные целые числа в диапазоне от * до 10000. С помощью функций Мах и Min из письменных упражнеЯИЙ 11.22 и 11.23 вычислите максимальный и минимальный узлы-
11.6	Модифицируйте задачу о конкордансе (программа 11.6) таким образом, чтобы число встреч каждого слова в каждой строке распечатывало^ в следующем формате: номер строки (число встреч) Например, <Вход>	one	two one two three <Выход>	one	2:	1(2) three	1:	1(1) two	2:	1(2)
11.7
a)	Напишите функцию void LinkedSort(Array<int>& A);
которая сортирует массив путем включения его элементов в упорядоченный связи ттпътй список и копирования отсортированных данных обратно в А.
б) Напишите функцию
void TreeSort(Array<int>& А);
которая сортирует массив А путем включения его элементов в бинарное дерево поиска, симметричного прохождения этого дерева и копирования отсортированных данных обратно в А. (Совет: Для симметричного про-* хождения и присвоения значений элементам массива напишите рекурсивную функцию
void inorderAssign(TreeNode<int> *t, Array<int>& A, int i);
в) Напишите главную процедуру, создающую массив 10000 случайных целых чисел и использующую системный таймер для определения быстродействия функций а) и б). Обе функции должны сортировать одни и те же данные.
11.8 Арифметическое выражение, включающее бинарные операторы сложения (+), вычитания (-), умножения (*) и деления (/), может быть представлено в виде бинарного дерева, где каждый оператор имеет двух сыновей — операнд или подвыражение. Листовой узел содержит операнд, а нелистовой — бинарный оператор. Левое и правое поддеревья оператора описывают подвыражения, которые вычисляются и используются в качестве операндов этого оператора. Например, выражению a+b*c/d-e соответствует следующее бинарное дерево:
а)	Выполните прямой, симметричный и обратный методы прохождения этого дерева. Какая связь существует между этими методами и префиксной, инфиксной и постфиксной записями данного выражения?
б)	Для каждого из приведенных ниже арифметических выражений постройте соответствующее бинарное дерево. Осуществив прохождение дерева, выдайте префиксную, инфиксную и постфиксную формы выражения.
(l)a + b = c*d + e
(2)	/ а - b * с d
(3)	abed/-*
(4)	* “ / 4-abcde
сыновей
в)	Можно разработать рекурсивный алгоритм ввода выражения в фиксной форме и построения соответствующего бинарного Дерсйа Если элемент выражения является операндом, используйте его авач для создания листового узла, в котором поля указателей соде NULL.
Если элемент выражения является оператором, назначьте его данных узлу дерева, а затем создайте его левого и правого Напишите функцию void BuildExpTree(TreeNode<char> *t, char * & exp);
которая строит бинарное дерево по префиксной форме выражения держащейся в строке ехр. Предполагается, что операнды являются одв° буквенными идентификаторами в диапазоне от а до z, а оператоп° представлены символами	’*’ и ’/’.	1411
г)	Напишите main-программу, которая вводит выражение и создает би нариое дерево. Распечатайте дерево вертикально с помощью PrintVtree и выдайте инфиксную и постфиксную формы этого выражения.
11.9 Используя функцию ТСоруТгее из письменного упражнения 11.18, создайте в главной программе бинарное дерево поиска с 10-ю узлами, содержащими целые числа. Задайте значение параметра target и скопируйте узлы дерева с помощью ТСоруТгее. Распечатайте исходное дерево н копию, используя PrintVTree.
Наследование и абстрактные классы
12.1.	Понятие о наследовании
12.2.	Наследование в С++
12.3.	Полиморфизм и виртуальные функции
12.4.	Абстрактные базовые классы
12.5.	Итераторы
12.6.	Упорядоченные списки
12.7.	Разнородные списки Письменные упражнения Упражнения по программированию
Наследование — фундаментальное понятие в объектно-бриентир0в программировании. В этой главе развиваются ключевые свойства на I ния, о которых кратко было упомянуто в гл. 1. Мы сосредоточив0^ ' концепции наследования и ее реализации в C++ с помощью базового**6 * Shape и семейства производных от него классов геометрических фиг^^а
Полиморфизм и виртуальные функции популярно обсуждаются в р 12.3 и применяются в задаче отображения свойств геометрических объ
В разделе 12.4 развивается концепция абстрактного базового кл»6^ дополнение к обеспечиваемой им функциональности абстрактный баз^ ® класс обязывает реализовывать свои чистые виртуальные функции в водных от него классах. В качестве примера разрабатывается абстрах °Из базовый класс линейных и нелинейных списков общего вида. ТйЬ1Й t
Итератор — это объект, который осуществляет обход таких структур ных, как массивы, связанные списки или деревья. В качестве такового^ * является абстракцией элемента управления (control abstraction). В разд °В 12.5 итераторы разрабатываются путем определения абстрактного базе/** класса и образования на его основе итераторов для классов SeqList и Дгга Итератор Array используется для сортировки слиянием последовательно тей — методики, используемой также в гл. 14.
В разделе 12.6 наследование применяется для порождения класса упорядоченных списков из версии класса связанных списков SeqList, разработан, кого в гл. 9. Этот класс используется как фильтр для создания сортированных последовательностей, сливаемых при сортировке внешнего файла.
Необязательный для чтения раздел 12.7 показывает, как можно использовать наследование и полиморфизм для разработки массивов извязанных ; списков, содержащих объекты различных типов. Когда это полезно и при- । емлемо, наследование используется в последующих главах для разработки структур данных.
12.1. Понятие о наследовании
С точки зрения зоологии, наследование описывает общие признаки и особые характеристики видов. Например, пусть класс "Животные" представляет животных, включая обезьян, кошек, птиц и т.д. Несмотря на то что все они имеют признаки животных, существуют различные семейства с особыми характеристиками. Все животные в дальнейшем подразделяются на виды. Например, к семейству кошачьих относятся львы, тигры, гепард
и Т.Д.
Все существующие животные могут быть описаны иерархией до вида. "Семейство кошачьих принадлежит к царству животных ^^ару-прииадлежит к семейству кошачьих" и т.д. Более высокий уровень еЯця Живает признаки, присущие элементам более низкого уровня.
со	> чрез нсвли	ч*	
Иерархия наследования существует и в программировании. Можете вновь вернуться к разделу 1.4, где разрабатываются объекты типа Point, Line и Rectangle и устанавливаются отношения между ними с помощью наследова-вия- Эти классы включали в себя методы Draw, которые из базовой точки рисовали на экране фигуры. В данном разделе мы разработаем подобные классы для замкнутых фигур, таких, как окружности, прямоугольники и д#> & также общие для всех этих классов изолированные методы.
У наших геометрических объектов есть общие признаки. Все они являются формами, которые могут быть нарисованы на экране, и каждая фигура имеет базовую точку, фиксирующую ее положение. Например, мы очерчиваем окружность вокруг центра и позиционируем прямоугольник по его левому верхнему углу. Кроме того, каждая фигура заштриховывается (заполняется) по некоторому образцу, определяемому целочисленным значением. В большинстве графических библиотек отсутствие штриховки специфицируется нулем. Например, следующий график предусматривает вычерчивание окружности вокруг точки (xl, yl) со сплошным заполнением:. Прямоугольник рисуется из точки (х2, у2) с образцом штриховки в виде кирпичной кладки.
Из коллекции геометрических классов мы выделяем общие признаки — базовую точку и образец штриховки — и определяем класс Shape, который содержит эти элементы данных. Этот класс содержит также методы для получения координаты базовой точки, ее позиционирования и для выбора или смены образца заполнения. Системно-зависимый метод Draw инициализирует графическую систему таким образом, что операции построения различных геометрических фигур будут использовать указанный образец заполнения. Метод Draw является виртуальной функцией (virtual function), т.е. специально созданной для того, чтобы переопределяться в виртуальном классе- Виртуальные функции обсуждаются в разделе 12.3.
В первой главе мы разработали класс Circle вместе с операциями измерения площади и периметра (длины окружности). Эти операции могут применяться ко всем замкнутым фигурам, и поэтому мы включаем их в класс ^ларе. Методы в классе Shape не определяются, а служат шаблоном для своего определения в производных классах. Они называются чистыми вирильными функциями (pure virtual functions), a Shape — абстрактным клас-(abstract class). Мы приводим здесь эскиз описания класса Shape, хотя ^Ногие концепции развиваются в разделах 12.3 и 12.4. Различные элементы, ^йюченные в описание класса, анонсируют тему данной главы.
ciass г.аре
{
protected: float x, у; int fillpat;
// координаты базовой точки
public:	// конструктор, действующий по умолчанию
Shape(float h=0, float v=0, int fill=O); ............................... । // виртуальная функция, вызываемая методом Draw в производном // классе, инициализирует образец заполнения virtual void Draw(void) const;
// производные классы должны определять методы
// для вычисления площади и периметра
virtual float Area(void) const * 0;
virtual float Perimeter(void) const = 0;
}
Мы используем класс Shape в иерархии наследования. В каждом случае наш производный класс использует методы класса Shape и создает свои специфи ческие методы, которые перекрывают родовые методы абстрактного класса Например, объект типа Circle есть Shape (базовая точка в центре) с радиусом Он содержит метод Draw для отображения заштрихованной окружности на поверхности чертежа. Этот класс имеет свои особые методы Area и Perimeter использующие радиус и константу PI. В цепочке наследования Circle является производным от Shape.
Аналогично объект типа Rectangle есть Shape (базовая точка в левом верхнем углу) с длиной и шириной. Метод Draw строит прямоугольник, используя его длину и ширину и заштриховывая его внутри по заданному образцу. Формулы
area — length * width
perimeter -= 2 * (length + width)
являются базисом для методов Area и Perimeter. В цепочке наследования Rectangle является производным от Shape.
Терминология наследования
В C++ наследование определяется для классов. Эта концепция предполага ет, что производный класс (derived class) наследует данные и операции ба^ вого класса (base class). Производный класс сам может являться базовым отношению к другому слою наследования. Система классов, которая испо зует наследование, образует иерархию наследования (class hierarchy)-
u.a: -i8Ci -<.x ...	со::1ВС1СТВую-
^его базового класса, который, в свою очередь, называется также суперклассом (superclass).
12.2. Наследование в C++
Базовый класс, являющийся родоначальником цепочки наследования, й1йеет обычное объявление. В объявлении производного класса указывается родство с базовым классом.
рдЗОБИИ КЛАСС
у объявление обычного класса для языка C++ class BaseCL
1 <данные и методы»
I
ПРОИЗВОДНЫЙ КЛАСС
// объявление производного класса со ссылкой на его базовый класс class DerivedCL: public BaseCL
{
сданные и методы» )
Здесь BaseCL — это наименование базового класса, который наследуется классом DerivedCL. Ключевое слово public указывает, что используется открытое наследование. Производный класс в C++ может определяться с открытым (public), закрытым (private) и защищенным (protected) наследованием. В большинстве программных проектов используется открытое наследование. Защищенный тип наследования применяется редко. Закрытый тип рассматривается в упражнениях.
Пример 12.1
1. Класс Shape является базовым для производного класса Circle.
class Shape {Олементы»)
class Circle: public Shape // Класс Circle наследует класс Shape {Олементы»}
2. В цепочке наследования Животные—Кошачьи-Тигр объявление классов таково:
class Animal {Олементы»)
class Cat: public Animal
{Олементы»}
class Tiger: public Cat {Олементы»)
JiR открытом наследовании закрытые элементы базового класса остаются Рытыми и доступны только функциям-членам базового класса. К открытым
eirrsM опасного класса могут обращаться все соункции-члены произВ(, класса и любая программа, использующая производный класс. Кроме тог^Ь°1° открытых и закрытых элементов в C++ определяются защищенные (рго+°’ элементы, которые имеют особое значение в базовом классе. При васдеДоес^) базового класса его защищенные элементы доступны только через метол изводного класса.	ДЬ1 пРо.
class BaseCL
{
private:
{«Элементы:»} protected:
{«Элементы:»)
public:
{«Элементы»)
<
// доступны только элементам BaseCL
// доступны как элементам DerivedCL, // так и BaseCL
// доступны всем программам-клиентам
В иерархической цепочке, содержащей несколько производных классов каждый из них сохраняет доступ ко всем защищенным и общедоступны^ элементам базовых классов более высокого уровня. На рис. 12.1 показаны один и два производных класса. Стрелки слева указывают доступ элементов производного класса к данным различных видов базового класса. Правая сторона каждой структурной схемы показывает, что клиент может обращать, ся только к открытым членам базового и производных классов.
Один производный класс
Два производных класса
Рис.12.1. Доступ к элементам данных базового класса при открытом наследовании
Конструкторы и производные классы
меТоД^
В цепочке наследования производный объект наследует данные и " ой3-базового класса. Мы говорим, что базовый класс является подтипом
Одвого класса. Ресурсы производного Нового объекта.
объекта включают в себя ресурсы
Данные базового класса
Данные производного класса
Производный объект
При создании производного объекта вызывается его конструктор для инициализации элементов данных производного объекта. Одновременно объект наследует данные базового класса, которые инициализируются конструктором базового класса. Поскольку конструктор вызывается в момент объявления объекта. должно происходить взаимодействие между конструкторами базового и производного классов. Когда объявляется производный объект, сначала выполняется конструктор базового класса, а затем — производного класса. Интуитивно ясно, что начало цепочки наследования должно быть построено базовы-мым классом, так как производный класс часто использует данные базового класса. Если цепочка длиннее, чем два класса, процесс инициализации начинается с самого первого базового класса и распространяется далее по цепочке производных классов:
DerivedCL obj; // вызывается конструктор BaseCL;
//а затем конструктор DerivedCL
Когда конструктору базового класса требуются параметры, конструктор производного класса должен явно вызвать базовый конструктор и передать ему необходимые параметры. Это делается путем размещения имени конструктора и параметров базового класса в списке инициализации параметров конструктора производного класса. Если у базового класса есть конструктор, выполняемый по умолчанию, и предполагаются его значения по умолчанию, то производному классу, в принципе, не нужно явно вызывать такой конструктор. Однако хорошо бы все-таки это делать.
Пример 12.2
Пусть конструктор базового класса объявляется следующим образом:
'	BaseCL(int n, char ch); // конструктор, имеющий два параметра
В общем случае список параметров для конструктора производного класса включает в себя параметры базового класса.
// список параметров конструктора включает в себя как минимум
// два параметра конструктора базового класса . DerivedCL(int n, char ch, int sz);
Производный конструктор должен инициализировать объект базового класса путем явного вызова конструктора базового класса в списке ини-. Циалиэации. Вот пример реализации конструктора производного класса.
// вызвать конструктор BaseCL(n, ch) в списке инициализации
VSj ff Выражение data(sz) — стандартное присвоение значения sz
If элементам данных
DerivedCL::DerivedCL (int n, char ch, int sz) :
iv	BaseCL(n, ch), data(sz)
О
4b
Деструкторы в цепочке наследования вызываются в порядке, ч вызовам конструкторов. В первую очередь вызывается деструктор изводиого класса, затем деструкторы для объектов, затем Деструкто * базовых классов в порядке, обратном появлению этих классов. понятно, что производный объект создается после базового и поэтому быть ликвидирован раньше. Если у производного класса нет деструк^^ у базового есть, то деструктор для производного класса генерируетсТ°₽ч’t магически. Этот деструктор уничтожает элементы производного к/ запускает деструктор базового класса.	ССа *
Разрешение конфликтов имен при наследовании. В цепочке наследов классы могут содержать элементы с идентичными именами. Области лей **** элемента производного класса и элемента базового класса различны, несм^^ i иа то что имена этих элементов одинаковы. Объявление элемента в водном классе скрывает объявление элемента с тем же именем в базов*3 классе, но не перегружает его. Для ссылки на метод базового класса с т°* же именем, что и в производном, должен использоваться оператор области действия класса
Рассмотрим цепочку class BaseCL { public;
class DerivedCL: public BaseCL {
public:
void F(void);
void G(int x);
void F(void);
void G(float x};
}:
);
Предположим, что из функции G в производном классе происходит обращение к функции G базового класса. Тогда в производном классе следует применить оператор BaseCL::, чтобы получить доступ к методу G базового класса.
void DerivedCL::G{float x) {
BaseCL::G(x) // оператор области действия в функции-члене
ь-
Для программы, использующей производный объект, обращение к F । рабатывается методом F из класса DerivedCL. Вызов функции F базово класса должен сопровождаться оператором области действия.
derived OBJ;
Вызовы из программы, использующей OBJ
OBJ.FO;	// обращение к F производного класса
OBJ.base::F();	// обращение к F базового класса
Приложение: наследование класса Shape. В разделе 12.1 мы в^едСпавв^е класс Shape как пример абстрактного базового класса. Его методы и \angie. могут использоваться классами геометрических фигур Circle и к^ерев Проиллюстрируем технические детали наследования, объявив клаСС зВодЗ^ качестве базового, а затем образовав на его основе класс Circle. ^Р0**^^* класс Rectangle рассматривается в разделе 12.3 при обсуждении вир^У функций.
унификация класса Shape
---
тО родовой класс, определяющий точку, образец заполнения и методы доступа / к этим параметрам. этот класс наследуется классами геометрических фигур, / которые выдают рисунок фигуры, а также вычисляют ее площадь и периметр.
ciass Shape
protected:
*// горизонтальная и вертикальная экранные координаты точки, // измеряемые в пикселах, используются методами производных float к, у;
классов
// образец заполнения для графических функций int fillpat;
public:
// конструктор
Shape (float h=0, float v=0, int fill«=O);
// методы доступа к координатам базовой точки
float GetX(void) const;	// возвращает координату x
float GetY(void) const;	// возвращает координату у
void SetPoint (float h, float v);	// изменяет базовую точку
// чистые виртуальные функции, производный класс обязан
// определить свои методы Area и Perimeter
virtual float Area(void) const • 0;
virtual float Perimeter (void) const = 0;
// виртуальная функция, вызываемая методом Draw в производном // классе, инициализирует образец заполнения.
virtual void Draw(void) const; )
ОПИСАНИЕ
По умолчанию конструктор задает базовую точку (0,0) в левом верхнем углу окна. Нулевой образец заполнения обычно предполагает его отсутствие.
Методы GetX и GetY возвращают координаты х и у базовой точки. SetPoint позволяет клиенту изменять базовую точку. Похожие методы GetFill и SetFill обеспечивают доступ к образцу заполнения.
Метод Draw инициализирует графическую систему таким образом, что фи-Пфы заполняются по образцу fillpat. Предполагается, что клиент сам несет 0Тветственность за открытие графического окна и закрытие поверхности чер-тенса. Методы Area и Perimeter являются чистыми виртуальными функциями. w объявляются в классе Shape и ведут себя, как шаблоны. Эти методы
4°Шны определяться во всех производных классах, наследующих Shape.
!**лиэация класса Shape
Полное объявление класса Shape находится в файле geometry .h. В этом 35еле Детально описывается конструктор и функция Draw.
U Инструктору требуются координаты базовой точки и образец заполнения. ' Иент может изменять их значения с помощью методов SetPoint и SetFill.
7 к
St °НСТруКТОр задает начальные значения координат и образец заполнения
j£({<:: Shape (float h, float v, int fill);
y(v), fillpat(fill)
^^етод Draw вызывает графическую функцию SetFillStyle. Когда произвол-Класс рисует конкретную фигуру, используется данный стиль заполнения.
void Shape::Draw(void) const (
SetFillstyle(fillpat); // вызов функции графической системы )
Производный класс Circle
В первой главе мы объявили класс Circle, чтобы иметь радиус и мето вычисления площади и периметра. Радиус передавался конструктору создания объекта и не был доступен. В этом разделе мы расширим включив сюда возможности рисования и методы доступа к радиусу. рисования наследуют базовую точку и образец заполнения из класса ShaT0;i111
Базовая точка
Otf ЬЯВ Л Д Ю^Е
// константа, используемая методами Area и Perimeter const float PI - 3.14159;
// объявление класса Circle на основе класса Shape
class Circle: public Shape {
protected:
// если класс Circle становится базовым, то производные // классы могут иметь доступ к радиусу
float radius;
public:
// конструктор, параметрами являются координаты центра,
// радиус и образец заполнения
Circle(float h-0. float v-0, float radiuS"0, int fill - 0);
11 методы доступа к радиусу float GetRadius(void) const; void SetRadius(float r);
// метод Draw для окружности вызывает Draw из базового класса virtual void Draw(void) const;
// измерительные методы
virtual float Area(void) const;
virtual float Perimeter(void) const;
ОПИСАНИЕ
Метод Draw рисует окружность вокруг базовой точки (х,у) с радиУс°м г Объявление и реализация класса находятся в файле geometry.h.
Реализация класса Circle
. ь сОдер*
Реализация класса Circle предполагает наличие файла geometry.*1» ^,.rCje жащего графические операции нижнего уровня. Конструктору класса передаются параметры для инициализации базового класса Shape и еГ° иое-члеи — radius.
./ конструктор
', параметры h и v задают начальное положение базовой
', точки в классе Shape. Point(h,v) представляет центр окружности.
/ параметр fill задает начальный образец заполнения для класса Shape.
/ параметр г используется исключительно классом Circle.
/ базовый объект класса Shape инициализируется конструктором
/ Shape(h, vt fill) в списке инициализации
.,cie:: Circle (float h, float v, float r, int fill):
Shape (h, v, fill), radius(r)
0
Операция рисования Вызывайте метод Draw из базового класса (Shape: :Draw), дтобы задавать образец заполнения. Поскольку данные в базовом классе имеют тип Доступа protected, метод Draw класса Circle может к ним обращаться. Однако программа, использующая объект, ие имеет иепосредственного доступа к координатам базовой точки.
// нарисовать окружность заданного радиуса с центром (х,у) void Circle:: Draw (void) const
Shape::Draw();	// задает образец заполнения
DrawCircle(x,y, radius);
)
Программа 12.1. Вычерчивание окружностей
Эта программа демонстрирует применение классов Shape и Circle. После объявления двух Circle-объектов выполняется ряд методов из классов Shape и Circle.
linclude <iostream.h>
linclude "graphlib-h"
linclude "geometry ,h’’
void main (void)
(
// объявить объекты С с заполнением 7 и D без заполнения Circle С(1.0, 1.0, 0.5, 7), D(2.0, 1.0, 0.33);
char eol; // используется для задержки перед рисованием фигур
cout « "Координаты С: " « C.GetXO « " и " « С.GetY() « endl;
cout « "Периметр С: '' « С.Perimeter () « endl;
cout « "Площадь С: " « C.AreaO « endl;
cout « "Нажмите Enter, чтобы увидеть фигуры:
cin.get(eol);	// ждать нажатия клавиши Enter
// системный вызов для инициализации поверхности чертежа IhitGraphics();
нарисовать окружность С с радиусом 0.5 и заполнением 7 C-Draw();
f I для окружности D задать центр=(1.5, 1 - 8), радиус=0.25,
f образец заполнения = 11
D-SetPoint(1.5, 1.8);
D-SetRadius(.25);
D.SetFill(11);
D.Draw();
11 выдержать паузу и закрыть графическую систему viewpause();
ShutdownGraphi.cs ();
>
/*
<Выполнение программы 12.1>
Координаты С: 1 и 1
Периметр С: 3.14159
Площадь С: 0.785398
Нажмите Enter, чтобы увидеть фигуры:
•/
Что нельзя наследовать
В то время как производный класс наследует доступ к защищенным данным-членам базового класса, некоторые члены и свойства базового класса по наследству не передаются. Не наследуются конструкторы. Следовательно, они не объявляются как виртуальные методы. Если конструктору базового класса требуются параметры, то производный класс должен иметь свой собственный конструктор, который вызывает конструктор базового класса. ДрУ жественность тоже не наследуется. Если функция F является дружественной для класса А и класс В образован из А, то F ие становится автоматически дружественной для В.
12.3. Полиморфизм и виртуальные функции
Понятие полиморфизма с нетехнической точки зрения обсуждалось в деле 1.3, и было бы полезно перечитать тот материал. В данном раздел® расширим наше представление о полиморфизме и приведем некоторые у меры-	кЭе
Объектно-ориеитирование программирование вводит в обращение свойство, называемое полиморфизмом (polymorphism). Этот термин взЛ ор. древнегреческого и означает ’’много форм". В программировании оВ физм означает то, что один и тот же метод может быть определен для	og^-
различных типов. Конкретное поведение метода будет зависеть от тиПдавяЯ екта. C++ поддерживает полиморфизм с помощью динамического свЯЗЬ+оце)-(dynamic binding) и виртуальных функций-членов (virtual member ^UI1Cpe8rP-Динамическое связывание позволяет различным объектам в системе Р
ogaTb на одни и те же сообщения в специфической для их типа манере, гтриемник сообщения- определяется динамически во время исполнения.
Чтобы использовать полиморфизм, объявите в базовом классе функцию-яен как виртуальную, поставив ключевое слово virtual впереди объявления, ^апример, в классе BaseCL функции F и G объявлены виртуальными: class BaseCL
’ private:
public:
virtual void F(int n);
virtual void G(long m) ;
h
Во время объявления производного класса в него должны быть включены функции-члены F и G с точно такими же списками параметров*. В производном классе слово virtual не является обязательным, так как атрибут виртуальности переходит по наследству от базового класса. Тем не меиее указание virtual в производном классе приветствуется, чтобы не вынуждать читателя заглядывать в базовый класс для выяснения зтого вопроса.
class DerivedCL: public BaseCL {
private:
public:
virtual void Flint n);
virtual void Gllong ki);
);
Поскольку цепочка наследования и виртуальные функции определены, мы можем обсудить новые условия доступа для наших членов класса. Пусть DObj есть объект типа DerivedCL:
DerivedCL DObj;
Числовая функция F в производном классе доступна с помощью имени объекта. Функция F в базовом классе доступна с помощью имени объекта и оператора области действия базового класса:
^-Е(П);	// функция-член производного класса
°bj.BaseCL: :F(n);	// функция-член базового класса
Эти вызовы являются примерами статического связывания (static binding), омпилятор представляет себе, что клиент каждый раз вызывает особую вер-р — из базового класса и из производного. Однако полиморфизм проявля-_ лишь тогда, когда используются указатели или ссылки. Рассмотрим объ-
nasecL Jasecij erivedCL
об^°ск°льку производный класс является подтипом базового, производный руеект Может быть присвоен базовому. В процессе этого присваивания копи-Тся “Га часть данных производного объекта, которая присутствует в базовом
*F, < BObj; DObj
BObj = DObj;
" i
Копирование членов
Данные производного класса <•____-_____________
базового класса
Данные базового класса
1
Базовый объект
Данные производного кдасса
Производный объект
В то же время присвоение базового объекта производному недопустим как некоторые элементы данных производного класса могут оказаться в°*Так деленными. В качестве примера рассмотрим следующие операторы присв0^'
BObj  DObj;	// копирует базовую часть данных в BObj
DObj = BObj;	// недопустимо, т.к. часть данных, относящаяся
// к производному классу остается неопределенной
В контексте указателей и ссылок указатель базового класса может указы вать на производный объект, поэтому допустимы следующие присваивания-
Q = &Bobj;	// присваивает адрес объекта типа BaseCL указателю класса BaseCL
Р = &DObj;	// присваивает адрес объекта типа DerivedCL указателю класса 3asec
Оператор
Q->F(п);	// вызов метода F базового класса
вызывает функцию F базового класса. Подобный оператор для указателя Р иллюстрирует суть полиморфизма, поскольку он вызывает метод F производного класса, хотя Р является указателем на класс BaseCL.
P->F(n);	// вызов метода г производного класса
Когда доступ осуществляется через указатель или ссылку, C++ определяет, какую версию функции вызывать, основываясь на конкретном объекте, адресуемом данным указателем или ссылкой. Этот процесс называется динамическим связыванием.
Каждый объект, имеющий как минимум одну виртуальную функцию, содержит указатель на таблицу виртуальных функции (virtual function table). Эта таблица содержит начальные адреса всех виртуальных функций, объявленных в классе. Когда виртуальная функция вызывается по указателю или по ссылке, система использует адреса объектов для обращения к указателю »а таблицу виртуальных функций, отыскивает там адрес функции и вызывает ее.
class DerivedCL: public BaseCL Таблица виртуальных функций
В нашем примере Р указывает на объект типа DerivedCL, поэтому BbLggo-ется версия F, определенная в классе DerivedCL. Это позволяет со3Д ть .еВця образные объекты, адресуемые указателем базового класса. Во время вЬ1ПОд)актД' виртуальной функции вызывается та ее версия, которая соответствует ’P3lt-ческому типу объекта. Полиморфизм позволяет повторно использовать*^, ции, принимающие в качестве аргументов указатель или ссылку баэово а(,сау. са, вместе с новыми версиями виртуальных функций в производных н
Демонстрация полиморфизма
]\1ы начали эту* главу простым примером наследования в зоологической „рархии. От царства животных мы перешли к семейству кошачьих и далее к 0О£Кретному виду — тигру. Тигр "принадлежит к" кошачьим, кошачьи "принадлежат к” животным. Приведенные ниже классы моделируют эту иерархию *помощью классов Animal, Cat и Tiger. Каждый класс содержит символьную S-року» которая инициализируется конструктором и предусмотрена для специфической информации об объекте. Каждый класс имеет метод Identify, расхватывающий эту информацию.
Объявление класса Animal
class Animal (
private:
char animalName ( 20 ];
public:
Animal (char nma(]) {
strcopy(animalName, nma); }
virtual void Identify (void) {
cout « "Я ” « animalName « " животное" « endl; )
Объявление класса Cat
(lass Cat: public Animal
Private:
char catName[20];
Public;
Cat (char nmc[], char nma[J): Animal (nma) strcopy(catName, nmc);
^irtual void Identify (void)
Animal::Identif у {);
cout « "я " « catName « " кот" « endl;
1 1
Объявление класса Tiger
.1
class Tiger: public Cat
private:
char tigerName{20);
public:
Tiger (char nmt[], char nmc[], char nmaL1]): cat (nnc, nma) (
strcopy(tigerName, nmt);
)
virtual void Identify(void) (
cat: -.Identify() ;
cout « "Я *' « tigerName « '* тигр" « endl;
Программа 12.2. Полиморфизм класса Animal
Эта программа иллюстрирует статическое и динамическое связывание двух функций — Announcel и Announces — которые вызывают метод Identify для объекта, переданного им в качестве параметра. Используются два различных способа передачи параметров.
Передача объекта Animal в функцию Announcel по значению void Announcel (Animal а)
(
// пример статического связывания, компилятор управляет
// выполнением метода Identify — члена объекта типа Animal
cout « "Вызов функции Identify в статической Announcel:" « endl;
а.Identify(); cout « endl; )
Передача объекта Animal в функцию Announces по ссылке void Announces (Animal *ра) (
// пример динамического связывания, вызывается метод Identify
// того объекта, на который указывает ра
cout « "Вызов функции identify в динамической Announces:" « endl;
Cat
pa->ldentify(); cout « endl;
)
В main-программе объявляется объект А типа Animal, объект С ТЙП* сЯ и объект Т типа Tiger. С помощью функций Announce иллюстрирУ*^ эффект различного способа передачи параметров. Статическое евязыв рассматривается на примере передачи в Announcel объекта Т типа п Полиморфизм демонстрируется тремя отдельными вызовами функпи м nounceS, в которую передаются указатели на объекты А, С и Т. Ете а. примером полиморфизма является обращение к методу Identify че₽е е вь1-затель класса Animal, указывающий на объект типа Cat. В РезультайсТрй' зывается метод Identify для класса Cat. Оставшаяся часть кода ДеМ° рб'Ь' рует присвоение производного объекта базовому. Данные производно екта, унаследованные от базового класса, копируются в правую ча1^а1.Ь-
Упомянутые классы и функции Announce находятся в файле
flinclude <iostream.h>^
flinclude <string.h>
flinclude "animal.h"
void main (void)
* Animal A (’’млекопитающее"), *p;
Cat C("домашний", "теплокровное");
Tiger T("бенгальский", "дикий", "хищное");
// статическое связывание. Announcel имеет параметр Т типа Tiger //и выполняет метод Identify из Animal
Announcel(Т); // статическое связывание; вызов метода Animal
// примеры полиморфизма, поскольку параметр является указателем, // Announce2 использует динамическое связывание для выполнения
// метода Identify фактического параметра-объекта.
Announce2(&А); // динамическое связывание;'вызов метода Animal
Announce2(&С); // динамическое связывание; вызов метода Cat
Announce2(&Т); Н динамическое связывание; вызов метода Tiger
// непосредственное обращение к методу Identify класса Animal
A. Identify О;	// статическое связывание
cout « endl;
И динамическое связывание, вызов метода Cat р - fiC;
p->Identify();
cout « endl;
// присвоение объекта типа Tiger объекту типа Animal.
// копируются данные, наследуемые от Animal
А ж Т;
A. Identify();	//
cout « endl;
)
*/
«Выполнение программы 12.2>
Вызов функции Identify при статическом связывании: я хищное животное
Вызов функции Identify при динамическом связывании; я млекопитающее животное
Вызов функции identify при динамическом связывании*. я теплокровное животное
Я домашний кот
Вызов функции Identify при
Я Я
хищное животное
ДИКИЙ кот Сенгальский тигр
динамическом связывании:
Я млекопитающее животное
„ Теплокровное животное домашний кот
Я
/wXllniKoe животное
Приложение: геометрические фигуры и виртуальные Мет0
Класс Shape может быть использован в качестве базового для по изводных геометрических классов, включая Circle и Rectangle. Приложении мы дадим спецификацию класса Rectangle и применим те с классом Circle в одной программе для .иллюстрации виртуальна Blaet ций.
-		У length (длина)	
	X		width (ширина)
В классе Rectangle базовой точкой является верхний левый угол объекта Подобно классу Circle, Rectangle подменяет метод Draw базового класса своим собственным виртуальным методом Draw, отображающим прямоугольник В классе Rectangle также определяются методы Area (длина * ширина) и Ре. rimeter ( 2 * (длина+ширина)) вместе с методами для получения и изменения значений длины и ширины. Мы представляем объявление класса и отсылаем читателя к файлу geometry.h в программном приложении.
// производный класс Rectangle; наследует класс Shape class Rectangle: public Shape
{
protected:
// защищенные элементы данных, описывающие прямоугольник
float length, width;
public:
// конструктор, получающий в качестве параметров
// координаты базовой точки, длину, ширину и образец заполнения Rectangle (float h-=C, float v-0, float 1-0,
float w-0, int fill - 0);
11 методы
float GetLength(void) const;
void SetLength(float 1};
float GetWidth(void) const;
void SetWidth (float w) ;
// подменить виртуальные функции базового класса
virtual void Draw(void) const; // визуальное отображение прямоугольника virtual float Area(void) const;
virtual float Perimeter(void) const;
Программа 12.3. Геометрические классы и виртуальные функции
Эта программа иллюстрирует динамическое связывание и полиморфизм для базового класса Shape и производных от него классов Circle и Rectangle. Объект С типа Circle является статическим, а переменные one, two и three — указателями на объекты типа Shape.
У При использовании статического связывания площадь и длина окружности объекта С вычисляются методами С.Агеа() и C.Perimeter(). Указателям класса Shape присваиваются адреса динамически создаваемых объектов типа Circle и Rectangle. При использовании динамического связывания, площадь и периметр объекта вычисляются методами Area/Perimeter из соответствующего производного класса. Указателю three присваивается адрес Circle-объекта С. Таким образом мы динамически связываемся с соответствующими методами класса Circle при вызове функций для вычисления площади и периметра объекта С.
Динамическое связывание позволяет использовать три указателя базового класса, чтобы выполнить метод Draw для трех фигур.
^include <iostream.h>
^include "graphlib.h"
^include "geometry.h”
void main (void)
{
// окружность С с центром в точке (3,1) и радиусом 0.25
// переменная three является указателем типа Shape на окружность С
Circle С(3,1, .25, 11);
Shape *one, *two, *three = &С;
char eol;
// окружность *опе имеет центр в точке (1,1) и радиус 0.5 // прямоугольник *two базируется в точке (2,2) //и имеет длину и ширину, равные 0.5 one = new Circle(1,1, .5, 4);
two = new Rectangle(2,2, .5, .5, 6);
cout « "Площадь/периметр С и фигур 1—3:" « endl;
cout « "С: ” « с.Area() « " ” « С.Perimeter() « endl; cout « "1: ’ « one->Area() « " ”
« one->Perimeter() « endl;
cout « "2: " « two->Area() « " "
« two->Perimeter() « endl;
cout « "3: " « three->Area() « “ "
« three->Perimeter() « endl;
Cout « "Нажмите Enter, чтобы увидеть фигуры: cin.get(eol);	// ждать нажатия клавиши Enter
//инициализация графической системы InitGraphics();
°ne->Draw();	//	нарисовать	окружность
two->Draw();	//	нарисовать	прямоугольник
three->Draw();	//	нарисовать	окружность
выдержать паузу и закрыть графическую систему ’lewPauseO;
j ®hutdownGraphics();
«Выполнение программы 12.3>
Площадь/периметр С и фигур 1—3:
С: 0.196349 1.570795
1:	0.785398 3.14159
2:	0.25 2
3:	0-196349 1.570795
Нажмите Enter, чтобы узидеть фигуры:
*/
Виртуальные методы и деструктор
Деструктор класса должен быть определен, когда класс распределяет динамическую память. Если класс будет использоваться в качестве базового, его деструктор должен быть виртуальным. Этот тонкий, но важный момент должен учитываться при сохранении списков объектов по указателям базового класса. Если деструктор базового класса не является виртуальным, то базовый класс, ссылающийся на объект производного класса не будет вызывать деструктор этого класса. Эта проблема иллюстрируется следующей ситуацией. Пусть конструктор базового класса BaseCL динамически размещает массив из семи целых чисел. Для освобождения памяти должен быть разработан деструктор.
class BaseCL {
public:
BaseCL (...);	// разместить 7-элементный массив
~BasecL (void);	И деструктор (не виртуальный)
)
Класс DerivedCL наследует BaseCL и выполняет те же действия.
class DerivedCL (
public:
DerivedCL (...);	// разместить 7-элементный массив
-DerivedCL (void);	// деструктор (не виртуальный)
)	ирЯ
Предположим, что р является указателем класса BaseCL, которому сваивается динамический объект типа DerivedCL, и затем мы вызывав14 v цию delete:
BaseCL *р = new DerivedCLt); // построить новый объект типа DerivedCL
delete р;
// вызвать деструктор базового класса
Динамические данные, порожденные производным классом, не уничтожайся» Если же деструктор базового класса объявлен виртуальным, вызывается 1°стрУкт0Р производного класса. Деструктор базового класса тоже вызывался, но не раиь™® производного.
еТ в* общем случае, если класс будет использоваться в качестве базового в оархии наследования, он обязательно должен иметь виртуальный деструктор- Даже если этот Деструктор ничего не будет делать. Например, virtual BaseCL:: ~BaseCL (void)
П
12.4. Абстрактные базовые классы
Наше обсуждение наследования привело к использованию виртуальных методов базового класса одновременно с методами, имеющими те же имена, 0О принадлежащими к производным классам. Поскольку базовый метод определяется как виртуальный, можно использовать динамическое связывание и гарантировать таким образом вызов правильной версии. Например, в классе Shape определяется виртуальный метод Draw, имеющий примитивную задачу установки образца заполнения. Каждый из наших производных геометрических классов имеет свой собственный метод Draw, подменяющий базовый и рисующий конкретную фигуру. В том же классе Shape мы определили виртуальные методы для вычисления площади и периметра. Эти операции не имеют смысла для объектов типа Shape, которые состоят из базовой точки и образца заполнения. Подразумевается, что эти операции будут подменяться в производных геометрических классах. Объявляя эти операции в базовом классе как виртуальные методы, мы гарантируем, что динамическое связывание будет вызывать корректную версию метода для конкретного геометрического объекта. Определим функции, возвращающие О.
И Определение функции-заглушки в базовом классе float Shape:: Area (void) const (
return 0.0?	// площадь точки
)
float Shape::Perimeter(void) const
! retuxn 0.0;	// периметр точки
Вместо того чтобы вынуждать программиста создавать подобные заглушки, ++ допускает использование чистых виртуальных функций (pure virtual Actions), путем добавления "=0" к определению. Например, ^tuai fioat Area(void) const = 0;
tual float Perimeter(void) const =0?
Применение чистой виртуальной функции в базовом классе подразумевает, 0 немедленной ее реализации не будет. В то же время это объявление й едписывает реализацию функции в каждом производном классе. Например, т ^Дом производном геометрическом классе должны быть определены ме-Area и Perimeter. Включая чистые виртуальные функции в класс Shape, Тем самым гарантируем невозможность создания отдельных объектов а Shape. Этот класс может лишь служить базовым для другого класса.
Класс с одной или несколькими чистыми виртуальными функциям^ вается абстрактным (abstract class). Любой класс, образованный от eg кого класса обязан обеспечить реализацию каждой чистой виртуальной1^^-ции, иначе он также будет абстрактным и не сможет порождать обп
Пример 12.3
Абстрактный класс BaseCL содержит две чистые виртуальные ции и поэтому является абстрактным базовым классом.

class BaseCL {
public:
virtual void F(void) - 0;	// чистая виртуальная функция
virtual void G(void) - 0;	// чистая виртуальная функция
}»
Производный класс DerivedCL определяет F, но не G и поэтому тается абстрактным.
class DerivedCL: public BaseCL {
public:
// поскольку функция G не определена, нельзя объявите
// объект типа DerivedCL. класс остается абстрактным
// базовым классом для другого производного класса,
//в котором будет определена функция G	«
virtual void F(void);
}»•
Следующее объявление повлечет за собой ошибку компиляции: DerivedCL D;
Ошибка: г
нельзя создать экземпляр абстрактного класса 'DerivedCL*
Абстрактный базовый класс List
Абстрактный класс служит шаблоном для своих производных классов. Он может содержать данные и методы, совместно используемые всеми произвол ными классами. С помощью чистых виртуальных функций он обеспечива объявления общедоступных методов, которые должны быть реализованыJBP изводными классами. В качестве примера мы разрабатываем абсТРаК^уЮ класс List как шаблон для списковых коллекций. Этот класс имеет п?Ре1!- „е й (член класса) size, используемую для определения методов ListEmpty. Эти функции доступны каждому производному классу, обеСят0в, веющему корректное сохранение size при включении или удалении ®л®Vgjnpty а также при очистке списка. Несмотря на то что методы ListSize и № представляются в базовом классе, они могут быть либо подменены в п^я1отсй ном классе, либо восприняты по умолчанию. Остальные методы °^ъЯВяТЬсЯ в как чистые виртуальные функции базового класса и должны подмен производном классе. Функция Insert зависит от конкретного класса вый ций. В одном образовании Insert может помещать данные в последовав список, а для бинарного дерева или словаря требуется совершенно в® ритм включения.
спецификация класса List
I ^late <class T>
I ciass List
1 protected:
// число элементов списка/ обновляемое производным классом int size;
. public:
// конструктор
List(void);
// методы доступа к списку
virtual int ListSize(void) const;
virtual int ListEmpty(void) const;
virtual int Find (Tfi item) - C;
// методы модификации списка
virtual void Insert (const T& item) =0;
virtual void Delete (const Te item) =0;
virtual void ClearList (void) = 0;
Реализация методов класса List
В любом производном классе методы модификации списка должны поддерживать size — член базового класса. Начальное значение О присваивается этой переменной конструктором класса List.
// конструктор устанавливает size в 0
template <class Т>
int List<T>::List(void): size(O) (}
Методы ListSize и ListEmpty класса List зависят только от значения size. Они реализуются в базовом классе и затем используются любым производным классом.
И возвратить размер списка
template <class Т>
int List<T>:: List (void) const
/ проверить, пуст ли список template <class T>
List<T>:: ListEmpty (void) const
return size — 0;
Образование класса SeqList из абстрактного базового класса List
Первый раз мы представили класс SeqList в гл. 1 и в последующих главах казали реализацию массива и связанного списка. Теперь мы снова рас-SeqList в качестве класса, образованного от абстрактного класса Методы DeleteFront и GetData отсутствуют в абстрактном классе, так °ви применимы только к последовательному списку.
Спецификация класса SeqList
ОБЪЯВЛЕНИЕ
template <class Т>
class SeqList: public List
(
protected:	।
11 связанный список, доступный производным классам LinkedLiSt<T> Hist;
public:
// конструктор
SeqList(void);
Il методы доступа к списку virtual int Find (те item);
T GetData(int pos);
// методы модификации списка virtual void Insert (const T4 item); virtual void Delete (const T& item); T DeleteFront(void);
virtual void ClearList (void);
// для объекта типа SeqListIterator требуется доступ к Hist friend class SeqListiterator<T>;	•
);
ОПИСАНИЕ
Являясь наследником абстрактного класса List, класс SeqList должен поддерживать указанные в List операции. Поскольку SeqList реализует последовательный список, в этот производный класс должны быть добавлены метод GetData, принимающий позицию элемента в качестве параметра, и метод DeleteFront, удаляющий первый элемент списка.
Методы Insert, Delete и ClearList поддерживают защищенный элемент данных базового класса size, поэтому методы ListSize и ListEmpty подменять ие нужно.
Прохождение объекта типа SeqList можно выполнить с помощью средства, называемого итератором (iterator). Этот инструмент, объявляемый как объект типа SeqListlterator, должен иметь доступ к Hist, что обеспечивается объявлением класса SeqListlterator дружественным. Итераторы обсуждаются в следующем разделе. Производная версия класса SeqList вкючена в файл seqlist2.h-
Реализация производной версии класса SeqList
Основная часть работы по реализации этого класса была сделана в гл-9-Нам необходимо определить функции Insert, Delete, ClearList и Find. № повторяем их определения, сделанные в классе LinkedList, но Д°®авЛ^иТ поддержку значения size из класса List. Например, метод Insert выгляд следующим образом:
// использовать метод InsertRear для включения элемента в хвост списка template <class Т>
void SeqList<T>::Insert(const T& item) <
Hist. InsertRear (item) ;
size++; // обновить size в классе List
}	_ «ласе*
Конструктор производного класса SeqList вызывает конструктор List, который обнуляет size.
// конструктор умолчания
1 < инициализация базового класса ieinplat® <class Т>
geqList<T>::SeqList(void): List<T>()
О
12.5. Итераторы
Многие алгоритмы обработки списков предполагают, что мы сканируем элементы и попутно совершаем какое-то действие. Производный от List класс предоставляет методы для добавления и удаления данных. В общем случае в рем отсутствуют методы, специально предназначенные для прохождения списка. Подразумевается, что прохождение осуществляет некий внешний процесс, который поддерживает номер текущей записи списка.
В случае массива или объекта L типа SeqList мы можем выполнить прохождение, используя цикл и индекс позиции. Для объекта L типа SeqList доступ к данным класса осуществляется посредством метода GetData.
for (pos “С; pos < ListSizeO; роз++)
cout « L.GetData(pos) « "
Для бинарных деревьев, хешированных таблиц и словарей процесс прохождения списка более сложен. Например, прохождение дерева является рекурсивным и должно выполняться рекурсивным прямым, обратным или симметричным методами. Эти методы могут быть добавлены в класс обработки бинарных деревьев. Однако рекурсивные функции не позволяют клиенту остановить процесс прохождения, выполнить другую задачу и продолжить итерацию. Как мы увидим в гл. 13, итерационное прохождение может быть выполнено путем сохранения указателей на узлы дерева в стеке. Классу деревьев не потребуется содержать итерационную версию для каждого способа прохождения, даже если клиент не может выполнить прохождение дерева или может постоянно использовать один метод прохождения. Предпочтительно отделять абстракцию данных от абстракции управления. Решением проблемы прохождения списка является создание класса итераторов, задачей которого будет прохождение элементов таких структур данных, как связанные списки или деревья. Итератор инициализируется так, чтобы указывать На начало списка (на голову, корень н т.д.). У итератора есть методы Next() и EndOfListQ, обеспечивающие продвижение по списку. Объект-итератор сохраняет запись состояния итерации между обращениями к Next.
С помощью итератора клиент может приостановить процесс прохождения, ?Р°верить содержимое элемента данных, а также выполнить другие задачи. Клиенту дается средство прохождения списка, не требующее сохранения внутренних индексов или указателей. Имея класс, включающий дружественный итератор, мы можем связывать с этим классом некоторый подлежащий Локированию объект и обеспечивать доступ к его элементам через итератор. Ри реализации методов итератора используется структура внутреннего предвидения списков.
этом разделе дается общее обсуждение итераторов. С помощью вирту-в функций мы объявляем абстрактный базовый класс, используемый к ачестве основы для конструирования всех итераторов. Этот абстрактный Сс предоставляет общий интерфейс для всех операций итератора, несмотря т° что производные итераторы реализуются по-разному.
..
Абстрактный базовый класс Iterator
Мы определяем абстрактный класс Iterator как шаблон для итераторов ков общего вида. Каждый из представляемых далее итераторов обрааозСГ1Ис этого класса, который находится в файле iterator.h.	Ма
Спецификация класса Iterator
ОБЪЯВЛЕНИЕ template <class т> class Iterator { protected:
// флажок, показывающий, достиг* ли итератор конца списка. // должен поддерживаться производными классами int iterationComplete;
public:
// конструктор Iterator(void);
// обязательные методы итератора virtual void Next(void) = 0; virtual void Reset(void) =0;
// методы для выборки/модификации данных virtual Т& Data(void) x С;
// проверка конца списка virtual int EndofList(void) const;
};
ОБСУЖДЕНИЕ
Итератор является средством прохождения списка. Его основные методы: Reset (установка на первый элемент списка), Next (установка позиции на следующий элемент), EndOfList (обнаружение конца списка). Функция Data осуществляет доступ к данным текущего элемента списка.
Реализация класса Iterator
Этот абстрактный класс имеет единственный элемент данных, iterationCom-plete, который должен поддерживаться методами Next и Reset в каждом производном классе. Из функций реализованы только конструктор и метод EndOfUs -
// конструктор, устанавливает iterationComplete в С (False) template <class Т>
iterator<T>::Iterator(void): iterationComplete(0) {>
Метод EndOfList просто возвращает значение iterationComplete. ®тоТт^сТ. жок устанавливается в 1 (True) производным методом Reset, если СПИСОУ1?В 1 Метод Next в производном классе должен устанавливать iterationCompleie при выходе за верхнюю границу списка.
Образование итераторов для списка
пСДОВ°^
Класс SeqList широко использовался в этой книге и послужил ° с для разработки абстрактного класса List. Ввиду его важности мы ва
тератора последовательных списков. Этот итератор хранит указатель listPtr, Называющий на сканируемый в данный момент объект типа SeqList. Поскольку SeqListlterator является дружественным по отношению к пронзвод-аоМУ классу SeqList, допускается обращение к закрытым элементам данных класса SeqList.
спецификация класса SeqListlterator
ОКЪЯВЛХНИЕ
/ seqListIterator образован от абстрактного класса iterator
template cclass T>
class SeqListlterator: public iterator<T>
< .
private:
// локальный указатель на объект SeqList
SeqList<T> ‘listPtr;
// по. мере прохода по списку необходимо хранить предыдущую и текущую позицию Node<T> ‘prevPtr, ‘currPtr;
public:
// конструктор
SeqListlterator (SeqList<T>& 1st);
// обязательные методы прохождения virtual void Next(void);
virtual void Reset(void);
// методы для выборки/модификации данных virtual Т& Data(void);
11 установить итератор для прохождения нового списка void SetList(SeqList<T>* 1st);
)
ОЕСУЖДЕНИЕ
Этот итератор реализует виртуальные функции Next, Reset и Data, которые были объявлены как чистые виртуальные функции в базовом классе Iterator. Метод SetList является специфичным для класса SeqListlterator и позволяет клиенту присваивать итератор другому объекту типа SeqList. Класс SeqList вместе с итератором находятся в файле seqlist2.h.
ПРИМЕР
s®qList<int> L;	// создать список
SeqListlterator<int> iter(L); // создать итератор и присоединить к списку L cout « iter.Data();	// распечатать текущее значение данных
tec-Next();	// перейти на следующую позицию в списке
* Цикл, выполняющий проход по списку и распечатывающий его элементы
Ог titer.Reset(); I iter.EndOfListО; iter.Next О )
cout « iter.Data () « ”
Построение итератора SeqList
v ?Те₽атоР» создаваемый конструктором, ограничен определенным классом t, и все его операции применимы к последовательному списку. Итератор ₽аййт указатель на объект типа SeqList.
Piet °°Ле пРисоеДинения итератора к списку мы инициализируем iterationCom-16 и устанавливаем текущую позицию на первый элемент списка.
П конструктор, инициализировать базовый класс и локальный указатель SeqList template <class Т>
SeqListIterator<T>::SeqListlterator(SeqList<T>fc 1st): Iterator<T>{), listPtr(filst) { // выяснить, пуст ли список iterationcomplete - list Pt r->lli st. ListEmptyO; // позиционировать итератор на начало списка Beset();
)
Reset устанавливает итератор в начальное состояние, инициализируя itera-tionComplete и устанавливая указатели prevPtr и currPtr на свои позиции в начале списка. Класс SeqListlterator является также дружественным по отношению к классу LinkedList и, следовательно, имеет доступ к члену класса front. // перейти к началу списка 7 template <class Т>	i
void SeqListIterator<T>::Reset(void)	•
{ // переприсвоить состояние итерации iterationComplete = listPtr->llist.ListEmptyOi ft вернуться, если список пуст if (listPtr->llist.front « NULL) return;
// установить механизм прохождения списка с первого узла prevPtr = NULL;
currFtr « listPtr~>llist.front; )
Метод SetList является эквивалентом конструктора времени исполнения. Новый объект 1st типа SeqList передается в качестве параметра, и теперь итератор идет по списку 1st. Переназначьте listPtr и вызовите Reset.
// сейчас итератор должен проходить список 1st.
// переназначьте listPtr и	вызовите Reset.	>
template <class Т>	’
void SeqListlterator<T>::SetList(SeqList<T>& 1st)
<	j
listPtr = Slst;
// инициализировать механизм прохождения для списка 1st Reset();
)
Итератор получает доступ к данным текущего элемента списка с п°м вТ» метода Data(). Эта функция возвращает значение данных текущего эл ^с0К списка, используя currPtr для доступа к данным узла LinkedList. Если с пуст или итератор находится в конце списка, выполнение програ**1***1 кращается после выдачи сообщения об ошибке.
*	J
// возвратить данные, расположенные ^emplate <class Т>
void SeqListIterator<T>::Data(void)
в текущем элементе списка
1 // ошибка, если список пуст или прохождение уже завершено
if (listPtr->llist-ListEmpty() II currPtr == NULL)
* cerr « "Data: недопустимая ссылка!" « endl;
exit(1)•
return curr₽tr->data;
I
Продвижение от элемента к элементу обеспечивается методом Next. Процесс сканирования продолжается до тех пор, пока текущая позиция не достигнет конца списка. Это событие отражается значением члена iterationComplete класса Iterator, который должен поддерживаться функцией Next.
// продвинуться к следующему элементу списка template <class Т>
void SeqListIterator<T>::Next(void)
{
// если currPtr “ NULL, мы в конце списка
if (currPtr “и NULL) return;
// передвинуть указатели prevFtr/currPtr на один узел вперед
prevPtr - currPtr;
currPtr - currPtr->NextNode();
// если обнаружен конец связанного списка,
// установить флажок “итерация завершена"
if (currPtr “ NULL)
iterationComplete - 1;
Программа 12.4. Использование класса SeqListlterator
Некая компания ежемесячно создает записи Salesperson, состоящие из личного номера продавца и количества проданного товара. Список salesList содержит накопленные за некоторый отрезок времени записи Salesperson. Во втором списке, idList, хранятся только личные номера служащих. Из файла sales.dat вводится информация о продажах за несколько месяцев, и каждая запись включается в salesList. Поскольку записи охватывают несколько месяцев, одному продавцу может соответствовать несколько записей. Однако в список idList каждый сотрудник включается только единожды.
После ввода Данных соответствующим спискам назначаются итераторы idlter и saleslter. Сканируя список idList, мы идентифицируем каждого служащего по его личному номеру и передаем этот номер в качестве параметра функции PrintTotalSales. Эта функция сканирует список salesList и Подсчитывает суммарное количество товара, проданного сотрудником с дан-нЫм личным номером. В конце распечатывается личный номер служащего и суммарное количество проданного им товара.
—‘—
^include <lostream.h>
include <fstream.h>
include "seqlist2.h"
---------------------------------—
// использовать класс SeqList, наследующий класс List, и SeqListIterate // запись, содержащая личный номер продавца и количество проданного ТовГ struct Salesperson { int idno;
int units;
// оператор »- сравнивает служащих по личному номеру
int operator -= (const Salesperson &a, const Salesperson fib) (
return a.idno b.idno;
// взять id в качестве ключа и пройти список.
// суммировать количество товара, проданное сотрудником с личным номером id // печатать результат
void PrintTotalSales(SeqList<SalesPerson> & L, int id) {
// объявить переменную типа Salesperson и инициализировать поля записи Salesperson salesP - (id^ 0);
// объявить итератор последовательного списка
//и использовать его для прохождения списка SeqListIterator<SalesPerson> iter(L);
for(iter.Reset(); (iter.EndOfList(); iter.Next() )
// если происходит совпадение c id, прибавить количество товару
if (iter.Data()	salesP)
sales.Р +- (iter.Data()).units;
// печатать личный номер и суммарное количество продаж cout « "Служащий " « salesP. idno
« " Количество проданного товара ’’ « salesP.units « endl;
)
void main(void)
(
// список, содержащий записи типа Salesperson, // и список личных номеров сотрудников SeqList<SalesPerson> SalesList;
SeqList<int> idList;
ifstream salesFile;	// Входной файл
Salesperson salesP; // Переменная для ввода int i;
// открыть входной файл
salesFile.open("sales.dat", ios:: in | ios::nocreate); if (!salesFile) {
cerr « "Файл sales.dat не найден!’*;
exit(1);
}
// читать данные в форме "личный номер количество -товара" // до конца файла
while ((salesFile.eof()) (
// ввести поля данных и вставить в список salesList salesFile » salesP.idno » salesP.units;
salesList.Insert(salesP);
// если id отсутствует в idList, включить этот id
if (!idList.Find(sales?.idno)) idList.Insert(sales?.idno);
// создать итераторы для этих двух списков
SeqListIterator<int> idlter(idList);
SeqListIterator<Sales₽erson> saleslter(salesList);
// сканировать список личных номеров и передавать каждый номер
//в функцию PrintTotalSales для добавления количества // проданного товара к общему числу его продаж
for(idlter. Reset(); Iidlter.EndofList(); idlter.Next() )
PrintTotalSales(salesList, idlter.Data());
)
/*
<Файл sales.dat>
300	40
100	45
200	20
200	в»
100	50
300	10
400	40
200	30
300	10
СПрОГОН программы 12.4>
Служащий 300 Количество проданного товара 70
Служащий 100 Количество проданного товара 95
Служащий 200 Количество проданного товара 110
Служащий 400 Количество проданного товара 40 */
Итератор массива
Стремясь привязать итераторы к классам списков, мы, возможно, упустили из вцду класс Array. Между тем итератор массивов является весьма полезной абстракцией. Настроив итератор так, чтобы тот начинался и заканчивался на конкретных элементах, можно исключить работу с индексами, "роме того, один и тот же массив может обрабатываться несколькими итераторами одновременно. Здесь приводится пример использования нескольких ИтеРаторов при слиянии двух отсортированных последовательностей, находящихся в одном массиве.
Спецификация класса Arrayiterator
^ЯВЛЕНИЕ
,lriclude "iterator.h"
Cla₽lete <class T>
( ®s Arrayiterator: public iterator<T> ₽Jtivate:
' начальная, текущая и конечная точ lnt вtartIndex;
int currentindex; int finishindex;
// адрес объекта типа Array, подлежащего сканированию
Array<T> *arr;
*
public:	i
// конструктор
Arrayiterator(Array<T>fi A, int start=O, int finish=-l);
11 стандартные операции итератора, обусловленные базовым классом virtual void Next(void);
virtual void Reset(void);
virtual T& Data(void);
};
ОБСУЖДЕНИЕ
Конструктор связывает объект типа Array с итератором и инициализируй начальный и конечный индексы массива. По умолчанию начальный индекс равен О (итератор находится на первом элементе массива), а конечный индекс равен -1 (верхней границей массива является индекс последнего элемента) На любом шаге итерации currlndex является индексом текущего элемента массива. Его начальное значение равно startindex. Класс Arrayiterator находится в файле arriter.h. .
Класс Arrayiterator имеет минимальный набор общедоступных функций-членов, подменяющих чистые виртуальные методы базового класса.
ПРИМЕР
// массив 50 чисел с плавающей точкой от 0 до 49
Array<double> А(50);
// итератор массива сканирует А от 3-го до 10-го индекса Arraylterator<double> arriter(Arr, 3, 10);
// печатать массива с 3-го по 10-й элемент for (arriter. Reset О; !arriter.EndOfList(); arriter.Next() ) cout « arriter.Data() « ”
Приложение: слияние сортированных последовательностей
В главе 14 формально изучаются алгоритмы сортировки, включая и внешнюю сортировку слиянием, которая упорядочивает файл данных на Дис^' Этот алгоритм разделяет список элементов на сортированные подсписки, зываемые последовательностями (runs).
ОПРЕДЕЛЕНИЕ
Xj
В списке Хо, Xi, ..., Хп1 последовательностью является подсписок
Xa+i, ..., Хь, где
Xi<Xi+i	при а<КЬ
Ха-1>Ха	при а>0
Хъ+1<Хь	при b<n—1
Например, подсписок Х2 ...	Х5 есть последовательность в массиве
X: 20 35	15 25 30 65	50 70 10
В процессе слияния последовательности вкладываются друг в друга, со-давая тем самым более длинные упорядоченные подсписки до тех пор, рока в результате не получится отсортированный массив.
Список А: 3	6	23	35	2	4	6
Последовательность #1 Последовательность #2
Это приложение реализует лишь очень ограниченную часть полного алгоритма. Предполагается, что данные хранятся в виде двух последовательностей в N-элементном массиве. Первая последовательность заключена в диапазоне от О до R-1, вторая — от R до N-1. Например, в семиэлементном массиве А последовательности разделяются на индексе R = 4.
Поэлементное слияние порождает сортированный список. Текущая точка прохождения устанавливается на начало каждой последовательности. Значения в текущей точке сравниваются, и наименьшее из них копируется в массив. Когда значение в последовательности обработано, выполняется шаг вперед к следующему числу и сравнение продолжается. Поскольку подсписки изначально упорядочены, элементы копируются в выходной массив в сортированном порядке. Когда одна из последовательностей заканчивается, оставшиеся члены другой последовательности копируются в выходной массив.
Этот алгоритм изящно реализуется с помощью трех итераторов: left, right и output. Итератор left проходит первую последовательность, right — вторую, a output используется для записи данных в выходной массив. Пример работы алгоритма показан на рис. 12.2.
Программа 12.5. Слияние сортированных последовательностей
Функция Merge получает две последовательности, расположенные в массиве А, и сливает их в выходной массив Out. Этот используют итераторы left н right, которые инициализируются параметрами lowlndex, endOfRunlndex н highlndex. Итератор output записывает отсортированные данные в Out. Процесс прекращается по достижении конца одной из последовательностей. Функция Сору дописывает , данные, оставшиеся в Другой последовательности, в Массив Out. После сбрасывания итератора output в начальное состояние отсортированный список копируется обратно в А.
Эта программа вводит 20 целых чисел из файла rundata. В процессе ввода Мы сохраняем данные в массиве А и распознаем индекс конца последовательности, который потребуется функции Merge. Функция Merge сортирует Массив, который затем распечатывается.
^include <iostream.h>
include <fstream.h>
J^clude "array.h” inciude "arriter.h”
^копирование одного массива в другой с помощью их итераторов
Copy (Array iterator<int>b Source, Arraylterator<int>fi Dept)
LLiarl
I 3 I 6 |23|35| 2 | 4 | 6 |
left
right
Шаг 2
left
right
ШагЗ
left
right
Шаг 4
left
right
Шаг 5
right
Шаг 6
right достиг конца последовательности
оставшиеся члены первом' последовательности переписываются в выходной массив
Ji
Рис 12.2. Слияние сортированных последовательностей
while ( ’Source.EndofList() ) {
Dest.Data() - Source. Data();
Source.Next();
Dest.NextO ;
}
// слияние сортированных последовательностей в массиве А.
// первая последовательность заключена в диапазоне индексов
// lowlndex..endOfRunlndex-1, // вторая - в диапазоне endOfRunindex.-hig dex
void Merge(Array<int>& A, int lowlndex, int endOfRunindex, int highlndex)
{
// массив, в котором объединяются сортированные последовательности Array<int> Out(A.ListSize());
// итератор left сканирует 1-ю последовательность;
ь // итератор right сканирует 2-ю последовательность;
Arraylterator<int> left(A, lowlndex, endOfRunlndex-l);
Arraylterator<int> right(A, endOfRunindex, highlndex);
ff итератор output записывает отсортированные данные в Out Arraylterator<int> output(Out);
If копировать» пока не кончится одна или обе последовательности while (.’left.EndOfList() && !sight.EndOfListО)
// если элемент "левой" последовательности с итератором left меньше или
// равен элемент "правой" последовательности, то записать его в массив Out. // перейти к следующему элементу "левой” последовательности
if (left.Data() <- right.Data()) (
output.Data()  left.DataO; Left-Next();
}
// иначе записать в Out элемент "правой” последовательности
// и перейти к следующему элементу "правой" последовательности else
<
output.Data() - right.Data();
right.Next О ;
> output.Next();	// продвинуть итератор выходного массива
}
// если одна из последовательностей не обработана до конца,
// скопировать этот остаток в массив Out if (Heft.EndOfList(В
Copy(left, output);
else (fright.EndOfList())
Copy(right, output);
If сбросить итератор выходного массива и скопировать Out в А output.Reset();
Arraylterator<int> final(А); // массив для копирования обратно в А Copy (output, final);
} void main (void) «
ff массив для сортированных последовательностей, введенных из потока fin Array<int> А{20);
ifstream fin;
int i;
int endOfRun • 0;
11 открыть файл rundata
fin.open("rundata", ios::in | ios::nocreate);
if (!fin) (
cerr « "Нельзя открыть файл rundata", « endl; exit(l);
)
11 читать 20' чисел, представленных в виде двух
// сортированных последовательностей
fin » А[0);
for (i=l; i<20; i++)
fin » A(i] ;
if (A(i) < A(i-U) endOfRun • i;
// слияние последовательностей Merge(А, 0, endOfRun, 19);
// распечатать отсортированный массив по 10 чисел в строке for (i-0; i<20; 1++)
<
cout « A[i] « "
if (i =» 9) cout « endl;
)
I
<Файл rundata>
1 3 6 9 12 23 33 45 55 68 88 95
2 8 12 25 33 48 55 75
<Выполнение программы 12.5>
1 2 3 6 8 9 12 12 23 25
33 33 45 48 55 55 68 75 88 95
*/
Реализация класса Arrayiterator
Конструктор задает начальное состояние итератора. Он привязывает итератор к массиву и инициализирует три индекса. Если для индексов startindex и finishlndex используются значения по умолчанию (0 и -1), то итератор проходит через весь массив.
// конструктор, инициализирует базовый класс и данные-члены template <class T>
ArrayIterator<T>::Arrayiterator(Array<T>4 A, int start, int finish): arr(&A) (
7/ последний доступный индекс массива int Hast = A. Listsize () - 1;
// инициализировать индексы, если finish « -1,
//то сканируется весь массив
currentindex - startindex - start;
finishindex - finish -1 ? finish : Hast;
// индексы должны быть в границах массива
if (!((startIndex>-0 fie startIndex<-ilast) &&
(finishIndex>-0 && finishlndex<™ilast) && (startindex <» finishindex)))
(
cerr « "Arrayiterator: Неверные параметры индекса!" « endl;
exit(1);
)
Reset переустанавливает текущий индекс на стартовую точку и w iterationComplete, показывая тем самым, что начался новый процесс ПР дения.
// сброс итератора массива template <class Т> void Arraylterator<T>::Reset(void)
1 if установить текущий индекс на начало массива currentindex » startindex;
ц итерация еще не завершена
iterationComplete « 0;
Метод Data использует currentindex для доступа к данным-членам. Если текущая точка прохождения заходит за верхнюю границу списка, генерируется сообщение об ошибке и программа прекращается.
’ возвратить значение текущего элемента массива temple® <class Т>
ть A.rrayIterator<T>:: Data (void)
! // если весь массив пройден, то вызов метода невозможен
if (iterationComplete)
cerr « "Итератор прошел весь список до конца!" « endl;
exit(1);
)
return (*arr) [currentIndex];
)
Если итерация завершается, метод Next просто возвращает управление. В противном случае он увеличивает currentindex и обновляет логическую переменную базового класса iterationComplete.
// перейти к следующему элементу массива
template <class Т>
void Arraylterator<T>::Next (void) (
// если итерация не завершена, увеличить currentIndex
// если пройден finishindex, то итерация завершена
if (‘iterationComplete)
I
currentlndex++;
if (currentindex > finishindex)
iterationComplete - 1;
12.6. Упорядоченные списки
Класс SeqList создает список, элементы которого добавляются в хвост. В Результате получается неупорядоченный список. Однако во многих приложе-з Ях требуется списковая структура с таким условием включения, при котором Дементы запоминаются в некотором порядке. В этом случае приложение смо-6т эффективно определять наличие того или иного элемента в списке, а также вводить элементы в виде отсортированных последовательностей.
с ^тобы создать упорядоченный список, мы используем класс SeqList в каче-е базового и образуем на его основе класс OrderedList, который вставляет йае^енты в возрастающем порядке с помощью оператора Это пример Ле^ования в действии. Мы переопределяем только метод Insert, поскольку пЛ/Фугие операции не влияют на упорядочение и могут быть унаследованы ’’базового класса.
Спецификация класса OrderedList
ОБЪЯВЛЯЮТ
((include "seqlist2.h"
template cclass T>
class OrderedList: public SeqList<T> (
public:
// конструктор
OrderedList(void);
// подменить метод Insert для формирования упорядоченного списка virtual void Insert (const t& item):
};
ОПИСАНИЯ
Все операции, за исключением Insert, взяты из SeqList, так как они влияют на упорядочение. Поэтому должен быть объявлен только метод Insert чтобы подменить одноименный метод из SeqList. Эта функция сканирует спи* сок и включает в него элементы, сохраняя порядок.
Класс OrderedList находится в файле ordlist.h.
Реализация класса OrderedList
В классе OrderedList определяется конструктор, который просто вызывает конструктор класса SeqList. Тем самым инициализируется этот базовый класс, а он в свою очередь инициализирует свой базовый класс List. Мы имем пример трех-классовой иерархической цепи.
// конструктор, инициализировать базовый класс template <class T>
OrderedList::OrderedList(void): SeqList<T>() ()
В этом классе определяется новая функция Insert, которая включает элементы в подходящее место списка. Новый метод Insert использует встроенный в класс LinkedList механизм поиска первого элемента, большего, чем включаемый элемент. Метод InsertAt используется для включения в связанный список нового узла в текущем месте. Если новое значение больше, чем все имеющиеся, оно дописывается в хвост списка. Метод Insert отвечает за обновление переменной size, определенной в базовом классе List.
// вставить элемент в список в возрастающем порядке template <class Т>
void OrderedList:: Insert (const T& item) <
// использовать механизм прохождения связанных списков
// для обнаружения места вставки
for( llist.Reset() ; !llist.EndOfList»; llist.Next» )
if (item < llist.Data())
break;
// вставить item в текущем месте llist.InsertAt(item);
size++;
oIi0C8*8
Приложение: длинные последовательности. В программе 12.5 ° сор' часть алгоритма сортировки слиянием, который включал слияние Дву
0рованных последовательностей в одну, тоже сортированную. В программе предполагалось, что ваши входные данные уже заранее разбиты на две пос-едовательности. Сейчас мы обсудим методику фильтрации (предварительной бработки) данных для получения более длинных последовательностей.
° Предположим, что большой блок данных хранится в случайном порядке в массиве или на диске. Тогда эти данные можно представить в виде ряда коротих последовательностей. Например, следующее множество из 15-и символов состоит из восьми последовательностей.
Char Array: [a k] [g] [с m t] [е п] (1] (с г s] [с b f]
Попытка использовать сортировку слиянием для упорядочения этих данных бздла бы тщетной ввиду значительного числа коротких последовательностей, родлежащих объединению. В нашем примере четыре слияния дают следующие последовательности.
[а д к] [с е m t] [с 1 г s] [b с fl
Сортировка слиянием предписывает объединить на следующем проходе эти четыре последовательности в две и затем создать полностью отсортированный список. Алгоритм работал бы лучше, если бы изначально последовательности имели разумную длину. Этого можно достичь путем сканирования элементов и объединения нх в сортированные подсписки. Алгоритм внешней сортировки должен противостоять относительно медленному времени доступа к диску и часто включает в себя фильтр для предварительной обработки данных. Мы должны постараться, чтобы время, затраченное на фильтрацию данных, повышало бы общую эффективность алгоритма.
Упорядоченный список является примером простого фильтра. Предположим, что исходный массив или файл содержит N элементов. Мы вставляем каждую группу из к элементов в некоторый упорядоченный список, а затем копируем этот список обратно в массив. Этот фильтр гарантирует, что последовательности будут иметь длину, по крайней мере, к. Например, пусть к—5, и мы обрабатываем данные массива CharArray. Тогда результат будет таким:
[а с д к т] (с е 1 п с} (b с f г э]
Усовершенствованная версия этого фильтра приводится в гл. 13.
Программа 12.6. Длинные последовательности
Эта программа фильтрует массив 100 случайных целых чисел в диапазоне от 100 до 999 в последовательности, по крайней мере, из 25 элементов, Используя упорядоченный список. Каждой новое случайное число вставляется в объект L типа OrderedList. Для каждых 25 элементов функция Сору Удаляет эти элементы из списка L и вставляет их обратно в массив А. Программа заканчивается печатью результирующего массива А.
include <iostream.h>
^include "ordlist. h**
’include "array.h"
“include "arriter.h" ’include "random.h"
U
пройти целочисленный массив и распечатать каждый элемент по ю чисел в строке
19 3*К- 425
void PrintList(Array<int>s A) {
// использовать итератор массива Arraylterator<int> iter(A);
int count;
// прохождение и печать списка count - 1;	t
for(iter.Reset(); liter.EndofList (); iter.NextO, count++) {
cout « iter.Data() « " П печатать no 10 чисел в строке if (count % 10 =- 0)
cout « endl;
// удалять элементы из упорядоченного списка L и вставлять их в массив а // обновить loadindex, указывающий следующий индекс в А
void Copy(OrderedList<int> &L, Array<int> &А, int &loadlnd^x)
while (IL.ListEmptyO)
A[loadlndex++] - 1.DeleteFront(}; i void main(void) (
// создать последовательности в А с помощью упорядоченного списка L Array<int> А(100);
OrderedList<int> L;
// генератор случайных чисел RandomNumber rnd;
int i, loadindex - 0;
// сгенерировать 100 случайных чисел в диапазоне от 100 до 999.
// отфильтровать их через 25-элементный упорядоченный список.
// после заполнения списка копировать его в массив А for (i-1; i<-100; 1++) {
L.Insert(rnd.Random(900) + 100);
if (i % 25 — 0) Ccpy(L, A, loadindex);
)
// печатать итоговый массив A PrintList(A);
)
/*
«Выполнение программы 12.6>
110	116	149	152	162	240	345	370	422	492
500	532	578	601	715	730	732	754	815	833
650	903	929	947	958	105	132	139	139	190
205	216	221	243	287	348	350	445	466	507
513	524	604	634	641	730	784	940	969	982
296	375	412	437	457	466	507	550	594	652
725	728	771	799	803	815	859	879	909	915
940	990	991	992	994	101	118	123	155	310
343	368	372	434	443	489	515	529	557	574
641	739	774	784	829	875	883	922	967	972
12.7. Разнородные списки
Коллекция, хранящая объекты одинакового типа называется однородной homogeneous). До сих пор мы рассматривали только однородные коллекции. Коллекция, содержащая объекты различных типов, называется разнородной heterogeneous). Поскольку типы данных в C++ определяются в момент ком-вилянии, мы должны представить новую методику для реализации разнородных коллекций. В данном разделе мы реализуем разнородные массивы и свя-ддзные списки, предполагая, что все имеющиеся там объекты образованы от общего для всех них базового класса.
Разнородные массивы
Всеобъемлющее обсуждение разнородных массивом выходит за рамки данной книги. Мы ограничимся массивами указателей на объекты различных типов. Рассмотрим еще раз пример из гл. 1, иллюстрирующий полиморфизм, ради удобства сформулируем его снова.
Имеется набор базовых операций, необходимых для покраски любого дома. Дома различных типов требуют различной технологии покраски. Например, деревянные стены можно шлифовать, а пластиковую облицовку можно мыть. В контексте объектно-ориентированного программирования эти дома представляют собой различные классы, образованные от базового класса (House) дом, который содержит общие для всех операции покраски. Технология покраски (метод Paint) ассоциируется с каждым классом.
Дом (House)
Базовый класс House содержит идентификационную строку "дом” и вир-туальный метод Paint, который распечатывает ее. Каждый производный класс вменяет метод Paint и показывает тип дома, подлежащего покраске.
°азовый класс в иерархии технологий покраски домов Rouse
Private: id; // идентификатор дома
,f конструктор, присвоить идентификатору дома значение "дом"
«ouse (void)
। td " "дом";
String Public?
f/ виртуальный метод, печатает символьную строку "дом" virtual void Faint(void) {
cout « id?
)
Каждый производный класс содержит символьную строку, идентил рующую тип дома. Виртуальный метод Paint распечатывает эту стгиГ^’ вызывает базовый метод Paint. Объявление класса WoodFrameHouse 8 дится в качестве модели. Полное описание классов домов находится в л houses.h.
class WoodFrameHouse: public House (
private:
// идентификатор дома	i
String id;	1
public:
// конструктор.
WoodFrameHouse(void): House() { id ~ "деревянный”
’	I
// виртуальный метод, распечатывает id
//и вызывает Paint базового класса virtual void Faint(void) (
cout « "Покрасить *’ « id « ”	*
House::Paint();
) );
Чтобы описать понятие разнородного массива, определим массив contrac-torList (подрядчики), состоящий из пяти указателей на базовый класс House. Массив инициализируется посредством случайной выборки В массив заносится случайная выборка объектов, имеющих тип WoodFrameHouse (деревянный), StuccoHouse (оштукатуренный) или VinylSidedHouse (пластиковый). Например, такая:
Можно рассматривать этот массив указателей как список адресов домоц подлежащих покраске. Подрядчик распределяет работы по бри В нашем примере подрядчик дает каждой бригаде адрес дома и что они сообразят, как покрасить дом, когда увидят, какого он типа-
Программа 12.7. Разнородный массив___________________________________
Эта программа проходит массив contractorList и вызывает метод Paint для каждого объекта. Поскольку каждый объект адресуется указателем, динамическое связывание гарантирует, что будет выполнен именно тот paint, который нужен. Это соответствует выписыванию нарядов на малярные работы.
flinclude <iostream.h>
flinclude "random, h" // датчик случайных чисел
flinclude "houses, h" // иерархия покрасочных технологий
void main (void) (
// динамический список адресов объектов
House *contractorList(5];
RandomNumber rnd;
ll построить список пяти домов, подлежащих покраске for (int i=0; i<5; i++)
11 выбрать случайным образом дом типа 0, 1 или 2.
// создать объект и занести его адрес в contractorList switch(гnd.Random(3)) (
case 0: contractorList[i] = new WoodFrameHouse;
break;
case 1: contractorList[i] - new StuccoHouse;
break;
case 2: contractorList[i] - new VinylSidedHouse; break;
)
// покрасить дома с помощью метода Paint, поскольку он виртуальный, // используется динамическое связывание и вызывается правильный метод for (i=0; i<5; i++)
contractorList[ij->Paint();
)
/•
<Прогон программы 12.7>
Покрасить деревянный дом
Покрасить оштукатуренный дом
Покрасить пластиковый дом
Покрасить оштукатуренный дом
Покрасить деревянный дом
Разнородные связанные списки
Как и в разнородных массивах, каждый объект в разнородном списке Раэован общего для всех базового класса. Каждая базовая составляющая °ъекта содержит указатель на следующий объект в списке. Благодаря поли-арфизму указатель используется для выполнения методов производного объ-невзирая на его тип.
е Проиллюстрируем эти понятия на связанном списке геометрических объ-т°в> образованных от варианта класса Shape.
Спецификация класса NodeShape
ОБЪЯВЛЕНИИ йinclude "graphlib.h"
class NodeShape {
protected:	•
// координаты базовой точки, образец заполнения
И и указатель на следующий узел
float х, у;
int fillpat;
NodeShape *next;
public:
// конструктор
NodeShape(float h-0, float v-0, int fill“O);
// виртуальная функция рисования virtual void Draw(void) const;
// методы обработки списков
void InsertAfter(NodeShape *p);
NodeShape *DeleteAfter(void):
NodeShape *Next(void); };
ОПИСАНИЕ <
Координаты (к,у) задают базовую точку для производного объекта, который должен быть нарисован и заштрихован по образцу fillpat. Метбд Draw инициализирует образец заполнения в графической системе и указатель next, указывающий на следующий объект типа NodeShape в связанном списке. Методы InsertAfter и DeleteAfter поддерживают кольцевой список посредством включения или удаления узла, следующего' за текущим. Метод Next возвращает указатель на следующий узел.	*
Класс NodeShape находится в файле shapelst.h.
Реализация класса NodeShape
Реализация класса NodeShape сделана по образцу класса CNode (см. гл. 9). Так как предполагается кольцевой список, конструктор должен создать начальный узел, который указывает на самого себя.
// конструктор, задает начальные значения базовой точки, // образца заполнения и указателя next
NodeShape::NodeShape(float h, float v, int fill): x(h), y(v), fillpat(fill) {  next - this;
J
Образование связанных геометрических классов. Геометрические классы CircleFigure и RectangleFigure являются производными от класса NodeShape. В дополнение к методам базового класса они содержат метод Draw, перекры-яа1ощий виртуальный метод Draw базового класса. Методы Area и Perimeter ве включаются. Мы используем класс CircleFigure для иллюстрации понятий.
.. класс CircleFigure, образованный от класса NodeShape class CircleFigure; public NodeShape
1 protected:
11 радиус окружности float radius;
public:
// конструктор
CircleFigure(float h, float v, float r, int fill);
11 виртуальная функция рисования окружности virtual void Draw(void) const;
M
// конструктор, инициализирует базовый класс и радиус
CircleFigure::CircleFigure(float h, float v, float r, int fill):
NodeShape(h, v, fill), radius(r) {}
// задать образец заполнения посредством вызова базового метода Draw
// и нарисовать окружность
void CircleFigure:: Draw (void) const {
NodeShape:: Draw ();
Drawcircle(x, y, radius); )
Мы также включили в файл shapelst.h новый геометрический класс Right-Triangle, который описывает прямоугольный треугольник с помощью координат самой левой точки его гипотенузы, базы и высоты.
Чтобы сформировать связанный список, объявим заголовок, имеющий тип NodeShape и имя listHeader. Начиная с этого заголовка, будем динамически с°здавать узлы и с помощью InsertAfter включать их в список последовательно друг за другом. Например, следующая итерация создает четырехэле-^Нтный список, в котором чередуются объекты-окружности и объекты-тре-Уг°льники:
If заголовок списка и указатель для создания нового списка NodeShape listHeader, *р;
float х, у, radius, height;
И установить р на начало списка р “ «listHeader;
// включить 4 узла в список for (int i«0; i<4; i++) (
// координаты базовой точки
cout « "Введите х и у:
cin » х » у;
if (i % 2 == 0)	// если i четное, добавить окружность
{
cout « "Введите радиус окружности: cin » radius;
// включить объект с заполнением i в список p->InsertAfter(new Circle(х,у,radius, i));
)
else // если i нечетное, добавить прямоугольный треугольник (
cout « "Введите базу и высоту для прямоугольного треугольника: cin » base » height;
p->InsertAfter(new RightTriangle(x, у,radius,i));
)
// передвинуть p на только что созданный узел
р ш p->Next(); )
Динамическое связывание имеет принципиальное значение во время прохождения списка и визуального отображения содержащихся в нем объектов. В приведенном ниже фрагменте кода указатель р указывает либо на объект типа Circle, либо на объект типа RightTriangle. Поскольку Draw является виртуальной функцией, выполняется метод Draw того или иного производного класса.	г
р = listHeader.Next{);
while (р != «listHeader) {
p->Draw();
p = p->Next(); )
Теперь мы готовы поставить задачу создания и управления разнородными списками целиком.
Программа 12.8. Разнородные списки	______
Эта программа создает связанный список, состоящий из °®ъекТ0Б го Circle, Rectangle и RightTriangle. Файл figures содержит элементы списка в следующем формате:
<фигура> <координаты базовой точки> <параметры фигуры>
Фигура описывается буквой с (окружность), г (прямоугольник) (прямоугольный треугольник). Координатами базовой точки являет с чисел с плавающей точкой. Параметрами являются окружность к роны. Ниже приводится пример входных записей.
c 0.5 0.5 0.25	// окружность с центром (1/2, 1/2) и радиусом 1/4
r 1.0 0.25 .5 .5	// прямоугольник с базовой точкой (1, 1/4) // и сторонами 1/2, 1/2
t 2.0 0.75 .25 .5	// прямоугольный треугольник с базовой точкой (2, 3/4) // и сторонами 1/4, 1/2
Программа читает файл и формирует связанный список геометрических объектов. В процессе прохождения списка фигуры отображаются визуально.
#include <iostream.h>
flinclude <fstream.h>
flinclude <stdlib.h>'
flinclude "graphlib.h"
flinclude "shapelst.h"
void main(void) <
// listHeader — заголовок кольцевого списка форм NodeShape listHeader, *p, *nFig;
// фигуры: с (окружность), г (прямоугольник), t (прямоугольный треугольник) char figType;
// начальный образец заполнения — нет заполнения int pat “ 0;
float x, у, radius, length, width, tb, th;
// входной поток fin ifstream fin;
// открыть файл figures, содержащий описания фигур fin.open("figures", ios::in | ios::nocreate); if (’fin)
{
cerr « "Нельзя открыть файл figures" « endl;
exit(1);
}
11 установить p на начало списка P = blistHeader;
// прочитать файл до конца и построить связанный список фигур while (1 fin.eof())
{
// ввести тип фигуры и координаты базовой точки
fin » figType;
if (fin.eof())
break;
fin » x » y;
// построить конкретную фигуру
switch(figType)
case *c* :
// ввести радиус и включить окружность в список fin » radius;
nFig = new CircleFigure(x,у,radius,pat);
p->Inserthfter(nFig);
break;
case 'r':
// ввести длину и ширину и включить прямоугольник в список fin » length » width;
f
nFig  new RectangleFigure(x,у,length,width,pat);
p->InsertAfter(nFig);
break; case 't’;
// ввести базу и высоту и включить прямоугольный треугольник fin » th » th;
nFig = new RightTriangleFigure(x,у,tb,th,pat);
p->InsertAfter(nFig); break;
) // сменить образец заполнения, продвинуть указатель pat = (pat+1) % 12;
р - p->Next();
} // инициализировать графическую систему initGraphics();
// начиная с 1~й фигуры, пройти по списку и нарисовать каждую фигуру р “ listHeader.Next();
while (р != &listHeader) - <
p->Draw();
p  p->Next();
}
// организовать паузу для просмотра фигур и закрыть графическую систему ViewPause();
ShutdownGraphics();	,
) /*
<Прогон программы 12.8>
<см. график>
*/
Письменные упражнения
12,1	иераР'
а)	По образцу зоологической иерархии из раздела 12.1 постройте хическое дерево для следующих понятий:
Транспортное средство, автомобиль, дизель, газ, самолет, электромобиль, турбовинтовой, реактивный.
б)	Задайте базовые классы для классов "Электромобиль” и "Реактивный".
в)	Перечислите все классы, являющиеся одновременно и базовыми, и производными.
г)	Какие классы образованы от класса 'Транспортное средство”?
12.2 Пусть даны следующие объявления
class base
 (
private:
BaseFriv;
protected;
Base_Prot;
public:
BasePub
};
Class DERIVED: public BASE
{
private:
Derived_Friv;
protected:
Derived Prot;
public:
Derived_Pub );
а)	Представленные базовый и производный класс содержат закрытые, защищенные и открытые члены. Заполните приведенную ниже таблицу, показав тем самым права доступа клиента или объекта к этим членам. Крестик в строке говорит о том, что объект имеет доступ к члену класса. Например, представитель производного класса имеет доступ к защищенным членам базового класса, а клиент может обращаться к открытым членам базового класса.
*	Base_Priv	BaseProt	Base_Pub	Derived_Priv	DerivedProt	Derived_Pub
BASE						
.DERIVED						
КЛИЕНТ			[x]			
6)	Класс может быть образован с использованием закрытого наследования. В этом случае открытые и защищенные члены базового класса являются доступными для производного класса. Однако для клиента производного класса (программы, использующей данный производный класс) открытые члены базового класса считаются закрытыми и недоступны. Этот тип наследование иногда применяется, когда базовый класс служит просто связующим звеном для производных классов. Заполните таблицу
Для этого типа наследования.
BaseProt Base_Pub
Derived_Pfiv DerivedProt DenvedPub
12.3 Дана схема класса Base. Укажите ошибки в объявлениях проиав классов.
class Base {
public:
Base (int a, int b);
a)	class DerivedCLl: public Base (
private: int q;
public:
DerivedCLl (int z): q(z); ()
6)	class DerivedCL2: public Base {
private:
public:
// DerivedCL? не имеет конструктора
12.4 Даны следующие схемы базового и производного классов: class BaseCL ( protected: int datal;
int data2;
public:
BaseCL(int a, int b-0): datal(a), data2(b) <)
BaseCL(void): datal(0), data2(0) ()
};
class berivedCL { private: int data3;
public:
// Конструктор *1
DerivedCL(int a, int b, int c=0);
// Конструктор 12 DerivedCL(int a);
а)	Напишите конструктор #1 таким образом, чтобы а предназначалось производному классу, а Ъ и с — базовому.
б)	Напишите конструктор #2 таким образом, чтобы а предназначалось производному классу и при этом использовался конструктор базового класса, действующий по умолчанию.
в)	Подразумевая определение конструктора класса DerivedCL, покажите значения datal, data2 и dataS в следующих объектах:
DerivedCL obj 1 (1,2), obj2(3,4,5), obj3(8);
12.5	Следующая программа иллюстрирует порядок выполнения конструкторов и деструкторов в цепочке наследования. У каждого из трех классов Basel, Вазе2 и Derived есть конструктор и деструктор. Покажите, что выдаст эта программа на выходе.
^include <iostream.h>
class Basel
{
public:
Basel(void)
(
cout « "Вызван конструктор Basel." « endl;
)
-Basel(void)
{
cout « "Вызван деструктор Basel." « endl;
)
};
class Base2
{
public:
Base2(void)
{
cout « "Вызван конструктор Base2." « endl;
}
-Base2(void)
{
cout « "Вызван деструктор Base2." « endl;
} );
class Derived: public Basel, public Base2 {
public:
Derived(void): Basel(), Base2()
{
cout « "Вызван конструктор Derived." « endl;
}
-Derived(void)
{
cout « "Вызван деструктор Derived." « endl;
}
>;
void main (void) {
Derived objD;
Basel objBl;
{
Base2 objB2; }
12.6	Дана следующая цепочка наследования:
class Base
public:
void F(vo±d);
void G(int x);
class Derived: public Base {
public:
void F(void);
void G(float x);
};
void Derived::G(float x) <
...	j
Base::G(10); // использование оператора спецификации области действия
Рассмотрим объявление
Derived OBJ;
а)	Как клиент обращается к функции F базового класса?
б)	Как клиент обращается к функции F производного класса?
в)	Как компилятор будет реагировать на оператор OBJ.G(20)?
Замечание: Как было показано в разделе 12.5, нежелательно перекрывать невиртуальные функции базового класса.
12.7
В разделе 12.2 была построена цепочка наследования, состоящая и3 абстрактного базового класса Shape и класса Circle. Приведенная и0*® программа использует эти классы. Прочитайте программу и ответь на вопросы, приведенные далее.
♦include <iostream.h>
♦include "graphlib.h"
♦include "geometry.h"
void main(void)
I
Circle C;
C.SetPoint{1,2);
С.SetKadius(0.5);
cout « C.GetXO « ” " « C.GetYО « endl;
C.SetPoint(C.GetXO, 3);
cout « C.GetXO « " •* « C.GetY() « endl;
C.SetFill(11);
InitGraphics();
C.DrawQ ;
ViewPause();
ShutddwnGraphi.es () ;
а) Почему функцию GetX можно вызывать из производного класса? б) Почему к х можно обращаться из метода Draw?
в) Что будет на выходе этой программы?
г) Почему оператор
С.SetPoint(3,5);
является допустимым, а операторы
С.х - 3;
С.У - 5;
нет?
12.8	Что будет на выходе этой программы?
((include <iostream.h>
#include <string.h>
class Base
{
private;
char msg(30];
protected: int n;
public:
Base(char s[], int m*0): n(m) ( strcopy (msg, s);
}
void output(void) <
cout « n « endl « msg « endl;
) );
class Derivedl: public Base {
private: int n;
public:
Derivedl (int m«=l): Base ("Base", m-1), n(m) О
void output (void) (
cout « n « endl;
Base::output();
—"’-<4 1 };
class Derived2: public Derivedl ( private: int n;
public: Derived2 (int ma2): Derivedl (m-1), 'n(m) () void output (void) < cout « n « endl; Derivedl::output(); } ); void main (void) < Base В("Base Class", 1); Derived2 D;
B.output(); D.output(); }
12.9	Почему методы Area и Perimeter класса Shape являются чистыми виртуальными функциями?
12.10	Даны следующие объявления классов:	.
class Base	_
{ private: int х,у,-}; class Derived: public Base < private: int z; 1; Рассмотрим объявления Base В; Derived D;
а)	Допустимо ли присвоение
В = D;
Почему? Проиллюстрируйте свой ответ картинкой.
6)	Допустимо ли присвоение
D ~ В;
Почему? Проиллюстрируйте свой ответ картинкой. 12.11 Даны следующие классы:
class BaseCL {
protected:
int one;
public:
BaseCL(int a): one(a)
<}
virtual void Identify(void) (
cout « one « endl; )
);
class DerivedCL: public BaseCL <
protected:
int two;
public;
DerivedCL(int a, int b): BaseCL(a), two(b) (}
virtual void Identify(void) <
cout « one « " " « two « endl; )
и функции:
void Announce1(BaseCL x)
{
x.ldentifyO;
)
void Announce2(BaseCLK x) {
x.ldentifyO;
)
void АппоипсеЗ(BaseCL *x) (
x->identify();
>
Покажите, что будет на выходе следующего фрагмента кода:
BaseCL А(7), *р, *агг[3);
DerivedCL В(3,5), С(2,4);
Announcel(А);
Announce1(С);
Announce2(В);
Announces(&С);
Р « &С;
P->Identify();
for (int i=0; КЗ; i++)
if (i—1)
arr[i) » new BaseCL(7);
else
arrJi] - new DerivedCL(i, i+1); for (i=o; i<3; i++)
arr [i] -identify ();
12.12
12.13
12.14
12.15
Объясните, почему деструктор должен объявляться виртуад^ любом классе, который может служить базовым.	ь
Разработайте абстрактный базовый класс StackBase, в котором ляются стековые операции Push, Pop, Peek н StackEmpty.
класс должен содержать защищенную целочисленную переменн^°ВЬ1^ mElements н метод StackEmpty, возвращающий значение этой менной. Производный класс Stack должен увеличивать numEleCe^' с каждой операцией Push н уменьшать ее с каждой операцией3 Реализуйте производный класс Stack двумя разными способами-мощью массива и с помощью связанного списка.	' с ц°-
Выполните предыдущее упражнение для абстрактного класса Queue. Base, описывающего очередь. Этот класс должен содержать как нимум один метод, не являющийся чистой виртуальной функцией.
Что такое итератор? Почему итератор часто должен быть дружестве ным по отношению к классу, элементы которого он обрабатывает? Как понимать то, что итератор является абстракцией управления?
12.16	Разработайте класс Queue, образуя его от абстрактного класса Queue-Base (см. упр. 12.14) н используя объект типа SeqList. Образуйте из класса SeqListlterator класс Queueiterator и сделайте его дружественным по отношению к QueueBase. Для этого нужен лишь конструктор.
12.17	Напишите функцию
template <class Т>	•*
Т GetRear(Queue<T>& q);
которая возвращает последний в очереди элемент. Если очередь пуста, выдайте сообщение об ошибке и завершите программу. Используйте Queueiterator, разработанный в предыдущем упражнении.
12.18	Пусть имеется массив символьных строк. С помощью Arrayiterator просканируйте массив и замените все символы табуляции четырьмя пробелами.
12.19	Напишите функцию
void RemoveDuplicates(Array<int>& А);
которая удаляет из массива все дубликаты данных и соответствующим образом изменяет размер объекта. Например, если исходный масси А имеет 20 элементов
А = (1, 3, 5, 3, 2, 3, 1, 4, б, 3, 5, 4, 2, б, 7, 8, 1, 3, 9, 1},
то после вызова RemoveDuplicates
А= {1, 3, 5, 2, 4, 6, 7, 8, 9} {A.ListSize() = 9)
12.20	Напишите функцию
template <class Т>
Т Max(Iterator<T>& colllter);
которая ищет максимальное значение среди данных в той к°лЛ®рй>гор для которой существует итератор colllter. Предполагается, чт° оО
”>" определен для типа Т. Заметьте, что эта функция использует тот факт, что методы итератора являются виртуальными.
to21 Покажите, как можно использовать виртуальные функции для фор-
* ' мирования массива указателей на объекты Circle и Rectangle (разнородный массив), а также для прохождения массива и распечатки площади и периметра фигур.
Упражнения по программированию
12.1 Реализуйте цепочку наследования из письменного упражнения 12.1. Каждый конструктор класса должен содержать метод Identify, распечатывающий информацию о своем базовом классе и о себе самом. Напишите тестовую программу, в которой объявляются объекты каждого типа.
12.2 Из прямоугольных поверхностей можно построить короб.
Напишите классы Rectangle и Box, которые реализуют эту иерархию. Класс Rectangle имеет методы для вычисления площади и объема, причем в последнем случае всегда возвращается нулевой объем. Класс Box также имеет методы для вычисления площади и объема. Испытайте эти классы в главной процедуре, которая запрашивает тип фигуры и ее размеры. Определите объект каждого типа н распечатайте площадь и объем фигуры.
^•3 Из класса SeqList образуйте производный класс MidList с помощью следующего объявления:
template <class Т>
class MidList: public SeqList<T> {
public:
<Конструктор>
virtual void Insert(const Tfi elt); virtual void Delete(const Ts elt); );
Метод Insert включает elt в середину списка. Метод Delete уд элемент из середины списка. При этом подразумевается, что под^46-тель сам контролирует размер списка и гарантирует, что хотя би q элемент там есть. Реализуйте класс MidList и используйте его в дующей тестовой программе:	CJ1€
Ввести пять целых чисел и включить их в список с помощью Ills Распечатать список.
Удалить два числа из списка. Еще раз распечатать список и его размер 12.4 В этом упражнении требуется разработать иерархическую структур для задачи обработки данных. Класс Employee содержит элементы яш name (имя) и ssn (номер страховки), конструктор и операц^ PrintEmployeelnfo, которая распечатывает поля name и ssn. Эти данньд связаны с информацией о служащих с постоянными окладами. Проя3. водный класс SalaryEmployee содержит поле salary (месячный оклад) и операцию PrintEmployeelnfo, которая распечатывает данные как из базового, так и из производного класса. В дополнение к информации, имеющейся в базовом классе Employee, данные по временным сотруд. никам включают в себя почасовую ставку и количество отработанных в данном месяце часов. Эта информация хранится в классе Тешрр.щ. ployee, который состоит из элементов hourlypay и hoursworked и операции PrintEmployeelnfo.
Реализуйте эту иерархию и поместите в файл employee.h. Напишите главную процедуру, в которой объявляются объекты для ок лад никои и почасовиков и для
каждого объекта вызывается PrintEmployeelnfo.
Класс Employee ’ Данные name ssn
Операции Employee
Класс SalaryEmployee (Данные Л
salary
Операции SalaryEmployee
PrintEmployeelnfo^
Класс TempEmployee
Данные hourlypay hoursworked
Операции
TempEmployee
PrintEmployeelnfo^
12.5	Создайте новую реализацию класса Array как класса, образованно^^ класса List. При этом вы должны столкнуться с важной структурирования. Базовый класс List содержит ряд чистых B®^>^JaCce ных методов, которые обязаны перекрываться в производном Array. Некоторые из них, возможно, не имеют смысла для пр°изВ класса. Следующая таблица показывает проблемные методы и п вам при повторном определении.
ListSize
возвращает число элементов объекта Array
ListEmpty
Возвращает False, поскольку предполагается, что массив никогда не пуст ClearList
Ошибка! Операция не имеет смысла для массивов Find
Выполняет последовательный поиск элемента данных
Insert
Ошибка! Массив есть структура прямого доступа.
Операция вставки неопределена для массивов.
Delete
Ошибка! Операция удаления элемента неопределена для массивов.
Проверьте свою реализацию, запустив программу 12.5.
12.6	Используйте любую реализацию класса Stack, разработанную в письменном упражнении 12.13, для чтения символьной строки н распознавания палиндрома.
12.7	В этом упражнении используются классы Queue и Queueiterator, разработанные в письменных упражнениях 12.14 и 12.16. В тестовой программе вводите список целых чисел, пока не встретите О. Попутно вставляйте положительные числа в одну очередь, а отрицательные — в другую. Используйте объекты Queueiterator для сканирования и распечатки обеих очередей.
12.8	Напишите программу для тестирования функции GetRear, разработанной в письменном упражнении 12.17.
12.9	В главной программе введите несколько строк из файла и каждую из них запишите в массив символьных строк. Используйте Arrayiterator для сканирования массива, в ходе которого все символы табуляции заменяются четырьмя пробелами. Распечатайте модифицированные строки. Обратите внимание, что в этом упражнении используется результат письменного упражнения 12.18.
12.10	Проверьте функцию RemoveDuplicates, реализованную вами в письменном упражнении 12.19, на следующей главной программе.
void main (void) {
Array<int> A (20);
int data(] = {1, 3, 5, 3, 2, 3, 1, 4, 6, 3, 5, 4. 2, б, 7, 8, 1, 3, 9, 71; for (int i-0; i<20; i++)
A[i] = data(i];
RemoveDuplicates(A);
for (i=0; i<A.ListSize(); i++)
cout « A[i] « ” cout « endl;
)
/*
<Прогон программы>
135246789
*/
12 i
1	Определите объект Array<int>, содержащий целые числа 1..10, и объект SeqList<char>, содержащий буквы ’а’..’е’. С помощью функции
I
ж.
Мах из письменного упражнения 12.20 распечатайте максц^ значение для каждого из списков.
1	2.12 Добавьте методы Area и Perimeter в классы NodeShape, Circlep RectangleFigure и RightTriangleFigure (см. раздел 12.7). в б классе NodeShape определите методы для возврата нуля. По об30*05* программы 12.8 разработайте программу, которая создает разноги?^' список производных объектов. Программа должна проходить по списку и распечатывать площадь и периметр каждой фигуры. b^To,<V ром проходе должны быть нарисованы сами фигуры.	Вт®
Более сложные нелинейные структуры
13.1.	Бинарные деревья, представляемые массивами
13.2.	Пирамиды
13.3.	Реализация класса Heap
13.4.	Приоритетные очереди
13.5.	AVL-деревья
13.6.	Класс AVLTree
13.7.	Итераторы деревьев
13.8.	Графы
13.9.	Класс Graph
Письменные упражнения
Упражнения по программированию
В этой главе мы продолжим изучение бинарных деревьев и познаком с новыми нелинейными структурами. В гл. 11 деревья представлялись в динамически порождаемых узлов. В настоящей же главе описываются которые моделируют массивы в виде законченных бинарных деревьев Ч используются в приложениях, связанных с пирамидальными структур турнирной сортировкой. Мы подробно остановимся на пирамидах и рассмо^4 ’ их применение в пирамидальной сортировке и очередях приоритетов.
Деревья бинарного поиска реализуют списки и обеспечивают среднее в поиска порядка O(log2n). Однако на несбалансированных деревьях эффектность поисковых алгоритмов снижается. Мы рассмотрим новый тнп дерев^6 называемых сбалансированными или AVL-деревьями1, в которых подпет?6*1 ваются хорошие поисковые характеристики бинарного дерева.	₽*а'
В гл. 12 были представлены итераторы, с помощью которых реализовав классы SeqListlterator и Arrayiterator. В данной главе концепция итератоп распространяется на деревья и графы. Это мощное средство сканирования п зволяет осуществлять прохождение нелинейных структур с помощью просты, методов, применяемых обычно к линейным спискам. Здесь мы разрабатывав симметричный итератор дерева, который расширяет возможности деревьев Это' используется для реализации алгоритма сортировки с помощью дерева.
Обобщением иерархической структуры является граф, который состоит из вершин и ребер, соединяющих вершины. Графы — важный раздел дискретной математики. Они играют основную роль р целом ряде классических алгоритмов, широко применяющихся в исследовании операций. Эта глава завершается изложением основ теории графов и разработкой класса Graph, который будет использоваться во многих приложениях.
13.1. Бинарные деревья, представляемые массивами
В гл. 11 для построения бинарных деревьев мы используем узлы дерева-Каждый узел имеет поле данных и поля указателей на правое и левое поддеревья данного узла. Пустое дерево представляется нулевым указателем. Вставки и удаления производятся путем динамического размещения узлов и ЦРИ' своения значений полям указателей. Это представление используется для целой группы деревьев от вырожденных до законченных. В данном ₽азЛ!л вводится последовательное представление деревьев с помощью массивов. J- Р этом данные хранятся в элементах массива, а узлы указываются индекса * Мы выявим очень близкое родство между массивом и законченным бинарны деревом — взаимосвязь, используемую в пирамидах и очередях приорите
Вспомним из гл. 11, что законченное бинарное дерево глубины и со,це^леВа все возможные узлы на уровнях до п-1, а узлы уровня п располагаются у направо подряд (без дыр). Массив А есть последовательный список, эЛ которого могут представлять узлы законченного бинарного дерева с к А[0]; потомками первого уровня А[1] и А[2]; потомками второго уровня aJS А[4], А[5] н А[6] н т.д. Корневой узел имеет индекс 0, а всем остальным^У^. индексы назначаются в порядке, определяемом поперечным (уровень з нем) методом прохождения. На рис. 13.1 показано законченное бинар рево для массива А из десяти элементов.
int А[10] - (5, 1, 3, 9, 6, 2, 4, 7, 0, 8)
. .......... npii*. ne?et
1 По фамилиям их изобретателей — Г. М. Адельсона-Вельского и Е. М. Ландиса [1J-
Рис 13.1. Законченное бинарное дерево для 10-элементного массива А
ШЕзПЕПа LuuLims] mueJLAJLJ ujcsjmcza Г~4~1 Пб~1 r~io~l СЯ ШЕ] л 'E'
Эквивалентное представление в виде массива
Несмотря на то, что массивы обеспечивают естественное представление деревьев, возникает проблема, связанная с отсутствующими узлами, которым должны соответствовать неиспользуемые элементы массива. В следующем примере массив имеет четыре неиспользуемых элемента, т.е. треть занимаемого деревом пространства. Вырожденное дерево, имеющее только правые поддеревья, дает в этом смысле еще худший результат.
Преимущества представляемых массивами деревьев обнаруживаются тогда, когда требуется прямой доступ к узлам. Индексы, идентифицирующие сыновей и родителя данного узла, вычисляются просто. В таблице 13.1 представлено Дерево, изображенное на рис. 13.1. Здесь для каждого уровня указаны узлы, в также их родители и сыновья.
Для каждого узла A[i] в N-элементном массиве индекс его сыновей вычисляется по формулам:
Индекс левого сына = 2*1 (неопределен при 2*i + 1 > N)
Индекс правого сына «• 2*1 + 2 (неопределен при 2*1 + 2 > N)
Таблица 13.1
2р°вень	Родитель	Значение	Левый сын	Правый сын
	0	А[0] = S	1	2
I	1	А[1] = 1	2	4
— -	2	А[2] = 3	5	6
С	' ' 1  I	3	А[3] = 9	7	8
—	4	А[4] = 6	9	1O=NULL
	5	А(5] = 2	11=NULL	12=NULL
Т"——	6	А[6] = 4	13=NULL	14=NULL
	7	А[7] = 7	—	—
	8	А(8] = б	—	—
			9	А(9] = 8	—	—
Поднимаясь от сыновей к родителю, мы замечаем, что родителе А[3] и А[4] является А[1], родителем А[5] и А[6] — А[2] и т.д\у^ формула для вычисления родителя узла A[i] следующая:	д’
Индекс родителя = (i-l)/2 (неопределен при i-0)
Пример 13.1
Во время прохождения последовательно представленного можно идти вниз к сыновьям или вверх к родителю. Ниже прив^6^4 примеры путей для следующего дерева:
р
I
Jdl
Й1
$ о*
1 е р
1.	Начиная с корня, выбрать путь, проходящий через меньтпих сыяо-вей.
Путь: А[0] = 7, А[2] = 9, А[6] = 3
2.	Начиная с корня, выбрать путь, проходящий через левых сыновей.
Путь: А[0] - 7, А[1] - 10, А[Ь] = 12, А[7] - 3
3.	Начиная с А[10], выбрать путь, проходящий через родителей.
Путь: А[10] = 2, А[4] - 2, А[1] = 10, А[0] = 7
Приложение: турнирная сортировка
Бинарные деревья находят важное применение в качестве деревьев принятия решения, в которых каждый узел представляет ситуацию, имеющую два возможных исхода. В частности, для представления спортивного турлиР8, проводимого по схеме с выбываниями. Каждый нелистовой узел соответствует победителю встречи между двумя троками. Листовые узлы дают стартов состав участников и распределение их по парам. Например, победителе теннисного турнира является Дэвид, выигравший финальную встречу Доном. Оба спортсмена вышли в финал, выиграв предварительные ма Дон победил Алана, а Дэвид — Мэнни. Все игры турнира и их резуль могут быть записаны в виде дерева.
Дон
—- Дон
Алан --------
Дэвид
Мэн-ни	Победитель
Дэвид
Дэвид -------
турнире с выбывя илями победитель определяется очень скоро. Например» Для четырех игроков понадобится всего три матча, а для 24 = 16 участ-яиков — 24 - 1 = 15 встреч.
И ТУРНИР выявляет победителя, но со вторым лучшим игроком пока не все -0< Поскольку Дон проиграл финал победителю турнира, он может и не казаться вторым лучшим игроком. Нам нужно дать шанс Мэнни, так как тот °грал матч первого круга с, быть может, единственным игроком, способным ого победить. Чтобы выявить второго лучшего игрока, нужно исключить Дэ-е^.а и реорганизовать турнирное дерево, устроив матч между Доном и Мэнни.
Как только определится победитель этого матча, мы сможем правильно распределить места.
Выиграл Мэнни: Места Дэвид Мэнни Дон Алан
Выиграл Дон:	Места Дэвид Дон Мэнни Алан
Турнирное дерево может использоваться для сортировки списка из N элементов. Рассмотрим эффективный алгоритм, использующий дерево, представленное в виде массива. Пусть имеется последовательно представленное дерево, содержащее N элементов — листовых узлов в нижнем ряду. Эти элементы запоминаются на уровне к, где 2к > N. Предположим, что список сортируется по возрастанию. Мы сравниваем каждую пару элементов и запоминаем меньший из них (победителя) в родительском узле. Процесс продолжается до тех пор, пока наименьший элемент (победитель турнира) не окажется в корневом узле. Например, приведенное ниже дерево задает следующее начальное состояние массива из N -= 8 целых чисел. Элементы запоминаются на уровне 3, где 28 = 8.
МВ] = {35, 25, 50, 20, 15, 45, 10, 40}
в Со второго уровня начинаются "игры" — в родительские узлы помещаются ^еныпие значения в парах. Например, ’’игру” между элементами Тгее[7] ПОпее№] выигрывает меньший из них, и значение 25 записывается в Тгее[3]. 3Уль°бвые сравнения проводятся также на втором и первом уровнях. В рейд тате последнего сравнения наименьший элемент попадает в корень дерева Уровне О.
Начальные сравнения
40
TreeI8)
TreellO] Тгее[11]
50
Тгее[9]
35
TreeI12) Тгее[13]
в корневом узле, он уд^
-2i pjj следую^
Тгее[7]
Как только наименьший элемент оказывается
ется со своего старого места на дереве и копируется в массив. В первой' в А[0] записывается 10, а затем дерево обновляется для поиска наименьшего элемента. В турнирной модели некоторые матчи должны^610 сыграны повторно. Поскольку число 10 изначально было в AJ13], проигт^ ший в первом круге А[14] = 40 должен снова участвовать в турнире, /и Г копируется в свой родительский узел А] 6], а затем снова проводятся матч в индексе 6 (15 побеждает 40) и в индексе 2 (15 побеждает 20). В результате 15 попадает в корень и становится вторым наименьшим элементом списка Корень копируется в Ар], и процесс продолжается.
Процесс продолжается до тех пор, пока все листья не будут удалены. В нашем примере последний (наибольший) узел играет серию матчей, в которых побеждает всех по умолчанию. После копирования числа 50 в А[7] мы и° лучаем отсортированный список.
Вычислительная эффективность. Эффективность турнирной с°Рт1^1яВлеЯ*!Я ставляет O(n logan). В массиве, содержащем п — 2к элементов, ДлЯ ® М| когД* наименьшего элемента требуется n—1 сравнений. Это становится яснь*
У замечаем, что половина участников выбывает после каждого круга по мере ^сдвижения к корню. Общее число матчей равно
2k-i + 2k-2 -+I ... + 21 + 1 = п-1
Дерево обновляется, и оставшиеся п-1 элементов обрабатываются посредст-ом к-1 сравнений вдоль пути, проходящего через родительские узлы. Общее ейсло сравнений равно
(П-1) + (k—1)*(п—1) = (п-1) + (n-l)*(log2n-l) - (n-1) log2n
Хотя количество сравнений в турнирной сортировке составляет О(п logsn), использование пустот значительно менее эффективно. Дереву требуется 2 * п-1 узлов, чтобы вместить k—1 кругов соревнования.
Алгоритм Tournaments or t. Для реализации турнирной сортировки определим класс DataNode и создадим представленное массивом дерево из объектов этого типа. Членами класса являются элемент данных, его место в нижнем ряду дерева и флажок, показывающий, участвует ли еще этот элемент в турнире- Для сравнения узлов используется перегруженный оператор <=".
template <class Т> class DataNode
{
public:
// элемент данных, индекс в массиве, логический флажок
Т data;
int index;
int active;
friend int operator (const DataNode<T> ix, const DataNode<T> &y);
);
Сортировка реализуется с помощью функции TournamentSort и утилиты UpdateTree, которая производит сравнения вдоль пути предков. Полный листинг функций и переменных, обеспечивающих турнирную сортировку, находится в файле toursortJi.
// сформировать последовательное дерево, скопировать туда элементы массива; // отсортировать элементы и скопировать их обратно в массив
template <class Т>
void Tournamentsort (Т а[], int n>
DataNode<T> *tree; // корень дерева
DataNode<T> item;
Мининальная степень двойки, большая или равная п nt bottomRowSize;
!1 число узлов в полном дереве, нижний ряд которого
имеет bottomRowSize узлов int treesize;
in /Ильный индекс нижнего ряда узлов loadindex;
Rt j;
// «_
Ь Тфеделить требуемый размер памяти для нижнего ряда узлов tomRowSize  PowerOfTwo(n);
П вычислить размер дерева и динамически создать его узлы treesize • 2 * bottoniRowSize - 1;
tree - new. DataNode<T>(treesize];
// скопировать массив в дерево объектов типа DataNode j - 0;
for (i»loadindex; i<treesize; i++)
item.index  i; if (j < n) (
item, active - 1; item.data « a[j++];
else
item.active - 0; tree[i] « item;
// выполнить начальные сравнения для определения наименьшего элемента i - loadindex;
while (i > 0)	X
< \ j - i;
while (j < 2*i);	// обработать пары соревнующихся
{
И проведение матча, сравнить treelj] q.ero соперником tree[j+lj fl скопировать победителя в родительский узел if (!treelj+1].active 11 tree[j] < treelj+11)
tree!(j-l)/2) » treelj]; else
tree[(j-1)/2] - treelj+1]; j +- 2;	// перейти к следующей nape
// обработать оставшиеся n-l элементов, скопировать победителя //из корня в массив, сделать победителя неактивным, обновить // дерево, разрешив сопернику победителя снова войти в турнир for (i-0; i<n-l; i++) <
a[i] - tree[0].data;
tree{tree(0).index].active = 0;
UpdateTree(tree, tree[0].index);
)
// скопировать наибольшее значение в массив
a [n-l] ** tree[0] .data;
В функцию UpdateTree передается индекс i, указывающий исХо^н<^уда. ложение наименьшего текущего элемента в нижнем ряду дерева. Это У ляемый узел (становится неактивным). Значению, которое "проиграло верительный раунд последнему победителю (наименьшему значению)» Р шается снова войти в турнир.
// параметр i есть начальный индекс текущего наименьшего элемента // в списке (победителя турнира) template <class т> void UpdateTree(DataNode<T> *tree, int i)
I
int j;
jf определить соперника победителя, позволить ему продолжить
// турнир, копируя его в родительский узел.
if (i % 2 =» 0)
tree I (i-1)/2] = tree[i-1]; // соперник - левый узел eise
tree [(i-1)/2] = tree[i+ll; // соперник — правый узел
И переиграть те матчи, в которых принимал участие
// только что исключенный из турнира игрок
i - (i-1)/2;
utile (i > 0)
f // соперником является правый или левый узел?
if (i % 2 =• О)
i - i-1;
else
j - 1+1;
// проверить, является ли соперник активным
if ('tree[i].active I] !tree[j].active)
if (treelil.active)
tree[(i-1)/2] - tree[i);
else
tree! (i-1)/21 ~tree[j];
11 устроить соревнование.
Ц победителя скопировать в родительский узел else
if (tree[i) < treeljl)
treeI(i-11/2] = tree[ij;
else
tree[ (i-1)/2] « treeljl;
// перейти к следующему кругу соревнования (родительский уровень) i = (i-1)/2;
I
U Турнир с новым соперником закончен.
// очередное наименьшее значение находится в корневом узле
13.2.	Пирамиды
Представляемые массивами деревья находят применение в имеющих боль» юое значение приложениях с пирамидами (heaps), являющимися законченными бинарными деревьями, имеющими упорядочение узлов по уровням. В максимальной пирамиде (maximum heap) родительский узел больше или равен каждОМу из своих сыновей. В минимальной пирамиде (minimum heap) роди* ельский узел меньше или равен каждому из своих сыновей. Эти ситуации 3°бражены на рис. 13.2. В максимальной пирамиде корень содержит наиболь-йи элемент, а в минимальной — наименьший. В этой книге рассматриваются йимальные пирамиды.
Пирамида как список
йй^иРамида является списком, который хранит некоторый набор данных в Пиарного дерева. Пирамидальное упорядочение предполагает, что каж-М УЗел пиРамиДы содержит значение, которое меньше или равно значению aaaijOro из его сыновей. При таком упорядочении корень содержит наименьшее ба^йие данных. Как абстрактная списковая структура пирамида допускает дойне и удаление элементов. Процесс включения не подразумевает, что новый
элемент занимает конкретное место, а лишь требует, чтобы поддержи пирамидальное упорядочение. Однако при удалении из списка выбрась наименьший элемент (корень). Пирамида используется в тех приложена клиенту требуется прямой доступ к минимальному элементу. Как спи рамида не имеет операции поиска и осуществляет прямой доступ к мин °К ному элементу в режиме "только чтение". Все алгоритмы обработки и сами должны обновлять дерево и поддерживать пирамидальное упорядо^^
(А) Минимальная пирамида (9 узлов)
(С) Максимальная пирамида (9 узлов)
(D) Максимальная пирамида (4 узла)
Рис, 13.2. Максимальные и минимальные пирамиды
Пирамида является очень эффективной структурой управления списками, которая пользуется преимуществами полного бинарного дерева. При каждой операции включения или удаления пирамида восстанавливает свое упорядочение посредством сканирования только коротких путей от корня вниз до конца дерева. Важными приложениями пирамид являются очереди приоритетов и сортировка элементов списка. Вместо того чтобы использовать более медленные алгоритмы сортировки, можно включить элементы списка в пирамиду и отсортировать их, постоянно удаляя корневой узел. Это дает чрезвычайно быстрый алгоритм сортировки.
Обсудим внутреннюю организацию пирамиды в нашем классе Heap- Алгоритмы включения и исключения элементов представляются в реализации методов Insert и Delete. Пример 13.2 исследует пирамиды и иллюстриру некоторые операции над ними.
Пример 13.2
1.	Создание пирамиды. Массив имеет соотвествующее представлен^^, виде дерева. В общем случае это дерево не является пира Пирамида создается переупорядочением элементов массива.
Исходный список: 4010 30	Пирамида: 10 40 30
2.	Вставка элемента. Новый элемент добавляется в конец списка, а затем дерево реорганизуется с целью восстановления пирамидальной структуры. Например, для добавления в список числа 15 производятся следующие действия:
Записать 15 в А[3] Переупорядочить дерево
3.	Удаление элемента. Удаляется всегда корень дерева (А[0]). Освободившееся место занимает последний элемент списка. Дерево реорганизуется с целью восстановления пирамидальной структуры. Например, для исключения числа 10 производятся следующие действия:
Удалить 10 из А[10]	Переместить 40 из А[3]	Восстановить дерево
Класс Heap
Как и любой линейный или нелинейный список, класс пирамид имеет операции включения и исключения элементов, а также операции, которые возвращают информацию о состоянии объекта, например, размер списка.
Спецификация класса Heap
ОБЪЯВЛЕНИЕ
include <iostream.h>
include <stdlib.h>
template <class T>
ciass Heap
Private:
// hlist указывает на массив, который может быть динамически создан
И конструктором (inArray == 0) или передан как параметр (inArray == 1)
Т *hlist;
int inArray;
Максимальный и текущий размеры пирамиды
1пз- maxheapsize;
1Rt heapsize; // определяет конец списка
1 ^.Функция вывода сообщений об ошибке oid error (char errmsg[]);
^.Утилиты восстановления пирамидальной структуры
°id FiiterDown(int i);
1(3 FUterUp(int i);
.A 425
public:
// конструкторы и деструктор
Heap (int maxsize);
Heap (T arr[], int n);
Heap (const Heap<T>6 H);
~Heap(void);
// создать пустую пирамиду
// преобразовать arr в пирамиду
// конструктор копий
// деструктор
// перегруженные операторы: *«•, ”[)’*, "Т*" Неар<Т> operator- (const Heap<T>s ths);
const ТЬ operator!] (int i);
// методы обработки списков int ListSize(void) const; int ListEmpty(void) const; int ListFull(void) const; void Insert(const T& item); T Delete(void);
void ClearList(void);
ОПИСАНИЕ
Первый конструктор принимает параметр size и использует его для динамического выделения памяти под массив. В исходном состоянии пирамида пуста, и новые элементы включаются в нее с помощью метода Insert. Деструктор, конструктор копирования и оператор присваивания поддерживают использование динамической памяти. Второй конструктор принимает в качестве параметра массив и преобразует его в пирамиду. Таким образом, клиент может навязать пирамидальную структуру любому существующему массиву и воспользоваться свойствами пирамиды.
Перегруженный оператор индекса '*[]" позволяет клиенту обращаться к объекту типа пирамиды как к массиву. Поскольку этот оператор возвращает ссылку на константу, доступ осуществляется лишь в режиме "только чтение".
Методы ListEmpty, ListSize и ListFull возвращают информацию о текущем состоянии пирамиды.
Метод Delete всегда исключает из пирамиды первый (наименьший) элемент. Метод Insert включает элемент в список и поддерживает пирамидальное упорядочение.
ПРИМЕР
Heap<rnt> H(4);		// 4-элементная пирамида целых чисел /f 4-элементный массив // преобразовать массив А в пирамиду К
int A[] = {15, Heap<int> К(A,	10, 40, 30}; 4);	
H.Insert(85); «.Insert(40); cout « H.Delete]);		// вставить 65 в пирамиду И // вставить 40 в пирамиду И // напечатать 40 — наименьший элемент з
// распечатать массив, представляющий пирамиду А for (int i=0; i<4; i++)
cout « K[i] « ” ";	// напечатать 10 15 40 30
K[0) ~ 99;
// недопустимый оператор
Программа 13.1. Иллюстрация класса Heap
Эта программа начинается с инициализации массива А, а затем преобразует его в пирамиду.
А: 50, 20, 60, 65, 15, 25, 10, 30, 4, 45
Элементы исключаются из пирамиды и распечатываются до тех пор, пока пирамида не опустеет. Поскольку пирамида реорганизуется после каждого исключения, элементы распечатываются по возрастанию.
finclude <iostream.h>
I include "heap.h"
// распечатать массив, состоящий из'п элементов template <class Т>
void PrintList (Т А[), int n) {
for (int i«=0; i<n; i++) cout « A(i] « ” cout « endl;
I
void main (void) (
// исходный массив
int A(10) - (50, 20, 60, 65, 15, 25, 10, 30, 4, 45}
cout « "Исходный массив:" « endl;
PrintList (A, 10);
fl преобразование А в пирамиду
h«ap<int> H(A,10);
ll распечатать новую версию массива А
cout « "Пирамида:" « endl;
PrintList(А, 10);
cout « "удаление элементов из пирамиды:" « endl;
' непрерывно извлекать наименьшее значение
whiie (1н.ListEmpty ())
cout « Н.Delete() « " ";
j Cout « endl;
/*
пРогон программы 13.1>
5д*°Яный массив:
П^20 60 65 15 25 10 30 4 45
4	15	10	20 45	25	60	30	65 50
Удаление элементов из пирамиды: 4	10	15	20 25	30	45	50	60 65
13.3.	Реализация класса Heap
Здесь мы подробно обсудим операции вставки и удаления для а также методы FilterUp и FilterDown. Эти вспомогательные методы за реорганизацию пирамиды при ее создании или изменении.
ПиРаийд
Операция включения элемента в пирамиду. Вначале элемент добавляе в конец списка. Однако при этом может нарушиться условие пирамидал” ности. Если новый элемент имеет значение меньшее, чем у его родителя узлы меняются местами. Возможные ситуации представлены на следующее рисунке.
Новый элемент является правым сыном меньшим, чем родительским узел
Новый элемент является левым сыном меньшим, чем родительский узел
Этот обмен восстанавливает условие пирамидальности для данного родительского узла, однако может нарушить условие пирамидальности для высших уровней дерева. Теперь мы должны рассмотреть нового родителя как сына и проверить условие пирамидальности для более старшего родителя-Если новый элемент меньше, следует переместить его выше. Таким образо новый элемент поднимается вверх по дереву вдоль пути, проходящего чер^ его предков. Рассмотрим следующий пример для 9-элементной пирамиды
Н.Insert(в);	// вставить элемент 8 в пирамиду
Вставить 8 в А[9]. Вставить новый элемент в конец пиРамиДЬ'еНТС5 позиция определяется индексом heapsize, хранящим текущее число эле в пирамиде.	=|
Отправить значение 8 по пути предков. Сравнить 8 с родителем 20. Поскольку сын меньше своего родителя, поменять их значения местами (А). Продолжить движение по пути предков. Теперь элемент 8 меньше своего ро-ителя Н[1]=10 и поэтому меняется с ним местами. Процесс завершается, так следующий родитель удовлетворяет условию пирами дальности.
Процесс включения элементов сканирует путь предков и завершается, встретив ’’маленького” (меньше чем новый элемент) родителя или достигнув корневого узла. Так как у корневого узла нет родителя, новое значение помещается в корень.
Чтобы поместить узел в правильную позицию, операция вставки использует метод FilterUp.
// утилита для восстановления пирамиды, начиная с индекса i, // подниматься вверх по дереву, переходя от предка к предку.
// менять элементы местами, если сын меньше родителя
template <class Т>
void Неар<Т>::FilterUp (int i)
(
int currentpos, parentpos;
T target;
// currentpos — индекс текущей позиции на пути предков.
// target — вставляемое значение, для которого выбирается
// правильная позиция в пирамиде
currentpos = i;
parentpos  (i-1)/2;
target = hlist[i];
Н подниматься к корню по пути родителей
while (currentpos !- 0)
// если родитель <e target, то все в порядке.
if (hlist[parentpos] <» target)
break;
else
// поменять местами родителя с сыном и обновить индексы
U для проверки следующего родителя
// переместить данные из родительской позиции в текущую.
// назначить родительскую позицию текущей.
// проверить следующего родителя
blist[currentpos] = hlist[parentpos];
currentpos и parentpos;
j Parentpos » Icurrentpos-1)/2;
)
^.правильная позиция найдена, поместить туда target
} lst(currentpos] « target;
-------------------------------------------------------------------------
Открытый метод Insert проверяет сначала заполненность пирами затем начинает операцию включения. После записи элемента в конёп ***’ ь миды вызывается FilterUp для ее реорганизации.	4 Сй1Ч.
// вставить в пирамиду новый элемент и восстановить ее структуру template <class Т>
void Неар<Т>::Insert(const T& item) {
// проверить, заполнена ли пирамида и выйти, если да
if (heapsize == maxheapsize)	—
error ("Пирамида заполнена");
// записать элемент в конец пирамиды и увеличить heapsize.
// вызвать FilterUp для восстановления пирамидального упорядочения
hlist[heapsize1 = item;
FilterUp(heapsize);
heapsize++;
Удаление из пирамиды. Данные удаляются всегда из корня дерева. После такого удаления корень остается ничем не занятым и сначала заполняется последним элементом пирамиды. Однако такая замена может нарушить условие пирамидальности. Поэтому требуется пробежать по всем Меньшим потомкам и найти подходящее место для только что помещенного в корень элемента. Если он больше любого своего сына, мы должны поменять местами этот элемент с его наименьшим сыном. Движение по пути меньших сыновей продолжается до тех пор, пока элемент не займет правильную позицию в качестве родителя или пока не будет достигнут конец списка. В последнем случае элемент помещается в листовой узел. Например, в приведенной ниже пирамиде удаляется корневой узел 5.
Удалить корневой узел 5 и заменить его последним узлом 22. Последний элемент пирамиды копируется в корень. Новый корень может не удовлетворять условию пирамидальности, и требуется отправиться йо пути, проходящему через меньших сыновей, чтобы подыскать для нового корня правильную позицию.
Передвигать число 22 от корня вниз по пути, проходящему $ меньших сыновей. Сравнить корень 22 с его сыновьями. Наимень из двух сын Н[1] меньше, чем 22, поэтому следует поменять их еТСя (А). Находясь теперь на первом уровне, новый родитель сравни со своими сыновьями Н[3] и Н[4]. Наименьший из них имеет зн 11 и поэтому должен поменяться местами со своим родите Теперь дерево удовлетворяет условию пирамидальности.
10
10
11
%
Метод
22Т	Tsoi
(§)	(§) Йх (§) (Sj (§)
(25) (g)
(A)	(B)
Delete. Чтобы поместить узел в правильную позицию, операция использует метод FilterDown. Эта функция получает в качестве
Параметра индекс 1, с которого начинается сканирование. При удалении метод FilterDown вызывается с параметром О, так как замещающее значение копируется из последнего элемента пирамиды в ее корень. Метод FilterDown используется также конструктором для построения пирамиды.
// утилита для восстановления пирамиды, начиная с индекса i,
// менять местами родителя и сына так, чтобы поддерево,
// начинающееся в узле i, было пирамидой
template <class T>
void Неар<Т>:: FilterDown (int i) f
int currentpos, childpos;
T target;
// начать с узла 1 и присвоить ело значение переменной target currentpos “ i;
target = hlist[i];
// вычислить индекс левого сына и начать движение вниз по пути, // проходящему через меньших сыновей до конца списка childpos = 2 * i + 1;
while (childpos < heapsize) // пока не конец списка I
И индекс правого сына равен childpos+1. присвоить переменной
// childpos индекс наименьшего из двух сыновей
if ((childpos+1 < heapsize) &&
(hlist[chxldpos+1] <= hlist[childpos])) childpos = childpos +1;
Ч если родитель меньше сына, пирамида в порядке, выход
if (target <= hlist[childposI) break;
else {
I i переместить значение меньшего сына в родительский узел.
И теперь позиция меньшего сына не занята hlist[currentpos] - hlist[childpos];
Ч обновить индексы и продолжить сканирование currentpos = childpos;
I childpos = 2 * currentpos +1;
i \fti
hli °Местить target в только что ставшую незанятой позицию । БЧcurrentpos] = target;
метод Delete копирует значение из корневого узла во временную а затем замещает корень последним элементом пирамиды. После
А
Яе^Рьхтый ^ную,
этого heapsize уменьшается на единицу. FilterDown реорганизует Значение, сохраненное во временной переменной, возвращается кд
// возвратить значение корневого элемента и обновить пирамиду.
// попытка удаления элемента из пустой пирамиды влечет за собой
// выдачу сообщения об ошибке и прекращение программы
template cclass Т>
Т Неар<Т>::Delete(void)
<13
Т tempitem;
И проверить, пуста ли пирамида if (heapsize “== 0) error ("Пирамида пуста");
// копировать корень в tempi tern, заменить корень последним элементом // пирамиды и произвести декремент переменной heapsize tempitem - hlist[0];
hlistfO] » hlist(heapsize-1]; heapsize—;
// вызвать FilterDown для установки нового значения корня FilterDown(О);
// возвратить исходное значение корня return tempitem;
Преобразование массива в пирамиду. Один из конструкторов класса Heap использует существующий массив в качестве входного списка и преобразует его в пирамиду. Ко всем иелистовым узлам применяется метод FilterDown. Индекс последнего элемента пирамиды равен п-1. Индекс его родителя равен
и определяет последний нелистовой узел пирамиды. Этот индекс является начальным для преобразования массива. Если применить метод FilterDown ко всем индексам от currentpos до О, то можно гарантировать, что каждой родительский узел будет удовлетворять условию пирамидальности. В качестве примера рассмотрим целочисленный массив
int А[10] «= {9, 12,- 17, 30, 50, 20, 60, 65, 4, 19}
Индексы листьев: 5, 6, ..., 9
Индексы родительских узлов: 4, 3, ..., 0
Исходный список
Приведенные ниже рисунки иллюстрируют процесс преобразовани^вО миды. Для всех вызовов FilterDown соответствующее поддерево вЫД рисунках треугольником.
filterDou>n(4). Фортель H[4] — 50 больше своего сына H[9] = 19 и поэтому должен поменяться с ним местами (А).
Поставить на место число 50 с помощью FilterDown(4)
(А)
FilterDownf 3 ). Родитель Н[3] * 30 больше своего сына Н[8] = 19 и поэтому должен поменяться с ним местами (В).
Поставить на место число 30 с помощью FilterDown(3) (В)
На уровне 2 родитель Н[2] = 17 уже удовлетворяет условию пирамидальности, поэтому вызов FilterDown(2) не производит никаких перестановок.
FilterDownf 1). Родитель Н[1] = 12 больше своего сына Н[3] — 19 и поэтому должен поменяться с ним местами (С).
FilterDown(O). Процесс прекращается в корневом узле. Родитель Н[0] = 9 должен поменяться местами со своим сыном Н[1]. Результирующее дерево является пирамидой.
КОНСТРУКТОВ
// конструктор преобразует исходный массив в пирамиду.
// этот массив и его размер передаются в качестве параметров template <class Т>
Неар<Т>::Неар(Т arr[], int п) {
int j, currentpos;
// n <- 0 является недопустимым размером массива
if (п <- О)
error ("Неправильная размерность* массива");
/ / использовать п для установки размера пирамида и максимального i
// копировать массив arr в список пирамиды
maxheapsize - п;
heapsize - п;
hlist  arr;
// присвоить переменной currentpos индекс последнего родителя.
// вызывать FilterDown в цикле с индексами currentpos.. О currentpos “ (heapsize-2)/2;
while (currentpos >- 0)
I
// выполнить условие пирамидальности для поддерева
// с корнем hlist[currentpos]
FilterDown(currentpos);
currentpos—;
)
// присвоить флажку inArray значение True
inArray - 1;

Приложение: пирамидальная сортировка
Пирамидальная сортировка имеет эффективность <(п logan). Алгоритм использует тот факт, что наименьший элемент находится в корне (индекс 0) и что метод Delete возвращает это значение.
Для осуществления пирамидальной сортировки массива А объявите объект типа Heap с массивом А в качестве параметра. Конструктор преобразует А в пирамиду. Сортировка осуществляется последовательным исключением А[0] и включением его в A[N-1], A[N-2]..А[1]. Вспомните, что после исключен^
элемента из пирамиды элемент, бывший до этого хвостовым, замещает кора^1 и с этого момента больше не является частью пирамиды. Мы имеем возможн скопировать удаленный элемент в эту позицию. В процессе пирамидальной с тировки очередные наименьшие элементы удаляются и последовательно зап наются в хвостовой части массива. Таким образом, массив А сортирУ61^^ убыванию. В качестве упражнения читателю предлагается построить симальных пирамид, с помощью которого массив сортируется по возрас $0.
Пирамидальная сортировка пятиэлементного массива А осуществляв
средством следующих действий: int А[] - {50, 20,75, 35, 25}
35) (50,
Исходная пирамида
Удалить 20 и запомнить в А[4] Удалить 25 и запомнить в А[3]
Поскольку единственный оставшийся элемент 75 является корием, массив отсортирован: А — 75 50 35 25 20.
Ниже приводится реализация алгоритма пирамидальной сортировки.
Функция HeapSort находится в файле heapsort.h.
Функция HeapSort
flinclude "heap.h’’ // класс Heap
11 отсортировать массив А по убыванию template <class T>
void HeapSort (T All, int n)
// конструктор, преобразующий А в пирамиду Heap<T> Н(А, п);
т elt;
Н цикл заполнения элементов А[п-1] — А[1] for (int i=n-l; i>=l; i—) (
// исключить наименьший элемент ИЗ пирамиды и запомнить его в A[i] elt = И. Delete О;
A[i] - elt;
)
вычислительная эффективность пирамидальной сортировки. Массив, со-
-лагций п элементов соответствует законченному бинарному дереву глу-^а°й k = log2n. Начальная фаза преобразования массива в пирамиду требует № операций FilterDown. Каждой из них требуется ие более к сравнений. а второй фазе сортировки операция FilterDown выполняется п-*1 раз. В Удхпем случае она требует к сравнений. Объединив обе фазы, получим худ-случай сложности пирамидальной сортировки:
к*| + к*(п-1) = к* Г—
Зп
-Л । f3n
- 11 = log2n х К- - 1
образом, сложность алгоритма имеет порядок O(n log2n).
с Пирамидальная сортировка не требует никакой дополнительной памяти, ЛькУ производится на месте. Турнирная сортировка является алгоритмом *ДКа O(n log2n), ио требует создания последовательно представляемого мае-
сивом дерева из 2(к+1) узлов, где к — наименьшее целое, при котором ц Некоторые O(n log2n) сложные сортировки дают О(п2) в худшем случдё мером может служить сортировка, рассмотренная в разделе 13.7. Пирам’ ная сортировка всегда имеет сложность O(n log2n) независимо от.исх^^1' распределения данных.
Программа 13.2. Сравнение методов сортировки
Массив А, содержащий 2000 случайных целых чисел, сортирует'^’ помощью функции HeapSort (пирамидальная сортировка). В целях сп* * с нения массивы В и С заполняются теми же элементами и сортируют^' помощью функций TournamentSort (турнирная сортировка) и Exchanges* ° (обменая сортировка). Функция ExchangeSort находится в файле аггвоги? Сортировки хронометрируются функцией TickCount, которая возвращае' число 1/60 долей секунды, прошедших с момента старта системы. CoJ тировка обменом, имеющая сложность О(п2), позволит четко представить быстродействие турнирного и пирамидального методов, имеющих сложность O(n log2n). Функция PrintFirst_Last распечатывает первые и пос-ледние пять элементов массива. Код этой функции не включен в листинг программы. Его можно найти в программном приложении в файле prgl3_2.cpp.
♦include <iostream.h>
*
♦include "random.h"
♦include "arrsort.h"
♦include "toursort.h"
♦include "heapsort.h"
♦include "ticks.h''
enum SortType {heap, tournament, exchange};	\
void TimeSort (int *A, int n, char *sortName, SortType sort) {
long tcount;
// TickCount — системная функция.
И возвращает число 1/60 долей
// секунды с момента старта системы
cout « "Испытывается " « sortName «	« endl;
// засечь время, отсортировать массив А. подсчитать затраченное // время в 1/60 долях секунды
tcount = TickCount();
switch{sort) {
case heap:	HeapSort(A,n);
break;
case tournament: TournamentSort(A, n);
break;
case exchange: ‘ ExchangeSort(A, n);
break;
}
tcount - TickCount() - tcount;
// распечатать 5 первых и 5 последних элементов
// отсортированного массива
for (int i~0; i<5; i++>
cout « A[i] « ’’
cout «
for (i-n-5; i<n; i++>
cout « A(i] « "
cout « endl;
cout « "Продолжительность " « tcount « "\n\n";
J
void main (void)
// указатели массивов А, В и С
int *А, *в, *С;
RandomNumber rnd;
11 динамическое выделение памяти и загрузка массивов д - new int [2000];
В = new int [2000];	
С - new int [2000];	'
11 загрузить в массивы одни и те же 2000 случайных чисел • for (int i-0; К2000; i++)
A[i] - B[i] - C[i] - rnd.Random(10000);
TimeSort(A, 2000, "пирамидальная сортировка ", heap);
delete [] A;
TimeSort(B, 2000, "турнирная сортировка ", heap);
delete (] B;
TimeSort(C, 2000, "сортировка обменом ", heap);
delete [] C;
}
«Прогон программы 13.2>
Испытывается пирамидальная сортировка :
9999 9996 9996 9995 9990 ... 11 10 9 6 3
Продолжительность 16
Испытывается турнирная сортировка :
3 6 9 10 11 ... 9990 9995 9996 9996 9999
Продолжительность 36
Испытывается сортировка обменом :
3 6 9 10 11 ... 9990 9995 9996 9996 9999
Продолжительность 81В
13.4. Очереди приоритетов
. береди приоритетов рассматривались в гл. 5 в использовались в задаче Цедирования событий. Клиенту был предоставлен доступ к оператору вставив й оператору удаления, который удалял из списка элемент с наивысшим Рпт’РИ’гетом. В главе 5 для реализации списка, лежащего в основе объекта \5Ue» использовался массив.
Этом разделе очередь приоритетов реализуется с помощью пирамиды. хЦ°ЛькУ мы используем минимальную пирамиду, предполагается, что эле-имеют возрастающие приоритеты. Операция удаления из пирамиды ^од^ает наименьший (с наивысшим приоритетом) элемент очереди приори-^-Пирамидальная реализация обеспечивает высокую эффективность мето-так как требует только O(logan) сравнений. Это соизмеримо с О(п) ЧеЙЙЯМИ в реализации с помощью массива.
Данный раздел завершается рассмотрением фильтра, преобразуют^ сив элементов в длинные последовательности1. Такой фильтр, использи° редь приоритетов, существенно повышает эффективность сортировки нием при упорядочении больших наборов данных файла. Эта тема -об ется в гл. 14.
Спецификация класса PQueue (пирамидальная версия)
ОБЪЯВЛЕНИЕ
#include "heap.h"
template cclass T>
class PQueue {
private:
// пирамида, в которой хранится очередь
Неар<Т> *ptrHeap;
public:
// конструктор
PQueue (int sz);
// операции модификации очереди приоритетов
void PQInsert(const ТЬ item);
т FQDelete(void);
void ClearFQ(void);
// методы опроса состояния очереди приоритетов
int PQEmpty(void) const;
int PQFull(void) const;
int PQLength(void) const;
};
ОПИСАНИЕ
В конструктор передается параметр sz, который используется для динамического размещения структуры, адресуемой указателем ptrHeap. Методы реализуются простым вызовом соответствующего метода в классе Heap. Например, FQDelete использует метод исключения элемента из пирамиды.
// удалить первый элемент очереди посредством удаления корня
// соответствующей пирамиды, возвратить удаленное значение template <class Т>
Т PQueue<T>::FQDelete(void) {
return ptrHeap->Delete();
)
Реализация PQeue находится в файле pqueue.h.
Приложение: длинные последовательности
Сортировка слиянием является основным алгоритмом упорядочения бол^ ших файлов. Его эффективность возрастает, если данные фильтрую145^' предварительно преобразуются в длинные последовательности. В гл. уже видели один такой фильтр, который вводит сразу к элементов Д ga и сортирует их. В этом случае минимальная длина последовательностей Р
1 Другие названия: серии, отрезки, цепочки. — Прим. пер.
j. В данном приложении используется к-элементная очередь приоритетов и Издаются последовательности, длины которых часто существенно превышают , v Алгоритм читает элементы из исходного списка А и пропускает их через г.йльтр очереди приоритетов. Элементы возвращаются в исходный список в । дорме длинных последовательностей.
' 9 Проиллюстрируем алгоритм иа примере. Пусть массив А имеет 12 целых | чйсел, а приоритетная очередь PQ1 является фильтром с к=4 элементами. • „Qi хранит элементы, которые в конечном счете попадут в текущую последовательность. Вторая приоритетная очередь, PQ2, содержит элементы для бедующей последовательности. Для сканирования массива используются два индекса. Переменная loadindex указывает элемент, который вводится в дан-&Ь1Й момент. Переменная currlndex указывает последний элемент, покинув-Йлй очередь PQ1 и вернувшийся в исходный массив. В нашем примере массив А изначально разбит на шесть последовательностей, самая длинная яэ которых содержит три элемента:
[13] [6 61 96] [26) [1 72 91] [37] [25 97] [21]
После фильтрации получатся три последовательности, самая длинная из которых будет содержать семь элементов.
Вначале в PQ1 загружаются элементы А[0]...А[3]. Так как очередь приоритетов удаляет элементы в возрастающем порядке, у нас уже есть средство для сортировки, по крайней мере, четырех элементов последовательности. Но мы поступим даже лучше. Исключим иэ PQ1 первый элемент (с минимальным значением) и присвоим его Afcurrlndex] ~ А[0] = 6. Это число вачинает первую последовательность, а в PQ1 остается незаполненное место. Поскольку первые четыре элемента массива А были скопированы ь PQ1, продолжим с четвертого элемента (loadindex = 4). На каждом шаге сравни-’ ваются Afcurrlndex] и Afloadlndex]. Если первый из них больше, он в конце 1 концов попадет в текущую последовательность и поэтому запоминается в । PQ1- В противном случае он попадет в следующую последовательность и ' поэтому запоминается в PQ2. Опишем это действие для каждого элемента в = нашем примере. После обработки некоторого элемента мы используем следующий формат для перечисления загруженных элементов в А, элементов которые еще должны считываться, и содержимое обеих очередей приоритетов:
А: <элементы, загружаемые в последовательности> Afloadlndex]: Оставшиеся элементы>
PQ1: <содержимое текущей последовательности PQ2: <содержимое следующей последовательиости>
Выполнить по шагам: Элемент А[4]=26 > Alcurrlndex]=6. Запомнить 26 в PQ1; и удалить 13 из PQ1; поместить 13 в АН].
А: 6 13	AI5]...A(11]: 1 72 91 37 25 97 21
PQ1: 61 96 26 PQ2: <пусто>
Элемент А[5]=1 < Afcurrlndex]—13. Поэтому 1 принадлежит следующей последовательности. Запомнить 1 в PQ2; исключить 26 из PQ1; поместить 26 в А[2].
А: 6 13 26	А[6]...А[11]: 72 91 37 25 97 21
1 PQ1: 61 96	PQ2: 1
Элемент А[6]=72 больше, чем элемент 26 в текущей последовательности.
Запомнить А[6] в PQ1; исключить 61 из PQ1. Аналогично, следующий
---
элемент 91 попадает в PQ1 прежде, чем произойдет исключена-мента 72 и запись его в текущую последовательность по инле*Л/ э^е-rIndex-4.	У «UN
А: 6 13 26 61 72	AI8]...A[11J: 37 25 97 21
PQ1: 91 96	PQ2: 1
Элементы А[8]=37 и А[9]=25 больше, чем 72 и попадут в следуй последовательность. Они запоминаются в PQ2. Одновременно из*^10 исключаются два элемента и помещаются в массив, а очередь р остается пустой.	™
А: 6 13 26 61 72 91 96	А[10]...А[11]: 97 21
PQ1: <пусто>	PQ2: 1 37 25
Мы сформировали текущую последовательность и можем начинать с дующую. Скопируем PQ2 в PQ1 и исключим наименьший элемент и вновь заполненной очереди PQ1. В нашем примере удаляем 1 из Реп и начнем следующую последовательность.	4
А: 6 13 26 61 72 91 96 1	А[10]...А[11]: 97 21
PQ1: 25 37	PQ2: <пусто>
Элемент А[10]—97 > 1*и запоминается в PQ1. Затем минимальное значение 25 исключается из PQ1.
А: 6 13 26 61 72 91 96 1 25	АЦ1]: 21
PQ1: 37 97	PQ2: <пусто>
Элемент А{11]—21 < 25 и должен ждать следующую последовательность.
Он запоминается в PQ2, а 37 исключается из PQ1.
А: 6 13 26 61 72 91 96 1 25 37 <весь список пройден>
PQ1: 97	PQ2: 21
Сканирование исходного списка завершено. Исключить все элементы из PQ1 и поместить их в текущую последовательность. Затем все эле-, менты из PQ2 поместить в следующую последовательность.
Последовательность 1: 6 13 26 61 72 91 96
Последовательность 2: 1 25 37 97
Последовательность 3: 21
Алгоритм Runs. Алгоритм порождения длинных последовательностей реализуется с помощью класса LongRunFilter. Его закрытые данные-члены включают массив и две приоритетные очереди, содержащие текущую и еле дующую последовательности. Конструктор связывает объект данного клас с массивом и создает соответствующие очереди приоритетов. Алгоритм держивается закрытыми методами LoadPQ, который включает элементы сива в очередь PQ1, и CopyPQ, который копирует элементы из PQ2 ®
ОБЪЯВЛЕНИЕ
template <с1азз т> class LongRunFilter {
private:
// указатели, определяющие ключевые параметры в фильтре // список А и две очереди приоритетов — PQ1 и PQ2 Т *А; PQueue<T> *PQ1, *PQ2;
jnt loadindex;
// размер массива и очередей приоритетов
jnt arraySize;
int filtersize;
// копирование PQ2 в PQ1
void CopyPQ (void);
Il аагруэка массива л в очередь приоритетов PQ1 void LoadPQ (void);
public:
// конструктор и деструктор
bongRunFilter(Т arr[], int 0, int sz);
~LongRunFilter(void);
// создание длинных последовательностей
void LoadRuns (void);
Il оценка последовательностей void PrintRuns (void) const; int CountRuns(void) const;
);
ОПИСАНИЕ
Конструктор инициализирует данные-члены и загружает элементы из массива в PQ1, формируя таким образом элементы первой последовательности. Метод LoadRuns является главным алгоритмом, преобразующим элементы массива в длинные последовательности.
Методы PrintRuns и CountRuns служат для иллюстрации алгоритма. Они используются для сравнения последовательностей до и после вызова LoadRuns.
Полная реализация класса LongRunFilter находится в файле longrun.h.
И сканировать массив А и создать длинные последовательности, U пропуская элементы через фильтр template <class Т>
void LongRunFilter<T>::LoadRuns (void)
T value;
int currlndex; = 0;
(filterSize = 0)
return;
(
если элемент больше или равен a (currlndex]
начать с загрузки наименьшего элемента из PQ1 в А Mcurrlndex] = PQl->PQDelete();
' заполнить PQ1 элементами из А
/теперь просмотреть элементы, оставшиеся в А
{Не (loadindex < arraySize)
И Рассмотреть очередной элемент списка
value ® A(loadlndex++];
у если элемент больше или равен а (currlndex],
•	он принадлежит текущей последовательности
•	и попадает в PQ1. в противном случае он копируется if
^1E>Ql->PQinsert (value);
else
₽Ql~>PQInsert(value);
в PQ2 и в конечном счете попадает в следующую последовательность (A(currlndex) <= value)
// если PQ1 пуста, текущая последовательность сформирована.
// скопировать PQ2 в PQ1 и начать следующую последовательность if (PQl->PQEmptyO)
CopyPQO;
// веять элемент из PQ1 и включить его в последовательность
if (!PQl->PQEmpty())
A[++currIndex] = PQl->PQDelete;
}
// удалить элементы из текущей последовательности,
//а затем из следующей
while (!PQl->PQEmpty())
A(++currlndex] - PQl->PQDelete;
while (!PQ2->PQEmpty())
A[++currIndex]  PQ2->PQDelete;
Программа 13.3. Длинные последовательности
Эта программа иллюстрирует применение фильтра. В первом примере берется небольшой массив из 15 элементов и фильтруется с помощью 4-элементных очередей приоритетов. На выход выдается перечень после* довательностеЙ до и после вызова фильтра. В более практическом примере обрабатывается 10000-элементный массив, который фильтруется 4s помощью 5-, 50- и 500-элементных очередей приоритетов. В каждом случае распечатывается число итоговых последовательностей.
♦include <iostream.h>
♦include "random.h"
♦include "longrun.h"
11 копирование массива А в массив В void CopyArray(int A[], int BI], int n> {
for (int i=0; i<n; i++)
B[i] = A[i];
void mainO
<
11 исходный 15-элементный массив для иллюстрации фильтра int demoArray[15 ];
// большие 10000-элементные массивы для подсчета последовательностей int *А ® new int[16000], *В - new int[10000];
RandomNumber rnd;
// создать 15 случайных чисел; сформировать фильтр
for (i=0; i<15; i++)
demoArray[i] = rnd.Random(100);
LongRunFilter<int> F(demoArray, 15, 4);
// распечатать список до и после создания длинных последовательностей
Нс помощью 4-элементного фильтра
cout « "Исходные последовательности" « endl;
F.PrintRuns();
cout « endl;
F.LoadRuns();
cout « "Отфильтрованные последовательности" « endl; F. PrintRuns(); cout « endl;
// сформировать массив из 10000 случайных чисел for (i=0; i<10000; i++) A[i] = rnd.Random(25000);
cout « "Последовательности, полученные с помощью 3-х фильтров" « endl;
LongRunFilter<int> LR(A, 10000, 0); cout « "Число последовательностей в исходном массиве: " « LR.CountRuns() « endl;
// тестирование 5- , 50-'и 500-элементных фильтров for (i“0; i<3; i++) {
Copy Array(А, В, 10000);
LongRunFilter<int> LR(B, 10000, filtersize);
// создать длинные последовательности LR.LoadRuns();
cout « " Число последовательностей после фильтра " « filterSize « " = " « LR.CountRuns () « endl;
// 10-кратное увеличение размера фильтра filtersize * 10;
) ) /* <Прогон программы 13.3>
Исходные последовательности 36 22	79
26	84
44	88
44	66	81
19	86
40 2	47
Отфильтрованные последовательности 22 26 36 44 44 66 79 81 84 86 88 2	19 40 47
Последовательности, полученные с помощью 3-х фильтров
Количество последовательностей в исходном массиве: 5077
Число последовательностей после фильтра 5 <= 991
Число последовательностей после фильтра 50  101
Число последовательностей после фильтра 500 = 11
*/
13.5. AVL-деревья
Бинарные деревья поиска предназначены для быстрого доступа к данным. q ^Д®але дерево является разумно сбалансированным и имеет высоту порядка '10в2И). Однако при некоторых данных дерево может оказаться вырожден-
ным. Тогда высота его будет О(п), и доступ к данным существенно замедл В этом разделе мы рассмотрим модифицированный класс Деревьев, ок4Тся-ющих всеми преимуществами бинарных деревьев поиска и никогда рождающихся. Они называются сбалансированными или АУТЬ-деревьями* сбалансированностью будем понимать то, что для каждого узла дерева вк" обоих его поддеревьев различаются не более чем на I1 * * *.	bIC°Tbi
Новые методы включения и исключения в классе AVL-деревьев гаъ руют, что все узлы останутся сбалансированными по высоте. На pnci?1' показаны эквивалентные представления массива AVL-деревом и бинап ° деревом поиска. Верхняя пара деревьев представляет простой пятиэлем ный массив А, отсортированный по возрастанию. Нижняя пара дере»8* представляет массив В. Бинарное дерево поиска имеет высоту 5, в то вс как высота AVL-дерева равна 2. В общем случае высота сбалансированно*11 дерева не превышает O(log2n). Таким образом, AVL-дерево является мошной структурой хранения, обеспечивающей быстрый доступ к данным,
А[5] - {1,2,3,4,5}	В[8] = {20, 30, 80, 40, 10, 60, S0, 70}
В этом разделе используется подход, принятый в гл. 11, когда поисковое дерево строилось отдельно от своих узлов. Сначала мы разработаем класс AVLTreeNode, а затем используем объекты этого типа для конструирования класса AVLTree. Предметом пристального внимания будут методы Insert и Delete. Они требуют тщательного проектирования, поскольку должны гарантировать, что все узлы нового дерева останутся сбалансированными по высоте.
Узлы AVL-дерева
AVL-деревья имеют представление, похожее на бинарные деревья поиска. Все операции идентичны, за исключением методов Insert и Delete, которые должны постоянно отслеживать соотношение высот левого и правого поддеревьев узла. Для сохранения этой информации мы расширили определение объ- -екта TreeNode, включив поле balanceFactor (показатель сбалансированности), которое содержит разность высот правого и левого поддеревьев.
left
data
balanceFactor
AVLTreeNode
baianceFactor-height(right subtree)- height(left subtree}
Если balanceFactor отрицателен, то узел "перевешивает влево”, так ка* высота левого поддерева больше, чем высота правого поддерева. При поло» тельном balanceFactor узел "перевешивает вправо”. Сбалансированный по вы соте узел имеет balanceFactor = 0. В AVL-дереве показатель сбалансированное должен быть в диапазоне [-1, 1].	w
На рис. 13.4 изображены AVL-деревья с пометками -1, 0 и +1 на кажд узле, показывающими относительный размер левого и правого поддеревье
-1: Высота левого поддерева на 1 больше высоты правого подДеРеВв’ 0: Высоты обоих поддеревьев одинаковы.
1 Строго говоря, этот критерий нужно называть AVL-сбалансированностью в отличие <ут
ной сбалансированности, когда для каждого узла дерева количества узлов в лев^гггсбвлв®'
. поддеревьях различаются не более чем на 1. В этой главе всегда подразумевается
сированность. — Прим, псрев.
+1: Высота правого поддерева на 1 больше высоты левого поддерева.
Рис 133. Представление массива с помощью бинарного дерева поиска и AVL-дерева
Используя свойства наследования, можно образовать класс AVLTreeNode на базе класса TreeNode. Объект типа AVLTreeNode наследует поля из класса TreeNode и добавляет к ним поле balanceFactor. Данные-члены left и right класса TreeNode являются защищенными, поэтому AVLTreeNode или другие производные классы имеют к ним доступ. Класс AVLTreeNode и все сопро-пождающие его программы находятся в файле avltree.h.
^^иификация класса AVLTreeNode
°5ьЯВЛВНИВ
t«taiCJlejUu,K класса TreeNode
ciZXate «class т>
i 5 AVLTreeNode: public TreeNode<T>
Ptivate:
У Дополнительный член класса balanceFactor;
Z Используются методами класса AVLTree и позволяют Избегать "перевешивания" узлов
AVLTreeNode<T>* & Left (void);
AVLTreeNode<T>* & Right(void);
public:
// конструктор
AVLTreeNode(const T& item, AVLTreeNode<T> *iptr = NULL, AVLTreeNode<T> *rptr « NULL, int balfac = 0);
11 возвратить левый/правый указатель узла типа TreeNode, //в качестве указателя узла типа AVLTreeNode; выполнить // приведение типов
AVLTreeNode<T> *Left(void) const;
AVLTreeNode<T> *Right(void) const;
// метод для доступа к новому полю данных int GetBalanceFactor(void);
// методы -класса AVLTree должны иметь доступ к Left и Right friend class AVLTree<T>;
);
ОПИСАНИЕ
Элемент данных balanceFactor является закрытым, так как обновлять его должны только сбалансированные операции включения и исключения.
Параметры, передаваемые в коимруктор, содержат данные для базовой структуры типа TreeNode. По умолчанию параметр balfac равен 0.
Доступ к полям указателей осуществляется с помощью методов Left и Right. Новые определения для этих методов обязательны, поскольку они возвращают указатель на стуктуру AVLTreeNode.
Основные причины, по которым деструктор объявляется виртуальным, обсуждались в разделе 12.3. Поскольку класс AVLTree образован на базе класса BinSTree, будем использовать деструктор базового класса и ClearList. Эти методы удаляют узлы с помощью оператора delete. В каждом случае указатель ссылается на объект типа AVLTreeNode, а ие TreeNode. Если деструктор базового класса TreeNode виртуальный, то при вызове delete используется динамическое связывание и удаляется объект типа AVLTreeNode.
ПРИМЕРЫ
AVLTreeNode<char> *root; // корень AVL-дерева
// эта функция создает AVL-дерево, изображенное ниже.
// каждому узлу присваивается показатель сбалансированности
• void MakeAVLChaxTree(AVLTreeNode<char>* broot) (
AVLTreeNode<char> *a, *b, *c, *d, *e;
e - new AVLTreeNode<char>('E', NULL, NULL, 0);, d • new AVLTreeNode<char>(*D', NULL, NULL, 0); c - new AVLTreeNode<char>(rC', e, NULL, -1);
b - new AVLTreeNode<char>('B', NULL, d, 1);
a - new AVLTreeNode<char> (' A', b, c, 0); root = a;
Реализация класса AVLTreeNode. Конструктор класса AVLTreeNode вызывает конструктор базового класса и инициализирует balanceFactor.
// конструктор, инициализирует balanceFactor и базовый класс.
// нулевые начальные значения полей указателей
// (по умолчанию) инициализируют узел как лист, template <class Т>
nvLTreeNode<T>::AVLTreeNode (const T& item,
AVLTreeNode<T> *lptr, AVLTreeNode<T> *rptr, int balfac):
TreeNode<T>(item, Iptr, rptr), balanceFactor(balfac)
(I
Методы Left и Right в классе AVLTreeNode упрощают доступ к полям паяных. При попытке обратиться к левому сыну с помощью базового метода Left возвращается указатель на объект типа TreeNode. Чтобы получить указатель на узел сбалансированного дерева, требуется преобразование типов. Например, ftVLTreeNode<T> *р, *q;
q» p->Left();	// недопустимая операция
(AVLTreeNode<T> *)p->LeftО;	// необходимое приведение типа
Во избежание постоянного преобразования типа указателей мы определяем методы Left и Right для класса AVLTreeNode, возвращающие указатели на объекты типа AVLTreeNode.
template <class T>
AVLlreeNode<T>* AVLTreeNode::Left(void) (
return ((AVLTreeNode<T> *)left; )
13.6.	Класс AVLTree
AVL-дерево представляет списковую структуру, похожую на бинарное дерево поиска, с одним дополнительным условием: дерево должно оставаться сбалансированным по высоте после каждой операции включения или удаления. Поскольку AVL-дерево является расширенным бинарным деревом поиска, класс AVLTree строится на базе класса BinSTree и является его наследником.
Методы Insert и Delete должны подменяться для выполнения AVL-условия. кРоме того, в производном классе определяются конструктор копирования и перегруженный оператор присваивания, так как мы строим дерево с большей узловой структурой.
Спецификация класса AVLTree
I ОвЬЯВЛЕНИЕ
со Значения показателя сбалансированности узла
nst int leftheavy = -1;
Con *nt balanced = 1;
1 st int rightheavy - 1;
// _
te Разводный класс поисковых деревьев
<class T>
Ss AVLTree: public BinSTree<T>
private:
// выделение памяти
AVLTreeNode<T> *GetAVLTreeNode(const Ть item, AVLTreeNode<T> *lptr, AVLTreeNode<T> *rptr);
// используется конструктором копирования и оператором присваивания AVLTreeNode<T> *CopyTree(AVLTreeNode<T> *t);
// используется методами Insert и Delete для восстановления
// AVL-условий после операций включения/исключения
void SingleRotateLeft (AVLrreeNode<T>* &p);
void SingleRotateRight (AVLTreeNode<T>* Sp);
void DoubleRotateleft (AVLTreeNode<T>* &p) ;
void DoubleRotateRight (AVLTreeNode<T>* ep);
void UpdateLeftTree (AVLTreeNode<T>* stree, int &reviseBalanceFactor);
void DpdateRightTree (AVLTreeNode<T>* stree, int &reviseBalanceFactor);
// специальные эерсии методов Insert и Delete
void AVLlnsert(AVLTreeNode<T>* &tree,
AVLTreeNode<T>* newNode, int &reviseBalanceFactor);
void AVLDelete(AVLTreeNode<T>* &tree,
AVLTreeNode<T>* newNode, int &reviseBalanceFactor);
public:
// конструкторы
AVLTree(void);
AVLTree(const AVLTree<T>& tree);
// оператор присваивания
AVLTree<T>& operator= (const AVLTree<T>& tree);
// стандартные методы обработки списков
virtual void Insert(const T& item);
virtual void Delete(const T& item); );
ОПИСАНИЕ
Константы leftheavy, balanced и rightheavy используются в операциях вставки/удаления для описания показателя сбалансированности узла.
Метод GetAVLTreeNode управляет выделением памяти для класса. По умолчанию balanceFactor нового узла равен нулю.
В этом классе заново определяется функция СоруТгее для использования с конструктором копирования и перегруженным оператором присваивания-Несмотря на то, что алгоритм идентичен алгоритму для функции СоруТгее класса BinSTree, новая версия корректно создает расширенные объекты типа AVLTreeNode при построении нового дерева.
Функции AVLlnsert и AVLDelete реализуют методы Insert и Delete, coo ветственно. Они используют закрытые методы наподобие SingleRotate.be-Открытые методы Insert и Delete объявлены как виртуальные и ио^епЯ^сЯ соответствующие функции базового класса. Остальные операции наследую от класса BinSTree.
ПРИМЕР
AVLTree<int> avltree;	If AVLTree-список целых чисел
BinSTree<int> bintree;	// BinSTree-список целых чисел
for (int 1=1; i<=5; i++)
1
1	bintree.Insert(i);	// создать дерево A
avltree.Insert(i);	// создать дерево В
} (
avl tree.Delete(3) ;	11 удалить 3 из AVL-дерева
// функция AVLPrintTree эквивалентна функции вертикальной распечатки
// дерева из гл. 11. кроме собственно данных для каждого узла
// распечатываются показатели сбалансированности, дерево (С) есть дерево (В)
// без удаленного узла 3. AVLPrintTree находится в файле avltree.h
AVLPrintTree((AVLTreeNode<int> *)avltree.GetRoot(), 0);
Распределение памяти для AVLTree
Класс AVLTree образован от класса BinSTree и наследует большинство его операций. Для создания расширенных объектов типа AVLTreeNode мы разработали отдельные методы выделения памяти и копирования.
11	разместить в памяти объект типа AVLTreeNode. прервать программу, И если во время выделения памяти произошла ошибка template <class Т>
WLTreeNode<T> *AVLTree<T>:;GetAVLTreeNode(const T& item, AVLTreeNode<T> *lptr, AVLTreeNode<T> *rptr)
ftVLTreeNode<T> *p;
₽ = new AVLTreeNode<T> (item, Iptr, rptr);
If (p =« NULL)
cerr « "Ошибка выделения памяти!" « endl; exit(l);
} return p;
удаления узлов AVL-дерева достаточно методов базового класса. Метод •л. е*еТгее из класса BinSTree задействует виртуальный деструктор класса YeeNode.
-------------------------------------------
Метод Insert класса AVLTree. Преимущество AVL-деревьев состою сбалансированности, которая поддерживается соответствующими алгол Ь ми вставки/удаления. Опишем метод Insert для класса AVLTree, к Ит,йа-перекрывает одноименную операцию базового класса BinSTree. При°Т°₽14й зации метода Insert для запоминания элемента используется рекуъ^^' функция AVLInsert. Сначала приведем код метода Insert на C++ сосредоточим внимание на рекурсивном методе AVLInsert, реализуют Зате* горитм Адельсона-Вельского н Ландиса.	14 ал.
template <class Т>
void AVLTree<T>::Insert(const T& item) (
// объявить указатель AVL-дерева, используя метод // базового класса GetRoot.
// произвести приведение типов для указателей
AVLTreeNode<T> *treeRoot • (AVLTreeNode<T> *)GetRoot(), *newNode;
// флажок, используемый функцией AVLInsert для перебалансировки узлов int reviseBalanceFactor • 0;
// создать новый узел AVL-дерева с нулевыми полями указателей newNode = GetAVLTreeNode(item, NULL, NULL);
// вызвать рекурсивную процедуру для фактической вставки элемента AVLInsert(treeRoot, newNode, reviseBalancefactor);
fl присвоить новые значения элементам данных базового класса root - treeRoot;	-
current - newNode; size++;
}
Ядром алгоритма включения является рекурсивный метод AVLInsert. Как и его аналог в классе BinSTree, этот метод осуществляет прохождение левого поддерева, если item меньше данного узла, и правого (поддерева, если item больше или равен данному узлу. Эта закрытая функция имеет параметр с именем tree, в котором находится запись текущего узла при сканировании, новый узел для вставки в дерево, флажок revisebalanceFactor. При сканировании левого или правого поддерева некоторого узла, этот флажок является признаком изменения любого параметра balanceFactor в поддереве- Если Да« то нужно проверить, сохранилась ли AVL-сбалансированность всего дерева-Если в результате включения нового узла она оказалась нарушенной, то мы обязаны восстановить равновесие. Данный алгоритм рассматривается на ряДе примеров.
Алгоритм AVL-вставки. Процесс включения является почти таким же, я и для бинарного дерева поиска. Осуществляется рекурсивный спуск по леВ и правым сыновьям, пока не встретится пустое поддерево, а затем производи пробное включение нового узла в этом месте. В течение этого процесса посещаем каждый узел на пути поиска от корневого к новому элементу-
Поскольку процесс рекурсивный, обработка узлов ведется в обратном^^ рядке. При этом показатель сбалансированности родительского узла м в скорректировать после изучения эффекта от добавления нового элеме одно из поддеревьев. Необходимость корректировки определяется дого узла, входящего в поисковый маршрут. Есть три возможных ситУ
п первых двух случаях узел сохраняет сбалансированность и реорганизация одДеРевьев н® требуется, а нужно лишь скорректировать показатель сбалансированности данного узла. В третьем случае расбалансировка дерева требует
одинарного или двойного поворотов узлов.
Случай 1. Узел на поисковом маршруте изначально является сбалансированным (balanceFactor — О). После включения в поддерево нового
элемента узел стал перевешивать влево или вправо в зависимости от того, в какое поддерево было произведено включение. Если элемент был включен в левое поддерево, показателю сбалансированности присваивается -1, а если в правое, то 1. Например, на пути 40-50-60 каждый узел сбалансирован. После включения узла 55 показатели сба-
лансированности изменяются.
До включения узла 55
После включения узла 55
Случай 2. Одно из поддеревьев узла перевешивает, и новый узел включается в более легкое поддерево. Узел становится сбалансированным. Сравните, например, состояния дерева до и после включения узла 55.
До включения узла 55
После включения узла 55
Случай 3. Одно из поддеревьев узла перевешивает, и новый узел включается в более тяжелое поддерево. Тем самым нарушается условие сбалансированности, так как balanceFactor выходит за пределы -1..1. Чтобы восстановить равновесие, нужно выполнить поворот.
во ^>ассмотРИ1'4 пример. Предположим, дерево разбалансировалось слева и мы ^СТаНаВЛИВаем Равновесие> вызывая одну из функций поворота вправо. Раз-авсировка справа влечет за собой симметричные действия.
Го Казанное иллюстрируется следующими рисунками. При разработке ал-₽йтМа поворота мы включили дополнительные детали.
у3^етоД AVLlnsert. Продвигаясь вдоль некоторого пути для вставки нового Кор ’ Этот рекурсивный метод распознает все три указанных выше случая ектировки. При нарушении условия сбалансированности восстановление
^^•Ьесия осуществляется с помощью функций UpdateLeftTree и Up-
Одинарный поворот
До корректировки
После корректировки
Двойной поворот
template <class Т> void AVLTree<T>::AVLlnsert(AVLTreeNode<T>* atree, AVLTreeNode<T>* newNode, int &reviseBalanceFactor) {
// флажок "Показатель сбалансированности был изменен" int rebalanceCurrNode;
// встретилось пустое поддерево, пора включать новый узе?! if (tree — NULL) <
11 вставить новый узел tree “ newNode;
// объявить новый узел сбалансированным tree->balanceFactor - balanced;
// сообщить об изменении показателя сбалансированности reviseBalanceFactor » 1;
)
// рекурсивно спускаться по левому поддереву,
// если новый узел меньше текущего else if (newNode->data < tree->data) {
AVLlnsert(tree->Left(), newNode, rebalanceCurrNode); // проверить, нужно ли корректировать balanceFactor if (rebalanceCurrNode) {
// включение слева от узла, перевешивающего влево, будет нарушено // условие сбалансированности; выполнить поворот (случай 3) if (tree->balanceFactor == leftheavy)
UpdateLeftTree(tree, reviseBalanceFactor);
// вставка слева от сбалансированного узла. // узел станет перевешивать влево (случай 1) else if (tree->balanceFactor •“> balanced) (
tree->balanceFactor - leftheavy;
reviseBalanceFactor « 1; ) // вставка слева от узла, перевешивающего вправо. // узел станет сбалансированным (случай 2)
else <
tree->balanceFactor - balanced;
reviseBalanceFactor =0; ) > else
// перебалансировка не требуется, не опрашивать предыдущие узлы reviseBalanceFactor =0;
)
// иначе рекурсивно спускаться по правому поддереву else if (newNode->data < tree->data)
I AVLlnsert(tree->Right(), newNode, rebalanceCurrNode);
// проверить/ нужно ли корректировать balanceFactor if (rebalanceCurrNode)
I
// вставка справа от узла, перевешивающего вправо, будет нарушено // условие сбалансированности; выполнить поворот (случай 3) if (tree->balanceFactor «= rightheavy)
UpdateRightTree(tree, reviseBalanceFactor);
// вставка справа от сбалансированного узла.
// узел станет перевешивать вправо (случай 1) else if (tree->balanceFactor == balanced) (
tree->balanceFactor = xightheavy;
reviseBalanceFactor ® 1;
.)
// вставка справа от узла, перевешивающего влево.
// узел станет сбалансированным (случай 2) else {
tree->balanceFactor - balanced;
reviseBalanceFactor = 0; }
)
else
// перебалансировка не требуется, не опрашивать предыдущие узлы reviseBalanceFactor =0;
)
Метод AVLlnsert распознает случай 3, когда, нарушается AVL-условие. Для выполнения перебалансировки используются методы UpdateLeftTree и Up-LlateRightTree. Они выполняют одинарный или двойной поворот для уравновешивания узла, а затем сбрасывают флажок reviseBalanceFactor. Перед тем как Осудить специфические детели поворотов, приведем программный код функ-41111 UpdateLeftTree.
template <class Т>
01d AVLTree<T>::UpdateLeftTree(AVLTreeNode<T>* sp,
j	int reviseBalanceFactor)
AvbTreeNode<T> *lc;
У == P~>Left();
перевешивает левое поддерево?
‘ (lc->balanceFactor == leftheavy)
SingleROtateRight(pj.	// однократный поворот
reviseBalanceFactor = 0;
}
11 перевешивает правое подлерево?
else if (lc~>balanceFactor =« rightheavy) (
// выполнить двойной поворот
DoubleRot a teRight (р);
// теперь корень уравновешен
reviseBalanceFactor = 0;
Повороты. Повороты необходимы, когда родительский узел Р станови расбалансированным. Одинарный поворот вправо (single right rotation) m?4 исходит тогда, когда родительский узел Р и его левый сын LC начинают пев вешивать влево после включения узла в позицию X. В результате, такого ново рота LC замещает своего родителя, который становится правым сыном. Бывщ^ правое поддерево узла LC (ST) присоединяется к Р в качестве левого поддерева Это сохраняет упорядоченность, так как узлы в ST больше или равны узлу LC но меньше узла Р. Поворот уравновешивает как родителя, так и его левого сына*
// выполнить поворот по часовой стрелке вокруг узла р.
// сделать 1с новой точкой вращения
template <class Т>
void AVLTree<T>::SingleRotateRight (AVLTreeNode<T>* &p) {
// левое, перевешивающее поддерево узла р
AVLTreeNode<T> *1с;
// назначить 1с левым поддеревом
1с = p->Left();
// скорректировать показатель сбалансированности для // родительского узла и его левого сына p->balanceFactor - balanced;
lc->balanceFactor ® balanced;
// правое поддерево узла 1с в любом случае должно оставаться справа // от 1с. выполнить это условие, сделав st левым поддеревом узла р p->Left() - lc->Right();
// переместить р в правое поддерево узла 1с.
// сделать 1с новой точкой вращения. lc->Right() = р;
р  1с;
Попытка включить узел 5 в изображенное ниже AVL-дерево нарушает AVL-сдовие для узла 30 из равновесия. Одновременно левое поддерево узла 15 (LC) ’ Звонится перегруженным. Для переупорядочения узлов вызывается проце-CLpa SingleRotateRight. В результате родительский узел (30) становится сба-^дсированным, а узел 10 — перевешивающим влево.
Исходное дерево
Показатели сбалансированности до поворота
Двойной поворот вправо (double rigyn rotation) происходит тогда, когда родительский узел (Р) становится перевешивающим влево» а его левый сын (LC) — перевешивающим вправо. NP — корень правого перевешивающего поддерева узла LC. Тогда в результате поворота узел NP замещает родительский узел. На следующих далее рисунках показаны два случая включения нового узла в качестве сына узла NP. В обоих случаях NP становится родительским узлом, а бывший родитель Р становится правым сыном NP.
На верхней схеме мы видим сдвиг узла Х1} после того как он был вставлен ® левое поддерево узла NP. На нижней схеме изображено перемещение узла * после его включения в правое поддерево NP.
// двойной поворот вправо вокруг узла р template <с1ааз Т>
void AVLTree<T>::DoubleRotateRight (AVLTreeNode<T>* Sp) (
// два поддерева, подлежащих повороту AVLTreeNode<T> *1с, *пр;
И узел 1с <- узел пр < узел р
1с  p->Left (); П левый сын узла р
пр “ lc->Right();	// правый сын узла 1с
// обновить показатели сбалансированности в узлах р, 1с и пр if (np->balanceFactor =• rightheavy) {
p->balanceFactor - balanced;
lc->balanceFactor - rightheavy,-} else {
p->balanceFactor =• rightheavy;
lc->balanceFactor = balanced; ) np->balanceFactor - balanced;
// перед тем как заменить родительский узел р,
// следует отсоединить его старых детей и присоединить новых lc->Right()  np->Left();
np->Left() — 1с;
p->Left()  np->Right();
np->Right() - p;
p - np;
Двойной поворот иллюстрируется на изображенном ниже дереве. Попытка включить узел 25 разбалансирует корневой узел 50. В этом случае узел 20 (LC) приобретает слишком высокое правое поддерево и требуется двойной поворот. Новым родительским узлом (NP) становится узел 40. Старый родительскйй узел становится его правым сыном и присоединяет к себе узел 45, который также переходит с левой стороны дерева.
Оценка сбалансированных деревьев
Ценность AVL-деревьев зависит от приложения, поскольку
ОНИ^ дополнительных затрат на поддержание сбалансированности при вкЛ1°Хале-или исключении узлов. Если в дереве постоянно происходят вставки ния элементов, эти операции могут значительно снизить быстродеис>	а в
другой стороны, если ваши данные превращают бинарное дерево и^зо-вырожденное, вы теряете поисковую эффективность и вынуждены и вать AVL-дерево.
Для сбалансированного дерева не существует наихудшего случая, так как является почти полным бинарным деревом. Сложность операции поиска оставляет O(log2n). Опыт показывает, что повороты требуются примерно в одовине случаев включений и удалений. Сложность балансировки обусловливает применение AVL-деревьев только там, где поиск является доминирующей операцией.
Программа 13.4. Оценка AVL-деревьев
Эта программа сравнивает сбалансированное и обычное бинарное дерево поиска, каждое из которых содержит N случайных чисел. Они хранятся в едином массиве и включаются в оба дерева. Для каждого элемента массива осуществляется его поиск в обоих деревьях. Длины поисковых путей суммируются, а затем подсчитывается средняя длина поиска по каждому дереву.
Программа прогоняется на 1000- и на 10000-элементном массивах. Обратите внимание, что на случайных данных поисковые характеристики AVL-дерева несколько лучше. В самом худшем случае вырожденное дерево поиска, содержащее 1000 элементов, имеет среднюю глубину 500, в то время как средняя глубина AVL-дерева всегда равна 9.
tinclude <iostream.h>
^include "bstree.h"
Iinclude "avltree.h"
Iinclude *random.h"
// загрузить массив, бинарное поисковое дерево и AVL-дерево
// одинаковыми множествами, состоящими из п случайных чисел от 0 до 999
void SetupLists(BinSTree<int> fiTreel, AVLTree<int> &Tree2, int A(], int n)
<
int if
RandomNumber rnd;
// запомнить случайное число в массиве А, а также включить его
//в бинарное дерево поиска и в AVL-дерево
for (i=>0; i<n; i++)
<
A[i] » rnd.Random(1000) ;
Treel.Insert(A{i));
Tree2.insert(A[i]);
}
)
Ч поиск элемента item на дереве t. накапливается суммарная длина поиска template <class Т>
^oid PathLength(TreeNode<T> *t, long &totallength, int item)
11 возврат, если элемент найден или отсутствует в списке
if (t »- NULL || t->data — item) return;
else
(
// перейти на следующий уровень.
И увеличить суммарную длину пути поиска
totallength*+;
if (item < t->data)
Зак. 425
PathLength(t->Left(), totallength, Item); else
PathLength(t->Right(), totallength, item); >
)	.	I
void main (void); i
II переменные для деревьев и массива
BinSTree<int> binTree;
AVLTree<int> avlTree; int *A;
// суммарные длины поисковых путей элементов массива	।
//в бинарном дереве поиска и в AVL-дереве
long totalLengthBintree - О, totalLengfhAVLTree =0; int n, i; 9
cout « "Сколько узлов на дереве? cin » n;
// загрузить случайными числами массив и оба дерева SetupLists(binTree, avlTree, A, n) ;
for (i=0; i<n; i++) {
PathLength(binTree.GetRoot(), totalLengthBintree, A[i]);
PathLength((TreeNode<int> *)avlTree.getRoot(). totalLengthAVLTree, A[i]);
)
cout « "Средняя длина поиска для бинарного дерева =w"
« float(totalLengthBintree)/п « endl;
cout « "Средняя длина поиска для сбалансированного дерева = ”
« float(totalLengthAVLTree)/п « endl;
} /*
<Прогон #1 программы 13.4>
Сколько узлов на дереве? 1000
Средняя длина поиска для бинарного дерева - 10.256
Средняя длина поиска для сбалансированного дерева - 7.901
<Прогон #2 программы 13.4>
Сколько узлов на дереве? 10000
Средняя длина поиска для бинарного дерева «= 12.2622
Средняя длина поиска для сбалансированного дерева = 6.5632
*/
13.7. Итераторы деревьев
Мы уже убедились в силе итераторов, применяемых для обхода т нейных структур, как массивы и последовательные списки. Скан^Р ji узлов дерева более сложно, так как дерево является нелинейной гл. I1 существует не один порядок прохождения. Утилиты класса TreeNode	цро-
реализуют прямой, симметричный н обратный алгоритмы рекУРсКВ тотЛ, хождения. Проблема каждого из этих методов прохождения состоит в
0 завершения рекурсивного процесса из него невозможно выйти. Нельзя ос-^овить сканиронятте, проверить содержимое узла, выполнить какие-нибудь тперации с данными, а затем вновь продолжить сканирование со следующего ° _а дерева. Используя же итератор, клиент получает средство сканирования лов дерева, как если бы они представляли собой линейный список, без обре-^лительных деталей алгоритмов прохождения, лежащих в основе процесса.
Итератор симметричного метода прохождения
В гл. 12 мы разработали абстрактный класс Iterator для создания множества базовых методов прохождения списков. Класс Iterator задает общий формат для методов прохождения независимо от деталей их реализации в базовом классе. В данном разделе на основе этого базового класса строится итератор симметричного бинарного дерева. Симметричное прохождение бинарного дерева поиска, в процессе которого узлы посещаются в порядке возрастания их значений, является полезным инструментом. Конструирова-вие итераторов прямого, поперечного и обратного методов прохождения предлагается в качестве упражнений.
ОБЪЯВЛЕНИЕ
// итератор симметричного прохождения бинарного дерева.
// использует базовый класс Iterator template <class Т> class Inorderiterator: public iterator<T> {
private:
// поддерживать стек адресов узлов
Stack< TreeNode <T> * > S;
И корень дерева и текущий узел
TreeNode<T> *root, *current;
11 сканирование левого поддерева, используется функцией Next TreeNode<T> *GoFarLeft(TreeNode<T> *t);
public:
// конструктор
Inorderiterator(TreeNode<T> *tree);
// реализации базовых операций прохождения virtual void Next (void);
virtual void Reset (void);
virtual T& Data(void);
// назначение итератору нового дерева ). v°id SetTree (TreeNode<T> *tree);
0°«%нив
Класс Inorderiterator построен по общему для всех итераторов образцу. ®ТоД EndOfList определен в базовом классе Iterator. Конструктор инициа-ска?>Ует 6азовый класс и с помощью GoFarLeft определяет начальный узел
’Жирования. Класс Inorderiterator находится в файле treeiter.h.
I'ord°de<int> *rOOt;
erIterator treeiter(root);
^спечатать начальный узел сканирования.
// бинарное дерево
// присоединить итератор
// для смешанного прохождения это самый левый узел дерева cout « treeiter. Data О ;
11 сканирование узлов и печать их значений	%
for (treeiter.Reset(); 1treeiter.EndOfList(); treeiter.Next()) cout « treeiter.Data() « "
Реализация класса Inorderiterator
Итерационный симметричный метод прохождения вмулирует рекур^ сканирование с помощью стека адресов узлов. Начиная с корня, осуществля* спуск вдоль левых поддеревьев. По пути указатель каждого пройденного запоминается в стеке. Процесс останавливается на узле с нулевым левым ук & телем, который становится первым посещаемым узлом в симметричном * пировании. Спуск от узла t и запоминание адресов узлов в стеке выполняет м? GoFarLeft. Вызов этого метода с t=root определяет первый посещаемый узел Од
Указатель, возвращаемый функцией GoFarLeft
// вернуть адрес крайнего узла на левой ветви узла t.
// запомнить в стеке адреса всех пройденных узлов template <class Т>
TreeNode<T> *lnorderlterator<T>::GoFArLeft(TreeNode<T> {
// если t="NULL, вернуть NULL
if (t == NULL)
return NULL;

// пока не встретится узел с нулевым левым указателем,
// спускаться по левым ветвям, запоминая в стеке S
// адреса пройденных узлов, возвратить указатель на этот узел while (t->Left() !- NULL) {
S.Push(t);
t = t->Left(); ) return t;
После инициализации базового класса конструктор присваивает эле^еТ. данных root адрес корня бинарного дерева поиска. Узел для начала сЯ с ричного сканирования получается в результате вызова функции ^°^аГ^цтиМ root в качестве параметра. Это возвращаемое значение становится теку для указателя типа TreeNode.
// инициализировать флажок iterationComplete. базовый класс
// сбрасывает его, но дерево может оказаться пустым, начальным
// узлом сканирования является самый крайний слева узел.
template <class Т>
Inorderlterator<T>::Inorderiterator(TreeNode<T> *tree):
Iterator<T>(), root(tree)
* 4terationCompiete = (root == NULL);
1 current = GoFarLeft(root)
t
рДетод Reset по существу является таким же, как и конструктор, за исклю-еяем того, что он очищает стек.
? Перед первым обращением к Next указатель current уже указывает на пер-jjg узел симметричного сканирования. Метод Next работает по следующему Алгоритму.
j 1. Если правая ветвь узла не пуста, перейти к его правому сыну и осу-i * ществить спуск по левым ветвям до узла с нулевым левым указателем, попутно запоминая в стеке адреса пройденных узлов.
2. Если правая ветвь узла пуста, то сканирование его левой ветви, самого узла и его правой ветви завершено. Адрес следующего узла, подлежащего обработке, находится в стеке. Если стек не пуст, удалить следующий узел. Если же стек пуст, то все узлы обработаны и сканирование завершено.
Итерационное прохождение дерева, состоящего из пяти узлов, изображено на следующем рисунке.
te®plate <class т>
S01d InorderIterator<T>:: Next (void)
ошибка, если все узлы уже посещались (iterationcomplete)
Cerr « "Next: итератор прошел конец списка!" « endl;
, eXit(l);
1 // Currerit “ текущий обрабатываемый узел.
Z/ е®ли есть правое поддерево, спуститься до конца по его левой ветви, । • попутно запоминая в стеке адреса пройденных узлов
I	(current->Right () != NULL)
Uftent = GoFarLeft(current->Right());
। 4 nn
/z 'Равого поддерева нет, но в стеке есть другие узлы,
.,£П°Длехащие обработке, вытолкнуть из стека новый текущий адрес
® if (1S.StackEmpty())	// продвинуться вверх по дереву
Utrent = S.PopO;
// нет ни правого поддерева ни узлов в стеке, сканирование завершено else
iterationComplete - 1;
}
Приложение: алгоритм TreeSort
Когда объект типа Inorderiterator осуществляет прохождение дерева цОк узлы -проходятся в сортированном порядке и, следовательно, можно пости С1<а’ еще один алгоритм сортировки, называемый TreeSort. Этот алгоритм пре?Ть лагает, что элементы изначально хранятся в массиве. Поисковое дерево слу& фильтром, куда элементы массива копируются в соответствии с алгоритм включения в бинарное дерево поиска. Осуществляя симметричное прохож^ ние этого дерева и записывая элементы снова в массив, мы получаем в резу^' тате отсортированный список. На рис. 13.5 показана сортировка 8-эДементнот массива. Указанный алгоритм реализуется функцией TreeSort, которая нахо дится в файле treesort.h.
#include "bstree.h”
#include "tree!ter.h"
// использование бинарного дерева поиска для сортировки массива template <class Т>	(
void TreeSort(Т arr[], int n) {
// бинарное BinSTree<T> int i;
дерево поиска, в которое копируется массив sortTree;
// включить каждый элемент массива в поисковое дерево for (i«0; i<n; i++)
sortTree.Insert(arrIi]);
// объявить итератор симметричного прохождения для sortTree Inorderlterator<T> treeSortlter(sortTree.GetRoot());
// выполнить симметричное прохождение дерева.
// скопировать каждый элемент снова в массив
i = 0; while (!treeSortlter.EndOfList()) {
arr[i++] = treeSortlter.Data();
treeSortlter.Next();
) }
Эффективность сортировки включением в дерево. Ожидаемое числ0_^го нений, необходимых для включения узла в бинарное дерево поиска» Р
^(logzn). Поскольку в дерево включается п элементов, средняя эффективность должна быть O(n log2n). Однако в худшем случае, когда исходный список отсортирован в обратном порядке, она составит О(п2). Соответствующее дерево сОиска вырождается в связанный список. Покажем, что худший случай требует 0(п2) сравнений. Первое включение требует О сравнений. Второе включение — вух сравнений (одно с корнем и одно для определения того, в какое поддерево следует вставлять данное значение). Третье включение требует трех сравнений, |.е — четырех, .... n-е включение требует п сравнений. Тогда общее число сравнений равно:
с+ 2 + 3 + ... ♦ п  (1 + 2 + 3 + ... +п) - 1 - щп+1)/2 -1 - О{п2)
Для каждого узла дерева память должна выделяться динамически, поэтому худший случай не лучше, чем сортировка обменом.
Когда л случайных значений повторно вставляются в бинарное дерево поиска, можно ожидать, что дерево будет относительно сбалансированным. Наи-дучшим случаем является законченное бинарное дерево. Для этого случая иожно оценить верхнюю границу, рассмотрев полное дерево глубиной d. На i«oM уровне (1 <i<d) имеется 2* узлов. Поскольку для помещения узла на уровень i требуется i+l сравнение, сортировка на полном дереве требует (i+1) * 2* сравнений для включения всех элементов на уровень i. Если вспомнить, что п =» 2(<н1)-1, то верхняя граница меры эффективности выражается следующим неравенством:
d	d
£ (i + 1)2 £ (d + 1) J 21 - (d + l)(2d+1 - 2) - (d + l)(2d+1 - 1 - 1)
1=1	1=1
и (d + l)(n - 1) = (n - 1) log2(n + 1) - 0(n log2n)
Таким образом, эффективность алгоритма в лучшем случае составит O(n loggn).
13.8. Графы
Дерево есть иерархическая структура, которая состоит из узлов, исходящих от корня. Узлы соединяются указателями от родителя к сыновьям. В этом разделе мы познакомимся с графами, которые являются обобщенными иерархическими структурами. Граф состоит из множества элементов данных, называемых вершинами (vertices), и множества ребер (edges), соединяющих эти вершины попарно. Ребро Е = (Vi, Vj) соединяет вершины Vi и Vj.
Вершины - {Vo> V„ V2, V8, .... VmJ}
Ребра = {Eq, Ei. E2, E2. .... Ea.i}
Пусть вершины обозначают города, а ребра — дорожное сообщение между В11ми. Движение по дорогам может происходить в обоих направлениях, и поэ-
Вершины V = (A,B,CD,EJ
Ребра Е =• {(A,B),(A,C).(A.D).(B.A),(B Ы (к « (CA).(D.B).(D.E).(E,B).(E.b)} lE)-
Вершины V «{A.B.C.D.E}
Ребра Е = {(A,C)XA.D),(B,A).(B.D).(D,E),(E,B)}
Рис.13.6. Направленный и ненаправленный графы
тому ребра графа G не имеют направлений. Такой граф называется ненапрвлед ным (undirected graph).
Если ребра представляют систему связи с однонаправленными информационными потоками, то граф в этом случае становится направленным графом (directed graph), или орграфом (digraph). На рис. 13.6 показаны графы обоих типов. Мы сосредоточим внимание на орграфах.
В орграфе ребро задается парой (Vif Vj), где Vi — начальная вершина, а Vj — конечная вершина. Путь (path) P(VS, VE) есть последовательность вершин VS“VR, VK+1, ...» Vr+t=Vb, где Vs — начальная вершина, VE — конечная вершина, а каждая пара членов последовательности есть ребро. В орграфе указывается направленный путь от VB к VE, но пути от VE к VB может и не быть. Например, для орграфа на рис. 13.6
Путь(А,В) - {A,D,E,B}
Путь(Е,С) =- {Е,В,А,С}
Путь(В,А) = (В,А}
Путь(С,Е> - {}	// пути нет
Связанные компоненты
С понятием пути связано понятие связанности орграфа. Две вершины V, и Vj связаны (connected), если существует путь от Vi к Vj. Орграф является сильно связанным (strongly connected), если в нем существует направленный путь от любой вершины к любой другой. Орграф является слабо связанным (weakly connected), если для каждой пары вершин Vj и Vj существует на правленный путь P(Vi, Vj) или P(Vj, Vj). Связанность графов иллюстрируете на рис. 13.7.
(О
(С) Слабо связанна
(А) Не сильно или слабо связанньй (В) Сильно связанный
Рис. 13.7. Сильно и слабо связанные компоненты орграфа
Мы расширили понятие сильносвязанных вершин до сильно связанной ком-10центы (strongly connected component) — максимального множества вершин Jyj, где для каждой пары Vi и Vj существует путь от Vj к Vj и путь от Vj к Vi. тТйКЛ (cycle) — это путь, проходящий через три или более вершины и связы-ающий некоторую вершину саму с собой. В ориентированном графе (С) на ® с. 13.7 существуют циклы для вершин А (А—>С->В—>А), В и С. Граф, не ^держащий циклов, называется ациклическим (acycle).
Во взвешенном орграфе (weighted digraph) каждому ребру приписано значение, или вес. На транспортном графе веса могут представлять расстояния между городами. На графе планирования работ веса ребер определяют продолжительность конкретной работы.
Бетони- Подключение рованме канализации
Покрытие крыши
13.9. Класс Graph
В этом разделе мы опишем структуру данных для взвешенного орграфа. Начнем с математического определения графа как основы абстрактного типа данных (ADT) Graph. Вершины задаются в виде списка элементов, а ребра — в вцде списка упорядоченных пар вершин.
Объявление абстрактного типа данных Graph
Взвешенный орграф состоит из вершин и взвешенных ребер. ADT включает 3 себя операции, которые будут добавлять или удалять эти элементы данных. Для каждой вершины Vj определяются все смежные с ней вершины Vjf Которые соединяются с Vi ребрами E(Vit Vj).
Graph
Д&нныо
Множество вершин {vp и ребер {Ejj. Ребро есть пара (vi# ур, которая Указывает на связь вершины Vt с вершиной Vj. Приписанный каждому ребру вес определяет стоимость прохождения по этому ребру.
Горации Конструктор Вход:	нет
Обработка: Ins&rtVertex Вход:	Создает граф в виде множества вершин и ребер. Новая вершина.
Предусловия: Обработка: Выход:
Постусловия: InsertEdge
Вход:
Предусловия:
Обработка:
Выход:
Постусловия: DeleteVertex
Вход:
Предусловия: Обработка:
Выход:
Постусловия:
DeleteEdge Вход: Предусловия: Обработка:
Выход:
Постусловия: GetNeighbors
Вход:
Предусловия: Обработка:
Выход:
Постусловия:
GetWeight
Вход:
Предусловия: Обработка: Выход:
Постусловия:
Конец ADT Graph
Нет
Вставить новую вершину в множество вершин.
Нет
Список вершин увеличивается.
Пара вершин V± и Vj с весом W.
v± и Vj должны принадлежать множеству вершин, а рес (v1# Vj) не должно принадлежать множеству ребер.
Вставить ребро (vv Vj) с весом и в множество ребер Нет
Множество ребер увеличивается.
Ссылка на вершину VD.
Входная вершина должна принадлежать множеств^ вершин
Удалить вершину vD из списка вершин. Удалить все входя и исходящие ребра этой вершины.	е
Нет
Множество вершин и множество ребер модифицируются.
Пара вершин V± и Vj.
Входные вершины должны принадлежать множеству вершин.
Если ребро (vlr Vj) существует, удалить его из множества ребер.
Нет
Множество ребер модифицируется.
Вершина v.
Нет
Идентифицировать все смежные с V вершины VE, такие, что (V, VE) есть ребро.
Список смежных вершин.	I
Нет
Пара вершин VA и Vj.
Входные вершины должны принадлежать множеству вершин.
Выдать вес ребра (V±, Vj), если оно существует.
Вес ребра или 0, если ребра не существует.
Нет
Представление графов. Существует много способов представления °Р^ рафов. Можно просто хранить вершины в виде последовательного сп Vo, Vi....Vm-1, а ребра задавать квадратной матрицей размером ш
называемой матрицей смежности (adjcency matrix). Здесь строка 1 и бец j соответствуют вершинам Vi и Vj. Каждый элемент (i, j) этой маТ^длЯ содержит вес ребра Eij = (Vi, Vj) или 0, если такого ребра нет. невзвешенного орграфа элементы матрицы смежности содержат 0 ** gB0-показывая отсутствие или наличие соответствующего ребра. Ниже г дятся примеры орграфов со своими матрицами смежности.
(A)
0 2 10 0
0 0 S 0 0
0 4 0 0 0
0 0 7 0 0
0 3 0 0 0
0 1110 10 10 0
1 0 0 0 0 0 0 0 0 1 О 0 1 о о
В другом способе представления графов каждая вершина ассоциируется со связанным списком смежных с ней вершин. Эта динамическая модель хранит информацию лишь о фактически принадлежащих графу вершинах. Для взвешенного орграфа каждый узел связанного списка содержит поле веса. Примеры спискового представления орграфов даны ниже.
Вершины Список смежных вершин
Вершины Список смежных вершин
Класс Graph, рассматриваемый в этом разделе, использует матричное пред-Зление ребер. Мы используем статическую модель графа, которая пред-олаГает конечное число вершин. Матричное представление упрощает реа-зьцяю класса и позволяет сосредоточиться на целом ряде алгоритмов об-ботки графов. Реализация на основе связанных списков предлагается в
упражнениях. Основными особенностями класса Graph являются цре ление ADT Graph, метод ReadGraph и ряд поисковых алгоритмов, ocvn"^’ вляющих прохождение вершин способами "сначала в глубину” и "св г в ширину". Данный класс включает также итератор списка вершив использования в приложениях.
Спецификация класса Graph
ОБЪЯВЛЕНИЕ
const int MaxGraphSize -25;
template «class T>
class Graph (
private:
// основные данные включают список вершин, матрицу смежности
И и текущий размер (число вершин) графа
SeqList<T> vertexList;
int edge [MaxGraphSize];
int graphsize;
// методы для поиска вершины и указания ее позиции в списке int FindVertex(SeqList<T> &L, const T& vertex);
int GetVertexPos(const T& vertex);
public:
// конструктор Graph(void);
// методы тестирования графа int GraphEmpty(void) const; int GraphFull(void) const;
// методы обработки данных
int NumberOfVertices(void) const;
int NumberOfEdges(void) const;	i
int GetUeight(const T& vertexl, const T& vertex2);
SeqList<T>& GetNeighbors(const Ts vertex);
ft методы модификации графа
void Insertvertex(const Ts vertex);
void InsertEdge(const T& vertexl, const T& vertex2, int weight);
void DeleteVertex(const T* vertex);
void DeleteEdge(const T& vertexl, const T& vertexZ);
// утилиты
void ReadGraph(char •filename);
int MinimumPath(const T& sVertex, const TS sVertex);
SeqList<T>& DepthFirstSearch(const Ts beginVertex);
SeqList<T>& BreadthFirstSearch(const T& beginVertex);
// итератор для обхода вершин
friend class VertexIterator<T>;
>;
ОПИСАНИЕ
Данные-члены класса включают вершины, хранящиеся в виде посЛ-^еЙ тельного списка, ребра, представленные двумерной целочисленной Mfl,rpgSa-смежности, и переменную graphsize, являющуюся счетчиком верпл®* чение graphsize возвращается функцией NumberOfVertices.
граф с символьными вершинами ввести данные из graph.dat
Утилита FindVertex проверяет наличие вершины в списке L и используется ₽ поисковых методах. Метод GetVertexPos вычисляет позицию вершины vertex в vertexList. Эта позиция соответствует индексу строки или столбца в матрице нежности.
Методу ReadGraph передается в качестве параметра нмя файла с входным описанием вершин и ребер графа.
Класс Vertexiterator является производным от класса SeqListlterator и позволяет осуществлять прохождение вершин. Итератор упрощает приложения.
ПРИМЕМ
Graph <char> G;	//
G. ReadGraph("graph.dat");	//
// пример входного описания графа количество вершин> 4 рершинаО	А
рершина1	В
Зершина2	С
ВершинаЗ	D
«Количество ребер> 5 реброО ВесО	А С 1
ребро!	Bed \	A	D	1
ребро2	Вес2	В	А	1
реброЗ	ВесЗ	С	В	1
Ребро4	Вес 4	D	А	1
vertexlterator<char> viter(G); SeqLis t<char>L ;
for (viter.Reset(); !viter.EndOfList(); viter.Next()) (
cout « "Вершины, смежные с вершиной " « viter.Data() «	";
L « G.GetNeighbcrs(viter.Data()); // распечатать смежные вершины SeqListIterator<char> liter(L);	// список смежных вершин
for (liter.Reset (); !liter.EndofList()г liter.Next()) cout « liter.Data() « " }
// итератор для вершин
Реализация класса Graph
Конструктор класса Graph ’’отвечает" за инициализацию матрицы смежнос-размера MaxGraphSize х MaxGraphSize и обнуление переменной graphsize. Конструктором обнуляется каждый элемент матрицы для указания на отсутствие ребер.
Ч конструктор, обнуляет матрицу смежности и переменную graphsize opiate <class T> paPh<T>:: Graph (void)
f°r (int i-0; i<MaxGraphSize; i++)
£©r (int j-0; j«MaxGraphSize; j++)
„ ®<3ge(i] [j] - 0;
I ytaphsize  0;
Подсчет компонентов графа. Переменная graphsize хранит размер списка РШин. Обращение к этому закрытому члену класса осуществляется посредством ^Да NumberOfVertices. Оператор GraphEmpty проверяет, пуст ли список.
Доступ к компонентам графа. Компоненты графа содержатся в спие шин и матрице смежности. Итератор вершин, являясь дружественным*8 8ef * ношению к классу Graph, позволяет сканировать список вершин. Этот °7' тор — наследник класса SeqListlterator.	’ , т
template ass Т> class Vertexiterator: public SeqList!terator<T> <
public:
Vertexiterator(Graph<T>b G);
>;
Конструктор просто инициализирует базовый класс для прохождения с ка вершин vertexList.	СПйс-
template ass Т>
VertexIterator<T>::Vertexlteratcr(Graph<T>b G):
SeqListIterator<T> (G.vertexList) {)
Итератор сканирует элементы vertexList и используется для реализации функции GetVertexPos, которая осуществляет сканирование списка вершин и возвращает позицию вершины в этом списке.
template ass т>
int Graph<T>::GetVertexPos(const T& vertex) (
SeqListIterator<T> liter(vertexList);
int pos  0;
while(!liter.EndOfList()	liter.Data() 1" vertex) "
(
pos++;
liter.Next()i
) return pos; )
Метод GetWeight возвращает вес ребра, соединяющего vertexl и vertex2. Чтобы получить позиции этих двух вершин в списке, а следовательно, и строку со столбцом в матрице смежности, используется функция GetVertexPos. Если любая из двух вершин отсутствует в списке вершин, метод возвращает -1«
Метод GetNeighbors создает список вершин, смежных с vertex. Этот список является выходным параметром и может быть просканирован с помощью итератора последовательных списков. Если vertex не имеет смежных вершив, метод возвращает пустой список.
// возвратить список смежных вершин template <class Т>
SeqList<T>4 Graph::GetNeighbors(const ть vertex) {
Se<JList<T> *L;
SeqListIterator<T> viter(vertexList);
П создать пустой список
L	= new SeqList<T>;
// позиция в списке, соответствующая номеру строки матрицы смежности
int pos • GetVertexPos(vertex);
11	если вершины vertex нет в списке вершин, закончить if (роз -• -1)
(
cerr « "GetNeighbors: такой вершины нет в графе." « endl;
return *L;	// вернуть пустой список
сканировать строку матрицы смежности и включать в список
!/ все вершины, имеющие ребро ненулевого веса из vertex
fOr (int i=0; Kgraphsize; i++)
1 if (edge(pos)[ij > 0)
L->lnsert(viter.Data());
viter.Next();
return *L;
)
Обновление вершин и ребер. Чтобы вставить ребро, мы используем GetVertexPos для проверки наличия vertexl и vertex2 в списке вершин. Если какая-либо из них не будет обнаружена, выдается сообщение об ошибке и осуществляется возврат управления. Если позиции post и pos2 получены, метод InsertEdge записывает вес ребра в элемент (posl, роз2) матрицы смежности. Эта операция выполняется за время О(п), поскольку каждый вызов GetVertexPos требует О(п) времени.
Метод DeleteVertex класса Graph удаляет вершину из графа. Если вершины нет в списке, выдается сообщение об ошибке и осуществляется возврат управления. В противном случае удаляются все ребра, соединяющие удаляемую вершину с другими вершинами. При этом в матрице смежности должны быть скорректированы три области. Поэтому эта операция выполняется за время 0(п2), так как каждая область является частью матрицы п х п.
Область I: Сдвинуть индекс столбца влево
Область II: Сдвинуть индекс строки вверх и индекс столбца влево
Область III: Сдвинуть индекс строки вверх
Удалить вершину из списка вершим и скорректировать матрицу смежности, удали принадлежащие этой вершине ребра
.^n'₽late <class Т>
, Graph<T>: :DeleteVertex(const TS vertex)
Ч получить позицию вершины в списке вершин
*nt pos = GetVertexPos (vertex) ;
lnt row, col;
1	если такой вершимы нет,
1	(pos «- -1)
сообщить об этом и вернуть управление
(
се г г « "Delete Vertex: вершины нет графы" « endl; return;
}
// удалить вершину и уменьшить graphsize vertexList.Delete(vertex);
graphsize—;
// матрица смежности делится на три области
for (row=0; row<pos; row++)	// область I
for (col-pos+1; col<graphsize; col++)
edge(row][col-1] - edge[row][col];
for (row=pos+l; row<graphsize; row++) // область II
for (col-pos+1; coKgraphsize; col++)
edge[row-1][col-1] = edge[row][col];
for (row=pos+l; row<graphsize; row++) // область III
for (col“0; col<pos; col++}
edge[row-1][col] = edge[row][col];
Удаление ребра производится путем удаления связи между двумя вершина ми. После проверки наличия вершин в vertexList метод DeleteEdge присваивает данному ребру нулевой вес, оставляя все другие ребра неизменными. Если такого ребра нет в графе, процедура выдает сообщение об ошибке и завершается.
Способы прохождения графов
Для прохождения нелинейных структур требуется разработать стратегию доступа к узлам и маркирования узлов после обработки. Поисковые методы для бинарных деревьев имеют свои аналоги для графов. В нисходящем обходе бинарного дерева применяется такая стратегия, при которой сначала выполняется обработка узла, а затем уже продвижение вниз по поддереву. Обобщением прямого метода прохождения для графов является поиск "сначала в глубину” (depth-first). Начальная вершина передается в качестве параметра и становится первой обрабатываемой вершиной. По мере продвижения вниз до тупика смежные вершины запоминаются в стеке, с тем чтобы можно было к ним вернуться и продолжить поиск по другому пути в случае, если еще остались необработанные вершины. Обработанные вершины образуют множест всех вершин, достижимых из начальной вершины.	а
Характерное для деревьев поперечное сканирование начинается с К°Р® ’ я обход узлов осуществляется уровень за уровнем сверху вниз. Анооги4 стратегия применяется при поиске "сначала в ширину” (breadth-first) ва
лах, когда, начиная с некоторой начальной вершины, производится обработка каждой смежной с ней вершины. Затем сканирование продолжается на следующем уровне смежных вершин и т.д. до конца пути. При этом для запоминания неясных вершин используется очередь. Проиллюстрируем оба поисковых алго-
на следующем графе. В данном случае начальной вершиной является А.
Поиск ’’сначала в глубину". Для хранения обработанных вершин используйся список L, а для запоминания смежных вершин — стек S. Поместив начальную вершину в стек, мы начинаем итерационный процесс выталкивания рершины из стека и ее обработки. Когда стек становится пустым, процесс завершается и возвращает список обработанных вершин. На каждом шаге используется следующая стратегия.
Вытолкнуть вершину V из стека и проверить по списку L, была ли она обработана. Если нет, произвести обработку этой вершины, а также воспользоваться удобным случаем и получить список смежных с ней вершин. Включить V в список L, чтобы избежать повторной обработки. Поместить в стек те смежные с V вершины, которых еще нет в списке L.
В нашем примере предполагалось, что вершина А является начальной. Поиск начинается с выталкивания А из стека и обработки этой вершины. Затем д включается в список обработанных вершин, а смежные с ней вершины В и G помещаются в стек. После обработки А стек S и список L выглядят следующим образом:
G __________
В | А
Стек S Список L
Итерация продолжается. Вершина G выталкивается из стека. Так как этой вершины пока нет в списке L, она включается в него, а в стек помещается единственная смежная с ней вершина F:
F
В
Список L
Стек5
Вытолкнув из стека вершину F и поместив ее в список L, мы достигаем тупика, поскольку смежная с F вершина А уже находится в L. В стеке остается вершина В, которая была идентифицирована как смежная с А на первой фазе Поиска:
G
А | G | F Список L
Стек 5
Вершины В и С обрабатываются в указанном порядке. Теперь стек содержит ВеРШины D и Е, являющиеся смежными с вершиной С:
Е
D
Стек5
F
С
А
G | F | В Список L вершины Е две смежные вершины: D и F, и обе они являются подходя-для записи в стек. Однако F уже была обработана на пути A-G-F и
22 Зак. 425
поэтому пропускается. Зато вершина D помещается в стек дважды, поск ваш алгоритм не "знает’’, что D достижима иа С:
D D
Стек5
Список L
Поиск завершается после обработки вершины D. Второй экземпляр D игнорируется, так как эта вершина уже находится в списке L:

A	G	F	В	c	E	D
Список L
// начиная с начальной вершины, сформировать список вершин, // обрабатываемых в порядке обхода "сначала в глубину" template <class Т>
SeqList <Т> & Graph<T>::DepthFirstSearch(const TS beginVertex} {
// стек для временного хранения вершин, ожидающих обработки Stack<T> S;
// L - список пройденных вершин. adjL содержит вершины, // смежные с текущей. L создается динамически, поэтому можно И возвратить его адрес
SeqList<T> *L, adjL,-
// iteradjL - итератор списка смежных вершим SeqListIterator<T> iteradjL(adjL);
Т vertex;
// инициализировать выходной список.
// поместить начальную вершину в стек
L = new SeqList<T>;
S.Push(beginVertex);
// продолжать сканирование, пока не опустеет стек
while (!S.StackEmpty())	1
(	I
// вытолкнуть очередную вершину	к
vertex - S.Pop();
// проверить ее наличие в списке L if (lFlndVertex(*L, vertex))
<
И если нет, включить вершину в L,
//а также получить все смежные с ней вершины
(*L).Insert(vertex);
adjL ~ GetNeighbors(vertex);
// установить итератор на текущий adjL iteradjL.SetList(adjL);
// сканировать список смежных вершин.
// помещать в стек те из них, которые отсутствуют в списке L for (iteradjL.Reset(); IiteradjL.EndOfListО; iteradjL.NextО)
if (I Findvertex(*L, iteradjL.Data())) S.Push(iteradjL.Data());
) ) // возвратить выходной список return *L;
)
Поиск "сначала в ширину". Как и в поперечном прохождении бинарного дерева, при поиске "сначала в ширину" для хранения вершин используется очередь, а не стек. Итерационный процесс продолжается до тех пор, пока очередь не опустеет.
Удалить вершину V из очереди и проверить ее наличие в списке обработанных вершин. Если вершины V нет в списке L, включить ее в этот список. Одновременно получить все смежные с V вершины и вставить в очередь те из них, которые отсутствуют в списке обработанных вершин.
Если применить этот алгоритм к рассмотренному в предыдущем примере графу, то вершины будут обрабатываться в следующем порядке:
ABGCFDE
Анализ сложности. В описанных алгоритмах поиска посещение каждой вершины требует времени вычислений О(п). При добавлении вершины в список обработанных вершин для обнаружения смежных с ней вершин проверяется строка матрицы смежности. Каждая строка — это О(п), следовательно общее время вычислений равно п*О(п) = О(п2). Число сравнений, требующихся в случае матричного представлении графа, не зависит от количества ребер в графе. Даже если в графе относительно мало ребер ("разреженный граф”)) мы обязаны произвести и сравнений для каждой вершины. В списковом представлении графа быстродействие алгоритма поиска зависит от плотности ребер в графе. В лучшем случае ребер нет и длина каждого списка смежных вершин равна 1. Тогда время вычислений для каждого поиска будет О(п+п) = О(п). В худшем случае каждая вершина связана с каждой и длина каждого списка смежных вершин равна п. Тогда алгоритм поиска имеет порядок О(п2).
Приложения
Вспомним, что орграф является сильно связанным, если существует направленный путь от любой его вершины к любой другой. Сильная компонента (strong component) есть подмножество вершин, сильно связанных друг с Другом. Сильно связанный граф имеет одну сильную компоненту, но всякий граф может быть разбит на ряд сильных компонент. Например, на рис. 13.8 граф разбит иа три сильных компоненты.
В теории графов для определения сильных компонент используются классические алгоритмы. В данном приложении мы используем для этого поиск сначала в глубину”. Функция PathConnect проверяет существование направленного пути от вершины v к вершине w и возвращает булевские TRUE и false, соответственно.
template cclass T>
int PathConnect (Graph<T> sG, Tv, T w) <
SeqList<T> L;
// найти вершины, связанные c v
L - G.DepthFirstSearch(v);
Уl если w в их числе, вернуть трое if (L.Find(w)) return 1;
else
return 0,-
Функция ConnectedComponent начинает работу с пустого списка верщя именем markedList. Этот список все время содержит коллекцию вершин и с торые были обнаружены в сильной компоненте. Итератор осуществляет обх° вершин графа. Каждая вершина V проверяется на наличие в списке markedList* Если ее там нет» то должна быть построена новая сильная компонента, соде» жащая V. Список scList, который будет содержать новую сильную компоненту очищается, и с помощью поиска "сначала в глубину" формируется список L всех вершин, достижимых из V. Для каждой вершины списка L с помощью функции PathConnect проверяется существование пути обратно к V. Если таковой существует, вершина включается в scList и в markedList. Обратите внимание, что вершина вставляется в оба этих списка. Поскольку существует путь от V к каждой вершине из scList и путь от каждой вершины из scList обратно к V, то, следовательно, существует путь между любыми двумя вершинами из scList. Эти вершины и есть очередная сильная компонента. Так как каждая вершина в scList присутствует также в markedList, она не будет рассматривать
ся повторно.
template cclass Т>
void ConnectedComponent (Graph<T> fiG) {
Vertexlterator<T> viter(G);
SeqList<T> markedList, scList, L, K;
fox (viter.Reset(); !viter.EndOfList(); viter-Next!))
{ (
11	проверять в цикле каждую вершину viter.DataО
if (.'markedList. Find (viter. Data ()))
It если не помечен, включить в сильную компоненту {
scList.ClearList();
11 получить вершины, достижимые из viter.DataО
L - G.DepthFirstSearch(viter.Data());
// искать в списке вершины, из которых достижима вершина viter.DataО SeqListIterator<T> liter(L);
for (liter.Reset(); !liter.EndOfList(), liter.Next())
if (PathConnect(G, liter.Data(), viter.Data(J))
11 вставить вершины в текущую сильную компоненту и в markedList scList.Insert(liter.Data() >;
markedList.Insert(liter.Data());
)
PrintList(scList); // распечатать сильную компоненту cout « endl;
) ..
1
Программа 13.5. Сильные компоненты
Эта программа находит сильные компоненты в графе, изображенном на рис. 13.8. Граф вводится из файла sctest.dat с помощью ReadGraph. функции PathConnect, ConnectedComponent и PrintList находятся в файле conncomp. h.
flinclude <iostream.h> flinclude <fstream.h>
flinclude "graph-h" flinclude "coimcomp.h"
void main (void)
{
Graph<char> G;
G.ReadGraph("sctest.dat");
cout « "Сильные компоненты:" « endl;
ConnectedComponent(G);
сПрогон программы 13.5>
Сильные компоненты:
ABC
D G F
E
*/
Минимальный путь. Методы прохождения "сначала в глубину" и "сначала в ширину" находят вершины, достижимые из начальной вершины. При этом движение от вершины к вершине не оптимизируется в смысле минимального пути. Между тем во многих приложениях требуется выбрать путь с минимальной "стоимостью”, складывающейся из весов ребер, составляющих путь. Для решения этой задачи мы представляем класс Pathinfo. Объект, порождаемый этим классом, описывает путь, существующий между двумя вершинами, и его стоимость. Объекты типа Pathinfo запоминаются в очереди приоритетов, которая обеспечивает прямой доступ к объекту с минимальной стоимостью.
template <class Т> struct Pathinfo
т startv, endV;
^efnplate <class T>
operator <- (const PathInfo<T>4 a, const PathInfo<T>s b) . *eturn a.cost <= b.cost;
Так как между вершинами графа может существовать несколько разных Утей, объекты типа Pathinfo могут соответствовать одним и тем же вершинам, о Иметь разные стоимости. Например, в показанном ниже графе между вер-^ивами А и D существуют три пути с различными стоимостями.
Для сравнения стоимостей в классе Pathinfo определен оператор « Алгоритм проверяет объекты типа Pathinfo, хранящиеся в очереди приоо тетов, и выбирает объект с минимальной стоимостью. Определение мини' мального пути между начальной (sVertex) и конечной (eVertex) вершинами иллюстрируется иа следующем графе. Если между ними нет вообще никакого пути, алгоритм завершается выдачей соответствующего сообщения. Пусть вершина А будет начальной, a D — конечной.
Начать с создания первого объекта типа Pathinfo, соединяющего начальную вершину саму с собой при нулевой начальной стоимости. Включить объект в очередь приоритетов.
I A-а I о
Очередь приоритетов
Для нахождения минимального пути мы следуем итерационному процессу, который удаляет объекты из очереди приоритетов. Если конечная вершина в объекте есть eVertex, то мы имеем минимальный путь, стоимость которого находится в поле cost. В противном случае просматриваем все вершины, смежные с текущей конечной вершиной endV, и в искомый путь, начинающийся из sVertex, включается еще одно ребро.
В нашем примере мы хотим найти минимальный путь от А до D. Удаляе^ единственный объект Pathinfo, в котором endV = А. Если бы вершина являлась заданной конечной вершиной eVertex, процесс завершился »ы нулевой минимальной стоимостью. Поскольку вершина А ие есть ever» она запоминается в списке L, содержащим все вершины, до которых м . мальный путь из А известен. Смежными с А вершинами являются верШ1^ В, С и Е. Для каждой из них создается объект типа Pathinfo, и все объекты помещаются в очередь приоритетов. Стоимость пути из А до ка»м из этих вершин равна
стоимость(A, endV) + вес(endV, <смежная вершина>)
Объект Pathinfo	startV	endV	Стоимость
Од,в	А	В	4
О А, С	А	С	12
Оа.,е	А	Е	4
Объекты включаются в очередь приоритетов в следующем порядке:
| А —В | 4 |	| А-Е 4 А-С 1Z
Очередь приоритетов
На следующем шаге объект Од,в удаляется из очереди приоритетов. В яем вершина В есть endV с минимальной стоимостью 4. Поскольку вершины В нет в списке L, она включается в него. Ясно, что не существует последующего пути от А к В со стоимостью меньшей, чем 4. Если бы существовал путь А-Х-...-В и смежная с А вершина X находилась бы от нее иа расстоянии меньшем, чем 4, то вершина X оказалась бы первой в очереди приоритетов и была бы удалена оттуда раньше вершины В.
Смежными с В вершинами являются А, С и D. Так как А уже в списке L, объекты типа Pathinfo создаются для вершин С и D и включаются в очередь приоритетов.
Объект Pathinfo	startV	endV	Стоимость
Ов,с	В	С	10=4+6
Ob,d	В	С	12=4+8
Результирующая очередь приоритетов содержит четыре элемента. Обратите внимание, что два разных объекта заканчиваются в вершине С. Минимальный из них, имеющий стоимость 10, был только что добавлен в очередь приоритетов и представляет путь А-В-С. Прямой путь А-С, определенный на первом шаге, имеет стоимость 12.
|а-е
4
| А-С 1z[ [ В -С |ю
Очередь приоритетов
В -0| 12
После удаления объекта ОА В и установления минимальной стоимости пути ОТ А к Е равной 4 создается объект OE D стоимостью 14.
[ В-С [io| | А-С tt] [ В —D 112 [ I Е —D 14
Очередь приоритетов
После очередного удаления объекта с минимальной стоимостью, которым являлся Ов с, вершина С может быть добавлена в список L, так как 10 является минимальной стоимостью пути от А к С.
Поскольку конечной вершиной искомого минимального пути является D, ожидаем удаления объекта с endV=D. У вершины С есть смежные вершины В и D. Так как В уже обработана, в очередь приоритетов включается т°лько объект OC,D.
Объект Pathinfo startV endV	Стоимость
Oqu	С	О	24=10+14
После удаления объекта ОА,с из очереди приоритетов ои‘ отбрасыва так как С уже есть в списке. Теперь очередь приоритетов имеет три элеме^*’
[ В-D 112[	| E-D [14
I С~° I24
Очередь приоритетов
Удаляя OBiD из очереди приоритетов, мы тем самым устанавливаем нимальную стоимость пути от А к D равной 12.

template <class Т>
int Graph<T>::MinirnumPath(const T4 sVertex, const Tfi eVertex) <
// очередь приоритетов, в которую помещаются объекты,
И несущие информацию о стоимости путей из svertex
PQueue< PathInfo<T> > PQ(HaxGraphSize);
// используется при вставке/удалеиии объектов Pathinfo
//в очереди приоритетов
Pathinfo<T> pathData;
// L — список всех вершин, достижимых из svertex и стоимость
// которых уже учтена. adjL - список вершин, смежных с посещаемой
// в данный момент, для сканирования adjL используется итератор
// adjLiter
SeqList<T> L, adjL;
4 SeqListTterator<T> adjLiter(adjL);
T sv, ev;
int mincost;
// сформировать начальный элемент очереди приоритетов
pathData. startv - svertex;	**	,
pathData .endV • svertex;	'	/
// стоимость пути из svertex в svertex равна 0 pathData.cost • 0; PQ.PQInsert(pathData);
11 обрабатывать вершины, пока не будет найден минимальный путь //к eVertex или пока не опустеет очередь приоритетов while (IFQ.PQEmpty())	i
{	I
// удалить элемент приоритетной очереди и запомнить * 4 * * * * * * 11
// его конечную вершину и стоимость пути от svertex pathData  PQ.FQDelete();
ev • pathData.endV;
mincost ” pathData.cost;
Il если это eVertex,
//то минимальный путь от svertex к eVertex найден if (ev — eVertex)
break;__
// если конечная вершина уже имеется в L, //не рассматривать ее снова if (!FindVertex(L, ev)) (
// Включить ev в список L
L.Insert(ev);
11 найти все смежные c ev вершины. Для тех из них,
// которых нет в L, сформировать объекты Pathinfo с начальными // вершинами, равными ev, и включить их в очередь приоритетов sv « ev;
adjL - GetNeighbors (sv),-
Н новый список adjL сканируется итератором adj Liter
adjLiter.SetList(adjL);
for (adjLiter.Beset(): !adjLiter.EndofList(); adjLiter.Next()) (
ev - adjLiter.Data(); if (!Findvertex(L,ev)) (
// создать новый элемент приоритетной очереди
pathData.startv = sv;
pathData.endV - ev;
// стоимость равна текущей минимальной стоимости
// плюс стоимость перехода от sv к ev
pathData.cost - mincost + GetWeight(sv, ev);
PQ.PQInsert(pathData);
)
if (ev — eVertex) return mincost;
else
return -1;
Программа13.6. Система авиаперевозок
Транспортная система авиакомпании имеет список городов на некотором маршруте полетов. Пользователь вводит исходный город, а процедура определения минимального пути выдает кратчайшие расстояния между этим городом и всеми прочими пунктами назначения. Эта авиалиния соединяет главные города на Западе.
•include <fstream.h>
•include "strclass.h”
•include "graph.h”	// метод MinimumPath
^°id main (void)
// вершины типа символьных строк (названия городов) Graph<String> G;
String S;
// ввод описания транспортного графа G.ReadGraphf’’airline.dat’’);
// запросить аэропорт отправления
cout « "Выдать мин. расстояние при отправлении из *’; cin » S;
//с помощью итератора пройти список городов и определить // мин. расстояния от точки отправления VertexIterator<String> viter(G);
for (viter.Reset(); !viter.EndOfList(); viter.Next()) cout « "Минимальное расстояние от аэропорта " « S «
« ” до аэропорта " « viter.Data() « " = •’ « G.MininumPath(S, viter.Data()) « endl;
}
/*
<Прогой #1 программы 13.6>
Выдать минимальное расстояние при отправлении из Солт-Лэйк-Сити
Мин. расстояние от аэропорта Солт-Лэйк-Сити до аэропорта Солт-Лэйк-Сити = о
Мин. расстояние от аэропорта Солт-Лэйк-Сити до аэропорта Альбукерк = 604
Мин. расстояние от аэропорта Солт-Лэйк-Сити до аэропорта Феникс =648
Мин. расстояние от аэропорта Солт-Лэйк-Сити до аэропорта Сан-Франциско = 752
Мин. расстояние от аэропорта Солт-Лэйк-Сити до аэропорта Сан-Диего = 1003
<Прогон #2 программы 13.6>
Выдать мин. расстояние при отправлении из Сан-Франциско
Мин. расстояние от аэропорта Сан-Франциско до аэропорта Солт-Лэйк-Сити = 752
Мин. расстояние от аэропорта Сан-Франциско до аэропорта Альбукерк = 1195
Мин. расстояние от аэропорта Сан-Франциско до аэропорта Феникс =763
Мин. расстояние от аэропорта Сан-Франциско до аэропорта Сан-Франциско - 0
Мин. расстояние от аэропорта Сан-Франциско до аэропорта Саъ(-Диего - 504 */
Достижимость и алгоритм Уоршалла
Для каждой пары вершин некоторого графа говорят, что V, достижима из Vj тогда и только тогда, когда существует направленный путь от Vi к Vj. Это определяет отношение достижимости В (reachability relation В)* Для каждой вершины Vi поиск "сначала в глубину” находит все вершины, достижимые из Vj. При использовании поиска "сначала в ширину" получается серия списков достижимости, которые образуют отношение R:
Vi: <Список достижимости для Vi>
V2: <СпИСОК ДОСТИЖИМОСТИ ДЛЯ V2>
Vn: <Список достижимости для Vn>
Это же отношение можно также описать с помощью матрицы ДостиЯ^ мости (reachability matrix) размером n X п, которая содержит 1 в П°3ясКи (i,j), представляя тем самым VL R Vj. В следующем примере показаны с и матрица достижимости для изображенного здесь графа.
A
Списки достижимости
А: А В С D
В:	В D
С:	С В
D:
Матрица достижимости
1111 0 10 1 0 110 0 0 0 1
Матрицу достижимости можно использовать для проверки существования пути между двумя вершинами. Если элемент (i,j) равен 1, то существует минимальный путь между V, и Vj. Вершины в списке достижимости можно использовать для наращивания ребер в исходном графе. Если существует путь из вершины v к вершине w (w достижима из v), мы добавляем ребро E(v,w), соединяющее эти две вершины. Расширенный граф G1 состоит из вершин V графа G и ребер, связывающих вершины, которые соединены направленным путем. Такой расширенный граф называется транзитивным замыканием (transitive closure). Ниже приводится пример графа и его транзитивного замыкания.
Задача нахождения списка достижимости с помощью поиска "сначала в глубину" предлагается читателю в качестве упражнения. Более изящный подход применяется в знаменитом алгоритме Стефана Уоршалла. Матрица Достижимости некоторого графа может быть построена путем присвоения 1 каждой паре вершин, связанных общей вершиной. Предположим, мы строим матрицу достижимости R и вершинам а, Ь, с соответствуют индексы i, j, k.
Если R[i][j] “ 1 и R[i][k] = 1, установить R[i][j] = 1.
Алгоритм Уоршалла проверяет все возможные тройки с помощью трех вложенных циклов по индексам i, j и к. Для каждой пары (i,j) добавляется ребро если существует вершина Vk, такая, что ребра E(vi,vk) и E(vk,vj) нахо-^ятся в расширенном графе. Повторяя этот процесс, мы соединяем дополни-ельвыми ребрами любую пару достижимых вершин. В результате получается МаТРИца достижимости.
Предположим, что вершины v и w достижимы через направленный связывающий пять вершии. Тогда существует последовательность ве формирующих путь
V « Х1, х2, х8, х4, х5 = W
Имея путь от v до w, мы должны показать в матрице достижимости алгоритм Уоршалла в конце концов даст тот же путь. С помощью " Чт° вложенных циклов мы проверяем все возможные тройки вершин. Допус^6* вершины идут в порядке Xi~xs. В процессе просмотра различных троек шина х2 идентифицируется как общий узел, связывающий хх и х3. След тельно, согласно алгоритма Уоршалла мы вводим новое ребро Е(х1,х3) па' пары xlt х4 общей связующей вершиной является х8, так как путь, соединяю^ -Xj и х8, был найден на предыдущем шаге итерации. Поэтому мы создаем ребьо Е(хьх4). Таким же образом х4 становится общей вершиной, связывающей у и х5, и мы добавляем ребро Е(х1,х5) и присваиваем элементу R[l][5] значение /
Проиллюстрируем алгоритм Уоршалла на следующем графе. Здесь дополнительные ребра, добавленные для формирования транзитивного замыкания изображены пунктиром.
1110 1 0 110 1 0 0 10 0 11111 0 0 10 1
Алгоритм Уоршалла имеет время вычисления О(п3). При сканировании матрицы смежности применяются три вложенных цикла. Списковое представление графа также дает сложность О(п3).
Программа 13.7. Использование алгоритма Уоршалла
Алгоритм Уоршалла используется для создания и печати матрицы Д° стижимости.
#include Kinclude
<iostream.h>
<fstream.h>
^include
graph.h
<class T>
template
void Marshall(Graph<T>& G) <
VertexIterator<T> vi(G), vj(G);
int 1, j, k;
int WSM[MaxGraphSize] [MaxGraphSize]; // матрица Уоршалла int n = G.NumberOfVertices();
// создать исходную матрицу
for (vi.Reset (); i=0; !vi.EndOfList () ; vi.NextO, i++)
for (vj. ResetO; i=0; !vj .EndofList (); vj.NextO, j++)
if (i =~ j)
WSM[i] [i] =» 1;
else
WSM[i][j) » G.GetWeight(vi.Dataf), vj.DataO);
// просмотреть все тройки, записать 1 в WSM, если существует ребро
// между vi и vj или существует тройка vi-vk-vj, соединяющая
И vi и vj
for (i=0; i<n; i++)
for (j=0; j<n; j++]
for (k-0; k<rt; k++)
WSM[i](j) (= WSM(i) [k] & WSM[k]j];
// распечатать каждую вершину и ее строку из матрицы достижимости for (vi.Resetf); i=Q? !vi.EndOfList(); vi.NextO, i++) (
cout « vx.Datal) «
for (j=0; j<n; j++)
cout « WSM(iHj) « "
cout « endl; }
} '
void main(void)
{
Graph<char> G;
G. ReadGraph ("Marshall. dat")
cout « "Матрица достижимости:" « endl; Warshall(G);
)
<Прогон программы 13.7>
Матрица достижимости:
A; 1 1 1 0 1
В: 0 1 1 0 1
С: 0 0 1 О О
D: 1 1 1 1 1
Е: 0 0 1 0 1
*/

Письменные упражнения
13.1	Нарисуйте законченное дерево для каждого из следующих массивов: a) int А[8] = {5, 9, 3, 6, 2, 1, 4, 7}
6)	char *В = "array-based tree"
13.2	Для каждого из изображенных ниже деревьев задайте соответствующий массив.
(С)
13.3	Напишите функцию, выполняющую а) прямое и б) симметричное прохождение представленного массивом дерева. В качествеобразца используйте ранее разработанный код для списковогс/предс^вления деревьев.
13.4	Пусть А есть массив, состоящий из 70 элементов и представляющий дерево.
а)	Является ли А[45] листовым узлом?
б)	Какой индекс у первого листового узла?
в)	Кто родитель узла А[50]?	I
г)	Кто является сыном узла А[10]?	'ч
д)	Имеет ли какой-нибудь узел только одного сына?
е)	Какова глубина этого дерева?
ж)	Сколько листьев у этого дерева?
13.5
Т
а)	Напишите функцию, которая принимает N-элементный массив типа и выполняет поперечное прохождение представляемого им дерева. Дл запоминания элементов ваша процедура должна использовать очередь-Распечатайте элементы по уровням (один уровень на строке).
б)	Сможете ли вы разработать более простую версию поперечного обхоДа-используя тот факт, что дерево представлено в виде массива?
13.6	Покажите, что в законченном бинарном дереве число листовых У3^ больше и равно числу полистовых узлов. Покажите, что в полном парном дереве число листовых узлов больше, чем нелистовых.
13.7	Законченное бинарное дерево, содержащее 60 узлов, представлено в виде массива.
а)	Сколько уровеней в этом дереве?
б)	Сколько узлов являются листовыми? Нелистовыми?
в)	Какой индекс у родителя узла В[35]?
г)	Какие индексы у сыновей узла В[20]?
д)	Какой индекс у первого листового узла? У первого узла, имеющего одногохЫна?
е)	Какие индексы у узлов четвертого уровня?
13.8	Распишите по шагам турнирную сортировку массива А — {7, 10, 3, 9, 4, 12, 15, 5, 1, 8}.
13.9	Модифицируйте турнирную сортировку так, чтобы листовые и иелис-товые узлы содержали фактические данные из массива. Оформите модифицированный код в виде функции ModTournamentSort.
13.10	Скажите, являются ли приведенные ниже бинарные деревья пирамидами (минимальными или максимальными)?
13.Ц	Для каждого дерева, ие являющегося пирамидой, из предыдущего упражнения создайте минимальную и максимальную пирамиды. Для каждой минимальной (максимальной) пирамиды создайте соответствующую максимальную (минимальную) пирамиду.
13.12	С помощью FiterDown и конструктора сделайте из следующего дерева минимальную пирамиду.
13.13
ЬНо
Даны пирамида А и пирамида В. Над каждой из них последовател произведите указанные ниже операции.
(А)
Пирамида А
а)	вставить 15
б)	вставить 35
в)	удалить 10
г)	вставить 40
д)	вставить 10
Пирамида В а) удалить 22 б) вставить 35 в) вставить 65 г) удалить 15 д) удалить 27 е) вставить 5
13.14
а)	Каково наибольшее число узлов, которое может существовать в дереве, являющемся минимальной пирамидой и бинарным деревом поиска одновременно? Дублирующиеся значения не допускаются.
б)	Каково наибольшее число узлов, которое может существовать в дереве, являющемся максимальной пирамидой и бинарным деревом поиска одновременно? Дублирующиеся значения не допускаются.
13.1	5 Для следующей пирамиды перечислите узлы, составляющие указав ный путь.
а)	Путь родителей, начинающийся в узле 47.
б)	Путь родителей, начинающийся в узле 71.
в)	Путь через минимальных сыновей, начинающийся в узле 35.
г)	Путь через минимальных сыновей, начинающийся в узле 10-
д)	Путь через минимальных сыновей, начинающийся в узле 40 на пер
уровне.
10
25) (70
72) (47
80) (85
71) (45
13.1	6 Постройте минимальную пирамиду, для каждого из перечисленных ниже массивов.
a)	int А(10] = {40, 20, 70, 30, 90, 10, 50, 100, 60, 80);
б)	int А[8] = (3, 90, 45, 6, 16, 45, 3, 88};
в)	char *В = "heapify”;
г)	char *В - ’’minimal heap”;
13.1	7 Реализуйте класс очередей приоритетов PQueue с помощью класса BinSTree. (Совет, Модифицируйте метод PQDelete для поиска и удаления минимального элемента дерева.)
13.1	8 Постройте AVL-дерево для каждой из указанных последовательностей.
a)	<int> 30, 50, 25, 70, 60, 40, 55, 45, 20
б)	<int> 44, 22, 66, 11, 0, 33, 77, 55, 88,
в)	<int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
г)	<String> tree, AVL, insert, delete, find, root, search
д)	<String> class, object, public, private, derived, base, inherit, method, constructor, abstract
13.1	9 Разработайте итерационную функцию прямого сканирования Preor-derl. Она должна эмулировать следующую рекурсивную функцию Preorder.
template <class Т>
void Preorder (TreeNode<T> *t, void visit (T& item)) {
while (t != NULL)
{
visit(t->data);
Preorder(t~>Left(). visit); t = t->Right();
После обработки узла обрабатывается сначала левое, а затем правое поддеревья. Итерационное прямое прохождение должно эмулировать рекурсивное сканирование с помощью стека адресов узлов. Предположим, мы обработали узел А бинарного дерева. Теперь мы должны обработать его левое поддерево и вернуться к предыдущей точке, чтобы обработать его правое поддерево. Чтобы помнить необходимость посещения правого поддерева, мы помещаем правый указатель узла А в стек. После обработки всех узлов левого поддерева А этот указатель выталкивается из стека, и мы возвращаемся к прохождению правого поддерева. Описанные ситуации показаны на следующих рисунках.
id
состоит из следующих шагов.
Алгоритм итерационного прохождения Начиная с корня, выполнять в цикле 1. Посетить узел
2.	Сохранить адрес его правого сына в стеке адресов.
3.	Перейти к его левому сыну.
Всякий раз, когда в цикле встречается нулевой узел, обрабатываются правые сыновья, адреса которых последовательно выталкиваются из стека. Цикл прекращается, когда встретится нулевой указатель и в стеке больше нет правых сыновей.	/	/
13.20 Функция Postorder_I осуществляет восходящее прохождение дерева. Однако задача более сложна, чем просто восходящее или смешанное прохождение. Требуется различать спуск пр-.левому поддереву (состояние О) и возврат к родительскому узлу (состояние 1). При движении вверх по дереву возможны два действия — посещение правого поддерева узла или обработка самого узла. Состояние хранится в целочисленной переменной state. Если state —= О, происходит Движение вниз по дереву. Если state == О, происходит движение вверх. Когда мы попадаем: в узел сверху, родитель этого узла находится на вершине стека. Чтобы определить, пришли ли мы слева, нужно сравнить указатель текуШеГ° узла с левым указателем родительского узла. Если они согласуются у родителя есть правое поддерево, следует перейти в это поддерево, противном случае нужно обработать узел и продолжить движение ввер
Нарисуйте несколько диаграмм, как те, что на рис. 13.5 и 13.6, чтобы проиллюстрировать работу этого алгоритма, реализованного следующей про-граммой.
#include ciostream. h>
#include '‘treenode. h" #include "treelib.h" #include "stack.h"
void printchar(char& item) {
cout « item « "
J
template cclass T>
void Postorder_I(TreeNode<T> *t, void visit(T& item)) {
Stack<TreeNode<T> *> S; TreeNode<T> *child; int state = 0, scanOver = 0;
while (!scanOver) 1 if (state «==• 0) <
if (t ! = NULL) < S.Push(t); t = t->Left();
.} else state = 1;
* } else ( if (S.StackEmptyO) scanOver = 1; else { child = t; t = S.PeekO ;
if (child == t->Left() && t->Right() != NULL) {
t • t->Right(); state ~ 0;
} else { visit(t->data); S.PopO;
} }
} }
void main (void) < TreeNode<char> *root;
MakeCharTree(root, 0); PrintTree(root, 0); cout « endl;
Postorder_I(root, printchar);
cout « endl;
} <Прогон>
С
A D
В D В E С A
13.21	Для каждого из приведенных ниже графов задайте матрицу смеж и списки смежности.	Н°Стй
на следу10' отвечайте-
13.22	Используйте графы из предыдущего упражнения и ответьте щие вопросы, касающиеся путей. Если пути не существует, ’’Пути нет".
а)	Найти в графе (А) направленный путь от Е к В.
б)	Найти в графе (В) направленный путь от А к Е.
в)	Найти в графе (С) направленные пути от В к Е и от Е к В.
г)	Найти в графе (В) все узлы, смежные с А (с Е).	#
д)	Найти в графе (С) такие узлы X, для которых имеется путь Р(А’ Р(Х,А).
е)	Перечислите связанные компоненты в каждом графе.
ж)	Какой из этих графов является сильно связанным (слабо
свдав»»им)?
3.23 Возьмите графы из письменного упражнения 13.21. Перечислите вер-j шины в порядке их прохождения методом "сначала в глубину" и методом "сначала в ширину".
! а) В графе (А), начав с вершины А.
6) В графе (В), начав с вершины А.
! в) В графе (С), начав с вершины А.
г) В графе (В), начав с вершины В.
13.24 Создайте частичную реализацию класса Graph, основанного на списковом представлении графов. Определите класс Vertexinfo, содержа-'	щий имя вершины и связанный список смежных с ней вершин. Ин-
;	формация о смежных вершинах должна храниться в виде структуры,
включающей имя конечной вершины и вес ребра, соединяющего начальную и конечную вершины. Для сравнения наименований вершин следует добавить в класс Vertexinfo перегруженный оператор сравне-, ния.
а)	Реализуйте конструктор.
б)	Реализуйте методы GraphEmpty, NumberOfVertices, GetNeighbors, In-sertVertex, InsertEdge и DepthFirstSearch.
13.25
а)	Модифицируйте функцию MinimumPath, чтобы создать функцию MinimumLength, которая находит путь между двумя вершинами, содержащий минимальное число вершин. Если не существует вообще никакого пути, возвращайте -1.
б)	Создайте функцию VertexLength, которая принимает в качестве параметров граф G и начальную вершину V и распечатывает вершины графа, а также длины путей до этих вершин от V. Используйте функцию MinimumLength из упражнения а).
13.26 Опишите действие следующей функции:
।	template <class Т>
Seqbist<T> RV(Graph<T> sG)
{
SeqList<T> L;
Vertexiterator<T> viter(G);
ListIterator<T> liter(L);
for (viter.Reset(); Iviter.EndOfListО; viter.Next()) (
cout « viter.Data() « ":
L = G.BreadthFirstSearch(viter.Data());
liter.SetList(L);
PrintList(L);
cout « endl;
} }
13-27 Постройте соответствующий граф по каждой из следующих матриц смежности. Считайте, что вершины являются буквами А, В, С и т.д.
а) О 1 1 1
1	10 11
110 1
1110
<1Э
б) 0 110 0
0 0 0 1 0
1 0 0 0 1
10 0 10
0 10 10
Упражнения по программированию
13.1 С помощью функций, разработанных вами в письменном упражнении 13.3, определите число листовых узлов, число узлов, имеющих по одному сыну и число узлов, имеющих по два сына. Используйте глобальные переменные nochild, onechild и twochild в программе, которая тестирует и распечатывает следующие глобальные данные:
int А[50];	// хранит данные 0, 1, 2,	49
int А[100];	// хранит данные 0, 1, 2, .... 99
13.2
а)	Разработайте тестовую программу для упорядочения с помощь10 нирной сортировки следующих символьных строк. Создайте массив ектов типа String и передайте его в функцию TournamentSort.
class, object, public, private, derived, base, inherit, method, constru tor, abstract
б)	Создайте массив нз 100 случайных чисел от 0 до 999 и отсорт-^Р g его по возрастанию с помощью модифицированной функции тур раС. сортировки ModToumamentSort из письменного упражнения 13-печатайте результирующий массив.
3.3	Модифицируйте класс Heap, приспособив его для создания максимальных пирамид-’ Вызовите этот новый класс МахНеар и протестируйте его операции с помощью варианта программы 13.3, сортирующей элементы массива.
^3.4 Реализуйте функции FilterUp и FilterDown. класса Heap рекурсивным способом. Протестируйте нх на массиве из 15 случайных чисел в диапазоне от О до 99. Используйте функцию HDelete для удаления N элементов из пирамиды и печати их значении.
(3.5 Н екоторой компьютерной системе каждой работающей программе (процесс^-назначается приоритет. Наивысшим приоритетом является О, а самым низким — 39. Когда приходит время выполнить некоторый процесс, в очередь приоритетов вставляется запись запроса на обработку. Когда ЦП свободен, запись удаляется и соответствующий процесс запускается на выполнение. Запись запроса на обработку имеет следующий формат:
struct ProcessRequestRecord { int priority;
String name; };
Поле name идентифицирует конкретный процесс. Сгенерируйте случайным образом 10 записей запроса на обработку с именами "Process А", "Process В", ..., "Process J" и приоритетами в. диапазоне от 0 до 39. Распечатайте каждую запись, а затем включите их в приоритетную очередь. Потом удаляйте из очереди и распечатывайте каждую запись.
13.6	Определите структуру, содержащую значение данных и приоритет.
template <class т> struct PrxorityData {
T data;
int priority;
};
Используйте эту структуру и очередь приоритетов для реализации класса Queue. (Совет. Объявите очередь как список записей типа Priority-Data. Элементы запоминаются в очереди приоритетов в порядке, определяемом полем priority каждой записи. Определите целочисленную переменную PL, увеличивающуюся на единицу всякий раз, когда очередной элемент включается в очередь. Пусть значение PL и будет приоритетом очередной включаемой в очередь записи.)
Протестируйте свою очередь, введя пять целых чисел и запомнив их в очереди с помощью QInsert. Затем последовательно удаляйте и распечатывайте элементы, пока очередь не опустеет.
13,7	Используйте модель из предыдущего упражнения и класс МахНеар из упражнения 13.3 для реализации стека с помощью очереди приоритетов. Испытайте новый класс, введя пять целых чисел и запомнив их в стеке с помощью Push. Затем последовательно удаляйте с помощью Pop и распечатывайте элементы, пока очередь не опустеет.
13.8	Разработайте класс итераторов ArrPreorderlterator для прямо хождения представленных массивами деревьев. Объявите его щим образом:	СЛе^То.
template <class Т>
class ArrPreorderlterator: public iterator<T> {
private:
Stack<int> S;
T *A;
int arxaySize;
int current;
public:
ArrFreorderlterator(T *Arr, int n);*
virtual void Next(void);
virtual void Reset(void);
virtual T& Data(void); }}
Протестируйте этот класс с помощью дерева, представленного в виде следующего массива:
int А[15] - {1, 4, 6, 2, 8, 9, 12, 25, 23, 55, 18, 78, 58, 14, 45);
и распечатайте его по уровням и в нисходящем порядке.
13.9	Унарный оператор ++ может быть перегруженным только в качестве функции-члена класса. Итераторная функция Next реализуется с помощью этого оператора естественным образом. Ниже приводится объявление этого оператора и пример его использования.
void operator** (void);	S'
Прохождение бинарного дерева поиска.
BinSTree<T> *tree;
lnorder!terator<T> inorderlter(tree.GetRoot())V while (Iinorderlter.EndOfList()) (
inorderIter++; )
13.10
Реализуйте метод Next класса Inorderiterator с помощью ”++ и ил пытайте его в программе, которая включает 25 случайных целых 4110 в бинарное дерево поиска, а затем сортирует их с помощью функд» TreeSort.
Разработайте класс итераторов Levelorderlterator для попереч301^^ хождения бинарного дерева поиска. Объявление втого класса выгл следующим образом:
template <class Т>
class Levelorderlterator: public lterator<T> (
private:
Queue<TreeNode<T> *> S;
TreeNode<T> *root, ‘current;
public:
Levelorderlterator(TreeNode<T>* 1st);
virtual void Next(void);
virtual void Reset(void);
virtual те Data(void);
);
В главной программе создайте бинарное дерево поиска, содержащее следующие данные:
int data[] = {100, 55, 135, 145, 25, 106, BS, 90, 5, 26, 67, 45, 99, 33);
Распечатайте это дерево с помощью PrintTree, а затем осуществите его поперечный обход с помощью Levelorderlterator.
13.11 Разработайте класс Postorderiterator по следующему алгоритму.
Инициализировать итератор посредством помещения адресов всех узлов в стек в процессе NRL-обхода. Во время прохождения текущий узел всегда находится на вершине стека» и очередной узел можно получить посредством операции выталкивания из стека.
(В)
----------------------------------------------------------—^13
Испытайте итератор, осуществив прохождение и печать узлов из предыдущего упражнения.
13.12 Разработайте класс Postorderiterator путем модификации алгоритм ратного прохождения из письменного упражнения 13.20. Испы итератор, осуществив прохождение и печать узлов дерева из ния 13.10.	₽*Не-
13.13
13.14
С помощью ReadGraph создайте файл данных для графов А и В. пишите главную программу, которая вводит эти данные, а затем Ис пользует функцию VertexLength из письменного упражнения 12.25 ДЛн распечатки расстояния каждой вершины от А. Испытайте свою пр0. грамму на приведенных ниже графах.
Примените алгоритм Уоршалла к графам из предыдущего упражнеНйа и распечатайте матрицу достижимости для каждого графа;
Организация коллекций
▼
14.1. Основные алгоритмы сортировки массивов
14.2. 'Быстрая сортировка'
143. Хеширование
14.4.	Класс хеш-таблиц
14.5.	Производительность методов поиска
14.6.	Бинарные файлы и операции с данными на внешних носителях
14.7.	Словари
Письменные упражнения
Упражнения по программированию
^^14
Мы завершаем нашу книгу общей главой по организации данных будет рассмотрен ряд классических алгоритмов сортировки. В преди главах сортировка служила иллюстрацией возможностей списковых Например, поразрядная сортировка была представлена как приложен редей. Теперь же мы сосредоточимся на сортировке массивов и позвано * с классическими алгоритмами сортировки посредством выбора, сортй<аВ1Ся методом пузырька и сортировки вставками, которые требуют 0(п2) сраав^08Еи Хотя эти алгоритмы на практике не слишком эффективны для боль^^' количества элементов, они иллюстрируют основные подходы к сортип001^ массивов. Последним будет рассмотрен знаменитый алгоритм "быстрой тировки".	Сор’
В гл. 4 были представлены алгоритмы последовательного и бинарн поиска» являющиеся базовыми алгоритмами поиска в списках. В настоят^ главе мы пополним наше знание о поиске и изучим хеширование, при ко* тором для быстрого доступа к элементам данных используется ключ, а слож ность поиска имеет порядок 0(1). Мы разработаем обобщенный класс хещ таблиц, допускающий произвольные типы данных.
В этой книге внимание было сосредоточено на данных, размещаемых в оперативной памяти. Для больших наборов данных, хранимых на диске требуются методы внешнего доступа. Мы рассмотрим класс BinFile, предназначенный для обработки двоичных файлов, и на его методах проиллюстрируем алгоритм внешнего индексно-последовательного поиска и алгоритм внешней сортировки слиянием.
Раздел, посвященный ассоциативным массивам, или словарям, обобщает понятие индекса массива, что позволяет организовать данные с помощью нечисловых индексов. Мы воспользуемся ассоциативными массивами для построения небольшого словаря.	/	/
14.1 Основные алгоритмы сортировки массивов
Начнем с трех алгоритмов, составляющих основу методики сортировки "на месте" по возрастанию. Для каждого алгоритма оценим его вычисли тельную эффективность.
Сортировка посредством выбора
Сортировка выбором моделирует наш повседневный опыт. Воспитате^ детского сада часто используют эту методику, чтобы выстроить ^eTegB0$ росту. При этом самый маленький ребенок выбирается из неупорядоч группы детей и перемещается в шеренгу, выстраиваемую по росту-процесс, иллюстрируемый следующими несколькими рисунками, прод ется до тех пор, пока все дети не окажутся в упорядоченной шеРеНпеЯвь1*
Для компьютерного алгоритма предполагается, что п элементов евОм хранятся в массиве А и по этому массиву выполняется п-1 проход. ₽ месТаМЯ проходе выбирается наименьший элемент, который затем меняется ® с А[0]. После этого неупорядоченными остаются элементы А[1] ••• следующем проходе просматривается неупорядоченная хвостовая я слв-ка, откуда выбирается наименьший элемент и запоминается в AL J
Выбрать Дэбби
Начальная шеренга
выбрать Рона
Том Дебби Майк
Выбрать Майка
Дебби
Упорядоченная шеренга
Рон
Рон Том
Рон Том Майк
Рон Том Майк Дебби
gbi6pdTb Томэ
дующем проходе производится поиск наименьшего элемента в подсписке А[2] ... А[п-1]. Найденное значение меняется местами с А[2]. Таким образом выполняется п-1 проход, после чего неупорядоченный хвост списка сокращается до одного элемента, который и является наибольшим.
Рассмотрим сортировку посредством выбора на массиве, содержащем пять целых чисел: 50, 20, 40, 75 и 35.
проход 2
Проход 0: Выбрать 20
Поменять местами 20 и А[0]
Проход 1: Выбрать 35
Поменять местами 35 и А[1]
Проход 2: Выбрать 40
Поменять местами 40 и А[2]
20	35	40	75	<50Ч
ГТ^ проход 3
Проход 3: Выбрать 50
Поменять местами 50 и А[3]
20	35	40	50	75
Отсортированный список
® i-ом проходе сканируется подсписок A[i] ... А[п-1] и переменной smallln-8 * Ч^исваивается индекс наименьшего элемента в этом подсписке. Затем ^Менты A[i] и AfsmaUbidex] меняются местами. Функция SelectionSort и ^Дита Swap находятся в файле arrsort.h.
If отсортировать n-элементный массив типа Т, //с помощью алгоритма сортировки посредством выбора template <class т> void Selectionsort(Т А[], int n) (
// индекс наименьшего элемента на каждом проходе int smalllndex;
int i, j;
for (i=0; i<n-l; i++) (
// начать проход с индекса i; установить smalllndex в i smallIndex =i;
for (j=i+l; j<n; j++)
// обновить smalllndex, если найден меньший элемент if (A[jJ < A[smalllndex]) smalllndex = j;
//no окончании поместить наименьший элемент в A(i] Swap(A[ij, AJsmalllndex]);
} }
<3
Вычислительная сложность сортировки посредством выбора. Сортировка посредством выбора требует фиксированного числа сравнений» зависящего только от размера массива, а не от начального распределения в нем данных. В i-ом проходе число сравнений с элементами A[i+1] ... А(п-1] равно
(n-1) - (i+1) + 1 = п - i - 1
Общее число сравнений равно
п-2	п-2	__
£(П - 1) - i = (п - 1)2 - £ i = (П - 1)2 - (1>~*)(/ ~2)/= | П(П - 1) о	о
Сложность алгоритма, измеряемая числом сравнений, равна О(п2), а число, обменов равно О(п). Наилучшего и наихудшёго случаев не существует, так как алгоритм делает фиксированное число проходов, в каждом из которых сканируется определенное число элементов. Пирамидальная сортировка, имеющая сложность O(n logs п), является обобщением метода сортировки посредством выбора.
Сортировка методом пузырька
В гл. 2 мы познакомились с обменной сортировкой, требующей п-1 ДР ходов и фиксированного числа сравнений в каждом из них. В этом разд' мы обсудим пузырьковую сортировку, при которой также в каждом пр° выполняется ряд обменов.	я д0
Для сортировки n-элементного массива А методом пузырька требует п-1 проходов. В каждом проходе сравниваются соседние элементы, мВ, первый из них больше или равен второму, эти элементы меняются м к К моменту окончания каждого прохода наибольший элемент подвима вершине текущего подсписка, подобно пузырьку воздуха в кипягце , а Например, по окончании прохода О хвост списка (А[п-1]) отсортир головная часть остается неупорядоченной.	xpaSllT
Рассмотрим эти проходы подробнее. Переменная lastExchangelnde последний участвующий в обмене индекс и приравнивается нулю в каждого прохода. В нулевом проходе сравниваются соседние элемей
a[1]), (A[l], A[2]), ...,(A[n-2], A[n-1]). В каждой nape (A[i], A[i+lJ) элементы j меняются местами при условии, что A[i+1] < A[i], а значение lastExchange-! j^dex становится' равным 1. В конце этого прохода наибольшее значение  „называется в элементе А[п-1], а значение lastExchangelndex показывает, что Jce элементы в хвостовой части списка от A[lastExchangeIndex+l] до А[п-1]  ^^сортированы. В очередном проходе сравниваются соседние элементы в под-। списке А[О]—А[ lastExchangelndex]. Процесс прекращается при lastExchange-
jndex — О. Алгоритм совершает максимум п-1 проход.
Рассмотрим пузырьковую сортировку на массиве, содержащем пять целых чисел: 50, 20, 40, 75 и 35.
проход 0	50	20	40	75	35	Поменять местами 50 и 20
1 _	1						
1	20	50	40	75	35	Поменять местами 50 и 40
1 1—1						
	20	40	50	75	35	50 и 75 упорядочены
1	1						
	20	40	50	75	35	Поменять местами 75 и 35
1	1						
	20	40	50	35		75 - наибольший элемент LastExchangelndex = 3
Поскольку lastExchangelndex не равен нулю, процесс продолжается. В проходе 1 сканируется подсписок от А[0] до A[3]=A[lastExchangeIndex]. Новым значением lastExchangelndex становится 2.
проход 1	20	40	50	35		20 и 40 упорядочены
1	1						
	20	40	50	35		40 и 50 упорядочены
1	1						
	20	40	50	35		Поменять местами 50 и 35
1	1						50 - наибольший элемент lastExchangelndex = 0
	20	40	35			
В проходе 2 сканируется подсписок от А[0] до А[2] и элементы 40 и 35 меняются местами. lastExchangelndex становится равным 1.
проход 2
20	40	35		W':
20	35	40	>.’. г.	
I____________________I
20 и 40 упорядочены
Поменять местами 40 и 35
lastExchangelndex -1
i > ® проходе 3 выполняется единственное сравнение (20 и 35). Обменов нет, • ^xchangelndex == 0, и процесс прекращается.
проход 3	20	35	40	; 50^	"‘‘.а1	20 и 35 упорядочены
	1	1					
	20	35	40	50	75	Упорядоченный список lastExchangelndex — 0
// BubbleSort принимает массив А и его размер п -
// сортировка выполняется посредством ряда проходов, пока lastExchano^T , template <class т>	9е1аде1{
void Bubblesort (Т А[], int п) (
int i, j;
// индекс последнего элемента, участвовавшего в обмене int lastExchangeIndex;
// i — индекс последнего элемента в подсписке i = п-1;	__
// продолжать процесс, пока не будет произведено ни одного обмена while (г > 0)
{
// начальное значение lastExchangeIndex	lastExchangeIndex - 0;
// сканировать подсписок A(01'—A(i]
for (j-0; j < i; j++)
Il менять местами соседние элементы и обновить lastExchangelndex if (A[j+1) < А{j]) {
Swap(A[j], A(j+1]); lastExchangelndex = j; )
// присвоить i значение индекса последнего обмена, продолжить сортировку
i - lastExchangelndex;
} }
Вычислительная сложность сортировки методом пузырька
При пузырьковой сортировке сохраняется индекс последнего обмена во избежание избыточного просмотра. Это придает алгоритму заметную эффективность в некоторых особых случаях. Самое замечательное — это то, что пузырьковая сортировка совершает всего один проход по списку, уже упорядоченному по возрастанию. Таким образом, в лучшем случае эффективность равна О(п). Худший случай для пузырьковой сортировки — когда список упорядочен по убыванию. Тогда необходимо выполнять все п-1 проходов-В i-ом проходе производится (n - i - 1) сравнений и (n - i - 1) обменов-Сложность наихудшего случая составляет О(п2) равнений и О(п2) обменов-Анализ общего случая затруднен из-за возможности пропуска некоторых про-ходов. Можно показать, что среднее количество проходов к равно О(п) и, следовательно, общее число сравнений равно О(п2). Даже если пузЫрьК0В сортировка выполняется менее чем за п-1 проходов, это требует, как прави ♦ большего числа обменов, чем при сортировке посредством выбора, и п0Э12еД. ее средняя производительность меньше. В общем случае сортировка посре ством выбора превосходит пузырьковую за счет меньшего числа обмево 
Сортировка вставками
Сортировка вставками похожа на хорошо всем знакомый процесс ния карточек с именами. Регистратор заносит каждое имя на карте4 У мером 127x76, а затем упорядочивает карточки по алфавиту, вставЛ^Т)Оцесс точку в верхнюю часть стопки в подходящее место. Опишем этот ЦР на примере нашего пятиэлементного списка А — 50, 20, 40, 75, 35-
50
Начать с элемента 50
Обработка 20
Обработка 40
Обработка 75
Обработка 35
В функцию InsertionSort передается массив А и длина списка п. Рассмотрим । i-ыЙ проход (l<i<n-1). Подсписок А[0]—A[i—1] уже отсортирован по возраста-! нию. В качестве вставляемого (TARGET) выберем элемент A[i] и будем продвн-| гать его к началу списка, сравнивая с элементами A[i—1], A[i-2] и т.д. Просмотр заканчивается на элементе A[j], который меньше или равен TARGET или находится в начале списка (j — 0). По мере продвижения к началу списка каждый элемент сдвигается вправо (A[j] *• A[j-1]). Когда подходящее место для A[i] будет найдено, этот элемент вставляется в точку j.
// Сортировка вставками упорядочивает подсписки A[0]...A(i],
// 1 <= i <“ n-1. Для каждого i A(i) вставляется в подходящую
// позицию А(j ]
template <class Т>
void InsertionSort(Т A[], int n)
{
int i, j;
T temp;
// i определяет подсписок A[0]...A[i]
for (i-1; i<n; i++)
{
// индекс j пробегает вниз no списку от A[i] в процессе
// поиска правильной позиции вставляемого значения
j • 1;
temp - A[i];
// обнаружить подходящую позицию для вставки, сканируя подсписок,
// иска temp < A[j-1] или пока не встретится качало списка
while (j > 0 && temp < A[j-1])
(
11 сдвинуть элементы вправо, чтобы освободить место для вставки
A(j] = Mj-iJ;
j--;
// точка вставки найдена; вставить temp
A[j] - temp;
»
।
Вычислительная эффективность сортировки вставками. Сортировка встав-цми требует фиксированного числа проходов. В п-1 проходах включаются ^«Менты А[1]—А[п-1]. В i-ом проходе включения производятся в подсписок —A[i] и требуют в среднем i/2 сравнений. Общее число сравнений равно
V2 + 2/2 + 3/2 + ... + (n-2)/2 + (n-l)/2 = п(п-1)/4
4 ?3зак.425
В отличие от других методов, сортировка вставками не использует об Сложность алгоритма измеряется числом сравнений и равна О(п2). Наи случай — когда исходный список уже отсортирован. Тогда в i-ом проходе производится в точке A[i], а общее число сравнений равно п-1, т.е. оло СТав^ составляет О(п). Наихудший случай возникает, когда список отсортироТЙОСТь убыванию. Тогда каждая вставка происходит в точке А[0] и требует i cpasBQfi Общее число сравнений равно п(п-1)/2, т.е. сложность составляет О(п2). ВеНаЙ.
14.2. "Быстрая сортировка"
К этому моменту мы рассмотрели ряд О(п2)-сложных алгоритмов con ровки массивов "на месте*'. Алгоритмы, использующие деревья (турнипп сортировка, сортировка посредством поискового дерева), обеспечивают знТ чительно лучшую производительность O(n log2n). Несмотря на то, что оии требуют копирования массива в дерево и обратно, эти затраты покрываются за счет большей эффективности самой сортировки. Широко используемый метод пирамидальной сортировки также обрабатывает массив "на месте" и имеет эффективность O(n log2n). Однако "быстрая сортировка”, которую изобрел К. Хоар, для большинства приложений превосходит пирамидальную сортировку и является самой быстрой из известных до сих пор.
Описание ’’быстрой сортировки"
Как и для большинства алгоритмов сортировки, методика "быстрой сортировки" взята из повседневного опыта. Чтобы отсортировать большую стопку алфавитных карточек по именам, можно разЬит> ее цаГдве меньшие стопки относительно какой-нибудь буквы, например К./Все имена, меньшие или равные К, идут в одну стопку, а остальные — в другую. Затем каждая стопка снова делится на две. Например, на рис. 14.1 точками разбиения являются буквы F и R. Процесс разбиения на все меньшие и меньшие стопки продолжается.
В алгоритме "быстрой сортировки” применяется метод разбиения с определением центрального элемента. Так как мы не можем позволить себе удовольствие разбрасывать стопки по всему столу, как при сортировке алфавитных карточек, элементы разбиваются на группы внутри массива. Рассмотрим алгоритм "быстрой сортировки” на примере, а затем обсудим технические детали. Пусть дан массив, состоящий из 10 целых чисел:
А = 800, 150, 300, 600, 550, 650, 400, 350, 450, 700
Рис. 14.1. Разбиение для «быстрой сортировки»
фаза сканирования. Массив простирается от индекса low — О до индекса high = 9. Его середина приходится на индекс mid — 4. Первым центральным эЛементом является A[mid] = 550. Таким образом, все элементы массива А сбиваются иа два подсписка: S; и S^. Меньший из них (Si) будет содержать элементы, меньшие или равные центральному. Подсписок Sh будет содержать элементы большие, чем центральный. Поскольку заранее известно, что центральный элемент в конечном итоге будет последним в подсписке Si, мы временно передвигаем его в начало массива, меняя местами с А[0] (A[low]). Это позволяет сканировать подсписок А[1]—А[9] с помощью двух индексов: gcanUp и scanDown. Начальное значение scanUp соответствует индексу 1 (low+1)* Эта переменная адресует элементы подсписка 8>. Переменная scan-pown адресует элементы подсписка Sh и имеет начальное значение 9 (high). Целью прохода является определение элементов для каждого подсписка.
550
150	300	600	800	650	400	350	450	700
scanUp	scanDown
Оригинальность "быстрой сортировки” заключается во взаимодействии двух индексов в процессе сканирования списка. Индекс scanUp перемещается вверх по списку, a scanDown — вниз. Мы продвигаем scanUp вперед и ищем элемент A(scanUp] больший, чем центральный. В этом месте сканирование останавливается, и мы готовимся переместить найденный элемент в верхний подсписок. Перед тем как это перемещение произойдет, мы продвигаем индекс scanDown вниз по списку и находим элемент, меньший или равный центральному. Таким образом, у нас есть два элемента, которые находятся ве в тех подсписках, и их можно менять местами.
Swap (A[3canUp], А[scanDown]);	// менять местами партнеров
Этот процесс продолжается до тех пор, пока scanUp и scanDown не зайдут Друг за друга (scanUp = 6, scanDown = 5). В этот момент scanDown оказывается в подсписке, элементы которого меньше или равны центральному. Мы попали в точку разбиения двух списков и указали окончательную позицию для центрального элемента. В нашем примере поменялись местами числа 600 и 450, 800 и 350, 650 и 400.
550
150	300 600 800 650 400 350 450	700
scanDown
scanUp
Затем происходит обмен значениями центрального элемента А[0] с Afscan-, Ъо*п]:
SkaP !А[0), A[scanDown]>;
। В результате получился подсписок А[0]—А[4], элементы которого меньше подсписка А[6]—А{9]. Центральный элемент (550) разделяет два ^Дениска, каждый из которых равен примерно половине исходного списка. а Подсписка обрабатываются по одному и тому же алгоритму. Мы называем 0 Рекурсивной фазой.
•'-а£
400	150	300	450	350	' 550	650	800	600	700
г:	। i i
А[0]-А[4]
А16]-А[9]

Рекурсивная фаза. Одним и тем же методом обрабатываются два Пп ка: Sj (A[OJ—А[4]) и Sh (A[5j—А[9]).	Дс^-
Подсписок Sj. Подсписок определяется диапазоном от индекса low «= п high = 4. Его середина приходится иа индекс mid = 2. Центральным До ментом является A[mid] = 300. Поменять местами центральный элемент6 Aflow] и присвоить начальные значения индексам scanUp и scanDown; С
scanUp - 1 = low + 1 scanDown » 4 = high
Сканирование вверх останавливается на индексе 2 <А[2] > центрального элемента)
Сканирование вниз останавливается на индексе 1 (А(1] < центрального элемента)
Начальные значения
300 14
150 400 450 350
После сканирования
150	400	450	350
scanDown scanUp
scanUp
scanDown
Так как scanDown < scanUp, процесс останавливается. При этом scanDown является точкой разделения двух еще меньших подсписков: А[0] и А[2]-А[4]. Завершить обработку, поменяв местами AfspanDown] = 150 и A[low] = 300. Заметьте, что положение центрального элемента дает^Пам одноэлементный и трехэлементный подсписки. Рекурсивный процесс прекращается на пустом или одноэлементном подсписках.
I 150 ‘ЗМ 400 450 350
LJ— I I
A|OJ	А[21-А|4]
Подсписок 8ь. Диапазон подсписка — от индекса low = 6 до high = 9. Его середина приходится на индекс mid = 7. Центральным элементом являет A[mid] - 800. Поменять местами центральный элемент с A[low] и присво начальные значения индексам scanUp и scanDown:
scanUp - 7 - low + 1
scanDown » 9 = high
Сканирование вверх останавливается по достижении конца списка (scanUp=101 scanDown остается на начальной позиции
Начальные значения
После сканирования
800
650 600 700
800
650 600 700
конец списка
scanUp scanDown
scanDown .scanUp
Так как scanDown < scanUp, процесс останавливается. При этом*sC	по-
является точкой вставки центрального элемента. Завершить обработ
доняв местами A[scanDown] = 700 и A[low] = 800. Обратите внимание, что положение центрального элемента дает нам трехэлементный и пустой подсписки. Рекурсивный процесс прекращается на пустом или одноэлементном подсписках.
700	650	600	600
Завершение сортировки
Обработать подсписок 400, 450, 350 (А[2]—А[4]). Центральным элементом является 450.
На фазе сканирования элементы выстраиваются в порядке 350, 400, 450. Для двухэлементного подсписка 350, 400 понадобится еще один рекурсивный вызов.
Обработать подсписок 700, 650, 600 (А[6]—А[8]). Центральным элементом является 650.
После сканирования элементы выстраиваются в порядке 600, 650, 700.
Числа 600 и 700 образуют два одноэлементных подсписка.
"Быстрая сортировка" завершена. Результатом является следующий сортированный список:
А = 150, 300, 350, 400, 450, 550, 600, 650, 700, 800
Алгоритм Quicksort
Этот рекурсивный алгоритм разделяет список A[low]—Afhigh] по центральному элементу, который выбирается из середины списка:
pivot « A(mid];	// mid - (high+low)/2
После обмена местами центрального элемента с A[low], задаются начальные значения индексам scanUp = low + 1 и scanDown = high, указывающих иа начало и конец списка, соответственно. Алгоритм управляет этими двумя индексами. Сначала scanUp продвигается вверх по списку, пока не превысит значение scanDown или пока не встретится элемент больший, чем центральный.
Ч индекс scanUp пробегав? по элементам,
Ч меньшим или равным центральному
while (scanUp <- scanDown SS A[scanUp] <= pivot)
scanUp++;	// перейти к следующему элементу
После позиционирования scanUp индекс scanDown продвигается вниз по сПИску, пока не встретится элемент, меньший или равный центральному.
сканировать верхний подсписок в обратном направлении.
Ч сканировать верхний подсписок в обратном направлении.
1 остановиться, когда scanDown укажет на элемент, меньший
1 или равный центральному, сканирование должно прекратиться Лп₽и Allow] - pivot
«ile (pivot < A[scanDown])
scanDown—;
pivot
По окончании этого цикла (и при условии что scanUp < scanDown) оба ййдекса адресуют два элемента, находящихся не в тех подсписках. Эти эле-меняются местами.
// поменять местами больший элемент из нижнего подсписка //с меньшим элементом из верхнего подсписка
Swap (A[scanUp]t A[scanDown]);
Обмен элементов прекращается, когда scanDown становится меньше scanUp. В этот момент scanDown указывает начало левого подсписка, кот Чем содержит меньшие или равные центральному элементы. Индекс scanDown новится центральным. Взять центральный элемент из A[low]:	П СТа-
Swap(A[low], А(scanDown]);
Для обработки подсписков используется рекурсия. После обнаружен точки разбиения мы рекурсивно вызываем Quicksort с параметрами low, mid*! и mid+1, high. Условие останова возникает, когда в подсписке остается* Мек * 1 двух элементов, так как одноэлементный или пустой массив всегда упорядоче^ Функция Quicksort находится в файле arrsort.h.
// Quicksort принимает в качестве параметров массив
//и предельные значения его индексов
template <class Т>
void Quicksort(Т А[], int low, int high)
I
// локальные переменные, содержащие индекс середины mid, // центральный элемент и индексы сканирования
t pivot;
int scanUp, scanDown;
int mid;
// если диапазон индексов не включает в себя как минимум два элемента, вернуться if (high - low <- О) return;	*
else
// если в подсписке два элемента, сравнить их межЬу собой
//и поменять местами при необходимости	(
if (high - low 1) {
if (A(high] < A[low]) Swap(Allow], A[high]); return;
}
// получить индекс середины и присвоить указываемое им значение // центральному значению
mid - (low + high)/2;	V
pivot = A [mid];
// поменять местами центральный и начальный элементы списка
//и инициализировать индексы scanUp и scanDown
Swap (A[mid], Allow]);
scanUp = lokw + 1;
scanDown - high;
// искать элементы, расположенные не в тех подсписках-
// остановиться при scanDown < scanUp do
I
// продвигаться вверх по нижнему подсписку; остановиться, //. когда scanUp укажет на верхний подсписок или если // указываемый этим индексом элемент > центрального
while (scanUp <- scanDown && A[scanUp] <= pivot) scanup++;
// продвигаться вниз по верхнему подсписку; остановиться, // если scanDown укажет элемент <= центрального.
// остановка на элементе A[low] гарантируется
while (pivot < A[scanDown])
scanDown—;
// если индексы все еще в своих подсписках, то они указывают // два элемента, находящихся не в тех подсписках.
If Поменять их местами if (scanUp < scanDown)
{
Swap (A[scanUp], A [scanDown]); >
)
while (scanUp < scanDown);
/f копировать центральный элемент в точку разбиения Allow] = A[scanDown];
A [scanDown] = pivot;
// если нижний подсписок (low...scanDown-1) имеет 2 или более
// элементов, выполнить рекурсивный вызов
if (low < scanDown-1)
Quicksort(A, low, scanDown-1);
// если верхний подсписок (scanDown+1___high) имеет 2 или более
// элементов, выполнить рекурсивный вызов
if (scanDown+1 < high)
Quicksort(A, scanDown+1, high);
Вычислительная сложность "быстрой сортировки". Общий анализ эффективности "быстрой сортировки” достаточно труден. Будет лучше показать ее вычислительную сложность, подсчитав число сравнений при некоторых идеальных допущениях. Допустим, что и — степень двойки, п = 2к (к = log2n), а центральный элемент располагается точно посередине каждого списка и разбивает его на два подсписка примерно одинаковой длины.
При первом сканировании производится п-1 сравнений. В результате создаются два подсписка размером п/2. На следующей фазе обработка каждого подсписка требует примерно п/2 сравнений. Общее число сравнений на этой фазе равно 2(п/2) = п. На следующей фазе обрабатываются четыре подсписка, что требует 4(п/4) сравнений, и т.д. В конце концов процесс разбиения прекращается после к проходов, когда получившиеся подсписки содержат по одному элементу. Общее число сравнений приблизительно равно
п + 2(п/2) + 4(п/4) + ... + n(n/n) = n + n + ... + n = n*k = n* log2n
Для списка общего вида вычислительная сложность "быстрой сортировки” Равна O(n log2n). Идеальный случай, который мы только что рассмотрели, Фактически возникает тогда, когда массив уже отсортирован по возрастанию. 1огДа центральный элемент попадает точно в середину каждого подсписка.
Список отсортированный по возрастанию
10	20	30	40	50	60	70	80
пограничный элемент
Если массив отсортирован по убыванию, то иа первом проходе центп элемент находится в середине списка и меняется местами с каждым том как в нижнем, так и в правом подсписке. Результирующий список**1**®' отсортирован, алгоритм имеет сложность порядка O(n log2n). ’ * П°Чти
Список отсортированный по убыванию
| 80 | 70 | 60 | 50 | 40 | 30 | 20 | 10~|
Поменять местами A[low] и A[mid], Выполнить проход
|_50 I 70 I 60 I 80 | 40 I 30 I 20 I 10
---- f f f f
После первого прохода
Но I W I 20 I зоТздТвдГбоКг
Наихудшим сценарием для "быстрой сортировки" будет тот, при котов центральный элемент все время попадает в одноэлементный подсписок, а всМ прочие элементы остаются во втором подсписке. Это происходит тогда,’ когда центральным всегда является наименьший элемент. Рассмотрим последов^ тельность 3, 8,1, 5, 9. В первом проходе производится п сравнений, а больший подсписок содержит п-1 элементов. В следующем проходе этот подсписок требует п-1 сравнений и дает подсписок из п-2 элементов и т.д. Общее число сравнений равно
п + п-1 + п-2 + ... + 2 = п(п+1)/2 - 1
Сложность худшего случая равна О(п2), т.е. не лучше, чем для сортировок вставками и выбором» Однако этот случай является паталогическим и маловероятен на практике. В общем, средняя производительность "быстрой сортировки" выше, чем у всех рассмотренных нами сортировок.
Алгоритм Quicksort выбирается за основу в большинстве универсальных сортирующих утилит. Если вы не можете смириться с производительностью наихудшего случая, используйте пирамидальную сортировку — более устойчивый алгоритм, сложность которого равна O(n logzn) и зависит только от размера списка.
Сравнение алгоритмов сортировки массивов
Мы сравнили алгоритмы сортировки, испытав их йа массивах, содержащих 4000, 8000,10000, 15000 и 20000 целых чисел, соответственно. Время выполнения измерено в тиках (1/60 доля секунды). Среди всех алгоритмов порядка О(п2) время сортировки вставками отражает тот факт, что на i-ом проходе требуется лишь i/2 сравнений. Этот алгоритм явно превосходит все прояи сортировки порядка О(п2). Заметьте, что самую худшую общую производитель^ ность демонстрирует сортировка методом пузырька. Результаты испытан
показаны на рис. 14.2.
	Обменная сортировка	Сортировка выбором	Пузырьковая сортировка	Сортировка вставками____—-
п = 4000	12.23	17.30	15.78	5-67
п “ 8000	49.95	29.43	64.03	23.15
п - 10000	77.47	46.02	99.10	35.43
л = 15000	173.97	103.00	223.28	80.23
п = 20000	313.33	185.05	399 47	143.67
Для иллюстрации эффективности алгоритмов сортировки в экстремальных случаях используются массивы из 20000 элементов, отсортированных по возрастанию и по убыванию. При сортировке методом пузырька и сортировке вставками выполняется только один проход массива, упорядоченного по возрастанию, в то время как сортировка посредством выбора зависит только от размера списка и выполняет 19999 проходов. Упорядоченность данных по убыванию является наихудшим случаем для пузырьковой, обменной и сортировки вставками, зато сортировка выбором выполняется, как обычно.
Сортировки сложности О(п2) упорядоченных массивов
	Обменная сортировка	Сортировка выбором	Пузырьковая сортировка	Сортировка вставками
п « 8000 (упорядочен по возрастанию)	185.27	185.78	.03	.05
л = 8000 (упорядочен убыванию)	526.17	199.0	584.67	286.92
В общем случае Quicksort является самым быстрым алгоритмом. Благодаря своей эффективности, равной O(n log2n), он явно превосходит любой ^горитм порядка О(п2). Судя по результатам испытаний, приведенных на
14.3,	он также быстрее любой из сортировок порядка O(n log2n), расширенных нами в гл. 13. Обратите внимание, что эффективность "быстрой с°ртировки" составляет O(n log2n) даже в экстремальных случаях. Зато сор-’йровка посредством поискового дерева становится в этих случаях О(п2) слож-так как формируемое дерево является вырожденным.
Сортировки порядка О(п login)
	Турнирная сортировка	Сортировка посредством дерева	Пирамидальная сортировка	"6ь1страя со₽тир0ВКа.,
n = 4000	0.28	0.32	0.13	
л = 8000	0.63	0.68	0,28	017
n = 10000	0.90	0.92	0.35	022
n = 15000	1.30	1.40	0.58	033	-
n = 20000	1.95	1.88	0.77	0.47	--
л = 8000 (упорядочен по возрастанию)	1-77	262.27	0.75	0.23
n = 8000 (упорядочен по убыванию)	1.65	275.70	0.80	0.28
				
Рис. 14.3. Сравнение сортировок порядка O(n logjn)
Программа 14.3. Сравнение сортировок
Эта программа осуществляет сравнение алгоритмов сортировки данных, представленных на рис. 14.2 и 14.3. Здесь дается только базовая структура программы, а полный листинг находится в файле prgl4 1.cpp. Хронометраж производится с помощью функции TickCount, возвращающей число 1/60 долей секунды, прошедших с момента старта программы. Эта функция находится в файле ticks.h.
flinclude <iostream.h>
flinclude "arrsort.h"
// перечислимый тип, описывающий начальное
// состояние массива данных
enum Ordering {randomorder, ascending, descending)?
// перечислимый тип, идентифицирующий алгоритм сортировки
enum SortType {exchange, selection, bubble, insertioi tournament, tree, heap, quick);
// копировать n-злеыентный массив у в массив х void Copy lint * *х, int *у, int п) {
for (int х“0; i<n; i++)
*x++ - *y++;
)
// общая сортирующая функция, которая принимает исходный массив //с заданной упорядоченностью элементов и применяет указанный
// алгоритм сортировки
void Sort(int а[], int n, SortType type, Ordering order)
(
long time;
cout « ’’Сортировка " « n;
// вывести тип упорядоченности switch(order)
case random:	cout « " элементов.
break;
case ascending: cout « " элементов, упорядоченных по возрастанию break;
case descending: cout « " элементов, упорядоченных no убыванию. " break;
}
// засечь время time = TickCount();
// вывести тип сортировки и выполнить ее switch(type) (
case exchange: ExchangeSort(a, n);
cout « "Сортировка обменом: "; break; case selection: Selectionsort(a, n);
cout « "Сортировка выбором: *’;
*	break;
case bubble:	............
case insertion: .............
case tournament: ....... case tree:	............
case heap:	............
case quick:	............
>
// подсчитать время выполнения в секундах time = TickCount() - time; cout « time/60.0 « endl; )
// запустить сортировки для n чисел, // расположенных в заданном порядке void RunTest(int п, ordering order) (
int i;
int *a, *b;
SortType stype; RandomNumber rnd;
// выделить память для двух поэлементных массивов а и b а = new int (п); b = new int (n);
// определить тип упорядоченности данных if (order - randomorder)
II заполнить массив b случайными числами for (i=0; i<n; i++) { b[i] = rnd.Random(n); > else
// данные, отсортированные по возрастанию for (i=0; i<n; i++) {
b[i] = i;
) else
// данные, отсортированные no убыванию for (i=0; i<n; i++) {
b[i] = n - 1 - i;
else
// копировать данные в массив а. выполнить каждую сортировку
for(stype»exchange; stype<=quick; stype=sortType(stype+1))
Copy(a, b, n);
Sort(a, n, stype, order);
}
// Удалить оба динамических массива
delete [] а;
delete [] b;
}
// сортировать 4000, 8000, 10000, 15000 и 20000 случайных чисел.
// затем отсортировать 20000 элементов, упорядоченных по возрастанию,
//и 20000 элементов, упорядоченных по убыванию
void main (void)
(
int nelts[5] = {4000, 8000, 10000, 15000, 20000), i;
cout.precision(3);
cout.set f(ios::fixed | ios::showpoint);
for (1-0; i<5; i++)
RunTest(nelts[i], randomorder);
RunTest(20000, ascending);
RunTest(20000, descending);
14.3. Хеширование
В этой книге мы вывели ряд списковых структур, позволяющих ИР° ме-клиенту осуществлять поиск и выборку данных. В каждой такой стрУ*
! меТОД Find выполняет обход списка и ищет элемент данных, совпадающий с ' ^дючом. При этом эффективность поиска зависит от структуры списка. В
дучае последовательного списка метод Find гарантированно просматривает^ | л(ц) элементов, в то время как в случае бинарных поисковых деревьев и При I ^gapHOM поиске обеспечивается более высокая эффективность O(log2n). В I идеале нам хотелось бы выбирать данные за время 0(1). В этом случае число . „^обходимых сравнений не зависит от количества элементов данных. Выборка ! элемента осуществляется за время 0(1) при использовании в качестве индекса  в массиве некоторого ключа. Например, блюда из меню в закусочной в целях
прощения бухгалтерского учета обозначаются номерами. Какой-нибудь де-дпкатес типа "бастурма, маринованная в водке'* в базе данных обозначается просто #2. Владельцу закусочной остается лишь сопоставить ключ 2 с записью : s списке.
Ключ	Наименование	Цена Продано штук
|	2 [Бастурма, маринованная в водке $3.50 |	43
к
к
Lo

Ln-2
Элементы меню
Мы знаем и другие примеры. Файл клиентов пункта проката видеокассет содержит семизначные номера телефонов. Номер телефона используется в качестве ключа для доступа к конкретной записи файла клиентов.
| Номер телефона | Имя клиента, название фильма и т. д.
Ключи не обязательно должны быть числовыми. Например, формируемая компилятором таблица символов (symbol table) содержит все используемые в программе идентификаторы вместе с сопутствующей каждому из них информацией. Ключом для доступа к конкретной записи является сам идентификатор.
।
Ключи и хеш-функция
, В общем случае ключи ие настолько просты, как в примере с закусочной. I Несмотря на то, что они обеспечивают доступ к данным, ключи, как правило, ' Ве являются непосредственными индексами в массиве записей. Например, телефонный номер может идентифицировать клиента, но вряд ли пункт проката хранит десятимиллионный массив.
Запись клиента
9999998 9999999
	
В большинстве приложений ключ обеспечивает косвенную ссылку на дан-Ключ отображается во множество целых чисел посредством хеш-функ-; (hash function). Полученное в результате значение затем используется ; Доступа к данным. Давайте исследуем эту идею.
i Предположим, есть множество записей с целочисленными ключами. Хеш-Фикция HF отображает ключ в целочисленный индекс из диапазона О...п-1.
С хеш-функцией связана так называемая хеш-таблица (hash table) которой пронумерованы от О до п-1 и хранят сами данные или ссыт/*6^ данные.
Пример 14.1
Предположим, Key — положительное целое, a HF(Key) — значение младшей цифры числа Key. Тогда диапазон индексов равен 0___9. На-
пример, если Key = 49, HF(Key) = HF(49) = 9. Эта хеш-функция в качестве возвращаемого значение использует остаток от деления на 10
// Хеш-функция, возвращающая младшую цифру ключа int HF(int key) (
return key % 10,-}
Часто отображение, осуществляемое хеш-функцией, является отображением "многие к одному" и приводит к коллизиям (collisions). В примере 14.1 HF(49) = HF(29) = 9. При возникновении коллизии два или более ключа ассоциируются с одной и той же ячейкой хеш-таблицы;Поскольку два ключа не могут занимать одну и ту же ячейку в таблице, мы должны разработать стратегию разрешения коллизий- Схемы разрешения коллизий обсуждаются после знакомства с некоторыми типами хеш-функций.
Хеш-функции
Хеш-функция должна отображать ключ в целое число из диапазона О...п-l-При этом количество коллизий должно быть ограниченным, а вычисление самой хеш-функции — очень быстрым. Некоторые методы удовлетворяю
этим требованиям.	%
Наиболее часто используется метод деления (division method), требуют двух шагов. Сперва ключ должен быть преобразован в целое число, я 3 полученное значение вписывается в диапазон О...п-l с помощью опера получения остатка. На практике метод деления используется в болыпя®
приложений с хешированием.
Пример 14.2
1. Ключ — пятизначное число. Хеш-функция извлекает две младшие цифры. Например, если это число равно 56389, то HF(56389) = 89. Две младшие цифры являются остатком от деления на 100.
int HF(int key) < return key % 100	// метод деления на 100
}
Эффективность хеш-функции зависит от того, обеспечивает ли она равномерное рассеивание ключей в диапазоне О...п-l. Если две последние цифры соответствуют году рождения, то будет слишком много коллизий при идентификации подростков, играющих в юно шеской бейсбольной лиге.
2. Ключ — символьная строка языка C++. Хеш-функция отображает эту строку в целое число посредством суммирования первого и последнего символов и последующего деления на 101 (размер таблицы).
// хеш-функция для символьной строки.
// возвращает значение в диапазоне от 0 до 100 int HF(char *key) { int len = strlen(key), hasnf = 0;
// если длина ключа равна 0 или 1, возвратить кеу[0] .
// иначе сложить первый и последний символ if (len <= 1) hashf = keylO];
else
hashf = key[0] + key[len-l];
return hashf % 101; }
Эта хеш-функция приводит к коллизии при одинаковых первом и последнем символах строки. Например, строки "start” и "slant" будут отображаться в индекс 29. Так же ведет себя хеш-функция, суммирующая все символы строки.
int HF(char *key) ( int hashf =0;
// просуммировать все символы строки и разделить на 101 while (*key)
hashf «•= *key++;
return hashf % 101; }
Строки "bad." и "dab" преобразуются в один и тот же индекс. Лучшие результаты дает хеш-функция, производящая перемешивание битов в символах. Пример более удачной хеш-функции для строк представлен вместе с программой 14.2.
В общем случае при больших п индексы имеют больший разброс. Кроме того, математическая теория утверждает, что распределение будет более равномерным, если п — простое число.
Другие методы хеширования
Метод середины квадрата (midsquare technique) предусматривает ппепк вование ключа в целое число, возведение его в квадрат и возвращение стве значения функции последовательности битов, извлеченных из сен этого квадрата. Предположим, что ключ есть целое 32-битовое число следующая хеш-функция извлекает средние 10 бит возведенного в ключа.	ВаАРат
//возвратить средние 10 бит произведения key*key int HF(int key);
{
key *= key;	// возвести ключ в квадрат
key »= 11;	// отбросить 11 младших бит
return key % 1024	// возвратить 10 младших бит
При мультипликативном методе (multiplicative method) используется случайное действительное число f в диапазоне 0<f<l. Дробная часть пронзве дения f*key лежит в диапазоне от 0 до 1. Если это произведение умножить на п (размер хеш-таблицы), то целая часть полученного произведения даст значение хеш-функции в диапазоне от 0 до n—1.
// хеш-фуикция, использующая мультипликативный метод;
// возвращает значение в диапазоне 0...700
int HF(int key);
{
static RandomNumber rnd;
float f;
// умножить ключ на случайное число из диапазона 0...1 f =* key * rnd. fRandom () ;
И взять дробную часть f - t - int(f);
// возвратить число в диапазоне О...п-l
return 701*f;
Разрешение коллизий
Несмотря на то, что два или более Ключей могут хешироваться одинаково, они не могут занимать в хеш-таблице одну и ту же ячейку. Нам остаются два пути: либо найти для нового ключа другую позицию в таблице, либо создать для каждого значения хеш-функции отдельный список, в котором будут все ключи, отображающиеся при хешировании в это значение. Ооа варианта представляют собой две классические стратегии разрешения коллизий — открытую адресацию с линейным опробыванием (linear probe open addressing) и метод цепочек (chaining with separate lists). Мы цроиллюстР,\ руем на примере открытую адресацию, а сосредоточимся главным образ на втором методе, поскольку эта стратегия является доминирующей.
Открытая адресация с линейным опробыванием. Эта методика пред11 гает, что каждая ячейка таблицы помечена как незанятая. Поэтому добавлении нового ключа всегда можно определить, занята ли данная яч таблицы или нет. Если да, алгоритм осуществляет "опробывание" п° к^йе пока не встретится "открытый адрес" (свободное место). Отсюда и ваЗБ1{Л1о-метода. Если размер таблицы велик относительно числа хранимых там
чеЙ, метод работает хорошо, поскольку хеш-функция будет равномерно рассеивать ключи по всему диапазону и число коллизий будет минимальным.
мере того как коэффициент заполнения таблицы приближается к 1, эффективность процесса заметно падает. Проиллюстрируем линейное опробы-вание на примере семи записей.
Пример 14.3
Предположим, что данные имеют тип DataRecord и хранятся в 11-элементной таблице.
struct DataRecord (
int key;
int data;
>;
В качестве хеш-функции HF используется остаток от деления на 11, принимающий значения в диапазоне 0—10.
HF(item) =* item, key % 11
В таблице хранятся следующие данные. Каждый элемент помечен числом проб, необходимых для его размещения в таблице.
Список: {54,1), (77,3), (94,5), (89,7), {14,В), {45,2}, <76,9}
Хеш-таблица
[77(1)13 89(1)1 7 45(2)12 14(1)1 8 76(6)1 9	1 94(1)1 5 j 1~	 54(1)11
0	1	23456789	10
Хеширование первых пяти ключей дает пять различных индексов, по которым эти ключи запоминаются в таблице. Например, HF({54,1}) ж 10, и этот элемент попадает в ТаЫе[10]. Первая коллизия возникает между ключами 89 и 45, так как оба они отображаются в индекс 1. Элемент данных {89,7} вдет первым в списке и занимает позицию ТаЫе[1]. При попытке записать {45,2} оказывается, что место ТаЫе[1] уже занято. Тогда начинается последовательное опробывание ячеек таблицы с целью нахождения свободного места. В данном случае это ТаЫе[2]. На ключе 76 эффективность алгоритма сильно падает. Этот ключ хешируется в индекс 10 — место, уже занятое. В процессе оп-робывания осуществляется просмотр еще пяти ячеек, прежде чем будет найдено свободное место в ТаЫе[4]. Общее число проб для размещения в таблице всех элементов списка равно 13, т.е. в среднем 1,9 проб на элемент.
Реализация алгоритма открытой адресации дана в упражнениях.
Метод цепочек. При другом подходе к хешированию таблица рассматривается как массив связанных списков или деревьев. Каждая такая цепочка вызывается .блоком (bucket) и содержит записи, отображаемые хеш-функцией в один и тот же табличный адрес. Эта стратегия разрешения коллизий на-ЭЬ1вается методом цепочек.
Хеш-таблица
Если таблица является массивом связанных списков, то элемент данзп. просто вставляется в соответствующий список в качестве нового узла. Чтоб обнаружить элемент данных, нужно применить хеш-функцию для определен Ы нужного связанного списка и выполнить там последовательный поиск. я
Пример 14.4
Проиллюстрируем метод цепочек на семи записях типа DataRecord и хеш-функции HF из примера 14.3.
Список: {54,1}, (77,3), {94,5), (89,7), {14,8}, {45,2}, {76,9}
HF{item) - item.key % 11
Каждый новый элемент данных вставляется в хвост соответствующего связанного списка. В следующей таблице каждое значение данных сопровождается числом проб, требуемых для запоминания этого значения в таблице.
»| 45(2) f 2 I NULL 1
Заметьте, что если считать пробой вставку нового узла, то их обгц число при включении семи элементов равно 9, т.е. в среднем 1,3 ПР° На элемент данных.
В общем случае метод цепочек быстрее открытой адресации, так сматривает только те ключи, которые попадают в один и тот же таб адрес. Кроме того, открытая адресация предполагает наличие таблицы ^^дарованного размера, в то время как в методе цепочек элементы таблицымят#-ются динамически, а длина списка ограничена лишь количеством п Основным недостатком метода цепочек являются дополнительные затра мяти на поля указателей. В общем случае динамическая структуре пепочек более предпочтительна для хеширования.
14.4. Класс хеш-таблиц
В этом разделе определяется общий класс HashTable, осуществляющий хеширование методом цепочек. Этот класс образуется от базового абстрактного класса List и обеспечивает механизм хранения с очень эффективными метода-ми доступа. Допускаются данные любого типа с тем лишь ограничением, что для этого типа данных должен быть определен оператор ==. Чтобы сравнить ключевые поля двух элементов данных, прикладная программа должна перегрузить оператор ==.
Мы также рассмотрим класс HashTablelterator, облегчающий обработку данных в хеш-таблице» Объект типа HashTablelterator находит важное применение при сортировке и печати данных.
Объявления и реализации этих классов находятся в файле hash.h.
Спецификация класса HashTablelterator
ОБЪЯВЛЕНИЕ
^include "array.h"
#include "list.h"
^include "link.h"
Iinclude " i t e r at or. h"
template <class T>
class HashTablelterator;
template <class T>
class HashTable: public List<T> <
protected:
П число блоков; представляет размер таблицы int numBuckets;
// хеш-таблица есть массив связанных списков Дггау< LinkedList<T> > buckets;
// хеш-функция и адрес элемента данных, //к которому обращались последний раз unsigned long (*hf)(Т key);
Т ‘current;
public:
If конструктор с параметрами, включающими
// размер таблицы и хеш-функцию
HashTable(int nbuckets, unsigned long hashffT key));
if методы обработки списков
virtual void Insert(const T& key);
virtual int Find(T& key);
virtual void Delete(const TS key);
virtual void ClearList(void);
void Update (const та key) ;
// дружественный итератор, имеющий доступ к // данным-членам
friend class HashTableIterator<T>
ОдИСАНИЕ
Объект типа HashTable есть список элементов типа Т. В нем реализованы Ьсе методы, которые требует абстрактный базовый класс List. Прикладная
программа должна задать размер таблицы и хеш-функцию, преобразуют элемент типа Т в длинное целое без знака. Такой тип возвращаемого значе 10 допускает хеш-функции для широкого диапазона данных. Деление на р»-/11'3 таблицы осуществляется внутри.	МеР
Методы Insert, Find, Delete и ClearList являются базовыми методами работки списков. Отдельный метод Update служит для обновления элемев°б уже имеющегося в таблице.	а»
Методы ListSize и ListEmpty реализованы в базовом классе. Элемент д ных current всегда указывает на последнее доступное значение данных, о используется методом Update и производными классами, которые должны возвращать ссылки. Пример такого класса дан в разделе 14.7.
ПРИМЕР
Предположим, что NameRecord есть запись, содержащая поле наименова ния и поле счетчика.
struct NameRecord (
String name;
int count;
);
// 101-элементная т //и имеющая хеш-фу;	аблица, содержащая данные типа NameRecord нкцию hash
HashTable<NameRecor	d> HF( 101,hash);
// вставить запись NameRecord rec;	("Betsy",!} в таблицу // переменная типа NameRecord
rec. name = "Betsy’'; rec.count “ 1; HF.Insert(rec);	И присвоение name « "Betsy // и count = 1 // Вставить запись
COUt « HF.ListSize	О; // распечатать размер таблицы
// выбрать значение // увеличить поле с rec.name = '’Betsy”;	данных, соответствующее ключу "Betsy", четчика на 1 и обновить запись
if (HF.Find(rec) ( rec. cout +” 1; HF.Update(rec); 1	// найти "Betsy" // обновить поле данных // обновить запись в таблице
J else cerr « "Ошибка:	\"Ключ Betsy должен быть в таблице.\"\n,-i
Класс HashTabl держит методы Д)	lelterator образован из абстрактного класса Iterator и с° 1Я просмотра данных в таблице.
Спецификация класса HashTablelterator
ОБЪЯВЛЕНИЕ
template cclass T>
class HashTablelterator: public Iterator<T> (
private:
// указатель таблицы, подлежащей обходу HashTable<T> *HashTable;
// индекс текущего просматриваемого блока // и указатель на связанный список int currentBucket;
LinkedList<T> *currBucket₽tr;
// утилита для реализации метода Next void SearchNextNode(int cb);
public:
Il конструктор
HashTablelterator (HashTable<T>S ht);
// базовые методы итератора
virtual void Next(void);
virtual void Reset(void);
virtual T& Data(void)i
!I подготовить итератор для сканирования другой таблицы
void SetList(HashTable<T>& 1st);
Ь'
ОПИСАНИЕ
Метод Next выполняет прохождение таблицы список (блок) за списком, проходя узлы каждого списка. Значения данных, выдаваемые итератором, никак не упорядочены. Для обнаружения очередного списка, подлежащего прохождению, метод Next использует функцию SearchNextNode.
пример
U объявить итератор для объекта HF типа HashTable HashTableiterator<NameRecord> Liter(HF);
11 сканировать все элементы базы данных
for (hiter.Reset(); !hiter.EndofList; hiter.Next()) (
rec ” hiter. Data ();
cout « rec.паше « ": ” « rec.count « endl; )
Приложение: частота символьных строк
Класс HashTable используется для хранения множества символьных строк и определения частоты их появления в файле. Каждая символьная строка хранится в объекте типа NameRecord, содержащем наименование строки и частоту ее повторяемости.
struct NameRecord (
String паше;
int count;
I;
Хеш-функция перемешивает биты символов строки путем сдвига текущего значения функции на три бита влево (умножая на 8) перед тем, как прибавить следующий символ. Для n-символьной строки с0с1...сл.2сл.1
п-1
hash(s) = 5} Ci 8n-i-1
i=l
Такое вычисление предотвращает проблемы хеширования символьных СтРок, рассмотренные в примере 14.2.
Функция для использования в классе HashTable
^signed long hash (NameRecord elem)
unsigned long hashval = 0;
// сдвинуть hashval на три бита влево и // сложить со следующим символом for (int i=0; i<elem.Length(); i++)
hashval - (hashval « 3) + elem.name[i] ; return hashval
Программа 14.2. Вычисление Частот символьных строк
Эта программа вводит символьные строки из файла strin.gs.dat и зац0 минает их в 101-влементной таблице. Каждая символьная строка вводится из файла и, если еще не встречалась ранее, помещается в таблицу. дЛя дублирующихся строк из хеш-таблицы выбирается соответствующая за пись, где и производится увеличение на единицу поля счетчика. В конце программы определяется итератор, который используется для просмотра и печати всей таблицы. Определения NameRecord, хеш-функции и оператора =— для данных типа NameRecord находятся в файле strfreq.h.
((include <iostream.h>
Оinclude <fstream.h>
((include <stdlib.h>
#include "hash-h"
#include ”strclass.h"
((include "strfreq.h"
void main (void)
(
// ввести символьные строки из входного потока	s'"
ifstream fin;	f
NameRecord rec;
String token;
HashTable<NameRecord> HF(101, hash);
fin.open("strings.dat" 1, ios:: in | ios::nocreate);
if (Ifin) {
cerr « "Невозможно открыть V'strings.datV!" « endl;
exit(l);	J
)	I
while (fin » rec.name)
// искать строку в таблице, если найдена, обновить поле count if (HF.Find(rec))
{ rec.count += 1; HF.Update(rec);
)
else
(
rec.count = 1;
HF.Insert(rec);
)
}
// печатать символьные строки вместе с частотами HashTableIterator<NameRecord> hi ter (HF) ;
for(hiter.Reset О; !hiter.EndOfList(); hiter.Next()) (
rec = hiter.Data();
cout « rec.name «	*' « rec.count « endl;
)
I
/*
<Файл strings.dat?
Columbus Washington Napoleon Washington Lee Grant Washington Lincoln Grant Columbus Washington
<Прогон программы 14.2>
Lee: 1
Washington: 4
Lincoln: 1
Napoleon: 1 Grant: 2
Columbus: 2
*/
Реализация класса HashTable
Данный класс образован от абстрактного класса last, предоставляющего методы ListSize и ListEmpty. Мы обсудим элементы данных класса HashTable и операции, реализующие чистые виртуальные функции Insert, Find, Delete и ClearList.
Ключевым элементом данных класса является объект buckets типа Array, который определяет массив связанных списков, образующих хеш-таблицу. Указатель функции hf определяет хеш-функцию, a numBuckets является размером таблицы. Указатель current идентифицирует последний элемент данных, к которому осуществлялся доступ тем илн иным методом класса. Его значение задается методами Find и Insert и используется методом Update для обновления данных в таблице.
Методы обработки списков. Метод Insert вычисляет значение хеш-функции (индекс блока) и ищет объект типа LinkedList, чтобы проверить, есть ли уже такой элемент данных в таблице или нет. Если есть, то Insert обновляет этот элемент данных, устанавливает на него указатель current и возвращает управление. Если такого элемента в таблице нет, Insert добавляет его в хвост списка. Устанавливает на него указатель current и увеличивает размер списка.
template <class Т>
void HashTable<T>::Insert(const T4 key)
// hashval — индекс блока (связанного списка)
int hashval « int(hf(key) % numBuckets);
Il 1st — псевдоним для buckets[hashval].
Il помогает обойтись без индексов
LinkedList<T>b 1st - buckets[hashval];
for (1st.Reset(); ’1st.EndOfList(); 1st.Next())
Il если ключ совпал, обновить данные и выйти
if (1st.Data() — key)
1st.Data() = key;
current = blst.Data(); return;
}
11 данные, соответствующие этому ключу, не найдены, вставить элемент в 1st.InsertRear(key);	Список
current - &Lst.Data() size++; }
Метод Find применяет хеш-функцию и просматривает указанный в pe3v тате список на предмет совпадения с входным параметром. Если совпаден обнаружено, метод копирует данные в key, устанавливает указатель current 6 соответствующий узел и возвращает True. В противном случает метод возв^ щает False.	₽а
template «class Т> int HashTable<T>::Find(Тб key) (
// вычислить значение хеш-функции и установить 1st
// на начало соответствующего связанного списка
int hashval = int(hf(key) % NumBuckets);
LinkedList<T>& 1st - buckets[hashval];
// просматривать узлы связанного списка в поисках key
for (1st.Reset(); !1st.EndOfList(); 1st.Next())
’ // если ключ совпал, получить данные, установить current и выйти if (1st.Data() =" key) <
key = 1st.Data();
current = &lst.Data();
return 1;	// вернуть True
} return 0;	// иначе вернуть False
}
Метод Delete просматривает указанный список и удю^яет узел, если совпадение произошло. Этот метод (вместе с методами ClearList и Update) находится в файле hash.h.
Реализация класса HashTablelterator
Этот класс должен просматривать данные, разбросанные по хеш-таблице. Поэтому он более интересен и более сложен с точки зрения реализации, чем класс HashTable. Обход элементов таблицы начинается с поиска непустого блока в массиве списков. Обнаружив непустой блок, мы просматриваем все узлы этого списка, а затем продолжаем процесс, взяв другой непустой блок. Итератор заканчивает обход, когда просмотрен последний непустой блок-
Итератор должен быть привязан к списку. В данном случае переменной has Table присваивается адрес таблицы. Поскольку класс HashTablelterator явлЯ^1Г(1 дружественным по отношению к HashTable, он имеет доступ ко всем закрыт данным-членам последнего, включая массив buckets и его размер numBuckets. 1 менная currentBucket является индексом связанного списка, который пР<хяйа'1Хие вается в данный момент, a currBucketPtr — указателем этого списка. Прохожд® с каждого блока осуществляется итератором, встроенным в класс LinkedList. На Р 14.4 показано, как итератор проходит таблицу с четырьмя элементами.	а>
Метод SearchNextNode вызывается для обнаружения очередного ^^^а подлежащего прохождению. Просматриваются все блоки, начиная с cb,
HashTablelterator
Итератор извлекает 10 2 22 29
Рис. 14.4. Итератор хэш-таблиц
не встретится непустой список. Переменной currentBucket присваивается индекс этого списка, а переменной currBucketPtr — его адрес. Если непустых списков нет, происходит возврат с currentBucket = — 1.
// начиная с cb, искать следующий непустой список для просмотра
template <class t>
void HashTableIterator<T>::SearchNextNode(int cb)
{
currentBucket • -1;
// если индекс cb больше размера таблицы, прекратить поиск
if (cb > hashTable->numBuckets) return;
// иначе искать, начиная с текущего списка до конца таблицы,
// непустой блок и обновить частные элементы данных
for (int i“cb; i<hashTable->numBuckets; i++)
if (!hashTable->buckets(i].ListEmpty())
{
// перед тем как вернуться, установить currentBucket равным i
//ив currBucketPtr поместить адрес нового непустого списка
currBucketPtr - &hashTable->buckets[i];
currBucketPtr ->Reset ();
currentBucket - i;
return;
I
I
Конструктор инициализирует базовый класс Iterator и присваивает закрытому указателю hashTable адрес таблицы. Непустой список обнаруживается с Помощью вызова SearchNextNode с нулевым параметром.
У конструктор, инициализирует базовый класс и класс HashTable ' i SearchNextNode идентифицирует первый непустой блок в таблице template cclass Т>
H«shTableIterator<T>::HashTablelterator(HashTable<T>& hf)•
Iterator<T> (hf), HashTable*(&hf)
} SearchNextNode(0);
С помощью метода Next осуществляется продвижение-вперед по теките списку на один элемент. По достижении конца списка функция SearchNeyJx^ настраивает итератор на следующий непустой блок.
// перейти к следующему элементу данных в таблице template <class Т>
void HashTablelterator<T>::Next(void) {
11 продвинуться к следующему узлу текущего списка currBucketPtr->Next();
// по достижении конца списка вызвать SearcftNextNode И для поиска следующего непустого блока в таблице
i f (currBucketPtr->EndOfList())
SearchNextNode(++currentBucket);
// установить флажок iterationComplete, если непустых списков // больше нет
iterationComplete - currentBucket =« -1;
14.5. Производительность методов поиска
Мы представили в этой книге четыре метода поиска: последовательный бинарный, поиск на бинарном дереве и хеширование. Быстродействие того или иного метода обычно зависит от среднего числа сравнений, необходимых для обнаружения элемента данных. Мы показали, что эффективность последовательного поиска равна О(п), а бинарного поиска и поиска на дереве — O(Iog2n).
Анализ производительности хеширования более увлекательный. Здесь производительность зависит от качества хеш-функции и от размера таблицы. Хорошая хеш-функция дает равномерное распределений значений. При относительно большой таблице число коллизий сокращается. Размер таблицы влияет на коэффициент заполнения (load factor) таблицы. Если таблица состоит из m ячеек, п из которых заняты, то коэффициент заполнения X определяется следующим образом:
X = п/ш
Когда таблица пуста, А. = О. По мере того как данные добавляются в таблицу» Л растет, как и вероятность коллизий. Прн открытой адресации А, достигает своего максимального значения, равного 1, когда таблица заполнена (ш ~ п^' При использовании метода цепочек списки могут быть как угодно длинными,
поэтому X может превысить 1.
Для оценки сложности хеширования по методу цепочек можно выдвинуть следующие интуитивные соображения. Наихудшим случаем является то , при котором все элементы данных отображаются в один и тот же табличи адрес. Если связанный список содержит п элементов, время поиска сост
О(п), т.е. в худшем случае производительность равна О(п).	е.
Для среднего случая при относительно равномерном распределении эн ний хеш-функции мы ожидаем А, — n/m элементов в каждом связанном сП» Следовательно, время поиска в каждом связанном списке равно О(А) - оГ. Если предполагается, что количество элементов, размещаемых в таблип®’ раничено некоторым числом, скажем R*m, то время поиска в каждом с уп равно O(R*m/m) = O(R) = 0(1), т.е. метод цепочек имеет порядок 0(1)' "
 £ данным в хеш-таблице, реализованной по методу цепочек, производится за фиксированное время, не зависящее от количества данных.
Формальный математический анализ хеширования выходит за рамки этой 1СНИГИ- В табл. 14.1 для каждого метода хеширования даны формулы приблизительного расчета числа проб, необходимых при успешном и безуспешном поиске в достаточно большой таблице. Каждая формула есть функция коэффициента заполнения X. Подробное обсуждение этих и других резуль-; татов можно найти в [19]. Когда X = 1, успешный поиск требует в среднем ш/2 проб, а безуспешный — m проб.
Таблица 14.1 формулы для оценки сложности методов хеширования
	Число проб при успешном поиске	Число проб при безуспешном поиске
Открытая адресация	1	1	1 л 2(1-X) + 2-X*’	1	 1 А . 2(1-Л)2 + 2' Х*1
Метод цепочек	1 х 1 + 2	
Из этой таблицы следует, что метод открытой адресации достаточно хорош при небольшом коэффициенте заполнения. В общем случае метод цепочек лучше. Например, когда m = n (X = 1), то методу цепочек требуется только 1,5 пробы для успешного поиска, в то время как при открытой адресации просматривается вся таблица и требуется в среднем т/2 проб. Когда таблица заполнена наполовину, метод цепочек требует 1,26 проб прн успешном поиске, а открытая адресация — 1,5.
Очевидно, что хеширование является чрезвычайно быстрым методом поиска. Однако каждый из четырех поисковых методов имеет свое применение. Последовательный поиск эффективен при малом числе элементов и в тех случаях, когда данные не нужно сортировать. Бинарный поиск очень быстр, но требует чтобы массив данных был отсортирован. Этот метод не годится 1 для данных, значения которых определяются во время выполнения программы (например, таблица символов в компиляторе), поскольку упорядоченный массив — далеко не благоприятная среда для операций удаления и вставки. Для этих задач подходят бинарное дерево поиска и хеширование. Бинарное Дерево поиска не столь быстрое, но обладает привлекательным эффектом Упорядочения данных при выполнении симметричного прохождения. Когда ^жен быстрый доступ к неупорядоченным данным, хеширование — лучший Метод.
14-6. Бинарные файлы и операции с данными на внешних носителях
Во многих приложениях требуется доступ к данным, расположенным в *аЙлах на диске. В этом разделе дается обзор ввода/вывода бинарных файлов с помощью класса fstream (файл fstream.h). Мы рассмотрим класс BinFile, • ^держащий методы для открытия и закрытия бинарных файлов, для доступа ^Дельным записям файла и для блочного ввода/вывода. Большие наборы Жадных могут содержать миллионы записей, которые невозможно разместить
в памяти одновременно. Для управления ими нужны алгоритмы внеш сортировки и поиска. Мы дадим лишь краткое введение в эту тему, поско детальное обсуждение файлов, а также внешней сортировки и поиска за рамки данной книги.	’ °^ит
Бинарные файлы
Текстовый файл содержит строки ASCII-символов, разделенные символ конца строки. Бинарный файл состоит из записей, которые варьируются одного байта до сложных структур, включающих целые числа, числа с Пл°Т вающей точкой и массивы. С аппаратной точки зрения записи файла пре ставляют собой блоки данных фиксированной длины, хранящиеся на диске Блоки, как правило, несмежные. Однако с логической точки зрения записи располагаются в файле последовательно. Файловая система позволяет осу-ществлять доступ как к отдельным записям, так и ко всему файлу целиком рассматривая последний как массив записей. Во время ввода/вывода данных система поддерживает файловый указатель (file pointer) — текущую позицию в файле.
Файл как структура прямого доступа
Ro	R1	R2	R3	r4			Ri	-		Rn-2	Rn-1
0	12	3	4
Текущая позиция п-2	п-1
Файл является также последовательной структурой, которая сохраняет файловый указатель в текущей позиции внутри данных. Операции ввода/вывода обращаются к данным в текущей позиции, которая затем продвигается к следующей записи.	z	г
i
Файл как структура последовательного доступа
Начало	Текущая позиция Конец
Встроенный в C++ класс fstream описывает файловые объекты, которые могут использоваться как для ввода, так и для вывода. Создавая объек . мы используем метод open для назначения файлу физического имени н р жима доступа. Возможные режимы определены в базовом классе ios.
Режим in out trunc nocreate	Действие открыть файл для чтения открыть файл для записи удалить запись до чтения или записи если файл не существует, не создавать пустой файл» возвратить ошибочное состояние потока
binary	открыть файл, считая его бинарным (не текстов
j Пример 14.5
j
I «include <fstream.h> fstream fs	// объявление файла
// открыть текстовый файл Phone для ввода. // если такого файла нет, сообщить об ошибке
1 f.open("Phone”, ios::in I ios::nocreate);
\ fstream ti	/I объявление файла
// открыть бинарный файл для ввода f.open("DataBase", ios::in | ios::out I ios::binary);
Каждый файловый объект имеет ассоциированный с ним файловый указатель, который указывает на текущую позицию для ввода или вывода. Функция tellg() возвращает смещение в байтах от начала файла до текущей позиции во входном файле. Функция tellp() возвращает смещение в байтах от начала файла до текущей позиции в выходном файле. Функции seekg() и seekp() позволяют передвинуть текущий файловый указатель. Все эти функции принимают в качестве параметра смещение, измеряемое числом байтов относительно начала файла (beg), конца файла (end) или текущей позиции в файле (cur). Если файл используется как для ввода, так и для вывода, пользуйтесь функциями tellg и seekg.
смещение	смещение . смещение 1—-—*	-	1	*	смещение
BEG	CUR	END
Следующий код иллюстрирует действие функций seekg и tellg:
// Бинарный файл целых чисел fstream f;
f .openCdatafijle", ios::in | ios::nocreate I ios::binary);
И сбросить текущую позицию на начало файла f.seekgfO, ios::beg);
// установить текущую позицию на последний элемент данных f .seekg(-sizeof(int), ios::end};
Ч передвинуть текущую позицию к следующей записи t-seekg(-sizeof(int), ios::cur);
Ч переместиться к концу файла
-seekg(0, ios::end);
1! распечатать число байтов в файле cout « f. tellg () « endl;
11 Распечатать число элементов данных в файле c©ut « f.tellg()/sizeof(int);
Класс fstream имеет базовые методы read и write, выполняющие ввод/вывод й°Тока байтов. Каждому методу передаются адрес буфера и счетчик пересыла-емых байтов. Буфер является массивом символов, в котором данные запоминаются в том виде, каком они принимаются или посылаются на диск. Операции рВоДа/вывода данных несимвольного типа требуют' приведения к типу char, ^^ример, следующие операции передают блок целых чисел:
fstream f;	If объявление файла
int data = 30, A[20];
// записать число 30 как блок символов длиной sizeof (int) f.write((char*) &data, sizeof(int));
// прочитать 2.0 чисел из файла f в массив А
f.read((char*)а, 20*sizeof(int));
Класс BinFile
Файловый ввод/вывод используется во многих приложениях. В этом ъдз ле мы абстрагируем файл от какого бы то ни было приложения и определи*е' некоторый класс, обеспечивающий общие операции обработки бинарных лов. Это пример класса, полностью скрывающего от пользователя системны детали нижнего уровня. Поскольку этот класс определен как шаблон, порож даемые нм файлы могут содержать различные типы данных.
Спецификация класса BinFile
ОН ъЯВлкНИЕ
// системные файлы, содержащие методы для обработки файлов
#include <iostream.h>
#include <fstream.h> linclude <stdlib.h>
linclude "strclass.h"
// тип доступа enum Access (IN, OUT, INCUT);
// тип смещения в операциях поиска
enum SeekType (BEG, CUR, END);	/
template <class T> class BinFile {
private:
// файловый поток co своим именем и типом доступа fstream f;
Access accessType; // тип доступа	(
String fname;	// физическое имя файла
int fileOpen;	// файл открыт?
И параметры, характеризующие файл как структуру прямого доступа int Tsize;	И размер записи
int filesize;	И число записей
// выдает сообщение об ошибке и завершает программу void Error(char *msg);
public:
// конструкторы и деструктор
BinFile(const Strings fileName, Access atype ” OUT);
-BinFile(void);
// конструктор копирования.
// объект должен передаваться по ссылке.
// завершает программу BinFile(BinFile<T>& bf);
// утилиты обслуживания файла void Clear(void); // очистить файл от записей
void Delete (void) ;	11 закрыть-файл и удалить его
void Close(void); // закрыть файл
int EndFile{);	// проверить условие конца файла
long Size();	// вернуть число записей в файле
void Reset(void); // установить файл на первую запись // переместить файловый указатель на pos записей относительно // начала файла, текущей позиции или конца файла void Seek(long pos, SeekType mode);
// блочное чтение n элементов данных в буфер с адресом А int ReadfT *А, int п);
// блочная запись п элементов данных из буфера с адресом А void Write(Т *А, int п);
// выбрать запись, расположенную в текущей позиции т Peek(void);
// копировать data в запись, расположенную в позиции pos void Write (const T& data, long pos);
// читать запись по индекс, pos
т Read (long pos);
// записать запись в конец файла
void Append (Т item) ;
Ь*
ОПИСАНИЕ
Конструктор отвечает за открытие файла и инициализацию параметров класса. Создавая объект типа BinFile, программа должна указывать режим доступа к файлу (IN, OUT или INOUT). Если файл открывается в режиме OUT, он очищается. Файл, открывающийся в режиме IN, должен существовать, иначе будет выдано сообщение об ошибке с последующим завершением программы. Если файл объявлен как INOUT, то записи можно как вводить, так и выводить. Открыв такой файл, конструктор устанавливает fileOpen в 1 (True), показывая тем самым, допускается операция чтения.
При вызове конструктора копий выдается сообщение об ошибке. В момент создания файловому объекту приписывается физический файл. Допуская копирование файла, можно было бы потребовать, чтобы новый объект обязательно открывал тот же самый файл. Однако на некоторых системах это невозмож-во, а если и допускается, то может привести к опасной ситуации. Объект типа BinFile должен передаваться по ссылке.
Файл может обрабатываться как структура прямого доступа. Методы Read и Write принимают в качестве параметра индекс записи pos н вводят или выводят элемент данных по этой позиции. Методы блочного чтения/записи используются Для ввода/вывода сразу нескольких записей. Блочный Read возвращает число прочитанных записей или О, если встретился конец файла. Возможна ситуация, когда в файле остается менее п записей. Следовательно, возвращаемое значение может быть меньше п. В качестве параметров передаются адрес буфера данных и число записей. Передача начинается с текущей позиции в файле. Метод Реек позволяет выбрать текущую запись, не продвигая файловый указатель.
Метод EndFile возвращает логическое значение, сигнализирующее о том, ли достигнут конец файла. Используйте этот метод только для входных Файлов. Для файлов других типов проверяйте в цикле индекс текущей записи и останавливайтесь, когда переменная цикла превысит индекс последней записи.
Метод Close закрывает поток, но не удаляет физический файл. Используйте Uose, если файл должен быть открыт другим объектом, возможно, в другом Режиме.
Метод Clear очищает файл от записей, оставляя его открытым и имеющим Улевой размер. Метод Delete закрывает файл и удаляет его с диска. Эти методы Р&сывают флажок fileOpen в О. После этого любая попытка обратиться к 'Рэйлу заканчивается прекращением программы. Метод Seek позволяет пере-
местить файловый указатель. Параметр mode указывает базу, относиТе которой отсчитывается смещение pos, и соответствует началу файла, тек^Ьао позиции или концу файла.	|
ПРИМЕР
Ц файл целых чисел, предназначенный для ввода/вывода; // физическое имя demofile
BinFile<int> BFC'demofile", INOUT);
BinFile<int> BG("outfile", OUT); // файл для вывода целых чисел
int i, m 3 5, n 3 10, a[10);	// переменные целого типа
// Эти данные будут выведены в denofile int vals[) 3 (30,40,50,60,70,60,90,100};
for (i-0; i<5; i++)
EF.Write(&i,1);
BF. Append (m) ;	// записать 5 в конец файла
BF.Reset();	// встать на начало файла
BF.Write(n,0)	// записать 10 в начало файла
cout « BF.Sized « endl;	// распечатать размер файла (6)
cout « BF-Read(3) « endl;	// распечатать третью запись
BF.Read(A,2);	// ввести два числа в массив А
cout « А[0) « *’ " « А[Ц; // и распечатать их BF.Reset();	// встать на начало файла
cout « BF.Peekd « endl; // распечатать текущую запись (10) BF.Read(A,4);	// ввести четыре числа в массив А
BG. Write (А, 4);	// вывести А(0]—А[3] в файл BG
А[0] * 2;	// удвоить А[0]
BG.Write(А(0],О);	// вывести новое значение в первую запись
BF.Seek(2,beg);	// переместиться ко второй записи файла BF
BF.Write(vals,8);	// записать 30..100 в файл.,—
// начиная со второй записи
BF. Reset О;	// вернуться к началу/файла demo file
// Читать и распечатывать demofile for (i=0; i<BF.Sized; i++) (
BF.Read(&m,1); cout « m « " } cout « endl;
BF. Deleted; BG.Close(); BinFile<int> BH("outfile", IN) While (IBH.EndFileO) { BH.Read(&m, 1); cout « m « ’’ ”; ) cout « endl; BH. closed; «Результирующая распечатка> 6 3 4 5 10 10 1 30 40 50 60 70 60 90 100 20 1 2 3	// удалить файл BF // закрыть outfile // открыть outfiie для ввода // читать и распечатывать outfile | // закрыть outfile 1
f'CM-  -  >ИЯ КЛЗ'.'-а
Полная реализация класса содержится в файле binfile.h. В этом разделе мы обсудим конструктор, метод прямого чтения, блочный вывод п записей н утилиту Clear.
Конструктор отвечает за открытие файла и инициализацию параметров класса. Создавая объект, мы передаем конструктору имя файла и тип доступа.
</ конструктор, открывает файл с заданным именем и типом доступа te»Plate «class Т>
BjnFile<T>::BinFile(const Strings fileName, Access atype)
// операция открытия потока зависит от типа доступа.
// для IN файл не создается, если он не существует.
// для OUT все существующие в нем данные удаляются.
// для INOUT файл пригоден и для ввода, и для вывода
if (atype == IN)
f.open(fileName, ios::in 1 ios::trunc 1 ios::binary;
else if (atype -- OUT)
f.open(fileName, ios::out I ios::trunc | ios::binary;
else
f.open(fileName, ios::in | ios::out I ios::binary;
if (If)
Error("Конструктор BinFile: не могу открыть файл");
else
fileOpen =1;
accessType - atype;
// подсчитать число записей в файле
// Tsize — размер типа данных Т (длина записи)
Tsize - sizeof(Т);
if (accessType “= IN |I accessType  INOUT) <
// подсчитать число записей во входном файле, переместившись
//к его концу, вызвав tellg и затем разделив полученную длину
// файла в байтах на длину записи
f.seekg(0, ios::end);
filesize - f.tellg()/Tsize;
f.seekg(0, ios::beg);
)
else
fileSize - 0;	// размер для выходного файла
// записать имя физического файла в fname
fname « fileName;
Доступ к файлу. С помощью метода seekg файл можно рассматривать как Массив прямого доступа. Метод Read имеет параметр pos. Комбинируя размер записи и параметр pos, метод seekg перемещает файловый указатель на конкретную запись и извлекает ее из файла.
Ч метод Read возвращает запись, идущую в файле под номером pos template «class Т>
BinFiie<T>::Read (long pos)
.переменная для хранения записи
т data;
24 Зак. 425
if (!fileOpen)
Error ("BinFile Read(int pos): файл закрыт”);
// метод Read недопустим для выходных файлов
if (accessType == OUT)
Error("Недопустимая операция доступа к файлу");
// проверить попадание роз в диапазон 0..fileSize-1
else if (pos < 0 I I pos >- fileSize)
Error("Недопустимая операция доступа к файлу”);
/ / переместить файловый указатель и извлечь данные
//с помощью метода read класса fstream
f .seekg(pos*Tsize, ios::beg);
f.read((char *)sdata,Tsize);
// если файл входной и мы прочитали все записи,
// установить флажок конца потока
if (accessType == IN)
if (f.tellgO/Tsize >= filesize)
f.clear(ios::eofbit);	// установить бит eof
return data;
)
Когда файл используется как устройство последовательного доступа, метод Write можно определить так, чтобы он копировал в файл сразу несколько записей. Адрес выводимых данных и число записей передаются в качестве параметров. Метод write класса fstream копирует поток байтов в выходной файл. Поскольку вывод может начинаться не с начала файла, следует позаботиться о правильном значении fileSize.
// выводит n-элементный массив А в файл
template cclass Т>
void BinFile<T>::Write(Т *A, int n) (
long previousRecords;
// для входных файлов операция записи недопустима
if (accessType = IN)
Error("Недопустимая операция доступа к файлу”);
if (IfileOpen)
Error ("BinFile Write(T *A, int n): файл закрыт");
// вычислить новый размер файла, вызвать tellg для подсчета // числа записей, предшествующих точке вывода, определить, // увеличился ли размер файла, если да, увеличить fileSize на // число добавляемых записей
previousRecords = f.tellg()/Tsize;	1
if (previousRecords + n > fileSize)
fileSize + previousRecords + n - fileSize;
11 число выводимых байтов равно n * Tsize f.write((char *)A, Tsize*n);
Утилиты. В данном классе имеется целый ряд полезных методов^ управления файлом. Метод Clear удаляет все имеющиеся в файле зйПВйС. сперва закрывая файл, а затем вновь открывая его в режиме trun^gbIe-пользуя набор параметров файла, которые хранятся как частные Да члены класса.
у/ метод Clear удаляем записи ,аила, сначала закрывая его, II а затем открывая вновь template <class Т>
void BinFlle<T>::Clear(void)
// входной файл очищать нельзя
if (accessType = IN)
Error("Недопустимая операция доступа к файлу");
// закрыть, а затем вновь открыть файл f.close();
if (accessType — OUT)
f.open(fname, ios::out I ios::trunc | ios::binary); else
f.open(fname, ios::in | ios::out I ios::trunc I ios::binary); if (If)
Error("BinFile Clear: не могу повторно открыть файл”);
fileSize - 0;
Внешний поиск
Ранее мы рассмотрели ряд внутренних списковых структур хранения данных. Подобный набор структур можно определить для данных, хранящихся в файле. Эффективность методов внешней сортировки и поиска зависит от организации записей файла. Мы распространим концепцию хеширования на файловые структуры и используем методы класса BinFile для доступа к данным.
Методика хеширования обеспечивает высокую эффективность поиска и может быть применена к внешним структурам. Хеш-функция ставит в соответствие каждой записи целое число нэ диапазона О-..n—1. Это число может служить индексом в массиве записей, где данные запоминались методом открытой адресации. В более эффективном методе цепочек это число может использоваться как индекс в массиве списков. Оба этих метода хранения можно применить к файлам. В этом разделе мы будем иметь дело с методом цепочек, при котором файл содержит связанные списки записей. Мы создадим в памяти хеш-таблицу и с ее помощью будем обращаться к более медленному дисковому устройству.
Пусть запись содержит данные вместе с файловым индексом.
data	nextindex
FaleDataRecord
Эти записи хранятся на диске в виде связанного списка. Поле nextindex Указывает позицию следующей записи файла. Чтобы сформировать связанные списки, создадим в памяти хеш-таблицу, которая ссылается на связанные списки в файле. Хеш-функция отображает каждую запись в табличный индекс.
hashtable[n];	// массив файловых индексов
Хеш-таблица представляется в памяти в виде n-элементного массива. Изначально таблица пуста (каждая ячейка содержит -1), показывая тем самым, что Залисей в файле нет. Как только мы вводим запись из базы данных, хеш-функ-Цйя определяет индекс в таблице. Если соответствующая ячейка таблицы Куста, мы запоминаем саму запись на диске, а ее позицию в файле — в таблице.
Теперь ячейка таблицы содержит дисковый адрес первой отображаемой в эту ячейку записи. Этот адрес можно использовать для доступа к соответствующему связанному списку и вставить туда новую запись. Процесс вставки заключается в выводе записи на диск и обновлении указателя в поле nextindex.
Проиллюстрируем этот процесс на простом примере, который, тем не менее, выражаетет главные особенности. Пусть наши данные имеют целый тип и запоминаются в файле в виде списка записей FileDataRecord.
// узел списка, в котором хранится запись struct FileDataRecord
{
// в нашем примере поле data есть целое число, на практике
// чаще всего поле data является сложной записью
int data;
int next Index;	/ / ссылка на следующую запись
};	f
Хеш-функция отображает каждое значение данных в другое целое число, выражаемое младшей цифрой исходного числа.
h(data) • data % 10;	// h<456) = 6; h(891) - 1; h(26) - 6
В нашем примере в файле запоминаются следующие данные:
456 64 84 101 144
Первые два числа отображаются в пустые ячейки таблицы и, следовательно, могут быть сразу вставлены в файл в качестве узлов. Первый Узе запоминается в позиции 0, а второй — в позиции 1. Номера позиций зано сятся в соответствующие ячейки таблицы.
Ячейка таблицы, соответствующая числу 84, содержит 1 — номер файла, являющейся первой в некотором связанном списке записей-запись (84) вставляется в начало этого списка.
После загрузки чисел 101 и 104 файл содержит пять записей FileDataRe-cord, которые логически представляют собой три связанных списка. Головы этих списков содержатся в известных ячейках таблицы.
В данном методе хранения эффективно используется прямой доступ к файлу. Файл формируется путем последовательного добавления записей и обновления соответствующих ячеек таблицы. Часто сама таблица запоминается в виде отдельного файла и загружается оттуда в память, когда требуется = поработать с основным файлом.
Программа 14.3. Внешнее хеширование
Эта программа иллюстрирует ранее рассмотренный алгоритм внешнего хеширования. Функция LoadRecord добавляет в файл новую запись, а функция PrintList распечатывает связанный список записей, соответствующий некоторому хеш-адресу. Каждая запись вставляется в начало своего связанного списка. Исключение дубликатов не производится. Главная процедура включает в файл 50 случайных чисел от 0 до 999. У пользователя запрашивается какой-нибудь хеш-индекс, а затем распечатываются элементы соответствующего связанного списка.
flinclude <iostгearn.h>
flinclude "random.h" flinclude "binfile.h"
const long Empty - -1:
// узел списка, в которой хранится запись struct FileDataRecord {
П ъ нашем примере поле data есть целое число, на практике
// чаще всего поле data само является сложной записью int data;
int nextindex;	// ссылка на следующую запись в файле
};
// startindex — индекс в таблице, передается по ссылке, чтобы ,	// можно было обновлять голову списка
void LoadRecord(BinFile<FileDataRecord> &bf, long Kstartindex, FileDataRecord sdr)
{
// если таблица не пуста, startindex указывает на первую запись // списка, в противном случае startindex = 1
dr.nextindex - startindex;
I startindex “ bf.Sizef);
// добавить в файл новую запись bf-Append(dr);
I
// сканировать узлы списка в файле и распечатывать значения данных
void PrintList(BinFile<FiieDataRecord> &bf, long sstartindex)
// index — индекс первой записи списка long index - startindex;
FileDataRecord rec;
// index продвигается к концу списка (до index = -1)
while (index !- Empty) {
// прочитать запись, распечатать поле данных и перейти к следующей
гес - bf.Read(index);	ПИС1’
cout « rec.data « "
index = rec.nextindex ;
}
cout « endl;
)
void main(void) {
l! таблица голов списков записей в файле.
// область значений хеш-функции равна 0..9
long HashTable(10];
11 генератор случайных чисел и запись с данными RandomNumber rnd;
FileDataRecord dr;
int i, item, request;
// открыть файл DRfile для ввода/вывода
BinFile<FileDataRecord> dataFile("DRfile”. INOUT);
// инициализировать таблицу пустыми ячейками
for (i=0; i<10; i++) hashTable[i] - Empty;
// ввести 50 случайных чисел от 0 до 999
for (i=0; i<50; i++)	/
{
item = rnd.Random(1000) ;
// сформировать запись и вывести ее в файл dr. data = item;
LoadRecord(dataFile, hashTable(item % 10J, dr); >
// запросить индекс в хеш-таблице
//и распечатать соответствующий список
cout « "Введите номер ячейки хеш-таблицы:
cin » request;	।
cout « "Печать элементов данных, хешируемых вучисло " « request « endl;
PrintList(dataFile, hashTable(request]);
// удалить файл dataFile.Delete!);
}
/*
<Прогон программы 14.3>
Введите номер ячейки хеш-таблицы: 5
Печать элементов данных, хешируемых в число 5
835 385 205 185 455 5
Внешняя сортировка
Сортировка данных иа внешних носителях составляет специальную проблему, когда файл настолько велик, что не умещается в оперативной памяти. Поскольку все данные нельзя расположить в одном, массиве, мы должны использовать для их хранения временные файлы. В этом разделе рассматривается внешняя сортировка слиянием с помощью трех файлов. Мы обсудим алгоритмы как прямого, так и естественного слияния, использующего длинные последовательности. Эти алгоритмы могут быть расширены до п-путевого слияния, в котором задействовано более чем 3 файла.
В гл. 12 мы рассмотрели простое слияние, которое объединяет два упорядоченных списка в один. Сортировка прямым слиянием использует этот родход, объединяя подсписки фиксированной длины. Пусть сортируемые элементы хранятся в файле fC, а файлы fA и fB являются временными и служат для разбиения данных. Тогда алгоритм сортировки можно представить последовательностью следующих шагов:
1.	Разбить fC пополам, попеременно записывая его элементы то в fA, то в fB. Таким образом в каждом новом файле создается последовательность одноэлементных подсписков.
2.	Сопоставить подсписки. Выбрать один элемент из fA и один элемент из fB. Объединить их в двухэлементный подсписок и записать в fC. Продолжать до тех пор, пока все элементы в обоих файлах не будут снова скопированы в fC.
3.	Повторить шаг 1, попеременно записывая двухэлементные подсписки файла fC в файлы fА и fB.
4.	Попарно слить все двухэлементные подсписки файлов fА н fB в четырехэлементные подсписки файла fC.
5.	Повторять шаг, на котором fC разбивается пополам, образуя четырех-, восьми- и т.д. -элементные подсписки в файлах fA и fB. Затем сливать каждую пару подсписков в восьми-, шестнадцати- и т.д. -элементные подсписки файла fC. Процесс завершается в тот момент, когда в fА и fB образуется по одному упорядоченному списку, которые окончательно сливаются в отсортированный файл fC.
Проиллюстрируем сортировку прямым слиянием на примере двадцати целых чисел.
5 15 35 30 20 45 35 5 65 75 40 50 60 70 30 40 25 10 45 55
3 - 4
7 - 8
11-12
15-16
19 - 20
На первом шаге fC разбивается на два временных файла по 10 одно ментных подсписка в каждом. На втором шаге посредством слияния созп> ЭЛе' файл упорядоченных пар fC.	даеТся
5-8
На третьем шаге файл упорядоченных пар fC разбивается пополам н файлы fA и fB, которые затем попарно сливаются в файл упорядоченных четверок fC.
Разбиение и последующее попарное слияние файлов происходит еще три раза. В этих проходах в fC создаются упорядоченные 8-, 16- и наконец 20-элементные подсписки. После финального прохода fC становится отсортированным файлом. При создании 8- и 16-элементных подсписков в хвосте файла fA остается "непарный" подсписок, который просто копируется снова в fC. В финальном проходе 16-элементный подсписок в fA сливается с 4-элементным подсписком в fB и процесс завершается.
Анализ сортировки прямым слиянием. Сортировка прямым слиянием стоит из серии проходов, начинающейся с одноэлементных подсписков, каждом проходе длина подсписков удваивается, пока не достигнет св предельного значения s > п. Для этого требуется log2n отдельных во время которых все п элементов копируются сначала во временные Ф& а затем снова в fC. Таким образом, сортировка прямым слиянием тР 2 * n * log2n обращений к данным, что составляет сложность порядка О(п ° 2
Организация коллекций 729
Сортировка естественным слиянием
Сортировка прямым слиянием использует упорядоченные подсписки с начальной длиной 1, удваивающейся на каждом проходе. В конце концов упорядоченные подсписки охватывают весь файл, и сортировка завершается. При этом несоизмеримое количество времени тратится на короткие подсписки — на их разбиения и последующие слияния. Эффективность алгоритма намного возрастает на длинных подсписках, так как требуется меньше проходов и файловые операции не должны выполняться столь часто. В данном разделе мы модифицируем сортировку прямым слиянием таким образом, чтобы она начиналась с относительно длинных подсписков и, следовательно, стала более эффективной. Усовершенствованному алгоритму требуется файл fC н буфер в оперативной памяти для создания упорядоченных подсписков. Данные исходного файла читаются в буфер поблочно. Каждый блок сортируется с помощью какого-нибудь быстрого алгоритма внутренней сортировки (например, Quicksort). Отсортированные блоки попеременно копируются в файлы fА и f В. Слияние начинается с подсписков, которые уже с самого начала имеют большую длину.
Чтобы оценить влияние длины исходных подсписков, сравните по табл. 14.2 времена сортировок 30000 случайных чисел при различных размерах блока.
Реализация естественного слияния. Функция MergeSort создает два временных файла и осуществляет серию проходов, разбивающих исходный файл на файлы £А и £В, которые затем снова сливаются в исходный файл fC. Это продолжается до тех пор, пока в файле fC не окажется единственная отсортированная последовательность.
П функция для сортировки файла fC, использующая последовательности
И длиной blocksize, сначала блоки данных вводятся и сортируются с
И помощью алгоритма "быстрой сортировки", а затем записываются в качестве
И последовательностей во временные файлы fA и fB
template <class Т>
void MergeSort(BinFile<T>& fC, int blocksize) {
II временные файлы для разбиения исходного файла
BinFile<T> fACfileA", INOUT);
BinFile<T> fBC’fileB", INOUT);
H длина файла и длина блока
int size = int (fC.SizeO), n = blocksize;
int k = 1, useA = 1, readcount;
T *A;
Ii установить файл fС на начало fC.Reset();
it если файл маленький, ввести данные из £С, отсортировать
//и скопировать обратно
if (size <- blocksize) (
// создать буфер для блока данных и выполнить блочное чтение
А = new T[size]; if (А “ NOLL) {
cerr « "MergeSort: ошибка распределения памяти” « endl;
exit(1);
)
fC. Read(A.size);
730
// отсортировать блок данных
Quicksorts, 0, (int)size-1);
// очистить файл и снова записать туда отсортированные данные fС.Clear();
fC.Write(A, size);
И освободить память, выделенную под буфер, и вернуться delete [] А;
return;
)
else
{
// создать буфер для блока данных и читать блоки до конца файла
А - new Т[blocksize);
if (А — NULL)
(
cerr « "MergeSort: ошибка распределения памяти” « endl; exit(1);
)
while (ifC.EndFilet))
(
readCount « fC.Read(A, blocksize);
if (readCount — 0)
break;

Il сортировать блоки и попеременно записывать отсортированные // последовательности в файлы fA и fB
Quicksort(А, 0, readcount-1);
if (useA)
fA.Write(A, readcount);
else
fB.Write(A, readcount);
useA - !useA;
) _______________________________________________________
delete [) А;
)
// слить отсортированные последовательности обратно в файл fC
Merge(fA, fB, fC, blocksize);
11 удвоить размер отсортированных последовательностей k *- 2;
п - k * blockSize;
// если n больше или равно длине файла, то в fC только одна // последовательность, т.е. файл отсортирован	.
while (п < size)	i
<	V
// на каждом Проходе разбивать последовательности и снова И сливать их в последовательности удвоенной длины
Split(fA, fB, fC, k, blocksize);
Merge(fA, fB, fC, n);
k *= 2;
n - k * blocksize;
)
// удалить временные файлы fA. Deleted;
fB. Deleted ;

Организация коллекций 731
В каждом проходе функция Split сканирует файл fC и поочередно копирует его последовательности в файлы fА н fB. При каждом вызове этой функции размер подсписков уже удвоен и равен k * blockSize. Поскольку blockSize представляет собой длину буфера, подсписок выводится в файл в виде к блоков. Процесс прекращается, когда все последовательности файла fC скопированы во временные файлы.
// сканировать файл fC и поочередно копировать его последовательности //в файлы fА и fB. на текущем проходе длина последовательностей
// равна k * blockSize
template <class Т>
void Split(BinFile<T> &fA, BinFile<T> bfB, BinFile<T> &fC, int k, int blocksize)
{
int useA =1;
int i = 0;
int readCount;
// для блочного ввода/вывода размер блока равен blocksize Т *А - new T(blockSize);
if (А == NULL) {
cerr « "MergeSort: ошибка распределения памяти" « endl; exit(1);
)
// инициализация файлов перед разбиением fA.Clear();
fВ.Clear(); fC.Reset();
// распределить последовательности файла fC while (!fС.EndFileО) {
// ввести блок данных в динамический массив
// readcount — число введенных элементов данных readcount - fC.Read(A, blockSize);
// если readcount равен нулю, достигнут конец файла
if (readCount = 0) break;
11 если useA=True, записать блок в ЕА; иначе — в fB if (useA)
f A.Write(A, readcount); else
fВ.Write(A, readCount);
// сменить выходной файл после вывода к блоков if (++i == k)
(
i = 0;
useA » luseA;
} )
// освободить динамическую память delete I) A;
732
Как только последовательности скопированы во временные файлы м начинать их слияние. Этот процесс управляется функцией Merge, кот^^0 объединяет пары последовательностей из временных файлов, создана °₽&Я каждой пары одну упорядоченную последовательность. Если в файле f д Из зывается лишняя последовательность, она просто копируется в fС с помл^Ка‘ CopyTail.
// слить последовательности длиной п из файлов fA и fВ в файл fC template <class T>
void Merge (BinFile<T> &fA, BinFile<T> efB, BinFile<T> bfC, int n)
(
// currA и currB — текущие позиции в последовательностях.
// взятых из каждого файла int curга  1, currB =1;
11 элементы данных, введенные из fA и fB соответственно.
// флажки haveA/haveB показывают, откуда был введен элемент данных Т dataA, dataB;
int haveA, haveB;
// инициализировать файлы перед слиянием
fA.Reset();
fB.Reset О ;
fC.Clear();
// взять no одному элементу из каждого файла
fA.Read(&dataA, 1);
fB.Readl&dataB, 1);
for (,•;)
(
II если dataA<=dataB, скопировать dataA в fC и обновить // текущую позицию в текущей последовательности из файла- fA if (dataA <= dataB)	/
{
fC.Write(tdataA, 1);
Уf взять следующий элемент из fA. если элемент не найден, // достигнут конец файла и хвост fB должен быть скопирован // в fC. если текущая позиция больше п, то последовательность // из fA просмотрена и в fC следует скопировать хвост файла fB if ((haveA = fA.Read(4dataA,1)) -= 0 | | ++currA > n) {
Н скопировать dataB в fC. обновить текущую позицию в fB
fC.Write(&dataBr 1);
currB++;
CopyTail(fВ, fC, currB, n);	1
// размер файла fA больше или равен размеру файла fB
// если конец файла fA, дело сделано
if (!haveA)
break;
// иначе новая последовательность. сбросить текущую позицию currA «= 1;
// взять следующий элемент из fB. если там ничего нет,
// то в fA остается только одна последовательность, которую // следует скопировать в fC. скопировать текущий элемент из // файла fA, перед тем как выйти из цикла
if ((haveB - fB.Read(&dataB,1)) == О) <
fC.Write (fcdataA, 1);.
коллекций
733
currA = 2;
break;
}
// иначе сбросить текущую позицию в последовательности из fB currB = 1;
) else {
// скопировать dataB в fC и обновить текущую позицию в fB fC.Write(&dataB, 1);
// поверить конец последовательности из fB или конец файла fB
if ((haveB - fB.Readl&dataB,1)) == 0 11 ++currB > n) {
// если конец, записать элемент, который уже прочитан из fA,
// обновить его позицию, а затем записать хвост последовательности fС.Write(&dataA,1) currA++;
CopyTail(fA, fC, currA, n);
// если в fB больше нет элементов, сбросить текущую позицию
// в fА и подготовиться к копированию последней последовательности
// из fA
currB =1;
if ((haveА = fA-Read(«dataA,1)) == 0) break;
currA =1;
// скопировать хвост последней последовательности из fA, // если таковой существует
if (haveA ss !haveB)
CopyTail(fA, fC, currA, n);
Сливая две последовательности, мы достигаем конца одной из них раньше, чем другой. Функция CopyTail копирует в выходной файл хвост другой последовательности.
II п - текущий размер последовательности, скопировать хвост
II последовательности из файла fX в файл fY. переменная
! / currRunPos — текущий индекс в последовательности tenjpiate «class Т>
void CopyTail (BinFile<T> 4fX, BinFile«T> &fY,
int bcurrRunPos, int n)
T data;
II копировать каждый элемент, начиная с текущей позиции //до конца последовательности
*hile (currRunPos <= п)
{
// если вводить больше нечего, достигнут конец файла
// и, следовательно, конец последовательности
if (fX. Read («data, 1) — 0)
return;
// обновить текущую позицию и записать элемент в файл fY currRun₽os++;
fY.Write(«data, 1);	7
}
734
Программа 14.4. Тестирование функции MergeSort
Эта программа сортирует методом естественного слияния файл, с6дев~^~ щиЙ 1000 случайных чисел, используя 100-элементные последовательно Й* * Файл создается функцией LoadFile. С помощью PrintFile распечатываю*2' первые 45 элементов исходного и отсортированного файлов.	Тся
finclude <io8tream.h>
#include <iomanip.h>
#include "binfile.h"
#include "merge.h"
*include "random.h"
11 распечатать элементы файла f по 9 элементов в строке void PrintFile (BinFile<int> &f, long n) (
// инициализировать n по размеру файла int data;
long i;
n - (f.Sized < n) ? f.Sized : n;
// установить файловый указатель на начало файла f .Reset d:
II последовательное сканирование файла, читать и распечатывать И каждый элемент, начинать каждый 10-й элемент с новой строки for <i—0,- i<n; i++) {
if (i % 9 -- 0) cout « endl;
f. Read (bda ta, 1);	_____.
cout « setw(5) « data « "
) cout « endl;
)
II создать файл, содержащий n случайных чисел в диапазоне 0—327€7 void LoadFile(BinFile<int> &f, int nJ { int i, item; RandomNumber rnd;	,
// инициализировать файл f.Reset();
tI заполнить файл случайными числами	L
for (i-0; i<n; i++) {
item “ rnd.Random(32768L);
f.Write(bitem, 1>; )
)
void main(void) {
// файл fC заполняется случайными числами и сортируется BinFlle<int> fC(HfileC", INOUT);
И создать файл 1000 случайных чисел
LoadFile(fC, 1000);
Организация коллекции
// распечатать первые 45 элементов исходного файла
cout « "Первые 45 элементов исходного файла:’1 « endl;
PrintFile(fC, 45); cout « endl;
// выполнить сортировку слиянием MergeSort(fC, 100);
// распечатать первые 45 элементов отсортированного файла cout « "первые 45 элементов отсортированного файла:" « endl; PrintFile(fС, 45);
// удалить файл fC.Delete();								
1 <ПрОГО1	4 программы 14.4>							
Первые	45 элементов		исходного файла:					
14879	26060	28442	20710	19366	10959	17112	7880	22963
16103	22910	6789	4976	19024	1470	25654	31721	28709
997	23378	14186	14986	21650	7351	25237	28059	5942
9593	20294	27928	8267	9837	17191	8398	18261	21620
5139 Первые	964 10393 45 элементов		16777 15915 18986 22175 отсортированного файла:				2697	20409
19	76	94	98	106	119	188	192	236
259	308	344	346	371	383.	424	463	558
570	605	614	714	741	756	794	861	864
891	910	923	964	979	997	1000	1007	1029
1051 */	1079	1112	1223	1232	1347	1470	1515	1558
14.7. Словари
Доступ к элементу массива осуществляется по индексу, который указывает позицию элемента в массиве. Например, если А — массив, то элемент А[п] расположен в n-ой ячейке массива. Индекс не хранится как часть данных. Словарь (таблица, ассоциативный массив) есть индексируемая структура данных, подобная массиву. Однако как индексы, так и сами словарные данные могут быть любого типа. Например, если CommonWords — словарь, то Common-i Words[”decide' ] может быть определением слова "decide". В отличие от массивов, словарный индекс, скорее, связан с элементом данных, чем точно указывает, гДе этот элемент хранится. Кроме того, число элементов словаря не ограничено.
Словарь называют ассоциативной структурой, поскольку он хранит список ключей н ассоциируемых с ними значений данных. Например, толковый словарь является таблицей слов (ключей) и их дефиниций (значений). Словари | отличаются от массивов тем, что фактическое расположение их элементов скрыто. Доступ никогда не производится путем прямого указания позиции в описке, а осуществляется только по ключу.
Данные запоминаются в словаре в виде множества пар ключ-значение, называемых также ассоциациями. Эти пары могут храниться в связанном списке, дереве или хеш-таблице. Если данные упорядочены по ключам, то говорят, Г Что таблица упорядочена. Рис. 14.5 иллюстрирует смысл всех этих понятий.
736
DicEntry['f or"]------------
| 'and' |
L lor' |
I'what' |	|
I ’bur i
L ‘af J
Связанный список, дерево, хеш-таблица
Рис. 14.5. Словарь (ассоциативный массив)
Чтобы реализовать пригодный для работы словарь, следует сперва разрабо тать методы хранения пар ключ-значение в рамках класса KeyValue. Каждая пара ключ-значение есть объект типа KeyValue с постоянным ключом. Любые два таких объекта можно сравнивать посредством операторов == и <. Сравнение происходит по ключам. Это будет нашим первым шаблоном класса, имеющим два параметра: К — тип ключа и Т — тип значения, ассоциируемого с ключом.
Спецификация класса KeyValue
ОБЪЯВЛЕНИЕ
template <class К, class Т>
class KeyValue (
protected:
11 после инициализации ключ не может быть изменен const К key;
public:
// словарные данные являются общедоступными Т value;
KeyValue(К KeyValue, Т datavalue);
11 операторы присваивания, не изменяют ключ KeyValue<K,T>s operator- (const KeyValue<K,Т>& rhs);
// операторы сравнения, сравнивают два ключа / int operator== (const KeyValue<K,T>& value) const; int operator™ (const Ks keyval) const;
int operator< (const KeyValue<K,T>& value) const; int operator< (const Kfi keyval) const;
// метод доступа к ключу
К Key(void) const;
ОПИСАНИЕ
Конструктор создает пару ключ-значение. Конструктора по умолчанию Если объект создан, ключ изменять нельзя. Оператор присваивания 3^и58еТ вает только собственно данные (не ключ), а оператор отношения срав ключи (а не сами данные). Метод Key предназначен для чтения ключа.
ПРИМЕР
Определяется пара ключ-значение, содержащая номер социально® сТ^ , ки в качестве символьного ключа и запись типа Data в качестве значен
Организация коллекц,*»
struct Data
(
char name(301;
int yearsThisCompany;
int jobclass;
float salary;
Data empData = {’’Джордж Уильямс", 10, 5, 45000.00};
KeyValue<String, Data* Employee("345789553", empData);
Реализация этого класса очень проста, несмотря на довольно изощренную концепцию словаря. Выберем класс для хранения объектов типа KeyValue. Для хранения упорядоченных пар ключ-значение подойдут классы OrderedList, BinSTree и AVLTree, а для неупорядоченных — SeqList или HashTable. Все эти классы имеют методы Insert, Delete, Find н т.д. Чтобы создать словарь мы должны дополнить этот набор оператором индексирования []. Этот оператор связывает ключ, указываемый в качестве индекса, с полем данных соответствующего объекта KeyValue. В этой книге мы использовали наследование для выражения отношения "является". Еще одно применение наследования — расширение функциональности базового класса. Мы дополним с помощью наследования коллекцию пар ключ-значение оператором индексирования й другими специфическими словарными операциями. На рис. 14.6 показано, как класс Dictionary может быть образован из нескольких базовых классов.
OrderedList	BinSTree	HashTable
Классы упорядоченных словарей
Неупорядоченный словарь
Рис. 14.6. Расширение коллекции объектов ключ-значение до словаря
Спецификация класса Dictionary
Объявленив
flinclude "keyval.h"
flinclude ’’bstree.h’’
flinclude "tree!ter.h"
template <class K, class T>
class Dictionary: public BinSTree< KeyValue<K,T> >
// значение, присваиваемое элементу словаря по умолчанию.
// используется оператором индексирования, а «также методами
// InDictionary и DeleteKey private:
Т defaultvalue;

public:
// конструктор
Dictionary(const T& defval);
// оператор индексирования T& operator(] (const K& index);
II дополнительные словарные методы int InDictionary(const K& keyval); void DeleteKey(const K& keyval);
ОПИСАНИЯ
Оператор индексирования выполняет большую часть работы. Он проверяв наличие заданного ключа в словаре. Если словарный элемент с таким ключом существует, оператор возвращает ссылку на этот элемент. Если нет, то создается новая словарная статья и возвращается ссылка на нее. Таким образом любое создание или обновление словарных данных происходит с помощью оператора индексирования. Поскольку для класса KeyValue не создается конструктор, действующий по умолчанию, ключ и данные должны быть указаны. Поэтому при создании объекта оператор [] должен иметь некоторое значение словарной статьи по умолчанию. Оно передается в конструктору качестве параметра. Это значение следует выбирать с осторожностью, так, чтобы новый словарный элемент мог участвовать в выражениях.
Например, словарь может содержать символьный ключ — слово — и символьное значение — дефиницию этого слова. Тогда объект BasicDict типа Dictionary объявляется следующим образом:
// словарная статья по умолчанию пуста
Dictionary<String, String? BasicDict(' *);
Предположим, что следующий оператор употребляется впервые:
BasicDict["секстет"] +- "Группа из шести исполнителей";
Оператор [] создает пустую словарную статью с ключом ’'секстет”. Строковый оператор +« сцепляет с пустой строкой строку "Группа из шести исполнителей”, создавая тем самым дефиницию слова "секстет”.
Метод InDictionary проверяет наличие в словаре пары ключ-значение с ключом keyval, а метод DeleteKey удаляет словарную статью, имеющую ключ keyval. Методы ListEmpty, ListSize и ClearList определены в базовом классе. Методы Insert, Find и Delete также могут непосредственно работать с объектами Типа KeyValue, но сам факт их использования для слодарей является несколько необычным.
В большинстве приложений требуется итератор словаря, чтобы собира данные для вывода. Поскольку объект типа Dictionary образуется из класс BinSTree, класс Dictionaryiterator можно вывести и^ класса Inorderiterator.
template <class К, class Т>
class Dictionaryiterator: public lnorderIterator<KeyValue<K,T> (
public:
11 конструктор
DictionaryIterator(Dictionary<K,T>4 diet);
// начать итерацию нового словаря void Setbist(Dictionary<K,T>& diet);
// конструктор, diet "расширяет" объект BinSTree и использует его
Организация коллекции___________________________________________________________
// общедоступный метод GetRoot для инициализации Оазового класса
// Inorderiterator
template <class К, class Т>
DietionaryIterator<К,T>::Dictionary!terator(Dietionary<K, T>& diet): lnorderlterator< KeyValue<K,T> > (diet.GetRoot())
{}
// использовать метод SetTree базового класса template <class К, class T>
void DictionaryIterator<K/T>::SetList(Dietionary<KrT> & diet) {
SetTree(diet.GetRoot ());
)
Реализации классов Dictionary и Dictionaryiterator находятся в файле dict.h.
Программа 14.5. Построение толкового словаря
Эта программа создает объект wordDictionary типа Dictionary со строковым ключом и данными. Значением словарной статьи по умолчанию является пустая'строка. Файл defs.dat содержит список слов и их дефиниций. Ключевое слово находится в начале строки и завершается пробелом. Остальная часть строки содержит дефиницию. Слова вводятся в цикле и используются в качестве ключей для добавления своих дефиниций в словарь.
Словарный итератор dictlter используется для прохождения словаря, во время которого для каждой найденной пары ключ-значение вызывается функция PrintEntry для распечатки словарной статьи. Эта функция сначала распечатывает ключевое слово и следующее за ним тире, а затем построчно выводит его дефиницию по 65 символов в строке без переносов по слогам.
flinclude <fstream.h> flinclude <stdlib.h>
# include "keyval.h"
flinclude "dict.h'1
flinclude "strclass-h"
// итератор, просматривающий объекты KeyValue
// кйасс Dictionary
// пары ключ-значение имеют тип String
// распечатать объект KeyValue, содержащий ключевое слово word
// и его дефиницию (и)
void PrintEntry(const KeyValue<String,String>& word) <
KeyValue<String, String> w = word;
// поскольку после ключевого слова выводится " -
// дефиниция распечатывается с позиции, равной Length(word) * 3
int linepos - w.Key().Length() +3; int i;
// распечатать слово и " - "
cout « w.Key() ” -
11 распечатать дефиницию на 65-символьных строках while (iw.value.lsEmptyO)
{
// определить, умещается ли еще не распечатанная часть в 65-символьной // строке, вычислить индекс последнего символа в строке
if (w.value.LengthО > 65-linepos)
(
// текст не умещается в строке, двигаясь в обратном направлении // найти первый пробел, не переносить слова по слогам.
i = 64-linepos;
while(w.value[i] !- ’ *)
I else
11 текст умещается в строке
i = w.value.Length() - 1;
Il вывести часть текста дефиниции, которая умещается в строке cout « w.value.Substr(O, i+1) « endl;
// удалить только что распечатанную часть текста.
// приготовиться к переходу на новую строку
w.value.Remove(0, i+1);
linepos = 0;
)
void main (void) {
// входной поток данных
ifstream fin;
String word, definition;
// словарь
Dictionary<String,String* wordDictionary("");
// открыть файл defs.dat ключевых слов и их дефиниций fin.open("defs.dat", ios::in | ios::nocreate) ;
if ('fin)
(
cerr « "Файл defs.dat не найден" « endl;
exit(1);
// прочитать слово и его дефиницию, с помощью оператора индексирования // включить статью в словарь или обновить существующую дефиницию, // дополнив ее текущей while (fin » word) {
if (fin.eof(J)
break;
// прочитать пробел, следующий за ключевым словом
definition.Readstring(fin);
wordDictionary(word] +- definition;
// объявить итератор для нисходящего обхода словаря DictionaryIterator<String,String> dictlter(wordDictionary);
// просматривать словарь, распечатывать каждое ключевое слово
// и его дефиницию(и)
cout « "Толковый словарь: ” « endl « endl;
for (dictlter.Reset(); !dietIter.EndofList{); dictlter.Next())
(
PrintEntry(dictlter.Data());
cout « endl;
wordDictionary.ClearList();
)
/•
<Файл defs.dat*
Организация коллекции
Программа Последовательность операций, выполняемых компьютером.
Финишировать Заканчивать, завершать.
Причина То, из чего следует результат.
Секстет группа из шести исполнителей.
Программа Перечень действий, приветственных речей, музыкальных пьес и т.п.
Скорость Быстрота, проворность.
Скорость Перемещение за единицу времени.
Секстет Музыкальная композиция для шести инструментов.
Шапка Головной убор.
Шапка Газетный заголовок шириной на всю полосу.
<Прогон программы 14.5>
Толковый словарь:
Причина - То, из чего следует результат.
Программа - Последовательность операций, выполняемых компьютером.
Перечень действий, приветственных речей, музыкальных пьес и т.п.
Секстет - Группа из шести исполнителей.
Музыкальная композиция для шести инструментов.
Скорость - Быстрота, проворность. Перемещение за единицу времени.
Финишировать - Заканчивать, завершать.
Шапка - Головной убор. Газетный заголовок шириной на всю полосу.
*/
Реализация класса Dictionary
Конструктор инициализирует базовый класс и задает значение словарной статьи по умолчанию.
И конструктор, инициализирует базовый класс и задает значение
И словарной статьи по умолчанию
template <class К, class Т>
Dictionary<K,T>:: Dictionary (const Т& defaultval):
BinSTxee< KeyValue<K,T> >(), defaultValue(defaultva1)
I)
Оператор индексирования создает объект targetKey типа KeyValue с заданным ключом и значением данных по умолчанию и ищет этот ключ на дереве. Если ключ не найден, targetKey вставляется в дерево. Элемент базового класса current устанавливается на только что найденный или вставленный узел. Оператор возвращает ссылку на значение данных в этом узле.
// оператор индексирования, здесь делается почти вся работа template <class К, class Т>
Ть Dictionary<K,Т>::operator[) (const KS index) (
// определить целевой объект типа KeyValue, содержащий
// данные задаваемые по умолчанию
KeyValue<K,Т> targetKey(index, defaultvalue);
// искать ключ, если ие найден, вставить targetKey
if (!Find(targetKey))
Insert(targetKey);
// возвратить ссылку на найденные или вставленные данные
return current->data.value;
----------------------------------------------
Функция InDictionary создает объект tmp типа KeyValue с заданным чом и значением данных по умолчанию, ищет этот ключ на дереве иНл1°' вращает результат поиска.	и в°а'
// проверить, существует ли объект типа KeyValue //с данным ключом
template <class к, class Т> int Dictionary<K,T>::InDictionary(const Kfi keyval) <
// определить целевой объект типа KeyValue, содержащий
// данные задаваемые по умолчанию
KeyValue<K,T> tmpC index, defaultvalue.);
int retval = 1; I
// искать tmp на дереве, вернуть результат
if (!Find(tmp))
retval =0;
return retval; }
Функция DeleteKey создает объект tmp типа KeyValue с заданным ключом и значением данных по умолчанию и удаляет этот словарный элемент из дерева.
// удалить объект типа KeyValue с данным ключом из словаря template <class К, class Т>
void Dictionary<K,Т>::DeleteKey(const Кб keyval) <
KeyValue<K,T> tmp(index, defaultvalue);
Delete (tmp); )
Письменные упражнения
14.1
а)	Отсортируйте числовую последовательность 8, 4, 1, 9, 2, 1, 7, 4 посредством выбора. Отображайте состояние списка после каждого про* хода.
б)	Повторите пункт а) для символьной последовательности V, В, L, А, Z, Y, С, И, S, S, В, Н.
14.2	В предыдущем упражнении выполните сортировку вставками.
14.3	Отсортируйте символьную последовательность С, А, М, Т, В, В, А, Ь методом включения. Проследите каждый шаг сортировки.
14.4	I
а)	Какова эффективность сортировки посредством выбора, в случае маС сива п одинаковых элементов?
б)	Ответьте на вопрос а) для случаев сортировки вставками и метод©14 пузырька.
14.5	Отсортируйте массив А, используя метод пузырька. После ка>к прохода показывайте сам список и подсписок, подлежащий сортир0 А -= 85, 40, 10, 95, 20, 15, 70, 45, 40, 90, 80, 10
14.6	Отсортируйте массив А с помощью "быстрой сортировки", центральный элемент из середины списка. Во время каждого пр°
Организация коллекции
фиксируйте все обмены элементов между нижним и верхним подсписками. Показывайте состояние последовательности после каждого прохода.
А = 790, 175, 284, 581, 374, 799, 852. 685, 486, 347
14.7	В другой версии алгоритма ’’быстрой сортировки" в качестве центрального элемента выбирается A[low], а не A[mid]. Эта версия тоже является О(п 1о§2п)-сложной, но поведение худшего случая изменяется. Как?
14.8	Массив А следует отсортировать посредством включения его элементов в двусвязный список. Вставьте элемент в текущую точку и перемещайте его вперед по списку, если новый элемент больше текущего, или назад, если меньше. Напишите функцию DoubleSort, реализующую этот метод сортировки.
template <class Т>
void DoubleSort (Т а(}, int n);
Голова	Хвост
14.9	Оцените алгоритмическую сложность метода сортировки из предыдущего упражнения. Рассмотрите наилучший, наихудший и средний случаи.
14.10	Какой из основных алгоритмов сортировки (выбором, вставками или пузырьковый) наиболее эффективен для обработки уже отсортированного списка? А если этот список отсортирован в обратном порядке?
14.11	В настоящей книге мы рассмотрели следующие методы сортировки: включением в бинарное дерево, пузырьковый, обменный, пирамидальный, вставками, поразрядный, выбором, турнирный.
Для каждой из этих сортировок укажите сложность, потребности в памяти и дайте некоторые комментарии по поводу ее эффективности. В комментариях можно отразить следующие моменты: возможность досрочного окончания процесса в случае уже отсортированного списка, вероятность наихудшего случая, число обменов и величину константы пропорциональности (большое О).
14.12	Метод сортировки называют устойчивым (stable), если на любом шаге алгоритма два одинаковых элемента не меняются местами друг относительно друга. Например, в пятиэлементном массиве 51 55 12 52 33 устойчивая сортировка гарантирует, что результирующая последовательность будет иметь следующий порядок: 51 б2 12 33 55 Классифицируйте методы сортировки из предыдущего упражнения с точки зрения их устойчивости.
14.13	Покажите, что хеш-функция hash(x) = х % ш.
неприемлема при четном ш. Изменится ли ситуация при нечетном ш? (Совет. Рассмотрите распределение четных и нечетных случайных чисел.)
14.14	Предположим, что хеш-функция имеет следующие характеристи Ключи 257 и 567 отображаются в 3
987 и 313 отображаются в 6
734, 189 и 575 отображаются в 5
122 и 391 отображаются в 8
Ключи
Ключи
Ключи
Ключи вставляются в таблицу в следующем порядке: 257 ©87 ю*
575, 189, 734, 567, 313, 391.	’	’
а)	Покажите позиции этих данных в таблице, если коллизии разреш ются методом открытой адресации.	а"
НТ 0123456789 Ю
										
б)	Покажите позиции этих данных в таблице, если коллизии разрешаются методом цепочек.
НТ
0123456789	10
14.15	Повторите предыдущее упражнение при обратном порядке вставки в таблицу.
14.16	Для отображения данных в табличные индексы используйте хеш-функцию hashf(х) = х % 11. Данные вставляются в таблицу в следующем порядке: 11, 13, 12, 34, 38, 33, 27, 22.
а)	Постройте хеш-таблицу методом открытой адресации.
б)	Постройте хеш-таблицу методом цепочек.
в)	Для обоих методов определите коэффициент заполнения, среднее число проб, необходимое для обнаружения элемента в таблице и среднее число проб для констатации отсутствия элемента в таблице.
14.17	Покажите, что хеш-функция, отображающая символьные строки в целые числа посредством суммирования символов в строке, не является хорошей. Рассмотрите, как с помощью сдвигов можно исправить си-
туацию.
14.18	При разработке хеш-функции иногда применяется метод свертки (f°^ ing). Ключ разбивается на части, которые зат^м комбинируются таким образом, чтобы получилось меньшее число. Это число используется качестве значения хеш-функции или уменьшается еще раз посредств деления. Предположим, в некоторой программе в качестве ключа пользуется иомер социальной страховки. Разобьем ключ на три ^У11 по три цифры и сложим их. Получится некоторое число в ди 3^ О..2997. Например, номер 523456795 даст индекс 523 + 456 + • 1774. Напишите хеш-функцию
int hashf(char *ssn);
реализующую этот метод. (Совет. 1ужно извлечь подстроки и преоо-разовать их в целое число.)
14.19	Дана следующая хеш-функция:
unsigned short hash(unsigned short key) (
return (key » 4). % 256;
1
а)	Каков размер хеш-таблицы?
б)	Чему равны hashf(16) и hashf(257)?
в)	Что вообще делает эта хеш-функция?
14.20	Дана следующая хеш-функция:
unsigned long hash(unsigned long key) (
return (key » 8) % 65536;
}
а)	Каков размер хеш-таблицы?
б)	Чему равны hashf(16) и hashf(10000)?
в)	Что вообще делает эта хеш-функция?
14.21	Проблемой открытой адресации является скопление (clustering) конфликтующих ключей.
0
Скопление
Скопление
Скопление
TableSize-l
Предположим, в таблице есть N ячеек. Если хеш-функция хорошая, то какова вероятность хеширования в индекс р? Если ключ попал в ячейку р, то ячейка р+1 может быть занята ключом, хешированным в р или в р+1. Какова вероятность занятия ячейки р+1? Какова вероятность занятия ячейки р+2? Объясните, почему вообще возникают скопления.
4.22	Если ключ отображается в занятую ячейку таблицы с номером index, метод открытой адресации по схеме линейного опробывания выполняет функцию
index = (index+1) % m;	11 проверить следующий ийдекс
Эта функция называется функцией рехеширования (rehash function), а метод разрешения коллизий — рехешированием (rehashing). Линей-
ное опробывание благоприятствует скоплению ключей. Однако л ция рехеширования может рассеивать ключи лучше. Два целых р и q называются взаимно простыми, если ие имеют общего дел1?ИСЛа иного чем 1. Например, 3 и 10 — взаимно простые числа. геЛй» и 35.	aR и 18
Пусть в методе открытой адресации используется следующая фун рехеширования:	у К1^я
index = (index + d) % m;
где d и m — взаимно простые числа1. Последовательное примене этой функции порождает индексы от 0 до т-1. При линейном пг?Ие бывании d = 1.	°П₽0-
а)	Если d и т не являются взаимно простыми, некоторые ячейки таблив пропускаются. Покажите, что при d = 3 и m = 93 функция
index = (index +3) % 93
попадает лишь в каждую третью ячейку таблицы.
б)	Покажите, что если m — простое число и d < m, то вся таблица покрывается функцией рехеширования.
в)	Выполните упражнение 14.16а, используя функцию рехеширования
index = (index +5) % 11
14.23	Хеш-таблицы хорошо подходят для тех приложений, где основной операцией является поиск и выборка. Запись вставляется в таблицу, а затем много раз выбирается оттуда. Однако хеширование методом открытой адресации не слишком удобно для тех приложений, где требуются удаления данных из хеш-таблицы.
Рассмотрим следующую таблицу из 101 ячейки и хеш-функцию hashf(key) = key % 11.
а)	Удалите ключ 304, поместив в ячейку 1 число -1. Что произойдет при поиске ключа 707? Объясните, почему для решения задачи удаления в общем виде недостаточно просто пометить ячейку как незанятую?
б)	Решение этой проблемы предусматривает запись в ячейку, содержащую удаляемый элемент, ключа DeletedData. При поиске ключа все ячейки, содержащие DeletedData, пропускаются. Для удаленных ключей используйте значение -2. Покажите, что при таком подходе удаление ключа 304 не помешает корректному поиску ключа 707.
Операции вставки и выборки в алгоритме открытой адресации должны быть модифицированы с учетом удалений.
в)	Опишите алгоритм удаления табличного элемента.
г)	Опишите алгоритм обнаружения элемента в таблице.
д)	Опишите алгоритм включения элемента в таблицу.
14.24	Еще одним методом разрешения коллизий, который иногда ется, является связывание в срастающиеся списки. Этот метод П°Д
----------- лучвЙйоГ0
1 Этот метод разрешения коллизий называют еще открытой адресацией по схеме слу опробывания. — Прим, перев.
открытой адресации, но конфликтующие ключи, которые должны располагаться в таблице ниже и т.д. по кругу, сцепляются вместе с помощью связанного списка. Возможны ситуации, когда цепочка содержит ключи, первоначально хеширующиеся в разные ячейки таблицы. Тогда говорят, что эти списки срастаются. Например, если hashf(x) = х % 7 и в таблицу включаются 12, 3, 5, 20 и 7, мы имеем следующую картину:
-1 обозначает пустую ячейку и NULL-укаэатель
0 1 2 3 4 5 6
20	1
7	-1
-1	-1
3	-1
-1	-1
12	6
5	0
а)	Выполните упражнение 14.16а, используя метод связывания в срастающиеся списки.
б)	Как вы думаете, сравним ли данный метод с методами открытой адресации и методом цепочек с точки зрения быстродействия? Классифицируйте все эти методы по быстродействию.
в)	Проще ли решается проблема удаления, чем в методе открытой адресации? Поясните.
14.25 Даны множество ключей ко, ki, ..., kn-i и совершенная хеш-функция (perfect hashing function) И — хеш-функция, не порождающая коллизий. Нет смысла искать совершенную хеш-функцию, если множество ключей не является постоянным. Однако для таблицы символов компилятора (содержащую зарезервированные слова while, template, class и т.д.) совершенная хеш-функция крайне желательна. Тогда для определения того, является ли некоторый идентификатор зарезервированным словом, потребуется лишь одна проба.
Найти совершенную хеш-функцию даже для конкретного набора ключей очень сложно. Обсуждение этого предмета выходит за рамки данной книги. Кроме того, если данный набор ключей пополнится новыми ключами, совершенная хеш-функция, как правило, перестает быть совершенной.
а)	Даны множество целочисленных ключей 81, 129, 301, 38, 434, 216, 412, 487, 234 и хеш-функция
Н(х) - (х+18)/63
Является ли данная хеш-функция совершенной?
б)	Дан набор символьных ключей
Bret, Jane, Shirley, Bryce, Michelle, Heather
Придумайте совершенную хеш-функцию для 7-элементной таблицы.
14.26 Дано следующее описание класса текстовых файлов, в котором моделируются файловые операция языка Паскаль. Реализуйте этот класс с помощью операций класса fstream языка C++.
enum Access {IN, OUT}
class PascalTextFile
private:
fstream f;
char fname[64];
Access accesstype;
int isOpen;
void Error(char *msg);
public:
PascalTextFile(void);
void Assign(char *filename);
void Reset(void);
void Rewrite (void);
int EndFile(void);
void Close(void);
int PRead(T A(), int n);
void PWrite(T A(], int n);
// определяет поток данных файла
// файловый поток Си++
// имя файла
// входной или выходной поток
// используется методом Reset
// используется для печати ошибок
// конструктор
// задает имя файла
// открывает файл для ввода
// открывает файл для вывода
// читает флаг конца файла
// закрывает файл
// читает п символов в А
// записывает п символов в А
Упражнения по программированию
14.1	Напишите программу, создающую упорядоченный список N случайных чисел из диапазона 0—1000 с помощью алгоритма включения в двусвязный список из письменного упражнения 14.8. Распечатайте отсортированную последовательность.
14.2	Реализуйте следующий алгоритм:
Разбить n-элементный список пополам. Отсортировать каждую половину с помощью сортировки выбором, а затем слить обе половины, а) Проанализируйте сложность этой сортировки.
б) Используйте этот алгоритм для сортировки 20000 случайных чисел и измерьте время выполнения программы.
г) Запустите ту же программу, но использующую обычную сортировку посредством выбора. Какая версия работает быстрее?
14.3	В разделе 14.6 обсуждалась сортировка файлов прямым слиянием. Реализуйте внутреннюю версию этого алгоритма для сортировки п-эле-ментного массива. Отсортируйте с помощью этой программы 1000 случайно сгенерированных чисел с двойной точностью. Распечатайте первые и последние 20 элементов отсортированного массива.
14.4	Дана следующая структура:
struct TwoKey (
int primary;
int secondary;
Создайте массив из 100 записей этого типа. Поле primary coflepoSe случайное число в диапазоне 0..9, а поле secondary — в диаС^япо-0..100. Модифицируйте алгоритм сортировки вставками для ^^рти-чения по двум ключам. Новый алгоритм должен производитЬ черця ровку по вторичному ключу для каждого фиксированного зна
первичного ключа.. отсортируйте с его помощью ранее созданным массив. Распечатайте массив в формате primary (secondary).
14.5	Сортировка Шелла, названная так по имени своего изобретателя Дональда Шелла, является простым и довольно эффективным алгоритмом. Она начинается с разбиения исходного n-элементного списка на к подсписков:
а[0], а[к+О], а[2к+0], ...
а[1], а[к+1], а[2к+1], ...
а[к-1], а[к+(к-1)], а[2к+(к-1)], ...
Подсписок начинается с первого элемента а[1] в диапазоне а[0] ... а[к-1] н включает в себя каждый последующий к ый элемент. Например, при к — 4 следующий массив разбивается на четыре подсписка:
7586249130
Подсписок #0	7	2	3
Подсписок #1	5	4	0
Подсписок #2	8	9
Подсписок #3	6	1
Отсортируйте каждый подсписок сортировкой вставками. В нашем примере получатся следующие подсписки:
Подсписок #0	2	3	7
Подсписок #1	0	4	5
Подсписок #2	8	9
Подсписок #3	1	6
и частично отсортированный массив 208134967 5.
Повторите процесс с к — к/3. Продолжайте так до к — 1, при котором список получается отсортированным. Оптимальный выбор начального значения к — задача теории алгоритмов. Алгоритм является успешным, поскольку обмен данных происходит в несмежных сегментах массйва. В результате элемент перемещается гораздо ближе к своей окончательной позиции, чем при обмене соседних элементов в сортировке простыми вставками.
Создайте в главной процедуре массив 100 случайных целых чисел в диапазоне 0—999. Для сортировки Шелла возьмите начальное значение к = 40. Распечатайте исходный и отсортированный списки по 10 чисел в строке.
14.6	В этом упражнении разрабатывается простая программа орфографического контроля. В программном приложении к этой книге имеется файл words, который содержит 500 наиболее часто употребляющихся слов. Прочитайте этот файл и вставьте все имеющиеся там слова в хеш-таблицу. Прочитайте текстовый документ и разбейте его на отдельные слова с помощью следующей несложной функции:
// извлечь слово, начинающееся с буквы и состоящее из букв и цифр int GetWord (ifstreams fin, char w[[])
char с; int i - 0;
// пропустить все не буквы while (fin.get(с) &< lisalpha(c));
// возвратить 0 по окончании файла if (fin.eofO) return 0;
// записать первую букву слова ыЦ++] - с;
// собрать буквы и цифры. Завершить слово нулем
while (fin.get(с) && (isalpha(c) II isdigit(c))) w[i++) - c;
w[i] - ’ Non-
return 1; }
Используя хеш-таблицу, распечатайте слова, в которых могут быть орфографические ошибки.
14.7	Разработайте классы OpenProbe и OpenProbelterator, поддерживающие хеш-таблицы, которые используют метод открытой адресации. Ниже дана спецификация класса OpenProbe. Для реализации класса используйте письменное упражнение 14.23. // формат записей таблицы template cclass Т> struct TableRecord ( И доступен (да или мет) int available; Т data; );
template cclass T>
class OpenProbe: public List«T>	s'
( protected: // динамически создаваемая таблица и ее размер TableRecord<T> *table; int tablesize;
// хеш-функция unsigned long (*hf) (T key);
// индекс ячейки, к которой последний раз было обращение int lastindex;
public: // конструктор, деструктор OpenProbe(int tabsize, unsigned long hashf(T kejH); -OpenProbe(void);
// стандартные методы обработки списков virtual void Insert(const TS key); virtual void Delete(const TS key); virtual int Find(T& key); virtual void ClearList(void);
// обновить ячейку, к которой последний раз было обращение void Update(const Т& key) ;
friend class OpenProbeiterator<T>;
J;
Используйте эти классы в программе 14.2.
14.8	Поместите объявление класса PascalTextFile из письменного упражнения 14.27 в файл ptf.h. Напишите программу, которая с помощью этого класса читает файл ptf.h, преобразует каждую строчную букву в прописную и записывает их в файл ptf.uc. Используйте соответствующую команду вашей операционной системы для распечатки содержимого файла ptf.uc.
14.9	Используйте класс BinFile для следующих программ.
а)	Запись Person определяет последовательность полей в некоторой базе данных.
struct {	Person	
char	first(20];	// имя
char	last[20];	// фамилия
char );	id(4];	// четырехзначный идентификатор
Определите функцию DelimRec, параметрами которой являются запись Person и буфер.
void DelimRec (const Person &p, char ^buffer);
Эта функция преобразует каждое поле записи в символьную строку переменной длины, заканчивающуюся разделителем Три поля сцепляются друг с другом в буфере. Например,
Person:	first	Tom
last	Davis
id	6192
Вуфер:	Tom|Davis|6192|
Напишите программу, которая вводит пять записей Person по одному полю в строке и создает файл символов reel .out. Для каждой записи создайте компактный буфер длиной п и выведите в файл размер этого буфера в виде двухбайтового короткого целого и п символов, находящихся в буфере. Распечатайте содержимое reel.out с помощью утилиты шестнадцатеричного дампа, если таковая имеется в вашей системе.
б)	Напишите программу, которая вводит последовательные записи из файла recl.out, расширяя каждое поле до его фиксированной длины, определенной в записи Person. Если нужно, дополняйте поле пробелами справа. Теперь запись Person имеет длину 44 байта. Выведите новые записи в файл rec2.out.
в)	Напишите программу поиска заданного четырехзначного идентификатора в файле rec2.out. В случае удачи распечатайте имя и фамилию найденного человека.
.«цующии тип записи;
struct CharRec {
char Key;
int count;
С помощью класса BinFile создайте файл letcount из 26 таких аапис & содержащих в поле key буквы от ’А’ до *Z* и О в поле count. Прочитай текстовый файл, преобразуя каждую букву в прописную и обновля^ поле count в соответствующей записи бинарного файла letcount? Ра * печатайте частоту каждой буквы. Отсортируйте записи по полю count методом естественного слияния (см. раздел 14.6) с длиной блока, рав ной 4. Распечатайте отсортированный файл.
14.11 Образуйте класс упорядоченных словарей из класса OrderedList. Ис~ пользуйте этот класс, а также связанный с ним итератор в программ ме 14.5.
14.12 Образуйте класс словарей из класса HashTable^ Используйте этот класс, а также связанный с ним итератор в программе 14.5. Распечатайте словарные объекты в порядке извлечения итератором. Отсортируйте результаты обхода и распечатайте словарные объекты в алфавитном порядке.
Приложение
Ответы на избранные письменные упражнения
Глава 1
1.2
a) ADT Cylinder Данные
Радиус.л высота цилиндра представляются положительными числами с плавающей точкой.
Операции
Конструктор Начальные значения: Радиус и высота цилиндра. Обработка:	Задать начальные значения радиуса
и высоты цилиндра. Area
Вход:	Нет
Предусловия: Обработка:
Выход:
Постусловия: volume
Вход:
Предусловия: Обработка:
Выход:
Постусловия:
Конец ADT Cylinder
Нет
Вычислить площадь цилиндра по заданным радиусу и высоте.
Возвратить величину площади
Нет
Нет
* Нет
Вычислить объем цилиндра по заданным радиусу и высоте.
Возвратить величину объема, нет
1.3 Пусть Cyl и Hole — цилиндры с радиусами R и Rh, а С — окружность с радиусом Rh.
а)	Результирующий объем геометрического тела равен Cyl.Volume() — Hole.Voliimef).
6)	Площадь этого геометрического тела равна Су1.Агеа() + Но1е.Агеа() — 4*С.Агеа().
1.5 const float PI - 3.141S9;
class Cylinder
t
25 Зак. 425
private:
float radius, height;
public:
Cylinder(float r, float h>: radius(r), height(h) {) float Area(void)
(return 2.0*PI‘radius(radius+height);} float Volume(void)
{return PI*radius*radius‘height;} );
1.11 a)
Два или более объектов в некоторой иерархии наследования класс имеют одноименные методы, выполняющие разные задачи. Это свойств позволяет объектам различных классов отвечать на одно и то же Со° общение. Приемник сообщения определяется динамически в процессе выполнения программы.
Глава 2
2.1
a)	5;
6)	14;
в)	55;
r) 127
2.3
a)	26;
6)	1055;
в)	4332;
r) 255;
д)	65536;
e)	17;
ж)	57;
з)	73;
и)	FF
2.4
a)	C;
б)	A6;
в)	F2;
r) BDE3;
д) 11000010000;
e) 1010111100100000
2.5
a)	32 50 32;
6)	32 32 40
2.8
a)	*N’;
б)	’K’;
в)	’**:42io, 1010102 V:ИЗю, IIIIOOOI2 <сг>:13ю, 11012
2.9 V 113 8
2.П
а)	6.75
б)	.111 ... 111 ... = 2+ 4+ 8 + •” + 2“ + "• = 1
При стремлении п к бесконечности дробная часть стремится к 1. Следовательно десятичный эквивалент равен 3 + 1 = 4.
2.12
б) 1.001
2.14
a) 40f00000;
г) 29.125
2.17 X = 55, Y = 10, А = {5.3, 6.8, 8.9, 1, 5.5, 3.3}
2.18
а)	(1) Для А выделяется 10 байт. (2) &А[3] = 6000 + 2*3 ~ 6006, &А[1] = 6000 + 2*1 - 6002
б)	(1) 33 (2) А = (60, 50000, -10000, 10, 33} (3) &А[3] = 2050 + 4*3 -2062
2.20
а)	30 * 2 в 60 байт.
б)	&А[3][2] - 1000 + 3*12 + 2*12 = 1040, &А[1][4] = 1000 + 1*12 + 4*2 = 1020
2.22
а)	Ч’, Ч’, NULL;
б)	Stockton, С.А. March 5, 1994;
в)	1;
г)	1
2.23 void strinsert (char *s, char *t, int i) <
char tmp(128];	// храни? хвост s
if (i > strlen(s))	// выход no достижении хвостового нуля
return;
strcpy(tmp, &s[i]);	// скопировать хвост s в tmp
strcpy(&s[1], t);	// скопировать t на место хвоста
strcpy(s, tmp); // сцепить с хвостом из tmp }
2.25
6) void ₽toCStr(char *s) (
int n - *s++;	// взять счетчик Сайтов
while (n—)	Il передвинуть каждый символ влево
*(s-1) - *s++;	// на одну позицию
*s “ 0;	// завершить формирование строки
)
2.27 Complex cadd(Complex& x, Complexr y) (
Complex sum - (x.real+y.real, x.imag+y.imag); return sum;
)
Complex cmul(Complex^ x. Complexs y) {
Complex product = {x.real*y.real - x.imag*y.imag, x.real*y.imag + x.imag*y.real);
return product; )
Глава 3	_
3.	2
6)	class Box {
private:
float length, width, height;
public:
Box(float 1, float w, float h);
float GetLength(void) const;
float GetWidth(void) const;
float GetHeight(void) const;
float Area(void) const;
float Volume(void) const;
);
Box::Box(float 1, float w, float h): length(l), width(w), height(h) {} float Box::Area(void) {
return 2-0 * <l*w + l*h + w*h); }
в)	Напишите функцию Qualify(Box В), которая возвращает 0, если ящик бракованный, или 1 - в противном случае.
if ( (2* (В.GetLength() + B.GetWidtho) + В.GetHeight О) < ЮС ) return 1; Н и ч.д-
3.3
a)	private и public должны заканчиваться двоеточием Последняя закрывающая фигурная скобка должна эаканчиваться-точкой с запятой
б)	Y(int n, int m): p(n), q(m) {}
3.4
a)	class х (
private:
int a, b, с;
public:
X(int x=l, int y-1, int z-1);
int f(void);
>;
6)	X:X(int x, int y, int z): a(x), b(y), c(z) {}
3.5	class Student	\
{ ...	V
public:
Student(int id, int studgradepts, int studunits): s tudentid(id), gradepts(studgradepts), units(studunits) {CompUteGPA();}
);
void Student::UpdateGradeInfo(int newunits, int newgradepts)
(
units +« newunits;
gradepts +« newgradepts;
ComputeGPA(); }
3.8
a)	CardDeck;: CardDeck (void) (
for (int i«0; i<52; x++)
cards[i] = i;
currentcard « 0; }
void CardDeck::Shuffle(void) (
static RandomNumber rnd;
int randindex, tmp;
for (int i-0; i<52; i++) (
randindex « i + rnd.Random (52-i);
tmp « cards[ij;
cards[i] = cards{randindex];
cards [randlndex] = tmp;
}
currentcard «= 0; )
б)	В DealHand объявить локальный целочисленный массив размером 52. С помощью GetCard записать в массив п значений карт. Отсортировать массив, используя, например обменную сортировку, которая описана в гл. 2. Распечатать массив в цикле с помощью PrintCard.
3.9	Temperature Average(Temperatures a[], int n) <
float avgLow «0.0, avgHigh “0.0;
for (int i=0; i<n; i++) {
avgLow +•= a[i] .GetLowTempO;
avgHigh +- a (i ].GetHighTemp();
)
avgLow /- n;
avgHigh « /- n;
return Temperature(avgLow, avgHigh); )
3.11	RandomNumber rnd;
a)	int (rnd. f Random () <• 0.2) —
6)	int weight; weight « 140 + rnd.Random(91);
3.12
а)	Замечание. Статический член класса определяется вне класса, ио может быть доступен только для функций-членов класса. Все объекты разделяют значение статического элемента данных класса, ((include "random.h" class Event (
private:
int lowTime, highTime;
static RandomNumber rnd;
public:
Event (int low  0, .int high « 1): lowTime (low), highTime(high)
( if (lowTime > highTime)
•	"нижняя граница превышает верхнюю.”
« endl; exit (1);
) > int GetEvent(void) (return lowTime + rnd.Random(highTime-lowTime+1);) );
// rnd — статический элемент данных класса Event RandomNumber Event::rnd;
в) Event A[5] «= (Event(10,20), Event(10,20), Event(10,20), Event(10,20), Event(10,20)};
// использовать конструктор по умолчанию
Event В[5];	// lowTime = О; highTime — 1
б)	Determinate — (1) (-7) (-4) = 28
Глава 4
4.2
а) массив;
в)	стек;
д) множество;
ж) файл;
и) пирамида;
к) словарь.
4.4
б) п2 + 6п + 7 < п2 + п2 + п2 = Зп2, п > 6
г) П8 + П2 - 1П8 + П2 - 1	2	1^2 о 2 Ъ 1
----------<-----------= п2 + п---< п2 + n 2n2, п £ 1
п + 1	П	п
4.5
а)	п = 10 б) 2п + п® < 2п + 2п = 2 (2п), п > 10
4.7	К logsn < Кп, п > 1, поэтому этот алгоритм также имеет порядок О(п).
4.8
б)	О(п);	(
в)	О(п2).
4.11
а)	(3) п/2, т.е. О(п);
б)	(1) 1	со
4.14 Удаляет из списка максимальный элемент. L должен^ЛередаватьсЯ ссылке, поэтому внутренний программный стек обновляется.
Глава 5
5.3 <строка 1> 22 <строка 2> 9 <строка 3> 8 <строка 4> 18
5-4 void StackClear(Stacks S) (
while (!S.StackEmpty()) S.PopO;
}
5.6 Копирует стек Si в S2 с помощью промежуточного стека tmp.
5.7 int stacksize(Stack S) {
int size « 0;
// s передается по значению, программный стек не изменяется while (!S.StackEmpty()) { size++;
s.P^pl);
return size;
1
5.9 void Select Item (Stacks S, int n) {
Stack Q;
int i, foundn - 0;
while (IS.StackEmpty()) {
i - S.Pop();
if S.PopO;
{ foundn++; break;
}
Q.Push(i); } while (!Q.StackEmpty())
S.Push(Q.PopO);
if (foundn) S.Push(n);
)
5.10
б) а Ы- d e - /
5.11
6) a*(b + c)
5.14 <строка 1> 3 <строка 2> 18 <строка 3> 22 <строка 4> 9
5.15 Выстраивает очередь в обратном порядке. Переменная Q должна передаваться по ссылке, чтобы параметр менялся в процессе выполнения программы.
5.18 DataType PQueue::PQDelete(void)
(
DataType min;
int i, minindex - 0;
if (count > 0) (
min - pqlist[0];	// положить pqlist[0] минимальным
// обработать остальные элементы, обновляя значение и индекс // минимального элемента for (i-0; Kcount; 1++) if (pqlist[i] < min) // новый минимальный элемент равен pqlist(i) // новый индекс минимального элемента равен i { min " pqlist [i];
minindex - i;

.++
)
// сдвинуть влево pqlist[minindex+1]..pqlist[count-1] i - min index;
while (i < count-1)
( pqlistd] = pqlist[i+l] ; i++;
)
count—;	// уменьшить счетчик
)
// pqlist пуст; завершить программу else
I
cerr « "Попытка удаления из пустой приоритетной очереди!" « endl;
exit(1);
)
return min; // возвратить минимальное значение
Глава 6
ел
а)	Правило #1. Списки параметров не отличаются друг от друга. Функции 1 и 2 имеют одинаковые списки, и задаваемые по умолчанию параметры функции 3 ие перегружаются.
б)	Правило #1. Списки параметров различны. Перегрузка сделана правильно.
в)	Правило #2. Функции 1 и 2 допустимы, так как тип enum считается отличающимся от других типов. Однако typedef в функции 3 не оказывает никакого влияния. Компилятор полагает список параметров имеющим форму ”int& х" — такую же, что и для функции 1.
6.3 Максимум из а и b равен 99. Максимум из а, b и с равен 153. 1.0 + maxfhl, h2) — 1.05. Максимум из t, и и v равен 70000.
6.5 Обмен символьных строк языка C++.
void Swap (char *s, char *t)
// предполагается, что s и t содержат не более 79 символов {
char tmp[80];
strcpy(tmp, s);
strcpy(s, t);
strcpy(t, tmp);
}
6.7
a)	ModClass::Modeless (int v): .dataval (v % 7) {} Modeless ModClass::operator+ (const ModClassa x) { return ModClass(dataval+x.dataval); >
6)	ModClass;:operator* (const ModClass& x, const ModClass& 'y)
{ return ModClass(x.dataval * y.dataval); }
в)	ModClass inverse(const ModClass& x) —
ModClass prod value;
for (int i«0; i<7; i++)
{ value " ModClass(i);
prod • x * Modeless(i);
if (prod.GetValue() “ 1) break;
)
return value;
)
6.9 complex::Complex(double x, double y): real(x), imag(y) {} Complex complex::operator* (Complex x) const
{ return Complex(real+x.real, imag+x.imag); }
Complex Complex::operator/ (Complex x) const
{ double denom - x.real*x.real * x.imag*x.ircag;
return Complex((real*x.real + imag*x.imag)/denom, (imag*x.real - real*x.imag)/denom);
}
// вывод в формате (real, imag)
ostream& operator« (©streams ost, const Complexe x) (
ostr « ' (' « x.real « •	« x.imag «	;
return ostr;
}
6.П
a)	return ModClass(num/den);
6)	return Rational(dataval);
6.13
a)	Set::Set(int a[], int n) (
for (int i«0; i<SETSIZE; 1++) member(i) - FALSE;
for (i=0; i<n; i++) member(a[i]J = TRUE;
}
6)	int operator"4 (int n. Set x)
{ if (n<0 11 n>=SETSIZE)
{ cerr « "Оператор л: неверный операнд" « n « endl; exit(1);
} return (x.member[n]);
}
в)	(i) {1, 4, 8, 17, 25, 33, 53, 63}; (ii) {1,25}; (in) 0; (iv) 1
г)	<выход> <строка 1> {1,2,3,5,7,9,25} <строка 2> {2,3} <строка 3> 55 <строка 4> 55 is in A
Д) Set Set::operator* (Set x) const
(
int i;
Set tmp;
for (i=0; i<SETSIZE; i++)
tmp.member [ij = member[i] + x.member[ij;
return tmp;
J
void Set::insert(int n) {
if (n<0 || n>=SETSIZE)
{ cerr « "Insert: неверный параметр " « n « endl; exit(1);
}
member(n) = TRUE;
)
Глава 7
7.1
a)	template <class T> т Max(const T bx, const T fiy) { return (x<y) ? у : x; }
6)	char *Max(char* x, char* y)
{ return (strcmp(x,y) < О) ? у : x; }
7.3 template <class T>
int Max(T Arr(], int n) {
T currMax - Arr[0];	// подразумевается n>0
int currMaxIndex = 0; for (int i=0; i<n; i++)
if (curMax < Arr[i]) {
currMax = Arr[i);
CurrMaxIndex = i;
}
return currMaxIndex;
}
7.5 template <class T>
void Insertorder(T A[], int n, T elem) (
int i-0; // подразумевается n>0
while (i<n A[i]<elem) // найти позицию вставки i++;
if (i < n)
for (int j-n; j>i; j—) // сдвинуть хвост вправо A[j] - Alj-1);
A(i] = elem;
Глава 8
8.1
a)	0123412345
б)	Нет. Указателю p присваивается два разных адреса при обращении к новой функции. Чтобы р-10 указывал на начало первого списка десяти целых чисел, новая функция должна выделить память в последовательных блоках.
8.2
a)	int *рх = new int(5);	----.
б)	а = new longln];
в)	р = new DemoC; p->two = 500000; p->three - 3.14;
г)	p = new DemoD; p->one; p->two =35; p->three = 1.78;
strcpy (p->name, ’’Bob C++");
Д) delete px; delete (] a; delete p; delete p;
8.3
a)	Dynamiclnt::Dynamiclnt(int) {pn = new int(n);}
6)	Dynamiclnt::Dynamiclnt(const Dynamiclnt &x)
(pn = new int(*x.pn);}
в)	Dynamiclnt:;operator int(void) // возвратить целое значение (return *pn;}
г)	istreams operator» (isteram istr, Dynamiclnts x) {
istr » *(x.pn);
return istr;
)
8.4
a)	p = new DynamicInt(50);
6)	г — new Dynamiclnt[3]; // каждый элемент имеет нулевое значение
в)	for (int i=0; i<10; i++) a[i].SetVal(100); // или a[i] = DynamicInt(lOO); r) delete p; delete [] q;
8.8
a)	DynamicType<int> *p = new DynamicType<int> (5);
6)	cout « *p; // используется перегруженный оператор « cout « p->GetValue(); // используется фуикция-член класса cout « int(p);	// используется оператор преобразования в целое
в)	Выделяет память под массив 65-ти объектов типа DynamicType<char>. Каждый элемент этого массива равен NULL.
Выделяет память под один объект типа DynamicType<char> и присваивает ему значение ’А’.
г)	85;
Д) 85;
е) D D 68;
ж) delete р; delete с; delate Q; // ошибка: переменная Q не создавалась динамически
8.9
а)	Параметр х передается по значению, и, следовательно, будет вызываться конструктор копирования. Конструктор копирования постоянно вызывал бы самого себя в программе, выполняющей бесконечный цикл.
б)	Вы не могли бы иметь цепочку операторов присваивания С = В — А;
8.	11 Оператор ’+=’ прибавляет правую часть г к текущему объекту Rational, а затем присваивает результат этому же объекту. Текущий объект задается с помощью *this. Значение ”*this + г” присваивается текущему объекту (*this) и возвращается в точку вызова.
8.12
a)	ArrCL<int> А(20); ArrCL<char> В; ArrCL<float> С(25);
б)	Класс ArrCL выполняет проверку границ массива. А[80] выходит за границы.
в)	20-элементный массив агг содержит элементы 2, 4, 6, 8, ..., 40, сумма которых (20 * 42)/2 = 10 * 42 = 420. Обе функции вычисляют одно и то же значение.
8.13
a)	"Have а”;
б)	"nice day!";
в)	"Have a nice day!";
г)	"Have a nice day!"
8.14
a)	10;
б)	у;
в)	1;
г)	Индекс 24 находится вне диапазона.
д)	хуа52с;
ж) abcl2ABCxya52cba
8.16
a)	15;
б)	10;
в) 65520;
г) 1;
Д) 8
8.17	(1) соответствует функции three; (4) соответствует функции one
8.19	template cclass Т> Set<T> Set<T>::operator~ (void) const
Set<T> tmp (setrange) ;
for (int i=0; i<arraysize; i++)	// сформировать универсальное мно:
tmp.member [i] = ~tmp.member[i]; // присвоить каждому элементу tmp
// значение 0=111...Ill return tmp-*this; // возвратить разность между универсальным //и текущим множествами
8.20
a)	Set<T> UniversalSet(n); UniversalSet = -UniversalSet;
6)	template cclass T>
Set<T>Difference(const Set<T>& S, const Set<T>& T) {return S* ~T;}
Глава 9
9.1
a)	2 8;
6)	5 3;
в)	7 7;
r) 15 15;
Д) 17 17
9.3 Node<int> *head = NULL, *p;
for (int i=20; i>0; i—)
{ p - new Node<int>(i, head}; head = p;
)
p = head;
while (p != NULL)
(
cout « p->data « " “;
p = p->NextNode();
9.6
6) Следующий узел снова ссылается на р. в) Следующий узел ссылается сам на себя.
9.7
a) template <class т>
void InsertFront(Node<T> header, T item) {
Node<T> *p = new Node<T>(item);
header.InsertAfter(p);
9.9	Начиная с текущего узла, сканировать оставшуюся часть списка и прибавлять 7 к содержимому каждого узла.
9.10	Удаляет из списка первый узел и вставляет его в конец списка.
9.11	template <class Т>
int CountKey(Node<T> 'head, T key)
(
Node<T> *p » head;
int count • 0;
while (p 1- NULL)
<
if (p->data -- key) count++;
p  p->NextNode();
} return count;
)
9.14
a) 10 8 6 4 2;
6) 2 4 6 8 10;
в) 10 8 6 4 2;
r) 10 8 6 4 2
9.15
a) 60 70 80 90 100;
6) 20 40 60 80 100;
в) 20 10 30 40 50 60 70 80 90 100
9.17 void OddEveniLinkedList<int>& L, LinkedList<int>4 LI, LinkedList<int>s L2) (
L.Reset ();
while (!L.EndOfList())
(
if (L.Data() 12 — 1)
LI.InsertAfter(L.Data());
else
L2.insertAfter(L.Data()) ;
L.NextO;
) )
9.19 template <class T>
void DeleteRear(LinkedList<T>s L) {
L.Reset();
for (int i-0; i<L.ListSize () -1; i++)	*>
L.NextO г
L. DeleteAt О ;
)
9.23 Переставляет элементы связанного списка в обратном порядке, копируя их в промежуточный стек, а оттуда обратно в связанный список.
9.27 Каждая очередь имеет объект типа LinkedList, включенный посредством объединения. Когда объекты очереди присваиваются друг другу, вызывается перегруженный оператор из класса LinkedList и один список копируется в другой.
9.29 template <class Т>
void Insertorder(CNode<T> 'header, Cnode<T> *newNode) {
CNode<T> *curr - header->NextNode(), *prev = header;
while (curr != header && curr->data < newNode->data)
prev ~ curr;
curr •= curr->NextNode{); }
prev->InsertAfter(newNode); }
9.32 template <class T>
DNode<T> *DNode<T>::DeleteNodeRight(void) (
DNode<T> *tempPtr = next? // сохранить адрес узла if (next " this)
return NULL;	// указывает на самого себя; выйти!
// текущий узел указывает на преемника tempPtr
right = tempPtr->right;
// преемник tempPtr снова указывает на текущий узел tempPtr->right->left « this;
}
Глава 10
10.1 Результат зависит от того, в каком порядке компилятор вычисляет операнды. Если п=8 и левый операнд вычисляется первым, результатом будет 3*2! = 6. Если в первую очередь вычисляется правый операнд, в результате получается 2*2! =» 4.
10.2 1 1 5 18 41 121 365 1093 8281 984 ...
10.4	125
10.5	котсеркереп отЭ
10.7	float avg(float а(], int n)
(
if (n =- 1)
return a[0];
else return float (n-l)/n*avg(a, n-l) + a[n-l]/n;
)
10.8	int rstrlen(char *s)
( ------------------------------------------------------------
if (*s — 0)
return 0;
else
return l+rstrlen(s+l);
10.12 Решение: 1 2 6 7 11 12
9
10
Глава 11
11.2
а)	3;
б)	2
11.6	Да
11.7
а)	16;
б)	42;
в)	Прямое прохождение: 50 45 85 15 5 40 38 36 42 43 46 65 75 70 85
11.8	Вставляет узлы в бинарное дерево поиска. Передача указателя по ссылке позволяет изменять корень и поля указателей узлов.
11.9
а)	Прямое прохождение: М F Т N V U
6)	Обратное прохождение: A D I R О L F 11.10
а)	RNL-прохождение: V U Т N М F
б)	RLN-прохождениё: R О I L A D F
в)	Поперечное прохождение: ROTARYCUBL 11.14
a)ACFEIHBDG
11.20	template <ciass Т>
void PostOrder_Right (TreeNode<T> *t, void visit(T& item)) <
if (t !- NULL) {
// рекурсивное прохождение завершается на пустом поддереве PostOrderRight(t->Right()г visit); // пройти правое поддерево Postorder_Right(t->Left(), visit); Ц пройти левое поддерево visit(t->data);	// обработать узел
) }
11.22	template <class T>
TreeNode<T> *Max(TreeNode<T> *t) (
while (t->Right() ! = NULL)
t - t->Right();
return t; )
11.26	template <class T>
int NodeLevel(TreeNode<T> *t, const T& elem) {
int level - -1;
while (t !- NULL) <
level++;
if (t->data “ elem) break;
else if (elem < t-> data)
t - t->Left();
else
t - t->Right(); )
if (t NULL) level “ -1; return level;
Глава 12
12.2
	Base_Priv	Base_Prot	Base_Pub	Derived—Priv	Derived_Pfot	Derived Pub
BASE	X	X	X			
DERIVED		X	X	X	X	X
КЛИЕНТ			-			X
12.8
а) Конструктор производного класса не вызывает конструктор базового класса.
12.4
a)	DerivedCL::DerivedCL(int а» int b, int c): data8(a), BaseCL(b,c){}
6)	DerivedCL: :DerivedCL(int a): data3(a), BaseCL() {}
в)	datal data2 data3
objl	2	0	1
obj2	4	5	8
objS	0	0	8
12.5 Вызван конструктор Basel.
Вызван конструктор Base2. Вызван конструктор Derived. Вызван конструктор Basel.
Вызван конструктор Base2.
Вызван деструктор Вазе2.
Вызван деструктор Basel. Вызван деструктор Derived.	-—-
Вызван деструктор Base2. Вызван деструктор Basel.
12.7
a)	GetX — открытый метод класса Shape.
б)	Так как х является защищенным элементом данных в классе Shape, к нему можно обращаться из производных классов, но не из программы-клиента.
12.8 «прока 1> 1 «прока 2> Base Class «трока 3> 2 «^трока 4> 1 «трока 5> 0 «трока 6> Base
12.11 «трока 1> 7 «трока 2> 2 «трока 3> 8 5 «прока 4> 2 4 «трока 5> 2 4 «трока 6> 0 1 «трока 7> 7 «трока 8> 2 8
12.13 template <class Т> class StackBase { protected:
int numElemencs;
public: StackBase(void): numElements(0) { } virtual void Push (const T& itexn) = 0;
virtual T Pop(void) - 0; virtual T Peek(void) - 0/ virtual int StackEmpty(void) { return numElements •== 0 ) );
Реализуйте производный класс Stack с помощью массива (гл. 5) или связанного списка (гл. 9).
12.19 int LookForMatch (Array<int>& A, int end, int elem) (
Arraylterator«int> alter(A, 0, end-1); while (1aiter.EndOfList()) < if (alter.Data() == elem) return 1;
alter.Next(); ) return 0;
}
void RemoveDuplicates(Array<int>& A) {
ArrayIterator<int> assign(A), inarch(A); int assignindex; if (A.ListSize() <= 1) return;
assign.Next();
inarch. Next {);
assignindex =1; while (march.EndOfList()) (
if (1LookForMatch(A, assignindex, inarch.Data())) ( assign.Data() - march.Data90;
assign.Next О; as signlndex++; ) march.Next();
}
A. Resize(assignindex); )
12.20 template «class T> int Max(Iterator<T>& colllter, T& maxval) // перейти к первому элементу (
colllter.Reset();
// если это конец списка, то список пуст if (colllter.EndOfList()) return 0;
11 взять первый элемент списка и начать сравнения maxval * colliter.Data();
for(colllter.Next(); !colllter.EndOfList(); colllter.Next()) if (colllter.Data() > maxval) maxval - colllter.Data();
return 1;	/ / успешный выход
Глава 13
13.1 Дерево (В): 60 80 80 65 40 5 50 10 90 15 70
13.3 template «class Т>
void Freorder (T A[), int currindex, int n, void visit(T& item) <
if (currindex < n) {
visit (A[currindex]);	// обработать узел
Preorder(A, 2*currindex+l, n, visit); // обход левого поддерева
Preorder(A, 2*curxindex+2, n, visit); // обход правого поддерева
13.4
а) Да;
в) А[24];
д) Да А[84]
13.6 Последний уровень имеет 2“ элементов. Число нелистовых узлов оапьп
1 + 2 + 4 + ... + 2"1 - 2*п - 1.	Р Но
13.7
а) 5;
г) 41 и 42;
е) 15—80
13.10 Дерево (Ь) является минимальной пирамидой, а дерево (f) — максимальной.
13.13 Начальное состояние пирамиды А: 5 10 20 25 50
Вставить 15: 5 10 15 25 50 20
Вставить 35: 10 15 25 50 20 35
Удалить 5: 10 25 15 85 50 20
Вставить 40: 10 25 15 85 50 20 40
Вставить 10: 10 10 15 25 50 20 40 85
13.15
а) 47 45 40 10;
в) 85 40 45
13.16
б) 8, 6, 88, 88, 16, 45, 45, 90;
в) "aehpify"
13.21 Для графа (В)
Матрица смежности	Представление в виде списков смежности
ABCDE	A: CD
А00110	В: CD
В 0 0 1 1 0	С:
С 0 0 0 0 0	D:
DOOOOO	Е: А С D
Е 1 0 1 1 0
13.22
6) Пути нет;
г) Из А существует путь в С и D.
13.23
б) Прохождение "сначала в глубину": А Е D С; прохождение "сначаЛа в ширину** : А С D Е.
13.26 Проходит граф методом "сначала в ширину*', распечатывая попутно каждую вершину.
Глава 14
14.1
а) Проход 0: 14892174
Проход 1: 11892474
Проход 2: 11298474
Проход 3: 11248974
Проход 4: 11244978
Проход 5: 11244798
Проход 6: 11244789
14.2 Проход 0: 48192174
Проход 1: 14892174
Проход 2: 14892174
Проход 3: 12489174
Проход 4: 11248974
Проход 5: 11247894
Проход 6: 11244789
14.7 Когда список уже отсортирован по возрастанию или по убыванию, алгоритм имеет порядок О(п2).
14.13 Если г — х % m и q — частное от х/m, то х mq + г и г — х — mq. Поскольку m четно, то при четном х значение хеш-функции четно. Все четные ключи хешируются в четные табличные индексы. Хеш-коды недостаточно рассеяны по таблице.
14.14
012	3	45	6789	10
391	Пусто	Пусто	257	567	575	987	189	122	734	313
14.19
б) hashf(16) _ 1; hashf(257) - 16
14.21 Если хеш-функция хорошая, вероятность хеширования в ячейк - -равна 1/N. Когда ключ занял i-ю ячейку таблицы, вероятность задо 1 нения ячейки i+1 равна 2/N. Вероятность заполнения ячейки i+o равна 3/N. Скопление происходит из-за того, что вероятность попал ния в группу смежных ячеек становится больше, чем вероятност? попадания в одиночную ячейку таблицы.
14.22
а)	Покажем, что если хеширование происходит в индекс i, то последе нательное рехеширование снова приводит нас к этому индексу. Пусть i = (i+3k) % 93 для некоторого к. Это значит, что i+3k — 93q+i ддя некоторого q и, следовательно, 3k e 93q. Минимальным решением этого уравнения являются к=31 и q=l. После 31-й итерации функция рехеширования снова возвращается к i, т.е. покрывает только 1/3 таблицы.
б)	Если d < ш, то d и тп — взаимно простые н вся таблица покрывается функцией рехеширования.
14.24
012345678	9	Ю
38 8
3 13
-I 12 4 34 6 |38 7
3 -1 27 -1 22 -1 пусто -1 пусто ~Т|
14.25
а)	Да;
б)	int H(char *s) {return (s[2] - ’a’) % 7;}
14.26 void PascalTextFile::Assign(char *filename) {strcpy(fname, filename);)
void PascalTextFile::Reset(void); <
if (isOpen) f.seek(0, ios::beg);
else
{
accesstype - IN;
f.open(fname, ios::in | ios::nocreate);
if (f !- NULL) isOpen++;
else
Error(•'Невозможно открыть файл");
) ) void PascalTextFile::PWrite(char A[], int n) {
if (accesstype " IN)
Error ("Недопустимая операция доступа к файлу");
if (IisOpen)
Error ("Файл закрыт");
f.write(A,n);
1
1.	Адельсон-Вельский Г. М., Ландис Е. М. Один алгоритм организации информации. — Доклады АН СССР. Серия математическая, т. 146, 1962, N 2 — с. 263—266.
2.	Aho А. V., Hopcroft J. Е., Ullman J. D. Data structures and algorithms. — Reading, MA: Addison-Wesley, 1988.
3.	Basse S. Computer algorithms (2nd ed.). — Reading, MA: Addison-Wesley, 1988.
4.	Bar-David T. Object oriented design for C++. — Englewood Cliffs, NJ: Prentice Hall, 1993.
5.	Booch G. Object oriented design. — Redwood City, CA: Benjamin/Cum-, mings, 1991.
6.	Budd T. A. Classical data structures in C++. — Reading, MA: Addison-Wesley, 1994.
7.	Carrano F. M. Data abstraction and problem solving with C++, walls and mirrors — Redwood City, CA: Benjamin/Cummings, 1995.
8.	Collins W. J. Data stuctures, an object-oriented approach. — Reading, MA: Addison-Wesley, 1992.
9.	Dale N., Lilly S. C. Pascal plus data structure, algorithms and advanced programming (3rd ed.). — Lexington, MA: D. C. Heath, 1991.
10.	Decker R., Hirshfield S. Working classes, data structures and algorithms using C++. — Boston, MA: PWS, 1996.
11.	Ellis M. A., Stroustrup B. The annotated C++ reference manual. — Reading, MA: Addison-Wesley, 1992.
12.	Flaming B. Practical data structures in C++. — New York: Wiley, 1993.
13.	Flaming B. Turbo C++: Step-by-step. — New York: Wiley, 1993.
14.	Headington M. R., Riley D. D. Data abstraction and structures using C++. — Lexington, MA: D. C. Heath, 1994.
15.	Horowitz E., Sahni S., Mehta D. Fundamentals of data structures in C++. — New York. W. H. Freeman, 1995.
16.	Horstman C. S. Mastering object-oriented design in C++. — New York: Wiley, 1995.
17.	Knuth D. E. The art of computer programming, vol. 1: Fundamental algorithms (2nd ed.). — Reading, MA: Addison-Wesley, 1973.
18.	Knuth D. E. The art of computer programming, vol. 2: Seminumerical algorithms (2nd ed.). — Reading, MA: Addison-Wesley, 1973.
19.	Knuth D. E. The art of computer programming, vol. 3: Sorting and searching (2nd ed.). — Reading, MA: Addison-Wesley, 1973.
20.	Kruse R. L. Data structures and program design. — Englewood Cliffs, NJ: Prentice Hall, 1994.
21.	Lewis T. G. Smith M. Z. Applying data structures (2nd ed.). — Boston: Houghton-Miff in, 1982.
22.	Martin R. Designing object-oriented C++ applications using the Booch method. — Englewood Cliffs, NJ: Prentice Hall, 1995.
23.	Model M. Data structures, data abstraction. — Englewood Cliffs \tt Prentice Hall, 1994.	’
24.	Murray R. B. C++ strategies and tactics. — Reading, MA: Addi« Wesley, 1998.	.	n'
25.	Naps T. L. Introduction to data structures and algorithm analysis — Paul, MN: West, 1992.	‘	k
26.	Pohl I. Object-oriented programming in C++. — Redwood City, CA: Ben jamin/Cummings, 1993.
27.	Pothering G. J., Naps T. L. Introduction to data structures and algorithm analysis with C++. — St. Paul, MN: West, 1995.
28.	Schildt H. C++: The complete reference. — Berkeley, CA: Osborne McGraw-Hill, 1991.
29.	Sedgewich R. Algorithms in C++. — Reading, MA: Addison-Wesley, 1992
30.	Standish T. A. Data structures, algorithms, and software principles. -Reading, MA: Addison-Wesley, 1994.
31.	Stroustrup B. The C++ programming language (2nd ed.). — Reading, MA: Addison-Wesley, 1991.
32.	Stubbs D. F., Webre N. W. Data structures with abstract data types and C. — Pacific Grove, CA: Brooks/Cole, 1989.
33.	Tenenbaum A. М.» Langsam Y., Augenstein M. J. Data structures using C. — Englewood Cliffs, NJ: Prentice Hall, 1990.
34.	Weiss M. A. Data structures and algorithms analysis in C++. — Redwood City, CA: Benjamin/Cummings, 1994.
35.	Winder R. Developing C++ software. — New York: Wiley, 1991.
36.	Wirth N. Algorithms + data structures = programs. — Englewood Cliffs, NJ: Prentice Hall, 1976.
37.	Wirth N. Algorithms and data structures. — Englewood Cliffs, NJ: Prentice Hall, 1986.
Предметный указатель
абстрактный базовый класс - класс
-	списковый класс
-	тип данных
абстракция элемента управления
адрес, память
активизирующая ЗАПИСЬ алгебраическое выражение —- инфиксное - - постфиксное —- префиксное алгоритм Уоршалла алгоритмы деревьев ---вычисление глубины ---вычисление количества листовых узлов
— горизонтальная печать дерева
---копирование дерева
---обратное сканирование
— поперечное сканирование
— по уровневое сканирование
---симметричное сканирование
---удаление дерева алгоритмы сортировки ---"быстрая"
---treesort-сортировка ---вставками
—	двусвязного списка
—	методом пузырька
--- обменная
—	поразрядная
---сортировка посредством выбора ---со связанными списками
--- турнирная
анализ наилучшего случая
-	наихудшего случая (алгоритма)
-	сложности (алгоритма)
---алгоритм Уоршалла
---поиск "сначала в ширину"
---сравнение О(п 1ов2п)-сортировок
---"быстрая сортировка"
---AVL-дерево
---алгоритм сопоставления с образцом
•--бинарное дерево поиска
--- бинарный поиск
•	— законченное бинарное дерево
—	обменная сортировка
-	— операции с очередью
—	операции с очередью приоритетов
abstract base class	48,559
- class	48,541,56
- list class	560-563
- data type	2
control abstraction	540
address, memory	57
activation record	443
algebraic expression	193
	 infix	193
	postfix	193
	prefix	587
Warshall algorithm	666
tree algorithms	489-503
	computing the depth	492
— counting leaf nodes	492
— horizontal tree printing	493
---copying a tree	495
---postorder scan	490
- - breadth-first csan	500
---level-order scan.	500
— inorder scan	489
---deleting a tree	498-499
sorting algorithms
---quiksort	690
---treesort	646
---insertion sort	688
---doubly linked list sort	408
— bubble sort	686
—	- exchange sort	85
---radix sort	209
—	selection sort	684
—	- linked list sort	369
—	tournament sort	602
best case analysis	157
worst case analysis	157
complexity analysis	155-159
—	Warshall algorithm	667
—	breadth-first search	659
— compare O(n logan) sorts	697
---quicksort	695
- - AVL tree	628
---pattern matching algorithm	325
— binary search tree	504
----- search	166
---complete binary tree	482
— exchange sort	155
---queue operations	206
- — priority queue operations	217
—	- операции со стеком
---пирамидальная сортировка
—	поиск сначала в глубину-
---поразрядная сортировка — последовательный поиск ---сортировка включением в дерево ---сортировка вставками ---сортировка методом пузырька — сортировка посредством выбора — сортировка прямым слиянием ---сортировка со связанными списками — сравнение О(п2)-сортировок ---турнирная сортировка ---хеширование ---числа Фибоначчи ассоциативность операций ассоциативные массивы
Б
базовый класс байт
бинарное дерево
---вертикальная печать
---вырожденное
-— горизонтальная печать
--- законченное
---класс TreeNode
— копирование дерева
---левый-правый сын
---описание
---определение
--- полное
---поперечное сканирование
---построение
---по уровневое сканирование
— симметричный метод прохождения
---(порядок) ---структура узла ---удаление дерева бинарные деревья, представляемые
массивами
бинарный оператор - поиск
— неформальный анализ
---рекурсивная форма
— сравнение последовательного и бинарного методов
---формальный анализ биномиальные коэффициенты бит
битовые операции
—-и
— исключающее или
---не
— или
блок (метод цепочек при хешировании) буферизация печати
быстрая сортировка
-— stack operations	189
- — heapsort	619
— depth-first search	659 .
— radix sort	212
— sequential search	161
	 tree sort	646
— isertion sort	689
— bubble sort	688
- — selection sort	686
	straight merge sort	728
	linked list sort	370
— compare O(n2) sorts	696
— tounament sort	604
— hashing	714
— Fibonacci numbers	466-468
associativity of operators	278
associative arrays	151
base class	81
byte	57
binary tree	479-482
— upright (vertical) tree printing	500
	degenerate	481
	horizontal tree printing	493
-— complete	482
	TreeNode class	483
— copy a tree	495
	left-right child	481
— description	479
- - definition	479
- - full	482
	breadth first scan	500
	building	485
— level scan	500
— inorder traversal	489
- - LNR, LRN, etc.	489
— node structure	483
— deleting a tree	498
array-based binary trees	600-602
binary operator	193
- search	503
	informal analysis	166
— recursive form	443-445
	compare sequential	164
— formal analysis	166
Binomial coefficients	472
bit	57
bit operations	327-328
	and	327
	exclusive or	327
	not	327
	or	327
bucket	706
print spooler	394-400
quicksort	690
в
верхняя треугольная матрица	upper triangular matrix	120
вершина графа	vertex of graph	647
— стека	top of stack	182
вещественное число	real number	
- - АВТ	- - ADT	60
	мантисса		mantissa	60
	научный формат		scientific notation	60
	определение		definition	60
	порядок (экспонента)	— exponent	60
	представление		representation	60
вещественные типы данных	- data types	60
взвешенный орграф	weighted digraph	649
виртуальная функция	virtual function	550-552
	 деструктор		destructor	558
	и полиморфизм		and polymorphism	550
	 описание		description	49-50,541
	 таблица		table	552
	 чистая		pure	541, 559
внешние структуры данных	external data structures	77
внешний файловый поиск	- file search	723
внутренние структуры данных	internal data structures	77
возврат (прохождение лабиринта)	backtracking	436
возможности программного	program design features	
конструирования		
	объектная разработка		object design	38
	сквозной стуктуированный		structured walkthrough	45
контроль		
	структурное дерево		structure tree	39
	тестирование объектов		object testing	40,45
	устойчивость к ошибкам		robustness	46
вращение AVL-дерева	rotation in AVL tree	
~ - двойное		double	639
— единичное		single	638
входной приоритет	input precedence	280
вызов по значению	call by value	115
- по ссылке	- reference	115
выражение	expression	
- вычисление (оценка)	- evaluation	193
- деревья	- trees	438
г
глубина дерева	depth of a tree	480
голова связанного списка	head of a linked list	353, 358
граф	graph	647
- ADT	- ADT	649
ациклический	- acyclic	649
" вершины	— vertices	647
- взвешенный орграф	- weighted digraph	649
~ матрица достижимости	- reachability matrix	666
~ - смежности	- adjacency matrix	650
" направленный (орграф)	- directed (digraph)	648
~ ненаправленный	- undirected	648
" приложение: сильные компоненты	- application: strong components	659
" путь	- path	648
~ ребра	— edges	647
связанные вершины	- connected vertices	648
t z о		
		
- сильно связанный	- strongly connected	648
- сильные компоненты	— strong components	659
- слабо связанный	- weakly connected	648
- транзитивное замыкание	- transitive closure	667-
- цикл	- cycle	649
группа	group	153
д		1 i
двоичные числе	binary numbers	56 -
двоичный файл	— file	715
двумерный массив	two-dimensional array	
	определение		definition	68
— хранение	— storage	69
двусвязный список	doubly linked list	410
	приложение: сортировка вставками 	application: insertion sort		406
дерево	tree	479-480
- бинарного поиска	binary search tree	503-507
	ADT		ADT	508
	вставка узла		inserting a node	617
—— класс BinStree		BinStree class	608
	ключ		key	505
	описание		description	503
	~ приложение: конкорданс		application: concordance	525
		 симметричное прохождение		inorder traversal (sort)	511
(сортировка)		
	счетчики появлений		application: occurrence counts	513
	удаление узла		deleting a node	519
- бинарное дерево	- binary tree	480
- высота, см. глубина	- height, see depth	
- глубина	- depth	480
- корень	- root	479
- левый сын	- left child	481
- лист	- leaf	479
- описание	- description	479
- определение	- definition	479
- поддерево	- subtree	479
- правый сын	— right child	479
- предки-потомки	~ ancestors-descendents	479
- путь	— path	_ 479
- сын-родитель	- children-parent	z	479
- терминология	- terminology	479
- тип коллекции	- collection type	152
- узел	- node	483
- уровень	- level	480
деструктор	destructor	296-296, 291
динамический	dynamic	408
- выделение массива	— array allocation	292
- массив	- array	147
- объект	- object	293-29?
- память	- memory	64
- связывание	— binding	49
- структуры данных	— data structures	291
дискретный тип	discrete type	60
длинная последовательность '	long run	577
	сортировка слиянием		merge sort	729
доступ, см. прямой доступ,	access, see direct access, sequential	
последовательный доступ	access	244-245
дружественные функции	friend functions	
		j
3
заголовочный узел	header node	401
задача Джозефуса	Josephus problem	403
- о комитетах	committee problem	448-450
задняя рекурсия	tail recursion	46S
закрытое наследование	private inheritance	687
запись (как набор данных)	record	
	ADT	-ADT	77
	определение	- definition	76
защищенные методы	protected members	644
знаковый бит	sign bit	57
И изменение состояния	state change	25
индекс, массив	index, array	66
инициализатор, см. конструктор	initializer, see constructor	
инкапсуляция	encapsulation	24
инфиксный	infix	
- вычисление выражения	- expression evaluation	277-286
- формат	- notation	163
итератор	iterator	563
итераторы дерева	tree iterators	642
К каркас разработки	design framework	89
квадратичное время (О(п2))	quadratic time (O(n2))	160
квадратная матрица	square matrix	120
класс	class	
- (в книге): Queue (связанный список)	- (in book): Queue (linked list)	388
— - Animal		Animal	553-555
	Array		Array	803
	Calculator		Calculator	. 195
	Circle (производный)		Circle (derived)	548
	CNode		CNode	401
— — Date		Date	118
	Dice		Dice	40
	DNode	— - DNode	410
	DynamicClass		DynamicClass	293
	Event		Event	223
	Heap	— - Heap	612
	Iterator (абстрактный)		Iterator (abstract class)	564
	Line		Line	30
	LinkedList		LinkedList	374
~ — Liat (абстрактный)		List (abstract)	660
	MathOperator		MathOperator	281
	Maze		Maze	462
- - - Node		Node	353
~	NodeShape		NodeShape	582
- - — OrderedList		OrderedList	36
- - - Point		Point	29
	PQueue		PQueue	214
"	RandomNumber		RandomNumber	111-112
- — Rational		Rational	247
~	Rectangle		Rectangle	101
~	SeqList		SeqList	36,168
~ - SeqList (производный)		SeqList (derlvde)	561
"	SeqList (связанный список)		SeqList (linked list)	391
	 Shape		Shape	546
		Simulation		Simulation	225
	Spooler (для печати)		Spooler (print)	396
	Stack		Stack	184
	Stack (шаблонный)		Stack (template)	276
	String		String	310
	Temperature		Temperature	108
	TriMat		TriMat	12-129
	Vec2d		Vec2d	243
	Window		Window	412
- Array	Array class	
	деструктор		destructor	305
	конструктор		constructor	305
	конструктор копирования		copy constructor	305-306
	метод Resize		Resize method	308-309
	 объявление		declaration	303
	оператор индексации	- — index operator	306
	преобразования указателя		pointer conversion operator	307-309
- Arrayiterator	Arrayiterator class	574
- AVLTree	AVLTree class	631-641
	метод UpdateLeftTree		UpdateLeftTree method	637
	AVLInsert		Avllnsert method	636
	DoubleRotation		DoubleRotation method	639
	GetAVLTreeNode		GetAVLTreeNode method	633
	Insert		Insert method	634
	объявление	— declaration	631
- AVLTreeNode	AVLTreeNode class	629-631
	конструктор		constructor	631
— метод Left		Left method	631
	объявление		declaration	629
	реализация		implementation	631
- Binfile	BinFile class	718
	конструктор		constructor	721
	метод Clear		Clear method	719
	EndFile	— EndFile method	719
	Peek		Peek method	719
	Read		Read method	721
	(блочный)		(block) method	721
	Write		Write method	722
	(блочный)		(block) method	.722
	 объявление		declaration	718
	реализация	— implementation	721
- BinSTree	BinSTree class	508-51U. 515-52
	конструктор		constructor	515
	метод Delete	— Delete method	523
	Find		Find method	516
	FindNode	— FindNode method	516
	Insert	— Insert method	517
	Update	— Update method	524
	объявление		declaration	510
- - оператор присваивания		assignment operator	515
	реализация		implementation	51Э
	управление память^		memory management	515
- Calculator	Calculator class	202
	метод Run		Run method	197
	Compute	— Compute method	196
	 объявление		declaration	19b
	реализация		implementation	196
~ Circle ---объявление —• описание — реализация - CNode
-	- DeleteAf ter метод — InsertAfter метод — конструктор - - объявление — реализация
-	Date
-	Dice
-	Dictionary
* — конструктор — метод DeleteKey -------InDictionary — объявление — реализация - DNode
---метод DeleteNode
— реализация — конструктор — метод InaertLeft -----InsertRight
-	DynamicClass — деструктор — конструктор — конструктор копирования — оператор присваивания
-	Event
-	Graph
— конструктор
---метод Delete Vertex
-----GetNeighbors
-----GetVertexPos
-----GetWeight
-----InsertEdge
-----Vertexiterator
— минимальный путь — объявление
— прохождение "сначала в глубину" -----"сначала в ширину" (метод)
— реализация - HashTable — метод Find --- Insert — объявление — реализация - HashTablelterator ---конструктор — метод Next
-----SearchNextNode — объявление — реализация - Heap
— конструктор
— метод Delete
-----FilterDown
-----FilterUp
Circle class	548
---declaration-------------------------648 ---------------------------------------description	648 —- implementation	548
CNode class — DeleteAfter method	403
----InsertAfter method	403 - ~ constructor	402
—	declaration	401
—	implementation	402
Date class	118
- class	41
Dictionary class	737
----constructor	741 — DeleteKey method	742
----InDictionary method	742 — declaration	737
----implementation	741
DNode class	407
— DeleteNode method	411
---implementation----------------------410 ---------------------------------------constructor	410 — InsertLeft method	411
----Insert Right method	411
DynamicClass class — destructor	205
— constructor	293-294
---copy constructor--------------------300 ---------------------------------------assignment operator	297 Event class	228
Graph class	652
—	constructor	653
—	DeleteVertex method	665
—	GetNeighbors method	654
—	GetVertexPoe method	654
---GetWeight method	654 ---------------------------------------------------InsertEdge method----------------------------------654 ---------------------------------------------------Vertexiterator class	654 — minimum path	661
—	declaration	652
-	— depth-first graph traversal	656
—	— breadth-first traversal	656
—	implementation	653
HashTable class	707
—	Find method	712
— Insert method	711
----declaration	707 - - implementation	711
HashTablelterator class	711
~ — constructor	713
—	Next method	714
—	SearchNextNode method	713
—	declaration	708
----implementation	712
Heap class	609
—	constructor	618
—	- Delete method	615
—	FilterDown method	615
—	FilterUp method	613
	Insert		Insert method	614
	объявление		declaration	609 .
	пирамидальная сортировка		heapsort	618
	реализация		implementation	-612
- ifstream	ifstream class	80
- Inorderiterator	Inorderiterator class	
	конструктор		constructor	644
	метод Next	— Next method	645
	объявление		declaration	643
	реализация		implamentation	<544
- Ios	Ios class	80
- Istream	Istream class	80
- Istrtream	Istrtream class	81
- KeyValue	KeyValue class	736
- Line	Line class	30
- LinkedList	LinkedList class	371-376,
		381-38:
	конструктор		constructor	382
— метод InsertAt		InsertAt method	385
	ClearList		ClearList method	383
	CopyList		CopyList method	383
	Data		Data method	385
	DeleteAt		DeleteAt method	387
	Next		Next method	384
	Reset	— Reset method	384
	методы выделения памяти		memory allocation methods	382
	объявление		declaration	374
	описание операций		describing operations	372-374
	приложение: конкатенированные		application: concatenating lists	377
списки		
	сортировка выбором		selection sort	378
- — - удаление дубликатов		removing duplicates	379
	проектирование списка		designing the class	371
	реализация		implementation	381
	указатель на первый узел		front pointer	371
	на последний узел		rear pointer	371
	на предыдущий узел		previous pointer (prevPtr)	371
	на текущий узел		current pointer (currPtr)	371
- List (абстрактный)	List class (abstract)	560-Б63
	 объявление		declaration	561
	реализация		implementation	561
- MathOperator	MathOperator class				
	конструктор		constructor	281
	метод Evaluate		Evaluate method	282
— объявление		declaration	281
	оператор сравнения		comparison operator	282
— Maze	Maze class	
	объявление		declaration	462
	реализация		imlementation	463
- Node	Node class	353-350
	конструктор		constructor	356
— метод DeleteAfter		DeleteAfter method	357
	InsertAfter	— InsertAfter method	857
	NextNode		NextNode method	856
	объявление		declaration	855
	реализация		implementation	856
- NodeShape	NodeShape class	582
- ofstream	ofstream class	80
- OperendList	OperandList class	85
	метод Insert	-— Insert method	576
	объявление		declaration	576
— — реализация		implementation	576
- Ostream	Ostream class	80
- Ostrstream	Ostrstream class	81
- Point	Point class	29-30
— Pqueue	PQueue class	214-217
- - метод Pqdeiete		Pqdeiete	216
	Pqinsert	— Pqinsert	215
— объявление	— declaration	214
	(пирамидальная версия)		(heap version)	622
	реализация	— implementation	215
- Queue (массив)	Queue class (array)	199
	конструктор		constructor	204
—	метод Qdelete	•	Qdelete method	206
	Qinsert		Qinsert method	205
		 объявление		declaration	201
		 реализация		implementation	202
— (связанный список)	— (linked linked)	389-392
		объявление		declaration	389
	реализация		implementation	390
- RandomNumber	RandomNumber class	110-114
	конструктор		constructor	111-112
	метод fRandom		fRandom method	112
	Random		Random method	112
— объявление	- - declaration	110
— реализация		implementation	112
- Rational	Rational class	247-258
	метод Reduce		Reduce method	255
— объявление		declaration	247
	операторы (как дружественные)		operators (as friends)	247
	(как члены)		(as members)	249
	(преобразование типа)	—- - (type conversion)	252
	потоковые операторы		stream operators	248
- Rectangle	Rectangle class	101-107
- SeqList (массив)	SeqList class (array)	168-175
	метод Delete			Delete method	170
	Find		Find method	171
		 — GetData		GetData method	170
	Insert		Insert method	169
	объявление		declaration	168
	реализация	—— implementation	168
	(производный)	— (derived)	561-563
	объявление		declaration	562
	реализация		implementation	562
— (связанный список)	— (linked list)	391-392
	 объявление		declaration	391
	приложение: сравнение		application: efficiency	392-394
эффективности	comparison	
	реализация		implementation	392
- SeqLietlterator	SeqListlterator class	
— объявление		declaration	565
— реализация		implementation	565
~ Shape	Shape class	
— объявление		declaration	547
— описание	— description	546
- - реализация		implementation	547
~ Simulation	Simulation class	
— конструктор		constructor	226
	метод NextArrvalTime 	объявление - Spooler (печать)		NextArrvalTime method 	declaration Spooler (print) class	227 22Б
	объявление		declaration	396
	реализация		implementation	397
- Stack	Stack class	184-189
	 конструктор	— constructor	187
	метод ClearStack		ClearStack method	188
	Peek		Peek method	188
	Pop		Pop method	187
	Push		Push method	187
	StackEmpty		StackEmpty method	188
	StackFull		StackFull method	188
	объявление		declaration	186
	реализация		implementation	187
- String	String class	310-320
	ввод/вывод	- - I/O	31»
	FindLast	— FindLast	31»
	конкатенация		concatenation	814
	 конструктор		constructor	315
	метод ReadString		ReadString method	31»
	Substr		Substr method	318
	объявление		declaration	311
	оператор присваивания		assignment operator	316
	приложение: тестовая программа		application: teat program	314
	реализация		implementation	315
	сравнение строк		string comparison	316
- Temperature	Temperature class	108
- TreeNode	TreeNode class	484
	конструктор	- - constructor	485
	метод FreeTreeNode		FreeTreeNode method	486
	GetTreeNode		GetTreeNode method	486
	 объявление	- — declaration	484
-TriMat	TriMat class	124-129
- Vec2d	Vec2d class	243
	объявление		declaration	244
— операторы (как дружественные)		operators (as friends)	243
	(как члены класса)		(as members)	262
	скалярное произведение - Window 	объявление		scalar multiplication Window class 	declaration	413
	реализация		implementation	415
- заголовок	class: head	100
- закрытая часть	- private part	100
- защищенная часть	- protected part	100
- иерархия	- hierarchy	542
- инкапсуляция	- encapsulation	24
- конструктор	- constuctor	101 24,100
— методы	— methods	
— наследование	— inheritance	81
- объект	- object	100
- объявление	- declaration	25
- оператор разрешения области действия	— scope resolution operator	103
- открытая часть	— puplic part — imlementation	100 25,102
- реализация		
- скрытие информации	— information hiding	24 103 24.100
- список инициализации	— member initialization list	
- члена класса	- members	
- тело	— body	100
классы животных	animal classes	563-665
- итераторов	Iterator classes	569
	Arrayiterator	— Arrayiterator	
- - Inorderiterator		Inorderiterator	643
-— SeqListlterator		SeqListlterator	565
	Vertexiterator		Vertexiterator	652
КЛЮЧ	key	82
коллизия	collision	
— определение	- definition	704
- разрешение	- resolution	704
комбинаторика	combinatorics	437
композиция объектов	composition of objects	28 30
компонента постусловий ADT	ADT Postconditions component	21
- предусловий ADT	- Preconditions component	21
- процесса ADT	— Process component	21
конкатенация списков	concatenating lists	377
конкорданс	concordance	525-529
конструирование функций узлов дерева	tree node function desing	486
конструктор	constructor	21
- копирования	copy constructor	291.300-302
- умолчания	default constructor	117
корень дерева	root of tree	152.479
косвенная рекурсия	inderact recursion	434
коэффициент заполнения	load factor	714
кубическое время (О(п8))	cubic time (O(n3))	160
Л		
линейная коллекция	linear collection	144-152
линейное время	- time (o(n))	160
линейный	—	
- последовательный список	- sequential list	148
листовой узел	leaf node	479
логарифмическое время (O(log2n), O(nlog2n))	logarithmic time (O(iogan), Ofnlogan))	160
м		
максимальная пирамида	maximum heap	608
массив: ADT	Array ADT	65
- границы	- index bounds	67
- дескриптор	— dope vector	67
- проверка границ	- bounds checking	308-805
~ ТИП	- type	65-71
~ ~ коллекции	- collection type	147
~ хранение	- storage	66
матрица	matrix	120
~ достижимости	reachability matrix	666
- коэффициентов	coefficient matrix	132
~ смежности	adjacency matrix	650
Метод Реек (стековый)	Peek (stack) method	188
“ вставки	insertion method	
- - BinFile (Write)		BinFile (Write)	722
~ - HaatTable (Insert)	— HastTable (Insert)	711
~ ~ LinkedList (InsertRear)		LinkedList (InsertRear)	375
~ ~ Queue (QInsert)	— Queue (QInsert)	205
" ~ String (Insert)	— String (Insert)	812
- ” AVLTree (AVUnaert)	— AVLTree (AVLInsert)	636
' ~ “ (Insert)		(Insert)	634
	BinSTree (Insert)	-— BinSTree (Insert)	618
	Graph (InsertEdge)		Graph (InsertEdge)	652
	(InsertVertex)		(Insertvertex)	652
	Heap (Insert)		Heap (Insert)	614
	LinkedList (InsertAf ter)		LinkedList (InsertAf ter)	875
	(InsertAt)		(InsertAt)	385
	(InsertFront)		(InsertFront)	375
	OrderedList (Insert)	— OrderedList (Insert)	575
	PQueue (PQIneert)		PQueue (PQInsert)	215
	SeqList (Insert)	— SeqList (Insert)	169
— Set (Insert)		Set (Insert)	366
	Stack (Push)	- - Stack (Push)	187
	String (operatori-)		String (operatori-)	312,317
- деления (хеширование)	division method (hashing)	702
- середины квадрата (хеширование)	midsquare technique (hashing)	704
- удаления	deletion method	(	
	BinSTree (Delete)		BinSTree (Delete)	523
— Dictionary (DeleteKey)	— Dictionary (DeleteKey)	742
- - Graph (DeleteEdge)	— Graph (DeleteEdge)	652
— SeqList (Delete)	-— SeqList (Delete)	170
- - Set (Delete)		Set (Delete)	336
— Stack (Pop)		Stack (Pop)	187
—- Graph (DeleteVertex)		Graph (DeleteVertex)	655
	HashTable (Delete)		HashTable (Delete)	707
	Heap (Delete)	— Heap (Delete)	615
	LinkedList (DeleteAt)		LinkedList (DeleteAt)	387
	(DeleteFont)		(DeleteFont)	375
	PQueue (FQDelete)	— PQueue (FQDelete)	216
	Queue (QDelete)		Queue (QDelete)	206
— цепочек (хеширование)	chaining with separate lists	704
методы класса	method in a class	24
- поиска	retrieval methods	
	BinFile (block Read)	— BinFile (block Read)	721
	(Read)		(Read)	721
—- BinSTree (Find)	— BinSTree (Find)	515
	NasTable (Find)		NasTable (Find)	712
	Queue (Qfront)	— Queue (Qfront)	201
- - SeqList (Find)		SeqList (Find)	171
	Set (IsMember)		Set (IsMember)	335
— Stack (Peek)		Stack (Peek)	188
— String (Find)	— String (Find)	312
	(FindLast)		(FindLast)	319
	(Subetr)		(Substr)	317
методы прохождения дерева	tree traversals	489
минимальная пирамида	minimum heap	608
минимальный путь	- path	661
множественное наследование	multiple inheritance	37
множественные конструкторы	— constructors	117
множество	set	
- Set (integral type)	Set class (integral type)	334
	operator!- (объединение)		operator!- (union)	335
— —- конструктор		constructor	329
	метод Delete		Delete method	330
	Insert		Insert method	385
IsMember		IsMember method	385
— — - объявление		declaration	(	336
	 описание		description	836
	потоковые операторы		I/O stream operates	334
ввода/вывода
		реализация -	класс Set (модель массива) 	(целочисленный тип) -	описание (целочисленный тип)			implementation set: Set class (array model) 	(integral type) - description (integral type)	336 263 328 826-327,329
- решето Эратосфена	- application: Sieve of Eratosthenes	332
- тип коллекции	- collection type	154
моделирование	simulation	220-232
- событие прихода	— arrival event	223
- - ухода	- departure event	223
мультипликативный метод (хеширование) multiplicative method (hashing)		704
н		
набор символов кода ASCII	ASCII character set	59
надежные массивы	safe arrays	303
наибольший общий делитель	GCD (Greatest Common Devisor)	254
направленный граф	directed graph	648
наследование	inheritance	
- абстрактный класс	- abstract class	541
- базовый класс	- base class	542
- виртуальная функция	- virtual function	541
— функция-член		member function	550
- динамическое связывание	- dynamic binding	550
- защищенные члены (класса)	— protected members	544
- иерархия класса	- class hierarchy	542
- концепция	- concept	28
- множественное (наследование)	- multiple	37
- определение	- definition	540
- открытое наследование	— public inheritance	543
- подкласс	- subclass	543
- полиморфизм	— polymorphism	550
- приложение: геометрические фигуры	— application: geometric figures	556
- производный класс	- derived class	542
- статическое связывание	- static binding	551
- суперкласс	- superclass	543
- чистая виртуальная функция	- pure virtual function	541
начало связанного списка	front of linked list	353
нелинейная коллекция	nonlinear collection	144,152-155
ненаправленный граф	undirected graph	643
нисходящая программная разработка	top-down program disign	38
нотация Big-O	Big-О notation	156
О		
обменная сортировка	exchange sort	85-86
обратная польская запись	Reverse Polish notation	193
обратный метод прохождения	postorder traversal	490
объект	object	
- как возвращаемое значение	- as a return value	115
- как параметр функции	- as a function parameter	115
~ композиция	- composition	28
~ наследование	- inheritance	28,31-32
~ определение	- definition	20
- тестирование	- testing	45
объектно-ориентированное	object-oriented programming	
программирование		
- - виртуальная функция	— virtual function	49
“ - абстрактный базовый класс	— abstract base class	48
	Бъярн Страуструп	— Btroustrup, Bjame	47
---динамическое связывание ---композиция
---множественное наследование ---наследование
---повторное использование кода ---полиморфизм ---построение программы
---тестирование ---чистая виртуальная функция объекты и передача информации объявление (класса) однородный - массив
- список оператор delete --- описание ---определение - new --- описание ---определение ---ошибка выделения памяти - адреса & - индекса [] - преобразования типа ---к типу объекта ---из объектного типа - присваивания операция извлечения (из стека) ---метод Stack --- описание — помещения в стек ---метод Stack --- описание
- разрешения области действия определение AVL-дерева орграф, см. направленный граф открытая адресация с линейным опробованием
- секция класса открытое наследование отладчик на уровне кода очередь - приоритетов - - ADT ---класс Pqueue ---(пирамидальная версия) ---определение ---приложение: длинные последовательности
-----моделирование, управляемое событиями
---сервисная поддержка - ADT - определение
— приложение: партнеры по танцу ---поразрядная сортировка - реализация - тип коллекции
	dynamic binding	650	
	composition	28	тт
	multiple Inhritance	37	11
	inheritance	28, 31	naJ
	reusability of software	36 '	ne]
	polimorphism	560	ne]
	program design	88-43,45.45	- <
	testing	45	- 1
	pure virtual function	49	- 1
objects and information passing	116	~~ *
declaration, class	26	— 1
homogeneous		—
- array	66	—
- list	166	-
delete operator	64	—
— description	290	—
	definition	293	n<
new operator		nt
	description	290	nl
	definition	64	—
	insufficient memory error	292	—
address operator &	64	—
index operator []	303,306	—
type conversion operator		П
	to object type	252	n
	from object type	253	n
assignment operator	297	0
pop operation		r
	Stack method	187	r
	description	149,182	I
push operator		1
	Stack method	187	1 J
	description	149,182	1
scope resolution operator	108	1
AVL tree: definition	627	
digraph, aee directed graph		
linear probe open addressing	704	
public class section	24	
- inheritance	543	
source level debugger	46 .	1
queue	198-206	
priority queue	212-217	
- - ADT	213	
	PQueue class	214	
	(heap version)	622	
	definition	212	
	applicaion: long runs	622	
	event-driven simulation	220	
	support aervices	217	
queue: ADT	199	
- definition	198	
- application: dance partners	206	
	radix sort	209	
- implementation	202	
- collection type	149 •	
п
палиндром	palindrome	189
первым вошел-первым вышел перегрузка	first-in-first-out overloading	199
- оператора	operator overloading	241
-—• потока	stream operator overloading	251
- функции	function overloading	258
- внешние функции	overloading: external function	241
- дружественными функциями	- with friend functions	244
- оператор индекса [ ]	- index operator [ ]	-
— присваивания —	— assignment operator —	299
- оператора	- operator	241
- операторы потока	- stream operators	250-252
— преобразования	- conversion operators	252-254
- функциями-членами класса	- with class members	242
передача сообщения	message passing	25
перестановки	permutations	451-455
пирамида	heap	607-612
- максимальная	- ПМКППШП	608
- минимальная	- minimum	608
- определение	- definition	607
- порядок	- order	607
пирамидальная сортировка	heapsort	618
повторное использование кода	reusability of software	35
поддерево	subtree	479
поиск "сначала в ширину"	breadth-first search	6B7
полиморфизм	polimorfism	48-50
полное бинарное дерево	full binary tree	482
поля записи	fields of a record	76
поперечное сканирование	level-order scan	500
поразрядная сортировка	radix sort	209
последним пришел-первым вышел последовательный доступ	last-in-first-out sequential access	183
— массив	— array	65
— файл	- - file	77
- поиск	- search	82.161-162
— алгоритм		algorithm	161
— быстрый	— fast	287
— сравнение с бинарным		compare binary search	164
~ список	- list	166-175
- - ADT	- - ADT	33
— класс SeqList		SeqList class	36,168.391
— описание	— description	32.166
— приложение: хранение видео-фильмов	— - application: video store	173
- - тип коллекций	— collection type	146
постоянная единица времени (0(1)) постфиксный	constant time (0(1)) postfix	160
~ вычисление	- evaluation	198
~ форма представления	- notation	198
поток cerr	cerr stream	80
~ stream (стандартный вывод)	cout stream	80
- ввода	cin stream	80
' описание	stream, description	78. 150
ПОТОКИ в C++ Потоковый класс	C++ streams stream class	77-78
~ - fetream		fstream	81
~ - ifstream		ifstream	80
	ios	— ios	80
	istream	— istream	80
	ofstream	— ofstream	80
— ostream		ostream	80
- объект	- object	
—- cerr (стандартный поток ошибок)	- - cerr	80
	cin (стандартный ввод)	— cin	80
	COUt (стандартный ВЫВОД)	— cout	80
предусловия, см. ADT	precondition. See ADT	
приоритет операции	precedence of perators	
	 входной приоритет	— input precedence	280
- - стековый приоритет	— stack precedence	280
производный класс	derived class	31
прохождение "сначала в глубину"	depth-first graph traversal	666
прошитые деревья	threaded trees	635
прямой доступ	direct access	
	массив	— array	147
	определение		definition	146
	файл	- - file	715
путь в графе	path in a graph	648
- в дереве	- in a tree	479
Р		
разделяй и властвуй (метод)	divide and conquer	433
разнородный	heterogeneous	
- массив	- array	57»
- список	- list	579
- тип	- type	77
разработка программного продукта	software development	
	 методология	— methodology	88
	повторное использование	— reusability of software	35
ранг	rank	278
распределение памяти	memory allocation	
	динамическое		dynamic	290,292
	статическое		static	290
рациональное число	rational number	245-247
	нормализованная форма	-— standardized form	246
	представление		 representation	245
	приложение: решение лив. уравнений	— application: solving linear equations	256
— редуцированная форма		reduced form	245
ребро графа	edge of graph	647
рекурсия	recursion	432-439
- бинарный поиск	- binary search	446
- биномиальные коэффициенты	- binomial coefficients	472
— задача о комитетах	- committee problem	448-450
- задняя рекурсия	- tail recursion	469
- комбинаторика	- combinatorics	488
- лабиринт	- maze	436,460
- определение	- recursive definition	433
- перестановки	- permutations	451-455
- синтаксические деревья	— expression trees	438
- стек времени исполнения	- runtime stack	443-445
- степенная функция	- power function	442
— треугольник Паскаля	- Pascal’s triangle	473
- факториал	- factorial	439
- Ханойская башня	- Tower of Hanoi	435,455
— числа Фибоначчи	- Fibonacci numbers	466
- шаг рекурсии	- recursive step	433
Решето Эратосфена	Sieve of Eratosthenes	332
родитель родительский узел	parent — node	479 479
С		
самоссылающаяся структура	self-referencing structure	885
сбалансированное дерево	balanced (AVL) tree	628
связанный	connected	647
- список	linked list	358-361,
		363-364
	удаление узла		deleting a node	364
	введение		introduction	350
— вставка в начало	- - inserting at front	358
	 голова	— head	358
	 описание		description	351
— приложение: буферизация печати		application: print spooler	394
	головоломка		word jumble	862
	список выпускников		graduation	365
	управление окнами		window management	411
— прохождение	— traversal	359
	создание узла	— creating a node	358
	упорядоченного списка		an ordered list	367
— сортировка с использованием		sorting using ordered list	369
связанного списка		
— удаление начального элемента		deleting a front	363
		 элементов списка		clearing a list	369
- вершины графа	connected: graph vertices	647
- граф, сильно связанный	— strongly	648
— слабо связанный	— - weakly	648
связный список: вставка в хвост	linked list: inserting at rear	861
— печать списка	— — printing a list	360
сеть	network	155
сильно связанный граф	strongly connected graph	648
символьный тип	character type	58-60
симметричный метод прохождения	inorder traversal	58
системная эффективность	system efficiency	155
скрытие информации	information hiding	24
слабо связанный граф	weakly connected graph	648
слияние	merge	
- сортировка прямым слиянием	- straight merge sort	727
- сортированных последовательностей	- sorted runs	570-574
словарь	dictionary	735
- ассоциативная структура	— association structure	735
- класс Dictionaryiterator	— Dictionaryiterator class	738
- определение	- definition	151
- приложение: построение толкового	— application: word building	739
словаря случайное число	random number	
— seed-значение	— seed	110
— генератор	—~ generator	110
— класс RandomNumber		RandomNumber class	110
событие прихода (событийное	arrival event	223
моделирование) - ухода (событийное моделирование)	departure event	223
сопоставление с образцом	pattern matching	320
сортировка вставками	insertion sort	688
~ методом пузырька	bubble sort	686
- на месте	in-place sorting	212
- посредством выбора	selection sort	684
~ при помощи внешнего файла	externa] file sort	727
спецификатор const (константы)	const qualifier	27
список инициализации членов (класса)	member initialization list	103
статическая: память	static: memory	64
статические: структуры данных	- data structures	290,
статический	static	
- массив	— array	147
статическое: связывание	- binding	551
стек	stack	182-180
— времени исполнения	runtime stack	443
— операнда	operand stack	278
- операторов	operator stack	279
- ADT	stack: ADT	184
- описание	- description	182
— определение	- definition	182
- полный	- full	183
- приложение: вывод (чисел) с различными основаниями	- application: multibase output	191
	вычисление выражения	— expression evaluation	193
	инфиксное выражение, оценивание		infix expression, evaluation	279
	палиндром	— palindrome	189
	постфиксный калькулятор	— postfix calculator	195
- пустой	- empty	183
- тип коллекции	- collection type	149
степенная функция (рекурсивная форма)	power function (recursive form)	442
строка в C++	String type (C++)	78
- ADT	string ADT	72
- описание	- description	71
строки в C++ т	C++ strings	78-74
текстовый файл	text file	77-80
типы данных (определяемые языком)	data types (language-defined)	
— двоичный файл	— binary file	80
	двумерный массив		two dimensional array	68
-— действительный		real	60
	 запись		 record	77
	массив		 array	65
— перечисления		enumerated	62
	символьный		character	68
	строки C++		C++ strings	73
— строчный		string	72
‘— структура C++		C++ struct	77
	текстовый файл	— text file	79
- - указатель		pointer	64
’— целочисленный	- - integer	55
	бинарное дерево	— binary tree	162
	граф	— graph	154
	запись (как данные)		 record	- 148
	линейный список		linear list	148
	массив		 array	147
— множество		set	164
	 очередь		 queue	149
— - приоритетов	— priority queue	150
	последовательный список	— sequential list	36
	 словарь	— dictionary	151
— случайные числа	— random numbers	110
	стек		 stack	149
	строка		string	147
— файл		file	150
-— хеш-таблица типы перечисления транзитивное замыкание треугольные матрицы	— hash table enumerated types transitive closure trangular matrices	151 62-68 667 120-12»
У узел	node	
- ADT	-ADT	854
— в дереве	- in a tree	152
- в связанном списке	- in linked list	351-852
- определение	- definition	852
узел-часовой	sentinel node	401
узлы AVL-деревьев	AVL tree: nodes	628
указатель	pointer	
- this	this pointer	800
- ADT	pointer: ADT	64
— операция преобразования	- conversion operator	303
- определение	- definition	63
унарный оператор	unary operator	54,193
универсальное множество	universal set	325
упорядоченный список	ordered list	149
- - ADT	- - ADT	34
— алгоритм создания	— creation algorithm	367
— приложение: длинные		application: long runs	577
последовательности управление окнами	window management	411-418
управляемое событиями моделирование.	event-driven simulation, see simulation	
см. моделирование уровень дерева	level in a tree	480
условие останова (цикла)	stopping condition	433
ф файл	file	77-7»
- потоковый метод seekp	- seekp stream method	717
	tellg	- tellg stream method	71?
-ADT	- ADT	79
- внешнее хеширование	- external hashing	725
- внешний поиск	( - двоичный файл	— search	723
	- binary file	715
- класс BinFile	- BinFile class	718
- последовательный	- sequential	717
~ потоковый метод seekg	- seekg stream method	717
	tellp	- tellp stream method	717
~ режим (записи/чтения)	- mode	716
- сортировка прямым слиянием	- straight merge sort	727
~ текстовый файл	- text file	80
- тип коллекции	- collection type	150
- указатель	- pointer	716
- файловый указатель	- file pointer	78
фактор сбалансированности AVL-дерева	AVL tree: balance factor	628
форма Бэкуса Наура	Bakus-Naur form	434
функция (MakeSearchTree	building a tree search (MakeSearchTree)	511
~ доступа	access function	67
- - к массиву	array access function	67
~ факториала (рекурсивная форма)	factorial function (recursive form)	442
X хвост связанного списка	rear of linked list	353
хеширование	hashing	700
- класс HashTable	- HashTable class	707
- коллизии	- collisions	702
- метод деления	- division metod	702
— свертки	- folding method	744'
	середины квадрата	- midsquare technique	704
	 цепочек	- chaining with separate lists	704
— мультипликативный метод	- multiplicative method	704
- описание	- description	700
- открытая адресация с линейным	- linear probe open addressing	704
опробованием — рехеширование	- rehashing	746
- хеш-таблица	- hash table	701
- хеш-функция		function	701
хеш-таблица	hash table	
- анализ сложности		complexity analysis	714
- итератор		 iterator	708
- определение		definition	700
хеш-функция	- function	701
ц		
целочисленный тип	integer types	54-57
цикл, в графе	cycle, in a graph	649
циклический	circular	400
- связанный список	- linked list	400-403
	класс Cnode			 Cnode class	400
	приложение: Джоэефус		application: Josephus	403
ч		
числа Фибоначчи	Fibonacci numbers	466
число без знака	unsigned number	57
— с фиксированной точкой	fixed point number	60
чистая виртуальная функция, см.	pure virtual function, see virtual	
виртуальная функция	function	
ш		
шаблон	template	270-275
- ключевое слово	- keyword	270
- метод	- method	274
- объект	- object	274
~ объявление класса	- class declaration	274
- синтаксис	- syntax	270
- класс Stack	- Stack class	276-277
- список параметров	- parameter list	270
- функция	- function	270-273
шестнадцатеричный	hexadecimal	90
э		
экземпляр класса	instance of a class	102
экспоненциальное время (О(2п))	exponential time (0(2®))	160
эффективность использования памяти	space efficiency	156
Я		
язык C++	C++ language	47
— Бьярн Страуструп		Stroustrup, BJame	47
	историческое развитие		historical development	47
	определение	- - definition	47
Index
abstract base class	абстрактный базовый класс	48, 559
- class	- класс	48,541,560
- data type	- тип данных	2
- list class	- списковый класс	560-563
access function	функция доступа	67
access, see direct access, sequential	доступ, см. прямой доступ,	
access	последовательный доступ	
activation record	активизирующая запись	443
address operator &	оператор адреса &	64
- memory	адрес, память	57
adjacency matrix	матрица смежности	650
ADT Accumulator	ADT Accumulator	133
- Array	- Array	65
- Binary Search Tree	- бинарного дерева	507
- Character	- Character	58
- Definition	- Definition	20
- Enumerated	- Enumerated	62
- File	-File.	79
- Format	- Format	21-22
- Graph	- Graph	649
— Input component	- Вход	21
- Integer	- Integer	55
- Node	- Node	354
- OrderedList	- OrderedList	35
— Output component	- Выход	21
- Pointer	- Pointer	64
— Postconditions component	компонента постусловий ADT	21
- PQueue	ADT PQueue	213
- Preconditions component	компонента предусловий ADT	21
- Process component	- процесса ADT	21
- Queue	ADT Queue	199
- Real	- Real	60
- Record	- Record	77
- SeqList	- SeqList	за
- Stack	- Stack	184
~ String	- String	72
algebraic expression	алгебраическое выражение	193
— infix	— инфиксное	193
— - postfix	•— постфиксное	193
— prefix	-— префиксное	587
animal classes	классы животных	553-555
array access function	функция доступа к массиву	67
Array ADT	массив: ADT	65
- class	класс Array	
	constructor		конструктор	305
	copy constructor		копирования	305-306
	declaration		объявление	303
- - destructor		деструктор	305
— index operator		оператор индексации	306
— — pointer conversion operator		преобразования указателя	307-309
	Resize method array: bounds checking		метод Resize массив: проверка границ	308-ЗОВ 303-305
- collection type	- тип коллекции	147
— dope vector	- дескриптор	67
- index bounds	- границы	67
- storage	- хранение	66
- type	- тип	65-71
array-based binary trees	бинарные деревья, представляемые массивами	600-602
Arrayiterator class	класс Arrayiterator	574 -
arrival event	событие прихода (событийное моделирование)	223
ASCII character set	набор символов кода ASCII	59
assignment operator	оператор присваивания	297
associative arrays	ассоциативные массивы	151
associativity of operators	ассоциативность операций	278
AVL tree	AVL-дерево	627
— balance factor	фактор сбалансированности AVL-дерева	628
	compare with BinSTree	AVL tree: сравнение с BinSTree	641
	definition	определение AVL-дерева	627
	 nodes	узлы AVL-деревьев	628
AVLTree class	класс AVLTree	631-641
	SingleRotation method	AVLTree class: метод SingleRotation	638
—- Avllnsert method	класс AVLTree: метод AVUnsert	636
	declaration	— объявление	631
— DoubleRotation method		метод DoubleRotation	639
— GetAVLTreeNode method		GetAVLTreeNode	633
- - Insert method	- -— Insert	634
	UpdateLef tTree method		UpdateLef tTree	637
AVLTreeNode class	класс AVLTreeNode	629-631
	constructor	— конструктор	631
	declaration		объявление	629
	implementation		реализация	631
	Left method		метод Left	631
В		
backtracking	возврат (прохождение лабиринта)	436
Bakus-Naur form	форма Бакуса Наура	434
balanced (AVL) tree	сбалансированное дерево	628
base class	базовый класс	31
best case analysis	анализ наилучшего случая	157
Big-О notation	нотация Big-0	156
binary file	двоичный файл	715
- numbers	двоичные числа	56
- operator	бинарный оператор	193
- search	- поиск	503
	tree	дерево бинарного поиска	503-507
	ADT		ADT	508
— -— application: concordance		приложение: конкорданс	525
	occurrence counts		счетчики появлений	513
	description	—	описание	503
	BinStree class		класс BinStree	508
	deleting a node		удаление узла	519
	inorder traversal (sort)		симметричное прохождение (сортировка)	511
	inserting a node		вставка узла	517
	key		ключ	505
	compare sequential	бинарный поиск: сравнение последовательного и бинарного методов	164
---formal analysis ---informal analysis ---recursive form - tree
- - breadth first scan
— — complete
- — copy a tree
---deleting a tree
---horizontal tree printing
---inorder traversal
---left-right child ---level scan
- - LNR, LRN, etc.
---node structure - - TreeNode class
—	upright (vertical) tree printing — building -----definition
— degenerate - - description ---full BinFile class — Clear method — constructor - - declaration
---EndFile method
-	— implementation
-	— Peek method
—	Read (block) method
-----method
—	Write (block) method 	method Binomial coefficients BinSTree class
—	 assignment operator
---constructor
---declaration
— Delete method
---Find/method
---FindNode method
— implementation - - Insert method — memory management — Update method bit
- operations — and
- - exclusive or
---not — or BNF, see Bakus-Naur form breadth-first search bubble sort bucket
building a tree search (MakeSearchTree)
byte
---формальный анализ ---неформальный анализ ---рекурсивная форма бинарное дерево
—- поперечное сканирование --- законченное
---копирование дерева ---удаление дерева ---горизонтальная печать ---симметричный метод прохождения — — левый-правый сын ---по уровневое сканирование
---симметричный метод прохождения (порядок)
---структура узла ---класс TreeNode
— вертикальная печать
---построение ---определение — вырожденное — описание ---полное класс Binfile ---метод Clear ---конструктор --- объявление ---метод EndFile - - реализация ---метод Реек ---Read (блочный)
-----Writs (блочный)
биномиальные коэффициенты класс BinSTree
---оператор присваивания
---конструктор — объявление ---метод Delete ---Find ---FindNode *— реализация ~ - метод Insert ---управление памятью ---метод Update бит битовые операции — и ---исключающее или ---не — или BNF, см. форма Бэкуса Наура поиск "сначала в ширину" сортировка методом пузырька блок (метод цепочек при хешировании) функция (MakeSearchTree
байт
166
166 443-445 479-482
500
482
496
498
493
489
481
500
489
488
483
500
485
479
481
479
482
718
719
721
718
719
721
719
721
721
722
722
472
508-510, 516-524
515
515
510
523
516
516
515
517
515
524
57 327-328
327
327
327
327
657
686
705
511
57
с
C++ language	язык C++	47
— definition		определение	47
	historical development		историческое развитие	47
	Stroustrup, Bjarne		Бьярн Страуструп	47
- streams	потоки в C++	77-78
- strings	строки в C++	78-74
Calculator class	класс Calculator	202
— Compute method	— метод Compute	196 -
— declaration		объявление	185
- — implementation	— реализация	106
— Run method		метод Run	197
call by reference	вызов по ссылке	115
- by value	- по значению	115
cerr stream	поток cerr	80
chaining with separate lists	метод цепочек (хеширование)	704
Character ADT	ADT Character	58
- type	символьный тип	58-60
cin stream	поток ввода	80
Circle class	класс Circle	548
	declaration	— объявление	548
- - description	— описание	548
	implementation	— реализация	548
circular	циклический	400
- linked list	- связанный список	400-403
	application: Josephus		приложение: Джозефус	403
—- - Cnode class		класс Cnode	400
class	класс	
— (in book): Animal	- (в книге): Animal	553-555
	Array		Array	303
	Calculator		Calculator	195
	Circle (derived)		Circle (производный)	548
- - CNode	- - CNode	401
	Date		Date	118
	Dice	— Dice	40
	DNode		DNode	410
— DynamicClass		DynamicClass	293
— Event		Event	223
	Heap		Heap	512
- - Iterator (abstract class)		Iterator (абстрактный)	564
— Line		Line	30
	LinkedList	— LinkedList	374
	List (abstract)		list (абстрактный)	560
- — MathOperator		MathOperator	281
	Maze		Maze	462
	Node		Node	853
	NodeShape		NodeShape	582
	OrderedList	- - OrderedList	36
	Point		Point	29
	PQueue		PQueue	214
— Queue (linked list)		Queue (связанный список)	388
	RandomNumber		RandomNumber	111-112
	Rational		Rational	247
— Rectangle		Rectangle	101
	SeqList		SeqList	36.168
	(derivde)		(производный)	561
	(linked list)		(связанный список)	391
	 Shape		 Shape	646
— Simulation		Simulation	225
— Spooler (print)	— Spooler (для печати)	396
— Stack		Stack	184
	(template)		(шаблонный)	276
— String		String	310
	Temperature	- - Temperature	108
— TriMat		TriMat	12-129
- - Vec2d	- - Vec2d	243
— Window	- - Window	412
- body	- тело	100
- constrictor	- конструктор	101
- declaration	- объявление	26
- encapsulation	- инкапсуляция	24
- head	- заголовок	100
- hierarchy	- иерархия	542
— imlementation	- реализация	25,102
- information hiding	— скрытие информации	24
- inheritance	- наследование	31
- member initialization list	- список инициализации	103
- members	- члена класса	24,100
- methods	— методы	24,100
- object	- объект	100
- private part	- закрытая часть	100
- protected part	- защищенная часть	100
- puplic part	- открытая часть	100
- scope resolution operator	- оператор разрешения области действия	103
CNode class	- CNode	
	constructor	— конструктор	402
	declaration	— объявление	401
	DeleteAfter method		DeleteAfter метод	403
	implementation	- — реализация	402
	InsertAfter method		InsertAfter метод	403
coefficient matrix	матрица коэффициентов	132
collection types	типы коллекции	
— array		массив	147
-— binary tree		бинарное дерево	152
	dictionary		 словарь	151
- - file		файл	150
— graph		граф	154
	hash table		хеш-таблица	151
	linear list	— линейный список	148
	priority queue		очередь приоритетов	150
	queue		 очередь	149
— random numbers		случайные числа	110
	 record	— запись (как данные)	148
- - sequential list	— последовательный список	36
	set		множество	154
— stack	— стек	149
— string	— строка	147
collision	коллизия	
“ definition	- определение	704
- resolution	— разрешение	704
combinatorics	комбинаторика	437
committee problem	задача о комитетах	448-450
complexity analysis	анализ сложности (алгоритма)	155-159
— AVL tree		AVL-дерево	628
	binary search		бинарный поиск	166
	tree	— - бинарное дерево поиска	504
----breadth-first search
----bubble sort
— compare O(n logzn) sorts
----O(n2) sorts
— complete binary tree
— depth-first search
— exchange sort
----Fibonacci numbers
— hashing
—- heapsort
— isertion sort
— linked list sort
----pattern matching algorithm
— priority queue operations
----queue operations
----quicksort
— radix sort
— selection sort
----sequential search
----stack operations
- - straight merge sort
----tounament sort
----tree sort
----Warshall algorithm composition of objects concatenating lists concordance connected
- graph vertices
— strongly
----weakly
const qualifier
constant time (0(1)) constructor
control abstraction copy constructor
cout stream
cubic time (O(n3)) cycle, tn a graph
D
data types (language-defined) — array
----binary file
— C++ strings
----struct — character — enumerated — integer
— pointer
----real
— record
— string
----text file
----two dimensional array
Date class
declaration, class default constructor
---поиск 'сначала в ширину".
— - сортировка методом пузырька
---сравнение О(п квдп)-сортировок
-----О(пг)-сортировок
---законченное бинарное дерево
— поиск "сначала в глубину" ---обменная сортировка ---числа Фибоначчи
-	— хеширование
—	пирамидальная сортировка — сортировка вставками
-----со связанными списками
-	— алгоритм сопоставления с образцом 	операции с очередью приоритетов 	с очередью
—--"быстрая сортировка" ---поразрядная сортировка - ~ сортировка посредством выбора
-	— последовательный поиск
-	— операции со стеком
---сортировка прямым слиянием — турнирная сортировка
-	— сортировка включением в дерево 	алгоритм Уоршалла композиция объектов конкатенация списков конкорданс связанный
-	вершины графа
-	граф, сильно связанный 	слабо связанный спецификатор const (константы) постоянная единица времени (0(1)) конструктор
абстракция элемента управления конструктор копирования
поток stream (стандартный вывод) кубическое время (О(п3)) цикл, в графе
659
688
697
696
482
659
155 466-468
714
619
689
870
325
217
206
695
212
686
161
189
728
604
646
667
28-30
377 525-529
647
647
648
648
27
160
21
540
201
300-302
80
160
649
типы данных (определяемые языком)
— массив	65
— двоичный файл	80
	строки C++	73
	структура C++	77
	символьный	58
— перечисления	62
	целочисленный	55
— указатель	64
— действительный	60
	запись	77
	строчный	72
— текстовый файл	79
	двумерный массив	68
класс Date	118
объявление (класса)	25
конструктор умолчания	117
delete operator	оператор delete	64
	definition		определение	263
— description	•— описание	290
deletion method	метод удаления	
	BinSTree (Delete)		BinSTree (Delete)	523
	Dictionary (DeleteKey)		Dictionary (DeleteKey)	742
	Graph (DeleteEdge)		Graph (DeleteEdge)	652
	(DeleteVertex)		(DeleteVertex)	655
	HashTable (Delete)		HashTable (Delete)	707
	Heap (Delete)	— Heap (Delete)	615
- - LinkedList (DeleteAt)		LinkedList (DeleteAt)	387
	(DeleteFont)		(DeleteFont)	875
— PQueue (FQDelete)		PQueue (FQDelete)	216
	Queue (QDelete)		Queue (QDelete)	206
	SeqList (Delete)		SeqList (Delete)	170
— Set (Delete)		Set (Delete)	336
- - Stack (Pop)		Stack (Pop)	187
departure event	событие ухода (событийное ноделирование) 223	
depth of a tree	глубина дерева	480
depth-first graph traversal	прохождение "сначала в глубину"	656
derived class	производный класс	31
design framework	каркас разработки	39
destructor	деструктор	291,
		295-296
Dice class	класс Dice	41
dictionary	словарь	735
Dictionary class	класс Dictionary	787
— constructor		конструктор	741
	declaration	— объявление	737
	DeleteKey method	— метод DeleteKey	742
— implementation	— реализация	741
	InDictionary method	— метод InDictionary	742
- application: word building	словарь: приложение: построение	739
	толкового словаря	
- association structure	- ассоциативная структура	735
- definition	- определение	151
- Dictionaryiterator class	- класс Dictionaryiterator	738
digraph, see directed graph	орграф, см. направленный граф	
direct access	прямой доступ	
	 array	— массив	147
	definition		определение	146
	file	— файл	715
directed graph	направленный граф	648
discrete type	дискретный тип	60
divide and conquer	- разделяй и властвуй (метод)	433
division method (hashing)	метод деления (хеширование)	702
DNode class	класс DNode	407
— constructor		конструктор	410
- r- DeleteNode method	—- метод DeleteNode	411
	implementation		реализация	410
	InsertLeft method		метод InsertLeft	411
	InsertRight method		InsertRight	411
doubly linked list	двусвязный список	410
—- - application: insertion sort	— приложение: сортировка вставками	406
dynamic	динамический	408
- array	- массив	147
— allocation	- выделение массива	292
- binding	- связывание	49
- data structures	- структуры данных	291
- memory - object DynamicClass class 	assignment operator — constructor — copy constructor — destructor	- память - объект класс DynamicClass 	оператор присваивания 	 конструктор 	копирования 	деструктор	64 293-297 297 293-294 300 296
E		
edge of graph	ребро графа	647
encapsulation	инкапсуляция	24
enumerated types	типы перечисления	62-63
Enumerated ADT	Enumerated ADT	62
Event class	класс Event	223
event-driven simulation, see simulation	управляемое событиями моделирование,	
	см. моделирование	
exchange sort	обменная сортировка	86-86
exponential time (O(2n))	экспоненциальное время (О(2п))	160
expression	выражение	
- evaluation	- вычисление (оценка)	193
- trees	- деревья	488
external data structures	внешние структуры данных	77
- file search	внешний файловый поиск	723
— sort	сортировка при помощи внешнего файла	727
F		
factorial function (recursive form)	функция факториала (рекурсивная форма)	439
Fibonacci numbers	числа Фибоначчи	466
fields of a record	поля записи	76
FIFO, see first-in-first-out	FIFO, см. первым вошел-первым вышел	
file	файл	77-79
- ADT	- ADT	79
- binary file	- двоичный файл	715
- BinFile class	- класс BinFile	718
- collection type	- тип коллекции	150
- external hashing	- внешнее хеширование	725
— search	- внешний поиск	723
- file pointer	- файловый указатель	78
- mode	- режим (эаписи/чтения)	716
- pointer	- указатель	416
- seekg stream method	- потоковый метод seekg	717
- seekp stream method		seekp	717
- sequential	— последовательный	717
— straight merge sort	— сортировка прямым слиянием	727
- tellg stream method	- потоковый метод tellg	717
- tellp stream method		tellp	717
- text file	- текстовый файл	80
first-in-first-out	первым вошел-первым вышел	199
fixed point number	число с фиксированной точкой	60
friend functions	дружественные функции	244-245
front of linked list	начало связанного списка	363
full binary tree	полное бинарное дерево	482
function overloading	перегрузка функции	258
G		
GCD (Greatest Common Devisor)	наибольший общий делитель	254
graph	граф	647
Graph class	класс Graph	—-	652
	breadth-first traversal	—- прохождение "сначала в ширину" (метод)	656
	constructor		конструктор	653
— declaration		 объявление	6Б2
— DeleteVertex method	- - метод DeleteVertex	6ББ
	GetNeighbors method		GetNeighbors	664
— GetVertexPos method		GetVertexPos	664
— GetWeight method		GetWeight	654
	implementation	— реализация	653
	InsertEdge method	— метод InsertEdge	6Б4
	minimum path	- - минимальный путь	661
— Vertexiterator class		метод Vertexiterator	654
— depth-first graph traversal	— прохождение "сначала в глубину"	656
graph: acyclic	граф: ациклический	649
- adjacency matrix	- матрица смежности	660
- ADT	- ADT	649
- application: strong components	- приложение: сильные компоненты	659
- connected vertices	- связанные вершины	648
- cycle	- цикл	649
- directed (digraph)	- направленный (орграф)	648
- edges	- ребра	648
- path	- путь	648
- reachability matrix	- матрица достижимости	666
- strong components	- сильные компоненты	659
- strongly connected	- сильно связанный	648
- transitive closure	— транзитивное замыкание	667
— undirected	— ненаправленный	648
- vertices	— вершины	647
- weakly connected	— слабо связанный	648
- weighted digraph	- взвешенный орграф	649
group	группа	153
H	t	
hash function	хеш-функция	701
- table	хеш-таблица	
— complexity analysis	- анализ сложности	714
	definition	— определение	700
	 iterator	- итератор	708
hashing	хеширование	700
- chaining with separate lists	- метод цепочек	704
- collisions	- коллизии	702
- description	- описание	700
- division metod	- метод деления	702
- folding method		 свертки	744
- hash function	— хеш-функция	701
— table	- хеш-таблица	701
- HashTable class	- класс HashTable	707
~ linear probe open addressing	- открытая адресация с линейным опробованием	704
- midsquare technique	- метод середины квадрата	704
- multiplicative method	- мультипликативный метод	704
- rehashing	- рехеширование	745
HashTable class	класс HashTable	707
— declaration		объявление	707
— Find method		метод Find	712
~ - implementation		реализация	711
— Insert method		метод Insert	711
HashTablelterator class	— HashTablelterator	711
— constructor	— конструктор	718
	declaration		 объявление	708
	implementation		реализация	712
	Next method	— — метод Next	714
	SearchNextNode method		SearchNextNode	713
head of a linked list	голова связанного списка	353, 358
header node	заголовочный узел	401
heap	пирамида	607-612
Heap class	класс Heap	609
	constructor		конструктор	618
— declaration	— объявление	609
— Delete method	—- метод Delete	615
— FilterDown method		FilterDown	615
	FilterUp method		FilterUp	613
	 heapsort		пирамидальная сортировка	618
— implementation		реализация	612
	Insert method		метод Insert	614
heap: definition	пирамида: определение	607
- maximum	- максимальная	608
- minimum	- минимальная	608
- order	- порядок	607
heapsort heterogeneous	пирамидальная сортировка разнородный	618
- array	- массив	579
- list	— список	579
- type	- тип	77
hexadecimal homogeneous	шестнадцатеричный однородный	90
- array	- массив	65
- list	однородный: список	166
I
ifstream class	класс ifstream	80
inderact recursion	косвенная рекурсия	434
index operator []	оператор индекса []	303, 306
- array	индекс, массив	65
infix	инфиксный	
- expression evaluation	- вычисление выражения	277-285
- notation	- формат	193
information hiding	скрытие информации	24
inheritance	наследование	
- abstract class	— абстрактный класс	541
- application: geometric figures	- приложение: геометрические фигуры	556 '
- base class	- базовый класс	542
- class hierarchy	- иерархия класса	542
- concept	- концепция	28
- definition	- определение	540
- derived class	— производный класс	542
- dynamic binding	- динамическое связывание	550
- multiple	- множественное (наследование)	37
- polymorphism	- полиморфизм	550
- protected members	- защищенные члены (класса)	544
- public inheritance	- открытое наследование	543
- pure virtual function	— чистая виртуальная функция	541
- static binding	- статическое связывание	551
- subclass	- подкласс	548
- superclass	- суперкласс	543
- virtual function	— виртуальная функция	541
— member function		функция-член	550
initializer, see constructor	инициализатор, см. конструктор	
inorder traversal	симметричный метод прохождения	58
Inorderiterator class — constructor	класс Inorderiterator 	конструктор	644
— declaration		объявление	643
	implementation		реализация	644
	Next method	— метод Next	645
in-place sorting	сортировка на месте	212
input precedence	входной приоритет	280
insertion method 	AVLTree (AVUnsert)	метод вставки 	AVLTree (AVUnsert)	636
	(Insert)		(Insert)	634
	BinFile (Write)		BinFile (Write)	722
— BinSTree (Insert)		BinSTree (Insert)	517
	Graph (InsertEdge)	—• Graph (InsertEdge)	652
	(Insertvertex)		(InsertVertex)	652
— HastTable (Insert)	— HastTable (Insert)	711
- - Heap (Insert)		Heap (Insert)	614
— LinkedList (InsertAfter)		LinkedList (InsertAfter)	875
	(InsertAt)		(InsertAt)	385
	(InsertFront)		(InsertFront)	375
	(InsertRear)		(InsertRear)	375
	OrderedLiat (Insert)		OrderedList (Insert)	575
— PQueue (PQInsert)	— PQueue (PQInsert)	215
—- Queue (QInsert)		Queue (QInsert)	205
	SeqList (Insert)	।		SeqList (Insert)	169
	Set (Insert)	— Set (Insert)	366
— Stack (Push)		Stack (Push)	187
	String (Insert)		String (Insert)	312
	(operator^-)			 (operator-)-)	812» 317
insertion sort	сортировка вставками	688
instance of a class	экземпляр класса	102
Integer ADT	Integer ADT	55
integer types	целочисленный тип	54-57
internal data structures	внутренние структуры данных	77
Ice class	класс Ios	80
Istream class	- Istream	80
Istrtream class	— Istrtream	81
iterator	итератор	563
Iterator (abstract class) 	declaration	Iterator (абстрактный класс) 	объявление	564
		 implementation		реализация	564
iterator classes 	Arrayiterator	классы итераторов — Arrayiterator	569
	Inorderiterator	— — Inorderiterator	643
	SeqListlterator		SeqListlterator	565
	Vertexiterator		Vertexiterator	652
J		
Josephus problem	задача Джозефуса	403
К		
key	ключ	82
KeyValue class	класс KeyValue	736
L		
last-in-first-out	последним пришел-первым вышел	183
leaf node	листовой узел	479
level in a tree	уровень дерева	480
level-order scan	поперечное сканирование	500
LIFO, see lut-ln-f Irat-cut	LIFO, см. последним пришел-первым вышел	
Line class	класс Line	80
linear	линейный	82 '
- collection	линейная коллекция	144-152
- probe open addressing	открытая адресация с линейным опробованием	704
- time (o(n))	линейное время	160
- sequential list	линейный: последовательный список	148
linked Liat	связанный список	358-361, 363-364
— application: graduation		приложение: список выпускников	365
	print spooler	- — буферизация печати	394
	window management		управление окнами	411
	word jumble		головоломка	362
— clearing a list		удаление злементов списка	369
— creating a node		создание узла	358
	an ordered list		упорядоченного списка	367
— deleting a front	— удаление начального алемента	363
	a node		узла	364
— description	— описание	851
— head	— голова	858
— inserting at front	— вставка в начало	358
	at rear		 в хвост	361
— introduction		введение	350
— printing a list		печать списка	360
— sorting using ordered list	— сортировка с использованием связанного списка	369
	traversal	— прохождение	859
LinkedList class	класс LinkedList	371-376. 381-338
— application: concatenating lists	— приложение: конкатенированные списки	377
	removing duplicates		удаление дубликатов	879
	selection sort		 сортировка выбором	378
	ClearList method	— метод ClearList	383
— constructor	— конструктор	382
— CopyList method	— метод CopyList	383
	current pointer (currPtr)	— указатель на текущий узел	371
— Data method	— метод Data	385
— declaration	— объявление	874
	DeleteAt method	— метод DeleteAt	387
— describing operations	— описание операций	872-374
— designing the class	— проектирование списка	371
	front pointer	— указатель на первый узел	371
	implementation	~ ~ реализация	381
-— InsertAt method		метод InsertAt	885
— memory allocation methods	— методы выделения памяти	382
	Next method	— метод Next	884
— previous pointer (prevPtr)	— указатель на предыдущий узел	371
	rear pointer		на последний узел	371
	Reset method		метод Reset	884
List class (abstract)	класс List (абстрактный)	560-563
— declaration	— объявление	561
	implementation	— реализация	561
load factor	коэффициент заполнения	714
logarithmic time (O(log2n), O(nlog2n))	логарифмическое время (O(log2n), O(nlog2n))	160
long run	длинная последовательность	577
— merge sort		сортировка слиянием	729
м
MathOperator class	класс MathOperator	
— comparison operator	—- оператор сравнения	282
	constructor		конструктор	281
— declaration		объявление	281
— Evaluate method		метод Evaluate	282
matrix	матрица	120
maximum heap	максимальная пирамида	608
Maze class	класс Maze	
	declaration		объявление	462
	imlementation		реализация	463
member initialization list	список инициализации членов (класса)	103
memory allocation	распределение памяти	
— dynamic	— динамическое	290, 292
	 static		 статическое	290
merge	слияние	
- sorted runs	— сортированных последовательностей	570-574
- straight merge sort	— сортировка прямым слиянием	727
message passing	передача сообщения	26
method in a class	методы класса	24
midsquare technique (hashing)	метод середины квадрата (хеширование)	704
minimum heap	минимальная пирамида	608
- path	минимальный путь	661
multiple constructors	множественные конструкторы	117
- inheritance	множественное наследование	37
multiplicative method (hashing)	мультипликативный метод (хеширование)	704
N		
network	сеть	155
new operator	оператор new	
	definition		определение	64
— description		описание	290
	insufficient memory error	— ошибка выделения памяти	292
node	узел	
Node class	класс Node	353-358
— constructor		конструктор	356
	declaration	— объявление	355
— DeleteAfter method		метод DeleteAfter	357
	implementation	— - реализация	356
	InsertAfter method		метод InsertAfter	857
— NextNode method		NextNode	856
node: ADT	узел: ADT	354
- definition	- определение	352
- in a tree	- в дереве	152
- in linked list	- в связанном списке	851-352
NodeShape class	класс NodeShape	582
nonlinear collection	нелинейная коллекция	144. 152-155
NULL pointer 0	NULL-указатель	853
object	объект	
- as a function parameter	- как параметр функции	115
- as a return value	- как возвращаемое значение	115
- composition	- композиция	28
- definition	- определение	20
- inheritance	— наследование	28, 31-32
- testing	— тестирование	45
object-oriented programming	объектно-ориентированное	
— abstract base class	программирование 	абстрактный базовый класс	48
	composition		КОМПОЗИЦИЯ	28
	dynamic binding		динамическое связывание	550
— inheritance		наследование	28, 31
	multiple inhritance		множественное наследование	37
	polimorphism		полиморфизм	550
— program design		построение программы	38-43, •
—- pure virtual function	- - чистая виртуальная функция	45-46 40
	reusability of software	— повторное использование кода	35
	Stroustrup, Bjarne		Бъярн Страуструп	47
	testing		тестирование	45
	virtual function		виртуальная функция	49
objects and information passing	объекты и передача информации	115
ofstream class	класс ofstream	80
operand stack	стек операнда	278
OperandList class	класс OperandList	35
operator overloading	перегрузка оператора	241
— stack	стек операторов	279
ordered list	упорядоченный список	149
- - ADT	- - ADT	84
— application: long runs		приложение: длинные	577
— creation algorithm	последовательности 	алгоритм создания	367
OrderedList class implementation	класс OrderedList: реализация	576
— Insert method		метод Insert	576
	declaration	—- объявление	576
Ostream class	класс Ostream	80
Ostrstream class	- Ostrstream	81
overloading	перегрузке	
- assignment operator —	- оператор присваивания —	299
- conversion operators	- операторы преобразования	252-254
- external function	- внешние функции	241
- index operator [ ]	- оператор индекса [ ]	
- operator	- оператора	241
- stream operators	- операторы потока	250-262
- with class members	- функциями-членами класса	242
- with friend functions	- дружественными функциями	244
P		
palindrome	палиндром	189
parent	родитель	
- node	родительский: узел	479
path in a graph	путь в графе	648
- in a tree	- в дереве	479
pattern matching	сопоставление с образцом	820
Peek (stack) method	метод Реек (стековый)	188
permutations	перестановки	451-466
Point class	класс Point	29-30
pointer	указатель	
-ADT	- ADT	64
— conversion operator	- операция преобразования	303
- definition	- определение	63
polimorfism	полиморфизм	48-50
pop operation	операция извлечения (из стека)	
-— description		. - описание	149, 182
	Stack method			метод Stack	187
postfix	постфиксный	
- evaluation	— вычисление	198
- notation	- форма представления	193
postorder traversal	обратный метод прохождения	490
power function (recursive form)	степенная функция (рекурсивная форма)	442
PQueue class	класс Pqueue	214-217
	declaration		объявление	214
	(heap version)		(пирамидальная версия)	622
— implementation		 реализация	215
	Pqdelete		метод Pqdelete	216
	Pqinsert		Pqinsert	215
precedence of perators	приоритет операции	
—- input precedence		 входной приоритет	280
— stack precedence	— стековый приоритет	280
precondition, See ADT	предусловия, см. ADT	
print spooler	буферизация печати	894-400
priority queue	очередь приоритетов	212-217
- - ADT	- - ADT	213
	applicaion: long runs	— приложение: длинные последовательности	622
	support services		сервисная поддержка	217
	event-driven simulation	— приложение: моделирование, управляемое событиями	220
- - definition	—- определение	212
	PQueue class	— класс Pqueue	214
	(heap version)		(пирамидальная версия)	622
private inheritance	закрытое наследование	587
PRN, see Reverse Polish notation	PRN, см обратная польская запись	
program design features	возможности программного конструирования	
	object design		объектная разработка	38
	 testing		тестирование объектов	45
	robustness	- — — устойчивость к ошибкам	46
	structure tree		структурное дерево	39
	structured walkthrough		сквозной стуктуированный контроль	45
	testing		тестирование объектов	40
protected members	защищенные методы	544
public class section	открытая секция класса	24
- inheritance	открытое наследование	543
pure virtual function, see virtual	чистая виртуальная функция, см»	
function	виртуальная функция	
push operator	операция помещения в стек	
	description		описание	149, 182
	Stack method			— метод Stack	187
Q		
quadratic time (O(n2))	квадратичное время (О(п2))	160
queue	очередь	198-206
Queue class (array)	класс Queue (массив)	199
	constructor		 конструктор	204
		 declaration		объявление	201
	implementation			реализация	202
	Qdelete method		метод Qdelete	206
	Qinsert method		Qinsert	205
	(linked linked)		(связанный список)	889-392
	declaration	•	объявление	389
	implementation	- - — реализация	890
queue: ADT	очередь: ADT	199
- application: dance partners	- приложение: партнеры по танцу	206
	radix sort		поразрядная сортировка	209
- collection type	— тип коллекции	149
- definition	- определение	198
- implementation	- реализация	202
quicksort	быстрая сортировка	690
R
radix sort	поразрядная сортировка	209 \
random number	случайное число	
— generator	— генератор	110
— RandomNumber class		класс RandomNumber	110
— seed		seed-значение	110
RandomNumber class	класс RandomNumber	110 114
— constructor		конструктор	111 112
- - declaration		 объявление	110
— fRandom method		метод fRandom	112
— implementation		реализация	112
— Random method		метод Random	112
rank	ранг	278
Rational class	класс Rational	247-258
	declaration	— объявление	247
— operators (as friends)		операторы (как дружественные)	247
	(as members)		(как члены)	249
		(type conversion)		(преобразование типа)	252
	Reduce method		метод Reduce	255
— stream operators		потоковые операторы	248
rational number	рациональное число	245-247
	application: solving linear		приложение: решение линейных	256
equations	уравнений	
— reduced form		редуцированная форма	245
— representation		представление	245
— standardized form		нормализованная форма	246
reachability matrix	матрица достижимости	666
real data types	вещественные типы данных	60
- number	вещественное число	
- - ADT	- - ADT	60
	definition	— определение	60
— exponent		порядок (экспонента)	60
	mantissa		 мантисса	60
	representation	— представление	60
— scientific notation	— научный формат	60
rear of linked list	хвост связанного списка	353
record	запись (как набор данных)	
-ADT		ADT	77
- definition		определение	76
Rectangle class	класс Rectangle	101-107
recursion	рекурсия	432-439
- binary search	- бинарный поиск	446
- binomial coefficients	- биномиальные коэффициенты	472
- combinatorics	- комбинаторика	438
- committee problem	- задача о комитетах	448-450
- expression trees	- синтаксические деревья	488
- factorial	- факториал	439
- Fibonacci numbers	- числа Фибоначчи	466
- maze	— лабиринт	436, 460
- Pascal’s triangle	- треугольник Паскаля	473
- permutations	- перестановки	451-455
- power function	- степенная функция	442
- recursive definition	- определение	438
	step	- шаг рекурсии	433
- runtime stack	- стек времени исполнения	448-445
- tail recursion	- задняя рекурсия	469
— Tower of Hanoi	- Ханойская башня	435. 455
retrieval methods •	BinFile (block Read)	методы поиска 	BinFile (block Read)	721
	(Read)		(Read)	721
	BinSTree (Find)		BinSTree (Find)	515
	NasTable (Find)		NasTable (Find)	712
	Queue (Qfront)		Queue (Qfront)	201
	SeqList (Find)		SeqList (Find)	171
	Set (IsMember)		Set (IsMember)	335
	Stack (Peek)		Stack (Peek)	188
	String (Find)		String (Find)	312
	(FindLast)			 (FindLast)	319
	(Substr)		(Substr)	317
reusability of software	повторное использование кода	35
Reverse Polish notation	обратная польская запись	193
root of tree	корень дерева	152, 479
rotation in AVL tree - — - double	вращение AVL-дерева 	 двойное	639
—	single	— единичное	638
runtime stack	стек Бремени исполнения	443
s		
safe arrays	надежные массивы	303
scope resolution operator	операция разрешения области действия	103
selection sort	сортировка посредством выбора	684
self-referencing structure	само ссылающаяся структура	335
sentinel node	узел-часовой	401
SeqList class (array)	класс SeqList (массив)	168-175
	declaration		 объявление	168
	Delete method		метод Delete	170
	Find method		Find	171
	GetData method		GetData	170
	implementation		реализация	168
	Insert method		метод Insert	169
	(derived)	— (производный)	561-563
	declaration		объявление	562
	Implementation		реализация	562
	(linked list)		(связанный список)	391-392
	application: efficiency		приложение: сравнение	392-394
comparison 		 declaration	эффективности 	объявление	391
	implementation		реализация	392
SeqListlterator class 	declaration	- SeqListlterator 	объявление	565
— implementation		реализация		565
sequential access — array	последовательный доступ 	массив	65
- - file		файл	77
- list	- список	166-175
- - ADT	- - ADT	33
	application: video store	— приложение: хранение видео фильмов	173
	collection type	— тип коллекции	146
- — description		описание	32,166
---SeqList class
- search
---algorithm
---compare binary search ---fast set
Set class (integral type) ---constructor ---declaration ---Delete method ---description ------- I/O stream operatos
-------implementation —------Insert method
-------IsMember method --------------operator!- (union) set: Set class (array model) -------(integral type)
- application: Sieve of Eratosthenes - collection type - description (integral type)
Shape class ---declaration ----description - - implementation Sieve of Eratosthenes sign bit simulation Simulation class ----constructor ----declaration ----NextArrvalTime method simulation: arrival event - departure event software development ----methodology ----reusability of software sorting algorithms ----bubble sort ----doubly linked list sort ----exchange sort ----insertion sort ----Unked list sort — quiksort ----radix sort ----selection sort ----tournament sort ---- treesort source level debugger space efficiency Spooler (print) class ----declaration ----implementation square matrix stack Stack class
- - класс SeqLiet
- поиск
---алгоритм
— - сравнение с бинарным
---быстрый множество
- Set (integral type)
-------конструктор
------ объявление ------------ - метод Delete ------ описание
------— потоковые операторы ввода/вывода
-------- реализация
-------метод Insert
-------метод IsMember
-------operator!- (объединение)
-	класс Set (модель массива)
---(целочисленный тип)
-	решето Эратосфена
-	тип коллекции
-	описание (целочисленный тип)
класс Shape
--- объявление
- — описание
---реализация
Решето Эратосфена знаковый бит
моделирование
класс Simulation
---конструктор --- объявление
---метод NextArrvalTime моделирование: событие прихода - - ухода
разработка программного продукта
---методология
- — повторное использование алгоритмы сортировки
---методом пузырька
-— двусвязного списка
---обменная
--- вставками
---сортировка со связанными списки
---"быстрая’'
---поразрядная
---сортировка посредством выбора
- - турнирная
---treesort-сортировка отладчик на уровне кода эффективность использования памяти
класс Spooler (печать)
---объявление
---реализация квадратная матрица стек
класс Stack
36,168, 391
82, 161-162
161 •
164
287
334
329
336 \
330
336
334
336
335
335
335
263
328
332
154
325-327, 329
547
546
547
332
57
220-232
226
225
227
223
223
38
35
686
408
85
688
369
690
209
684
602
646
46
156
396
397
120 182-189 184-189
	CiearStack method		метод CiearStack	188
	constructor	•	конструктор	187
	declaration		 объявление	186
	implementation		реализация	187
	Peek method		метод Peek	188
	Pop method		Pop	187
- - Push method		Push	187
	StackEmpty method		StackEmpty	188
	StackFull method		StackFull	188
stack precedence	стековый приоритет	
-ADT	Стек: ADT	184
- application: expression evaluation	— приложение: вычисление выражения	193
	infix expression, evaluation		инфиксное выражение, оценивание	279
	multibase output		вывод (чисел) с различными	191
	основаниями	
-— palindrome		 палиндром	189
	postfix calculator		постфиксный калькулятор	195
- collection type	- тип коллекции	149
- definition	— определение	182
- description	— описание	182
- empty	- пустой	183
- full	- полный	183
state change	изменение состояния	25
static	статический	
- array	— массив	147
- binding	статическое: связывание	551
- data structures	статические: структуры данных	290
- memory	статическая: память	64
stopping condition	условие останова (цикла)	433
stream class	потоковый класс	
	fstream		fstream	81
	ifstream		ifstream	80
— ios	—• ios	80
	istream		istream	80
	ofstream	- — ofstream	80
	ostream		ostream	80
	cerr		cerr (стандартный поток ошибок)	80
	cin		cin (стандартный ввод)	80
	cout		cout (стандартный вывод)	80
— operator overloading	перегрузка оператора потока	251
- description	поток, описание	78,150
String class	класс String	810-320
	application: test program	—- приложение: тестовая программа	314
	assignment operator		оператор присваивания	316
	concatenation	- - конкатенация	314
	constructor		 конструктор	315
	declaration		 объявление	311
	FindLast		FindLast	319
--I/O		ВВОД/вЫВОД	319
	implementation	- - реализация	315
	Readstring method		метод Readstring	319
•— string comparison		сравнение строк	316
	Substr method	- - метод Substr	318
- type (C++)	строка в C++	73
string: ADT	- ADT	72
- description	- описание	71
strongly connected graph	сильно связанный граф	648
subtree	поддерево	479
system efficiency	системная эффективность	155
т
tail recursion Temperature class template
-	class declaration
-	function
-	keyword - method
-	object
-	parameter list
-	Stack class
-	syntax text file this pointer threaded trees top of stack top-down program disign trangular matrices transitive closure
tree
- algorithms
---breadth-first csan
---computing the depth ---copying a tree ---counting leaf nodes
---deleting a tree
---horizontal tree printing ---inorder scan
---level-order scan ---postorder scan - iterators
-	node function desing - traversals
-	ancestors-descendenta
-	binary tree
-	children-parent
-	collection type definition
—	depth
-	description
-	height, see depth
-	leaf
-	left child
-	level - node
-	path
-	right child
-	root
-	subtree
-	terminology TreeNode class - - constructor 	declaration — - FreeTreeNode method -  GetTreeNode method treesort TriMat class
задняя рекурсия	469
класс Temperature	Ю8
шаблон	270-275
-	объявление класса	274
-	функция	270-273
—	ключевое слово	270
-	метод	274
—	объект	274
-	список параметров	270
- класс Stack	276-277
-	синтаксис	270
текстовый файл	77-80
указатель this	300
прошитые деревья	535
вершина стека	182
нисходящая программная разработка	38
треугольные матрицы	120-129
транзитивное замыкание	667
дерево	479-480
алгоритмы деревьев	489-503
---поперечное сканирование	500 	вычисление глубины	492 	копирование дерева-495 -----------------------------------------------------------вычисление количества листовых	492
узлов
---удаление дерева	498-499 	горизонтальная печать дерева	493 	симметричное сканирование-489 ------------------------------------------------------------------по уровневое сканирование-----------------------------------------500 ------------------------------------------------------------------обратное сканирование	490 итераторы дерева	642
конструирование функций узлов дерева 486 методы прохождения дерева	489
дерево: предки-потомки	479
-	бинарное дерево	480
-	сын-родитель	479
-	тип коллекции	162
-	определение	479
-	глубина	480
-	описание	479
-	высота, см. глубина - лист	479
-	левый сын	481
-	уровень	480
-	узел	483
-	путь	479
~ правый сын	,	479
-	корень	479
-	поддерево	479
-	терминология	479
класс TreeNode	484
-	конструктор	485
---объявление	484 	метод FreeTreeNode	486 	GetTreeNode	486
treeeort-сортнровка	'	646
класс TriMat	124-129
two-dimensional array ---definition --- storage type conversion operator - --from object type
-----to object type
u
unary operator undirected graph universal 6et unsigned number upper triangular matrix
V
Vec2d class ---declaration ---operators (as friends) ---(as members) ---scalar multiplication vertex of graph virtual function ---and polymorphism ---description ---destructor ---pure ---table
w
War shall algorithm weakly connected graph weighted digraph Window class ---declaration ---implementation window management worst case analysis
двумерный массив
---определение	68
- - хранение	69
оператор преобразования типа ---из объектного типа	253
---к типу объекта	252
унарный оператор	54,193
ненаправленный граф	648
универсальное множество	325
число без знака	57
верхняя треугольная матрица	120
класс Vec2d
--- объявление
---операторы (как дружественные)
---(как члены класса)
---скалярное произведение
вершина графа
виртуальная функция
---и полиморфизм
--- описание
---деструктор
--- чистая
---таблица
алгоритм Уоршалла слабо связанный граф взвешенный орграф класс Window --- объявление
---реализация управление окнами анализ наихудшего случая (алгоритма)
243
244
248
262
262
647 550-552
550 49-50, 541 558 541,559 552
666
648
649
413
415 411-418
157
Научно-популярное издание
Уильям Топп, Уильям Форд
Структуры данных в C++
Компьютерная верстка Свиридова К.А.
Подписано в печать 11.07г2000. Формат 70x100 1/|б- Усл. псч. л. 66,3 Гарнитура Школьная. Бумага газетная. Печать офсетная.
Доп. тираж 3000 экз. Заказ425
ЗАО «Издательство БИНОМ», 2000 г.
103473, Москва, Краснопролетарская, 16
Лицензия на издательскую деятельность № 065249 от 26 июня 1997 г.
Отпечатано с готового оригинал-макета в типографии И ПО Профмздат 109044, Москва, Крутицкий вал, 18.