Глава J. Стек и стековые языки Урок J3. Использование нескольких стеков До сих пор все наши манипуляции были связаны с одним-единственным стеком, и мы радовались тому, насколько содержательны возможности его использования. А нельзя ли их еще расширить, располагая несколькими стеками одновременно? Что может дать нам второй стек? Первое, что приходит на ум: появляется место для хранения временной, промежуточной, информации. Сразу находится и применение. Так, хорошо знакомую процедуру обмена данными Swap (см. главу A) никак не реализовать в варианте Swap (< верхний элемент >, < «предыдущий» элемент >) с одним стеком, а с двумя – легко. Вообще говоря, ситуация достаточно типична: нам просто не хватает ресурса – рабочей памяти. Еще Архимед сказал: «Дайте мне рычаг, и я переверну мир», – в нашем случае лишняя доступная ячейка и есть тот необходимый рычаг. Между прочим, – это специально не оговаривалось! – мы неявно учитывали существование хотя бы одной рабочей ячейки, которой располагает исполнитель (процессор): в противном случае данные, вынимаемые из стека, в лучшем случае удавалось бы помещать в выходной поток, и ни о каких арифметических операциях над ними речи не было. Разумеется, можно снабдить исполнитель не единственной собственной ячейкой, и именно таков набор регистров «обычного» процессора. Развивать дальше рассуждения на эту «аппаратную» тему мы не станем. Но обратим ваше внимание, несколько забегая вперед, что в стековых языках, которым посвящены следующие занятия, наличие подобного рода памяти неявно предполагается. При том, непосредственного доступа к ней у программиста нет (просто нет необходимости), но в его распоряжении – библиотека стандартных подпрограмм, очевидно, нереализуемых без этого ресурса. Вернемся к варианту с единственной собственной ячейкой исполнителя, считая ее обязательным атрибутом. И обзаведемся вторым стеком. При «столь мощном» ресурсном обеспечении запрограммировать процедуру стекового обмена уже нетрудно. Естественно, в качестве параметра стандартных стековых операций придется указывать номер стека, примерно так: pop (<стек_1>) или push (<стек_2>). Можно, заодно, обзавестись и процедурой pop&push (<стек_1>, <стек_2>), назначение которой очевидно. Упражнение J3-1 a) Напишите процедуру Swap (<стек>), осуществляющую обмен двух верхних элементов стека в предположении, что в нем не менее двух элементов и доступен еще один стек. b) Напишите процедуру Pop&Push (<стек_1>, <стек_2>). Программирование с использованием более чем одного стека – отнюдь не экзотика. Оказывается, в этой главе мы уже встречались с существованием одновременно двух стеков. Вспомним (см. Занятие J 1) механизм 2 эмуляции стека на базе вектора; не все возможности этой структуры были тогда востребованы. Фактически, в нашем распоряжении оказываются 2 стека: первый, тот же самый, – стек данных, а второй – стек свободных элементов, включающий все элементы вектора, не вошедшие в стек данных. Указатель top связан с вершинами обоих стеков, его изменение противоположным образом отражается на объеме каждого из них: когда один растет в размере, – другой, наоборот, уменьшается. Соответственно, в приведенном ранее описании следовало бы теперь указать, что при top=0 пуст стек данных, а при top=N пуст стек свободных элементов. При такой интерпретации нам нужно контролировать возникновение двух, не сочетающихся, состояний: пуст стек_1 и пуст стек_2. С точки зрения стековой идеологии, этот подход представляется более естественным, поскольку ситуация переполнения стека данных, которую допускать-то нельзя, вообще исключена благодаря «обычному» контролю пустоты стека свободных элементов. Упражнение J3-2 На основе представления стека данных DataStack и стека свободных элементов FreeStack как частей вектора фиксированной длины (см. Упражнение J1-1) напишите a) булевские функции isDataStackEmpty и isFreeStackEmpty; b) функции Pop и StackTop и процедуру Push, используя указанные булевские функции. Убедившись в работоспособности и полезности двухстекового механизма, вы уже не станете удивляться, если столкнетесь и с бóльшим числом стеков. Характерное применение «многостекового» механизма – это процесс упорядочения информации. При этом процедуры типа Swap незаменимы. Упражнение J3-3 В этих примерах предлагается использовать только стековый механизм. Постарайтесь в своих решениях обойтись как можно меньшим числом стеков. a) Подумайте, как поменять местами 2 элемента из входного потока? b) Как упорядочить набор из 3-х элементов входного потока? c) Как реализовать сортировку входного потока? Так как мы уже достаточно много внимания уделили в других главах нашего курса разнообразным и многочисленным алгоритмам сортировки, то хочется обратиться к примеру из другой области. Вероятно, вам известна т.н. задача о ханойских башнях (рис. J3-1). Если нет, то краткая формулировка введет вас в курс дела, а обсуждать здесь легенды, связанные с названием задачи, мы не станем. Рис. J3-1. Ханойские башни Итак, имеется 3 стержня, – назовем их левым, средним и правым, – на них можно нанизывать диски. Любой диск, а всего их 64 штуки, разрешено надевать либо на свободный стержень, либо на стержень, верхний диск которого имеет больший диаметр, чем укладываемый. Шаг алгоритма состоит в том, что какойнибудь диск переносится с одного стержня на другой. В начальном положении все диски нанизаны на левый стержень и, по условиям задачи, необходимо перенести их на правый стержень. Поскольку операция снятия диска со стержня применима только к верхнему диску, а операция надевания диска осуществляется тоже только сверху, то вполне очевидно, что мы имеем дело с тремя стеками. Переформулируем теперь задание: переместить упорядоченную по возрастанию (считая от вершины стека) группу элементов из одного стека в другой, используя еще один стек в качестве вспомогательного и таким образом, чтобы содержимое каждого из трех стеков в процессе исполнения оставалось упорядоченным по возрастанию. Шаг алгоритма состоит из двух последовательных стековых операций: Pop (i) и Push (j), где i, j ∈ {left, middle, right} & i≠j, и в исходном состоянии i = left. Тот факт, что задача разрешима, мы здесь не станем доказывать, а пока лишь продемонстрируем идею решения на примерах. Заметим при этом, что процесс перекладывания 64, согласно условию, дисков займет очень много времени, поэтому имеет смысл уменьшить их количество N. Для N=2 последовательность шагов такова: Номер шага алгоритма 0 1 2 3 Содержимое стека Left 1 2 2 Пусто Пусто Содержимое стека Middle Пусто 1 1 Пусто Содержимое стека Right Пусто Пусто 2 1 2 Содержимое стека Left 1 2 3 2 3 3 3 Пусто 1 1 Пусто Содержимое стека Middle Пусто Пусто 2 1 2 1 2 2 Пусто Пусто Содержимое стека Right Пусто 1 1 Пусто 3 3 2 3 1 2 3 Для N=3: Номер шага алгоритма 0 1 2 3 4 5 6 7 Каково же количество шагов M алгоритма в общем случае? Учитывая, что при N=1 диск переносится за один шаг (M=1), при N=2 имеем M=3, а при N=1 – уже M=7, то можно предположить, что между исходным числом дисков и необходимым количеством шагов имеется зависимость вида M=2N-1. Упражнение J3-4 Напишите программу Hanoi, реализующую описанный выше механизм. Ваша программа должна генерировать последовательность строк, в каждой из которых указываются по две очередных операции Pop и Push с соответствующими аргументами. Во входном текстовом файле записано натуральное число N – количество дисков. В выходной текстовый файл поместить M строк, в каждую строку – пару указанных вызовов процедур в формате, отраженном в приведенном ниже примере. Пример входного и соответствующего выходного файлов (для N=2): Hanoi.in Hanoi.out 2 Pop(1) Push(2) Pop(1) Push(3) Pop(2) Push(3) Если последнее задание вызывает у вас затруднения, – познакомьтесь с приводимым ниже алгоритмом и попробуйте вновь вернуться к программе. А идея состоит в том, что: • из N дисков со стержня left нужно N-1 верхних перенести на стержень middle; • по завершении самый большой диск оставался на стержне left; переносим его на стержень right; • осталось перенести N-1 диск (а это мы уже умеем) со стержня middle на стержень right. Если программная реализация и теперь вызывает проблемы, то полная ясность наступит после знакомства с механизмом рекурсии, который нам еще предстоит обсуждать в этой главе. А пока для вас не составит труда выполнить Упражнение J3-5 Докажите правильность нашего предположения, что M = 2N-1, воспользовавшись методом математической индукции. Завершая тяжелую работу «переносчиков дисков», обратим еще раз внимание на постановку задачи: обойтись в ней без стеков невозможно – разрушится вся конструкция условия; уменьшить число стеков до двух нельзя, поскольку задача становится неразрешимой; можно, конечно, увеличить их число, но это «неинтересно», да и никак не опровергает тезис о полезности многостековой обработки! Пусть добавлять новые стеки в дополнение к трем «ханойским» не стоит, но любопытно найти иное применение набору из четырех указанных структур. Что ж, если вы пользуетесь одной из операционных систем семейства Microsoft Windows, то пример у вас под рукой. Мы имеем в виду пасьянс «Солитер», включаемый производителем в состав названного продукта, – надо полагать, в рекламных целях. Вероятно, можно найти приложения, где стеков еще больше. Предоставляем это читателю. Надеемся, он поделится с нами удачной находкой.