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

69

Вопросы к зачету по курсу «Алгоритмы и структуры данных»:

1.Обоснование выбора подходящей структуры данных на примере задачи «Ряд Фаррея»

Любой алгоритм (хороший или плохой) требует одной или более структур данных для представления элементов проблемы, которую нужно решить, и информации, вычисляемой в процессе решения. Структура данных - это набор данных, связанных специальным образом. Хотя понятие «структура данных» (прежде всего) означает определенный способ организации взаимосвязей между компонентами (и способ представления этих взаимосвязей), но по существу подразумевает и набор операций для работы с ее компонентами.

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

Пример 1. Построение ряда Фарея.

Построить ряд Фарея Fn порядка n - последовательность всех несократимых дробей:

§ из интервала [0,1] в порядке возрастания,

§ знаменатели которых не превосходят n.

Например, для n=6: 0/1, 1/6, 1/5, 1/4, 1/3, 2/5, 1/2, 3/5, 2/3, 3/4, 4/5, 5/6, 1/1.

Можно конечно сначала построить последовательность всех дробей со знаменателем, не превосходящим n, потом отсеять сократимые и упорядочить оставшиеся. Но остается неприятный осадок – а насколько обоснована необходимость такого, в общем-то, окольного пути с не ясно насколько нужными и обременительными промежуточными вычислениями.

Можно попробовать строить последовательность сразу в требуемом порядке. Но быстро проясняется, что построение очередных элементов этого ряда связано с подбором нужного из возрастающего количества кандидатов. В теории чисел доказано, что ряд Фарея Fn можно строить, используя рекуррентное соотношение:

§ F1 = 0/1, 1/1.

§ для n>1 Fn можно построить по Fn-1: если для пары соседних, назовём их (a/b, c/d), выполнено (b+d)=n, то вставляем между ними новый (a+c)/(b+d).

6

А это уже почти программа. При этом, поскольку речь идет о конечной последовательности, представляется естественным для хранения элементов ряда использовать структуру данных массив:

VAR f:ARRAY[0..?] OF RECORD p,q:INTEGER END; BEGIN READ(n);

{Строим F1:} f[0].p:=0;f[0].q:=1;f[1].p:=1;f[1].q:=1;k:=2;

FOR i:=2 TO n DO BEGIN

{Строим Fi по Fi-1, просматривая все пары соседних:} j:=0;

WHILE j<=(k-2) DO BEGIN IF (f[j].q+f[j+1].q)=i THEN

BEGIN {вставляем новый}; j:=j+1;k:=k+1 END; j:=j+1 END

END END.

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

§ сдвинуть: f[j+1..k-1] → f[j+2..k]

§ положить: f[j+1].p:=f[j].p+f[j+1].p; f[j+1].q:=i

...причём один и тот же элемент придется перемещать (на одну позицию) много раз, а точнее ровно столько раз, сколько вставок до него понадобиться сделать, знать бы «сколько», можно было бы сразу положить «куда надо»... Можно ли устранить «повторное вычисление (работу)»? Проанализируем

ситуацию, попытаемся понять причину появления этого «повторного вычисления», возможно яснее станет, как устранить причину, а тогда возможно пропадут и последствия...

Алгоритм в явном виде использует два отношения порядка:

§ порядок возрастания элементов ряда Фарея - он необходим для проверки условия вставки и для вычисления собственно значения нового элемента;

§ порядок появления новых элементов ряда Фарея - он есть, он отличается от порядка возрастания и надо как-то где-то сохранять появляющееся. Если хранить в порядке возрастания, то получаем проблему «сдвигов», а если хранить в порядке появления, то получаем проблему «как вычислить значение нового элемента» (или проблему «подбора»). Выбранная структура представления данных задает единственный вариант перебора её элементов – в порядке индексации. Можно ли модифицировать структуру данных для хранения информации об обоих порядках? - подобрать способ хранения, подходящий и для добавления новых элементов, и для просмотра в порядке возрастания.

Ответ положительный, такой способ хранения имеется:

§ Будем хранить элементы ряда Фарея в векторе, но в порядке появления, это удобно при сохранении нового элемента.

