Amortized analysis is a powerful technique used to analyze the time complexity of algorithms, especially when an occasional expensive operation is offset by many cheap ones. Let’s explore this concept step-by-step with a concrete example.
Understanding the Problem
Suppose we have a fully filled array of elements, and we want to insert one more element. The typical algorithm to do this involves:
- Creating a new array of size
.
- Copying
elements from the old array into the new one.
- Inserting the new element.
The complexity of copying elements is
, and inserting a new element is
. Therefore, the total time complexity is:
Can We Improve It?
The question arises: Can we design a data structure where inserting elements is always and retrieving any element is also
?
The answer is yes, and one such structure is Java’s ArrayList. It offers:
- Infinite logical capacity.
- Element insertion complexity:
(amortized).
- Single element retrieval complexity:
.
But how is this achieved?
Internal Working of ArrayList
Internally, ArrayList
uses an array. When the internal array is full, it creates a new array of twice the size. Then:
- Copies all existing elements into the new array.
- Inserts the new element.
Example: Inserting 7 into [5, 3, 1, 9]
triggers resizing:
- New array is created with size
(if current size is
).
- Old elements are copied:
time.
- New element is inserted:
.
We call such insertions boundary insertions—those that trigger array resizing.
So:
- Non-boundary insertions:
- Boundary insertions:
Need for Amortized Analysis
Using worst-case time complexity () for each insertion would be too pessimistic because most insertions are cheap. Instead, we use amortized analysis, which averages the cost over a sequence of operations.
Amortized analysis answers:
What is the average cost of each operation in a sequence of operations?
Aggregation Method (Most Common Approach)
Steps:
- Compute total time for a sequence of
insertions (including cheap and expensive ones).
- Use the formula:
Let’s understand this with an example: inserting elements from to
into an initially empty
ArrayList
.
Visualizing Resizing and Copy Operations
Let’s build a table:
Insertion Index | Boundary Element? | Copy Operations |
0 | Yes | 0 |
1 | Yes | ![]() |
2 | Yes | ![]() |
4 | Yes | ![]() |
8 | Yes | ![]() |
16 | Yes | ![]() |
Each time the size reaches a power of 2, a resize (boundary insertion) is triggered. So:
- Total copies due to resizing:
- Number of insertions:
(from 0 to 16)
Using the formula for geometric series:
Since , then
. So total copy operations =
.
Also, there are insertions, each with
insertion cost, which totals to
.
Total Complexity of n Insertions
Total time:
Amortized Cost Per Insertion
Using the formula:
Summary and Final Notes
- Worst-case cost of a single insertion:
- Best-case cost of a single insertion:
- Amortized cost per insertion (over a sequence):
That’s why ArrayList
can offer fast average-time insertions, even though some insertions take linear time.