Table of Contents

1. What is an Array? 2. Time Complexity 3. Basic Operations with Code 4. Common Array Techniques 5. Strings 6. LeetCode Problems to Practice 7. Practice Quiz

1. What is an Array?

An array is a collection of elements stored in contiguous (adjacent) memory locations. Because the elements sit right next to each other in memory, you can jump to any element instantly if you know its index. This is why array access is O(1) -- the computer calculates the exact memory address with simple arithmetic.

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

Fixed-Size vs Dynamic Arrays

In low-level languages like C or Java, arrays have a fixed size -- you declare how many elements they hold at creation and that is it. You cannot grow or shrink them.

In Python and JavaScript, you work with dynamic arrays (Python's list, JavaScript's Array). These automatically resize when you add more elements than the current capacity allows. Under the hood, they allocate a new, larger block of memory and copy everything over. This resizing is why appending is O(1) amortized -- most appends are instant, but occasionally one triggers a costly resize.

Warning

Do not confuse "array" in C (fixed, contiguous, typed) with Python's list (dynamic, can hold mixed types). When interviewers say "array," they typically mean the logical concept -- an indexed, ordered collection -- regardless of language.

Memory Layout

Here is how an array of integers looks in memory. Each cell is the same size, and the index determines the offset from the base address:

Memory Address: 0x1000 0x1004 0x1008 0x100C 0x1010 +----------+----------+----------+----------+----------+ arr = | 10 | 20 | 30 | 40 | 50 | +----------+----------+----------+----------+----------+ Index: [0] [1] [2] [3] [4] To access arr[3]: address = 0x1000 + (3 * 4 bytes) = 0x100C --> value: 40 This is why random access is O(1) -- just one multiplication and one addition.

Why O(1) Random Access Matters

Because every element is the same size and stored contiguously, accessing arr[0] and arr[999999] takes the exact same amount of time. No scanning, no traversal -- just a direct memory lookup. This is the superpower of arrays compared to linked lists, where you would need to walk through every node.

Tip

Arrays are cache-friendly. Because elements are adjacent in memory, your CPU cache loads nearby elements automatically. Iterating through an array is one of the fastest operations a computer can do.

2. Time Complexity

Understanding the time complexity of every operation is critical. This table is your cheat sheet for array operations:

Operation Fixed-Size Array Dynamic Array Why?
Access by index O(1) O(1) Direct address calculation
Search (unsorted) O(n) O(n) Must check each element
Search (sorted) O(log n) O(log n) Binary search
Insert at end N/A (fixed size) O(1) amortized Occasional resize costs O(n)
Insert at index O(n) O(n) Must shift elements right
Delete from end N/A O(1) No shifting needed
Delete at index O(n) O(n) Must shift elements left
Space complexity: O(n) -- arrays store n elements
Example -- Why Insert at Index is O(n)

To insert 99 at index 2 in [10, 20, 30, 40, 50]:

Step 1: Shift 30, 40, 50 one position right.

Step 2: Place 99 at index 2.

Result: [10, 20, 99, 30, 40, 50]. In the worst case (inserting at index 0), you shift all n elements.

3. Basic Operations with Code

Creating Arrays

Both Python and JavaScript create dynamic arrays with similar syntax:

Python
# Create an array (list in Python)
arr = [1, 2, 3, 4, 5]

# Create an empty array
empty = []

# Create array of n zeros
zeros = [0] * 10

# Create array with list comprehension
squares = [i**2 for i in range(5)]
# [0, 1, 4, 9, 16]
JavaScript
// Create an array
const arr = [1, 2, 3, 4, 5];

// Create an empty array
const empty = [];

// Create array of n zeros
const zeros = new Array(10).fill(0);

// Create array with Array.from
const squares = Array.from(
  {length: 5}, (_, i) => i ** 2
);
// [0, 1, 4, 9, 16]

Accessing Elements

Access any element in O(1) using its index. Arrays are zero-indexed in both languages:

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

# Access by index
first = arr[0]       # 10
third = arr[2]       # 30

# Negative indexing (Python-only!)
last = arr[-1]       # 50
second_last = arr[-2] # 40

