Free Handbook · Every example compiled & verified

Problem Solving

A repeatable way to crack coding problems in PHP: read it, break it down, test small cases, then apply one of eight patterns that cover most interviews.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 14 · what you'll be able to do

  • Turn a problem statement into inputs, outputs, constraints and edge cases before writing any code
  • Test a solution with a tiny hand-rolled check function instead of guessing
  • Estimate the Big-O a problem needs from its input limits, in plain English
  • Recognise and apply eight patterns in PHP: two pointers, sliding window, array-as-hash-map, stack, backtracking, sorting, BFS/DFS and DP-lite
01

Read the problem before touching the keyboard

Most failed coding rounds fail in the first two minutes: the candidate starts typing a solution to a problem they have not fully read. Slow down and write these five things down (in a comment, on the whiteboard, or out loud) before any code.

  1. 1
    Restate it

    One sentence, in your own words. "Given a list of prices, return the biggest profit from one buy followed by one later sell."

  2. 2
    Inputs and outputs, with types

    array $prices of ints in, int out. Declare them: function maxProfit(array $prices): int. Can the array be empty? What is returned then?

  3. 3
    Constraints

    How big is n? Can values be negative? Duplicates? Already sorted? These decide the algorithm (see the Big-O lesson below).

  4. 4
    Two examples by hand

    One normal, one tricky. Work them out on paper. If you cannot do it by hand, you cannot code it.

  5. 5
    Edge cases

    Empty input, one element, all equal, already sorted, numeric strings where you expected ints ("10" < "9" compares as numbers, but "10a" < "9" compares as strings, see Module 01).

phpmain.php
<?php
// Restated: best profit from one buy then one later sell; 0 if prices only fall.
// Input: list of ints (may be empty). Output: int >= 0.
function maxProfit(array $prices): int
{
    $lowest = PHP_INT_MAX;
    $best = 0;
    foreach ($prices as $price) {
        $lowest = min($lowest, $price);       // cheapest buy so far
        $best = max($best, $price - $lowest); // what if I sell today?
    }
    return $best;
}

echo maxProfit([7, 1, 5, 3, 6, 4]), "\n"; // normal: buy at 1, sell at 6
echo maxProfit([7, 6, 4, 3, 1]), "\n";    // prices only fall
echo maxProfit([]), "\n";                 // empty
echo maxProfit([5]), "\n";                // one day
Outputcompiled & run with real PHP
5
0
0
0

The two comments at the top are the reading step. The four calls are the examples and edge cases from that step, written before the function body, not bolted on after.

Your turn

Change the contract to "return [buyDay, sellDay], or null if no profit is possible". Which of the four calls now needs a different expected answer?

02

Break it down and test with small cases

Break the problem into pieces you can test separately: a helper that checks one thing, a loop that applies it. Then test with the smallest inputs that could break it. In an interview, and in your own practice, a tiny check() function is faster than setting up PHPUnit, and it shows the interviewer you verify your own code.

phpmain.php
<?php
function isPalindrome(string $s): bool
{
    $i = 0;
    $j = strlen($s) - 1;
    while ($i < $j) {
        if (!ctype_alnum($s[$i])) { $i++; continue; }
        if (!ctype_alnum($s[$j])) { $j--; continue; }
        if (strtolower($s[$i]) !== strtolower($s[$j])) return false;
        $i++;
        $j--;
    }
    return true;
}

$passed = $failed = 0;
$check = function (string $input, bool $expected) use (&$passed, &$failed): void {
    $got = isPalindrome($input);
    if ($got === $expected) { $passed++; return; }
    $failed++;
    echo "FAIL '$input': expected ", var_export($expected, true), ', got ', var_export($got, true), "\n";
};

$check('', true);                          // empty
$check('a', true);                         // one character
$check('ab', false);                       // smallest false case
$check('Aba', true);                       // mixed case
$check('A man, a plan, a canal: Panama', true);
$check('race a car', false);
$check('.,', true);                        // only punctuation
echo "$passed passed, $failed failed\n";
Outputcompiled & run with real PHP
7 passed, 0 failed

