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

Полезное программирование

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
1 Мб
Скачать
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);
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]