Understanding the efficiency of algorithms and data structures is crucial in computer science. Complexity analysis helps us predict how an algorithm will perform as the input size grows. We primarily focus on two aspects: Time Complexity and Space Complexity.
Time complexity measures the amount of time an algorithm takes to run as a function of the length of the input. It'''s not about measuring the exact time (which depends on the hardware, compiler, etc.), but rather about understanding the growth rate of the execution time.
Big O Notation (O): This is the most common notation used to describe the upper bound of an algorithm'''s running time – its worst-case scenario. It provides an asymptotic analysis of how the runtime scales with input size.
Note: When analyzing complexity, we usually care about the worst-case scenario (Big O), but sometimes average-case (Theta, Θ) and best-case (Omega, Ω) scenarios are also considered.
Space complexity measures the total amount of memory space an algorithm or data structure uses relative to the size of the input. It includes both the space used by the input itself and any auxiliary space used by the algorithm during execution (e.g., for variables, data structures).
def find_sum(numbers):
total = 0 # O(1) space for 'total'
for num in numbers: # Loop runs n times, where n is len(numbers)
total += num # O(1) operation
return total
# Time Complexity: O(n) because the loop iterates through each element once.
# Space Complexity: O(1) (if we don'''t count input space) because only one variable 'total' is used for computation regardless of input size.
# If input space is counted, it would be O(n) for storing 'numbers'. Typically, auxiliary space is what we focus on for space complexity analysis beyond input.
| Data Structure | Avg. Access | Avg. Search | Avg. Insertion | Avg. Deletion | Worst Space |
|---|---|---|---|---|---|
| Array/List (Python) | O(1) | O(n) | O(n)* (append O(1) amortized) | O(n) | O(n) |
| Stack (Python List) | O(1) (peek) | N/A (search not typical) | O(1) (push) | O(1) (pop) | O(n) |
| Queue (collections.deque) | O(1) (peek) | N/A | O(1) (enqueue) | O(1) (dequeue) | O(n) |
| Singly Linked List | O(n) | O(n) | O(1) (head), O(n) (tail/middle) | O(1) (head), O(n) (tail/middle) | O(n) |
| Hash Table (Python Dict) | N/A (use search) | O(1) | O(1) | O(1) | O(n) |
| Binary Search Tree (BST) | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
| Balanced BST (AVL, Red-Black) | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
| Graph (Adjacency List) | N/A | O(V+E) (BFS/DFS) | O(1) (add edge/vertex) | O(V+E) (remove edge), O(V+E) (remove vertex) | O(V+E) |
| Graph (Adjacency Matrix) | N/A | O(V^2) (BFS/DFS) | O(1) (add edge if matrix exists), O(V^2) (add vertex) | O(1) (remove edge), O(V^2) (remove vertex) | O(V^2) |
*Python list insertion/deletion at arbitrary positions is O(n). append is O(1) amortized.
Choosing the right data structure and algorithm based on their complexity is key to writing efficient and scalable software.