Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Полезное программирование
.pdf
161Улучшаем алгоритм
отсортировать элементы массива по возрастанию. Алгоритм, напоминаю, очень
простой — сравниваем первый элемент со вторым, если они следуют в неправильном порядке, то меняем их местами. Потом делаем то же самое со вторым и
третьим элементами. Тривиальное наблюдение — если при первом сравнении
мы поменяли первый и второй элементы, то при втором сравнении в роли второго элемента выступает бывший первый.
Отсюда нетрудно заметить, что если самый большой элемент массива нахо-
дится у нас в самой плохой (удалённой от правильного места) первой позиции,
то за всего один проход по массиву он доберётся до своего правильного, последнего места. Если не верите, то проверьте на бумажке. Если же самый маленький
элемент массива оказался в самой плохой для него последней позиции, то ему
потребуется N-1 проходов по массиву, чтобы попасть на законную первую позицию. Здесь N — количество элементов.
Впрочем, всё уже придумано до нас. В улучшенном алгоритме сначала мы
идём по массиву от первого элемента до последнего, а затем от последнего элемента до первого. У такого алгоритма уже есть и своё название — Cocktail Sort.
При пузырьковой сортировки пузырьки всплывают вверх, а здесь они мечутся
туда-сюда в обе стороны, что, видимо, у англоязычных программистов ассоциируется со взбиванием коктейля.
procedure CocktailSort( var a : array of single);
var
gamover : boolean;
tmp : single;
i : integer;
begin
repeat
gamover:=true;
for i:=Low(a) to High(a)-1 do begin
if a[i] > a[i+1] then begin
tmp:=a[i];
a[i]:=a[i+1];
a[i+1]:=tmp;
gamover:=false;
end;
end;
for i:=High(a) to Low(a)+1 do begin
if a[i] < a[i+1] then begin
tmp:=a[i];
a[i]:=a[i+1];
a[i+1]:=tmp;
gamover:=false;
end;
end;

162 Глава 7. Мелкие технические подробности
until gamover;
end;
Там, откуда я взял алгоритм на псевдокоде, между двумя внутренними цикла-
ми была дополнительная проверка. Если за время первого прохода по массиву не
было ни одной перестановки элементов, то работа завершалась, не входя во второй цикл. Это один из глупейших способов оптимизации среди когда-либо виденных мною.
Попробуем эту процедуру и замерим время, на том же массиве, разумеется.
Интуитивно кажется, что должно работать быстрее, причём ощутимо быстрее,
чуть ли не в два раза. Увы, интуиция обманывает.
Есть и другая (не моя) рекомендация по улучшению пузырьковой сортировки.
Как уже замечено, после первого прохода по массиву самый большой элемент
гарантированно оказывается на своём последнем месте. После второго прохода
правильным элементом будет занято предпоследнее место и так далее. Напрашивается мысль — после каждого пройденного цикла уменьшать количество итераций следующего цикла на единицу. Это для натуральной пузырьковой сортировки. Для коктейльного мутанта итерации надо уменьшать и с начала массива. Запрограммируйте, замерьте время, и сделайте выводы самостоятельно. А я даже
пробовать не буду.
После всего этого я заглянул в очень полезную и рекомендуемую (мною) для
тщательного изучения (вами) книгу Рода Стивенса «Delphi. Готовые алгоритмы»,
в раздел о Пузырьковой Сортировке. Кроме двух только что испробованных
(вами) усовершенствований я нашёл там загадочный совет хранить переставляемые на большое расстояние элементы массива во временных переменных. Во
первых, мы уже почти это пробовали. Во вторых, а как догадаться заранее, что
элемент будет перемещён последовательно на несколько ступеней вверх?
Короче, как сейчас в таких случаях принято говорить, Один я умный в белом
пальто стою красивый.
Меняем алгоритм
Сделаем наконец с нашей сортировкой именно то, что все авторитеты совето-
вали сделать с самого начала — выбрать другой алгоритм.
Возьмём то, что называется comb sort — сортировка расчёской. Она простая,
но непонятная. Лично мне пришлось задуматься. А поскольку я человек по характеру скорее недобрый, предлагаю задуматься и вам и запрограммировать алгоритм самому. Но, поскольку во мне есть и что-то общечеловеческое, сразу предагаю результат.

