📦 Репозиторий занятия 30

Урок 30. List comprehension. Стек и очередь

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции219 строк

List comprehension: от базового шаблона до разбора на блоки

less_16__list_comprehension__stack__queue/theory_01__lists_comprehension.md

Проходит все формы генератора списков — базовый, с условием после for, с тернарным if...else внутри выражения, вложенный двойной for, LC внутри LC для матриц, несколько условий подряд и flattening. Финальная и самая ценная часть — формальный алгоритм из 4 шагов, который превращает любой, даже многоярусный, list comprehension в эквивалентный набор вложенных циклов и if, с разобранным примером на пяти уровнях вложенности.

  • [x**2 for x in nums] — базовый шаблон [выражение for переменная in iterable if условие]
  • ['even' if x % 2 == 0 else 'odd' for x in nums] — тернарный if внутри выражения
  • [(x, y) for x in [...] for y in [...]] — вложенный двойной for, порядок циклов
  • [[x*y for x in range(1,4)] for y in range(1,4)] — LC внутри LC для матрицы
  • [x for row in matrix for x in row] — flattening вложенных списков
  • Алгоритм разбора сложного LC на блоки: result=[], перенос for/if по порядку, result.append(выражение)
Показать начало файла (219 строк всего)
## Определение

**List comprehension** (LC) — это способ создания списка из итерируемого объекта в одной строке  
с помощью выражения, цикла и (опционально) условий.

Общий шаблон:

```python
[выражение for переменная in итерируемый_объект if условие]
```

⚠️ ⚠️ ⚠️ 
ВАЖНО: 
Если логика выражения сложнее базового шаблона, его КРАЙНЕ ЖЕЛАТЕЛЬНО разбить на блоки. 

---

## 1. Базовый LC

```python
nums = [1, 2, 3, 4, 5]
squares = [x**2 for x in nums]
print(squares)  # [1, 4, 9, 16, 25]
```

---

## 2. LC с условием (`if` после цикла)

```python
nums = [1, 2, 3, 4, 5]
even = [x for x in nums if x % 2 == 0]
print(even)  # [2, 4]
```

---

## 3. LC с тернарным оператором (`if ... else ...`)

Тут `if` стоит **внутри выражения**, а не после цикла.

```python
nums = [1, 2, 3, 4, 5]
labels = ["even" if x % 2 == 0 else "odd" for x in nums]
print(labels)  # ['odd', 'even', 'odd', 'even', 'odd']
```
Аналогично:
```python
labels_2 = []
for x in nums:
    labels_2.append("even" if x % 2 == 0 else "odd")
    
print(labels_2)  # ['odd', 'even', 'odd', 'even', 'odd']

# --- или -------------------------------------

labels_3 = []
for x in nums:
    if x % 2 == 0:
        labels_3.append("even")
…
Проверьте себя: В шаблоне [x for row in matrix for x in row] какой цикл внешний, а какой внутренний, если разворачивать его в обычные for — и что изменится, если поменять их местами?
2
MarkdownРазбор концепции52 строк

zip(): объединение нескольких итерируемых в тюплы

less_16__list_comprehension__stack__queue/theory_02__zip.md

Короткое определение zip() как функции, которая параллельно проходит по нескольким итерируемым объектам и склеивает элементы с одинаковым индексом в тюплы, с явным предупреждением: если длины разные, результат обрезается по самому короткому объекту без ошибки.

  • zip(iter1, iter2, ..., iterN) — синтаксис, возвращает итератор тюплов
  • list(zip(nums, letters)) — базовая пара списков
  • zip(a, b, c) — три списка одновременно
  • zip([1,2,3,4], ['x','y']) — обрезание по минимальной длине без исключения
Показать начало файла (52 строк всего)
## Определение

`zip()` ― это функция, которая **объединяет несколько итерируемых объектов** в ОДИН итератор тюплов. 

Если объекты разной длины, длина результата будет равна длине наименьшего объекта.


Синтаксис:

```python
zip(iter1, iter2, ..., iterN)
```

---

## 1. Простейший пример

```python
nums = [1, 2, 3]
letters = ['a', 'b', 'c']

zipped = list(zip(nums, letters))
print(zipped)  # [(1, 'a'), (2, 'b'), (3, 'c')]
```

---

## 2. Несколько списков

```python
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]

print(list(zip(a, b, c)))
# [(1, 4, 7), (2, 5, 8), (3, 6, 9)]
```

---

## 3. Разная длина списков

⚠️ `zip()` работает до минимальной длины.

```python
a = [1, 2, 3, 4]
b = ['x', 'y']

print(list(zip(a, b)))
# [(1, 'x'), (2, 'y')]
```
Проверьте себя: Почему zip() не бросает исключение, если списки разной длины, а просто останавливается на меньшем — и как получить все элементы, включая 'хвост' более длинного списка?
3
MarkdownРазбор концепции49 строк

Стек (LIFO) на списке: push, pop, peek и проверка скобок

less_16__list_comprehension__stack__queue/theory_03__stack.md

Определяет стек как структуру LIFO и реализует его на обычном list, где append() играет роль push, pop() — pop, а stack[-1] — peek (с оговоркой, что peek — термин из теории структур данных, а не метод списка). Практический пример check_parentheses() проверяет сбалансированность скобок в строке — классическая задача на стек, разобранная на трёх тестовых строках.

  • LIFO — Last In, First Out
  • stack.append(x) — push, stack.pop() — pop, stack[-1] — peek
  • check_parentheses(s) — алгоритм проверки баланса скобок через стек
  • check_parentheses('(())') → True, '(()' → False, '())(' → False
Показать начало файла (49 строк всего)
## Определение

**Стек (stack)** — это структура данных типа **LIFO** 
(*Last In, First Out* — «последним пришёл, первым ушёл»).



## Простейшая реализация на списке

```python
stack = []  # создаём пустой стек