# Length
size = len(arr)      # 5
JavaScript
const arr = [10, 20, 30, 40, 50];

// Access by index
const first = arr[0];       // 10
const third = arr[2];       // 30

// Last element (no negative indexing)
const last = arr[arr.length - 1]; // 50
// Or use .at() in modern JS
const alsoLast = arr.at(-1);  // 50

// Length
const size = arr.length;    // 5

Traversal

Traversal means visiting every element. There are multiple ways to loop through an array:

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

# Method 1: Simple for loop
for val in arr:
    print(val)

# Method 2: With index using enumerate
for i, val in enumerate(arr):
    print(f"Index {i}: {val}")

# Method 3: Index-based loop
for i in range(len(arr)):
    print(arr[i])

# Method 4: While loop
i = 0
while i < len(arr):
    print(arr[i])
    i += 1
JavaScript
const arr = [10, 20, 30, 40];

// Method 1: for...of loop
for (const val of arr) {
    console.log(val);
}

// Method 2: forEach with index
arr.forEach((val, i) => {
    console.log(`Index ${i}: ${val}`);
});

// Method 3: Classic for loop
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

// Method 4: While loop
let i = 0;
while (i < arr.length) {
    console.log(arr[i]);
    i++;
}
Tip

In Python, prefer for val in arr for simple iteration and for i, val in enumerate(arr) when you need the index. The range(len(arr)) pattern is considered less Pythonic but is sometimes necessary for two-pointer patterns.

Inserting Elements

Adding elements to a dynamic array:

Python
arr = [1, 2, 3]

# Append to end -- O(1) amortized
arr.append(4)
# [1, 2, 3, 4]

# Insert at specific index -- O(n)
arr.insert(1, 99)
# [1, 99, 2, 3, 4]

# Extend with another list -- O(k)
arr.extend([5, 6])
# [1, 99, 2, 3, 4, 5, 6]

# Concatenation (creates new list)
new_arr = arr + [7, 8]
JavaScript
const arr = [1, 2, 3];

// Push to end -- O(1) amortized
arr.push(4);
// [1, 2, 3, 4]

// Insert at specific index -- O(n)
arr.splice(1, 0, 99);
// [1, 99, 2, 3, 4]

// Push multiple elements
arr.push(5, 6);
// [1, 99, 2, 3, 4, 5, 6]

// Unshift to front -- O(n)
arr.unshift(0);
// [0, 1, 99, 2, 3, 4, 5, 6]

Deleting Elements

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

# Pop last element -- O(1)
last = arr.pop()
# last = 50, arr = [10, 20, 30, 40]

# Pop at specific index -- O(n)
second = arr.pop(1)
# second = 20, arr = [10, 30, 40]

# Remove by value -- O(n)
arr.remove(30)
# arr = [10, 40]

# Delete by index
del arr[0]
# arr = [40]

# Clear entire list
arr.clear()
# arr = []
JavaScript
const arr = [10, 20, 30, 40, 50];

// Pop last element -- O(1)
const last = arr.pop();
// last = 50, arr = [10, 20, 30, 40]

// Remove at index using splice -- O(n)
const removed = arr.splice(1, 1);
// removed = [20], arr = [10, 30, 40]

// Shift removes first element -- O(n)
const first = arr.shift();
// first = 10, arr = [30, 40]

// Filter to remove by value -- O(n)
let filtered = arr.filter(x => x !== 30);
// filtered = [40]

// Clear entire array
arr.length = 0;
// arr = []

Slicing

Slicing extracts a sub-array without modifying the original. Both languages use start-inclusive, end-exclusive ranges:

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

# Basic slice: arr[start:end]
sub = arr[1:4]
# [20, 30, 40]

# From beginning
first_three = arr[:3]
# [10, 20, 30]

# To end
last_two = arr[3:]
# [40, 50]

# With step
every_other = arr[::2]
# [10, 30, 50]

# Reverse an array
reversed_arr = arr[::-1]
# [50, 40, 30, 20, 10]

# Shallow copy
copy = arr[:]
JavaScript
const arr = [10, 20, 30, 40, 50];

