Skip to main content

Arrays and Strings

Learning Objectives

By the end of this page you should be able to:

  • Explain how arrays are laid out in memory and why that makes indexing O(1)
  • Distinguish static arrays from dynamic arrays (like Python lists) and explain amortized O(1) append
  • State the time complexity of insertion, deletion, search, and access for arrays and strings
  • Apply the two-pointer and sliding window patterns to solve array/string problems in O(n)
  • Explain why strings are immutable in languages like Python and Java, and what that costs you
  • Identify when an array is the wrong data structure for a problem

Quick Answer

An array is a block of memory holding elements of the same type, stored back-to-back, so any element can be reached instantly by calculating its address from an index. A string is, under the hood, just an array of characters. Arrays matter because they're the fastest possible structure for random access (O(1)) and they underpin nearly every other data structure — hash tables, heaps, and dynamic arrays like Python's list are all built on top of a raw array. The tradeoff is rigidity: inserting or deleting in the middle means shifting every element after it, which costs O(n). Most "array problems" in interviews and coursework are really about working around that shifting cost using techniques like two pointers or sliding windows instead of naive nested loops.

Core Content

Memory Layout: Why Arrays Are Fast

The whole reason arrays exist as a concept is contiguous memory. When you declare arr = [10, 20, 30, 40, 50], the runtime reserves one continuous block of memory and computes the address of any element with simple arithmetic:

address(arr[i]) = base_address + (i * size_of_element)

That formula is why arr[3] takes the same amount of time whether the array has 10 elements or 10 million — there's no searching involved, just one multiplication and one addition. This is the single most important fact about arrays: O(1) random access comes from contiguous memory, not from indexing syntax. A linked list can also give you node.value, but it can't give you O(1) access at position i, because it has to walk the chain from the head.

Static Arrays vs Dynamic Arrays

