Глава 45 Очереди и стеки



Хорошей вещи найдется уйма применений. Взять хотя бы строки, из которых мы построим сейчас инструменты системных программистов – очереди и стеки.

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

Вовочка в потоке событий

Переживайте неприятности по мере их поступления – эта житейская мудрость касается любого из нас. Жизнь – это поток событий, барахтаясь в котором, мы поминутно решаем: прервать ли начатое дело и хвататься за другую проблему? Или отложить эту новую проблему «на потом»?

Смотрите: вот компьютер и Вовочка, мирно играющий в «звездные войны». Входит мама: «Вова, ты уроки сделал?». Вова неохотно откладывает игру и приступает к урокам. Через полчаса мама снова беспокоит его: «Вовочка, я не могу отойти от плиты. Сгоняй-ка быстро за луком». Уроки откладываются, и Вова отправляется в магазин. По пути он видит гоняющих мяч приятелей, – им не хватает вратаря. Как не помочь друзьям? Поход в магазин откладывается, и Вовочка на страже ворот. Делу время, а потехе час. Закончилась игра, и можно выполнить мамино поручение. Купив луку, Вова доделывает отложенные уроки и возвращается к первому занятию – «звездным войнам».

Вовочка обрабатывал события по принципу стека, – скажет об этом системный программист. Другое название стека – дисциплина обслуживания LIFO, что значит «Last-In, First-Out», или по-русски: «последним пришел – первым ушел». Мы соприкоснулись со стеком, разбирая рекурсивную процедуру быстрой сортировки. Помните пример с укладкой рюкзака? А вот ещё пример: магазин для патронов. Патрон, вставленный в магазин последним, выстрелит первым, и наоборот.

Итак, пример с Вовочкой показал, что важные события мы обслуживаем по принципу стека.

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

Или другой пример – печатание документов на сетевом принтере. Когда принтер занят, операционная система ставит запросы на печать в очередь. А затем – по мере готовности принтера – выбирает файлы из этой очереди и отправляет принтеру.

За очередью тоже закрепилось свое сокращение – FIFO, что значит «First-In, First-Out» – «первым пришел, первым ушел». В отличие от стека, в очередь попадают маловажные события.

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

Танцевальный кружок

В городе набирали детей в кружок бального танца. Запись в кружок вела преподающая в нём дама. Приходящих мальчишек и девчонок она стремилась объединять в пары при первой возможности с тем, чтобы они сразу приступали к занятиям. Однако на запись дети приходили поодиночке в разное время и в случайном порядке, – то несколько мальчиков подряд, то несколько девочек. Дама не слыхала о системном программировании, а потому прибегла к здравому смыслу. Взяв пару записных книжек, она поступала так.

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

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



Рис. 99 – Запись в танцевальный кружок

Теперь станьте на место учителя: к вам приходят то мальчик, то девочка, но вы не знаете, кто будет следующим, – это поток, и в нём доступен только первый его элемент. Организовать входной поток можно посимвольным вводом «мальчиков» и «девочек». Но мы сделаем ещё проще: представим поток детишек строкой, и будем считать, что к преподавателю они являются слева направо по одному. Например, строка


    ZHJKqwertASDyuiopQWERTYUIOPasdf


означает, что первым явится мальчик по имени Z, а последней – девочка по имени f. Первая пара составится из мальчика Z и девочки q. Это упрощение не меняет сути нашей модели, но избавляет её от второстепенных деталей.

Итак, с учетом всех договоренностей, явим задачу в окончательном виде. Дана строка, состоящая из больших и маленьких букв латинского алфавита – «мальчиков» и «девочек». Мы должны сформировать другую строку, состоящую из тех же символов, но следующих попарно: сначала большая буква – «мальчик», затем маленькая – «девочка». Пары разделяются пробелом. Например, для указанной выше строки, пары должны быть составлены так:


    Zq Hw Je Kr At Sy Du Qi Wo Ep Ra Ts Yd Uf


А напоследок программа должна напечатать имена тех, кто временно остался без пары. Здесь это будут пришедшие в числе последних мальчики I, O и P.

