Глава J. Стек и стековые языки Урок J3. Использование

advertisement
Глава 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, то пример у вас под рукой. Мы имеем в виду пасьянс «Солитер»,
включаемый производителем в состав названного продукта, – надо полагать, в
рекламных целях.
Вероятно, можно найти приложения, где стеков еще больше. Предоставляем это
читателю. Надеемся, он поделится с нами удачной находкой.
Download