Arrays & Strings
The substrate for almost everything else: indexing, in-place tricks, and the cost of the operations you take for granted.
Arrays are the default shape of interview input because they are simple to draw and brutal about cost. You can jump to nums[i] instantly, but you pay for anything that forces elements to move.
Strings look similar, but in Python they are immutable. That one fact changes how you build and edit them: repeated concatenation copies; collecting pieces and calling "".join(...) does one final copy.
The core habit is to separate reading from writing. Often one index scans the original data while another writes the compacted result in place.
The mental model
An array stores elements in contiguous slots. If the runtime knows the address of slot 0 and the size of each element reference, it can compute the address of slot i directly. That is why indexing is O(1).
Python’s list is a dynamic array: when it fills up, Python allocates a larger backing array and copies the old references into it. One unlucky append can be O(n), but because the array grows by extra capacity instead of one slot at a time, a long sequence of appends averages to amortized O(1) per append.
nums = []
for x in range(5):
nums.append(x) # amortized O(1)
third = nums[2] # O(1) indexing
nums.insert(1, 99) # O(n): shifts everything from index 1 onward
nums.pop(1) # O(n): shifts elements left
For strings, treat each edit as producing a new string:
# Bad for many pieces: each += copies the growing prefix.
out = ""
for part in parts:
out += part
# Good: collect references, then copy once.
out = "".join(parts)
For in-place array edits, keep a write index:
def keep_positive(nums):
write = 0
for x in nums:
if x > 0:
nums[write] = x
write += 1
return nums[:write]
The read loop sees every element once. The write pointer marks the next slot in the compacted prefix.
Pattern recognition
Variations
Worked problems
Try to name the cost before opening each solution. The code is short because the pattern is the point.
Remove Duplicates from Sorted Array
Given a sorted list of numbers, rewrite it so each distinct value appears once at the front. Return the count of distinct values; anything after that prefix does not matter.
Approach. Sorted order groups duplicates together, so the first occurrence of a value is exactly the one that differs from the last value we kept. Let write be the length of the unique prefix. Scan from left to right; whenever nums[i] changes from nums[write - 1], copy it into nums[write] and grow the prefix.
Show solution
def remove_duplicates(nums):
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[write - 1]:
nums[write] = nums[read]
write += 1
return writeComplexity. O(n) time, O(1) extra space. Each element is read once, and the array is rewritten in place.
Move Zeroes
Rearrange a list so all nonzero values keep their original order at the front and all zeroes move to the end. Do it in place.
Approach. First compact the nonzero values with a write index. After that, every position from write to the end must be zero. This avoids repeated swaps and makes the stability requirement automatic.
Show solution
def move_zeroes(nums):
write = 0
for x in nums:
if x != 0:
nums[write] = x
write += 1
for i in range(write, len(nums)):
nums[i] = 0Complexity. O(n) time, O(1) extra space. There are two linear passes, which still collapse to O(n).
Product of Array Except Self
For each index, return the product of every number except the one at that index. Do not use division.
Approach. The answer at i is product of everything to the left times product of everything to the right. Fill ans[i] with the left product during a left-to-right pass. Then walk right-to-left while carrying the right product and multiply it into ans[i].
Show solution
def product_except_self(nums):
ans = [1] * len(nums)
left = 1
for i, x in enumerate(nums):
ans[i] = left
left *= x
right = 1
for i in range(len(nums) - 1, -1, -1):
ans[i] *= right
right *= nums[i]
return ansComplexity. O(n) time, O(1) extra space if the output array is not counted. The two running products replace separate prefix and suffix arrays.
The classic pitfall
For strings, the same rule is hiding in a different form: repeated concatenation moves the whole growing prefix again and again. Build a list of pieces, then join once.