An Brief Introduction to Amortized Analysis
Sometimes we want to design data structures that trade per-operation efficiency for overall efficiency. To analyze efficiency of these data structures, we will need to use amortized analysis.
Examples
The two-stack queue
- Maintain an In stack and an Out stack.
- To enqueue an element, push it onto the In stack.
- Each enqueue takes time
. - Just push an item onto the In stack.
- Each enqueue takes time
- To dequeue an element:
- If the Out stack is nonempty, pop it.
- If the Out stack is empty, pop lements from the In stack, pushing them into the Out stack. Then dequeue as usual.
- Dequeues can vary in their runtime.
- Could be
if the Out stack isn't empty. - Could be
if the Out stack is empty. - It's wrong, but useful, to pretend the cost of a dequeue is
. - Some operations take more time than this.
- However, if we pretend each operation takes time
, then the sum of all the costs never underestimates the total.
- Could be
- Intuition: We only do expensive dequeues after a long run of cheap
enqueues.
- Provided we move all items from In to Out at once, and provided that enqueue items accumulate slowly, this is a fast strategy.
- Key fact: Any series of
operations on a two-stack queue will take time . - Each item is pushed into at most two stacks and popped from at most two stacks.
- Adding up the work done per element across all
operations, we can do at most work.
Dynamic arrays
A dynamic array is the most common way to implement a list of values.
Maintain an array slihtly biggerthan the one you need. When you run out of space, double the array size and copy the elements over.
Most appends to a dynamic array take time
. Infrequently, we do
work to copy all elements from the old array to a new one. Claim: The cost of doing
appends to an initially empty dynamic array is always . The array doubles at sizes
, etc. The very last doubling is at the largest power of two less than
. This is at most . Total work done across all doubling is at most
It's wrong, but useful, to pretend that the cost of an append is
.
Building B-Trees
- You are given a sorted list of
values and a value of . - What's the most efficient way to construct a B-tree of order
holding these values? - Idea 1: Insertthe items into an empty B-tree in sorted order.
- Cost:
, due to the top-down search.
- Cost:
- Idea 2: Since all insertions will happen at the rightmost leaf,
store a pointer to that leaf. Add new values by appending to this leaf,
then doing any necessary splits.
- Cost: The cost of an insert varies based on the shape of the tree.
- If no splits are required, the cost is
. - If one split is required, the cost is
. - If we have to split all the way up, the cost is
- If no splits are required, the cost is
- Using our worst-case cost across
inserts gives a runtime bound of - Claim: The cost of
inserts is always - If is wrong, but useful, to pretend that the cost of an insert is
- Some operations take more time than this
- However, pretending each insert takes time
never underestimates the total amount of work done across all operations.
- Cost: The cost of an insert varies based on the shape of the tree.
- Idea 1: Insertthe items into an empty B-tree in sorted order.
Amortized analysis
The setup
We now have three examples of data strucutres where
individual operations may be slow, but
any series of operations is fast
Giving weak upper bounds on the cost of each operation is not useful for making predictions.
How can we clearly communicate when a situation like this one exists?
Key idea
Backcharge expensive operations to cheaper ones.
- For the real costs, most operations are fast, but we can't get a nice upper bound on any one operation cost.
- For the amortized costs, each operation is still reasonably fast, and all of them are nicely bounded from above.
Assign each operation a (fake) cost called its amortized cost such that, for any series of operations performed, the following is true:
Key intuition: amortized costs shift work backwards from expensive operations onto cheaper ones.
Formalizing this idea
Assigning amortized costs
The approach we've taken so far for assigning amortized costs is called an aggregate analysis.
- Directly compute the maximum possible work done across any sequence of operations, then divide that by the number of operations.
This approach works well here, but it doesn't scale well to more complex data structures.
What if different operations contribute to / clean up messes in different ways?
What if it's not clear what sequence is the worst-case sequence of operations?
In practice, we tend to use a different strategy called the potential method to assign amortized costs.
Potential functions
To assign amortized costs, we'll need to measure how "messy" the data structure is.
For each data structure, we define a potential function
such that is small when the data strucuture is "clean" is large when the data structure is "messy"
Once we've chosen a potential function
, we define the amortized cost of an operation to be: where
is the difference between just after the operation finishes and just before the operation started: Intuitively:
if
increases, the data structure got "messier", and the amortized cost is higher than the real cost. If
decreases, the data strucutre got "cleaner", and the amortized cost is lower than the real cost.
Let's make two assumptions:
is always nonnegative. Thus,
.
For example, for two-stack queues:
height of In stack. Theorem: The amortized cost of anny enqueue or dequeue operation on a two-stack queue is
.
For example, for dynamic arrays
Goal: Choose potential function
such that the amortized cost of an append is . Intuition:
should measure how "messy" the data strucuture is. - Having lots of free slots means there's very little mess.
- Having few free slots means there's a lot of mess.
We can come up with
Theorem: The amortized cost of an append to a dynamic array is
.
For example, building B-trees.
What is the actual cost of appending an element?
- Suppose that we perform splits at
layers in the tree. - Each split takes time
to copy and move keys around. - Total cost:
.
- Suppose that we perform splits at
Goal: Pick a potential function
so that we can offset this cost and make each append cost amortized . Let
be the number of keys on the right spine Each split moves (roughly) half the keys from the split node into a node off the right spine.
Change in potential per split:
. Net
.