只有两种状态(0|1)的情况,如 true/false,用二进制位标记 —— 大幅节省空间

【目标】

用 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)可考虑设计一个区间,标记前,先进行判断,区间外的单独标记。这样便可以兼顾。

这需要进一步设计……期待其他伙伴,或者你我有闲情时……

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值