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

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

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

51

2)условие в строчке 3 ложно, но условие в строчке 5 истинно;

3)условия в строчках 3 и 5 ложны, но условие в строчке 7 истинно;

4)условия в строчках 3, 5 и 7 ложны.

Детали доказательства оставляем на усмотрение читателя.

Пример работы алгоритма слияния. В массиве A содержится 3 элемента:

{5, 13, 13}, а в массиве B

4 элемента: {7, 9, 10, 12}. В табл. 3.1 по шагам показа­

но изменение переменных i, i1, i2 и действия с массивами.

 

 

 

 

 

 

 

Таблица 3.1

 

 

 

 

 

 

 

 

 

 

 

 

 

i

i1

 

i2

 

Сравниваются

 

C[i]

 

 

1

1

 

1

 

A[1]=5 и B[1]=7

 

5

 

 

2

2

 

1

 

A[2]=13 и B[1]=7

 

7

 

 

3

2

 

2

 

A[2]=13 и B[2]=9

 

9

 

 

4

2

 

3

 

A[2]=13 и B[3]=10

 

10

 

 

5

2

 

4

 

A[2]=13 и B[4]=12

 

12

 

 

6

2

 

5

 

B весь переписан

 

13

 

 

7

3

 

5

 

B весь переписан

 

13

 

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

Следующий алгоритм из упорядоченного по неубыванию массива, в котором мо­ гут встречаться расположенные подряд элементы с одинаковым значением, формиру­ ет упорядоченный строго по возрастанию массив, содержащий все различные значе­ ния из исходного массива.

Пример 3.13. Алгоритм удаления повторяющихся элементов из упорядоченного массива. Входной массив A содержит n элементов, результирующий массив C бу­ дет содержать m элементов.

 

 

 

m:=1; C[1]:=A[1];

{1}

 

for i:=2 to n do

{2}

 

if C[m]<A[i] then

{3}

 

begin

{4}

(3.14)

m:=m+1;

{5}

 

C[m]:=A[i]

{6}

 

end

{7}

 

Доказательство правильности алгоритма оставляем в качестве упражнения. Заме­ тим лишь, что если в 3-й строчке алгоритма операцию ’<’ заменить операцией ’<>’, то алгоритм сможет правильно обработать также упорядоченные по невозрас­ танию массивы.

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

52

А теперь рассмотрим задачу поиска. Пусть задан массив A из n элементов и

поисковое значение p. Требуется найти такой номер i элемента массива, для кото­ рого A[i]=p. В результате поиска может получиться одна из трех ситуаций:

1)существует единственный элемент с номером i, для которого A[i]=p;

2)A[i]≠p при любых i=1,2,...,n;

3) существует несколько элементов с номерами i1,i2,... таких, что

A[i1]=p,A[i2]=p,...

В первом и третьем случае поиск считается успешным, причем в третьем случае

очень часто достаточно найти хотя бы один элемент, равный поисковому значению. Будем считать, что если поиск успешный, то 1≤i≤n, а если нет, то i=0.

Пример 3.14. Алгоритм поиска одного элемента в неупорядоченном массиве:

i:=0; k:=1; while k<=n do

if A[k]=p then (3.15) begin i:=k; k:=n+1 end

else k:=k+1;

Доказательство правильности алгоритма оставляем в качестве упражнения. Его необходимо провести для двух случаев (когда в массиве есть хотя бы один искомый элемент, и когда такого элемента нет).

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

Оказывается, поиск можно значительно ускорить, если массив упорядочен. Идея быстрого алгоритма поиска состоит в том, что в массиве выделяется такая область «подозрительных» элементов, что если искомый элемент в массиве существует, он обязательно будет внутри этой области. Каждое сравнение выполняется таким об­

разом, чтобы область сокращалась вдвое.

Пусть область подозрительных элементов: A[b], A[b+1], ...,A[e]. Вна­

чале b=1,e=n. Сравнение поискового значения p производится с элементом A[c], где c=(b+e)div 2, т.е. с элементом в середине области. При этом возмож­

ны два исхода:

1)A[c]<p. Искомый элемент, если он существует, расположен среди элементов

A[c+1],...,A[e];

2)A[c]≥p. Искомый элемент, если он существует, расположен среди элементов

A[b],...,A[c].

В первом случае можно положить b=c+1, во втором e=c. Сравнения следует продолжать, пока подозрительная область не сожмется до одного элемента. После этого придется провести сравнение с этим единственным элементом, из-за того, что в массиве может не существовать ни одного элемента, равного p. Так как каждое сравнение уменьшает область в два (или почти в два) раза, алгоритм получил назва­ ние дихотомический поиск, или поиск методом деления пополам.

