LearnAbout

Arrays & Strings Fundamentals

On this page
Quick reference
Index read / writeO(1)
Append (at the end)O(1) amortized
Insert / delete at an arbitrary indexO(n)
Unsorted search (find a value)O(n)
String concatenation, naive (in a loop)O(nΒ²) total
String concatenation, via bufferO(n) total
The Idea

An array is a row of numbered lockers in memory. Each locker is the same size. The lockers are in order, side by side. Thus the computer does not search for locker #47. The computer calculates the address directly: base address + 47 Γ— (size of one locker). This is one multiplication and one addition. This is why a read of arr[47] has the same cost for an array of 100 elements or 100 million elements. A linked list (Phase 2) is different. In a linked list, each node tells you only where the next node is. To reach the 47th node, you must go through 46 nodes first. An array has a strict rule: each element must be the same size and must be in one continuous block. In exchange, an array gives you immediate access to any position.

A string is an array of characters. It uses the same continuous block and the same index-to-address calculation. But many languages make strings immutable. In Python, Java, and JavaScript, you cannot change a string value after you create it. When you "edit" a string, you do not change the original locker row. The computer builds a completely new string and points your variable at the new string. This is not important when you do it one time. This is very important when it happens inside a loop.

Memorize this core trade. An array gives O(1) access to any index, because the address is only arithmetic. No search is necessary. All other array operations (append, insert, delete) come from the same continuous-block rule. Sometimes the rule helps you. Sometimes the rule has a cost.
Build It
Structures
structure Array:
    data          // one continuous block of memory, one slot for each index
    length        // the number of slots that hold real elements now
    capacity      // the number of slots that are allocated (>= length)

// address(i) = base_address + i * element_size
// This is only arithmetic, with no search. This formula is why index access is O(1).
read / write by index β€” O(1)
function readAt(arr, i):
    return arr.data[i]              // one address calculation and one memory access

function writeAt(arr, i, value):
    arr.data[i] = value             // the same arithmetic, but a write instead of a read
Both directions of the address formula have the same cost. A read by index and a write by index are O(1) for the same reason.
append β€” O(1) amortized
function append(arr, value):
    if arr.length == arr.capacity:       // there is no more room in the current block
        newCapacity = arr.capacity * 2    // multiply the capacity by two, do not add one slot
        newData = allocate(newCapacity)
        copy arr.data into newData         // O(n) β€” the computer must move each element
        arr.data = newData
        arr.capacity = newCapacity
    arr.data[arr.length] = value
    arr.length += 1
Most appends are O(1). There is room, so the append is only a write. Sometimes the block is full. Then a resize copies each element, which is an O(n) event. "Amortized" means this: spread that O(n) cost over all the O(1) appends before it. The average cost for each append is still O(1). You must multiply the capacity by two, not add a fixed amount. This math is correct because each resize buys enough extra room. Resizes become much more rare as the array becomes larger.
insert / delete at an arbitrary index β€” O(n)
function insertAt(arr, i, value):
    // Each element from i to the end must move one slot to the right to make a gap.
    // You cannot make room without a physical move of these elements.
    for j from arr.length down to i + 1:
        arr.data[j] = arr.data[j - 1]
    arr.data[i] = value
    arr.length += 1

function deleteAt(arr, i):
    // This is the opposite: each element after i moves one slot to the left to close the gap.
    for j from i to arr.length - 2:
        arr.data[j] = arr.data[j + 1]
    arr.length -= 1
An insert or delete at the front is the worst case. Almost the full array must move. An insert or delete at the very end is the cheap case. Nothing must move. This end case is the same as the append above.
building a string β€” naive concatenation vs. a buffer
// Naive: this looks like one simple loop, but each += builds a completely new string.
function buildNaive(words):
    result = ""
    for w in words:
        result = result + w    // This copies the full result each time.
    return result

// Buffer: collect the pieces in a resizable array (append is O(1) amortized, see above).
// Then build the real string one time at the end.
function buildWithBuffer(words):
    buffer = []
    for w in words:
        buffer.append(w)        // O(1) amortized for each piece
    return join(buffer)         // one O(n) pass over the total output length
Strings are immutable, so result + w cannot extend result in place. It allocates a new string with the length of all the content so far. It copies the old content in. Then it adds w. If you do this n times, you copy 1 + 2 + ... + n characters in total, which is O(nΒ²). This is the same "hidden O(n) inside a loop" problem from Big-O Notation, but with a string.
Know It

