Arrays

Data Structures & Algorithms

Arrays

The Foundation of All Data Structures

An array is a contiguous block of memory holding elements of the same type. Almost every other data structure is built on arrays or inspired by them.

Real-world analogy: A row of numbered post-office boxes. Every box has a fixed number (index), the same size, next to its neighbors. To reach box 47, you go directly — not through boxes 1 to 46. That directness is what makes arrays O(1) access.


Part 1 — Memory Layout

int arr[5] = {10, 20, 30, 40, 50};
Memory Address:  1000   1004   1008   1012   1016
Value stored:     10     20     30     40     50
Array index:     [0]    [1]    [2]    [3]    [4]

Why O(1) access:

address of arr[i] = base_address + (i × sizeof(element))
address of arr[3] = 1000 + (3 × 4) = 1012   ← one multiply + one add

Cache friendliness: CPUs fetch 64 bytes (a cache line = 16 integers) at once. Accessing arr[0] loads arr[0..15] into cache. arr[1] through arr[15] are then instant. This makes arrays much faster than linked lists in practice despite the same algorithmic complexity.


Part 2 — Static vs Dynamic Arrays

// Static — size fixed at compile time, lives on stack
int arr[100];
int arr2[5] = {1, 2, 3, 4, 5};
int arr3[5] = {};     // all zeros

// Dynamic — std::vector (use this in C++)
#include <vector>
vector<int> v;             // empty
vector<int> v2(5, 0);      // {0,0,0,0,0}
vector<int> v3 = {1,2,3};  // initializer list

v.push_back(10);     // add to end — O(1) amortized
v.pop_back();        // remove from end — O(1)
v[i];                // access — O(1)
v.size();            // count — O(1)
v.insert(v.begin()+i, x); // insert at i — O(n)
v.erase(v.begin()+i);     // erase at i — O(n)
v.reserve(1000);     // pre-allocate to avoid reallocations

How vector grows:

Capacity doubles when full:
Size: 1  2  3  4  5  6  7  8  9
Cap:  1  2  4  4  8  8  8  8  16

Total copies over n push_backs ≤ 1+2+4+...+n ≤ 2n → O(1) amortized

Part 3 — Time Complexities

Operation           Time        Notes
Access arr[i]       O(1)        Direct address calculation
Search (unsorted)   O(n)        Must check every element
Search (sorted)     O(log n)    Binary search
Insert at end       O(1) amort  Vector doubling
Insert at i         O(n)        Shifts elements right
Delete at end       O(1)
Delete at i         O(n)        Shifts elements left
Sort                O(n log n)  Optimal comparison sort

Part 4 — Core Techniques

Two Pointers

Two indices that move to reduce O(n²) problems to O(n).

Visualization — pair sum in sorted array:

arr = [1, 3, 4, 5, 7, 9]   target = 12

Step 1: left=0(1), right=5(9)  sum=10 < 12 → move left right
        [1, 3, 4, 5, 7, 9]
         ↑              ↑
Step 2: left=1(3), right=5(9)  sum=12 = 12 ✓ FOUND
        [1, 3, 4, 5, 7, 9]
            ↑           ↑
bool hasPairSum(vector<int>& arr, int target) {
    int left = 0, right = arr.size() - 1;
    while (left < right) {
        int sum = arr[left] + arr[right];
        if (sum == target) return true;
        else if (sum < target) left++;
        else right--;
    }
    return false;
}
// Time: O(n)  Space: O(1)

Remove duplicates from sorted array:

[1, 1, 2, 3, 3, 4]
 w  r              → equal, skip
 w     r           → different, write++
    w     r        → different, write++
       w     r     → equal, skip
       w        r  → different, write++
          w

Result: [1, 2, 3, 4, ...]  length = 4
int removeDuplicates(vector<int>& arr) {
    int write = 0;
    for (int read = 1; read < arr.size(); read++)
        if (arr[read] != arr[write]) arr[++write] = arr[read];
    return write + 1;
}

Sliding Window

Window of size k slides across array. Update in O(1) by adding new, removing old.

