фон
Несколько лет назад, когда я был студентом, нам дали домашнее задание по амортизированному анализу. Я не смог решить одну из проблем. Я спрашивал об этом в теории , но удовлетворительного результата не было. Я помню курс, на котором Т.А. настаивал на том, что он не смог доказать, и сказал, что забыл доказательство, и ... [вы знаете, что].
Сегодня я вспомнил проблему. Я все еще хотел знать, так что вот оно ...
Вопрос
Можно ли реализовать стек, используя две очереди , чтобы операции PUSH и POP выполнялись за амортизированное время O (1) ? Если да, подскажите как?
Примечание: Ситуация довольно легко , если мы хотим реализовать очередь с двумя стеками (с соответствующими операциями Епдиеей и DEQUEUE ). Пожалуйста, обратите внимание на разницу.
PS: вышеупомянутая проблема не сама домашняя работа. Домашняя работа не требует каких-либо нижних оценок; просто реализация и анализ времени выполнения.
Ответы:
У меня нет фактического ответа, но вот некоторые доказательства того, что проблема открыта:
Это не упоминается в «Ming Li», «Luc Longpré» и «Paul MB Vitányi», «Сила очереди», Structures 1986, в которой рассматривается ряд других тесно связанных моделей
Это не упоминается в Мартине Хюне, «О силе нескольких очередей», Теор. Комп. Sci. 1993, последующая статья.
Это не упоминается в Holger Petersen, «Стеки против Deques», COCOON 2001.
Бертон Розенберг, "Быстрое недетерминированное распознавание контекстно-свободных языков с использованием двух очередей", Информ. Proc. Lett. 1998, дает O (n log n) алгоритм с двумя очередями для распознавания любого КЛЛ с использованием двух очередей. Но недетерминированный пуш-автомат может распознавать КЛЛ за линейное время. Таким образом, если бы имитация стека с двумя очередями быстрее, чем O (log n) на операцию, Розенберг и его судьи должны были бы знать об этом.
источник
Ответ ниже - «обман», поскольку он не использует пробел между операциями, но сами операции могут использовать больше, чем пробел. Смотрите в другом месте в этой теме ответ, у которого нет этой проблемы.O(1)
Хотя у меня нет ответа на ваш точный вопрос, я нашел алгоритм, который работает в время вместоO(n). Я считаю, что это жестко, хотя у меня нет доказательств. Во всяком случае, алгоритм показывает, что пытаться доказать нижнюю границуO(n)бесполезно, поэтому он может помочь в ответе на ваш вопрос.O(n−−√) O(n) O(n)
Я представляю два алгоритма, первый из которых представляет собой простой алгоритм с временем выполнения для Pop, а второй - с O ( √O(n) время работы для поп. Я описываю первый в основном из-за его простоты, так что второй легче понять.O(n−−√)
Более подробно: первый не использует дополнительного пробела, имеет толчок в худшем случае (и амортизированный) и O ( n ) в худшем случае (и амортизированный), но поведение в худшем случае не всегда срабатывает. Поскольку он не использует никакого дополнительного пространства за пределами двух очередей, он немного «лучше», чем решение, предложенное Россом Снайдером.O(1) O(n)
Второе использует одно целочисленное поле (таким образом, дополнительный пробел), имеет O ( 1 ) худший случай (и амортизируется) Push и O ( √O(1) O(1) амортизированный поп. Поэтому время его выполнения значительно лучше, чем у «простого» подхода, но все же использует дополнительное пространство.O(n−−√)
Первый алгоритм
Мы имеем две очереди: очередь и очереди ы е гр ö н д . е я г с т будет наша «толчок очереди», а с е с о н д будет очередь уже «порядка стека».first second first second
C # код для первого алгоритма
Это может быть вполне читабельно, даже если вы никогда раньше не видели C #. Если вы не знаете, что такое дженерики, просто замените все экземпляры 'T' на 'string' в своем уме для набора строк.
Анализ
Очевидно, Push работает за раз. Поп может касаться все внутри е я г с т и с е с о п d величины постоянного раза, поэтому мы имеем O ( п ) в худшем случае. Алгоритм демонстрирует это поведение (например), если один помещает n элементов в стек, а затем многократно выполняет одну операцию Push и одну операцию Pop подряд.O ( 1 ) еirst second O(n) n
Второй алгоритм
Мы имеем две очереди: очередь и очереди ы е гр ö н д . е я г с т будет наша «толчок очереди», а с е с о н д будет очередь уже «порядка стека».first second first second
Это адаптированная версия первого алгоритма, в котором мы не сразу «перетасовка» содержимое в ы е гр ö н д . Вместо этого, если е я г ы т содержит достаточно малое количество элементов по сравнению с ами е гр ö н д (а именно квадратный корень из числа элементов в ы е с ô п г ), мы только Реорганизовать е я г с т в порядке стека и не сливать его сfirst second first second second first .second
C # код для первого алгоритма
Это может быть вполне читабельно, даже если вы никогда раньше не видели C #. Если вы не знаете, что такое дженерики, просто замените все экземпляры 'T' на 'string' в своем уме для набора строк.
Анализ
Очевидно, Push работает за раз.O(1)
Поп работает в амортизированное время Есть два случая: если| еяRсект| < √O(n−−√) , затем мы перетасовываемfirstв порядок стеков вO(|first|)=O(√|first|<|second|−−−−−−−√ first время Если| еяRсект| ≥ √O(|first|)=O(n−−√) тогда у нас должно было быть как минимум√|first|≥|second|−−−−−−−√ призывает толкать. Следовательно, мы можем ударить только в этом случае каждый √n−−√ звонки в Push и Pop. Фактическое время работы для этого случая -O(n), поэтому амортизированное время -O( n).n−−√ O(n) .O(nn√)=O(n−−√)
Конечная нота
Это можно исключить дополнительную переменную за счет создания Pop операции, при наличии Pop РеорганизоватьфяRсектпри каждом вызове вместо тогоНажмите делать всю работу.O(n−−√) first
источник
После некоторых комментариев к моему предыдущему ответу мне стало ясно, что я более или менее изменяю: я использовал дополнительное пространство ( дополнительное место во втором алгоритме) во время выполнения моего метода Pop.O(n−−√)
Следующий алгоритм не использует никакого дополнительного пробела между методами и только дополнительного пробела во время выполнения Push и Pop. Push имеет O ( √O(1) амортизированное время выполнения, и Pop имеетO(1)наихудшее (и амортизированное) время выполнения.O(n−−√) O(1)
Примечание для модераторов: я не совсем уверен, является ли мое решение сделать это отдельным ответом правильным. Я думал, что не должен удалять свой первоначальный ответ, так как он все еще может иметь какое-то отношение к вопросу.
Алгоритм
Мы имеем две очереди: очередь и очереди ы е гр ö н д . е я г с т будет наш «кэш», а с е с о н д будет нашим основным «хранения». Обе очереди всегда будут в «порядке стека». е я г с т будет содержать в верхней части стека и элементы ы е гр ö н д будет содержать в нижней части стека элементов. Размер е я гfirst second first second first second всегда будет не более квадратного корня из s e c o n d .first second
C # код для первого алгоритма
Этот код должен быть достаточно читабельным, даже если вы никогда раньше не видели C #. Если вы не знаете, что такое дженерики, просто замените все экземпляры 'T' на 'string' в своем уме для набора строк.
Анализ
Очевидно, что Pop работает за раз в худшем случае.O(1)
Push работает в амортизированное время Есть два случая: если| еяRсект| < √O(n−−√) затем Push принимаетO(√|first|<|second|−−−−−−−√ время Если| еяRсект| ≥ √O(n−−√) Затем нажмите занимаетO(п)время, но после этой операциифяRсектбудет пустым. Это займетO(√|first|≥|second|−−−−−−−√ O(n) first время, прежде чем мы получим этот случай снова, поэтому амортизированное время равноO( nO(n−−√) времяO(nn√)=O(n−−√)
источник
first.Enqueue(first.Dequeue())
. Вы что-то опечатали?Я утверждаю, что у нас есть амортизированная стоимость за операцию. Алгоритм Алекса дает верхнюю границу. Чтобы доказать нижнюю границу, я приведу наихудшую последовательность ходов PUSH и POP.Θ(N−−√)
источник
Насколько я знаю, это новая идея ...
источник
Не используя дополнительное пространство, возможно, используя приоритетную очередь и заставляя каждое новое нажатие придавать ей больший приоритет, чем предыдущее? Тем не менее, не будет O (1), хотя.
источник
Я не могу заставить очереди реализовывать стек в амортизированном постоянном времени. Тем не менее, я могу придумать способ получить две очереди для реализации стека в худшем случае с линейным временем.
Конечно, мы можем добавить еще один бит внешнего состояния, которое сообщает нам, была ли последняя операция push или pop. Мы можем отложить перемещение всего из одной очереди в другую, пока не получим две операции push подряд. Это также делает операцию pop немного более сложной. Это дает нам O (1) амортизируемую сложность для операции pop. К сожалению, толчок остается линейным.
Все это работает, потому что каждый раз, когда выполняется операция push, новый элемент помещается в начало пустой очереди, а полная очередь добавляется в его хвостовую часть, эффективно изменяя элементы.
Если вы хотите амортизировать операции с постоянным временем, вам, вероятно, придется сделать что-то более умное.
источник
Существует тривиальное решение, если ваша очередь допускает фронтальную загрузку, для которой требуется только одна очередь (или, более конкретно, deque.) Возможно, именно такой тип очереди имел в виду курс TA в исходном вопросе?
Без разрешения фронтальной загрузки вот еще одно решение:
Этот алгоритм требует две очереди и два указателя, мы будем называть их Q1, Q2, основной и дополнительный, соответственно. После инициализации Q1 и Q2 пусты, первичные точки указывают на Q1, а вторичные указывают на Q2.
Операция PUSH тривиальна, она состоит только из:
Операция POP немного более сложна; это требует спулинга всех, кроме последнего элемента первичной очереди, во вторичную очередь, замены указателей и возврата последнего оставшегося элемента из исходной очереди:
Проверка границ не выполняется, и это не O (1).
Когда я набираю это, я вижу, что это можно сделать с одной очередью, используя цикл for вместо цикла while, как это сделал Алекс. В любом случае, операция PUSH - это O (1), а операция POP - O (n).
Вот еще одно решение, использующее две очереди и один указатель, называемые Q1, Q2 и queue_p соответственно:
После инициализации Q1 и Q2 пусты и queue_p указывает на Q1.
Опять же, операция PUSH тривиальна, но требует еще одного дополнительного шага - наведения queue_p на другую очередь:
Операция POP аналогична предыдущей, но теперь есть n / 2 элементов, которые необходимо повернуть через очередь:
Операция PUSH по-прежнему O (1), но теперь операция POP - O (n / 2).
Лично для этой проблемы я предпочитаю идею реализации единой двусторонней очереди (deque) и при необходимости называть ее стеком.
источник
источник
Стек может быть реализован с использованием двух очередей, используя в качестве второй очереди вторую очередь. Когда элементы помещаются в стек, они добавляются в конец очереди. Каждый раз, когда элемент извлекается, n - 1 элементов первой очереди должны быть перемещены во вторую, а оставшийся элемент возвращается. открытый класс QueueStack реализует IStack {private IQueue q1 = new Queue (); private IQueue q2 = новая очередь (); public void push (E e) {q1.enqueue (e) // O (1)} public E pop (E e) {while (1 <q1.size ()) // O (n) {q2.enqueue ( q1.dequeue ()); } sw apQueues (); return q2.dequeue (); } частный void swapQueues () {IQueue Q = q2; q2 = q1; q1 = Q; }}
источник