1. Тип таблицы
2. Инициализация таблицы
3. Включение новой записи в
неупорядоченную таблицу
4. Поиск в неупорядоченной таблице
5. Метод барьера
6. Двоичный поиск
7. Сортировка таблицы
8. Включение новой записи в
упорядоченную таблицу
9. Удаление записи из таблицы
Таблица - это массив записей. Основные операции с таблицами: включение записи, удаление записи, поиск записи по заданному признаку, сортировка записей таблицы по одному или нескольким полям.
Назначение сортировки состоит в ускорении операции поиска, так как наличие каких бы то ни было элементов упорядоченности таблицы может быть использовано для этой цели. Сразу отметим, что сортировка таблицы - это достаточно трудоёмкая операция в вычислительном отношении. Очевидно, что в тех случаях, когда трудозатраты на сортировку не компенсируются в последующем их сокращением на поиск, следует использовать неупорядоченную таблицу.
Определим тип таблицы и саму таблицу:
const M = 7; {Макс. размер таблицы} type TRec = record {Тип записей таблицы} Tag: Char; {Тэг записи} Key: string[5]; {Ключ записи} Info: string[10]; {Значение записи} end; TTab = array[1..M] of TRec; {Тип таблицы} var Tab: TTab; {Таблица} Cnt: Integer; {Фактический размер таблицы}
Тэг - вспомогательное поле
записи, которое может служить для указания
каких-либо её признаков, например, что запись
удалена.
Ключ - одно из полей записи, по
которому предполагается выполнять поиск в
таблице или её сортировку.
Значение - обобщённое обозначение
других полей записи, которые используются в
чисто информационном смысле.
В этом разделе мы будем исходить из предположения, что в таблице нет записей с одинаковыми значениями ключей.
Пример. Предположим, что таблица Tab - это телефонный справочник. Поле Key в этом случае может быть номером телефона (например, '23-34'), а поле Info - именем клиента (например, 'Bess').
Выполняется, как правило, в начале выполнения программы. Пример.
Cnt := 0;
и/или
for i:=1 to M do Tab[i].Tag:=' ';
Пример.
var NewRec: TRec; . . . {Ввод или определение значений переменных NewKey и NewInfo} . . . with NewRec do begin Key:=NewKey; Info:=NewInfo end; if Cnt >= M then Error := 1; Inc(Cnt);( Tab[Cnt] := NewRec;
Здесь предполагается, что Error - это глобальная переменная, которая предназначена для фиксации кода ошибки. В данном случае будем считать, что 1 означает "Переполнение таблицы". Предполагается, что до выполнения операции управляющая часть программы обнуляет переменную Error, а после выполнения операции анализирует её значение и принимает соответствующие меры.
Задача поиска состоит в определении индекса записи таблицы, поле Key которой равно заданному значению - аргументу поиска.
Оформим решение этой задачи в форме процедуры Search. Составим её спецификацию.
Спецификация процедуры Search -
"Поиск записи в неупорядоченной таблице"
1. Заголовок: procedure Search(Arg: string; var
Index, ErrCode: Integer);
2. Входные параметры: Arg - аргумент поиска.
3. Выходные параметры: Index - индекс искомой записи;
ErrCode - код завершения.
4. Глобальные имена: Tab, Cnt.
5. Функция: Определение индекса Index,
удовлетворяющего условию Tab[Index].Key = Arg, c
формированием кода завершения ErrCode: 0 - запись
найдена, 1 - запись не найдена.
6. Используемые процедуры: Нет.
7. Особые случаи: Нет.
Отметим, что в процедуре Search мы используем несколько другой метод обработки ошибок, чем выше, - формирование кода завершения процедуры. По значению кода завершения вызывающая программа принимает решение о дальнейшем поведении.
procedure Search(Arg: string; var Index, ErrCode: Integer); var i: Integer; begin for i:=1 to Cnt do if Tab[i].Key = Arg then begin Index:=i; ErrCode:=0; Exit end; ErrCode:=1; {Запись не найдена} end; {Search}
В заключение этого раздела остановимся на понятии спецификации. Большинство пунктов спецификации достаточно очевидны. Некоторого пояснения требуют лишь пункты 6 и 7. В пункте 6 перечисляются имена процедур, которые вызываются в данной процедуре. Пункт 7 содержит информацию о способах передачи данных между поцедурой и её окружением, минуя механизмы передачи параметров и глобальных переменных. Обычно такую роль выполняют файлы.
Роль спецификаций в программировании трудно переоценить. Для программиста-разработчика процедуры спецификация это документ, играющий роль задания на разработку. Для программиста-пользователя она даёт исчерпывающую информацию о способе включения процедуры в программную систему.
Позволяет несколько увеличить производительность предыдущей процедуры за счёт сокращения количества проверок условий в теле цикла.
procedure Search(Arg: string; var Index, ErrCode: Integer); var i: Integer; begin Tab[Cnt+1].Key:=Arg; { Установка баръера } i:=1; while Tab[i].Key <> Arg do Inc(i); if i = Cnt + 1 then begin ErrCode:=1; Exit end; {Запись не найдена} Index:=i; ErrCode:=0; end; {Search}
Метод барьера требует, чтобы в таблице всегда находился хотя-бы один свободный элемент, следующий за элементом с индексом Cnt. Это необходимо учитывать при определении факта переполнения таблицы.
Может быть использован в случае упорядоченной таблицы. Идея двоичного поиска состоит в последовательном делении области поиска пополам. Характер убывающей геометрической прогрессии этого процесса приводит к резкому сокращению времени поиска. Предположим, что записи в таблице упорядочены по возрастанию ключа. Далее предположим, что Up..Down - диапазон индексов записей таблицы, одна из которых, возможно, содержит искомый ключ. Определим середину диапазона Up..Down - i = (Up + Down) div 2. Теперь, если Tab[i].Key < Arg, то это в силу упорядоченности таблицы означает, что индекс искомой записи может находиться только в диапазоне i+1..Down. Если же Tab[i].Key > Arg, то - в диапазоне Up..i-1. Мы видим, что проверка всего двух условий приводит к сокращению области поиска сразу в два раза! На следующем шаге полученная область уменьшится ещё в два раза, и т.д.
procedure Search(Arg: string; var Index, ErrCode: Integer); var i: Integer; begin Up:=1; Down:=Cnt; repeat i:=(Up + Down) div 2; if Tab[i].Key <= Arg then Up:=i+1; if Tab[i].Key >= Arg then Down:=i-1; until Up > Down; if Up = Down + 2 then begin Index:=i; ErrCode:=0; Exit end; ErrCode:=1; {Запись не найдена} end; {Search}
Сравним данный метод с предыдущим. По объёму кода несколько больше процедура двоичного поиска. Оценим производительность каждого метода. Время поиска методом перебора, который применяется для поиска в неупорядоченной таблице, может быть оценено по формуле Tп = Kп*Cnt/2. Для двоичного поиска - Tд = Kд*log2(Cnt). Здесь Kп и Kд - это коэффициенты, пропорциональные времени выполнения операторов тела цикла соответствующих методов. Грубая оценка даёт Kд ~= 5*Kп. Если предположить, что Kп=1, то для таблицы, содержащей 1000 записей, получим Tп = 500, а Tд = 50 некоторых условных единиц времени. Вывод очевиден. Однако, следует заметить, что поддержание таблицы в упорядоченном состоянии тоже требует затрат времени и окончательный выбор метода следует делать с учетом всех факторов. На практике двоичный поиск применяют тогда, когда таблица изменяется редко или не изменяется совсем.
Задача сортировки состоит в перестановке записей таблицы таким образом, чтобы выполнялось условие Tab[i].Key < Tab[i+1].Key, i = 1..Cnt-1. Сортировка таблицы это достаточно трудоёмкая операция. Мы рассмотрим сортировку методом простых вставок.
var R: TRec; i,j: Integer; . . . for i:=2 to Cnt do begin R:=Tab[i]; j:=i-1; while (j > 0) and (R.Key < Tab[j].Key) do begin Tab[j+1]:=Tab[j]; Dec(j) end; Tab[j+1]:=R; end;
Идея метода простых вставок состоит в следующем. Предположим что начальная часть таблицы 1..i-1 уже отсортирована. В частности она может состоять всего из одной записи, первой. Запись Tab[i] сохраняется в R. Далее определяется местоположение записи R в части таблицы 1..i-1. Для этого R.Key последовательно сравнивается с ключами записей таблицы, начиная с j = i - 1 в сторону уменьшения индекса j, пока не будет нарушено условие R.Key < Tab[j].Key. Попутно записи, для которых условие R.Key < Tab[j].Key выполняется, сдвигаются на одну позицию в сторону конца таблицы, освобождая тем самым место для включения записи R. В результате выполнения этого процесса размер отсортированной части таблицы увеличивается на 1. Остаётся продолжить эти действия для оставшихся записей массива.
Это наиболее эффективный метод сортировки из числа наиболее простых. Он отлично работает, когда таблица состоит из 30..50 элементов. Для таблиц большего размера приходится применять другие, более изощрённые методы.
Другой способ получения упорядоченной таблицы состоит в выполнении включения каждой новой записи в таблицу таким образом, чтобы свойство её упорядоченности сохранялось.
Спецификация процедуры Insert -
"Включение записи в упорядоченную таблицу"
1. Заголовок: procedure Insert(R: TRec);
2. Входные параметры: R - включаемая запись.
3. Выходные параметры: нет.
4. Глобальные имена: TRec, Tab, Cnt, M, ErrCode.
5. Функция: Включение записи R в упорядоченную
таблицу Tab с сохранением свойства её
упорядоченности. Формирование кода завершения
ErrCode: 0 - нормальное включение, 1 - таблица
переполнена.
6. Используемые модули: Нет.
7. Особые случаи: Нет.
procedure Insert(R: TRec); var j: Integer; begin {Tab is full} if Cnt >= M then begin ErrCode:=1; Exit end; ErrCode:=0; j:=Cnt; while (j>0) and (R.Key < Tab[j].Key) do begin Tab[j+1]:=Tab[j]; Dec(j) end; Tab[j+1]:=R; Inc(Cnt); end; {Insert}
Включение в этом примере выполняется так же, как внутренний цикл в алгоритме сортировки простыми вставками. Кроме этого пример иллюстрирует ещё раз способ передачи кода завершения, при котором используется глобальная переменная.
Предполагаем, что индекс удаляемой записи (Index) уже известен. Как правило, он является результатом выполнения предыдущей операции поиска.
var i: Integer; . . . for i:=Index to Cnt-1 do Tab[i]:=Tab[i+1]; Dec(Cnt);
Основная часть времени выполнения операции уходит на сдвиг элементов хвоста таблицы на одну позицию, чтобы заполнить брешь, возникающую при удалении записи.
Для тех программ, в которых велика интенсивность выполнения операций удаления, возникает задача ускорения этой операции. Стандартный подход в решении этой проблемы основан на использовании поля записи Tag - в него просто записывают соответствующий признак, обозначающий, что запись удалена. На самом деле она удаляется лишь логически. Место в таблице, занимаемое подобными записями, называют мусором. Естественно, что наличие таких записей в таблице должно учитываться в алгоритмах выполнения других операций. По мере накопления мусора появляется необходимость в его удалении. Для этого предназначена операция сжатия массива. Она запускается оператором по мере необходимости, или автоматически при неудачной попытке включить запись в таблицу.
var i,j: Integer; . . . j:=0; for i:=1 to Cnt do if Tab[i].Tag<>'D' then begin Inc(j); Tab[j]:=Tab[i] end; Cnt:=j;
Copyright г Барков Валерий Андреевич, 2000