// Basic slice: arr.slice(start, end)
const sub = arr.slice(1, 4);
// [20, 30, 40]

// From beginning
const firstThree = arr.slice(0, 3);
// [10, 20, 30]

// To end
const lastTwo = arr.slice(3);
// [40, 50]

// No step parameter -- use filter
const everyOther = arr.filter(
  (_, i) => i % 2 === 0
);
// [10, 30, 50]

// Reverse (mutates original!)
const rev = [...arr].reverse();
// [50, 40, 30, 20, 10]

// Shallow copy
const copy = [...arr];
Warning

In JavaScript, .reverse() mutates the original array. Always spread into a new array first with [...arr].reverse() if you need to preserve the original. Python's arr[::-1] creates a new list and does not mutate.

Searching (Linear Search)

Linear search checks each element one by one. It is O(n) in the worst case:

Python
def linear_search(arr, target):
    """Return index of target, or -1 if not found."""
    for i, val in enumerate(arr):
        if val == target:
            return i
    return -1

# Usage
arr = [5, 3, 8, 1, 9]
idx = linear_search(arr, 8)  # 2
idx = linear_search(arr, 7)  # -1

# Built-in alternatives:
8 in arr          # True (O(n))
arr.index(8)       # 2 (raises ValueError if not found)
JavaScript
function linearSearch(arr, target) {
    // Return index of target, or -1
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i;
        }
    }
    return -1;
}

// Usage
const arr = [5, 3, 8, 1, 9];
linearSearch(arr, 8);  // 2
linearSearch(arr, 7);  // -1

// Built-in alternatives:
arr.includes(8);      // true (O(n))
arr.indexOf(8);       // 2 (-1 if not found)

Sorting

Python
arr = [5, 2, 8, 1, 9, 3]

# Sort in-place -- O(n log n)
arr.sort()
# arr = [1, 2, 3, 5, 8, 9]

# Sort descending
arr.sort(reverse=True)
# arr = [9, 8, 5, 3, 2, 1]

# Create new sorted list (original unchanged)
original = [5, 2, 8]
new_sorted = sorted(original)
# original = [5, 2, 8]
# new_sorted = [2, 5, 8]

# Sort with custom key
words = ["banana", "fig", "cherry"]
words.sort(key=len)
# ["fig", "banana", "cherry"]
JavaScript
const arr = [5, 2, 8, 1, 9, 3];

// Sort in-place -- O(n log n)
// WARNING: default sort is lexicographic!
arr.sort((a, b) => a - b);
// arr = [1, 2, 3, 5, 8, 9]

// Sort descending
arr.sort((a, b) => b - a);
// arr = [9, 8, 5, 3, 2, 1]

// Create new sorted array
const original = [5, 2, 8];
const newSorted = [...original].sort(
  (a, b) => a - b
);
// original = [5, 2, 8] (unchanged)

// Sort with custom comparator
const words = ["banana", "fig", "cherry"];
words.sort((a, b) => a.length - b.length);
// ["fig", "banana", "cherry"]
Critical JavaScript Gotcha

JavaScript's default .sort() converts elements to strings and sorts lexicographically. This means [10, 9, 80].sort() gives [10, 80, 9] -- not [9, 10, 80]. Always pass a comparator when sorting numbers: arr.sort((a, b) => a - b).

4. Common Array Techniques

These three techniques show up in the majority of array interview problems. Learn them deeply and you will recognize them instantly.

Two Pointers

The two-pointer technique uses two indices that move through the array, typically from opposite ends or at different speeds. It reduces brute-force O(n^2) solutions to O(n) by eliminating redundant comparisons.

When to use Two Pointers: - Array is sorted (or can be sorted) - You need to find a pair/triplet that satisfies a condition - You need to compare elements from both ends - You need to partition or rearrange in-place

Example: Two Sum on a Sorted Array

Given a sorted array and a target, find two numbers that add up to the target. Return their indices.

Walkthrough

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

Left pointer at index 0 (value 1), right pointer at index 5 (value 15).

