【算法学习day05】异或运算

异或

异或操作:相同为0不同为1
a = 7 0000 0111
b = 13 0000 1101
a ^ b = 10 0000 1010

异或运算又叫无进位相加
0000 0111
0000 1101
0000 1010 每一位相加但是不进位

异或的性质

(1) 0 ^ N = N

异或为无进位相加,0的二进制表示全是0,与N异或当然还是N

(2) N ^ N = 0

任何一个数与自身异或为0,N的某一位为0,0+0相加是0;N的某一位是1,1+1相加也是0,因此异或后每一位都是0

(3) 异或满足交换律和结合律

a ^ b = b ^ a

(a ^ b) ^ c = a ^ (b ^ c)

同样的一批数,如abcde,不管如何异或,最后得到的结果一定都是相同的f

证明:因为每一位上只要有偶数个1就一定是0,有奇数个1就一定是1,改变顺序不会影响每一位上0和1的个数,也就不会影响每一位上异或的结果

异或有关题目

1. 如何不使用临时变量交换两个变量

使用临时变量:

int tmp = a;
a = b;
b = tmp;

使用异或:

a = a ^ b;
b = a ^ b;
a = a ^ b;

证明过程如下:

设最开始a = x,b = y

a = a ^ b 经过第一步后 只是对a的赋值操作,b不变
a = a ^ b = x ^ y
b = y

b = a ^ b 经过第二步后 只是对b的复制操作,a不变
a = x ^ y
b = x ^ y ^ y = x ^ (y ^ y) = x ^ 0 = x

a = a ^ b 经过第三步后
a = x ^ y ^ x = (x ^ x) ^ y = 0 ^ y = y
b = x

经过三步之后,a和b的值完成了交换

使用异或交换两个值的前提是,a和b是独立的两个内存区域,如果a和b指向同一个内存地址,则异或会变成0 因此在数组元素交换中这样写是错误的

int i = 0;
int j = 0;
arr[i] = arr[i] ^ arr[j];  // i和j指向同一个内存区域,会导致arr[i] = 0
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];

2. 一个数组中有一个数出现了奇数次,其他数都出现了偶数次,找到这一个数

如果不使用位运算,可以使用哈希表记录每一个数出现的频率,进而判断谁出现了奇数次

哈希表额外空间复杂度O(N)

如果使用异或操作,可以只申请一个变量eor = 0(exclusive or异或),让eor = eor ^ 数组中每一个数,最后返回eor的值,即出现奇数次的数

证明如下:

首先由性质3可以知道原始数组中各个数的顺序不影响最后异或的结果

由性质2和性质1,任意数N异或N本身为0,任意数N异或0为N,如果某个数有偶数个,一定会是 N^N=0 ^N = N ^N = 0依次循环,因此偶数个的数异或结果一定为0

而奇数个的数M最后一定会异或的只剩一个M(0 ^ M = M),eor初值为0,eor ^ 0 ^ M = 0 ^ M = M

因此用eor与所有的数依次异或,最后的值就是奇数个的数M

// arr中只有一种数,出现奇数次
public static void printOddTimesNum1(int[] arr) {
	int eor = 0;
	for (int i = 0; i < arr.length; i++) {
		eor ^= arr[i];
	}
	System.out.println(eor);
}

3. 提取一个int型变量最右侧的1

如a = 01101110010000,要求返回的ans=00000000010000

a & (-a)

这个数为0000…1…0000

证明如下:
a = 01101110010000
~a = 10010001101111
~a+1 = 10010001110000 ~a+1等价于-a
a&(-a)= 00000000010000

~a会让a最右侧的1左侧都取相反的,最右侧1后面的0全变成1,加上1会让后面1都变成0,同时进位产生1,这个1也就是对应的初始a里面最右边的1(1→0→1)

4. 一个数组中有两种数出现了奇数次,其他数都出现了偶数次,找到并这两种数

步骤

步骤如下:

假设arr[]数组中a和b出现了奇数次,a≠b

(1) 用eor=0变量遍历并异或数组的每一个元素,最后eor结果一定是a^b

由于a≠b,所以eor=a^b≠0,对应二进制表示中一定有1(不可能全是0),用第三题的方法来拿到最右侧的1,由于异或的定义,如果异或结果为1,说明a和b在这一位不一样(相异为1)