§ А для хранения информации о порядке возрастания добавим специальное поле next, с помощью которого свяжем элементы ряда Фарея в цепочку в порядке возрастания.

Например, для n=6 – см. рисунок на следующей странице.

VAR f:ARRAY[0..?] OF RECORD p,q,next:INTEGER END; BEGIN

READ(n);

{Строим F1:} f[0].p:=0; f[0].q:=1; f[0].next:=1;

f[1].p:=1; f[1].q:=1; f[1].next:=0; k:=2;

FOR i:=2 TO n DO

{Строим Fi по Fi-1, просматривая все пары соседних в Fi-1:}

BEGIN j1:=0; j2:=f[j1].next;

WHILE j2<>0 {второй в этой паре имеется} DO BEGIN

IF (f[j1].q+f[j2].q)=i THEN {вставляем между ними:} BEGIN

7

{добавить в конец:} f[k].p:=f[j1].p+f[j2].p; f[k].q:=i;

{связать по возрастанию:} f[k].next:=j2; f[j1].next:=k;

{учесть новый:}k:=k+1

END; {к следующей паре:} j1:=j2; j2:=f[j1].next

END END END.

p q next

0 0 1 11

1 1 1 0 X

2 1 2 9

3 1 3 8

4 2 3 6

5 1 4 3

6 3 4 10

7 1 5 5

8 2 5 2

9 3 5 4

10 4 5 12

11 1 6 7

12 5 6 1

Мы подобрали структуру данных, которая позволяет: хранить последовательность и эффективно выполнять для неё операцию «вставить» (элемент в текущую позицию) без обременительных расходов на «сдвиги». Это классическая структура данных – связанный список. Но вопросы ещё остались:

§ Для этой структуры данных требуется дополнительное пространство памяти для хранения значений поля next (ориентировочно 0.5 от первоначально использованной)1.

§ Преимущество этой структуры данных в устранении расходов времени на «сдвиги» очевидно, но как оценить, имеется ли реальный выигрыш по времени в сравнении например с первоначальным алгоритмом – построить, отсеять и упорядочить? и даже если он есть, то насколько он существенный.

§ К тому же, нельзя сказать, что последний алгоритм ничего не делает лишнего. Если бы на каждый выходной элемент ряда Фарея он выполнял не более C операций (для подходящей константы C), то его эффективность представлялась бы достаточно обоснованной. Однако дела обстоят не так – для каждого i при построении Fi алгоритм просматривает все пары, а добавляет новый элемент только для некоторых их них. Чтобы конструктивно обсуждать эти вопросы и сравнивать различные подходы, нужна

подходящая мера для используемых ресурсов - времени работы и объема памяти.

Пример 2. Лексикографическая сортировка [2 п.3.2.].

Упорядочить последовательность (длины n) слов (длины k) в алфавите a..z. Обозначим через wi[m..k] последовательность символов с номерами от m до k для слова wi. Проведем рассуждение, используя классический метод последовательных уточнений:

1 Это плата за возможность поддержки единовременно двух порядков. Платить приходится за все! Весь вопрос – сколько?

8

§ Пусть последовательность W=w1w2 ... wn уже упорядочена в следующем смысле:

i≤j Þ wi[m..k]≤wj[m..k]. Т.е. в сравнении слов принимали участие только символы в позициях m..k, а символы в позициях 1..(m-1) игнорируются (смотрим на слово с правого конца, а символы после m-го просто не видим «в тумане»).

§ Распределим слова последовательности W по значению (m-1)-го символа на 26 последовательностей (карманов) Q[a..z]. Т.е. слово w попадает в карман Q[a], если (m-1)-й символ w[m-1]=’a’, в карман Q[b], если w[m-1]=’b’ и далее в алфавитном порядке. После чего соберем последовательности Q[a..z] снова в

последовательность W.

В итоге снова получим последовательность W, но уже (точнее) упорядоченную в смысле:

i≤j Þ wi[(m-1)..k]≤wj[(m-1)..k] (т.е. теперь видим подальше до (m-1)-го символа), конечно