1 + 15 = 16 > 12 --> Move right pointer left.

1 + 11 = 12 = target --> Found it! Return [0, 4].

Key insight: if the sum is too big, decreasing the right side makes it smaller. If the sum is too small, increasing the left side makes it bigger.

Python
def two_sum_sorted(arr, target):
    """
    Find two numbers in sorted array
    that add up to target.
    Returns their indices.
    """
    left = 0
    right = len(arr) - 1

    while left < right:
        current_sum = arr[left] + arr[right]

        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1   # Need bigger sum
        else:
            right -= 1  # Need smaller sum

    return []  # No pair found

# Usage
arr = [1, 3, 5, 7, 11, 15]
print(two_sum_sorted(arr, 12))
# [0, 4]  (arr[0] + arr[4] = 1 + 11 = 12)
JavaScript
function twoSumSorted(arr, target) {
    /**
     * Find two numbers in sorted array
     * that add up to target.
     * Returns their indices.
     */
    let left = 0;
    let right = arr.length - 1;

    while (left < right) {
        const sum = arr[left] + arr[right];

        if (sum === target) {
            return [left, right];
        } else if (sum < target) {
            left++;   // Need bigger sum
        } else {
            right--;  // Need smaller sum
        }
    }

    return [];  // No pair found
}

// Usage
const arr = [1, 3, 5, 7, 11, 15];
console.log(twoSumSorted(arr, 12));
// [0, 4]  (arr[0] + arr[4] = 1 + 11 = 12)
Two Pointers Complexity: Time O(n) | Space O(1)
Two Pointers -- Variant Decision Rule:

Converging (left/right): Array is sorted AND you need pairs that satisfy a condition → O(n)
Same direction (fast/slow): Partition, filter, or remove in-place → O(n)
Sliding Window (special case): Contiguous subarray/substring with constraint → O(n)

Key rule: arr[i:j] slicing in Python creates a new array and is O(j-i), not O(1).

Sliding Window

The sliding window technique maintains a "window" (a contiguous sub-array) that slides across the array. Instead of recomputing the entire window from scratch at every position, you add the new element entering the window and remove the element leaving it. This turns O(n*k) brute-force into O(n).

When to use Sliding Window: - Problem involves contiguous sub-arrays/substrings - You need to find max/min sum of size k - You need longest/shortest sub-array with some property - Fixed window: size k is given - Variable window: find optimal size that satisfies condition

Example: Maximum Sum Subarray of Size k

Given an array and a window size k, find the maximum sum of any contiguous subarray of length k.

Walkthrough

arr = [2, 1, 5, 1, 3, 2], k = 3

Window [2, 1, 5] = 8

Slide: remove 2, add 1 --> [1, 5, 1] = 7

Slide: remove 1, add 3 --> [5, 1, 3] = 9

Slide: remove 5, add 2 --> [1, 3, 2] = 6

Maximum sum = 9

Python
def max_sum_subarray(arr, k):
    """
    Find maximum sum of any contiguous
    subarray of size k.
    """
    if len(arr) < k:
        return 0

    # Calculate sum of first window
    window_sum = sum(arr[:k])
    max_sum = window_sum

    # Slide the window
    for i in range(k, len(arr)):
        # Add new element, remove old
        window_sum += arr[i] - arr[i - k]
        max_sum = max(max_sum, window_sum)

    return max_sum

# Usage
arr = [2, 1, 5, 1, 3, 2]
print(max_sum_subarray(arr, 3))
# 9  (subarray [5, 1, 3])
JavaScript
function maxSumSubarray(arr, k) {
    /**
     * Find maximum sum of any contiguous
     * subarray of size k.
     */
    if (arr.length < k) return 0;

    // Calculate sum of first window
    let windowSum = 0;
    for (let i = 0; i < k; i++) {
        windowSum += arr[i];
    }
    let maxSum = windowSum;

    // Slide the window
    for (let i = k; i < arr.length; i++) {
        // Add new element, remove old
        windowSum += arr[i] - arr[i - k];
        maxSum = Math.max(maxSum, windowSum);
    }

    return maxSum;
}

