zack965 / php-ds-algo
Classic data structures and algorithms implemented from scratch in PHP.
Requires (Dev)
- phpunit/phpunit: ^13.2
README
A PHP library implementing classic data structures and algorithms from scratch, PSR-4 autoloaded under Zack\PhpDsAlgo\. No framework, no HTTP layer, no external runtime dependencies — just data structures, algorithms, and a PHPUnit test suite.
Table of Contents
- Requirements
- Installation
- Quick Start
- Data Structures
- Algorithms
- Exceptions & Error Handling
- Known Quirks & Gotchas
- Testing
- Project Structure
- Roadmap
- License
Requirements
- PHP >= 8.1
- Composer
Installation
composer require zack965/php-ds-algo
For local development against a clone of this repo:
composer install
composer dump-autoload # regenerate the PSR-4 autoload map after adding/moving classes
Quick Start
<?php require 'vendor/autoload.php'; use Zack\PhpDsAlgo\DataStructure\LinkedList\Single\SingleLinkedList; use Zack\PhpDsAlgo\DataStructure\Stack\ArrayStack; use Zack\PhpDsAlgo\Algorithmes\ArraySortAlgorythmes; $list = SingleLinkedList::of([3, 1, 2])->append(4)->prepend(0); echo implode(', ', $list->toArrayValues()); // 0, 3, 1, 2, 4 $stack = ArrayStack::of(1, 2, 3); echo $stack->pop(); // 3 $sorted = ArraySortAlgorythmes::bubbleSort([5, 3, 1, 4, 2]); echo implode(', ', $sorted); // 1, 2, 3, 4, 5
Data Structures
All data structures live under src/DataStructure/. SingleLinkedList, DoublyLinkedList, and Graph are persistent/immutable: operations that look like mutations (append, insert, removeAt, addNode, ...) never change the receiver — they return a new instance and leave the original untouched. ArrayStack and Queue are the opposite: genuinely mutable. Their "mutating" methods change the object in place and return $this for chaining. Keep this split in mind — it's the single biggest behavioral difference between the two groups.
SingleLinkedList
Zack\PhpDsAlgo\DataStructure\LinkedList\Single\SingleLinkedList — implements ILinkedList, IteratorAggregate. Private constructor; build one via a static factory.
Creating a list
use Zack\PhpDsAlgo\DataStructure\LinkedList\Single\SingleLinkedList; use Zack\PhpDsAlgo\DataStructure\LinkedList\Single\SingleLinkedListNode; SingleLinkedList::empty(); // empty list SingleLinkedList::of([1, 2, 3]); // from an array of values SingleLinkedList::fromIterable($generator); // from any iterable (foreach-based) SingleLinkedList::fromNodes([ new SingleLinkedListNode(1), new SingleLinkedListNode(2), ]); // from pre-built nodes (linked together for you) SingleLinkedList::ofObjects($nodes); // alias for fromNodes()
Insertion (each call returns a new list)
$list = SingleLinkedList::of([2, 3]); $list->prepend(1); // [1, 2, 3] $list->append(4); // [2, 3, 4] $list->insert(99, 1); // [2, 99, 3] — insert 99 at index 1 $list->insertBeforeNode(3, 99); // [2, 99, 3] — insert 99 immediately before the node holding 3 $list->insertAfterNode(2, 99); // [2, 99, 3] — insert 99 immediately after the node holding 2 // $list itself is untouched by all of the above — capture the return value: $list = $list->append(4);
insertBeforeNode()/insertAfterNode() locate the target by value (via indexOf(), loose == comparison) and throw InvalidArgumentException (ErrorMessages::NO_NODE_WITH_THIS_VALUE) if it isn't found. Inserting before the head or after the tail both work correctly (no need to special-case list boundaries yourself).
Removal
$list->removeByValue(2); // removes the first node holding 2 $list->removeAt(0); // removes by index $list->removeHead(); $list->removeTail(); $list->clear(); // empties the list — throws if already empty $list->clearAndKeepHead(); // truncates to just the head node
Access
$list->get(1); // SingleLinkedListNode at index 1 $list->getTail(); // last node $list->contains(2); // returns the matching node, or throws $list->indexOf(2); // returns the index, or throws $list->getLength(); // int $list->getHead(); // ?SingleLinkedListNode
Transformations & functional methods
$list->reverse(); // new, reversed list $list->toArray(); // SingleLinkedListNode[] $list->toArrayValues(); // raw values, e.g. [1, 2, 3] $list->map(fn($v) => $v * 2); // new list $list->filter(fn($v) => $v % 2 === 0); // new list $list->reduce(fn($carry, $v) => $carry + $v, 0); // scalar
Iteration
SingleLinkedList implements IteratorAggregate — foreach yields SingleLinkedListNode objects, not raw values:
foreach ($list as $node) { echo $node->getValue(); }
All error paths throw InvalidArgumentException with a message from Zack\PhpDsAlgo\Constants\ErrorMessages (LINKEDLIST_IS_EMPTY, INDEX_OUT_OF_BOUND, NO_NODE_WITH_THIS_VALUE).
DoublyLinkedList
Zack\PhpDsAlgo\DataStructure\LinkedList\Doubly\DoublyLinkedList — implements IDoublyLinkedList, IteratorAggregate (note: not ILinkedList — it's a separate interface). Same persistent/immutable pattern and near-identical API surface to SingleLinkedList, plus backward traversal via DoublyLinkedListNode::getPrevious().
use Zack\PhpDsAlgo\DataStructure\LinkedList\Doubly\DoublyLinkedList; $list = DoublyLinkedList::of([1, 2, 3]); $list = $list->append(4)->prepend(0); // [0, 1, 2, 3, 4] $list = $list->insertBeforeNode(2, 99); // insert 99 before the node holding 2 $list = $list->insertAfterNode(2, 99); // insert 99 after the node holding 2 $list = $list->removeByValue(99); $list = $list->removeAt(0); $list = $list->reverse(); foreach ($list as $node) { echo $node->getValue(), ' prev=', $node->getPrevious()?->getValue(); }
Supported operations mirror SingleLinkedList exactly: prepend, append, insert, insertBeforeNode, insertAfterNode, removeByValue, removeAt, removeHead, removeTail, clear, clearAndKeepHead, get, getTail, contains, indexOf, reverse, toArray, toArrayValues, map, filter, reduce.
DoublyLinkedListNode differs slightly from SingleLinkedListNode: it has getPrevious()/setPrevious(?DoublyLinkedListNode $previous) in addition to getNext()/setNext(), but has no setValue() — nodes are value-immutable once created (only SingleLinkedListNode lets you mutate a node's value in place).
Gotcha:
DoublyLinkedList::fromIterable()only works with a realarray(or anotherCountable+ArrayAccessiterable) — it internally doescount($values)and$values[$i]index access, so passing aGeneratorthrows aTypeError/Error.SingleLinkedList::fromIterable()doesn't have this restriction (it uses a plainforeach). If you need to build aDoublyLinkedListfrom a generator, collect it into an array first:DoublyLinkedList::fromIterable(iterator_to_array($generator)).
ArrayStack
Zack\PhpDsAlgo\DataStructure\Stack\ArrayStack — implements IStack, IteratorAggregate. Mutable (unlike the linked lists above) — push/pop/clear change the stack in place.
use Zack\PhpDsAlgo\DataStructure\Stack\ArrayStack; ArrayStack::empty(); ArrayStack::of(1, 2, 3); // variadic ArrayStack::fromArray([1, 2, 3]); ArrayStack::fromIterable($iterable); new ArrayStack([1, 2, 3]); // constructor is public too $stack = ArrayStack::of(1, 2, 3); $stack->push(4); // mutates $stack in place, returns $this (chainable) $stack->pop(); // removes & returns the top value — throws if empty $stack->peek(); // top value without removing it — throws if empty $stack->bottom(); // bottom value — throws if empty $stack->isEmpty(); // bool $stack->contains(2); // bool, strict (===) comparison $stack->clear(); // empties the stack in place, returns $this $stack->toArray(); // bottom-to-top order $stack->count(); // int // Chaining, since push()/clear() return $this: $stack->push(1)->push(2)->push(3);
Iteration is top-to-bottom and yields raw values (not nodes):
foreach (ArrayStack::of(1, 2, 3) as $value) { echo $value; // 3, 2, 1 }
Errors (pop()/peek()/bottom() on an empty stack) throw InvalidArgumentException with a plain message ("The stack is already empty") — not one of the ErrorMessages constants used by the linked lists.
Queue
Zack\PhpDsAlgo\DataStructure\Queue\Queue — implements IQueue (extends Countable). Mutable, like ArrayStack. Has no static factories — construct it directly with an array, and note the capacity setup requirement below.
use Zack\PhpDsAlgo\DataStructure\Queue\Queue; $queue = new Queue([]); $queue->setMaxCapacity(10); // REQUIRED before enqueue()/isFull()/getMaxCapacity() — see note below $queue->enqueue('a')->enqueue('b')->enqueue('c'); // chainable, mutates in place $queue->dequeue(); // removes & returns the front item — throws if empty $queue->front(); // peek the front item $queue->rear(); // peek the back item $queue->isEmpty(); // bool $queue->isFull(); // bool, compares count() to maxCapacity $queue->contains('b'); // bool, strict comparison $queue->clear(); // empties in place $queue->toArray(); // front-to-rear order $queue->toIterable(); // generator, front-to-rear $queue->getMaxCapacity();
Required setup —
setMaxCapacity():maxCapacityhas no default value. Callingenqueue(),isFull(), orgetMaxCapacity()beforesetMaxCapacity()throws a PHPError("must not be accessed before initialization"), not anInvalidArgumentException. Always callsetMaxCapacity()right after construction if you'll use any of those three methods.
Queuedoes not implementIteratorAggregate— you cannotforeachaQueuedirectly. UsetoIterable()ortoArray()instead.
See Known Quirks & Gotchas for two more Queue-specific edge cases (capacity off-by-one, front()/rear() on an empty queue) worth knowing about before relying on them.
Graph
Zack\PhpDsAlgo\DataStructure\Graph\Graph — implements IGraph. Persistent/immutable like the linked lists. Supports directed/undirected and weighted/unweighted graphs, backed by an adjacency list of GraphEdge objects.
use Zack\PhpDsAlgo\DataStructure\Graph\Graph; $graph = new Graph(directed: true, weighted: false); // both flags default as shown $graph->addNode('A')->addNode('B')->addNode('C'); $graph->addEdge('A', 'B'); $graph->addEdge('A', 'C', weight: 5, metadata: ['label' => 'road']); $graph->hasNode('A'); // bool $graph->hasEdge('A', 'B'); // bool $graph->getEdge('A', 'C')->getWeight(); // 5 (or null if unweighted) $graph->getNeighbors('A'); // GraphEdge[] leaving 'A' $graph->getOutgoingEdges('A'); // same idea, scanning the whole adjacency list $graph->getIncomingEdges('C'); // edges arriving at 'C' $graph->getIncidentEdges('A'); // edges touching 'A' in either direction $graph->getNodes(); // ['A', 'B', 'C'] $graph->getNodesCount(); // 3 $graph->getEdgeCount(); // 2 $graph->isDirected(); // bool $graph->isWeighted(); // bool $graph->isEmpty(); // bool $graph->removeEdge('A', 'B'); $graph->removeNode('C'); // also drops every edge touching 'C' $graph->clearEdges(); // keeps nodes, drops all edges $graph->clear(); // drops everything $graph->display(); // pretty-prints the adjacency list to stdout
Building from a plain adjacency list
$graph = new Graph(); $graph->buildFromAdjencyList([ 'A' => ['B', 'C'], 'B' => ['C'], ]); // nodes A, B, C are created automatically (including 'C', which only ever // appears as a destination); edges A→B, A→C, B→C are added. // buildFromAdjencyList() clears any existing graph state first.
Traversal
Graph doesn't have bfs()/dfs() methods itself — traversal is done via separate algorithm classes (see Graph Traversal below):
use Zack\PhpDsAlgo\Algorithmes\GraphBreadthFirstTraversal; use Zack\PhpDsAlgo\Algorithmes\GraphDepthFirstTraversal; GraphBreadthFirstTraversal::traverse($graph, 'A'); // ['A', 'B', 'C'] GraphDepthFirstTraversal::traverse($graph, 'A'); // ['A', 'C', 'B']
addEdge() allows duplicate edges to the same destination (no uniqueness check) — calling addEdge('A', 'B') twice gives you two distinct GraphEdge objects in getNeighbors('A').
GraphEdge (getSourceNode(), getDestinationNode(), getWeight(): int|float|null, getMetadata(): array) and GraphNode (getValue()) are separate small value-object classes under the same namespace. GraphNode is currently a standalone class — Graph stores nodes as plain int|string values internally and never actually constructs a GraphNode. GraphEdge, by contrast, is used throughout Graph's adjacency list.
Graphcannot beforeach'd directly. It has agetIterator(): Traversablemethod, but the class onlyimplements IGraph— andIGraphdoes not extendIteratorAggregate. Soforeach ($graph as ...)silently iterates zero times (PHP falls back to iterating public properties, andGraphhas none) rather than erroring or callinggetIterator(). CallgetIterator()explicitly instead:foreach ($graph->getIterator() as $sourceNode => $edges) { // $edges is that node's GraphEdge[] }
Error handling: addNode() on a duplicate throws DuplicateNodeException; getEdge() on a missing edge throws EdgeNotFoundException; traversal helpers throw NotFoundException for an unknown start node; most other node/edge lookups throw plain InvalidArgumentException.
Algorithms
Algorithm classes live under src/Algorithmes/ and are static-method utility classes operating on plain PHP arrays — fully decoupled from the data structures above.
Sorting — ArraySortAlgorythmes
Zack\PhpDsAlgo\Algorithmes\ArraySortAlgorythmes
use Zack\PhpDsAlgo\Algorithmes\ArraySortAlgorythmes; ArraySortAlgorythmes::bubbleSort([5, 3, 1, 4, 2]); // [1, 2, 3, 4, 5] ArraySortAlgorythmes::selectionSort([5, 3, 1, 4, 2]); // [1, 2, 3, 4, 5] ArraySortAlgorythmes::insertionSort([5, 3, 1, 4, 2]); // [1, 2, 3, 4, 5]
All three take an array by value and return a new sorted array — the input is never mutated. This is the canonical sorting implementation; src/SortingAlgorithms.php (top-level Zack\PhpDsAlgo namespace) is a legacy duplicate of selectionSort() kept only for backward compatibility — don't build new code against it.
Searching — ArraySearchAlogorthme
Zack\PhpDsAlgo\Algorithmes\ArraySearchAlogorthme — all four methods return the found index, or -1 if not found. All require a sorted array.
use Zack\PhpDsAlgo\Algorithmes\ArraySearchAlogorthme; // binarySearch(array $nums, int $target, int $start, int $end = 0) ArraySearchAlogorthme::binarySearch([10, 20, 30, 40, 50], 30, 0, 4); // 2 // exponentialSearchImplementation(array $nums, int $target) ArraySearchAlogorthme::exponentialSearchImplementation([1, 2, 3, 4, 5, 6, 7, 8], 8); // 7 // interpolationSearchRecursive(array $data, int $target, int $low, int $high) // best suited to roughly-uniformly-distributed integer data ArraySearchAlogorthme::interpolationSearchRecursive([10, 20, 30, 40, 50], 30, 0, 4); // 2 // jumpSearch(array $data, int $target, int $jumpSize, int $start, int $end, int $jumpIndex) // caller manages block boundaries manually — see below ArraySearchAlogorthme::jumpSearch([1, 3, 5, 7, 9, 11, 13, 15], 11, 3, 0, 2, 0); // 5
jumpSearch() has no convenience wrapper — you must pass the first block's $start/$end yourself (typically 0 and $jumpSize - 1) and $jumpIndex = 0; the method advances the block internally on each recursive call.
binarySearch()'s parameter order is ($nums, $target, $start, $end = 0) — note $start comes before $end, and $end is the one with a default. Passing arguments in the wrong order is a silent logic bug, not a type error, since both are int.
Sliding Window — SlidingWindow
Zack\PhpDsAlgo\Algorithmes\SlidingWindow
use Zack\PhpDsAlgo\Algorithmes\SlidingWindow; SlidingWindow::processFixedSizeSlidingWindow( [1, 2, 3, 4, 5], 3, // window size function (array $window, int $startIndex) { echo implode(',', $window), ' @ ', $startIndex, PHP_EOL; } ); // 1,2,3 @ 0 // 2,3,4 @ 1 // 3,4,5 @ 2
If $size <= 0 or $size is larger than the array, the callback is never invoked (no error).
Graph Traversal — BFS / DFS
Zack\PhpDsAlgo\Algorithmes\GraphBreadthFirstTraversal / GraphDepthFirstTraversal — each has one static traverse(IGraph $graph, int|string $start): array method, returning visited nodes in traversal order. Both throw NotFoundException if $start isn't a node in the graph.
use Zack\PhpDsAlgo\Algorithmes\GraphBreadthFirstTraversal; use Zack\PhpDsAlgo\Algorithmes\GraphDepthFirstTraversal; GraphBreadthFirstTraversal::traverse($graph, 'A'); GraphDepthFirstTraversal::traverse($graph, 'A');
DFS is stack-based (iterative, not recursive) — for a node with neighbors [B, C], C is explored before B (last-pushed, first-popped).
General Array Helpers — GeneralArrayAlgorithms
Zack\PhpDsAlgo\Algorithmes\GeneralArrayAlgorithms
use Zack\PhpDsAlgo\Algorithmes\GeneralArrayAlgorithms; GeneralArrayAlgorithms::hasDuplicates([1, 2, 3, 2]); // true GeneralArrayAlgorithms::contains([1, 2, 3], 2); // true, strict (===) comparison
Low-level Helpers — AlgorythmesGlobalHelpers
Zack\PhpDsAlgo\Helpers\Algorythmes\AlgorythmesGlobalHelpers — shared primitives used internally by the sorting/searching algorithms above; also usable directly.
use Zack\PhpDsAlgo\Helpers\Algorythmes\AlgorythmesGlobalHelpers; AlgorythmesGlobalHelpers::isBetween(5, 1, 10); // true, inclusive on both ends $nums = [1, 2, 3]; AlgorythmesGlobalHelpers::swapValuesOfArray($nums, 0, 2); // by reference; $nums is now [3, 2, 1] // throws InvalidArgumentException if either index doesn't exist in the array
InsertCorrectly(array &$nums, int $value) also exists on this class but is currently an empty, unimplemented stub — calling it is a safe no-op (returns null, leaves the array untouched), not a working insertion-sort primitive.
Exceptions & Error Handling
| Exception | Thrown by | Notes |
|---|---|---|
InvalidArgumentException (SPL) |
Most linked-list, stack, and queue error paths | Linked lists use constants from Zack\PhpDsAlgo\Constants\ErrorMessages (LINKEDLIST_IS_EMPTY, INDEX_OUT_OF_BOUND, NO_NODE_WITH_THIS_VALUE); ArrayStack/Queue/Graph mostly use plain inline messages instead |
Zack\PhpDsAlgo\Exception\NotFoundException |
GraphBreadthFirstTraversal::traverse(), GraphDepthFirstTraversal::traverse() |
Built via NotFoundException::nodeNotFound($value) |
Zack\PhpDsAlgo\Exception\DuplicateNodeException |
Graph::addNode() on a duplicate |
Built via DuplicateNodeException::nodeDuplicate($value) |
Zack\PhpDsAlgo\Exception\EdgeNotFoundException |
Graph::getEdge() on a missing edge |
Built via EdgeNotFoundException::edgeNotFound($source, $destination) |
use Zack\PhpDsAlgo\Exception\NotFoundException; try { GraphBreadthFirstTraversal::traverse($graph, 'unknown-node'); } catch (NotFoundException $e) { echo $e->getMessage(); // "Node with value 'unknown-node' was not found." }
Known Quirks & Gotchas
A few behaviors worth knowing before you rely on them — none of these are "wrong" enough to change without a deliberate decision, but all of them have surprised someone while building this library:
Queue::enqueue()'s capacity check is off by one. It compares with>instead of>=, so a queue withsetMaxCapacity(2)actually accepts 3 items beforeenqueue()throws, andisFull()briefly reportsfalseagain once that 3rd item is in.Queue::front()/Queue::rear()don't check for an empty queue, despiteIQueue's docblock promising@throws InvalidArgumentException. Calling either on an empty queue just triggers a PHP "undefined array key" warning and returnsnull.Queue's constructor doesn't reindex array keys (noarray_values()), unlikeArrayStack::fromArray(). Building aQueuefrom a non-sequential array (e.g.[5 => 'a', 9 => 'b']) will breakfront()/rear(), which assume index0andcount - 1.GraphEdge::getWeight()returnsnullfor an unweighted edge —int|float|null, notint|float. Always null-check (or useGraph::isWeighted()) before doing arithmetic on it.- A handful of defensive null/false guards are unreachable in practice.
Graph::removeNode(), andinsert()/removeAt()/get()on both linked lists, each have a redundant guard clause that's already preceded by an equivalent bounds check earlier in the same method — under the classes' normal invariants they can never actually trigger. Harmless, just dead code. ArraySearchAlogorthme::interpolationSearchRecursive()computes its estimated position with float division and uses the (possibly fractional) result both as an array index and as a recursiveintargument — PHP emits an implicit float-to-int-conversion deprecation notice in that case. It still returns the correct result; it's just noisy.
Testing
composer test # full suite (vendor/bin/phpunit) vendor/bin/phpunit tests/Unit/DataStructure # just the data-structure tests vendor/bin/phpunit --filter QueueTest # a single test class vendor/bin/phpunit --coverage-text # coverage summary (needs Xdebug or PCOV)
There is no other linter or static analysis tool configured — php -l path/to/File.php is the extent of syntax checking beyond the test suite.
Project Structure
src/
├── Algorithmes/ # static utility classes operating on plain arrays
├── Constants/ # ErrorMessages
├── Contracts/ # interfaces: ILinkedList, IDoublyLinkedList, IStack, IQueue, IGraph
├── DataStructure/
│ ├── LinkedList/Single/ # SingleLinkedList, SingleLinkedListNode
│ ├── LinkedList/Doubly/ # DoublyLinkedList, DoublyLinkedListNode
│ ├── Stack/ # ArrayStack
│ ├── Queue/ # Queue
│ └── Graph/ # Graph, GraphNode, GraphEdge
├── Exception/ # NotFoundException, DuplicateNodeException, EdgeNotFoundException
├── Helpers/Algorythmes/ # AlgorythmesGlobalHelpers
├── SortingAlgorithms.php # legacy duplicate — not canonical, see Sorting section above
└── index.php # scratch/demo entrypoint
Note the intentional misspellings (Algorythmes, Alogorthme) used consistently across namespaces and folder names — they're not typos to "fix," PSR-4 resolution depends on them matching exactly.
Roadmap
See TODO.md and features.md for the current backlog. Highlights: graph algorithms (Dijkstra, topological sort, cycle detection, connected components), merge/quick sort, a Binary Search Tree, and further out — deque, priority queue/heap, AVL/Red-Black trees, trie, hash table, disjoint set.
License
MIT