Arrays ─ The Foundation

Hi everyone ✋

Welcome to the very first stop on our DSA Deep Dive roadmap 🗺️ ─ Array & Hashing. This is the foundation the entire tree grows from, so we’re going to take our time and cover it properly across a few posts. And there’s no better place to begin than with the humble array ─ the data structure you’ve used a hundred times, and the one nearly every other structure is secretly built on.

Today’s post is all about arrays as a problem-solving tool ─ what they really are under the hood, which operations are cheap and which are costly, and the handful of array techniques (in-place tricks, two passes, and prefix sums) that show up in problem after problem.

Let’s take a deep dive 🦴

What is an array, really? 📦

An array is a collection of elements of the same type, stored right next to each other in memory ─ one contiguous block, no gaps. That single property, contiguity, is the source of everything special about arrays.

If we’ve read the pointers series, this will ring a bell ─ the name of an array is really the address of its first element, and the elements march along from there, each exactly one sizeof(element) after the last. That’s why nums[i] and *(nums + i) mean the exact same thing ─

#include <iostream>
using namespace std;

int main()
{
    int nums[5] = { 10, 20, 30, 40, 50 };

    cout << nums[2]     << endl;   // 30
    cout << *(nums + 2) << endl;   // 30 ─ indexing is just address math!

    return 0;
}

The output will be ─

30
30

Both lines reach the same box, because nums[2] is defined as “start at nums, jump forward 2 elements, and look there.” Hold on to that idea ─ it’s the reason arrays are so fast. Let’s picture it.

My little reference ─ a row of numbered lockers 🔢

Here’s the picture to keep in mind ─ an array is a row of identical, numbered lockers 🔢, all the same size, lined up wall to wall with no space between them. Locker 0, locker 1, locker 2, and so on. Each locker holds exactly one item of the same kind.

Because the lockers are uniform and lined up, we can walk straight to locker #7 without opening 0 through 6 first ─ we just count “seven locker-widths from the start” and we’re there. That’s instant access, and it’s the array’s superpower.

But the same rigid layout has a cost. The wall was built with a fixed number of lockers (a plain array can’t grow). And if we ever want to insert a new locker in the middle ─ say, at position 3 ─ then locker 3 and everyone after has to shuffle down one spot to make room. Neat and fast to reach; slow to rearrange. Keep that row of lockers in mind ─ every array trade-off below comes straight from it.

Why indexing is instant ─ O(1)

Let’s make the locker magic precise. To find element i, the computer does one small calculation ─

address of nums[i]  =  base address  +  i × sizeof(element)

That’s it ─ a multiply and an add, no matter how big the array is. Reaching nums[5] in a 10-element array costs exactly the same as reaching nums[5] in a ten-million-element array. This is what we mean by O(1)constant time, independent of the array’s size ⚡. It’s the single most useful guarantee in all of problem solving, and it’s why arrays sit underneath so many other structures.

Choosing our array ─ raw array vs std::array vs vector 📦

C++ actually gives us three array-like tools, and picking the right one is a complexity decision more than a style one. Let’s line them up ─

  • Raw C array (int nums[5]) ─ the classic. Size fixed at compile time, lives on the stack, decays to a bare pointer (so it doesn’t even know its own size).
  • std::array<int, 5> ─ the modern fixed-size array. Same stack storage and zero overhead as a raw array, but it’s a proper container ─ it knows its .size() and doesn’t decay.
  • std::vector<int> ─ the dynamic array. Lives on the heap and grows as we add to it, managing its own memory.
#include <iostream>
#include <array>
#include <vector>
using namespace std;

int main()
{
    array<int, 3> fixed = { 1, 2, 3 };   // fixed-size, stack, knows its size

    vector<int> dynamic = { 1, 2, 3 };
    dynamic.push_back(4);                // grow on the fly
    dynamic.push_back(5);

    cout << "fixed size:   " << fixed.size()   << endl;
    cout << "dynamic size: " << dynamic.size() << endl;

    return 0;
}

The output will be ─

fixed size:   3
dynamic size: 5

Here’s the part that actually decides it ─ the complexity ─

