异或运算,指的是二进制的位运算
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();
}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷
857

被折叠的 条评论
为什么被折叠?