The closure captures $passed and $failed by reference (use (&$passed, &$failed)). Without the & it would count into copies and always print 0.

Your turn

Break isPalindrome on purpose (drop the two strtolower calls) and run it. Exactly two cases report FAIL. That is how a good small-case list pinpoints a bug.

The order to test in
Empty, one element, two elements, the example from the problem, then one case aimed at each branch of your code. Small inputs make a wrong answer obvious by eye.
03

Big-O in plain English

Big-O answers one question: if the input gets ten times bigger, how much slower does this get? O(n) gets ten times slower. O(n²) gets a hundred times slower. O(log n) barely notices. PHP does very roughly 107 to 108 simple operations per second (with OPcache and the JIT towards the top of that range), which turns the constraints in a problem statement into a direct hint about which algorithm is expected.

Rules of thumb, not laws. They are good enough to rule out the wrong approach before you write it.
If n is up to…You can affordTypical approach
10 to 20O(2n) or O(n!)Backtracking: try every subset or ordering
~300O(n³)Three nested loops, small DP tables
~3,000O(n²)Two nested loops, 2-D DP
~105 to 106O(n log n)Sort first, a heap, binary search inside a loop
~107O(n)One pass: two pointers, sliding window, array-as-hash-map
Anything largerO(log n) or O(1)Binary search on the answer, a formula
phpmain.php
<?php
// O(n^2): compare every pair
function nestedOps(array $a): int
{
    $ops = 0;
    $n = count($a);
    for ($i = 0; $i < $n; $i++) {
        for ($j = $i + 1; $j < $n; $j++) {
            $ops++;
            if ($a[$i] === $a[$j]) return $ops;
        }
    }
    return $ops;
}

// O(n): remember what we have seen, as array KEYS
function setOps(array $a): int
{
    $ops = 0;
    $seen = [];
    foreach ($a as $x) {
        $ops++;
        if (isset($seen[$x])) return $ops;
        $seen[$x] = true;
    }
    return $ops;
}

foreach ([1_000, 10_000] as $n) {
    $a = range(0, $n - 1); // no duplicates: the worst case
    echo "n=$n  nested=", nestedOps($a), '  set=', setOps($a), "\n";
}
Outputcompiled & run with real PHP
n=1000  nested=499500  set=1000
n=10000  nested=49995000  set=10000

Ten times the input: the nested version does a hundred times the work, the set version ten times. At n = 106 the nested version would need half a trillion comparisons.

Your turn

Add a third function that sorts a copy and compares neighbours. Count its operations as n for the neighbour pass. What is its Big-O, including the sort?

04

Pattern 1: two pointers

Signal: a sorted array (or a string) and a question about pairs, or "do it in place". Put one index at each end and move them towards each other based on a comparison. Each step rules out a whole row of candidate pairs, so an O(n²) "try every pair" becomes O(n) with O(1) extra memory.

phpmain.php
<?php
// sorted input: indices of two numbers that add up to $target, or null
function pairSum(array $a, int $target): ?array
{
    $i = 0;
    $j = count($a) - 1;
    while ($i < $j) {
        $sum = $a[$i] + $a[$j];
        if ($sum === $target) return [$i, $j];
        if ($sum < $target) $i++; // need bigger: move the left pointer up
        else $j--;                // need smaller: move the right pointer down
    }
    return null;
}

$a = [1, 3, 4, 6, 8, 11];
echo json_encode(pairSum($a, 10)), "\n";
echo json_encode(pairSum($a, 2)), "\n";
Outputcompiled & run with real PHP
[2,3]
null
Your turn

Write sortedSquares(array $a): array for a sorted array that may hold negatives: [-4, -1, 0, 3, 10] gives [0, 1, 9, 16, 100]. Fill the result from the back, each time taking the larger of abs($a[$i]) and abs($a[$j]).