Property Raw array / std::array std::vector
Access / update [i] O(1) O(1)
Append an element ✗ can’t grow O(1) amortised
Insert / erase in middle O(n) (manual) O(n)
Size fixed at compile time dynamic at run time
Lives on the stack (fast, but small) the heap (large-friendly)
Knows .size() raw: ✗ · std::array: ✓

Read that table and the verdict writes itself ─ for the same O(1) access, a vector gives us everything a fixed array does plus the freedom to grow, at no asymptotic cost. So in this series, vector is our default. 🥇

The two reasons we’d ever pick a fixed-size array come down to complexity and constants too ─

  1. The size is a small, known compile-time constant and we want the last drop of constant-factor speed ─ then std::array skips even the tiny heap bookkeeping a vector carries. (Prefer std::array over the raw C array; same speed, but safer.)
  2. We’re storing something genuinely large. The stack is small (a few megabytes), so a huge fixed array like int big[10'000'000] inside a function will overflow the stack and crash. A vector puts that data on the heap, where big allocations belong. This alone makes vector the correct choice for large inputs ─ a real complexity-of-memory fact, not a preference.

About that “amortised O(1)” for push_back ─ it’s worth understanding, because it’s a lovely piece of complexity reasoning. When a vector fills up, it doesn’t grow by one; it doubles its capacity and copies everything over. That copy is O(n), but it happens rarely enough that, averaged over all the appends, each one costs O(1). We can watch it happen ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> v;
    for (int i = 0; i < 9; i++) {
        v.push_back(i);
        cout << "size=" << v.size() << "  capacity=" << v.capacity() << endl;
    }
    return 0;
}

The output (on this compiler) will be ─

size=1  capacity=1
size=2  capacity=2
size=3  capacity=4
size=4  capacity=4
size=5  capacity=8
size=6  capacity=8
size=7  capacity=8
size=8  capacity=8
size=9  capacity=16

See the capacity double ─ 1 → 2 → 4 → 8 → 16 ─ only when it’s exceeded. That doubling is exactly what makes push_back amortised O(1). And if we already know roughly how many elements are coming, we can skip the reallocations entirely with reserve

vector<int> v;
v.reserve(1000);   // pre-allocate capacity once ─ no reallocation while growing

reserve is a genuine constant-factor win worth remembering (that quiet reallocation-and-copy is the very hazard from the dangling pointers post ─ here we’re sidestepping it). One more complexity fact in the vector‘s favour ─ because its data is one contiguous heap block, it enjoys the same excellent cache locality as a raw array, so iterating it is fast in practice, not just on paper. For a deeper tour, our STL vector post has us covered. From here on, “array” mostly means vector.

The real cost of array operations ⏱️

To solve problems well, we need to know exactly which array operations are cheap and which are expensive ─ because that knowledge is what turns a slow solution into a fast one. Here’s the whole story in one table.

Operation Cost Why
Access nums[i] O(1) direct address math
Update nums[i] = x O(1) write straight to the spot
push_back (add to end) O(1) amortised usually just a write; occasionally grows
Insert / erase at the front or middle O(n) everything after must shift over
Search an unsorted array O(n) might have to check every element
Search a sorted array (binary search) O(log n) halve the range each step
Iterate over all elements O(n) visit each one once

The big lesson hiding in that table ─ arrays are brilliant at reaching and overwriting a known position, and poor at inserting or removing anywhere but the end. That single fact quietly shapes which problems arrays are the right tool for. Notice, too, that searching an unsorted array is O(n) ─ that’s exactly the weakness hashing will fix in our next post 😉.

Iterating over an array 🔁

Most array work is a loop over the elements, and C++ gives us a few clean ways to write one.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> nums = { 1, 2, 3, 4 };

    // 1) classic index loop ─ when we need the index i
    for (size_t i = 0; i < nums.size(); i++)
        cout << nums[i] << " ";
    cout << endl;

    // 2) range-based for by reference ─ modify each element in place
    for (int &n : nums)
        n *= 10;

    // 3) range-based for by const reference ─ read only, no copy
    for (const int &n : nums)
        cout << n << " ";
    cout << endl;

    return 0;
}

The output will be ─

1 2 3 4 
10 20 30 40 

