Splay Tree
This page briefly introduces how to maintain a binary search tree using Splay.
Definition
Splay Tree, or Splay Tree, is a balanced binary search tree. It continuously rotates a node to the root through splay operations, ensuring the tree still satisfies the properties of a binary search tree. It can perform insertion, search, and deletion operations in amortized \(O(\log N)\) time while maintaining balance and avoiding degeneration into a chain.
The Splay tree was invented by Daniel Sleator and Robert Tarjan in 1985.
Basic Structure and Operations
This section discusses the basic structure of the Splay tree and its core operations, with the most important being the splay operation.
A Splay tree is a binary search tree. When searching for a value, it satisfies the property: the value of any node in the left subtree \(<\) the value of the root node \(<\) the value of any node in the right subtree.
Maintaining Information
This article uses arrays to simulate pointers to implement the Splay tree. The following information needs to be maintained:
| rt | id | fa[i] | ch[i][0/1] | val[i] | cnt[i] | sz[i] |
|---|---|---|---|---|---|---|
| Root node ID | Number of used nodes | Parent | Left/right child IDs | Node weight | Weight occurrence count | Subtree size |
During initialization, all information can be set to zero.
Auxiliary Operations
First, some simple auxiliary operations:
dir(x): Determines whether node \(x\) is the left or right child of its parent node.push_up(x): After changing the node's position, updates node \(x\)'s information based on its children's information.
Implementation
1 2 3 | |
Rotation Operation
To keep the Splay tree balanced, rotation operations are needed. The purpose of rotation is to move a node up one position.
Rotation must ensure:
- The in-order traversal of the entire Splay tree remains unchanged (the binary search tree properties are not violated).
- The information maintained by the affected nodes remains correct and valid.
rtmust point to the root node after rotation.
In Splay trees, rotations are divided into two types: left rotation and right rotation.
Observing the diagram, if we want to move node \(x\) (the \(1\) in left rotation and the \(2\) in right rotation) up through rotation, the direction of rotation is uniquely determined by whether the node is the left or right child of its parent. Therefore, when implementing the rotation operation, we only need to pass the node \(x\) to be moved up.
Detailed analysis of rotation steps: (Assume the node to be moved up is \(x\), taking right rotation as an example)
- First, record node \(x\)'s parent node \(y\), and \(y\)'s parent node \(z\) (which may be empty), and record whether \(x\) is the left or right child of \(y\).
- In the order from bottom to top in the tree after rotation, update \(y\)'s left child to \(x\)'s right child, \(x\)'s right child to \(y\), and if \(z\) is not empty, update \(z\)'s child to \(x\).
- In the same order, update the parent node of \(y\)'s current left child (if it exists) to \(y\), \(y\)'s parent node to \(x\), and \(x\)'s parent node to \(z\).
- Maintain node information from bottom to top.
Implementation
1 2 3 4 5 6 7 8 9 10 11 12 | |
When implementing all functions, be careful not to modify the information of node \(0\).
Splay Operation
The Splay tree requires that after accessing a node \(x\), it must be forcibly rotated to the root node. This operation is also called the splay operation.
Let the recently accessed node be \(x\). To perform a splay operation, we need to apply a series of splay steps to \(x\). Each time a splay step is applied to \(x\), the distance from \(x\) to the root node becomes shorter. Define \(p\) as the parent node of \(x\). There are three types of splay steps:
-
zig: Operates when \(p\) is the root node. The Splay tree rotates according to the edge between \(x\) and \(p\). zig exists to handle parity issues and is executed only as the last step of the splay operation when \(x\) has an odd depth at the beginning of the splay operation.
That is, directly right-rotate or left-rotate \(x\) (Figures 1, 2).
-
zig-zig: Operates when \(p\) is not the root node and both \(x\) and \(p\) are right children or both are left children. The example below shows the case where both \(x\) and \(p\) are left children. The Splay tree first rotates along the edge connecting \(p\) and its parent \(g\), then rotates along the edge connecting \(x\) and \(p\).
That is, first right-rotate or left-rotate \(p\), then right-rotate or left-rotate \(x\) (Figures 3, 4).
-
zig-zag: Operates when \(p\) is not the root node and one of \(x\) and \(p\) is a right child and the other is a left child. The Splay tree first rotates along the edge between \(p\) and \(x\), then rotates along the new resulting edge between \(x\) and \(g\).
That is, first left-rotate then right-rotate \(x\), or first right-rotate then left-rotate \(x\) (Figures 5, 6).
Tip
Readers are encouraged to try simulating the six rotation scenarios themselves to understand the basic idea of the splay operation.
Comparing the three splay steps, the key to distinguishing which operation to use is to determine whether \(x\) is a child of the root node and whether \(x\) and its parent are on the same side relative to their respective parents.
The implementation provided here can specify any root node \(z\) and move any node \(x\) within its subtree up to \(z\):
- First, record the parent node \(w\) of the root node \(z\), so that we can use
fa[x] == wto determine if \(x\) is already at the root. - Record the current parent node \(y\) of \(x\). If \(y\) and \(w\) are the same, it means \(x\) has reached the root node.
- Otherwise, use
fa[y] == wto determine if \(y\) is the root node. If it is, perform a zig operation to rotate \(x\). If not, usedir(x) == dir(y)to decide whether to use zig-zig or zig-zag. The former rotates \(y\) first and then \(x\), while the latter rotates \(x\) twice directly.
Implementation
1 2 3 4 5 6 7 | |
The splay operation is the core operation of the Splay tree and the key step that ensures its time complexity guarantee. Please ensure that after each downward access to a node, a splay operation is performed.
Additionally, the splay operation updates the information of all nodes on the path from the current node \(x\) to the root node \(z\) from bottom to top. It is precisely because of this that we can modify a non-root node and then move it up to the root via a splay operation to complete the update of the entire tree's information.
Time Complexity
The complexity of performing \(m\) splay operations on a Splay tree of size \(n\) is \(O((n+m)\log n)\), with an amortized single-operation complexity of \(O(\log n)\).
Complexity Proof Based on Potential Analysis
To prove this, we only need to analyze the complexity of the three operations: zig, zig-zig, and zig-zag. For this, we use potential analysis, studying the change in potential to derive the amortized complexity of the operations. Suppose we perform \(m\) splay operations on a Splay tree containing \(n\) nodes. The analysis can be done as follows:
Definitions:
- Potential of a single node: \(w(x) = \log(\text{size}(x))\), where \(\text{size}(x)\) represents the size of the subtree rooted at node \(x\).
- Potential of the entire tree: \(\varphi = \sum w(x)\), i.e., the sum of the potentials of all nodes in the tree. The initial potential satisfies \(\varphi_0 \leq n \log n\).
- Amortized cost of the \(i\)-th operation: \(c_i = t_i + \varphi_i - \varphi_{i-1}\), where \(t_i\) is the actual operation cost, and \(\varphi_i\) and \(\varphi_{i-1}\) are the potentials after and before the operation, respectively.
Properties:
-
If \(p\) is the parent of \(x\), then \(w(p) \geq w(x)\), i.e., the parent's potential is not less than the child's potential.
-
Since the subtree size of the root node remains unchanged before and after the operation, the potential of the root node does not change during the operation.
-
If \(\text{size}(p)\ge\text{size}(x)+\text{size}(y)\), then \(2w(p) - w(x) - w(y) \geq 2\).
Proof of Property 3
According to the inequality of arithmetic and geometric means,
Next, we perform potential analysis on the zig, zig-zig, and zig-zag operations separately. Let the potentials of node \(x\) before and after the operation be \(w(x)\) and \(w'(x)\), respectively. The node notation is consistent with the above section.
zig: According to properties 1 and 2, \(w(p) = w'(x)\), and \(w'(x) \geq w'(p)\). Thus, the amortized cost is
zig-zig: According to properties 1 and 2, \(w(g) = w'(x)\), \(w'(x) \geq w'(p)\), and \(w(x) \leq w(p)\). Because
according to property 3, we have
Thus, the amortized cost is
zig-zag: According to properties 1 and 2, \(w(g) = w'(x)\), and \(w(p) \geq w(x)\). Because \(\text{size}'(x)>\text{size}'(p)+\text{size}'(g)\), according to property 3, we have
Thus, the amortized cost is
Single Splay Operation:
Let \(w^{(n)}(x)=(w^{(n-1)})'(x)\) and \(w^{(0)}(x)=w(x)\). Suppose a splay operation accesses nodes \(x_{1}, x_{2}, \cdots, x_{n}\) in sequence, and finally \(x_{1}\) becomes the root node. This must involve several zig-zig and zig-zag operations and at most one zig operation. The amortized cost of the first two types of operations does not exceed \(3(w'(x)-w(x))\), and the amortized cost of the last operation does not exceed \(3(w'(x) - w(x))+1\). Therefore, the total amortized cost does not exceed
Hence, the amortized complexity of a single splay operation is \(O(\log n)\). Consequently, the time complexity of insertion, query, deletion, and other operations based on splaying is also amortized \(O(\log n)\).
Conclusion:
After performing \(m\) splay operations, the actual cost is
Therefore, the actual time complexity of \(m\) splay operations is \(O((m+n)\log n)\).
Why can the rebalancing operation of a Splay tree achieve an amortized complexity of \(O(\log n)\)?
The naive rebalancing idea is to repeatedly rotate a node upward until it becomes the root. The problem with this naive approach is that for a chain-like tree where all children are left (or right) children, it is equivalent to repeatedly performing zig operations. Thus, the constant term \(1\) in the amortized cost of the zig operation accumulates continuously, resulting in a final amortized complexity of \(O(\log n+n)\). The design of the Splay tree's rebalancing operation avoids the constant accumulation in consecutive zig scenarios, ensuring that during a complete splay operation, at most one standalone zig operation is performed, thereby optimizing the time complexity.
Balanced Tree Operations
This section discusses methods for implementing common balanced tree operations based on the Splay tree. Among them, the more important ones are finding elements by value or rank, which can locate a specific element and move it up to the root for subsequent processing.
As an example, this section will discuss the implementation of the template problem Normal Balanced Tree.
Search by Value
As a binary search tree, we can search for the corresponding node by value \(v\) by comparing the value \(v\) to be found with the value of the current node. After finding it, move the element up to the root.
Note that there are often situations where the corresponding node does not exist in the tree. For such cases, record the last accessed node (i.e., \(y\) in the implementation) and move \(y\) up to the root. At this point, the value stored in node \(y\) must be either the largest among all elements less than \(v\) (i.e., the predecessor of \(v\)) or the smallest among all elements greater than \(v\) (i.e., the successor of \(v\)). This is because the search process ensures that the left subtree always stores values less than \(v\), and the right subtree always stores values greater than \(v\).
Implementation
1 2 3 4 5 | |
This implementation allows specifying any node \(z\) as the root node and searching by value within its subtree.
Access by Rank
Because subtree size information is recorded, the Splay tree can also access elements by rank, i.e., find the \(k\)-th smallest element in the tree.
Let \(k\) be the remaining rank. The specific steps are as follows:
- If the left subtree is non-empty and the remaining rank \(k\) is not greater than the size of the left subtree, then search in the left subtree.
- Otherwise, if \(k\) is not greater than the size of the left subtree plus the root, then the root node is the one we are looking for.
- Otherwise, subtract the size of the left subtree and the root from \(k\), and continue searching in the right subtree.
- Move the finally found element up to the root.
Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
This implementation needs to ensure that the rank \(k\) does not exceed the tree size at root \(z\).
Operation \(4\) in the template problem requires returning a value by rank. Simply call this method and return the value.
Implementation
1 2 3 4 5 | |
Merge Operation
Sometimes we need to merge two Splay trees.
Let the root nodes of the two trees be \(x\) and \(y\). To ensure the result is still a binary search tree, we require that the maximum value in tree \(x\) is less than the minimum value in tree \(y\). This condition can usually be satisfied because the two trees are often split from a larger subtree.
The merge operation is as follows:
- If one or both of \(x\) and \(y\) are empty trees, directly return the root node of the non-empty tree or an empty tree.
- Otherwise, use
loc(y, 1)to move the minimum value in tree \(y\) up to the root \(y\), then set its left child (which must be empty at this point) to \(x\), update node information, and return node \(y\).
Implementation
1 2 3 4 5 6 7 8 | |
The split operation is similar. Therefore, the Splay tree can simulate the approach of non-rotating treap for various operations, including interval operations. Later sections will introduce interval operation handling methods more characteristic of Splay trees.
Insert Operation
The insert operation is a relatively complex process. The specific steps are as follows: (Assume the value to be inserted is \(v\))
- Similar to the search-by-value process, search downward for the node storing \(v\) or an empty node based on \(v\), recording the parent node \(y\) during the process.
- If a node \(x\) storing \(v\) exists, directly update its information; otherwise, create a new node \(x\).
- Perform a splay operation to move the last node \(x\) up to the root.
Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
This implementation allows directly inserting a value into an empty tree. If you don't want to handle empty trees, you can insert a dummy node into the tree in advance.
Delete Operation
The delete operation is also a relatively complex operation. The specific steps are as follows: (Assume the value to be deleted is \(v\))
- First, search for the node storing it by value \(v\) and move it up to the root.
- If the node storing it does not exist, return directly. (The splay operation has already been performed in the previous step.)
- Otherwise, update the node information.
- If the resulting root node is an empty node, merge the left and right subtrees as the new root node. Note that before merging, the parent nodes of the roots of the two subtrees need to be updated to empty.
Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Query Rank
Directly access the node by value \(v\) (and move it up to the root), then return the corresponding value.
Note that when \(v\) does not exist, the relationship between the root returned by the method find(rt, v) and \(v\) cannot be determined and needs to be discussed separately.
Implementation
1 2 3 4 | |
Query Predecessor
The predecessor is defined as the largest number less than \(v\). The specific steps are as follows:
- Access the node by value \(v\) (and move it up to the root).
- If the value at the root is less than \(v\), then it must be the largest one, so return it directly.
- Otherwise, find the maximum value in the left subtree and move it up to the root.
The last step is equivalent to directly calling loc(ch[rt][0], sz[ch[rt][0]]), but it omits unnecessary checks.
Implementation
1 2 3 4 5 6 7 8 9 | |
This implementation allows the predecessor not to exist, in which case it returns \(-1\).
Query Successor
The successor is defined as the smallest number greater than \(x\). The query method is similar to the predecessor, except that the maximum value in the left subtree is replaced by the minimum value in the right subtree, i.e., calling loc(ch[rt][1], 1).
Implementation
1 2 3 4 5 6 7 8 9 | |
Reference Implementation
At the end of this section, a reference implementation for the template problem Normal Balanced Tree is provided.
Reference Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
Sequence Operations
Splay trees can also be applied to sequences to maintain interval information. Compared to segment trees, Splay trees have a larger constant factor but support more complex sequence operations, such as interval reversal. As mentioned earlier, Splay trees also support split and merge operations, so they can simulate non-rotating treap for interval operations, which will not be discussed further here. This section mainly discusses the implementation method of interval operations based on splay operations.
The Splay tree built from a sequence has the following properties:
- The in-order traversal of the Splay tree corresponds to the left-to-right traversal of the original sequence.
- A node in the Splay tree represents an element in the original sequence.
- A subtree in the Splay tree represents an interval in the original sequence.
Because of the splay operation, we can quickly extract the Splay subtree representing a certain interval.
As an example, this section will discuss the implementation of the template problem Literary Balanced Tree.
Building the Tree from a Sequence
Before operations, we need to build the Splay tree from the given sequence. Based on the properties of the Splay tree, we can directly build a chain with only left children. The time complexity is \(O(n)\).
Reference Implementation
1 2 3 4 5 6 7 8 9 10 | |
The final splay operation updates node information from bottom to top. For the convenience of later interval operations, two sentinel nodes are added to the left and right sides of the sequence.
Interval Reversal
Taking interval reversal as an example, we can understand the method of interval operations: (Let the interval be \([L,R]\))
- First, move node \(L-1\) up to the root node, then in its right subtree, move node \(R+1\) up to the root of the right subtree.
- At this point, let \(x\) be the left child of the root's right child. Then the subtree rooted at \(x\) corresponds to the interval \([L,R]\).
- Perform the operation on interval \([L,R]\) at \(x\) and mark it with a lazy tag.
- Propagate the tag once at \(x\), then use the splay operation to move \(x\) up to the root.
The operation needed in the first step is the "access by rank" from the balanced tree operations discussed earlier, because the label of an element is its rank. Due to the management of lazy tags, its implementation is slightly different from the above.
Reference Implementation
1 2 3 4 5 6 7 8 | |
The splay operation in the last step is not to ensure correct complexity but to update node information. Because the splay operation involves the left and right children of node \(x\), we need to propagate the tag at node \(x\) once beforehand. Of course, for the interval reversal operation specifically, reversing the subinterval does not affect ancestor nodes, so omitting this step is also correct. This implementation retains these two lines to illustrate the general approach.
Lazy Tag Management
First, we need the auxiliary functions lazy_reverse(x) and push_down(x). The former swaps left and right children and updates the lazy tag; the latter propagates the tag downward.
Reference Implementation
1 2 3 4 5 6 7 8 9 10 11 12 | |
Then, we only need to propagate tags when moving downward through nodes. The operations required by the template problem are relatively simple; only the operation of finding by rank (i.e., loc) involves accessing nodes downward. Note that we need to propagate tags before the function accesses a new node each time.
Reference Implementation
1 2 3 4 5 6 7 8 9 10 11 12 | |
Because all lazy tags on the traversed path have been removed when accessing nodes downward, we no longer need to handle lazy tags when moving nodes up using the splay operation. However, we must be careful with the node involved in the interval operation: it is also on the path of the splay operation, but it has just been operated on and may have tags that have not been propagated yet. We need to propagate them first before performing the splay operation, as done above.
Reference Implementation
At the end of this section, a reference implementation for the template problem Literary Balanced Tree is provided.
Reference Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
Exercises
These problems are straightforward applications of Splay trees maintaining binary search trees:
- 【Template】Normal Balanced Tree
- 【Template】Literary Balanced Tree
- 「HNOI2002」Business Statistics
- 「HNOI2004」Pet Adoption Center
Splay trees also appear in more complex application scenarios:
- 「Cerc2007」robotic sort
- 「HNOI2011」Bracket Repair / 「JSOI2011」Bracket Sequence
- Second-Order Balanced Tree (Tree of Trees)
- BZOJ 2827 Thousands of Birds
- 「Lydsy1706 Monthly Contest」K-th Smallest Value Query
- POJ3580 SuperMemo
References and Notes
Parts of this article are referenced from the algocode algorithm blog. Special thanks!
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用