PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Skip List – probabilistic data structure O(log n), PHP implementation

by Henryk Tews / Friday, 22 May 2026 / Published in Algorytmy

Skip List is a probabilistic data structure invented by William Pugh in 1990. It combines the simplicity of a singly linked list with the performance of a binary tree – search, insert and delete operations run in O(log n) on average, without the need for balancing like AVL or Red-Black Trees. Rarely used in PHP, but it illustrates perfectly how randomness can replace complex balancing algorithms.

The idea – multiple levels of pointers

A regular singly linked list is O(n) for search – you have to traverse every element. Skip List adds “express lanes” – additional pointers that skip over many elements. The higher the level, the less frequently elements appear (randomly, with probability 0.5).

Level 3: 1 --------> 50 -----------------> 100
Level 2: 1 --> 20 -> 50 --> 70 ----------> 100
Level 1: 1 --> 20 -> 50 --> 70 --> 85 --> 100
Level 0: 1 -> 10 -> 20 -> 30 -> 50 -> 70 -> 85 -> 90 -> 100

Searching for 70:
- Start level 3: 1, next 50 < 70, continue. Next 100 > 70, drop level.
- Level 2: 50, next 70 = found!
- Instead of 8 steps (list) - 4 steps.

Implementation in PHP

<?php

declare(strict_types=1);

class SkipListNode
{
    /** @var SkipListNode[] */
    public array $next;

    public function __construct(
        public readonly int|float|null $key,
        public mixed $value,
        int $levels
    ) {
        $this->next = array_fill(0, $levels, null);
    }
}

class SkipList
{
    private SkipListNode $head;
    private int $currentMaxLevel = 1;
    private int $size = 0;

    private const MAX_LEVELS  = 16;
    private const PROBABILITY = 0.5;

    public function __construct()
    {
        $this->head = new SkipListNode(null, null, self::MAX_LEVELS);
    }

    private function randomLevel(): int
    {
        $level = 1;
        while ($level < self::MAX_LEVELS && (mt_rand() / mt_getrandmax()) < self::PROBABILITY) {
            $level++;
        }
        return $level;
    }

    public function insert(int|float $key, mixed $value): void
    {
        $update  = array_fill(0, self::MAX_LEVELS, null);
        $current = $this->head;

        for ($i = $this->currentMaxLevel - 1; $i >= 0; $i--) {
            while ($current->next[$i] !== null && $current->next[$i]->key < $key) {
                $current = $current->next[$i];
            }
            $update[$i] = $current;
        }

        $current = $current->next[0];
        if ($current !== null && $current->key === $key) {
            $current->value = $value;
            return;
        }

        $newLevel = $this->randomLevel();
        if ($newLevel > $this->currentMaxLevel) {
            for ($i = $this->currentMaxLevel; $i < $newLevel; $i++) {
                $update[$i] = $this->head;
            }
            $this->currentMaxLevel = $newLevel;
        }

        $newNode = new SkipListNode($key, $value, $newLevel);
        for ($i = 0; $i < $newLevel; $i++) {
            $newNode->next[$i]    = $update[$i]->next[$i];
            $update[$i]->next[$i] = $newNode;
        }

        $this->size++;
    }

    public function search(int|float $key): mixed
    {
        $current = $this->head;

        for ($i = $this->currentMaxLevel - 1; $i >= 0; $i--) {
            while ($current->next[$i] !== null && $current->next[$i]->key < $key) {
                $current = $current->next[$i];
            }
        }

        $current = $current->next[0];
        return ($current !== null && $current->key === $key) ? $current->value : null;
    }

    public function delete(int|float $key): bool
    {
        $update  = array_fill(0, self::MAX_LEVELS, null);
        $current = $this->head;

        for ($i = $this->currentMaxLevel - 1; $i >= 0; $i--) {
            while ($current->next[$i] !== null && $current->next[$i]->key < $key) {
                $current = $current->next[$i];
            }
            $update[$i] = $current;
        }

        $current = $current->next[0];
        if ($current === null || $current->key !== $key) {
            return false;
        }

        for ($i = 0; $i < $this->currentMaxLevel; $i++) {
            if ($update[$i]->next[$i] !== $current) break;
            $update[$i]->next[$i] = $current->next[$i];
        }

        $this->size--;
        return true;
    }