Reach for the index loop when the position i itself matters (comparing neighbours, or two-pointer work). Reach for for (int& n : nums) to change elements in place, and for (const int& n : nums) to read them without copying ─ exactly the reference habits from the pointers vs references post 🥳.

Modifying in place ─ saving space 💾

Here’s a habit that separates a rough solution from an elegant one ─ whenever we can, we change the array itself instead of building a brand-new one. That drops our extra memory from O(n) to O(1), and interviewers love it.

A perfect example is reversing an array using two indices that walk toward each other, swapping as they go ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> nums = { 1, 2, 3, 4, 5 };

    int left = 0, right = nums.size() - 1;
    while (left < right) {
        swap(nums[left], nums[right]);   // swap the ends
        left++;
        right--;                          // walk inward
    }

    for (int n : nums) cout << n << " ";
    cout << endl;

    return 0;
}

The output will be ─

5 4 3 2 1 

No second array, no extra memory ─ just two indices closing in from the ends. (And notice we’ve quietly met our next roadmap topic, Two Pointers, hiding in plain sight 👀.) The mindset is worth internalising early ─ before allocating a whole new array, ask “could I just rearrange the one I already have?”

The two-pass idea 🔄

Sometimes a single loop isn’t enough, because acting on an element requires knowing something about the whole array first. The fix is beautifully simple ─ make two passes. Use the first pass to gather the fact we need, then a second pass to act on it.

Say we want to count how many numbers are above the array’s average. We can’t judge “above average” until we’ve seen every number ─ so pass one computes the average, and pass two does the counting ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> nums = { 4, 8, 6, 2 };

    // PASS 1 ─ compute the total, then the average
    int sum = 0;
    for (int n : nums) sum += n;
    double avg = (double)sum / nums.size();

    // PASS 2 ─ now that we know the average, count what beats it
    int aboveAvg = 0;
    for (int n : nums)
        if (n > avg) aboveAvg++;

    cout << "average = " << avg << ", above average = " << aboveAvg << endl;

    return 0;
}

The output will be ─

average = 5, above average = 2

Two clean O(n) passes still total O(n) ─ making two passes doesn’t slow us down in Big-O terms, and it keeps the logic clear. Whenever you hit a problem that needs global information before acting locally, reach for two passes. This exact shape ─ “gather first, act second” ─ leads us straight into the single most important array technique 👇.

Prefix sums ─ precompute once, answer instantly 🧮

Here’s the array technique that shows up everywhere ─ prefix sums (also called running totals). The idea: do a little work up front so that later questions become instant.

Imagine we’ll be asked, over and over, “what’s the sum of the elements between index l and r?” Answering each query by looping is O(n) per question. Instead, we build a prefix array once, where prefix[i] holds the sum of everything before index i. Then any range sum becomes a single subtraction ─ O(1) per query 🎉.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> nums = { 3, 1, 4, 1, 5, 9 };

    // prefix[i] = sum of nums[0 .. i-1]; prefix[0] = 0
    vector<int> prefix(nums.size() + 1, 0);
    for (size_t i = 0; i < nums.size(); i++)
        prefix[i + 1] = prefix[i] + nums[i];

    // sum of nums[l..r]  =  prefix[r+1] - prefix[l]
    int l = 1, r = 3;                        // nums[1..3] = 1 + 4 + 1
    cout << "sum of [" << l << ".." << r << "] = "
         << prefix[r + 1] - prefix[l] << endl;

    return 0;
}

The output will be ─

sum of [1..3] = 6

We paid O(n) once to build the prefix array, and now every range-sum question is a single subtraction. That “precompute once, answer forever” trade ─ spend a bit of setup and memory to make later work trivial ─ is one of the most powerful patterns in all of problem solving, and it’s the seed of dynamic programming much later on the roadmap 🌱.

The challenges of arrays ─ and how to beat them ⚠️

Arrays are simple, but they come with a handful of classic traps. Let’s meet each one with a concrete example ─ and its fix ─ because seeing the trap fire is the best way to remember to dodge it.

1 ─ Out-of-bounds access. Reading or writing past the end is undefined behaviour ─ no error, just silent corruption or a crash (the wild-pointer hazard from the dangling pointers post). The safety net is .at(), which throws instead of running off the end ─

