- •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.1. Поиск
Ничто не сравнится с массивом, если нам нужно хранить статические табличные данные. Инициализация во время компиляции делает задачу конструирования таких массивов простой и легкой. (В Java инициализация происходит во время выполнения, но это можно считать незначительной деталью реализации, пока массивы не слишком велики.) В программе для распознания слов, слишком часто употребляемых в плохой прозе, мы можем написать:
char *flab[] = {
"actually",
"just",
"quite",
"really",
NULL };
Функция поиска должна знать, сколько в массиве элементов. Один из способов сообщить ей это — передать длину массива в виде аргумента; второй способ, использованный здесь, — поместить в конце массива элемент-маркер NULL:
/* lookup: последовательный поиск слова в массиве */
int lookup(char *word, char *array[])
{
int i;
for (i = 0; array[i] ! = NULL;
if (strcmp(word, array[i] ) == 0)
return i;
return -1;
}
В С и C++ для передачи в качестве параметра массив строк можно опи-'•сать как char *array[] или char **array. Эти две формы эквивалентны, но в первой сразу видно, как будет использоваться параметр.
Предлагаемый поисковый алгоритм называется последовательным поиском, потому что он просматривает по очереди все элементы, сравни-|вая их с искомым. Когда данных немного, последовательный поиск работает достаточно быстро. Есть стандартные библиотечные функции, которые выполняют последовательный поиск для определенных типов ' данных. Например, в языках С и C++ функции st reh г и st rst r ищут пер-; вое вхождение заданного символа или подстроки в строку, в Java у клас-[са String есть метод indexOf, а обобщенные функции поиска в C++ find применимы к большинству типов данных. Если такая функция суще-|ствует для нужного вам типа данных, то используйте ее.
Последовательный поиск достаточно прост, но время его работы ; прямо пропорционально количеству данных, которые нужно про-, смотреть; удвоение количества элементов приведет к удвоению вре-: мени на поиск, если искомого элемента в массиве нет. Это линейное [соотношение (время выполнения является линейной функцией от 'размера данных), поэтому такой метод также называется линейным [поиском.
Вот пример массива более реалистичного размера из программы, выполняющей синтаксический разбор текста, написанного на HTML, где определены имена более чем сотни отдельных символов:
typedef struct Nameval Nameval;
struct Nameval {
char *name;
int value;
};
/* символы HTML например AElig - лигатура А и Е. */
/* значения в кодировке Unicode/13010646 */
Nameval htmlchars[] = {
"AElig", 0х00с6, /*лигатура А и Е*/
"Aacute", 0x00c1, /*А с акцентом*/
"Acirc", 0x0c2, /*А с кружочком*/
/*.......*/
"zeta", 0х0Зb6, /* греческая дзета */
Для объемистого массива вроде этого более эффективно было бы использовать двоичный поиск. Алгоритм двоичного поиска является систематизированной версией поиска слова в словаре. Проверяем средний элемент. Если это значение больше, чем нужное, то ищем далее в первой части; в противном случае ищем во второй части. Повторяем до тех пор, пока не найдем нужный элемент или не убедимся, что его в массиве нет.
Для двоичного поиска таблица должна быть отсортирована, как в данном случае (в любом случае это полезно; люди тоже быстрее находят требуемое в отсортированных таблицах), а также должно быть известно, сколько элементов в таблице. Здесь может помочь макрофункция NELEMS из первой главы:
printf("Таблица HTML содержит %d слов\n", NELEMS(htmlchars));
Функция двоичного поиска для этой таблицы могла бы выглядеть так:
/* lookup: двоичный поиск имени name в таблице tab;
возвращается индекс */
int lookup(char *name, Nameval tab[], int ntab)
{
int low, high, mid, cmp;
low = 0;
high = ntab - 1;
while (low <= high) {
mid = (low + high) / 2;
cmp = strcmp(name, tab[mid].name);
if (cmp < 0)
high = mid - 1;
else if (cmp > 0)
low = mid + 1;
else /* совпадение найдено */
return mid;
}
return -1; /* совпадений нет */
Объединяя все это вместе, мы можем написать:
half = Iookup("frac12", htmlchars, NELEMS(htmlchars));
для определения индекса, под которым символ '/2 (одна вторая) стоит, в массиве htmlchars.
Двоичный поиск отбрасывает за каждый шаг половину данных, поэтому количество шагов пропорционально тому, сколько раз мы можем поделить и на 2, пока у нас не останется один элемент. Без учета округления это число равно Iog2 п. Если у нас в массиве 1000 элементов, то линейный поиск займет до 1000 шагов, в то время как двоичный — только около 10; при миллионе элементов линейный поиск займет миллион шагов, а двоичный — 20. Очевидно, чем больше число элементов, тем больше преимущество двоичного поиска. Начиная с некоторого зависящего от реализации размера данных, двоичный поиск работает быстрее, чем линейный.
