[数据结构和算法01]一篇文章让你深刻理解异或

前言

最近在学习数据结构和算法,觉得这部分知识偏数学方面、偏思考,需要记录下来反复观看思考、总结才能掌握,遂决定做成一个系列,这是第一篇,我会坚持更新下去的,由于就是将平时所学记录下来,所以不会咬文嚼字的抠概念,列表格数据,而是直接上干货,希望我的分享可以帮助到大家。也希望大家多多支持

一.异或运算

1.什么是位运算

这是一个位运算,所谓位运算,就是对二进制位的运算,我们知道在计算机中存储的所有东西归根结底都是0/1这样的二进制序列,最直观的就是数字,比如我们在java(或者C/C++)中定义的int,它占了32个bit位,这个位的取值只能是0或者1,逢2进位,和我们十进制逢10进位是一个道理。

2.什么是异或运算

异或运算是一种运算,它的操作数有两个,结果会得到一个数,它对两个数的二进制进行比较,如果二进制位数对应相同,比如 1 和 1 或者 0 和 0,那么异或结果就是0,也就是相同为0;如果二进制位对应不同,比如 1 和 0(0和1),那么异或结果就是1,也就是不同为1。

a异或b写作a^b(就是电脑键盘上6那个键按住shift)

画图举个例子:

还有第二种理解异或的方式,这种方式我觉得是神来之笔,以前我只知道上面的相同为0,不同为1,这个其实还挺抽象的,特别是写一些算法题的时候。

那么第二种理解就是,可以把异或运算理解为无进位相加,比如上面的例子,1和0异或为1+0=1,0和0异或为0+0=0,而1和1异或为1+1=0,因为不能进位,所以直接变成0,这个是有关二进制加法的东西,两个1相加,这一位就变成0了,而且要进一位到前面,但这里是无进位相加,一定要记住。

有了这个理解,就可以轻易的理解很多东西了,比如下面两个:

a ^ 0 = a;

a ^ a = 0;

怎么解释?第一个:a和0异或,无进位相加!!a的每一位加上0,那不等于没变吗,所以还是a。

第二个:a和a自己异或,无进位相加!!位数是1的那些位异或1(也就是异或自己),根据无进位相加,变为0;位数是0的那些位异或0,还是0,所以全都变成0了,最终结果就是0。上面两个是很常用的,需要理解。

还要知道异或满足两个运算律:

交换律:a ^ b = b ^ a

结合律: (a ^ b) ^ c = a ^ (b ^ c)

只要你接受了我前面说的异或就是无进位相加,那就很好解释了,两个数相加,交换律和结合律能不满足吗?

异或还有一个用途,就是可以交换两个数:

int a = 甲;

int b = 乙;

具体算法是这样的:

a = a ^ b;

b = a ^ b;

a = a ^ b;

解释:

第一步: a = 甲 ^ 乙  b = 乙

第二步: a = 甲 ^ 乙  b = 甲 ^ 乙 ^ 乙(前面说了自己异或自己是0,0异或任何数是这个数本身,所以b = 甲)

第三步: a = 甲 ^ 乙 ^ 甲(实际上就是乙) b = 甲

这就完成交换了,这比我们平常使用一个中间变量要好一点,不用开辟新空间了,两个数倒腾一下就实现交换了

二.练习题

 ok,理解了上面就可以写一些题目了,比如:

简单题:

1.面试题 17.04. 消失的数字 - 力扣(LeetCode)

我说说我的想法,这里要找消失的一个数字,并且给了范围是0~n之间的数,那我们怎么找?可以使用异或操作,其实就是上面两个式子,下面给出结果并解释:

class Solution {

    public int missingNumber(int[] nums) {
        int res = 0;
        for (int i = 0; i < nums.length; ++i) {
            res ^= i;
            res ^= nums[i];
        }
        res ^= nums.length;
        return res;
    }

}
解释:定义res用于存异或后的结果,我们要找出0~n中消失的数,我们知道,一个数只要被异或两次(比如 a^a),那么根据无进位相加,他就会变为0,而这个0对于异或操作完全没有任何影响,所以我们要数组中的数全部异或两次,那么剩下来的那个一定就是缺少的

