PHP usort 函数底层排序

引出

最近在一个项目中, 需要对一个数组的顺序进行调整, 允许手动将某一个元素提到数组的开头位置. 在这里, 使用了PHP中的usort函数进行了数组的排序, 代码大致如下:

usort($arr, function ($a, $b){// 这里添加了 order 字段, 默认为0, 将order大的提到前边return $b['order'] - $a['order'];
});

但是, 今天我大哥突然告诉我, php的usort是不稳定的, 也就是在两个元素相等的情况下, 不能够保证两个元素的位置不变.

在我想到的排序算法中: 选择冒泡插入快排希尔堆排计数归并, 其中可以稳定排序的算法有: 冒泡插入归并. 而这几个算法, 时间复杂度较小的是: 快排归并堆排. 时间复杂度是O(log n). 如果要选择一款既能够保证稳定性, 时间复杂度又小的算法, 二者取交集也得选择 归并 吧.

但是, 毕竟我不是PHP作者, 咱也不知道人家到底用的是什么, 于是乎, 我决定实验一下, 下面这段代码产生了:

// 生成一个随机数组
$arr = [];
for ($j = 0; $j < 100; $j++){$arr[] = ['id' => $j,'order' => random_int(0, 3),];
}
// 按照order进行排序
usort($arr, function ($a, $b){return $b['order'] - $a['order'];
});
// 验证是否稳定
$lastValue = null;
foreach ($arr as $value){if(empty($lastValue)){$lastValue = $value;continue;}// 若当前元素与上一个元素order相同, 判断if($value['order'] == $lastValue['order']){// 当前不稳定了, 把数组打印出来if($value['id'] < $lastValue['id']){echo '不稳定', PHP_EOL;exit;}}$lastValue = $value;
}

经过验证, 果然, 我哥诚不欺我. 但是, 我记得我之前也测试过, 数组顺序没有变化啊, 我尝试将数组的长度缩小为4, 突然发现, 是我错了.

分析

既然确定了usort函数是不稳定的排序, 那么他到底是如何进行排序的呢? 我决定尝试着到PHP的源码中挑战一下.

到PHP官方 https://www.php.net/downloads 将源码下载下来. 解压完了也没太看懂目录结构, 既然知道是c语言写的, 尝试文件夹搜索 array.c , 嗯, 搜到了, 将文件打开. 搜索 usort. 嗯, 有的.

再去 php_usort 函数看看:

static void php_usort(INTERNAL_FUNCTION_PARAMETERS, compare_func_t compare_func, zend_bool renumber) /* {{{ */
{zval *array;zend_array *arr;zend_bool retval;PHP_ARRAY_CMP_FUNC_VARS;PHP_ARRAY_CMP_FUNC_BACKUP();// 这个 zend 打头的函数一看就是虚拟机相关的, 不管了ZEND_PARSE_PARAMETERS_START(2, 2)Z_PARAM_ARRAY_EX2(array, 0, 1, 0)Z_PARAM_FUNC(BG(user_compare_fci), BG(user_compare_fci_cache))ZEND_PARSE_PARAMETERS_END_EX( PHP_ARRAY_CMP_FUNC_RESTORE(); return );arr = Z_ARR_P(array);// 一看就是对数组进行判空, 跳过if (zend_hash_num_elements(arr) == 0)  {PHP_ARRAY_CMP_FUNC_RESTORE();RETURN_TRUE;}/* Copy array, so the in-place modifications will not be visible to the callback function */arr = zend_array_dup(arr);// 真正进行排序的方法retval = zend_hash_sort(arr, compare_func, renumber) != FAILURE;// 一些善后操作zval_ptr_dtor(array);ZVAL_ARR(array, arr);PHP_ARRAY_CMP_FUNC_RESTORE();RETURN_BOOL(retval);
}

简单看了一下, 找到真正的排序方法zend_hash_sort, OK, 再去这个函数里看看. 那么问题来了, 这个函数在哪呢? 找不到? 暴力破解, 简单写了个Python代码, 将所有文件中带有 zend_hash_sort 的文件都打印出来:

很幸运, 在第一个文件中就找到了.

什么? 是个宏? OK, 正好刚写了程序, 我再重新找一下 zend_hash_sort_ex 函数在哪里.

经过一番苦苦寻找, 终于在 Zend/zend_hash.c 文件下找到了最终的排序算法. 其他的没看懂, 但是, 这里有一句我知道, 是排序的关键:

好吧, 又去调 sort函数, 通过查看, 这个sort函数是本函数的第二个参数, 那在返回去看zend_hash_sort的宏定义, 嗯, 是 zend_sort 函数, 成吧, 再去找这个函数. 发现并不在这两个文件下, 再动用我临时写的Python脚本(这都用三次了, 要不我把他好好封装一下??). 最终在Zend/zend_sort.c 文件中找到. 到此, 原谅我太菜了, 在自己阅读并进行了大量搜索之后, 还是没太看懂排序的流程.