(2) 不妨设a^b最右侧的1在第k位上(也就是a的第k位和b的第k位不一样)

k从右往左数,从0开始数

假设a的第k位是0,b的第k位是1,将arr的所有元素分成第k位是0(a在其中)和第三位是1(b在其中)的两类

这两类中一定会有出现次数为偶数次的数,而且这两类是互斥的,不可能某个数同时在这两类里面。

(3) 再申请一个变量eor’=0(之前的eor = a ^ b)遍历并异或arr数组中第三位是0的数,最后异或完的结果就是a;

(4) 用eor ^ eor’ 就是另一个数b

例子

只看步骤会比较抽象,下面拿一个具体的例子来解释这个步骤

arr[] = [6, 6, 6, 10, 4, 4, 12, 12, 12, 12, 3, 3]

其中a=6出现了3次,b=10出现了1次均为奇数次,12、4、3出现了偶数次

a=6的二进制表示:00110
b=10的二进制表示:01010
4的二进制表示:00100
12的二进制表示:01100
3的二进制表示:00011

用eor=0异或数组里的每一个数,由于4、12、3都是出现偶数次,因此异或完结果仍是0,6异或两次也是0,
最后eor = 0 ^ 6 ^ 10 = 6 ^ 10 = 01100

取eor最右边的第一个1,也就是第2位的1,将arr分成两类,一类是第2位为1的,一类是第2位为0的,因此

a = 6, 4, 12分为一组,
b = 10, 3分为一组

用新的eor’ = 0与第一组进行异或,由于4和12都是出现偶数次的数,因此最后eor’=a=6,这样就可以把a找出来

再用eor’ = a ^ eor = a ^ b ,即a ^ a ^ b = b,这样就可以把b=3求出来

// arr中有两种数(a和b)出现奇数次
    public static void printOddTimesNum2(int[] arr) {
        int eor = 0;
        for (int i = 0; i < arr.length; i++) {
            eor ^= arr[i]; // eor = a ^ b ≠ 0
        }
        // rightOne = 0000..1000... 只有一个1剩下都是0
        int rightOne = eor & (-eor); // 提取eor最右侧的1
        int eor2 = 0; // eor' 只异或一个分组的
        for (int i = 0; i < arr.length; i++) {
            if ((arr[i] & rightOne) != 0) { // arr[i]中最右侧的1跟rightOne位置相同的为一组,即第k位为1的
                eor2 ^= arr[i]; // 异或完就是其中一个
            }
        }
        System.out.println(eor2 + " " + (eor ^ eor2)); // eor ^ eor2为另一个
    }

5. 一个数组中一个数出现K次,其他数都出现M次,M>1,K<M 找到出现了K次的数,要求额外空间复杂度O(1),时间复杂度O(N)

声明:如果后面出现“真命天子”,指的是出现K次的那个数,也称为K对应的那个数

额外空间复杂度O(1)相当于把哈希表ban掉了

数组arr 取K=3,M=5,含义就是有一个数出现了3次,其他数都出现了5次 M>1且K<M

可以把一个数转成二进制数数组,比如8可以写成一个长度为32的数组[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0]

准备一个辅助数组t,长度固定为32(额外空间复杂度为常数,O(1))初始化为全0
int[] t = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

每处理一个数,就把这个数的二进制数组表示中的1加到t数组对应位置上

某一位如果不是M的整数倍,K对应的那个数一定出现在这一位上,K对应的那个数对应的二进制数数组在这一位上一定是1(题目限制了K < M,因此K不可能是M的倍数)

比如4(0100)出现了3次,1(0001)和3(0011)出现了7次,M=3,K=7,t数组在第0位上的值为0+2*7=14,是7的整数倍,说明4(M对应的那个数)对应的二进制数组第0位一定是0;