procedure CombSort( var a : array of single);
var
size : integer;
gap : integer;
shrink : single;
gamover : boolean;
tmp : single;
num : integer;
begin
size:=Length(a);
shrink:=1.3;
gap:=size;
repeat
gap:=Trunc( gap/shrink);
if gap < 1
then gap:=1;
gamover:=true;
num:=0;
while (num + gap < size) do begin
if a[num] > a[num+gap] then begin
tmp:=a[num];
a[num]:=a[num+gap];
a[num+gap]:=tmp;
gamover:=false;
end;
num:=num+1;
end;
until (gap=1) and gamover;
end;
163Меняем алгоритм
Замерьте время. Подумайте. Ещё раз замерьте. Ещё раз подумайте. Хорошо ли
это? А хорошо ли, что двадцать предыдущих страниц мы занимались ерундой?
А вообще, как заметил вечно правый Керниган, если какая-то фигня занима-
ет один процент времени от выполнения всей программы, нафига её оптимизировать? То есть, конечно, он использовал немного другие слова, но смысл был
тот же.

ГЛАВА 8.
Техника программирования,
или
Это вы обязательно должны знать
Абстрактные рассуждения
или
а оно нам надо?
В основном эта глава должна была рассказывать об указателях, объяснять ука-
затели, показывать применение указателей. Потому что без этого полноценно
программировать нельзя. Но, по мере написания текста, у меня стали возникать
кощунственные мысли — а действительно ли указатели так уж необходимы? Может, ну их? Ведь есть же языки программирования без указателей и на некоторых
из этих языков даже можно программировать. Поэтому вместо вдалбливания в
голову читателя мысль о необходимости указателей я решил немного поразмышлять. Вслух. Сам с собой. На примерах, само собой.
Но всё же двадцать шестой раз повторяю — каждый настоящий программист
должен понимать указатели! Я специально не говорю — знать указатели, потому
что знать там нечего, почти. Указатель — это очень просто. Указатель — это, всего-навсего, переменная, которая указывает на другую переменную. Всё, больше
ничего знать не надо. Это даже проще, чем концепция параметров процедуры,
которые передаются, с незапамятных времен, или по значению или по ссылке, а
теперь ещё и по умолчанию, не говоря уже про вариант const. Это даже не безобидные целые и плавающие переменные, которые, несмотря на свою внешнюю
безобидность, могут быть знаковыми и беззнаковыми, иметь разную разрядность, да ещё и сопровождаться для плавающих чудесными ошибки округления
и приведения (смотри термин «сдвинуть кучу песка»).
А вот указатели всегда имеют один и тот же тип. Все указатели, говоря други-
ми словами, абсолютно одинаковы, по крайней мере, для данного конкретного
языка программирования, они одинаковы.
А теперь я повторюсь, где-то я уже эту мысль излагал, но мне кажется в книге,
которая так и не была опубликована.

