Free Handbook · Every example compiled & verified

Data Structures & Algorithms

Build stacks, queues, linked lists, trees, heaps and graphs by hand in PHP, then use arrays and the SPL classes the way an interviewer expects.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 13 · what you'll be able to do

  • Explain what a PHP array really is (an ordered hash map) and what each array function costs
  • Build a stack, a queue, a linked list, a binary search tree and a min-heap by hand
  • Use SplStack, SplQueue, SplMinHeap and SplPriorityQueue, and know when a plain array is better
  • Traverse a graph with BFS and DFS, and find the shortest path in an unweighted graph
  • Write binary search, merge sort and bottom-up dynamic programming, and sort with usort and the spaceship operator
01

The cost table: pick a structure by what it costs

A data structure is a trade-off: it makes some operations cheap by making others expensive. Big-O describes how the work grows as the input grows. O(1) means "the same work no matter how big", O(n) means "work grows in step with the size", and O(log n) means "doubling the input adds one step". In PHP the question is usually not which class to use but which array function: isset($map[$key]) is O(1), while in_array($value, $list) reads every element.

The one to remember: looking up a KEY is O(1), searching for a VALUE is O(n). If you search for the same values repeatedly, flip them into keys.
OperationCostWhy
$a[$key], isset($a[$key]), array_key_existsO(1) averagehash lookup
$a[] = $x, array_push, array_popO(1) amortisedworks at the end, nothing moves
array_shift, array_unshiftO(n)every integer key is renumbered
in_array, array_searchO(n)scans values one by one
unset($a[$key])O(1)removes one bucket, leaves a gap in the keys
sort, usort, ksortO(n log n)hybrid insertion sort + quicksort, stable since PHP 8.0
array_merge, array_slice, array_valuesO(n)builds a new array
SplStack push/pop, SplQueue enqueue/dequeueO(1)doubly linked list
SplMinHeap insert/extract, SplPriorityQueueO(log n)binary heap
phpmain.php
<?php
$data = range(0, 999_999);
$target = 999_999;

$linearSteps = 0;
foreach ($data as $x) {
    $linearSteps++;
    if ($x === $target) break;
}

$binarySteps = 0;
$lo = 0;
$hi = count($data) - 1;
while ($lo <= $hi) {
    $binarySteps++;
    $mid = intdiv($lo + $hi, 2);
    if ($data[$mid] === $target) break;
    if ($data[$mid] < $target) $lo = $mid + 1;
    else $hi = $mid - 1;
}

echo "Linear search: $linearSteps steps\n";
echo "Binary search: $binarySteps steps\n";
Outputcompiled & run with real PHP
Linear search: 1000000 steps
Binary search: 20 steps

The same question, "where is 999,999?", answered by an O(n) and an O(log n) algorithm over one million sorted numbers.

Your turn

Change $target to 0. Linear search now wins. Why does Big-O still call binary search the better algorithm?

02

PHP arrays: one ordered hash map doing every job

Other languages have separate list, map and set types. PHP has one: array, an ordered hash map. A "list" is just an array whose keys happen to be 0, 1, 2, … and PHP stores those compactly (a "packed" array). The consequence juniors trip on: removing an element with unset leaves a gap in the keys, and a gappy array is no longer a list. array_is_list() tells you which one you have, and array_values() renumbers.

phpmain.php
<?php
$list = [10, 20, 30];
$list[] = 40;
echo count($list), "\n";

unset($list[1]);
var_dump(array_is_list($list));
echo json_encode($list), "\n";

$list = array_values($list);
var_dump(array_is_list($list));
echo json_encode($list), "\n";
Outputcompiled & run with real PHP
4
bool(false)
{"0":10,"2":30,"3":40}
bool(true)
[10,30,40]

After unset the array still works in PHP, but json_encode turns it into an object. Frontends that expected a JSON array break here.

Because keys are hashed, an array is also PHP's set: store the items as keys and membership becomes isset, which is O(1). array_count_values is the built-in frequency counter, and arsort sorts by value while keeping keys (stable, so ties stay in first-seen order).

phpmain.php
<?php
$seen = [];
foreach (['php', 'sql', 'php', 'docker', 'sql'] as $tag) {
    $seen[$tag] = true;
}
echo implode(', ', array_keys($seen)), "\n";
var_dump(isset($seen['docker']), isset($seen['redis']));

