位操作的妙用,出现K次的数字问题
2022-11-23 22:39:39 # 算法 # 位运算

1. 问题描述

给定一个数组arr,其中只有一个数字出现了K次,剩余的数字都出现M次,且$1\le K<M$,找出这个出现K次的数。

2. 暴力做法

使用哈希表统计每一个数出现的次数,最后遍历哈希表,返回出现K次的数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.HashMap;

public class AppearKTimesNumber {

public static int findKTimesNum(int[] arr, int k, int m) {
int len = arr.length, i = 0;
HashMap<Integer, Integer> countMap = new HashMap<>();
for(; i < len; ++i) {
Integer cnt = countMap.get(arr[i]);
if(cnt == null) cnt = 0;
++cnt;
countMap.put(arr[i], cnt);
}
for(Integer number : countMap.keySet())
if(countMap.get(number) == k) return number;
return -1;
}
}

时间复杂度$O(N)$,空间复杂度$O(N)$。

3. 最优解

int类型的底层是32位2进制数,我们统计所有数的二进制位上1的个数,我们假设答案ans上某一位是1,那么这个1要么出现$K$次,要么出现$K+xM(x \ge 0)$次,如果ans上的某一位是0,那么这一位上1的个数,只可能是$M$的倍数。因此我们可以根据某一位上1出现的次数能否被$M$整除,就可以知道答案ans这一位是0还是1,如下图所示:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AppearKTimesNumber2 {

public static int findKTimesNum(int[] arr, int k, int m) {
int len = arr.length, i = 0, j, ans = 0;
int[] bitCnt = new int[32];
for(; i < len; ++i)
for(j = 0; j < 32; ++j) // 遍历数组每一个数字的每一位,记录每一位上1出现的总次数
if((arr[i] & (1 << j)) != 0) ++bitCnt[j];
for(j = 0; j < 32; ++j)
if(bitCnt[j] % m != 0) ans |= 1 << j;// 如果这一位上1的出现次数和m取模不是0,那么ans这一位一定是1
return ans;
}
}

时间复杂度$O(N)$,空间复杂度$O(1)$。

4. 测试程序

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
import java.util.Random;
import java.util.Arrays;
import java.util.HashSet;

public class AppearKTimesNumberTest {

public static void main(String[] args) {
int times = 100_0000, maxM = 10;
while(times-- > 0) {
Random r = new Random();
int m = r.nextInt(maxM) + 2;
int k = r.nextInt(m - 1) + 1;
int mTime = r.nextInt(10) + 1;
int size = m * mTime + k;
int[] arr1 = new int[size], arr2 = new int[size], arr3 = new int[size];
HashSet<Integer> set = new HashSet<>();
int j = 0;
for(int i = 0; i < mTime; ++i) {
int num = r.nextInt();
while(set.contains(num)) num = r.nextInt();
for(int x = 0; x < m; ++x) {
arr1[j] = arr2[j] = arr3[j] = num;
++j;
}
}
int num = r.nextInt();
while(set.contains(num)) num = r.nextInt();
for(int x = 0; x < k; ++x) {
arr1[j] = arr2[j] = arr3[j] = num;
++j;
}
int ans1 = AppearKTimesNumber.findKTimesNum(arr1, k, m);
int ans2 = AppearKTimesNumber2.findKTimesNum(arr2, k, m);
if(ans1 != ans2) {
System.out.println("Oops!, wrong answer, ans1 = " + ans1 + ", ans2 = " + ans2 + ", k = " + k + ", m = " + m);
System.out.println("Origin array: " + Arrays.toString(arr3));
return;
}
System.out.println("Testcase: " + times + " done!");
}
System.out.println("Test done successfully!");
}
}

5. 升级问题

给定一个数组arr,其中只有一个数字出现了N次,剩余的数字都出现M次,且$1\le N<M$,如果$N=K$,那么返回这个出现K次的数,否则返回-1。

这个题目只是出现次数不一定是$K$次了,做法和上面的最优解大致相同,只是有一个小细节,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AppearKTimesNumber3 {

public static int findKTimesNum(int[] arr, int k, int m) {
int len = arr.length, i = 0, j, ans = 0;
int[] bitCnt = new int[32];
for(; i < len; ++i)
for(j = 0; j < 32; ++j)
if((arr[i] & (1 << j)) != 0) ++bitCnt[j];
for(j = 0; j < 32; ++j) {
int mod = bitCnt[j] % m;
if(mod == k) ans |= 1 << j;
else if(mod != 0) return -1;
}
if(ans == 0) {// 细节,如果ans是0的话,那么并不能保证arr中有0,就算有0,那么也不能保证0刚好出现K次,因此必须再扫一遍,查看0出现的次数
int cnt = 0;
for(int n: arr)
if(n == 0) ++cnt;
if(cnt != k) return -1;
}
return ans;
}
}

时间空间复杂度和上面的最优解一样

6. 升级问题测试程序

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
import java.util.Random;
import java.util.Arrays;
import java.util.HashSet;

public class AppearKTimesNumberTest2 {

public static void main(String[] args) {
int times = 100_0000, maxM = 10;
while(times-- > 0) {
Random r = new Random();
int m = r.nextInt(maxM) + 2;
int k = r.nextInt(m - 1) + 1;
int mTime = r.nextInt(10) + 1;
int realK = r.nextInt(2) == 0 ? k + 1 : k;
int size = m * mTime + realK;
int[] arr1 = new int[size], arr2 = new int[size], arr3 = new int[size];
HashSet<Integer> set = new HashSet<>();
int j = 0;
for(int i = 0; i < mTime; ++i) {
int num = r.nextInt();
while(set.contains(num)) num = r.nextInt();
for(int x = 0; x < m; ++x) {
arr1[j] = arr2[j] = arr3[j] = num;
++j;
}
}
int num = r.nextInt();
while(set.contains(num)) num = r.nextInt();
for(int x = 0; x < realK; ++x) {
arr1[j] = arr2[j] = arr3[j] = num;
++j;
}
int ans1 = AppearKTimesNumber.findKTimesNum(arr1, k, m);
int ans2 = AppearKTimesNumber3.findKTimesNum(arr2, k, m);
if(ans1 != ans2) {
System.out.println("Oops!, wrong answer, ans1 = " + ans1 + ", ans2 = " + ans2 + ", k = " + k + ", m = " + m + ", realK: " + realK);
System.out.println("Origin array: " + Arrays.toString(arr3));
return;
}
System.out.println("Testcase: " + times + " done!");
}
System.out.println("Test done successfully!");
}
}