только при соблюдении следующих условий распределения и сборки:

§ при распределении по карманам Q[a..z] добавляем слова в конец, чтобы порядок,

имеющийся в W, сохранился внутри каждого кармана;

§ при сборке карманов в исходную последовательность W сначала переписываем

слова из Q[a], потом слова из Q[b] и т.д.

Таким образом, слова в W после сборки будут упорядочены по символу в (m-1)-й позиции,

а внутри пачки с одинаковым символом в этой позиции слова будут упорядочены как в W

до распределения.

Любая исходная последовательность упорядочена в вышеописанном смысле при

m=k+1 (т.е. в начальной ситуации, когда мы совсем плохо видим...), а на каждом шаге мы

получаем более точную упорядоченность, неумолимо приближаясь к решению задачи при

m=1.

Какие структуры данных потребуются для реализации этого алгоритма:

§ W: ARRAY[1..n] OF STRING[k];

§ Q: ARRAY[‘a’..’z’] OF ARRAY[1..n] OF STRING[k];

Использование в Q такого массива строк представляется изначально избыточным. Просчитать заранее, сколько строк должен содержать один карман не удается, это определяется исходной последовательностью. В самом деле, каждый карман может оказаться пустым в результате текущего распределения, а с другой стороны – все слова из последовательности W могут попасть в один из карманов. Однако предложенный вариант опять оставляет неприятный осадок – для хранения n слов приходится резервировать пространство памяти, способное хранить 26*n слов 2.

Устранить этот необоснованный перерасход памяти можно с помощью структуры

данных «связанный список».

// Здесь будем хранить последовательность W:

W: ARRAY[1..n] OF RECORD w:STRING[k]; next:0..n END;

// В случае связного представления последовательности

// нет необходимости перемещать её элементы, достаточно

// изменять только связи между ними. Поэтому в Q будем хранить

// не элементы, а только номера позиций первого и последнего

// элемента каждого кармана. Сами элементы будут оставаться в W

// на своих местах, но связи между ними надо будет аккуратно

// корректировать в соответствии с порядком, определяемым

// внутри каждого из карманов:

Q: ARRAY[‘a’..’z’] OF RECORD first,last:0..n END;

// Сохраним входную последовательность в связанном списке W:

2 26 – число букв в используемом алфавите.

9

FOR i:=1 TO n-1 DO BEGIN READLN(W[i].w);W[i].next:=i+1 END;

READLN(W[n].w); W[n].next:=0; Wfirst:=1; Wlast:=n;

// Выполним k распределений-сборок:

FOR m:=k DOWNTO 1 DO BEGIN

// Очистим карманы:

FOR s:=’a’ TO ’z’ DO BEGIN Q[s].first:=0;Q[s].last:=0 END;

// Распределим слова последовательности W по карманам:

i:=Wfirst;

WHILE i<>0 DO BEGIN s:=W[i].w[m];

IF Q[s].first=0 THEN {карман был пустой:} Q[s].first:=i

ELSE BEGIN j:=Q[s].last; W[j].next:=i END; Q[s].last:=i;

i:=W[i].next; W[Q[s].last].next:=0

END; // Теперь вместо одной последовательности, начинающейся

// в позиции с номером Wfirst, имеем несколько

// последовательностей, начинающихся в позициях

// Q[s].first (для s: a..z)

// Проведем сборку W, причем учтем, что нет необходимости

// корректировать все связи, достаточно только связать

// последний элемент каждого кармана с первым элементом

// следующего кармана:

Wfirst:=0; Wlast:=0;

FOR s:=‘a’ TO ’z’ DO BEGIN j:=Q[s].first;

IF j<>0 THEN BEGIN {карман не пустой:}

IF Wfirst=0 THEN Wfirst:=j ELSE W[Wlast].next:=j;

Wlast:=Q[s].last

END

END END

Мы подобрали структуру данных, которая позволяет устранить вышеотмеченный необоснованный перерасход памяти, правда дополнительное пространство памяти для организации связного спискового представления последовательностей всё же потребуется.

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