#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;

int main()
{
    vector<int> nums = { 1, 2, 3 };

    // nums[5];          // ☠️ undefined behaviour ─ silent, no warning

    try {
        nums.at(5);      // ✅ safe ─ throws std::out_of_range
    } catch (const out_of_range&) {
        cout << "caught out-of-range access" << endl;
    }
    return 0;
}

Output ─ caught out-of-range access. Use [] in hot loops (fast, unchecked) and .at() when we want the guard rail ─ a genuine speed-vs-safety trade.

2 ─ Off-by-one errors. The < vs <=, the i + 1, the size() - 1 ─ boundaries are where bugs breed. Whenever we peek at a neighbour nums[i + 1], the loop must stop before the last element, or i + 1 runs off the end ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> nums = { 1, 3, 2, 4 };

    // compare each pair (i, i+1) ─ note the "+ 1 < size()"
    bool increasing = true;
    for (size_t i = 0; i + 1 < nums.size(); i++)   // ✅ keeps i+1 in bounds
        if (nums[i] >= nums[i + 1]) increasing = false;

    cout << (increasing ? "increasing" : "not increasing") << endl;
    return 0;
}

Output ─ not increasing. The fix is a habit: prefer half-open ranges [l, r), write the bound so the largest index touched is valid, and always test the tiny cases ─ empty, one element, two elements.

3 ─ The unsigned size() trap. nums.size() returns an unsigned type, so size() - 1 on an empty vector isn’t -1 ─ it wraps around to a gigantic number ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> empty;

    cout << "size() - 1      = " << (empty.size() - 1)      << endl;  // ☠️ huge!
    cout << "(int)size() - 1 = " << ((int)empty.size() - 1) << endl;  // ✅ -1
    return 0;
}

Output ─

size() - 1      = 18446744073709551615
(int)size() - 1 = -1

That monstrous number is what wrecks a loop like for (int i = nums.size() - 1; i >= 0; i--) on empty input. Fix ─ cast to signed when subtracting, or guard with if (!nums.empty()) first.

4 ─ Costly middle insert / erase — O(n). Removing anywhere but the end shifts every later element down to fill the gap ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> v = { 10, 20, 30, 40 };

    v.erase(v.begin() + 1);   // ☠️ O(n) ─ 30 and 40 shift left to close the hole

    for (int n : v) cout << n << " ";
    cout << endl;
    return 0;
}

Output ─ 10 30 40. That shift is the cost. Fix ─ if order doesn’t matter, use the swap-and-pop trick from the Tips section to delete in O(1); and if we truly need cheap middle insertion, that’s a signal for a different structure (a linked list, later on the roadmap).

5 ─ Slow search in unsorted data — O(n). Finding a value means scanning the whole thing. If we’ll search repeatedly, we pre-process once so each lookup gets cheap ─

#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_set>
using namespace std;

int main()
{
    vector<int> nums = { 5, 2, 8, 1, 9 };

    // ☠️ linear scan ─ O(n) every single time
    bool found = false;
    for (int n : nums) if (n == 8) found = true;
    cout << "linear:        " << found << endl;

    // ✅ A) sort once (O(n log n)), then binary_search ─ O(log n) each
    sort(nums.begin(), nums.end());
    cout << "binary_search: " << binary_search(nums.begin(), nums.end(), 8) << endl;

    // ✅ B) drop into a hash set ─ O(1) average per lookup
    unordered_set<int> seen(nums.begin(), nums.end());
    cout << "hash set:      " << seen.count(8) << endl;
    return 0;
}

Output ─ linear: 1, binary_search: 1, hash set: 1. Sorting unlocks O(log n) search; hashing unlocks O(1) ─ and that hashing route is exactly where our next post goes 😉.

6 ─ Reallocation invalidates pointers and iterators. When a vector outgrows its capacity, it moves its storage to a bigger block ─ and any raw pointer, reference, or iterator into the old block now dangles ─

#include <iostream>
#include <vector>
#include <cstdint>
using namespace std;