165Абстрактные рассуждения илиа оно нам надо?
Первое, что должен понять и понимает обязательно каждый программист —
понятие переменной. Есть переменная, и есть её значение. Всё. Это понимают
если не все, то почти все.
Второй уровень понимания — массив. Есть массивы, есть индекс массива,
указывающий на элемент массива, есть значение элемента массива, на который
указывает индекс массива. Если будущий программист понял про переменную,
то он обычно понимает и про массив и с этого момента считается не будущим
программистом, а программистом настоящим. Большинство на этом месте останавливаются в своем развитии и программируют долго и счастливо.
Третий уровень — указатель. Указатель указывает на другую переменную. Эта
другая переменная может быть, к примеру, массивом, со своими индексами.
Проблема скорее не в том, что появляется ещё один уровень ссылок, проблема в
том, что возникают некоторые труднопонимаемые моменты, специфические
именно для указателей. Изначально указатель не указывает никуда. Неинициализированная переменная тоже не содержит ничего, но этим «ничего» очень часто оказывается ноль, и, по этой счастливой случайности, всё благополучно работает. Ноль в указателе, напротив, гарантированно рушит всю программу. Неизбежное при работе с указателями выделение и освобождение памяти добавляет
остроты.
Есть общее правило — программа должна быть законченной и симметричной,
причём на любом этапе её написания. Законченность программы обозначает,
что она транслируется без ошибок. Этого мало. Если в программе вызывается
конструктор класса, но не вызывается деструктор, то такая программа транслируется, но это очень подозрительная программа. Если программа имеет и конструктор и деструктор, но они вызываются в разных процедурах, и, хуже того, на
разных уровнях вложенности — это даже опаснее.
Симметричность означает, что если написал begin, тут же напиши end, пусть
даже между ними пока ничего не будет. Если вызвал конструктор, тут же вызови
и деструктор, причём на том же уровне, то есть в той же процедуре. Вас это не
устраивает? Именно для вашего случая это не подходит? Скромнее надо быть,
девушки! Симметрия превыше всего.
Указатели очень часто эту симметрию нарушают — память выделяется где-то
снаружи, созданный объект (в широком смысле) передаётся в объект-контейнер,
который и распоряжается дальнейшей судьбой созданного объекта, в частности,
освобождает его память.
Кроме того, некоторые сущности, назовем их так, в Дельфи являются на са-
мом деле замаскированными указателями — например, экземпляры классов и
строки, которые в глубине души являются псевдообъектами. Не беда, когда
взрослые люди упорно передают указатели на некоторую переменную не по значению, а по ссылке, мотивируя это тем, что значение переменной, на которую
указывает указатель, будет меняться! Горе, когда они передают по ссылке объект
потому, что изменится одно из его полей (свойств)…

166 Глава 8. Техника программирования,или Это вы обязательно должны знать
Кратко о предмете обсуждения
Указатели — это просто. Но, поскольку некоторые упорно их не понимают, я
объясню. Потом ещё раз. И ещё. Я вообще ужасно терпеливый и добрый.
Жалкие трусы придумали тормоз,
То же скажу о гаишниках в форме… © ОСП
Указатели, как и файлы, бывают типированные и нетипированные. Типиро-
ванные — для жалких трусов. Больше о них не вспоминаем.
Уточнение. Технически указатель содержит адрес некоторой другой перемен-
ной. Это уточнение означает, что с указателями можно обращаться как с обычными целыми числами. В C++ это просто и незатейливо, в Дельфи мовершенно
умышленно предприняты все усилия, чтобы максимально затруднить эти манипуляции. Впрочем, написав две-три нехитрых функции, можно обращаться с
указателями так же легко и просто, как и с числами целого типа.
// Можно не читать. Длинное
Можно не читать, если вы чётко понимаете что такое указатель, а для других я объясню. Когда я был не просто маленький, а был очень маленький, то я имел счастье
программировать для машины M-20 (а может M-220, какая разница). Это была большая, красивая машина, она занимала целую комнату (или этаж). Программировали
мы сначала не на каком-то языке программирования, не на ассемблере даже, а чисто
в машинных кодах.
Сколько у этой машины было памяти, я уже точно не помню— то ли четыре килобайта, то ли все двенадцать. Числа были сорокапятиразрядные. Википедия утверждает, что числа были плавающие, но это неправда. На самом деле, это был просто набор
битов (или бит).
Программа обычно располагалась начиная с ячейки 000. Программы у нас были
учебные и маленькие и данные традиционно размещались с адреса 100. Чем хороша
такая архитектура, так это тем, что её можно окинуть одним взглядом— вот оно всё,
под руками. Да, и обратите внимание, 100— это не адрес в смысле смещения в байтах
от начала памяти, 100 это адрес в смысле номера ячейки. Как я заметил, это существенно упрощает понимание.
Итак, у нас есть ячейка номер 100. Вячейке находится число 200. Если быть точным, то мы не знаем число ли там находится. Находится там последовательность битов, она же машинное слово, которая (которое) может быть числом с плавающей точкой, цепочкой символов или целым числом. Ведь мы программировали в машинных
кодах, а все эти сущности, целые и плавающие, возникают только на уровне языка
программирования. Так что точнее было бы сказать, что в ячейке находится просто
200. Извините за философию, но таково программирование.
Мы можем смотреть на это двести, как на целое число и, в таком его качестве, увеличить его на единицу. Точнее, мы должны поместить в ячейку 101 (число) 1, а потом
дать команду сложить числа в двух ячейках как целые и поместить результат в ячейку
100. Если правильно помню, команда выглядела так:

167Рядом лежащие вопросы
0001 0100 0101 // 0001±ɤɨɞɫɥɨɠɟɧɢɹɤɨɦɦɟɧɬɚɪɢɟɜɬɚɦɧɟɛɵɥɨ
Но мы можем смотреть на это 200 не как на число, а как на номер ячейки памяти.
Вэтой ячейке находится, разумеется тоже какое-то число, например 2000. От того, что
мы считаем это число адресом, ничего, по сути, не меняется. Мы точно так же можем
увеличить значение ячейки 100 на единицу, после чего она будет указывать на ячейку
памяти 201, в которой, опять-таки к примеру, находится число 2100.
Всё, ушёл плакать.
// конец того что Можно не читать
При объявлении указателя, переменной традиционно даётся имя P:
var
p : pointer;
Чему равен уже объявленный, но ещё не инициализированный указатель?
Как правило, нулю, но в нём может быть и мусор. Впрочем, это неважно, ноль в
качестве значения указателя — такой же мусор.
Указатель представляет ценность, как правило, только тогда, когда указывает
на какую-то область памяти. Эта память может быть распределена динамически,
то есть руками программиста, в нужный момент, тогда программист должен подумать и о её, памяти, освобождении (в нужный момент). Другой типичный случай, это когда указатель ссылается на область памяти, выделенную автоматически, то есть при объявлении переменной. Извращения мы здесь не рассматриваем — при желании указатель может указывать почти на что угодно. В качестве
особого случая, наверное, следует рассмотреть нетипированные параметры.
Основной метод применения указателей — это динамическое выделение па-
мяти. Когда это бывает нужно, и почему нельзя обойтись автоматически объявленными переменными? Отчасти это правильный вопрос. Работу упростили динамические массивы — то есть массивы, длина которых может меняться в произвольный момент выполнения программы, то есть уже после их объявления. Но
только отчасти, с указателями всё-таки нагляднее и, не побоюсь этого слова,
проще.
Рядом лежащие вопросы
Везде написано, что так программировать нельзя. Нельзя, потому что непра-
вильно. И действительно, это неправильно. Но придётся.
К работе с указателями примыкает непосредственная работа с памятью. Это
бывает надо. Реализована эта работа через процедуру Move. Нередко случается,
что в классе, в котором реализуется наш метод, уже есть свой метод Move, в таких
случаях к процедуре надо обращаться квалифицированно с указанием имени мо-