# push
stack.append(1)
stack.append(2)
stack.append(3)
print(stack)  # [1, 2, 3]

# pop
top = stack.pop()
print("Сняли:", top)      # 3
print("Стек:", stack)     # [1, 2]

# peek (смотрим верхний элемент) - просто название операции из теории структур данных, а не метод списков!
print("Верхний:", stack[-1])  # 2
```


---

## Пример использования (проверка скобок)

```python
def check_parentheses(s):
    stack = []
    for char in s:
        if char == "(":
            stack.append(char)  # push
        elif char == ")":
            if not stack:
                return False
            stack.pop()         # pop
    return not stack

print(check_parentheses("(())"))   # True
print(check_parentheses("(()"))    # False
print(check_parentheses("())("))   # False
```
Проверьте себя: В check_parentheses() почему проверка `if not stack: return False` стоит именно перед stack.pop(), а не после — что сломается, если её убрать?
4
MarkdownРазбор концепции58 строк

Очередь (FIFO): реализация на list против collections.deque

less_16__list_comprehension__stack__queue/theory_04__queue.md

Определяет очередь как структуру FIFO и сравнивает две реализации: на обычном list, где dequeue делается через pop(0), и на collections.deque, где для этого есть popleft(). Ключевой практический вывод помечен предупреждением: pop(0) на списке — это O(n), потому что сдвигаются все элементы, тогда как popleft() у deque работает за O(1), поэтому для очередей рекомендован именно deque.

  • FIFO — First In, First Out, аналогия с очередью в магазине
  • queue.append(x) / queue.pop(0) — enqueue/dequeue на list
  • ⚠️ pop(0) — O(n), сдвиг всех элементов
  • from collections import deque; queue.popleft() — O(1) dequeue
  • queue[0] — peek для обеих реализаций
Показать начало файла (58 строк всего)
## Определение

**Очередь (queue)** — это структура данных типа **FIFO**  
(*First In, First Out* — «первым пришёл, первым ушёл»).

Пример из жизни: очередь в магазине — первый вошёл, первый обслужен.


---

## 1. Реализация на `list`

```python
queue = []

# enqueue
queue.append(1)
queue.append(2)
queue.append(3)
print(queue)  # [1, 2, 3]

# dequeue (удаляем из начала!)
first = queue.pop(0)
print("Обслужили:", first)  # 1
print("Очередь:", queue)    # [2, 3]

# peek
print("Первый:", queue[0])  # 2
```

⚠️ Минус: `pop(0)` медленный (O(n)), потому что все элементы сдвигаются.

---

## 2. Реализация на `collections.deque` (рекомендуется)

```python
from collections import deque

queue = deque()

# enqueue
queue.append("a")
queue.append("b")
queue.append("c")
print(queue)  # deque(['a', 'b', 'c'])

# dequeue
print(queue.popleft())  # a
print(queue.popleft())  # b
print(queue)            # deque(['c'])

# peek
print(queue[0])         # c
```

Здесь `popleft()` работает за O(1), поэтому `deque` → идеален для очередей.
Проверьте себя: Почему pop(0) на списке медленнее popleft() на deque, и что физически происходит в памяти при удалении первого элемента списка?
5
MarkdownРазбор концепции36 строк

Устойчивость сортировки: что сохраняет sorted() при равных ключах

less_16__list_comprehension__stack__queue/theory_05__stable_sort.md

Короткий файл про стабильность sorted() и .sort() в Python: если два элемента равны по критерию сравнения, их взаимный порядок после сортировки останется таким же, как до неё. Завершается «вопросом на засыпку» без прямого ответа — сравнивает сортировку по len (где видно сохранение исходного порядка у слов одинаковой длины) с сортировкой без key (где 'равенство' по длине не действует, поэтому порядок слов одинаковой длины другой).

  • Определение: стабильная сортировка сохраняет относительный порядок равных элементов
  • Python: sorted() и .sort() всегда устойчивы
  • sorted(words, key=len) — apple/banana и dog/bat/cat сохраняют исходный взаимный порядок внутри своей длины
  • sorted(words) без key — сравнение идёт по всей строке, а не по длине, поэтому порядок другой
Показать начало файла (36 строк всего)
## Определение

**Устойчивость сортировки** (англ. **stable sort**) — это свойство алгоритма сортировки,  
при котором **сохраняется относительный порядок элементов с одинаковыми ключами**.

То есть, если два элемента равны с точки зрения сравнения критерия сортировки,  
то ПОСЛЕ сортировки они будут расположены в том же порядке, что и БЫЛИ ДО сортировки.

В Python `sorted` и `.sort()` всегда устойчивы.
---

## Пример

### Устойчивый алгоритм

```python
words = ["apple", "dog", "bat", "cat", "banana"]

# сортируем по длине слов
sorted_words = sorted(words, key=len)
print(sorted_words)
# ['dog', 'bat', 'cat', 'apple', 'banana']
```

ВОПРОС "на засыпку":

Почему тогда здесь равные по длине элементы изменили свой порядок?

```python
words = ["apple", "dog", "bat", "cat", "banana"]

# сортируем по длине слов
sorted_words = sorted(words)
print(sorted_words)
# ['apple', 'banana', 'bat', 'cat', 'dog']
```
Проверьте себя: В файле задан вопрос 'на засыпку': почему при sorted(words) без key порядок 'bat', 'cat', 'dog' отличается от их порядка в sorted(words, key=len)? Разница именно в критерии сравнения или в устойчивости сортировки?