异或运算的魔法

异或运算大家都知道指的是对于两个数转准成二进制之后,相同位置上的如果同时为或者,那么异或的结果就是0,不同就是1,比如01异或00结果是01,但是时间长了相信大家都很容易记混,那么有一种很好记的方式,就是:异或其实就是无进位的加法,比如00异或01就是00+01,但是要直接丢弃进位,所以异或运算具有以下特性:

1、任何数与0异或,结果是这个数本身

2、任何数和它自己异或,结果是0

那么异或运算可以解决哪些问题呢

第一题:不用临时变量,交换两个数(其实这个题是很恶心人的,用一个临时变量怎么了)

    private void exchange(int a, int b) {
        System.err.println(a + "->" + b);
        a = a ^ b;//b的值不变
        b = a ^ b;//把前面a的值代入,等同于b=a^b^b;
        a = a ^ b;//把前面a的值代入,等同于b=a^b^a;
        System.err.println(a + "->" + b);
    }

    @Test
    public void testExchange() {
        exchange(2, 3);
    }

第二题:给定一个数组,只有一个数在其中出现了奇数次,其他的数出现偶数次,找出这个数

思路:用一个临时变量tmp,初始值为0,从数组开头一直异或到结尾,最后tmp就是要找的数

举个例子[1,2,3,4,1,2,4],我们要找的数据是3,根据前面的结论,任何一个数异或0等于它本身,同一个数异或它自己等于,那么这个数组其实我们把它排序看看[1,1,2,2,3,4,4],所以1,2,4在与tmp异或完之后tmp等于0,最终结果就是3了

    private int findOddNumTimes(int[] arr) {
        int tmp = 0;
        for (int i : arr) {
            tmp = tmp ^ i;
        }
        return tmp;
    }

    @Test
    public void testfindOddNumTimes() {
        System.err.println(findOddNumTimes(new int[]{1, 2, 3, 4, 1, 2, 4}));
    }

穿插一个小插曲:如何提取一个数最右侧的1呢,比如3的二进制是00000....11,提取之后就是00000.....01,实现思路如下:

在java里面一个正数的相反数表示形式是它自身取反加一,比如2的二进制是00000000 00000000 00000000 00000010 ,-2则是11111111 11111111 11111111 11111110,2取反(~2)是11111111 11111111 11111111 11111101 ,也就是说-2=~2+1

    private String toBinary(int number) {
        String binaryString = String.format("%32s", Integer.toBinaryString(number)).replace(' ', '0');
        // 在每8位之间添加空格
        String formattedString = binaryString.replaceAll("(?<=\\G.{8})", " ");
        return formattedString;
    }

    @Test
    public void testToBinary() {
        System.err.println(toBinary(3));
        System.err.println(toBinary(~3));
        System.err.println(toBinary(-3));
        System.err.println(toBinary(3 & (-3)));
    }

第三题:给定一个数组,有两个数在其中出现了奇数次,且这两个数不相等,其他的数出现偶数次,找出这两个数

思路:假设数组是[1,3,2,4,2,4,1,3,1,3]那么我们要找的数据就是1和3,我们先用一个临时变量tmp,初始值为0,从数组的头到尾异或一遍,那么最后tmp就等于1^3;因为题目中有说明两个数不相等,所以tmp的值一定不等于0,那么也就是说这两个数至少有一位是不同的,比如前面的tmp=1^3,在第二位上1(0000....01)和2(0000....11)是不同的,那么我们可以把整个数组分成两部分,第一部分是第二位为0的也就是1和4,第二部分是第二位不为0的,也就是2和3,这样一来,那些出现偶数次的数也会被分成两类,同时我们也把1和3分开在两组中了,我们再用一个临时变量tmp1,初始值为0,从头到尾只异或第二位为1的数据,那么就可以把3筛选出来了(因为和3一组的2是出现了偶数次的呦),tmp1的值就是3,然后用tmp1和tmp异或就得到了1

    private void findTwoOddNumTimes(int arr[]) {
        int tmp = 0;
        for (int i : arr) {
            tmp = tmp ^ i;
        }
        int rightOne = tmp & (-tmp);
        int tmp1 = 0;
        for (int i : arr) {
            if ((rightOne & i) != 0) {//筛选第二位为1的数据
                tmp1 = tmp1 ^ i;
            }
        }
        System.err.println(tmp1);
        System.err.println(tmp1 ^ tmp);
    }

    @Test
    public void testfindTwoOddNumTimes() {
        int[] arr = new int[]{1, 3, 2, 4, 2, 4, 1, 3, 1, 3};
        findTwoOddNumTimes(arr);
    }

第四题:给定一个数组,其中只有一个数出现了k次,其他的数都出现m次,且k<m,找出这个数,要求空间复杂度为O(1)

思路:我们用一个长度为32(因为int类型是32位)的临时数组tmp,我们把数组中每一个数的二进对应的位上为1的数据累加到tmp对应的下标下面,举个例子,假设原数组为[5,5,7,7,7,6,6,6]

我们要找的数是5,k=2,m=3,5的二进制是101,6是110,7是111,把他们的二进制对应的位相加,也就是3个111加上3个110加上2个101,得到的数据就是865,也就是说tmp数组的前三个数是865因为k是小于m的所以如果tmp下标对应的数据不能被m整除,那么这个下标对应的值就一定是由我们要找的数和其他的数加上来的,比如前面的6是可以整除3的,也就是说这个6一定是7和6对应的位上的1相加得来的,那么8和5对应的位就一定有5对应的位上出现了1,于是我们想办法把1还原成5即可:

 private int onlyKtimes(int[] arr, int k, int m) {
        if (k >= m) {
            throw new RuntimeException("给定数据不符合条件");
        }
        int[] tmp = new int[32];
        for (int num : arr) {
            for (int i = 0; i <= 31; i++) {
                if (((num >> i) & 1) != 0) {
                    tmp[i]++;
                }
            }
        }
        int result = 0;
        for (int i = 0; i < 32; i++) {
            if (tmp[i] % m != 0) {
                result = result | (1 << i);
            }
        }
        return result;
    }

    @Test
    public void testonlyKtimes() {
        int[] arr = new int[]{1, 1, 4, 3, 3, 5, 5, 4, -1};
        System.err.println(onlyKtimes(arr, 1, 2));
    }

PS:异或运算实现加减法:用位运算实现加减乘除法-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值