Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Костюк - Основы программирования

.pdf
Скачиваний:
134
Добавлен:
30.05.2015
Размер:
1.3 Mб
Скачать

Глава 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 таким, что KL<<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;

Конец примера.