将4换成5(0101),则t数组在第0位上的值为7*2+3=17,不是7的整数倍,说明5(M对应的那个数)对应的二进制数组在第0位上一定是1

    // arr中只有一种数出现了K次,其他数都出现了M次
    public static int onlyKTimes(int[] arr, int k, int m) {
        int[] t = new int[32];
        // t[i] 表示i位置的1出现了几个
        for (int num : arr) {
            for (int i = 0; i < 32; i ++) { // 固定32次,因此时间复杂度仍是O(N)
                if (((num >> i) & 1) != 0) { // num的第i位是1
                    t[i]++;
                }
                // 也可以直接写成 t[i] += (num >> 1) & 1;
            }
        }
        int ans = 0;
        for (int i = 0; i < 32; i ++) {
            if (t[i] % m != 0) { //说明k对应的那个数在第i位上是1
                ans |= (1 << i); // |是或运算 相当于把第i位的1给塞进ans里,也就是更新答案了 1 << i只有第i位有1,其他位置都是0,因此不改变其他位置
            }
        }
        return ans;
    }

使用对数器来验证代码

    // 经典解 → 哈希表
    public static int test(int[] arr, int k, int m) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int num : arr) {
            if (map.containsKey(num)) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }
        for(int num : map.keySet()) {
            if(map.get(num) == k) {
                return num;
            }
        }
        return -1;
    }
    // arr中只有一种数出现了K次,其他数都出现了M次
    public static int onlyKTimes(int[] arr, int k, int m) {
        int[] t = new int[32];
        // t[i] 表示i位置的1出现了几个
        for (int num : arr) {
            for (int i = 0; i < 32; i ++) { // 固定32次,因此时间复杂度仍是O(N)
                if (((num >> i) & 1) != 0) { // num的第i位是1
                    t[i]++;
                }
                // 也可以直接写成 t[i] += (num >> 1) & 1;
            }
        }
        int ans = 0;
        for (int i = 0; i < 32; i ++) {
            if (t[i] % m != 0) { //说明k对应的那个数在第i位上是1
                ans |= (1 << i); // |是或运算 相当于把第i位的1给塞进ans里,也就是更新答案了 1 << i只有第i位有1,其他位置都是0,因此不改变其他位置
            }
        }
        return ans;
    }

    public static int[] randomArray(int maxKinds, int range, int k, int m) {
        int kTimeNum = randomNumber(range); // k种数
        // 至少两种数
        int numKinds = (int)(Math.random() * maxKinds) + 2;
        // 一种数出现k次,其他数出现m次 → k * 1 + (numKinds - 1) * m
        int[] arr = new int[k + (numKinds - 1) * m];
        int index = 0;

        for(; index < k; index++) { //填k个一种数、
            arr[index] = kTimeNum;
        }
        numKinds--; //还有这些种数要去填

        HashSet<Integer> set = new HashSet<>(); //哈希表
        set.add(kTimeNum); // 把k次的数加进去
        while (numKinds != 0) { // 剩余种数没有到0
            int curNum = 0;
            do {
                curNum = randomNumber(range);
            } while(set.contains(curNum)); // roll到已经有的数重新roll 保证每次roll到数都是新的
            set.add(curNum); //把新的数加进去
            numKinds--;
            for (int i = 0; i < m; i++) {
                arr[index++] = curNum; // 填m个
            }
        }
        // arr填好了
        // 打乱顺序 → 随机交换位置
        for (int i = 0; i < arr.length; i++) {
            // i 位置的数 随机与j位置做交换
            int j = (int)(Math.random() * arr.length); // 0 ~ N - 1
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
        return arr;

    }
    // [-range, range]
    public static int randomNumber(int range) {
        return ((int)((Math.random() * range) + 1)) - ((int)((Math.random() * range) + 1));
    }
    public static void main(String[] args) {
        int kinds = 4;
        int range = 200;
        int testTime = 100000;
        int max = 9;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int a = (int) (Math.random() * max) + 1; // 生成1-9的整数
            int b = (int) (Math.random() * max) + 1; // 1-9
            int k = Math.min(a, b);
            int m = Math.max(a, b);
            // 去掉k = m的情况 保证 k < m
            if (k == m) {
                m++;
            }
            int[] arr = randomArray(kinds, range, k, m);
            int ans1 = test(arr, k, m);
            int ans2 = onlyKTimes(arr, k, m);
            if (ans1 != ans2) {
                System.out.println("出错了");
            }
        }
        System.out.println("测试结束");
    }

进一步要求:如果数组中其他数出现M次只有一种数出现K次,就返回K;但如果其他数出现了M次,但这个数没有出现K次,返回-1