168 Глава 8. Техника программирования,или Это вы обязательно должны знать
дуля — System.Move. Процедура делает то же, что и одноимённая ассемблерная
команда. Далее в примерах int имеет тип integer, p — pointer.
Типичные задачи. Выделить память и переслать целое в область памяти и об-
ратно:
GetMem( p, 1024);
Move( int, p^, 4);
Move( p^, int, 4);
Чуть сложнее — не в исполнении, в понимании.
var
a : array[1..100] of integer;
b : array[1..100] of integer;
begin
Move( a, b[51], 4*15);
Что мы тут делаем? Пересылаем первые пятнадцать элементов массива A в
середину массива B. Обратите внимание, что если бы элементы второго массива
имели тип single, транслятору было бы всё равно. Более того, транслятору было
бы всё равно, даже если бы элементы второго массива имели тип byte, что не
очень хорошо и потенциально опасно.
В комплекте идёт процедура FillChar, изначально предназначенная для запол-
нения строки одним повторяющимся символом. Но используется она почти исключительно для заполнения неважно чего нулями. Пусть sA – array[1..100]
of single. Заполнить всё нулями с помощью этой процедуры проще, чем на-
писать цикл, особенно, если массив двумерный:
FillChar( sA, SizeOf(sA), #0);
Можно применять эту процедуру и к записям, и к массивам записей, и к за-
писям, содержащим массивы. Одно условие — надо хорошо понимать, что именно вы делаете.
Подумаем конкретно, на примерах. Первый
пример
Подумаем мы о необходимости применения указателей, можно ли обойтись
без них и, главное, нужно ли.
Задача первая. Есть файл. Файл здесь и далее — это последовательность бай-
тов, записанных на каком-то реальном носителе, не в оперативной памяти. Надо

169Подумаем конкретно, на примерах. Первый пример
в этом файле что-то найти, например, бодрящее слово из трёх букв (end). Син-
таксический поиск по образцу задача интересная сама по себе (посмотрите в
книгах, на которые я уже ссылался), поэтому отвлекаться на него не будем, а
упростим задачу до безобразия. Давайте найдём первый нулевой байт в файле, то
есть узнаем его смещение.
Сначала решим задачу, как если бы единственным доступным программным
инструментом у нас был этот самый файл, и прилагающиеся к нему процедуры,
то есть в языке никаких других конструкций у нас просто нет.
var
F : File;
b : byte;
whereZ : integer;
i : integer;
begin
AssignFile( F, ‘gr-01.bmp’);
ReSet( F, 1);
whereZ:=-1;
for i:=0 to FileSize(F)-1 do begin
BlockRead( F, b, 1);
if b = 0 then begin
whereZ:=i;
Break;
end;
end;
CloseFile(F);
ShowMessage( ‘whereZ = ‘ + IntToStr(whereZ));
end;
Предполагая в читателе знакомство с файлами, хотя бы даже и самое элемен-
тарное, вынужден объясниться только о причине начала цикла от нуля, что мне,
вообще говоря, совершенно несвойственно. Мы ищем смещение, то есть для первого байта в файле оно равно нулю. Поэтому совершенно естественно и цикл
организовать с нуля. По этой же причине, если требуемый байт не обнаружен,
возвращается не ноль, а минус единица.
Мы могли бы написать программу чуть иначе:
Seek(F,i);
BlockRead( F, b, 1);

170 Глава 8. Техника программирования,или Это вы обязательно должны знать
То есть, получив вместо одной строчки две, одна из которых совершено лиш-
няя. Смысл этого, во-первых, напомнить, как можно перемещаться по файлу в
произвольную позицию, а во-вторых, унифицировать нашу программу для сравнения с её дальнейшими модифицированными вариантами.
Теперь главный вопрос — хороша ли наша программа? Вопрос странный —
наша программа, бесспорно, работает, и этого достаточно, чтобы заткнуть рот
всем критикам. Другой вопрос — а хорошо ли она работает? В нашем случае слово хорошо является синонимом слова быстро, и вопрос в том, нельзя ли её заставить работать быстрее. Когда вам зададут такой вопрос, вы должны не раздумывая ответить — а оно надо? Насколько важно быстродействие этой процедуры,
ведь это не самостоятельная программа, а маленькая окраина чего-то большого.
Сколько раз она вообще вызывается, за всё время выполнения основной программы, или за сутки. Велика ли будет экономия, если процедура заработает в
десять или в тысячу раз быстрее? Стоит ли этого ускорения труд программиста,
или наоборот? Всё, я расстроился.
Если успокоиться и посмотреть холодным взглядом, то очевидно, что про-
грамма наша работать будет достаточно быстро. Сейчас, в наше время, и насколько я понимаю, при обращении к файлу он весь засасывается в кэш и все
перемещения по нему равнозначны работе с оперативной памятью. Проблемы
возникнут если файл действительно большой. Раньше было не то. Раньше, когда
мы читали файл, то читался только один кластер — составлявший от половины
килобайта до нескольких, немногих килобайт — поэтому любое движение по
файлу обходилось очень дорого. Не исключаю, что при каких-то особых обстоятельствах это и сейчас так.
Если для нас важно сократить количество обращений к физическому носите-
лю, радикальным способом решения проблемы будет прочитать весь файл в память сразу. Тогда мы будем абсолютно уверены, что обращений к носителю у нас
ровно одно, меньше никак нельзя. Решим задачу традиционно, с помощью указателей.
var
F : File;
p,pWrk : pointer;
skoka : integer;
b : byte;
whereZ : integer;
i : integer;
begin
AssignFile( F, ‘gr-01.bmp’);
ReSet( F, 1);
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