Если логика программы вам ясна, разрешим теперь главный вопрос: как организовать очередь символов? Ведь очередь – это не просто массив данных, а механизм, содержащий и хранилище данных и процедуры для работы с ними.

Сделаем так. Элементы очереди – символы – будем хранить в строковых переменных. К ним добавим ещё две процедуры: одну – для установки элемента в очередь, другую (это будет функция) – для извлечения из очереди первого элемента. Назовем их соответственно PutInQue – «поставить в очередь» и GetFromQue – «извлечь из очереди» (Queue – «очередь» или «хвост»). Всё это представлено в программе «P_45_1».


{ P_45_1 – Запись в танцевальный кружок }


{ Постановка символа arg в очередь Que }


procedure PutInQue(var Que: string; arg: char);

begin

Que:= Que + arg; { добавляем в конец строки }

end;


{ Выбор из очереди Que элемента в параметр arg }


function GetFromQue(var Que: string; var arg: char): boolean;

begin

if Length(Que) = 0     { если очередь пуста }

    then GetFromQue:= false

    else begin

    GetFromQue:= true;     { если не пуста }

    arg:= Que[1];     { запоминаем первый элемент }

    Delete (Que, 1, 1); { и удаляем его из очереди }

    end

end;


    { Глобальные переменные }

var S_IN : string; { входной поток – символы }

    S_OUT : string; { выходной поток (пары) }

    Boys : string; { очередь мальчиков }

    Girls : string; { очередь девочек }

    c1,c2 : char; { очередная пара – символы строки }

    i : integer;     { индекс во входном потоке }


begin {--- Главная программа ---}


{ задаем (вводим) входной поток: A..Z – мальчики, a..z – девочки }

S_IN:='ZHJKqwertASDyuiopQWERTYUIOPasdf';

S_OUT:='';     { выходной поток пока пуст }

Boys:=''; Girls:=''; { Очищаем очереди мальчиков и девочек }


{ Цикл обработки входного потока }

for i:=1 to Length(S_IN) do begin

    c1:= S_IN[i]; { выбираем из входного потока }

    if c1 in ['A'..'Z']

    then begin { если это мальчик…}

    { если в очереди есть девочка }

    if GetFromQue(Girls, c2)

    { добавляем пару в выходной поток }

    then S_OUT:= S_OUT+c1+c2+’ ’

    { а иначе помещаем мальчика в очередь }

    else PutInQue(Boys, c1);

    end

    else begin { а если это девочка…}

    { если в очереди есть мальчик }

    if GetFromQue(Boys, c2)

    { добавляем пару в выходной поток }

    then S_OUT:= S_OUT+c2+c1+’ ’

    { а иначе помещаем девочку в очередь }

    else PutInQue(Girls, c1);

    end

end;

Writeln('Входной поток:' );

Writeln(S_IN);

Writeln('Выходной поток:' );

Writeln(S_OUT);


if Length(Boys)>0 then begin

    Writeln('В очереди мальчиков остались:' );

    Writeln(Boys);

end;


if Length(Girls)>0 then begin

    Writeln('В очереди девочек остались:' );

    Writeln(Girls);

end;

Readln;

end.


Процедура PutInQue просто добавляет символ в конец строки. Строго говоря, если длина строки достигнет 255, то новый символ не попадет в очередь. Но мы не станем усложнять программу дополнительными проверками, – считаем, что емкости очереди нам достаточно.

Но для функции GetFromQue, выбирающей из очереди первый символ, контроль строки на пустоту необходим, иначе работа модели нарушится. Функция возвращает состояние очереди, бывшее до извлечения символа (TRUE, если очередь не была пуста). А сам извлекаемый символ возвращается через параметр arg, – это ссылка на символьную переменную. Вот, пожалуй, и вся премудрость. Испытайте эту программу. Добавьте операторы печати для наблюдения за очередями.

Скитания товарного вагона

