【算法笔记】异或运算

异或运算,指的是二进制的位运算

1、异或运算

  • 异或运算
  • 认识异或运算:二进制位相同为0,不同为1,可以记成无进位相加!
  • 异或运算的性质
  • 1、0^a == a a^a == 0
  • 2、异或运算满足交换律和结合律:
  • a^b = b^a
  • a(bc) = (ab)c

2、相关题目和应用

题目一:如何不用额外变量交换两个数

思路:根据异或运算的性质

  • 1、先将a的值设置为a^b
  • 2、再将b的值设置为ab,代入上一步的结果,b=abb,因为bb=0,则b=a
  • 3、再将a的值设置为ab,代入上面两步的结果,a=ab,b=a,则a=aba,所以a=b
  • 注意:
  • 异或方法交换两个数的本质是将当前的数的位置当成了临时变量,所以在第一步中,a=a^b,第二步中b=a
  • 所以代入第三步的时候因该是a=aba=a=b
/**
     * 题目一:如何不用额外变量交换两个数
     * 思路:根据异或运算的性质
     * 1、先将a的值设置为a^b
     * 2、再将b的值设置为a^b,代入上一步的结果,b=a^b^b,因为b^b=0,则b=a
     * 3、再将a的值设置为a^b,代入上面两步的结果,a=a^b,b=a,则a=a^b^a,所以a=b
     * 注意:
     * 异或方法交换两个数的本质是将当前的数的位置当成了临时变量,所以在第一步中,a=a^b,第二步中b=a
     * 所以代入第三步的时候因该是a=a^b^a=a=b
     */
    public static void swap(int a, int b) {
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;
    }

    /**
     * 用异或交换数组中的两个数
     */
    public static void swap(int[] arr, int i, int j) {
        if (i == j) {
            // 异或方法交换两个数的本质是将当前的数的位置当成了临时变量
            // 所以,i和j不能是同一个位置,如果是同一个位置,会将当前位置的数变成0
            return;
        }
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

题目二:数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到这种数

思路:

  • 根据异或运算的性质
  • 一个数和自己异或就变成了0,0和自己异或是其本身,
  • 可以得出一个结论:一个数和本身异或偶数次,就会变成0,一个数和本身异或奇数次,还是本身
  • 所以,将数组中的所有数异或起来,最后异或的结果就是出现了奇数次的数
    /**
     * 题目二:数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到这种数
     * 思路:
     * 根据异或运算的性质
     * 一个数和自己异或就变成了0,0和自己异或是其本身,
     * 可以得出一个结论:一个数和本身异或偶数次,就会变成0,一个数和本身异或奇数次,还是本身
     * 所以,将数组中的所有数异或起来,最后异或的结果就是出现了奇数次的数
     */
    public static int getOddTimeNum(int[] arr) {
        if (arr == null || arr.length == 0) {
            throw new IllegalArgumentException("数组不能为空");
        }
        int xor = 0;
        for (int i : arr) {
            xor ^= i;
        }
        return xor;
    }

题目三:获取一个数二进制中最右侧的1所对应的数

思路:

  • 最右侧的1对应的数指的是一个数转换成二进制以后,每个位上要么是1,要么是0,我们将这个二进制数的最右侧的1保留下来,其他的都设置为0,这个数就是最右侧的1对应的数
  • 例如:10100,最右侧的1对应的数就是00100
    过程:
  • 这个题的重点是如何将最右侧的1保留下来,其他的都设置为0,下面是思路
  • 1、将这个数取反,例如10100取反就是01011
  • 2、将按位取反后的数加1,这样就会产生进位,结果恰好是最右侧的1和原来数字最右侧1对应,产生进位的位置都变成了0,示例中01011+1=01100
  • 3、再将这个数和取反加一后的数字按位与,这样就会将最右侧的1保留下来,其他的都设置为0,例如 10100 & 01100 = 00100
  • 4、所以,我们可以通过num&(~num+1)来获取最右侧的1所对应的数,根据补码性质,一个数的相反数就是其反码加1,所以可以得出下面的结论
  • num&(~num+1) = num&(-num)
  • 5、所以,我们可以通过num&(-num)来获取最右侧的1所对应的数
/**
     * 题目三:获取一个数二进制中最右侧的1所对应的数
     * 思路:
     * 最右侧的1对应的数指的是一个数转换成二进制以后,每个位上要么是1,要么是0,我们将这个二进制数的最右侧的1保留下来,其他的都设置为0,这个数就是最右侧的1对应的数
     * 例如:10100,最右侧的1对应的数就是00100
     * 过程:
     * 这个题的重点是如何将最右侧的1保留下来,其他的都设置为0,下面是思路
     * 1、将这个数取反,例如10100取反就是01011
     * 2、将按位取反后的数加1,这样就会产生进位,结果恰好是最右侧的1和原来数字最右侧1对应,产生进位的位置都变成了0,示例中01011+1=01100
     * 3、再将这个数和取反加一后的数字按位与,这样就会将最右侧的1保留下来,其他的都设置为0,例如 10100 & 01100 = 00100
     * 4、所以,我们可以通过num&(~num+1)来获取最右侧的1所对应的数,根据补码性质,一个数的相反数就是其反码加1,所以可以得出下面的结论
     * num&(~num+1) = num&(-num)
     * 5、所以,我们可以通过num&(-num)来获取最右侧的1所对应的数
     */
    public static int getFarRightOneTimeNum(int num) {
        return num & (-num);
    }
public static void main(String[] args) {
        System.out.println("-----测试数组中有一个数出现奇数次-----");
        System.out.println(getOddTimeNum(new int[]{1, 1, 3, 3, 5, 6, 6, 8, 8, 10, 10}));
    }

题目四:个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到这两种数

思路:

  • 题目二我们找到了一种出现奇数次的数,这个题目中是两种奇数次,比题目二更难一点
  • 1、结合题目二的思路,如果我们将所有的数都异或起来,最后异或后的结果就是 两个奇数次的数异或的结果,如果将这两种数设为a和b,则异或的结果xor=a^b,现在的问题是如何从异或结果中分开这两个数的问题。
  • 2、两个数如果同一个位置相同,异或后这个位置就会成为0,因此如果我们取出异或结果最右侧为1的数rightOne,代表a、b中该位置只有一个是1,另一个是0
  • 3、这个时候我们再将数组中该位置位1的数单独异或一次,得到的结果xor1,代表了a、b中最右侧位置为1的一个数
  • 4、再将xor和xor1进行异或,就可以得到另一个数
/**
     * 题目四:个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到这两种数
     * 思路:
     * 题目二我们找到了一种出现奇数次的数,这个题目中是两种奇数次,比题目二更难一点
     * 1、结合题目二的思路,如果我们将所有的数都异或起来,最后异或后的结果就是 两个奇数次的数异或的结果,如果将这两种数设为a和b,则异或的结果xor=a^b,现在的问题是如何从异或结果中分开这两个数的问题。
     * 2、两个数如果同一个位置相同,异或后这个位置就会成为0,因此如果我们取出异或结果最右侧为1的数rightOne,代表a、b中该位置只有一个是1,另一个是0
     * 3、这个时候我们再将数组中该位置位1的数单独异或一次,得到的结果xor1,代表了a、b中最右侧位置为1的一个数
     * 4、再将xor和xor1进行异或,就可以得到另一个数
     */
    public static DoubleOddResult getDoubleOddTimeNum(int[] arr) {
        if (arr == null || arr.length == 0) {
            throw new IllegalArgumentException("数组不能为空");
        }
        // 首先得到xor=a^b
        int xor = 0;
        for (int i : arr) {
            xor ^= i;
        }
        // 得到最右侧的1所对应的数(题目三的结论)
        int rightOne = xor & (-xor);
        // 将数组中最右侧为1的数提取出来,单独异或,得到a或者b
        int xor1 = 0;
        for (int i : arr) {
            if ((i & rightOne) != 0) {
                xor1 ^= i;
            }
        }
        // 得到另一个数
        int xor2 = xor ^ xor1;
        return new DoubleOddResult(xor1, xor2);
    }


    public static void main(String[] args) {
        System.out.println("-----测试数组中有一个数出现奇数次-----");
        System.out.println(getOddTimeNum(new int[]{1, 1, 3, 3, 5, 6, 6, 8, 8, 10, 10}));
        System.out.println("-----测试数组中有两个数出现奇数次-----");
        DoubleOddResult result = getDoubleOddTimeNum(new int[]{1, 1, 3, 3, 5, 6, 6, 7, 8, 8, 10, 10});
        System.out.println(result.num1);
        System.out.println(result.num2);
    }


    static class DoubleOddResult {
        public DoubleOddResult(int num1, int num2) {
            this.num1 = num1;
            this.num2 = num2;
        }

        int num1;
        int num2;
    }

题目五:数组中所有的数都出现了M次,只有一种数出现了K次,且1 <= K < M(输入一定能够保证),返回出现K次的数

思路:

  • 这个题目最简单的办法就是用一个map,将每个数出现的次数统计出来,然后遍历map,找到出现次数为K的数,只是用了map,空间复杂度就会变高
  • 顺着这个思路,我们可以用和一个int占用同样大小(java中int是4个字节,32位)的空间的数组help,用help数组对应的下边来记录原来数中对应位置为1的个数
  • 因为其他数字都出现了M次,那对应位置为1的个数和一定是能被M整除的,如果不能被整除,说明出现K次的数在这个位置上为1,我们就把这个位置还原
  • 最后还原得到的数,就是出现了K次的数

代码和对数期测试方法如下:

import java.util.HashMap;
import java.util.HashSet;

public class KM {

    /**
     * 题目五:数组中所有的数都出现了M次,只有一种数出现了K次,且1 <= K < M(输入一定能够保证),返回出现K次的数
     * 思路:
     * 这个题目最简单的办法就是用一个map,将每个数出现的次数统计出来,然后遍历map,找到出现次数为K的数,只是用了map,空间复杂度就会变高
     * 顺着这个思路,我们可以用和一个int占用同样大小(java中int是4个字节,32位)的空间的数组help,用help数组对应的下边来记录原来数中对应位置为1的个数
     * 因为其他数字都出现了M次,那对应位置为1的个数和一定是能被M整除的,如果不能被整除,说明出现K次的数在这个位置上为1,我们就把这个位置还原
     * 最后还原得到的数,就是出现了K次的数
     */
    public static int km(int[] arr, int k, int m) {
        if (arr == null || arr.length == 0) {
            throw new IllegalArgumentException("数组不能为空");
        }
        int[] help = new int[32];
        // 用help数组对应下标的数字记录原数组中对应位置为1的个数,例如help[0]记录的是原数组中所有数的二进制表示中,第0位为1的个数
        for (int num : arr) {
            for (int i = 0; i < 32; i++) {
                // num >> i 代表将i位置的数移动到最右侧,然后 & 1,将其他的位置上的数字都置为0,只保留该位置的数
                help[i] += (num >> i) & 1;
            }
        }
        // 保存还原的结果的数字
        int result = 0;
        // 遍历help数组,将对应位置为1的个数不是M的倍数的位置,还原到result中
        for (int i = 0; i < 32; i++) {
            if (help[i] % m != 0) {
                // 说明出现K次的数在这个位置上为1,将这个位置上的数还原到result中
                result |= (1 << i);
            }
        }
        return result;
    }

    /**
     * 用对数器的方法测试
     */
    public static void main(String[] args) {
        // 测试次数
        int testTime = 500000;
        // 数组最大长度
        int maxSize = 100;
        // 最大的多少种数
        int kinds = 5;
        // 用来限制k和m的最大值
        int max = 9;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            int a = (int) (Math.random() * max) + 1; // a 1 ~ 9
            int b = (int) (Math.random() * max) + 1; // b 1 ~ 9
            int k = Math.min(a, b);
            int m = Math.max(a, b);
            // k < m
            if (k == m) {
                m++;
            }
            // 先生成一个随机的数组
            int[] arr = generateKMRandomArray(kinds, maxSize, k, m);

            int result1 = km(arr, k, m);
            int result2 = kmComparator(arr, k, m);
//            System.out.println("原数组:");
//            printArray(arr);
//            System.out.printf("k的值:%d,m的值:%d,km算法的结果:%d,比较器的结果:%d\n", k, m, result1, result2);
            if (result1 != result2) {
                succeed = false;
                System.out.println("原数组:");
                printArray(arr);
                System.out.printf("k的值:%d,m的值:%d,km算法的结果:%d,比较器的结果:%d\n", k, m, result1, result2);
                break;
            }
        }
        System.out.println(succeed ? "successful!" : "error!");
    }

    /**
     * km问题的比较器,用来判断km算法的结果是否正确
     * 直接使用map统计每个数出现的次数,然后遍历map,找到出现次数为K的数
     */
    public static int kmComparator(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);
            }
        }
        int ans = 0;
        for (int num : map.keySet()) {
            if (map.get(num) == k) {
                ans = num;
                break;
            }
        }
        return ans;
    }

    /**
     * 生成一个满足km问题的随机数组
     */
    public static int[] generateKMRandomArray(int maxKinds, int maxSize, int k, int m) {

        // 一共有多少种数,最少要有出现k和m两种
        int numKinds = (int) (Math.random() * maxKinds) + 2;
        // 数组长度: k  + (numKinds - 1) * m
        int[] arr = new int[k + (numKinds - 1) * m];
        // 得到出现k次所对应的数
        int kTimeNum = randomNumber(maxSize);
        // 先把k的数字填进去
        int index = 0;
        for (; index < k; index++) {
            arr[index] = kTimeNum;
        }
        // 填入其他的数字
        numKinds--;
        // 用来验证生成的数字有没有用过
        HashSet<Integer> set = new HashSet<>();
        set.add(kTimeNum);
        while (numKinds != 0) {
            int curNum = 0;
            do {
                // 生成没有用过的数字
                curNum = randomNumber(maxSize);
            } while (set.contains(curNum));
            set.add(curNum);
            numKinds--;
            for (int i = 0; i < m; i++) {
                arr[index++] = curNum;
            }
        }
        // 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;
    }

    /**
     * 生成一个满足[-maxSize, +maxSize]的数
     */
    public static int randomNumber(int maxSize) {
        return (int) (Math.random() * (maxSize + 1)) - (int) (Math.random() * (maxSize + 1));
    }

    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

}

后记
  个人学习总结笔记,不能保证非常详细,轻喷

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值