Skip to content

Commit faa6b36

Browse files
Add splay tree (self-adjusting BST) (#15266)
* Add splay tree (self-adjusting BST) Implements a splay tree with insert, search and delete, each of which splays the accessed node to the root for amortized O(log n) access and locality of reference. Includes full type hints and doctests. Closes #13760 * Make Node a dataclass and add repr doctest (review feedback)
1 parent bf57384 commit faa6b36

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"""
2+
Splay Tree - a self-adjusting binary search tree.
3+
4+
A splay tree is a binary search tree with the additional property that
5+
recently accessed elements are quick to access again. Every access (search,
6+
insert or delete) moves the target node to the root through a sequence of
7+
rotations called "splaying". This gives an amortized time complexity of
8+
O(log n) per operation and makes the tree very efficient when the access
9+
pattern has locality of reference (a small subset of keys is touched often).
10+
11+
Reference: https://en.wikipedia.org/wiki/Splay_tree
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from collections.abc import Iterator
17+
from dataclasses import dataclass, field
18+
19+
20+
@dataclass
21+
class Node:
22+
"""
23+
A single node of a splay tree.
24+
25+
The ``left`` and ``right`` children are excluded from ``repr`` so that a
26+
node prints compactly instead of recursively dumping the whole subtree.
27+
28+
>>> Node(10)
29+
Node(key=10)
30+
"""
31+
32+
key: int
33+
left: Node | None = field(default=None, repr=False)
34+
right: Node | None = field(default=None, repr=False)
35+
36+
37+
class SplayTree:
38+
"""
39+
A self-adjusting binary search tree.
40+
41+
>>> tree = SplayTree()
42+
>>> tree.insert(10)
43+
>>> tree.insert(20)
44+
>>> tree.insert(30)
45+
>>> tree.root.key # last inserted key is splayed to the root
46+
30
47+
>>> tree.search(10)
48+
True
49+
>>> tree.root.key # the searched key is now the root
50+
10
51+
>>> tree.search(99)
52+
False
53+
>>> list(tree)
54+
[10, 20, 30]
55+
"""
56+
57+
def __init__(self) -> None:
58+
self.root: Node | None = None
59+
60+
def _rotate_right(self, node: Node) -> Node:
61+
"""
62+
Perform a right rotation around ``node`` and return the new subtree root.
63+
64+
node left
65+
/ \\ / \\
66+
left c --> a node
67+
/ \\ / \\
68+
a b b c
69+
"""
70+
left = node.left
71+
assert left is not None
72+
node.left = left.right
73+
left.right = node
74+
return left
75+
76+
def _rotate_left(self, node: Node) -> Node:
77+
"""
78+
Perform a left rotation around ``node`` and return the new subtree root.
79+
80+
node right
81+
/ \\ / \\
82+
a right --> node c
83+
/ \\ / \\
84+
b c a b
85+
"""
86+
right = node.right
87+
assert right is not None
88+
node.right = right.left
89+
right.left = node
90+
return right
91+
92+
def _splay(self, root: Node | None, key: int) -> Node | None:
93+
"""
94+
Splay the node with ``key`` (or the last node on the search path if
95+
``key`` is absent) to the root of the subtree and return the new root.
96+
This uses the classic bottom-up recursive formulation.
97+
"""
98+
if root is None or root.key == key:
99+
return root
100+
101+
if key < root.key:
102+
if root.left is None:
103+
return root
104+
if key < root.left.key:
105+
# Zig-Zig (left left)
106+
root.left.left = self._splay(root.left.left, key)
107+
root = self._rotate_right(root)
108+
elif key > root.left.key:
109+
# Zig-Zag (left right)
110+
root.left.right = self._splay(root.left.right, key)
111+
if root.left.right is not None:
112+
root.left = self._rotate_left(root.left)
113+
return root if root.left is None else self._rotate_right(root)
114+
else:
115+
if root.right is None:
116+
return root
117+
if key > root.right.key:
118+
# Zig-Zig (right right)
119+
root.right.right = self._splay(root.right.right, key)
120+
root = self._rotate_left(root)
121+
elif key < root.right.key:
122+
# Zig-Zag (right left)
123+
root.right.left = self._splay(root.right.left, key)
124+
if root.right.left is not None:
125+
root.right = self._rotate_right(root.right)
126+
return root if root.right is None else self._rotate_left(root)
127+
128+
def insert(self, key: int) -> None:
129+
"""
130+
Insert ``key`` into the tree and splay it to the root.
131+
132+
>>> tree = SplayTree()
133+
>>> for key in (5, 3, 8, 3): # duplicate keys are ignored
134+
... tree.insert(key)
135+
>>> list(tree)
136+
[3, 5, 8]
137+
>>> tree.root.key # the duplicate access splays 3 back to the root
138+
3
139+
"""
140+
if self.root is None:
141+
self.root = Node(key)
142+
return
143+
144+
self.root = self._splay(self.root, key)
145+
assert self.root is not None
146+
if self.root.key == key:
147+
return # key already present, it is now at the root
148+
149+
node = Node(key)
150+
if key < self.root.key:
151+
node.right = self.root
152+
node.left = self.root.left
153+
self.root.left = None
154+
else:
155+
node.left = self.root
156+
node.right = self.root.right
157+
self.root.right = None
158+
self.root = node
159+
160+
def search(self, key: int) -> bool:
161+
"""
162+
Return whether ``key`` is present and splay the last accessed node.
163+
164+
>>> tree = SplayTree()
165+
>>> tree.search(1)
166+
False
167+
>>> for key in (40, 20, 60):
168+
... tree.insert(key)
169+
>>> tree.search(20)
170+
True
171+
>>> tree.root.key
172+
20
173+
"""
174+
self.root = self._splay(self.root, key)
175+
return self.root is not None and self.root.key == key
176+
177+
def delete(self, key: int) -> None:
178+
"""
179+
Remove ``key`` from the tree if it is present.
180+
181+
>>> tree = SplayTree()
182+
>>> for key in (10, 20, 30, 40):
183+
... tree.insert(key)
184+
>>> tree.delete(20)
185+
>>> list(tree)
186+
[10, 30, 40]
187+
>>> tree.delete(99) # deleting an absent key is a no-op
188+
>>> list(tree)
189+
[10, 30, 40]
190+
>>> for key in (10, 30, 40):
191+
... tree.delete(key)
192+
>>> list(tree)
193+
[]
194+
"""
195+
if self.root is None:
196+
return
197+
198+
self.root = self._splay(self.root, key)
199+
assert self.root is not None
200+
if self.root.key != key:
201+
return # key not found
202+
203+
left, right = self.root.left, self.root.right
204+
if left is None:
205+
self.root = right
206+
else:
207+
# Splay the maximum of the left subtree to its root; it has no
208+
# right child, so the right subtree can be attached there.
209+
left = self._splay(left, key)
210+
assert left is not None
211+
left.right = right
212+
self.root = left
213+
214+
def __iter__(self) -> Iterator[int]:
215+
"""
216+
Yield the keys of the tree in ascending (in-order) order.
217+
218+
>>> tree = SplayTree()
219+
>>> for key in (7, 2, 9, 4, 1):
220+
... tree.insert(key)
221+
>>> list(tree)
222+
[1, 2, 4, 7, 9]
223+
"""
224+
225+
def in_order(node: Node | None) -> Iterator[int]:
226+
if node is not None:
227+
yield from in_order(node.left)
228+
yield node.key
229+
yield from in_order(node.right)
230+
231+
yield from in_order(self.root)
232+
233+
234+
if __name__ == "__main__":
235+
import doctest
236+
237+
doctest.testmod()

0 commit comments

Comments
 (0)