Visualization — max sum of size 3:

arr = [3, 1, 4, 1, 5, 9, 2, 6]

Window [3,1,4] sum=8
         remove 3, add 1 → [1,4,1] sum=6
                 remove 1, add 5 → [4,1,5] sum=10
                         remove 4, add 9 → [1,5,9] sum=15 ← max
                                 remove 1, add 2 → [5,9,2] sum=16 ← new max
                                         remove 5, add 6 → [9,2,6] sum=17 ← new max
int maxSumWindow(vector<int>& arr, int k) {
    int windowSum = 0;
    for (int i = 0; i < k; i++) windowSum += arr[i];
    int maxSum = windowSum;

    for (int i = k; i < arr.size(); i++) {
        windowSum += arr[i];      // add incoming
        windowSum -= arr[i - k];  // remove outgoing
        maxSum = max(maxSum, windowSum);
    }
    return maxSum;
}
// Time: O(n)  compare to brute force: O(nk)

Variable window:

// Longest subarray with sum ≤ k
int longestSubarray(vector<int>& arr, int k) {
    int left = 0, sum = 0, maxLen = 0;
    for (int right = 0; right < arr.size(); right++) {
        sum += arr[right];
        while (sum > k) sum -= arr[left++];   // shrink
        maxLen = max(maxLen, right - left + 1);
    }
    return maxLen;
}

Prefix Sum

Precompute cumulative sums for O(1) range queries.

arr    = [3,  1,  4,  1,  5,  9,  2,  6]
prefix = [0,  3,  4,  8,  9, 14, 23, 25, 31]

sum(arr[2..5]) = prefix[6] - prefix[2] = 23 - 4 = 19
verify: 4+1+5+9 = 19 ✓
vector<int> buildPrefix(vector<int>& arr) {
    vector<int> p(arr.size() + 1, 0);
    for (int i = 0; i < arr.size(); i++)
        p[i+1] = p[i] + arr[i];
    return p;
}

int rangeSum(vector<int>& p, int l, int r) {
    return p[r+1] - p[l];    // O(1) query
}

Count subarrays with sum = k:

int countSubarrays(vector<int>& arr, int k) {
    unordered_map<int,int> freq;
    freq[0] = 1;
    int sum = 0, count = 0;
    for (int x : arr) {
        sum += x;
        count += freq[sum - k];   // subarrays ending here with sum k
        freq[sum]++;
    }
    return count;
}
// Time: O(n)  — arr=[1,2,3], k=3 → 2 subarrays

Kadane’s Algorithm — Maximum Subarray

At each position: extend current subarray OR start fresh — whichever is larger.

Visualization:

arr  = [-2,  1, -3,  4, -1,  2,  1, -5,  4]
curr = [-2,  1, -2,  4,  3,  5,  6,  1,  5]
max  = [-2,  1,  1,  4,  4,  5,  6,  6,  6]

curr[i] = max(arr[i], curr[i-1] + arr[i])
        = "start fresh" vs "extend"
int maxSubarraySum(vector<int>& arr) {
    int curr = arr[0], best = arr[0];
    for (int i = 1; i < arr.size(); i++) {
        curr = max(arr[i], curr + arr[i]);
        best = max(best, curr);
    }
    return best;
}
// Time: O(n)  Space: O(1)
// [-2,1,-3,4,-1,2,1,-5,4] → 6 (subarray [4,-1,2,1])

Part 5 — Sorting Algorithms

Bubble Sort

Repeatedly swap adjacent out-of-order elements. Largest bubbles to end.

Visualization:

Pass 1: [5,3,8,1,2] → [3,5,1,2,8]  (8 bubbled to end)
Pass 2: [3,5,1,2,8] → [3,1,2,5,8]  (5 bubbled to position)
Pass 3: [3,1,2,5,8] → [1,2,3,5,8]
Pass 4: no swaps → done
void bubbleSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++) {
        bool swapped = false;
        for (int j = 0; j < n-i-1; j++)
            if (arr[j] > arr[j+1]) { swap(arr[j], arr[j+1]); swapped = true; }
        if (!swapped) break;  // already sorted
    }
}
// Best: O(n)  Avg/Worst: O(n²)  Space: O(1)  Stable: Yes