// Usage
const arr = [2, 1, 5, 1, 3, 2];
console.log(maxSumSubarray(arr, 3));
// 9  (subarray [5, 1, 3])
Sliding Window Complexity: Time O(n) | Space O(1)

Prefix Sum

A prefix sum array stores the cumulative sum up to each index. Once built (O(n) time), you can answer any "sum of elements from index i to j" query in O(1) time. Without prefix sums, each range sum query would take O(n).

prefix[0] = 0 prefix[i] = arr[0] + arr[1] + ... + arr[i-1] sum(arr[i..j]) = prefix[j+1] - prefix[i]
Walkthrough

arr = [2, 4, 6, 8, 10]

prefix = [0, 2, 6, 12, 20, 30]

Sum from index 1 to 3 = prefix[4] - prefix[1] = 20 - 2 = 18

Verify: arr[1] + arr[2] + arr[3] = 4 + 6 + 8 = 18

Python
def build_prefix_sum(arr):
    """Build prefix sum array."""
    prefix = [0] * (len(arr) + 1)
    for i in range(len(arr)):
        prefix[i + 1] = prefix[i] + arr[i]
    return prefix

def range_sum(prefix, i, j):
    """Sum of arr[i..j] in O(1)."""
    return prefix[j + 1] - prefix[i]

# Usage
arr = [2, 4, 6, 8, 10]
prefix = build_prefix_sum(arr)
# prefix = [0, 2, 6, 12, 20, 30]

print(range_sum(prefix, 1, 3))
# 18  (4 + 6 + 8)

print(range_sum(prefix, 0, 4))
# 30  (2 + 4 + 6 + 8 + 10)

print(range_sum(prefix, 2, 2))
# 6   (just arr[2])
JavaScript
function buildPrefixSum(arr) {
    /** Build prefix sum array. */
    const prefix = new Array(arr.length + 1).fill(0);
    for (let i = 0; i < arr.length; i++) {
        prefix[i + 1] = prefix[i] + arr[i];
    }
    return prefix;
}

function rangeSum(prefix, i, j) {
    /** Sum of arr[i..j] in O(1). */
    return prefix[j + 1] - prefix[i];
}

// Usage
const arr = [2, 4, 6, 8, 10];
const prefix = buildPrefixSum(arr);
// prefix = [0, 2, 6, 12, 20, 30]

console.log(rangeSum(prefix, 1, 3));
// 18  (4 + 6 + 8)

console.log(rangeSum(prefix, 0, 4));
// 30  (2 + 4 + 6 + 8 + 10)

console.log(rangeSum(prefix, 2, 2));
// 6   (just arr[2])
Prefix Sum Complexity: Build: O(n) time, O(n) space Each query: O(1) time Total for q queries: O(n + q) vs O(n * q) brute force
Tip

Prefix sums appear in disguise in many problems. Anytime you see "sum of subarray" or "range query," think prefix sums. The technique extends to 2D arrays (prefix sum matrices) for submatrix sum queries.

5. Strings

Strings as Arrays of Characters

A string is essentially a sequence (array) of characters. You can index into a string, iterate over it, and slice it just like an array. However, strings have one critical difference in both Python and JavaScript: they are immutable.

Immutability

In both Python and JavaScript, strings cannot be modified in-place. Every "modification" creates a new string. This means string concatenation in a loop is O(n^2) because each concatenation creates a new string and copies all previous characters.

Python
s = "hello"

# Strings are immutable
# s[0] = "H"  # TypeError!

# Index access (just like arrays)
first = s[0]   # "h"
last = s[-1]    # "o"

# Iterate like an array
for char in s:
    print(char)

# To "modify," convert to list first
chars = list(s)       # ['h','e','l','l','o']
chars[0] = "H"
s = "".join(chars)   # "Hello"

# Efficient concatenation: use join
parts = ["a", "b", "c"]
result = "".join(parts) # "abc" -- O(n)
JavaScript
const s = "hello";

// Strings are immutable
// s[0] = "H";  // Silently fails!

// Index access (just like arrays)
const first = s[0];        // "h"
const last = s[s.length - 1]; // "o"

