Splay Tree (расширяющееся дерево) — это самобалансирующееся бинарное дерево поиска, разработанное Даниэлем Слейтором и Робертом Тарьяном в 1985 году.
Ключевая идея: каждый раз при обращении к узлу он перемещается в корень дерева с помощью операции splay (серии поворотов). Нет хранения высоты, цвета, или других метаданных — только указатели parent/left/right.
class SplayNode:
def __init__(self, key, value=None):
self.key = key
self.value = value
self.left = None # левое поддерево (ключи < key)
self.right = None # правое поддерево (ключи > key)
self.parent = None # родитель
Splay перемещает узел x в корень через три случая поворотов:
Вставляем ключи: 10, 20, 5, 15, 30 и затем делаем find(5)
find(5) — узел 5 поднимается в корень:| Применение | Почему Splay Tree? |
|---|---|
| Кэш браузера | Недавно открытые URL снова нужны быстро → они в корне |
| Текстовый редактор | Последние правки локальны → cursor position быстро находится |
| Сетевой маршрутизатор | «Горячие» IP-адреса постоянно в топе |
| Windows NT Registry | Часто читаемые ключи всплывают наверх |
| Garbage Collector (JVM) | Поиск свободных блоков по размеру |
| Link-Cut Trees | Splay — строительный блок для динамических деревьев |
| Операция | Worst Case | Amortized | Best Case |
|---|---|---|---|
| Find | O(n) | O(log n) | O(1) |
| Insert | O(n) | O(log n) | O(1) |
| Delete | O(n) | O(log n) | O(1) |
| Split | O(n) | O(log n) | O(log n) |
| Merge | O(n) | O(log n) | O(log n) |
| k-й элемент | O(n) | O(log n) | O(1) |
⚠️ Worst case O(n) — одна операция, но серия из m операций всегда ≤ O(m log n)
| ✅ Преимущества | ❌ Недостатки |
|---|---|
| Простая реализация | Worst-case O(n) |
| Адаптация к паттерну доступа | Не для real-time |
| Отличная cache locality | Плохо при равномерном доступе |
| Split/Merge O(log n) | Не persistent |
| Хорош для skewed access | find меняет дерево |
| Характеристика | Splay Tree | B-Tree | B+Tree |
|---|---|---|---|
| Структура | Бинарное | M-арное | M-арное |
| Хранение данных | Все узлы | Все узлы | Только листья |
| Оптимизация | RAM / Cache | Диск | Диск + Range |
| Балансировка | Амортизированная | Строгая | Строгая |
| Высота дерева | O(log n) amotr. | O(log_m n) | O(log_m n) |
| Locality of reference | ✅ Отличная | ❌ Нет | ❌ Нет |
| Range queries | ⚠️ Средне | ⚠️ Средне | ✅ Отлично |
| Sequential scan | ❌ Плохо | ⚠️ Средне | ✅ Отлично |
| Overhead памяти | ✅ Минимальный | ⚠️ Средний | ⚠️ Средний |
| Метаданные узла | ❌ Нет | Degree/count | Degree/count |
| Worst-case 1 op | ❌ O(n) | ✅ O(log n) | ✅ O(log n) |
| Применение | Cache, RAM | FS, DB Index | СУБД, OS |
{
"title": { "text": "Относительная производительность по паттерну доступа", "left": "center" },
"tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } },
"legend": { "top": 30, "data": ["Splay Tree", "B-Tree", "B+Tree"] },
"xAxis": {
"type": "category",
"data": ["Случайный", "Локальный (80/20)", "Последовательный", "Range Scan", "Вставка потоком"]
},
"yAxis": {
"type": "value",
"name": "Score (выше = лучше)",
"max": 100
},
"series": [
{
"name": "Splay Tree",
"type": "bar",
"data": [55, 95, 30, 45, 60],
"itemStyle": { "color": "#4CAF50" }
},
{
"name": "B-Tree",
"type": "bar",
"data": [75, 65, 70, 70, 75],
"itemStyle": { "color": "#2196F3" }
},
{
"name": "B+Tree",
"type": "bar",
"data": [70, 60, 95, 98, 80],
"itemStyle": { "color": "#FF9800" }
}
]
}
class SplayTree:
def _right_rotate(self, x):
p = x.parent
b = x.right
# x поднимается, p опускается вправо
if p.parent:
if p.parent.left == p: p.parent.left = x
else: p.parent.right = x
x.parent = p.parent
x.right = p
p.parent = x
p.left = b
if b: b.parent = p
def _left_rotate(self, x):
p = x.parent
b = x.left
if p.parent:
if p.parent.left == p: p.parent.left = x
else: p.parent.right = x
x.parent = p.parent
x.left = p
p.parent = x
p.right = b
if b: b.parent = p
def splay(self, x):
while x.parent: # пока x не корень
p = x.parent
g = p.parent # дедушка
if not g:
# Zig
if p.left == x: self._right_rotate(x)
else: self._left_rotate(x)
elif g.left == p and p.left == x:
# Zig-Zig (левый-левый)
self._right_rotate(p)
self._right_rotate(x)
elif g.right == p and p.right == x:
# Zig-Zig (правый-правый)
self._left_rotate(p)
self._left_rotate(x)
else:
# Zig-Zag
if p.left == x: self._right_rotate(x); self._left_rotate(x)
else: self._left_rotate(x); self._right_rotate(x)
self.root = x
def find(self, key):
node = self.root
last = None
while node:
last = node
if key == node.key:
self.splay(node) # найден → в корень
return node
elif key < node.key: node = node.left
else: node = node.right
if last: self.splay(last) # не найден → splay последнего
return None
def insert(self, key, value=None):
if not self.root:
self.root = SplayNode(key, value); return
node = self.root
while True:
if key < node.key:
if not node.left:
node.left = SplayNode(key, value)
node.left.parent = node
self.splay(node.left); return
node = node.left
elif key > node.key:
if not node.right:
node.right = SplayNode(key, value)
node.right.parent = node
self.splay(node.right); return
node = node.right
else:
node.value = value # обновить
self.splay(node); return
def delete(self, key):
node = self.find(key)
if not node: return
# После splay — node в корне
left, right = self.root.left, self.root.right
if left: left.parent = None
if right: right.parent = None
if not left:
self.root = right
else:
# найти максимум в левом поддереве → станет новым корнем
self.root = left
m = left
while m.right: m = m.right
self.splay(m) # m → корень левого
self.root.right = right
if right: right.parent = self.root
Это главная теоретическая сила Splay Tree:
Если элемент
xбыл запрошенtопераций назад, то следующий доступ кxзаймёт O(log t)
Практически: часто используемые элементы живут близко к корню и находятся быстрее — структура адаптируется к реальному паттерну доступа автоматически.
| Сценарий | Рекомендация |
|---|---|
| Кэш с горячим подмножеством | ✅ Splay Tree |
| База данных, индексы на диске | ✅ B+Tree |
| Файловая система | ✅ B-Tree / B+Tree |
| In-memory с равномерным доступом | ✅ Red-Black Tree / AVL |
| Нужны гарантии worst-case | ✅ AVL / RB / B-Tree |
| Динамические деревья (Link-Cut) | ✅ Splay Tree |
| Multithreaded (много читателей) | ❌ Splay неудобен (write on read) |
💡 Главная интуиция: Splay Tree — это как стопка документов на столе: те, с которыми работаешь чаще, оказываются сверху. Никакой внешней организации — структура сама приспосабливается к вашей работе.