int main()
{
    vector<int> v = { 1, 2, 3 };
    v.reserve(3);                          // capacity is now exactly full

    uintptr_t before = (uintptr_t)&v[0];   // address of the storage
    v.push_back(4);                        // exceeds capacity ─ storage moves!
    uintptr_t after  = (uintptr_t)&v[0];

    cout << "storage moved? " << (before != after) << endl;   // ☠️ 1 ─ old pointer dangles
    cout << "v[0] via index = " << v[0] << endl;              // ✅ index still valid
    return 0;
}

Output ─ storage moved? 1, v[0] via index = 1. A pointer captured before the push_back would now point at freed memory. Fix ─ reserve up front when the size is known, or store indices (which survive a resize) instead of pointers.

7 ─ Accidental O(n) copies. Passing a big array by value silently copies every element on each call ─

#include <iostream>
#include <vector>
using namespace std;

// ☠️ by value ─ copies the ENTIRE vector every call
long sumByValue(vector<int> v)      { long s = 0; for (int n : v) s += n; return s; }

// ✅ by const reference ─ zero copy, read-only
long sumByRef(const vector<int>& v) { long s = 0; for (int n : v) s += n; return s; }

int main()
{
    vector<int> big(5, 7);   // { 7, 7, 7, 7, 7 }
    cout << sumByValue(big) << " " << sumByRef(big) << endl;
    return 0;
}

Output ─ 35 35. Same answer, but sumByValue paid an O(n) copy for nothing. Fix ─ pass by const vector<int>& to read it for free ─ the reference habit from the pointers vs references post.

Every one of these fixes is really the same move ─ know the cost, then refuse to pay it needlessly.

Tips & tricks of the array 🧰

A handful of array moves come up again and again ─ collect these and a huge slice of problems starts to look familiar.

1 ─ Difference arrays for O(1) range updates. Prefix sums gave us O(1) range queries; their mirror image, the difference array, gives O(1) range updates. To add a value across a whole range, we bump just two endpoints, then take a single prefix sum at the end to materialise the result ─

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int n = 5;
    vector<int> diff(n + 1, 0);

    diff[0] += 2;  diff[3] -= 2;   // add +2 across range [0..2]
    diff[1] += 3;  diff[4] -= 3;   // add +3 across range [1..3]

    vector<int> arr(n);
    arr[0] = diff[0];
    for (int i = 1; i < n; i++)
        arr[i] = arr[i - 1] + diff[i];   // one prefix pass rebuilds it

    for (int x : arr) cout << x << " ";
    cout << endl;
    return 0;
}

The output will be ─

2 5 5 3 0 

Each range update was O(1); a thousand of them still cost only one final O(n) pass 🎉.

2 ─ Swap-and-pop for O(1) removal. When order doesn’t matter, deleting from the middle needn’t be O(n) ─ swap the target with the last element, then pop_back

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> v = { 10, 20, 30, 40 };

    int i = 1;                  // remove value 20 (order doesn't matter)
    swap(v[i], v.back());       // move it to the end...
    v.pop_back();               // ...and drop the end ─ O(1)!

    for (int n : v) cout << n << " ";
    cout << endl;
    return 0;
}

The output will be ─

10 40 30 

3 ─ The array as its own hash (in-place marking). When values fall in a known range like [1, n], we can record “seen” inside the array itself ─ often by negating the slot a value points to ─ turning an O(n)-space set into O(1) extra space. Here we find which numbers in [1, n] are missing ─

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

int main()
{
    vector<int> nums = { 4, 3, 2, 7, 8, 2, 3, 1 };   // n=8, values in [1..8]

    // mark each value as "seen" by negating the slot it points to
    for (int x : nums) {
        int idx = abs(x) - 1;
        if (nums[idx] > 0) nums[idx] = -nums[idx];
    }

    // a still-positive slot i means the value (i+1) never appeared
    for (size_t i = 0; i < nums.size(); i++)
        if (nums[i] > 0) cout << (i + 1) << " ";
    cout << endl;
    return 0;
}

The output will be ─

5 6 

4 ─ Sort to unlock. Many problems collapse once the data is sorted ─ a one-time O(n log n) sort can turn an O(n²) scan into an O(n) two-pointer sweep or an O(log n) binary search. Always ask “would sorting make this easy?”