VisualizepairSum([1, 3, 4, 6, 8, 11], 10)Step 1 / 8
$i = 0;
$j = count($a) - 1;
while ($i < $j) {
$sum = $a[$i] + $a[$j];
if ($sum === $target) return [$i, $j];
if ($sum < $target) $i++;
else $j--;
}
Line 2

Pointers at both ends.

Variables now
$i0
$j5
All 8 steps as a table
StepLineWhat happenedVariables now
12Pointers at both ends.$i = 0 $j = 5
241 + 11 = 12, too big.$sum = 12
37Nothing can pair with 11 any more (1 is the smallest partner), so drop it.$j = 4
441 + 8 = 9, too small.$sum = 9
56Nothing can pair with 1 (8 is now the largest partner), so drop it.$i = 1
643 + 8 = 11, too big: drop 8. Then 3 + 6 = 9, too small: drop 3.$i = 2 $j = 3
744 + 6 = 10.$sum = 10
85Found: indices 2 and 3, after 5 checks instead of up to 15 pairs.
05

Pattern 2: sliding window

Signal: "longest / shortest / best contiguous subarray or substring such that…". Grow a window by moving its right edge; when the window breaks the rule, shrink it from the left. Each index enters and leaves the window at most once, so the whole scan is O(n).

phpmain.php
<?php
// length of the longest substring with no repeated character
function longestUnique(string $s): int
{
    $lastSeen = [];
    $best = $left = 0;
    for ($right = 0; $right < strlen($s); $right++) {
        $c = $s[$right];
        if (isset($lastSeen[$c]) && $lastSeen[$c] >= $left) {
            $left = $lastSeen[$c] + 1; // jump the left edge past the repeat
        }
        $lastSeen[$c] = $right;
        $best = max($best, $right - $left + 1);
    }
    return $best;
}

// fixed-size window: biggest sum of any $k consecutive numbers
function maxSumK(array $a, int $k): int
{
    $sum = array_sum(array_slice($a, 0, $k));
    $best = $sum;
    for ($i = $k; $i < count($a); $i++) {
        $sum += $a[$i] - $a[$i - $k]; // slide: add the new, drop the old
        $best = max($best, $sum);
    }
    return $best;
}

echo longestUnique('abcabcbb'), ' ', longestUnique('bbbb'), ' ', longestUnique('pwwkew'), "\n";
echo maxSumK([2, 1, 5, 1, 3, 2], 3), "\n";
Outputcompiled & run with real PHP
3 1 3
9

The check $lastSeen[$c] >= $left matters: a character last seen before the window started is not a repeat inside it.

Your turn

Write minLengthAtLeast(array $a, int $target): int: the shortest contiguous run of positive numbers whose sum is at least $target (0 if none). Grow right, then shrink left while the sum still qualifies. [2, 3, 1, 2, 4, 3] with target 7 gives 2.

06

Pattern 3: the array as a hash map

Signal: "find two that…", "count how many…", "group by…", "first duplicate". Trade memory for time: remember what you have seen in an array keyed by the thing you will look up later. Every lookup is O(1), so one pass is enough.

phpmain.php
<?php
// unsorted input: indices of two numbers that add up to $target
function twoSum(array $nums, int $target): ?array
{
    $indexOf = [];
    foreach ($nums as $i => $x) {
        $need = $target - $x;
        if (isset($indexOf[$need])) return [$indexOf[$need], $i];
        $indexOf[$x] = $i;
    }
    return null;
}

// group words that are anagrams of each other
function groupAnagrams(array $words): array
{
    $groups = [];
    foreach ($words as $w) {
        $letters = str_split($w);
        sort($letters);
        $groups[implode('', $letters)][] = $w;
    }
    return array_values($groups);
}

