В программировании существует потребность не только в изученных нами коллекциях. Одна из таких очередь. Она реализует подход к хранению данных по принципу «Первый вошёл – первый ушел».
Реализуйте класс Queue
, который не имеет параметров инициализации, но поддерживает методы:
push(item)
— добавить элемент в конец очереди;pop()
— «вытащить» первый элемент из очереди;is_empty()
— проверят очередь на пустоту.
Примечание
Ваше решение должно содержать только классы и функции.
В решении не должно быть вызовов инициализации требуемых классов.
Пример
Ввод
queue = Queue()
for item in range(10):
queue.push(item)
while not queue.is_empty():
print(queue.pop(), end=" ")
Вывод
0 1 2 3 4 5 6 7 8 9
Ввод
queue = Queue()
for item in ("Hello,", "world!"):
queue.push(item)
while not queue.is_empty():
print(queue.pop())
Вывод
Hello,
world!
Решение
С учетом прекрасной реализации списков в python задача элементарная.
Я приведу три решения, первое – минимально необходимое, чтобы пройти тесты, второе с “защитой от дурака” и третье – классическое, такое как если бы в python не было списков и нам пришлось бы писать очередь самостоятельно.
Рассмотрим первый вариант решения.
Решение целиком и полностью построено на списках и не содержит проверки на пустоту списка. Кроме того, оно использует общий экземпляр списка, а это значит, что нельзя одновременно в программе иметь два списка, это вызовет проблему. Но так как по состоянию на 21 декабря 2023 года в тестах Яндекса к этой задаче нет дожных проверок, этот код успешно проходит проверку.
Класс не содержит никакой инициализации, просто объявляет глобальную для всего класса переменную в которую и будут заноситься элементы очереди.
Метод push() просто добавляет элемент в конец нашего списка.
Метод pop() удаляет и возвращает первый элемент нашего списка. Если список окажется пуст, то возникнет ошибка.
Метод is_empty() сравнивает наш список с пустым списком и возвращает результат сравнения.
Второй вариант решения, чуть более продвинутый. Он по-прежнему базируется на встроенных списках, но при этом в нем есть проверка на пустоту списка в методе pop() и в нем можно иметь сколько угодно списков одновременно. Из недостатков, лучше бы оно генерировало ошибку, но так как контролируемую генерацию ошибок мы еще не проходили, остановимся на этом варианте. Как следствие наш список не может содержать элементов None, так как возврат этого значения расценивается как пустой список.
Третий вариант – классический. Такой, которым создавались механизмы очереди двадцать и более лет назад. Еще раньше, вместо объектов использовался тип переменных указатель, но сути решения это не меняло.
Решение основано на элементе Node который является базовым для односвязных списков, очередей, стеков и односвязных колец.
Класс Node представляет из себя объект, содержащий значение текущего элемента очереди и ссылку на следующий объект. В момент создания объекта ссылка на следующий объект определяется как пустое значение (None)
Класс Queue чуть сложнее и выглядит крайне запутанно, если вы не смогли осилить рекурсию. Но даже если не смогли – не отчаивайтесь, возможно самостоятельный разбор этой задачи с бумагой и карандашом продвинет вас в понимании рекурсии.
В момент инициализации он создает две переменные – head (голову) и tail (хвост) списка. Эти переменные нужны для того, чтобы иметь быстрый доступ к началу и концу списка соответственно. В самом начале они принимают “пустое” значение (None).
Метод push() принимает значение и проверяет не пуста ли наша очередь. Если она пуста, то head получает новый элемент Node, а tail указывает на голову списка (tail = head). В противном случае в tail.next помещается новый объект Node(), и сама переменная tail теперь указывает на этот новый объект. Эти операции важно выполнять именно в таком порядке.
Метод pop() проверяет не пуста ли очередь. Если очередь не пуста, то временной переменной присваивается содержимое поля head.item, а head теперь указывает на следующий элемент (head = head.next). Опять же, порядок операций крайне важен.
Метод is_empty() просто проверяет не указывает ли head в пустоту.
Посмотреть код
Решение
# решение наивное. в нем есть ошибки с точки зрения программирования, но оно успешно прозодит тесты Яндекса
class Queue:
queue = []
def push(self, item):
self.queue.append(item)
def pop(self):
return self.queue.pop(0)
def is_empty(self):
return self.queue == []
Решение
# решение c проверкой на допустимость операции. возвращает None если попытаться извлечь элемент когда список пуст
class Queue:
def __init__(self) -> None:
self.queue = []
def push(self, item):
self.queue.append(item)
def pop(self):
item = None
if not self.is_empty():
item, *self.queue = self.queue
return item
def is_empty(self):
return self.queue == []
Решение
# классическое решение без испольщовнаия встроенных списков.
class Node:
def __init__(self, item) -> None:
self.item = item
self.next = None
class Queue:
def __init__(self) -> None:
self.head = self.tail = None
def push(self, item):
new_node = Node(item)
if self.is_empty():
self.head = new_node
self.tail = self.head
else:
self.tail.next = new_node
self.tail = new_node
def pop(self):
if self.is_empty():
return None
temp = self.head.item
self.head = self.head.next
return temp
def is_empty(self):
return self.head is None
значение текущее, но называется он next??
можно попросить вас объяснить логику?))
У нас есть объект Node. В этом объекте мы храним значение, которое присвоено объекту (item) и ссылку на следующее значение (next). В это имеет смысл погружаться только после того, как изучили тему про объектно-ориентированное программирование.
Кроме того, из-за того, что в python понятие ссылки тщательно скрыто от программиста, понять как это работает именно на примере python довольно сложно. Тем не менее, нужно понимать, что в компьютере все расположено по какому-то физическому адресу в памяти и фактически любая переменная это всего лишь указатель на этот адрес.
При операции присвоения возможны два варианта поведения компьютера – он создает копию содержимого переменной по новому адресу и возвращает вам указатель на этот новый объект или он просто возвращает вам ссылку на объект, который уже расположен в памяти.
Первый тип присвоения мы видим при присвоении неизменяемых типов данных, таких как строки и числа.
Второй тип встречается при присвоении изменяемых типов, таких как списки, множества, словари.
Именно поэтому если не скопировать, а просто присвоить существующему списку новое имя, то список становится доступен по обоим именам.
Скажите, пожалуйста, на каком языке лучше изучить это понятие?? С++ ??
Базовый C/C++, Pascal.
Если академический интерес, то Pascal, если больше практический с возможностью в дальнейшем уйти в другие языки – C
По ссылке{1 – ссылка} ниже есть строка self = {BMW_540i} <ссылка object at 0x3711204g1>
это ссылка??
либо ссылка выглядит по другому??
интересно как должна выглядеть
в принципе я понимаю что это ссылки и отслеживаю что если один объект то и ссылки одинаковые
{1 – ссылка}
Да, это и есть ссылка. Число в шестнадцатеричной форме – это адрес в памяти. Когда я говорил о том, что ссылки спрятаны, я имел в виду, что вы почти никогда не можете влиять на то что будет присвоено или передано – содержимое или ссылка. Python это решает за вас.
Исключения довольно редки – например
В одном случае в print попадет ссылка на список, а в другом, содержимое списка.
так оказывается можно картинку постить?))
да, конечно. И код и картинки
почему при
else:
self.tail.next = new_node
переписывается и self.head.next ??? из-за того что мы первоначально создали ссылку
self.tail = self.head
???
Да. Так как это сложный объект, операция присвоения просто копирует указатель, а не содержимое. Поэтому self.tail и self.head указывают в одну и ту же область памяти. По этой причине изменение любого из атрибутов одного из объектов приведет к тому, что это изменение отобразиться сразу на обоих объектах.
Но стоит обратить внимание, что так происходит только при первом присвоении в ветке else. Дальше head и tail живут каждый собственной жизнью.
можно не согласиться?
self.tail.next = new_node
записываю в tail.next – item, next; автоматически записывается и в head – item, next, {1 – ссылка}, но потом следующая строка
self.tail = new_node
и уже tail становится без вложений в атрибутах
Получается head и tail связаны, просто tail каждый раз обновляем, а в head каждый раз спускаемся на одну ступеньку
Либо я не понимаю {1 – ссылка}
head всегда указывает на самый первый элемент очереди и он неизменен пока мы не удаляем первый элемент. он нужен для того, чтобы мы всегда могли пробежать по очереди от начала до конца, в противном случае мы бы не смогли найти первый элемент очереди.
после удаления первого элемента, он начинает указывать на следующий. tail при этом остается неизменным.
то есть при добавлении элемента, мы работаем с tail, а при удалении с head.
я разбираюсь постепенно, пока что мне не понятна реализация push, надеюсь поняв push вопросов в pop не будет
1) я вижу что мы меняем tail, но не понимаю почему меняется и head. не вижу где они связаны, конечно есть __init__(self.head = self.tail = None)
и if в push(self.tail = self.head)
Но вы пишете, что это разовая ситуация.(свою логику даже боюсь озвучивать)
сейчас я удалил строку
# self.tail = new_node
я ожидал что и head и tail будут иметь вложения, ведь эта строка, как мне кажется, переписывает tail после того как в tail.next было записано значение new_node(предыдущая строка)
Почему идёт запись(хранение ли) в head? но обращаемся к tail:
self.tail.next = new_node
self.tail = new_node
Либо это относится к вашим словам
и поэтому у меня недопонимание
В самом начале наша голова и хвост указывают на один несуществующий объект.
После первого присвоения они все еще указывают на один и тот же, но уже реально существующий объект.
Но, когда мы добавляем еще один элемент, то хвост изменяется, а голова остается где была.
Следует отметить, что в первой строке мы совершаем операцию, которая в ситуации, когда хвост и голова это один и тот же объект меняет и хвост и голову. А вот вторая строка эту связь разрывает.
Если же голова и хвост это разные объекты, то первая строка меняет только хвост, а вторая меняет указатель хвоста на новый последний элемент.
Все строки этого кода работают со ссылками, а не со значениями переменных. Собственно, это и есть “скрыто” и именно это и вызывает недопонимание.
мне кажется что всё таки не разрывает.
на скрине состояние перед обработкой строки:
self.tail = new_node {1 – ссылка}
либо я не правильно понимаю слово.
я полагаю связь остаётся, просто каждый раз мы перезаписываем tail {1 – ссылка}
если бы связи не было то мы бы не пришли от head к tail
или я не правильно понял??
head не имеет ни малейшего представления где находится tail и сколько элементов между ними. Он знает только где находится следующий. Аналогично и с tail – он в принципе не понимает где живет head и есть ли вообще перед ним какие-то элементы.
В этом смысле есть всего два состояния, когда head и tail связаны – когда они указывают на один и тот же элемент и когда в списке всего два элемента. И то во втором случае связь односторонняя: мы можем попасть из head в tail, а обратно – нет.
я полагаю у меня нет больше вопросов
Большое спасибо за столь развёрнутые ответы))))
Что происходит с последним элементом списка self.queue во втором решении после этого присваивания?
item, *self.queue = self.queue
Это так называемая распаковка. Первый элемент списка присваивается первой переменной из списка слева, а вторая переменная слева принимает все оставшиеся значения.
Таким образом с последним не происходит ничего, а вот первый отрезается и передается в отдельную переменную.
Написал программу:
a = [1, 2]
b, *a = a
print(a)
print(a[1])
Выдаёт при запуске:
[2]
Traceback (most recent call last):
File “…”, line 4, in <module>
print(a[1])
IndexError: list index out of range
Получается, что последний элемент списка “а” исчез?
перед выполнением распаковки b = None, a = [1, 2]
после выполнения b = 1 (первый элемент пошел в переменную и удален из списка), а = [2]. в a остался список из одного элемента.
таким образом последний элемент никуда не девается,просто исчезает первый и количество элементов в списка уменьшается на единицу.
Необычный механизм, спасибо.