不过, 虽然代码没看懂, 但是, 排序选择的算法我知道了

  1. 若数组长度小于等于16, 使用 插入排序
  2. 若数据长度大于16, 使用 快速排序 (快速排序对元素个数1024前后做了不同的处理, 应该是优化)

总结

再回想一下, 最开始的问题, 当数组长度小于4的时候, 顺序没有改变, 这个因为使用了稳定的插入排序. 当数组长度100的时候, 使用了不稳定的快速排序.

之后使用usort函数, 就把他当做不稳定的就可以了. 这样基本不会有问题的. 但是, 讲话了, 如果我就是需要一个稳定的排序算法怎么办?

来来来, 官方函数推荐给你https://www.php.net/manual/zh/function.uasort.php

If you want to keep the order when two members compare as equal, use this.
<?phpfunction stable_uasort(&$array, $cmp_function) {if(count($array) < 2) {return;}$halfway = count($array) / 2;$array1 = array_slice($array, 0, $halfway, TRUE);$array2 = array_slice($array, $halfway, NULL, TRUE);stable_uasort($array1, $cmp_function);stable_uasort($array2, $cmp_function);if(call_user_func($cmp_function, end($array1), reset($array2)) < 1) {$array = $array1 + $array2;return;}$array = array();reset($array1);reset($array2);while(current($array1) && current($array2)) {if(call_user_func($cmp_function, current($array1), current($array2)) < 1) {$array[key($array1)] = current($array1);next($array1);} else {$array[key($array2)] = current($array2);next($array2);}}while(current($array1)) {$array[key($array1)] = current($array1);next($array1);}while(current($array2)) {$array[key($array2)] = current($array2);next($array2);}return;
}

简单看了一下, 就是一个标准的归并排序.

这次是我的失误, 当初其实想到了排序的稳定性问题, 然后写了个demo验证了一下(就是长度为4的数组), 然后自认为是稳定的, 其实随便到网上搜一下, 都能搜到的问题的. 引以为鉴.


最后, 当我google找了一下, 发现第一条搜索就告诉了我, PHP的排序对不同长度分别使用了不同的排序算法. 这就尴尬了. 么事, 虽然最后对算法也没完全看懂, 但乐在其中

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/508428.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mac docker搭建开发环境

前言 刚买了一个mac本, 决定搭建一个纯docker的开发环境, 说到做到, 开始踩坑. 搭建 在搭建环境的过程中, 经历了很多错误, 例如为了令两个docker环境可以互通(如: nginx和php-fpm), 尝试了 link, network等等等等方式. 最后发现, 如果想实现两个docker环境的互通, 可以通过…

密钥交换算法: 迪菲-赫尔曼算法

概述 迪菲-赫尔曼算法用于通信双方交换密钥. 还记得之前介绍HTTPS协议的时候, 提到需要先通过对方公钥来进行密钥的交换, 然后再通过密钥对通信内容进行加密. 迪菲-赫尔曼算法就是用于交换密钥的. . 此算法与非对称加密算法不同哦. OK, 一起来看看吧. 引入 在正式介绍迪菲-…

纠错码简介

纠错码是个什么东西 引出 网络中的通信基于TCP和UDP两个通信协议, 这大家都知道的, 什么TCP的三次握手等等, 面试经常被问到. 三次握手是为了保证连接的正确建立. 但是, 在通信的时候, 你如何保证你的消息正确送达了呢? 有人说了, 有收到请求的响应包. 但我说的不是这个, 比…

计算机全加器简单实现

概述 用了这么久计算机, 都知道计算机有一个核心部件叫 CPU, 而 CPU中有一个小部件叫做全加器. 它是用来做什么的呢? 看名字就知道了, 做加法运算用的. 那么如何实现一个全加器呢? 你以为这又是一篇计算机内部原理的文章? 不, 放开那个女孩, 和我一起走进中学物理的课堂. …

天天看底层有什么用

疑问 其实之前我一直有个疑问, 每天工作在应用层, 就算知道 TCP 的传包, 包的校验等等, 在工作中其实是用不到的, 每天看这些东西用什么用呢? 其一, 对工作的内容其实是起不到任何作用的, 其二, 浪费的时间如果将其用在工作上是可以产出更多效益的. 那么, 带给我的到底是什么…

求最大连续子集

问题 前两天看到一道算法题, 想了几天, 然后到网上搜了搜, 基本和我想到的相契合. 来, 题目如下: 给出一个数组, 求出和最大的连续子集. 举个例子: 数组 [1, 2, 3, 4, 5] 那和最大的就是数组本身了. 但是, 如果中间出现负数, 那情况立刻就不一样了, 你需要考虑是否能够将负数…

数据压缩算法

概述 之前在听到数据压缩的时候, 想着肯定是某些高深莫测的算法, 能够完成数据的压缩这种事情, 最近看了看, 嗯, 至少咱还是能看懂的. 无损压缩 众所周知, 不管你是exe, word, txt, dmg等等, 在存储上都是以二进制进行存储的, 所以, 在讨论压缩时, 忽略文件格式即可, 只要将…

何为真何为假