Two questions decide the cost. First, is the position of the work fixed (an index that you already know)? Second, does the work depend on a search through other elements or a move of other elements? Fixed-position work is O(1). Work that must look at or move other elements increases with n.

OperationTimeSpaceWhy
Index read / writeO(1)O(1)This is only address arithmetic. It does not depend on the number of other elements in the array.
Append (at the end)O(1) amortizedO(1) amortizedThis is usually a direct write. The occasional O(n) resize-and-copy is spread over enough earlier appends. The average stays O(1).
Insert / delete at an arbitrary indexO(n)O(1)Each element after the target index must move one slot to open or close the gap. The worst case (index 0) moves almost the full array.
Unsorted search (find a value)O(n)O(1)There is no shortcut to the position of a value. The computer checks the elements one at a time until it finds the value or reaches the end.
String concatenation, naive (in a loop)O(nΒ²) totalO(nΒ²) total allocatedEach += copies all the content so far, because strings are immutable. n concatenations copy approximately 1 + 2 + ... + n characters.
String concatenation, via bufferO(n) totalO(n)To collect the pieces in a resizable array costs O(1) amortized for each piece. To join one time at the end is one O(n) pass.
Break It

Empty array

the array has zero elements

arr[0] on an empty array is not "the first element". There is no first element. A read, a search, or a loop over an empty array must be a valid operation with no effect, not a crash. Code that assumes "there is always at least one element" fails on the smallest legal input.

Off-by-one at the boundary

an index equals length instead of length βˆ’ 1

The last valid index of an array with length n is n βˆ’ 1, not n. A loop condition such as i <= length is a common error. Manual boundary math is also a common error, for example a midpoint, a window edge, or a last-common-index check like in Longest Common Prefix. These errors make an out-of-bounds read or write exactly one position past the real end.

Mutating an immutable string in place

code tries to change a character of a string directly

In languages with immutable strings, a statement such as s[0] = 'x' makes an error or does nothing useful. There is no "in place" operation. Each apparent edit makes a new string object. Code that assumes a string is a mutable character array (like an array) crashes or continues to use an old copy.

Modifying an array while iterating over it

elements are inserted or removed during a loop over the same array

If you delete index i while the loop cursor is also at i, each later element moves down one slot. The next step of the loop then skips the element that moved into the position it just left. The loop misses an element with no error. The safe method is to iterate backward when you delete, or to build a new array of the elements to keep instead of a change during the walk.

Use It
Remove Element
Overwrite in place. Walk through the array one time with a separate write position. The write position moves only when you must keep the current element. You do not move the full array and you do not need an extra array.
Easy
Merge Sorted Array
nums1 has extra empty space at the end. Fill it from the back. Compare the largest remaining values of both arrays. Put the larger value in the last open slot. Work backward, so you never overwrite a value that you have not read yet.
Easy
Plus One
Walk from the rightmost digit to the left. Carry a 9 β†’ 0 rollover into the next digit. If every digit was a 9, the number gets a new leading digit. A fixed-size in-place write cannot do this. So this one case needs a new, longer array.
Easy
Rotate Array
Reverse sub-ranges three times to rotate an array in place with no second array. First reverse the full array. Then reverse the first k elements. Then reverse the rest.
Medium
Longest Common Prefix
Compare the characters column by column across each string at the same index. Stop at once when two characters are different. Also stop when you reach the end of the shortest string. Use the first stop.
Easy
String to Integer (atoi)
Walk the string one time. Build the number digit by digit the same way base-10 place value works (result = result * 10 + digit). The parse itself is easy. The difficult part is the boundary conditions: leading whitespace, an optional sign, a stop at the first non-digit, and a limit to the 32-bit range.
Medium
Majority Element
You know the O(n) shape from Big-O Notation. Now write it. Keep a current candidate and a count as you scan one time from left to right. Increase the count when you see the candidate. In other cases, decrease the count and then change the candidate. Use no sort and no nested loop.
Easy
Best Time to Buy and Sell Stock
Use the same O(n) single-pass idea from Big-O Notation. Keep the lowest price that you have seen as you walk the array one time. At each day, compare the profit if you sell today against the best profit so far.
Easy
Zigzag Conversion
Simulate a write of the characters into numRows separate buffers. Move a current-row index down and then up in a zigzag as you place each character. Then concatenate the row buffers. You do not need a real 2D grid.
Medium
Valid Palindrome
Compare the string from both ends toward the middle, one step at a time. Skip characters that are not letters or numbers. Ignore case. Continue until the two positions meet in the middle.
Easy