Hubbry Logo
Splay treeSplay treeMain
Open search
Splay tree
Community hub
Splay tree
logo
7 pages, 0 posts
0 subscribers
Be the first to start a discussion here.
Be the first to start a discussion here.
Splay tree
Splay tree
from Wikipedia

Splay tree
TypeTree
Invented1985
Invented byDaniel Dominic Sleator and Robert Endre Tarjan
Complexities in big O notation
Space complexity
Space O(n)
Time complexity
Function Amortized Worst case
Search O(log n)[1]: 659  O(n)[2]: 1 
Insert O(log n)[1]: 659  O(n)
Delete O(log n)[1]: 659  O(n)

A splay tree is a binary search tree with the additional property that recently accessed elements are quick to access again. Like self-balancing binary search trees, a splay tree performs basic operations such as insertion, look-up and removal in O(log n) amortized time. For random access patterns drawn from a non-uniform random distribution, their amortized time can be faster than logarithmic, proportional to the entropy of the access pattern. For many patterns of non-random operations, also, splay trees can take better than logarithmic time, without requiring advance knowledge of the pattern. According to the unproven dynamic optimality conjecture, their performance on all access patterns is within a constant factor of the best possible performance that could be achieved by any other self-adjusting binary search tree, even one selected to fit that pattern. The splay tree was invented by Daniel Sleator and Robert Tarjan in 1985.[1]

All normal operations on a binary search tree are combined with one basic operation, called splaying. Splaying the tree for a certain element rearranges the tree so that the element is placed at the root of the tree. One way to do this with the basic search operation is to first perform a standard binary tree search for the element in question, and then use tree rotations in a specific fashion to bring the element to the top. Alternatively, a top-down algorithm can combine the search and the tree reorganization into a single phase.

Advantages

[edit]

Good performance for a splay tree depends on the fact that it is self-optimizing, in that frequently accessed nodes will move nearer to the root where they can be accessed more quickly. The worst-case height—though unlikely—is O(n), with the average being O(log n). Having frequently-used nodes near the root is an advantage for many practical applications (also see locality of reference), and is particularly useful for implementing caches and garbage collection algorithms.

Advantages include:

  • Comparable performance: Average-case performance is as efficient as other trees.[3]
  • Small memory footprint: Splay trees do not need to store any bookkeeping data.

Disadvantages

[edit]

The most significant disadvantage of splay trees is that the height of a splay tree can be linear.[2]: 1  For example, this will be the case after accessing all n elements in non-decreasing order. Since the height of a tree corresponds to the worst-case access time, this means that the actual cost of a single operation can be high. However the amortized access cost of this worst case is logarithmic, O(log n). Also, the expected access cost can be reduced to O(log n) by using a randomized variant.[4]

The representation of splay trees can change even when they are accessed in a 'read-only' manner (i.e. by find operations). This complicates the use of such splay trees in a multi-threaded environment. Specifically, extra management is needed if multiple threads are allowed to perform find operations concurrently. This also makes them unsuitable for general use in purely functional programming, although even there they can be used in limited ways to implement priority queues.

Finally, when the access pattern is random, the additional splaying overhead adds a significant constant factor to the cost compared to less-dynamic alternatives.

Operations

[edit]

Splaying

[edit]

When a node x is accessed, a splay operation is performed on x to move it to the root. A splay operation is a sequence of splay steps, each of which moves x closer to the root. By performing a splay operation on the node of interest after every access, the recently accessed nodes are kept near the root and the tree remains roughly balanced, so it provides the desired amortized time bounds.

Each particular step depends on three factors:

  • Whether x is the left or right child of its parent node, p,
  • whether p is the root or not, and if not
  • whether p is the left or right child of its parent, g (the grandparent of x).

There are three types of splay steps, each of which has two symmetric variants: left- and right-handed. For the sake of brevity, only one of these two is shown for each type. (In the following diagrams, circles indicate nodes of interest and triangles indicate sub-trees of arbitrary size.) The three types of splay steps are:

Zig step: this step is done when p is the root. The tree is rotated on the edge between x and p. Zig steps exist to deal with the parity issue, will be done only as the last step in a splay operation, and only when x has odd depth at the beginning of the operation.

Zig-zig step: this step is done when p is not the root and x and p are either both right children or are both left children. The picture below shows the case where x and p are both left children. The tree is rotated on the edge joining p with its parent g, then rotated on the edge joining x with p. Zig-zig steps are the only thing that differentiate splay trees from the rotate to root method introduced by Allen and Munro[5] prior to the introduction of splay trees.

Zig-zag step: this step is done when p is not the root and x is a right child and p is a left child or vice versa (x is left, p is right). The tree is rotated on the edge between p and x, and then rotated on the resulting edge between x and g.

Join

[edit]

Given two trees S and T such that all elements of S are smaller than the elements of T, the following steps can be used to join them to a single tree:

  • Splay the largest item in S. Now this item is in the root of S and has a null right child.
  • Set the right child of the new root to T.

Split

[edit]

Given a tree and an element x, return two new trees: one containing all elements less than or equal to x and the other containing all elements greater than x. This can be done in the following way:

  • Splay x. Now it is in the root so the tree to its left contains all elements smaller than x and the tree to its right contains all element larger than x.
  • Split the right subtree from the rest of the tree.

Insertion

[edit]

To insert a value x into a splay tree:

As a result, the newly inserted node x becomes the root of the tree.

Alternatively:

  • Use the split operation to split the tree at the value of x to two sub-trees: S and T.
  • Create a new tree in which x is the root, S is its left sub-tree and T its right sub-tree.

Deletion

[edit]

To delete a node x, use the same method as with a binary search tree:

  • If x has two children:
    • Swap its value with that of either the rightmost node of its left sub tree (its in-order predecessor) or the leftmost node of its right subtree (its in-order successor).
    • Remove that node instead.

In this way, deletion is reduced to the problem of removing a node with 0 or 1 children. Unlike a binary search tree, in a splay tree after deletion, we splay the parent of the removed node to the top of the tree.

Alternatively:

  • The node to be deleted is first splayed, i.e. brought to the root of the tree and then deleted. leaves the tree with two sub trees.
  • The two sub-trees are then joined using a "join" operation.

Implementation and variants

[edit]

Splaying, as mentioned above, is performed during a second, bottom-up pass over the access path of a node. It is possible to record the access path during the first pass for use during the second, but that requires extra space during the access operation. Another alternative is to keep a parent pointer in every node, which avoids the need for extra space during access operations but may reduce overall time efficiency because of the need to update those pointers.[1]

Another method which can be used is based on the argument that the tree can be restructured during the way down the access path instead of making a second pass. This top-down splaying routine uses three sets of nodes – left tree, right tree and middle tree. The first two contain all items of original tree known to be less than or greater than current item respectively. The middle tree consists of the sub-tree rooted at the current node. These three sets are updated down the access path while keeping the splay operations in check. Another method, semisplaying, modifies the zig-zig case to reduce the amount of restructuring done in all operations.[1][6]