还记得第一个提出日心说的人是谁么? 没错, 哥白尼. 但是在那个年代, 所有人都认为太阳围绕着地球转的年代. 哥白尼的发现打破了大家的常识, 甚至于有过了很多年, 日心说才逐渐被大众所接受. 即使到了现在, 生活中仍然留有地心说的影子, 比如"日出" “日落” 等等, 就…

TCP 三次握手的意义

概述 在网络的传输层协议中, 存在着两大悍将: TCP 和 UDP . 从前, 我傻傻的以为自己对他们虽谈不上精通, 但还是知道的, 但是, 我错了, 我被自己问住了, 我傻了. 啥也不是. UDP (这里为了介绍简单, 就不提数据在传输过程中的失真(纠错码)等情况了. 简单介绍一下, TCP才是今天…

随机数是如何生成的

引出 在现实中, 会有抛硬币猜正反的操作, 硬币要么是正, 要么是反, 在揭晓之前, 我们谁也不知道它现在的状态. 而这, 是因为其中存在着很大的不确定因素, 如抛硬币的力度、抛硬币的角度、接硬币的力度和角度、硬币的重量、当前风速等等. 但是在计算机中, 要想生成一个随机数,…

GO 文档笔记

前言 最开始写 GO 的时候, 发现方法的注释并不支持param, return等参数, 搞得我都不知道该如何给自己的方法写文档说明了. 而且网上搜了搜也没有搜到教程, 甚是郁闷. 今天找到了GO内置的文档工具: godoc. (我用的1.14.3版本貌似不是自带工具了, 需要安装(配置代理): go get g…

长整数的乘法运算

概述 都知道, 计算机中存储整数是存在着位数限制的, 所以如果需要计算100位的数字相乘, 因为编程本身是不支持存储这么大数字的, 所以就需要自己实现, 当然了, 各个编程语言都有大数的工具包, 何必重复造轮子, 但我还是忍不住好奇他们是如何实现的, 虽然最终没有翻到他们的底层…

如何将数字转换成口语中的文本串

概述 今天突发奇想, 写一个将数字转换成中文字符串的函数. 并不是将 1234 转成 1234 , 而是将 1234 转成 一千二百三十四. 本来以为很简单, 写下来之后发现还是有些坑的. 尝试 因为我是在写完最终版本, 回过头来整理的这篇文章, 所以中间很多尝试的步骤会有所遗漏. 以下简单…

设计模式故事会

想着用讲故事的方式对各个设计模式进行一个稍微形象一些的描述. 当然, 某些因为没有想到特别好的例子, 可能比喻的并不是那么的恰当, 望赐教. 绞尽脑汁啊, 尽力贴近设计模式其本来的意图. 主要为了加强一些自己对设计模式的理解. 进入正题: 小王是一名心怀梦想的90后青年, 他独…

有了 elseif 为什么还要 switch case

引出 你有没有想过既然有了if elseif, 为什么还要设计一个switch case的语法出来呢? 按理说, 一个语言的设计角度来说, 关键词越少越好吧, 而且多出来一种选择分支也没有看出太大用处. 以下几种switch case均可以写成if else的形式(java 代码): // 形式一 switch(a){case 1:…

计算矩阵中全1子矩阵的个数

前言 最近被我大哥安利了一道算法题, 这道题说难, 还不至于我做不出来, 说简单吧, 我还想不到最优解, 等把最优解告诉我之后, 我还正好能理解. 我甚至曾经怯怯的认为, 这题就是我哥专门给我找的, 嘿嘿, 心中说不出的小欢喜. 题来了, 此题出自力扣, 原题链接: https://leetco…

GO 的方法集

前言 之前在写 GOdemo 的时候, 写了这么一段程序(大概意思): package maintype Test struct { }func (test *Test) print() {println("test fun") }func main() {Test{}.print() }结果一编译就报错了: cannot call pointer method on Test literal 差不多意思是不…

GO 切片实力踩坑

概述 GO 语言的切片这两天用了用, 可以支持切割数组的中间部分. 但今天使用中, 出了 bug, 查了半天, 发现是切片的问题, 简单写个 demo 复现当时的情况: package mainimport "fmt"func main() {a : []int{1, 2, 3, 4, 5}b : a[2:4]b[0] 9fmt.Println(a) }你以为输…

搭建本地 HTTPS 环境

前言 之前写自己的网站时, 申请过免费的https证书. 最近想在自己本地搭一个, 结果忘了当初证书是怎么来的了. 本来想着去申请个免费的证书, 但想了想, 我只需要在自己本地能使用就行了, 我自己的环境, 那当然是我说了算了. 只要能够将证书构造出来, 安装到本地就可以识别了. 搜…

《人人都是产品经理》读后感

之前读书, 一般看的都是技术、算法、数学、哲学这些方面的, 为什么突然想看这本书呢? 身为一个开发人员, 每天打交道最多的, 除了开发和测试, 就是产品了. 还记得我刚来公司的时候, 在和产品的沟通过程中, 听到最多的话估计就是: 你要懂产品, 你懂了就不会做错. 确实有很多需求…