$counts = array_count_values(explode(' ', 'the cat and the hat and the bat'));
arsort($counts);
foreach ($counts as $word => $n) {
    echo "$word: $n\n";
}
Outputcompiled & run with real PHP
php, sql, docker
bool(true)
bool(false)
the: 3
and: 2
cat: 1
hat: 1
bat: 1
Your turn

Remove duplicates from [3, 1, 3, 2, 1] two ways: with array_unique, and with array_keys(array_flip(...)). Which one keeps the original keys?

Keys are converted
The string key "8" becomes the integer 8, true becomes 1, and a float key is truncated. So $a["8"] and $a[8] are the same slot. When the keys are user data (IDs from a CSV, phone numbers), remember they may come back as ints.
03

Stacks: last in, first out

A stack is a pile of plates: push onto the top, pop from the top. Undo history, the call stack itself, bracket matching and depth-first search all use one. A PHP array is already a fast stack, because $a[] = $x and array_pop only touch the end. Wrapping it in a class gives it a name and stops callers from reaching into the middle.

phpmain.php
<?php
final class Stack
{
    private array $items = [];

    public function push(mixed $x): void { $this->items[] = $x; }

    public function pop(): mixed
    {
        if ($this->items === []) throw new UnderflowException('pop from empty stack');
        return array_pop($this->items);
    }

    public function peek(): mixed { return $this->items[array_key_last($this->items)] ?? null; }
    public function isEmpty(): bool { return $this->items === []; }
}

$s = new Stack();
foreach (['a', 'b', 'c'] as $x) $s->push($x);
echo $s->peek(), "\n";
while (!$s->isEmpty()) echo $s->pop(), ' ';
echo "\n";

try {
    $s->pop();
} catch (UnderflowException $e) {
    echo 'Error: ', $e->getMessage(), "\n";
}
Outputcompiled & run with real PHP
c
c b a 
Error: pop from empty stack

The classic stack interview problem: are the brackets in a string balanced? Push every opener; on every closer, the top of the stack must be its partner. SplStack is the SPL version.

phpmain.php
<?php
function balanced(string $s): bool
{
    $pairs = [')' => '(', ']' => '[', '}' => '{'];
    $stack = new SplStack();
    foreach (str_split($s) as $ch) {
        if (in_array($ch, $pairs, true)) {
            $stack->push($ch);
        } elseif (isset($pairs[$ch])) {
            if ($stack->isEmpty() || $stack->pop() !== $pairs[$ch]) return false;
        }
    }
    return $stack->isEmpty();
}