Below there is an implementation of splay trees in C++, which uses pointers to represent each node on the tree. This implementation is based on bottom-up splaying version and uses the second method of deletion on a splay tree. Also, unlike the above definition, this C++ version does not splay the tree on finds – it only splays on insertions and deletions, and the find operation, therefore, has linear time complexity.

#include <functional>

#ifndef SPLAY_TREE
#define SPLAY_TREE

template<typename T, typename Comp = std::less<T>>
class splay_tree {
private:
  Comp comp;
  unsigned long p_size;
  
  struct node {
    node *left, *right;
    node *parent;
    T key;
    node(const T& init = T()) : left(nullptr), right(nullptr), parent(nullptr), key(init) { }
    ~node() {

    }
  } *root;
  
  void left_rotate(node *x) {
    node *y = x->right;
    if (y) {
      x->right = y->left;
      if (y->left) y->left->parent = x;
      y->parent = x->parent;
    }
    
    if (!x->parent) root = y;
    else if (x == x->parent->left) x->parent->left = y;
    else x->parent->right = y;
    if (y) y->left = x;
    x->parent = y;
  }
  
  void right_rotate(node *x) {
    node *y = x->left;
    if (y) {
      x->left = y->right;
      if (y->right) y->right->parent = x;
      y->parent = x->parent;
    }
    if (!x->parent) root = y;
    else if (x == x->parent->left) x->parent->left = y;
    else x->parent->right = y;
    if (y) y->right = x;
    x->parent = y;
  }
  
  void splay(node *x) {
    while (x->parent) {
      if (!x->parent->parent) {
        if (x->parent->left == x) right_rotate(x->parent);
        else left_rotate(x->parent);
      } else if (x->parent->left == x && x->parent->parent->left == x->parent) {
        right_rotate(x->parent->parent);
        right_rotate(x->parent);
      } else if (x->parent->right == x && x->parent->parent->right == x->parent) {
        left_rotate(x->parent->parent);
        left_rotate(x->parent);
      } else if (x->parent->left == x && x->parent->parent->right == x->parent) {
        right_rotate(x->parent);
        left_rotate(x->parent);
      } else {
        left_rotate(x->parent);
        right_rotate(x->parent);
      }
    }
  }
  
  void replace(node *u, node *v) {
    if (!u->parent) root = v;
    else if (u == u->parent->left) u->parent->left = v;
    else u->parent->right = v;
    if (v) v->parent = u->parent;
  }
  
  node* subtree_minimum(node *u) {
    while (u->left) u = u->left;
    return u;
  }
  
  node* subtree_maximum(node *u) {
    while (u->right) u = u->right;
    return u;
  }
public:
  splay_tree() : root(nullptr), p_size(0) { }
  
  void insert(const T &key) {
    node *z = root;
    node *p = nullptr;
    
    while (z) {
      p = z;
      if (comp(z->key, key)) z = z->right;
      else z = z->left;
    }
    
    z = new node(key);
    z->parent = p;
    
    if (!p) root = z;
    else if (comp(p->key, z->key)) p->right = z;
    else p->left = z;
    
    splay(z);
    p_size++;
  }
  
  node* find(const T &key) {
    node *z = root;
    while (z) {
      if (comp(z->key, key)) z = z->right;
      else if (comp(key, z->key)) z = z->left;
      else return z;
    }
    return nullptr;
  }
        
  void erase(const T &key) {
    node *z = find(key);
    if (!z) return;
    
    splay(z);
    
    if (!z->left) replace(z, z->right);
    else if (!z->right) replace(z, z->left);
    else {
      node *y = subtree_minimum(z->right);
      if (y->parent != z) {
        replace(y, y->right);
        y->right = z->right;
        y->right->parent = y;
      }
      replace(z, y);
      y->left = z->left;
      y->left->parent = y;
    }
    
    delete z;
    p_size--;
  }

/* //the alternative implementation
    void erase(const T &key) {
        node *z = find(key);
        if (!z) return;
        
        splay(z);
        
        node *s = z->left;
        node *t = z->right;
        delete z;
        
        node *sMax = NULL;
        if (s) {
            s->parent = NULL;
            sMax = subtree_maximum(s);
            splay(sMax);
            root = sMax;
        }
        if (t) {
            if (s)
                sMax->right = t;
            else
                root = t;
            t->parent = sMax;
        }
        
        p_size--;
    }
*/
  
  const T& minimum() { return subtree_minimum(root)->key; }
  const T& maximum() { return subtree_maximum(root)->key; }
  
  bool empty() const { return root == nullptr; }
  unsigned long size() const { return p_size; }
};

#endif // SPLAY_TREE

Analysis

[edit]

A simple amortized analysis of static splay trees can be carried out using the potential method. Define:

  • size(r) = the number of nodes in the sub-tree rooted at node r (including r).
  • rank(r) = log2(size(r)).
  • Φ = the sum of the ranks of all the nodes in the tree.

Φ will tend to be high for poorly balanced trees and low for well-balanced trees.

To apply the potential method, we first calculate ΔΦ: the change in the potential caused by a splay operation. We check each case separately. Denote by rank' the rank function after the operation. x, p and g are the nodes affected by the rotation operation (see figures above).

Zig step

[edit]
ΔΦ = rank'(p) − rank(p) + rank'(x) − rank(x)   [since only p and x change ranks]
= rank'(p) − rank(x) [since rank'(x)=rank(p)]
≤ rank'(x) − rank(x) [since rank'(p)<rank'(x)]

Zig-zig step

