LESSON 23 · INTERMEDIATE4 min read

Amortized Complexity: The Accounting Method & Dynamic Array Resizing

Master amortized analysis. Understand why ArrayList.add() is O(1) amortized despite occasional O(N) resizing, and model costs using the Aggregate and Banker's methods.

OpenJDK 21+ (LTS)Free to readModule 6 of 15
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Explain Amortized Analysis and distinguish it from Average-Case complexity.
  • Prove that dynamic array doubling incurs O(1) amortized cost per insertion.
  • Apply the Aggregate Method and Banker's (Accounting) Method to amortized algorithms.

01The Concept of Amortization

In real-world software, certain data structure operations are cheap most of the time, but occasionally trigger an expensive maintenance operation. If we only evaluated worst-case complexity, we would get an overly pessimistic, misleading view of the data structure's true performance.

Amortized Analysis evaluates the average cost of an operation over a sequence of N operations. Unlike probabilistic average-case analysis (which assumes an input distribution), amortized analysis provides a strict mathematical guarantee: over any sequence of N operations, the total cost will never exceed N * AmortizedCost, even in the worst case.

notes.txtPlain text
+---------------------------------------------------+|           Dynamic Array Resizing Timeline         |+---------------------------------------------------+| Inserts 1-4:  Cost 1 each (Total: 4)              || Insert 5:     Resize array 4 -> 8, Copy 4 items,  ||               Cost = 4 + 1 = 5                    || Inserts 6-8:  Cost 1 each (Total: 3)              || Insert 9:     Resize array 8 -> 16, Copy 8 items, ||               Cost = 8 + 1 = 9                    |+---------------------------------------------------+

02The Aggregate Method Proof

Consider a dynamic array that doubles its capacity whenever it fills. Let's calculate the total cost of inserting N elements starting from an array of size 1 using the Aggregate Method.

Each element requires 1 operation for standard insertion: that contributes N operations total. In addition, copying occurs when the array size reaches powers of 2 (1, 2, 4, 8, ..., up to N). The total copy cost is: 1 + 2 + 4 + 8 + ... + N/2 + N. This is a geometric series that sums to strictly less than 2N!

Therefore, the total cost for N insertions is N (insertions) + 2N (copies) = 3N operations. Dividing the total cost 3N by N insertions yields exactly 3 operations per insertion. Asymptotically, 3 is O(1). Thus, inserting into a dynamic array is O(1) Amortized Time!

notes.txtPlain text
Total Operations = N (direct writes) + (1 + 2 + 4 + ... + N) (copies)Total Operations < N + 2N = 3NAmortized Cost per Insertion = 3N / N = 3 = O(1) Amortized!

03The Banker's (Accounting) Method

Another intuitive way to model amortization is the Banker's (Accounting) Method. Imagine every insertion costs $1 of actual work, but we charge the user a fictitious fee of $3 per insertion.

$1 pays for the immediate insertion of the element. We store the remaining $2 as 'bank credit' associated with that element. When the array doubles later, half the elements have $2 in bank credits ($1 pays to move itself, and $1 pays to move an older element that has already exhausted its credit).

Because the bank balance never drops below zero, the $3 charge per insertion completely covers all future expensive copy operations, proving the O(1) amortized bound.

notes.txtPlain text
+---------------------------------------------------+|              Banker's Accounting Method           |+---------------------------------------------------+| Charge $3 per element:                            ||   - $1: Pays for current write                    ||   - $1: Saved to copy this element during resize  ||   - $1: Saved to copy a partner element           || Result: Resize costs are 100% pre-paid by credit! |+---------------------------------------------------+

04Why This Matters in Production

Understanding amortized complexity is crucial for low-latency systems. While ArrayList.add() is O(1) amortized, an individual insertion that triggers an array resize takes O(N) time. For an array with 10 million elements, that single resize pauses the thread to allocate and copy 10 million pointers.

In high-frequency trading or real-time audio systems where latency jitter must be strictly bounded, sudden O(N) resize spikes can violate SLAs. To eliminate resizing latency spikes entirely, always pre-allocate the capacity upfront with new ArrayList<>(expectedSize) or use circular ring buffers.

LET IT CLICK

The ideas worth keeping.

  • Amortized analysis guarantees average cost over a sequence of operations in the worst case.
  • ArrayList.add() is O(1) amortized because doubling forms a convergent geometric series.
  • Pre-allocate collection capacity to eliminate latency spikes and memory reallocation overhead.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Exercise 1: Amortized Doubling vs Fixed Expansion

What happens to the amortized insertion complexity if a dynamic array expands by adding a fixed 100 elements each time instead of multiplying by a factor of 1.5x or 2x?

your_turn_1.java☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.javaJava
Amortized Cost becomes O(N) per insertion! Adding fixed amounts requires N / 100 resizes. The copy cost becomes 100 + 200 + 300 + ... + N, summing to O(N^2) total work. O(N^2) / N = O(N) amortized.

Multiplicative scaling (geometric growth) is mathematically required to achieve O(1) amortized complexity; additive growth degrades to O(N).

02

Exercise 2: Pre-allocated Capacity Optimization

Write a benchmark comparing the time to add 1,000,000 elements to an un-sized ArrayList versus an ArrayList initialized with capacity 1,000,000.

your_turn_2.java☕Java
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.javaJava
var preAllocated = new ArrayList<Integer>(1_000_000);for (int i = 0; i < 1_000_000; i++) preAllocated.add(i);// Pre-allocated finishes in ~5ms; un-sized takes ~25ms due to 25+ re-allocations.

Pre-allocating removes all resize overhead and intermediate garbage array allocations.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What is the difference between Amortized Complexity and Average-Case Complexity?
02Why must a dynamic array grow geometrically (e.g. 1.5x or 2x) rather than additively (+50 items) to achieve O(1) amortized insertion?
03In what production scenario is an O(1) amortized operation undesirable?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Exercise 1: Amortized Doubling vs Fixed Expansion

What happens to the amortized insertion complexity if a dynamic array expands by adding a fixed 100 elements each time instead of multiplying by a factor of 1.5x or 2x?

EXPECTED OUTPUT
Build successful. (Java 15)
[Process exited normally]
Main.javaJava
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.