// Iterate like an array
for (const char of s) {
    console.log(char);
}

// To "modify," convert to array first
const chars = s.split("");
chars[0] = "H";
const newS = chars.join(""); // "Hello"

// Efficient concatenation: use join
const parts = ["a", "b", "c"];
const result = parts.join(""); // "abc"

Reverse a String

Python
def reverse_string(s):
    """Reverse a string."""
    return s[::-1]

# Or manually with two pointers:
def reverse_string_manual(s):
    chars = list(s)
    left, right = 0, len(chars) - 1
    while left < right:
        chars[left], chars[right] = (
            chars[right], chars[left]
        )
        left += 1
        right -= 1
    return "".join(chars)

print(reverse_string("hello"))
# "olleh"
JavaScript
function reverseString(s) {
    /** Reverse a string. */
    return s.split("").reverse().join("");
}

// Or manually with two pointers:
function reverseStringManual(s) {
    const chars = s.split("");
    let left = 0;
    let right = chars.length - 1;
    while (left < right) {
        [chars[left], chars[right]] =
            [chars[right], chars[left]];
        left++;
        right--;
    }
    return chars.join("");
}

console.log(reverseString("hello"));
// "olleh"

Check Palindrome

A palindrome reads the same forwards and backwards (e.g., "racecar", "madam").

Python
def is_palindrome(s):
    """Check if string is a palindrome."""
    # Clean: lowercase, letters/digits only
    s = "".join(
        c.lower() for c in s if c.isalnum()
    )
    return s == s[::-1]

# Two-pointer approach (O(1) extra space):
def is_palindrome_tp(s):
    s = "".join(
        c.lower() for c in s if c.isalnum()
    )
    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
print(is_palindrome("hello"))     # False
print(is_palindrome("A man, a plan, a canal: Panama"))
# True
JavaScript
function isPalindrome(s) {
    /** Check if string is a palindrome. */
    // Clean: lowercase, letters/digits only
    s = s.toLowerCase().replace(
        /[^a-z0-9]/g, ""
    );
    return s === s.split("").reverse().join("");
}