53

Пример 3.15. Алгоритм (3.16) реализует дихотомический поиск в упорядочен­ ном массиве.

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

щимся!

 

 

 

 

 

 

 

b:=1; e:=n;

{1}

 

 

while b<e do

{2}

 

 

begin

{3}

 

 

c:=(b+e)div 2;

{4}

 

 

if A[c]<p then b:=c+1

{5}

(3.16)

 

else e:=c

{6}

 

 

end;

{7}

 

 

if A[b]=p then i:=b

{8}

 

 

else i:=0

{9}

 

Доказательство правильности алгоритма можно провести методом инварианта для двух случаев (когда в массиве есть хотя бы один искомый элемент, и когда тако­ го элемента нет). При доказательстве завершимости надо проверить, что подозри­ тельная область на каждом шаге уменьшается, даже когда она содержит всего два элемента. Заметим, что при небрежной записи алгоритм легко сделать зацикливаю­ щимся!

Оценим трудоемкость алгоритма. Пусть целое m таково, что:

 

2m – 1 < k ≤ 2m ,

(3.20)

где k – размер подозрительной области, причем вначале k = n. При четном

k раз­

меры области уменьшаются ровно в два раза, а при нечетном – в два с округлением в большую или меньшую сторону. На каждом шаге m уменьшается на 1, и неравен­ ство (3.20) выполняется. Поэтому понадобится не более élog2 nù сравнений, чтобы

длина области стала равна 1. Учитывая еще одно сравнение после цикла, получим об­ щее количество сравнений C в наихудшем случае:

C = élog2 nù +1 .

(3.21)

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

 

Как видно из табл. 2.1, преимущество алгоритма дихотомического поиска

(3.16)

перед алгоритмом поиска (3.15) будет тем больше, чем больше размер массива.

На практике массивы сортируют как раз для того, чтобы ускорить последующий многократный поиск. Заметим, что если поиск будет проводиться всего один раз, то, сортируя массив, мы получим не ускорение, а замедление работы, так как трудоем­

54

кость лучших алгоритмов сортировки имеет порядок n log2 n, что намного больше трудоемкости поиска в неупорядоченном массиве.

Оба рассмотренных алгоритма поиска находят только один элемент массива, рав­ ный поисковому значению, причем элемент с наименьшим номером. Для алгоритма (3.15) это очевидно, для алгоритма (3.16) этот факт легко доказать. Иногда требуется найти все элементы, равные поисковому значению. В упорядоченном массиве эти

элементы располагаются подряд. Если в алгоритме (3.16)

в строчках 4–6 записать

действия в следующем виде:

 

c:=(b+e+1)div 2;

{4}

if A[c]<=p then b:=c

{5}

else e:=c-1

{6}

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

Рассмотрим еще одну задачу, в которой используется понятие упорядоченности. В заданном числовом массиве A из n элементов требуется определить начало и ко­ нец упорядоченного отрезка в нем, имеющего максимальную длину. Заметим, что минимальная длина такого отрезка равна единице, и что такие отрезки не могут перекрываться. Если некоторый отрезок заканчивается на i–м элементе массива, то следующий за ним отрезок начинается с (i+1)–го элемента. При этом последний от­ резок заканчивается концом массива.

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

 

 

 

b:=1; bm:=1; em:=1;

{1}

 

for i:=i to n do

{2}

 

if (i=n)or(A[i]>A[i+1]) then

{3}

 

begin {обнаружен конец очередного отрезка} {4}

(3.17)

if (i-b)>(em-bm) then

{5}

 

begin bm:=b; em:=i end;

{6}

 

b:=i+1

{7}

 

end

{8}

 

Доказательство правильности алгоритма можно провести методом инварианта, рассмотрев случай, когда условие в строчке 3 истинно, и случай, когда это условие ложно.

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

При решении различных задач с упорядоченными массивами иногда использует­

ся понятие косвенной упорядоченности. При этом сам массив

A

из n элементов

остается неупорядоченным, но задается дополнительный массив

I,

называемый ин­

дексным, для которого выполняются соотношения косвенной упорядоченности:

A[I[1]]≤ A[I[2]]≤ ... ≤A[I[n]].

 

(3.22)

55

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

1) элементам массива I присвоить начальные значения:

I[1]=1, I[2]=2, …, I[n]=n;