2.也是个简单题,和上面差不多,题目不贴了,大概说的就是,数组里面有一堆数,有一个数出现了奇数次,其他都出现了偶数次,求出这个数:

这个太简单了,用异或,把数组中的数全部进行异或,偶数的数都会变成0,因为自己异或自己总能找到配对的嘛,所以只有那个奇数出现的数会剩下来,这就是答案 

结果: 


3.第三题,这道题比较难,但和上面差不多;

题目:有一堆数,有两个不同的数出现奇数次,其他都出现偶数次,请你找出这两个数;

如果我们还是用上面的代码是无法完成的,因为他异或完只是一个数,比如:

你这样得到的是7 ^ 9的结果,那我们怎么找出这两个数呢?

这里就得好好分析了,

1.首先,我们已经知道,把整个数组异或后得到的就是这两个出现奇数次数字异或的结果,我假设这两个数就是a和b,那么结果得到的就是 eor = a^b;

2.题目要求a和b是不同的,那么我们就一定知道eor != 0,因为只有相同的数异或才是0,

3.那么既然eor != 0,那我是不是可以说eor的二进制位一定有一位是1(不可能全0),我假设就是第五位是1

4.那就说明,a和b的第五位肯定不同,这样他们异或出来的结果才有可能是1

5.因为a和b第五位一定不同,那我就可以画出这张图:

这张图代表了,数组中第五位为0和1的数的分类,a和b一定在两侧,不可能在同一侧,所以我定义变量 int eor2,用eor2只去异或第五位为1的数,那么偶数次出现的数中第五位为1的依旧会全部抵消,因为它们都是成对出现的,而只会剩下来一个数,这个数就是a或者b,当我们已经知道其中一个了,又知道a^b的结果了,用a^a^b或者b^a^b就可以知道另外一个了

总结:这里的精髓就在于,根据eor不为0,得出一定有一位是1,而这一位是a和b异或的结果,说明在这个位上a和b的二进制位是不一样的,所以我这里再对数组中的数进行异或,只不过这次我只异或这一位是1的数,而偶数出现的依旧抵消,不会影响最终结果,最终剩下来的只能是a或者b这种出现奇数次的。

那么我们的问题是怎么拿出这个a^b中的值位1的这个位呢? 可以采用一个算法,它可以求出二进制位中最右边的1

int rightOne = eor & (~eor + 1);

eg:

这个就可以取出最右侧的1即为rightOne,那我们拿到之后只需要用这个对数组中的数进行&运算,如果&出来的结果就是rightOne,就说明这个数的这一位是1,那么就允许它参与异或运算:

public class Text {
    public static void main(String[] args) {
        int[] arr = {1, 1, 3, 3, 4, 4, 4, 4, 6, 6, 7, 8, 8, 9};
        int res = 0;
        for (int i = 0; i < arr.length; i++) {
            res ^= arr[i];
        }
        int rightOne = res & (~res + 1);//取出最右边的1;
        int a = 0;
        for (int i = 0; i < arr.length; i++) {
            if ((arr[i] & rightOne) == rightOne) {
                a ^= arr[i];
            }
        }
        System.out.println("两个数分别为:" + a + "和" + (res ^ a));

    }
}

运行结果如下:

解释:最主要的是第二个for里面的if条件,它的意思是:用数组中的每一个数去&上rightOne,这个rightOne前面说过了它的二进制位除了res最右侧的1外其余全是0,所以让rightOne与数组中每一个数去进行&操作,只有与rightOne的1的位置相同的数才能进去if条件里面去参与异或运算:

图示:

这里还有个知识就是通过&运算你可以判断某一位是不是1或者0,比如这里通过&运算,我们知道了arr[0]的这一位是1,而arr[1]的这一位是0 

如果你耐心看到这,相信你对^运算有了全新的认识,希望我的分享能帮助到大家,谢谢阅读,下一期是关于排序的,马上更新!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值