例如,某个房间可从[灯,床,桌,椅,杯子,饮水机……]这些器具中挑选,从而组成这个房间的装潢。咱们可能会设计一个房间表,再设计一个器具表,再设计一个关系表,经过这个关系表来保存它们之间的对应关系。可是这样的效率明显是比较差的,须要同时查询三张表才能完成。php
为了避免适用关系表,咱们还能够在房间表中设计一个字段,经过一个有规律的字符串来保存器具表的器具ID,例如:算法
1,2,3,7
下面,咱们提供一种经过一个值来计算便可得到这一器具组合的结果,方法以下:sql
array( '1' => '灯' '2' => '床', '4' => '桌', '8' => '椅子', '16' => '饮水机', …… );
若是咱们将5保存到数据库中,咱们能够立马知道,这个房间有“灯”和“桌”,而若是保存的是23,则必定有“灯”“床”“桌”和“饮水机”。数据库
给每个器具一个给定的值,这个值必定是2的n次方(n>=0),这样就能够保证相加以后的值能够反解。这个状况的核心原理在于,给定任何数值的前面数值相加和,必定小于当前数值。如何进行反解呢?编程
例如咱们拿到一个值为N,那么咱们能够首先找到最大的2^n,肯定2^n是必定有的,若是没有2^n,就不可能相加获得N。数组
接下来咱们得到M = N - 2^n,找到最大的2^m,再进行M - 2^m,如此推论下去,直到减完为止。函数
那么怎得到最大的2^n呢?设计
$n = (int)log(N,2);
log函数在PHP4+以后内置,用于取对数,返回值为float类型,但咱们仅须要整数部分,所以前面加(int)。code
例如N=22,那么$n=4,再去计算2^4,就是16。排序
经过这个方法,咱们能够很是顺利的在一个数据表中用一个值保存多种状况。可是,这也有必定的适用范围,好比这些状况最好是固定不变的,2n值不能太大等等。经过这种方法能够用该值进行权重设计,进行排序,可是不能用于条件检索,好比你想检索数据库中包含“床”的房间,你就很差进行检索,由于大部分房间的该值可能都大于2.因此,在使用这种方法时,应该根据实际须要进行考虑。
更新:
在数据库中,咱们可使用一种序列化的类二进制字符串来保存多个值,当这个二进制值是以01组成时,实际上就能够换算成为一个十进制数,从而也就实现了一个十进制值保存多种状况的目的。
下面咱们来作一个演示。
例如咱们在订票系统中,规定某一个活动天天分为6个场次,每一个场次2个小时,所以实际上就把一天的12个小时分为了6份,分别是9:00-11:00,11:00-13:00,13:00-15:00,15:00-17:00,17:00-19:00:19:00-21:00
,咱们用“xxxxxx”(x取0或1)来表示,如今,咱们要记录这些场次是否所有被定完了,用1表示所有被订完,因此“010110”就表示11:00-13:00,15:00-17:00,17:00-19:00这三个场次已经被订完了,不能再对外售票。
咱们在数据库中怎么保存呢?
php提供了将二进制转换为十进制的函数bindec(),咱们先将二进制值转换为十进制值后,再保存到数据库中。而当咱们要使用时,从数据库中取出十进制值,再使用decbin()将值转换为二进制值,固然,咱们要补全最后获得的二进制值的位数,也就是前面加0,而后再进行字符串数组处理,进行对比。
在编程世界中,还有一个比较好玩的算法,叫“按位异或”。按位,就是以二进制的形式进行计算,“按位异或”就是两个位的值不一样时返回1,不然返回0。经过这个运算,咱们能够获得看上去很是复杂的结果。在php中,运算为“^”。下面咱们来进行一下演算。
001011 ^ 011010 = 010001 (1式,注意,开头的0会被忽略,所以不要把开头的0也算进来)
提按位异或有什么意义呢?由于二进制值能够和十进制值进行转换,所以咱们将二进制值转换为十进制值进行按位异或以后,获得的值也是十进制的,咱们只有将这些十进制数转换为二进制字串后,才能发现规律,可是若是咱们直接用十进制进行计算,却能快速获得结果。
下面咱们就来演算一次,咱们拿(1式)来看。若是将二进制数转换为十进制,咱们就能获得
11 ^ 26 = 17
那事实的结果是否是这样呢?你能够在你的php程序中写上:
<?php echo 11 ^ 26;
是的,结果就是这样。但是,这个复杂的运算有什么用呢?它能够用于比较。好比咱们的数据库中存放了11,转换为二进制就是“001011”,也就是表示这一天的场次中,对应的那三个时段已经满票了。可是若是咱们如今正好要进行对比,看看这一天中17:00-19:00这个时段是否满票,咱们怎么能准确知道11这个值转换为001011后,第5个位上的值是否为1呢?
咱们只须要用这种思路来解决便可:
xxxxxx ^ 000010 = ?
其中xxxxxx是咱们要对比的值,好比当它等于11时,也就是001011时,等式的右边会获得001001(9)。咱们再来看另外一个算式:
xxxxxx ^ 000000 = ?
等式右边会获得自己。
若是咱们再用001001(9)去按位异或000010,则会获得001011(11)。
咱们获得的结论就是,凡是用xxxxx去按位异或yyyyyy(其中只有一个y为1,其余全为0),获得的结果比自身小的,则对应位置上的值为1,获得的结果比自身大的,对应的位置上为0。经过这种方法,也就找到了哪一个时间段是被订满票的。
为何大于自身的,对应的位置上就必定为0呢?由于0^1=1,而二进制数是01构成的,也就是说0和1碰上0时,都不会变化,而只有0碰上1时才会变化。说白了,用任何一个二进制数去按位异或000100,结果发生的状况就两种,一种是第四个位置上的值由1变为0(结果值相对于自己值而言),这种状况下该值变小,一种是第四个位置上的值由0变为1,这种状况下该值变大。了解了这个原理以后,咱们只须要在数据库中保存二进制转换而来的十进制值,在查询时,用对比值(二进制转换而来的十进制值)去按位异或一下,便可获得咱们想要的结果。
咱们建立以下表结构,sale_over在实际存储时,咱们转换为十进制整数进行存储,这里方便演示用二进制表示。每次在用户下订单时对票数进行检查,若是该时段已经有20张票被订出,就在下表中更新一条记录,把对应的时段改成1.
tablename = objectorder
id | object_id | day | sale_over |
1 | 5 | 2015-08-23 | 011000 |
2 | 8 | 2015-08-24 | 100101 |
3 | 5 | 2015-08-25 | 010001 |
例如:
SELECT COUNT(id) FROM object_order WHERE object_id=8 AND day='2015-08-20' AND (hours ^ 2)<hours;
这样就能够判断出8月20号这天17:00-19:00这个时间段是否被订满(若是返回1,则表示被订满了)。
若是咱们不满意用大小比较来进行判断,咱们还能够深刻发现,按位异或结果与原值之间的差值,正好是用来异或的值,也就是知足下面的等式:
|m ^ n - m| = n (n为yyyyyy,只有一个y为1,其余为0)
|x|是指绝对值,当不取以为值,获得的为负数时,说明结果变小了,那么原值对应的位置上也就是1,而若是获得的为正数,说明结果变大,对应的位置上就为0。因此,上述sql,咱们还能够这样去改:
**SELECT COUNT(id) FROM object_order WHERE object_id=8 AND day='2015-08-20' AND (hours ^ 2 + 2)=hours;**
若是查到告终果,说明8这个活动8月20号这天17:00-19:00这个时间段被订满。
这种魔术般的使用方法,你是否思考过呢?
再议
实际上,一个二进制数,咱们将它转换为十进制时,将它的各个位置值(从右往左,以0为开始)做为次数求2的次幂,再乘以该位置上的数,再相加,即获得该二进制数对应的十进制数,例如:
10100 = 0(2^0) + 0(2^1) + 1(2^3) + 0(2^4) + 1*(2^5) = 8 + 32 = 40
这样去观察,就发现实际上8和32,就是咱们第一次接触这种算法时,将它们做为一个数组的索引值,进行物品的索引进行计算。
接下来,咱们要更换场景,每一个时段仅能够被一我的预订,用户每一次下订单完成以后,造成一条记录,这些记录以上述形式存储,获得以下订单数据表:
tablename = userorder
id | user_id | object_id | day | hours |
1 | 2 | 5 | 2015-08-23 | 011000 |
2 | 3 | 8 | 2015-08-24 | 100000 |
3 | 2 | 5 | 2015-08-24 | 000001 |
相似这样的订单记录,hours字段中每一个位置上的1最多出现1次,怎么样肯定某一天的全部票都已经定出去了呢?
其实这是最简单的,就是对该字段进行求和,例如:
SELECT SUM(hours) FROM user_order WHERE object_id=8 AND day='2015-08-20';
若是最终获得的值为111111,也就是十进制的63,则说明该天各个时段已订满,不能再进行预订。
最后一种状况则是对上面两张场景的结合,也就是每一个时段最多能够被预订20张票,数据库中记录的是单个用户的订单。
固然,遇到这种状况,其实咱们能够准备两张表,一张是用户的订单表:
tablename = userorder
id | user_id | object_id | day | hours |
1 | 2 | 5 | 2015-08-23 | 011000 |
2 | 3 | 8 | 2015-08-24 | 100000 |
3 | 2 | 5 | 2015-08-24 | 000001 |
(第一条记录表示用户2在2015-08-23这天预订了5这个活动的11点13点这两个时段的票)
一张用来在每次用户订单完成时,对该时段进行判断,若是这个时段已经卖出20张,就改成1,进行更新操做的场次预订状况表:
tablename = objectorder
id | object_id | day | sale_over |
1 | 5 | 2015-08-23 | 011000 |
2 | 8 | 2015-08-24 | 100101 |
3 | 5 | 2015-08-25 | 010001 |
可是这样的话,咱们经过该表,仅能判断是否卖完,而不知道已经卖了多少张。为了解决这个问题,咱们夸张的作法是,直接在这个表的基础上进行扩展,增长20个字段,每一个字段对应一个时段,用来记录所卖出的票数,可是这样实在太蠢了。因为二进制方式,没法在每一个位置上表示实际的值,例如在第2个位置上用3来表示卖出3张,这是咱们没法作到的,因此,咱们能够经过前面一张用户下的订单列表来进行计算,从而找出某个位置上是否已经存在20个1.
实际上,咱们如今要解决的,就是查出每一个时段已经订出了多少张票。
咱们能够用
SELECT COUNT(id) FROM user_order WHERE object_id=8 AND day='2015-08-20' AND (hours ^ 2 + 2)=hours;
这种方法就能够查出来某个时段的被订数量,若是返回值等于20,则说明该时段已经被定完了。可是,咱们如何从全部的记录中,找出那些天的席位被所有定光呢?由于咱们不打算使用objectorder表来记录,而是想直接经过userorder进行查询,因此咱们不只要判断某个位置上的为1的记录数是否为20,并且要判断全部的位置。
最笨的方法就是连续判断6次,对每一个位置都进行统计,最终进行判断。可是这明显不符合咱们的要求。
实际上,咱们仍然使用求和便可完成,咱们在前面进行求和时,只须要用111111进行对比,也就是十进制的63进行对比,而此次,咱们用20个111111进行对比,也就是63*20 = 1260进行对比便可。
SELECT SUM(hours) FROM user_order WHERE object_id=8 AND day='2015-08-20';
若是获得的返回值等于1260,说明这一天的全部场次已经彻底订出去了。
用这种方法处理数据库中保存有规律的多种状况保存,就变得轻松有趣了。