echo json_encode(twoSum([2, 7, 11, 15], 9)), "\n";
echo json_encode(twoSum([3, 2, 4], 6)), "\n";
foreach (groupAnagrams(['eat', 'tea', 'tan', 'ate', 'nat', 'bat']) as $g) {
    echo implode(',', $g), "\n";
}
Outputcompiled & run with real PHP
[0,1]
[1,2]
eat,tea,ate
tan,nat
bat

$groups[$key][] = $w creates the inner array on first use. That one line is PHP's version of a "default dict".

Your turn

Write firstUniqueChar(string $s): int: the index of the first character that appears exactly once, or -1. Two passes: count, then find. "leetcode" gives 0, "aabb" gives -1.

07

Pattern 4: stack

Signal: nesting (brackets, tags, paths), "undo", or "the next greater / smaller element". A monotonic stack keeps indexes whose values are still waiting for an answer; each new value pops every waiting index it answers. Every index is pushed and popped once: O(n).

phpmain.php
<?php
// for each day, how many days until a warmer temperature (0 if never)
function daysUntilWarmer(array $temps): array
{
    $answer = array_fill(0, count($temps), 0);
    $waiting = []; // indexes, temperatures decreasing from bottom to top
    foreach ($temps as $i => $t) {
        while ($waiting && $temps[end($waiting)] < $t) {
            $j = array_pop($waiting);
            $answer[$j] = $i - $j;
        }
        $waiting[] = $i;
    }
    return $answer;
}

// simplify a Unix path: /a/./b/../../c/ -> /c
function simplifyPath(string $path): string
{
    $stack = [];
    foreach (explode('/', $path) as $part) {
        if ($part === '' || $part === '.') continue;
        if ($part === '..') array_pop($stack);
        else $stack[] = $part;
    }
    return '/' . implode('/', $stack);
}

echo implode(' ', daysUntilWarmer([73, 74, 75, 71, 69, 72, 76, 73])), "\n";
echo simplifyPath('/a/./b/../../c/'), "\n";
echo simplifyPath('/../'), "\n";
Outputcompiled & run with real PHP
1 1 4 2 1 1 0 0
/c
/
Your turn

Evaluate Reverse Polish Notation: evalRpn(['2', '1', '+', '3', '*']) is 9. Push numbers; on an operator, pop two, apply, push the result.

08

Pattern 5: recursion and backtracking

Signal: "all combinations", "all permutations", "every way to…", and small n (under about 20). Build a candidate one choice at a time; recurse; then undo the choice and try the next one. The undo step (array_pop) is what makes it backtracking.

phpmain.php
<?php
function subsets(array $items): array
{
    $out = [];
    $current = [];
    $walk = function (int $start) use (&$walk, &$out, &$current, $items): void {
        $out[] = $current;                     // every node is a subset
        for ($i = $start; $i < count($items); $i++) {
            $current[] = $items[$i];           // choose
            $walk($i + 1);                     // explore
            array_pop($current);               // un-choose
        }
    };
    $walk(0);
    return $out;
}

function permutations(array $items): array
{
    if (count($items) <= 1) return [$items];
    $out = [];
    foreach ($items as $i => $x) {
        $rest = $items;
        unset($rest[$i]);
        foreach (permutations(array_values($rest)) as $p) {
            $out[] = [$x, ...$p];
        }
    }
    return $out;
}

echo implode(' ', array_map(fn($s) => '[' . implode(',', $s) . ']', subsets(['a', 'b', 'c']))), "\n";
echo implode(' ', array_map(fn($p) => implode('', $p), permutations([1, 2, 3]))), "\n";
Outputcompiled & run with real PHP
[] [a] [a,b] [a,b,c] [a,c] [b] [b,c] [c]
123 132 213 231 312 321

A recursive closure must capture itself by reference (use (&$walk)), because $walk does not exist yet when the closure is created.

Your turn

