- •1. Стиль 10
- •3. Проектирование и реализация 63
- •4. Интерфейсы 85
- •5. Отладка 115
- •6. Тестирование 134
- •7. Производительность 157
- •8. Переносимость 180
- •9. Нотация 203
- •Введение
- •Брайан в. Керниган
- •1.1. Имена
- •1.2. Выражения
- •Упражнение 1 -6
- •1.3. Стилевое единство и идиомы
- •1.4. Макрофункции
- •1.5. Загадочные числа
- •1.6. Комментарии
- •1.7. Стоит ли так беспокоиться?
- •Дополнительная литература
- •2.1. Поиск
- •2.2. Сортировка
- •2.3. Библиотеки
- •2.4. Быстрая сортировка на языке Java
- •2.5. "О большое"
- •2.6. Динамически расширяемые массивы
- •2.7. Списки
- •Упражнение 2-8
- •2.8. Деревья
- •Упражнение 2-15
- •2.10. Заключение
- •Дополнительная литература
- •Проектирование и реализация
- •3.1. Алгоритм цепей Маркова
- •3.2. Варианты структуры данных
- •3.3. Создание структуры данных в языке с
- •3.4. Генерация вывода
- •3.5.Java
- •Into the air. When water goes into the air it
- •3.7. Awk и Perl
- •3.8. Производительность
- •3.9. Уроки
- •Дополнительная литература
- •4. Интерфейсы
- •4.1. Значения, разделенные запятой
- •4.2. Прототип библиотеки
- •4.3. Библиотека для распространения
- •Упражнение 4-4
- •4.5 Принципы интерфейса
- •4.6. Управление ресурсами
- •4.7. Abort, Retry, Fail?
- •4.8. Пользовательские интерфейсы
- •Дополнительная литература
- •5. Отладка
- •5.1. Отладчики
- •5.2. Хорошие подсказки, простые ошибки
- •5.3, Трудные ошибки, нет зацепок
- •5.4. Последняя надежда
- •5.5. Невоспроизводимые ошибки
- •5.6. Средства отладки
- •5.7. Чужие ошибки
- •5.8. Заключение
- •Дополнительная литература
- •6. Тестирование
- •6.1. Тестируйте при написании кода
- •6.2. Систематическое тестирование
- •6.3. Автоматизация тестирования
- •6.4. Тестовые оснастки
- •6.5. Стрессовое тестирование
- •6.6. Полезные советы
- •6.7. Кто осуществляет тестирование?
- •6.8. Тестирование программы markov
- •6.9. Заключение
- •Дополнительная литература
- •7.Производительность
- •7.1. Узкое место
- •7.2. Замеры времени и профилирование
- •7.3. Стратегии ускорения
- •7.4. Настройка кода
- •7.5. Эффективное использование памяти
- •7.6. Предварительная оценка
- •7.7. Заключение
- •Дополнительная литература
- •8. Переносимость
- •8.1. Язык
- •8.2. Заголовочные файлы и библиотеки
- •8.3. Организация программы
- •8.4. Изоляция
- •8.5. Обмен данными
- •8.6. Порядок байтов
- •8.7. Переносимость и внесение усовершенствований
- •8.8. Интернационализация
- •8.9. Заключение
- •Дополнительная литература
- •9.1. Форматирование данных
- •9.2. Регулярные выражения
- •Упражнение 9-12
- •9.3. Программируемые инструменты
- •9.4. Интерпретаторы, компиляторы и виртуальные машины
- •9.5. Программы, которые пишут программы
- •9.6. Использование макросов для генерации кода
- •9.7. Компиляция "налету"
- •Дополнительная литература
- •Интерфейсы
- •Отладка
- •Тестирование
- •Производительность
- •Переносимость
2.5. "О большое"
Мы описывали трудоемкость алгоритма в зависимости от п, количества входных элементов. Поиск в неотсортированных данных занимает время, пропорциональное я; при использовании двоичного поиска по отсортированным данным время будет пропорционально log п. Время сортировки пропорционально п2 или п log п.
Нам нужно как-то уточнить эти высказывания, при этом абстрагируясь от таких деталей, как скорость процессора и качество компилятора (и программиста). Хотелось бы сравнивать время работы и затраты памяти алгоритмов вне зависимости от языка программирования, компилятора, архитектуры компьютера, скорости процессора, загруженности системы и других сложных факторов.
Для этой цели существует стандартная форма записи, которая называется "О большое". Основной параметр этой записи — п, размер входных данных, а сложность или время работы алгоритма выражается как функция от п. "О" — от английского order, то есть порядок. Например, фраза "Двоичный поиск имеет сложность O(logn)" означает, что для поиска в массиве из п элементов требуется порядка log п действий. Запись O(f(n)) предусматривает, что при достаточно больших п время выполнения пропорционально f(n), не быстрее, например, О(п2) или 0(п log п). Асимптотические оценки вроде этой полезны при теоретическом анализе и грубом сравнении алгоритмов, однако на практике разница в деталях может иметь большое значение. Например, алгоритм класса O(n2) с малым количеством дополнительных вычислений для малых п может работать быстрее, чем сложный алгоритм класса O(nlog п), однако при достаточно большом п алгоритм с медленнее возрастающей функцией поведения неизбежно будет работать быстрее.
Нам нужно различать также случаи наихудшего и ожидаемого поведения. Трудно строго определить, что такое "ожидаемое" поведение, потому что определение зависит от наших предположений о возможных входных данных. Обычно мы можем точно указать самый плохой случай, хотя иногда и здесь можно ошибиться. Для quicksort в самом плохом случае время работы растет как O(n2), а среднее ("ожидаемое") время — как O(п log п). Если каждый раз аккуратно выбирать элемент-разделитель, то мы можем свести вероятность квадратичного (то есть O(n2)) поведения практически к нулю; хорошо реализованная quicksort действительно обычно ведет себя как О(п log п). Вот основные случаи:
Запись |
Название времени |
Пример |
O(1) |
Константное |
Индексирование массива |
O(log п) |
Логарифмическое |
Двоичный поиск |
O(п) |
Линейное |
Сравнение строк |
O(n log n) |
и log и |
Quicksort |
O(п2) |
Квадратичное |
Простые методы сортировки |
O(п3) |
Кубическое |
Перемножение матриц |
O (2n) |
Экспоненциальное |
Перебор всех подмножеств |
Доступ к элементу в массиве — операция, работающая за константное (O(1)) время. Алгоритм, за каждый шаг отсеивающий половину входных данных, как двоичный поиск, обычно займет время O(log п). Сравнение двух строк длиной в п символов с помощью strcmp займет O(п). Традиционный алгоритм перемножения двух квадратных матриц порядка n занимает О(n3), поскольку каждый элемент получается в результате перемножения и пар чисел и суммирования результатов, а всего элементов n2.
Экспоненциальное время работы алгоритма обычно является результатом перебора всех вариантов: у множества из п элементов — 2n различных подмножеств, поэтому алгоритм, которому надо пройтись по всем подмножествам, будет выполняться за время O(2n), то есть будет экспоненциальным. Экспоненциальные алгоритмы обычно слишком долго работают, если только п не очень мало, поскольку добавление одного элемента удваивает время работы алгоритма. К сожалению, существует много задач, таких как, например, знаменитая "задача коммивояжера", для которых известны только экспоненциальные решения. Когда задача такова, часто вместо точных решений берут алгоритмы, находящие некоторое приближение к ответу.
Упражнение 2-3
Каковы входные данные для алгоритма quicksort, которые заставляют его работать медленнее всего, как в наихудшем случае? Попробуйте найти несколько наборов данных, сильно замедляющих библиотечную версию алгоритма. Автоматизируйте процесс, чтобы вы легко могли за давать параметры и проводить большое число экспериментов.
Упражнение 2-4
Придумайте и реализуйте алгоритм, который будет сортировать массив из п целых как можно медленнее. Только напишите его честно: алгоритм должен постепенно прогрессировать и в конце концов завершиться, и ваша реализация не должна использовать всяческие трюки вроде лишних пустых циклов. Какова получилась сложность вашего алгоритма как функция от n?
