【问题描述】 [面试题56 - I] [数组中数字出现的次数]
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。示例 1:输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
【解答思路】
1.位运算
【1,4,4,6】
-
根据异或的性质,相同异或结果为 0,相异的异或结果为 1,所以将数组中的数字做异或运算,得到两个只出现一次数字的异或结果。
1 ^ 4 ^ 4 ^ 6 = 7
-
由于两数字不同,异或结果不为 0,则其二进制中必然有一位是不相同的。
方法一: 7 & i = 0 (出现0 证明可以作为分组依据 )
方法二:7 & -7 = 0001
-
可以先找到不相同的位,就可以将数组分成两组,再像之前那样做异或运算。
class Solution {// 自己寻找最右边 1 的位置public int[] singleNumbers(int[] nums) {if (nums == null || nums.length < 2) return new int[0];int xor = 0; // 数组第一次异或的结果,即两个只出现一次的数的异或结果for (int num : nums) xor ^= num;// 寻找右边第一个 1,也就是最右边不相同的位int i = 1;while ((i & xor) == 0){i <<= 1;}int[] res = new int[2];for (int num : nums) {if ((i & num) == 0) res[0] ^= num;else res[1] ^= num;}return res;}// 位运算 xor & (-xor)public int[] singleNumbers(int[] nums) {if (nums == null || nums.length < 2) return new int[0];int xor = 0; // 表示两个只出现一次的数字的异或结果for (int num : nums) xor ^= num;xor &= -xor;int[] res = new int[2];for (int num : nums) {if ((xor & num) == 0) res[0] ^= num;else res[1] ^= num;}return res;}
}
2. HashSet
遍历数组,遇到的数如果 HashSet 中存在,就把这个数删除。如果不存在,就把它加入到 HashSet 中。最后 HashSet 中剩下的两个数就是我们要找的了。
时间复杂度:O(N) 空间复杂度:O(N)
public int[] singleNumber(int[] nums) {HashSet<Integer> set = new HashSet<>();for (int n : nums) {if (set.contains(n)) {set.remove(n);} else {set.add(n);}}int[] result = new int[2];int i = 0;for (int n : set) {result[i] = n;i++;}return result;
}
【总结】
1. 位运算
异或运算(^)
运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
5 ^ 1 = 0101 ^ 0001 = 0100 = 4
5 ^ 3 = 0101 ^ 0011 = 0110 = 6
用法
- 翻转指定位 对应位异或1
X=10101110,使X低4位翻转,用X ^0000 1111 = 1010 0001即可得到。 - 两个数是否相等 ==0
5 ^ 5 = 0101 ^ 0101 = 0000 = 0
与运算(&)
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0
5 & 1 = 0101 & 0001 = 0001 = 1
5 & 2 = 0101 & 0010 = 0000 = 0
用法
取指定位 对应位与1
设X=10101110,
取X的低4位,用 X & 0000 1111 = 00001110 即可得到;
或运算(|)
运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;
即 :参加运算的两个对象只要有一个为1,其值为1
用法:
指定位置置1 对应位或1
将X=10100000的低4位置1 ,用X | 0000 1111 = 1010 1111即可得到
取反运算(~)
运算规则:~1=0; ~0=1;
即:对一个二进制数按位取反,即将0变1,1变0
带符号左移运算(<<)
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2(右边补0)
3 << 1 = 0011 <<1 = 0110 = 6
带符号右移运算(>>)
正数操作数每右移一位,相当于该数除以2
(左补0 or 补1得看被移数是正还是负)
5 >> 1 = 0101 >> 1 = 0010 = 2
5 >> 2 = 0101 >> 2 = 0001 = 1
-14 >> 2 = 11110010 >> 2 = 11111100 = -4
无符号右移运算(>>>)
5 >>> 1 = 0101 >>> 1 = 0010 = 2
-14 >>>2 =11111111 11111111 1111111111110010 >>>2 = 00111111 11111111 1111111111111100 = 1073741820
移位总结
- 正数的左移与右移,负数的无符号右移,就是相应的补码移位所得,在高位补0即可。
- 负数的右移,就是补码高位补1,然后按位取反加1即可。
2. 位运算 判相等换位异或^ 取位与&1 置位或|1
3. HashMap 或 HashSet常见用法
3.1 HashSet
(1)增加
public boolean add(E e);
(2)删除
public boolean remove(Object j);
(3)对比查找
public boolean contains(Object j);
(4)清空集合
public void clear();
(5)获取长度
public int size();
3.2 HashMap
(1) 插入键值对数据
public V put(K key, V value)
(2)根据键值获取键值对值数据
public V get(Object key)
(3)获取Map中键值对的个数
public int size()
(4)判断Map集合中是否包含键为key的键值对
public boolean containsKey(Object key)
(5)判断Map集合中是否包含值为value的键值对
boolean containsValue(Object value)
(6)判断Map集合中是否没有任何键值对
public boolean isEmpty()
(7)清空Map集合中所有的键值对
public void clear()
(8)根据键值删除Map中键值对
public V remove(Object key)