Write combinationSum(array $candidates, int $target): array: every combination (reuse allowed) that adds up to target. [2, 3, 6, 7] with 7 gives [[2,2,3],[7]]. Stop recursing when the running sum exceeds target.

09

Pattern 6: sort first

Signal: intervals, "closest", "meeting rooms", "can these be scheduled", or any problem that gets easy once things are in order. Paying O(n log n) for usort often turns the rest into a single O(n) pass.

phpmain.php
<?php
function mergeIntervals(array $intervals): array
{
    usort($intervals, fn($a, $b) => $a[0] <=> $b[0]);
    $merged = [];
    foreach ($intervals as [$start, $end]) {
        $last = array_key_last($merged);
        if ($last !== null && $start <= $merged[$last][1]) {
            $merged[$last][1] = max($merged[$last][1], $end); // overlap: extend
        } else {
            $merged[] = [$start, $end];                       // gap: new interval
        }
    }
    return $merged;
}

echo json_encode(mergeIntervals([[8, 10], [1, 3], [2, 6], [15, 18], [17, 20]])), "\n";
echo json_encode(mergeIntervals([[1, 4], [4, 5]])), "\n";
Outputcompiled & run with real PHP
[[1,6],[8,10],[15,20]]
[[1,5]]
Your turn

Meeting rooms: given meetings as [start, end], return the fewest rooms needed. Sort the starts and the ends separately, then walk both with two pointers.

10

Pattern 7: BFS and DFS on a grid

Signal: a grid or map ("islands", "rooms", "shortest path in a maze"), or anything described as connections. Treat each cell as a node with up to four neighbours. DFS answers "which cells connect?"; BFS answers "how many steps at minimum?".

phpmain.php
<?php
function countIslands(array $grid): int
{
    $rows = count($grid);
    $cols = strlen($grid[0]);
    $seen = [];
    $sink = function (int $r, int $c) use (&$sink, &$seen, $grid, $rows, $cols): void {
        if ($r < 0 || $c < 0 || $r >= $rows || $c >= $cols) return;
        if ($grid[$r][$c] !== '#' || isset($seen["$r,$c"])) return;
        $seen["$r,$c"] = true;
        $sink($r + 1, $c); $sink($r - 1, $c); $sink($r, $c + 1); $sink($r, $c - 1);
    };
    $islands = 0;
    for ($r = 0; $r < $rows; $r++) {
        for ($c = 0; $c < $cols; $c++) {
            if ($grid[$r][$c] === '#' && !isset($seen["$r,$c"])) {
                $islands++;
                $sink($r, $c);
            }
        }
    }
    return $islands;
}

function shortestSteps(array $maze): int
{
    $rows = count($maze);
    $cols = strlen($maze[0]);
    $queue = new SplQueue();
    $queue->enqueue([0, 0, 0]);
    $seen = ['0,0' => true];
    while (!$queue->isEmpty()) {
        [$r, $c, $d] = $queue->dequeue();
        if ($r === $rows - 1 && $c === $cols - 1) return $d;
        foreach ([[1, 0], [-1, 0], [0, 1], [0, -1]] as [$dr, $dc]) {
            $nr = $r + $dr;
            $nc = $c + $dc;
            if ($nr < 0 || $nc < 0 || $nr >= $rows || $nc >= $cols) continue;
            if ($maze[$nr][$nc] === '#' || isset($seen["$nr,$nc"])) continue;
            $seen["$nr,$nc"] = true;
            $queue->enqueue([$nr, $nc, $d + 1]);
        }
    }
    return -1;
}

echo countIslands(['##..#', '#...#', '..#..', '.....', '##.##']), "\n";
echo shortestSteps(['..#.', '#...', '..#.', '.#..']), "\n";
Outputcompiled & run with real PHP
5
6

Strings are indexable, so each grid row is just a string and $grid[$r][$c] reads one cell. Keys like "2,3" make a set of visited cells.

Your turn

Change countIslands to return the size of the biggest island instead. Make $sink return the number of cells it sank.