有个坑就是真命天子为0的时候,需要单独检验,否则不知道0到底加了几次是否为K

这里对数器使用了0.5概率设为K,0.5概率设为<m的数的方式需要重点学习一下

    // 经典解 → 哈希表
    public static int test(int[] arr, int k, int m) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int num : arr) {
            if (map.containsKey(num)) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }
        for(int num : map.keySet()) {
            if(map.get(num) == k) {
                return num;
            }
        }
        return -1;
    }
    // arr中只有一种数出现了K次,其他数都出现了M次
    public static int onlyKTimes(int[] arr, int k, int m) {
        int[] t = new int[32];
        // t[i] 表示i位置的1出现了几个
        for (int num : arr) {
            for (int i = 0; i < 32; i ++) { // 固定32次,因此时间复杂度仍是O(N)
                if (((num >> i) & 1) != 0) { // num的第i位是1
                    t[i]++;
                }
                // 也可以直接写成 t[i] += (num >> 1) & 1;
            }
        }
        int ans = 0;
        for (int i = 0; i < 32; i ++) {
            // 如果是0出现了K次,则发现不出来 如0是真命天子,出现了5次但k=3 只会一直执行continue,不会返回-1

            if (t[i] % m == 0) {
                continue; // 这一位一定不是1
            }
            if (t[i] % m == k) { //取模完为k才说明该位是1而且出现了k次
                ans |= (1 << i);
            } else {
                return -1; // 该位是1而且出现了不是k次,返回-1
            }
        }
        // 单独检验0
        if (ans == 0) {
            int count = 0;
            for (int num : arr) {
                if (num == 0) {
                    count++;
                }
            }
            if (count != k) { // 单独统计数组中0出现的次数,与k相比看是否相等
                return -1;
            }
        }
        return ans;
    }

    public static int[] randomArray(int maxKinds, int range, int k, int m) {
        int kTimeNum = randomNumber(range); // k种数

        // 真命天子出现的次数 0.5概率真的出现了k次 0.5概率决定出现了几次都行(需要保证<m)
        int times = Math.random() < 0.5 ? k : ((int)(Math.random() * (m - 1)) + 1);
        // 后续代码中k都应该用times来替换
        // 至少两种数
        int numKinds = (int)(Math.random() * maxKinds) + 2;
        int[] arr = new int[times + (numKinds - 1) * m]; // 真命天子出现的次数 + 剩余数出现的次数
        int index = 0;
        for(; index < times; index++) { //填k个一种数、
            arr[index] = kTimeNum;
        }
        numKinds--; //还有这些种数要去填
        HashSet<Integer> set = new HashSet<>(); //哈希表
        set.add(kTimeNum);
        while (numKinds != 0) { // 剩余种数没有到0
            int curNum = 0;
            do {
                curNum = randomNumber(range);
            } while(set.contains(curNum)); // roll到已经有的数重新roll 保证每次roll到数都是新的
            set.add(curNum); //把新的数加进去
            numKinds--;
            for (int i = 0; i < m; i++) {
                arr[index++] = curNum; // 填m个
            }
        }
        // arr填好了
        // 打乱顺序 → 随机交换位置
        for (int i = 0; i < arr.length; i++) {
            // i 位置的数 随机与j位置做交换
            int j = (int)(Math.random() * arr.length); // 0 ~ N - 1
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
        return arr;
    }
    // [-range, range]
    public static int randomNumber(int range) {
        return ((int)((Math.random() * range) + 1)) - ((int)((Math.random() * range) + 1));
    }
    public static void main(String[] args) {
        int kinds = 4;
        int range = 200;
        int testTime = 100000;
        int max = 9;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int a = (int) (Math.random() * max) + 1; // 生成1-9的整数
            int b = (int) (Math.random() * max) + 1; // 1-9
            int k = Math.min(a, b);
            int m = Math.max(a, b);
            // 去掉k = m的情况 保证 k < m
            if (k == m) {
                m++;
            }
            int[] arr = randomArray(kinds, range, k, m);
            int ans1 = test(arr, k, m);
            int ans2 = onlyKTimes(arr, k, m);
            if (ans1 != ans2) {
                System.out.println("出错了");
            }
        }
        System.out.println("测试结束");
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值