Splay tree
A splay tree is a self-adjusting binary search tree introduced by Sleator and Tarjan[1]. It stores no colours, heights or sizes. Every search, insertion or deletion ends by splaying: rotating the accessed node, or the last node on the search path, up to the root. The tree can become arbitrarily unbalanced, but splaying roughly halves the depth of every node on the path it follows, and an amortized analysis shows that the expensive accesses pay for themselves[1][5].
Splaying
Splaying moves a node up two levels at a time. If and its parent are both left children, or both right children, the zig-zig step rotates over the grandparent and then over . If one is a left child and the other a right child, the zig-zag step rotates over and then over . A final single rotation, zig, is used when is a child of the root[1].
Rotating the grandparent first in the zig-zig case is essential. Rotating up one level at a time would also bring it to the root, but a path would stay a path, and accessing its nodes in order could cost each.
A functional splay tree in OCaml: splay returns the new tree, with a counter for rotations.
type t = Leaf | Node of t * int * tlet rotations = ref 0(* Bring x, or the last node on the search path to it, to the root. *)let rec splay x t =match t with| Leaf -> Leaf| Node (l, y, r) when x = y -> t| Node (l, y, r) when x < y -> (match l with| Leaf -> t| Node (ll, z, lr) when x = z -> incr rotations; Node (ll, z, Node (lr, y, r)) (* zig *)| Node (ll, z, lr) when x < z -> (match splay x ll with| Leaf -> incr rotations; Node (ll, z, Node (lr, y, r))| Node (a, w, b) -> (* zig-zig *)rotations := !rotations + 2; Node (a, w, Node (b, z, Node (lr, y, r))))| Node (ll, z, lr) -> (match splay x lr with| Leaf -> incr rotations; Node (ll, z, Node (lr, y, r))| Node (a, w, b) -> (* zig-zag *)rotations := !rotations + 2; Node (Node (ll, z, a), w, Node (b, y, r))))| Node (l, y, r) -> (match r with| Leaf -> t| Node (rl, z, rr) when x = z -> incr rotations; Node (Node (l, y, rl), z, rr)| Node (rl, z, rr) when x > z -> (match splay x rr with| Leaf -> incr rotations; Node (Node (l, y, rl), z, rr)| Node (a, w, b) ->rotations := !rotations + 2; Node (Node (Node (l, y, rl), z, a), w, b))| Node (rl, z, rr) -> (match splay x rl with| Leaf -> incr rotations; Node (Node (l, y, rl), z, rr)| Node (a, w, b) ->rotations := !rotations + 2; Node (Node (l, y, a), w, Node (b, z, rr))))let insert x t =match splay x t with| Leaf -> Node (Leaf, x, Leaf)| Node (l, y, r) as t ->if x = y then telse if x < y then Node (l, x, Node (Leaf, y, r))else Node (Node (l, y, Leaf), x, r)let mem x t = match splay x t with Node (_, y, _) as t -> (x = y, t) | Leaf -> (false, Leaf)
A degenerate tree, and the cost of accesses on it.
open Splaylet rec height = function Leaf -> 0 | Node (l, _, r) -> 1 + max (height l) (height r)let n = 1023
Running it.
after inserting 1..1023 in order: height 1023access 1: 1022 rotations, height now 513access 2: 511 rotations, height now 258access 3: 257 rotations, height now 132access 512: 67 rotations, height now 69accessing 1..1023 in order: 3509 rotations100,000 random accesses: 1145880 rotations, 11.5 per access
Inserting keys in increasing order leaves a path of 1023 nodes. Accessing the deepest key costs 1022 rotations, but leaves the tree about half as tall, and a few more accesses bring the height down to tens. Accessing all keys in order then costs 3.4 rotations each, and random accesses about 11.5, close to .
Amortized bounds
Give each node the potential , where is the size of its subtree, and let be the sum over all nodes. Sleator and Tarjan's access lemma states that splaying in a tree with root has amortized cost[1][5]
Many stronger properties follow. Static optimality: splay trees are within a constant factor of the best static tree for any access frequencies. Accessing all keys in order takes linear time in total[2], and accessing a key close in order to the previous one is cheap[3]. Whether splay trees are within a constant factor of every other binary search tree algorithm on every access sequence, the dynamic optimality conjecture, is open[1].
Splaying modifies the tree even on a lookup, so a functional splay tree must return the new tree from every operation, as above. Since the bounds are amortized, a persistent use that repeats an expensive access on an old version loses them. Okasaki's splay heaps use only the partitioning step of splaying and make a fast functional priority queue[4].
History
Daniel Sleator and Robert Tarjan introduced splay trees in 1985[1]. Tarjan proved the sequential access theorem the same year[2], and Cole proved the dynamic finger theorem in 2000[3].
see also
- Red-black treeA red-black tree is a binary search tree whose nodes are coloured red or black so that no red node has a red child and every path from the root to a leaf passes through the same number of black nodes. The two rules keep the height at most 2 log2(n + 1), so search, insertion and deletion take O(log n) time. Red-black trees are an encoding of 2-3-4 trees as binary trees, and Okasaki's functional version reduces insertion to one rebalancing rule with four cases.
- Pairing heapA pairing heap is a priority queue represented as a heap-ordered tree in which a node can have any number of children. Insertion and merging link two roots in constant time; deleting the minimum removes the root and combines its children in two passes, first merging them in pairs from left to right and then merging the results from right to left. Deletion takes logarithmic amortized time. Pairing heaps are short to write and fast in practice.
- Leftist heapA leftist heap is a priority queue represented as a heap-ordered binary tree in which, at every node, the right spine of the left child is at least as long as the right spine of the right child. The right spine of the whole tree then has at most log₂(n + 1) nodes, and two heaps can be merged in O(log n) time by merging their right spines like sorted lists. Insertion and deletion of the minimum are special cases of merging. Because merging copies only the right spines, leftist heaps are a standard persistent priority queue in functional languages.
further reading
- [1]D. D. Sleator, R. E. Tarjan, “Self-adjusting binary search trees”, Journal of the ACM 32 (1985).
- [2]R. E. Tarjan, “Sequential access in splay trees takes linear time”, Combinatorica 5 (1985).
- [3]R. Cole, “On the dynamic finger conjecture for splay trees. Part II: The proof”, SIAM Journal on Computing 30 (2000).
- [4]C. Okasaki, Purely Functional Data Structures, §5.4, Cambridge University Press (1998).
- [5]R. E. Tarjan, “Amortized computational complexity”, SIAM Journal on Algebraic and Discrete Methods 6 (1985).
last updated