【目标】
用 bit: 0|1(位)来标记 true|false 两个取值的情况。
【场景】
只存在正反两种取值的情况,如是否存在、是否在线等;要求数据相对集中,如int类型自动增长的id 就特别合适用本策略,分散性过高,则需要进行特殊处理,文末简要介绍。
【基础】
各数据类型,去除最高位作为符号位保留,其余二进制位可进行标记:
数据类型 | 字节数 | 位数 | 最大存储数据量 | 数据范围 | 可标记数据范围 |
Byte | 1字节 | 8位 | 255 | -128~127 | 127 |
short | 2字节 | 16位 | 65535 | -32768~32767 | 2^15-1 = 32767 |
int | 4字节 | 32位 | 2^32 -1 | -2^31~2^31 -1 | 2^31 -1 |
long | 8字节 | 64位 | 2^64 -1 | -2^63 ~2^63 -1 | 2^63 -1 |
【示例1】用 byte[] 数组标记:
对总量为14个队员的小组评奖,按从1~14编号,所有人都会获奖,奖项2中:一等奖和二等奖。
其中 1号、2号、5号、7号、8号、12号、13号 获得一等奖,其余都是二等奖。用Byte[] 数组进行储存。
数组长度:2 = 14/7(其中14为元素的总数,这里是14个队员;7为存储数据类型的位数-1,Byte类型数据1个字节8位,减1为7)。
如下图: 1号、2号、5号、7号落在第一组,用了第一个byte数据来进行标记;8号、12号、13号落在第二组,用第二个byte数据进行标记。本例,只用 2个 byte 数据便可以标记14个数(其实能标记 127 = 2^8 - 1 以内的数,即小于等于127。其他范围的数据,见上表,数据类型对应的“可标记数据范围”)。
【核心类】: ByteFlag.java
public static class ByteFlag{
public static void byteFlagByte(byte[] byteCollection, int maxNum){
byte numberTypeBit = 7; // 1 byte = 8 bit,需要减去1个符号位
int len = MathUtls.getLen(maxNum, numberTypeBit);
byte[] groups = new byte[len];
for (int i = 0;i < byteCollection.length;i++){
byte e = byteCollection[i];
int size = MathUtls.getLen(e, numberTypeBit);
byte index = (byte)(e % numberTypeBit);
byte group = groups[size - 1];
// 合并标记信息 到 byte标记值
byte merge = (byte) (group | MathUtls.pow((byte) 2, index));
// 合并标记信息 到 标记值
groups[size - 1] = merge;
}
System.out.println("需要标记数长度:"+len+",即 需要:"+len+"个byte数对"+Arrays.toString(byteCollection )+"进行标记");
// 检查元素
byte[] eles = {6, 9, 12};
for (int i = 0; i < eles.length; i++) {
byte ele = eles[i];
int index_of_group = MathUtls.getLen(ele, numberTypeBit);
byte inner_group_step = (byte) (ele % numberTypeBit);
// 带标记信息的标记值
byte group = groups[index_of_group - 1];
// 标记值 与 检查元素 进行比对(通过与&运算)
int check_result = group & MathUtls.pow((byte) 2, inner_group_step);
if(check_result > 0){
System.out.println("元素:" +ele+ ",在标记集合中!");
}else{
System.out.println("元素:" +ele+ ",不在标记集合中!");
}
}
}
public static void main(String[] args) {
byte[] byteCollection = {1,3,5,7,8,12,13};
int maxNum = 12; // 14个成员,最大编号 14
byteFlagByte(byteCollection, maxNum);
}
}
【测试结果】
需要标记数长度:2,即 需要:2个byte数对[1, 3, 5, 7, 8, 12, 13]进行标记
元素:6,不在标记集合中!
元素:9,不在标记集合中!
元素:12,在标记集合中!
效果如何?还可以吧,2个byte数,对14个小于127的数据进行了标记。
可是,稍稍做点调整,改一下测试数据 —— 加2个数25、127,最大值为127:
public static void main(String[] args) {
byte[] byteCollection = {1,3,5,7,8,12,13, 25, 127}; // 本组一共9个数
int maxNum = 127; // 最大编号 127
byteFlagByte(byteCollection, maxNum);
}
【测试结果】
需要标记数长度:19,即 需要:19个byte数对[1, 3, 5, 7, 8, 12, 13, 25, 127]进行标记
元素:6,不在标记集合中!
元素:9,不在标记集合中!
元素:12,在标记集合中!
Process finished with exit code 0
上面的测试结果,很惊讶吧!就这9个数,需要用19个byte数来存储,说好的节约空间呢?怎么感觉没节省,好像还增加了?!
我们来算一下:
每一个数各用一个int来存,共9个数:9*4=36字节;
用 byte[] 数组,需要一个包含19个byte的byte[]数组:19*1=19字节(实际运用中,一般数据量都较大,可以忽略唯一的一个外层数组对象占用的空间)。
其实也节省了,节省了一半,但还不够明显。两方面的不明显。
【遵循原理】
从1开始,每连续的7个数(8减去1)为一组,需要一个byte数来保存。空缺的数,(如果“正”用 1 标记、“反”用 0 标记)会被当做“反”对应二进制中的 0 不变。
【示例2】用 int[] 数组标记:
【核心类】: ByteFlag.java
public static class ByteFlag{
public static void byteFlagInt(){
int[] intCollection = {1,2,5,7,8,12,13,108,189,268,569,1987,2020};
int maxNum = 2020; // 最大编号 2020
int numberTypeBit = 31; // 1 int = 32 bit,减去1位符号位为31
int len = MathUtils.getLen(maxNum, numberTypeBit);
int[] groups = new int[len];
for (int i = 0;i < intCollection.length;i++){
int e = intCollection[i];
int size = MathUtils.getLen(e, numberTypeBit);
int index = e % numberTypeBit;
int group = groups[size - 1];
// 合并标记信息 到 int标记值
int merge = group | org.apache.commons.math.util.MathUtils.pow(2, index);
// 更新 标记值
groups[size - 1] = merge;
}
System.out.println("需要标记数长度:"+len+",即 需要:"+len+"个int数对"+Arrays.toString(intCollection)+"进行标记。");
// 检查元素
int[] eles = {9, 12, 1987};
for (int i = 0; i < eles.length; i++) {
int ele = eles[i];
int index_of_group = MathUtils.getLen(ele, numberTypeBit);
int inner_group_step = ele % numberTypeBit;
// 带标记信息的标记值
int group = groups[index_of_group - 1];
// 标记值 与 检查元素 进行比对(通过与&运算)
int check_result = group & org.apache.commons.math.util.MathUtils.pow(2, inner_group_step);
if(check_result > 0){
System.out.println("元素:" +ele+ ",在标记集合中!");
}else{
System.out.println("元素:" +ele+ ",不在标记集合中!");
}
}
}
public static void main(String[] args) {
byteFlagInt();
}
}
【测试结果】
需要标记数长度:66,即 需要:66个int数对[1, 2, 5, 7, 8, 12, 13, 108, 189, 268, 569, 1987, 2020]进行标记。
元素:9,不在标记集合中!
元素:12,在标记集合中!
元素:1987,在标记集合中!
Process finished with exit code 0
空间计算:
每一个数各用一个int来存,共13个数:13*4=52字节;
用 int[] 数组,区区13个数,需要一个包含66个 int 的 int[] 数组:66*4=264字节(实际运用中,一般数据量都较大,可以忽略唯一的一个外层数组对象占用的空间)。空间耗费率 增长 264 / 52 = 5.02(倍),不仅没节省,耗费增长5.02倍。已经较能说明问题了。这种数据分散较严重(离散程度高)的场景,不适用。
【工具类】: MathUtils.java
public static class MathUtils {
// 推算多少个标记数才够标记
public static int getLen(int maxNum, int numberTypeBit) {
return (maxNum % numberTypeBit == 0) ? (maxNum / numberTypeBit) : (maxNum / numberTypeBit + 1);
}
// 计算 k^e 因为是二进制表示数,本例中 k 取值 2
public static byte pow(byte k, byte e) throws IllegalArgumentException {
if (e < 0) {
throw MathRuntimeException.createIllegalArgumentException(LocalizedFormats.POWER_NEGATIVE_PARAMETERS, new Object[]{k, e});
} else {
byte result = 1;
for (byte k2p = k; e != 0; e >>= 1) {
if ((e & 1) != 0) {
result *= k2p;
}
k2p *= k2p;
}
return result;
}
}
}
【空间节省情况】:适用于连续数据标记
数据类型 | 字节数 | 标记连续数个数 | 127个从1开始的连续数 | 30000(3万个从1开始的连续数) | 30000(3万个从1开始的连续数):空间占用计算 |
Byte | 1字节 | 7 | 19个byte数 | (大于127)不可用 |
|
short | 2字节 | 15 | 9个short数 | 2000个short | (30000)*2/1024 = 58.59375(short单独标记,所用空间) (30000-2000)*2/1024=54.6875(节约空间) 2000*2/1024=3.90625(位标记,所用空间) 58.59375/3.90625=15(倍) |
int | 4字节 | 31 | 5个int数 | 968个int数 | (30000)*4/1024 = 117.1875(int单独标记,所用空间) (30000-968)*4/1024=113.40625(节约空间) 968*2/1024=3.78125(位标记,所用空间) 117.1875/3.78125=31(倍) |
long | 8字节 | 63 | 3个long数 | 477个long数 | (30000)*8/1024 = 234.375(long单独标记,所用空间) (30000-477)*8/1024=230.6484375(节约空间) 477*8/1024=3.7265625(位标记,所用空间) 58.59375/3.90625=63(倍) |
本文主要从节省空间的角度进行考虑,对 正方 两个结果的数据集进行标记,用0、1两个作为标记,标记结果仍然存入基本类型数据中。相信通过测试结果,您跟我一样,会对结果表示泄气,这怎么就节约空间了?这正是本例有意为之:任何非常规方案都有一定的适用范围,并非适用于所有地方,这正是它的特殊之处。所以,选取特殊方案解决问题时,应注重对应场景,判断是否真的有需要,能达到效果!最好,将“特殊方案”同“常规方案”用接近生产环境的数据进行测试,进行检验加以判断。
证伪的价值也是很大的,这里不得不老生常谈地提到爱迪生做发明,基本采用的就是排除法。所以,证伪是多么的重要。而公司往往重视证正,证伪被埋没了。关于对证伪的认可,听说华为公司有一套机制鼓励证伪。提供证据,证明提议方案的不可行,会收到奖励。这就消除了顾虑,调动求真的积极性。
在这过程中,存在两个需要进一步思考的地方:
(1)需要知道最大数,这个数直接决定了需要多长的数组,即多少个用于存放标记的数。这就联想起了 HashMap 中扩容。
(2)同桶算法一样,存在同样的问题,若数据分布不够集中,或突然出现一个巨大的数,其余数都比较集中,会造成浪费。
(3)可考虑设计一个区间,标记前,先进行判断,区间外的单独标记。这样便可以兼顾。
这需要进一步设计……期待其他伙伴,或者你我有闲情时……