По первому тесту выполнил дважды задания. Результат получается правильный (проверял калькулятором). Пишет, что "Задание не проверено" и предлагает повторить. |
Использование языка Free Pascal для обработки массивов
5.11 Использование указателей для работы с динамическими массивами
Все объявленные в программе статические переменные, которые мы рассматривали до этого момента, размещаются в одной непрерывной области оперативной памяти, которая называется сегментом данных. Для работы с массивами большой размерности можно воспользоваться так называемой динамической памятью, которая выделяется программе после запуска программы на выполнение. Размер динамической памяти можно варьировать в широких пределах. По умолчанию этот размер определяется всей доступной памятью ПК.
Динамическое размещение данных осуществляется компилятором непосредственно в процессе выполнения программы. При динамическом размещении заранее не известно количество размещаемых данных. Кроме того, к ним нельзя обращаться по именам, как к статическим переменным.
Оперативная память ПК представляет собой совокупность элементарных ячеек для хранения информации — байтов, каждый из которых имеет собственный номер. Эти номера называются адресами, они позволяют обращаться к любому байту памяти.
5.11.1 Работа с динамическими переменными и указателями
Free Pascal имеет гибкое средство управления памятью — указатели.
Указатель — переменная, которая в качестве своего значения содержит адрес байта памяти.
Как правило, указатель связывается с некоторым типом данных. В таком случае он называется типизированным. Для его объявления используется знак ^, который помещается перед соответствующим типом, например:
type massiv=array [ 1.. 2500 ] of real; var a :^ integer; b, c :^ real; d :^ massiv;
В языке Free Pascal можно объявлять указатель, не связывая его с конкретным типом данных. Для этого служит стандартный тип pointer, например:
var p, c, h : pointer;
Указатели такого рода будем называть нетипизированными. Поскольку нетипизированные указатели не связаны с конкретным типом, с их помощью удобно динамически размещать данные, структура и тип которых меняются в ходе работы программы.
Значениями указателей являются адреса переменных памяти, поэтому следовало бы ожидать, что значение одного из них можно передавать другому. На самом деле это не совсем так. Эта операция проводится только среди указателей, связанных с одними и теми же типами данных.
Например:
var p1, p2 :^ integer; p3 :^ real; pp : pointer;
В этом случае присваивание p1:=p2; допустимо, в то время как p1:=p3; запрещено, поскольку p1 и p3 указывают на разные типы данных. Это ограничение не распространяется на нетипизированные указатели, поэтому можно записать pp:=p3; p1:=pp; и достичь необходимого результата.
Вся динамическая память в Паскале представляет собой сплошной массив байтов, называемый "кучей". Физически куча располагается за областью памяти, которую занимает тело программы.
Начало кучи хранится в стандартной переменной heaporg, конец — в переменной heapend. Текущая граница незанятой динамической памяти хранится в указателе heapprt.
Память под любую динамическую переменную выделяется процедурой new, параметром обращения к которой является типизированный указатель. В результате обращения последний принимает значение, соответствующее динамическому адресу, начиная с которого можно разместить данные, например:
var i, j :^ integer; r :^ real; begin new( i ); new(R); new( j );
В результате выполнения первого оператора указатель i принимает значение, которое перед этим имел указатель кучи heapprt. Сам heapprt увеличивает своё значение на два, так как длина внутреннего представления типа integer, связанного с указателем i, составляет 4 байта. Оператор new(r) вызывает ещё одно смещение указателя heapprt, но уже на 8 байт, потому что такова длина внутреннего представления типа real. Аналогичная процедура применяется и для переменной любого другого типа. После того как указатель стал определять конкретный физический байт памяти, по этому адресу можно разместить любое значение соответствующего типа, для чего сразу за указателем без каких-либо пробелов ставится значок ^, например:
i ^:=4+3; j ^:=17; r ^:=2 * p i;
Таким образом, значение, на которое указывает указатель, то есть собственно данные, размещённые в куче, обозначаются значком ^, который ставится сразу за указателем. Если за последним этот значок отсутствует, то имеется в виду адрес, по которому размещаются данные. Динамически размещённые данные (но не их адрес!) можно использовать для констант и переменных соответствующего типа в любом месте, где это допустимо, например:
r ^:= sqr ( r^)+ sin ( r^+i ^) _2.3
Невозможен оператор
r := sqr ( r^)+ i ^;
так как указателю r нельзя присвоить значение вещественного типа.
Точно так же недопустим оператор
r ^:= sqr ( r );
поскольку значением указателя r является адрес, и его (в отличие от того значения, которое размещено по данному адресу) нельзя возводить в квадрат. Ошибочным будет и присваивание r^:=i, так как вещественным данным, на которые указывает r^, нельзя давать значение указателя (адрес). Динамическую память можно не только забирать из кучи, но и возвращать обратно. Для этого используется процедура dispose(p), где р — указатель, который не изменяет значение указателя, а лишь возвращает в кучу память, ранее связанную с указателем.
При работе с указателями и динамической памятью необходимо самостоятельно следить за правильностью использования процедур new, dispose и работы с адресами и динамическими переменными, так как транслятор эти ошибки не контролирует. Ошибки этого класса могут привести к зависанию компьютера, а то и к более серьёзным ошибкам!
Другая возможность состоит в освобождении целого фрагмента кучи. С этой целью перед началом выделения динамической памяти текущее значение указателя heapptr запоминается в переменной-указателе с помощью процедуры mark. Теперь можно в любой момент освободить фрагмент кучи, начиная с того адреса, который запомнила процедура mark, и до конца динамической памяти. Для этого используется процедура release.
Процедура mark запоминает текущее указание кучи heapptr (обращение с помощью mark(ptr), где ptr — указатель любого типа, в котором будет возвращено текущее значение heapptr). Процедура release(ptr), где ptr — указатель любого типа, освобождает участок кучи от адреса, хранящегося в указателе до конца кучи.
5.11.2 Работа с динамическими массивами с помощью процедур getmem и freemem
Для работы с указателями любого типа используются процедуры getmem, freemem. Процедура getmem(p,size), где р — указатель, size — размер в байтах выделяемого фрагмента динамической памяти (size типа word), резервирует за указателем фрагмент динамической памяти требуемого размера.
Процедура freemem(p,size), где р — указатель, size — размер в байтах освобождаемого фрагмента динамической памяти (size типа word), возвращает в кучу фрагмент динамической памяти, который был зарезервирован за указателем. При применении процедуры к уже освобождённому участку памяти возникает ошибка.
После рассмотрения основных принципов и процедур работы с указателями возникает вопрос: а зачем всё это нужно? В основном, для того, чтобы работать с так называемыми динамическими массивами. Последние представляют собой массивы переменной длины, память под которые может выделяться (и изменяться) в процессе выполнения программы, как при каждом новом её запуске, так и в разных её частях. Обращение к i-му элементу динамического массива х имеет вид x^[i].
Рассмотрим процесс функционирования динамических массивов на примере решения следующей задачи.
Вспомним решение задачи традиционным способом.
program din_mas1; var x : array [ 1.. 150 ] of real; i, n : integer; max, min : real; begin writeln ( ’введите размер массива ’ ); readln ( n ); for i :=1 to N do begin write ( ’ x [ ’, i, ’ ]= ’ ); readln ( x [ i ] ); end; max:=x [ 1 ]; min:=x [ 1 ]; for i :=2 to N do begin if x [ i ] > max then max:=x [ i ]; if x [ i ] < min then min:=x [ i ]; end; writeln ( ’максимум= ’,max : 1 : 4 ); writeln ( ’минимум= ’, min : 1 : 4 ); end.
Теперь рассмотрим процесс решения задачи с использованием указателей. Распределение памяти проводим с помощью процедур new—dispose (программа din_mas2) или getmem—freemem (программа din_mas3).
program din_mas2; type massiw= array [ 1.. 150 ] of real; var x :^ massiw; i, n : integer; max, min : real; begin {Выделяем память под динамический массив из 150 вещественных чисел.} new( x ); writeln ( ’Введите размер массива ’ ); readln ( n ); for i :=1 to n do begin write ( ’ x ( ’, i, ’ )= ’ ); readln ( x ^[ i ] ); end; max:=x ^ [ 1 ]; min:=x ^ [ 1 ]; for i :=2 to n do begin if x ^[ i ] > max then max:=x ^[ i ]; if x ^[ i ] < min then min:=x ^[ i ]; end; writeln ( ’максимум= ’,max : 1 : 4, ’ минимум= ’, min : 1 : 4 ); {Освобождаем память.} dispose ( x ); end.
program din_mas3; type massiw=array [ 1.. 150 ] of real; var x :^ massiw; i, n : integer; max, min : real; begin writeln ( ’Введите размер массива ’ ); readln ( n ); {Выделяем память под n элементов массива.} getmem( x, n * sizeof ( real ) ); for i :=1 to n do begin write ( ’ x ( ’, i, ’ )= ’ ); readln ( x ^[ i ] ); end; max:=x ^ [ 1 ]; min:=x ^ [ 1 ]; for i :=2 to n do begin if x ^[ i ] > max then max:=x ^[ i ]; if x ^[ i ] < min then min:=x ^[ i ]; end; writeln ( ’максимум= ’,max : 1 : 4, ’ минимум= ’, min : 1 : 4 ); {Освобождаем память.} freemem ( x, n * sizeof ( real ) ); end.
При работе с динамическими переменными необходимо соблюдать следующий порядок работы:
- Описать указатели.
- Выделить память под массив (функции new или getmem).
- Обработать динамический массив.
- Освободить память (функции dispose или freemem).
5.12 Примеры программ
Решение задачи заключается в следующем. Последовательно перебираются элементы массива А. Если среди них находятся отрицательные, то они записываются в массив В. На рисунке 5.32 видно, что первый отрицательный элемент хранится в массиве А под номером три, второй и третий под номерами пять и шесть соответственно, а четвёртый под номером восемь. В массиве В этим элементам присваиваются номера один, два, три и четыре.
Поэтому для их формирования необходимо определить дополнительную переменную. В блок-схеме, приведённой на рисунке 5.33 роль такой переменной выполняет переменная m. В процессе формирования массива B в переменной m хранится номер сформированного элемента. Вначале в массиве B нет ни одного элемента, и поэтому m=0 (блок 2). В цикле (блок 5) последовательно перебираем все элементы A, если очередной элемент массива A отрицателен, то переменная m увеличивается на единицу, а значение элемента массива А записывается в массив В под номером m (блок 6). В блоке 7 проверяем, были ли в массиве A отрицательные элементы и сформирован ли массив B. В результате этого алгоритма будет сформирован массив B отрицательных чисел, состоящий из m чисел.
Приведённая ниже программа реализует описанный алгоритм.
var a, b : array [ 1.. 200 ] of word; k,m, i : byte; begin write ( ’введите размерность массива к= ’ ); readln ( k ); m: = 0; for i :=1 to k do begin write ( ’A[ ’, i, ’ ]= ’ ); readln (A[ i ] ); if A[ i ]<0 then begin m:=m+1; B[m] : =A[ i ]; end; end; if m>0 then for i :=1 to m do write (B[ i ], ’ ’ ) else write ( ’В массиве нет отрицательных элементов ! ! ! ’ ); end.
Алгоритм решения этой задачи основывается на алгоритме перезаписи элементов, удовлетворяющих какому-либо условию из одного массива, в другой, который был подробно рассмотрен в предыдущей задаче. Блок-схема решения задачи 5.4 представлена на рис. 5.34.
Текст программы с комментариями приведён ниже.
program mas_four; var y, z : array [ 1.. 50 ] of integer; i, k, n : integer; begin writeln ( ’Введите n<=50 ’ ); readln ( n ); for i :=1 to n do //Ввод массива y. begin write ( ’ y [ ’, i, ’ ]= ’ ); readln ( y [ i ] ); end; k : = 0; //Перезапись отрицательных чисел из массива y в массив z. for i :=1 to n do if y [ i ] < 0 then begin k:=k+1; z [ k ] : = y [ i ]; end; //Перезапись положительных чисел из массива y в массив z. for i :=1 to n do if y [ i ] >0 then begin k:=k+1; z [ k ] : = y [ i ] end; //Перезапись нулевых чисел из массива y в массив z. for i :=1 to n do if y [ i ]=0 then begin k:=k+1; z [ k ] : = y [ i ]; end; //Вывод массива y. writeln ( ’Массив y : ’ ); for i :=1 to n do write ( y [ i ], ’ ’ ); writeln; //Вывод массива z. writeln ( ’Массив z : ’ ); for i :=1 to n do write ( z [ i ], ’ ’ ); writeln; end.
Алгоритм решения задачи состоит в следующем: меняем местами 1-й и n-й элементы, затем 2-й и n-1-й элементы, и так до середины массива; элемент с номером i следует поменять с элементом n+1-i. Блок-схема обмена элементов в массиве представлена на рис. 5.35.
Ниже приведён текст консольного приложения задачи 5.5.
program mas_five; type massiv=array [ 1.. 100 ] of real; var x : massiv; i, n : integer; b : real; begin //Ввод размера массива. writeln ( ’Введите размер массива ’ ); readln ( n ); //Ввод массива. for i :=1 to n do begin write ( ’ x [ ’, i, ’ ]= ’ ); readln ( x [ i ] ); end; //Перебирается первая половина массива, и меняются местами //элементы 1-й c n–м, 2–й с (n-1),... i-й c (n+1-i)-м. for i :=1 to n div 2 do begin b:=x [ n+1 - i ]; x [ n+1 - i ] : = x [ i ]; x [ i ] : = b; end; //Вывод преобразованного массива. writeln ( ’Преобразованный массив ’ ); for i :=1 to n do write ( x [ i ] : 1 : 2, ’ - ’ ); end.
Вначале количество нулевых элементов равно нулю (k=0). Последовательно перебираем все элементы массива. Если встречается нулевой элемент, то количество нулевых элементов увеличиваем на 1 (k:=k+1). Если количество нулевых элементов меньше или равно 4, то удаляем очередной нулевой элемент. Если встречаем пятый нулевой элемент (k>4), то аварийно выходим из цикла (дальнейшая обработка массива бесполезна).
Блок-схема представлена на рис. 5.36.
Текст программы с комментариями приведён ниже.
const n=20; var X: array [ 1.. n ] of byte; k, i, j : integer; begin for i :=1 to n do readln (X[ i ] ); k : = 0; {Количество нулевых элементов.} j : = 1; {Номер элемента в массиве Х.} while j<=n do {Пока не конец массива.} begin if x [ j ]=0 then {Если нашли нулевой элемент, то} begin k:=k+1; {посчитать его номер} if k>4 then break {Если k превышает 4, то выйти из цикла.} Else {Иначе удаляем j-й элемент из массива.} for i := j to n_k do X[ i ] : =X[ i + 1 ]; End {Если встретился ненулевой элемент, то просто переходим к следующему.} else j := j +1; {Если элемент ненулевой.} end; {Вывод на печать измененного массива.} for i :=1 to n-k do write (X[ i ], ’ ’ ); end.
Идея алгоритма состоит в следующем. Вначале сумма равна 0. Последовательно перебираем все элементы; если очередной элемент простой, то добавляем его к сумме. Для проверки, является ли число простым, напишем функцию prostoe. Блок-схема этой функции представлена на рис. 5.37.
Заголовок функции Prostoe имеет вид:
function Prostoe (N: integer ) : boolean;
Функция возвращает значение true, если число N является простым. В противном случае результатом функции является значение false.
Блок-схема решения задачи 5.7 изображена на рис. 5.38.
Ниже приведён текст программы, реализующей этот алгоритм, с комментариями.
program mas7; function prostoe (N: integer ) : boolean; var i : integer; pr : boolean; begin if N<1 then pr := false else begin {Предположим, что текущее число простое.} pr := true; for i :=2 to N div 2 do if (N mod i = 0) then {Если найдется хотя бы один делитель кроме единицы и самого числа, то} begin {изменить значение логической переменной} pr := false; {и досрочно выйти из цикла.} break; end; end; prostoe := pr; end; var c : array [ 1.. 50 ] of word; i, n : byte; S : word; begin write ( ’Введите размерность массива n= ’ ); readln ( n ); for i :=1 to n do begin write ( ’Введите ’, i, ’й - элемент массива ’ ); readln (C[ i ] ); end; S : = 0; for i :=1 to n do {Если число простое, то накапливать сумму.} if prostoe (C[ i ] ) then S:=S+C[ i ]; {Вывод найденной суммы.} writeln ( ’Сумма простых чисел массива S= ’, S ); end.
ЗАДАЧА 5.8. Определить, есть ли в заданном массиве серии элементов, состоящих из знакочередующихся чисел (рис. 5.39). Если есть, то вывести на экран количество таких серий.
Идея алгоритма состоит в следующем. Вначале количество серий (kol) равно нулю, а длина серии (k) равна единице9Это связано с тем, что минимальное количество элементов в серии равно 2.. Последовательно в цикле от 1 до n-1 сравниваются соседние элементы (первый и второй, второй и третий,..., предпоследний и последний). Если произведение соседних элементов отрицательно, то количество элементов в серии10Количество подряд идущих знакочередующихся элементов. увеличиваем на 1. Если произведение неотрицательно, то возможны два варианта: либо сейчас оборвалась серия из знакочередующихся элементов, либо встретилась очередная пара элементов одного знака. Выбор из этих двух вариантов можно осуществить, сравнив переменную k с единицей. Если k>1, то это означает, что серия знакочередующихся элементов оборвалась и количество серий kol надо увеличить на 1, и после обрыва серии количество элементов в ней положить равным одному (k=1). После выхода из цикла проверяем, не было ли в конце массива серии из знакочередующихся элементов. Если такая серия была (k>1), то количество серий (kol) опять увеличиваем на 1. В завершении выводим количество серий из знакочередующихся элементов — переменную kol. Блок-схема решения задачи 5.8 представлена на рис. 5.40.
Ниже приведён текст консольного приложения на языке Free Pascal.
var x : array [ 1.. 50 ] of real; n, i, k, kol : integer; begin write ( ’ n= ’ ); readln ( n ); for i :=1 to n do read ( x [ i ] ); {Так как минимальная серия состоит из двух элементов,} {k присвоим значение 1.} k : = 1; {Длина серии.} kol : = 0; {Количество серий в массиве.} for i :=1 to n-1 do {Если при умножении двух соседних элементов результат - отрицательное} {число, то элементы имеют разный знак.} if x [ i ] * x [ i +1]<0 then k:=k+1 {Показатель продолжения серии.} else begin {Если серия разорвалась, то увеличить счётчик подсчёта количества} {серий.} if k>1 then kol := kol +1; {Подготовить показатель продолжения серии} {к возможному появлению следующей серии.} k : = 1; end; {Проверка, не было ли серии в конце массива.} if k>1 then {Если да, увеличить счетчик еще на единицу.} kol := kol +1; if kol >0 then write ( ’Количество знакочередующихся серий= ’, kol ) else write ( ’Знакочередующихся серий нет ’ ) end.
Далее рассмотрим чуть более сложную задачу на серии.
Для максимальной серии будем хранить её длину (max) и номер последнего элемента (kon_max).
Эта задача похожа на предыдущую, отличие заключается в том, что надо фиксировать не только тот факт, что серия кончилась, но и саму серию. Серия может характеризоваться двумя из трёх параметров: первый элемент серии, последний элемент серии, длина серии. В связи с тем, что мы фиксируем серию в момент её окончания, то в качестве параметров серии будем использовать последний элемент серии (kon) и её длину (k).
Алгоритм решения этой задачи следующий. Вначале количество серий (kol) и её длина (k) равны нулю. Перебираем последовательно все элементы, если текущий элемент равен 1, то количество элементов в серии11Количество подряд идущих единиц. увеличиваем на 1. Если текущий элемент не равен 1, то возможны два варианта: либо сейчас оборвалась серия из единиц, либо встретился очередной неравный единице элемент. Выбор из этих двух вариантов можно осуществить, сравнив переменную k с единицей. Если k>1, сейчас оборвалась серия из единиц, и количество таких серий (kol) надо увеличить на 1, зафиксировать конец серии (kon:=i-1) и длину серии (dlina:=k). После этого необходимо проверить порядковый номер серии. Если это первая серия (kol=1), то объявляем ее максимальной, в переменную max записываем длину текущей серии k, в переменную kon_max — kon (последний элемент текущей серии). Если это не первая серия (kol>1), то длину текущей серии (k) сравниваем с длиной серии максимальной длины (max). И если k>max, то текущую серию объявляем серией максимальной длины (max:=k; kon_max:=kon;). Если встретился не равный нулю элемент, надо количество элементов в серии положить равным нулю (k:=0).
После выхода из цикла надо также проверить, не было ли в конце серии, состоящей из единиц. Если серия была в конце, то следует обработать её так же, как и серию, которая встретилась в цикле.
Блок-схема решения задачи приведена на рис. 5.41.
Ниже приведён текст консольного приложения решения задачи.
var x : array [ 1.. 50 ] of integer; n, i, k, kol, kon, max, kon_max, dlina : integer; begin {Ввод размера массива.} write ( ’ n= ’ ); readln ( n ); {Ввод массива} writeln ( ’Массив Х ’ ); for i :=1 to n do read ( x [ i ] ); {Начальное присваивание длины серии и количества серий} k : = 0; {Длина серии.} kol : = 0; {Количество серий в массиве.} {Перебираем все элементы в массиве} for i :=1 to n do {Если текущий элемент равен 1, то} if x [ i ]=1 then {количество подряд идущих единиц увеличить на 1.} k:=k+1 else {Если текущий элемент не равен 1, то} begin {проверяем, была ли серия до этого, k>1?} if k>1 then {Если только что оборвалась серия, то} begin {увеличиваем количество серий.} kol := kol +1; {Фиксируем тот факт, что на предыдущем элементе серия закончилась,} kon:= i -1; {длина серии равна k.} dlina :=k; {Если это первая серия,} if kol=1 then {объявляем ее максимальной.} begin {Длина максимальной серии единиц.} max:= dlina; {Конец максимальной серии, состоящей из единиц, хранится в переменной} {kon_max.} kon_max:=kon; end {Если это не первая серия, состоящая из единиц,} else {то её длину сравниваем с длиной серии с максимальным количеством} {единиц.} if k>max then {Если длина текущей серии больше,} begin {то объявляем ее максимальной.} max:= dlina; kon_max:=kon; end; end; {Если текущий элемент массива не равен 0, то количество подряд} {встречающихся единиц начинаем считать сначала (k:=0).} k : = 0; end; {Проверка, не было ли серии в конце массива.} if k>1 then {Если да, увеличить счётчик ещё на единицу.} begin kol := kol +1; {Серия закончилась на последнем элементе.} kon:=n; dlina:=k; {Обработка последней серии так, как это происходило в цикле.} if kol=1 then begin max:= d l i n a; kon_max:=kon; end else if k>max then begin max:= dlina; kon_max:=kon; end; end; {Если серии были, то} if kol >0 then {вывод информации о серии с максимальным количеством единиц.} begin writeln ( ’Количество серий, состоящих из единиц= ’, kol ); writeln ( ’Наибольшая серия начинается с номера ’, kon_max - max+1, ’, заканчивается номером ’, kon_max, ’, её длина равна ’, max) end {Вывод информации об отсутствии серий.} else writeln ( ’Нет серий, состоящих из единиц ’ ) end.