foreach (['{[()]}', '([)]', '((', 'f(x[0])'] as $s) {
    echo str_pad($s, 8), ' ', balanced($s) ? 'yes' : 'no', "\n";
}
Outputcompiled & run with real PHP
{[()]}   yes
([)]     no
((       no
f(x[0])  yes
Your turn

Return the position of the first bracket that breaks the balance instead of false.

Visualizebalanced("([)]")Step 1 / 5
$pairs = [')' => '(', ']' => '[', '}' => '{'];
$stack = new SplStack();
foreach (str_split($s) as $ch) {
if (in_array($ch, $pairs, true)) {
$stack->push($ch);
} elseif (isset($pairs[$ch])) {
if ($stack->isEmpty() || $stack->pop() !== $pairs[$ch]) return false;
}
}
return $stack->isEmpty();
Line 3

First character is an opener.

Variables now
$ch'('
$stack[]
All 5 steps as a table
StepLineWhat happenedVariables now
13First character is an opener.$ch = '(' $stack = []
25Push it.$stack = ['(']
35Second character "[" is also an opener: push.$ch = '[' $stack = ['(', '[']
47Third character ")" is a closer. Pop the top, which is "[". Its partner should be "(".$ch = ')' popped = '[' $stack = ['(']
57"[" is not "(", so the brackets cross over. Return false without reading the rest.
04

Queues: first in, first out, and why array_shift is slow

A queue serves items in arrival order: jobs waiting for a worker, requests waiting for a rate limit, the frontier of a breadth-first search. The tempting PHP version is $q[] = $x to enqueue and array_shift($q) to dequeue. It works, but array_shift renumbers every remaining key, so draining a queue of n items costs O(n²). SplQueue is a doubly linked list: both ends are O(1).

phpmain.php
<?php
$q = new SplQueue();
foreach (['order-1', 'order-2', 'order-3'] as $job) {
    $q->enqueue($job);
}
echo 'waiting: ', count($q), "\n";

while (!$q->isEmpty()) {
    $job = $q->dequeue();
    echo "processing $job\n";
    if ($job === 'order-1') {
        $q->enqueue('order-1-retry');
    }
}
Outputcompiled & run with real PHP
waiting: 3
processing order-1
processing order-2
processing order-3
processing order-1-retry

A retry goes to the back of the line, behind everything that was already waiting.

phpmain.php
<?php
$n = 20_000;

$q = range(1, $n);
$start = hrtime(true);
while ($q) array_shift($q);
$shiftMs = (hrtime(true) - $start) / 1e6;

$spl = new SplQueue();
foreach (range(1, $n) as $x) $spl->enqueue($x);
$start = hrtime(true);
while (!$spl->isEmpty()) $spl->dequeue();
$splMs = (hrtime(true) - $start) / 1e6;

echo $shiftMs > $splMs * 5 ? "array_shift is much slower\n" : "about the same\n";
Outputcompiled & run with real PHP
array_shift is much slower

Timings vary by machine, so the program prints the verdict, not the milliseconds.

In real code
Inside one request, queues are short and an array is fine. A queue that must survive the request (emails to send, images to resize) does not live in PHP memory at all: it lives in Redis, RabbitMQ or a database table, and Laravel Queues or Symfony Messenger manage it. See Module 12.
05

A linked list built by hand

A linked list is a chain of nodes where each node holds a value and a reference to the next node. Inserting at the front is O(1) (no shifting), but reaching the k-th item is O(k) (no index). You will rarely build one at work, since SplDoublyLinkedList exists, but interviewers use it to test whether you can move references without losing nodes. Objects in PHP are handles, so $node->next = $other links nodes rather than copying them.

phpmain.php
<?php
final class Node
{
    public function __construct(public int $value, public ?Node $next = null) {}
}

final class LinkedList
{
    private ?Node $head = null;

    public function pushFront(int $v): void { $this->head = new Node($v, $this->head); }

    public function append(int $v): void
    {
        if ($this->head === null) { $this->head = new Node($v); return; }
        $cur = $this->head;
        while ($cur->next !== null) $cur = $cur->next;
        $cur->next = new Node($v);
    }

    public function reverse(): void
    {
        $prev = null;
        $cur = $this->head;
        while ($cur !== null) {
            $next = $cur->next;
            $cur->next = $prev;
            $prev = $cur;
            $cur = $next;
        }
        $this->head = $prev;
    }

    public function __toString(): string
    {
        $parts = [];
        for ($n = $this->head; $n !== null; $n = $n->next) $parts[] = $n->value;
        return implode(' -> ', $parts) ?: '(empty)';
    }
}

$list = new LinkedList();
$list->append(2);
$list->append(3);
$list->pushFront(1);
echo $list, "\n";
$list->reverse();
echo $list, "\n";
Outputcompiled & run with real PHP
1 -> 2 -> 3
3 -> 2 -> 1
Your turn

Add remove(int $v): bool that unlinks the first node holding $v. Handle the head separately.

Visualizereverse() on 1 -> 2 -> 3Step 1 / 7
$prev = null;
$cur = $this->head;
while ($cur !== null) {
$next = $cur->next;
$cur->next = $prev;
$prev = $cur;
$cur = $next;
}
$this->head = $prev;
Line 2

Start at the head. Nothing has been reversed yet.

Variables now
$prevnull
$cur1
All 7 steps as a table
StepLineWhat happenedVariables now
12Start at the head. Nothing has been reversed yet.$prev = null $cur = 1
24Remember where the rest of the list is before breaking the link.$next = 2
35Point node 1 backwards, at null. It will be the new tail.1.next = null
47Step forward: prev = 1, cur = 2.$prev = 1 $cur = 2
55Node 2 now points back at 1.2.next = 1 $next = 3
65Node 3 now points back at 2.3.next = 2 $prev = 3 $cur = null
79The loop ends when cur runs off the end. prev is the old tail, which is the new head.head = 3
06

Recursion and memoisation

A recursive function calls itself on a smaller version of the problem until it reaches a base case it can answer directly. Every call waits on the call stack for the one it made, so recursion depth costs memory. PHP has no tail-call optimisation, and since PHP 8.3 a runaway recursion stops with a clear "Maximum call stack size reached" error instead of a crash.

phpmain.php
<?php
function sumDigits(int $n): int
{
    if ($n < 10) return $n;              // base case
    return $n % 10 + sumDigits(intdiv($n, 10));
}

function fib(int $n, array &$memo = []): int
{
    if ($n <= 1) return $n;
    return $memo[$n] ??= fib($n - 1, $memo) + fib($n - 2, $memo);
}

echo sumDigits(98_765), "\n";
echo fib(10), "\n";
echo fib(90), "\n";
Outputcompiled & run with real PHP
35
55
2880067194370816120

Without the memo array, fib(90) would make roughly 1018 calls. With it, each value is computed once: O(n).

Recursion shines on data that is itself recursive, such as nested arrays, file trees and JSON. Flattening a nested array is a short recursive function:

phpmain.php
<?php
function flatten(array $items): array
{
    $out = [];
    foreach ($items as $item) {
        if (is_array($item)) {
            array_push($out, ...flatten($item));
        } else {
            $out[] = $item;
        }
    }
    return $out;
}

echo json_encode(flatten([1, [2, [3, [4]], 5], [], 6])), "\n";
Outputcompiled & run with real PHP
[1,2,3,4,5,6]
07

A binary search tree

A binary search tree keeps every value smaller than a node in its left subtree and every larger value in its right subtree. Search, insert and delete follow one path from the root, so they are O(log n) while the tree is balanced, and O(n) if you insert already-sorted data and it degrades into a chain. An in-order traversal (left, node, right) visits the values in sorted order.

phpmain.php
<?php
final class TreeNode
{
    public ?TreeNode $left = null;
    public ?TreeNode $right = null;
    public function __construct(public int $value) {}
}

function insert(?TreeNode $node, int $v): TreeNode
{
    if ($node === null) return new TreeNode($v);
    if ($v < $node->value) $node->left = insert($node->left, $v);
    elseif ($v > $node->value) $node->right = insert($node->right, $v);
    return $node;
}

function contains(?TreeNode $node, int $v): bool
{
    while ($node !== null) {
        if ($v === $node->value) return true;
        $node = $v < $node->value ? $node->left : $node->right;
    }
    return false;
}

function inOrder(?TreeNode $node, array &$out): void
{
    if ($node === null) return;
    inOrder($node->left, $out);
    $out[] = $node->value;
    inOrder($node->right, $out);
}

function height(?TreeNode $node): int
{
    return $node === null ? 0 : 1 + max(height($node->left), height($node->right));
}

$root = null;
foreach ([50, 30, 70, 20, 40, 60, 80] as $v) $root = insert($root, $v);

$sorted = [];
inOrder($root, $sorted);
echo implode(' ', $sorted), "\n";
var_dump(contains($root, 60), contains($root, 65));
echo 'height: ', height($root), "\n";

$chain = null;
foreach ([1, 2, 3, 4, 5, 6, 7] as $v) $chain = insert($chain, $v);
echo 'height after sorted inserts: ', height($chain), "\n";
Outputcompiled & run with real PHP
20 30 40 50 60 70 80
bool(true)
bool(false)
height: 3
height after sorted inserts: 7

Same seven values, two shapes. The balanced tree answers in at most 3 steps; the chain needs up to 7. Self-balancing trees (red-black, AVL) exist to prevent the second shape.

08

Heaps and priority queues

A heap always gives you the smallest (min-heap) or largest (max-heap) item in O(1), and inserting or removing one costs O(log n). It is the right structure for "next task by priority", "k largest scores" and Dijkstra's shortest path. SPL ships SplMinHeap, SplMaxHeap and SplPriorityQueue (a max-heap on priority).

phpmain.php
<?php
$heap = new SplMinHeap();
foreach ([42, 7, 19, 3, 25] as $x) $heap->insert($x);
echo 'smallest: ', $heap->top(), "\n";

$out = [];
while (!$heap->isEmpty()) $out[] = $heap->extract();
echo implode(' ', $out), "\n";

$tasks = new SplPriorityQueue();
$tasks->insert('write tests', 2);
$tasks->insert('fix production bug', 10);
$tasks->insert('update README', 1);
$tasks->insert('review pull request', 5);
while (!$tasks->isEmpty()) echo $tasks->extract(), "\n";
Outputcompiled & run with real PHP
smallest: 3
3 7 19 25 42
fix production bug
review pull request
write tests
update README

Every priority here is different on purpose: SplPriorityQueue does not promise any order between items with EQUAL priority.

The classic heap problem is top-k: keep a min-heap of size k while streaming the data. The smallest of your current top k sits at the top, so each new value only has to beat that one. That is O(n log k) time and O(k) memory, instead of sorting everything.

phpmain.php
<?php
function topK(iterable $values, int $k): array
{
    $heap = new SplMinHeap();
    foreach ($values as $v) {
        if (count($heap) < $k) {
            $heap->insert($v);
        } elseif ($v > $heap->top()) {
            $heap->extract();
            $heap->insert($v);
        }
    }
    $out = iterator_to_array($heap, false);
    rsort($out);
    return $out;
}

echo implode(', ', topK([5, 91, 17, 64, 3, 88, 42, 70], 3)), "\n";
Outputcompiled & run with real PHP
91, 88, 70
Your turn

Write the min-heap yourself on a plain array: insert appends and "sifts up" (swap with the parent at intdiv($i - 1, 2) while smaller); extract moves the last item to index 0 and "sifts down".

09

Graphs: BFS and DFS

A graph is nodes joined by edges: users who follow each other, services that call each other, pages that link to each other. In PHP the natural representation is an adjacency list: an array from each node to the array of its neighbours. Breadth-first search (BFS) explores level by level with a queue and finds the shortest path when every edge counts as one step. Depth-first search (DFS) goes as deep as it can first, with a stack or recursion, and is the tool for "can I reach it", cycle detection and topological order.

phpmain.php
<?php
$graph = [
    'home'    => ['blog', 'tools'],
    'blog'    => ['post-a', 'post-b'],
    'tools'   => ['ats'],
    'post-a'  => ['ats'],
    'post-b'  => [],
    'ats'     => ['pricing'],
    'pricing' => [],
];

function shortestPath(array $graph, string $from, string $to): ?array
{
    $queue = new SplQueue();
    $queue->enqueue($from);
    $cameFrom = [$from => null];
    while (!$queue->isEmpty()) {
        $node = $queue->dequeue();
        if ($node === $to) {
            $path = [];
            for ($n = $to; $n !== null; $n = $cameFrom[$n]) array_unshift($path, $n);
            return $path;
        }
        foreach ($graph[$node] as $next) {
            if (!array_key_exists($next, $cameFrom)) {
                $cameFrom[$next] = $node;
                $queue->enqueue($next);
            }
        }
    }
    return null;
}

function dfs(array $graph, string $node, array &$seen = []): array
{
    $seen[$node] = true;
    foreach ($graph[$node] as $next) {
        if (!isset($seen[$next])) dfs($graph, $next, $seen);
    }
    return array_keys($seen);
}

echo implode(' -> ', shortestPath($graph, 'home', 'pricing')), "\n";
echo implode(', ', dfs($graph, 'home')), "\n";
var_dump(shortestPath($graph, 'pricing', 'home'));
Outputcompiled & run with real PHP
home -> tools -> ats -> pricing
home, blog, post-a, ats, pricing, post-b, tools
NULL

BFS found the 3-click route through tools, not the 4-click one through the blog. The graph is directed, so there is no path back from pricing.

isset versus array_key_exists
The $cameFrom map stores null for the start node. isset($cameFrom['home']) is false for a key holding null, so an isset check would re-visit the start. array_key_exists asks "is the key there?", which is the question here.
VisualizeshortestPath(home → pricing): the queue level by levelStep 1 / 7
$queue->enqueue($from);
$cameFrom = [$from => null];
while (!$queue->isEmpty()) {
$node = $queue->dequeue();
if ($node === $to) { /* rebuild path */ }
foreach ($graph[$node] as $next) {
if (!array_key_exists($next, $cameFrom)) {
$cameFrom[$next] = $node;
$queue->enqueue($next);
}
}
}
Line 2

Start: only home is known.

Variables now
queue[home]
All 7 steps as a table
StepLineWhat happenedVariables now
12Start: only home is known.queue = [home]
29Visit home: discover blog and tools (distance 1).queue = [blog, tools]
39Visit blog: discover post-a and post-b (distance 2).queue = [tools, post-a, post-b]
49Visit tools: discover ats, reached FROM tools (distance 2).queue = [post-a, post-b, ats] cameFrom[ats] = tools
57Visit post-a: ats is already known, so the shorter route through tools is kept.
69Visit ats: discover pricing.queue = [pricing] cameFrom[pricing] = ats
75Dequeue pricing: found. Walk cameFrom backwards: pricing, ats, tools, home.
10

Binary search and sorting

Binary search halves a sorted range each step. The template below finds the first position where a value could be inserted while keeping the array sorted (the "lower bound"), which answers "is it there?", "how many are smaller?" and "where does it go?" with one function.

phpmain.php
<?php
function lowerBound(array $sorted, int $target): int
{
    $lo = 0;
    $hi = count($sorted);
    while ($lo < $hi) {
        $mid = intdiv($lo + $hi, 2);
        if ($sorted[$mid] < $target) $lo = $mid + 1;
        else $hi = $mid;
    }
    return $lo;
}

$prices = [5, 9, 9, 14, 20, 31];
foreach ([9, 10, 1, 40] as $t) {
    $i = lowerBound($prices, $t);
    $found = ($prices[$i] ?? null) === $t ? 'found' : 'not found';
    echo "$t -> index $i ($found)\n";
}
Outputcompiled & run with real PHP
9 -> index 1 (found)
10 -> index 3 (not found)
1 -> index 0 (not found)
40 -> index 6 (not found)

Merge sort splits the array in half, sorts each half recursively and merges the two sorted halves. It is always O(n log n) and stable, and it is the sort to write when an interviewer says "without using sort()".

phpmain.php
<?php
function mergeSort(array $a): array
{
    if (count($a) <= 1) return $a;
    $mid = intdiv(count($a), 2);
    $left = mergeSort(array_slice($a, 0, $mid));
    $right = mergeSort(array_slice($a, $mid));

    $out = [];
    $i = $j = 0;
    while ($i < count($left) && $j < count($right)) {
        $out[] = $left[$i] <= $right[$j] ? $left[$i++] : $right[$j++];
    }
    return [...$out, ...array_slice($left, $i), ...array_slice($right, $j)];
}

echo implode(' ', mergeSort([38, 27, 43, 3, 9, 82, 10])), "\n";
Outputcompiled & run with real PHP
3 9 10 27 38 43 82

At work you call the built-ins. sort and rsort reindex; asort/arsort sort by value and keep keys; ksort sorts by key; usort takes a comparison function. The spaceship operator <=> returns -1, 0 or 1, and comparing two arrays with it compares element by element, which gives multi-key sorting in one line. Since PHP 8.0 every sort is stable.

phpmain.php
<?php
$people = [
    ['name' => 'Ravi', 'dept' => 'data', 'salary' => 90],
    ['name' => 'Ana',  'dept' => 'web',  'salary' => 75],
    ['name' => 'Chen', 'dept' => 'data', 'salary' => 120],
    ['name' => 'Bola', 'dept' => 'web',  'salary' => 75],
];

// dept ascending, then salary descending
usort($people, fn($a, $b) => [$a['dept'], $b['salary']] <=> [$b['dept'], $a['salary']]);

foreach ($people as $p) {
    printf("%-5s %-4s %d\n", $p['name'], $p['dept'], $p['salary']);
}
Outputcompiled & run with real PHP
Chen  data 120
Ravi  data 90
Ana   web  75
Bola  web  75

Ana and Bola tie on both keys, so the stable sort keeps them in their original order.

11

Dynamic programming

Dynamic programming (DP) is recursion that remembers. When a problem breaks into overlapping sub-problems, solve each one once and store the answer, either top-down (the memo array from the recursion lesson) or bottom-up (fill a table from the smallest case). The hard part is always the same: say in one sentence what dp[i] means.

phpmain.php
<?php
// dp[a] = fewest coins that make amount a
function minCoins(array $coins, int $amount): int
{
    $dp = array_fill(0, $amount + 1, PHP_INT_MAX);
    $dp[0] = 0;
    for ($a = 1; $a <= $amount; $a++) {
        foreach ($coins as $c) {
            if ($c <= $a && $dp[$a - $c] !== PHP_INT_MAX) {
                $dp[$a] = min($dp[$a], $dp[$a - $c] + 1);
            }
        }
    }
    return $dp[$amount] === PHP_INT_MAX ? -1 : $dp[$amount];
}

echo minCoins([1, 5, 10, 25], 63), "\n";
echo minCoins([1, 3, 4], 6), "\n";
echo minCoins([5, 10], 3), "\n";
Outputcompiled & run with real PHP
6
2
-1

For coins 1, 3, 4 and amount 6, "always take the biggest coin" gives 4+1+1 (three coins). DP finds 3+3 (two coins).

phpmain.php
<?php
// dp[i][j] = length of the longest common subsequence of a[0..i) and b[0..j)
function lcs(string $a, string $b): int
{
    $m = strlen($a);
    $n = strlen($b);
    $dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
    for ($i = 1; $i <= $m; $i++) {
        for ($j = 1; $j <= $n; $j++) {
            $dp[$i][$j] = $a[$i - 1] === $b[$j - 1]
                ? $dp[$i - 1][$j - 1] + 1
                : max($dp[$i - 1][$j], $dp[$i][$j - 1]);
        }
    }
    return $dp[$m][$n];
}

echo lcs('ABCBDAB', 'BDCABA'), "\n";
echo lcs('kitten', 'sitting'), "\n";
Outputcompiled & run with real PHP
4
4
Your turn

PHP has levenshtein() built in. Compare levenshtein('kitten', 'sitting') with the LCS result and explain the difference in one sentence.

12

Cheat sheet: which PHP tool for which job

You need…UseCost
An ordered listarray with $a[] = $xO(1) append, O(1) index
Lookup by keyarray as a map, $a[$k] ?? $defaultO(1) average
"Have I seen this?"values as keys + isset (not in_array)O(1) average
A stackarray + array_pop, or SplStackO(1)
A queueSplQueue (not array_shift)O(1)
Smallest / largest nextSplMinHeap, SplMaxHeap, SplPriorityQueueO(log n)
A fixed-size list of ints, less memorySplFixedArrayO(1) index
Objects as keysSplObjectStorage or WeakMapO(1) average
Sorted outputsort/usort with <=>O(n log n), stable
Find in sorted databinary search (lower bound)O(log n)
Shortest path, unweightedBFS with SplQueueO(V + E)
Overlapping sub-problemsDP: memo array or bottom-up tableusually O(n) or O(n·m)
Big-O
How the work an algorithm does grows as the input grows, ignoring constant factors.
Ordered hash map
What a PHP array really is: keys hashed for O(1) lookup, with insertion order remembered.
Packed array
A PHP array whose keys are exactly 0, 1, 2, …; stored compactly. array_is_list() checks for it.
Amortised O(1)
Occasionally slow (a resize), but O(1) on average over many operations.
Stack / queue
Last-in-first-out and first-in-first-out collections.
Heap
A tree kept in an array where the smallest (or largest) item is always at the top.
BFS / DFS
Breadth-first search (level by level, a queue) and depth-first search (deep first, a stack or recursion).
Memoisation
Caching the result of a function call so the same sub-problem is solved only once.
Stable sort
A sort that keeps equal elements in their original order. Every PHP sort is stable since 8.0.
Quick check

A loop checks in_array($id, $blockedIds) for each of 50,000 incoming IDs against 50,000 blocked IDs. What is the best fix?

Quick check

Why is draining a large array with while ($q) array_shift($q); slow?

Frequently asked questions

Does PHP have lists, maps and sets like other languages?
PHP has one built-in type, the array, which is an ordered hash map. It works as a list (keys 0, 1, 2, …), as a map (any int or string keys) and as a set (store items as keys and check with isset). The SPL extension adds SplStack, SplQueue, SplMinHeap, SplPriorityQueue, SplFixedArray and SplObjectStorage for the cases an array handles badly.
Are data structures and algorithms asked in PHP interviews?
Less than in Java or C++ interviews, but yes: most PHP coding rounds include one or two array and string problems, and questions on the cost of in_array versus isset, array_shift, sorting with usort and recursion over nested arrays are common. Product companies with a separate DSA round ask the same problems as for any language.
Is SplQueue faster than an array in PHP?
For a queue, yes: SplQueue dequeues in O(1), while array_shift on an array renumbers every key and costs O(n). For a stack there is little difference, since array_pop is already O(1) and plain arrays are very well optimised.

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.