A static array (raw C-style array, or Java's int[]) has a fixed size decided at creation. You cannot grow it — resizing means allocating a brand-new block and copying everything over.

A dynamic array (Python list, Java ArrayList, C++ vector) looks resizable because the language hides the reallocation from you. Internally, it over-allocates: when the backing array fills up, it allocates a new block roughly 1.5x–2x the size and copies all existing elements into it.

# Dynamic array growth (conceptually what Python's list does internally)
arr = []
for i in range(6):
arr.append(i)
# Under the hood: if capacity is full, allocate new block (bigger),
# copy all old elements, then add the new one.

That occasional expensive copy (O(n)) is why we say append is amortized O(1) — most appends are O(1), and the rare O(n) resize gets "spread" across all the cheap ones when you average over many operations.

Array Operations and Complexity

arr = [10, 20, 30, 40, 50]

print(arr[0]) # Access by index -> O(1)
arr[2] = 35 # Update by index -> O(1)
arr.append(60) # Add to end -> amortized O(1)
arr.insert(2, 99) # Insert in middle -> O(n), shifts everything after index 2
arr.pop() # Remove from end -> O(1)
arr.remove(99) # Remove by value -> O(n), search + shift
99 in arr # Linear search -> O(n)
OperationStatic ArrayDynamic Array (Python list)Why
Access by indexO(1)O(1)Direct address calculation
Update by indexO(1)O(1)Same reason
Append at endN/A (fixed size)Amortized O(1)Occasional resize copy is spread out
Insert at arbitrary indexO(n)O(n)Every following element shifts right
Delete at arbitrary indexO(n)O(n)Every following element shifts left
Delete/insert at endO(1)O(1)Nothing to shift
Search by valueO(n)O(n)Must check elements one by one
Search in sorted arrayO(log n) with binary searchO(log n)Halve the search space each step

Strings as Character Arrays

Strings behave like arrays of characters, but with one crucial twist: in Python, Java, JavaScript, and C#, strings are immutable. You cannot change a character in place.

s = "Hello, World!"
print(s[0]) # 'H' -> O(1) access, same as arrays
print(s[0:5]) # 'Hello' -> slicing creates a NEW string, O(k) where k = slice length
print(s.upper()) # creates a new string entirely, doesn't modify s

# This is illegal in Python:
# s[0] = 'J' -> TypeError: 'str' object does not support item assignment

# To "modify" a string you must build a new one:
s = 'J' + s[1:] # O(n) — copies the whole thing

If you're doing heavy string building in a loop, repeated concatenation (result += char) is O(n) per operation in the worst case because each += may create a new string and copy everything, making a naive loop O(n²) overall. The fix is to collect pieces in a list and join once:

# Bad: O(n^2) in the worst case
result = ""
for c in some_chars:
result += c

# Good: O(n)
pieces = []
for c in some_chars:
pieces.append(c)
result = "".join(pieces)

Pattern 1: Two Pointers

Two pointers replace nested loops (O(n²)) with a single pass (O(n)) by moving two indices toward each other or in tandem, exploiting some structure (often sortedness) in the data.

def is_palindrome(s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True

print(is_palindrome("racecar")) # True

Another classic: given a sorted array, find two numbers that sum to a target, in O(n) instead of the O(n²) brute-force pair check.

def two_sum_sorted(arr, target):
left, right = 0, len(arr) - 1
while left < right:
current = arr[left] + arr[right]
if current == target:
return (left, right)
elif current < target:
left += 1 # need a bigger sum
else:
right -= 1 # need a smaller sum
return None

Pattern 2: Sliding Window

A sliding window keeps a moving subrange [left, right] and expands/shrinks it, avoiding recomputation from scratch. It's the go-to pattern for "longest/shortest substring/subarray satisfying condition X."

def longest_unique_substring(s: str) -> int:
seen = set()
left = 0
best = 0
for right in range(len(s)):
while s[right] in seen:
seen.remove(s[left])
left += 1
seen.add(s[right])
best = max(best, right - left + 1)
return best

print(longest_unique_substring("abcabcbb")) # 3 ("abc")

Each character enters and leaves the window at most once, so even though there's a while loop nested inside a for loop, the total work is O(n), not O(n²).

Real-World Examples

  • Image processing: a bitmap is a 2D array of pixel values; contiguous memory layout is exactly why image libraries can process rows so fast (cache-friendly sequential access).
  • Text editors: search/replace features rely on string scanning algorithms (like KMP) that are O(n + m) rather than naive O(n·m) matching.
  • Database engines: fixed-width row storage uses array-style offset math (base + row_number * row_size) to jump straight to a record without scanning the table.
  • Network packet buffers: fixed-size arrays (ring buffers) are used because their size is bounded and predictable, avoiding the overhead of dynamic resizing in a hot path.

Key Terms

TermDefinitionContext/Related
Contiguous memoryMemory addresses allocated back-to-back with no gapsEnables O(1) index access in arrays
Static arrayFixed-size array whose length is set at creation and cannot changeContrast with dynamic array
Dynamic arrayAn array-backed structure that automatically resizes (e.g., Python list)Uses amortized O(1) append
Amortized complexityAverage cost per operation over a sequence, even if some individual operations are expensiveExplains why append is "O(1)" despite occasional O(n) resizes
Index / subscriptThe integer offset used to access an element in an arrayarr[i]
Immutable stringA string whose contents cannot be changed after creationPython, Java, JS strings; forces new-object creation on "modification"
Two pointersA technique using two indices moving through a structure to avoid nested loopsPalindrome check, sorted two-sum
Sliding windowA technique that maintains a variable-size subrange and expands/shrinks it incrementallyLongest substring problems
In-place operationAn operation that modifies the existing array without allocating a new onearr.reverse(), arr.sort()
Time complexityA description of how an algorithm's runtime grows with input sizeBig-O notation

Common Mistakes

Misconception 1: "Arrays and lists are always the same thing." Why it's wrong: In low-level languages (C, Java's raw arrays), an array is a fixed-size, contiguous block you cannot resize. In Python, the built-in list is a dynamic array — it's resizable and can hold mixed types, which is a different structure with different guarantees, even though the syntax arr[i] looks identical. Correct explanation: Call a fixed-size, single-type block a "static array" and Python's list (or Java's ArrayList) a "dynamic array." They share O(1) indexed access but differ in resizing behavior and, in Python's case, type flexibility.

Misconception 2: "Inserting into an array is O(1) because I just assign a value." Why it's wrong: Students confuse arr[i] = x (an update to an existing slot, genuinely O(1)) with "inserting a new element at position i" (which requires shifting every element from i onward one slot to the right to make room, an O(n) operation). Correct explanation: Updating an existing index is O(1). Inserting a new element in the middle of an array is O(n) because of the shift. Only inserting/removing at the very end avoids the shift.

Misconception 3: "Since strings act like arrays, I can modify them in place with s[i] = x." Why it's wrong: In Python, Java, JavaScript, and C#, strings are immutable — the language deliberately disallows in-place mutation so strings can be safely shared, hashed, and cached (e.g., used as dictionary keys) without fear of being changed underneath you. Correct explanation: Any "modification" of a string actually creates a brand-new string object. If you need frequent in-place character edits, convert to a mutable structure first (e.g., a list of characters in Python, or StringBuilder in Java), edit that, then rejoin/convert back.

Comparison and Connections

ConceptArrayLinked List
Memory layoutContiguous blockScattered nodes connected by pointers
Access by indexO(1)O(n) — must traverse from head
Insert/delete at known positionO(n) — shiftingO(1) — just relink pointers, once you have a reference to the node
Insert/delete at endO(1) (dynamic array, amortized)O(1) if tail pointer kept, else O(n)
Memory overheadLow (just the data)Higher (extra pointer per node)
Cache performanceExcellent (sequential memory)Poor (nodes scattered in memory)
ConceptStatic ArrayDynamic Array
SizeFixed at creationGrows/shrinks automatically
Resize costNot possible without manual reallocationOccasional O(n) copy, amortized to O(1)
TypeUsually single, fixed typePython allows mixed types
ExampleC's int arr[10]Python list, Java ArrayList, C++ vector
ConceptMutable String (e.g., StringBuilder)Immutable String (Python str, Java String)
In-place editsYes, O(1) amortized appendNo — every "edit" builds a new string
Safe as dict/hash keyRisky if mutated after insertionAlways safe — content never changes
Best use caseBuilding strings piece by piece in a loopPassing around, comparing, hashing text

Practice Questions

Recall

  1. What is the time complexity of accessing an element by index in an array, and why? Answer guidance: O(1) — the address is computed directly with base + index * element_size, no traversal needed.
  2. Why are strings in Python considered immutable? Answer guidance: The language disallows changing characters in place so strings can be safely shared, hashed, and used as dictionary keys; any "change" creates a new string object.

Understanding

  1. Explain why appending to a Python list is called "amortized O(1)" rather than simply "O(1)." Answer guidance: Most appends just place the value in existing spare capacity (O(1)), but occasionally the backing array is full and must be reallocated and copied (O(n)). Averaged over many appends, the cost per operation works out to O(1).
  2. Why does inserting an element in the middle of an array cost O(n) but inserting at the end cost O(1) (amortized)? Answer guidance: Middle insertion requires shifting every subsequent element one slot right to preserve contiguity and order; end insertion has nothing after it to shift.

Application

  1. You need to check if a sorted array of integers contains any pair summing to a target value, without using extra memory for a hash set. Which pattern would you use and what's its time complexity? Answer guidance: Two pointers starting at both ends, moving inward based on whether the current sum is too high or too low; O(n) time, O(1) extra space.
  2. You need to find the longest substring of a string with no repeating characters. What pattern applies, and roughly how would you implement it? Answer guidance: Sliding window with a set tracking characters currently in the window; expand the right pointer, and when a duplicate is found, shrink from the left until the duplicate is removed. O(n) overall.

Analysis

  1. Compare using an array versus a linked list for a scenario where you frequently insert/remove at the front of a large collection but rarely access by index. Which is better and why? Answer guidance: Linked list — front insertion/removal is O(1) with a head pointer, versus O(n) for an array because everything must shift. Since index access isn't needed often, the array's main advantage (O(1) access) isn't being used.
  2. A junior developer writes a loop that builds a large string using result += next_piece thousands of times, then complains their code is slow. Diagnose the issue and propose a fix. Answer guidance: Because strings are immutable, each += may allocate a new string and copy all previous content, making the loop O(n²) in the worst case. Fix: append pieces to a list and use "".join(pieces) once at the end, which is O(n).

FAQ

Q: Is a Python list actually an array? A: Under the hood, yes — CPython implements list as a dynamic array of pointers to objects, not a linked list. That's why indexing is O(1) and why append is amortized O(1) rather than always O(1).

Q: Why can't I get O(1) insertion in the middle of an array like I can with a linked list? A: Because an array's whole speed advantage (O(1) indexing) depends on every element being at a precise, contiguous offset. Inserting in the middle without shifting would break that contiguity and destroy the O(1) access guarantee for later reads.

Q: When should I use two pointers versus sliding window? A: Use two pointers when you're looking at pairs of positions (often converging from both ends, frequently requiring sorted input) — like two-sum or palindrome checks. Use sliding window when you're tracking a contiguous subrange that grows and shrinks — like longest substring or subarray problems.

Q: Does string immutability mean strings are always slower than mutable alternatives? A: Not for read-heavy use (indexing, comparing, hashing) — those are just as fast as arrays. It only becomes a real cost in write-heavy scenarios like repeated concatenation, which is why languages provide mutable alternatives (StringBuilder, io.StringIO, joining a list) for that specific case.

Q: Why does binary search need a sorted array? A: Binary search works by eliminating half the remaining elements each step based on a comparison with the middle element. That elimination is only valid if you know everything to one side is guaranteed smaller (or larger) — which is only true when the array is sorted.

Q: What's the actual memory cost difference between an array and a linked list holding the same data? A: An array of n integers uses roughly n × (size of int) bytes. A singly linked list needs that same data plus one pointer per node (commonly 8 bytes on a 64-bit system), so it can use noticeably more memory, especially for small element types.

Quick Revision

  • Arrays store elements in contiguous memory → O(1) access via base + i * size.
  • Static arrays have fixed size; dynamic arrays (Python list) resize automatically.
  • Dynamic array append is amortized O(1); occasional resize copy is O(n).
  • Insert/delete at an arbitrary index: O(n) due to shifting elements.
  • Insert/delete at the end: O(1) (amortized for dynamic arrays).
  • Linear search: O(n); binary search on sorted data: O(log n).
  • Strings are immutable in Python/Java/JS — every "edit" creates a new string.
  • Naive string concatenation in a loop can be O(n²); use "".join() for O(n).
  • Two pointers: converge from both ends (or move in tandem) to avoid O(n²) nested loops — needs sorted or structured input.
  • Sliding window: maintain a moving subrange, expand/shrink to solve substring/subarray problems in O(n).
  • Arrays win on cache performance and index access; linked lists win on cheap insert/delete when you already hold a node reference.
  • Choosing the right pattern (two pointers vs sliding window vs brute force) is usually the difference between O(n) and O(n²) on interview problems.

Prerequisites

  • Basic understanding of variables, loops, and functions in a programming language
  • Big-O notation and how to reason about time/space complexity

Related Topics

  • Linked Lists (contrast in memory layout and operation costs)
  • Hash Tables (built using arrays plus a hashing function)
  • Sorting Algorithms (many rely on array indexing and swapping)

Next Topics

  • Stacks and Queues
  • Recursion and Backtracking
  • Searching Algorithms (linear vs. binary search in depth)