// Two-pointer approach:
function isPalindromeTP(s) {
    s = s.toLowerCase().replace(
        /[^a-z0-9]/g, ""
    );
    let left = 0;
    let right = s.length - 1;
    while (left < right) {
        if (s[left] !== s[right]) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

console.log(isPalindrome("racecar"));  // true
console.log(isPalindrome("hello"));    // false
console.log(isPalindrome(
    "A man, a plan, a canal: Panama"
)); // true

Anagram Check

Two strings are anagrams if they contain exactly the same characters in any order (e.g., "listen" and "silent").

Python
def is_anagram(s1, s2):
    """Check if two strings are anagrams."""
    # Method 1: Sort and compare -- O(n log n)
    return sorted(s1.lower()) == sorted(s2.lower())

def is_anagram_optimal(s1, s2):
    """O(n) using character frequency."""
    if len(s1) != len(s2):
        return False

    # Count character frequencies
    count = {}
    for c in s1.lower():
        count[c] = count.get(c, 0) + 1
    for c in s2.lower():
        count[c] = count.get(c, 0) - 1
        if count[c] < 0:
            return False

    return True

print(is_anagram("listen", "silent"))
# True
print(is_anagram("hello", "world"))
# False
JavaScript
function isAnagram(s1, s2) {
    /** Check if two strings are anagrams. */
    // Method 1: Sort and compare -- O(n log n)
    const sort = s =>
        s.toLowerCase().split("").sort().join("");
    return sort(s1) === sort(s2);
}

function isAnagramOptimal(s1, s2) {
    /** O(n) using character frequency. */
    if (s1.length !== s2.length) return false;

    const count = {};
    for (const c of s1.toLowerCase()) {
        count[c] = (count[c] || 0) + 1;
    }
    for (const c of s2.toLowerCase()) {
        if (!count[c]) return false;
        count[c]--;
    }

    return true;
}

console.log(isAnagram("listen", "silent"));
// true
console.log(isAnagram("hello", "world"));
// false
Interview Tip

The sorting approach (O(n log n)) is simpler to write and fewer lines. The frequency-count approach (O(n)) is optimal. In an interview, mention both and implement the optimal one. The interviewer wants to see that you know the tradeoff.

6. LeetCode Problems to Practice

These are the essential array/string problems ordered by difficulty. Solve them in order for the best learning experience.

Problem Difficulty Key Technique Approach Hint
Two Sum Easy Hash Map Store complement in hash map as you iterate
Best Time to Buy and Sell Stock Easy Tracking Min Track minimum price seen so far, compute max profit at each step
Contains Duplicate Easy Set Add to set; if element already in set, return true
Maximum Subarray Medium Kadane's Algorithm At each index, decide: extend current subarray or start fresh
Product of Array Except Self Medium Prefix / Suffix Build prefix product from left, then multiply with suffix from right
3Sum Medium Sort + Two Pointers Sort array, fix one number, use two pointers on remaining
Container With Most Water Medium Two Pointers Start from edges; move the shorter pointer inward
Merge Intervals Medium Sorting Sort by start time, merge overlapping intervals greedily
Trapping Rain Water Hard Two Pointers / Stack Water at each index = min(max_left, max_right) - height[i]

Full Solution: Two Sum

Problem: Given an array of integers nums and a target, return the indices of the two numbers that add up to the target. You may assume each input has exactly one solution.

Example

Input: nums = [2, 7, 11, 15], target = 9

Output: [0, 1] because nums[0] + nums[1] = 2 + 7 = 9

Approach: For each number, its complement is target - number. We store each number we have seen in a hash map. If the complement of the current number exists in the map, we found our pair.

Brute force: Check every pair --> O(n^2) Hash map: One pass, store seen values --> O(n) time, O(n) space
Python
def two_sum(nums, target):
    """
    LeetCode 1 - Two Sum
    Return indices of two numbers that
    add up to target.
    Time: O(n) | Space: O(n)
    """
    # Map: value -> index
    seen = {}

    for i, num in enumerate(nums):
        complement = target - num

        # Have we seen the complement?
        if complement in seen:
            return [seen[complement], i]

        # Store current number and index
        seen[num] = i

    return []  # No solution

# Test cases
print(two_sum([2, 7, 11, 15], 9))
# [0, 1]

print(two_sum([3, 2, 4], 6))
# [1, 2]

print(two_sum([3, 3], 6))
# [0, 1]
JavaScript
function twoSum(nums, target) {
    /**
     * LeetCode 1 - Two Sum
     * Return indices of two numbers that
     * add up to target.
     * Time: O(n) | Space: O(n)
     */
    // Map: value -> index
    const seen = new Map();

    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];

        // Have we seen the complement?
        if (seen.has(complement)) {
            return [seen.get(complement), i];
        }

        // Store current number and index
        seen.set(nums[i], i);
    }

    return [];  // No solution
}

// Test cases
console.log(twoSum([2, 7, 11, 15], 9));
// [0, 1]

console.log(twoSum([3, 2, 4], 6));
// [1, 2]

console.log(twoSum([3, 3], 6));
// [0, 1]
Why This Works

At index i, we ask: "Have I already seen a number that, combined with nums[i], equals the target?" The hash map lookup is O(1), so the total time is O(n). We only need one pass through the array.

