二进制的一些技巧与运用

Hello world!我是猿码叔叔,一个 4 年 java 开发经验的北漂。目前正在努力学习算法。

如果你有过一些开发经验,想简单了解一下二进制在 java 语言中的的基础运用与原理,那么本篇很适合你继续往下阅读。

一、介绍二进制

说起二进制,我们不得不从 0 开始。假设现在让我们数数,一般我们数到 9 的下一个数就会进十,变成 10。而二进制,就是数到 2 变成 10。任何进制包括  8 或 16 进制的进制原理都和这个类似。

我们用计算机存储的数据,都会使用二进制进行存储,也就是 n 个 0 与 n 个 1 进行组合。主要因为二进制的数制系统相较于其他进制只有 0 和 1 两个标识,简单且方便计算机识别与处理。

二、二进制的运算符

下面简单介绍这些运算符。

按位与(And)( & )

按位与的运算原理,若把 1 看作 true 的话与逻辑运算符 && 很像。当两个数的二进制同位都为 1 时,运算为 1,否则为 0。

(1 & 1) = (01 & 01) = 01

(1 & 0) = (01 & 00) = 00

(2 & 1) = (10 & 01) = 00

按位或(Or)( | ) 

按位或与逻辑运算的 || 很像。当两个数的二进制同位只要存在1个或2个 1 时,运算都为 1,否则为 0。

(1 | 1) = (01 | 01) = 01

(1 | 0) = (01 | 00) = 01

(2 | 1) = (10 | 01) = 11

按位异或(Xor)( ^ ) 

按位异或就是当两个数的二进制同位进行差值取绝对值。

(1 ^ 1) = (01 ^ 01) = 00

(1 ^ 0) = (01 ^ 00) = 01

(2 ^ 1) = (10 ^ 01) = 11

取反( ~ ) 

取反,顾名思义,就是将二进制每位上的 0 变为 1,1 变为 0。

(~2) = (~10) = 11111111111111111111111111111101

(~3) = (~11) = 11111111111111111111111111111100

左移( << ) 

左移,顾名思义,就是将二进制中右侧的第一个 1 到左侧的部分向左移动几个单元,也可以说是在二进制后补几个 0

(1 << 1) = (01 << 1) = 10

(9 << 3) = (1001 << 3) = 1001000

(2 << 3) = (10 << 3) = 10000

右移( >> ) 

右移,与左移相反,右移若没有位数可以移动,就是 0

(1 >> 1) = (01 >> 1) = 00

(9 >> 3) = (1001 >> 3) = 01

(2 >> 3) = (10 >> 3) = 00

 三、二进制的实际应用(占位)

为了更充分的利用刚刚学到的二进制运算符原理,下面我们直接进入今天的主题,用二进制都能干啥?

二进制如果利用得当,可以很大程度的提高程序的运行效率。

以下内容,均是围绕“占位”展开。所谓占位,就是二进制位 0 表示没有被占位,1 表示被占位。比如 1 的二进制 00000000000000000000000000000001,表示右往左第 1 位被占位。

1、字符串系列

  • 字符串与数字进行转换。

对于阿斯克码值中,数字范围与种类相对较小的就是英文的大小写字符。大小写分别为 26 种,也就是 A ~ Z 与 a ~ z。那么对于一个字符串来说,如果纪录这串字符中都使用了哪些字母,我们可以使用二进制来解决。

字符串:abcdefg - 0123456 - 1111111 - 127

解释:对于小写字符,a 可以看为 0,z 可以看作 25,最高不超过 int 类型的二进制最大位的31位。因此,这里不考虑原字符串的字母排列顺序以及每个字母的使用频次,那么使用 int 来存储所有使用过的字符完全足够。也就是说,127 这个数字告诉我们,它的二进制 0 到 6 位都是 1,那么通过阿斯克码转换,可以知道这个字符串使用了 a ~ g 这 7 个字符

代码:

    public int charToNum(String str) {
        int mask = 0;
        for (int c : str.toCharArray()) {
            mask |= (1 << c - 'a');
        }
        return mask;
    }

  • 两个字符串比较是否存在使用了相同的字符

基于前面的字符串转数字的技巧,我们可以更进一步的使用这种方式,来比较两个英文字符串是否存在相同字符。

字符串:hello vs world

              hello -  00000000100100010010000 - 18576

              world - 10000100100100000001000 - 4343816

解释:直观可以看到有 2 处的 1 处在同位,意味着存在 2 种冲突的字符

代码:

    public boolean conflict(String s1, String s2) {
        int a = charToNum(s1), b = charToNum(s2);
        return (a & b) != 0;
    }

 这里我们使用按位与(&)可以巧妙的解决两个数字的相同二进制位是否存在都为 1 的情况。

  • 求得两个字符中存在几种冲突的字符