    public function range(int|float $min, int|float $max): array
    {
        $result  = [];
        $current = $this->head;

        for ($i = $this->currentMaxLevel - 1; $i >= 0; $i--) {
            while ($current->next[$i] !== null && $current->next[$i]->key < $min) {
                $current = $current->next[$i];
            }
        }

        $current = $current->next[0];
        while ($current !== null && $current->key <= $max) {
            $result[$current->key] = $current->value;
            $current = $current->next[0];
        }

        return $result;
    }

    public function count(): int { return $this->size; }
}

Practical application – product price index

<?php

$priceIndex = new SkipList();

$products = [
    ['id' => 1, 'name' => 'Widget S',   'price' => 9.99],
    ['id' => 2, 'name' => 'Widget M',   'price' => 29.99],
    ['id' => 3, 'name' => 'Widget L',   'price' => 49.99],
    ['id' => 4, 'name' => 'Gadget Pro', 'price' => 99.99],
    ['id' => 5, 'name' => 'Gadget Max', 'price' => 149.99],
];

foreach ($products as $product) {
    $priceIndex->insert($product['price'], $product);
}

// Range query O(log n + k) where k = number of results
$inRange = $priceIndex->range(20.0, 100.0);
// Returns: Widget M (29.99), Widget L (49.99), Gadget Pro (99.99)

foreach ($inRange as $price => $product) {
    echo "{$product['name']}: {$price} PLN\n";
}

Benchmark: Skip List vs array_filter

<?php

$n        = 100_000;
$skipList = new SkipList();
$array    = [];

for ($i = 0; $i < $n; $i++) {
    $price = mt_rand(1, 100000) / 100;
    $skipList->insert($price, "product_{$i}");
    $array[] = ['price' => $price, 'id' => $i];
}

$min = 50.0; $max = 100.0;

$start = microtime(true);
for ($q = 0; $q < 1000; $q++) {
    $result = $skipList->range($min, $max);
}
$skipTime = (microtime(true) - $start) * 1000;

$start = microtime(true);
for ($q = 0; $q < 1000; $q++) {
    $result = array_filter($array, fn($p) => $p['price'] >= $min && $p['price'] <= $max);
}
$arrayTime = (microtime(true) - $start) * 1000;

echo "Skip List range: {$skipTime}ms\n";  // ~45ms
echo "array_filter:    {$arrayTime}ms\n"; // ~890ms - ~20x slower

Comparison with tree structures

Structure Search Insert Delete Implementation
Skip List O(log n) avg O(log n) avg O(log n) avg Simple
BST (unbalanced) O(n) worst O(n) worst O(n) worst Simple
AVL Tree O(log n) O(log n) O(log n) Complex
Red-Black Tree O(log n) O(log n) O(log n) Very complex
Hash Map O(1) avg O(1) avg O(1) avg Medium

Skip List vs Hash Map: Hash Map is faster for exact lookups, but does not support range queries. Skip List fills that niche - O(log n) for a range instead of O(n) for iterating a hash map.

Summary

Skip List is an elegant example of how randomness can replace complex balancing algorithms. The implementation is many times simpler than AVL or Red-Black Trees with comparable computational complexity. In PHP it is most useful for range queries on large in-memory datasets - filtering products by price, searching by date, indexing by any numeric key. Redis uses Skip List internally in Sorted Sets - which confirms the practical value of this structure. Next post: Flyweight pattern - object sharing and instance caching.

About Henryk Tews

What you can read next

Sorting in PHP – bubble sort, merge sort, usort() and the spaceship operator <=>
LRU Cache – O(1) implementation with HashMap and doubly linked list, per-request cache
BFS and DFS – graph traversal, shortest path, cycle detection

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}