2)везде в алгоритме упорядочения, где элемент массива A[j] используется в операции сравнения, заменить A[j] на A[I[j]];

3)везде в алгоритме упорядочения, где элемент массива A[j] используется в присваивании, заменить A[j] на I[j].

Доказательство правильности полученного при этом алгоритма оставляем в каче­ стве упражнения.

3.3 Алгоритмы с последовательными файлами

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

Во всех современных языках программирования предусмотрены средства работы с файлами, однако они несколько различаются для разных языков, компьютеров и трансляторов. Далее мы рассмотрим работу с файлами в системе Turbo Pascal для компьютеров с системой команд Х86 при использовании операционных систем MS DOS или различных версий Windows.

В программе текстовый файл описывается как особая переменная, имеющая

файловый тип text. Описание для этого может быть, например, следующим: var F:text;

Вначале необходимо связать файловую переменную F с файлом, находящимся на диске (пусть имя этого файла dan1.dat). Связь устанавливается процедурой assign (назначить):

assign(F,'dan1.dat');

Затем необходимо открыть файл для чтения. Для этого предназначена проце­ дура reset (установить):

reset(F);

56

После этого можно читать данные в память процедурой read (читать): read(F,...);

В конце работы файл нужно закрыть процедурой close (закрыть): close(F);

Рассмотрим простой пример чтения данных из файла.

Пример 3.17. Программа чтения данных из последовательного текстового файла. В файле dan1.dat вначале записано количество числовых значений, а затем сами

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

var F: text; n,i: integer; s,a: real;

begin assign(F,'dan1.dat'); reset(F);

read(F,n);

s:=0; (3.18) for i:=1 to n do

begin read(F,a); s:=s+a

end;

close(F); writeln(’Сумма = ’,s)

end.

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

Если количество входных данных в файле заранее неизвестно, то перед вводом очередной порции данных можно проверить, не достигнут ли конец файла. Это мож­ но сделать функцией eof (end of file – конец файла):

eof(F),

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

Мы рассмотрели действия с входным файлом. Выходной файл, связанный с файловой переменной F, открывается процедурой rewrite (перезаписать):

rewrite(F);

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

57

случаях данные будут записываться с самого начала файла. Непосредственно запись данных из памяти в файл производится процедурой write (записать):

write(F,...);

Выходной файл можно открыть также процедурой append (добавить): append(F);

В этом случае, если файл, связанный с файловой переменной F, уже существу­ ет, то все имеющиеся в нем данные сохраняются, а последующая запись ведется по­

сле имеющихся в файле данных. Если же такого файла нет, то он создается, так же как в процедуре rewrite.

Рассмотрим пример чтения символьных данных из одного файла и записи их в другой файл.

Пример 3.18. Программа чтения данных из последовательного текстового файла и записи их в другой последовательный текстовый файл. В файле dan2.dat записа­

ны произвольные символы (текст). Программа читает эти символы и записывает в файл dan3.dat, а также подсчитывает и выводит на экран их количество.

Описания в программе:

var F1,F2: text;

n: integer; (3.19) c: char;

Основной алгоритм:

assign(F1,'dan2.dat');

assign(F2,'dan3.dat'); reset(F1); rewrite(F2); n:=0;

while not eof(F1) do

begin read(F1,c); (3.19') write(F2,c);

n:=n+1

end;

close(F1); close(F2); writeln(’Длина файла = ’,n)

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

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

file (нетипизированный файл). Работа с ним несколько отличается от работы с тек­ стовым файлом. Для такого файла в процедуре reset или rewrite при откры­

58

тии файла указывается второй параметр – длина записи (логической единицы) счи­ тываемой или записываемой информации в байтах.

Для чтения и записи в нетипизированных файлах используются процедуры blockread и blockwrite соответственно. В них указываются по три параметра:

1) файл, 2) переменная для чтения или записи, 3) длина читаемой или записываемой информации в байтах.

Пример 3.19. Программа копирования произвольных двоичных данных из по­

следовательного нетипизированного файла в другой файл такого же типа. Файл dan2.dat – входной, файл dan3.dat – выходной.

var F1,F2: file; c: char;

begin assign(F1,'dan2.dat'); assign(F2,'dan3.dat'); reset(F1,1); rewrite(F2,1);

while not eof(F1) do (3.20) begin blockread(F1,c,1);

blockwrite(F2,c,1);

end;

close(F1); close(F2); end.

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

3.4 Алгоритмы с динамическими массивами и со списками

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