Selection Sort

Find minimum, place at front. Minimum swaps among O(n²) sorts.

Visualization:

[64,25,12,22,11]
 find min(11) → swap with arr[0]
[11,25,12,22,64]
    find min(12) → swap with arr[1]
[11,12,25,22,64]
       find min(22) → swap with arr[2]
[11,12,22,25,64]  ✓
void selectionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++) {
        int minIdx = i;
        for (int j = i+1; j < n; j++)
            if (arr[j] < arr[minIdx]) minIdx = j;
        swap(arr[i], arr[minIdx]);
    }
}
// Best/Avg/Worst: O(n²)  Space: O(1)  Stable: No  Swaps: O(n)

Insertion Sort

Build sorted section one element at a time. Like sorting playing cards.

Visualization:

[5, 2, 4, 6, 1]
[5 | 2, 4, 6, 1]   sorted=[5]
key=2: shift 5 → [2, 5 | 4, 6, 1]
key=4: shift 5 → [2, 4, 5 | 6, 1]
key=6: no shift → [2, 4, 5, 6 | 1]
key=1: shift 6,5,4,2 → [1, 2, 4, 5, 6]  ✓
void insertionSort(vector<int>& arr) {
    for (int i = 1; i < arr.size(); i++) {
        int key = arr[i], j = i-1;
        while (j >= 0 && arr[j] > key) { arr[j+1] = arr[j]; j--; }
        arr[j+1] = key;
    }
}
// Best: O(n)  Avg/Worst: O(n²)  Space: O(1)  Stable: Yes
// Best for: small arrays (n<50) and nearly sorted arrays

Merge Sort

Divide in half, recursively sort, merge. Classic divide-and-conquer.

Visualization:

[38,27,43,3,9,82,10]
    ↙             ↘
[38,27,43,3]   [9,82,10]
  ↙     ↘       ↙    ↘
[38,27] [43,3] [9,82] [10]
  ↙↘     ↙↘    ↙↘
[38][27][43][3][9][82]
  ↘↙     ↘↙    ↘↙
[27,38] [3,43] [9,82]  [10]
    ↘       ↙       ↘  ↙
  [3,27,38,43]    [9,10,82]
         ↘            ↙
    [3,9,10,27,38,43,82]  ✓

Merge step:

Left: [27,38]   Right: [3,43]
Compare: 3<27 → take 3  → [3]
Compare: 27<43 → take 27 → [3,27]
Compare: 38<43 → take 38 → [3,27,38]
Remaining: take 43       → [3,27,38,43] ✓
void merge(vector<int>& arr, int l, int m, int r) {
    vector<int> tmp;
    int i = l, j = m+1;
    while (i <= m && j <= r)
        tmp.push_back(arr[i] <= arr[j] ? arr[i++] : arr[j++]);
    while (i <= m) tmp.push_back(arr[i++]);
    while (j <= r) tmp.push_back(arr[j++]);
    for (int k = l; k <= r; k++) arr[k] = tmp[k-l];
}

void mergeSort(vector<int>& arr, int l, int r) {
    if (l >= r) return;
    int m = l + (r-l)/2;
    mergeSort(arr, l, m);
    mergeSort(arr, m+1, r);
    merge(arr, l, m, r);
}
// Call: mergeSort(arr, 0, arr.size()-1)
// Best/Avg/Worst: O(n log n)  Space: O(n)  Stable: Yes

Recurrence: T(n) = 2T(n/2) + O(n) → O(n log n) by Master Theorem.


Quick Sort

Pick pivot, partition around it, recurse. Fastest in practice.

Partition visualization:

arr = [3,6,8,10,1,2,1]   pivot = arr[6] = 1
i = -1

