【算法笔记】异或运算
异或运算,指的是二进制的位运算
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不能是同一个位置,如果是同一个位置,会将当前位置的数变成0return;}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^bint xor = 0;for (int i : arr) {xor ^= i;}// 得到最右侧的1所对应的数(题目三的结论)int rightOne = xor & (-xor);// 将数组中最右侧为1的数提取出来,单独异或,得到a或者bint 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 ~ 9int b = (int) (Math.random() * max) + 1; // b 1 ~ 9int k = Math.min(a, b);int m = Math.max(a, b);// k < mif (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) * mint[] 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-1int 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();}}
后记
个人学习总结笔记,不能保证非常详细,轻喷