Text
                    СТРУКТУРЫ ДАННЫХ
в C++
Уильям Топп
Уильям Форд
БИНОМ


Data Structures with C++ William Ford University of the Pacific William Topp University of the Pacific Prentice-Hall International, Inc.
Уильям Топп, Уильям Форд Структуры данных в C++ Перевод с английского под редакцией В. Кузьменко Москва ЗАО «Издательство БИНОМ» 199 9
УДК 004.422 ББК 32.973 Т58 Уильям Топп, Уильям Форд. Структуры данных в C++: Пер. с англ. — М.: ЗАО «Издательство БИНОМ», 1999. - 816 с: ил. В книге на основе так называемых абстрактных структур данных (ADT) рассматриваются как встроенные, так и определяемые пользователем типы данных в языке C++. Подробно излагаются вопросы организации структур данных для эффективной их обработки методами сортировки и поиска информации, построением стеков, очередей и деревьев. Книга будет интересна всем категориям программистов — от начинающих до профессионалов. Все права защищены. Никакая часть этой книги не может быть воспроизведена в любой форме или любыми средствами, электронными или механическими, включая фотографирование, магнитную запись или иные средства копирования или сохранения информации без письменного разрешения издательства. ISBN 5-7989-0017-7 (рус.) ISBN 0-13-320938-5 (англ.) Authorized translation from the English language edition. © Original copyright. Prentice Hall, Inc. A Simon & Schuster Company © Издание на русском языке ЗАО «Издательство БИНОМ*, 1999.
Содержание Предисловие 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 и OrderedList 36 1.5. Приложения с наследованием классов 37 1.6. Разработка объектно-ориентированных программ 38 Анализ задачи/определение программы 39 Разработка 39 Кодирование 40 Тестирование 40 Иллюстрация программной разработки: Dice график 40 1.7. Тестирование и сопровождение программы 45 Объектное тестирование 45 Тестирование управляющего модуля 45 Программное сопровождение и документирование 46 1.8. Язык программирования C++ 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.6. Массив (array) 65 Встроенный тип массива C++ 66 Сохранение одномерных массивов 66 Границы массива 67 Двумерные массивы 68 Сохранение двумерных массивов 69 2.7. Строковые константы и переменные 71 Строки C++ 73 Приложение: перестановка имен 74 2.8. Записи 76 Структуры C++ 77 2.9. Файлы 77 Иерархия потоков C++ 80 2.10. Приложения массива и записи 82 Последовательный поиск 82 Обменная сортировка 85 Подсчет зарезервированных слов C++ 87 Письменные упражнения 90 Упражнения по программированию 96 Глава 3. Абстрактные типы данных и классы 99 3.1. Пользовательский тип — КЛАСС 100 Объявление класса 100 Конструктор 101 Объявление объекта 102 Реализация класса 102 Реализация конструктора 103 Создание объектов 104 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
Коллекции групп 153 4.3. Анализ алгоритмов 155 Критерии эффективности 155 Общий порядок величин 159 4.4. Последовательный и бинарный поиск 161 Бинарный поиск 162 4.5. Базовый класс последовательного списка 166 Методы модификации списка 169 Письменные упражнения 175 Упражнения по программированию 178 Глава 5. Стеки и очереди 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.4. Операторы класса 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. Оценка рекурсии 466 Письменные упражнения 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
Реализация класса Arraylterator 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 13.7. Итераторы деревьев 642 Итератор симметричного метода прохождения 643 Реализация класса Inorderlterator . 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. Программное обеспечение находится в каталоге "/риЬ/С-Ь+". Читатели могут обращаться непосредственно к авторам для получения копии учебника. Информация для заказа предоставляется по электронной почте: посылайте запрос по адресу "billf@uop.edu", или по международной почте: пишите Bill Тор, 456 S. Regent Stockton, CA 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 Texas — El Paso) и Дуглас Смит (University of the Pacific) внесли большой вклад в эту работу. Их взгляды и поддержка были бесценны для авторов и значительно улучшили окончательную структуру книги. Уильям Форд Уильям Топп
глава 1 Введение 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 Операции UpdatestoekLevel AdjustUnitPrice Reorderltem 2. Игровая программа моделирует бросание набора костей. В этой разработке игральные кости описываются как абстрактный тип данных, которые включают число бросаемых костей, сумму очков в последнем бросании и список со значениями очков каждой кости в последнем бросании. Операции включают бросание костей (Toss), возвращение суммы очков в одном бросании (Total) и вывод очков для каждой отдельной кости (DisplayToss). Данные N diceTotai Dice List Операции Toss Total DisplayToss ADT — формат Для описания ADT используется формат, который включает заголовок с именем ADT, описание типа данных и список операций. Для каждой операции определяются входные (input) значения, предоставляемые клиентом, предусловия (preconditions), применяемые к данным до того, как операция может быть выполнена, и процесс (process), который выполняется операцией. После выполнения операции определяются выходные (output) значения, которые возвращаются клиенту, и постусловия (postconditions), указывающие на любые изменения данных. Большинство ADT имеют инициализирующую операцию (initializer), которая присваивает данным начальные значения. В среде языка C++ такой инициализатор называется конструктором (constructor). Мы используем этот термин для упрощения перехода от ADT к его преставлению в C++.
APT — формат ADT ADTJName Данные Описание структуры данных Операции Конструктор Начальные значения: Данные, используемые для инициализации объекта Процесс: Инициализация объекта Операция2 Вход: Данные от клиента Предусловия: Необходимое состояние системы перед выполнением операций Процесс: Действия, выполняемые с данными Выход: Данные, возвращаемые клиенту Постусловия: Состояние системы после выполнения операций Операция2 * • • Операцияп * • • Конец ADT Пример 1.2 1. Данные абстрактного типа Dice включают счетчик N числа игральных костей, которые используются в одном бросании, общую сумму очков и список из N элементов, который содержит значения очков, выпавших на каждой кости. ADT Dice Данные Число костей в каждом бросании — целое, большее либо равное 1. Целое значение, содержащее сумму очков всех костей в последнем бросании. Если N — число бросаемых костей, то число очков находится в диапазоне от N до 6N. Список, содержащий число очков каждой кости в бросании. Значение любого элемента списка находится в диапазоне от 1 до 6. Операции Конструктор Начальные значения: Число бросаемых костей Процесс: Инициализировать данные, определяющие число костей в каждом бросании Toss Вход: Нет Предусловия: Нет Процесс: Бросание костей и вычисление общей суммы очков Выход: Нет Постусловия: Общая сумма содержит сумму очков в бросании, а в списке находятся очки каждой кости DieTotal Вход: Нет Предусловия: Нет Процесс: Находит значение элемента, определяемого как сумма очков в последнем бросании
Выход: Возвращает сумму очков в последнем бросании Постусловия: Нет DisplayToss Вход: Нет Предусловия: Нет Процесс: Печатает список очков каждой кости в последнем бросании Выход: Нет Постусловия: Нет конец ADT Dice 2. Окружность определяется как набор точек, равноудаленных от точки, называемой центром. С целью графического отображения абстрактный тип данных для окружности включает как радиус (radius), так и положение центра. Для измеряющих приложений абстрактному типу данных требуется только радиус. Мы разрабатываем Circle ADT и включаем операции для вычисления площади (area) и длины окружности (circumference). Этот ADT применяется в следующем разделе для иллюстрации описания класса C++ и использования объектов при программировании приложений. radius radius Circumference Area ADT Circle Данные Неотрицательное действительное число, определяющее радиус окружности. Операции Конструктор Начальные значения: Радиус окружности Процесс: Присвоить радиусу начальное значение Area Вход: Нет Предусловия Нет Процесс: Вычислить площадь круга Выход: Возвратить площадь круга Постусловия: Нет Circumference Вход: Нет Предусловия Нет Процесс: Вычислить длину окружности Выход: Возвратить длину окружности Постусловия: Нет конец ADT Circle
1.2. Классы C++ и абстрактные типы Язык C++ поддерживает определяемый пользователем тип классов для представления абстрактных типов данных. Класс состоит из членов (members), которые включают значения данных и операции по обработке этих данных. Операции также называются методами (methods), поскольку они определяют методы доступа к данным. Переменная типа класса называется объектом (object). Класс содержит две отдельные части. Открытая (public) часть описывает интерфейс, позволяющий клиенту манипулировать объектами типа класса. Открытая часть представляет ADT и позволяет клиенту Класс private: Данные-члены: переменнаяь переменнаяг Внутренние операции public: Конструктор Операция1 Операцияг использовать объект и его операции без знания внутренних деталей реализации. Закрытая (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 фута и что клиент указывает радиус круглого бассейна. В качестве результата приложение должно определить стоимость строительства ограды и дорожки при планировании бассейна. Pool PoolRim Мы объявляем объект Circle с именем Pool, описывающий площадь плавательного бассейна. Второй объект — PoolRim, — это объект Circle, включающий как бассейн, так и окружающую дорожку. Конструктор вызывается при определении объекта. Для объекта Pool клиент задает радиус в качестве параметра, и затем использует радиус плюс 3 фута для определения объекта PoolRim. Для вызова операции класса задайте имя объекта, за которым следует точка (.) и операция. Например, Роо1.Агеа() и Circumference() вызывают операции Circle для Pool. Ограда располагается вдоль наружной стороны PoolRim. Вызовите операцию вычисления окружности PoolRim.Circumference() для вычисления стоимости ограды. FenceCost = PoolRim.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 в их объявлении и определении. Стоимость строительных материалов для сетки ограды и стоимость бетона задаются как константы. // рг01_01.срр #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 r); // вычисляющие функции 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 main() { 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) ; // вычислить стоимость ограды и выдать ее значение 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), который определяется своими конечными точками pi и р2 [Рис. 1.1 (А)]. базовая точка Прямоугольник — это четырехсторонняя фигура, чьи смежные стороны встречаются в прямых углах. Для рисования прямоугольник определяется двумя точками, которые отмечают верхний левый угол (ul) и нижний правый угол (1г) рамки [Рис. 1.1 (В)]. (А) Отрезок Цр1, р2) (В) Прямоугольник R(ul, lr) Рис. 1.1. Отрезок и прямоугольник Мы используем эти факты для создания классов Point, Line и Rectangle. Члены в классах Line и Rectangle являются объектами типа Point. Композиция — это важный инструмент в создании классов с объектами из других классов. Заметьте, что каждый класс имеет метод Draw для отображения рисунка на поверхности рисования. Класс Point содержит функции-члены для доступа к координатам х и у точки. Класс Point private: х у координаты public Конструктор, Draw, GetX, detY Класс Line private: Point pi, p2 public Конструктор, Draw Класс Rectangle private: Point ul, lr public: Конструктор, Draw Пример 1.3 Определите геометрический объект, задавая фигуру, за которой следуют имя объекта и параметры для указания объекта. 1. Point p(l,3); // объявляет объект point (1,3) 2. Point pl(4,2), p2(5,lb- Line 1(р1,р2); // линия: от (4,2) до (5,1) 3. Point pl(4,3), р2(6,4); Rectangle r(pl,p2); // прямоугольник: от (4,3) до р2(б,4) 4. Метод Draw в каждом классе делает наброски рисунка на поверхности рисования. p.DrawO; l.DrawO; r.DrawO;
Методы Draw C++ геометрические классы* Далее следуют объявления C++ для классов Point и Line. Заметьте, что конструктору для класса Line передаются координаты двух точек, определяющих линию. Каждый класс имеет функцию-член Draw, отображающую рисунок в области рисования. Спецификация класса Point ОБЪЯВЛЕНИЕ class Point { private: float x, у; // горизонтальная и вертикальная позиция public: Point (float h, float v); float GetX(void) const; // возвратить координату х float GetY(void) const; // возвратить координату у void Draw(void)Jconst; // нарисовать точку (х,у) >; Класс посредством композиции Line включает два объекта Point. Эти объекты инициализируются конструктором. Спецификация класса Line ОБЪЯВЛЕНИЕ class Line { private: Point PI, P2; // две конечные точки отрезка public: Line (Point a, Point b);// инициализировать PI и Р2 void Draw(void) const; // нарисовать отрезок }; Объекты и наследование Наследование — это интуитивное понятие, из которого мы можем извлекать примеры, основанные на каждодневном опыте. Все из нас наследуют
Проиллюстрируем наследование класса с помощью линейного списка, названного SeqList, который сохраняет информацию в последовательном порядке. Список — это важная и знакомая структура, используемая для ведения инвентаризационных записей, графика деловых встреч, типов и количества необходимых гастрономических товаров и т.д. Наследование возникает, когда мы объявляем упорядоченный список, который является особым типом последовательного списка. Упорядоченный список использует все базовые методы обработки списка из последовательного списка и обеспечивает свою собственную операцию вставки для того, чтобы элементы списка сохранялись в возрастающем порядке. В линейном списке, содержащем N элементов, любой элемент занимает одно из положений от 0 до N-1. Первое положение является передним, а последнее — конечным. На рис. 1.3 показан неупорядоченный список целых чисел с шестью элементами. Рис. 1.3. Неупорядоченный линейный список Базовые операции SeqList включают операцию Insert, которая добавляет новый элемент в конец списка (Рис. 1.4), и операцию Delete, которая удаляет первый элемент списка, соответствующий ключу. Вторая функция удаления, называемая DeleteFront, удаляет первый элемент в списке (Рис. 1.5). Структура определяет размер списка с помощью ListSize и предоставляет операцию Find, выполняющую поиск элемента в списке. Для управления данными пользователь может определить, является ли список пустым, и удалить его операцией ClearList. Insert (10) Рис. 1.4. Вставка значения 10 Delete (45) DeleteFront Рис. 1.5. Удаление элемента 45 и удаление первого элемента в списке Данный класс предоставляет метод GetData, позволяющий клиенту читать значение любого элемента списка. Например, для нахождения максимального значения в списке мы можем начать сканирование списка с нулевого элемента. Процесс заканчивается при достижении конца списка, который определяется с помощью ListSize. В каждом положении следует обновлять максимальное значение, если текущее значение (GetData) больше, чем текущий максимум. Например, для второго элемента списка число 22 сравнивается с
характерные черты от своих родителей такие, как раса, цвет глаз и тип крови. Мы можем думать о родителе как о базе, из которой мы наследуем характерные черты. Взаимосвязь иллюстрируется двумя объектами, связанными стрелкой, острие которой направлено к базовому объекту. Зоология формально изучает наследование у животных. На рис. 1.2 показана иерархия животных для млекопитающих, собак и колли. Млекопитающее — это теплокровное животное, которое имеет шерсть и вскармливает своих детенышей молоком. Собака — это млекопитающие, которое имеет клыки, ест мясо, имеет определенное строение скелета и является общественным животным. Колли — это собака с заостренной мордой, имеющая белый с рыжим окрас и хорошо развитые пастушеские инстинкты. Млекопитающее Теплокровное животное, имеет шерсть, вскармливает детенышей молоком Собака Имеет клыки, ест мясо, имеет определенное строение скелета, является общественным животным Колли Имеет заостренную морду, белый с рыжим окрас, хорошо развитые пастушеские инстинкты Рис. 1.2. Цепочка наследования у животных В иерархической цепочке класс наследует все характерные черты своего класса-предка. Например, собака имеет все черты млекопитающего плюс те, которые отличают ее от кошек, слонов и т.д. Порядок расположения классов указывает что Колли есть собака. Собака есть млекопитающее В этой цепочке класс млекопитающих определяется в качестве базового класса (base class) для собаки, а собака называется производным классом (derived class). Используя аналогию семейного наследования, мы говорим о базовом и производном классах как о родительском классе и классе-наследнике, соответственно. В случае расширенной цепочки наследник наследует характерные черты своего родительского и прародительского класса. Наследование в программировании Объектно-ориентированное программирование предоставляет механизм, посредством которого производному классу разрешается наследовать данные и операции от базового класса. Этот механизм, называемый наследование класса (class inheritance), позволяет производному классу использовать данные и операции, которые были определены ранее в базовом классе. Производный класс может добавлять новые операции или переписывать некоторые операции, так как он устанавливает методы для обработки его данных. Аналогично, ребенок может наследовать дом или автомашину от его (или ее) родителя. Затем он может затем использовать этот дом или автомашину. Если необходимо, наследник может модифицировать дом, чтобы он соответствовал его (или ее) особым условиям.
предыдущим максимумом, равным 3, поэтому текущее максимальное значение заменяется на 22. В конечном счете, число 23 определяется как максимальный элемент в списке. 3 <22 Новым максимумом будет 22 ADT SeqList Данные Неотрицательное целое число, указывающее количество элементов, находящихся в данный момент в списке (размер), и список элементов данных. Операции Коне труктор Начальные значения: Нет Процесс: Установка размера списка на О ListSize Вход: Нет Предусловия: Нет Процесс: Чтение размера списка Выход: Размер списка Постусловия: Нет ListEmpty Вход: Нет Предусловия: Нет Процесс: Проверка размера списка Выход: Возвращать TRUE, если список пустой; в противном случае — возвращать FALSE. Постусловия: Нет ClearList Вход: Нет Предусловия: Нет Процесс: Удаление всех элементов из списка и установка размера списка на 0. Выход: Нет Постусловия: Список пустой Find Вход: Элемент, который необходимо найти в списке. Предусловия: Нет Процесс: Сканирование списка для нахождения соответствующего элемента. Выход: Если соответствующий элемент списка не найден, возвращать FALSE; если он найден, возвращать TRUE и этот элемент. Постусловия: Нет Insert Вход: Элемент для вставки в список Предусловия: Нет
Процесс: Добавление этого элемента в конец списка. Выход: Нет Постусловия: Список имеет новый элемент; его размер увеличивается на 1. Delete Вход: Значение, которое должно быть удалено из списка. Предусловия: Нет Процесс: Сканирование списка и удаление первого найденного элемента в списке. Не выполнять никакого действия, если этот элемент не находится в списке. Выход: Нет Постусловия: Если соответствующий элемент найден, список уменьшается на один элемент. DeleteFront Вход: Нет Предусловия: Список не должен быть пустым. Процесс: Удаление первого элемента из списка. Выход: Возвращать значение удаляемого элемента Постусловия: Список имеет на один элемент меньше. GetData Вход: Положение (pos) в списке. Предусловия: Генерируется ошибка доступа, если pos меньше О или больше 0 (размер -1) Процесс: Получать значение в положении pos в списке. Выход: Значение элемента в положении pos. Постусловия: Нет Конец ADT SeqLlst Упорядоченные списки и наследование Упорядоченный список — это особый тип списка, в котором элементы сохраняются в возрастающем порядке. Как абстрактный тип данных этот особый список получает большинство своих операций из класса SeqList, за исключением операции Insert, которая должна добавлять новый элемент в положение, сохраняющее упорядочение (Рис. 1.6). insert(8) 23 45 Рис. 1.6. Упорядоченный список: Вставка элемента (8) Операции ListSize, ListEmpty, ClearList, Find и GetData независимы от любого упорядочения элементов. Операции Delete и DeleteFront удаляют элемент, но сохраняют остающиеся элементы упорядоченными. Далее следует ADT, отражающий сходство операций упорядоченного списка и SeqList.
ADT OrderedList Данные те же, что и для SeqList ADT Операции Конструктор выполняет конструктор базового класса ListSize тот же, что и для SeqList ADT ListEmpty тот же, что и для SeqList ADT ClearList тот же, что и для SeqList ADT Find тот же, что и для SeqList ADT Delete тот же, что и для SeqList ADT DeleteFront тот же, что и для SeqList ADT GetData тот же, что и для SeqList ADT Insert Вход: Элемент для вставки в список. Предусловия: Нет Процесс: Добавление элемента в положение, сохраняющее упорядочение. Выход: Нет Постусловия: Список имеет новый элемент, и его размер увеличивается на 1. Конец ADT OrderedList Класс OrderedList является производным от класса SeqList. Он наследует операции базового класса и модифицирует операцию Insert для упорядоченной вставки элементов. Класс SeqUst private: детали реализации public Коструктор Find ListEmpty Delete ClearList DeleteFront ListSize GetData Класс OrderedList private: public Конструктор. Insert Повторное использование кода Объектно-ориентированный подход к структурам данных способствует повторному использованию кода, который уже был разработан и протестирован, и может быть вставлен в ваше приложение. Мы уже рассматривали повторное использование кода с композицией. Наследование также является мощным инструментом для этой цели. Например, реализация класса упорядоченного списка требует от нас написания только методов Insert и конструктора. Все другие операции задаются кодом из класса SeqList. Повторное использование кода является важнейшим преимуществом в объектно-ориентированной разработке, поскольку оно сокращает время разработки и способствует однородности приложений и вариантов программного продукта. Например, при модернизации операционной системы к ней добавляются новые функции. В то же время, эта модернизация должна позволять выполнение существующих приложений. Одним из подходов является определение оригинальной операционной системы в качестве базового класса. Модернизированная система действует как производный класс с его новыми функциями и операциями.
Спецификации класса SeqList и OrderedList* Формальное описание класса SeqList приводится в главе 4. В этом разделе мы даем только спецификацию класса для того, чтобы вы могли соотнести этот класс и его методы с очень общим ADT. Класс OrderedList определяется для иллюстрации наследования. Тип элемента данных в списке представлен параметрическим именем DataType. Спецификация класса SeqList ОБЪЯВЛЕНИЕ class SeqList { private: // массив для хранения списка и число элементов текущего списка DataType listitem[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 DataType& 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) ; // вставить элемент по порядку }; ОПИСАНИЕ
Insert замещает метод базового класса с тем же именем. Она проходит по всему списку и вставляет элемент в положение, сохраняющее упорядочение списка. 1.5. Приложения с наследованием классов Понятие наследования класса имеет важное применение в программировании графического пользовательского интерфейса (grafical user interface — GUI) и системах баз данных. Графические приложения фокусируют внимание на окнах, меню, диалоговых окнах и так далее. Базовое окно — это структура данных с данными и операциями, являющимися общими для всех типов окон. Операции включают открытие окна, создание или изменение заголовка окна, установку линеек прокрутки и областей перетаскивания и т.д. Приложения GUI состоят из классов диалога, классов меню, классов текстовых окон и так далее, которые наследуют базовую структуру и операции от базового класса окна. Например, следующая иерархия класса включает класс Dialog и класс TextEdit, порожденные от класса Window. Класс Window Класс Dialog Класс TextEdit Этот пример класса Window показывает одиночное наследование, в котором производный класс имеет только один базовый класс. Однако при множественном наследовании (multiple inheritance) класс порождается от двух или более базовых классов. Некоторые приложения GUI используют это свойство. Программа текстового процессора объединяет редактор (editor) с менеджером просмотра (view manager) для пролистывания текста в некотором окне. Редактор читает строку символов и вносит изменения, вставляя и удаляя строки, и вводя информацию форматирования. Менеджер просмотра отвечает за копирование текста на экран с использованием информации о шрифте и окне. Редактор экрана может быть определен как производный класс, использующий класс Editor (редактора) и класс View (менеджера просмотра) в качестве базовых классов. Множественное наследование Класс View Класс Editor Класс Screen Editor
1.6. Разработка объектно-ориентированных программ Большие программные системы становятся все более сложными и требуют новых подходов к разработке. Традиционная разработка использует модель управления, которая предполагает наличие администратора верхнего уровня (top administrator), понимающего систему и поручающего задачи менеджерам. Такая нисходящая программная разработка (top-down program design) рассматривает систему как набор подпрограмм, состоящий из нескольких слоев. Главная программа Подпрограмма! Подпрограмма2 ПодпрограммаЗ На верхнем уровне главная программа управляет работой системы, выполняя последовательность вызовов подпрограмм, которые производят вычисления и возвращают информацию. Эти подпрограммы могут далее поручать выполнение некоторых задач другим подпрограммам. Элементы нисходящей программной разработки необходимы для всех систем. Однако, когда задача становится слишком большой, этот подход может оказаться неудачным, поскольку его сложность подавляет способность отдельного человека управлять такой иерархией подпрограмм. Кроме того, простые структурные изменения в подпрограммах около вершины иерархии могут потребовать дорогих и занимающих длительное время изменений в алгоритмах для подпрограмм, находящихся в нижней части диаграммы. Объектно-ориентированное программирование использует другую модель системной разработки. Оно рассматривает большую систему как набор объектов (агентов), которые взаимодействуют для выполнения задач. Каждый объект имеет методы, управляющие его данными. Целью программной разработки является создание читабельной и поддерживаемой архитектуры, которая может быть расширена, как диктует необходимость. Хорошо организованные системы легче понимать, разрабатывать и отлаживать. Все философии разработки пытаются преодолеть сложность программной системы с помощью принципа разделения и подчинения. Нисходящая программная разработка рассматривает систему как набор функциональных модулей, состоящий из слоев. Объектно-ориентированное программирование использует объекты как основу разработки. Не существует единственного способа программной разработки и строго определенного процесса, которому необходимо следовать. Разработка программ — это вид деятельности человека, который должен включать творческую свободу и гибкость. В этой книге мы обсуждаем общий подход, определяющий методологию разработки программного продукта (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]; // список очков двух костей // класс генератора случайных чисел, используемый для // моделирования бросаний RandcmNumber rnd; public: // конструктор Dice(void); // методы void Toss(void); int Total(void) const; void DisplayToss(void) const; }; Разработка управления процессом. Для построения диаграммы бросания костей главный модуль вызывает три подпрограммы, которые выполняют основные действия программы. Функция SimulateDieToss использует методы из класса Dice для бросания костей N раз. Draw Axes вызывает метод Draw в классе Line для рисования осей координат графика, a Plot рисует серию прямоугольников, которые образуют столбцовую диаграмму. Функция Plot вызывает Мах для определения максимального количества появлений любой возможной суммы. Это значение позволяет нам вычислить относительную высоту каждого прямоугольника диаграммы. Структурное дерево этой программы показано на рис. 1.8. Далее следуют объявления для каждого управляющего модуля в структурном дереве. Главная SimulateDieToss DrawAxes Plot Dice Line Rectangle Max Рис. 1.8. Древовидная структура программы Dice Graph
main Передаваемые параметры: Нет Выполнение: Запросить у пользователя количество бросаний костей в моделировании. Вызвать функцию SimulateDieToss для выполнения бросаний и записать количество раз, когда возникает каждая возможная сумма: (2 < Total <. 12). Нарисовать оси координат функцией DrawAxes и создать столбцовую диаграмму функцией Plot. Возвращаемые параметры: Нет SimulateDieToss Передаваемые параметры: tossTotal Массив tossTotal содержит количество появлений каждой суммы в диапазоне от 2 до 12. tossTotal [i] — это количество появлений суммы i при бросании костей tossCount раз. tossCount Количество бросаний 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 Передаваемый параметр: tossTotal Массив, содержащий количество появлений каждой возможной суммы, вычисленной в SimulateDieToss. Выполнение: Поиск максимальной суммы (maxTotal) в массиве tossTotal для диапазона индекса 2—12. Затем каждый элемент в массиве генерирует соответствующую часть (tossTotal [i]/ maxTotal) вертикального'диапазона графика. Разделить 6-дюймовый интервал оси х от (1.0, 3.0) до (7.0, 3.0) на 23 равных сегмента и построить соответствующие прямоугольники, чьи высоты — это функция (tossTotal [i]) / maxTotal*2.75, 2 < i й 12. Возвращаемые параметры: Нет Кодирование*. Завершает разработку программы кодирование управляющих модулей. Например, следующей программой задаются управляющие модули для dice-диаграммы: Программа 1.2. Диаграмма бросания костей Главная программа запрашивает пользователя ввести количество бросаний костей для моделирования. Наш запуск программы тестирует случай 500 000 бросаний костей. В конце выполнения задачи система ожидает нажатия клавиши или щелчка кнопкой мыши для завершения работы. Классы Line и Rectangle содержатся в файле figures.h, а класс Dice содержится в файле dice.h. Подпрограммы рисования графических примитивов находятся в graphlib.h. #include <iostream.h> #include "figures.h" #include "dice.h" iinclude "graphlib.h" // Бросание двух костей tossCount раз. // Запись числа выпавших "двоек" в tossTotal[2], // "троек" — в tossTotal[3] и так далее void SimulateDieToss( long tossTotal[], long tossCount ) { long tossEvent; int i; Dice D; // очистить каждый элемент массива tossTotal for (i=2; i 12; i++) tossTotal[i] = 0; // Бросание костей tossCount раз for (tossEvent = 1; tossEvent tossCount; tossEvent++ ) { D.TossO; // бросание костей tossTotal[D.Total()]++; // увеличение счетчика }
} // Нахождение максимального значения в массиве их п элементов long Max (long а [], int п) { long lmax - а[0] ; int i; for (i=l; i n; i++) if (a [i] lmax) lmax = a[i]; return lmax; } // Рисование двух осей 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)); HorizontalAxis.Draw(); } // Рисование графического столбца void Plot (long tossTotal [ ]) { const float vertspan = 3.0, scaleHeight = 2.75; float x, rectHeight, dx; long maxTotal; int i; // Нахождение максимального значения в массиве tossTotal. // Поиск для индексов в диапазоне 2-12 maxTotal =Мах(&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; SimulateDieToss(tossTotal, numTosses); //бросание InitGraphics (); // инициализация графической системы DrawAxes (); // рисование осей Plot (tossTotal); // построение графика ViewPause (); // ожидание нажатия клавиши или щечка мышью ShutdownGraphi.es (); // закрытие графической системы } /* Запуск программы рг01_02.срр Введите число бросаний: 50000 */ 1.7. Тестирование и сопровождение программы Использование объектно-ориентированного программирования способствует созданию систем, позволяющих выполнять независимое тестирование объектов и повторно использовать написанные ранее классы. Эти преимущества уменьшают риск появления ошибок при создании сложных программных систем, поскольку они развиваются, расширяясь из меньших систем, в которых вы уверены. Тестирование выполняется в процессе разработки программной системы. Объектное тестирование Тип класса — это самодостаточная структура данных, которая может передавать информацию внешнему компоненту программы и от него. Мы можем тестировать каждый класс, создав короткую программу, вызывающую каждый public-метод. Эта дополнительная программа иллюстрирует тестирование методов в классе Dice. Тестирование управляющего модуля Программа должна быть до конца протестирована путем ее выполнения с тщательно отобранными данными. Перед началом выполнения этой задачи правильность программы часто может быть оценена с помощью структурного сквозного контроля (structure walk-through), в котором программист показывает полную структуру и реализацию другому программисту и объясняет, что
именно происходит, от объектной разработки и до разработки управляющих модулей. Этот процесс часто вскрывает концептуальные ошибки, проясняет логику программы и подсказывает тесты, которые могут быть выполнены. Современные компиляторы поддерживают отладчик на уровне исходного кода (source-level debugger), который позволяет отслеживать отдельные команды и останавливает работу в выбранных контрольных точках. Во время управляемого выполнения значения переменных могут отображаться на экране, позволяя сравнивать выборочные части программы до и после возникновения ошибки. Основной проверкой правильности программы является ее выполнение с наборами хорошо подобранных данных. Выполняя тестирование, программист убеждается в правильности программы. Программисту следует также использовать неверные входные данные для проверки кода на устойчивость к ошибкам (robustness), которая определяет способность программы идентифицировать неверные данные и возвращать сообщения об ошибках. Данные для тестирования отбираются с помощью различных подходов. Одним из методов является так называемый метод "надеюсь и молюсь", когда программист запускает программу несколько раз с простыми данными и, если она работает, то он (или она) продолжает разработку. Более разумным подходом является выбор серии входных данных, которая тестирует различные алгоритмы в программе. Ввод должен включать простые, типовые и экстремальные данные, которые тестируют специальные случаи в nporpaiv ме, и неверные данные, проверяющие способность программы реагировать на ошибки ввода. Полностью структурированный тест рассматривает логическую структуру программы. Этот подход предполагает, что программа не тестируется до конца, если некоторая часть кода не была уже выполнена. Исчерпывающее тестирование требует от программиста выбора данных для проверки различных алгоритмов в программе: каждого условного оператора, каждой конструкции цикла, каждого вызова функции и так далее. Некоторые компиляторы предоставляют функции протоколирования, которые указывают количество вызовов функций в программе. Программное сопровождение и документирование Для удовлетворения дополнительным требованиям компьютерные программы часто нуждаются в обновлении. Объектно-ориентированное программирование упрощает программное сопровождение. Наследование классов делает возможным повторное использование существующих программ. Эти инструменты эффективны при поддержке хорошим программным документированием, описывающим классы и управляющие модули для того, чтобы помогать пользователям понимать программу и ее правильное выполнение. Большие программы обычно поддерживаются руководством пользователя, которое включает информацию по установке программного обеспечения и одну или более обучающие программы для иллюстрации центральных возможностей программного продукта. Объектные спецификации и структурные диаграммы управляющих модулей являются превосходными инструментами программного документирования. В исходном программном коде комментарии описывают действие отдельных функций и классов. Комментарии также помещаются там, где логика какого-либо алгоритма является особенно трудной.
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 T> 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 WoodFrame Stucco VinylSided Paint Paint Paint
объектно-ориентированного программирования особые малярные задачи для каждого вида дома задаются в производном классе, который наследует базовый класс House. Допустим, что каждый производный класс имеет операцию Paint. Класс House имеет операцию Paint, которая задается как виртуальная функция. Предположим, что BigWoody — это объект типа Wood- Frame. Мы можем указать BigWoody покрасить дом, вызывая явно операцию Paint. Это называется статическим связыванием (static binding). BigWoody. Paint ( ); // static binding Допустим, однако, что подрядчик имеет список адресов домов, которые нуждаются в покраске, и что он передает сообщение своей команде маляров пойти по адресам в списке и покрасить эти дома. В данном случае каждое сообщение привязано не к определенному дому, а скорее — к адресу дома- объекта в списке. Команда маляров приходит к дому и выбирает правильную малярную операцию Paint, после того, как она увидит тип дома. Этот процесс известен как динамическое связывание (dynamic binding). (House at address 414) .Paint ( ); // dynamic 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 Какова связь между С и C++? 1.10 Перечислите некоторые компиляторы C++ общего использования. К каким компиляторам вы имеете доступ? Для каждого компилятора укажите, предоставляет ли он интегрированную среду разработки (IDE), в которой имеются редактор, компилятор, компоновщик, отладчик и исполняющая система. Альтернативой является компилятор с командной строкой.
1.11 (а) Объясните, что подразумевается под полиморфизмом. (б) Графическая система инкапсулирует операции окна в базовом классе 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++ использует иерархию наследования для реализации файлов. Другое приложение показывает обработку трех различных типов файлов. .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 являются целыми выражениями, а N - целая переменная. Присваивание = N - и Присваивает значение выражения и переменной N Бинарные арифметические операции + u + v Сложение u — v Вычитание * u * v Умножение / u / v Деление нацело % u % v Остаток от деления нацело Унарные арифметические операции -и Изменение знака (унарный минус) + +и То же, что и и (унарный плюс) Операции отношения (Выражение отношения — это истинность заданного условия) =* и == v Результат — TRUE, если и эквивалентно v !=* и != v Результат — TRUE, если и не эквивалентно v < и < v Результат — TRUE, если и меньше v <= и <= v Результат — TRUE, если и меньше, либо равно v > и > v Результат — TRUE, если и больше v >= и >== v Результат — TRUE, если и больше, либо равно v Конец ADT Integer Пример 2.1 3+5 (выражение имеет значение 8) val =25/20 (val - 1) rem * 25 % 20 (rem = 5)
Компьютерное хранение целых чисел Реализация целых чисел обеспечивается описаниями типа языка программирования и компьютерными аппаратными средствами. Компьютерные системы сохраняют целые числа в блоках памяти с фиксированной длиной. Результирующая область значений лежит в ограниченном диапазоне. Длина блока памяти и диапазон значений зависят от реализации. Для того, чтобы способствовать некоторой стандартизации, языки программирования предоставляют простые встроенные типы данных для коротких и длинных целых. Когда требуются очень большие целые значения, приложение должно предоставлять библиотеку подпрограмм для выполнения операций. Библиотека целых операций может расширить реализацию целых до любой длины, хотя эти подпрограммы значительно снижают эффективность системы, исполняющей приложения. В компьютере целые хранятся как двоичные (бинарные) числа (binary numbers), состоящие из различных последовательностей цифр 0 и 1. Это представление моделируется на базе 10 (или десятичной системы, которая использует цифры 0, 1, 2 ,..., 9). Десятичное число сохраняется как набор цифр d0 , d2 , d2f и т.д., представляющий степени 10. Например, й-разрядное число Nw = dki dk2 „di... d2 d0 , для О < dt < 9 представляет Nio = dk.jflO*11)* dk.2(10k2)+...+di(10i)+...+ drflO'J+do (10°) Индекс 10 указывает на то, что N записано как десятичное число. Например, десятичное целое из четырех цифр 2589 представляет 258910 = 2(103)+ 5(102)+ 8(10!)+ 9(10°) = 2(1000)+ 5(100)4- 8(10)+ 9 Двоичные целые используют цифры 0 и 1 и степени 2-х (2° = 1, 21 = 2, 22 = 4, 23 = 8, 24 — 16 и так далее). Например, 1310 имеет двоичное представление 1310 = 1(23) + 1(22) + 0(2Х) + 1(2°) = 11012 Двоичная цифра называется бит (bit), сокращение, включающее Ы от binary и t — от digit. В общем виде /г-разрядное или fe-битовое двоичное число имеет представление: N2 ^ ^А-1 &fe-2 ••• Ь} ... bj bp = bk.1(2kl)+bk2(2k'2)+... +Ь^2г;+... +b1(21)+b0(2°), 0 <bx< 1 Следующее 6-битовое число дает двоичное представление 42. Десятичное значение двоичного числа вычисляется добавлением членов в сумме: 1010102= 1(25) + 0(24) + 1(23) + 0(22) + Ц21) + 0(2°) = 4210 Пример 2.2 Вычислить десятичное значение двоичного числа: 1101012= 1(25) + 1(24) + 0(23) + 1(22) + 0(2*) + 1(2°) = 5310 100001102=1(27) + 1(22) + Ц21) = 13410
Преобразование десятичного числа в его двоичный эквивалент может быть выполнено нахождением самой большой степени 2-х, которая меньше, чем это число или равна этому числу. Прогрессия степеней 2-х включает значения 1, 2, 4, 8, 16, 32, 64 и так далее. Это дает ведущую цифру в этом двоичном представлении. Остающиеся степени 2-х заполняются по убывающей до 0. Например, рассмотрим значение 35. Числом с самой большой степенью 2-х, меньшим, чем 35, является 32 = 25, это подразумевает, что 35 является 6-разрядным двоичным числом: 3510=1(32) + 0(16) + 0(8) + 0(4) + 1(2) + 1 = 1000112 Чистые двоичные числа являются просто суммой степеней 2-х. Они не имеют знака, ассоциированного с ними, и о них говорят как о беззнаковых числах (unsigned numbers). Эти числа представляют положительные целые. Отрицательные целые используют либо представление точного дополнения, либо — величины со знаком. В любом формате специальный бит, называемый знаковым битом (sign bit), указывает знак числа. Данные в памяти Числа сохраняются в памяти как последовательности двоичных цифр фиксированной длины. Распространенные длины включают 8, 16, и 32 бита. Последовательность битов измеряется 8-битовой единицей, называемой байт (byte). 0 0 10 0 0 11 Число 35 как байт В таблице 2.1 приводится диапазон беззнаковых чисел и чисел со знаком для этих распространенных длин. Таблица 2.1 Диапазоны чисел и размер в байтах Размер 8 (1 байт) 16 (2 байта) 32 (4 байта) Диапазон беззнакового числа 0 .. 255=28 " 1 0 .. 65535=216Н 0 .. 4294967295=232-1 Диапазон числа со знаком -27 =-128 .. 127=27-1 -215=-32768 .. 32767=215Н -231 .. 231-1 Компьютерная память — это последовательность байтов, к которой обращаются с помощью адресов 0, 1, 2, 3 и так далее. Адрес (address) целого в памяти — это местоположение первого байта последовательности. Рис.2.1 иллюстрирует вид памяти с числом 8710 = 10101112, находящимся в одном байте с адресом 3, и числом 50010 = 00000001111101002 с адресом 4. Адрес 0 1 2 01010111 3 00000001 11110100 4 5 Рис. 2.1. Вид памяти
Представление целых в языке C++ Целыми типами в языке C++ являются: int, short int и long int. Тип short int (short) предоставляет 16-битовые (2-байтные) целые значения в диапазоне от -32768 до 32767. Тип long int (long) предоставляет самый широкий диапазон целых значений и в большинстве систем реализуется с 32 битами (4 байтами), и поэтому его диапазон составляет от -231 до 231 - 1. Общий тип int идентифицирует целые, чья длина в битах зависит от компьютера и компилятора. Обычно компиляторы используют 16-битовые или 32-битовые целые. В некоторых случаях пользователь имеет возможность выбирать длину целого в качестве опции. Целые типы данных устанавливают область значений данных и задают арифметические операторы и операторы отношения. Каждый тип данных задает реализацию целого ADT с ограничением, что целые значения находятся в некотором конечном диапазоне. 2.2. Символьные типы Символьные данные включают алфавитно-цифровые элементы, которые определяют заглавные и строчные буквы, цифры, знаки пунктуации и специальные символы. Компьютерная индустрия использует различные представления символов для приложений. Набор символов ASCII из 128 элементов имеет самое широкое применение для текстовой обработки, ввода и вывода текста и передачи данных. Мы используем набор ASCII для нашего символьного ADT. Подобно целым, символы ASCII включают отношение порядка, определяющее набор операторов отношения. В случае алфавитных символов буквы следуют словарному порядку. В этом отношении все заглавные буквы меньше, чем строчные: T<W, b<d, T<b ADT Character Данные Набор символов ASCII Операции Присваивание Значение символа может присваиваться символьной переменной. Операция отношения Шесть стандартных операций отношения применяются к символам с использованием ASCII словарного отношения порядка. Конец ADT Character Символы ASCII Большинство компьютерных систем используют стандартную систему кодирования ASCII для символьного представления. Символы ASCII хранятся как 7-битовый целый код в 8-битовом числе. 27 = 128 различных кодов подразделяются на 95 печатаемых и 33 управляющих символа. Управляющий символ используется при передаче данных и вызывает выполнение управляющей функции устройством, например, перемещение курсора дисплея на одну строку вниз.
В таблице 2.2 показан печатаемый набор символов ASCII. Символ "пробел" представлен как ♦. Десятичный код для каждого символа задается десятичной позицией, которой соответствует номер строки, и единичной, — которой соответствует номер столбца. Например, символ "Т" имеет ASCII-значение 8410 и хранится в двоичном виде как 010101002. В символьном наборе ASCII десятичные цифры и алфавитные символы находятся в правильно определенных диацазонах (табл. 2.3). Это делает более легким преобразование между заглавными и строчными буквами и преобразование цифры ASCII ('0' ... '9') в соответствующее число (0 ... 9). Набор печатаемых символов ASCII Таблица 2.2 Левая цифра 3 4 5 6 7 8 9 10 11 12 Правая цифра 0 ( 2 < F Р Z d п X 1 ) 3 = G Q [ е о У 2 ♦ * 4 > Н R \ f Р z 3 j + 5 • I S ] g q { 4 - г 6 @ J T л h г 5 # • 7 A К U i s } 6 $ / 8 В L V > j t - 7 % 0 9 С M w a k u 8 & 1 • D N X b I V 9 i * E 0 Y с m w Коды 00-31 и 127 - управляющие символы, которые являются непечатаемыми. Таблица 2.3 Символьные диапазоны ASCII Символы ASCII Пробел Десятичный знак Символ верхнего регистра Символ нижнего регистра Десятичный 32 48-57 65-90 97-122 Двоичный 00100000 00110000-00111001 01000001-01011010 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 (V) = 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. Число с фиксированной точкой является просто особым случаем числа с плавающей точкой с порядком 0. Как в случае с целыми и символами, вещественные числа составляют абстрактный тип данных. Стандартные арифметические и операции отношения применяются к вещественным числам, используемым вместо целых. ADT Real Данные Числа, описанные в формате с фиксированной или плавающей точкой. Операции • Приев аивание Вещественное выражение может быть присвоено действительной переменной. Арифметические операторы Стандартные двоичные и унарные арифметические операции применяются к вещественным числам вместо целых. Никаких других операторов не имеется Операции отношения К вещественным числам применяются шесть стандартных операций отношения. конец ADT Real Представление вещественных чисел Как и для целых чисел, область вещественного числа не имеет предела. Значения вещественных чисел являются безграничными и в отрицательном, и в положительном направлении, а мантисса распределяет действительные числа на последовательность точек числовой оси. Вещественные числа реализуются в конечном блоке памяти, ограничивающем диапазон значений и образующем дискретные точки числовой оси.
В течение многих лет специалисты использовали разнообразные форматы для хранения чисел с плавающей точкой. Формат IEEE (Института инженеров по электротехнике и радиоэлектронике) является широко используемым стандартом. Вы знакомы с вещественными числами, которые используют формат с фиксированной точкой. Таким является число, разделяемое на целую часть и дробную, цифры которой умножаются на 1/10, 1/100, 1/1000 и так далее. Десятичная точка разделяет эти части: 25.638 = 2(10!) + 5(10°) 4- 6(101) + 3(10'2) +8(10"3) Как в случае с целыми числами, имеются соответствующие двоичные представления для вещественных чисел с фиксированной точкой. Эти числа содержат целую и двоичную дробную части и двоичную точку с дробными цифрами, соответствующими 1/2, 1/4, 1/8 и т. д. Общая форма такого представления: N = ьп... Ъ0 . fe-i^". = Ъп2п + ... + Ъ02° + Ь^2г + Ъ-22~2 +... Например: 1011.1102 = 1(23) + Ц21) + 1(2°) + Ц2-1) + 1(2"2) + 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 - 0.4062510 Преобразовать десятичное число в двоичное число с плавающей точкой: 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 = ±£>nDni - - • DjD0 . d2d2. . . dn 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 является 0, а значение True — это 1. Переменная Done определяется как булева с первоначальным значением False. enum Boolean (False, True); Boolean Done = False; 2. Месяцы года объявляются как тип перечисления. По умолчанию первоначальным значением Jan является 0. Однако, целая последовательность может начинаться с другого значения путем присваивания этого значения первому элементу. В этом случае 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 байтов вправо от р. Тип данных j char | int (2 байта) | double (4 байта) Текущий адрес р = 5000 р = 5000 р = 5000 Новый адрес р + 1 = 5001 р + 3 = 5000 + 3*2 = 5006 р - б = 5000 -6*4 = 4976 | Указатель использует адресный оператор "&", который возвращает адрес в памяти элемента данных. Напротив, оператор "*" ссылается на данные, ассоциированные со значением указателя. Упорядочение указателей происходит путем сравнения их беззнаковых целых значений. Динамическая память (dynamic memory) — это новая память распределяемая во время выполнения программы. Динамическая память отличается от статической памяти (static memory), чье наличие определяется до начала выполнения программы. Динамическая память описывается в главе 8. Оператор new принимает тип Т, динамически выделяет память для элемента типа Т и возвращает указатель на память, которую он выделил. Оператор 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. Арифметическая + u + i Указывает на элемент, размещенный на i элементов данных правее элемента, на который ссылается и. ~ и — i Указывает на элемент, размещенный на i элементов левее элемента, на который ссылается и. ~~ и — v Возвращает число элементов базового типа, которые находятся между двумя указателями. Отношения К указателям применяются шесть стандартных операторов отношения путем сравнения их беззнаковых целых значений. Конец ADT Pointer
Значения указателя Значение указателя — это адрес памяти, использующий 16, 32 или более битов в зависимости от машинной архитектуры. В качестве примера: PC — это указатель на char (1 байт), а РХ — это указатель на short int (2 байта). char str[] = ABCDEFG; char *PC = str; //PC указывает на строку str short X = 33; short *PC = &X; // РХ указывает на Х типа short Следующие операторы иллюстрируют основные операции с указателями. cout « *РС « endl; // напечатать 'А' РС+= 4; // сдвинуть PC вправо на четыре символа cout « *РС « endl; // напечатать 'Е' PC—; // сдвинуть PC влево на один символ cout « *РС « endl; // напечатать 'D' cout « *РХ + 3 « endl; // напечать 36 = 33 + 3 Операции new и delete обсуждаются в главе 8. 2.6. Массив (array) Массив является примером набора данных. Одномерный массив — это конечный, последовательный список элементов одного и того же типа данных — однородный массив (homogeneous array). Последовательность определяет первый элемент, второй элемент и так далее. С каждым элементом ассоциирован целый индекс (index), определяющий позицию элемента в списке. Массив имеет оператор индекса, который делает возможным прямой доступ (direct access) к элементам в списке при сохранении или возвращении какого-либо элемента. А0 А, А2 А3 ADT Array Данные Набор N — элементов одного и того же типа данных; индексы выбираются из диапазона целых от 0 до N — 1, которые определяют позицию элементов в списке и обеспечивают прямой доступ к элементам. Индекс 0 ссылается на первый элемент в этом списке, индекс 1 ссылается на второй элемент и т.д. Операции Индексирование [ ] Вход: Индекс Предусловия: Индекс находится в диапазоне от 0 до N — 1. Процесс: В правой части оператора присваивания оператор индексирования возвращает данные из элемента; в левой части оператора присваивания оператор индексирования возвращает адрес элемента массива, который сохраняет правую часть выражения. Выход: Если операция индексирования находится в правой части оператора присваивания, то операция выбирает данные из массива и возвращает эти данные клиенту. Постусловия: Если операция индексирования находится в левой части, то заменяется соответствующий элемент массива. Конец ADT Array
Встроенный тип массива C++ В качестве части своего базового синтаксиса C++ предоставляет встроенный тип статического массива, который определяет список элементов одного и того же типа. Явное объявление задает постоянный размер массива N и указывает, что индексы находятся в диапазоне от 0 до N-1. Объявление в примере 2.6 определяет статический массив C++. В главе 8 мы будем создавать динамические массивы, используя указатели. Пример 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 e A[i] ; // возвратить данные из A[i] и присвоить их переменной t A[i] - A[i + 1] = х; // присвоить х элементу A[i +1]. Второе присваивание сохраняет данные из A[i +1] в A[i]. Сохранение одномерных массивов C++ одномерный массив А логически сохраняется как последовательное упорядочение элементов в памяти. Каждый элемент относится к одному и тому же типу данных. А[0] A[i] t i Address A[0] Address A[i] В C++ имя массива является константой и рассматривается как адрес первого элемента этого массива. Так, в объявлении Type A[ArraySize];
имя массива А является константой и местоположением в памяти первого элемента А[0]. Элементы А[1], А[2] и т.д. следуют за ним последовательно. Допустим, что sizeof(Type) = М, весь массив А занимает М * ArraySize байтов. А[0] А[1] А[2] А[3] Компилятор задает таблицу, называемую дескриптор массива (dope vector), для ведения записи характеристик массива. Таблица включает информацию о размере каждого элемента, начальный адрес массива и количество элементов в этом массиве: Начальный адрес: А Количество элементов массива: ArraySize Размер типа: М = sizeof(Type) Эта таблица используется компилятором также для реализации функции доступа (access function), которая определяет адрес элемента в памяти. Функция ArrayAccess использует начальный адрес массива и размер типа данных для установки индекса I по адресу А[1]: Adress A[I] « ArrayAccess (A, I, M) ArrayAccess задается: ArrayAccess (А, I, М) = А + I * М; Пример 2.7 Предположим, что float сохраняется с использованием 4 байт (sizeof(float) = 4) и массив Height начинается в памяти по адресу 20000. float Height [35]; Элемент массива Height[18] размещается по адресу 20000 + 18 * 4 = 20072 Границы массива ADT массива предполагает, что индексы находятся в целом диапазоне от 0 до N — 1, где N — это размер массива. То же самое и в C++. В действительности, большинство компиляторов C++ при доступе к элементу массива не генерируют код, который тестирует, находится ли индекс вне пределов массива. Например, следующая последовательность будет приниматься большинством компиляторов: int V = 20; int A[20]; //размер массива 20; индексный диапазон 0—19 A[V] =0; //индекс V больше, чем верхний предел
Массив занимает память в области данных пользователя. Функция доступа к массиву определяет адрес в памяти для отдельного элемента, и обычно не производится проверки того, находится ли адрес на самом деле в диапазоне элементов массива. В результате C++ программа может использовать индексы, находящиеся вне указанного диапазона. Адреса для элементов, которые находятся вне диапазона массива, могут все же находиться в области данных пользователя. В процессе выполнения программа может замещать другие переменные и вызывать нежелательные ошибки. На рисунке 2.3 показан вид программы в памяти. System Memory User Program User Data Block int N int arr[20]; float D; Array Space Рис 2.З. Размещение памяти в интервале данных пользователя Некоторые компиляторы реализуют проверку индекса массива, генерируя код времени исполнения для проверки того, находятся ли индексы массива внутри диапазона. Поскольку дополнительный код замедляет выполнение, большинство программистов используют проверку массива только во время программной разработки. Как только код отлажен, эта опция выключается, и программа перекомпилируется на более эффективный код. Другим подходом к проблеме является разработка "надежного массива", который реагирует на неправильные индексные ссылки и выводит сообщение об ошибке. Надежные массивы разрабатываются в главе 8. Двумерные массивы Двумерный массив, часто называемый матрицей, является структурированным типом данных, который создается путем вложения одномерных массивов. Доступ к элементам выполняется по индексам строк и столбцов. Например, на следующем рисунке представлен массив Т из 32 элементов с 4 строками и 8 столбцами. Значение 10 доступно с помощью пары индексов (строка, столбец) (1, 2), а -3 — с помощью индексов (2, 6). Концепция двумерного массива может быть расширена для охвата основных многомерных массивов, элементы в которых доступны с помощью трех или более индексов. Двумерные массивы имеют примененние в таких различных областях, как обработка данных и численное решение дифференциальных уравнений в частных производных.
Столбец Строка В С+4- объявление двумерного массива Т определяет количество строк, количество столбцов и тип данных элементов массива: type T [RowCount] [ColumnCount]; Ссылка на элементы массива Т производится с помощью индексов строки и столбца: T[i] [j], 0 <= i <= RowCount - 1, 0 <= j <= ColumnCount — 1 Например, матрица Т — это массив целых размером 4x8: int T[4] [8]; Значение Т[1][2] = 10 и Т[2][6] = -3. Мы можем представлять двумерный массив как список одномерных массивов. Например, Т[0] — это строка 0, которая состоит из ColumnCount отдельных элементов. Данная концепция полезна, когда двумерный массив передается в качестве параметра. Нотация int Т[][8] указывает на то, что Т является списком из 8-элементных массивов. Сохранение двумерных массивов Двумерный массив может инициализироваться присваиванием элементам по одной строке каждый раз. Например, массив Т задает таблицу размером 3x4: int T[3] [4] = {{20, 5, - 30, 0}, {-40, 15, 100, 80}, {3, 0, 0 -1)}; Как массив, элементы сохраняются в следующем порядке: первая строка, вторая строка, третья строка (рис. 2.4). Для доступа к элементу в памяти компилятор расширяет дескриптор массива для включения информации о количестве столбцов и длине каждой строки и использует новую функцию доступа, называемую Matrix Access, для возвращения адреса элемента.
Столбец S * 20 -40 3 5 15 0 -30 100 0 0 80 -1 Начальный адрес: Т Количество арок: RowCount Количество столбцов: ColumnCount Размер типа: М = sizeof(Type) Длина строки: RS = M*ColumnCount //длина всей строки Функция MatrixAccess принимает пару индексов строки и столбца (I,J) и возвращает адрес элемента T[I][J]: Address T[I] [J] = MatrixAccess (T, I, J) - T + (I * RS) + (J * M) Значение (I * RS) дает количество байтов, необходимое для хранения I строк данных. Значение (J * М) дает количество байтов для хранения первых J элементов в строке I. Построчное хранение массива 20 5 -30 0 Строка#0 -40 15 100 80 Строка#1 3 0 0 -1 Строка#2 Рис 2.4. Хранение матрицы Т Пример 2.8 Пусть Т будет матрицей 3x4 на рис. 2.4. Предположим, что длина целого равна 2, и матрица хранится в памяти по адресу 1000. Начальный адрес: 1000 Количество строк: 3 Количество столбцов: 4 Размер типа: 2 = sizeof(int) Длина строки: 8 = 2*4// длина всей строки 20 5 -30 0 / Строка#0 -40 15 100 80 / Строка#1 3 0 0 -1 / Строка#2
1. Адреса для строк в памяти следующие: Строка 0: Адрес 1000 Строка 1: Адрес 1000 + 1*8 = 1008 (строка — это 8 байт) Строка 2: Адрес 1000 + 2*8 = 1016 2. Адресом Т[1][3] является: ArrayAccess (Т, 1, 3)= 1000 + (1 * 8) + (2 * 3) = 1014 2.7. Строковые константы и переменные Массив — это структурированный тип данных, содержащий однородный список элементов. Особая форма массива содержит символьные данные, которые определяют имена, слова, предложения и так далее. Структура, называемая строкой (string), обращается с символами как с одним объектом и предоставляет операции для доступа к последовательностям символов в строке. Строка является важнейшей структурой данных для большинства приложений, использующих алфавитно-цифровые данные. Эта структура необходима для управления текстовой обработкой с ее операциями редактирования, ее алгоритмами поиска/замены и т. д. Например, лингвисту может потребоваться информация о количестве появлений определенного слова в документе, или программист может использовать шаблоны поиска/замены для изменения исходного кода в документе. Большинство языков объявляют строковые структуры и предоставляют встроенные операторы и библиотечные функции для управления строками. Для определения длины строки структура может включать 0 в конце строки (строка с нулевым завершающим символом или NULL-символом — NULL-terminated string) или отдельный параметр длины. Следующие представления строки содержат 6-символьную строку STRING. Строка с нулевым символом в конце S т R 1 N G NULL Строка со счетчиком длины 6 S т R 1 N G Длина Серия операций обрабатывает строку как единый блок символов. Например, мы можем определить длину строки, копировать одну строку в другую, объединять строки (конкатенация) и обрабатывать подстроки операциями вставки, удаления и отождествления* Строки имеют также операцию сравнения, позволяющую их упорядочивать. Эта операция использует ASCII-упорядочивание. Например:
"Baker" меньше, чем "Martin" // В следует перед М "Smith" меньше, чем "Smithson" "Barber" следует перед "barber" //заглавная В предшествует строчной b "123Stop" меньше, чем "AAA" //числа предшествуют буквам 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 и одиночный символ СН
Предусловия: Нет Процесс: Поиск STR для последнего появления символа СН. Выход: Возвращать адрес места, содержащего последнее появление СН в STR или 0, если этот символ не найден. Постусловия: Нет Чтение Вход: Файловый поток, символы которого считываются, и строка STR для сохранения символов. Предусловия: Нет Процесс: Считывание последовательности символов из потока в строку STR. Выход: Нет Постусловия: Строке STR присваиваются считываемые символы Запись Вход: Строка, которая содержит символы для выхода, и поток, в который символы записываются. Предусловия: Нет Процесс: Пересылка строки символов в поток. Выход: Выходной поток изменяется. Постусловия: Нет Конец ADT String Строки C++ Глава 8 содержит спецификацию и реализацию C++ класса String. Этот класс содержит расширенный набор операторов сравнения и операций ввода/вывода. В этой главе мы используем строки с NULL-символами и C++ строковую библиотеку для реализации ADT. Строка в C++ — это строка с нулевым завершающим символом, в которой NULL-символ обозначается символом 0 в коде ASCII. Компилятор определяет строковую константу (string literal) как последовательность символов, заключенную в двойные кавычки. Строковая переменная (string variable) — это символьный массив, который содержит последовательность символов с NULL-символом в конце. Следующее объявление создает символьный массив и присваивает строковую константу массиву: char STR[9] = МА 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 etrlen(char *s); cout << strlen(sl) « endl; // выходное значение равно 12 cout « strlen(s2) « endl; // выходное значение равно 8 2. Копирование char* etrcpy(char *sl, *s2); strcpy(s3,sl); // s3 = "dir/bin/appl" 3. Конкатенация char *etrcat(char *sl, *s2); strcat(s3, /); strcat(s3, s2); // s3="dir/bin/appl/file.asm" 4. Сравнение int strcmp(char *sl, *s2);) result - strcmp("baker", "Baker"); // result > 0 result - strcmp("12"/ "12"); // result = 0 result = strcmp("Joe", "Joseph"); // result < 0 5. Индекс char *etrchr(char *s, int c); p = strchar(s2, ' .'); II V указывает на '/' после bin if (P) strcpy(p, ".cpp"); // s2 « "file.cpp" 6. Правый индекс char *etrrchr(char *s, int c); p = strrchr(sl, '/'); II P указывает на ' /' после bin if (p) *p =■ 0; // закончить строку после bin; s2 = "dir/bin" 7. Считать StreamVariable » s 8. Записать StreamVariable << s cin > si; // если входная строка - "hello world", то si ccut < si; // указывает на строку // "hello world" // выход - "hello world" Строковая библиотека C++ <string.h> содержит гибкий набор функций строкового управления, который непосредственно реализует большинство операций ADT. В таблице 2.4 перечислены ключевые строковые функции C++. Приложение: перестановка имен Строковая прикладная программа иллюстрирует использование строковых библиотечных функций C++. В этом приложении функции strchr(), strcpy() и strcat() объединяются для копирования имени такого, как "John Doe" в "Doe", "John", в строку Newname. Следующие операторы реализуют этот алгоритм: char Name[10] = "John Doe", Newname[30]; char *p; Оператор 1: p * strchrfName, ' '); Возвратить указатель р на первый пробел в переменной Name. Первая буква фамилии находится по адресу р+1. J 1 О h п D 0 е NULL Name Р
Оператор 2: *р = 0; // заменить пробел на нулевой символ J 0 h n NULL D 0 e NULL Name p Оператор 3: strcpy(Newname, p+1); // копировать фамилию в Newname J t Name D 0 0 h e n NULL p+1 NULL D 0 e NULL Newname Оператор 4: strcat (Newname, ", " ); // добавить ',' и пробел к Newname D О e i NULL Newname Оператор 5: strcat (Newname, Name); // добавить к Newname имя J 0 h n NULL ' t Name D 0 e • J 0 h n null: Newname Программа 2.1. Перестановка имени Данная программа использует операторы 1 — 5 для перестановки имени. Эти шаги содержатся в функции ReverseName. Цикл главной программы тестирует алгоритм на трех строках ввода. Выходом в каждом случае является переставленное имя. // рг02_01.срр iinclude <iostream.h> ♦include <string.h> // перестановка имени и фамилии и разделение их запятой // результат копируется в Newname void ReverseName(char *name, char *newName) {
char *p; // поиск первого пробела в name и замена пробела // NULL-символом р = strchr(name,' ' ) ; *р« 0; // копировать фамилию в Newname, добавить ", "и // присоединить к Newname имя strcpy(newName,p+1); streat(newName,", "); streat(newName,name); *p = ' } void main (void) { char name [ 32 ], newName [ 32 ]; int i; // считать и обработать три имени for (i = 0; i < 3; i++) { cin.getline(name,32,'\n'); ReverseName(name,newName); cout << "Переставленное имя: " « newName « endl « endl; } } /* Оапуск программы 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 Целое T Major ип перечисления GPA Действительное
Поля Name и Local Address содержат строковые данные. Age и GPA являются численными типами, a Major — это тип перечисления. Полагая, что Том — студент, мы получаем доступ к отдельным полям, объединяя записи имени и поля с использованием оператора доступа ".": Tom.Name Tom.Age Tom.GPA Тот.Major « Запись позволяет объединять данные различных типов (неоднородные типы — heterogeneous types) в структуре. В отличие от массива, запись описывает единственное значение, а не список значений. ADT Record Данные Элемент, содержащий набор полей неоднородного типа. Каждое поле имеет имя, обеспечивающее прямой доступ к данным в поле. Операции Оператор доступа Вход: Имя записи (recname) и поле Предусловия: Нет Процесс: Доступ к данным в поле Выход: При нахождении данных возвращать значение поля клиенту. Постусловия: При сохранении данных запись изменяется Конец ADT Record Структуры C++ 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 позволяет устанавливать файловый указатель в нужную по- Память компьютера Монитор Выходной поток Запоминающее устройство большой емкости
зицию в файле. Эта операция предполагает, что мы имеем доступ ко всем символам в файле и можем перемещаться в переднюю, заднюю и промежуточную позицию. Чаще всего операция установки используется для дисковых файлов. Файл обычно присоединяется к потоку в одном из трех режимов: read-only, write-only и read-write. Режимы read-only и write-only указывают на то, что поток используется для ввода или вывода, соответственно. Режим read-write обеспечивает поток данных в обоих направлениях. ADT File Данные Определение внешнего файла и направления потока данных. Последовательность данных, которые считываются из файла или записываются в файл. Операции Орел Вход: Имя файла и направление потока. Предусловия: Для ввода должен существовать внешний файл. Процесс: Связывание потока с файлом. Выход: Флажок, указывающий на успешность операции. Постусловия: Данные могут последовательно перемещаться между внешним файлом и системной памятью посредством потока. Close Вход: Нет Предусловия: Нет Процесс: Отделение потока от файла. Выход: Нет Постусловия: Данные больше не могут перемещаться посредством потока между внешним файлом и системной памятью. Чтение Вход: Массив размером N для хранения блоков данных. Предусловия: Поток должен быть открыт в направлении только для чтения или чтения-записи. Процесс: Ввод N символов из потока в массив. Остановка в конце файла. Выход: Возвращать количество символов, которые считываются. Постусловия: Файловый указатель перемещается вперед на N символов. Запись Вход: Массив; count N Предусловия: Поток должен быть открыт с направлением только для записи или для чтения-записи. Процесс: Вывод N символов из массива в поток. Выход: Возвращать количество символов, которые записываются. Постусловия: Поток содержит данные вывода, и файловый указатель перемещается вперед на N символов. Установка Вход: Параметры для переустановки файлового указателя. Предусловия: Нет Процесс: Переустановка файлового указателя. Выход: Возвращать флажок, указывающий на успешность установки. Постусловия: Устанавливается новый файловый указатель. Конец ADT File
Иерархия потоков C++ C++ обеспечивает файловое управление с потоковой системой ввода/вывода, которая реализуется путем использования иерархии классов, как частично показано на рис. 2.7. Поток C++ является объектом, соответствующим классу в этой иерархии. Каждый поток определяет файл и направление потока данных. Корневым классом в иерархии является ios, содержащий данные и операции для всех производных классов. Этот класс содержит флажки, которые определяют специфические атрибуты потока и методы форматирования, которые действительны для ввода и вывода. Например: cout.setf(ios:: fixed); устанавливает режим отображения для вещественных чисел на фиксированный формат, а не на экспоненциальный. Классы istream и ostream предоставляют базовые операции ввода и вывода и используются как базовые классы для остальной части потоковой иерархии ввода/вывода. Класс istream_withassign — это вариант istream, который позволяет выполнять объектное присваивание. Предопределенный объект cin является объектом этого класса. Предопределенные объекты cout и сегг — это объекты типа класса ostream_withassign. Во время выполнения эти три потока открыты для ввода с клавиатуры и вывода на экран. Объявления этих классов включены в файл <iostream.h>. Класс ifstream является производным класса istream и используется для дискового файлового ввода; аналогично of stream используется для дискового файлового вывода. Эти классы объявляются в файле <fstream.h>. Оба класса содержат операцию Open для присоединения файла к потоку и операцию Close для отделения потока от файла. Двумя типами дисковых файлов являются текстовые файлы (text files) и бинарные файлы (binary files). Текстовый файл содержит символы ASCII и является печатаемым, тогда как бинарный файл содержит чистые бинарные данные. Например, редактор использует текстовые файлы, а программа — электронная таблица создает и использует бинарные файлы. Пример текс- ios istream ostream istrstream istream_ withassign ifstream iostream ofstream ostream_ withassign ostrstream fstream strstream Рис.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 // pr02_02.cpp #include <iostream.h> #include <fstream.h> #include <strstream.h> #include <stdlib.h> #include <string.h> void main(void) { // ввести текстовый файл, содержащий имена и значения ifstream fin; char name[30], outputstr[256]; //декларировать выходной поток, основанный на массиве и // использующий outputstr ostrstream outs(outputstr, sizeof(outputstr)); double value; // открыть для ввода файл 'names.dat', // убедиться в его существовании fin.open("names.dat", ios::in | ios::nocreate); if (!fin) {
cerr « "Невозможно открыть файл 'names.dat' " « endl; exit(1); } // читать имена и значения, // записывать в поток outs как 'имя = значение ' while(fin >> name) { fin >> value; outs « name « " = " « value « " "; } // NULL-символ для выходной строки outs << ends; cout << outputstr « endl; } /* <•' names, da t"> start 55 breakloop 225.39 stop 23 Оапуск программы 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. Кеу=б N=5 8 3 6 2 6 А-список Возвращаемое значение 2. Key = 6, Start = 3, n = 2. Начинать с А[3] и искать в списке, возвращая указатель на первое появление элемента 6. Кеу=6 N=2 8 3 6 2 6 * А список Возвращаемое значение 3. Key = 9, Start = 0, n = 5. Начинать с первого элемента и искать в списке число 9. Когда оно не найдено, возвращать значение -1. Кеу=9 N=5 8 3 6 2 6 А=список Возвращаемое значение = -1 Алгоритм последовательного поиска применяется к любому массиву, для которого оператор "==" определяется для типа элемента. Общий алгоритм поиска требует шаблонов и перегрузки операторов. Эти темы обсуждаются в главах 6 и 7. Следующая функция реализует последовательный поиск для массива целых: Функция последовательного поиска int SeqSearch(int list[], int start, int n, int key) { for (int i=start; i < n; i++) if (list [i] == key) return i; } return -1; } Программа 2.З. Повторяемый поиск Данная программа тестирует функцию последовательного поиска, подсчитывая количество появлений ключа в списке. Главная программа сначала вводит 10 целых чисел в массив А и затем запрашивает ключ. Программа выполняет повторяемые вызовы SeqSearch, используя различный начальный индекс. В исходном положении мы начинаем с индекса
О, начала массива. После каждого вызова SeqSearch счетчик количества появлений увеличивается, если ключ находится; в противном случае поиск прекращается, и счетчик является возвращаемым значением. Если ключ найден, возвращаемое значение определяет его позицию в списке. Следующий вызов SeqSearch выполняется со значения start, равного положению элемента, находящегося непосредственно справа от последнего найденного. // рг02__03.срр #include <iostream.h> // поиск в массиве из п целых значений элемента по ключу; // возвратить указатель на этот элемент или NULL, если элемент не найден int SeqSearch(int list[], int start, int n, int key) { for(int i=start;i < n; i++) ■ if (list[ij == key) return i; // возвратить индекс соответствующего элемента return -1; // неудачный поиск, возвратить -1 } void main(void) { int A[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,pos,10,key)) != -1) { COUnt++; // продвинуться к следующему целому после найденного pos++; } 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, эти два элемента меняются местами. После выполнения всех сравнений наименьший элемент помещается в позицию с индексом 0. Индекс 0 Исходный список Действие Обмен Нет обмена Обмен Полученный список Индекс 1: При уже помещенном в позицию с индексом 0 самом маленьком элементе рассмотрим подсписок 8, 6, 3. Принимаются во внимание только элементы от индекса 1 до конца списка. Элемент с индексом 1 сравнивается с последующими элементами с индексами 2 и 3. Для каждого сравнения, если больший элемент находится в позиции с индексом 1, то два элемента меняются местами. После выполнения сравнений второй наименьший элемент в списке сохраняется в позиции с индексом 1. Индекс 1 Исходный список Действие Полученный список Обмен Обмен Индекс 2: Рассмотрим подсписок 8, 6. Этот процесс продолжается для подсписка из двух элементов с индексами 2 и 3. Между элементами выполняется простое сравнение, в результате которого происходит обмен. Индекс 2 Исходный список Действие Полученный список Обмен
У нас остался только один элемент с индексом 3, и список отсортирован. Отсортированный список 2 3 6 8 В C++ функция ExchangeSort использует вложенные циклы. Предположим, что размер списка задается значением п. Внешний цикл приращивает индекс i в диапазоне от 0 до п-2. Для каждого индекса i сравним последующие элементы при j=i+l, i+2, ..., п-1. Выполним сравнение и поменяем местами элементы, если listfi] > list[j]. Программа 2.4. Сортировка списка Эта программа иллюстрирует алгоритм сортировки. Список из 15 целых в диапазоне от 0 до 99 заполняет list. ExchangeSort упорядочивает список, используя функцию Swap для того, чтобы поменять местами два элемента массива. Программа выдает на экран список до и после сортировки. // рг02_04.срр #include <iostream.h> // поменять значения двух переменных целого типа х и у void Swap(int & х, int & у) { int temp - х; // сохранить первоначальное значение х х = у; // заменить х на у у - temp; // присвоить переменной у // первоначальное значение х } // сортировать целый массив n-элементов а в возрастающем порядке void ExchangeSort(int а[], int n) { int i, j; // реализовать n — 1 проходов.найти правильные значения // в а[],...,а[п-2]. for(i = 0; i < n-1; i++) // поместить минимум из а[п+1]...а[п-1] в a[i] for(j = i+1; j < n; j++) // заменить if a[i] > a[j] if (a[i] > a[j]) Swap(a[i], a[j]); } // пройти по списку, печатая каждое значение void PrintList(int a[], int n) { for (int i = 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 « "Исходный список \n"; PrintList(list,15); ExchangeSort(list,15); cout « endl «"Отсортированный список" « endl; PrintList(list,15); ) /* Оапуск программы pr02_04 . cpp> Исходный список 38 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 84 86 V Подсчет зарезервированных слов C++ В разделе 2.8 обсуждается тип записи, который реализуется в C++ как struct. В качестве иллюстрации записей программа подсчитывает количество раз, когда в файле появляются зарезервированные слова "else", "for", "if", "include" и "while". Эта программа использует строковые переменные также, как массив записей. Основной структурой данных программы является struct Key Word, чьи поля состоят из строковой переменной keyword и count типа integer: struct Keyword { char keyword[20]; int count; }/ В массиве KeyWordTable создается таблица для пяти зарезервированных слов. Каждый элемент в этой таблице инициализируется указанием зарезервированного слова и начального значения счетчика count=0. Например, первый инициализатор массива {"else", 0} приводит к тому, что элемент Кеу- WordTable[0] содержит строку "else" со значением счетчика 0: Keyword KeyWordTable[ ] = { {"else", 0}, {"for", 0}, {"if", 0}, {"include", 0}, {"while", 0} }; Программа читает отдельные слова в файле с помощью функции Get Word. Словом является любая последовательность символов, которая начинается с буквы и продолжается произвольным количеством букв или цифр. Например, когда представлена строка Expression: 3+5=8 (N1 + N2 = N3) GetWord извлекает слова "Expression", "Nl", "N2", и "N3" и отбрасывает другие символы. Функция SeqSearch сканирует таблицу, выполняя поиск соответствия ключевому слову. Когда поиск завершается успешно, функция возвращает индекс соответствующей записи, увеличивая на единицу поле count.
Программа 2.5. Подсчет зарезервированных слов Эта программа читает собственный исходный код в качестве ввода. В цикл читается каждое слово и вызывается функция SeqSearch для определения того, соответствует ли ввод зарезервированному слову в KeyWordTable. Если так, поле count в записи увеличивается на единицу. После завершения ввода, количество появлений каждого ключевого слова выводится на экран. Программа имеет интересный оператор, который динамически вычисляет количество элементов в массиве KeyWordTable с помощью выражения sizeof (KeyWordTable)/ sizeof (Keyword) Это выражение предоставляет независимый от системы метод вычисления количества элементов в каком-либо массиве. Если другие ключевые слова добавляются к этой таблице, последующая компиляция генерирует новый подсчет элементов. // рг02_05.срр #include <Iostream.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} ); // настраиваемый алгоритм поиска слов int SeqSearch(Keyword *tab, int n, char *word) { int i; // сканировать список, сравнивать word с keyword в текущей записи for (i=0; i < n; i++, tab++) if (strcmp(word, tab-keyword) == 0) return i; // при совпадении вернуть индекс return -1; // к сожалению, нет совпадения } // извлечь слово, начинающееся с буквы и, возможно, // другие буквы/цифры int GetWord(ifstreams fin, char w[]) { char c; int i = 0; // пропустить не алфавитный ввод while ( fin.get(с) && lisalpha(c) ) ; // вернуть 0 (Неудача) в конце файла
if (fin.eof () ) return 0; // записать первый символ word w[i++] =с; // собирать буквы, цифры и символ окончания строки while ( fin.get (с) && ( isalpha(c) | | isdigit(c) ) ) w[i++] = с; w[i] = 'Nonreturn 1; // вернуть 1 (Успех) } void main (void) { const int MAXWORD = 50; // максимальный размер любого слова // объявить и инициализировать размер таблицы const int NKEYWORDS = sizeof(KeyWordTable)/sizeof(Keyword); int n; char word [MAXWORD] , c; ifstream fin; // открыть файл с проверкой ошибки fin.open("pr02_05.cpp", ios::in | ios::nocreate); if (!fin) { cerr « "Невозможно открыть файл 'pr02_05.cpp' " « endl; exit (1) ; } // извлекать слова до конца файла while (GetWord(fin,word)) // при совпадении с таблицей keyword увеличивать счетчик if ((n= SeqSearch(KeyWordTable,NKEYWORDS,word)) != -1) KeyWordTable[n].count++; // сканировать таблицу keyword и печатать поля записи for (n = 0; n < NKEYWORDS; n++) if (KeyWordTable[n].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 = 256, 163 = 4096 и так далее. В форме позиционной нотации примерами шестнадцатиричных чисел являются 17Е, 48 и FFFF8000. Числа преобразуются в десятичную форму расширением степеней 16 точно так же, как степени 2-х расширяются для двоичных чисел. Например, шестнадцатиричное число 2A3Fi6 преобразуется в десятичное расширением степеней 16-и. 2A3F16 = 2(1б3) + А(1б2 ) +3 (161) +F(16°) = 2(4096) +10 (256) +3 (16) +15 (1) - 8192 + 2560 +48 +48 + 15 = 1018510 Преобразуйте каждое шестнадцатиричное число в десятичное (а) 1А (б) 41F (в) 10ЕС (г) FF (д) 10000 Преобразуйте каждое десятичное число в шестнадцатиричное (е) 23 (ж) 87 (з) 115 (и) 255 2.4 Основной причиной введения шестнадцатиричных чисел является их естественное соответствие двоичным числам. Они обеспечивают компактное представление двоичных данных и адресов памяти. Шестнадцатиричные цифры имеют 4-битовое двоичное представление в диапазоне 0-15. Следующая таблица показывает соответствие между двоичными и шестнадцатиричными цифрами: Шестнадцатиричные 0 1 2 3 4 5 6 7 Двоичные 0000 0001 0010 0011 0100 0101 0110 0111 Шестнадцатиричные 8 9 А В С D Е F Двоичные 1000 1001 1010 1011 1100 1101 1110 1111
Для представления двоичного числа в шестнадцатиричном формате начинайте с правого конца числа и разделяйте биты на группы из четырех битов, добавляя начальный 0 слева в последней группе, если необходимо. Запишите каждую группу из 4-х битов как шестнадцатиричное число. Например: 1111000111011102 » 0111 1000 1110 1110 = 78ЕЕ1б Для преобразования шестнадцатиричного числа в двоичное выполните обратное действие и запишите каждое шестнадцатиричное число как 4 бита. Рассмотрим следующий пример: А7891б = 1010 0111 1000 1001 = 10100111100010012 Преобразуйте двоичные числа в шестнадцатиричные: (а) 1100 (б) 1010 ОНО (в) 1111 0010 (г) 1011 1101 1110 ООН Преобразуйте шестнадцатиричные числа в двоичные: (д) 061016 (е) AF2016 2.5 C++ позволяет программисту вводить и выводить числа в шестнадцатиричном представлении. При помещении манипулятора "hex" в поток режим ввода или вывода чисел становится шестнадцатиричным. Этот режим действует до тех пор, пока он не поменяется на десятичный с помощью манипулятора "dec". Например: cin >> hex » t » dec » u; // t читается как шестнадцатиричное; u — как десятичное <ввод 100 25б> t = 1001б и и = 25б10 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. Выполните то же для оператора сравнения !=. 2.7 Булев тип определяет данные, которые имеют значения True или False. Некоторые языки программирования определяют базовый булев тип с рядом встроенных функций для обработки этих данных. C++ ассоциирует булево значение с каждым числовым выражением. (а) Определите булев ADT, описывающий область данных, и его операции. (б) Опишите реализацию этого ADT, используя языковые конструкции C++.
2.8 (а) Какой символ ASCII соответствует десятичному числу 78? (б) Какой символ ASCII соответствует двоичному числу 1001011г? (в) Каковы коды ASCII для символов "*", "q" и возврата каретки? Дайте ответы в десятичном и шестнадцатиричном представлении. 2.9 Что печатается следующим фрагментом кода? cout « char (86) « " " « int ( ' q' ) « " " « char( int ("0") + 8) « endl; 2.10 Объясните, почему оператор % (остаток) не дается в ADT для вещественных чисел. 2.11 Преобразуйте каждое двоичное число с фиксированной точкой в десятичное: (а) 110.110 (б) 1010.0101 (в) 1110.00001 (г) 11.111 . . . 111 . . . (Совет: Используйте формулу для суммы геометрического ряда). 2.12 Преобразуйте каждое десятичное число с фиксированной точкой в двоичное: (а) 2.25 (б) 1.125 (в) 1.0875 2.13 (а) Существует ли наименьшее положительное действительное число в ADT для вещественных чисел? Почему да или почему нет? (б) Когда в компьютере следует использовать вещественное числа, существует ли наименьшее положительное вещественное число? Почему да или почему нет? 2.14 Формат IEEE с плавающей точкой сохраняет знак числа отдельно, а порядок и мантиссу — как беззнаковые числа. Нормализованная форма позволяет получить уникальное представление для каждого числа с плавающей точкой. Нормализованная форма: Число с плавающей точкой задается так, что имеет одну не равную нулю цифру слева от двоичной точки N = ± 1 .d1d2 • • • dn_x 2е Число с плавающей точкой 0.0 сохраняется со знаком, порядком и мантиссой 0. В качестве примера: два двоичных числа преобразуются в представление в нормализованной форме. Двоичное число Нормализованная форма 1101.101 х 21 1.1011010 х 24 0.0011 х 2б 1.1 х 23 Тридцати-двух-битовые числа с плавающей точкой сохраняются в нормализованной форме с использованием внутреннего формата IEEE.
Знак Самый левый бит используется для знака. "+" имеет знаковый разряд 0, и "-" имеет знаковый разряд 1. Порядок Порядок задается 8-ю битами. Для обеспечения сохранения всех порядков как положительных (беззнаковых) чисел, формат IEEE задает использование нотации "excess -127" для порядка. Сохраняемый порядок (Ехр3) создается добавлением 127 к реальному порядку. Exps = Exp + 127 Истинный порядок Сохраняемый порядок Диапазон Диапазон -127 < Ехр < 128 0< Exps < 255 Мантисса Допустим, что число сохраняется в нормализованной форме, начальная цифра 1 скрыта, дробные цифры сохраняются в 23-битовой мантиссе, задается точность 24 бита. Знак 1 бит Порядок 8 битов Мантисса 23бита В качестве примера вычислим внутреннее представление -0.1875. Нормализованная форма (-) 1.100 * 2~3 Знак 1 Порядок Exps = -3 + 127 = 124 = 011111002 Мантисса <1>1000000 ... 0 -0.1875 - 10111110010000000000000000000000 Запишите каждое число в 32-битовой форме IEEE с плавающей точкой: (а) 7.5 (б) -1/4 Каково значение следующих 32-битовых чисел в формате IEEE в десятичной форме? Каждое число дается в шестнадцатиричной форме. (в) С1800000 (г) 41Е90000 2.15 (а) Перечислите в календарном порядке месяцы года, которые имеют символ "р" в имени. Это перечислимый тип. (б) Напишите реализацию C++ для перечислимого типа. (в) Какой месяц соответствует целому числу 4 в реализации C++? Какова позиция октября? (г) Напишите это перечисление в алфавитном порядке. Имеют ли какие- либо месяцы одну и ту же позицию в обоих списках? 2.16 Добавьте операции successor и predecessor к ADT для перечислимых типов. Используйте полные спецификации. Successor возвращает следующий элемент в списке, и predecessor возвращает предыдущий элемент. Будьте осторожны при определении того, что происходит на границах списка.
2.17 С учетом следующих объявлений и операторов укажите содержимое X, Y и А после выполнения этих операторов: int X =4, Y=7, *РХ = &Х, *PY; double А[ ] = {2.3, 4.5, 8.9, 1.0, 5.5, 3.5}, *РА - А; PY = &Y; (*РХ)--; *PY += *РХ; PY = РХ; *PY = 55; *РА += 3.0; РА++; *РА++ =* 6.8; РА+= 2; *++РА = 3.3; 2.18 (а) А объявляется как А[5]; short A[5]; Сколько байтов выделяется для массива А? Если адрес массива А = 6000, вычислите адрес А[3] и А[1]. (б) Предположим такое объявление: long А[ ] - {30, 500000, -100000, 5, 33}; Если длинное слово занимает 4 байта и адрес А равен 2050, □ Каково содержимое с адресом 2066? D Удвойте содержимое с адресом 2050 и адресом 2062. Выпишите массив А. □ Какой адрес у А[3]? 2.19 Предположим, что А — это массив размером mxn с индексами строк в диапазоне 0 — (ш-1) и индексами столбцов в диапазоне 0 — (п-1). Генерируйте функцию доступа, вычисляющую адрес A[row] [col], полагая, что элементы сохраняются столбцами. 2.20 А объявляется как short А[5] [б]; (а) Сколько байтов выделено для массива А? (б) Если адрес массива А = 1000, вычислите адрес А[3] [2] и А[1] [4]. (в) Какой элемент массива помещается по адресу 1020? По адресу 1034? 2.21 (а) Объявите строку Name с вашим именем в качестве начального значения. (б) Рассмотрите объявления строковой переменной: char sl[50], s2[50]; и операторы ввода: cin » SI >> S2; Каково значение SI и S2 для строки ввода "Джордж спешит!"? Каково значение S1 и S2 при вводе следующего текста (Ф— это пустой символ, а 1| — это конец строки.)?: Next* ^^<->Word
2.22 Рассмотрим следующие строковые объявления: char SI[30] = "Stockton, CA", S2[30] = "March 5, 1994м, *р; char S3 [30]; (а) Каково значение *p после выполнения каждого следующего оператора? р = strchr (SI, 't'); р = strrchr (SI, '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) ; удаляет последовательность п символов из строки s, начиная с индекса i. Если индекс i больше, чем длина s или равен ей, то никакие символы не удаляются. Если i+/z больше, чем длина s или равено ей, то удаляется конец строки, начиная с индекса i. Реализуйте strdelete, используя библиотечные функции C++ strlen и strcpy. 2.25 Альтернативой использованию строк с NULL-символом является помещение счетчика символов в первый элемент символьного массива. Это называется форматом со счетчиком байтов, и такие строки часто называют строками Паскаля, поскольку программные системы на языке Паскаль используют этот формат для строк. (а) Реализуйте функцию strcat, полагая, что строки сохраняются в формате со счетчиком байтов. (б) Функции PtoCStr и CtoCStr выполняют преобразование этих двух строковых форматов: void PtoCStr(char *s); // конвертировать s из Pascal в C++ void CtoPStr(char *s); // конвертировать s из C++ в Pascal Реализуйте эти две функции.
2.26 Добавьте оператор присваивания "=" к ADT записи, используя полную спецификацию. Точно определите, какое действие выполняется во время присваивания. 2.27 Комплексное число имеет форму х + iy, где i2 = -1. Комплексные числа имеют широкое применение в математике, физике и технике. Они имеют арифметику, управляемую рядом правил, включая следующие: Пусть и = а + ib, v = с + id u + v = (а + с) + i <b + d) u — v = (a — с) + i (b — d) u v ac + bd c2 + d2 + i be - ad c2 + d2 Представьте комплексное число, используя следующую структуру: struct Complex { float real; float imag; } и реализуйте следующие функции, которые выполняют операции с комплексными числами: Complex cadd(Complex& х, Complex& у); // х + у Complex csub{Complex& x, Complex& у); // х — у Complex cmul(Complex^ х, Complex& у); // х * у Complex cdiv(Complex& х, Complexs у); // х / у 2.28 Добавьте операцию FileSize к ADT для потоков. Она должна возвращать количество символов в файле. Точно укажите, для каких предусловий эта операция имеет смысл. (Совет: Как насчет cin/cout?) 2.29 Четко различайте текстовый и двоичный файл. Как вы думаете, возможно ли разработать программу, принимающую имя файла в качестве входа и определить текстовый он или бинарный? Упражнения по программированию 2.1 Напишите функцию void BaseOut(unsigned int n, int b) которая выводит п с основанием b, 2 < b <. 10. Напечатайте каждое число в диапазоне 2 < п < 50 с основанием 2, 4, 5, 8 и 9. 2.2 Напишите функцию void Octln(unsigned int& n); которая читает число с основанием 8 (восьмеричное) и присваивает его п. Используйте Octln в главной программе, которая читает следующие восьмеричные числа и печатает десятичные эквиваленты: 7, 177, 127, 7776, 177777 2.3 Напишите программу, которая объявляет три целые переменные i, у, k. Введите значение для i в десятичной форме и значения для у и ft — в шестнадцатиричной. Напечатайте все три переменные и в шестнадцатиричной, и в десятичной форме.
2.4 Изучите дискретность представления вещественных чисел на вашем компьютере путем вычисления 1 + D для D = 1/10, 1/100, 1/1000, ..., 1/10п до тех пор, пока 1 + D == 1.0. Если вы имеете доступ более, чем к одной машинной архитектуре, попробуйте выполнить это на других машинах. 2.5 Рассмотрим перечислимый тип enum DaysOfWeek {Sun, Mon, Tue, Wed, Thurs, Fri, Sat}; Напишите функцию void GetDay(DaysOfWeek& day); которая читает имя дня с клавиатуры как строку и присваивает дню соответствующее значение элемента перечисления. Напишите также функцию void PutDay(DaysOfWeek day) которая записывает значение элемента перечисления на экране. Разработайте главную программу для тестирования этих двух функций. 2.6 Введите ряд слов до конца файла, преобразуя каждое в слово на ломаной латыни. Если слово начинается с согласного, переместите первый символ слова на последнюю позицию и присоедините "ау". Если слово начинается с гласного, просто присоедините "ау". Например: Вход: this is simple Выход: histay isay implesay 2.7 Строка текста может быть зашифрована с использованием табуляцион- ного соответствия, которое ассоциирует каждую букву алфавита с уникальной буквой. Например, табуляционное соответствие abcdefghijklmnopqrstuvwxyz ==> ngzqtcobmuhelkpdawxfyivrsj устанавливает соответствие между "encrypt" и "tkzwsdf". Напишите программу, которая читает текст до конца файла и выводит зашифрованную форму. 2.8 Создайте свою программу табуляционного соответствия, которая выполняет установку соответствия, обратную той, которая использовалась в упражнении 2.7. Введите зашифрованный файл и выведите на экран его расшифрованную форму. 2.9 Напишите программу, которая вызывает выход за границы одного или более индексов массива. Доведите программу до такого состояния, чтобы она "разрушалась". Притворитесь, что вы не знаете, в чем заключается проблема. Используйте любой имеющийся в вашем распоряжении отладчик и диагностируйте причину такого поведения. 2.10 Измените сортировку обмена так, чтобы она сортировала список в порядке убывания. Протестируйте новый алгоритм, написав главную программу, подобную программе 2.4. 2.11 Рассмотрим объявление записи struct Month { char name[10]; // имя месяца int monthnum; // число дней в месяце };
(а) Напишите функцию void SortByName(Month months[ ], int n) ; которая сортирует массив с элементами типа Month, сравнивая имена (используйте функцию strcmp в C++). Напишите также функцию void SotrByDays (Month months[ ], int n); которая сортирует список, сравнивая количество дней в месяце. Напишите главную программу, которая объявляет массив, содержащий все месяцы года и сортирует его, используя обе функции. Выведите на экран каждый отсортированный список. (б) Заметьте, что сортировка списка месяцев по количеству дней создает связи соперничества. Когда это происходит, метод сортировки может использовать вторичный ключ (secondary key) для устранения этих связей. Напишите функцию void Sort2ByDays(Month months[ ], int n); которая сортирует список, сравнивая сначала количество дней, и, если связь возникает, разбивает ее, сравнивая имена. Используйте эту функцию в главной программе для распечатки упорядоченного списка всех месяцев года, упорядоченных по количеству дней в месяце. 2.12 Напишите программу, которая читает текстовый файл и выводит на экран счетчик количества появлений знаков пунктуации(. , ! ?). 2.13 Используя cin.getline, читайте строку, начинающуюся с имени функции из одного символа, за которым следуют последовательности "х" с символами "+" и "-", вставленными в промежутки. Строка не может заканчиваться символом "+" или "-". Образуйте строку в форме: SingleCharFuncName (х) = х**п ± х**щ±. . . в массиве, используя выход на базе массива. Если порядком является равным 1, то опустите "**1". Запишите каждую строку в файл "funcs.val". Например, строки Fxxx + xx — x Gxx — xxx + xxxx создают файл "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 - 3z2 + 4z - 2 Определите полиномиал для следующих значений z: z = 2 +3i, -1 + i, 1 + i, 1 - i, 1 + Oi Заметьте, что последние три значения являются корнями от f.
глава Абстрактные типы данных и классы Т 3.1. Пользовательский тип - КЛАСС 3.2. Примеры классов 3.3. Объекты и передача информации 3.4. Массивы объектов 3.5. Множественные конструкторы 3.6. Практическое применение: треугольные матрицы Письменные упражнения Упражнения по программированию
В главе 1 были даны абстрактные типы данных (ADT) и их представление в качестве классов C++. Это введение описывает структуру класса, которая обеспечивает инкапсуляцию данных и скрытие информации. В этой главе содержится более полное описание базовых концепций класса. Мы рассматриваем разработку и использование конструкторов класса, реализацию методов класса и использование классов с другими структурами данных. Для обеспечения хорошего понимания классов читателем мы разрабатываем широкий диапазон примеров и используем их в законченных программах. Выбранные соответствующим образом ADT иллюстрируют связь между абстрактной структурой и объявлением класса. 3.1. Пользовательский тип — КЛАСС Класс — это определяемый пользователем тип с данными и функциями (методами), называемыми членами (members) класса. Переменная типа класс называется объект (object). Класс создает различные уровни доступа к его членам, разделяя объявление на части: private, protected и public. Часть private (закрытая) объекта может быть доступна только для функций-членов в этом классе. Часть public (открытая) объекта может быть доступна для внешних элементов программы, в области действия которых находится этот объект (рис. 3.1). Protected (защищенные) члены используются с производными классами и описываются в главе 12, посвященной наследованию. Члены класса private: Данные Операторы public: Данные Операторы Внешние программные единицы Рис. 3.1. Доступ к методам класса Объявление класса Объявление класса начинается с заголовка класса (class head), состоящего из зарезервированного слова class, за которым следует имя класса. Члены класса определяются в теле класса (class body), которое заключается в фигурные скобки и заканчивается точкой с запятой. Зарезервированные слова public и private разделяют члены класса, и эти спецификаторы доступа заканчиваются двоеточием. Члены класса объявляются как переменные C++, а методы задаются как объявления функций C++. Общая форма объявления класса такова: class Имя_класса { private: // Закрытые данные // Объявление закрытых методов // public: // Открытые данные // Объявление открытых методов // };
Следует, по возможности, помещать члены класса в закрытую секцию. В результате этого значение данных обновляется только функцией-членом класса. Это предотвращает нежелательные изменения в данных кодом использующего класс приложения. Пример 3.1 Класс Rectangle При геометрических измерениях прямоугольник определяется его длиной и шириной. Это позволяет нам вычислять периметр и площадь фигуры. Параметры длины и ширины и операции объединяются для образования абстрактного типа данных прямоугольной фигуры. Мы оставляем спецификацию ADT в качестве упражнения и разрабатываем класс Rectangle C++, который реализует этот ADT. Класс содержит конструктор и набор методов — GetLength, PutLength, Get Width и 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, Get Width, Perimeter и Area имеют ключевое слово const после списка параметров. Это объявляет каждый метод как константный. В определении константного метода никакой элемент данных не может быть изменен. Другими словами, выполнение метода, объявленного как const, не изменяет состояния объекта Rectangle. Если первый спецификатор доступа опускается, начальные члены в классе являются закрытыми по умолчанию. Члены класса являются закрытыми до первого появления открытой или защищенной спецификации. C++ позволяет программисту чередовать закрытую, защищенную и открытую секции, хотя это обычно не рекомендуется. Конструктор Функция, называемая конструктором (constructor) класса, имеет то же имя, что и класс. Подобно другим функциям C++, конструктору могут передаваться параметры, используемые для инициализации одного или более данных-членов класса. В классе Rectangle конструктору дается имя Rectangle, и он принимает параметры 1 и w, используемые для инициализации длины и ширины объекта,
соответственно. Заметьте, что эти параметры имеют значения, по умолчанию, которые указывают, что используется значение 0, когда параметр 1 или w не передается явно. Пример 3.1 иллюстрирует объявление класса (class definition), так как методы описываются только объявлениями функций. Код C++ для определения отдельных функций создает реализацию класса (class implementation). Объявление объекта Объявление класса описывает новый тип данных. Объявление объекта типа класс создает экземпляр (instance) класса. Это делает реальным объект типа класс и автоматически вызывает конструктор для инициализации некоторых или всех данных-членов класса. Параметры для объекта передаются конструктору заключением их в скобки после имени объекта. Заметьте, что конструктор не имеет возвращаемого типа, поскольку вызывается только во время создания объекта: ClassName object(<parameters>); //список параметров может быть пустым Например, следующие объявления создают два объекта типа Rectangle: Rectangle room(12, 10); Rectangle t; //использование параметров по умолчанию (0, 0). Каждый объект имеет полный диапазон данных-членов и методов, объявляемых в классе. Открытые члены доступны с использованием имени объекта и имени члена, разделяемых "." (точкой). Например: х - room.Area(); // присваивает х площадь = 12 * 10 = 120 t.PutLength(20); // присваивает 20 как длину объекта Rectangle // Текущая длина равна 0, так как используются // параметры по умолчанию, cout < t.GetWidthO; // выводит текущую ширину, которая = 0 по умолчанию В объявлении объекта Room конструктор первоначально устанавливает значение длины, равным 12, а ширины — 10. Клиент может изменять размеры, используя методы доступа PutLength и PutWidth: room.PutLength(15); // изменение длины и ширины на 15 и 12 room.PutWidth(12) ; Объявление класса не обязательно должно включать конструктор. Это действие, которое не рекомендуется и не используется в этой книге, оставляет объект с неинициализированными данными в точке его объявления. Например, класс Rectangle может не иметь конструктора, а клиент мог бы задать длину и ширину с помощью открытых методов доступа. Включая в класс конструктор, мы обеспечиваем правильную инициализацию важных данных. Конструктор позволяет объекту инициализировать его собственные данные-члены класса. Класс Rectangle содержит члены класса типа float. В общем, класс может содержать элементы любого допустимого типа C++, даже других классов. Однако, класс не может содержать объект его собственного типа в качестве члена. Реализация класса Каждый метод в объявлении класса должен быть определен. Определения функций могут быть заданы в теле класса (встроенный код) или вне его. При помещении функции вне тела имя класса, за которым следует два двое-
точия, должно предшествовать имени этой функции. Символ "::" называется операцией разрешения области действия (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; } C++ предоставляет специальный синтаксис для инициализации членов класса. Список инициализации членов (member initialization list) — это список имен данных-членов класса, разделенных запятыми, за каждым из которых следует начальное его значение, заключенное в скобки. Начальные значения обычно являются параметрами конструктора, которые присваиваются соответствующим данным-членам класса в списке. Список инициализации членов помещается после заголовка функции и отделяется от списка параметров двоеточием:
ClassName: :ClassName (parm list) : datax (parir^), . . . , datantparn^) Например, параметры конструктора 1 и w могут быть присвоены данным- членам класса length и width: Rectangle::Rectangle(float 1, float w) : length (1), width(w) {} Создание объектов Один объект может использоваться для инициализации другого в каком- либо объявлении. Например, следующий оператор является правильным: Rectangle square(10, 10), yard = square, S; Объект square создается с length и width, равными 10. Второй объект yard создается с начальными данными, копируемыми из объекта square. Объект S имеет length и width, по умолчанию равными 0. Объекты могут свободно присваиваться один другому. Если только пользователь не создает пользовательский оператор присваивания, присваивание объекта может выполняться побитовым копированием данных-членов класса. Например, присваивание S = yard; копирует все данные из объекта yard в объект S. В этом случае length и width объекта yard копируются в length и width объекта S. Объект может быть создан ссылкой на его конструктор. Например, объявление Rectangle(10,5) создает временный объект с lengh = 10 и width = 5. В следующем операторе операция присваивания копирует данные из временного объекта в rectangle S: S = Rectangle(10,5); Пример 3.2 1. Операторы S = Rectangle(10,5); cout « S.Area О « endl; приводят к выводу в поток cout числа 50. 2. Оператор cout « Rectangle(10,5).GetWidth() « endl; ВЫВОДИТ ЧИСЛО 5. Программа 3.1. Использование класса Rectangle В этой программе вычисляется относительная стоимость отделочных работ передней стороны гаража. Пользователь задает размеры передней стороны гаража, а программа выдает различные размеры и стоимость двери. Пользователь замечает, что при выборе большей двери требуется меньше материала для обшивки и опалубки для кладки бетона. Учитывая стоимость пиломатериалов большая дверь может быть более экономичной.
Предположим, что опалубка проходит по периметру передней стороны и периметру проема двери. Мы запрашиваем у пользователя размер передней стороны гаража и затем вводим цикл, позволяющий выбрать размер двери. Цикл заканчивается, когда пользователем выбирается опция "Quit". Для каждого выбора двери программа определяет стоимость отделки передней стороны гаража и выводит это значение. Мы задаем константами стоимость деревянной обшивки $2 за кв. фут и стоимость опалубки на $0.50 за погонный фут. Опалубка Обшивка Дверь Длина опалубки равна сумме периметров передней стороны гаража и двери. Стоимость обшивки равна площади передней стороны гаража минус площадь двери. // рг03_01.срр #include <iostream.h> 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; >; // конструктор, выполняет присваивания: length=l, width=w Rectangle::Rectangle (float 1, float w) : length(1), width(w) {} // возвратить длину прямоугольника float Rectangle::GetLength (void) const { return length; } // изменить длину прямоугольника void Rectangle::PutLength (float 1) {
length = 1; } // возвратить ширину прямоугольника float Rectangle: :GetWidth (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 completedSelections = 0; // опция из меню, выделенная пользователем char doorOption; // длина/ширина и стоимость двери float glength, gwidth, doorCost; // общая стоимость, включая дверь, обшивку и опалубку float totalCost; cout « "Введите длину и ширину гаража: "; cin » glength » gwidth; // создать объект garage (гараж) с размерами по умолчанию // создать объект door (дверь) с размерами по умолчанию Rectangle garage (glength, gwidth) ; Rectangle door; while (!completedSelections) { 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 x10; $480)" « endl; cout « endl; cin »doorOption; if (doorOption « ' q' ) completedSelections = 1; else
{ switch (doorOption) { case ' 1' :door.PutLength(12); // 12 x 8 ($380) door.PutWidth(8); doorCost = 380; break; case'2':door.PutLength(12) ; //12x10 ($420) door.PutWidth(lO); doorCost =420; break; case '3':door.PutLength(16); // 16 x 8 ($450) door.PutWidth(8); doorCost = 450; break; case '4':door.PutLength(12); // 16 x 10 ($480) door.PutWidth(lO) ; 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 (16 х8; $450) Дверь 4 (16 х 10; $480) Общая стоимость двери, обшивки и опалубки: $720 Введите 1-4 или ' q' для выхода Дверь 1 (12 х8; $380) Дверь 2 (12 х 10; $420) Дверь 3 (16 х8; $450) Дверь 4 (16 х 10; $480) q */ 3.2. Примеры классов Следующие два примера классов иллюстрируют конструкторы класса в C++. Класс Temperature поддерживает записи значений высокой и низкой температуры. В качестве приложения объект мог бы иметь высокую (точка кипения) и низкую (точка замерзания) температуры воды. ADT RandomNumber определяет тип для создания последовательности целых или с плавающей точкой случайных чисел. В реализации C++ конструктор позволяет клиенту самому инициализировать последовательность случайных чисел или использовать программный способ получения последовательности с системно-зависимой функцией времени.
Класс Temperature Класс Temperature содержит информацию о значениях высокой и низкой температуры. Конструктор присваивает начальные значения двум закрытым данным-членам highTemp и lowTemp, которые являются числами с плавающей точкой. Метод UpdateTemp принимает новое значение данных и определяет, должно ли обновляться одно из значений температуры в объекте. Если отмечается новое самое низкое значение, то обновляется lowTemp. Аналогично, новое самое высокое значение изменит highTemp. Этот класс имеет два метода доступа к данным: GetHighTemp возвращает самую высокую температуру, a GetLowTemp возвращает самую низкую температуру. Спецификация класса Temperature ОБЪЯВЛЕНИЕ class Temperature { private: float highTemp, lowTemp; // закрытые данные-члены public: Temperature (float h, float 1); void UpdateTemp(float temp); float GetHighTemp(void) const; float GetLowTemp(void) const; }; ОБСУЖДЕНИЕ Конструктору должны быть переданы начальные высокая и низкая температуры для объекта. Эти значения могут быть изменены методом UpdateTemp. Методы GetLowTemp и GetHighTemp являются константными функциями, так как они не изменяют никакие данные-члены в классе. Класс описан в файле "temp.h". ПРИМЕР /Уточка кипения/замерзания воды по Фаренгейту Temperature fwater{212, 32); //точка кипения/замерзания воды по Цельсию Temperature cwater(100, 0); cout « Вода замерзает при << 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): highTemp(h), lowTemp(1)
{} //обновление текущих показаний температуры void Temperature::UpdateTemp (float temp) { if (temp> highTemp) highTemp = temp; else if (temp < lowTemp) lowTemp = temp; } // возвратить high (самая высокая температура) float Temperature::GetHighTemp (void) const { return highTemp; } // возвратить low (самая низкая температура) float Temperature::GetLowTemp (void) const { return lowTemp; } Программа З.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 « "В полдень: Наивысшая :" << today.GetHighTemp (); cout « " Низшая " « today.GetLowTempO « endl; cout « "Введите вечернюю температуру: "; cin » temp; // обновить объект для включения вечерней температуры today.UpdateTemp(temp); cout << "Сегодня наивысшая :" « today.GetHighTemp(); cout « " Низшая " « today.GetLowTempO « endl; } /* Оапуск программы pr03_02.cpp> Введите температуру в полдень: 80 В полдень: Наивысшая :80 Низшая 50 Введите вечернюю температуру: 40 Сегодня наивысшая :80 Низшая 40 */
Класс случайных чисел Для многих приложений требуются случайные данные, представляющие случайные события. Моделирование самолета, тестирующее реакцию летчика на непредвиденные изменения в поведении самолета, карточная игра, предполагающая, что дилер использует тасованную колоду, и изучение сбыта, предполагающее вариации в прибытии клиентов, — все это примеры компьютерных приложений, которые опираются на случайные данные. Компьютер использует генератор случайных чисел (random number generator), который выдает числа в фиксированном диапазоне таким образом, что числа равномерно распределяются в этом диапазоне. Генератор использует детерминистический алгоритм, который начинается с начального значения данных, называемого значением, инициализирующим алгоритм, или seed-значением. Алгоритм манипулирует этим значением для генерирования последовательности чисел. Этот процесс является детерминистическим, так как он берет начальное значение и выполняет фиксированный набор инструкций. Выход является уникальным, определенным данными и инструкциями. По существу, компьютер не производит истинные случайные числа, а создает последовательности псевдослучайных чисел (pseudorandom numbers), которые распределяются равномерно в диапазоне. Вследствие начальной зависимости от seed-значения, генератор создает ту же последовательность при использовании одного и того же seed-значения. Способность повторять случайную последовательность используется в исследованиях моделирования, где в приложении необходимо сравнить различные стратегии, реагирующие на один и тот же набор случайных условий. Например, имитатор полета использует одну и ту же последовательность случайных чисел для сравнения эффективности реакции двух летчиков на аварию самолета. Каждый летчик подвергается одному и тому же набору событий. Однако, если seed-значение изменяется каждый раз при запуске имитатора, мы имеем уникальное моделирование. Эта уникальность свойственна игре, которая обычно создает различную последовательность событий каждый раз в процессе игры. Большинство компиляторов предоставляют библиотечные функции, реализующие генератор псевдослучайных чисел. К сожалению, вариация этой реализации в зависимости от компилятора является значительной. Для предоставления генератора случайных чисел, переносимого из системы в систему, мы создаем класс RandomNumber. Этот класс содержит seed-значение, которое должно инициализироваться клиентом. В соответствии с начальным seed- значением генератор создает псевдослучайную последовательность. Класс обеспечивает автоматический выбор seed-значения, когда конструктору не передается никакого значения, и позволяет клиенту создавать независимые псевдослучайные последовательности. Спецификация класса RandomNumber ОБЪЯВЛЕНИЕ ♦include <time.h> // используется для генерации случайного числа //по текущему seed-значению const unsigned long maxshort - 65536L; const unsigned long multiplier = 1194211693L; const unsigned long adder = 12345L;
class RandomNumber { private: // закрытый член класса, содержащий текущее seed-значение unsigned long randSeed; public: // конструктор, параметр 0 (по умолчанию) задает автоматический // выбор seed-значения RandomNumber(unsigned long s » 0); // генерировать случайное целое в диапазоне [0, п-1] unsigned short Random(unsigned long n); // генерировать действительное число в диапазоне [0, 1.0] double fRandom(void); }; ОПИСАНИЕ Начальное seed-значение — это беззнаковое длинное число. Метод Random принимает беззнаковый длинный параметр п < 65536 и возвращает 16-битовое беззнаковое короткое значение в диапазоне 0,. . • , п - 1. Заметьте, что если возвращаемое методом Random значение присваивается целой переменной со знаком, то это значение может интерпретироваться как отрицательное, если п не будет удовлетворять неравенству п < 215 = 32768. Функция fRandom возвращает число с плавающей точкой в диапазоне 0 < fRandom() < 1.0. ПРИМЕР 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(lOO) « " "; // <sample> 93 21 45 5 3 Пример 3.3 Создание случайных данных 1. Значение грани кости находится в диапазоне 1 — 6 (шесть вариантов). Для имитации бросания кости используйте функцию die.Random(6), которая возвращает значения в диапазоне 0 — 5. Затем прибавьте 1 для перевода случайного числа в нужный диапазон. RandomNumber Die //использует автоматич. seeding dicevalue = die.Random(б) +1; 2. Объект FNum использует автоматическое задание seed-значения для создания случайной последовательности: RandomNumber FNum; Для вычисления плавающего значения в диапазоне 50 <, х < 75 генерируйте случайное число в диапазоне 0 — 25, умножая результат fRandom на 25. Это расширяет диапазон случайных чисел от 1-й единицы (0 < х < 1) до 25 единиц (0 < х < 25). Преобразуйте нижнюю границу нового диапазона, добавив 50: value = FNum. fRandom() *25 + 50; //умножение на 25; прибавление 50
Реализация класса RandomNumber Для создания псевдослучайных чисел мы используем линейный конгруэнтный алгоритм. Этот алгоритм использует большой нечетный постоянный множитель и постоянное слагаемое вместе с 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) { if (s == 0) randSeed = time(0); //использование системной функции 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]/float(NTOSSES) Это значение используется для помещения символа * в относительном местоположении между 1-й и 72-й позицией строки. Результирующий график является аппроксимацией биномиального распределения. #include <iostream.h> #include <iomanip.h> #include "random.h" // включает генератор случайных чисел // "бросить" numberCoins монет и возвратить общее число // выпадений лицевой стороны int TossCoins(int numberCoins) { static RandomNumber coinToss; int i, tosses = 0; 1 Здесь под лицевой стороной подразумевается та сторона монеты, на которой изображен монарх или президент. — Прим. ред.
for (i«0;i < numberCoins;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+l;j++) heads[j] = 0; // "бросать" монеты NTOSSES раз и записывать результаты в массив heads for (i=0;i< NTOSSES;i++) heads[TossCoins(NCOINS)]++; // печатать график частот for (i=0;i < NCOINS+l;i++) { position = int(float(heads[i])/float(NTOSSES) * 72); cout « setw(6) « i « " "; for (j=0;j <position-1;j++) cout « " " ; // ' *' относительное число бросаний с i лицевыми сторонами cout « ' *' « endl; } } /* Оапуск программы рг03_03 . срр> 0 * 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * */ 33. Объекты и передача информации Объект является экземпляром типа данных и как таковой может передаваться в качестве параметра функции или возвращаться как значение функции. Подобно другим типам C++, объектный параметр может передаваться по значению или по ссылке. Положения этого раздела иллюстрируются примерами из класса Temperature.
Объект как возвращаемое значение Любой тип класса может быть возвращаемым типом функции. Например, функция SetDailyTemp принимает в качестве параметра массив чисел, представляющий показания температуры, извлекает максимальное и минимальное показания из списка и возвращает объект Temperature с этими крайними значениями. Temperature SetDailyTemp (float readingf], int n) { //создание t с 1-ми значениями high и low Temperature t(reading[0], reading[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 T) { return T.GetHighTemp() - Т.GetLowTemp(); } Функция Celsius использует вызов по ссылке (call by reference) параметра Т типа Temperature, который, как первоначально подразумевалось, содержит значения по Фаренгейту. Функция создает объект типа Temperature, чьи самое высокое и самое низкое показания преобразуются в значения по Цельсию, и присваивает его объекту Т. void Celsius(Temperatures T) { float hi, low; //с = 5/9 * (f-32) hi - float (5)/9 * (T.GetHighTempO -32); low = float(5)/9 * (T.GetLowTempO -32); T = 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); //параметры по умолчанию После объявления массива длина и ширина каждого объекта roomfi] имеют нулевые значения: cout « room[25].GetLengh() //выход 0; cout « room[25].GetWidth() //выход 0; room[25].PutLengh(lO) //установка длины комнаты[25] на 10 room[25].PutWidth(5) //установка ширины комнаты[25] на 5 Объявление массива объектов Rectangle поднимает важную проблему, касающуюся массиЕов и классов. Если конструктор класса Rectangle не имеет параметров по умолчанию, объявление массива room вызовет ошибку, потому что каждый массив будет требовать параметры. Объявлению потребуется список инициализаторов массива, который управляет каждым элементом в массиве. Например, для объявления массива room из 100 элементов и установки параметров длины и ширины на 0 потребуется список инициализаторов 100 объектов Rectangle. В действительности это на практикуется. Rectangle room[100] = {Rectangle(0, 0), . . . , Rectangle(0, 0)};
Для объявления массива объектов мы предоставляем конструктору значения по умолчанию или просто создаем конструктор без параметров. Конструктор умолчания Конструктор умолчания (default costructor) — это конструктор, не требующий никаких параметров. Это бывает, когда конструктор не имеет параметров или когда каждый параметр имеет значение по умолчанию. В этой главе класс Rectangle содержит конструктор умолчания, тогда как класс Temperature требует параметров при объявлении объекта. Класс Rectangle КОНСТРУКТОР Rectangle(float 1=0, float w=0); Конструктор содержит параметры 1 и w со значением по умолчанию 0. При создании массива Rectangle значения по умолчанию присваиваются каждому объекту. Rectangle R[25]; //каждый элемент имеет значение Rectangle(0, 0) Класс Temperature КОНСТРУКТОР Temperature(float h, float 1); Класс Temperature не содержит конструктор по умолчанию. Вместо этого, объекту должно быть дано начальное значение для высокой и низкой температуры. Объявление объектов today и week является недействительным! Temperature today; //недействительно: отсутствуют параметры Temperature week[7]; //Temperature не имеет конструктора по умолчанию 3.5. Множественные конструкторы До сих пор в наших классах мы разрабатывали как default-, так и nonde- fault-конструкторы1. В результате предыдущих рассуждений вы можете предположить, что они являются взаимоисключающими, поскольку все классы имели одиночный конструктор. C++ "признает" нашу потребность в разнообразии способов инициализации объекта и позволяет определять множественные конструкторы в одном и том же классе. Компилятор использует перегрузку функции для выбора правильной формы конструктора, когда мы создаем объект. Концепция перегрузки функции и ее правила обсуждаются в главе 6. Multiple-конструкторы добавляют большие возможности классу. Особый тип multiple-конструктора, называемый конструктором копирования (copy constructor), используется со многими классами, содержащими динамические данные-члены. Конструктор копирования описывается в главе 8. 1 Следует отметить, что в русских изданиях термин default constructor встречается как конструктор умолчания или default-конструктор. В то же время под термином nondefault constructor (или nondefault-конструктор) следует понимать конструктор с самым обычным синтаксисом, из которого не следуют никакие дополнительные свойства конструктора. — Прим. ред.
месяц 1 < m < 12 день 1 < d < 31 год 1900 <, у £ 1999 Класс Date иллюстрирует использование multiple конструкторов. Этот класс имеет три данных-члена, которые обозначают месяц, день и год в дате. Один конструктор имеет три параметра, соответствующие трем данным-членам. Действием конструктора является инициализация этих переменных. Второй конструктор позволяет клиенту объявлять дату как строку в форме "mm/dd/yy", читает эту строку и преобразует пары символов "mm" в месяц, "dd" в день и "уу" в год. Для каждого конструктора мы подразумеваем, что параметр, задающий год — это значение из двух цифр в диапазоне 00-99. Фактический год сохраняется добавлением 1900 к начальному значению: year - 1900 + уу Класс Date имеет метод, который выводит на экран полную дату с названием месяца, дня и значением года. Например, первый день в двадцатом веке был 1 января 1900 Спецификация класса Date ОБЪЯВЛЕНИЕ #include <string.h> #include <strstream.h> class Date { private: // закрытые члены, которые определяют дату int month, day, year/ public: // конструкторы, дата по умолчанию — Январь 1, 1900 Date (int m = 1, int d = 1, int у = 0); Date (char *dstr); // вывод данных в формате "месяц день, год" void PrintDate (void); }; ОПИСАНИЕ Для построения Date-объектов используются два конструктора, отличающиеся параметрами. Компилятор выбирает конкретный конструктор во время создания Date-объекта. Следующие примеры демонстрируют создание объектов. ПРИМЕРЫ Date dayl(6, 6, 44); // 6 июня 1944 Date day2; // значение по умолчанию для 1 января 1990 date day3("12/31/99"); // 31 декабря 1999 Реализация класса Date Сердцевиной класса Date являются два его конструктора, которые определяют дату, передавая значения месяца, дня и года или строки "mm/dd/yy".
Первый конструктор имеет три параметра со значениями по умолчанию, соответствующими 1 января 1900 года. Со значениями по умолчанию конструктор квалифицируется как конструктор умолчания: // конструктор, day и year задаются как целые ram dd yy Date::Date (int m, int d, int y) : month(m), day(d) { 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/ ); При выводе метод Print дает текст полной даты, включающий название месяца, дня и год. Массив months содержит пустую строку (индекс 0) и 12 названий для календарных месяцев. Значение месяца используется как индекс в массиве для печати названия месяца. // печать даты с полным названием месяца void Date::PrintDate (void) { // статический массив с названиями месяцев static char *Months[] = {"","Январь","Февраль", "Март","Апрель","Май", "Июнь","Июль","Август", "Сентябрь","Октябрь", "Ноябрь","Декабрь"}; cout « Months [month] « " " « day « ", "« year ; }; Программа 3.4. Дата двадцатого века Тестовая прграмма использует конструкторы для установки демонстрационных объектов. Получаемые в результате данные печатаются. Класс Date содержится в файле "date.h".
#include <iostream.h> #include "date.h" // включение класса Date void main(void) { // Date-объекты с целыми, умалчиваемыми и строчными параметрами Date dayl(6,6,44) ; // Июнь б, 1944 Date day2; // Январь 1, 1900 Date day3("12/31/99"); // Декабрь 31, 1999 cout « "День Д во Второй Мировой войне — "; dayl.PrintDate(); cout « endl; cout « "Первый день 20-ого века — "; day2.PrintDate(); cout << endl; cout « "Последний день 20-ого века — "; day3.PrintDate(); cout « endl; } /* <Выполнение программы 3.4> День Д во Второй Мировой войне — Июнь б, 1944 Первый день 20-ого века — Январь 1, 1900 Последний день 20-ого века — Декабрь 31, 1999 */ 3.6. Практическое применение: Треугольные матрицы Двумерный массив, часто называемый матрицей (matrix), предоставляет важную для математики структуру данных. В этом разделе мы исследуем квадратные матрицы (square matrices — матрицы с одинаковым числом строк и столбцов), чьи элементы данных являются действительными числами. Мы разрабатываем класс TriMat, определяющий верхние треугольные матрицы (upper triangular matrices), в которых все элементы, находящиеся ниже диагонали, имеют нулевые значения. В математических терминах, Ау=0 для j<i. Верхний треугольник определяется элементами Ау для j>i. Эти матрицы имеют важные алгебраические
свойства и используются для решения систем уравнений. Реализация операций верхней треугольной матрицы в классе TriMat показывает способ эффективного хранения треугольной матрицы в виде одномерного массива. Свойства верхней треугольной матрицы Если верхняя треугольная матрица имеет л2 элементов, приблизительно половина из них являются нулевыми и нет необходимости сохранять их явно. Конкретно, если мы вычитаем п диагональных элементов из суммы п2 элементов, то половина оставшихся элементов являются нулевыми. Например, при л=25 имеется 300 элементов со значением 0: (П2 _ п)/2 = (252 _ 25)/2 = (625 — 25)/2 = 300 Далее следует набор операций для треугольных матриц. Мы определяем сложение, вычитание и умножение матриц, а также детерминант, который имеет важное применение для решения уравнений. Сумма или разность двух треугольных матриц А и В получается в результате сложения или вычитания соответствующих элементов матриц. Результирующая матрица является треугольной. Сложение С = А + В где С — это треугольная матрица с элементами Ctj = Ац + Bij. Вычитание С = А — В где С — это треугольная матрица с элементами dj = Aij - Bq. Умножение С = А * В Результирующая матрица С — это треугольная матрица с элементами С*,/, значения которых вычисляются из элементов строки i матрицы А и столбца у матрицы В: Citr(Aii0*B0,j) + (Au*Bltj) + (Ait2*B2J) + . . . + (Aitnl*Bn.u) Например, если Со,2 — это сумма произведений элементов строки 0 матрицы А и колонки 2 матрицы В. 1*4+1*1+0*3=5
Произведение матриц А и В: Для общей квадратной матрицы детерминант является сложной для вычисления функцией, однако вычислить детерминант треугольной матрицы не трудно. Просто получите произведение элементов на диагонали. Хранение треугольной матрицы Применение для хранения верхней треугольной матрицы стандартного двумерного массива требует использования всей памяти размером л2, несмотря на прогнозируемые нули, расположенные ниже диагонали. Для исключения этого пространства мы сохраняем элементы из треугольной матрицы в одномерном массиве М. Все элементы ниже главной диагонали не сохраняются. Таблица 3.1 показывает количество элементов, которые сохраняются в каждой строке. Таблица 3.1 Хранение треугольной матрицы Строка 0 1 2 ... п-2 п-1 Число элементов п п-1 п-2 ф •» 2 1 Элементы (Ао, о . . ■ Ао, n-i) (Ai( i . . . Ai, n-i) (A2, 2 . . . A2, n-i) • • • (An-2. n-2 • • • An-2, n-i) (An-1, n-i) Алгоритму сохранения требуется функция доступа, которая должна определять местоположение в массиве М элемента Ау. Для j < i элемент Ay является равным 0 и не сохраняется в М. Для j > i функция доступа использует информацию о числе сохраняемых элементов в каждой строке вплоть до строки i. Эта информация может быть вычислена для каждой строки i и сохранена в массиве (rowTable) для использования функцией доступа.
Строка 0 1 2 3 rowTable rowTable[0] = 0 rowTable[1] = n rowTable [2] = n + n-1 rowTable [3] = n + n-l+n-2 Замечание 0 элементов перед строкой 0 п элементов перед строкой 1 (от строки 0) п + п-1 элементов перед строкой 2 элементы перед строкой 3 n-1 rowTable[n-1] = n + n-1 + ... +2 Строка 0 1 2 rowTable rowTable[0] «■ 0 rowTable[1] - 3 rowTable[2] - 5 Замечание 0 элементов, сохраненных перед строкой 0 3 элемента строки 0 (110) 5 элементов из строк 0 и 1 (11021) Row Table Элементы треугольной матрицы сохраняются по строкам в массиве М. Массив М Элементы строки 0 Элементы строки 1 Элементы строки 2 С учетом того, что элементы треугольной матрицы сохраняются построчно в массиве М, функция доступа для Ац использует следующие параметры: Индексы i и j, Массив rowTable Алгоритм доступа к элементу Ау заключается в следующем: 1. Если j<i, Aij = 0 и этот элемент не сохрдняется. 2. Если j>i, то получается значение rowTable[i], являющееся количеством элементов, которые сохраняются в массиве М, для элементов до строки L В строке i первые i элементов являются нулевыми и не сохраняются в М. Элемент Aij помещается в M[rowTable[i] + (j — i)]. Пример 3.4 Рассмотрим матрицу X размера 3x3
Пример 3.5 Рассмотрим треугольную матрицу Х[3][3] из примера 3.4: 1. X0t2 =M[rowTable[0] + (2 — 0)] =М[0 + 2] =М[2] = 0 2. Xi,o не сохраняются 3. Xlt2 =M[rowTable[l] + (2 — 1)] =М[3 4- 1] =М[4] = 1 Класс TriMat Класс TriMat реализует ряд операций треугольной матрицы. Вычитание и умножение треугольной матрицы оставлены для упражнений в конце главы. Учитывая то ограничение, что мы должны использовать только статические массивы, наш класс ограничивает размер строки и столбца числом 25. При этом мы будем иметь 300 = (252 — 25)/2 нулевых элементов, поэтому массив М должен содержать 325 элементов. Спецификация класса TriMat ОБЪЯВЛЕНИЕ #include <iostream.h> #include <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 TriMat& A) const; double DelMat(void) const; // матричные операции ввода/вывода void ReadMat(void); void WriteMat(void) const;
// получить размерность матрицы int GetDimension(void) const; }; ОПИСАНИЕ Конструктор принимает число строк и столбцов матрицы. Методы PutEle- ment и GetElement сохраняют и возвращают элементы верхней треугольной матрицы. GetElement возвращает 0 для элементов ниже диагонали. AddMat возвращает сумму матрицы А с текущим объектом. Этот метод не изменяет значение текущей матрицы. Операторы ввода/вывода ReadMat и WriteMat работают со всеми элементами матрицы п х п. Сам метод ReadMat сохраняет только верхне-треугольные элементы матрицы. ПРИМЕР #include trimat.h // включить класс TriMat TriMat A(10), В(10), С(10); // треугольные матрицы 10x10 A.ReadMat(); // ввести матрицы А и В В.ReadMat(); С = A.AddMat(В); // вычислить С = А + В С.WriteMat(); // печатать С Реализация класса TriMat Конструктор инициализирует закрытый член п параметром matsize. Таким образом задается число строк и столбцов матрицы. Этот же параметр используется для инициализации массива rowTable, который используется для доступа к элементам матрицы. Если matsize превышает ROWLIMIT, выдается сообщение об ошибке и выполнение программы прерывается. // инициализация п и rowTable TriMat::TriMat(int matsize) { int storedElements = 0; // прервать программу, если matsize больше ROWLIMIT if (matsize > ROWLIMIT) { cerr « "Превышен размер матрицы" « ROWLIMIT « « "x" « ROWLIMIT « endl; exit (1); } n = matsize; // задать таблицу for(int i = 0; i < n; i++) { rowTable[i] = storedElements; storedElements += n — i; } } Матричные методы доступа. Ключевым моментом при работе с треугольными матрицами является возможность эффективного хранения ненулевых элементов в линейном массиве. Чтобы достичь такой эффективности и все же использовать обычные двумерные индексы i и j для доступа к элементу матрицы, нам необходимы функции PutElement и GetElement для сохранения и возвращения элементов матрицы в массиве.
Метод GetDimension предоставляет клиенту доступ к размеру матрицы. Эта информация может использоваться для обеспечения того, чтобы методам доступа передавались параметры, соответствующие правильной строке и столбцу: // возвратить размерность матрицы п int TriMat::GetDimension(void) 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 i >- п) II (j < О I | j >= n)) { cerr « "PutElement: индекс вне диапазона 0 — " « n-1 « endl; exit (l); } // все элементы ниже диагонали игнорируются if (j >= i) M[rowTable[i] + j-i] = item; } Для получения любого элемента метод GetElement проверяет индексы i и j. Если i или j не находится в диапазоне 0 . . (п — 1), программа заканчивается. Если j<i, то элемент находится в нижней треугольной матрице со значением 0. GetElement просто возвращает несохраняемое значение 0. В противном случае, j>i, и метод доступа может возвращать элемент из массива М: // получить матричный элемент [i, j] массива М double TriMat::GetElement(int i, int j) const { // прервать программу, если индексы вне индексного диапазона if ((i < 0 | | i >= n) || (j < 0 | I j >- n)) < cerr « "GetElement: индекс вне диапазона 0 — " « n-1 « endl; exit (1); } if (j >- i) // вернуть элемент, если он выше диагонали return M[rowTable[i] + j-i]; else // элемент равен 0, если он ниже диагонали return 0; } Ввод/вывод матричных объектов. Традиционно, ввод матрицы подразумевает, что данные вводятся построчно с полным набором значений строк, и столбцов. В объекте TriMat нижняя треугольная матрица является нулевой и значения не сохраняются в массиве. Тем не менее, пользователю предлагается ввести эти нулевые значения для сохранения обычного матричного ввода. // читать элементы матрицы построчно, клиент должен ввести // все (п х п) элементов void TriMat::ReadMat(void)
{ double item; int i, j; for (i = 0; i < n; i++) // сканировать строки for (j = 0; j < n; j++) // для каждой строки сканировать столбцы { cin >> item; //читать [i, j ] -й элемент матрицы PutElement (item, i, j ); // сохранить этот элемент } } // построчная выдача в поток элементов матрицы void TriMat::WriteMat(void) const { int i, j; // установка режима выдачи cout.setf(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 = X + Y используйте оператор Z = X.AddMat(Y); Алгоритм сложения двух объектов типа TriMat возвращает новую матрицу В с элементами By = CurrentObjecty + Ay: // возвращает сумму текущей и матрицы А. // текущий объект не изменяется TriMat TriMat::AddMat (const TriMat& A) const { int i,j; double itemCurrent, itemA; TriMat B(A.n); // в В будет искомая сумма for (i « 0; i < n; i++) // цикл по строкам { for (j - i; j < n; j++) // пропускать элементы ниже диагонали { itemCurrent = GetElement(i,j); itemA = A.GetElement(i,j); B.PutElement (itemCurrent + itemA, i, j); } } return B;
Метод DetMat возвращает детерминант текущего объекта. Возвращаемое значение — это действительное число, которое является произведением элементов диагонали. Полный текст кода для реализации класса TriMat можно найти в программном приложении. Программа 3.5. Операции с классом TriMat Тестовая программа иллюстрирует класс TriMat с операциями ввода/вывода, а также матричного суммирования и определения детерминанта. Каждая секция программы снабжена комментариями. #include <iostream.h> #include <iomanip.h> #include "trimat.h" // включить класс TriMat void main(void) { int n; // задать размер однородной матрицы cout << "Каков размер матрицы? "; cin >> п; // объявить три матрицы размером (n x п) TriMat A(n), B(n), C(n); // читать матрицы А и В cout « "Введите некоторую " << п « " х " « п « " треугольную марицу" << endl; A.ReadMat(); cout << endl; cout << "Введите некоторую " << n << " x " << n << " треугольную марицу" << endl; B.ReadMatO ; cout « endl; // выполнить операции и напечать результат cout « "Сумма А + в" << endl; С = A.AddMat(В); C.WriteMatO ; cout « endl; cout « "Детерминант A+B= " « С.DetMat{) « endl; } /* <Выполнение программы 3.5> Каков размер матрицы? 4 Введите некоторую 4x4 треугольную марицу 12-45 0 2 4 1 0 0 3 7 0 0 0 5 Введите некоторую 4x4 треугольную матрицу
14 6 7 О 2 б 12 0 0 3 1 0 0 0 2 Сумма. А + В 2.000 6.000 2.000 12.000 0.000 4.000 10.000 13.000 0.000 0.000 6.000 8.000 0.000 0.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; } void printX(void); } (б) class Y { private: int p; int q/ public Y (int n, int m) : n(p) q(m); { } };
3.4 (а) Объявите спецификацию для класса X, который имеет следующее: Закрытые члены: Целые переменные а, Ь, с. Открытые члены: Конструктор, который присваивает значения переменным а, Ъ, с; значения по умолчанию будут равны 1. Функцию F, возвращающую максимум переменных а, Ь, с. (б) Напишите конструктор для класса 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 UpdateGradeInfo(int newunits, int newgradepts); }; Этот класс ведет запись отметок для студента. Переменные gradepts и units используются методом ComputeGPA для присваивания средней успеваемости студента переменной gpa. Используйте формулу: gpa = gradepts/units Напишите код для этой функции-члена. Конструктор и ComputeGPA должны быть выполнены как код in-line. 3.6 ADT Calendar содержит элементы данных year и логическое значение leapyr. Его операции следующие: Конструктор Инициализирует данные-члены year и leapyr. NumDays(mm.dd) Возвращение количества дней с самого начала года до заданного месяца mm и дня dd. Leapyear(void) Указывает, является ли год високосным. PrintDate(ndays) Печатает дату 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 PrintCard(int с); }; (а) Реализуйте эти методы. СОВЕТ: Для тасования карт используйте цикл со сканированием 52-х карт. Для карты i выберите случайное число в диапазоне от i до 51 и поменяйте местами карту с этим случайным индексом и карту с индексом i. (б) Запишите функцию void DealHand(CardDeck& d, int n) ; которая сдает п карт из d, сортирует их и печатает их значения. 3.9 Используя класс Temperature из раздела 3.2, напишите функцию: Temperature Average( temperature a[ ], int n); которая возвращает объект типа Temperature, содержащий среднюю низкую и высокую температуры п показаний. 3.10 Используйте класс Date из раздела 3.5. (а) Измените класс Date для включения метода IncrementDate. Он принимает положительное число дней в диапазоне 0-365, добавляет его к текущей дате и возвращает объект, имеющий новую дату. (б) Сделайте так, чтобы параметр для IncrementDate мог принимать отрицательные значения. 3.11 Покажите использование класса случайных чисел для моделирования следующего: (а) Одна пятая часть автомобилей штата не соответствует стандартам по вредным эмиссиям. Используйте fRandom, чтобы определить, отвечает ли этим стандартам случайно выбранная автомашина. (б) Вес особи в популяции варьируется в пределах 140-230 фунтов. Используйте Random для выбора веса какого-либо человека в этой популяции. 3.12 Рассмотрите класс Event, которому передано начальное значение для нижнего и верхнего предела времени какого-либо события. Границы принимают значение по умолчанию 0 и 1. Операции:
Конструктор Инициализировать границы данных. Если нижняя граница выходит за верхнюю границу, печатать сообщение об ошибке и выходить из программы. GetEvent Получать случайное событие в диапазоне: нижняя граница — верхняя граница. (а) Реализуйте этот класс, используя код in-line. (б) Реализуйте этот класс, определяя функции-члены вне объявления класса. (в) Приложение требует массив из пяти объектов Event, где каждый элемент принимает значение в диапазоне от 10 до 20. Как вы инициализируете массив? Проще ли решается данная задача, если диапазон для каждого объекта будет 0-1? 3.13 Напишите функцию Datelnterval, которая принимает два объекта класса Calendar из письменного упражнения 3.6 и возвращает число дней между двумя этими датами. 3.14* Матрицы могут использоваться для решения систем уравнений с п неизвестными. Мы показываем алгоритм для системы с тремя неизвестными. В этой системе уравнений элементы Ау являются коэффициентами неизвестных Хо, Xi и Хг. В правой части уравнений даются элементы Q. Эти уравнения могут быть описаны одним матричным уравнением где матрица называется матрицей коэффициентов (coefficient matrix). Например, система уравнений 1Х0 + 1ХХ + 0Х2 = 4 -ЗХ0 - lXj + 1Х2 = -11 2Х0 + 2Х: + 2Х2 « 14 соответствует матричному уравнению Теорема математики утверждает, что эти уравнения могут быть сведены к системе эквивалентных уравнений, в которой матрица коэффициентов является треугольной. В нашем примере: 1. Исключите элемент Аю = -3.
Умножьте элементы в строке 0 на константу 3 и сложите элементы строки 0 с элементами строки 1. 2. Исключите элемент Аго = 2. Умножьте элементы строки 0 на константу -2 и сложите элементы строки 0 с элементами строки 2. В этом процессе исключается также член Агь Матричное уравнение для новой системы имеет треугольную матрицу коэффициентов. (а) Сведите алгебраическую систему к уравнению, включающему треугольную матрицу коэффициентов (б) Найдите детерминант этой матрицы коэффициентов. Упражнения по программированию 3.1 Многие программные приложения используют постоянно обновляемый сумматор (accumulator). В качестве простого примера абстрактного типа данных предположим, что Accumulator — это тип данных, которые обновляются операцией сложения и выводятся с использованием операции печати. ADT Accumulator Данные Действительное значение для суммирования Операции Initialize Вход: Действительное значение N. Предусловия: Нет Процесс: Присваивание N в качестве значения суммы. Выход: Нет Постусловия: Сумма инициализируется. Add Вход: Действительное число N.
Предусловия: Нет Процесс: Сложение N с суммой. Выход: Нет Постусловия: Сумма обновляется. Print Вход: Нет Предусловия: Нет Процесс: Чтение суммы. Выход: Печать суммы. Постусловия: Нет Конец ADT Accumulator Банковское приложение считывает начальный баланс и последовательность операций. Отрицательная операция определяет дебет, а положительная — кредит. Используются три объекта типа Accumulator. Объект Balance определяется со стартовым балансом в качестве параметра конструктора. Объекты Debits и Credits имеют начальное значение 0 и используются для поддержки определения текущей суммы дебетных и кредитных операций. Оператор Add обновляет сумму в объектах. Считайте последовательность операций, заканчивающихся на операции 0.00. Печатайте окончательные значения баланса, дебетов и кредитов. 3.2 Напишите main-функцию, которая использует класс, реализованный в письменном упражнении 3.5 со следующими данными: Студент Id 1047 3050 | 0020 Grade Points 120 75 100 Units | 40 20 _75 J (а) Печатайте информацию по каждому студенту. (б) Последний студент (ID 0020) имеет дополнительные записи из летней школы. Обновите запись со следующими новыми данными: успеваемость 40 при 10 часах. Печатайте новые данные для этого студента. 3.3 Расширьте класс Circle из раздела 1.3 для вычисления площади сектора. Площадь сектора определяется по формуле (Q/360)*7ir2. Сектор Используйте этот класс для решения следующей задачи: Круглая игровая площадка определяется как объект с радиусом 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 = Number_Rolls* 4,00. 3.4 Напишите класс, содержащий индикатор пола (М или F), возраст и ID-номер в диапазоне от 0 до 100. Операции включают Read, Print и функции Getld/GetAge/GetGender, которые возвращают ID, возраст и пол человека, сохраняемые в объекте. Напишите программное приложение, определяющее объекты Young- Women и OldMen. Программа вводит информацию о ряде людей и присваивает данные о самых молодых женщинах в YoungWomen и самых старых мужчинах в OldMen. Ввод завершается ID-номером О. Используя 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; } (а) Реализуйте класс Geometry, используя внешние функции. Первый конструктор для окружности будет иметь один параметр и следовательно присваивать объект типа Circle переменной figuretype. Другой конструктор, будет присваивать этой переменной объект типа Rectangle. Вычисляющие методы должны проверять тип figuretype перед вычислением возвращаемого значения. (б) Пользователь вводит внутренний радиус, который затем используется для создания маленькой окружности. Используйте эту информацию для объявления описывающего окружность прямоугольника и внешней окружности, описывающей этот прямоугольник. Печатайте периметр, площадь и диагональ каждого объекта.
радиус Вычислите площадь внешней полосы между двумя окружностями (площадь вне маленькой окружности, но внутри большой окружности). Вычислите периметр маленькой области, помеченной символом "X". 3.6 Класс Ref подсчитывает количество положительных (>0) и отрицательных (<0) чисел, "представленных на рассмотрение". Конструктор не имеет параметров и инициализирует элементы данных 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 выполняется успешно. 3.7 Рассмотрим следующее объявление класса: 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); }; Напишите функцию main по частям от (а) до (е): (а) Напишите реализацию для функций-членов. Используйте строковую функцию 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 из п элементов и возвращает индекс объекта, имя которого соответствует ключу. Если ключ не найден в Arr, возвращается -1. (д) Вызывайте Search три раза с разными именами для проверки правильности ее работы. 3.8 Используйте класс CardDeck, разработанный в письменном упражнении 3.8, для карточной игры под названием Hi-Low. Сдайте пять карт. Для каждой карты спросите игрока, будет ли случайно вытащенная из оставшихся карт в колоде карта больше или меньше данной карты. Туз — самая большая карта любой масти, и масти упорядочены от трефовой до пиковой. Печатайте количество удачных догадок. 3.9 В данном упражнении используйте класс Calendar, разработанный в письменном упражнении 3.6. Запишите функцию int Daylnterval(Calendar С, int mml, intddl, intmm2/ int dd2);
возвращающую количество дней между двумя данными. Напишите функцию main, выполняющую следующее: 1. Печатает, является ли текущий год високосным. 2. Использует NumDays для определения количества дней от начала года до Рождества. 3. Передает результат (2) функции PrintDate и проверяет правильность печати даты. 4. Включает вычисление количества дней от сегодняшнего дня до Рождества. 5. Вычисляет количество дней между 1 февраля и 1 марта. 3.10 Расширьте класс Dice из главы 1 до бросания п костей, п<:20. Если количество костей не дается конструктору, он устанавливается на значение по умолчанию 2. Используйте класс Dice для решения следующей задачи: Объявите массив из 30 объектов Dice. Инициализируйте каждый элемент для бросания пяти (5) костей. Вам понадобиться использовать конструктор умолчания в объявлении, а затем — цикл для инициализации каждого элемента пяти костей. Выполните Toss для каждого элемента. Сканируйте список и определите, сколько раз была брошена 5 или 12. Укажите, сколько раз сумма повторялась в следующем бросании. Например, 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 в измерение m (а) Реализуйте этот класс. Один дюйм равен 2,54 см. (б) Напишите функцию, сканирующую список и преобразующую элементы в единицы измерения, заданные как параметр. Подразумевается, что объекты выражены в других единицах измерения. (в) Напишите функцию, которая сортирует массив объектов Height. Подразумевается, что каждый объект использует одну и ту же единицу измерения.
(г) Напишите функцию, сканирующую массив и возвращающую объект, представляющий самого высокого человека. Считайте, что все объекты используют одну и ту же единицу измерения. (д) Напишите программу для тестирования класса, создав список из пяти элементов типа Height. Инициализируйте первые три из них в объявлении и считайте последние два. Используйте функции, разработанные в частях (Ь), (с) и (d). 3.12 В банке с одним только кассиром заметили, что клиентские операции занимают интервал времени 5-10 минут. Очередь из 10 клиентов образовалась в момент открытия. Используйте класс Event, разработанный в письменном упражнении 3.12 для вычисления времени, необходимого кассиру, чтобы обслужить 10 клиентов. 3.13 Эллипс или овал определяется описывающим прямоугольником, размеры которого 2а х 2Ь. Константы а и b называются полуосями эллипса. Эллипс, полуоси которого имеют одну и ту же длину, являетя окружностью. Математическое уравнение эллипса имеет вид: (х - х0)2/а2 + (у - у0)2/Ь2 = 1 и его площадь равна nab. Разработайте класс Ellipse, функции-члены которого состоят из конструктора и метода Area. Используйте Ellips и класс Rectangle для решения следующей задачи: Необходимо построить овальный плавательный бассейн, полуоси которого являются длинами 30 и 40, внутри прямоугольной площади 80 х 60. Стоимость бассейна $25 000. Площадь снаружи бассейна необходимо зацементировать. Стоимость цемента составляет $50/кв.фут. Вычислите общую стоимость строительства. 3.14 Данные о бейсболисте включают номер игрока (number), количество раз, когда он отбивающий (times at bat), количество ударов по мячу (hits) и средний результат (NumberHits/ NumberAtBats). Эта информация сохраняется как данные-члены в закрытой секции класса Baseball. Все параметры конструктора имеют значения по умолчанию: номер униформы устанавливается равным -1, а значения числа отбиваний и ударов — 0. Применение для номера униформы значения по умолчанию подразумевает, что номер униформы игрока, количество ударов и количество раз, когда игрок является отбивающим, считываются с ис-
пользованием ReadPlayer. Для известного номера униформы функция- член ReadPlayer вводит количество ударов и количество отбиваний. Метод GetBatAve возвращает средний результат отбиваний. Закрытый метод ComputeBatAve используется как утилита конструктором, a ReadPlayer — для задания данного-члена, содержащего средний результат отбиваний. Метод WritePlayer выводит всю информацию об игроке в формате: Player <UniformNo> Average < BattingAvg> Средний результат выводится как целое число из трех цифр. Например, если количество ударов 30, а количество раз, когда игрок был отбивающим, равно 100, то средний результат выводится методом WritePlayer как 300. Объявление класса Baseball class Baseball { private: int playerno; int atbats; int hits; float batave; //ComputeBatAve дается с inline-кодом. float ComputeBatAve (void) const //закрытый метод { if(playerno == -1 atbats == 0) return(0); else returne(float(hits)/atbats); { public: Baseball (int n = -1, int ab - 0, int h = 0) ; void ReadPlayer(void) ; void WritePlayer(void) const; float GetBatAve(void) const; }; Реализуйте класс Baseball и используйте его в функции main следующим образом: 1. Объявите четыре объекта: Catcher Номер униформы 10, 100 отбиваний, 10 ударов Shortstop Имеется только номер униформы 44 Centerfielder Нет никакой информации Maxobject Нет никакой информации 2. Считайте необходимую информацию для объектов shortstop и centerfielder. 3. Выпишите всю информацию для объектов catcher, shortstop и centerfielder. 4. Используя операцию GetBatAve и присваивание объекта, присвойте игрока с самым высоким средним результатом объекту maxobject и распечатайте информацию.
3.15 Добавьте вычитание и умножение треугольной матрицы к классу TriMat и протестируйте их в программе, подобной программе 3.5. 3.16 При решении общей n x n системы алгебраических уравнений ряд операций сводит задачу к решению уравнения треугольной матрицы. Уравнение треугольной матрицы имеет единственное решение, при условии, что детерминант матрицы коэффициентов является ненулевым. Набор алгебраических уравнений получают умножением каждой строки в матрице коэффициентов на столбцовый массив неизвестных. Решая уравнения в порядке от п — 1 до 0, мы получаем единственное решение для переменных Xn-i, Xn-2, ..., Xi,Xo. Например, треугольная система уравнений, описанная в письменном упражнении 3.14, решается применением этого метода: Уравнение 0: ix0 + ixx + ox2 = 4 Уравнение 1: 2х1 + ix2 =1 Уравнение 2: 2х2 = б Решение для Х2 В уравнении 2 х2 = 6/2 = з Решение для Хх: В уравнении 1 подставьте 3 для Х2; решите уравнение для неизвестного Хх. 2Х1 + 3 = 1 Решение для Х0: В уравнении 0 подставьте -1 для Хг и 3 для Х2; решите уравнение для неизвестного Х0. Х0 - 1 = 4 Х0 = 5 Окончательное решение: Хо =5, Xi = -1, Х2 = 3. Объедините эти идеи и разработайте функцию void SolveEqn(const TriMat& A, double X[ ], double C[ ]); Она определяет единственное решение, если оно существует, общего уравнения треугольной матрицы (а) Используйте SolveEqn в программе для решения примера системы уравнений.
(б) Решите систему уравнений в письменном упражнении 3.14 (а).
глава Шеф Классы коллекций 4.1. Линейные коллекции 4.2. Нелинейные коллекции 4.3. Анализ алгоритмов 4.4. Последовательный и бинарный поиск 4.5. Базовый класс последовательного списка Письменные упражнения Упражнения по программированию
В главе 2 описываются базовые типы данных, которые непосредственно поддерживаются языком программирования и включают примитивные числовые и символьные данные, а также массивы, строки и записи. Эти структурированные типы данных являются примерами коллекций (collections), которые сохраняют данные и предоставляют операции доступа, добавляющие, удаляющие или обновляющие элементы данных. Изучению типов коллекций уделяется основное внимание в данной книге. Коллекции подразделяются на две основные категории: линейные и нелинейные. На рис. 4.1 приводятся методы доступа к данным для дальнейшего деления категорий и перечисления структур данных, представленных в этой книге. В данной главе приводится краткий обзор каждой коллекции вместе с описанием ее данных, операций и некоторых случаев практического использования. Линейная (linear) коллекция содержит список элементов, упорядоченных по положению (рис.4.2). В этом списке имеется первый элемент, второй и т.д. Массив с индексом, отражающим порядок элементов, является основным примером линейной коллекции. Нелинейная (nonlinear) коллекция определяет элементы без позиционного упорядочения. Например, цепочка управления рабочими на заводе или комплект мячей в сетке — это нелинейные коллекции (рис. 4.3). Эта глава включает также исследование эффективности алгоритмов. Мы описываем факторы, определяющие эффективность и вводим нотацию Big-0 (большая О) в качестве ее критерия. Этот критерий используется на протяжение всей книги для сравнения и сопоставления различных алгоритмов. Класс SeqList из главы 1 является основным типом коллекций. В данной главе описывается реализация этого класса на базе массива. Этот класс рассматривается также в главе 9, когда мы определяем реализацию связанного списка. В главе 12 SeqList используется с наследованием для создания упорядоченного списка. Когда C++ реализует коллекции как классы, компилятор требует параметры функции, чтобы иметь специфические типы данных, и выполняет тщательную проверку типа на предмет совместимости. Для наиболее общей реализации типов коллекций мы вводим классы шаблонов (template) C++ в Коллекции Линейные Нелинейные С индексным доступом С прямым доступом С последовательным доступом Иерархические Групповые Словарь Hash- таблица Массив Запись Файл Список Стек Очередь Очередь приоритетов Дерево Heap- дерево Набор Граф Рис. 4.1. Иерархия коллекций
главе 7. Классы шаблонов пишутся с использованием параметризованного имени, такого как Т для типа данных, управляемых коллекцией. Когда объявляется какой-либо объект, фактический тип для Т задается как параметр. Шаблоны являются мощным инструментом C++, позволяющим выполнять параметризованное объявление классов. Например, предположим, что класс коллекции имеет массив из 10 элементов. Первый элемент Второй элемент Третий элемент Последний элемент Рис. 4.2. Линейная коллекция Менеджер завода Менеджер производства Менеджер сбыта Рабочий Рабочий Рабочий Рабочий Рабочий Рис. 4.3. Нелинейные коллекции Первое объявление определяет массив целых. Версия шаблонов не предполагает определенного типа, а позволяет классу использовать параметризованное имя Т для типа элемента массива. Фактический тип указывается во время объявления объекта. Объявление 1 class Collection { • • • * int A[10]; //массив целых является данным-"членом } Collection object; //A — это массив целых Объявление 2 template <class T> class Collection {
* • • • Т А[10] ; //параметризованное объявление массива //задает Т при объявлении объекта } Collection<int> object; //А — это массив целых Collection<char> object; //A — это массив символов 4.1. Описание линейных коллекций Метод доступа для элементов различает линейные коллекции, показанные на рис. 4.1. С помощью прямого доступа (direct access) мы можем выбирать элемент непосредственно, не обращаясь сначала к предшествующим элементам в списке. Например, символы в строке могут быть доступны непосредственно. Третья буква в слове LIMEAR употреблена ошибочно. Первые две буквы написаны правильно. Мы можем исправить третью букву, не обращаясь сначала к первым двум буквам. В некоторых линейных коллекциях, называемых последовательными списками (sequential lists), прямой доступ невозможен. Вы обращаетесь к элементу, начиная с начала списка и двигаясь по списку до нужного элемента. Например, в бейсболе отбивающий благополучно достигает третьего пункта (base) только после первого и второго. Пример парковочного гаража может служить для сравнения списков с возможным прямым доступом и последовательных списков. Следующая диаграмма описывает гараж, в котором рядом с машинами имеется свободный проход. Служащий может выводить машину 3 из гаража, садясь непосредственно в нее и используя свободный проход. #0 #1 #2 #3 #4 Прямой доступ к машине 3 Следующая диаграмма иллюстрирует гараж с последовательной парковкой, в котором все машины паркуются в один ряд. Служащий имеет только последовательный доступ к машине. Чтобы вывести машину 3, он должен переместить машины 0 — 3 в таком порядке: #0 #1 #2 #3 #4 Последовательный доступ к машине 3
Коллекции с прямым доступом Массив (array) — это коллекция элементов, имеющих один и тот же тип данных, с прямым доступом посредством целого индекса. Aq A, A2 •• Aj •• An.-| Коллекция Array Данные Коллекция объектов одного и того же (однородного) типа. Операции Данные в каждом местоположении в массиве доступны непосредственно с помощью целого индекса. Статический массив (static array) содержит фиксированное количество элементов и задается в памяти во время компиляции. Динамический массив (dynamic array) создается с использованием методов динамического распределения памяти и его размер может быть изменен. Массив — это структура данных, которая может использоваться для хранения списка. В случае с последовательным списком массив позволяет выполнять эффективное добавление элементов в конец списка. Эта структура менее эффективна при удалении элемента, поскольку мы должны часто сдвигать элементы. Такой же сдвиг происходит, когда новые элементы вставляются в массив, хранящий упорядоченный список. Список M5I 201301 351401 Список 115 1201301351401 Вставить 25 45 120 на 301351401 Удалить 20 СШ 130135140 т Глава 8 знакомит с классом Array, расширяющим концепцию простого массива. Этот класс предоставляет новый индексный оператор, который перед сохранением или возвращением данных проверяет, находится ли соответствующий этим данным индекс в допустимом диапазоне. Класс, реализующий такие безопасные массивы (safe arrays), позволяет клиенту динамически распределять массив во время исполнения приложения. Символьная строка (character string) — это массив символов с ассоциированными операциями, которые определяют длину строки, склеивают (конкатенируют) две строки, удаляют подстроку и так далее. Общий класс String, имеющий расширенный набор строковых операций, разработан в главе 8. Коллекция String Данные Коллекция символов с известной длиной
Операции Имеются операции для определения длины строки, копирования одной строки в другую или их конкатенации, сравнения двух строк, выполнения сопоставления с образцом, ввода и вывода из строк. Запись (record) — это базовая структура коллекций для сохранения данных, которые могут состоять из разных типов. Для многих приложений различные элементы данных ассоциированы с одним объектом. Например, авиабилет включает такие данные, как номер рейса, номер места, имя пассажира, стоимость, данные об агенте и так далее. Единственный билетный объект — это набор полей разных типов. Коллекция записи связывает поля при обеспечении прямого доступа к данным в отдельных полях. Коллекция Record Данные Элемент с коллекцией полей, возможно, различных типов. Операции Точечный оператор (dot operator) обеспечивает прямой доступ к данным в поле. Коллекции с последовательным доступом Более общей коллекцией является список, сохраняющий элементы в последовательном порядке. Структура, называемая линейным списком (linear list), содержит произвольное число элементов. Размер списка изменяется добавлением или удалением элемента из этого списка, а ссылка на элементы в списке выполняется по их положению. Первый элемент находится в голове или в начале списка, последний элемент находится в конце списка. Каждый элемент, за исключением последнего, имеет единственный последующий элемент. 1-й 2-й 3-й 4-й • • • п-й передний последний Коллекция List Данные Произвольная коллекция объектов одного и того же (однородного) типа. Операции Для ссылки на отдельные элементы мы должны идти по списку от его начальной точки, проходя от элемента к элементу до достижения нужного местоположения. Вставки и удаления изменяют размер списка. Коллекция линейного списка может иметь любое количество элементов и подразумевает, что эта коллекция будет расширяться или сужаться по мере добавления новых элементов в список или удаления резидентных элементов. Эта структура списка является ограничивающей, когда необходим доступ к произвольным элементам, так как в ней нет прямого доступа. Для доступа к элементам списка необходимо выполнять прохождение элементов от начальной точки в списке. В зависимости от используемого метода, мы можем перемещаться одним из двух способов: слева направо или в обоих направлениях. В этой главе мы разрабатываем класс, который реализует последовательный список, используя массив. Результирующий список ограничивается размером массива. Более мощная реализация, описанная в главе 9, снимает все ограничения на размер использованием связанных списков и динамических структур.
Список покупок является примером последовательного списка. Покупатель первоначально создает список, записывая названия товаров. Делая покупки, он вычеркивает названия из списка, когда товары найдены или больше не нужны. Упорядоченный линейный список (ordered linear list) — это линейный список, данные которого упорядочены относительно друг друга. Например, список 3, 5, 6, 12, 18, 33 расположен в числовом порядке, а список 1, 6, 2, 5, 8 — нет. Бинарный поиск, описываемый в этой главе, является алгоритмом, использующим упорядоченный список. Стеки и очереди — это особые версии линейного списка с ограниченным доступом к элементам данных. В стеке (stack) элементы добавляются и удаляются только в один конец списка, называемый вершиной (top). Полка для подносов в столовой — это знакомый пример. Операция удаления элемента из списка называется извлечением из стека (popping the stack). О добавлении элемента в список говорится как о помещении (pushing) элемента в стек. вершина вершина Поместить в стек Извлечь из стека При помещении элемента в стек все другие элементы, находящиеся в данный момент в стеке, опускаются вниз, уступая место на вершине новому элементу. Когда элементы удаляются из стека, они перемещаются в обратном порядке. Последний элемент, помещенный в стек, является первым извлекаемым из стека. О таком типе хранения элементов говорят как о магазинном порядке (last -in/first-out (LIFO) — последним пришел/первым ушел). Коллекция Stack Данные Список элементов, которые могут быть доступны только на вершине списка. Операции Список поддерживает операции push и pop. Push добавляет новый элемент в вершину списка, и pop удаляет элемент из вершины списка. Мы вводим стеки в ряд приложений, которые включают оценку выражений, рекурсию и прохождение дерева. В этих случаях мы просматриваем элементы и затем обращаемся к ним в порядке LIFO. При помощи стека компиляторы передают параметры функциям, а также используют стек для хранения локальных переменных. Очередь (queue) — это список с доступом только в начале и в конце списка. Элементы вставляются в конец списка и удаляются из начала. При использовании обоих концов списка элементы оставляют очередь в том же порядке, в каком они поступают. Хранение элементов соответствует порядку поступления (first-in/first-out (FIFO) — первым пришел/первым утел).
Q-вставка последний Q-удаление передний Коллекция Queue Данные Список элементов с доступом в начале и в конце списка. Операции Добавление элемента в конец списка и удаление элемента из начала списка. Очередь является полезной коллекцией для ведения списков очередников. Моделью очереди является очередь обслуживания в банке или обслуживание покупателей в продовольственном отделе. Очереди находят машинное применение в моделирующих исследованиях и осуществляют планирование заданий в рамках операционной системы. Для некоторых приложений мы изменяем структуру очереди, устанавливая очередность элементов. При удалении объекта из списка определяется элемент с наивысшим приоритетом. Эта коллекция, называемая очередью приоритетов (priority queue), имеет операции insert (вставить) и delete (удалить). Где вставляются данные, является несущественным. Важным является то, что операция delete выбирает элемент с наивысшим приоритетом. В больничном отделении скорой помощи используется очередь приоритетов. Пациенты обслуживаются в порядке поступления, если только их состояние не является угрожающим для жизни, что дает им наивысший приоритет и первоочередной доступ к экстренной медицинской помощи. Коллекция Queue Priority Данные Список элементов, такой, что каждый элемент имеет приоритет. Операции Добавление элемента в список. При удалении элемента извлекается элемент с наивысшим приоритетом. Очереди приоритетов используются для планирования заданий в рамках операционной системы. Задания с наивысшим приоритетом должны выполняться в первую очередь. Очереди приоритетов используются также в моделировании, управляемом прерываниями (event-driven simulation). Например, в практическом приложении в главе 5 выполняется моделирование потока клиентов в банк и из банка. Каждый тип события ( появление или уход) вставляется в очередь приоритетов. Самое раннее по времени событие удаляется и обслуживается первым. В машинной системе файл (file) — это внешняя коллекция, которая имеет ассоциированную структуру данных, называемую потоком (stream). Мы приравниваем file к его stream и сосредоточиваем внимание на потоке данных. Прямой доступ осуществляется только к дисковому файлу, ленточные же файлы являются последовательными. Операция read удаляет данные из потока ввода, а операция write добавляет новые данные в конец потока вывода. Файл часто используется для хранения большого количества данных. Например, во время компиляции программы генерируются большие таблицы и часто сохраняются во временных файлах.
Коллекция File Данные Последовательность байтов, ассоциированная с внешним устройством. Данные перемещаются посредством потока к устройству и из него. Операции Открытие файла, считывание данных из файла, запись данных в файл, поиск указанного адреса в файле (прямой доступ), закрытие файла. Универсальная индексация Массив — это классическая коллекция, позволяющая иметь прямой доступ к каждому элементу, посредством целого индекса. Для многих приложений мы связываем с записью данных некоторый ключ, использующийся для доступа к записи. Когда вы звоните в банк или в страховую компанию для получения информации, вы даете ваш номер банковского счета, который становится ключом для нахождения записи этого счета. Коллекция, называемая хеш-таблица (hash table), сохраняет данные, связанные с ключом. Ключ трансформируется в целый индекс, используемый для нахождения данных. В одном часто используемом методе хеш-таблиц целое значение — это индекс в массиве коллекций. После преобразования ключа в индекс выполняется поиск ассоциированной коллекции. Ключ не обязательно должен быть целым числом. Например, запись данных может состоять из имени, классификации работы, количества лет работы в компании, жалования и так далее. "Уилсон, Сандра Р." 3 15 42500 В этом случае строка, указывающая имя, является ключом. Обычный словарь — это коллекция слов и их определений. Вы ищете слово, используя его как ключ. В структурах данных, коллекция, называемая словарем (dictionary), состоит из набора пар ключ-значение, называемых ассоциациями (associations). Ключ Значение Ассоциация Например, ключом может быть слово, а значением — строка, указывающая определение слова. К значению в ассоциации осуществляется прямой доступ с использованием ключа в качестве индекса. В результате, словарь подобен массиву, за исключением того, что индексы не должны быть целыми значениями. Например, если Diet является коллекцией dictionary, ищите определение слова dog, ссылаясь на Dict[dog]. Словари часто называют ассоциативными массивами (associative arrays), потому что они связывают (ассоциируют) общий индекс со значением данных.
above dog long Значение . . . 1 Значение . . . ■ • • Значение . . . count « Dict[dog] « endl; 4.2. Описание нелинейных коллекций На рис. 4.1 показано, что нелинейные коллекции разделяются на иерархические и групповые структуры. Иерархическая коллекция (hierarchical collection) — это масса элементов, которые разделяются по уровням. Элементы на данном уровне могут иметь несколько наследников на следующем уровне. Мы вводим особую иерархическую коллекцию, называемую деревом (tree), в которой все элементы данных происходят из одного источника, называемого корнем (root). Элементы в дереве называются узлами (nodes), каждый из которых указывает на нисходящие узлы, называемые детьми (children). Каждый элемент, за исключением корня, имеет единственного предка. Пути вниз по дереву начинаются в корне и развиваются по направлению к нижним уровням от родителя к ребенку. Корень Дерево является идеальной структурой для описания файловой системы с каталогами и подкаталогами. Модель для дерева — это организационная схема в бизнесе, определяющая цепочку управления, начиная с босса (СЕО, президента), и далее — к вице-президентам, супервайзерам и так далее. В этой книге мы рассматриваем особую форму дерева, в котором каждый узел имеет самое большее два потомка. Такая структура, бинарное дерево (binary tree), имеет важное применение в оценке арифметических выражений и в теории компиляции. С дополнительным упорядочением дерево становится деревом бинарного поиска (binary search tree), которое эффективно сохраняет большие объемы данных. Деревья бинарного поиска обеспечивают быстрый доступ к элементам, располагая узлы так, что данные можно находить, перемещаясь вниз по короткому пути из корневого узла. На рис. 4.4 показано дерево с 16 узлами. Самый длинный путь от корня к узлу включает четыре ветви. Предположим, что дерево относительно заполнено узлами, отношение
узлов к длине пути значительно улучшается по мере того, как мы увеличиваем размер дерева. Пример: если дерево бинарного поиска имеет 220 — 1 = 1 048 575 узлов, которые расположены на минимальном количестве уровней, то элемент данных можно найти, посещая не более, чем 20 узлов. Особое дерево бинарного поиска — это AVL-дерево, гарантирующее равномерное распределение узлов и обеспечивающее очень короткое время поиска. Корень Рис. 4.4. Дерево с 16 узлами Коллекция Tree Данные Иерархическая коллекция узлов, происходящих из корня. Каждый узел указывает на узлы-сыновья, которые сами являются корнями поддеревьев. Операции Структура дерева позволяет добавлять и удалять узлы. Несмотря на то, что дерево — это нелинейная структура, алгоритмы прохождения деревьев позволяют нам посещать отдельные узлы и осуществлять поиск ключа. Heap-дерево — это особая версия дерева, в котором самый маленький элемент всегда занимает корневой узел. Операция delete удаляет корневой узел, и обе операции insert и delete вызывают такую реорганизацию дерева, что самый маленький элемент вновь занимает корень такого дерева. Heap- дерево использует очень эффективные алгоритмы реорганизации, просматривая только короткие пути от корня вниз к концу дерева. Heap-дерево может использоваться для упорядочения списка элементов. Вместо использования медленных алгоритмов сортировки мы упорядочиваем их, повторно удаляя корневой узел из heap-дерева. Это позволяет получить быструю сортировку (heap-сортировку). Кроме того, при использовании heap-дерева наиболее часто реализуется очередь приоритетов. Коллекции групп Группа (group) представляет те нелинейные коллекции, которые содержат элементы без какого-либо упорядочения. Множество уникальных элементов является примером группы. Операции над коллекцией типа множество включают объединение (union) и пересечение (intersection). Другие операции над множеством тестируют на членство и отношение подмножеств. В главе 8 мы
вводим класс Set с перегрузкой операторов для реализации операций над множествами. S » {1.2.3}. Т » {3.8.5} S объединение Т-{1,2,3,8,5} 5 пересечение Т -{3} Коллекция Set Данные Неупорядоченная коллекция объектов без дубликатов. Операции Бинарные операции членства, объединения, пересечения и дифференциации, которые возвращают новое множество. Ряд операторов, тестирующих отношения подмножеств. Множество (set) — это коллекция, находящая применение, когда данные являются неупорядоченными и каждый элемент данных является единственным в своем роде, уникальным. Например, группа регистрации избирателей составляет банк телефонных номеров для того, чтобы звонить лицам, находящимся в списке. Каждый раз, когда группа контактирует с человеком из банка номеров, его имя помещается в список номеров, по которым позвонили, и удаляется из банка. Конечно, группа людей, которым еще не позвонили, тоже является множеством. Группа регистрации избирателей продолжает звонить, пока множество номеров, по которым не позвонили, не будет пустым, или не будет сделано разумное количество попыток позвонить. Граф (graph) — это структура данных, задающая набор вершин и набор связей, соединяющих вершины. Графы находят применение в планировании заданий, транспортировании и так далее. Например, строитель дома должен заключать контракты на этапы строительной работы. План работы должен быть составлен так, чтобы обеспечить выполнение всей подготовительной работы к моменту, когда начнется новый этап строительства. Например, кровельщики не могут начать свою работу, пока строители не завершат работу по сооружению дома, а строительные работы не могут быть выполнены, пока не будет заложен бетонный фундамент. Подвод водоснабжения Начало Подвод коллектора Сантехнические работы Строительные работы Закладка фундамента Кровельные работы
Коллекция Graph Данные Набор вершин и набор соединительных связей. Операции Как коллекция вершин и связей граф имеет операции для добавления и удаления этих элементов. Алгоритмы просмотра начинаются в заданной вершине и находят все другие вершины, которые достижимы из начальных вершин. Другие алгоритмы просмотра выполняют оба просмотра графа — в глубину и в ширину. Сеть (network) — это особая форма графа, которая присваивает вес каждой связи. Вес указывает стоимость использования связи при прохождении графа. Например, в следующей сети вершины представляют города, а вес, присваиваемый связям, — это расстояния между парами городов. 752 Salt Lake City 604 San Francisco .648 763 504 San Diego )~355 Phoenix Albuquerque 4.3. Анализ алгоритмов В этой книге мы разрабатываем, классы реализующие коллекции данных. Для реализации методов класса часто используются классические алгоритмы. Мы часто описываем подробно разработку и реализацию этих алгоритмов и анализируем их эффективность. Клиент судит о программе по ее корректности, легкости исцользования и эффективности. Легкость использования и корректность программы зависит от процедур разработки и тестирования. На эффективность программы влияет множество факторов, которые включают внутреннюю машинную систему, количество памяти, имеющейся для управления данными и сложность алгоритмов. Мы кратко рассматриваем эти факторы и затем сосредоточиваем внимание на вычислительной сложности алгоритмов. Мы разрабатываем критерии эффективности, позволяющие нам измерять эффективность какого-либо алгоритма в терминах размера коллекции. Критерии не зависят от определенной машинной системы и измеряют абстрактные характеристики эффективности алгоритмов. Для создания численной меры эффективности нами используется нотация Big-0. Критерии эффективности Алгоритм, в конечном счете, выполняется в машинной системе со специфическим набором команд и периферийными устройствами. Для отдельной системы какой-либо алгоритм может быть разработан для полного использования преимуществ данного компьютера и поэтому достигает высокой степени эффективности. Критерий, называемый системной эффективностью (system efficiency), сравнивает скорость выполнения двух или более алгоритмов, которые разработаны для выполнения одной и той же задачи. Выполняя эти алгоритмы на одном компьютере с одними и теми же наборами данных, мы
можем определить относительное время, используя внутренние системные часы. Оценка времени становится мерой системной эффективности для каждого из алгоритмов. При работе с некоторыми алгоритмами могут стать проблемой ограничения памяти. Процесс может потребовать большого временного хранения, ограничивающего размер первоначального набора данных, или вызвать требующую времени дисковую подкачку. Эффективность пространства (space efficiency) — это мера относительного количества внутренней памяти, используемой каким-либо алгоритмом. Она может указать, какого типа компьютер способен выполнять этот алгоритм и полную системную эффективность алгоритма. Вследствие увеличения объема памяти в новых системах, анализ пространственной эффективности становится менее важным. Третий критерий эффективности рассматривает внутреннюю структуру алгоритма, анализируя его разработку, включая количество тестов сравнения, итераций и операторов присваивания, используемых алгоритмом. Эти типы измерений являются независимыми от какой-либо отдельной машинной системы. Критерий измеряет вычислительную сложность алгоритма относительно п, количества элементов данных в коллекции. Мы называем эти критерии вычислительной эффективностью (computational efficiency) алгоритма и разрабатываем нотацию Big-О для построения измерений, являющихся функциями п. Нотация Big-O. Интуитивно вычислительная эффективность алгоритма измеряется количеством обрабатываемых им данных для определения ключевых операций алгоритма. Эти операции могут зависеть от типа коллекции данных, количества данных и их начального упорядочения. Нахождение минимального элемента в массиве — это простой алгоритм, основная операция которого включает сравнение элементов данных. Для массива с п элементами алгоритм требует п — 1 сравнений и мера эффективности пропорциональна п. Другие алгоритмы являются более сложными. Для обменной сортировки, описанной в главе 2, обработка данных включает серию сравнений в каждом прохождении. Если А — это массив из п элементов, то обменная сортировка выполняет п — 1 проходов. На рис. 4.5 показан этот алгоритм. После прохода элементы с X сохраняются Проход 1 п-1 сравнений Проход 2 п-2 сравнений Проход i n-i сравнений Проход N-1 1 сравнение Рис. 4.5. Проходы в обменной сортировке
Проход 1: Сравнение п-1 — элементов А[1] . . . А[п — 1] с А[0] и, если необходимо, такой обмен элементов, чтобы А[0] всегда имел наименьшее значение. Проход 2: Сравнение п-2 — элементов А[2] . . . А[п — 1] с А[1]. Проход i: Для общего случая, сравнение п4 — элементов A[i] . . . A[n — i] с A[i — 1]. Общее число сравнений в сортировке обмена задается арифметическим рядом f(n) от 1 до п-1: f(n) = (п — 1) + (п — 2) + . . . + 3 + 2 + 1 = п(п — 1)/2 Количество сравнений зависит от п2. Для обработки данных общих классов коллекций таких, как последовательные списки и деревья, мы используем сравнения в качестве меры эффективности алгоритмов. Алгоритмы зависят также от начального упорядочения данных. Например, нахождение минимального значения в массиве значительно упрощается, если мы знаем, что эти данные упорядочены. В возрастающем списке минимальное значение занимает первую позицию. Это значение находится в конце убывающего списка. В этих случаях вычислительная сложность включает единственный доступ к данным, который может быть выполнен в постоянную единицу времени. В примере с сортировкой, если список упорядочен, не требуется никакого обмена. Это условие наилучшего случая, и оно представляет наиболее эффективное выполнение алгоритма. Однако, если список отсортирован в обратном порядке, каждое сравнение приводит к обмену. Это условие наихудшего случая для сортировки. Общий случай предполагает некоторое промежуточное количество обменов в зависимости от порядка данных в списке. Для алгоритмов поиска и сортировки в классе коллекций мы используем количество сравнений как доминантное действие и меру вычислительной эффективности. Наш анализ определяет также начальное упорядочение данных, в котором можно различать наилучший случай (best case), наихудший случай (worst case) и средний случай (average case) для алгоритма. Средний случай — это ожидаемая эффективность алгоритма, если он выполняется много раз со случайным набором значений данных. Определяя вычислительную эффективность алгоритма, мы ассоциируем функцию f(n) с количеством сравнений. В действительности, точная форма функции может быть трудна для определения, и поэтому мы используем методы аппроксимации для определения хорошей верхней границы функции. Мы определяем простую функцию g(n) и константу К так, что K*g(n) превышает f(n) по мере того, как п значительно возрастает. Для большого значения п поведение f(n) ограничивается произведением функции g(ri) на некоторую константу. Мы используем эту математическую концепцию, называемую нотацией Big-О, чтобы дать меру вычислительной эффективности. Определение: Функция /(п) имеет порядок 0(£(л)), если имеется константа К и счетчик л0, такие, что f(n) < K*g(n), для п > /г0. Интуитивно это означает, что функция g в конечном счете превышает значение функции /. Мы говорим, что вычислительная сложность (computational complexity) (или порядок) алгоритма равна 0(g(n)). Традиционно значение Big-О для алгоритма структур данных выбирается среди небольшого набора полиномиальных, логарифмических и экспоненциальных функций. Для классических структур данных эти функции дают наилучшие верхние границы вычислительной сложности алгоритмов.
Kg(n) f(n) В примере с обменной сортировкой мы ищем функцию gf которая ограничивает f(n). В таблице 4.1 рассматриваются g(n) = х/2 п2 и f(n) для разных значений л. В конечном счете, функция f(n) ограничивается величиной l/2 g(n), где g(n) = п2. В этом случае возможное условие появляется непосредственно при п0 = 1 и К = 1/2. f(n) < l/2 п2 для всех п >_ 1 Мы говорим, что f(n) имеет порядок 0(g(n)) = 0(n2), поэтому вычислительная сложность обменной сортировки составляет 0(п2). Анализ наилучшего и наихудшего случаев также приводит к той же мере сложности, так как обменная сортировка всегда требует 1/2 п(п — 1) сравнений. Этот алгоритм сортировки требует порядка 0(п2) единиц времени для вычисления независимо от начального порядка данных. В нашем исследовании сортировки мы обнаружим, что некоторые алгоритмы имеют вычислительную сложность (порядок) 0(п log2n) для достаточно большого п0- Количество сравнений < К п log2 п для п > п0. В таблице 4.1 сравниваются значения л2 и п log2n. Заметьте, насколько более эффективным является алгоритм сортировки 0(п log2n), чем обменная сортировка. Например, в случае со списком из 10 000 элементов количество сравнений для обменной сортировки ограничивается величиной 100 000 000, тогда как более эффективный алгоритм имеет количество сравнений, ограниченное величиной 132 000. Новая сортировка приблизительно в 750 раз более эффективна. Таблица 4.1 п 10 100 1000 5000 10000 (1/2)п2 50 5.000 500.000 12.500.000 50.000.000 S(n) = n2/2 - п-2 45 4.950 499.500 12.497.500 49/995.000 п 5 10 100 1000 10000 п2 25 100 10000 1000000 100000000 п !одгп 11,6 33,2 664,3 9965,7 132877,1
При выполнении Big-O-аппроксимации функции f(n) мы используем термин доминирование для определения вычислительной сложности. Небольшой опыт работы с неравенствами дает возможность математически проверить эту стратегию. Например, в случае функции f(n) = п + 2 терм п является доминирующим. Функция g(n) = n используется в следующем неравенстве для проверки того, что / имеет порядок О(п). f(n) = и + 2</1 + гс = 2*л для п > 2 / также имеет порядок 0(п2) или 0(п3), так как g(n) = п2 и £(п) = п3 ограничивают /(я). Мы выбираем О(п), что представляет наилучшую оценку для этой функции. Пример 4.1 1. f(n) = п2 + п + 16 Доминирующий терм — п29 а / имеет порядок 0(п2). f(n) = п2 + п + 1 < п2 + п2 + п2 = Зп2 для п > 1 2. f(n) = sqrt(n+3) Доминирующий терм — sqrt(n), a / имеет порядок 0(sqrt(n)) f(n) = sqrt(n+3) < sqrt(n+n)=sqrt(2n)=sqrt(2)*sqrt(n) для п >, 3 3. /fra,) = 2Л + л + 2 Доминирующий терм — 2n, a / имеет порядок 0(2п). f(n) = 2п + п + 2 < 2п + 2п +2п = 3*2П, для п > 1 Сложность алгоритма. Big-O-оценка дает меру времени выполнения (runtime) алгоритма. Обычно алгоритм имеет разную вычислительную эффективность для наилучшего и наихудшего случаев, поэтому мы вычисляем конкретное значение Big-О для каждого случая. В разделе 4.4 излагается метод нахождения времении выполнения для последовательного и бинарного поиска. Каждый алгоритм имеет порядок для наилучшего и наихудшего случая, которые различны. Наилучший случай для алгоритма часто не важен, так как эти обстоятельства являются исключительными и неподходящими для решения о выборе какого-либо алгоритма. Наихудший случай может быть важен, так как эти обстоятельства будут наиболее негативно влиять на ваше приложение. Клиент может не допускать наихудшего случая и может предпочесть, чтобы вы выбрали алгоритм, который имеет более узкий диапазон эффективности. В общем, довольно трудно математически определить среднюю эффективность какого-либо алгоритма. Мы будем использовать только очень простые измерения ожидаемых значений и оставим математические детали для курса по теории сложности. Общий порядок величин Небольшой набор различных порядков определяет сложность большинства алгоритмов структур данных. Мы определяем различные порядки и описываем алгоритмы, приводящие в результате к таким оценкам.
Если алгоритм — порядка 0(1), то этот порядок не зависит от количества элементов данных в коллекции. Этот алгоритм выполняется за постоянную единицу времени (constant time). Например, присваивание некоторого значения элементу списка массива имеет порядок 0(1), при условии, что вы храните индекс, который определяет конец списка. Сохранение этого элемента включает только простой оператор присваивания. Прямая вставка в конец списка начало конец Алгоритм О(п) является линейным (linear). Сложность этого алгоритма пропорциональна размеру списка. Например, вставка элемента в конец списка п элементов будет линейной, если мы не храним ссылку на конец списка. Подразумевая, что мы можем просматривать элемент за элементом, алгоритм требует, чтобы мы протестировали п элементов перед определением конца списка. Порядком этого процесса является О(п). Нахождение максимального элемента в массиве из п элементов — это О(п), потому что должен быть проверен каждый из п элементов. Последовательная вставка в конец списка начало конец Ряд алгоритмов имеют порядок, включающий log2n, и называются логарифмическими (logarithmic). Эта сложность возникает, когда алгоритм неоднократно подразделяет данные на подсписки, длиной 1/2, 1/4, 1/8, и так далее от оригинального размера списка. Логарифмические порядки возникают при работе с бинарными деревьями. Бинарный поиск, изложенный в разделе 4.4, имеет сложность среднего и наихудшего случаев O(log2n). В главах 13 и 14 описываются алгоритмы сортировки с использованием дерева и быстрая сортировка порядка 0(п log2n). Алгоритмы, имеющие порядок 0(гс2), являются квадратическими (quadratic). Наиболее простые алгоритмы сортировки такие, как обменная сортировка, имеют порядок 0(п2). Квадратические алгоритмы используются на практике только для относительно небольших значений п. Всякий раз, когда п удваивается, время выполнения такого алгоритма увеличивается на множитель 4. Алгоритм показывает кубическое (cubic) время, если его порядок равен 0(л3), и такие алгоритмы очень медленные. Всякий раз, когда п удваивается, время выполнения алгоритма увеличивается в восемь раз. Алгоритм Уоршела, применимый к графам, — это алгоритм порядка 0(п3). Алгоритм со сложностью 0(2п) имеет экспоненциальную сложность (exponential complexity). Такие алгоритмы выполняются настолько медленно, что они используются только при малых значениях п. Этот тип сложности часто ассоциируется с проблемами, требующими неоднократного поиска дерева решений.
В таблице 4.2 приводятся линейный, квадратичный, кубический, экспоненциальный и логарифмический порядки величины для выбранных значений п. Из таблицы очевидно, что следует избегать использования кубических и экспоненциальных алгоритмов, если только значение п не мало. Таблица 4.2 Оценка порядка алгоритмов п 2 4 8 16 32 128 1024 65536 1од2П 1 2 3 4 5 7 10 16 п !од2П 2 8 24 64 160 896 10240 1048576 п2 4 16 64 256 1024 16384 1048576 4294967296 п3 8 64 512 4096 32768 2097152 1073741824 2.8 х 1014 2П 4 16 256 65536 4294967296 3.4 х 1038 1.8 х 10308 Избегайте! 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 (list[i] == key) return i; //возвращать индекс соответствующего элемента return -1; //поиск неудачный, возвращать -1 } При определении порядка алгоритма последовательного поиска различают поведение наилучшего и наихудшего случаев. Наилучшему случаю соответствует нахождение ключа в первом элементе списка. Время выполнения алгоритма при этом составляет 0(1). Наихудший случай имеет место, когда этот ключ не находится в списке или обнаруживается в конце списка. Он требует
проверки всех п элементов и имеет порядок О(л). Средний случай требует небольшого количества вероятностных рассуждений. Для случайного списка совпадение с ключом может с одинаковой вероятностью появиться в любой позиции списка. После выполнения проверок большого количества элементов средняя позиция совпадения — это срединный элемент (midpoint) п/2. Эта промежуточная точка анализируется после п/2 сравнений, что определяет ожидаемую стоимость поиска. По этой причине мы говорим, что средняя эффективность последовательного поиска составляет О(п). Бинарный поиск Последовательный поиск применим для любого списка. Если список является упорядоченным, алгоритм, называемый бинарный поиск (binary search), предоставляет улучшенный метод поиска. Ваш опыт по нахождению номера в большом телефонном справочнике — это модель такого алгоритма. Зная нужные имя и фамилию, вы открываете справочник ближе к началу, середине или концу, в зависимости от первой буквы фамилии. Вам может повезти, и вы сразу попадете на нужную страницу. В противном случае вы переходите к более ранней или более поздней странице в справочнике в зависимости от относительного местоположения имени человека по алфавиту. Например, если имя человека начинается с буквы R, а вы находитесь на странице с именами на букву Т, вы переходите на более раннюю страницу. Процесс продолжается до тех пор, пока вы не найдете соответствие или не обнаружите, что этого имени нет в справочнике. Соответствующая идея применима к поиску в упорядоченном списке. Мы идем к середине списка и ищем быстрое соответствие ключа значению срединного элемента. Если нам не удается найти соответствия, мы смотрим на относительный размер ключа и значение срединного элемента и затем перемещаемся в нижнюю или верхнюю половину списка. В общем, если мы знаем, как упорядочены данные, мы можем использовать эту информацию, чтобы сократить время поиска. Следующие шаги описывают алгоритм. Предположим, что список упорядочен, как массив. Индексами в концах списка являются: low = 0 и high = п — 1, где п — это количество элементов в массиве. 1. Сравнить индекс срединного элемента массива: mid * <low+high)/2. 2. Сравнить значение в срединном элементе с key (ключ). key key low mid high low mid high low mid high Совпадение найдено Поиск в левой половине Поиск в правой половине Если совпадение найдено, возвращать индекс mid для нахождения ключа, if (A[mid] -- key) return(mid); key
Если AJmid] < key, совпадение должно происходить в диапазоне индексов mid-fl . . . high, в правой половине рассматриваемого списка. Это верно, потому что список упорядочен. Новыми границами являются low=mid+l и high. Если key < A[mid], совпадение должно происходить в диапазоне индексов low . . . mid-1, в левой половине списка. Новыми границами являются low и high=mid-l. key key low mid - 1 = high low = mid + 1 high Проверка левой половины Проверка правой половины Алгоритм уточняет местоположение совпадающего с ключом элемента, деля пополам длину интервала, в котором может находиться этот элемент, и затем выполняя тот же алгоритм поиска в меньшем подсписке. В конце концов, если искомый элемент не находится в списке, low превысит high, и алгоритм возвращает индикатор сбоя — 1 (совпадение не произошло). Пример 4.2 Рассмотрим массив целых А. Этот пример дает выборку алгоритма для заданного ключа 33. low = О high = 8 mid = (0 + 8)/2 « 4 33 > A[mid] low — 5 high = 8 mid = (5 + 8)/2 = 6 33 > A[mid] low = 7 high «= 8 mid = (7 + 8)/2 = 7 33 > A[mid] Success! Заметьте, что этот алгоритм требует трех (3) сравнений. При линейном поиске в списке требуется восемь (8) сравнений.
Реализация бинарного поиска Функция использует параметризованное имя DataType, которое должно поддерживать оба оператора: равенства (==) и меньше чем (<)• Первоначально low равно 0, 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 -l; // элемент не найден } Реализация последовательного и бинарного поиска включена в файл 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[], int n) {
int i, j, temp; for (i=0;i < n-l; i++) // поместить минимум элементов a[i] . . .a[n-1] в 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 А[1000], В[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« 0; i < 500; i++) B[i] - rnd.Random(2000) ; cout « "Время последовательного поиска" << endl; tcount = TickCount (); // время начала 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++J if (BinSearch(A/0,999,B[i]) !=-1) matchCount++; tcount=TickCount() —tcount; // cout « "Бинарный поиск занимает " « tcount/60.0 « " секунд для " « matchCount « " совпадений." « endl; } /* <Выполнение программы 4 . 1> Время последовательного поиска Последовательный поиск занимает 0.816667 секунд для 181 совпадений. Время бинарного поиска Бинарный поиск занимает 0.016667 секунд для 181 совпадений. */
Неформальный анализ для бинарного поиска. Наилучший случай имеет место, когда совпадающий с ключом элемент находится в середине списка. При этом порядок алгоритма составляет 0(1 )f так как требуется только одно тестирующее сравнение равенства. При наихудшем случае, когда элемент не находится в списке или определяется в последнем сравнении, имеем порядок 0(log2n). Мы можем интуитивно вывести этот порядок. Наихудший случай возникает, когда мы должны уменьшать подсписок до длины 1. Каждая итерация, которая не находит соответствие, уменьшает длину подсписка на множитель 2. Размеры подсписков следующие: п п/2 п/4 п/8 ... 1 Разделение на подсписки требует т итераций, где т — это приблизительно log2n (см. подробный анализ). Для наихудшего случая мы имеем начальное сравнение в середине списка и затем — ряд итераций log2n. Каждая итерация требует одну операцию сравнения: Total Comparisons = 1 + log2n В результате наихудшим случаем для бинарного поиска является 0(log2ri). Этот результат проверяется имперически программой 4.1. Отношение времени выполнения последовательного поиска ко времени выполнения бинарного поиска равно 49,0. Теоретическое отношение ожидаемого времени приблизительно составляет 500/(log2l000)= 50,2. Формальный анализ бинарного поиска. Первая итерация цикла имеет дело со всем списком. Каждая последующая итерация делит пополам размер подсписка. Так, размерами списка для алгоритма являются п п/21 п/22 п/23 п/24 . . . п/2т В конце концов будет такое целое т, что п/2ш<2 или n<2m+1 Так как m — это первое целое, для которого n/2m<2, то должно быть верно n/2m-l>2 или 2m<n Из этого следует, что 2m<n<2m+1 Возьмите логарифм каждой части неравенства и получите /о^г^действи- тельному числу х: m<\og2n=x<m+1 Значение m — это наибольшее целое, которое .<х и задается int(x). Например, если n=50, log250=5,644. Следовательно, m=int(5,644)= 5 Можно показать, что средний случай также составляет 0(log2n). 4.5. Базовый класс последовательного списка Товары для покупки, автобусное расписание, телефонный справочник, налоговые таблицы и инвентаризационные записи являются примерами списков. В каждом случае объекты включают последовательность элементов. Во многих приложениях ведется какой-либо список. Например, перечень товаров пред-
приятия содержит информацию о поставках и заказах, персонал офиса создает платежную ведомость для списка работников компании, ключевые слова для компилятора сохраняются в списке зарезервированных слов и так далее. В главе 1 описывался ADT для базового последовательного списка. Операции базового списка включают вставку нового элемента в конец списка, удаление элемента, доступ к элементу в списке по позиции и очистку списка. Мы имеем также операции для тестирования, является ли список пустым, или находится ли какой-либо элемент в списке. В качестве примера этому из реальной жизни рассмотрим список продуктов для покупки в универсаме (Рис.4.6). Когда вы идете по универсаму, вы решаете купить дополнительные товары и добавляете их в конец списка. Когда товар найден, вы удаляете его из списка. Список с этими простыми операциями может использоваться для решения существенных задач. Приложение описывает отдел видеотоваров, в котором ведется список имеющихся фильмов и список клиентов. Когда какой-либо фильм выдается клиенту, он переходит из списка имеющихся фильмов в список клиентов. При возврате фильма происходит обратный процесс. Первоначальный список Вычеркнули картофель Добавили рыбу Вычеркнули хлеб Добавили молоко Рис.4.6. Список покупок Список ADT описывает однородные списки (homogeneous lists), в которых каждый элемент имеет один и тот же тип данных, называемый DataType. В определении ADT не упоминается о том, как хранятся элементы. Для этого может использоваться массив или связанный список с применением динамического распределения памяти. Реализации операций Insert, Delete и Find зависят от метода, используемого для хранения элементов списка. В главе 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 listitem[MaxListSize]; 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 DataTypefc item); DataType DeleteFront(void); void ClearList(void); >; ОПИСАНИЕ Объявление и реализация находятся в файле aseqlist.h. Имя DataType используется для представления общего типа данных. Перед включением класса из файла используйте typedef для связывания имени DataType с конкретным типом. Переменная size поддерживает текущий размер списка. Первоначально размер установлен на 0. Так как статический массив используется для реализации списка, константа 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(0) {}
Методы модификации списка Метод Insert добавляет новый элемент в конец списка и увеличивает длину на 1, Если при этом превышается размер массива listitem, то метод выводит сообщение об ошибке и завершает программу. Ограничение на размер списка снимается в главе 9, где класс реализуется с использованием связанного списка. вставка (10) Элемент параметра передается в качестве ссылки константе. Если размер DataType большой, использование ссылочного параметра позволяет избежать неэффективного копирования данных, которое необходимо в вызове параметра по значению. Ключевое слово const указывает на то, что фактический параметр не может быть изменен. Этот же тип передачи параметра используется методом Delete. Insert // вставить элемент в хвост списка, прервать выполнение // программы, если размер списка превысит MaxListSize void SeqList::Insert(const DataTypeS item) { // проверка размера списка if (size+1 > MaxListSize) { cerr «"Превышен максимальный размер списка" « endl; exit(1); > // индекс хвоста равен размеру текущего списка listitem[sizej = item; size++; } Метод Delete определяет первое появление в списке заданного элемента. Функция требует, чтобы был определен оператор сравнения (==) для DataType. В некоторых случаях для этого может потребоваться, чтобы пользователь предоставил особую функцию, которая переопределяет оператор == для конкретного DataType. Эта тема формально излагается в главе 6. Если элемент не обнаруживается при индексе i, операция спокойно заканчивается без изменения списка. Если элемент найден, он удаляется из списка перемещением всех элементов с индексами i+1 к концу списка влево на одну позицию.
Например, удаление элемента со значением 45 из списка приведет к смещению влево хвостовых элементов 23 и 8. Длина списка изменяется с 6 на 5. Удаление элемента со значением 30 оставляет список неизменным. Delete // поиск и удаление элемента item из списка void SeqList::Delete(const DataTypefc item) { int i = 0; // поиск элемента while (i < size && «(item == listitem[i])) i++; if (i < size) { // передвинуть хвост списка на одну позицию влево while (i < size-1) { listitem[i] = listitem[i+i]; i++; } size—; // уменьшение size на 1 } } Методы доступа к списку. Метод GetData возвращает значение данных в позицию pos в списке. Если pos не находится в диапазоне от 0 до size-1, печатается сообщение об ошибке, и программа завершается. // возвращает значение элемента списка для индекса pos. если pos // не находится в диапазоне индексов списка, программа заканчивается //с сообщением об ошибке DataType SeqList::GetData(int pos) const { // прервать программу, если pos вне диапазона индексов списка if (pos < 0 || pos >= size) { cerr « "pos выходит за диапазон индексов!" « endl; exit(1); } return listitem[pos]; } Метод доступа Find принимает параметр, который служит в качестве ключа, и последовательно просматривает список для нахождения совпадения. Если список пустой или совпадение не найдено, Find возвращает 0 (False). Если элемент обнаруживается в списке в позиции с индексом i, Find присваивает запись из listitem[i] соответствующему элементу списка и возвращает 1 (True). Для данных, совпадающих с ключом, процесс присваивания значения данных элемента списка параметру является важнейшим в приложениях, касающихся записей данных. Например, предположим, что DataType — это структура с полем ключа и полем значения, и что оператор == тестирует
только поле ключа. При вводе элемент параметра может определять только поле ключа. При выводе элемент присваивается обоим полям. ключ ключ значение элемент (вводг ^элемент (вывод) SeqList ключ значение ключ значение ключ значение Совпадение Find // использовать item в качестве ключа для поиска в списке. // возвращать True, если элемент item находится в списке, и // False — в противном случае, если элемент найден, присвоить // его значение параметру item, передаваемому по ссылке int SeqList::Find(DataTypefi item) const { int i - 0; if (ListEmptyO ) return 0; // возвратить False, если список пуст while (i < size && ! (item » listitem[i])) i++; if (i < size) { item » listitem[i]; // присвоить item значение элемента списка return 1; // возвратить True } else return 0; // возвратить False } Класс SeqList не предоставляет метода для непосредственного изменения значения какого-либо элемента. Для выполнения такого изменения мы должны сначала найти этот элемент и возвратить запись данных, удалить этот элемент, изменить запись и вставить новые данные в список. Конечно, это изменяет положение элемента в списке, потому что новый элемент помещается в конец списка. Пример 4.3 Запись Inventoryltem содержит номер детали и количество деталей в запасе. Оператор == сравнивает две записи Inventoryltem, сравнивая поля partNumber. Выполняется поиск SeqList-объекта L для нахождения записи с partNumber 5. Если объект найден, запись обновляется увеличением поля count. struct Inventoryltem { int partNumber; int count; } int operator— (Invntoryltem x, Invntoryltem y) {
return x.partNumber == у.partNumber; } typedef Inventoryltem DataType; #include "aseqlist.h" * * * SeqList L; Inventoryltem item; • • • item.partNumber = 5; if(L.Find(item)) { L.Delete(item); item.count++; L.Insert(item); ) Так как любой элемент всегда вставляется в хвост списка, порядок (время выполнения) метода Insert зависит от п и равен O(l). Find выполняет последовательный поиск, поэтому среднее время его работы будет О(л). На протяжение многих проб метод Delete должен проверить в среднем п/2 элементов списка и должен перемещать в среднем п/2 элементов. Это означает, что среднее время выполнения для Delete составляет О(л). Порядок наихудшего случая для обоих методов Find и Delete также составляет О(п). Применение. Объекты SeqList используются для ведения списка имеющихся фильмов, и списка фильмов, взятых для просмотра клиентами в видеомагазине. Каждый элемент в списке является записью, которая состоит из названия фильма и (для проката) имени клиента. // структура записи для хранения данных о фильме и клиенте struct FilmData { char filmName[32]; char customerName[32]; } Так как метод Find в классе SeqList требует определения оператора сравнения ==, мы перегрузим этот оператор для структуры FilmData. Этот оператор проверяет имя файла, используя функцию C++ strcmp. // перегрузка == int operator -= (const FilmData &A, cost FilmData *B) { return strcmp(A.filmName, B.filmName); ) Чтобы использовать FilmData с классом SeqList, включите объявление typedef FilmData DataType; Определение оператора == для FilmData и DataType находятся в файле video.h. В видеомагазине ведется инвентаризационный список фильмов. Для простоты мы полагаем, что в магазине имеется только одна копия каждого фильма. Новый фильм добавляется в инвентаризационный список функцией Insert. Для проверки наличия фильма в списке используется функция Find. Если фильм найден, он удаляется из инвентаризационного списка фильмов и вставляется в список фильмов, отданных для просмотра.
Программа 4.2. Видеомагазин Main-программа эмулирует операции видеомагазина. Первоначально весь перечень фильмов считывается из файла films и сохраняется в списке с именем inventory List. Мы наблюдаем короткий промежуток времени деятельности видеомагазина и рассматриваем заказы четырех клиентов на прокат фильмов. В каждом случае мы вводим имя клиента и заказ фильма и определяем, имеется ли этот фильм в наличии в настоящее время. Если да, то мы удаляем его из инвентаризационного списка и добавляем клиента в список лиц, взявших фильмы напрокат. Если фильма нет в наличии, клиент уведомляется об этом. #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <string.h> tinclude "video.h" // объявления видео-данных #include "aseqlist.h" // включить класс SeqList // читать таблицу фильмов с диска void SetupInventoryList(seqList sinventoryList) { ifstream filmFile; FilmData fd; // открыть файл, с проверкой ошибок filmFile.open("Films", ios::in | ios::nocreate); if (!filmFile) { cerr « "Файл 'films' не найден!" « endl; exit(1); } // читать строки до конца файла; // вставлять наименования фильмов в инвентаризационный список while(filmFil.e.getline(fd.filmName,32,'\n')) inventoryList.Insert(fd); } // печать наименовании фильмов void PrintlnvemtoryList(const SeqList &inventoryList) { int i; FilmData fd; for (i - 0; i < inventoryList.ListSize (); i++) { fd «= inventoryList.GetData(i) ; // cout « fd.filmName « endl; // } > // цикл по списку клиентов, печать клиентов и фильмов void PrintCustomerList(const SeqList &customerList) { int i FilmData fd; for (i « 0; i < customerList.ListSize (); i++)
fd = customerList.GetData(i); // cout « fd.customerName « " (" « fd.filmName « ") " « endl; } } void main (void) { // SeqList invemtoryList, customerList; int i; // FilmData fdata; char customer [20]; SetupInventoryList (inventoryList); // читать файл с фильмами // запрос имени клиента и названия фильма. // если запрошенный фильм имеется в наличии, он вносится в список клиентов //и удаляется из списка фильмов; в противном случае выдается // сообщение об отсутствии фильма for (i = 0; i < 4; i++) { // ввод имени клиента и названия фильма cout « "Имя клиента: "; cin.getline(customer,32,'\n'); cout « "Запрашиваемый фильм: " ; cin.getline(fdata.filmName,32,'\n'); if (inventoryList.Find(fdata)) { strcpy(fdata.customerName, customer); // вставить название фильма в список клиентов customerList.Insert(fdata); // удалить из списка фильмов inventoryList.Delete(fdata); } else cout « "Сожалею! " « fdata. filmName « " отсутствует." « endl; } cout << endl; // печать списков клиентов и фильмов cout « "Клиенты, взявшие фильмы для просмотра" « endl; PrintCustomerList(customerList); cout « endl; cout « "Фильмы, оставшиеся в ведомости:"« endl; PrintlnventoryList(inventoryList); } /* <Входной файл "Films"> Война миров Касабланка Грязный Гарри Дом животных Десять заповедей Красавица и зверь Список Шиндлера Звуки музыки
La Strata Звездные войны <Выполнение программы 4.2> Имя клиента: Дон Бекер Запрашиваемый фильм: Дом животных Имя клиента: Тери Молтон Запрашиваемый фильм: Красавица и зверь Имя клиента: Деррик Лопез Запрашиваемый фильм: La Strata Имя клиента: Хиллари Дэн Запрашиваемый фильм: Дом животных Сожалею! Дом животных отсутствует. Клиенты, взявшие фильмы для просмотра Дон Бекер (Дом животных) Тери Молтон (Красавица и зверь) Деррик Лопез (La Strata) Фильмы, оставшиеся в ведомости: Война миров Касабланка Грязный Гарри Десять заповедей Список Шиндлера Звуки музыки Звездные войны */ Письменные упражнения 4.1 Объясните различие между линейной и нелинейной структурой данных. Дайте пример каждой. 4.2 Определите, какая структура данных является соответствующей для следующих ситуаций: (а) Сохранить абсолютное значение числа в элементе 5 целого списка. (б) Пройти по списку студентов в алфавитном порядке и напечатать отметки. (в) Когда арифметический оператор найден, два предыдущих числа удаляются из коллекции. (г) В исследовании с использованием моделирования каждое событие вставляется в коллекцию и удаляется в порядке его вставки. (д) Когда ферзь на шахматной доске может переместиться в какую-либо позицию, эта позиция вставляется в коллекцию. (е) Одно поле структуры данных является целым, другое — действительным значением, а последнее поле — это строка. (ж) Постоянно сохраняйте выходные данные программы, чтобы рассмотреть их позже. (з) Если ключ меньше, чем текущее значение в списке, рассмотрите предыдущие значения. (и) Минимальное значение всегда переходит в вершину списка. (к) Строка используется в качестве ключа для нахождения записи данных, находящейся где-то в коллекции. (л) Слово используется в качестве индекса для нахождения его определения в коллекции.
(м) В праздники телефонная сеть перегружена. Определите альтернативный набор путей для наилучшего распределения вызовов. 4;3 Ниже приведены меры сложности наихудшего случая для трех алгоритмов, которые решают одну и ту же задачу: Алгоритм 1 Алгоритм 2 Алгоритм 3 0(п2) 0(п log2n) 0(2n) Какой метод предпочтителен и почему? 4.4 Выполните анализ Big-О для каждой из следующих функций: (а) п + 5 (б) п2 +6п +7 (в) Vn + 3 (г) п3 + п2 - 1 п + 1 4.5 (а) Для какого значения п>1, 2п становится больше, чем и3? (б) Покажите, что 2п +п3 имеет порядок 0(2п). (в\ п2 + 5 v ' Дайте Big-O-оценку для — + 6 \og2n ? л + 3 4.6 Список целых ведется в массиве. Каков порядок алгоритма печати первого и последнего элемента в массиве? 4.7 Объясните, почему алгоритм порядка 0(log2n) имеет также порядок 0{п). 4.8 Каждый цикл является главным компонентом для алгоритма. Используйте нотацию Big-O, чтобы выразить время вычисления наихудшего случая для каждого из следующих алгоритмов как функции п. (а) for (dotprd=0.0,i=0;i < n;i++) dotprd +=a[i] * b[i]; (б) for (i=0;i<n;i++) if(a[i] == k) return 1; return 0; (в) for (i=0;i<n;i++) for (j=0;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++) entry +=a[i] [k] * b[k] [j]; c[i,j] = entry; } 4.9 Следующие коллекции из п элементов используются для сохранения данных. Каков порядок алгоритма нахождения минимального значения (а) в стеке? (б) в очереди приоритетов?
(в) в дереве бинарного поиска? (г) в последовательном списке с упорядочением по возрастанию? (д) в списке с возможностью прямого доступа к элементам с упорядочением по убыванию? 4.10 Последовательность чисел Фибоначчи имеет вид: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, . , . Первые два числа являются 1, и каждое последующее число Фибоначчи является суммой двух предшествующих. Эта последовательность описывается рекуррентным отношением fl = 1, f2=l, fn = fn-2 + fn-1, ДЛЯ П > 3 Следующая функция вычисляет n-ное число Фибоначчи. Каков ее порядок? long Fibonacci(int n) { long fnm2=l, fninl^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 Какое максимальное количество сравнений необходимо? 4.12 Предположим, SeqList-объект L состоит из элементов: 34 11 22 16 40 (а) Задайте элементы в списке после каждой из следующих команд: п « L.DeleteFront(); L.Insert(n); if (L.Find(L.GetData(0)*2) L.Delete(16); (б) Используя объект L, приведите выход для следующей последовательности команд:
for (int i=0; i <5; i++) { L.Insert(L.DeleteFront()); cout « L.GetData(i) << ; } 4.13 Напишите функцию для реализации указанной задачи (а) Добавить SeqList-объект L в хвост объекта К. void Concatenate(SeqListfi К, SeqList& L); (б) Поменяйте на обратный порядок элементов в SeqList-объекте L. void Reverse (SeqList& L); 4.14 Функция Ques берет SeqList-объект L, элементы которого все являются положительными целыми. Каково действие функции над списком? {1, 3, 7, 2, 15, 0, 12}? Почему L должен передаваться по ссылке? typedef int DataType; #include "aseqlist.h" int M(const SeqList &L) { int i, nival, length « L.ListSize (); if (length == 0) { cerr « Список пустой << endl; return -1; } mval = L.GetData(O); for (i= 1; i< length; I++) if (L.GetData(i) > mval) mval « L.GetData(i); return mval; } void Ques(SeqList &L) { int mval = M(L); L.Delete(mval); } 4.15 Объясните, почему необходима пересылка данных при реализации метода Delete в классе SeqList на базе массива. Упражнения по программированию 4.1 Используйте класс SeqList с DataType int для создания utility функции InsertMax: void InsertMax(SeqList& L, int elt); InsertMax помещает elt в список L, только если он больше всех существующих элементов в списке.
Напишите main-программу, которая читает 10 целых и вызывает In- sertMax для каждого. Напечатайте список. 4.2 Объявите запись struct Person { char name [20]; int age; char gender; }; и вызовите класс SeqList следующим образом: #inclucle <string.h> //needed for SeqList class method Find int operator»* (Person x, Person y) { return strcmp(x.name, y.name)-* 0; } typedef Person DataType; tinclude "aseqlist.h" (а) Напишите функцию void PrintByGender(const SeqListS L, char sex); которая проходит по списку L и печатает все записи, имеющие заданный пол. (б) Напишите функцию int InList (const SeqList&, char *nm, Persons p); которая определяет, существует ли в списке L запись с полем имени nm. Выполните это, создав объект Person с полем имени nm и используя метод Find. Нет необходимости инициализировать поля age и gender записи. Сравнение двух записей выполняется сравнением полей имени. Если совпадение происходит, присвоить запись параметру р и возвратить 1; в противном случае возвратить 0. Параметр р не должен изменяться, если только не будет найдено совпадение. (в) Напишите main-программу для тестирования этих функций. 4.3 Напишите программу, которая запрашивает у пользователя целое п, генерирует массив из п случайных целых в диапазоне 0 ... 999 и сортирует список, используя обменную сортировку. Задайте время выполнения сортировки, используя функцию TickCount, определенную в файле ticks.h. Выполните программу, используя п= 50, 500, 1000 и 10000. Это является экспериментальным подтверждением того, что обменная сортировка имеет порядок 0(п2). Примечание: Так как локальный массив из 1000 или 10000 элементов может превысить объем динамической области системы, выделить динамический массив со следующим синтаксисом для хранения элементов. Вы можете использовать общую нотацию массива a[i] с динамической структурой. int *a; //определяет указатель • • • а = new int [n]; //п равен 50, 500, 1000 или 10000
Напишите программу, которая запрашивает у пользователя целое п, генерирует массив из п случайных целых в диапазоне 0 ... 999 и сортирует список, используя обменную сортировку. Задайте время выполнения сортировки. Выполните программу, используя п = 50, 500, 1000 и 10000. Это является экспериментальным подтверждением того, что обменная сортировка имеет порядок 0(п2). 4.4 Это упражнение расширяет применение Программы 4.2 (Видеомагазин) для включения возврата фильмов. Спросите клиента, берет ли он фильм напрокат или возвращает. Если фильм возвращается, удалите его из списка фильмов, взятых напрокат, и вставьте его в инвентаризационный список. 4.5 Ситуация тестирования содержит пример структуры SeqList. Студенты сдают контрольные работы на стол преподавателя титульной стороной вниз (вставка в конец списка). Предположим, что взволнованный студент обнаруживает правильный ответ на какой-либо вопрос и хочет проверить, как он (или она) ответил. Преподаватель должен перевернуть стопку контрольных работ так, чтобы первая работа оказалась титульной стороной вверх, просмотреть работу, пока не будет найдена работа этого студента, и затем удалить контрольную работу из списка. После того, как студент закончит проверку работы, преподаватель вставляет ее в коцец списка. Напишите программу, которая использует класс SeqList для моделирования этой ситуации. Ассоциируйте студента с контрольной работой, используя следующую запись: struct Test { char name [30]; int testNumber; }; Цикл в main-программе управляет выполнением, читая целое: 1=Сдать контрольную работу 2=Позволить студенту проверить работу З-Возвратить взятую работу 4=Выйти из программы Выполните следующие действия: Ввод 1: Запросите имя и номер работы; вставьте работу в список sub- mittedTests. Ввод 2: Запросите только имя, удалите работу из submitted-Tests и вставьте ее в список borrowedTests. Ввод 3: Запросите имя, удалите запись из borrowedTests и вставьте ее в submittedTests. Ввод 4: Преподаватель готов уйти, и все взятые на время работы должны быть возвращены. Удалите все элементы из borrowedTests, вставляя их в submittedTests. Печатайте окончательный список. Вы должны определить оператор ==, чтобы определить, равны ли две записи Test. Выполните это, используя функцию #include <string.h> int operator== (const Test& tl, Tests t2) { return strcmp(tl.name/ t2.name) -= 0; }
глава Стеки и очереди 5.1. Стеки 5.2. Класс Stack 5.3. Оценка выражения 5.4. Очереди 5.5. Класс Queue 5.6. Очереди приоритетов 5.7. Практическое применение: управляемое событиями моделирование Письменные упражнения Упражнения по программированию
В этой главе мы обсуждаем более подробно классический стек и очередь, являющиеся структурами данных, сохраняющими и возвращающими элементы из защищенных частей списка. Описывается также очередь приоритетов, модифицированная версия очереди, в которой из списка удаляется элемент с наивысшим приоритетом. Стек, очередь и очередь приоритетов реализуются как классы C++. Основные идеи этой главы иллюстрируют два практических примера. Демонстрируется действие RPN-калькулятора со стеком операндов. Пример обслуживания очереди клиентов кассирами в банке показан с помощью событийного моделирования. Это приложение использует очередь приоритетов и знакомит с важным инструментом управления в бизнесе. 5.1. Стеки Стек является одной из наиболее используемых и наиболее важных структур данных. Стеки применяются очень часто. Например, распознавание синтаксиса в компиляторе, как и оценка выражений, основано на стеке. На нижнем уровне стеки используются для передачи параметров функциям, выполнения вызова функции и возвращения из нее. Стек (stack) — это список элементов, доступных только в одном конце списка. Элементы добавляются или удаляются из списка только в вершине (top) стека. Подносы в столовой или стопка коробок являются моделями стека. Стек предназначен для хранения элементов, доступных естественным путем в вершине списка. Представим шампур, на который нанизаны нарезанные овощи, подготовленные для шашлыка. На рис. 5.1 овощи на шампуре 1 расположены в порядке: лук, грибочек, зеленый перец и лук. Перед приготовлением шашлыка гость сообщает, что он не ест грибов, и их необходимо убрать. Эта просьба означает удалить лук (шампур 2), уда- лить грибочек (шампур 3) и затем вновь нанизать лук (шампур 4). Если гость не любит зеленый перец или лук, это доставит повару больше проблем. В структуре стека важнейшее место занимают операции, добавляющие и удаляющие элементы. Операция Push добавляет элемент в вершину стека. Об операции удаления элемента из стека говорят как об извлечении (to pop the stack) из стека. На рис. 5.2 показана последовательность операций Push и Pop. Последний вставленный в стек элемент является первым удаляемым
элементом. По этой прцчине о стеке говорят, что он имеет порядок LIFO (last-in/first-out) (последний пришел/первый ушел). Лук Зеленый перец Грибы Рис. 5.1. Стек овощей Абстрактное понятие стека допускает неопределенно большой список. Логически подносы в столовой могут складываться бесконечно. В действительности подносы находятся на полке, а овощи нанизаны на коротких шампурах. Когда полка или шампур переполнены, мы не можем добавить (Push) еще один элемент в стек. Стек достигает максимального количества элементов, которыми он может управлять. Эта ситуация поясняет значение условия полного стека (stack full). Другая крайность — вы не можете взять поднос с пустой полки. Условие пустого стека (stack empty) подразумевает, что вы не можете удалить (Pop) элемент. Описание ADT Stack включает только условие пустого стека. Условие полного стека является уместным в том случае, если реализация содержит верхнюю границу размера списка. Push A Push В Push С Pop Pop Push D Рис. 5.2. Помещение в стек и извлечение из него
ADT Stack Данные Список элементов с позицией top, указывающей на вершину стека. Операции Конструктор Начальные значения: Нет Процесс: Инициализация вершины стека. StackEmpty Вход: Нет Предусловия: Нет Процесс: Проверка, пустой ли стек. Выход: Возвращать True, если стек пустой, иначе возвращать False. Постусловия: Нет Pop Вход: Нет Предусловия: Стек не пустой. Процесс: Удаление элемента из вершины стека. Выход: Возвращать элемент из вершины стека. Постусловия: Элемент удаляется из вершины стека. Push Вход: Элемент для стека. Предусловия: Нет Процесс: Сохранение элемента в вершине стека. Выход: Нет Постусловия: Стек имеет новый элемент в вершине. Peek Вход: Нет Предусловия: Стек не пустой. Процесс: Нахождение значения элемента в вершине стека. Выход: Возвращать значение элемента из вершины стека. Постусловия: Стек неизменный. ClearStack Вход: Нет Предусловия: Нет Процесс: Удаление всех элементов из стека и переустановка вершины стека. Выход: Нет Постусловия: Стек переустановлен в начальные условия. Конец ADT Stack 5.2. Класс Stack Члены класса Stack включают список, индекс или указатель на вершину стека и набор стековых операций. Для хранения элементов стека используется массив. В результате размер стека не может превысить количества элементов в массиве и условие полного стека является релевантным. В главе 9 мы снимаем это ограничение, когда разрабатываем класс Stack, используя связанный список.
Объявление объекта типа Stack включает размер стека, который определяет максимальное количество элементов в списке. Размер имеет значение по умолчанию MaxStackSize = 50. Список (stacklist), максимальное количество элементов в стеке (size) и индекс (top) являются закрытыми членами, а операции — открытыми. top Реализация стека Первоначально стек пуст и top = -1. Элементы вводятся в массив (функция Push) в возрастающем порядке индексов (top = 0, 1, 2) и извлекаются из стека (функция Pop) в убывающем порядке индексов (top = 2, 1, 0). Например, следующий объект является стеком символов (DataType = char). После нескольких операций Push/Pop индекс top = 2, а элемент в вершине стека — это stacklist[top] = С. top- top«=2 Возрастающий индекс Пример 5.1 Данный пример иллюстрирует целый массив из 5 элементов с последовательностью операций Push 10; Push 25; Push 50; Pop; Pop. Индекс top увеличивается на 1 при операции Push и уменьшается на 1 при операции Pop. Пустой стек top = -1 Push 10 top = 0 Push 25 top = 1 Push 50 top = 2 Pop top = 1 Pop top =0
Спецификация класса Stack ОБЪЯВЛЕНИЕ #include <iostream.h> tinclude <stdlib.h> const int MaxStackSize « 50; class Stack { private: // закрытые данные-члены, массив стека и вершина (индекс) DataType stacklist[MaxStackSize]; int top; public: // конструктор; инициализирует вершину Stack (void); // операции модификации стека void Push (const DataType& item); DataType Pop (void); void ClearStack(void); // доступ к стеку DataType Peek (void) const; // методы проверки стека int StackEmpty(void) const; int StackFull(void) const; // реализация массива }; ОПИСАНИЕ Данные в стеке имеют тип DataType, который должен определяться с использованием оператора typedef. Пользователь должен проверять, полный ли стек, перед попыткой поместить в него элемент и проверять, не пустой ли стек, перед извлечением данных из него. Если предусловия для операции push или pop не удовлетворяются, печатается сообщение об ошибке и программа завершается. StackEmpty возвращает 1 (True), если стек пустой, и 0 (False) — в противном случае. Используйте StackEmpty, чтобы определить, может ли выполняться операция Pop. StackFull возвращает l(True), если стек полный, и 0 (False) — в противном случае. Используйте StackFull, чтобы определить, может ли выполняться операция Push. Clear Stack делает стек пустым, устанавливая 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 (!S.StackEmpty()) temp * S.Pop(); cout « temp « endl; S.ClearStackO; // очистить стек
Реализация класса Stack Конструктор Stack присваивает индексу top значение -1, что эквивалентно условию пустого стека. //инициализация вершины стека Stack::Stack (void) : top(-l) { > Операции стека. Две основные операции стека вставляют (Push) и удаляют (Pop) элемент из стека. Класс содержит операцию Peek, позволяющую клиенту выбирать данные из элемента в вершине стека, не удаляя в действительности этот элемент. Для того, чтобы поместить элемент в стек, увеличьте индекс top на 1 и присвойте новый элемент массиву stacklist. Попытка добавить элемент в полный стек приведет к сообщению об ошибке и завершению программы. // поместить элемент в стек void Stack::Push (const DataType& item) { // если стек полный, завершить выполнение программы if (top == MaxStackSize-1) { cerr « "Переполнение стека!" « endl; exit(l); } // увеличить индекс top и копировать item в массив stacklist top++; stacklist[top] « item; } Перед вставкой элемента После вставки элемента top item top = top + 1 Операция Pop извлекает элемент из стека, копируя сначала значение из вершины стека в локальную переменную temp и затем увеличивая top на 1. Переменная temp становится возвращаемым значением. Попытка извлечь элемент из пустого стека приводит к сообщению об ошибке и завершению программы. Перед операцией Pop После операции Pop item item top top = top - 1 Возвратить // взять элемент из стека DataType Stack::Pop (void) { DataType temp;
// стек пуст, завершить программу if (top == -1) { cerr << "Попытка обращения к пустому стеку! м << endl; exit(l); } // считать элемент в вершине стека temp = stacklist[top] ; // уменьшить top и возвратить значение из вершины стека top—; return temp; } Операция Peek в основном дублирует определение Pop с единственным важным исключением. Индекс top не уменьшается, оставляя стек нетронутым. // возвратить данные в вершине стека DataType Stack::Peek (void) const { // если стек пуст, завершить программу if (top == -1) { cerr << "Попытка считать данные из пустого стека!" << endl; exit(l); } return stacklist[top]; } Условия тестирования стека. Во время своего выполнения операции стека завершают программу при попытках клиента обращаться к стеку неправильно; например, когда мы пытаемся выполнить операцию Peek над пустым стеком. Для защиты целостности стека класс предусматривает операции тестирования состояния стека. Функция StackEmpty проверяет, является ли top равным -1. Если — да, стек пуст и возвращаемое значение — l(True); иначе возвращаемое значение — О (False). // тестирование стека на наличие в нем данных int Stack:-.StackEmpty (void) const { // возвратить логическое top == -1 return top == -1; } Функция StackFull проверяет, равен ли top значению MaxStackSize-1. Если так, то стек заполнен и возвращаемым значением будет 1 (True); иначе, возвращаемое значение — 0 (False). // проверка, заполнен ли стек int Stack::StackFull(void) const { return top == MaxStackSize-1; ) Метод ClearStack переустанавливает вершину стека на -1. Это восстанавливает начальное условие, определенное конструктором. // удалить все элементы из стека void Stack::ClearStack<void) { top = -1; }
Стековые операции Push и Pop используют прямой доступ к вершине стека и не зависят от количества элементов в списке. Таким образом, обе операции имеют время вычисления 0(1). Применение: Палиндромы. Когда DataType является типом char, приложение обрабатывает символьный стек. Приложение определяет палиндромы, являющиеся строками, которые читаются одинаково в прямом и обратном порядке. Пробелы не включаются. Например, dad, sees и madam im adam являются палиндромами, a good — нет. Программа 5.1 использует класс Stack для тестирования входной строки на наличие палиндрома. Программа 5.1. Палиндромы Эта программа читает строку текста, используя функцию cin.getlineQ, и вызывает функцию Deblank для удаления всех пробелов из текста. Функция Deblank копирует непустые символы во вторую строку программы, сканируя строку дважды, проверяет, является ли строка, лишенная пробелов, палиндромом. Во время первого просмотра каждый символ помещается в стек, создается список, содержащий текст в обратном порядке. Во время второго просмотра каждый символ сравнивается с элементом, который удаляется из стека. Просмотр завершается, если два символа не совпадают, в случае чего этот текст не является палиндромом. Если сравнения остаются правильными до тех пор, пока стек не будет пуст, этот текст является палиндромом. Исходная строка = "dad" Палиндром top Исходная строка = "good" Не палиндром top #include <iostream.h> ipragma hdrstop typedef char DataType; // элементы стека - символы #include "astack.h" // создает новую строку void Deblank(char *s, char *t) i // цикл до тех пор, пока не встретится NULL-символ while(*s != NULL) { // если символ - не пробел, копировать в новую строку if (*s != ' ' ) *t++ = *s; s-*- + ; // передвинуться к следующему символу } *t = NULL; // добавить NULL в конец новой строки }
void main (void) { const int True = 1, False = 0; Stack S; char palstring[80], deblankstring[80), c; int i = 0; int ispalindrome = True; // полагаем, что строка - палиндром // считать в полную строку cin.getline(palstring/80,' \n' ); // удалить пробелы из строки и поместить результат в deblankstring Deblank(palstring,deblankstring) ; // поместить символы выражения без пробелов в стек i-0; while(deblankstring[i] != 0) { S.Push(deblankstring[i]); i++; } // сравнение вершины стека с символами из оригинальной строки i- 0; while (!S.StackEmpty()) { с = S. Pop () ; // получить следующий символ из стека // если символы не совпадают, прервать цикл if (с != deblankstring[i]j { ispalindrome = False; // не палиндром break; } i++; } if (ispalindrome) cout « ' \"' « palstring « ' V" « " - палиндром" « endl; else cout « ' \n' « palstring « ' \M/ « " - не палиндром" << endl; } /* Оапуск 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" - не палиндром */
Применение: Вывод данных с различными основаниями. Операторы вывода многих языков программирования печатают числа в десятичном формате как значения по умолчанию. Стек может использоваться для печати чисел с другими основаниями. Бинарные (с основанием 2) числа описаны в главе 2, и мы полагаем, что вы можете применить рассмотренные там принципы для других оснований. Пример 5.2 В этом примере десятичные числа преобразуются в числа с заданным основанием. 1. (Основание 8) 28ю = 3*8+4 = 34в 2. (Основание 4) 72ю = 1*64+0*16+2*4+0 = 1020* 3. (Основание 2) 53ю =1*32+1*16+0*8+1*4+0*2+1 =1101012 Задача отображения на экране числа с недесятичным основанием решается с использованием стека. Мы описываем алгоритм для десятичного числа п, которое печатается как число с основанием В. 1. Крайняя правая цифра п — это п%В. Поместите ее в стек S. 2. Остальные цифры п задаются как п/В. Замените п на п/В. 3. Повторяйте шаги 1-2 до тех пор, пока не будет выполнено л=0 и не останется ни одной значащей цифры. 4. Теперь в стеке имеется новое представление N как числа с базой В. Выбирайте и печатайте символы из S до тех пор, пока стек не будет пуст. На рис. 5.3 показано преобразование п = 3553ю в число с основанием 8. Рисунок отображает рост стека при создании четырех восьмеричных цифр для п. Завершим алгоритм выборкой и затем печатью каждого символа из стека. Выходом является число 6741. п = 355310 Пустой стек п = 3553 п%8 = 1 п/8 = 444 п = 444 п%8 = 4 п/8 = 55 п = 55 п%8=7 п/8 = 6 п = б п%8 = 6 п/8 = 0 п-0 Рис. 5.3. Использование стека для печати числа в восьмеричном формате
Программа 5.2. Печать числа по любому основанию Программа представляет функцию выхода, которая принимает неотрицательное длинное целое num и основание В в диапазоне 2 - 9 и отображает num на экране как число с основанием В. Main-программа запрашивает у пользователя три неотрицательных целых числа и основания, а затем выводит эти числа с соответствующими основаниями. #include <iostream.h> #pragma hdrstop typedef int DataType; #include "astack.h" // печать целого 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): "; cin >> num >> В; cout << num << " основание " << В << " - "; MultibaseOutput(num, В); cout << endl; } } /* Оапуск программы 5.2> Введите неотрицательное десятичное число и основание (2<-В<=9) : 72 4 72 основание 4 - 1020 Введите неотрицательное десятичное число и основание (2<=В<=9): 53 2 53 основание 2 - 110101 Введите неотрицательное десятичное число и основание (2<=В<=9): 3553 8 3553 основание 8 - 6741 */
5.3. Оценка выражений Электронные калькуляторы иллюстрируют одно из основных применений стека. Пользователь вводит математическое выражение с числами (операндами) и операторы, а калькулятор использует один стек для вычисления числовых результатов. Алгоритм калькулятора предусматривает ввод выражения в определенном числовом формате. Такое выражение, как -8 + (4*12 + 5А2)/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). Например, инфиксное выражение "а + Ь" записывается в постфиксной форме как "a b +". В постфиксной форме переменные и числа вводятся по мере их появления, а оператор — там, где имеются два его операнда. Например, в следующем выражении знак * вводится непосредственно после его двух операндов b и с. Знак 4- вводится после того, как имеются его операнды а и (Ь*с). В постфиксном формате используется приоритет операторов. Оператор * появляется перед -К Infix: а + Ь*с = а + (Ь * с) Postfix: a b с * + Скобки в постфиксном формате не обязательны. Далее следует ряд инфиксных выражений и их постфиксные эквиваленты. Инфиксное выражение а*Ь + с a*b*c*d*e*f а + (b*c +d)/e (b*b - 4*а*с)/(2*а) Постфиксное выражение а Ь*с + a b*c*d*e*f* a b c*d + e/+ b b*4a*c* - 2а*/
Постфиксная оценка Постфиксное выражение оценивается алгоритмом, который просматривает выражение слева направо и использует стек. Для этого примера мы полагаем, что все операторы являются бинарными. Унарные операторы мы охватываем в упражнениях. Выражение в постфиксном формате содержит только операнды и операторы. Мы читаем каждый терм и в зависимости от его типа выполняем действия. Если терм является операндом, помещаем его значение в стек. Если терм является оператором <ор>, дважды выполняем выборку из стека для возвращения операндов X и Y. Затем оцениваем выражение, используя оператор <ор> и помещаем результат X<op>Y обратно в стек. После чтения каждого терма в выражении вершина стека содержит результат. Пример 5.3 Инфиксное выражение 4+3*5 записывается в постфиксной форме как 4 3 5*+. Для его оценки требуется пять шагов. Шаги 1-3: Читать операнды 4 3 5 и помещать каждое число в стек. Шаг! Шаг 2 ШагЗ top top top Push 4 Push3 Push 5 Шаг 4: Читать оператор * и оценить выражение выборкой верхних двух операндов 5 и 3 и вычислением 3*5. Результат 15 поместить обратно в стек. top Шаг 4 Шаг 5: Читать оператор + и оценить выражение выборкой операндов 15 и 4 и вычислением 4+15 = 19. Результат 19 поместить обратно в стек, возвратить в качестве результата полученное выражение. top Шаг 5
Применение: постфиксный калькулятор Мы иллюстрируем вычисление постфиксного выражения, моделируя работу с RPN-калькулятором, имеющим операторы +,-,*,/ и л (возведение в степень). Калькулятор принимает данные с плавающей точкой и вычисляет выражения. Данные калькулятора и операции включаются в класс Calculator, а простая main-функция вызывает его методы. Класс Calculator содержит открытые функции-члены, которые вводят выражение и очищают калькулятор. Для оценки выражения используется ряд закрытых функций-членов. Спецификация класса Calculator ОБЪЯВЛЕНИЕ enum Boolean {False, True}; typedef double DataType; // калькулятор принимает числа типа double #include "astack.h" // включить файл с описанием класса Stack class Calculator { private: // закрытые члены: стек калькулятора и операторы Stack S; void Enter{double num); Boolean GetTwoOperands(doubles opndl, doubles opnd2); void Compute(char op); public: // конструктор Calculator(void); // вычислить выражение и очистить калькулятор void Run(void); void Clear(void); }; ОПИСАНИЕ Конструктор умолчания создает пустой стек калькулятора. Так как калькулятор работает постоянно, пользователь должен вызывать Clear для очистки стека калькулятора и обеспечения последующего вычисления нового выражения. Операция Run позволяет вводить выражение в RPN-формате. Ввод символа "=" завершает выражение. Отображается только конечное значение выражения. Сообщение об ошибке Missing operand (Отсутствует операнд) выводится, когда оператор не имеет двух операндов. Попытка разделить на ноль также вызывает сообщение об ошибке. В любом другом случае калькулятор очищает стек и готовится к новому вводу. ПРИМЕР Calculator CALC; //Создает объект CALC CALC.Run(); <Пример работы> 4 3* = 12 // оезультат выражения 4*3
Реализация класса Calculator Функции калькулятора выполняются рядом методов, которые позволяют клиенту вводить число, выполнять вычисление и печатать результат на экране. Определение класса находится в файле calc.h. Calculator::Calculator(void) {} Метод Enter принимает аргумент с плавающей точкой и помещает его в стек. //сохраняем значение данных в стеке void Calculator::Enter(double num) { S.Push(num); } Функция GetTwoOperands используется методом Compute для получения операндов из стека калькулятора и присваивания их параметрам выхода operandi и 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.1" « endl; return False; } opnd2 = S.Pop(); // извлечь левый операнд return True; } Все внутренние вычисления управляются методом Compute, который начинается вызовом GetTwoOperands для выборки двух верхних стековых значений. Если GetTwoOperands возвращает False, мы имеем неправильные операнды и метод Compute очищает стек калькулятора. Иначе Compute выполняет операцию, указанную символьным параметром ор ('+', '-', '*', '/\ ,А') и помещает результат в стек. При попытке деления на 0 печатается сообщение об ошибке, и стек калькулятора очищается. Для возведения в степень используется функция double pow(double х, double у); которая вычисляет ху. Она определяется в C++ библиотеке <math.h>. // выполнение операции void Calculator::Compute(char op) { Boolean result; double operandi, operand2; // извлечь два операнда и получить код завершения result = GetTwoOperands(operandi, operand2);
// при успешном завершении, выполнить оператор // и поместить результат в стек // иначе, очистить стек, проверять деление на 0. if (result » True) switch(op) { case '+': S.Push(operand2+operandl); break; case '-': S.Push(operand2-operandl); break/ case '*': S.Push(operand2*operandl); break; case '/': if (operandi »* 0.0) { cerr « "Деление на ноль!" « endl; S,ClearStack(); } else S.Push(operand2/operandi); break; case 'Л': S.Push(pow(operand2,operandi)); break; } else S.ClearStackO; // ошибка! очистить калькулятор } Основное действие калькулятора реализуется открытым методом Run, который выполняет оценку постфиксного выражения. Главный цикл в методе Run читает символы из потока ввода и завершается, когда считывается символ "=". Символы пробела игнорируются. Если символ является оператором (Ч-\ *-'» **\ 7*> '"')» соответствующая операция выполняется вызовом метода Compute. Если символ не является оператором, Run полагает, что проверяется первый символ операнда, поскольку поток должен содержать только операторы и операнды. Run помещает символ обратно в поток ввода, чтобы он мог быть последовательно считан как часть операнда с плавающей точкой. // считывать и оценивать постфиксное выражение // при вводе '=' остановиться void Calculator::Run(void) { char с; double newoperand; while(cin » с, с !~ '■') // читать до символа '«' (Выход) { switch(с) { case '+': // определение нужного оператора case '-': case '*': case '/': case 'Л': Compute(с); // имеется оператор; вычислить его break; default: //не оператор, возможно, операнд; вернуть символ cin.putback(c);
// читать операнд и передавать его в стек cin » newoperand; Enter(newoperand); break; } ) // ответ, сохраняемый в вершине стека печатать с использованием Peek if (!S.StackEmpty()) cout « S.Peek() « endl; } // очистить стек операндов void Calculator::Clear(void) { S.ClearStackO ; } Программа 5.З. Постфиксный калькулятор Объект CALC — это калькулятор. Первый запуск вычисляет длину гипотенузы прямоугольного треугольника со сторонами 6, 8 и 10. Два других запуска иллюстрируют обработку ошибок. #include "calc.h" void main (void) { Calculator CALC; CALC.RunO; } /* Оапуск 1 программы 5.3> 88*66* + .5 Л = 10 Оапуск #2 программ*! 5.3> 3 4 + * Missing operand! 3 4 + 8* = 56 Оапуск 3 программы 5.3> 10/- Деление на 0! */ 5.4. Очереди Очередь (queue) — это структура данных, которая сохраняет элементы в списке и обеспечивает доступ к данным только в двух концах списка (рис. 5.4). Элемент вставляется в конец списка и удаляется из начала списка. Приложения используют очередь для хранения элементов в порядке их поступления.
1-й 2-й 3-й 4-й |ПоследЛ Начало Рис.5.4. Очередь А начало конец А В начало конец А в с начало конец В с начало конец С Конец Добавление А Добавление В Добавление С Удаление А Удаление В начало конец Рис.5.5. Операции очереди Элементы удаляются из очереди в том же порядке, в котором они сохраняются и, следовательно, очередь обеспечивает FIFO (first-in/first-out) или FCFS- упорядочение (first-come/first-served). Обслуживание клиентов в очереди и буферизация задач принтера в системе входных и выходных потоков принтера — это классические примеры очередей. Очередь включает список и определенные ссылки на начальную и конечную позиции (рис. 5.5). Эти позиции используются для вставки и удаления элементов очереди. Подобно стеку, очередь сохраняет элементы параметризованного типа DataType. Подобно стеку, абстрактная очередь не ограничивает количество элементов ввода. Однако, если для реализации списка используется массив, может возникнуть условие полной очереди. ADT Queue данные Список элементов front: позиция первого элемента в очереди rear: позиция последнего элемента в очереди count: число элементов в очереди в любое данное время Операции Конструктор Начальные значения: Нет Процесс: Инициализация начала и конца очереди.
QLength Вход: Нет Предусловия: Нет Процесс: Определение количества элементов в очереди Выход: Возвращать количество элементов в очереди. Постусловия: Нет QEmpty Вход: Нет Предусловия: Нет Процесс: Проверка: пуста ли очередь. Выход: Возвращать 1 (True), если очередь пуста и 0 (False) иначе. Заметьте, что это условие эквивалентно проверке, равна ли QLength 0. Постусловия: Нет QDelete Вход: Нет Предусловия: Очередь не пуста. Процесс: Удаление элемента из начала очереди. Выход: Взвращать элемент, удаляемый из очереди. Постусловия: Элемент удаляется из очереди. Qlnsert Вход: Элемент для сохранения в очереди. Предусловия: Нет Процесс: Запись элемента в конец очереди. Выход: Нет Постусловия: Новый элемент добавляется в очередь QFront Вход: Нет Предусловия: Очередь не пуста. Процесс: Выборка значения элемента в начале очереди. Выход: Возвращать значение элемента в начале очереди. Постусловия: Нет ClearQueue Вход: Нет Предусловия: Нет Процесс: Удаление всех элементов из очереди и восстановление начальных условий. Выход: Нет Постусловия: Очередь пуста. Конец ADT Queue Пример 5.4 На рис. 5.6 показаны изменения в очереди из четырех элементов во время последовательности операций. В каждом случае приводится значение флажка QEmpty. Очереди широко используются в компьютерном моделировании, таком как моделирование очереди клиентов в банке. Многопользовательские операционные системы поддерживают очереди программ, ожидающих выполнения, и заданий, ожидающих печати.
Операция Список очереди Признак пустого списка Qlnsert (A) Qlnsert (В) QDelete О front rear А front rear А В front В rear TRUE FALSE FALSE FALSE front rear Рис.5.6. Изменения в очереди из четырех элементов во время операций 5.5. Класс Queue Класс Queue реализует ADT, используя массив для сохранения списка элементов и определяя переменные, которые поддерживают позиции front и rear. Так как для реализации списка используется массив, класс содержит метод Qfull для проверки, заполнен ли массив. Этот метод будет устранен, в главе 9, где представлена реализация очереди со связанным списком. Спецификация класса Queue ОБЪЯВЛЕНИЕ #include <iostream.h> #include <stdlib.h> // максимальный размер списка очереди const int MaxQSize = 50; class Queue { private: // массив и параметры очереди int front, rear, count; DataType qlist[MaxQSize]; public: // конструктор Queue (void); // initialize integer data members // операции модификации очереди void Qlnsert(const DataTypes item); DataType QDelete(void); void ClearQueue(void); // операции доступа DataType QFront(void) const;
// методы тестирования очереди int QLength(void) const; int QEmpty(void) const; int QFull(void) const; }; ОПИСАНИЕ Параметризованный тип DataType позволяет очереди управлять различными типами данных. Класс Queue содержит список (qlist), максимальный размер которого определяется константой MaxQSize. Данное-член count содержит число элементов в очереди. Это значение также определяет, является ли очередь полной или пустой. Qlnsert принимает элемент item типа DataType и вставляет его в конец очереди, a QDelete удаляет и возвращает элемент в начале очереди. Метод QFront возвращает значение элемента в начале очереди. Очередь следует тестировать при помощи метода QEmpty перед удалением элемента и метода QFull перед вставкой новых данных для проверки, пуста ли очередь или заполнена. Если предусловия для Qlnsert или QDelete нарушаются, программа печатает сообщение об ошибке и завершается. Объявление очереди и реализация содержатся в файле aqueue.h. ПРИМЕР typedef int DataType; ♦include aqueue.h Queue Q; //объявляем очередь Q.Qlnsert(30); //вставляем 30 в очередь Q.Qlnsert(70); //вставляем 70 в очередь cout «Q.QLength()«endl; //печатает 2 cout «Q.QFront () «endl; //печатает 30 if (!Q.QEmpty( )) cout «Q.QDelete( ); //печатает значение 30 cout «Q.QFront ( ) «endl; //печатает 70 Q/ClearQueue( ); //очистка очереди Реализация класса Queue Начало очереди определяется первым клиентом в очереди. Конец очереди — это место непосредственно за последним элементом очереди. Когда очередь полна, клиенты должны идти к другой расчетной очереди. На рис. 5.7 показаны изменения в очереди и некоторые проблемы, которые влияют на реализацию. Предположим, очередь ограничивается четырьмя клиентами. Вид 2 показывает, что после того, как клиента А обслужили, клиенты В и С перемещаются вперед. Вид 3: клиента В обслужили и С перемещается вперед. Вид 4: клиенты D, Е и F встают в очередь, заполняя ее, а клиент G должен встать в другую очередь. Эти виды отражают поведение клиентов в расчетной очереди. После того, как одного клиента обслужили, другие в очереди перемещаются вперед. В терминологии списка, элементы данных смещаются вперед на одну позицию каждый раз, когда какой-либо элемент покидает очередь. Эта модель не обеспечивает эффективную компьютерную реализацию. Предположим, очередь содержит 1000 элементов. Когда один элемент удаляется из начала, 999 элементов должны переместиться влево. Наша реализация очереди вводит круговую модель. Вместо сдвига элементов влево, когда один элемент удаляется, элементы очереди организованы логически в окружность. Переменная front всегда является местоположением первого элемента очереди, и она продвигается вправо по кругу по мере вы-
Вид№1 Ввести клиентов А, В, С Вид №2 Обслужить клиента А Вид №3 Обслужить клиента В Вид №4 Добавить клиентов D, E, F А front В front С front С В С rear D С rear Е rear F front G rear Рис. 5.7. Очередь из четырех элементов полнения удалений. Переменная rear является местоположением, где происходит следующая вставка. После вставки rear перемещается по кругу вправо. Переменная count поддерживает запись количества элементов в очереди, и если счетчик count равен значению MaxQSize, очередь заполнена. На рис. 5.8 показана круговая модель очереди. размер = 4 count » 3 размер = 4 count ■= 2 размер = 4 count » 3 rear front rear rear front front Очередь заполнена размер - 4 count = 4 Удалить А размер • 4 count ■ 3 Вставить D rear front front rear Вставить Е Удалить В Рис, 5.8. Круговая модель очереди
Реализуем круговое движение, используя операцию остатка от деления: Перемещение конца очереди вперед: rear = (rear+l)%MaxQSize; Перемещение начала вперед: front - (front+l)%MaxQSize; Пример 5.5 Используем целый массив qlist (размер = 4) из четырех элементов для реализации круговой очереди. Первоначально count = 0, и индексы front и rear имеют значение 0. На рис. 5.9 показана последовательность вставок и удалений круговой очереди. count * 0 3 Вставить 3 count «= 1 3 Вставить 5 count = 2 5 front = rear « *0 0 Вставить б count - 3 3 5 6 front = 0 rear = 3 Удалить 5 count = 2 6 2 rear * 0 front = 2 Удалить 2 count = 1 8 front = 0 rear = 1 Удалить 3 count * 2 front = 0 rear = 2 Вставить 2 count = 3 5 6 5 6 2 front - 1 rear = 3 rear = 0 front = 1 8 Вставить 8 count = 3 6 2 8 Удалить 6 count = 2 2 rear = 1 front = 2 rear = 1 front = 3 Удалить 8 count = 0 front = 1 rear = 1 front = 0 rear = 1 Рис 5.9. Последовательность вставок и удалений круговой очереди Конструктор Queue. Конструктор инициализирует элементы данных front, rear и count нулевыми значениями. Это задает пустую очередь // инициализация данных-членов: front, rear, count Queue::Queue (void) : front(0), rear(0), count(0) О Операции класса Queue. Для работы с очередью предоставляется ограниченный набор операций, которые добавляют новый (метод Qlnsert) или удаляют (метод Qdelete) элемент. Класс имеет также метод QFront, который позволяет делать выборку первого элемента очереди. Для некоторых приложений эта операция позволяет определять, должен ли элемент удаляться из списка. В этом разделе описываются операции обновления очереди, вставляющие и удаляющие элементы списка. Другие методы имеют модели в стековом классе и их можно найти в программном приложении в файле aqueue.h.
Перед началом процесса вставки индекс rear указывает на следующую позицию в списке. Новый элемент помещается в это место, и переменная count увеличивается на 1. qlist[rear] - item; count++; После помещения элемента в список индекс rear должен быть обновлен для указания на следующую позицию [Рис. 5.10 (А)]. Так как мы используем круговую модель, вставка может появиться в конце массива (qlist[size-l]) с перемещением rear к началу списка [рис. 5.10(B)]. Вычисление выполняется с помощью оператора остатка (%). rear - (rear+1) % MaxQSize; Qlneert // вставить item в очередь void Queue::Qlnsert (const DataType& item) { // закончить программу, если очередь заполнена if (count == MaxQSize) { cerr « "Переполнение очереди!" « endl; exit(1); } // увеличить count, присвоить значение item элементу массива // изменить значение rear count++; qlist[rear] = item; rear = (rear+1) % MaxQSize; } (A) До Qlnsert: count = 2 После Qlnsert: count = 3 ■ t front ■ t rear ■ t front ■ элемент t rear (B) До Qlnsert: count = 2 После Qlnsert: count = 3 ■ t front ■ t rear t rear ■ t front ж элемент Рис 5.10. Метод Qinsert Операция QDelete удаляет элемент из начала очереди, позиции, на которую ссылается индекс front. Мы начинаем процесс удаления, копируя значение во временную переменную и уменьшая счетчик очереди. item = qlist[front]; count—; В круговой модели необходимо перенести front в позицию следующего элемента в списке, используя оператор остатка от деления (%) (рис. 5.11). front = (front + 1) % MaxQSize;
Значение из временной позиции становится возвращаемым значецием. QDelete // удалить элемент из начала очереди // и возвратить его значение DataType Queue::QDelete(void) { DataType temp; // если очередь пуста, закончить программу) { cerr « "Удаление из пустой очереди!" « endl; exit(l); ) // записать значение в начало очереди temp » qlist[front]; // уменьшить count на единицу // продвинуть начало очередии возвратить прежнее значение //из начала count—; front - (front+1) % MaxQSize; return temp; (A) До QDelete: count = 3 После QDelete: count =2 (B) ■ элемент ■ ■ T T front rear До QDelete: count = 3 ■ элемент A A ■ T T т rear front front ■ ■ т front После QDelete: count =2 ■ T rear t rear элемент возвратить в элемент озврати ть Рис. 5.11. Метод QDelete Операции Qlnsert, QDelete и QFront имеют эффективность 0(1)9 поскольку каждый метод имеет прямой доступ к элементу либо в начале, либо в конце списка. Программа 5.4. Партнеры по танцу Танцы организуются в пятницу вечером. По мере того, как мужчины и женщины входят в танцевальный зал, мужчины выстраиваются в один ряд, а женщины — в другой. Когда танец начинается, партнеры выбираются по одному из начала каждого ряда. Если в этих рядах неодинаковое количество людей, лишний человек должен ждать следующего танца.
Данная программа получает имена мужчин и женщин, читая файл dance.dat. Каждый элемент данных файла имеет формат Sex Name где Sex — это один символ F или М. Все записи считываются из файла, и организуются очереди. Партнеры образуются удалением их из каждой очереди. Этот процесс останавливается, когда какая-либо очередь становится пустой. Если есть ожидающие люди, программа указывает, сколько их и печатает имя первого человека, который будет танцевать в следующем танце. iinclude <iostream.h> ♦include <iomanip.h> iinclude <fstream.h> #pragma hdrstop // record that declares a dancer struct Person { char name[203; char sex; // ' F' (женщина) ; 'М' (мужчина) }; // очередь содержит список объектов типа Person typedef Person DataType; iinclude "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); } // считать входную строку, которая включает // пол, имя и возраст while(fin.get(p.sex)) // цикл до конца файла { fin.get(blankseparator); fin.getline(p.name,20,' \n'); // вставить в соответствующую очередь if (p.sex »- ' F') femaleDancers.Qlnsert(p); else maleDancers.Qlnsert(p); } // установить пару танцоров, получением партнеров
//из двух очередей // закончить, когда одна из очередей окажется пустой cout « "Партнеры по танцу: " « endl « endl; while (!femaleDancers.QEmpty() && !maleDancers.QEmptyО) { p * femaleDancers.QDeleteO; cout « p.name « " "; // сообщить имя женщины p « maleDancers.QDelete(); cout « p.name « endl; // сообщить имя мужчины } cout « endl; // если в какой-либо очереди кто-либо остался, // сообщить имя первого (первой) из них if (!femaleDancers.QEmpty()) { cout « "Следующего танца ожидают " « femaleDancers.QLength() « " дамы" « endl; cout « femaleDancers.QFront().name « " первой получит партнера." « endl; } else if (!maleDancers.QEmpty()) { cout « "Следующего танца ожидают " « maleDancers.QLength() << " кавалера." « endl; cout « maleDancers.QFront().name « " первым получит патнера." « endl; } } /* <Файл "dance.dat"> M George Thompson F Jane Andrews F Sandra Williams M Bill Brooks M Bob Carlson F Shirley Granley F Louise Sanderson M Dave Evans M Harold Brown F Roberta Edwards M Dan Gromley M 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 первым получит патнера. */
Применение: использование очереди для сортировки данных. На заре компьютеризации для упорядочения стопки перфокарт использовался механический сортировщик. Следующая программа моделирует действие этого сортировщика. Для объяснения процесса предположим, что перфокарты содержат числа из двух цифр в диапазоне 00-99, и сортировщик имеет десять бункеров с номерами 0-9. При сортировке выполняются два прохода для обработки сначала по позициям единиц, а затем — десятков. Каждая перфокарта попадает в соответствующий бункер. Такая сортировка называется поразрядной сортировкой (radix sort) и может быть расширена до сортировки чисел любого размера. Начальный список: 91 46 85 15 92 35 31 22 В проходе 1 перфокарты распределяются по позициям единиц. Перфокарты выбираются из бункеров в порядке от 0 до 9. Список после прохода 1: 91 31 92 22 85 15 35 46 В проходе 2 перфокарты распределяются по позициям десятков. Перфокарты выбираются из бункеров в порядке от 0 до 9. Список после прохода 2: 15 22 31 35 46 85 91 92 После двух проходов список становится упорядоченным. Интуитивно проход 1 приводит к тому, что все перфокарты с меньшими единичными цифрами предшествуют перфокартам с большими единичными цифрами. Например, все числа, заканчивающиеся на 1, предшествуют числам, заканчивающимся на 2 и так далее. Для прохода 2 предположим, что две перфокарты имеют значение 3s и 3t при s<t. Они попадают в бункер 3 в таком порядке, что за 3s следует 3t (помните, что все перфокарты, заканчивающиеся на s предшествуют перфокартам, заканчивающимся на t после прохода 1). Так как каждый бункер является очередью, перфокарты покидают бункер 3 в порядке FIFO. Если после прохода 2 перфокарты снова собрать, они будут упорядочены.
Программа 5.5. Поразрядная сортировка Данная программа выполняет поразрядную сортировку чисел из двух цифр. Последовательность из 50 случайных чисел сохраняется в списке, представленном массивом L. int L(50); //содержит 50 случайных целых Массив очередей моделирует 10 сортировочных бункеров; Queue digitQueue[10]; // 10 очередей целых Функции Distribute передается массив чисел, массив очередей digitqueue и определенный пользователем дескриптор ones или tens для указания того, выполняется ли сортировка по позициям единиц (проход 1) или десятков (проход 2). В проходе 1 используется выражение L[i]%10 для доступа к цифре единиц, а затем — полученное значение для передачи L[i] соответствующей очереди. digitQueue[L[i] % 10].Qlnsert(L[i]) В проходе 2 используется выражение L[i]/10 для доступа к цифре десятков, а затем — полученное значение для передачи L[i] соответствующей очереди. digitQueue[L[i] / 10].Qlnsert(L[i]) Функция Collect просматривает массив очередей digitQueue в порядке цифр 0 — 9 и извлекает все элементы из каждой очереди в список L. while (!digitQueue[digit].QEmpty()) L[i++] = digitQueue[digit].QDelete(); Функция Print записывает числа в списке. Числа печатаются по 10 в строке с каждым числом, использующим пять позиций печати. #include <iostream.h> #pragma hdrstop #include "random.h" // объявление генератора случайных чисел typedef int DataType; #include "aqueue.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].Qlnsert(L[i]); else // вычислить цифру десятков и использовать ее как номер очереди digitQueue[L[i] / 10].Qlnsert(L[i]);
} // собрать элементы из очередей в массив void Collect(Queue digitQueue[], int L[]) { int i = 0, digit = 0; // сканировать массив очередей, // используя индексы 0, 1, 2, и т.д. for (digit = 0; digit < 10; digit++) // собирать элементы, пока очередь не опустеет, // копировать элементы снова в массив while (!digitQueue[digit].QEmpty()) L[i++] = digitQueue[digit].QDelete(); } // сканировать массив из п элементов и печатать каждый элемент. // в каждой строке печатать 10 элементов void PrintArray(int L[],int n) { int i = 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 rnd; // инициализировать массив случайными числами // в диапазоне 0-99 for (i = 0; i < 50; i++) L[i] =* rnd.Random(100) ; // распределить в 10 бункеров по цифрам единиц; // собрать и распечатать Distribute(L, digitQueue, 50, ones); Collect(digitQueue, L); PrintArray(L,50); // распределить в 10 бункеров по цифрам десятков; // собрать и распечатать отсортированный массив Distribute(L,digitQueue, 50, tens); Collect(digitQueue,L); PrintArray(L,50); } /* Оапуск программы 5.5>
40 62 3 25 88 3 24 52 69 83 70 72 73 16 98 7 25 54 69 84 20 82 33 46 68 11 27 55 70 85 51 82 54 86 69 12 29 59 72 86 11 62 24 36 79 15 33 62 72 88 81 72 84 67 89 16 36 62 73 89 21 52 55 17 29 17 40 63 79 92 12 83 15 27 69 20 46 65 81 97 52 63 65 7 99 21 51 67 82 98 92 23 85 97 59 23 52 68 82 99 */ Каждый проход выполняет О(п) операций, состоящих из деления, вставки в очередь и удаления из нее. Так как выполняется два прохода, порядок алгоритма поразрядной сортировки чисел из двух цифр составляет 0(2п) и также является линейным. Поразрядная сортировка может быть расширена до сортировки п чисел, каждое из которых имеет т цифр. В этом случае сложность составляет О(тп), так как поразрядная сортировка выполняет т проходов, каждый включающий О(п) операций. Этот алгоритм превосходит алгоритмы сортировки, имеющие порядок 0(п \og2n), такие как heapsort и quicksort, которые описываются в главах 13 и 14. Однако, поразрядная сортировка имеет меньшую эффективность по использованию памяти, чем эти in-place-сортиров- ки. Алгоритмы in-place-сортировок сортируют данные в оригинальном массиве и не используют временную память. Поразрядная сортировка требует использования 10 очередей. Каждая очередь имеет свою локальную память для front, rear, queue count и массива. Кроме того, поразрядная сортировка менее эффективна, если числа содержат много цифр, так как при этом возрастает произведение тп. 5.6. Очереди приоритетов Как уже описывалось, очередь — это структура данных, которая обеспечивает FIFO-порядок элементов. Очередь удаляет самый старый элемент из списка. Приложения часто требуют модифицированной версии памяти для очереди, в которой из списка удаляется элемент с высшим приоритетом. Эта структура, называемая очередью приоритетов (priority queue), имеет операции PQInsert и PQDelete. PQInsert просто вставляет элемент данных в список, а операция PQDelete удаляет из списка наиболее важный элемент (с высшим приоритетом), оцениваемый по некоторому внешнему критерию, который различает элементы в списке. Например, предположим, компания имеет централизованную секретарскую группу для обеспечения выполнения персоналом определенных задач. Политика компании рассматривает задание, выданное президентом компании, как имеющее высший приоритет, за которым следуют задания менеджеров, затем — задания супервайзеров и так далее. Должность человека в компании становится критерием, который оценивает относительную важность задания. Вместо управления заданиями на основе порядка first-come/first-served (очередь), секретарская группа выполняет задания в порядке их важности (очередь приоритетов). Очереди приоритетов находят применение в операционной системе, которая записывает процессы в список и затем выполняет их в порядке приоритетов.
Например, большинство операционных систем присваивают более низкий, чем другим процессам, приоритет выполнению печати. Приоритет 0 часто определяется как высший приоритет, а обычный приоритет имеет большее значение, такое как 20. Например, рассмотрим следующий список задач и их приоритетов: Задача № 1 20 Задача № 2 0 Задача № 3 40 Задача № 4 30 Задача № 5 10 Порядок хранения Задачи выполняются в порядке 2, 5, 1, 4 и 3. Задача № 2 Задача № 5 Задача № 1 Задача № 4 Задача № 3 Порядок выполнения В большинстве приложений элементы в очереди приоритетов являются парой ключ-значение (key-value pair), в которой ключ определяет уровень приоритета. Например, в операционной системе каждая задача имеет дескриптор задачи и уровень приоритета, служащий ключом. Уровень приоритета Дескриптор задачи При удалении элемента из очереди приоритетов в списке могут находиться несколько элементов с одним и тем же уровнем приоритета. В этом случае мы можем потребовать, чтобы эти элементы рассматривались как очередь. В результате элементы с одним и тем же приоритетом обслуживались бы в порядке их поступления. В следующем ADT мы не делаем никаких допущений по поводу порядка элементов с одним и тем же уровнем приоритета. Очередь приоритетов описывает список с операциями для добавления или удаления элементов из списка. Имеется серия операций, которая определяет длину списка и указывает, пуст ли список. ADT PQueue Данные Список элементов. Операции Конструктор Начальные значения: Нет Процесс: Инициализация количества элементов списка нулевым значением PQLength Вход: Нет Предусловия: Нет Процесс: Определение количества элементов в списке. Выход: Возвращать количество элементов в списке. Постусловия: Нет PQEmpty Вход: Нет Предусловия: Нет Процесс:- Проверка/ является ли количество элементов списка равным 0.
Выход: Возвращать l(True), если в списке нет элементов, и 0 (False) — иначе. Постусловия: Нет PQInsert Вход: Элемент для сохранения в списке. Предусловия: Нет Процесс: Сохранение элемента в списке. Это увеличивает длину списка на 1. Выход: Нет Постусловия: Список имеет новые элемент и длину. PQDlelete Вход: Нет Предусловия: Очередь приоритетов не пуста. Процесс: Удаление элемента с высшим приоритетом из списка. это уменьшает длину списка на 1. Выход: Возвращать элемент, удаленный из списка. Постусловия: Элемент удаляется из списка, который теперь имеет на один элемент меньше. ClearPQ Вход: Нет Предусловия: Нет Процесс: Удаление всех элементов из очереди приоритетов и восстановление начальных условий. Выход: Нет Постусловия: Очередь приоритетов пуста. Конец ADT PQueue Класс PQueue В этой книге приводятся различные реализации очереди приоритетов. В каждом случае выделяется объект list для сохранения элементов. Мы используем параметр count и методы доступа к списку для вставки и удаления элементов. В этой главе элементы, сохраняемые в массиве, имеют параметризованный тип DataType. В последующих главах используются упорядоченные списки и динамические области для сохранения элементов в очереди приоритетов. Спецификация класса PQueue ОБЪЯВЛЕНИЕ #include <iostream.h> #include <stdlib.h> // максимальный размер массива очереди приоритетов const int MaxPQSize = 50; class PQueue { private: // массив очереди приоритетов и счетчик int count; DataType pqlist[MaxPQSize]; public: // конструктор
PQueue (void); // операции, модифицирующие очередь приоритетов void PQInsert(const DataType& item); DataType PQDelete(void); void ClearPQ(void)/ // тестирующие методы int PQEmpty(void) const; int PQFull(void) const; int PQLength(void) const; }/ ОПИСАНИЕ Константа MaxPQSize определяет размер массива pqlist. Метод PQInsert просто вставляет элементы в список. В спецификации не делается никаких допущений о том, где элемент помещается в списке. Метод PQDelete удаляет элемент с высшим приоритетом из списка. Мы полагаем, что элемент с высшим приоритетом — это элемент с наименьшим значением. Наименьшее значение определяется с использованием оператора сравнения "<", который должен быть определен для DataType. ПРИМЕР typedef int DataType; PQueue PQ; PQ.PQInsert(20); PQ.PQInsert(10); cout « PQ.PQLengthO « endl; // печать 2 N » PQ.PQDelete(); // извлечь N * 10 Реализация класса Pqueue Операции очереди приоритетов. Подобно обычной очереди, очередь приоритетов имеет операцию для вставки элемента. ADT не делает никаких допущений о том, в какое место списка помещается элемент, оставляя этот вопрос в качестве детали реализации в методе PQInsert. В данном случае мы сначала тестируем, заполнен ли список, и завершаем программу при этом условии. В противном случае, новый элемент вставляется в конец списка, местоположение которого указывается с помощью счетчика (count). До PQInsert: count = 4 После PQInsert: count = 5 20 40 10 30 20 40 10 30 50 Вставить 50 \ pqllst[4] PQInsezt // вставить элемент в очередь приоритетов void PQueue::PQInsert (const DataTypeb item) { // если уже све элементы массива pqlist использованы, // закончить завершить программу if (count « MaxPQSize) { cerr « "Переполнение очереди приоритетов!" << endl; exit(l)/
} // поместить элемент в конец списка //и увеличить count на единицу pqlist[count] = item; count++; ) Метод PQDelete удаляет из списка элемент с высшим приоритетом. Это условие не подразумевает, что мы выбираем первый элемент, когда имеются два или более элементов, как и не подразумевает, что элементы сохраняют какой-либо порядок во время процесса удаления. Это все — детали реализации. В данном случае мы сначала определяем, является ли список пустым, и завершаем программу, если условие является равным True. В противном случае, мы ищем минимальное значение и удаляем его из списка, уменьшая длину очереди (count) и заменяя этот элемент последним элементом в списке. Индекс последнего элемента становится новым значением count. В следующем примере минимальное значение (10) имеет элемент с индексом 2. Метод удаляет этот элемент, уменьшая длину списка на 1 и заменяя его последним элементом в списке (pqlist[count]). Затем удаляется 15, который находится в конце списка. count=5 Минимальный элемент с индексом 2 count=4 Минимальный элемент заменить последним Удалить 10 20 40 10 Т pqlistp] 15 50 20 40 50 15 Вставить 50 count=4 Минимальный элемент с индексом 3 count=3 Минимальный элемент заменить последним Удалить 15 20 40 50 Т pqlistP] 15 20 40 50 15 PQDelete // удаляет элемент из очереди приоритетов /У и возвращает его значение DataType PQueue::PQDelete(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[i]. новый индекс - i min = pqlist[i];
minindex = i; } // переместить хвостовой элемент на место минимального //и уменьшить на единицу count pqlist[minindex] = pqlist[count-1]; count--; } // массив qlist пуст, завершить программу else { cerr << "Удаление из пустой очереди приоритетов!" << endl; exit(l); } // возвратить минимальное значение return min; } Операция PQInsert имеет время вычисления (порядок) 0(1), так как она непосредственно добавляет элемент в конец списка. С другой стороны, операция PQDelete требует начального просмотра списка для определения минимального значения и его индекса. Эта операция имеет время вычисления 0(п)у где п — это текущая длина очереди приоритетов. Данное-член count содержит количество элементов в списке. Это значение используется в реализации методов PQLength, PQEmpty и PQFull. Код этих методов находится в программном приложении в файле apqueue.h. Приложение: службы поддержки компании Сотрудники компании определяются по категориям: менеджер, супервайзер и рабочий. Создав тип enum с различными категориями, мы имеем естественное упорядочение, дающее каждой уровень приоритета при заявке на выполнение работ. //уровень приоритета сотрудника (менеджер = 0, и т.д.) enunv Staff{Manager, Supervisor, Worker}; // Менеджер =0, и т. д. Работа службы поддержки компании выполняется общей секретарской группой. Каждый сотрудник Может сделать заказ на выполнение задания, заполнив форму, которая включает информацию о категории служащего, подающего заявку на выполнение работы, ID-номер задания, и указывая время, за которое будет выполнена работа. Эта информация сохраняется в записи JobRequest. Заявки на выполнение работ вводятся в очередь приоритетов с приоритетом, определяемым по категории сотрудника. Это упорядочение используется, чтобы определить операцию < для объектов типа JobRequest. // структура, определяющая запрос struct JobRequest { Staff staffPerson; int jobid; int joTime; }; // перегрузка оператора < для сравнения // двух объектов JobRequest int operator < (const JobRequest& a, const JobRequest& b) { return a.staffPerson < b.staffPerson; }
Файл job.dat содержит список заявок на выполнение заданий, которые загружаются в очередь приоритетов. Приложение подразумевает, что заявки помещены заранее и ожидают выполнения. Элементы извлекаются из очереди приоритетов и выполняются. Массив jobServicesUse содержит общее количество времени, потраченное на обслуживание каждого из различных типов сотрудников: //время, потраченное на работу для каждой категории сотрудников int jobServicesUse[3] = {0, 0, 0}/ Функции печати PrintJoblnfo и PrintSupportSummary выдают информацию о каждом задании и об общем количестве минут, потраченных на обслуживание каждой категории служащих в компании: // печать одной записи структуры JobRequest viod PrintJoblnfo(JobRequest PR) { switch (PR.staffPerson) { case Manager: cout « Manager ; break; case Supervisor: cout « Supervisor ; break; case Worker: cout « Worker ; break; } #include <iomanip.h> // печать общего времени работы, // выделенного каждой категории служащих void PrintJobSummery(int jobServiceUse[)) { cout « ХпВремя обслуживания по категориями; cout « Manager « setw(3) « cout « jobServicesUse[0] « endl; cout « Supervisor « setw(3) « cout « jobServicesUse[1] << endl; cout « Worker « setw(3) « cout « jobServicesUse[2] << endl; Программа 5.6. Выполнение заданий Каждая заявка на выполнение работы сохраняется как запись в файле job.dat. Эта запись задает категорию сотрудника (*М\ 43 \ 'W') и ID-номер задания, и время для выполнения. Записи читаются, пока не достигается конец файла, и каждая запись вставляется в очередь приоритетов jobPool. На выходе каждое задание извлекается из очереди приоритетов, и его информация печатается функцией PrintJoblnfo. Программа завершается печатью резюме по выполненным заданиям вызовом функции PrintJob- Summary. Структура JobRequest и функции работы с ней находятся в файле job.h. #include <iostream.h> #include <fstream.h> #pragma hdrstop #include "job.h"
// элементы очереди приоритетов имеют тип JobRequest typedef JobRequest DataType; #include "apqueue.h" // включить класс PQueue void main() { // обрабатывает до 25 заданий PQueue jobPool; // требуемые задания читаются из потока // с дескриптором fin ifstream fin; // время облуживания каждой категории служащих int jobServicesUse[3] = {0, 0, 0); JobRequest PR; char ch; // открыть файл job.dat для ввода // при ошибке завершить работу программы fin.open("job.dat", ios::in | ios:inocreate); if (!fin) { cerr « "Невозможно открыть файл job.dat" « endl; exit(l); } // читать файл. // вставлять каждое задание в очередь приоритетов jobPool. // каждая строка начинается с символа, характеризующего // служащего while (fin » ch) { // полю staffPerson присвоить категорию служащего switch(ch) { case 'M' : 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; // вставить задание в очередь приоритетов jobPool.PQInsert(PR); } // удалять задания из очереди приоритетов //и печатать эту информацию cout « " Категория Номер Время\п\п"; while (!jobPool.PQEmpty()) { PR = jobPool.PQDeleteO ; PrintJoblnfo(PR); // накапливать время для выполнения заданий
// каждой категории служащих jobServicesUsefint(PR.staffPerson)] += PR.jobTime; } PrintJobSummary(jobServicesUse); } /* <Входной файл job.dat> M 300 20 W 301 30 M 302 40 S 303 10 S 304 40 M 305 70 W 306 20 W 307 20 M 308 60 S 30$ 30 Категория Manager Manager Manager Manager Supervisor Supervisor Supervisor Worker Worker Worker Номер 300 302 308 305 309 303 304 306 307 301 Время 20 40 60 70 30 10 40 20 20 30 Время обслуживания по категориям Manager 190 Supervisor 80 Worker 70 */ 5.7. Практическое применение: управляемое событиями моделирование Задачей моделирования является создание модели реальной ситуации для лучшего ее понимания. Моделирование позволяет вводить различные условия и наблюдать их результаты. Например, имитатор полета побуждает летчика реагировать на неблагоприятные условия и измеряет скорость и соответствие реакции. При изучении рынка сбыта моделирование часто используется для измерения текущей деятельности или оценки расширения бизнеса. В этом разделе рассмотрены модели прихода и ухода клиентов банка по мере того, как они проходят через одну из п>2 очередей к кассиру. В заключении оценивается эффективность обслуживания вычислением среднего времени ожидания каждого клиента и времени занятости каждого кассира в процен-
тах. Используя моделирование, мы можем создать различные модели обслуживания и оценить их с точки зрения стоимости и эффективности. Такой подход выполняет управляемое событиями моделирование (event-driven simulation), которое определяет объекты для представления банковской деятельности. Мы используем вероятностные значения, описывающие различные Ьжидаемые частоты прихода клиентов и различное ожидаемое время обслуживания клиента кассиром. Моделирование позволяет использовать генератор случайных чисел для отражения прихода и ухода клиентов в течение рабочего дня в банке. Моделирование позволяет также изменять параметры и таким образом измерять относительное влияние на обслуживание, если мы изменяем поведение клиента или кассира. Например, предположим, что отделение маркетинга считает, что поощрительные подарки увеличат приход клиентов на 20%. Исследование с использованием моделирования повысит ожидаемые темпы прихода клиентов и эффективность работы кассира. В некоторый момент банку понадобятся дополнительные кассиры для поддержания приемлемого обслуживания, а дополнительный расход может свести к нулю прибыль от поощрительной кампании. Это моделирование снабжает банковского менеджера параметрами для оценки обслуживания клиентов. Если среднее время ожидания будет слишком долгим, менеджер может использовать еще одного кассира. Это моделирование можно повторять снова и снова, просто изменяя условия. Разработка приложения По мере выполнения приложение отслеживает приход и уход отдельных клиентов. Например, предположим, 50-й клиент приходит в банк в 2:30 (приход) для получения вида услуг, требующего 12 минут времени кассира. Данное моделирование подразумевает, что каждый кассир наглядно предоставляет график своей работы, так что новый клиент может определить, кто из кассиров его будет обслуживать и когда может начаться обслуживание. [Занят до 2:40| БАНК Свободен {Кассир 11 | Кассир 21 time(departure) = time(arrival) + servicetime time(departure) = 2:30 + 0:12 = 2:42
Событие Время Приход Уход Время прихода Время ухода Обслуживание Рассмотрим различные обстоятельства для 50-го клиента. Предположим, что два кассира заняты, и клиент должен занять место в очереди и ожидать обслуживания. Если Кассир 1 освобождается в 2:40, а Кассир 2 будет свободен в 2:33, новый клиент выбирает Кассира 2. После 3 минут ожидания и 12 минут совершения операции клиент уходит в 2:45, что близко ко времени, когда освобождается Кассир 2. time(departure) = time(arrival) + waittime + servicetime time(departure) » 2:30 + 0:03 +0:12 = 2:45 Приход Уход Событие Время Время прихода Время ухода ожидание обслуживание В нашем моделировании клиент выбирает кассира, который затем обновляет на окне вывеску следующего свободного времени для обслуживания. Занят до 2:40 БАНК |3анят до 2:33] [Занят до 2:40| БАНК ранят до 2:45| 1 Кассир 1| (Кассир 2| ) ( 1 Кассир Ц | Кассир 2] До прихода клиента После прихода клиента Ключевыми компонентами в этом моделировании обслуживания в банке являются события, включающие как приход, так и уход клиента, а реальное время рассматривается как последовательность случайных событий, которые отражают приход, обслуживание и уход клиента. Событие объявляется как класс 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 Поля данных объекта описывают всю соответствующую информацию, которая относится к потоку клиентов в системе. Методы класса обеспечивают доступ к полям данных, используемым для сбора информации об общей эффективности обслуживания. Спецификация класса Event ОБЪЯВЛЕНИЕ #include <iostream.h> ♦include random.h enum EventType {arrival, departure}; class Event { private: // данные-члены int time; // время события EventType etype; // тип события int customerlD; // номер клиента int tellerlD; // номер кассира int waittime; // время ожидания int servicetime; // время обслуживания public:
Event(void); Event(int t, EventType et, int en, int tn, int wt, int st); int GetTime(void); EventType GetEventType(void); int GetCustomerlD(void) const; int GetTellerlD(void) const; int GetWaitTime(void)const; int GetServiceTime(void)const; }; ОПИСАНИЕ Конструктор умолчания позволяет объявлять объект Event, чтобы позже можно было инициализировать (присваиванием) его данные-члены. Второй конструктор позволяет задать каждый параметр при объявлении события. Остальные Методы возвращают значения данных-членов. ПРИМЕР Время задается в минутах от начала выполнения моделирования. Event e; // объявление события конструктором умолчания // клиент 3 уходит в 120 минут. После 10 минут ожидания, //клиент затрачивает 5 минут на совершение операции. е = Event(120, departure, 3, 1, 10, 5); cout«e.GetService(); //выводится время обслуживания 5 Информация моделирования Во время выполнения моделирования мы накапливаем информацию о каждом кассире, указывающую общее количество клиентов, которых обслужил кассир, время, которое клиенты провели в ожидании и общее время обслуживания клиентов кассиром в течение дня. Вся эта информация накапливается в записи TellerStats, содержащей также поле finishServi.ee, представляющее значение вывески в окне. Запись TellerStats finishService totalCustomerCount totalCustomerWait totalService //Структура для информации о кассире struct TellerStats { int finishService; //когда кассир может обслуживать int totalCustomerCount; //общее количество обслуженных клиентов int totalCustomerWait; //общее время ожидания обслуживания клиентами int totalService; //общее время обслуживания клиентов }; При моделировании генерируется событие прихода и ухода для каждого клиента. Время всех событий отмечается, и они помещаются в очередь приоритетов. Событие с высшим приоритетом в очереди приоритетов — это событие с самой ранней отметкой времени. Структура списка позволяет удалять события, так чтобы мы перемещались в возрастающей временной последовательности с приходом и уходом клиентов. Моделирование обслуживания в банке использует генератор случайных чисел для определения следующего прихода клиента и времени текущего обслуживания клиента. Генератор обеспечивает то, что любой результат в
диапазоне значений одинаково возможен. Если при моделировании текущее событие прихода возникает в Т минут, следующий приход происходит произвольно в диапазоне от T+arrivalLow до T+arrivalHigh минут, а общее время обслуживания, необходимое для клиента, находится в диапазоне от serv- iceLow до serviceHigh минут. Например, предположим, что некоторый клиент приходит в 2 минуты и диапазон времени прихода следующего клиента составляет от arriveLow=6 до arriveHigh=14 минут. Существует вероятность 1/9 того, что будет наблюдаться любой из результатов б, 7, . . . , 14 минут. Текущий приход Следующий приход arrivalLow=4 arrivalLow=12 Если мы знаем что, последующее событие (приход другого клиента) будет в 9 минут, можно создать это событие и поместить его в очередь приоритетов для будущей обработки. Данные для моделирования и методы, реализующие эту задачу, содержатся в классе Simulation. Данные-члены включают продолжительность (length) моделирования (в минутах), количество кассиров, номер следующего клиента, массив записей TellerStats, содержащих информацию о каждом кассире, и очередь приоритетов, которая содержит список событий. Класс также содержит границы диапазонов для следующего прихода и для текущего обслуживания клиента. Спецификация класса Simulation ОБЪЯВЛЕНИЕ class Simulation { private: // данные для моделирования int simulationLength; // продолжительность моделирования int numTellers; // число кассиров int nextCustomer; // номер следующего клиента int arrivalLow, arrivalHigh; // диапазон прихода нового клиента int serviceLow, serviceHigh; // диапазон обслуживания TellerStats tstat[ll]; // максимальное число кассиров - 10 PQueue pq; // очередь приоритетов RandomNumber rnd; // использовать для времени прихода // обслуживания // закрытые методы, используемые функцией RunSimulation int NextArrivalTime(void); int GetServiceTime(void); int NextAvailableTeller(void); public: // конструктор Simulation(void); void RunSimulation(void); void PrintSimulationResults(void); }; ОПИСАНИЕ
Конструктор инициализирует массив типа TellerStats и член класса пех- tCustomer, который начинается с 1. Индекс 0 в массиве tstat не используется. Таким образом, номер кассира является и индексом в массиве tstat. Конструктор запрашивает пользователя ввести данные для задачи исследовательского моделирования. Данные включают продолжительность моделирования в минутах, количество кассиров и диапазон времени прихода и времени обслуживания. Класс предусматривает до 10 кассиров. Для каждого события прихода мы вызываем метод NextArrivalTime, чтобы определить, когда в банк придет следующий клиент. В то же время мы вызываем GetServiceTime, чтобы определить, как долго будет продолжаться текущее обслуживание клиента кассиром, и метод NextAvailableTeller для определения кассира, который будет обслуживать клиента. Метод RunSimulation выполняет задачу исследовательского моделирования, a PrintSimulationResults выводит окончательную статистику. Установка параметров моделирования Конструктор инициализирует данные и запрашивает у пользователя параметры моделирования. Simulation::Simulation(void) { int i; Event firstevent; // инициализация информационных параметров кассиров for(i - 1; i <« 10; i++) { tstat[i].finishService « 0; tstat[i].totalService - 0; tstat[ij.totalCustomerWait * 0; tstat[ij.totalCustomerCount - 0; ) nextCustomer - 1; cout « "Введите время моделирования в минутах: "; cin » simulationLength; cout « "Введите число кассиров банка; "; cin » numTellers; cout « "Введите диапазон времени приходов в минутах: "; cin » arrivalLow » arrivalHigh; cout << "Введите диапазон времени обслуживания в минутах: "; cin » serviceLow » serviceHigh; // генерить событие первого прихода клиента pq.PQInsert(Event(0,arrival,1,0,0,0)); ) Выполнение задачи моделирования Продолжительность моделирования (simulationLength) используется для определения, следует ли генерировать событие прихода для другого клиента. Если банк будет закрыт в планируемое время прихода нового клиента (arrival time>simulationLengtb), новые клиенты не принимаются, и моделирование завершается обслуживанием клиентов, остающихся в банке. Данное-член nextCustomer — это счетчик, ведущий запись количества клиентов.
Метод NextArrivalTime возвращает интервал времени до прихода следующего клиента. Метод использует генератор случайных чисел и параметры arrivalLow и arrivalHigh для генерирования возвращаемого значения. Сходный метод GetServiceTime использует параметры serviceLow и serviceffigh // определить случайное время следующего прихода int Simulation::NextArrivalTime(void) { return arrivalLow+rnd.Random(arrivalHigh-arrivalLow+l); > // определить случайное время обслуживания клиента int Simulation::GetServiceTime(void) { return serviceLow+rnd.Random(serv^ceHigh-serviceLow+l); } В этой задаче клиент приходит, смотрит на окно кассира и читает вывеску, указывающую, когда каждый кассир будет свободен. Исходя из этой информации, клиент выбирает кассира, который предоставит ему банковские услуги. Значение данных finishService в записи каждого кассира представляет вывеску, висящую в окне. Функция NextAvailableTeller просматривает массив кассиров и возвращает номер кассира с минимальным значением finishService. Если все кассиры будут заняты вплоть до времени закрытия банка, клиенту назначается произвольный кассир. // возвратить номер первого доступного кассира int Simulation: .'NextAvailableTeller (void) { // вначале предполагается, что все кассиры // освобождаются ко времени закрытия банка int minfinish = simulationLength; // назначить случайного кассира клиенту, пришедшему //до закрытия, но имеющему возможность получить обслуживание // после закрытия банка int minfinishindex - rnd.Random(nuraTellers) + 1; // найти кассира, который освобождается первым for (int i = 1; i <== numTellers; i++) if (tstat[i].finishService < minfinish) { minfinish = tstat[i].finishService; minfinishindex = i; } return minfinishindex; } Основная функция в задаче моделирования — это метод RunSimulation, который управляет очередью приоритетов событий. Первоначально очередь имеет единственное событие прихода, возникающее, когда банк открывается. С этого момента и далее процесс является непрекращающимся. Метод Run- Simulation является циклом, который извлекает события из очереди приоритетов. Такой цикл определяется как событийный (event loop), и он завершается при пустой очереди. Кассиры банка продолжают обслуживать клиентов после истечения времени работы банка (time>simulationLength) с учетом того, что клиенты пришли до закрытия банка. В заключительном резюме simulation- Length обновляется, чтобы предоставить время ухода последнего клиента, с учетом того, что этот уход наступает после времени закрытия банка. Если переменная е является объектом Event, содержащим последнее событие дня,
e.GetTime() возвращает время события, а продолжительность задачи моделирования вычисляется с использованием оператора: simulationLength = (e.GetTimeO <=simulationLength) ? simulationLength : e.GetTimeO; ПРИХОД 1. Объект вновь пришедшего клиента отвечает за генерирование следующего события прихода, которое затем помещается в очередь приоритетов для последующей обработки. Если следующий приход должен наступить после завершения задачи моделирования, это событие не учитывается. //вычисление времени следующего прихода, nexttime = e.GetTimeO + 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, то это — время, когда кассир освобождается для обслуживания другого клиента. Если значение finishService равно 0, то кассир свободен. В этом случае установим finishService на текущее время, чтобы отразить тот факт, что кассир будет теперь обслуживать клиента. Определим время, в течение которого клиент должен ожидать (waittime), вычитая текущее время из finishService. //если кассир свободен, заменить время на вывеске на текущее время if (tstat[tellerlD].finishService == 0) tstat [tellerlD] . finishService = e.GetTimeO; //вычислить время, когда клиент ожидает, вычитая //текущее время из времени на вывеске кассира waittime = tstat[tellerlD].finishService - e.GetTimeO; Этими значениями мы обновляем информацию TellerStats для кассира, который обслуживает клиента. Поле finishService увеличивается на время обслуживания, необходимое для текущего клиента. Оно теперь содержит время, когда кассир закончит обслуживание всех его (ее) текущих клиентов.
//обновлять статистику кассира tstat[tellerlD].totalCustomerWait +=waittime; tstat[tellerlD].totalCustomerCount++; tstat[tellerlD].totalService +=servicetime/ tstat[tellerlD].finishService +=servicetime; 3. Конечная задача включает определение события ухода и помещение его в очередь приоритетов. Мы имеем все необходимые параметры для создания события. Элемент данных события time etype customerlD tellerlD waittime servicetime Передаваемый параметр tstat[tellerlD].finishService departure e.GetCustomerlDO tellerlD waittime servicetime newevent = Event(tstat[tellerlD] . finishService, departure, e.GetCustomerlD(),tellerlD, waittime, servicetime); pq.PQInsert(newevent); УХОД Событие ухода дает нам доступ к истории деятельности клиента во время нахождения в банке. В задаче исследовательского моделирования эта информация может выводиться на экран. Для события ухода мы должны обновить поле finishService, если у кассира нет других клиентов. Это наступает, если текущее значение finishService равно времени ухода. В этом случае finishService устанавливается на 0. tellerlD = e.GetTellerlDO; //если никто не ждет кассира, отметить, что кассир свободен if (e.GetTimeO -- tstat[tellerlD] .finishService) tstat[tellerlD] .finishService = 0/ Резюме задачи моделирования. Для завершения моделирования вызываем функцию PrintSimulationResults. Она печатает резюме данных о клиентах и отдельных кассирах. Данные собираются из записей TellerStats, содержащих информацию о количестве клиентов, которых обслужил каждый кассир, и суммарное время ожидания клиентами обслуживания. for (i = 1; i<=numTellers; I++) { cumCustomers += tstat[i].totalCustomerCount; cumWait += tstat[i]. totalCustomerWait; } Окончательное резюме дает среднее время ожидания клиента и время занятости каждого кассира в процентах. cout « endl; cout « ******** Simulation Summary ******** « endl; cout « Simulation of « simulationLength << minutes << endl; cout « No. of Customers: « cumCustomers << endl; cout « Average Customer Wait:; avgCustWait = float(cumWait)/cumCustomers + 0.5; cout « avgCustWait « minutes « endl;
for(i*l;i<« numTellers;i++) { cout « n Teller # " « i « % Working: ; //отображать значение в процентах, округленное до ближайшего целого tellerWork * float(tstatfi].totalService)/simulationLength; tellerWorkPercent - tellerWork * 100.0 + 0.5; cout « tellerWorkPercent « endl; } Пример задачи моделирования. Main-программа определяет объект моделирования S и затем выполняет цикл event с использованием метода Run- Simulation. После завершения цикла event вызывается метод PrintSimula- tionResulte. Пример 5.6 Мы следим за задачей моделирования после определения этих параметров: simulationLength * 30 (минут) numTellers= 2 arriveLow » б arriveffigh = 10 serviceLow -» 18 serviceffigh * 20 Клиент 1 приходит в 0 минут Прмиод customerlD 1 tellerlD - waittime - servicetime - Клиент 1 генерирует событие прихода для клиента 2 в 7 минут и генерирует свое собственное событие ухода в 19 минут Приход customerlD 2 tellerlD - waittime - servicetime - Клиент 2 приходит в 7 минут. Уход customerlD 1 tellerlD 1 waittime 0 servicetime 19 19 Уход customerlD 1 tellerlD 1 waittime 0 servicetime 19 19
Клиент 2 генерирует событие прихода для клиента 3 в 16 минут и генерирует свое событие ухода в 25 минут. Приход Уход Уход customerlD 3 tellerlD - waittime - servicetime - customerlD 1 tellerlD 1 waittime 0 servicetime 19 customerlD 2 tellerlD 2 waittime 0 servicetime 18 16 19 25 Клиент З приходит в 16 минут. Клиент 3 генерирует событие прихода, которое наступает после закрытия банка, что прекращает создание событий прихода. Клиент 3 должен ожидать 3 минуты кассира 1 и генерирует свое событие ухода в 37 минут. Уход Уход customerlD 1 tellerlD 1 waittime 0 servicetime 19 customerlD 2 tellerlD 2 waittime 0 servicetime 18 19 25 События ухода удаляются из очереди приоритетов в таком порядке: клиент 1 — в 19 минут, клиент 2 — в 25 минут и клиент 3 — в 37 минут. Уход customerlD 1 tellerlD 1 waittime 0 servicetime 19 Уход customerlD 2 tellerlD 2 waittime 0 servicetime 18 Уход customerlD 3 tellerlD 1 waittime 3 servicetime 18 19 25 37 Банк закрывается после 37 минут обслуживания. Программа 5.7 Код и выполнение Эта программа запускается дважды. Данные первого запуска соответствуют данным в примере 5.6. Во втором запуске используется продолжительность задачи 480 минут (8 часов). Метод вывода PrintSimulation- Results указывает количество постоянных клиентов (patrons), среднее время ожидания для каждого клиента и общее время занятости каждого кассира. Реализация классов Event, TellerStats и Simulation содержится в файле sim.h.
#include "sim.h" void main(void) { // S - объект для моделирования Simulation S; // запустить моделирование S.RunSimulation(); // печатать результаты 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: б 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 18 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 Подчеркните правильное. СЩК — это структура, реализующая порядок: (a) first-in/last-out (b) last-in/first-out (с) first-come/first-serve (d) first-in/first-out (e) last-in/last-out
5.2 Напишите два программных приложения для стеков. 5.3 Какой выход имеет следующая последовательность операций стека? (DataType = int): Stack S int x = 5, у = 3; S.Push (8); S.Push(9); S.Push(y); x = S.Pop{); S.Push(18); x = S.Pop(); S.Push(22); while (! S.StackEmpty()) { у = S.Pop(); cout « у « endl; } cout « x <<endl; 5.4 Напишите функцию void StackClear(Stacks S) ; которая очищает стек S. Почему важно, чтобы объект S передавался по ссылке? 5.5 Что выполняет следующая функция? (DataType = int): void Ques5(Stacks S) { int ar'r[64], n = 0, I; int elt; while (!S.StackEmpty()) a[n++] = S.PopO ; for(i=0;i< n;i++) S.Push(a{i] ; } 5.6 Что выполняет следующий сегмент кода? Stack SI, S2, tmp; DataType x; while (!Sl.StackEmpty()) { x = Sl.PopO ; tmp. Push (x) ; } while (!tmp.StackEmpty()) {. x = tmp.Pop(); Sl.Push(x); S2.Push(x); } 5.7 Напишите функцию int StackSize(Stack S); которая использует операции стека, чтобы возвращать количество элементов в стеке S.
5.8 Что выполняет следующая функция Ques8? (DataType = int): void Ques8(Stacks S, int n) { Stack Q/ int I; while(!S.StackEmpty()) { i = S.Pop(); if (I !=n) Q.Push(i); } while(!Q.StackEmpty()) { i * Q.PopO ; S.Push(i); ) } 5.9 Если DataType является int, напишите функцию void SelectItem(Stack& S, int n) ; которая использует операции стека для нахождения первого появления элемента в стеке S и перемещает его в вершину стека. Поддерживайте упорядочение для других элементов. 5.10 Преобразуйте следующие инфиксные выражения в постфиксные: (а) а 4- Ь*с (б) (a+)/(d-e) (в) (Ь2 - 4*а*с)/(2*а) 5.11 Напишите следующие выражения в инфиксной форме: (а) a b + с* (б) a b с + * (в) a b с d e ++**e 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 Какой выход имеет следующая последовательность операций очереди? (DataType = int):
Queue Q; DataType x - 5, у * 3; Q.QInsert (8); Q.QlnsertO); Q.QInsert(y); x e Q.QDeleteO ; Q.QInsert(18); x * Q.QDeleteO ; Q.QInsert(22); while <!Q.QEmptyO) { у - Q.QDeleteO ; cout « у « endl; } cout « x « endl; 5.15 Что выполняет следующая функция? (DataType = int): void QueslS(Queues Q, int n - 50) { Stack S; int elt; while (IQ.QEmptyO) { elt - Q.QDeleteO ; S.Push<elt); ) while (!S.StackEmpty()) { elt - S.PopO; Q.QInsert(elt); ) ) Почему важно, чтобы объект Q передавался по ссылке? 5.16 Что выполняет следующий сегмент кода? (DataType « int) Queue Ql, Q2; int n « 0, x; • * * while (JQl.QEmptyO) { x «Ql.QDeleteO ; Q2.QInsert (x); n++; } for (int i-0;i <n;i++) { x » Q2.QDelete О; Ql.QInsert(x); Q2.QInsert(x); ) 5.17 Предположим, что список очереди приоритетов содержит целые значения с оператором "меньше, чем" (<), определяющим очередь приоритетов. Текущий список содержит элементы. Записывая методы 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 Стек может использоваться для распознавания определенных типов образов. Рассмотрим образец STRING1#STRING2, где никакая строка не содержит "#" и STRING2 должна быть обратна STRING 1. Например, строка "123&~a#a~&321" совпадает с образцом, но строка "a2qd#dq3a" — нет. Напишите программу, которая считывает пять строк и указывает, совпадает ли каждая строка с образцом. 5.4 Во второй модели стека стек растет в направлении уменьшения индексов массива. Первоначально стек пуст и Тор=21. После помещения трех символов в стек индекс Тор — 18, и элемент в вершине стека — это LIST[TOP] = С. top top Убывающий индекс В этой модели индекс Тор уменьшается с каждой операцией Push. Напишите реализацию для класса Stack, используя эту модель. Протестируйте свою работу, запуская программу 5.1. 5.5 Массив может использоваться для сохранения двух стеков, один, растущий с левого конца, второй, уменьшающийся с правого конца. top. top2 Возрастание стека Возрастание стека
(а) Каково условие для того, чтобы Si был пуст? Когда S2 пуст? (б) Каково условие для того, чтобы Si был полным? Когда S2 полный? (в) Реализуйте класс DualStack, объявление которого задается с помощью const int MaxDuaiStackSize = 100 class DualStack { private: int topi, top2; DataType stackStorage[MaxDuaiStackSize]; public: DualStack(void); //помещаем elt в стек п void Push(DataType elt, int n); //извлекаем из стека п DataType Pop(int n); //выборка в стеке п DataType Peek(int n); //стек п пуст? int StackEmpty(int n ); //стек п полный? int StackFull(int 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 operandi, doubles operand2); void Enter(double num); void Compute(char op); public:
Calculator(void); void Run(void); void Clear(void); void Variable(void); //NEW double Eval(double x); //NEW }; Операция Variable позволяет пользователю вводить постфиксное выражение, содержащее переменную х, равно как числа и операторы. Например, введите выражение хх*Зх* + 5 + когда вам необходимо оценить инфиксное выражение х2 + Зх + 5. Когда каждый компонент считан, выполните ввод в массив expComponent. Поле типа каждого элемента ввода устанавливается так: 1= число, 2 — переменная х, 3 = +, 4 = -, 5 = *, 6 = /. Если тип = 1, то число сохраняется в поле значения. Увеличивайте expsize на единицу для каждого нового элемента ввода. Функция-член Eval выполняется циклично для членов expsize массива expComponent и использует стек для оценки выражения. Всякий раз, когда какой-либо элемент имеет поле типа 2, помещайте параметр х в стек. Напишите main-программу, оценивающую выражение (х2 + 1)/ (х4 + 8х2 + 5х + 3) для х =0, 101, 102, 103, . . . , 1(R 5.9 Измените задачу моделирования в программе 5.7, чтобы она включала следующую информацию: (а) Выводить среднее время, которое клиент проводит в банке. Время клиента измеряется от прихода до ухода. (б) В настоящее время все кассиры остаются в банке до тех пор, пока не уходит последний клиент. Это обходится дорого, если кассиры задерживаются после закрытия банка для обслуживания пришедшего поздно клиента. Дайте возможность кассиру уйти, если банк закрывается и нет ожидающих обслуживания клиентов. Совет: добавьте поле к записи TellerStat, которое указывает общее время, которое кассир проводит в банке. Используйте эту переменную для вычисления времени в процентах, в течение которого кассир занят. 5.10 Предположим, в банке создается отдельная очередь к каждому кассиру. Когда клиент приходит, он (или она) выбирает самую короткую очередь, а не оценивает загруженность кассиров. Измените программу моделирования 5.7 для указания среднего времени ожидания клиента и объема работы, выполненной каждым кассиром. Сравните результаты с моделью банковского обслуживания с единственной очередью к кассирам.
глава Абстрактные операторы 6.1. Описание перегрузки операторов 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 = P.AddMat (Q); S = R.MultMat (Q.AddMat(P)); Перегруженные операторы R = Р + Q S = R * (Q + Р) Реляционные выражения определяют порядок между элементами. Мы говорим, что целое 3 является положительным или что 10 меньше, чем 15. 3 >=0 10 < 15 Концепция порядка применима к структурам данных, отличным от чисел. В классе Data в главе 3, например, две даты (объекты) могут сравниваться по хронологии года. Date(б, б, 44) < (Date(12, 25,80) //День-Д наступил раньше рождества 1980 Date(4, 1, 99) == Date {"4/1/99") //Два способа создать дату 1 апреля Date(7, 1, 94) < Date("8/1/94") //июль наступает перед августом C++ позволяет выполнять перегрузку большинства присущих ему операторов, включая операторы потока. Программист не может создавать новые символы оператора, а должен перегружать существующие операторы C++. Метод PrintDate в классе Date принимает объект D и выводит его поля в стандартном формате. Тот же метод может быть переписан как оператор "«" и использоваться в потоке cout. Например, Date-объект D(12,31,99) определяет последний день двадцатого века. Чтобы создать выходную строку Последний день 20-го века — 31 декабря 1999 мы можем использовать метод print из главы 3 или перегрузить оператор вывода. PrintDate Method: cout <<"Последний день 20-го века -" D.PrintDate(); Overloaded "«" Method: со1^<<"Последний день 20-го века -"<< D; Перегрузка операторов — это тема, которая описывается в нескольких главах. В данной главе мы разрабатываем класс рациональных чисел для иллюстрации ключевых понятий. Вы, конечно, знакомы с "дробями" и имеете школьный опыт работы с операциями. Класс Rational дает хороший пример арифметических и операций отношения, преобразования типов между целыми значениями и действительными числами и простого определения для перегруженного потока ввода/вывода.
6.1. Описание перегрузки операторов Термин перегрузка операторов (operator overloading) подразумевает, что операторы языка, такие как +,!=,[] и = могут быть переопределены для типа класса. Этот тип класса дает возможность реализации операций с абстрактными типами данных с использованием знакомых символов операций. C++ предоставляет различные методы определения перегрузки операторов, включающие определяемые пользователем внешние функции, члены класса и дружественные функции. C++ требует, чтобы по крайней мере один аргумент перегруженного оператора был объектом или ссылкой на объект. Определяемые пользователем внешние функции В классе SeqList методы Find и Delete требуют определения оператора отношения "==" для DataType. Если оператор не присущ этому типу, пользователь должен явно задать перегруженную версию. Объявление использует идентификатор "operator" и символ "==". Так как результатом операции является True или False, возвращаемое значение будет типа int: int operator==(const DataTypei a, const Datatypes b); Например, предположим, список содержит записи Employee, которые состоят из поля ID и другой информации, включающей имя, адрес и так далее. struct Employee { int ID; • • • } ID Имя Адрес ■ ■ ■ Операция отношения "равно" сравнивает поля Ю: int operator == (const Employees a, const Employees b) { return a.ID ==b.ID; //сравнение ID-полей } Перегруженный оператор должен иметь аргумент типа класс. Заметьте, что следующая попытка перегрузить "==" для строк C++ будет неудачной, потому что никакой аргумент не является объектом или ссылкой на объект. int operator==(char *s, char *t)//неверный оператор { return strcmp(s, t)== 0; } Когда оператор отношения "равно" будет объявлен, пользователь может использовать функции Find или Delete класса SeqList: typedef Employee DataType; //тип данных Employee #include "aseqlist.h" ■ * • SeqList L;
Employee emp; //объявляем объект SeqList. • л т emp.ID=1000; //ищем служащего с ID=1000 if (L.Find(emp)) L.Delete(emp); //если найден, удаляем этого служащего Чтобы использовать класс SeqList, пользователь должен иметь технические знания для определения оператора "==" как внешней функции. Это является дополнительным, но обязательным требованием, поскольку оператор "==" присоединяется к типу данных, а не к классу. Члены класса Арифметические операторы и операторы отношения происходят из систем счисления. Они позволяют программисту объединять операнды в выражениях, используя инфиксную запись. Класс может иметь методы, объединяющие объекты сходным образом. В большинстве случаев методы могут записываться как перегруженные функции-члены с использованием стандартных операторов С4--К Когда левый операнд является объектом, оператор может выполняться как метод, который определяется для этого объекта. Например, двумерные векторы имеют операторы, включающие сложение, отрицание и скалярное произведение векторов. Сложение векторов Сложим два вектора u=(ui,U2) и v=(vi,V2), образуя вектор, компоненты которого являются суммой компонентов каждого вектора (см. часть (а) рис. 6.1). u+v = (ux+v^i^+va) Отрицание векторов Образуем отрицание векторов u=(ui,U2), беря отрицательное значение каждого компонента. Новый вектор имеет ту же величину, но противоположное направление (см. часть (Ь) рис. 6.1). -(ui,u2)=(-iii, -u2) Скалярное произведение векторов Вектор u=(ui,U2), умноженный на вектор u=(vi,V2), является числом, полученным сложением произведений соответствующих компонентов (см. часть (с) рис. 6.1). Скалярное произведение векторов является произведением модулей векторов (magnitude) на косинус угла между ними. v*w=u1v1+u2v2=magnitude(v)*magnitude(w)*cos(q) (VI, V2) (U1, U2) u + v Ul * V1 + U2 * V2 (а) Сложение (б) Вычитание (с) Произведение Рис. 6.1. Векторные операции
Мы объявляем класс векторов Vec2d с координатами х и у как закрытыми данными-членами. Этот класс имеет конструктор, два бинарных оператора (сложение, скалярное произведение) и унарный оператор (отрицание) в качестве функций-членов, а также дружественные функции, определяющие умножение вектора на скаляр и потоковый вывод. Использование дружественных функций описывается в следующем разделе. class Vec2d { private: double xl,x2 //компоненты public: //конструктор со значениями по умолчанию Vec2d(double h=0.G, double v=0.0); //функции-члены Vec2d operator-(void); //вычитание Vec2d operatori-(const Vec2d& V); //сложение double operator*(const Vec2d& V); //скалярное произведение //дружественные функции friend Vec2d operator*(const double c,const Vec2d& V); friend ostreams operator<<(ostream& os, const Vec2d& U); >; Как в функциях-членах в операторах сложения, отрицания и скалярного произведения предполагается, что текущий объект является левым операндом в выражении. Предположим, U и V- это векторы. Сложение. Выражение U + V использует бинарный оператор "+", связанный с объектом U. Метод operator4- вызывается с параметром V. U.operator+(V) //возвращает значение Vec2d(x+V.x, у + V.y) Отрицание. Выражение -U использует унарный оператор "-". Метод operator- выполняется для объекта U. U.operator- () //возвращает значение Vec2d(-x, -у) Скалярное произведение. Подобно сложению, выражение U*V использует бинарный оператор *'*", связанный с объектом U. Произведение вычисляется методом текущего объекта U и параметра V. U.operator*(V) //возвращает значение х * V.x +y * V.y Пример с объектами: Vec2d U(l, 2), V(2, 3); U + V = (1, 2) + (2, 3) « (3, 5) -U - -(1, 2) = (-1, -2) U *V = (1, 2) * (2, 3) = 8 Перегрузка операторов требует соблюдения определенных условий. 1. Операторы должны соблюдать приоритет операций, ассоциативность и количество операндов, диктуемое встроенным определением этого оператора. Например, "*" — это бинарный оператор и должен всегда, когда он перегружается, иметь два параметра. 2. Все операторы в C++ могут быть перегружены, за исключением следующих: , (оператор "запятая") sizeof :: (оператор области действия) ?: (условное выражение)
3. Перегруженные операторы не могут иметь аргументов по умолчанию; другими словами, все операнды для оператора должны присутствовать. Например, следующее объявление оператора является неправильным: double Vec2d::operator* (vec2d V = Vec2d(l,l)); 4. Когда оператор перегружается как функция-член, объект, связанный с этим оператором, всегда является крайним слева операндом. 5. Как функции-члены унарные операторы не принимают никаких аргументов, а бинарные операторы принимают один аргумент. Дружественные функции Обычно объектно-ориентированное программирование требует, чтобы только функции-члены имели доступ к закрытым данным какого-либо класса. Это обеспечивает инкапсуляцию и скрытие информации. Этот принцип следует расширить до перегрузки оператора везде, где возможно. В некоторых ситуациях, однако, использование перегрузки с функцией-членом невозможно или слишком неудобно. Поэтому необходимо использовать дружественные функции (friend functions), которые определяются вне класса, но имеют доступ к закрытым данным-членам. Например, умножение вектора на скаляр является еще одной формой умножения векторов, где каждый компонент вектора умножается на числовое значение. Число (скаляр) с, умноженное на вектор u=(ui,ui), является вектором, компоненты которого образуются умножением каждого компонента и на с: c*u=(cu1,cu2) Это естественная операция перегрузки с использованием оператора '*'. Однако, перегрузка оператора с использованием функции-члена не позволяет помещать скалярное значение С как левый операнд. Левым операндом должен быть вектор. Мы определяем, что '*' является оператором вне класса, имеющим параметры С и U, и имеет доступ к закрытым членам х и у параметра U. Это выполняется определением оператора как дружественного внутри объявления класса. Дружественная функция объявляется помещением ключевого слова friend перед объявлением функции в классе. friend Vec2d operator * (const double с, const Vec2d& U); Так как дружественная функция является внешней для класса, она не находится в области действия класса и реализуется как стандартная функция. Заметьте, что ключевое слово friend не повторяется в объявлении функции. Vec2d operator* (double с, Vec2d U) { return Vec2d(c*U.x, c*U.y); }
При использовании умножения вектора на скаляр оператору '*' передаются оба аргумента, и скаляр должен быть левым параметром. Vec2d 7(8,5), Z; Z = 3.0 * Y; //результатом является (24,15) Использование дружественных функций и права доступа к ним описываются в следующем перечне правил 1. Объявление дружественной функции выполняется помещением объявления функции внутрь класса, которому предшествует ключевое слово friend. Фактическое определение дружественной функции дается вне блока класса. Дружественная функция определяется подобно любой обычной функции C++ и не является членом класса. 2. Ключевое слово friend используется только в объявлении функции в классе, а не с определением функции. 3. Дружественная функция имеет доступ к закрытым членам класса. 4. Дружественная функция получает доступ к членам класса только при передаче ей объектов как параметров и использовании записи с точкой для ссылки на члена. 5. Для перегрузки унарного оператора как друга в качестве параметра передается операнд. При перегрузке бинарного оператора как друга в качестве параметров передаются оба операнда. Например: friend Vec2d operator-(Vec2d X); //унарный "минус" friend Vec2d operator+(Vec2d X, Vec2d Y); //бинарное сложение 6.2. Система рациональных чисел Рациональные числа — это множество частных P/Q, где Р и Q — это целые, a Q * 0. Число Р называется числителем или нумератором (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), в которой числитель и знаменатель не имеют общего делителя. Рациональное число в редуцированной форме является наиболее репрезентативным значением из коллекции эквивалентных чисел. Например, 2/3 — это редуцированная форма в коллекции 2/3, 10/15, 50/75 и так далее. Чтобы создать редуцированную форму какого-либо числа, числитель и знаменатель нужно разделить на их наибольший общий делитель (GCD, greatest common denominator). Например: 10/15 = 2/3 (GCD(10,15) = 5; 10/5 = 2 15/5 = 3) 24/21 = 8/7 (GCD (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 — это нормализованная форма Арифметика рациональных чисел Читатель знаком со сложением, вычитанием и сравнением дробей с использованием общих деноминаторов и правил умножения и деления этих чисел. Далее приводятся несколько примеров, которые предлагают формальные алгоритмы для реализации операций в классе рациональных чисел. Сложение/Вычитание. Сложим (вычтем) два рациональных числа, создав эквивалентную дробь с общим знаменателем. Затем сложим (вычтем) числители. I 5 8*1 3*5 8*1+3*5 23 (+) 3*8~3*8 + 3*8~ 3*8 "24 I _ А - 1 * 8 _ 3*5 _ 1*8-3*5 _ -7 ^ 3 8"з*8 3 * 8 ~ 3*8 ~ 24 Умножение/Деление. Умножим два рациональных числа, умножая числители и знаменатели. Выполним деление, инвертируя второй операнд (меняя местами числитель и знаменатель) и перемножая члены дробей. W 3*8 3*8 24 (/) 1/5 = 1,8=_8_ 1 } 3 8 3 5 15 Сравнение. Все операторы отношения используют один и тот же принцип: Создаем эквивалентные дроби и сравниваем числители: /ч15 1*85*3 (<) 77 < ■=■ эквивалентно отношению ——г- < 38 3*88*3 Данное отношение ВЕРНО (TRUE), так как 1*8 < 3*5 (8<15) Пример 6.1 Этот пример иллюстрирует операции с рациональными числами. Предположим, U = 4/5, V = -3/8 и W = 20/25. l' U + V 5 + 8 40 Ч 40 40 v ) \ ) и „4 * ( 3) ~12 V 5 8 40 QO 1 К U > V, так как ^ > - 4£ (32 > -15) 40 40 0 тт ,„ 4 100 20 100 2. U = = W, таккак 5=-^ ^ = 125
Преобразование рациональных чисел Множество рациональных чисел включает целые как подмножество. В представлении целого как рационального числа используется деноминатор 1. Следовательно, (целое) 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; // закрытый коструктор, используемый // арифметическими операторами Rational(long num, long denom); // функции-утилиты void Standardize(void); long gcd(long m, long n) const; public: // конструкторы преобразуют: // int->Rational, double->Rational Rational(int num=0, int denom=l); Rational(double x); // ввод/вывод friend istream& operator» (istreamft istr,
Rational &r); friend ©streams operator« (ostream& ostr, const Rationale d); // бинарные операторы: // сложить, вычесть, умножить, разделить Rational operator+ (Rational r) 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 operator<= (Rational r) const; int operator== (Rational r) const; int operator!= (Rational r) const; int operator> (Rational r) const; int operators (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) числа в рациональное (Rational) число. Они описываются в разделе, посвященном операторам преобразования типа. Для приложений мы используем методы-утилиты GetNumerator и GetDenominator для доступа к данным-членам рационального числа. Оператор ввода « является дружественной функцией, которая считывает рациональное число в форме P/Q. При выводе это число имеет форму P/Q. Попытка ввести рациональное число с деноминатором 0 вызывает ошибку и завершение программы. пример Rational A(-4,5), В, С; // А - это -4/5, В и С - это 0/1 cin » С; // при вводе -12/-18 // Standardize сохраняет 12/18 cout « С.GetNumerator(); // печать числителя 12
С.Reduce О; // привести С к форме 2/3 В = С + А; //В - это 2/3 + (-4/5) = -2/15 cout « -В; // вызвать оператор минус; вывести 2/15 cout « float(A); // конвертировать в действительное число; // вывести -.8 cout « .5 + Rational(3); // преобразовать .5 в 1/2; // вывести сумму: 1/2 + 3/1 = 7/2 6.4. Операторы класса Rational как функции-члены Класс Rational объявляет арифметические операторы и операторы отношения как перегруженные функции-члены. Каждый бинарный оператор имеет параметр, который является правым операндом. Предположим, u, v и w — это объекты типа Rational в выражении: w = u + 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 + b * с (+) ы + у=_+_=__+_=___ Для реализации а и b — это данные-члены num и den левого операнда, который является объектом, связанным с оператором "+". Числа end являются данными-членами v.num и v.den правого операнда: // Rational-суммирование Rational Rational::operator+ (Rational r) const { return Rational(num*r.den + den*r.num, den*r.den); } Деление ("/") выполняется инвертированием правого операнда и затем перемножением членов. Числа а и b соответствуют данным-членам левого операнда. Числа end относятся к данным-членам в объекте v, правом
операнде. Так как результат может иметь отрицательный деноминатор, частное нормализуется. ( } и~Ъ d~Ъ* с"ъ*с II Rational-деление Rational Rational::operator/ (Rational r) const < Rational temp = Rational(num*r.den, den*r.num); // убедиться, что деноминатор - положительный temp.Standardize(); return temp; } Для оценки оператора отношения сравним нумераторы после приведения рациональных чисел к общему деноминатору. Тогда для оператора отношения "равно" ("==") возвращаемым значением является True, когда нумераторы равны. Далее следуют операторы логического эквивалента (<^>). ^^а с а * d b * с <=> = = b * d b * d Протестируйте условие a * d === b * c: II отношение равно int Rational::operator== (Rational r) const { return num*r.den *- den*r.num; } Унарный оператор минус "-" работает с данными-членами единственного операнда, определяющего эту операцию. Вычисление просто делает отрицательным нумератор: // минус для Rational Rational Rational::operator- (void) const { return Rational(-num, den); } 6.5. Операторы потока класса Rational как дружественные функции Файл <iostream.h> содержит объявления для двух классов с именами ostream и istream, которые обеспечивают потоковый вывод и потоковый ввод, соответственно. Потоковая система ввода/вывода предоставляет определения для потоковых операторов ввода/вывода "»" и "«" в случае элементарных типов char, int, short, long, float, double и char*. Например: Ввод Вывод istream & operator»(short v) ; ostream & operator«(short v); istream & operator»(double v) ; ostream & operator«(double v);
Потоковые операторы можно перегружать для реализации ввода/вывода определяемого пользователем типа. Например, с классом Date операторы "«" и "»" могут обеспечивать потоковый ввод/вывод тем же способом, который использовался для простых типов. Например: Date D; cin»D //<ввод>10/5/75 cout«D //<вывод> October 5, 1975 Если бы операторы для класса Date должны были быть перегруженными как функции-члены, их было бы необходимо объявлять явно в <iostream.h>. Классу ostream пришлось бы иметь перегруженную версию "«", которая принимает параметр Date. Это явно непрактично, и поэтому мы используем дружественную перегрузку, которая определяет этот оператор вне класса, но позволяет ему иметь доступ к закрытым данным-членам в классе. Перегрузка потокового оператора использует структурированный формат, который далее показан для общего типа класса CL. class CL { ■ • • public: • • • friend istreamfi operator>>(istream& istr, CL& Variable); friend ostream& operator<<(ostream& ostr, const CL& Value); } Параметр istr представляет поток ввода, такой как cin, и ostr представляет поток вывода, такой как cout. Так как процесс ввода/вывода изменяет состояние потока, параметр должен передаваться по ссылке. Для ввода, элементу данных Variable присваивается значение из потока, поэтому оно передается по ссылке. Функция возвращает ссылку на istream, чтобы оператор мог использоваться в такой последовательности как cin » m » n; В случае вывода Value копируется в поток вывода. Так как данные не изменяются, Value передается как константная ссылка. Это позволяет избежать копирования возможно большого объекта по значению. Функция возвращает ссылку на ostream (output), чтобы оператор мог использоваться в такой последовательности как cout « m « n; Реализация операторов потока класса Rational Ввод и вывод рациональных чисел реализуется перегрузкой потоковых операторов. С вводом мы считываем число в форме P/Q, где Q*0. Программа завершается, если введенный деноминатор равен 0. // перегрузка оператора ввода потока, ввод в форме P/Q istreamfi operator >> (istreamb istr, Rationale r) { char с; // для чтения разделителя ' /' // как друг оператор ">>" имеет доступ
// к номинатору и деноминатору объекта г istr >> r.nura » с >> r.den; // проверка деноминатора на равенство нулю if (r.den === 0) I cerr « "Нулевой деноминатор!\n"; exit(1); ) // приведение объекта г к стандартной форме г.Standardize(); return istr; } Перегруженный потоковый оператор вывода записывает рациональное число в форме P/Q. // перегруженный оператор вывода потока, форма: P/Q ostream& operator « (ostream& ostr, const Rationalb r) { // как друг оператор "»" имеет доступ // к номинатору и деноминатору объекта г ostr << r.num << '/' << r.den; return ostr; } 6.6. Преобразование рациональных чисел Класс Rational иллюстрирует операторы преобразования типа. Программист может реализовать пользовательские операторы преобразования, аналогичные операторам, предусмотренным для стандартных типов. Конкретнее, мы сосредотачиваем внимание на преобразовании между объектами типа класс и соответствующего типа данными C++. Преобразование в объектный тип Конструктор класса может использоваться для построения объекта. Как таковой, конструктор принимает его параметры ввода и преобразует их в объект. Класс Rational имеет два конструктора, которые служат в качестве операторов преобразования типа. Первый конструктор преобразует целое в рациональное число, а второй преобразует число с плавающей точкой в рациональное число. При вызове конструктора с единственным целым параметром num он преобразует целое в эквивалентное рациональное num/1. Например, рассмотрим эти объявления и операторы: Rational P(7), 0(3,5), R, S; // явное преобразование 7 в 7/1 R - Rational(2); // явное преобразование 2 в 2/1 S = 5; // построить Rational(5) //и присвоить новый объект переменной S Объявления Q и R создают объекты Q=3/5 и R=0/1. Присваивание R = Rational(2) явно изменяет значение R на 2/1. Присваивание S = 5 приводит
к преобразованию типа. Компилятор принимает S = 5 как оператор S = Rational(5). // конструктор, рациональное число в форме num/den Rational::Rational(int p, int q): num(p), den(q) { if (den == 0) { cerr « "Нулевой деноминатор!" « endl; exit(1); } > Второй конструктор преобразует действительное число в рациональное. Например, следующие операторы создают рациональные числа А=3/2 и В=16/5: Rational А = Rational(1.5), В; // явное преобразование В = 3.2; // преобразовать 3.2 в Rational(1 б,5) Для преобразования необходим алгоритм, который аппроксимирует произвольное число с плавающей точкой в эквивалентное рациональное. Алгоритм включает сдвиг десятичной точки в числе с плавающей точкой. В зависимости от количества значащих цифр в действительном числе результат может быть только приближенным. После создания рационального числа вызываем функцию Reduce, сохраняющую рациональное число в более читабельной редуцированной форме. // конструктор, преобразует х типа double // к типу Rational Rational::Rational(double x) { double vail, val2; vail = 100000000L*x; val2 = 10000000L*x; num = long(vall-val2); den = 90000000L; Reduce(); } Преобразование из объектного типа Язык C++ определяет явные преобразования между простыми типами. Например, чтобы напечатать символ с в коде ASCII, мы можем использовать код: cout << int(с) << endl; // преобразовать char в int Между типами также определяются неявные преобразования. Например, если I — это целое long, a Y — это double, то оператор Y = I; //у = double (I) преобразует I в double и присваивает результат переменной Y. Класс может содержать одну или более функций-членов, которые преобразуют объект в значение другого типа данных. Для класса CL предположим, что нам необходимо преобразовать объект в тип с именем NewType. Оператор NewTypeQ принимает объект и возвращает значение типа NewType. Искомый тип New- Type часто является стандартным типом, таким как int или float. Являясь
унарным, оператор преобразования не содержит параметр. Так же, оператор преобразования не имеет возвращаемого типа, потому что он находится неявно в имени NewType. Объявление принимает форму: class CL { * • ■ • operator NewType(void); >; Оператор преобразования используется следующим образом: NewType a; CL obj; а - NewType(obj); //явное преобразование а » obj; //неявное преобразование Класс Rational содержит оператор, преобразующий объект в double. Этот оператор позволяет выполнять присваивание рациональных данных переменным с плавающей точкой. Преобразователь принимает рациональное число рЛь делит нумератор на деноминатор и возвращает результат как число с плавающей точкой. Например: 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. Использование рациональных чисел Перед тем, как разрабатывать приложения для рациональных чисел, опишем алгоритм для генерирования редуцированной формы дроби. Этот алгоритм включает нахождение наибольшего общего делителя (gcd) нумератора и деноминатора. Для создания редуцированной формы рационального числа метод Reduce использует закрытую функцию-член gcd, которая принимает два положительных целых параметра и возвращает наибольший из их общих делителей. Функция gcd реализуется в программном приложении. Для ненулевого рационального числа метод Reduce делит нумератор и деноминатор на их gcd:
void Rational::Reduce(void) { long bigdivisor, tempnumerator; // tempnumerator — модуль от num tempnumerator = (num < 0) ? -num : num; if (num == 0) den *= 1; // приведение к 0/1 else { // найти GCD положительных чисел: // tempnumerator и den bigdivisor = gcd(tempnumerator, den), if (bigdivisor > 1) { num /= bigdivisor; den /= bigdivisor; ) } ) Программа 6.1. Использование класса Rational Эта программа иллюстрирует основные возможности класса Rational. Здесь показаны различные методы преобразования, включая преобразование целого числа в рациональное и рационального числа в действительное. Демонстрируется также сложение, вычитание, умножение и деление рациональных чисел. Программа заканчивается неявным преобразованием числа с плавающей точкой в рациональное и наоборот — в число с плавающей точкой. Реализация класса Rational содержится в файле rational.h. ♦include <iostream.h> #pragma hdrstop tinclude "rational.h" // каждая операция сопровождается выводом void main(void) { Rational rl(5), r2, r3; float f; cout << "1. Rational-значение 5 is " << rl « endl; cout « "2. Введите рациональное число: "; cin >> rl; f = float(rl); cout << " Эквивалент с плавающей запятой: " « f « endl; cout « "3. Введите два рациональных числа: "; cin » rl » r2; cout « " Результаты: " « (rl+r2) « " ( + ) " « (rl-r2) « " (-) " « (rl*r2) « и (*) п « (rl/r2) « " (/) " « endl; if (rl < r2) cout « " Отношение (меньше чем) : " « rl « " < " « r2 « endl;
else if (rl == r2) cout « " Отношение (равно): " « rl « " == " << r2 « endl; else cout « " Отношение (больше чем) : " << rl « " > " « r2 « endl; 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 -10/4 200/4 Смешанное число 2 1/2 -2 1/2 50 В алгебре основной задачей является решение общего уравнения дроби 2/ЗХ + 2 = 4/5 Процесс включает изоляцию члена X перестановкой 2 в правую часть уравнения: 2/ЗХ = -6/5 Решение получают, разделив обе части уравнения на рациональное число 2/3, коэффициент X. Реализуем этот процесс в функции SolveEquation. х = -6/5 * 3/2 = -18/10 = -9/5 (редуцированное)
Программа 6.2. Утилиты для рациональных чисел Программа использует ряд утилит для рациональных чисел при выполнении некоторых вычислений. PrintMixedNumber // печатать число как смешанное SoiveEquation // решить общее уравнение // (a/b)X + (c/d) - <e/f) Действие каждого оператора явно описывается в операторе вывода. #include <iostream.h> #include <stdlib.h> #pragma hdrstop #include "rational.h" // печатать Rational-число как смешанное: <+/-)N p/q void PrintMixedNumber (Rational x) { // целая часть Rational-числа х int wholepart * int(x.GetNumerator() / x.GetDenominator()); // сохранить дробную часть смешанного числа Rational fractionpart = х - Rational(wholepart); // если дробной части нет, печатать целую if (fractionpart ==* Rational (0)) cout << wholepart « " "; else { // вычислить дробную часть fractionpart.Reduce(); // печатать знак без целой части if (wholepart < 0) fractionpart ■ -fractionpart; if (wholepart !*= 0) cout « wholepart « " " « fractionpart « " "; else cout << fractionpart « " "; } } // решить ax + b - с, где a, b, с — рациональные числа Rational SoiveEquation(Rational a, Rational b, Rational c) { // проверить а на равенство нулю if (a « Rational(0)) { cout « "Уравнение не имеет решений." << endl; // возвратить Rational(0), если решений нет return Rational(0); } else return (c-b)/a; } void main(void) { Rational rl, r2, r3, ans; cout « "Введите коэффициенты для " "'a/b X + c/d = e/f : ";
cin » rl » r2 » r3; cout « "Приведенное уравнение: " « rl «"Xя " « (r3-r2) « endl; ans - SolveEquation(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 программы 6.2> Введите коэффициенты для ' а/Ь X + c/d = e/f : 2/3 -7/8 -3/8 Приведенное уравнение: 2/3 X * 32/64 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 перегруженной функции, которая имеет целые параметры, ком-
пилятор может создавать совпадение, преобразуя эту переменную в int. Компилятор не выполняет преобразование параметра, если выбор приводит к неоднозначности. Для каждого из следующих примеров укажите правило, которое применимо. Если правило нарушается, опишите ошибку. (а) Предположим, что следующие сегменты кода используются для перегрузки функции f. <function 1> int f(int x, int у) { return x*y; ) <function 3 int f(int x=l, int y=7) { return x + у + x*y; } <function 2> double f(int x, int y) { return x*y; } (б) Функция max перегружается четырьмя отдельными определениями <function 1> int max(int x, int y) ( return x>y? x : y; ) <function 3> int шах(int x, int y, int z) { int lmax *x; if(y>lmax) lmax=»y; if (z > lmax) lmax - z; return lmax; } <function 2> double max(double x, double y) { return x>y? x : y; ) <function 4> int max (void) { int a, b; cin »a»b; return abs(a) > abs(b)? a : b; ) (в) Эти три версии функции read предназначены для различения ввода данных целого и перечисляемого типа. <function 1> void read(int& х) { cin»x; > <function 3> typedef int Logical; <function 2> enum Boolean{FALSE,TRUE}; void read(Boolean& x) ( char c; cin» c; x = (c«'T') ? TRUErFALSE; }
const int TRUE=1, FALSE^O; void read(Logical& x) { 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 и с равен "<<ma:-;(a,b,c) ; cout «"1.0 + max (hi, h2) -"«1.0 + max (hi, h2); cout «"Максимум t, u и v равен "« max(t,u,v); 6.4 Являются ли следующие функции различными с целью перегрузки? Почему? (а) enum El{one, two}; (6) type def double scientific; enum E2{three,four); int f(int x, El y) ; double f(double x); int f(int x, E2 y); scientific f(scientific x); 6.5 Напишите перегруженные версии функции Swap, которая может принимать два int, float и string(char*) параметра. void Swap(int& a, int&b); void Swap(float& x, floats y); void Swap(char *s, char *t); 6.6 Объясните различие между перегрузкой оператора с использованием функции-члена и дружественной функции. 6.7 Класс ModClass имеет единственный целый данное-член dataval в диапазоне 0 . . .6. Конструктор принимает любое положительное целое v и присваивает данному-члену dataval остаток от деления на 7. dataval=v% 7 Оператор сложения складывает два объекта, суммируя их данные-члены и находя остаток после деления на 7. Например:
ModClass a(10)/ //dataval в а равен 3; ModClass b(6); //dataval в b равен 6; ModClass c; // c-a + b имеет значение (3+6) % 7 -2 class ModClass { private: int dataval; public: ModClass{int v * o) ; ModClass operator+(const ModClassS x); int GetValue(void) const; }; (а) Реализуйте методы этого класса. (б) Объявите и реализуйте оператор "*" как друга ModClass. Оператор умножает поля значений в двух объектах ModClass и находит остаток после деления на 7. (в) Напишите функцию ModClass Inverse(ModValue& x); которая принимает объект х с ненулевым значением и возвращает значение у, так что х*у=1 (у называется обратным значением х). (Совет: неоднократно умножайте х на объекты со значениями от1 до 6. Один из этих объектов является обратным). (г) Перегрузите потоковый вывод для ModClass и добавьте этот метод к классу. (д) Замените GetValue, перегрузив оператор преобразования int(). Этот оператор преобразует объект ModClass в целое, возвращая dataval. operator int (void); (е) Напишите функцию void Solve(ModClass a, ModClass& x, ModClass b); которая решает уравнение ах = b для х, вызывая метод Inverse. 6.8 Добавьте полный набор операторов отношения в класс Date из главы 3. Две даты необходимо сравнить по хронологии года. Например: Date(5,5,77) > Date(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 =a+ ib, v = с +id Величина (u) =sqrt(a2+b2) Комплексное число, соответствующее действительному числу f, равно f+iO
Вещественная часть u = a Мнимая часть u=sqrt(a2+b2) u + v = (а + с) + i(b + d) u - v = (а - с) 4- i(b - d) u*v = (ас - bd) + i(ad 4- be) ac + bd /be - adN u/v = — —- + l — —- c2 + d2 c2 + d2 v J -u = -a 4- i(-b) Реализуйте класс Complex, объявление которого является следующим: class Complex { private: double real; double imag; 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 x) const; //Отрицание Complex operator- (void) const; //Оператор потокового ввода/вывода //вывод в формате (real, imag) friend ostream& operator« (ostream& ostr,const Complex& x) ; }; 6.10 Добавьте методы GetReal и Getlmag в класс Complex из письменного упражнения 6.9. Они возвращают вещественную и мнимую части комплексного числа. Используйте эти методы для написания функции Distance, которая вычисляет расстояние между двумя комплексными числами. double Distance (const Complex &a, const Complex &b); 6.11 (а) Добавьте преобразователь в класс Rational, который возвращает объект ModClass. (б) Добавьте преобразователь в ModClass, который возвращает объект Rational 6.12 (а) Реализуйте класс Vec2d из раздела 6.1 (б) Добавьте функцию-член в класс Vec2d, который обеспечивает скалярное произведение, где операнд скаляра находится в правой части. Реализуйте оператор и как функцию-член, и как дружественную функцию.
Пример: Vec2d v(3,5); cout«v*2«" "<<2*v«endl; <output> (6,10) (6,10) 6.13 Множество — это коллекция объектов, которые выбраны из группы объектов, называемой универсальным множеством. Множество записывается как список, разделяемый запятыми и заключенный в фигурные скобки. X = {Ii, I2, 1з, . . . , Im} В данной задаче элементы множества выбраны из целых в диапазоне от 0 до 499. Класс поддерживает ряд бинарных операций для множеств X и Y. D Множество Union(isJ) XUY — это множество, содержащее все элементы в X и все элементы bY без дублирования. □ Множество Intersection(O) X О Y — это множество, содержащее все элементы, которые находятся и в X, и в Y. XUY ХПУ X - (0. 3. 20, 55}, Y - {4. 20. 45. 55} X U У - {0. 3. 4. 20. 45. 55} ХП Y - {20. 55} Множество Membership^) nGX является True, если элемент п является членом множества X; иначе оно равно False. X ={0, 3, 20, 55}//20 G X является True, 35 € X является False Спецификация класса Set В этом объявлении множество — это список элементов, выбранных из диапазона целых 0 ...SETSIZE -1, где SETSIZE является 500. Операции ввода/вывода, объединения, пересечения и принадлежности элементов определяют управление множеством. отьявлжниж const int SETSIZE » 500; const int False ■ 0, True ■ 1; class Set { private: // данные-члены класса set int member[SETSIZE]; public: // конструктор, создает пустое множество Set(void);
// конструктор, создает множество с начальными // элементами а[0], . . . , а[п-1] Set(int all, int n) ; // операторы объединения {+), пересечения (*), принадлежности (А) Set operator+ (Set x) const; Set operator* (Set x) const; friend int operator* (int elt, Set x); // методы вставки и удаления void Insert (int n); // добавить элемент п к множеству set void Delete(int); // удалить элемент п // операторы вывода Set friend ostreamfi operator« (ostream& ostr, const Sets 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 0123456789 499 Потоковый оператор вывода должен печатать множества с элементами, разделяемыми запятыми и заключенными в фигурные скобки. Например: int setvalsf ] = {4, 7, 12}; Set S, T(setvals, 3); S.Insert(5); cout « S « endl; cout « T « endl; <output> {5} {4, 7, 12) (а) Реализуйте конструкторы Set. (б) Реализуйте функцию-член множества в ('"'). Возвращается True, если member[n] является равными True; иначе возвращается False. Так как приоритет оператора '"' является относительно низким, выражение, включающее '"' следует заключать в скобки для надежности. Например: Set A; • • * if ({0 Л А) == 0) //проверка на нахождение 0 во множестве А
(в) S — это множество {1, 4, 17, 25} и Т — это множество {1,8,25,33,53,63}. Выполните следующие операции: (I) S + Т (ii) S * Т (ш) 5^S (iv) 4* (S+T) (v) 25Л (S*T) (г) Укажите действие следующей последовательности операторов: int a[] * {1,2,3,5}; int b[] = {2,3,7,8,25}; int n; Set A(a,4), Bib,5), C; С = A+B; cout « C; С « A*B; ccut « C; cin >> n; // введите 55 A.Insert(n); if (пЛА) cout << Удачно « endl; (д) Реализуйте остальные методы Set Упражнения по программированию 6.1 Эта программа использует результат письменного упражнения 6.5. Н