Rilasciato · in miglioramento
Guida a Analisi della complessità · 4/6
Per ora questo capitolo è disponibile solo in inglese.
This chapter compares the growth rates you meet most often, uses amortized analysis to explain why appending to a dynamic array is "O(1) on average", and covers space complexity including the recursion stack.
| Class | Name | Typical example | When n doubles |
|---|---|---|---|
O(1) | Constant | Array indexing, hash lookup (average) | Unchanged |
O(log n) | Logarithmic | Binary search, balanced tree lookup | One more step |
O(n) | Linear | Linear search, summing | 2x |
O(n log n) | Linearithmic | Merge sort, heapsort | A bit more than 2x |
O(n²) | Quadratic | All pairs, insertion sort (worst) | 4x |
O(2ⁿ) | Exponential | Listing all subsets | Squared (2x per extra element) |
O(n!) | Factorial | Listing all permutations | Explodes |
Rough operation counts make the gaps obvious:
| n | log₂ n | n log₂ n | n² | 2ⁿ | n! |
|---|---|---|---|---|---|
| 10 | 3.3 | 33 | 100 | 1,024 | about 3.6 × 10⁶ |
| 100 | 6.6 | 664 | 10⁴ | about 1.3 × 10³⁰ | about 9.3 × 10¹⁵⁷ |
| 1,000 | 10 | about 10⁴ | 10⁶ | about 10³⁰¹ | out of reach |
| 10⁶ | 20 | about 2 × 10⁷ | 10¹² | out of reach | out of reach |
As a rule of thumb, compiled languages do around 10⁸ simple operations per second and Python around 10⁷. At n = 10⁵ an O(n²) algorithm needs 10¹⁰ operations, tens of seconds even in C++. That estimate is how you read the input limits of a coding problem:
| Input size n | Roughly feasible in one second |
|---|---|
| about 10 | O(n!), |
O(2ⁿ · n)| about 20 | O(2ⁿ) |
| about 500 | O(n³) |
| about 5,000 | O(n²) |
| 10⁵ to 10⁶ | O(n log n), O(n) |
| larger | O(log n), O(1) |
Python's list, C++'s std::vector and Java's ArrayList allocate a bigger array and copy every element when the current one is full. That one append costs O(n), but if capacity grows by a constant factor, n appends cost O(n) in total, so each costs O(1) amortized.
Doubling the capacity copies 1 + 2 + 4 + ... elements; the last term is smaller than n, so the sum is always below 2n. Growing by a fixed amount instead (say 10 slots) copies 10 + 20 + 30 + ... elements, which adds up to O(n²).
def total_copies(n, grow):
capacity, size, copies = 1, 0, 0
for _ in range(n):
if size == capacity:
copies += size # copy existing elements to the new array
capacity = grow(capacity)
size += 1
return copies
for n in (1_000, 10_000, 100_000):
doubling = total_copies(n, lambda c: c * 2)
plus_ten = total_copies(n, lambda c: c + 10)
print(n, doubling, plus_ten)
# 1000 1023 49600
# 10000 16383 4996000
# 100000 131071 499960000Doubling stays proportional to n (under 2n), while growing by ten multiplies the copies by about 100 when n grows tenfold. Unlike average-case analysis, amortized analysis involves no probability: it is a guarantee over the whole sequence of operations. CPython's list grows by about 1.125x plus a small constant, which still gives amortized O(1). You can watch the allocation change only occasionally:
import sys
items, last = [], sys.getsizeof([])
for i in range(40):
items.append(i)
size = sys.getsizeof(items)
if size != last:
print(len(items), size) # only lengths where a reallocation happened
last = sizeTotal space includes the input; auxiliary space excludes it. An "in-place" algorithm usually means O(1) or O(log n) auxiliary space. Every recursive call uses a stack frame, so recursion depth is a space cost.
def sum_recursive(items, i=0): # time O(n), stack space O(n)
if i == len(items):
return 0
return items[i] + sum_recursive(items, i + 1)
def sum_iterative(items): # time O(n), auxiliary space O(1)
total = 0
for x in items:
total += x
return total
print(sum_iterative(range(100_000))) # 4999950000
# sum_recursive(list(range(100_000))) exceeds the default recursion limit (about 1000): RecursionError| Algorithm | Time (worst) | Auxiliary space |
|---|---|---|
| Linear search | O(n) | O(1) |
| Binary search (loop) | O(log n) | O(1) |
| Binary search (recursive) | O(log n) | O(log n) |
| Merge sort | O(n log n) | O(n) |
| Quicksort | O(n²), average O(n log n) | O(log n) on average |
| Two-sum (hash map) | O(n) average | O(n) |
append amortized O(1); growing by a fixed amount makes it O(n²) overall.
0 commenti
Accedi · Accedi per lasciare un commento.
Scrivi tu il primo commento.