Частично проблема выделения памяти для массива может быть решена с помо­ щью средств динамического распределения памяти. (Это возможно, если в алгоритме размер массива можно точно вычислить перед его использованием.) В Турбо Паскале для этого требуется описать тип данных – массив с максимально возможной размер­ ностью и указатель на него. Пример такого описания:

type masint=array[1..32766]of integer; pmasint=^masint;

59

В примере учтено, что переменная типа integer занимает 2 байта, а макси­

мальный размер любого массива должен быть меньше 65536 байт. После этого мож­ но описывать переменные-указатели (типа pmasint):

var pm1,pm2: pmasint;

Далее в программе с помощью стандартной процедуры getmem можно выде­ лить память требуемого объема, на которую ссылается соответствующая переменнаяуказатель, например:

getmem(pm1,2*n);

getmem(pm2,2*m);

Теперь доступен один массив из n элементов (типа integer), адресуемый че­ рез переменную-указатель pm1, и один массив из m элементов, адресуемый через pm2 (понятно, что переменным n и m предварительно должны быть присвоены ка­ кие-то значения). Обращение к элементам этих массивов производится через указа­ тели, например:

pm1^[i]:=-5; pm2^[j]:=pm1^[i]+10;

Символ “^” обозначает переход от указателя к объекту, на который тот указы­ вает. Когда работа с массивами заканчивается, занятую ими память следует освобо­ дить (чтобы позже использовать для других массивов):

freemem(pm1,2*n);

freemem(pm2,2*m);

Замечание 1. В системе Turbo Pascal предусмотрена опция “Range check error”, которую можно включить, и тогда при любом обращении к элементам массива будет происходить проверка на допустимость значений индексов. Однако для динамиче­ ских массивов, для которых тип описан с максимально возможным количеством эле­

ментов, включение опции не дает никакого эффекта. Так, для приведенного выше примера, если записать pm1^[2*n+1], произойдет обращение к несуществующему

элементу массива, но эта ошибка не будет обнаружена.

Замечание 2. Если требуется использовать массив с размерностью 2 и выше, то динамическую память все равно придется выделять для одномерного массива, длина которого равна количеству элементов в исходном массиве. При этом полный одно­ мерный индекс для отображения исходного массива на динамическом массиве при­ дется вычислять «вручную». Так, например, если требуется использовать двумерный массив A из n строк и m столбцов с нумерацией от 1 по каждому из индексов, то вначале необходимо выделить память для динамического массива из n*m элементов

с нумерацией от 1, адресуемого указателем pD. Далее везде вместо A[i,j] следу­ ет писать pD^[(i-1)*m+j].

Конец замечаний.

60

Если в задаче необходимо многократно удалять или вставлять отдельные элемен­ ты массива, то эффективнее и удобнее использовать не массивы, а списочные струк­ туры (которые для краткости называют просто списками).

Линейный список представляет собой последовательно соединенные элементы списка. Каждый элемент списка состоит из двух частей: содержимого и указателя. Так как содержимое определяется решаемой задачей и может быть любого типа, то в целом элемент списка представляется структурой данных, называемой записью (record). Запись содержит несколько полей, типы которых могут быть различны­ ми. Каждое поле записи обозначается отдельным именем. Пример описания типов данных для элементов списка:

type pel=^elem;

elem=record s:integer; p:pel end;

Здесь s – содержимое, p – указатель на следующий элемент списка.

Для работы с элементами списка необходимо описать несколько переменныхуказателей, например:

var p1,p2,p3: pel;

С помощью стандартной процедуры new можно многократно выделять память для элементов списка. При этом на создаваемые элементы списка будут ссылаться соответствующие переменные-указатели, например:

new(p1); new(p2);

Далее содержимому созданных элементов списка можно присвоить какие-либо значения, а сами элементы объединить в последовательность, присваивая указателю в первом элементе списка ссылку на второй элемент, а указателю во втором элементе

– нулевую ссылку nil, которая означает, что второй элемент является последним в линейном списке:

p1^.s:=5; p1^.p:=p2; p2^.s:=10; p2^.p:=nil;

Если в последнем присваивании записать: p2^.p:=p1, то получится цикличе­ ский список, в котором из последнего элемента можно по ссылке перейти к первому элементу. Полученные при этом списки (из двух элементов каждый) на рис. 3.1 пред­ ставлены в графическом виде.

 

5

 

 

 

 

10

 

 

5

 

 

 

 

10

 

 

 

 

 

 

 

 

 

 

 

 

 

p1

p2

p1

p2