异或运算 (XOR) 是一种常见的二进制运算
异或运算的规则可以简单概括为:“相同为0,相异为1”。也可以理解为不进位的加法。
异或运算具有以下几个重要的数学性质,这些性质在计算机科学中应用广泛:
- 交换律:A⊕B=B⊕A
- 结合律:(A⊕B)⊕C=A⊕(B⊕C)—-可以通过“无进位相加”去理解
- 恒等律:任何数与 0 进行异或运算,结果仍是它本身。A⊕0=A
- 归零律:任何数与自身进行异或运算,结果为 0。A⊕A=0
1.如何不用额外变量交换两个数
交换步骤如下
a = a ^ b
- 此时,
a
的新值包含了a
和b
的信息。
- 此时,
b = a ^ b
- 将第 1 步计算出的新
a
值(即a ^ b
)与原b
进行异或运算。 - 根据异或运算的性质: b=(a⊕b)⊕b=a⊕(b⊕b)=a⊕0=a
- 此时,
b
的值已经被成功替换为原来的a
值。
- 将第 1 步计算出的新
a = a ^ b
- 将第 1 步计算出的新
a
值(a ^ b
)与第 2 步计算出的新b
值(也就是原a
值)进行异或运算。 - 根据异或运算的性质: a=(a⊕b)⊕a=(a⊕a)⊕b=0⊕b=b
- 此时,
a
的值已被成功替换为原来的b
值。
- 将第 1 步计算出的新
【注意】:交换数组中两个位置的数,可以这么写,但是有前提条件\(i \ne j\):
1
2
3
4
5
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
如果\(i = j\),那么arr[i] ^ arr[j]
的结果就是0,那么第二行,就会得到arr[j] = 0 ^ arr[j];
,则arr[j]
就是arr[j]
,紧接着第三行就是arr[i] = 0 ^ arr[j]
,arr[i]
也会变成arr[j]
。
即如果\(i=j\)无论 arr[i]
原本是什么值,经过这三步异或运算后,arr[i]
的值最终都会变为 0。这并非是简单地保持不变或者变成另一个数,而是会丢失原始数据。因为i等于j,所以指向同一块内存区域,都变成了0.
因此,这个异或交换法必须满足\(i\ne j\)这个前提条件,否则会发生错误。
2.一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数
常规方法:哈希表
但是用异或运算可以使得空间复杂度更好。
试题链接:https://www.nowcoder.com/practice/d0ef3e33e63a49dd99c90aeef306b0fc
解法:(这题是ACM风格)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.Scanner;
import java.io.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
int n = (int)in.nval;
in.nextToken();
int[] arr = new int[n];
for (int i = 0;i < n;i++) {
arr[i] = (int)in.nval;
in.nextToken();
}
int ans = findOddNum(arr);
out.println(ans);
}
out.flush();
br.close();
out.close();
}
public static int findOddNum(int[] arr) {
int ans = 0;
for (int i = 0;i < arr.length;i++) {
ans ^= arr[i];
}
return ans;
}
}
3.怎么把一个int类型的数提取出最右侧的1来
方法是:N&(-N)
,就是&
上自己的相反数
N & (~N + 1)
就能高效地提取出 N
二进制表示中最右边的“1”。这种方法通常用于快速计算一个数中“1”的个数,或者在树状数组中进行索引操作。
这么做的原因是:~N + 1
的二进制表示中,最右边的“1”会和 N
中最右边的“1”处于同一个位置。而且,它左边的所有位都与 N
相反。所以
N & (~N + 1)
:
- 在最右边的“1”的左边,
N
和~N + 1
的位是相反的,所以按位与的结果都是“0”。 - 在最右边的“1”这个位置上,
N
和~N + 1
都是“1”,所以按位与的结果是“1”。 - 在最右边的“1”的右边,
N
和~N + 1
都是“0”,所以按位与的结果都是“0”。
4.一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
题目链接:https://www.nowcoder.com/questionTerminal/863d84dc0b2d4e639dc17f12701ed3ac
(上面这个测试链接是ACM风格的)
另外这道题被包装成了力扣的一道题LCR 177. 撞色搭配
这种题目更加具有扩展性的问法是:
一个数组中有一种数出现K次,其他数都出现了M次, 已知M > 1,K < M,找到出现了K次的数 要求额外空间复杂度O(1),时间复杂度O(N)
显然题目要求已经将哈希表的解法给否定了。
先看2个数出现奇数次的情况。
首先如果还是将所有数进行异或,最终得到的结果就是ans=a^b
,因为\(a \ne b\)所以\(ans \ne 0\)。
即ans的二进制表示中,一定有一个位置上是1.
利用上面的解法,找到ans中最右侧的1.那么说明a与b这个位置上的数一定是不同的,一定某个数这个位置是0,某个数这个位置是1.(下面的讨论假设ans的1出现在第i位置上,假设a的i位置上是1,b的i位置上是0)。
现在整个数组可以分为2个阵营,如下图所示:
现在变量eor仍旧去异或数组中的数,但是只异或i位置是1的数。这样就会得到eor = a
,
如何得到b呢?就是ans ^ eor
所以上面测试链接的解法是:(注意这题是ACM风格的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.Scanner;
import java.io.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
int n = (int)in.nval;
in.nextToken();
int[] arr = new int[n];
for (int i = 0;i < n;i++) {
arr[i] = (int)in.nval;
in.nextToken();
}
int[] ans = getNums(arr, n);
int small = ans[0] < ans[1] ? ans[0] : ans[1];
int big = ans[0] == small ? ans[1] : ans[0];
out.print(small + " ");
out.print(big);
}
out.flush();
br.close();
out.close();
}
public static int[] getNums(int[] arr, int n) {
int ans = 0;
for (int i = 0;i < n;i++) {
ans ^= arr[i];
}
// ans = a ^ b
// 找到最右侧的1
int rightOne = ans & (~ans + 1);
int eor = 0;
for (int i = 0;i < n;i++) {
if ((arr[i] & rightOne) != 0) {
eor ^= arr[i];
}
}
// a = eor
// b = ans ^ eor
return new int[] {eor, ans ^ eor};
}
}
5.一个数组中有一种数出现K次,其他数都出现了M次
一个数组中有一种数出现K次,其他数都出现了M次, 已知M > 1,K < M,找到出现了K次的数 要求额外空间复杂度O(1),时间复杂度O(N)
准备一个int[] t = new int[32]
,任何一个数(int类型)都可以表示成32位的二进制,那么对于一个数组nums,可以算每一个数每一位上1出现的个数,存储在数组t中。
只需要判断t中每个元素是否被M整除,就可以判断出现K次的数这个位置是不是1
能被M整除,那么出现K次的数这个位置不是1;不能被M整除,出现K次的数这个位置是1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int onlyKTimes(int[] arr,int k,int m){
int[] t=new int[32];
for(int num:arr){
for(int i=0; i<=31; i++){
t[i]+=(num>>i)&1;
}
}
int ans=0;
for(int i=0; i<32; i++){
if((t[i]%m)!=0){// 在第i位上有1
ans |= (1<<i);// 1左移i个位置
}
}
return ans;
}
相关题可以看下面的例子。
k=1,m=3的例子就是力扣137题
6.力扣136
https://leetcode.cn/problems/single-number/
1
2
3
4
5
6
7
8
9
class Solution {
public int singleNumber(int[] nums) {
int ans = 0;
for (int i = 0;i < nums.length;i++) {
ans ^= nums[i];
}
return ans;
}
}
7.力扣137
https://leetcode.cn/problems/single-number-ii/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int singleNumber(int[] nums) {
return getKtimesBetweenM(nums, 1, 3);
}
public int getKtimesBetweenM(int[] arr, int k, int m) {
int[] t = new int[32];
for (int i : arr) {
for (int j = 0;j <= 31;j++) {
t[j] += ((i>>j)& 1);
}
}
int ans = 0;
for (int i = 0;i <= 31;i++) {
if (t[i] % m != 0) {
ans |= (1 << i);
}
}
return ans;
}
}
8.LCR 177. 撞色搭配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int[] sockCollocation(int[] sockets) {
int ans = 0;
for (int i : sockets) {
ans ^= i;
}
// ans = a ^ b
// 找到某个1,比如最右侧1
int rightOne = ans & (~ans + 1);
int eor = 0;
for (int i : sockets) {
if ((i & rightOne) != 0) {
eor ^= i;
}
}
// eor = a;
return new int[] {eor, eor ^ ans};
}
}
9.力扣260. 只出现一次的数字 III
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int[] singleNumber(int[] nums) {
int ans = 0;
for (int i : nums) {
ans ^= i;
}
// ans = a ^ b
int rightOne = ans & (~ans + 1);
int eor = 0;
for (int i : nums) {
if ((i & rightOne) != 0) {
eor ^= i;
}
}
// eor = a
return new int[] {eor, eor ^ ans};
}
}
10.LCR 004. 只出现一次的数字 II
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int singleNumber(int[] nums) {
return getKtimesBetweenM(nums, 1, 3);
}
public int getKtimesBetweenM(int[] arr, int k, int M) {
int[] t = new int[32];
for (int i : arr) {
for (int j = 0;j <= 31;j++) {
t[j] += ((i>>j) & 1);
}
}
int ans = 0;
for (int i=0;i <= 31;i++) {
if (t[i] % M != 0) {
ans |= (1 << i);
}
}
return ans;
}
}
11.牛客类似题目
https://www.nowcoder.com/practice/c04bd25f0396471b90dfc30d96b9109b?fromPut=????pc????_???????_2008263481696810321082
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int singleNumber (int[] nums) {
int ans = 0;
for (int i : nums) {
ans ^= i;
}
return ans;
}
}
https://www.nowcoder.com/practice/389fc1c3d3be4479a154f63f495abff8?tpId=13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型一维数组
*/
public int[] FindNumsAppearOnce (int[] nums) {
int ans = 0;
for (int i : nums) {
ans ^= i;
}
// ans = a ^ b
int rightOne = ans & (~ans + 1);
int eor = 0;
for (int i : nums) {
if ((i & rightOne) != 0) {
eor ^= i;
}
}
// eor = a
int b = eor ^ ans;
if (b < eor) return new int[] {b, eor};
return new int[]{eor, b};
}
}
https://www.nowcoder.com/practice/85cb47fc0c6c483fab7e5cefab54d9e5?tpId=365&tqId=2313031&ru=/exam/oj&qru=/ta/spring-deliver-2024/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D365
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int singleNumber (int[] nums) {
return getKFromM(nums, 1, 3);
}
public int getKFromM(int[] arr, int k, int m) {
int[] t = new int[32];
for (int n : arr) {
for (int i = 0;i <= 31;i++) {
t[i] += ((n >> i) & 1);
}
}
int ans = 0;
for (int i = 0;i <= 31;i++) {
if (t[i] % m != 0) {
ans |= (1 << i);
}
}
return ans;
}
}
https://www.nowcoder.com/practice/1097ca585245418ea2efd0e8b4d9eb7a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.*;
public class Solution {
/**
*
* @param A int整型一维数组
* @return int整型
*/
public int singleNumber (int[] A) {
return getKfromM(A, 1, 3);
}
public int getKfromM(int[] arr, int k, int M) {
int[] t = new int[32];
for (int n : arr) {
for (int i = 0;i <= 31;i++) {
t[i] += ((n >> i) & 1);
}
}
int ans = 0;
for (int i = 0;i <= 31;i++) {
if (t[i] % M != 0) {
ans |= (1 << i);
}
}
return ans;
}
}