Костюк - Основы программирования
.pdfГлава 5 Алгоритмы с множествами, символьными строками и таблицами
5.1 Представление множеств
Пусть задано N некоторых объектов (множество объектов). В теории множеств рассматривают не сами объекты, а их совокупности. Если объекты перенумеровать числами от 1 до N, каждый объект будет определяться своим номером. При этом любым двум различным объектам будут приписаны различные номера. Все множе ство рассматриваемых объектов называют исходным множеством или универсу мом. Некоторые из объектов универсума можно каким-либо образом выделить. То гда образуется множество выделенных объектов, его называют просто множеством. Рассмотрим различные способы задания множеств.
Задание множества двоичным (битовым) массивом. Пусть N – количество объектов в универсуме. Необходимо определить массив M из N элементов, которые могут принимать два значения. Это может быть, например, целочисленный массив, в котором каждый элемент может принимать значение 0 или 1. Для некоторой эконо
мии памяти его тип можно задать как byte. Массив может также быть описан, как логический (boolean), в нем каждый элемент может принимать значение false
или true. Тогда конкретное множество задается набором значений элементов мас сива M. Если M[i]=1 (или true), то i–й объект универсума присутствует в мно жестве, а если M[i]=0 (или false) – то нет. Рассмотрим выполнение основных
действий с массивом M.
Проверка принадлежности объекта множеству выполняется очевидным об |
|||||
разом: если M[i]=1, то объект номер i универсума принадлежит множеству. |
|||||
Добавление объекта номер |
i в множество: M[i]=1. Если объект номер i |
||||
уже есть в множестве, то ничего не изменится. |
M[i]=0. |
||||
Удаление объекта номер |
i |
из множества: |
|||
Вычисление мощности K |
множества (количества элементов в нем): |
||||
|
K:=0; |
|
|
|
|
|
for i:=1 to N do |
|
|
(5.1) |
|
|
if M[i]=1 then K:=K+1; |
|
|
92
Выполнение этих действий (кроме последнего) весьма эффективно, однако если N велико, то для массива M потребуется много памяти (N байт), независимо от того, какова мощность множества, даже если она ограничена некоторым числом L таким, что K≤L<<N.
Задание множества целочисленным массивом. В некоторых задачах количе ство объектов в универсуме ограничено большим, например, максимально возмож ным целочисленным значением, но мощность множества всегда ограничена некото рым числом L. Тогда можно определить целочисленный массив M из L элементов и целочисленную переменную K, которая определяет, сколько элементов из M за полнено в текущий момент. Значения M[1],...,M[K] задают номера объектов, принадлежащих множеству. Заметим, что в таком массиве не должно быть элементов с одинаковыми значениями! Если в массиве такие элементы имеются, то лишние можно выбросить алгоритмом (3.14).
Рассмотрим выполнение основных действий с массивом M, представляющим множество. Для того чтобы алгоритмы были более эффективны, номера объектов
можно упорядочить.
Проверка принадлежности объекта номер i множеству выполняется после довательным просмотром за K шагов или (при упорядоченности массива M) алго ритмом дихотомического поиска (3.16) за log2 K шагов.
Добавление объекта номер i в множество. Вначале последовательным про
смотром за K шагов проверяется, не принадлежит ли уже объект номер i множе ству. Если нет, то выполняются действия: K:=K+1; M[K]:=i;
При упорядоченности массива M алгоритмом дихотомического поиска (3.16) за log2 K шагов проводится поиск. Пусть j – номер элемента в массиве M, полученный алгоритмом поиска (этот элемент больше или равен i). Если j>i, то элементы
M[j],...,M[K] сдвигаются вправо на 1 позицию, и выполняются действия:
K:=K+1; M[j]:=i;
Удаление объекта номер i из множества. Вначале последовательным про
смотром за K шагов проверяется, принадлежит ли объект номер i множеству. Если
M[j]=i, то выполняются действия: M[j]:=M[K]; K:=K-1;
При упорядоченности массива M алгоритмом дихотомического поиска (3.16) за
log2 K шагов проводится поиск. Пусть j – номер элемента в массиве M, полученный |
|
алгоритмом поиска (этот элемент больше или равен i). Если |
j=i, то элементы |
M[j+1],...,M[K] сдвигаются влево на 1 позицию, и K |
уменьшается на 1: |
K:=K-1; |
|
Вычисление мощности множества не требует дополнительных действий (мощ ность множества равна K).
Задание множества списком номеров его элементов. Из номеров элементов универсума, принадлежащих конкретному множеству, можно сформировать не толь
93
ко массив, но и список. Для этого можно использовать описания типа, приведенные в алгоритме (3.21). Рассмотрим выполнение основных действий с линейным списком, представляющим множество. Пусть на первый элемент этого списка указывает указа тель pb, а на последний – указатель pe. Пусть K – количество элементов множе ства. В рассматриваемых ниже алгоритмах предполагается, что множество не пу
стое. Если этого гарантировать нельзя, то алгоритмы необходимо дополнить соот ветствующей проверкой: if pb<>nil then …
Проверка принадлежности объекта номер i множеству выполняется после довательным просмотром за K шагов. Упорядоченность списка иногда позволяет прекратить просмотр раньше, чем будет достигнут конец списка, однако не изменяет трудоемкость алгоритма в наихудшем. Алгоритм поиска для упорядоченного по воз растанию списка:
p:=pb;
while (p<>nil)and(p^.s<i) do (5.2) p:=p^.p;
if (p<>nil)and(p^.s=i) then НАЙДЕН else НЕТ
Добавление объекта номер i в множество. Вначале последовательным про смотром за K шагов проверяется, не принадлежит ли уже объект номер i множе
ству. Если нет, то объект добавляется в начало списка: new(p); p^.s:=i; p^.p:=pb; pb:=p;
или в конец списка:
new(p); p^.s:=i; p^.p:=nil; pe^.p:=p; pe:=p;
При упорядоченности списка сначала отыскивается место для добавляемого элемента, а затем вставка, если такого элемента в списке нет:
|
|
|
if pb^.s>i then |
{вставка в начало списка} |
|
begin new(p); p^.s:=i; p^.p:=pb; pb:=p end |
|
|
else begin p:=pb; |
|
|
while (p<>nil)and(p^.s<i) do |
|
|
begin p1:=p; p:=p^.p end; |
(5.3) |
|
if (p=nil)or(p^.s>i) then begin |
|
|
|
{вставка внутрь или в конец списка} |
|
new(p2); p2^.s:=i; p2^.p:=p; p1^.p:=p2 |
|
|
end |
|
|
end |
|
|
Удаление объекта номер i из множества. Вначале последовательным про смотром за K шагов проверяется, принадлежит ли объект номер i множеству, и если принадлежит, то в каком элементе списка он расположен. После чего найден ный элемент удаляется. Эти действия иногда могут быть несколько эффективнее для упорядоченного списка: в алгоритме (5.4) просмотр элементов в списке заканчивает
94
ся не только тогда, когда искомый элемент найден внутри списка, но и тогда, когда очередной элемент списка в поле s будет содержать значение, большее i.
if pb^.s=i then {удаление начального злемента из списка} begin p:=pb; pb:=pb^.p; free(p) end
else begin p:=pb;
while (p<>nil)and(p^.s<i) do
begin p1:=p; p:=p^.p end; (5.4) if (p<>nil)and(p^.s=i) then begin
{ удаление злемента внутри списка} p1^.p:=p^.p; free(p)
end end
Вычисление мощности множества выполняется последовательным просмот ром всех элементов списка:
|
|
|
|
|
|
K:=0; p:=pb; |
|
|
|
|
while (p<>nil) do |
|
(5.5) |
|
|
begin K:=K+1; p:=p^.p end; |
|
|
|
|
Выполнение операций над множествами. |
|
|
|
|
Определение 5.1. Объединением множеств A и |
B называется такое множество |
||
C, C = A U B, содержащее все те элементы, которые имеются либо в A, либо в |
B, |
|||
либо в A и в B одновременно, и только эти элементы. |
|
|||
|
Определение 5.2. Пересечением множеств A и |
B называется такое множество |
||
C, C = A ∩ B, содержащее все те элементы, которые имеются одновременно в A и в |
||||
B, и только эти элементы. |
|
|
|
|
|
Определение 5.3. Разностью множеств A и B |
называется такое множество |
C, |
|
C = A \ B, содержащее все те элементы, которые имеются в A, но отсутствуют в |
B, |
|||
и только эти элементы. |
|
|
|
|
|
Определение 5.4. Симметрической разностью множеств A и B называется такое |
|||
множество C, C = A Å B , содержащее все те элементы, которые имеются в A, |
но |
отсутствуют в B, и все элементы, которые имеются в B, но отсутствуют в A и толь ко эти элементы.
Рассмотрим алгоритмы выполнения операций для описанных способов представ ления множеств.
Пример 5.1. Алгоритмы вычисления операций над множествами, заданными двоичными массивами A и B (множества определены на одном и том же универсу ме из N элементов). Размер массивов A и B должен быть равен N. Результат в массиве C (также размером N элементов).
Алгоритм объединения множеств:
95
for i:=1 to N do
if (A[i]=1)or(B[i]=1) then C[i]:=1 else C[i]:=0;
Алгоритм пересечения множеств:
for i:=1 to N do
if (A[i]=1)and(B[i]=1) then C[i]:=1 else C[i]:=0;
Алгоритм вычисления разности множеств:
for i:=1 to N do
if (A[i]=1)and(B[i]=0) then C[i]:=1 else C[i]:=0;
Алгоритм вычисления симметрической разности множеств:
for i:=1 to N do
if ((A[i]=1)xor(B[i]=1)) then C[i]:=1 else C[i]:=0;
Конец примера.
(5.6)
(5.7)
(5.8)
(5.9)
Пример 5.2. Алгоритмы вычисления операций над множествами, заданными упорядоченными целочисленными массивами A и B с количеством заполненных элементов K1 и K2 соответственно. Результат в массиве C, количество заполнен ных элементов в нем – K3, вычисляется в процессе работы алгоритмов.
i1:=1; i2:=1; K3:=0; |
|
|
while (i1<=K1)or(i2<=K2) do |
|
|
if i1>K1 then |
|
|
begin |
K3:=K3+1; C[K3]:=B[i2]; i2:=i2+1 end |
|
else if |
i2>K2 then |
|
begin |
K3:=K3+1; C[K3]:=A[i1]; i1:=i1+1 end |
|
else if |
A[i1]<B[i2] then |
(5.10) |
begin |
K3:=K3+1; C[K3]:=A[i1]; i1:=i1+1 end |
|
else if |
A[i1]>B[i2] then |
|
begin |
K3:=K3+1; C[K3]:=B[i2]; i2:=i2+1 end |
|
else |
|
|
begin |
|
|
K3:=K3+1; C[K3]:=B[i2]; |
|
|
i2:=i2+1; i1:=i1+1 |
|
|
end |
|
|
Алгоритмы строятся на основе алгоритма слияния (3.13). При этом количество элементов, заданных в описании массива, должно быть достаточным для размещения
96
результата. Если же этого гарантировать нельзя, то в алгоритмах следует предусмот реть соответствующие проверки.
Алгоритм объединения множеств (5.10). Количество элементов, которое может быть записано в массиве C, находится в пределах от max(K1, K2) до K1+K2.
Алгоритм пересечения множеств (5.11). Количество элементов, которое может быть записано в массиве C, не превышает min(K1, K2).
i1:=1; i2:=1; K3:=0; |
|
while (i1<=K1)and(i2<=K2) do |
|
if A[i1]<B[i2] then i1:=i1+1 |
|
else if A[i1]>B[i2] then i2:=i2+1 |
|
else |
(5.11) |
begin |
|
K3:=K3+1; C[K3]:=B[i2]; |
|
i2:=i2+1; i1:=i1+1 |
|
end |
|
Трудоемкость рассмотренных алгоритмов не превышает K1+K2. |
|
Конец примера. |
|
Если множества заданы неупорядоченными массивами, то прямое выполнение рассмотренных операций потребует алгоритмов с двойным вложенным циклом, име ющих трудоемкость K1*K2. Поэтому для выполнения какой-либо операции целесо образно предварительно упорядочить массивы (например, сортировкой по методу слияния) за K1*log2 K1+K2*log2 K2 шагов, а затем применить соответствующий ал горитм из примера 5.2.
Пример 5.3. Алгоритмы вычисления операций над множествами, заданными упорядоченными списками. Алгоритмы строятся на основе алгоритма слияния двух упорядоченных по возрастанию линейных списков (3.24), но, в отличие от него, со здают выходные списки из новых элементов.
Алгоритм объединения множеств:
new(p3); pr:=p3; pq:=p1; pt:=p2;
{создан дополнительный элемент выходного списка} while (pq<>nil)or(pt<>nil) do
{цикл, пока оба входных списка не просмотрены} begin new(ps); pr^.p:=ps; pr:=ps;
if pt=nil then {создан новый элемент выходного списка} begin pr^.s:=pq^.s; pq:=pq^.p end
else if pq=nil then (5.12) begin pr^.s:=pt^.s; pt:=pt^.p end
else if pq^.s<pt^.s then
begin pr^.s:=pq^.s; pq:=pq^.p end
97
else if pq^.s>pt^.s then
begin pr^.s:=pt^.s; pt:=pt^.p end
else begin pr^.s:=pt^.s;pt:=pt^.p;pq:=pq^.p end end;
pr^.p:=nil; pr:=p3; p3:=p3^.p; free(pr);
Вначале в алгоритме создается дополнительный пустой элемент выходного списка, а затем в цикле просматриваются очередные элементы входных списков, как в алгоритме объединения множеств (5.10). После завершения цикла дополнительный элемент выходного списка удаляется.
Указатели p1 и p2 указывают на начальные элементы двух входных списков, p3 указывает на начальный элемент выходного создаваемого списка. При этом до
пускается, чтобы как входные списки, так и выходной список были пустыми (тогда значение соответствующего указателя равно nil).
Алгоритм пересечения множеств (5.13). Здесь также вначале создается дополни тельный пустой элемент выходного списка, который в конце удаляется. Проверки в цикле аналогичны проверкам в алгоритме (5.11).
new(p3); pr:=p3; pq:=p1; pt:=p2;
{создан дополнительный элемент выходного списка} while (pq<>nil)and(pt<>nil) do
{цикл, пока оба входных списка не пусты} if pq^.s<pt^.s then pq:=pq^.p
else if pq^.s>pt^.s then pq:=pq^.p (5.13)
else begin {создан новый элемент выходного списка} new(ps); pr^.p:=ps; pr:=ps; pr^.s:=pt^.s;pt:=pt^.p;pq:=pq^.p
end;
pr^.p:=nil; pr:=p3; p3:=p3^.p; free(pr);
{удален дополнительный элемент выходного списка}
Трудоемкость рассмотренных алгоритмов не превышает суммарной длины вход ных списков.
Алгоритмы вычисления разности и симметрической разности множеств, задан ных списками, читателю предлагается написать самостоятельно.
Конец примера.
5.2 Алгоритмы обработки символьных строк
Символьную строку можно описать в программе, как одномерный массив симво лов (имеющих тип char), и тогда можно выполнять действия над отдельными сим волами, как элементами массива. Кроме того, в Паскале имеется специальный тип string, с переменными такого типа можно производить операции целиком, без вы
98
деления отдельных символов. Строки можно присваивать, склеивать (операцией +), а также сравнивать (операциями “=”, “<” и т.п.). При описании строки выделяется па мять для размещения строки с некоторой максимальной длиной (до 255 символов в Turbo Pascal), а при выполнении операций со строкой ее текущая длина может изме няться. Функция length выдает длину строки в данный момент.
Сравнение отдельных символов, каждый из которых занимает один байт, произ водится в соответствии с их внутренними кодами, т.е. порядковыми номерами в ко довой таблице. Например, при использовании кода ASCII, код символа “+” равен 43, код символа “(” равен 40. Коды символов, изображающих цифры “0”, …, “9” имеют идущие подряд номера от 48 до 57, а коды заглавных букв латинского алфавита строк имеют номера в диапазоне от 65 до 90. Сравнение двух строк символов производится
влексикографическом порядке по следующим правилам:
1)вначале сравниваются между собой первые символы обеих строк, затем вто рые символы и т.д.;
2)сравнение продолжается до первого несовпадения двух сравниваемых симво
лов, при этом большей считается та строка, код очередного сравниваемого символа
укоторой больше;
3)если первая строка полностью совпадает с началом более длинной второй строки, то первая строка считается меньшей.
Иногда при сравнении строк некоторые различные символы требуется считать равными, например, заглавные и строчные буквы. Для такого сравнения можно ис
пользовать таблицу перекодировки, которая описывается как одномерный целочис ленный массив констант T с нумерацией от 0 до 255. Значение элемента T[d] должно быть равно новому коду для символа, имеющего код d. Получить числовой
код символа, заданного в переменной типа char, можно функцией ord. Тогда
вместо непосредственного сравнения двух символьных переменных c1 и c2 необ ходимо сравнивать T[ord(c1)] и T[ord(c2)].
Пример 5.4. Сравнение символьных строк с отождествлением заглавных и строчных букв. Отождествляются буквы для латинского и русского алфавитов. Кро ме того, в русском алфавите считаются равными буквы “е” и ”ё”. Таблица перекоди ровки для кода ASCII (DOS) будет следующей (см. также приложение В):
const T:array[0..255]of byte=
( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15, 16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,
32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,
48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,
64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,
80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95, 96,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79, (5.14)
80, 81, 82, 83, 84, 85, 86, 87,
99
88, 89, 90,123,124,125,126,127,
128,129,130,131,132,133,134,135,
136,137,138,139,140,141,142,143,
144,145,146,147,148,149,150,151,
152,153,154,155,156,157,158,159,
128,129,130,131,132,133,134,135,
136,137,138,139,140,141,142,143,
176,177,178,179,180,181,182,183,
184,185,186,187,188,189,190,191,
192,193,194,195,196,197,198,199,
200,201,202,203,204,205,206,207,
208,209,210,211,212,213,214,215,
216,217,218,219,220,221,222,223,
144,145,146,147,148,149,150,151,
152,153,154,155,156,157,158,159,
133,133,242,243,244,245,246,247,
248,249,250,251,252,253,254,255);
Тип элементов в таблице описан (с целью экономии памяти) как byte, значения всех кодов принадлежат диапазону от 0 до 255.
function scomp(var s1,s2:string):integer; var i,r,n1,n2:integer;
begin n1:=length(s1); n2:=length(s2); r:=0; i:=1;
while (i<=n1)and(i<=n2)and (T[ord(s1[i])]=T[ord(s2[i])]) do i:=i+1;
if (i<=n1)and(i<=n2) then begin
if T[ord(s1[i])]<T[ord(s2[i])] then r:=-1 else r:=1
end
else if (i<=n1) then r:=-1 else if (i<=n2) then r:=1; scomp:=r
end;
(5.14’)
так как
(5.15)
Функция scomp (5.15) реализует сравнение двух строк с помощью таблицы
перекодировки (5.14).
Функция scomp выдает –1, если строка s1 меньше s2, выдает 0, если строки равны, и выдает +1, если строка s1 больше s2.
Нетрудно видеть, что трудоемкость алгоритма линейная и определяется количе ством символов в более короткой строке.
Конец примера.
100
В ряде задач используются массивы элементов типа string (строки символов). Такие массивы можно упорядочивать, в них можно производить поиск. Если при этом использовать стандартные операции сравнения строк, то алгоритмы сортировки массивов строк и алгоритмы поиска внешне ничем не будут отличаться от аналогич ных алгоритмов для числовых массивов. Если же требуется сравнение с отождествле нием заглавных и строчных букв, то все операции сравнения в алгоритмах необходи мо заменить вызовами функции scomp.
Одной из важных задач обработки символьных строк является контекстный поиск. Задача контекстного поиска состоит в том, что в строке (тексте) необходимо найти подстроку, совпадающую со строкой-образцом.
Рассмотрим простой алгоритм контекстного поиска. Пусть S – текст длиной n символов, d – строка-образец. Вначале ищется символ текста S[i], равный перво
му символу образца d[1]. При совпадении сравниваются следующие символы: S[i+1] и d[2], S[i+2] и d[3] и т.д. до конца образца. Если для всех пар об
наружится совпадение, то поиск завершится успешно, если нет, то i увеличится на единицу, и сравнение будет продолжаться. В общем случае возможны три ситуации:
1)нет ни одного полного совпадения с образцом;
2)есть только одно совпадение с образцом;
3)существует более одного совпадения с образцом, при этом возможно даже их перекрытие, как, например, в тексте “мамама” для образца “мама”.
Пример 5.5. Контекстный поиск в символьном массиве S длиной n по образ цу-строке d длиной m. Результат: i – номер символа S[i], совпадающего с пер вым символом образца. Если полного совпадения нет, то i=0.
Алгоритм (5.16) находит только одно (первое) совпадение, но его нетрудно изме нить для поиска всех совпадений.
Алгоритм имеет трудоемкость в наихудшем n ∙ m. Действительно, так как несов падение может обнаружиться при сравнении последнего, m–го символа образца, то для перехода от сравнения образца с i–м символом текста к сравнению с (i+1)–м символом текста потребуется от одного до m сравнений символов.
m:=length(d); p:=0; i:=1; while (p=0)and(i<=n-m+1) do
if (d[1]<>S[i]) then i:=i+1 else begin j:=1;
while (j<m)and(d[j+1]=S[i+j])do (5.16) j:=j+1;
if j=m then p:=1 else i:=i+1 end;
if p=0 then i:=0;
Конец примера.