多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

php经典算法集锦

php经典算法集锦 前言PHP 的标准库足够厚实sort()、array_search()、array_unique()能覆盖日常九成需求于是很多人会觉得「手写算法没意义」。这个看法对一半生产代码确实应该优先用内置函数但理解算法的价值不在「重复实现一遍」而在于三件事——知道复杂度决定数据量翻倍时会慢多少、知道内置函数在什么情况下会给出你不想要的结果、遇到内置函数解决不了的问题时知道该往哪个方向想。另一个常见的误区是只背结论不看边界。比如二分查找写得很顺边界收缩写成$hi $mid就会死循环array_unique()默认按字符串比较1和1会被当成重复。这些都不是「算法没学好」而是边界没抠准。本文按排序、查找、数值、数组技巧四组整理经典算法每个都给出可以直接执行的 PHP 实现并附上复杂度对照。文中不给出任何性能倍数——不同版本、不同数据分布差异很大需要数字请自己跑 benchmark。一、排序四种基础排序?php // 适用于 PHP 7.1用到了列表解构赋值declare(strict_types1);// 冒泡排序相邻比较每轮把当前最大值推到末尾function bubbleSort(array $arr): array{$n count($arr);for ($i 0; $i $n - 1; $i) {$swapped false;for ($j 0; $j $n - 1 - $i; $j) {if ($arr[$j] $arr[$j 1]) {[$arr[$j], $arr[$j 1]] [$arr[$j 1], $arr[$j]];$swapped true;}}if (!$swapped) {break; // 本轮没有交换说明已经有序}}return $arr;}// 插入排序把当前元素插到前面已排好序的合适位置function insertionSort(array $arr): array{$n count($arr);for ($i 1; $i $n; $i) {$cur $arr[$i];$j $i - 1;while ($j 0 $arr[$j] $cur) {$arr[$j 1] $arr[$j];$j--;}$arr[$j 1] $cur;}return $arr;}// 选择排序每轮从未排序区间里挑最小值交换到区间开头function selectionSort(array $arr): array{$n count($arr);for ($i 0; $i $n - 1; $i) {$min $i;for ($j $i 1; $j $n; $j) {if ($arr[$j] $arr[$min]) {$min $j;}}if ($min ! $i) {[$arr[$i], $arr[$min]] [$arr[$min], $arr[$i]];}}return $arr;}归并排序需要分成「分」与「合」两步合的过程用一个独立函数?php // 适用于 PHP 7.0用到了 intdivdeclare(strict_types1);function mergeSort(array $arr): array{$n count($arr);if ($n 2) {return $arr;}$mid intdiv($n, 2);return mergeTwo(mergeSort(array_slice($arr, 0, $mid)),mergeSort(array_slice($arr, $mid)));}function mergeTwo(array $a, array $b): array{$out [];$i 0;$j 0;$na count($a);$nb count($b);while ($i $na $j $nb) {// 用 保证稳定性相等时先取左边的if ($a[$i] $b[$j]) {$out[] $a[$i];} else {$out[] $b[$j];}}while ($i $na) { $out[] $a[$i]; }while ($j $nb) { $out[] $b[$j]; }return $out;}复杂度对照这是分析结论不是实测数字算法平均时间最坏时间额外空间稳定冒泡排序O(n²)O(n²)O(1)是插入排序O(n²)O(n²)O(1)是选择排序O(n²)O(n²)O(1)否归并排序O(n log n)O(n log n)O(n)是快速排序O(n log n)O(n²)O(log n)否PHP 内置sort()C 实现——自 PHP 8.0 起稳定关于最后一行PHP 的sort()、usort()用 C 实现没有 PHP 用户态函数调用和逐元素装箱的开销所以生产环境应当优先使用它们。PHP 8.0 起排序函数改为稳定排序这是 php-src 的 UPGRADING 中明确记录的变化也就是说相等元素的相对顺序会被保留。二、查找顺序与二分顺序查找就是foreach一遍这里只说二分。二分要求数组已经有序每次比较中间元素、砍掉一半区间?php // 适用于 PHP 7.0declare(strict_types1);/*** param listint $sorted 已升序排列的数组* return int 找到时返回下标找不到返回 -1*/function binarySearch(array $sorted, int $target): int{$lo 0;$hi count($sorted) - 1;while ($lo $hi) {$mid intdiv($lo $hi, 2);if ($sorted[$mid] $target) {return $mid;}if ($sorted[$mid] $target) {$lo $mid 1; // 目标在右半区} else {$hi $mid - 1; // 目标在左半区}}return -1;}$data [1, 3, 5, 7, 9, 11];var_dump(binarySearch($data, 7)); // int(3)var_dump(binarySearch($data, 8)); // int(-1)两个要点$lo $hi用的是小于等于区间非空才继续两条分支都必须把区间真正缩小$mid 1与$mid - 1否则会死循环。二分的时间复杂度是 O(log n)——每次砍一半。三、数值与字符串经典题?php // 适用于 PHP 7.0declare(strict_types1);// 阶乘迭代版function factorial(int $n): int{if ($n 0) {throw new InvalidArgumentException(n 不能为负);}$r 1;for ($i 2; $i $n; $i) {$r * $i;}return $r;}// 斐波那契迭代版O(n)function fib(int $n): int{if ($n 0) {throw new InvalidArgumentException(n 不能为负);}$a 0;$b 1;for ($i 0; $i $n; $i) {[$a, $b] [$b, $a $b];}return $a; // fib(0)0, fib(1)1, fib(2)1 ...}// 素数筛埃拉托斯特尼筛法function sieve(int $n): array{if ($n 2) {return [];}$isPrime array_fill(0, $n 1, true);$isPrime[0] false;$isPrime[1] false;for ($i 2; $i * $i $n; $i) {if (!$isPrime[$i]) {continue;}// 从 i*i 开始划掉更小的倍数已经被更小的素数划过了for ($j $i * $i; $j $n; $j $i) {$isPrime[$j] false;}}return array_keys(array_filter($isPrime));}// 最大公约数辗转相除法function gcd(int $a, int $b): int{$a abs($a);$b abs($b);while ($b ! 0) {[$a, $b] [$b, $a % $b];}return $a;}// 回文判断单字节字符集function isPalindrome(string $s): bool{return $s strrev($s);}// 回文判断UTF-8按字符而非字节比较function isPalindromeUtf8(string $s): bool{$chars preg_split(//u, $s, -1, PREG_SPLIT_NO_EMPTY);return $chars array_reverse($chars);}echo factorial(5), PHP_EOL; // 120echo fib(10), PHP_EOL; // 55echo implode(,, sieve(20)), PHP_EOL; // 2,3,5,7,11,13,17,19echo gcd(48, 18), PHP_EOL; // 6var_dump(isPalindromeUtf8(上海自来水来自海上)); // bool(true)几处必须注意的点factorial()用的是 PHP 原生int64 位平台上最大能表示到20!243290200817664000021!会溢出成float——此时因为返回类型声明为int严格模式下会抛TypeError。真要算大数得用bcmath或gmp扩展bcmath需要 php-bcmathgmp需要 php-gmp。fib()用迭代而不是朴素递归。朴素递归版每层都重复计算两个子问题复杂度是 O(2^n)n 稍大就跑不动迭代版是 O(n)、O(1) 空间。strrev()按字节反转遇到 UTF-8 中文会破坏字符所以多字节场景必须用preg_split(//u, ...)或mb_*系列按字符处理。筛法从$i * $i开始划这是埃拉托斯特尼筛法的标准优化正确性依赖「小于i²的合数一定有小于i的质因数」。四、数组技巧?php // 适用于 PHP 7.0declare(strict_types1);// 两数之和用哈希表把「找配对」降到一趟遍历时间 O(n)、空间 O(n)function twoSum(array $nums, int $target): ?array{$seen []; // 值 下标foreach ($nums as $i $v) {$need $target - $v;if (isset($seen[$need])) {return [$seen[$need], $i];}$seen[$v] $i;}return null;}// 最大子数组和Kadane 算法function maxSubArraySum(array $nums): int{if ($nums []) {throw new InvalidArgumentException(数组不能为空);}$best $nums[0];$cur $nums[0];for ($i 1, $n count($nums); $i $n; $i) {$cur max($nums[$i], $cur $nums[$i]);$best max($best, $cur);}return $best;}var_dump(twoSum([2, 7, 11, 15], 9)); // array(2) { [0] int(0) [1] int(1) }var_dump(maxSubArraySum([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // int(6)twoSum()里用到了 PHP 数组的一个关键性质数组是哈希表HashTable以整数或字符串为键的查找是 O(1) 平均复杂度。判断键是否存在用isset()就够比array_key_exists()更快因为前者不需要区分「值是否为 null」但如果你的数据里可能出现null值就必须改用array_key_exists()否则会把null误判成「不存在」。常见坑点❌ 用array_unique()去重混合类型的数组 —— ✅ 它默认按字符串比较1与1会被判为重复需要严格去重就先规范类型或自己用in_array($v, $out, true)累加。❌in_array($needle, $haystack)不传第三个参数 —— ✅ 默认是松散比较容易出意外判断身份一律写in_array($needle, $haystack, true)。❌ 二分查找写成$hi $mid或$lo $mid—— ✅ 区间没有真正缩小会死循环必须是$mid - 1/$mid 1。❌ 归并排序合并时用而不是—— ✅ 相等时先取右边会让排序不稳定要稳定就用先取左边。❌ 用朴素递归求 Fibonacci —— ✅ 复杂度 O(2^n)同一子问题被反复计算改迭代 O(n)或加记忆化缓存。❌ 以为 PHP 的整数可以无限大 —— ✅ 64 位有符号整数20!已接近上限21!起溢出为float大数运算要用bcmath/gmp。❌ 用strrev()处理含中文的回文判断 —— ✅ 它按字节反转会破坏多字节字符用preg_split(//u, $s, -1, PREG_SPLIT_NO_EMPTY)按字符拆分。❌ 把 O(n²) 的排序用在线上大批量数据上 —— ✅ 数据量翻倍耗时会变成约四倍优先用 C 实现的sort()/usort()或换成 O(n log n) 的归并排序。总结类别算法时间关键约束排序冒泡 / 插入 / 选择O(n²)小数据或教学用插入排序对近似有序数据表现好排序归并O(n log n)需要 O(n) 额外空间稳定排序快速平均 O(n log n)最坏 O(n²)枢轴选择决定最坏情况查找二分O(log n)数组必须已排序边界必须收缩数值筛法求素数O(n log log n)从 i² 开始划数值辗转相除O(log n)除数为 0 时结束数组哈希表配对O(n)依赖 PHP 数组的 O(1) 键查找结论学算法的产出不是「会背代码」而是拿到一个问题能立刻判断出「数据量翻倍会慢几倍、会不会爆内存、边界在哪」。写 PHP 业务代码时默认选择内置函数C 实现、且 PHP 8.0 起排序稳定当内置函数语义不匹配比如array_unique()的字符串比较或规模超出承受范围时再换上文里这些实现。最后一句提醒本文所有复杂度都是分析结论任何「快多少倍」的说法都必须自己跑数据验证。
返回列表