j=0: 3>1, skip
j=1: 6>1, skip
j=2: 8>1, skip
j=3: 10>1, skip
j=4: 1≤1, i++, swap(arr[0],arr[4]) → [1,6,8,10,3,2,1]  i=0
j=5: 2>1, skip
end: swap(arr[i+1], pivot) → [1,1,8,10,3,2,6]  pivot at index 1

Left:[1]  Pivot:[1]  Right:[8,10,3,2,6]
int partition(vector<int>& arr, int lo, int hi) {
    int pivot = arr[hi], i = lo-1;
    for (int j = lo; j < hi; j++)
        if (arr[j] <= pivot) swap(arr[++i], arr[j]);
    swap(arr[i+1], arr[hi]);
    return i+1;
}

void quickSort(vector<int>& arr, int lo, int hi) {
    if (lo < hi) {
        // Randomize pivot to avoid O(n²) on sorted input
        swap(arr[lo + rand()%(hi-lo+1)], arr[hi]);
        int p = partition(arr, lo, hi);
        quickSort(arr, lo, p-1);
        quickSort(arr, p+1, hi);
    }
}
// Avg: O(n log n)  Worst: O(n²)  Space: O(log n)  Stable: No

Search sorted array by halving search space each step.

Visualization:

arr = [1,3,5,7,9,11,13,15]   target = 7

left=0, right=7, mid=3, arr[3]=7 → FOUND at index 3 ✓

Another: target = 6
left=0, right=7, mid=3, arr[3]=7  → 7>6, right=2
left=0, right=2, mid=1, arr[1]=3  → 3<6, left=2
left=2, right=2, mid=2, arr[2]=5  → 5<6, left=3
left=3 > right=2 → NOT FOUND (-1)
int binarySearch(vector<int>& arr, int target) {
    int left = 0, right = arr.size()-1;
    while (left <= right) {
        int mid = left + (right-left)/2;  // avoids overflow
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) left = mid+1;
        else right = mid-1;
    }
    return -1;
}
// Time: O(log n)  Space: O(1)

STL binary search:

// lower_bound: first position where arr[i] >= target
auto it = lower_bound(arr.begin(), arr.end(), target);

// upper_bound: first position where arr[i] > target
auto it2 = upper_bound(arr.begin(), arr.end(), target);

// Count occurrences:
int count = upper_bound(arr.begin(), arr.end(), x)
          - lower_bound(arr.begin(), arr.end(), x);

Sorting Comparison Table

Algorithm   Best       Average    Worst      Space   Stable
Bubble      O(n)       O(n²)      O(n²)      O(1)    Yes
Selection   O(n²)      O(n²)      O(n²)      O(1)    No
Insertion   O(n)       O(n²)      O(n²)      O(1)    Yes
Merge       O(n lg n)  O(n lg n)  O(n lg n)  O(n)    Yes
Quick       O(n lg n)  O(n lg n)  O(n²)      O(lg n) No

Lower bound: Any comparison-based sort requires Ω(n log n). There are n! orderings → binary decision tree needs height ≥ log₂(n!) ≈ n log n.


Practice Problems

Easy: 1. Find max and min in array. 2. Reverse array in-place. 3. Move zeros to end, keep relative order. 4. Find missing number in array of 1..n.

Medium: 5. Best time to buy and sell stock (one transaction). 6. Find minimum in rotated sorted array. 7. Maximum product subarray. 8. Count inversions (pairs i<j where arr[i]>arr[j]).


Answers

Problem 3:

void moveZeros(vector<int>& arr) {
    int write = 0;
    for (int x : arr) if (x != 0) arr[write++] = x;
    while (write < arr.size()) arr[write++] = 0;
}

Problem 5:

int maxProfit(vector<int>& prices) {
    int minPrice = INT_MAX, profit = 0;
    for (int p : prices) {
        minPrice = min(minPrice, p);
        profit = max(profit, p - minPrice);
    }
    return profit;
}

Problem 6:

int findMin(vector<int>& arr) {
    int lo = 0, hi = arr.size()-1;
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (arr[mid] > arr[hi]) lo = mid+1;
        else hi = mid;
    }
    return arr[lo];
}

References

start typing to search