Prerequisite knowledge: Time Complexity
This page introduces the basics of amortized complexity.
Introduction
Amortized analysis (Amortized Analysis) is a technique for analyzing the performance of algorithms and dynamic data structures. It focuses not only on the cost of a single operation, but also evaluates the average cost of a sequence of operations to provide a more accurate assessment of overall performance. Amortized analysis does not involve probability; it can only guarantee the average time spent by each operation in the worst case, and cannot determine the average performance of a system. In the worst case, amortized analysis distributes the cost of high-cost operations among low-cost operations, ensuring that the average cost of all operations remains within a reasonable range.
Amortized analysis usually uses three main analysis methods: aggregate analysis, accounting analysis, and potential analysis. Each method emphasizes a different aspect and applies to different scenarios, but their common goal is to optimize the overall performance of a data structure in the worst case by balancing operation costs.
Content
Consider a dynamic array, such as vector in C++, whose initial capacity is . Each time a new element is inserted, if the array is full, its size must be doubled, then the elements in the original array are copied to the new array, and finally the new element is inserted.
Next, using insertion into a dynamic array as an example, we analyze its amortized cost with the three methods of aggregate analysis, accounting analysis, and potential analysis.
Aggregate Analysis
Aggregate analysis (Aggregate Analysis) calculates the total cost of a sequence of operations and averages it over each operation to obtain the amortized time complexity of each operation.
For a dynamic array, we can first identify two key costs of an insertion operation:
- If the array is not full, the cost of the insertion operation is .
- If the array is full, the insertion operation requires expansion. The cost of copying elements after expansion is , where is the current size of the array.
Therefore, to calculate the total cost of n insertion operations, we can divide it into two parts:
- Cost of insertion operations: The direct cost of inserting a new element each time is constant time , so the total cost for operations is .
- Cost of array expansion: Each expansion involves copying the elements of the original array to the new array. These operations occur when the array size is , where is the largest power less than or equal to . The costs of the expansion operations are , whose sum is . This is the sum of a geometric sequence, and its result is .
Therefore, the total insertion cost of the array is , and the amortized cost per operation is . Even in the worst case, the average cost of each insertion operation is still constant time.
Accounting Analysis
The accounting method (Accounting Method) preallocates a fixed amortized cost for each operation to ensure that the total cost of all operations does not exceed the sum of these preallocated costs. The accounting method is similar to a mechanism of prepaying costs, in which low-cost operations store part of their cost to pay for future high-cost operations.
For a dynamic array, a fixed amortized cost can be assigned to each insertion operation to ensure that enough cost has been reserved when expansion is needed.
-
Cost allocation:
- Suppose the actual cost of each insertion operation is , and its amortized cost is set to .
- Of this, is used for the current insertion operation, and is used for a possible future expansion operation.
-
Cost usage:
- When the array is full, an expansion operation is required, with an actual cost of , where is the current size of the array.
- Suppose the number of elements in the array before expansion is . Since the elements in the latter half of the original array have collectively stored units of amortized cost during insertion, this is exactly enough to pay the cost of the expansion operation.
Here is a specific example:
Initial state:
arr = [1, 2, 3, 4] // Initial array
amount = [2, 2, 2, 2] // Prepaid cost for each element
// First expansion: the array is full and needs to be expanded
arr = [1, 2, 3, 4, null, null, null, null] // Array after expansion
amount = [2, 2, 0, 0, 0, 0, 0, 0] // The costs for 3 and 4 are used to pay for the expansion
// Continue inserting new elements until the array is full again
arr = [1, 2, 3, 4, 5, 6, 7, 8] // Continue filling the array
amount = [2, 2, 0, 0, 2, 2, 2, 2] // Newly inserted elements also store prepaid costs
// Second expansion: the array is full again and needs more space
arr = [1, 2, 3, 4, 5, 6, 7, 8, null, null, null, null, null, null, null, null] // Array after expansion
amount = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // The costs for 5, 6, 7, and 8 are used to pay for the expansion
The process above shows that the amortized cost stored by each insertion operation is sufficient to pay for future expansion operations, ensuring that the amortized cost of each operation remains .
Potential Analysis
The potential method (Potential Method) defines a potential function, usually denoted by , to measure the potential energy of a data structure, namely the resources reserved in the system state that can be used to pay for future high-cost operations. Changes in potential balance the total cost of an operation sequence, ensuring that the amortized cost of the entire algorithm remains within a reasonable range.
Principle
First, define the state as the state of the data structure at a certain point in time. This state may include the number of elements, capacity, pointers, and other information. Define the initial state as , the state before any operation has been performed.
Next, define a potential function to measure the potential of data structure state . It satisfies the following two properties:
- Initial potential: In the initial data structure state , the potential is .
- Non-negativity: For any state , the potential satisfies .
For each operation, its amortized cost is defined as:
Here, is the actual cost of the operation, and and represent the states of the data structure before and after the operation, respectively. This formula shows that the amortized cost equals the actual cost plus the change in potential. If an operation increases the potential (that is, ), the amortized cost increases; if an operation consumes potential (that is, ), the amortized cost decreases.
We can use the potential function to analyze the total cost of a sequence of operations. Let be the sequence of states produced starting from initial state after operations, and let be the actual cost of the -th operation. Then the amortized cost of the -th operation, , is:
Therefore, the total time cost of operations is:
Since , the upper bound on the total time cost is:
Therefore, if , then is an upper bound on the amortized complexity.
Example: Analysis of Dynamic Array Expansion
Taking insertion into a dynamic array vector as an example, define the following potential function :
Here, is the number of elements in the array, and is the current capacity of the array. This potential function reflects the amount of available space remaining in the array, namely the difference between the current capacity and the amount of space actually used.
-
Insertion operation (no expansion required):
- Operation cost: , because only one element needs to be inserted.
- Change in potential: After insertion, the number of elements increases by 1, and the potential increases by .
- Amortized cost:
-
Insertion operation (triggering expansion):
- Suppose the current capacity is . Inserting a new element triggers expansion, and the new capacity becomes .
- Operation cost: , because all elements need to be copied to the new array and the new element needs to be inserted.
- Change in potential: After expansion, the capacity increases and the potential decreases by an amount of .
- Amortized cost:
The analysis above shows that although the actual cost of an expansion operation is high, the overall amortized cost remains at the constant level due to the design of the potential function.
Extended Example: Stack Operations
Stack operations are one of the classic application scenarios of amortized analysis. Suppose stack S supports the following three operations:
| Operation | Description | Actual cost |
|---|---|---|
S.push(x) | Push element x onto the stack | |
S.pop() | Pop the top element of the stack | |
S.multi-pop(k) | Pop k elements from the top of the stack |
We analyze the amortized cost of these stack operations using aggregate analysis, accounting analysis, and potential analysis.
Aggregate Analysis Method
Aggregate analysis calculates the total cost of all operations and averages it over each operation to obtain the amortized cost.
- For
push(x)operations, each operation costs , so the total cost is . - For
pop()operations, each operation costs , so the total cost is . - For
multi-pop(k)operations, although the actual cost of each operation is , the number of elements popped by these operations cannot exceed the number of elements previously added bypush(x), so the total cost is still bounded by .
Since the total number of operations , the total cost is , and the amortized cost per operation is .
Accounting Analysis Method
Accounting analysis reserves part of the cost for each push(x) operation to pay for possible future pop() or multi-pop(k) operations.
S.push(x): Suppose the amortized cost of eachpush(x)operation is . unit is used for the current operation, and another unit is stored as credit to pay for a futurepop()ormulti-pop(k)operation.S.pop(): The actual cost is , but because previouspush(x)operations have prepaid unit of credit for it, the amortized cost is .S.multi-pop(k): The actual cost of popping each element is , which can be paid by the credit prepaid by the previouspush(x)operation for that element, so the amortized cost is .
The analysis above shows that the credit prepaid by a push operation is sufficient to pay for the future pop operation of that element, so the amortized cost of each operation is .
Potential Analysis Method
Potential analysis defines a potential function to measure the state of the stack and uses changes in potential to balance operation costs.
- Potential function: Let be the number of elements in the stack, namely . Each element contributes unit of potential.
S.push(x): Eachpush(x)operation increases the number of elements in the stack, increasing the potential by . Therefore, the amortized cost is .S.pop(): Eachpop()operation decreases the number of elements in the stack, decreasing the potential by . Therefore, the amortized cost is .S.multi-pop(k): Amulti-pop(k)operation pops elements, decreasing the potential by . Therefore, the amortized cost is .
With the potential function designed as above, the amortized cost of a push(x) operation is , while the amortized costs of pop() and multi-pop(k) operations are . Therefore, the amortized cost of all stack operations is .