Skip to content

Commit f713203

Browse files
agorinenkoAnton Gorinenko
andauthored
Двухсторонняя очередь (#14)
Co-authored-by: Anton Gorinenko <[email protected]>
1 parent faccf74 commit f713203

File tree

5 files changed

+105
-20
lines changed

5 files changed

+105
-20
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
[Очередь с приоритетом](tutorial/priority_queue.md) (в работе)
3939

40-
[Двухсторонняя очередь](tutorial/deque.md) (в работе)
40+
[Двухсторонняя очередь](tutorial/deque.md)
4141

4242
### Алгоритмы
4343

img/deque_1.png

41.5 KB
Loading

img/deque_2.png

18.5 KB
Loading

tutorial/deque.md

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,104 @@
1-
## Двухсторонняя очередь
1+
## Двусторонняя очередь
2+
3+
**Двусторонняя очередь** - абстрактный тип данных, в котором элементы можно добавлять и удалять как в начало, так и в
4+
конец. В python есть реализация двусторонней очереди "deque", которая является обобщением очереди и стека. Данная
5+
структура поддерживает эффективное извлечение элемента из любого конца за **O(1)**. Доступ к первому и последнему
6+
элементу осуществляется также за **O(1)**, но имеет временную сложность за **O(n)** для доступа к произвольному
7+
элементу.
8+
9+
Ниже приведена временная сложность операций для двусторонней очереди:
10+
11+
1. Добавление нового элемента справа/слева - **O(1)**.
12+
2. Удаление элемента справа/слева - **O(1)**.
13+
3. Расширение на m элементов справа/слева - **O(m)**.
14+
4. Вставка нового элемента в произвольную позицию - **O(n)**.
15+
5. Удаление элемента с заданным значением - **O(n)**.
16+
17+
## Реализация на базе массива
18+
19+
При использовании динамического массива в качестве основы при реализации двусторонней очереди мы свободно можем
20+
добавлять и удалять элементы из хвоста. Эти операции выполняются за константное время. Однако массив не предназначен для
21+
вставки и удаления из начала. Поэтому мы заранее создаем массив на определенное количество элементов и начинаем его
22+
заполнение из середины. При достижении левой границы нам следует создать новый массив большего размера и скопировать
23+
туда все элементы в середину. Перевыделение памяти и копирование туда старых значений ухудшает стабильность
24+
производительности операций вставки в оба конца. Поэтому в данном случае мы можем говорить только об амортизационной
25+
сложности операций.
26+
27+
![Реализация на базе массива](../img/deque_1.png)
28+
29+
## Реализация на базе двусвязного списка
30+
31+
Двустороннюю очередь очень просто и понятно реализовать на базе двусвязного списка. Основным минусом данного подхода
32+
является медленная операция выделения памяти для нового элемента. При работе с указателями также используется
33+
дополнительная память. Поэтому при хранении простых элементов(например, bool), перерасход может составить 200 и даже 300
34+
процентов.
35+
36+
## Реализация с использованием блоков
37+
38+
![Реализация с использованием блоков](../img/deque_2.png)
39+
40+
Идея реализации двусторонней очереди с использованием блоков объединяет оба подхода, описанных в предыдущих двух
41+
пунктах. В CPython структура deque представлена в виде статических массивов фиксированной длины размера 64, объединенных
42+
в двусвязный список. Это ускоряет доступ к отдельным элементам очереди по индексу и гарантирует, что удаляемый или
43+
извлекаемый элемент не затрагивает другие блоки данных. Другим преимуществом является то, что такое хранение позволяет
44+
полностью отказаться от функции realloc(), которая изменяет размер выделенного блока памяти, что приводит к более
45+
предсказуемой производительности. Использование блоков значительно снижает потребление памяти по сравнению с
46+
классическим двусвязным списком, где на одну ячейку данных приходятся две указателя на следующий и предыдущий элементы.
47+
В добавок к этому, статические массивы увеличивают вероятность использования кэша центрального процессора для уменьшения
48+
среднего времени доступа к компьютерной памяти.
49+
50+
Список блоков никогда не бывает пустым, поэтому ``d.leftblock``(самый левый блок) и ``d.rightblock``(самый правый блок)
51+
никогда не равны NULL. Первый элемент очереди определяется как ``d.leftblock[leftindex]``, а последний
52+
как ``d.rightblock[rightindex]``.
53+
54+
Индексы ``d.leftindex`` и ``d.rightindex`` всегда находятся в диапазоне ``0 <= index < BLOCKLEN``, по умолчанию BLOCKLEN
55+
равен 64.
56+
57+
Для поддержания симметричности операций вставки слева и справа точная взаимосвязь индексов такова:
58+
59+
```
60+
(d.leftindex + d.len - 1) % BLOCKLEN == d.rightindex
61+
```
62+
63+
Всякий раз, когда левый и правый блоки указывают на один и тот же массив (``d.leftblock == d.rightblock``), взаимосвязь
64+
становится следующей:
65+
66+
```
67+
d.leftindex + d.len - 1 == d.rightindex
68+
```
69+
70+
Однако, когда блоков более одного, ``d.leftindex`` и ``d.rightindex`` являются индексами в разных массивах. В этом
71+
случае любой из указателей может быть больше другого.
72+
73+
Для пустой очереди справедливо (CENTER равен ``((BLOCKLEN - 1) / 2)``):
74+
75+
1. ``d.len == 0``, длина очереди равна нулю
76+
1. ``d.leftblock == d.rightblock``, левый и правый блоки указывают на один и тот же массив
77+
1. ``d.leftindex == CENTER + 1``, левый индекс располагается в позиции 32
78+
1. ``d.rightindex == CENTER``, правый индекс равен 31
79+
80+
Ниже представлены основные структуры двусторонней очереди на CPython:
81+
82+
```
83+
#define BLOCKLEN 64
84+
#define CENTER ((BLOCKLEN - 1) / 2)
85+
86+
typedef struct BLOCK {
87+
struct BLOCK *leftlink;
88+
PyObject *data[BLOCKLEN];
89+
struct BLOCK *rightlink;
90+
} block;
91+
92+
typedef struct {
93+
PyObject_VAR_HEAD
94+
block *leftblock;
95+
block *rightblock;
96+
Py_ssize_t leftindex; /* 0 <= leftindex < BLOCKLEN */
97+
Py_ssize_t rightindex; /* 0 <= rightindex < BLOCKLEN */
98+
size_t state; /* incremented whenever the indices move */
99+
Py_ssize_t maxlen; /* maxlen is -1 for unbounded deques */
100+
Py_ssize_t numfreeblocks;
101+
block *freeblocks[MAXFREEBLOCKS];
102+
PyObject *weakreflist;
103+
} dequeobject;
104+
```

tutorial/queue_and_stack.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -125,24 +125,6 @@ class MyCircularQueue:
125125
return self._len == self.capacity
126126
```
127127

128-
## Двусторонняя очередь
129-
130-
Очереди также можно реализовать не только на динамическом массиве, но и на двусвязном списке. В python есть реализация
131-
двусторонней очереди "deque", которая является обобщением очереди и стека. Данная структура поддерживает эффективное
132-
извлечение элемента из любого конца за **O(1)**. Доступ к первому и последнему элементу осуществляется также за
133-
**O(1)**, но имеет временную сложность за **O(n)** для доступа к произвольному элементу.
134-
135-
Ниже приведена временная сложность операций для двусторонней очереди:
136-
137-
1. Добавление нового элемента справа/слева - **O(1)**.
138-
2. Удаление элемента справа/слева - **O(1)**.
139-
3. Расширение на m элементов справа/слева - **O(m)**.
140-
4. Вставка нового элемента в произвольную позицию - **O(n)**.
141-
5. Удаление элемента с заданным значением - **O(n)**.
142-
143-
Интересна реализация двусторонней очереди в С++, где используется сочетание статических массивов, объединенных в
144-
связанный список. Это ускоряет доступ к отдельным элементам очереди по индексу.
145-
146128
## Стек
147129

148130
Стек - структура типа ``Last-in-first-out (LIFO)`` или последний пришел, первый вышел. В ней элемент, добавленный

0 commit comments

Comments
 (0)