文章目录
异或
异或操作:相同为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("测试结束");
}

1万+

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