Прежде, чем углубиться в стек, вникнем в работу железной дороги. Вы знаете, как железнодорожники доставляют товарный вагон из пункта «А» в пункт «Б»? «Очень просто, – скажете, – цепляют к составу и тащат!» Тогда взгляните на рис. 100.



Рис.100 – Доставка вагонов между несколькими станциями

Здесь показаны пять железнодорожных станций, четыре из которых обозначены цифрами, а пятая – узловая станция – буквой «У». Предположим, что со станции «1» надо доставить несколько десятков вагонов на другие станции (по направлению стрелок). С этих станций тоже везут вагоны, но соответствующие стрелки я не показал. Тащить вагоны поодиночке разорительно! Поэтому их собирают в составы по нескольку десятков вагонов. Накопив такой состав на станции «1», железнодорожники доставляют его на узловую станцию; сюда же стекаются составы с других направлений. На узловой творится самое интересное, – здесь из одних составов формируют другие с тем, чтобы тащить их далее в нужном направлении. Эта работа называется сортировкой состава. В нашей стране сотни товарных станций, многие из которых узловые. Прежде чем попасть по назначению, вагон кочует между узловыми станциями, проходя через несколько сортировок. А вы говорите: просто, просто!

Но это ещё присказка, – сказка впереди.

Сортировочная горка

Итак, на узловых станциях формируют новые составы с тем, чтобы каждый вагон следовал далее в нужном направлении. Для сортировки устроены так называемые сортировочные горки. Горка – это слегка наклоненный участок пути; если отцепить от стоящего на нём состава вагон, последний покатится под горку. Но укатится недалеко, – под горкой устроено несколько тупиков. Тупик – это обычное состояние программиста, но здесь я говорю о других тупиках, железнодорожных. Это участки пути, ограниченные с одной стороны земляным валом. Уткнувшись в этот вал, вагон остановится. Горка соединяется с тупиками железнодорожными стрелками, переключая которые можно направить катящийся с горки вагон в тот или иной тупик (рис. 101).



Рис.101 – Схема сортировочной горки

Как сортируют состав? Основную работу выполняют двое: сцепщик и стрелочник (который всегда виноват!). От стоящего на горке состава сцепщик отсоединяет по очереди вагон за вагоном и сообщает стрелочнику по рации, в который из тупиков направить очередной вагон, – тому остается лишь переводить свои стрелки. Вкатившись в тупик, вагон тормозится земляным валом или слегка соударяется с уже стоящим там вагоном и автоматически сцепляется с ним. «Разбросав» по тупикам один состав, на горку выкатывают другой и продолжают сортировку, пока в тупиках не сформируются новые составы, готовые к дальнейшему пути.

Наша цель: смоделировать сортировочную горку, то есть создать программу, ведущую себя подобно такой горке. Вагоны заменим символами; следовательно, и состав и стоящие в тупиках вагоны мы представим строками. Будем считать первый символ строки первым вагоном состава, – он прицеплен к локомотиву. В тупике первым будет вагон, стоящий у земляного вала. Легко догадаться, что обрабатывать вагоны будем по принципу стека, поскольку сцепщику всегда доступен только последний вагон.

Договорившись об этом, сформулируем задачу окончательно. Дана строка символов (состав), из которой надо сформировать три других. Вагоны, обозначенные большими буквами ’A’…’Z’, отправим на станцию «A»; другие, обозначенные маленькими буквами ’a’…’z’, – поедут к станции «B», а третьи, обозначенные цифрами ’0’…’9’, – к станции «C». Программа должна сформировать три строки – это вновь собранные составы. Первый символ в них, – это вагон, прицепленный непосредственно к локомотиву.

Для решения задачи надо всего лишь в точности повторить действия сцепщика и стрелочника. Будем «отцеплять» символы от строки и «заталкивать» их в стеки – это наши тупики. Значит, надо построить механизм для стеков. Он будет похож на механизм для очереди: элементы храним в строковых переменных, а для занесения и извлечения элементов из стека учредим две процедуры. По традиции программисты называют эти процедуры так: Push – затолкнуть в стек, и Pop – вытолкнуть из стека.

