Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

2.6. Динамически расширяемые массивы

Массивы, использованные в нескольких предыдущих разделах, бы­ли статическими, их размер и содержимое задавались во время компи­ляции. Если потребовать, чтобы некоторую таблицу слов или символов HTML можно было изменять во время выполнения, то хэш-таблица была бы для этой цели более подходящей структурой. Вставка в отсор­тированный массив п элементов по одному занимает O(n2), чего стоит избегать при больших п.

Однако часто нам нужно работать с переменным, но небольшим числом значений, и тогда массивы по-прежнему могут применяться. Для уменьшения потерь при перераспределении памяти изменение размера должно происходить блоками, и для простоты массив дол­жен храниться вместе с информацией, необходимой для управления им. В C++ и Java это делается с помощью классов из стандартных библиотек, а в С мы можем добиться похожего результата с помощью структур.

Следующий код определяет расширяемый массив с элементами типа Nameval: новые элементы добавляются в хвост массива, который удлиняется при необходимости. Доступ к каждому элементу по его ин­дексу происходит за константное время. Эта конструкция аналогична векторным классам из библиотек C++ и Java.

typedef struct Nameval Nameval;

struct Nameval {

char *name;

int value;

};

struct NVtab {

int nval; /* текущее количество элементов */

int max; /* под сколько элементов выделена память */

Nameval *nameval; /* массив пар */

} nvtab;

enum { NVINIT = 1, NVGROW = 2 };

/* addname: добавить новое имя и значение в nvtab */

int addname(Nameval newname)

{

Nameval *nvp;

if (nvtab.nameval == NULL) { /* первый вызов */

nvtab.nameval =

(Nameval *) malloc(NVINIT * sizeof(Nameval));

if (nvtab.nameval == NULL)

return -1;

nvtab.max = NVINIT;

nvtab.nval = 0;

} else if (nvtab.nval >= nvtab.max) { /* расширить */

nvp = (Nameval *) realloc(nvtab.nameval,

(NVGROW*nvtab.max) * sizeof(Nameval));

if (nvp == NULL)

return -1;

nvtab.max *= NVGROW;

nvtab.nameval = nvp;

}

nvtab.nameval[nvtab.nval] = newname;

return nvtab.nval++;

}

Функция addname возвращает индекс только что добавленного элемента или -1 в случае возникновения ошибки.

Вызов realloc увеличивает массив до новых размеров, сохраняя су­ществующие элементы, и возвращает указатель на него или NULL при не­достатке памяти. Удвоение размера массива при каждом вызове realloc сохраняет средние "ожидаемые" затраты на копирование элемента по­стоянными; если бы массив увеличивался каждый раз только на один элемент, то производительность была бы порядка 0(и2). Поскольку при перераспределении памяти адрес массива может измениться, то про­грамма должна обращаться к элементам массива по индексам, а не через указатели. Заметьте, что код не таков:

? nvtab.nameval = (Nameval *) realloc(nvtab.nameval,

? (NVGROW*nvtab.max) * sizeof(Nameval));

потому что при такой форме если вызов realloc не сработает, то исходный массив будет утерян.

Мы задаем очень маленький начальный размер массива (NVINIT = 1). Это заставляет программу увеличивать массив почти сразу, что гаранти­рует проверку данной части программы. Начальное значение может быть и увеличено, когда программа начнет реально использоваться, од­нако затраты на стартовое расширение массива ничтожны.

Значение, возвращаемое real loc, не обязательно должно приводить­ся к его настоящему типу, поскольку в С не типизированные указатели (void *) приводятся к любому типу указателя автоматически. Однако в C++ это не так; здесь преобразование обязательно. Можно поспо­рить, что безопаснее: преобразовывать типы (это честнее и понятнее) или не преобразовывать (поскольку в преобразовании легко может закрасться ошибка). Мы выбрали преобразование, потому что тогда программа корректна как в С, так и в C++. Цена такого решения — уменьшение количества проверок на ошибки компилятором С, но это неважно, когда мы производим дополнительные проверки с помощью двух компиляторов.

Удаление элемента может быть более сложной операцией, поскольку нам нужно решить, что же делать с образовавшейся "дыркой" в массиве. Если порядок элементов не имеет значения, то проще всего переставить последний элемент на место удаленного. Однако, если порядок должен быть сохранен, нам нужно сдвинуть элементы после "дырки" на одну позицию:

/* delname: удалить первое совпавшее имя в массиве nvtab */

int delname(char *name)

{

int i;

for (i=0; i< nvtab.nval; i++)

if (strcmp(nvtab.nameval[i].name, name) == 0) {

memmove(nvtab.nameval+i, nvtab.nameval+i+1,

(nvtab.nval-(i+1)) * sizeof(Narneval));

nvtab.nval--;

return 1;

}

return 0;

}

Вызов memmove сдвигает массив, перемещая элементы вниз на одну пози­цию; memmove — стандартная библиотечная функция для копирования блоков памяти любого размера.

В стандарте ANSI С определены две функции: memcpy, которая рабо­тает быстрее, но может затереть память, если источник и приемник данных пересекаются, и memmove, которая может работать медленнее, но зато всегда корректна. Бремя выбора между скоростью и корректнос­тью не должно взваливаться на программиста; должна была бы быть только одна функция. Считайте, что это так и есть, и всегда используй­те memmove.

Мы могли бы заменить вызов memmove следующим циклом:

int j;

for (j = i; j < nvtab.nval-1; j++)

nvtab.nameval[j] = nvtab.nameval[j+1];

Однако мы предпочитаем использовать memmove, чтобы обезопасить себя от легко возникающей ошибки копирования элементов в непра­вильном порядке. Если бы мы вставляли элемент, а не удаляли, то цикл должен был бы идти вверх, чтобы не затереть элементы. Вызывая memmove, мы ограждаем себя от необходимости постоянно задумываться

об этом.

Альтернатива перемещению элементов - помечать удаленные элементы как неиспользуемые. Тогда для добавления элемента нам надо сначала найти неиспользуемую ячейку и увеличивать массив, только если свободного места не найдено. В данном примере эле­мент можно пометить как неиспользуемый, установив значения поля name в NULL.

Массивы — простейший способ группировки данных; вовсе не слу­чайно большинство языков имеют эффективные и удобные индекси­руемые массивы и даже представляют строки в виде массивов символов. Массивы просты в использовании, обладают константным доступом к любому элементу, хорошо работают с двоичным поиском и быстрой сортировкой, а также почти совсем не тратят лишних ресурсов. Для на­боров данных фиксированного размера, которые могут быть созданы даже во время компиляции, или же для гарантированно небольших объемов данных массивы подходят идеально. Однако хранение меня­ющегося набора значений в массиве может быть весьма ресурсоемким, поэтому, если количество элементов непредсказуемо и потенциально неограниченно, может оказаться удобнее использовать другую структу­ру данных.

Упражнение 2-5

В приведенном выше коде функция delname не вызывает realloc для ,! возврата системе памяти, освобожденной удалением элемента. Имеет ли смысл это делать? Как решить, стоит это делать или нет?

Упражнение 2-6

Внесите необходимые изменения в функции delname и addname, что­бы удаленные элементы помечались как неиспользуемые. Насколько остальная программа не зависит от этого изменения?

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]