Python Tutorial
Lists and Arrays
The foundational linear structure: contiguous, index-addressable storage for a sequence of elements.
Arrays vs Python Lists
A classic array is a fixed-size block of memory holding elements of one type, addressed by index. A Python list is a dynamic array: it grows automatically and can hold mixed types. Both give O(1) access by index because the address of element i is computed directly.
Core Operations and Their Cost
| Operation | Example | Time |
|---|---|---|
| Access by index | a[i] | O(1) |
| Update by index | a[i] = x | O(1) |
| Append | a.append(x) | O(1) amortized |
| Insert / delete at front | a.insert(0, x) | O(n) |
| Search by value | x in a | O(n) |
nums = [10, 20, 30, 40]
print(nums[2]) # 30 -> O(1)
nums.append(50) # [10,20,30,40,50] -> amortized O(1)
nums.insert(0, 5) # shifts everything right -> O(n)
nums.remove(30) # find + shift -> O(n)
print(nums) # [5, 10, 20, 40, 50]Why Append Is "Amortized" O(1)
A dynamic array over-allocates memory. Most appends just drop the value into a free slot (O(1)). Occasionally the array is full and must be copied to a larger block (O(n)), but that cost is spread across many cheap appends, averaging out to O(1).
Traversal and Common Patterns
nums = [4, 8, 15, 16, 23, 42]
# find max in one pass -> O(n)
largest = nums[0]
for x in nums[1:]:
if x > largest:
largest = x
print(largest) # 42
# two-pointer reverse in place -> O(n), O(1) extra space
i, j = 0, len(nums) - 1
while i < j:
nums[i], nums[j] = nums[j], nums[i]
i += 1
j -= 1
print(nums) # [42, 23, 16, 15, 8, 4]Typed Arrays and NumPy
When you need many numbers stored compactly, Python's array module or NumPy arrays use far less memory than a list of Python objects and run vectorized operations in C speed.
from array import array
scores = array("i", [90, 85, 77]) # 'i' = signed int, one type only
import numpy as np
a = np.array([1, 2, 3, 4])
print((a * 2).sum()) # 20, computed in a fast C loop2-D Arrays (Matrices)
grid = [[1, 2, 3],
[4, 5, 6]]
print(grid[1][2]) # 6
# WRONG: [[0]*3]*2 makes 2 references to the SAME row
# RIGHT: build independent rows
matrix = [[0] * 3 for _ in range(2)]
matrix[0][0] = 9
print(matrix) # [[9, 0, 0], [0, 0, 0]][[0]*3]*2 is a classic bug: all rows point to one list, so editing one edits all. Use a comprehension instead.
Best Practices
- Use a list for ordered, index-based data; a set/dict when you need fast membership.
- Avoid inserting/deleting at the front of a big list — use
collections.deque. - Use NumPy for large numeric arrays and math.
- Build 2-D lists with comprehensions to keep rows independent.
Try It Yourself
Exercise 1: Reverse a list in place using two pointers (no reverse()).
Show solution
a = [1, 2, 3, 4, 5]
i, j = 0, len(a) - 1
while i < j:
a[i], a[j] = a[j], a[i]
i += 1; j -= 1
print(a) # [5, 4, 3, 2, 1]Exercise 2: Why is inserting at the front of a list O(n)?
Show solution
Every existing element must shift one position to the right to make room.
📘 Real-World Deep Dive
Knowing <strong>DSA Lists Arrays (algorithms & data structures)</strong> well is what turns algorithms & data structures from a curiosity into a daily tool — you'll reach for it in nearly every real project.
Real-Life Scenario
An end-to-end usage of DSA Lists Arrays that you'd actually see in a data pipeline or analytics notebook.
Real-Life Example
class DynArray:
def __init__(self, capacity=4):
self.cap, self.size, self.data = capacity, 0, [None] * capacity
def push(self, v):
if self.size == self.cap:
self.cap *= 2
self.data += [None] * self.cap
self.data[self.size] = v; self.size += 1
def __repr__(self):
return repr(self.data[:self.size])
a = DynArray()
for i in range(6): a.push(i)
print(a)Expected Output
(see source)Common mistakes
- Off-by-one errors in binary-search: the standard idiom is
while lo <= hiwithmid = (lo + hi) // 2. - Recursive algorithms blow the stack for n > ~10⁴; convert to iterative with an explicit stack.
- Comparison algorithms (
sorted(iterable)) are stable by default in Python — surprising for Java/C++ users. - Treating DSA Lists Arrays as a black box without reading the docs — the API has subtle defaults that bite when you scale.
🚀 Performance & Best Practices
- Use
bisect.bisect_left/bisect_rightinstead of writing your own binary search. - Convert a sorted search into a tuple-access pattern with
numpy.searchsortedfor huge arrays. - Use
heapqfor priority queues instead of maintaining a sorted list manually. - When working with algorithms & data structures, prefer vectorised / batched operations over Python loops.
🧪 Try It Yourself
- Reproduce the snippet on a representative slice of your own data.
- Profile the snippet with
cProfileortimeitand find the single biggest improvement. - Generalise the snippet into a small, reusable function you can drop into future projects.