Процедура заталкивания в стек Push присоединяет символ к концу строки. Она точь-в-точь повторяет процедуру установки в очередь, только называется иначе.

А функция выталкивания из стека Pop возвращает последний символ строки, одновременно удаляя его оттуда. Если же стек окажется пуст, функция сообщит об этом. Сходство с функцией извлечения из очереди очевидно, разница лишь в позиции извлекаемого символа: для очереди это первый символ, а для стека – последний.

Теперь вам не составит труда разобраться в показанной ниже программе «P_45_2». Обратите внимание на отцепку вагонов от исходного состава: она тоже выполняется функцией выталкивания Pop, поскольку исходный состав трактуется как непустой стек.


{ P_45_2 – Сортировочная станция }


{ Помещение элемента в стек }

procedure Push(var aStack: string; arg: char);

begin

    aStack:= aStack + arg; { добавляем в конец строки }

end;


{ Извлечение элемента из стека }

function Pop(var aStack: string; var arg: char): boolean;

begin

if Length(aStack) = 0 { если стек пуст }

    then Pop:= false     { сообщаем об этом }

    else begin

    { возвращаем последний элемент }

    arg:= aStack[Length(aStack)];

    { и удаляем его из стека }

    Delete(aStack, Length(aStack), 1);

    Pop:= true; { признак того, что стек не был пуст }

    end;

end;


var S : string;     { исходный состав }

    SA, SB, SC : string; { три сортировочных тупика A,B,C}

    c : char;     { очередной вагон }

begin

S:= 'HEjd31kDJK62px912se3BKdwL9'; { Исходный состав }

Writeln('Исходный состав: '+S);

SA:=’’; SB:=’’; SC:=’’; { очистка тупиков }

{ Отцепляем вагоны от исходного состава и заталкиваем в тупики }

while Pop(S, c) do begin

    if c in ['A'..'Z'] then Push(SA, c);

    if c in ['a'..'z'] then Push(SB, c);

    if c in ['0'..'9'] then Push(SC, c);

end;

{ Теперь исходный состав пуст, то есть S='' }

{ Выкатываем вагоны из тупика A и цепляем к первому составу }

while Pop(SA, c) do Push(S, c);

Writeln('На станцию A : '+S);

S:=''; { Освобождаем пути }

{ Выкатываем вагоны из тупика B и цепляем ко второму составу }

while Pop(SB, c) do Push(S, c);

Writeln('На станцию B : '+S);

S:=''; { Освобождаем пути }

{ Выкатываем вагоны из тупика C и цепляем к третьему составу }

while Pop(SC, c) do Push(S, c);

Writeln('На станцию C : '+S);

Readln;

end.


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

Итоги

• Очереди и стеки – это механизмы, применяемые в системных программах. Элементами очередей и стеков могут быть любые объекты.

• Очередь обслуживает элементы по принципу «первый пришел – первый ушел» или сокращенно FIFO (First-In, First-Out).

• Стек обслуживает элементы по принципу «последний пришел – первый ушел» или сокращенно LIFO (Last-In, First-Out).

А слабо?

А) Исследуя модель танцевального кружка, можно заметить, что в любой момент одна из двух очередей обязательно пуста. В самом деле, если приходит больше мальчиков, то будет пуста девчоночья очередь и наоборот. Можно ли обойтись одной очередью? Придумайте, как это сделать.

Подсказка: добавьте функцию для тестирования очереди с тем, чтобы выяснить, не пуста ли она. И, если не пуста, то кто томится в ней – мальчик или девочка? Эта функция не должна изменять состояние очереди.

Б) На реальных станциях на горку последовательно загоняют несколько составов, а уж потом освобождают тупики. Добавьте в модель сортировочной горки возможность такой обработки. Исходные составы (строки) должны вводиться с клавиатуры, признак окончания ввода – пустая строка. Совет: выделите действия по сортировке одного состава в отдельную процедуру.

В) Постройте механизмы очереди и стека на базе массива символов, а не на базе строки. Какие дополнительные переменные здесь понадобятся?

Загрузка...