11

Pattern 8: DP-lite

Signal: "how many ways", "minimum cost", "maximum value", where the answer for n depends on the answers for smaller n. Write the one-sentence meaning of dp[i], the base case, and the step from smaller answers. Often you only need the last one or two values, not a whole table.

phpmain.php
<?php
// ways to climb n stairs taking 1 or 2 steps: ways(n) = ways(n-1) + ways(n-2)
function climbStairs(int $n): int
{
    [$a, $b] = [1, 1];
    for ($i = 2; $i <= $n; $i++) [$a, $b] = [$b, $a + $b];
    return $b;
}

// house robber: max sum with no two adjacent houses
function rob(array $houses): int
{
    $take = $skip = 0; // best total if we take / skip the previous house
    foreach ($houses as $h) {
        [$take, $skip] = [$skip + $h, max($take, $skip)];
    }
    return max($take, $skip);
}

echo climbStairs(5), ' ', climbStairs(10), "\n";
echo rob([2, 7, 9, 3, 1]), ' ', rob([2, 1, 1, 2]), "\n";
Outputcompiled & run with real PHP
8 89
12 4

[$a, $b] = [$b, $a + $b] evaluates the right side first, so the swap needs no temporary variable.

Your turn

Write uniquePaths(int $rows, int $cols): int: moves only right or down from the top-left to the bottom-right. Each cell's count is the cell above plus the cell to the left. A 3×7 grid gives 28.

12

Choosing the pattern

The problem says…TryTypical cost
sorted array, pairs, "in place"Two pointersO(n)
contiguous subarray / substring, "longest", "at most k"Sliding windowO(n)
"find two", "count", "group", "seen before"Array as hash mapO(n)
nesting, "next greater", undoStackO(n)
"all combinations / permutations", n ≤ 20BacktrackingO(2n) or O(n!)
intervals, schedules, "closest"Sort firstO(n log n)
grid, maze, connections, "minimum steps"BFS / DFSO(rows × cols)
"how many ways", "min cost", "max value"DPO(n) to O(n²)
Say it out loud
In an interview, name the pattern before coding it: "the input is sorted and we want a pair, so I will try two pointers, which is O(n)". Even if the first idea is wrong, the interviewer sees how you think, and can steer you. Silence is the most common reason a correct candidate is marked down.
Two pointers
Two indexes moving towards each other (or in the same direction) to avoid checking every pair.
Sliding window
A contiguous range grown from the right and shrunk from the left, so each element is visited at most twice.
Monotonic stack
A stack kept in increasing or decreasing order, used for "next greater / smaller element" questions.
Backtracking
Building candidates one choice at a time, recursing, then undoing the choice to try the next one.
DP state
The one-sentence meaning of dp[i]: what sub-problem each table cell answers.
Quick check

The problem: "Given an array of up to 100,000 integers, return the length of the longest contiguous subarray whose sum is at most k." All numbers are positive. Which approach fits?

Frequently asked questions

Can I use PHP for coding interviews?
Yes, most interview platforms (HackerRank, LeetCode, CoderPad) support PHP, and PHP-focused companies expect it. Its arrays double as lists, maps and sets, which makes many hash-map problems short. Know the costs: isset is O(1) while in_array is O(n), and array_shift is O(n), so use SplQueue for BFS.
What are the most common coding interview patterns?
Eight patterns cover most problems: two pointers, sliding window, hash map lookups, stack, recursion and backtracking, sorting first, BFS and DFS, and simple dynamic programming. Learning to recognise the signal words for each one matters more than memorising individual problems.
How do I know which algorithm a problem needs?
Look at the input limits. Up to about 20 items allows trying every combination; a few thousand allows O(n²); around a million needs O(n log n) or O(n). Then match the wording: "contiguous" suggests a sliding window, "sorted" suggests two pointers or binary search, "how many ways" suggests DP.

Finish the PHP handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.