5 ─ A few smaller habits. reserve when the final size is known (skip the reallocation churn); iterate backwards when erasing so removals don’t shift elements we haven’t reached; and keep hot inner loops simple, since arrays’ cache-friendliness rewards tight code.

These aren’t separate facts to memorise ─ they’re all one instinct: reshape or precompute the array so the expensive part becomes cheap 🥳.

The one-line decision 🌟

Boil all of that down and picking an array becomes a reflex ─ if a task is mostly indexing and iterating, an array (a vector) is the perfect tool, cheap and cache-friendly. If it’s mostly searching and inserting, arrays start to hurt (O(n)), and we should reach for a hash table or a different structure instead ─ which is precisely the road ahead 🛣️.

A gentle warm-up ─ Running Sum (LeetCode 1480) 🏋️

Let’s finish by turning our prefix-sum idea into an actual solved problem, as a taste of what the problem-solving posts will feel like.

The problem (Running Sum of 1d Array, #1480, paraphrased): given an array nums, return a new array where each position holds the running total up to and including that index ─ so element i becomes nums[0] + nums[1] + … + nums[i].

The intuition: this is a prefix sum ─ each running total is just the previous running total plus the current element. And since each element only needs the one before it, we can build the answer in place, with no extra array at all.

#include <iostream>
#include <vector>
using namespace std;

vector<int> runningSum(vector<int>& nums)
{
    for (size_t i = 1; i < nums.size(); i++)
        nums[i] += nums[i - 1];   // each element absorbs the running total
    return nums;
}

int main()
{
    vector<int> nums = { 1, 2, 3, 4 };
    vector<int> result = runningSum(nums);

    for (int n : result) cout << n << " ";
    cout << endl;

    return 0;
}

The output will be ─

1 3 6 10 

The walkthrough: we start at index 1 and let each element soak up its left neighbour ─ nums[1] += nums[0]3, then nums[2] += nums[1]6, then nums[3] += nums[2]10. One pass, done. Time: O(n), a single sweep. Space: O(1) extra, because we reused the input array. That elegant little in-place prefix sum ─ built from ideas we covered today ─ is a genuine O(n) / O(1) solution to a real LeetCode problem 🥳.

So what should we remember? 🤔

Let’s wrap up the foundations ─

  • An array stores same-type elements in one contiguous block, which is what makes it fast.
  • Picture a row of numbered lockers 🔢 ─ instant to reach any locker, but rigid to rearrange.
  • Indexing is O(1)nums[i] is just base + i × sizeof(element), so access costs the same no matter the size.
  • Default to std::vector ─ it matches a fixed array’s O(1) access, adds amortised O(1) push_back, and lives on the heap so large data won’t overflow the stack. Choose std::array only for a small fixed compile-time size; reserve when the count is known.
  • Know the cost table cold ─ access/update O(1), push_back O(1) amortised (via capacity doubling), but front/middle insert-or-erase and unsorted search are O(n).
  • Iterate with an index loop when the position matters, for (int& n : nums) to modify, for (const int& n : nums) to read (and to pass big arrays for free).
  • Modify in place to keep extra space at O(1), and use the two-pass idea when acting locally needs global info first.
  • Prefix sums precompute running totals once so range-queries become O(1); difference arrays do the mirror for range-updates ─ patterns that echo all the way to dynamic programming.
  • Dodge the classic traps ─ out-of-bounds (use .at() for a safety net), off-by-one (prefer half-open ranges), the unsigned-size() underflow, and reallocation invalidating pointers (store indices, or reserve).
  • Keep the tricks handy ─ swap-and-pop for O(1) removal, the array-as-its-own-hash for O(1) space, and sort to unlock two-pointer and binary-search speed-ups.

Arrays are simple, but simple is powerful ─ almost every technique later in this series is, at heart, a clever way of walking or reshaping an array 👀✨.

Congratulations 🥳🥳🥳
We’ve laid the foundation ─ the structure everything else is built on ─ and even solved our first LeetCode problem with it.

In the next post we tackle the array’s perfect partner ─ hashing ─ the trick that turns that painful O(n) search into a near-instant O(1) lookup, and unlocks a huge family of problems 😉

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *