Amortized analysis- Splay Tree
A splay tree is an efficient implementation of binary search trees that takes advantage of locality in the incoming lookup requests. Locality in this context is a tendency to look for the same element multiple times. A stream of requests exhibits no locality if every element is equally likely to be accessed at each point. For many applications, there is locality, and elements tend to be accessed repeatedly. A good example of an application with this property is a network router. Routers must decide on which outgoing wire to route the incoming packets, based on the IP address in the packets. The router needs a big table (a map) that can be used to look up an IP address and find out which outgoing connection to use. If an IP address has been used once, it is likely to be used again, perhaps many times. Splay trees are designed to provide good performance in this situation.
In addition, splay trees offer amortized O(lg n) performance. That is, a sequence of M operations on an n-node splay tree takes O(M lg n) time.
A splay tree is a binary search tree. It has one interesting difference, however: every operation on the tree causes the splay tree to reorganize itself, moving the target element to the root of the tree, without breaking the binary search tree invariant. If the next operation is for the same element, it can be retrieved immediately. In general, if a small number of elements are being heavily used, they will tend to be found near the top of the tree and are thus found quickly.
We have already seen a way to move an element upward in a binary search tree: tree rotation. When an element is accessed in a splay tree, tree rotations are used to move it to the top of the tree. Notice that the algorithm requires that we be able to update the tree in place, but the abstract view of the set of elements represented by the tree does not change and the rep invariant is maintained. This is an example of a benevolent side effect: a side effect that does not change the abstract view of the value represented.
To make the operations efficiently, whenever possible, rotations are done in pairs. This results in extremely good performance in practice. There are three kinds of tree rotations that are used to move elements upward in the tree. These rotations have two important effects: they move the node being splayed upward in the tree, and they also shorten the path to any nodes along the path to the splayed node. This latter effect means that splaying operations tend to make the tree more balanced.
Rotation 1: Simple rotation
The simple tree rotation used in AVL trees is also applied at the root of the splay tree, moving the splayed node x up to become the new tree root. Here we have A < x < B < y < C, and the splayed node is either x or y depending on which direction the rotation is. It is highlighted in red.
y x / \ / \ x C <-> A y / \ / \ A B B C
Rotation 2: Zig-Zig
Lower down in the tree rotations are performed in pairs so that nodes on the path from the splayed node to the root move closer to the root on average. In the "zig-zig" case, the splayed node is the left child of a left child or the right child of a right child ("zag-zag").
z x / \ / \ y D A y / \ <-> / \ (A < x < B < y < C < z < D) x C B z / \ / \ A B C D
Rotation 3: Zig-Zag
In the "zig-zag" case, the splayed node is the left child of a right child or vice-versa. The rotations produce a subtree whose height is less than that of the original tree. Thus, this rotation improves the balance of the tree. In each of the two cases shown, y is the splayed node:
z x y / \ / \ / \ y D / \ A z (A < y < B < x < z < D) / \ -> y z <- / \ A x / \ / \ x D / \ A B C D / \ B C B C
Amortized analysis
To show that splay trees deliver the promised amortized performance, we will use the banker's method, similar to the one used in our analysis of dynamic resizable arrays in class.
In this scenario, each node of the tree has a savings account containing a certain amount of money. When a node x is created, we overcharge the add operation that creates x, and deposit the extra credits to x's account. These credits will be used later to pay for restructuring operations on the tree.
Let S(x) be the subtree rooted at node x (including x) and |S(x)| the number of nodes in tree S(x). Define:
μ(x) = floor(log |S(x)|)
where floor(n) returns the smallest integer greater than n. We maintain the following credit invariant:
Node x always has at least μ(x) credits on deposit.
Notice that each operation can be performed using a constant number of splay operations plus a constant number of comparisons and pointer manipulations. The extra amount needed to be charged from the add operation and then deposited in x's account is at most μ(r) = log(floor(n)), where r is the root node of the tree, and n is the total number of elements in that tree. This overcharge does not increase the complexity of add more than O(log n). However, we know that some individual operations may take more than O(log n) time.
If we could withdraw some credits from our savings to reduce the cost of each splay operation to O(log n) time, it would immediatelly follow that the total amortized time required for a sequence of M operations is O(M log n). But we do not know how many credits we need to spend in order to perform each splay operation and, at the same time, to maintain the credit invariant. It turns out that one can prove that we need to spend no more than 3(μ(r) - μ(x)) + 1 credits, where r is the root of the tree. Let us demonstrate this in each of the three splay scenarios:
Simple rotation
Let μ and μ' be the values of μ before and after the splay operation, respectively. The only two nodes that change sizes are x (the child) and y (the parent). So the cost of the operation to maintain the invariant is
μ'(x) + μ'(y) − μ(x) − μ(y)
(this basically corresponds to what is needed to maintain the invariant after the splay operation, subtracted by the credits we had previously).
Since y decreases in size, μ'(y) <= μ'(x). Also notice that μ'(x) = μ(y). As a consequence, the cost of the operation is at most μ'(x) − μ(x). Finally, since x increases in size, μ'(x) − μ(x) is positive, and the cost is bounded by 3(μ'(x) − μ(x)).
Since we can afford to spend 3(μ'(x) − μ(x))+1, we are left with at least one credit left over to pay for the constant number of pointer manipulations and comparisons.
μ'(x) + μ'(y) − μ(x) − μ(y)
(this basically corresponds to what is needed to maintain the invariant after the splay operation, subtracted by the credits we had previously).
Since y decreases in size, μ'(y) <= μ'(x). Also notice that μ'(x) = μ(y). As a consequence, the cost of the operation is at most μ'(x) − μ(x). Finally, since x increases in size, μ'(x) − μ(x) is positive, and the cost is bounded by 3(μ'(x) − μ(x)).
Since we can afford to spend 3(μ'(x) − μ(x))+1, we are left with at least one credit left over to pay for the constant number of pointer manipulations and comparisons.
Zig-Zig rotation
Only nodes x, y, and z change in size. Thus, the cost to maintain the invariant is:
μ'(x) + μ'(y) + μ'(z) − μ(x) − μ(y) − μ(z)
Since the μ'(x) is the same as μ(z), this is equal to
= μ'(y) + μ'(z) − μ(x) - μ(y)
= (μ'(y) − μ(x)) + (μ'(z) - μ(y))
Note that μ'(x) is greater than μ'(z) and μ'(y), and μ(x) is less than μ(y), so this is at most
= (μ'(x) − μ(x)) + (μ'(x) - μ(x))
= 2(μ'(x) - μ(x))
We can afford to pay for this operation and still have μ'(x) − μ(x) + 1 credits left over to pay for the constant number of low-level operations needed to perform these two rotations.
Unfortunaltely, this may not always be the case: it may turn out that μ'(x) = μ(x), in which case we have nothing left over. It can be shown that in this case the cost of the operation is strictly negative. What is needed to be shown is that, if μ'(x) = μ(x), then
μ'(x) + μ'(y) + μ'(z) >= μ(x) + μ(y) + μ(z)
leads to a contradition. This proof is left to the reader as an exercise.
Therefore, if μ'(x) = μ(x), the invariant is maintained for free, and we can afford to spend our saved credits to pay for the low-level operations.
Zig-Zag rotation
Again, the amortized time is
μ'(x) + μ'(y) + μ'(z) −μ(x) − μ(y) − μ(z)
This case is similar to the previous one, and this completes our proof.
We have shown that, if we use our saved credits, at most 3(μ'(x) − μ(x))+1 is needed for each splay operation. Since μ(x) is at most floor(log n), a constant number of splay operations can be performed in O(log n), and this is all we need to perform any operation on a splay tree. It follows that the total time required for a sequence of M such operations is O(M log n).
We have shown that, if we use our saved credits, at most 3(μ'(x) − μ(x))+1 is needed for each splay operation. Since μ(x) is at most floor(log n), a constant number of splay operations can be performed in O(log n), and this is all we need to perform any operation on a splay tree. It follows that the total time required for a sequence of M such operations is O(M log n).
Comments
Post a Comment