[edit]
ΔΦ = rank'(g) − rank(g) + rank'(p) − rank(p) + rank'(x) − rank(x)
= rank'(g) + rank'(p) − rank(p) − rank(x)   [since rank'(x)=rank(g)]
≤ rank'(g) + rank'(x) − 2 rank(x) [since rank(x)<rank(p) and rank'(x)>rank'(p)]
≤ 3(rank'(x)−rank(x)) − 2 [due to the concavity of the log function]

Zig-zag step

[edit]
ΔΦ = rank'(g) − rank(g) + rank'(p) − rank(p) + rank'(x) − rank(x)
≤ rank'(g) + rank'(p) − 2 rank(x)   [since rank'(x)=rank(g) and rank(x)<rank(p)]
≤ 3(rank'(x)−rank(x)) − 2 [due to the concavity of the log function]

The amortized cost of any operation is ΔΦ plus the actual cost. The actual cost of any zig-zig or zig-zag operation is 2 since there are two rotations to make. Hence:

amortized-cost = cost + ΔΦ
≤ 3(rank'(x)−rank(x))

When summed over the entire splay operation, this telescopes to 1 + 3(rank(root)−rank(x)) which is O(log n), since we use The Zig operation at most once and the amortized cost of zig is at most 1+3(rank'(x)−rank(x)).

So now we know that the total amortized time for a sequence of m operations is:

To go from the amortized time to the actual time, we must add the decrease in potential from the initial state before any operation is done (Φi) to the final state after all operations are completed (Φf).

where the big O notation can be justified by the fact that for every node x, the minimum rank is 0 and the maximum rank is log(n).

Now we can finally bound the actual time:

Weighted analysis

[edit]

The above analysis can be generalized in the following way.

  • Assign to each node r a weight w(r).
  • Define size(r) = the sum of weights of nodes in the sub-tree rooted at node r (including r).
  • Define rank(r) and Φ exactly as above.

The same analysis applies and the amortized cost of a splaying operation is again:

where W is the sum of all weights.

The decrease from the initial to the final potential is bounded by:

since the maximum size of any single node is W and the minimum is w(x).

Hence the actual time is bounded by:

Performance theorems

[edit]

There are several theorems and conjectures regarding the worst-case runtime for performing a sequence S of m accesses in a splay tree containing n elements.

Balance TheoremThe cost of performing the sequence S is .

Proof

Take a constant weight, e.g. for every node x. Then .

This theorem implies that splay trees perform as well as static balanced binary search trees on sequences of at least n accesses.[1]

Static Optimality TheoremLet be the number of times element x is accessed in S. If every element is accessed at least once, then the cost of performing S is

Proof

Let . Then .

This theorem implies that splay trees perform as well as an optimum static binary search tree on sequences of at least n accesses.[7] They spend less time on the more frequent items.[1] Another way of stating the same result is that, on input sequences where the items are drawn independently at random from a non-uniform probability distribution on n items, the amortized expected (average case) cost of each access is proportional to the entropy of the distribution.[8]

Static Finger TheoremAssume that the items are numbered from 1 through n in ascending order. Let f be any fixed element (the 'finger'). Then the cost of performing S is .

Proof

Let . Then . The net potential drop is O (n log n) since the weight of any item is at least .[1]

Dynamic Finger TheoremAssume that the 'finger' for each step accessing an element y is the element accessed in the previous step, x. The cost of performing S is .[9][10]

Working Set TheoremAt any time during the sequence, let be the number of distinct elements accessed before the previous time element x was accessed. The cost of performing S is

Proof

Let . Note that here the weights change during the sequence. However, the sequence of weights is still a permutation of . So as before . The net potential drop is O (n log n).

This theorem is equivalent to splay trees having key-independent optimality.[1]

Scanning TheoremAlso known as the Sequential Access Theorem or the Queue theorem. Accessing the n elements of a splay tree in symmetric order takes O(n) time, regardless of the initial structure of the splay tree.[11] The tightest upper bound proven so far is .[12]

Dynamic optimality conjecture

[edit]
Unsolved problem in computer science
Do splay trees perform as well as any other binary search tree algorithm?

In addition to the proven performance guarantees for splay trees there is an unproven conjecture of great interest from the original Sleator and Tarjan paper. This conjecture is known as the dynamic optimality conjecture and it basically claims that splay trees perform as well as any other binary search tree algorithm up to a constant factor.

Dynamic Optimality Conjecture:[1] Let be any binary search tree algorithm that accesses an element by traversing the path from the root to at a cost of , and that between accesses can make any rotations in the tree at a cost of 1 per rotation. Let be the cost for to perform the sequence of accesses. Then the cost for a splay tree to perform the same accesses is .

There are several corollaries of the dynamic optimality conjecture that remain unproven:

Traversal Conjecture:[1] Let and be two splay trees containing the same elements. Let be the sequence obtained by visiting the elements in in preorder (i.e., depth first search order). The total cost of performing the sequence of accesses on is .
Deque Conjecture:[11][13][14] Let be a sequence of double-ended queue operations (push, pop, inject, eject). Then the cost of performing on a splay tree is .
Split Conjecture:[6] Let be any permutation of the elements of the splay tree. Then the cost of deleting the elements in the order , splitting each tree into two separate trees at each deleted item, is .

Variants

[edit]

In order to reduce the number of restructuring operations, it is possible to replace the splaying with semi-splaying, in which an element is splayed only halfway towards the root.[1][2]

Another way to reduce restructuring is to do full splaying, but only in some of the access operations – only when the access path is longer than a threshold, or only in the first m access operations.[1]

The CBTree augments the splay tree with access counts at each node and uses them to restructure infrequently. A variant of the CBTree called the LazyCBTree does at most one rotation on each lookup. This is used along with an optimistic hand-over-hand validation scheme to make a concurrent self-adjusting tree.[15]

Using pointer-compression techniques,[16] it is possible to construct a succinct splay tree.

See also

[edit]

Notes

[edit]

References

[edit]
[edit]
Revisions and contributorsEdit on WikipediaRead on Wikipedia
from Grokipedia
A splay tree is a self-adjusting in which every insertion, search, and deletion operation is followed by a restructuring step called splaying, which brings the accessed node to the root through a series of rotations, ensuring amortized O(log n) per operation on a tree with n nodes. It was invented by Daniel D. Sleator and Robert E. Tarjan in as a way to achieve efficient performance without explicit balancing mechanisms. The splaying operation maintains the property by performing one of three types of —zig (single for a direct child of the root), zig-zig (double for nodes aligned on the same side), or zig-zag (double for nodes on opposite sides)—to gradually move the target node upward along the access path. This restructuring amortizes costs over a sequence of operations using a potential function based on the tree's rank (logarithmic subtree sizes), bounding the total time for m operations at O(m log n). Splay trees support standard operations like insert, delete, split, join, and search, each typically involving a splay followed by local modifications to the tree structure. One key advantage of splay trees is their simplicity in implementation, as they require no additional balance fields beyond standard nodes (key, value, left, and right pointers), making them suitable for dynamic environments with unpredictable access patterns. They adapt to temporal locality by promoting frequently accessed nodes toward the root, achieving performance asymptotically close to the optimal static for stable access probabilities, often within a constant factor. Despite worst-case O(n) time for individual operations, their amortized efficiency has made them influential in areas like , network routing, and optimizations.

Definition and Background

Definition

A splay tree is a self-adjusting designed to optimize access times for frequently used elements by reorganizing the tree structure after each operation. Unlike traditional balanced binary search trees, a splay tree does not maintain explicit invariants such as height restrictions or color rules; instead, it relies on a called splaying to move recently accessed nodes to the root, thereby improving the amortized efficiency of subsequent operations. This self-adjustment ensures that the tree adapts dynamically to patterns in the access sequence without requiring prior knowledge of the data distribution. At its core, a splay tree adheres to the fundamental properties of a : each node stores a , with all keys in the left subtree being strictly less than the node's key and all keys in the right subtree being strictly greater. The basic node structure consists of the key value, pointers to the left and right child nodes, and optionally a pointer to support bottom-up implementations of the splay operation. Rotations performed during splaying preserve this in-order traversal property, ensuring that the tree remains a valid after every insertion, deletion, search, or other access. To illustrate, consider a simple splay tree with keys 1, 2, and 3 arranged in a right-skewed chain, where 1 is the , 2 is its right child, and 3 is the right child of 2. This configuration forms a degenerate with 2. After performing a search on key 3, which triggers a splay operation, node 3 is rotated to the , resulting in 3 with 2 as its left child and 1 as the left child of 2. The is now left-skewed with still 2, but the accessed element is positioned at the for faster future retrievals.

History

The splay tree was invented in 1985 by Daniel D. Sleator and Robert E. Tarjan while they were researchers at Bell Laboratories. Their work was motivated by the desire to create an efficient dynamic dictionary that leverages in access sequences, allowing recently accessed elements to be retrieved more quickly without the overhead of explicit balancing mechanisms found in traditional binary search trees. Sleator and Tarjan introduced the splay tree in their seminal paper "Self-Adjusting Binary Search Trees," published in the Journal of the Association for Computing Machinery. This publication detailed the splaying operation as a simple heuristic inspired by earlier self-adjusting techniques, such as the move-to-front rule for linear lists and path compression in union-find structures, which reorganize data based on usage patterns to improve amortized . In the context of the 1980s, splay trees emerged as a response to fixed-balance trees like AVL trees and red-black trees, which maintain O(log n) worst-case time but require additional space for balance information and more complex implementations during insertions and deletions. Splay trees prioritized simplicity and adaptability, achieving comparable amortized efficiency through self-adjustment. Key milestones include their integration into the BSD kernel in the early 2000s (specifically, 5.0 in 2003) for management, enhancing caching performance in operating systems. Later developments, such as the tango tree in the 2000s, built on splay tree concepts to explore stronger optimality bounds. Recent theoretical advancements, including progress toward the dynamic optimality conjecture in works from the late , have confirmed aspects of splay trees' efficiency for arbitrary access sequences.

Properties

Advantages

Splay trees achieve an amortized of O(log n) for search, insertion, and deletion operations on an n-node tree, where the splaying mechanism brings recently accessed nodes closer to the root, thereby exploiting access locality to improve subsequent operations. Compared to height-balanced trees such as AVL or red-black trees, splay trees offer a simpler , as they require no explicit balance factors, color attributes, or additional rotations beyond the splaying process itself. Furthermore, they maintain space efficiency with O(n overall storage, using no extra metadata per node beyond the standard binary search tree pointers and keys. Splay trees are particularly adaptive to access patterns exhibiting temporal locality, performing well in scenarios like caching where frequently accessed elements benefit from reduced path lengths after splaying. In empirical evaluations on real-world workloads, such as those in , , and applications, splay trees have demonstrated 23%–40% faster performance than red-black trees, attributed to fewer rotations and better handling of clustered or sequential accesses.

Disadvantages

Splay trees exhibit a worst-case of O(n for individual operations, such as when accessing a node in a degenerate tree that forms a linear chain, requiring up to n rotations during the splay step to bring the accessed node to the . This lack of a guaranteed O(log n) bound per operation distinguishes splay trees from height-balanced variants like AVL or red-black trees, rendering them unsuitable for real-time systems where predictable worst-case is essential, as individual operations can become prohibitively expensive. The splaying mechanism introduces higher constant factors compared to standard binary search tree operations, since even in a well-balanced tree, accessing a node typically triggers multiple rotations—often several per access—incurring additional overhead from these structural adjustments. Splay trees are particularly sensitive to access patterns, performing poorly on uniformly random sequences or adversarial inputs that lack temporal locality, where the frequent restructuring fails to yield efficiency gains and can even degrade performance relative to simpler balanced trees.

Operations

Splaying

Splaying is the core operation in a splay tree that rearranges the after accessing a node, promoting that node to the through a sequence of tree rotations while maintaining the (BST) in-order traversal property. This process amortizes the cost of future accesses by bringing recently used nodes closer to the root, though the immediate cost of splaying can be linear in the tree height. The rotations used in splaying are the same as those in AVL trees or other self-balancing BSTs, ensuring no violation of BST invariants. Two primary approaches exist for implementing splaying: bottom-up and top-down. The bottom-up method starts at the accessed node and performs rotations upward toward the , typically without requiring pointers in the node structure, but it involves single or double rotations based on the configuration with the and . In contrast, the top-down approach simulates the process from the downward using pointers to track and rotate nodes as it descends, avoiding the need for double rotations but increasing memory overhead per node. The original splay tree formulation by Sleator and Tarjan favored the bottom-up variant for its simplicity in pointer-based implementations. The general algorithm for bottom-up splaying operates iteratively: while the accessed node xx is not the , it examines the positions relative to its pp and gg (if they exist). If xx has no , it is already the and splaying terminates. If xx has a but no , a single "zig" is performed between xx and pp. Otherwise, if xx and pp are both left (or both right) children of their parents, a "zig-zig" double is applied: first rotate pp with gg, then rotate xx with pp. If xx is a left child and pp a right child (or vice versa), a "zig-zag" double occurs: first rotate xx with pp, then rotate xx (now to gg) with gg. This repeats until xx reaches the , preserving the BST order at each step. Here is pseudocode for the bottom-up splay operation, assuming nodes have left and right child pointers and the root is maintained separately:

function splay(x, [root](/page/Root)): while x != root: p = [parent](/page/Parent)(x) // Assume parent pointers or traversal to find if p is null: break // x is root g = [parent](/page/Parent)(p) if g is null: // zig: single [rotation](/page/Rotation) rotate(x, p) update_root_if_needed(root, x) break if (x == left(p) and p == left(g)) or (x == right(p) and p == right(g)): // zig-zig: double [rotation](/page/Rotation) rotate(p, g) rotate(x, p) update_root_if_needed(root, x) else: // zig-zag: double [rotation](/page/Rotation) rotate(x, p) rotate(x, g) update_root_if_needed(root, x) return x // Now the new root

function splay(x, [root](/page/Root)): while x != root: p = [parent](/page/Parent)(x) // Assume parent pointers or traversal to find if p is null: break // x is root g = [parent](/page/Parent)(p) if g is null: // zig: single [rotation](/page/Rotation) rotate(x, p) update_root_if_needed(root, x) break if (x == left(p) and p == left(g)) or (x == right(p) and p == right(g)): // zig-zig: double [rotation](/page/Rotation) rotate(p, g) rotate(x, p) update_root_if_needed(root, x) else: // zig-zag: double [rotation](/page/Rotation) rotate(x, p) rotate(x, g) update_root_if_needed(root, x) return x // Now the new root

The rotate function performs a standard BST rotation (left or right depending on position), and update_root_if_needed adjusts the tree's reference if the original was rotated. This handles null parents and cases explicitly to prevent errors. Consider a small splay tree with keys 1, 2, 3, 4, 5 in in-order, initially structured as 3 (left: 2 (left: 1)), right: 5 (left: 4). To splay key 1:
  1. Node 1's is 2, is 3 (both left children), so zig-zig: first on 3, promoting 2 to (left 1, right 3 right 5 left 4); then on 2, promoting 1 to (right 2 right 3 right 5 left 4).
Splaying completes with 1 at the root. The tree remains a valid BST. If instead splaying key 4 in the original tree:
  1. Node 4's parent is 5 (right child of 3), 4 is left child of 5, so zig-zag: first right-rotate on 5, making 4 the right child of 3 (right 5); then left-rotate on 3, promoting 4 to root (left 3 left 2 left 1, right 5).
This step-by-step transformation illustrates how splaying restructures the tree to favor the accessed node without altering search order.

Insertion and Deletion

In splay trees, the core operations—search, insertion, and deletion—are adapted from standard binary search trees (BSTs) by incorporating splaying after the initial traversal or modification to move the accessed or affected node to the , ensuring amortized . This approach leverages the self-adjusting to reduce future access times for recently used elements without explicit balancing. The operations maintain the BST invariant (left subtree keys less than node key, right subtree keys greater) throughout. Insertion in a splay tree proceeds by first performing a standard BST insertion: traverse from the root following the search path to locate the where the new key belongs, and attach the new node as a there. Immediately after, splay the newly inserted node to the root using the splaying procedure. An alternative formulation uses the split operation to decompose the tree at the insertion point, creates the new singleton node, and then joins the parts, but the effect is equivalent in terms of structure and cost. To illustrate, consider an initially empty splay tree. Inserting the key 10 creates a single-node tree with 10 at the root. Next, inserting 5 traverses left from 10 (since 5 < 10), attaches 5 as the left child, and splays 5 to the root, resulting in 5 as root with 10 as right child. Inserting 15 then traverses right from 5 to 10 (15 > 5, 15 > 10), attaches 15 as right child of 10, and splays 15 to the root via zig-zig (both right children), yielding root 15 left 10 left 5. This restructuring brings the new node to prominence for potential subsequent accesses. Deletion begins by searching for the target key, which splays it to the if found (or splays the last probed node if not). With the node at the , remove it by joining its left and right subtrees into a single , preserving the BST order. The join typically splays the maximum node (rightmost) in the left subtree to become the new if the left subtree is non-empty, then attaches the original right subtree as its right child; if no left subtree, the right subtree becomes the new directly. If the has no children, deletion simply empties the . For example, starting from the tree after the above insertions (root 15 left 10 left 5), deleting 10 splays 10 to root via single zig (right-rotate on 15), resulting in root 10 left 5 right 15; then joins left (5) and right (15) by splaying 5 (max of left) to root and attaching 15 as right, resulting in root 5 with right 15. This maintains balance heuristically through the splaying. The search operation follows BST traversal rules: start at the and descend left or right based on key comparisons until reaching the target node or a indicating absence. If the key is found, splay that node to the ; if not, splay the last node visited (the potential insertion point) to the instead. This ensures that future searches near the probed path benefit from the adjustment. Splay trees assume unique keys and do not inherently support duplicates, as the BST invariant relies on strict ordering. In practice, encountering a duplicate during insertion typically results in ignoring the new key or replacing the existing node's value, depending on the implementation policy for set or map semantics.

Split and Join

In splay trees, the split operation decomposes a single splay tree TT into two independent splay trees TLT_L and TRT_R, such that all keys in TLT_L are less than or equal to a given key kk, and all keys in TRT_R are strictly greater than kk. This is accomplished by first splaying the node with key kk (or its predecessor/successor if kk is absent) to the of TT, which brings the relevant splitting point to the top of the tree. Once splayed, TLT_L consists of the and its entire left subtree (possibly including the if its key equals kk), while TRT_R is detached as the right subtree (or adjusted accordingly if the 's key exceeds kk). The join operation, conversely, combines two splay trees TLT_L and TRT_R into a single splay tree TT, under the precondition that every key in TLT_L is strictly less than every key in TRT_R. To perform the join, the maximum key in TLT_L is splayed to its , ensuring the largest element is at the top. The of TRT_R is then attached directly as the right child of this new in TLT_L, forming the combined tree without further rotations. Both operations leverage the splaying mechanism for efficiency and run in amortized O(logn)O(\log n) time, where nn is the number of nodes in the tree, due to the amortized cost of splaying combined with constant-time subtree detachment or attachment at the root. The following pseudocode outlines the split operation (assuming standard splay tree node structure with left and right children):

function split(k, T): if T is empty: return (empty, empty) splay(k, T) // Splay k (or successor/predecessor) to root root = T.root if root.key <= k: T_R = root.right root.right = empty return (T, T_R) else: T_L = root.left root.left = empty return (T_L, T)

function split(k, T): if T is empty: return (empty, empty) splay(k, T) // Splay k (or successor/predecessor) to root root = T.root if root.key <= k: T_R = root.right root.right = empty return (T, T_R) else: T_L = root.left root.left = empty return (T_L, T)

For join, the pseudocode is:

function join(T_L, T_R): if T_L is empty: return T_R if T_R is empty: return T_L splay_max(T_L) // Splay the maximum key to root of T_L T_L.root.right = T_R.root return T_L

function join(T_L, T_R): if T_L is empty: return T_R if T_R is empty: return T_L splay_max(T_L) // Splay the maximum key to root of T_L T_L.root.right = T_R.root return T_L

where splay_max(T) splays the rightmost node (maximum key) to the root by repeatedly splaying right children. These operations are particularly efficient for scenarios involving dynamic set manipulations, such as merging disjoint ordered sets or supporting range queries by temporarily decomposing the tree into key-ordered segments. For instance, to extract keys in [a,b][a, b], one can split at a1a-1 to isolate the lower portion, then split the remainder at bb to obtain the range as a separate splay tree, process it, and rejoin the parts. As an illustrative example, consider a splay tree with keys {1, 3, 5, 7} initially structured as root 5 (left: 3 with left 1; right: 7). Splitting at key 4 splays 3 (predecessor) to the root, yielding TLT_L with {1, 3} (root 3, left 1) and TRT_R with {5, 7} (root 5, right 7). Joining TLT_L and TRT_R splays 3 (max in TLT_L) to root and attaches TRT_R as its right subtree, resulting in root 3 left 1 right 5 right 7. This demonstrates how split and join preserve the binary search tree property while enabling modular tree handling.

Analysis

Splaying Steps

The splaying process in a splay tree consists of a sequence of primitive rotation operations—known as zig, zig-zig, and zig-zag—that incrementally move the accessed node toward the root while preserving the binary search tree (BST) property, ensuring that in-order traversal remains unchanged. These steps are applied bottom-up along the access path, with each rotation adjusting parent-child relationships to promote the node without violating the ordering invariant. Although splay trees do not enforce a strict heap ordering, the rotations during ascent effectively balance the tree by deepening subtrees of less recently accessed nodes, contributing to amortized efficiency as analyzed elsewhere.

Zig Step

The zig step is the simplest primitive, performed as a single rotation when the target node xx is a direct child of the current root. This terminal operation brings xx to the root position in constant time. Specifically, if xx is the left child of the root p(x)p(x), a right rotation is applied on the edge between p(x)p(x) and xx; conversely, if xx is the right child, a left rotation is used. Pre-rotation configuration (assuming xx as left child of root pp):

p / \ x C / \ A B

p / \ x C / \ A B

Post-rotation (after right rotation on pp):

x / \ A p / \ B C

x / \ A p / \ B C

This rotation preserves the BST property by swapping the subtrees of xx and p(x)p(x) appropriately, maintaining symmetric order. Pseudocode for the zig step, integrated into the splay procedure, is as follows:

if p(x) is root then if x is left child of p(x) then rotate_right(p(x)) else rotate_left(p(x))

if p(x) is root then if x is left child of p(x) then rotate_right(p(x)) else rotate_left(p(x))

Here, rotate_right(y) promotes the left child of yy by adjusting pointers: the left child of yy becomes the new parent, with subtrees reattached to preserve order.

Zig-Zig Step

The zig-zig step applies a double rotation when the target node xx and its parent p(x)p(x) are both left children (or both right children) of their respective parents, and p(x)p(x) is not the root. This configuration allows xx to ascend two levels in a balanced manner, first rotating the edge between p(x)p(x) and the grandparent g(x)g(x), then rotating the edge between xx and the new parent (formerly p(x)p(x)). Pre-rotation configuration (both left children case, with grandparent gg):

g / p / x / \ A B

g / p / x / \ A B

Post-rotation (after right rotation on gg, then right rotation on pp):

x / \ A p / \ B g / C (subtree of g)

x / \ A p / \ B g / C (subtree of g)

The rotations ensure the BST property is maintained, as the first rotation promotes p(x)p(x) while reattaching subtrees AA, BB, and the former left subtree of gg, and the second similarly adjusts for xx. Each rotation takes O(1)O(1) time, and the step avoids skewing the tree path. Pseudocode excerpt:

if p(x) is left child of g(x) then rotate_right(g(x)) rotate_right(p(x)) else if p(x) is right child of g(x) then rotate_left(g(x)) rotate_left(p(x))

if p(x) is left child of g(x) then rotate_right(g(x)) rotate_right(p(x)) else if p(x) is right child of g(x) then rotate_left(g(x)) rotate_left(p(x))

The symmetric right-child case mirrors this with left rotations.

Zig-Zag Step

The zig-zag step uses a double rotation for the opposing-child case, where xx is a left child of p(x)p(x) but p(x)p(x) is a right child of g(x)g(x), or vice versa, and p(x)p(x) is not the root. This "zig-zag" pattern first rotates the edge between xx and p(x)p(x) to make xx a sibling of its former parent, then rotates the edge between xx and the new parent (formerly g(x)g(x)) to promote xx two levels. Pre-rotation configuration ( xx left of pp, pp right of gg):

g \ p / x / \ A B

g \ p / x / \ A B

Post-rotation (after right rotation on pp, then left rotation on gg):

x / \ A g / \ B p / C (subtree of p)

x / \ A g / \ B p / C (subtree of p)

This sequence preserves the BST ordering by carefully swapping and reattaching subtrees AA, BB, CC (the former left of pp), and the former right of gg. The first rotation aligns the path, and the second elevates xx, each in O(1)O(1) time, promoting balance during the ascent. Pseudocode excerpt:

if x is left child of p(x) and p(x) is right child of g(x) then rotate_right(p(x)) rotate_left(g(x)) else if x is right child of p(x) and p(x) is left child of g(x) then rotate_left(p(x)) rotate_right(g(x))

if x is left child of p(x) and p(x) is right child of g(x) then rotate_right(p(x)) rotate_left(g(x)) else if x is right child of p(x) and p(x) is left child of g(x) then rotate_left(p(x)) rotate_right(g(x))

The opposite configuration uses symmetric rotations.

Amortized Analysis

The amortized analysis of splay trees focuses on the average performance over a sequence of operations, rather than the worst-case cost of individual operations, which can reach O(n)O(n) in a tree with nn nodes due to deep, unbalanced structures. This approach aggregates costs across multiple operations to demonstrate that splay trees maintain efficiency in practice, even if occasional operations are expensive. By employing the potential method, the analysis accounts for "stored work" in the tree's structure that offsets high costs during splaying. The potential function Φ(T)\Phi(T) for a splay tree TT is defined as the sum over all nodes xx of logs(x)\log s(x), where s(x)s(x) is the size of the subtree rooted at xx (including xx itself). This non-negative function measures the tree's "disorder" or depth-related inefficiency. The amortized cost of an operation is the actual time cic_i plus the change in potential Φ(Ti)Φ(Ti1)\Phi(T_i) - \Phi(T_{i-1}), ensuring that the total amortized cost over mm operations bounds the total actual cost. During splaying, rotations typically decrease the potential by restructuring the tree to bring the accessed node closer to the root, with each rotation costing constant time but yielding a net potential drop that amortizes the effort. The cost of splaying a node xx to the root is bounded by O(logn)O(\log n) amortized time, as the potential decrease during the sequence of rotations compensates for the number of steps, which is at most O(logn)O(\log n) in aggregate. Specifically, each single rotation or double rotation in the splay reduces the potential by an amount proportional to the rank increase of xx, leading to an overall amortized bound of at most 3(logn)+13(\log n) + 1 for accessing any node. This holds because the potential change ensures that expensive splays are "prepaid" by prior operations that built up the potential. All major splay tree operations—search, insertion, deletion, split, and join—achieve O(logn)O(\log n) amortized time complexity, as each involves at most a constant number of non-splay steps plus one splay operation. For instance, insertion splays the new node after attaching it, and deletion splays the target before removal, both leveraging the splay bound. Over mm operations on an nn-node tree, the total time is O(mlogn)O(m \log n), assuming uniform access frequencies. To illustrate, consider a simple splay sequence on a right-skewed tree of three nodes (A root, B right child, C right child of B), accessing C, which requires a zig-zig step consisting of two rotations (assuming unit weights, so sizes are node counts). Initially, sizes are s(A)=3s(A)=3, s(B)=2s(B)=2, s(C)=1s(C)=1, so Φ=log3+log2+log11.58+1+0=2.58\Phi = \log 3 + \log 2 + \log 1 \approx 1.58 + 1 + 0 = 2.58. The first rotation (left rotation on A, the grandparent) brings B to the root, costing 1 unit actual time; new sizes: s(B)=3s(B)=3, s(A)=1s(A)=1, s(C)=1s(C)=1, Φ1.58+0+0=1.58\Phi \approx 1.58 + 0 + 0 = 1.58, potential drop of 1, amortized cost 11=01 - 1 = 0. The second rotation (left rotation on B, now the parent and root) brings C to the root, costing 1; final sizes: s(C)=3s(C)=3, s(B)=2s(B)=2, s(A)=1s(A)=1, Φ1.58+1+0=2.58\Phi \approx 1.58 + 1 + 0 = 2.58, potential increase of 1, amortized cost 1+1=21 + 1 = 2. Total actual cost 2, total amortized cost 2, with net potential change 0, showing how potential changes offset the costs over the sequence. This pattern scales, with deeper trees yielding larger drops during splaying.

Weighted Path Length

In splay trees, the weighted path length is defined as the sum over all nodes ii of widiw_i \cdot d_i, where wi>0w_i > 0 is the fixed weight (often representing access frequency) of node ii, and did_i is the depth of ii in the tree. This metric quantifies the total search cost under a weighted access model, as the time to access node ii is proportional to its depth. Splaying minimizes the weighted path length over a sequence of accesses by repeatedly rotating the accessed node toward the root, thereby reducing the depths of frequently accessed (higher-weight) nodes while potentially increasing depths of less frequent ones. This self-adjustment ensures that nodes with higher weights tend to occupy shallower positions, lowering the overall weighted path length amortized across operations. To analyze this, the potential function is Φ(T)=xr(x)\Phi(T) = \sum_x r(x), where the sum is over all nodes xx in the tree TT, and the rank r(x)=logs(x)r(x) = \log s(x) with s(x)s(x) denoting the size of the subtree rooted at xx, defined as the sum of weights of all nodes in that subtree. This potential, which approximates the logarithm of the weighted path length up to additive constants, increases when the tree becomes more unbalanced but decreases sufficiently during splaying to bound costs. The derivation proceeds by charging the actual cost of each in a splay to changes in potential. For a splay operation that brings node xx to the of TT with rank r(T)r(T), the amortized cost is at most 3(r(T)r(x))+13(r(T) - r(x)) + 1. Specifically, a single zig changes the potential by at most 3(r(x)r(x))3(r'(x) - r(x)); a zig-zig by at most 3(r(x)r(x))3(r'(x) - r(x)); and a zig-zag by at most 3(r(x)r(x))3(r'(x) - r(x)), where primes denote values after the . Summing over the O(logn)O(\log n) in a splay yields an amortized cost of O(logn)O(\log n), as r(T)r(x)=O(logn)r(T) - r(x) = O(\log n) for uniform weights, ensuring the weighted path length remains low on average.

Theorems and Conjectures

Performance Theorems

The Balance Theorem establishes that any sequence of mm accesses in an nn-node splay tree requires O((m+n)logn)O((m + n) \log n) time in total. This bound matches the performance of a balanced over a sequence of at least nn operations, ensuring that splay trees remain efficient even without prior knowledge of access patterns. The proof relies on an using a potential function based on the tree's structure, where each splay operation is charged at most O(logn)O(\log n) amortized time, with an initial potential of O(nlogn)O(n \log n) that bounds the total cost across all operations. The Static Optimality Theorem provides a tighter bound for scenarios with known, fixed access frequencies. If each of the nn items is accessed q(i)q(i) times with total accesses m=q(i)m = \sum q(i), the total access time is O(m+q(i)log(m/q(i)))O(m + \sum q(i) \log (m / q(i))), assuming every item is accessed at least once. This demonstrates that splay trees perform within a constant factor of an optimal static tailored to those frequencies, as the bound approximates the of the access distribution. The proof sketch employs a weighted potential function where each node ii is assigned weight q(i)/mq(i)/m, leading to an amortized splay cost of O(log(m/q(i)))O(\log (m / q(i))) per access, with the total potential change bounded by O(nlogn)O(n \log n). The Static Finger Theorem extends efficiency to cases with a designated "finger" pointer to a fixed item ff. For a sequence of mm accesses to items iji_j, the total time is O(nlogn+m+j=1mlog(ijf+1))O(n \log n + m + \sum_{j=1}^m \log (|i_j - f| + 1)), where distances are measured in the key order. This implies faster operations for accesses near the finger, akin to finger search trees, by leveraging the splaying mechanism to reduce path lengths relative to the fixed reference. The analysis uses a potential function incorporating distances to the finger, bounding the amortized cost per access by O(log(ijf+1))O(\log (|i_j - f| + 1)). These theorems collectively guarantee that splay trees are competitive with optimal offline binary s for static access patterns, using potential functions to compare splaying costs against idealized structures.

Dynamic Optimality

The dynamic optimality posits that splay trees achieve near-optimal performance among all binary s (BSTs) for any of accesses. Specifically, for any of m successful accesses on an n-node , the total time for splaying is at most O(n) plus a constant factor times the time required by any that performs each access by traversing from the to the accessed node (costing one plus the node's depth) and restructures the tree using an arbitrary number of rotations between accesses (each costing one). This , proposed by Daniel Sleator and in their seminal 1985 paper introducing splay trees, implies that splay trees are O(1)-competitive with the optimal offline BST for any access , without requiring prior knowledge of the . Although the full conjecture remains unproven, significant partial confirmations exist. In 2004, , Dion Harmon, John Iacono, and Mihai Pătraşcu introduced tango trees, which achieve O(log log n)-competitiveness against the optimal offline BST, providing the first sub-logarithmic competitive ratio and marking major progress toward constant competitiveness. Independently, Jérémy Derrien, Julien Clément, and Sylvie Baneyrolles developed multi-splay trees, another O(log log n)-competitive structure that replaces certain components of tango trees with splay trees to improve auxiliary operation bounds. These results confirm the in restricted settings, such as when access sequences are independent of key orderings, where splay trees are proven dynamically optimal. Empirical studies indicate that splay trees perform consistently close to the optimal offline BST across diverse access patterns, supporting the conjecture's plausibility without counterexamples identified to date. Recent theoretical advances in the , including work by Caleb Levy and , have proven dynamic optimality for splay trees on restricted classes of sequences, such as decomposable or working-set-bound sequences, further narrowing the gap to a full proof. As of 2025, the general case remains open, with no disproof or counterexamples found despite extensive analysis. If proven true, the conjecture would establish splay trees as a universal online BST solution, performing within a constant factor of any possible rotation-based BST—static or adaptive—for arbitrary online access sequences, eliminating the need for sequence-specific tuning.

Variants and Implementation

Implementation Techniques

Splay trees can be implemented using either bottom-up or top-down splaying approaches, each with distinct advantages in terms of pointer requirements and traversal efficiency. The bottom-up method, as originally described, involves first searching for the target node from the root and then performing a series of rotations along the access path to bring it to the root, typically requiring parent pointers to facilitate the upward rotations. This approach uses three rotation patterns—zig (single rotation), zig-zig (two consecutive rotations in the same direction), and zig-zag (two rotations in opposite directions)—and is straightforward but incurs the overhead of maintaining parent links. In contrast, the top-down splaying technique avoids parent pointers altogether by tracking the path from the root to the target during the initial search and restructuring the tree in a single downward pass using only zig and zig-zig operations, effectively simulating the rotations without revisiting nodes upward. Top-down implementations are often preferred in environments where memory for parent pointers is a concern, as they reduce storage needs while preserving the amortized performance guarantees. Implementations must carefully handle edge cases to maintain correctness and the binary search tree invariants. For an empty tree, operations like insertion simply allocate a new node without splaying, as there is no existing to adjust. When splaying a single-node tree, no rotations are performed, since the node is already the , avoiding unnecessary operations. Root changes, which occur after splaying a non-root node to the top, require updating the external pointer to the new , ensuring subsequent operations start from the correct . Language-specific considerations influence the choice of data structures and mutation strategies. In C++, nodes are typically represented as structs or classes with pointers to left and right children, allowing efficient in-place rotations via pointer reassignment, which aligns well with the imperative nature of the language. For functional languages like , splay trees achieve persistency through path copying, where rotations create new nodes along the affected path while sharing unchanged subtrees, enabling immutable versions of the tree after each operation without altering the original. A common optimization to mitigate worst-case deep trees during initial unbalanced phases is semi-splaying, which limits restructuring by moving the accessed node only halfway up the path (e.g., by performing rotations up to the ), reducing rotation overhead while still improving access times for frequent elements. The following illustrates a basic bottom-up splay function, focusing on rotation helpers; it assumes nodes have left, right, and parent pointers.

pseudocode

function rotate_left(x): y = x.right x.right = y.left if y.left != null: y.left.parent = x y.parent = x.parent if x.parent == null: root = y else if x == x.parent.left: x.parent.left = y else: x.parent.right = y y.left = x x.parent = y function rotate_right(x): y = x.left x.left = y.right if y.right != null: y.right.parent = x y.parent = x.parent if x.parent == null: root = y else if x == x.parent.right: x.parent.right = y else: x.parent.left = y y.right = x x.parent = y function splay(x): while x.parent != null: parent = x.parent grandparent = parent.parent if grandparent == null: // zig if x == parent.left: rotate_right(parent) else: rotate_left(parent) else if x == parent.left and parent == grandparent.left: // zig-zig rotate_right(grandparent) rotate_right(parent) else if x == parent.right and parent == grandparent.right: // zig-zig rotate_left(grandparent) rotate_left(parent) else: // zig-zag if x == parent.left: rotate_right(parent) rotate_left(x) else: rotate_left(parent) rotate_right(x)

function rotate_left(x): y = x.right x.right = y.left if y.left != null: y.left.parent = x y.parent = x.parent if x.parent == null: root = y else if x == x.parent.left: x.parent.left = y else: x.parent.right = y y.left = x x.parent = y function rotate_right(x): y = x.left x.left = y.right if y.right != null: y.right.parent = x y.parent = x.parent if x.parent == null: root = y else if x == x.parent.right: x.parent.right = y else: x.parent.left = y y.right = x x.parent = y function splay(x): while x.parent != null: parent = x.parent grandparent = parent.parent if grandparent == null: // zig if x == parent.left: rotate_right(parent) else: rotate_left(parent) else if x == parent.left and parent == grandparent.left: // zig-zig rotate_right(grandparent) rotate_right(parent) else if x == parent.right and parent == grandparent.right: // zig-zig rotate_left(grandparent) rotate_left(parent) else: // zig-zag if x == parent.left: rotate_right(parent) rotate_left(x) else: rotate_left(parent) rotate_right(x)

This structure ensures the binary search tree property is preserved after each rotation, with the splayed node becoming the new root.

Notable Variants

Semi-splay trees modify the standard splaying operation by splaying a node only up to half the depth of the from the , which provides amortized O(log n) time per operation with a reduced constant factor while preserving efficiency for frequent accesses. This variant reduces the number of rotations compared to full splaying, achieving a constant factor improvement in total rotations for certain access sequences. Multi-splay trees extend splay trees to support batch operations by splaying multiple nodes simultaneously along a common path, enabling O(log log n)-competitive performance against any while maintaining O(log n) worst-case access time. Introduced as a step toward resolving the dynamic optimality , multi-splay trees achieve this by performing a sequence of multi-rotations that adjust the tree structure more efficiently for sequences with locality. Tango trees represent a spine-based variant of splay trees that use a combination of splaying within a top tree and a backbone tree to simulate access costs, proving O(log log n)-competitiveness for the class of tree-access sequences and providing evidence toward dynamic optimality in restricted settings. This structure separates the search path into a splay tree for local adjustments and a global spine for overall balance, resulting in improved bounds over standard splay trees for non-uniform access patterns. Working set splay trees adapt the splaying to exploit properties by partially splaying nodes based on recent access history, reducing unnecessary rotations in cache-sensitive environments and achieving better performance for sequences with bounded compared to standard splay trees. This post-2000 variant focuses on cache-oblivious access patterns, where the tree layout minimizes traversals without explicit knowledge of cache parameters.

Applications and Comparisons

Applications

Splay trees are employed in various software libraries for implementing dictionaries and associative arrays, leveraging their amortized O(log n) performance for operations like insertion, deletion, and lookup, particularly in scenarios with temporal locality. In the GNU C++ Standard Template Library (STL) extensions, policy-based data structures include splay trees as an option for ordered containers, allowing developers to choose them over red-black trees for applications benefiting from self-adjustment. Splay trees are also used in the GCC compiler for various internal tasks. Similarly, the Boost C++ Libraries provide intrusive splay tree-based associative containers, such as splay_set and splay_multiset, optimized for caching and memory allocation tasks where frequent access patterns improve efficiency. In operating system kernels, splay trees support caching mechanisms, exploiting access locality to prioritize recently used data. The kernel utilizes splay trees for managing hash lists of I/O buffers associated with vnodes, enabling efficient organization of clean and dirty buffer lists during file operations, which enhances performance for repeated directory and file accesses. Similarly, the Windows kernel employs splay trees in management, networking, and code. This self-adjusting property aligns well with the irregular but localized patterns typical in workloads. Splay trees find application in network routing tables, where they adapt to frequent accesses of common prefixes or routes, reducing lookup times for high-traffic paths. A notable use is in packet algorithms, where splay trees model rule sets for efficient matching of packet headers against firewall or forwarding rules, achieving improved throughput in multi-field searches compared to static structures. This adaptability is particularly valuable in routers handling dynamic traffic patterns. Theoretically, splay trees serve as a foundational component in advanced dynamic graph algorithms, notably as the auxiliary structure in link-cut trees for representing forests of rooted trees. Introduced by Sleator and Tarjan, link-cut trees use splay trees to maintain paths in a collection of trees, supporting operations like linking, cutting, and path queries in amortized O(log n) time, which has broad implications for connectivity problems in graphs and geometric computations.

Comparisons with Other Trees

Splay trees differ from in their balancing mechanism, as splay trees rely on self-adjustment through splaying recently accessed nodes to the root without maintaining explicit height invariants, whereas enforce strict height balance by ensuring the heights of left and right subtrees differ by at most one via rotations. This makes splay trees simpler to implement, avoiding the need for balance factors at each node, but provide stricter worst-case height control, bounding the tree height at less than 1.4405 log n + 0.327 for n nodes. In contrast to red-black trees, which use color attributes to maintain balance and guarantee a worst-case height of less than 2 log (n + 1), splay trees offer amortized O(log n) performance without such auxiliary data, potentially requiring more rotations per operation but adapting to access patterns through locality. Red-black trees involve fewer rotations on average for balanced structures, providing predictable O(log n) worst-case bounds for all operations, while splay trees can degrade to O(n) in the worst single operation. Treaps balance via randomized priorities assigned to nodes, ensuring expected O(log n) height and operations through a structure combining search order and heap properties, unlike splay trees' deterministic, locality-driven adjustments that promote frequently accessed nodes without . This randomization in treaps provides probabilistic guarantees independent of access sequences, whereas splay trees exploit temporal locality for amortized efficiency. Empirical benchmarks indicate that splay trees outperform and red-black trees on sequential or clustered access patterns due to their adaptation to locality, achieving faster response times for repeated or ordered operations, while performing worse on purely random accesses where maintain slightly better search efficiency through stricter balance. For , experiments show they are competitive with splay trees across insert, delete, and search on both random and sequential workloads, often within a factor of 2 in execution time, though splay trees may edge out in locality-heavy scenarios. Splay trees are preferable for dynamic workloads exhibiting reuse or temporal locality, such as caching or network routing, where their adaptive nature yields superior amortized performance over sequences; in contrast, AVL, red-black, or structures suit applications requiring predictable worst-case or expected bounds, like databases with uniform random accesses.

References

Add your contribution
Related Hubs
User Avatar
No comments yet.