Full Solution: Maximum Subarray (Kadane's Algorithm)

Problem: Given an integer array nums, find the contiguous subarray with the largest sum and return its sum.

Example

Input: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

Output: 6 (subarray [4, -1, 2, 1] has the largest sum)

Approach (Kadane's Algorithm): At each index, you make a simple decision: either extend the current subarray by adding the current element, or start a new subarray from the current element. You pick whichever gives a larger value. Track the global maximum as you go.

Kadane's core logic: current_sum = max(nums[i], current_sum + nums[i]) max_sum = max(max_sum, current_sum) If current_sum + nums[i] < nums[i], the running sum is negative and "drags down" the current element. Better to start fresh.
Python
def max_subarray(nums):
    """
    LeetCode 53 - Maximum Subarray
    Kadane's Algorithm.
    Time: O(n) | Space: O(1)
    """
    current_sum = nums[0]
    max_sum = nums[0]

    for i in range(1, len(nums)):
        # Extend or start fresh?
        current_sum = max(
            nums[i],
            current_sum + nums[i]
        )
        # Update global max
        max_sum = max(max_sum, current_sum)

    return max_sum

# Test cases
print(max_subarray(
    [-2, 1, -3, 4, -1, 2, 1, -5, 4]
))
# 6  (subarray [4, -1, 2, 1])

print(max_subarray([1]))
# 1

print(max_subarray([-1, -2, -3]))
# -1  (least negative = largest)

print(max_subarray([5, 4, -1, 7, 8]))
# 23  (entire array)
JavaScript
function maxSubarray(nums) {
    /**
     * LeetCode 53 - Maximum Subarray
     * Kadane's Algorithm.
     * Time: O(n) | Space: O(1)
     */
    let currentSum = nums[0];
    let maxSum = nums[0];

    for (let i = 1; i < nums.length; i++) {
        // Extend or start fresh?
        currentSum = Math.max(
            nums[i],
            currentSum + nums[i]
        );
        // Update global max
        maxSum = Math.max(maxSum, currentSum);
    }

    return maxSum;
}

// Test cases
console.log(maxSubarray(
    [-2, 1, -3, 4, -1, 2, 1, -5, 4]
));
// 6  (subarray [4, -1, 2, 1])

console.log(maxSubarray([1]));
// 1

console.log(maxSubarray([-1, -2, -3]));
// -1  (least negative = largest)

console.log(maxSubarray([5, 4, -1, 7, 8]));
// 23  (entire array)
Step-by-Step Trace

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

 

i=0: current=-2, max=-2

i=1: max(1, -2+1) = max(1, -1) = 1, max=1

i=2: max(-3, 1-3) = max(-3, -2) = -2, max=1

i=3: max(4, -2+4) = max(4, 2) = 4, max=4

i=4: max(-1, 4-1) = max(-1, 3) = 3, max=4

i=5: max(2, 3+2) = max(2, 5) = 5, max=5

i=6: max(1, 5+1) = max(1, 6) = 6, max=6

i=7: max(-5, 6-5) = max(-5, 1) = 1, max=6

i=8: max(4, 1+4) = max(4, 5) = 5, max=6

 

Answer: 6 (subarray [4, -1, 2, 1] from indices 3 to 6)

7. Practice Quiz

Test your understanding of arrays and strings. Click an answer to check it.

Q1: What is the time complexity of accessing an element by index in an array?

B) O(1). Arrays store elements contiguously in memory. The address of any element can be computed directly: base_address + (index * element_size). This is a single arithmetic operation regardless of array size, making it constant time.

Q2: You have a sorted array and need to find two numbers that add up to a target. Which technique is most efficient?

C) Two pointers. Since the array is sorted, start with pointers at both ends. If the sum is too small, move the left pointer right. If too large, move the right pointer left. This finds the pair in O(n) time with O(1) space -- better than brute force O(n^2) and binary search O(n log n).

Q3: What happens when you insert an element at the beginning of a dynamic array of size n?

B) O(n). To make room at index 0, every existing element must be copied one position to the right. With n elements, that is n shift operations. This is why inserting at the beginning of an array is expensive -- and why linked lists exist for frequent front-insertions.

Q4: In Kadane's Algorithm for Maximum Subarray, what decision is made at each index?

B) Extend or start new. At each index i, Kadane's computes max(nums[i], current_sum + nums[i]). If the running sum has gone negative, adding to it would only drag down the current element, so it is better to start a fresh subarray from the current element.

Q5: Why is string concatenation in a loop (e.g., s += char) considered O(n^2) in Python?

B) Immutability causes copying. Since strings cannot be modified in-place, s += char creates an entirely new string and copies all existing characters plus the new one. After n iterations, you have copied 1 + 2 + 3 + ... + n = n(n+1)/2 characters total, which is O(n^2). The fix: collect characters in a list and use "".join(list) at the end for O(n) total.