文章目录

  • 一、前言
  • 二、位运算实现逻辑
    • 1、逻辑部分如下
    • 2、存入数据库部分的代码
    • 3、查询数据库示例
    • 4、php读取字段,并用位与运算解开存留信息
  • 三、偶然发现的bug(php大数计算问题)
    • 1、科学计数法
    • 2、科学计数法是否可以位运算?
    • 3、php的位运算受int范围限制?
    • 4、关于float类型14位的限制
    • 5、关于大数的计算

一、前言

有的时候我们需要多个字段来标识用户的一些状态,比如计算日存留这种问题,我们需要标识一个用户,他在哪天活跃,假如活跃的话,状态标识为1,该天不活跃则为0,建表如下:

`day1` tinyint(1) NOT NULL DEFAULT '0',`day2` tinyint(1) NOT NULL DEFAULT '0',`day3` tinyint(1) NOT NULL DEFAULT '0',`day4` tinyint(1) NOT NULL DEFAULT '0',`day5` tinyint(1) NOT NULL DEFAULT '0',`day6` tinyint(1) NOT NULL DEFAULT '0',`day7` tinyint(1) NOT NULL DEFAULT '0',`day8` tinyint(1) NOT NULL DEFAULT '0',`day9` tinyint(1) NOT NULL DEFAULT '0',
.........`day27` tinyint(1) NOT NULL DEFAULT '0',`day28` tinyint(1) NOT NULL DEFAULT '0',`day29` tinyint(1) NOT NULL DEFAULT '0',`day30` tinyint(1) NOT NULL DEFAULT '0',

这里可以看到,当我们要记录某个用户近30日的每天存留信息的话,需要新建30个字段,实在是恐怖。更关键的是维护十分困难。因此我们这里引出我的主题:位运算

先说结果,使用位运算,我们只需要新建一个字段即可标识用户的近30日存留:

`drr_daily` varchar(100) NOT NULL DEFAULT '0',

二、位运算实现逻辑

1、逻辑部分如下

    /*** d0是注册24小时内, d1是注册后24-48小时, 为次存,用2的指数来计算, d0的话, 0|pow(2,0-1)=0* 比如计算d1, 时间差在1*24*3600-2*24*3600之间的,所以是floor((($field - unix_timestamp(log_time))/(1*24*3600)))=1的就是, 用1表示, 即2^0, 所以需要-1* 比如计算d2, 时间差在2*24*3600-3*24*3600之间的,所以是floor((($field - unix_timestamp(log_time))/(1*24*3600)))=2的就是, 用2表示, 即2^1, 所以需要-1* 比如计算d3, 时间差在3*24*3600-4*24*3600之间的,所以是floor((($field - unix_timestamp(log_time))/(1*24*3600)))=3的就是, 用4表示, 即2^2* 结果按位或计算在一起, 所以前3天的就是(2^(1-1))|(2^(2-1))|(2^(3-1))=7, db中记录7, 计算的时候, 用按位与判断, 7&1=1, 7&2=2, 7&4=4,* 如果是第一天和第三天有记录, 则数据是(2^(1-1))|(2^(3-1))=5, db中记录5, 计算的时候, 用按位与判断, 5&1=1, 5&2=0, 5&4=4, 为0的就说明当天没有数据,* 如果是第八天和第20天有记录, 则数据是(2^(8-1))|(2^(20-1))=524416, db中记录524416, 计算的时候, 用按位与判断, 524416&1=0, 524416&2=0, 524416&128=128, 524416&524288=524288 为0的就说明当天没有数据,*/

如注释所示,我们这里用到的方法就是按照位或进行计算,把用户的存留信息全部按照2的n次幂(n = 存留天数 -1)给位或一下,获取一个唯一的值,存入到drr_daily字段中。然后需要判断用户存留的时候,再取出来,进行位与运算即可。

网上百度的时候发现有类似的用法,不过他们用的稍微少一些,是通过位运算来存储权限相关的信息,类似于linux的权限系统,大家可以参考下:

可以参考:

https://blog.csdn.net/kissxia/article/details/49584599
https://blog.csdn.net/e421083458/article/details/12975443
这里运用了按位与运算的特性:任意组合相加的值不会重复。

2、存入数据库部分的代码

$type = 'drr_daily';$setStr = " pow(2, x-1) ";  // x为时间差,即用户活跃时间与注册时间的时间差$sql = "update aaaa as a, bbbb as b set drr_{$type} = drr_{$type} | {$setStr} where a.uuid = b.uuid {$sqlWhere} ";
}

存储的时候,每次都对当前的drr_daily 字段的值 和计算出来的2^(n-1)的值进行位或计算,然后存储到数据库。

3、查询数据库示例

//按照查询当天注册用户在近30天内的留存
for ($i = 1; $i <= $this->days; $i++) {$ok = number_format(pow(2, $i - 1), 0, '', '');$sql = "select date_format(a.log_time, '%Y-%m-%d') as drr_day, count(*) as nums from xxxx as awhere 1 {$search_where} and drr_daily & {$ok} > 0 group by drr_day ";$dataRows =$xxx->queryAll();
}

结果示例:

结果形如:
不存在的则代表当日用户没有上线,无留存信息
array(3) {["2018-10-05"]=>array(7) {["Day 0"]=>string(1) "1"["Day 2"]=>string(1) "1"["Day 3"]=>string(1) "1"["Day 6"]=>string(1) "1"["Day 7"]=>string(1) "1"["Day 11"]=>string(1) "1"["Day 13"]=>string(1) "1"}

4、php读取字段,并用位与运算解开存留信息

function parseAOI($final_str)
{$base_num = [];$arr_finished = [];$final_str = doubleval($final_str); //参数转化为float类型for ($i = 0; $i <= 30; $i++) {$base_num[$i + 1] = number_format(pow(2, $i), 0, '', '');//取整2的$i次方$arr_finished[] = $i + 1;}$parse_val = [];//下面循环的$key代表天数foreach ($base_num as $key => $num) {if ($final_str & $num) {  // 这里对传入的参数和2的n次方进行位与运算$parse_val[$key] = 1;//位与结果为1则代表当日有存留信息} else {$parse_val[$key] = 0;}}return $parse_val;
}

如注释所示,这样我们就完美计算出了用户的每日存留信息。只需要一个字段即可,关键是逻辑上也清晰了很多,减少了不必要的冗余。

三、偶然发现的bug(php大数计算问题)

本来计算一个月存留信息是妥妥的,但是后来就想着能显示60天的留存信息岂不美哉,于是咱们拓展循环到60,结果发现了一些问题。

打印上面parseAOI()方法的key和位与计算的部分:

var_dump("$key D :".($final_str & $num));打印结果:
string(7) "27 D :0"
string(15) "28 D :134217728"
string(15) "29 D :268435456"
string(15) "30 D :536870912"
string(7) "31 D :0"
string(16) "32 D :1000005119"
string(16) "33 D :1000005119"
string(16) "34 D :1000005119"
string(16) "35 D :1000005119"

key=32的时候,num = 2147483648 (接下来超出int类型限制),之后的计算结果就不准确了,或者说用float类型来进行位运算本来就是有问题的,下面咱们再来探究下。

1、科学计数法

计算的时候发现,当达到2^47次方的时候,得出的值是科学计数法表示的大数,而当计算2^46的时候,得出的是浮点型,如下所示:

$nums = pow(2,46);
var_dump($nums);
47:float(1.4073748835533E+14)
46: float(70368744177664)

2、科学计数法是否可以位运算?

/第1,2,50天都有活跃的话
$a = 3;
$b =  number_format(pow(2, 50-1), 0, '', '');
var_dump($b); //string(15) "562949953421312"
$res = $a | $b;
var_dump($res);  //2147483647   使用字符串进行或计算是有问题的。如果不转成字符串,那么将会转化为科学计数法,计算位或的结果为3//第1,2,42天有活跃
$a = 3;
$b = pow(2,42-1);
var_dump($b); //float(2199023255552)
$res = $a | $b;
var_dump($res);  // 3
//这里的结果为3明显是不正常的。虽然上面实验当2^47时才显示是科学计数法,但是php的位运算,当整型的值超过最大限制2147483648之后,计算就开始出现错误。到这里就可以明确知道了,我们显然是不能计算大于45天的存留的,数据准确才是第一要素。

如代码所示,这里试验了转成字符串,又试验了科学计数法的位运算,显然是失败的。就如我们知道的,位运算是要转成二进制进行计算的,实际上相当于限定了是整型之间的计算,因此显然是失败的。

3、php的位运算受int范围限制?

这里涉及到了int的范围。一般来说范围是和操作系统的位数有关的。如果是32位的机器,int范围是:2^32 -1位,
检验方式是:

 $nums = pow(2, 31) - 1 + pow(2, 31);
var_dump("----------------------------");
$nums = pow(2, 30) - 1 + pow(2, 30);
$nums1 = pow(2, 32) - 1 + pow(2, 32);
var_dump($nums); // int(2147483647)
var_dump($nums1); // float(8589934591)   这里已经超出了int范围,转化为了float类型

如果是64位机器,那么范围是(-2^63 -1 ) 到 (2^63 - 1),检验方式:

pow(2, 62) - 1 + pow(2, 62);
$nums = pow(2, 62) - 1 + pow(2, 62);
var_dump($nums); //float(9.2233720368548E+18)

以上是理论上的,但是本地实验之后发现,本地虽然是64位的机器,但是int类型范围依然是2^32 -1,大于这个数则直接变成了float类型,已经不再是int类型了。但是这里测试又引出另一个问题,为什么当字符长度突破14位的时候,就显示科学计数法呢,这个14是何方神圣?

4、关于float类型14位的限制

这部分直接参考官方手册:
https://www.php.net/manual/zh/ini.core.php#ini.precision

打开php.ini,有个配置为:precision =14 ,官方手册解释为

precision integer :浮点数中显示有效数字的位数。-1 means that an enhanced algorithm for rounding such numbers will be used.

可以理解为精度的取值范围,在14位以内的就正常显示,大于14位的就用科学计数法表示,看下面的例子:

$num = 12345678.12345600000000000;
//整数部分为12345678 ,位数为 8 ,小数部分末尾的 0 不计入位数,位数为6,所以总位数为 8 + 6ini_set("precision", "12");
echo $num; // 12345678.1235
//超过精度值,显示的结果为 12345678.1235ini_set("precision", "3");
echo $num; // 1.23E+7
//超过精度值,且整数部分位数超过精度,小数部分舍弃,且整数部分只取3位ini_set("precision", "5");
echo $num; // 1.2346E+7
//超过精度值,且整数部分位数超过精度,小数部分舍弃,且整数部分只取5位,表现为科学计数法

上述例子中可以看到,精度值也关系到整数部分的截取。这样也就能理解,64位的机器虽然int范围可以到2^63 -1,但是为什么到2^46次方就显示为科学计数法了。并不是范围不够,而是精度位数的问题,整数位超过了规定的14位精度,因此显示为科学计数法。

5、关于大数的计算

如果要计算大数,那么必须在获取到值之后就转化为字符串,不然赋值的时候就直接变成科学计数法了。这里不讨论太多,只需要知道我们可以通过这种位运算,实现用户的31天内的存留计算即可。上限不要超过14位,也不要超过2^63-1位。超过14位则运算不准确,这也是phpfloat类型的问题,有兴趣的可以深究一下。

参考:https://segmentfault.com/q/1010000004524597

OK,通过一系列的探索,咱们也算是明确了这个方法的优点和劣势,关于大数计算的问题,php确实是有些劣势,我们使用的时候注意尽量不要超过int范围就好了。整体来说是一个不错的方法,这里分享给大家。

end

php使用位运算来实现日留存的算法相关推荐

  1. 位运算常用技巧分析汇总(算法进阶)

    文章目录 运算性质 异或运算的一些性质 秀秀伸手 1.只用位运算来完成两个整数相加 2.不用临时变量,交换a.b两个数的值 3.判断一个数是奇数还是偶数 3.快速计算2*n.2*n+1和n/2 4.` ...

  2. 位运算实现用户留存率

    统计留存率之前先弄清一下留存率的概念,百度百科中是这么说的: 用户在某段时间内开始使用应用,经过一段时间后,仍然继续使用应用的被认作是留存: 这部分用户占当时新增用户的比例即是留存率,会按照每隔1单位 ...

  3. 位运算+取某一位+java_Java位运算小节

    2019新春支付宝红包技术大揭秘在线峰会将于03-07日开始,点击这里报名届时即可参与大牛互动. 位运算表达式由操作数和位运算符组成,实现对整数类型的二进制数进行位运算.位运算符可以分为逻辑运算符(包 ...

  4. 0x01.基本算法 — 位运算

    目录 一.位运算 二.memset函数 三.移位运算 四.二进制状态压缩 五.成对变换 六.lowbit 七.相关习题 0.AcWing 26. 二进制中1的个数 1.Acwing 89. a^b(快 ...

  5. 位运算详解+竞赛常见用法总结

    目录 一.位运算详解 二.位运算应用 1.快速幂 2.给定一个数组A, 长度为n,求下面这段程序的值 3.数数字 4.数数字 2 5.nim博弈问题: 6.树状数组 7.判断一个数x是不是2的某次方 ...

  6. 快速排序算法_基于位运算的快速排序算法

    前言 如果你准备看这篇文章,我就当你是懂快速排序算法原理的. 下面是我在2018年10月3日想到的基于二进制位运算对正整数进行的一种快速排序算法,目前的代码只能对正整数进行有效的排序,当然,稍微修改一 ...

  7. 计算机中的进制位运算

    为什么计算机用二进制计数: 计算机是由电路构成的,电路只有0和1 两种状态. 不同进制间的换算: 在十进制中,个位的1代表10⁰=1,十位的1代表10¹=10,百位的1代表10²=100,所以:123 ...

  8. [GO语言基础] 四.算术运算、逻辑运算、赋值运算、位运算及编程练习

    作为网络安全初学者,会遇到采用Go语言开发的恶意样本.因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识:另一方面是分享与读者,希望大家一起进步.前文介绍了Golang的 ...

  9. 【BIT2021程设】7. 一夜发白《千字文》——Unicode和UTF-8、位运算

    写在前面: 本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验 ...

最新文章

  1. python 按从小到大的顺序组合成一个字典_将Python字典排列组合成字典列表
  2. MVP群聊某美国公司的应聘试题(压死九个还是一个)
  3. 如何看云服务器性能,从存储速度看云服务器性能测试
  4. 在MySQL数据库建立多对多的数据表关系
  5. 编写第一个Java程序:helloworld
  6. 突然发现一个很好用Golang的json库
  7. 解决Linux CentOS中cp -f 复制强制覆盖的命令无效的方法
  8. 找出本地分支正在跟踪哪个远程分支
  9. python柱形图绘制_Python Excel 绘制柱形图
  10. Oracle 11gR2 中 示例用户 安装说明
  11. VTM1.0代码阅读:xCheckRDCostMerge2Nx2N函数
  12. 看完吴恩达(Andrew Ng)机器学习视频的感受
  13. 计算机综合布线课程,综合布线工程课程教与学(教学大纲)
  14. SRS RTC NACK源码分析—1
  15. 免费学术资源(转自施一公博客)
  16. Windows server 2008 安装Hyper-V
  17. 新华三2018校园招聘笔试面试题学习
  18. 梅科尔工作室-李庆浩 深度学习-KNN算法
  19. ubuntu14.04更换国内软件源的方法
  20. 基于SSM校园学术报告管理平台毕业设计文案及源码

热门文章

  1. 微信小程序设置文本左对齐居中对齐右对齐setTextAlign的使用说明
  2. 自定义dropout
  3. python Manager dict
  4. pytorch 优化GPU显存占用,避免out of memory
  5. avcodec_encode_video2 -22
  6. 结构体 CString QString 成员赋值出错
  7. Tensorflow tf.placeholder函数
  8. for循环的一种加速方法
  9. AS-External-LSA
  10. 多线程还是多进程的选择