知道了是否冲突,如果更进一步需要知道存在几种冲突,我们可以考虑枚举二进制串中存在几个 1 即可。这里需要思考:使用哪些二进制运算符,以及如果一次性无法计算出某些结果,我们该如何组合使用二进制运算符,并设计一个程序来帮我们求得结果。单个二进制运算符的运算能力有限,因此当我们对二进制运算符掌握的足够牢靠,以及有相当够的经验,很多新的问题也可以尝试去使用二进制运算符解决。就好比当我们只知道加法时,对于 6 个 6 相加,我们需要一个一个的累加求得结果,而不是 6 乘以 6。

代码:

    public int countConflicts(String s1, String s2) {
        int binary = charToNum(s1) & charToNum(s2);
        int mask = 1, cnt = 0;
        // 31 是二进制 int 最大长度
        for (int i = 0; i < 31; ++i) {
            if ((binary & mask) != 0) { ++cnt; }
            mask <<= 1;
        }
        return cnt;
    }
  • 字符串与二进制的一些思考 

当然,int 类型的二进制位最大支持 31 位,更大的可以选择 long 类型的 63 位了。对于字符串转数字,只要计算的维度的取值范围不超过 63,我们都可以采取二进制的方式去进行程序设计。汉字不像英文字符仅有 26 个,但汉字的拼音确是由 26 个英文字母组成,如果你想象力足够丰富,是可以设计出意想不到的程序的。

2、数字系列

  • 数字使用二进制机制压缩

我们经常接触的可以使用二进制压缩的固定范围的数字类型有日期的月份天数

30天月份:4、6、9、11

压缩后:2640

二进制:101001010000

下面我们可以尝试一个案例。判断一个长度为 10 的日期字符串是否合法

代码:

    public static boolean dateValidation(String dateStr) {
        char[] cs = dateStr.toCharArray();
        int n = cs.length;
        int[] dateArr = new int[3];
        for (int i = 0, x = 0; i < 10; ++i) {
            if (i != 4 && i != 7) {
                dateArr[x] = dateArr[x] * 10 + (cs[i] - '0');
            }
            if (i == 4 || i == 7) { ++x; }
        }
        Date d = new Date();
        int year = d.getYear() + 1900;
        int month = d.getMonth() + 1;
        if (dateArr[0] > year || year - dateArr[0] > 2) {
            return false;
        }
        if (dateArr[1] > month || dateArr[1] < 1) {
            return false;
        }
        // {1 3 5 7 8 10 12} 31
        // {4 6 9 11} 30
        // {2} 平年 28 | 闰年 29
        boolean leap = (year % 100 == 0 && year % 400 == 0) || (year % 100 != 0 && year % 4 == 0);
        int maskOf30 = 2640; // maskOf31 = 5546
        if (dateArr[2] < 1 || dateArr[2] > 31 || ((1 << dateArr[1]) & maskOf30) != 0 && dateArr[2] > 30) {
            return false;
        }
        if (dateArr[1] == 2 && !leap && dateArr[2] > 28) {
            return false;
        }
        return dateArr[1] != 2 || !leap || dateArr[2] <= 29;
    }

代码片段中的 ((1 << dateArr[1]) & maskOf30) != 0,其实就是判断当前月份是否是 30 天的月份。如果你不这么写,也不用 API,采用 hash 表或 boolean 数组也是一个不错的办法,但没有做到极致。

3、类别数量超出 63 怎么压缩

        以下情况讨论只对常量作用最大,因为变量存在不确定性,很难抽象出具体的变化规律。

        我们知道,仅使用一个数字压缩对象的类型最多只能是 63,也就是对象的类型数量只能是小于等于 63 的。因为 Java 语言的所有数据类型中最大的二进制位数就是 63。 那大于 63 的怎么解决?比如全国有 293 个地级市,我们怎么压缩?

我们知道每个地级市都有对应的编号,这些编号都是唯一的,那么使得这个编号整体具有唯一性的可能只有该数字那么几个数位上的数字起着决定作用,举个栗子

a:   300012

b:   300015

c:   300017

 以上 3 个数字对编号唯一性起决定作用的只有从右往左的第一个数位。也有可能出现更多数位的情况。根据这几个影响唯一性数位,我们对每个地级市编号重新进行从 0 - n 编号。下面以 2 个数位为标准重建编号

    public int[][] remakeIndex(String[] codes, int x, int y) {
        int n = codes.length;
        int[][] indices = new int[9][9];
        int idx = 0;
        for (String c : codes) {
            indices[c.charAt(x) - '0'][c.charAt(y) - '0'] = idx++;
        }
        return indices;
    }

 重新建好编号后,我们取编号时,只需要根据原编号对应数位上的字符去获取新编号即可。

获取这些新的编号,我们要使用位移来压缩这些新的编号。假设这些新的编号最大为 80,显然超过了 63,此时我们创建一个数组,以 63 为基础,可以分为超过 63 和未超过 63 两个部分,那么这个数组的长度就是 2。

对于更大编号的压缩,可以根据这个原理去实现我们想要的程序。

    /**
     * @param compress 压缩数组
     * @param no 编号
     * @param base 以 63 为主
     */
    public void compressNo(long[] compress, int no, int base) {
        compress[no / base] |= 1L << (no % base);
    }

以上是我在算法学习中对二进制的一些技巧分享,欢迎指正,一起共勉学习。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿码叔叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值