§ 3. Классификация типов данных.
(Начни с определения понятия абстрактного типа данных как области - пары , FAA – см. §1).
Примеры базовой семантики конкретных типов языка Паскаль (в реальности, как мы хорошо знаем, каждый тип обогащен множеством других функций)
Real, integer - арифметика вещественных и целых чисел,
string - полугруппа слов относительно операции конкатенации (сцепления слов),
array[I] of T - множество всех функций IT, c операцией App применения (аппликации) функции к аргументу, App(a,i)=a[i],
record n1:T1;…;nm:Tm end - именованное декартово произведение (определение см. §1) основных множеств типов T1,…,Tm с конечным множеством имен {n1,…,nm}, с операцией аппликации, App(r,n)=r.n,
file of T - множество всех последовательностей NT, c операторами:
reset(f) маркер(f)=0; режим(f):=чтение;
read(f,x) inc(маркер(f));x:=f(маркер(f))
область определения: (режим(f)=чтение)& (маркер(f) Dom(f))
rewrite(f) маркер(f)=0; режим(f):=запись;f:=<> (пустая последовательность)
write(f,e) inc(маркер(f));f(маркер(f)):=e;
область определения: (режим(f)=запись)
eof(f) маркер(f) Dom(f) компонента f(маркер(f)) определена
где маркер(f), режим(f) - некоторые вспомогательные системные (т.е. предопределенные, не определяемые пользователем) переменные.
(В отличие от объектного подхода), классический процедурный стиль склоняется к раздельному описанию
а) способов определения основного множества значений А, при фиксированном классе функций F (собственно, и относимых к определению типа) и
б) способов определения класса F - преобразований значений типа (см. § 2).
Это определяет и соответствующую классификацию типов по способу определений значений основного множества.
-
Разделение между скалярными и производными типами уже проведено в §1. В последнем случае, значения структурированы. Общая схема такой структуры - именованное декартово произведение, т.е. некоторый класс функций. Различие производных типов - в конкретных областях определения и значений этих функций, и (фиксированных для каждого типа) операторов их обработки. Примеры скалярных типов в Паскале - integer, real, char, boolean, перечислимый, ограниченный, производных типов - string, text, array, file, record, set.
-
Стандартные и пользовательские типы. В первом случае множество значений преопределено, во втором (до)определяется программистом. Примеры стандартных типов - integer, real, char, boolean, string, text; пользовательских типов - перечислимый, ограниченный, array, file, record, set.
-
Порядковые и непорядковые типы. Понятие перебора (перечисления, пересчета) является одним из наиболее фундаментальных в программировании. Порядковые типы - те множества значений X, на которых определен (хотя бы один) порядок перебора, т.е. сравнение (линейный порядок) <, xлюбое сравнение определяет порядок перебора, а лишь такое, для которого определены (доступны)
-
функция следования (прямого перебора) succ, succ(x)=min{x': x'>x} и
-
функции предшествования (обратного перебора) pred, pred(x)=max{x’:x’
Примеры порядковых типов Паскаля -
integer - succ(n)=n+1,
char, - succ(ci)= ci+1 (ci - - i-ый символ в предопределенном языком фиксированном алфавите, таблице символов)
Boolean - false
Значения производных типов (array T1 of T2, record, file of T) обычно не считаются порядковыми, но нужный порядок можно (до)определить, если соответствующие базовые типы - порядковые (см. перечисление последовательностей в §10 и §11). Очень интересны примеры
-
типа real, на котором определено обычное сравнение чисел, но из теоретических «мощностных» соображений не может быть определена соответствующая функция следования - не существует наименьшего вещественного числа, большего данного.
-
множества всех бесконечных последовательностей, с теми же свойствами. (см. "Перечисление последовательностей").
Они не являются порядковыми типами, но могут быть приближены таковыми с любой наперед заданной степенью точности. Так, вещественные числа приближаются рациональными, бесконечные последовательности - конечными (подробнее - вспомни. курс мат. анализа)
-
Динамические и статические производные типы. Значения производных типов - функции. Область их определения может быть фиксирована (статический тип) - пример - множество всех функций [1,n]T для фиксированного n, либо нет (динамический тип), пример - множество всех функций [1,n]T для произвольного n. Примеры статических типов - record, статический array (в стандартном Паскале); динамических - file, string, динамический array ( в Object Pascal).
В последнем случае определение типа предполагает явное или неявное наличие соответствующих операторов расширения и сужения области определения. Примеры - оператор Паскаля SetLength явного установления области определения динамических массивов; оператор write увеличивает длину файла неявно.
Практический интерес представляют типы, которые можно назвать псевдодинамическими. Это функции, область определения которых не фиксирована, но содержится в некотором фиксированном множестве значений. Пример: множество всех последовательностей f с длиной менее заданной, f:[1..n]T, n -
Абстрактные типы. Совершенно иной смысл имеет условное разделение абстрактных и конкретных типов. Абстрактные типы - типы, точно определяемые как математические объекты (объекты некоторого языка спецификаций), но не как объекты данного языка программирования. Пример - комплексные числа и точки плоскости не есть тип Паскаля, тип record x,y: real end - тип Паскаля. Установление соответствия между (значениями и операциями) абстрактного и конкретного типов называется реализацией (определением, моделированием или представлением) абстрактного типа.
-
Тесно связано с понятием реализации (как способа определения) деление типов по способам доступа. На практике, программисту важен именно способ определения, задания объекта/функции. Другое дело, что такие определения можно снова точно сформулировать в терминах функций/операторов. Существует три важных вида таких определений: реккурентное (данные последовательного доступа, пример - файлы и списки, язык описания - функции следования), явное (данные прямого доступа, пример - массивы и записи, язык описания - оператор аппликации) и рекурсивное (пример - деревья и графы, язык описания - функции на последовательностях)
Пояснить классификацию типов на конкретном примере.
Задача: найти длину lmax самого длинного слова w (в текстовом файле f) и само это слово w; (слова представлены символьным списком.)
С абстрактной (логической) точки зрения, дана последовательность слов, с точки зрения реализации - динамический тип файл (последовательность символов). Здесь, постановка задачи уже фиксирует реализацию значений - но не операторов! Задача легко решается в терминах абстрактного типа (слова и их длины), основная сложность - реализовать соответствующие операции.
type
{список - абстрактный динамический пользовательский тип последовательного доступа}
slovo=^component;
component=record letter:char;next:slovo end;
{аналогично - найди место в каждой классификации для остальных типов, используемых в спецификации и реализации программы}
procedure P(var f:text; var lmax:integer; var w:slovo);
var v:slovo;
begin
lmax:=0;
w:=ПустоеСлово;
stop:= КончилисьСлова(f); v:=Oчередное слово(f); l:= Длина(v);
while not stop do
begin
if l>lMax then begin lmax:=l; Копировать(w,v) end;
{подготовка переменных к следующему проходу}
Уничтожить(v);
stop:= КончилисьСлова(f); v:=Oчередное слово(f); l:= Длина(v);
end;
Внимание - хитрость примера - обратное влияние или "побочный" эффект реализации - независимое определение функции КончилисьСлова ( остаток файла содержит хотя бы одно слово хотя бы один не пробел) делает практически невозможным (точнее, сильно неэффективным) реализацию процедур OчередноеСлово и Длина. Отдельные, с точки зрения логики, операторы, могут быть реализованы только совместно.
procedure P(var f:text; var lmax:integer; var w:slovo);
var v:slovo;
begin
lmax:=0;
w:=nil; {w:=ПустоеСлово}
ОпределиКончилисьСловаЕслиНетОпределиОчередноеСловоИЕгоДлину(f,stop,v,l);
{читай файл до непробела/начала слова, если таковое есть - читай все непробелы в список v, одновременно подсчитывая их число в l}
while not stop do
begin
if l>lMax then begin lmax:=l; Копировать(w,v) end;
{подготовка переменных к следующему проходу}
Уничтожить(v);
ОпределиКончилисьСловаЕслиНетОпределиОчередноеСловоИЕгоДлину(f,stop,v,l);
end;
Замечание. Если есть трудности с реализацией порождения, уничтожения и копирования списков - см. "Абстрактные линейные типы данных"
§4. Вычисление предикатов.
Предикат (свойство, условие) - произвольная функция с областью определения Boolean={true,false}. Пример (двуместных) предикатов - бинарные отношения, в частности, отношения сравнения <,>,=,<>.
Вычисление предикатов облегчается тем, что существует общематематический язык логики (исчисления предикатов) - исключительно выразительный и одновременно компактный - содержащий бинарные операции & (and, в синтаксисе Паскаля), (or), (not) - их относят к операциям алгебры логики, а также два квантора (оператора над множествами) и (отсутствуют в Паскале). Перевод формулы из языка ИП в программу на структурном языке программирование - техническая задача, при учете следующих основных моментов.
-
Написание (спецификация) формул может быть само по себе достаточно сложной задачей. Математическая логика располагает множеством стратегий таких спецификаций в виде алгоритмов записи любого предиката в некоторой канонической (нормальной) форме. Наиболее простые из таких форм для бескванторных формул - днф (дизъюнкция элементарных коньюнкций), кнф (конъюнкция элементарных дизъюнкций); по определению, элементарные формулы могут содержать отрицание, но не знаки бинарных логических операций.
-
Для кванторных формул канонической является т.н. предваренная нормальная форма вида Q1x1… Qn xn B(x1,…,xn,z) ( где Qi - либо либо , причем каждые два соседних квантора можно считать различными, а B уже не содержит кванторов).
Пример. Определить принадлежность точки p(x,y) к фигуре, составленной из 4 колец. Каждое кольцо задаваемется центром и длинами внутреннего и внешнего радиусов.
Решение - стратегия днф.
а) Точка принадлежит к фигуре, если она принадлежит одному из колец.
type
tPoint=record x,y:real end;
tRing=?
tFigure=array[1..n] of tRing;
function ПринадлежитФигуре(p: tPoint; figure:tFigure):boolean;
begin
ПринадлежитФигуре:=
ПринадлежитКольцу(p,figure[1]) or
ПринадлежитКольцу(p,figure[2]) or
ПринадлежитКольцу(p,figure[3])
end
b) Точка принадлежит кольцу, если она находится одновременно внутри внешнего и вне внутреннего окружностей кольца.
type
tRing =record center:tPoint; radius1,radius2:real end;
function ПринадлежитКольцу(p:tPoint; Ring:tRing):boolean;
begin
ПринадлежитКольцу:=
ПринадлежитОкружности(p,Ring.center,Ring.radius1) and
not ПринадлежитОкружности(p,Ring.center,Ring.radius2)
end;
-
Принадлежность точки P с заданными координатами Px,Py к окружности O c центром Ox,Oy и радиусом Oradius описывается атомарной формулой
(Px-Ox)2+(Py-Oy)2 Oradius2
function ПринадлежитОкружности(p:tPoint,center:tPoint; radius:real):boolean;
begin
ПринадлежитОкружности:= sqr(P.x-Center.x)+sqr(P.y-Center.y)<=sqr(radius)
end;
-
Алгоритмическое определение операций алгебры логики.
-
y:=not b if b=true then y:=false else y:=true
-
y:=b1 and b2 1) y:=false; if b1=true then if b2=true then y:=true; 2) y:=true; if b1=false then y:=false; if b2=false then y:=false;
-
y:=b1 or b2 1) y:=true; if b1=false then if b2=false then y:=false 2) y:=false; if b1=true then y:=true; if b2=true then y:=true
Внимание - указанные эквивалентности верны лишь для определенных значений b1,b2. Программы 1) определенные значения y при неопределенном значении b2, программы 2) - нет. Семантика 1) соответствует т.н. быстрому означиванию операций и определяют т.н. условные коньюнкцию и дизъюнкции - строго говоря, отличных от классических (семантика 2). В Паскале эта неоднозначность ("темное" место языка) разрешается директивой компилятора $B. Разные значения директивы может сделать одну и ту же программу верной или неверной!
Задача (линейный поиск)
type
tIndex=1..MaxLen;
ArrayOfT=array[tIndex] of T;
procedure Poisk(x:T; a:ArrayOfT; LenA:tIndex; var found:boolean; var index:tIndex);
begin
index:=1;
while (index<=LenA) and (x<>a[index]) do inc(index);
found:= index<=LenA
end;
При быстром означивании формула (index<=LenA) and (x<>a[index]) при index=LenA+1 дает false (программа верна), при классическом - значение неопределено (программа неверна). Совет: желательно избегать неопределенных значений -
index:=1; found:=false;
while (index<=LenA) and not found do if x=a[index] then found:=true else inc(index);
-
Вычисление кванторных формул.
-
y:= x X (B(x))
Рассмотрим частный (для классической логики), но наиболее важный для программирования (и конструктивной логики) случай. X - множество значений порядкового динамического типа, для которого определены функция следования (перебора) и предикат конца перебора значений - заданный, например, в виде сравнения. Тогда x X (B(x)) - это кратная дизъюнкция, а y есть первый true в реккурентной последовательности y0=false, yi+1= yi or B(xi) (либо false, если такового нет). Поэтому, можно воспользоваться общей схемой вычисления членов реккурентных последовательностей:
y:= x X (B(x))
y:=false; i:=1; while (i<=LenX) and not y do if B(xi) then y:=true else inc(i)
Точно также можно определить формулу x X (B(x)) как кратную конъюнкцию:
y:= x X (B(x))
y:=true; i:=1; while (i<=LenX) and y do if B(xi) then y:=false else inc(i)
Пример вычисления кванторной формулы. Последовательность A длины LenA (строго) периодическая (или, попросту - кратная), если оно состоит из двух или более одинаковых "сцепленных" подпоследовательностей некоторой длины k, называемой в этом случае периодом A. Выяснить, является ли данная последовательность строго периодической.
Спецификация
b:=A - строго периодическая
b:= k [1,LenA div 2] ( k - период A)
b:= k [1,LenA div 2] ( LenA mod k =0 i [1..LenA-k] (A[i]=A[i+k]))
type
tIndex:1..MaxLen;
tArray:array[tIndex] of T;
function Period(k:tIndex; a:tArray; LenA:tIndex):boolean;
var b2:boolean; UpperLimit2:tIndex; {= LenA-k }
begin
b2:=LenA mod k; UpperLimit2:= LenA-k; i:=1;
while (i<= UpperLimit2) and b do
if A[i]<>A[i+k] then b2:=false else inc(i)
Period:=b2
while
end;
function Periodic(a:tArray; LenA:tIndex):boolean;
var UpperLimit1:tIndex; {=LenA div 2} b1:boolean;
begin
b:=false; k:=1; UpperLimit:=LenA div 2;
while (k<=UpperLimit) and not b do
if period(k,A.LenA) then b:=true else inc(k);
Periodic:=b;
end;
§5. Поиск.
Основной интуитивный смысл структур данных - "хранилище" данных, обеспечивающее операции доступа (чтения) и записи, что, при формальном определении этих структур как функций соответствует вычислению следующих основных операторов
- аппликация (по хранилищу f и указателю/имени x определить значение f(x)),
Понятие хранилища данных тесно связано по смыслу, но существенно шире по объему понятия структуры (производного типа). В качестве хранилища, при фиксации кодирования, могут выступать и скалярные значения. Так, например, по основной теореме арифметики, любое n,n N, есть некоторое произведение степеней простых чисел p(i) a(i) (p(i) i-е простое число). Следовательно, его можно рассматривать как хранилище последовательности а, т.е. потенциальную или "виртуальную", т.е. воображаемую структуру.
Важно уточнить, что фактически может храниться не сама функция (в табличном виде, пример - данные прямого доступа - массивы и записи), но способ ее определения - реккурентный (данные последовательного доступа, пример - файлы) или рекурсивный (пример - деревья)
Задача поиска формулируется в двух внешне различных формулировках.
-
b:=y Val(f) x Dom(f) (f(x)=y) - выяснить, храниться ли данное значение y в структуре f.
-
x:=min{x': f(x')=y} определить первое (в некотором порядке) "имя", под которым храниться данное значение.
(задача - обратная к задаче доступа).
В реальности, эта одна задача. С конструктивной точки зрения, мы не не можем ответить на вопрос а), не предъявив такое x, что f(x)=y и не можем найти x в b), не выяснив, существует ли таковое вообще.
b,x:=y Val(f) x Dom(f) (f(x)=y), min{x' Dom(f): f(x')=y}
К задаче поиска легко сводяться многие важные задачи
-
выяснить, храниться ли в структуре f значение с заданным свойством, в частности,
-
выяснить, храниться ли в структуре f значение почти равное заданному (т.е. хорошее приближение заданного значения)
-
определить все "имена", под которым храниться данное значение (значение с заданным свойством) и т.п.
В любом случае, решение предполагает наличия некоторого порядка перебора на Dom(f)={x1,..,xn,..}.Тривиальное решение задачи - линейный поиск - мгновенно следует из схемы вычисления -свойств (см. "Вычисление свойств") в данном случае основанном на реккурентном отношении:
(*) x {x1,..,xn,..} (f(x)=y) (f(x1)=y) or x {x2,..,xn,..} (f(x)=y))
procedure LinearSearch(f:tStructure;y:tVal;var b:boolean;var x:tDom);
begin
b:=false; i:=1;
while (i<=LenDom) and not b do if f(xi)=y then begin b:=true;x:=xi end else inc(i)
end;
Понятно, это схема. Пересчет tDom и условие его окончания может выглядеть совершенно иначе.
Пример (поиск в конечном дереве и графе). Дерево (граф) в качестве хранилища - функция вида A*T, A - некоторый алфавит, A* - множество всех конечных последовательностей (ветвей дерева), а T - тип вершин дерева (графа). Для решения задачи поиска достаточно определить некоторый порядок перебора ветвей.
procedure LinearSearch(f:tTree;y:tVal;var b:boolean;var x:tDom);
begin
b:=false; x:=ПервоеСлово; {естественно взять в качестве первого пустое слово - других может и не быть!}
while not КончилисьСлова(f) and not b do
if f(x)=y then begin b:=true else x:=Следующее(x)
end;
Окончание решения - см. "Перечисление последовательностей".
Очевидно, в худшем случае алгоритм линейного поиска требует полного перебора всех элементов tDom. Можно ли ускорить поиск? Да, если предполагать наличие некоторого сравнения (линейного порядка) < на tDom (в отличие от tDom, это не обязательно порядок перебора) и рассматривать в качестве хранилищ упорядоченные последовательности (или, в общем случае, монотонные функции) f: tDomtVal
x,x' tDom (xx' f(x).f(x'))
Ограниченный поиск. Первая идея "сокращения пространства поиска" основана на простом наблюдении - в случае монотонности f бесполезно проверять равенство f(x)=y для тех x, для которых f(x)>y :
-
x tDom (f(x)=y) x {x1,…, xk} (f(x)=y), где k=min {k': f(xk) y}.
{предусловие - f монотонна (упорядочена)}
procedure RestrictedSearch(f:tOrderedStructure;y:tVal;var b:boolean;var x:tDom);
var UpperLimitFound:boolean; { x tDom (f(x) y)}; y1:tVal;
begin
{найди первое x, что f(x) y}
UpperLimitFound:=false; i:=1;
while (i<=LenDom) and not UpperLimitFound do
begin y1:=f(xi); if y1>=y then begin UpperLimitFound:=true;x:=xi end else inc(i); end;
if UpperLimitFound then b:=y1=y else b:=false
end;
Пример применения схемы - поиск в упорядоченном файле
{предусловие - f монотонна (упорядочена)}
procedure RestrictedSearch(f:tOrderedFile;y:tVal;var b:boolean;var n:Cardinal);
var UpperLimitFound:boolean; { x tDom (f(x) y)}; i:Cardinal;
begin
{найди первое x, что f(x) y}
UpperLimitFound:=false;
reset(f); {j:=1} i:=0;
while not eof(f) {(i<=LenDom)} and not UpperLimitFound do
begin read(f,y1); {y1:=f(j);inc(j)}
if y1 >=y then begin UpperLimitFound:=true;n:=i end else inc(i);
end;
if UpperLimitFound then b:=y1=y else b:=false
end;
Идея дихотомического поиска (поиска методом деления пополам) также предполагает упорядоченность f и основана на соотношении
(*) x {xn1,..,xn2} (f(x)=y)
x {xn1,..,xm} (f(x)=y)) xor x {xm+1,..,xn2} (f(x)=y)), где m=(n1+n2) div 2
(для монотонной f)
(f(xm)=y) xor
(f(xm)n1,..,xm-1} (f(x)=y)) xor
(f(xm)>y) & x {xm+1,..,xn2} (f(x)=y))
(т.е. в реальности делим Dom(f) на 3 части, не на 2)
{предусловие - f монотонна (упорядочена)}
procedure Dichotomy(f:tOrderedStructure;y:tVal;var b:boolean;var x:tDom);
UpperLimit, LowerLimit:tDom; {верхняя и нижняя границы пространства поиска}
begin
b:=false; UpperLimit:=1; LowerLimit:=LenDom;
while (UpperLimit>LowerLimit) and not b do
begin
m:= (UpperLimit+LowerLimit) div 2;
if f(xm)=y
then b:=true
else if f(xm)
end; {while}
end; { procedure Dichotomy }
Традиционно дихотомический поиск считается быстрым поиском, поскольку позволяет найти ответ не более, чем за ln2 n сравнений, в то время как верхняя оценка линейного поиска - n сравнений (где n=Card(Dom) - число элементов в пространстве поиска Dom). Однако, нельзя забывать, что, в отличие от линейного поиска, этот алгоритм требует вычисления f(xm), которое может оказаться неоправданно дорогим - например, в случае поиска в файле.
Идея дихотомии (как и ограниченного поиска!) непосредственно продолжается на деревья (и графы) специального вида - деревья поиска. Дерево в качестве хранилища есть некая функция f, f:A*T, где A* - множество путей (ветвей) дерева, а Т - содержимое его вершин. Разберем самый простой случай бинарных деревьев - A={0<1} (кратное обобщение на случай произвольного конечного A={a1<…< an} - техническая задача).
Функция f, f:{0<1}*T > - дерево поиска, если оно f монотонна относительно лексикографического порядка << на словах (см. "Перечисление последовательностей") : v1,v2 {0,1}* (v1<T f(v2))
procedure OrderedTreeSearch(f:tOrderedTree; y:tVal; var b:boolean; var x:tDom);
begin
b:=false; v:=ПустоеСлово;
while (v Dom) and not b do
begin
if f(v)=y then b:=true
else if f(v)>y then v:=v0 else v:=v1;
end;
end;
Здесь - операция приписывания (конкатенации) буквы к слову. В случае реализации деревьев ссылками (см. обозначения в "Нелинейные типы данных"):
v:=ПустоеСлово p:=Root;
v:=v0 p:=p^.left
v:=v1 p:=p^.right
v Dom p<>nil
где Root, p - ссылки на корень и текущую вершину дерева.
Достарыңызбен бөлісу: |