在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
示例 1:
输入:nums = [3,4,3,3]
输出:4
示例 2:
输入:nums = [9,1,7,9,7,9,7]
输出:1
解析:本题是上一篇文章题目的延申,在之前的文章中,我们聊到了在一个数组中有一个孤单数如果去找,在一个数组中有两个孤单数如何去找等题目。今天我们要聊的是在一个数组中,有一个孤单数,但是其它数不再是两两成对,而是三三成对了。让你找这个孤单数。
很明显,直接异或的招数不好使了,那怎么办呢?看见这个题目,最容易想到的思路就是HashMap了吧:
1)遍历数组,将每个数都放入map集合统计
2)遍历map,找到次数为1的数值即可。
public int singleNumber(int[] nums) {
//统计各个数字出现的次数,键为数字,值为出现的次数
Map<Integer,Integer> map =new HashMap<Integer,Integer>();
for(int i:nums){
if(!map.containsKey(i)){
map.put(i,1);
continue;
}
map.put(i,map.get(i)+1);
}
//遍历map中的键值对,查看值出现次数为1的键,即为答案
int result = 0;
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
if(entry.getValue()==1){
result = entry.getKey();
break;
}
}
return result;
}
这个思路的确简单,并且也能被大多数人接受。但是有一个弊端就是费时,该代码运行需要13ms的时间,所以不是我们首选方案。
那该怎么求呢?在数组内,3个3个是一样的,只有一个是孤单的,那我们尝试排个序?这样的话,一样的不就排到一起了吗??三三成组了,我们就可以把三个元素看作一个元素,来进行判断了啊,,,这个思路也许可行,我们可以试着往下想具体怎么做:
1)先将数组排序
2)数组三三成组,但是我们现在需要知道第一个成组的起始位置是哪里?我们只有知道了这个,才能从此位置开始,三个三个一组开始判断。所以判断,如果nums[0] == nums[1],那就说明从0开始成组(0,1,2)。这是因为只有一个孤单数,所以如果前两个数相等,那么他们肯定不是孤单数,如果前两个不等,那直接返回nums[0]即可。
此处可能会有疑问为什么不返回nums[1]: 因为只有一个孤单数,这个孤单数只可能在0位置,如果在1位置,那0位置不也成孤单数了吗??
3)开始循环,i从0开始,每次循环i增加3。循环条件怎么设置呢?
循环条件:i < (nums.length / 3) * 3;
上面循环条件的设定,是一个很常用的取整操作,我们要循环到小于该数组长度的最后一个三的倍数位。循环内部判断条件是nums[i] == nums[i+1]。如果相等,说明孤单数不在这里面,继续往下循环,如果不等,说明nums[i]就是循环数,此处同样道理不可能是nums[i+1],因为如果nums[i+1]是孤单数,那nums[i]不就也是单独一个的了??
4)经过上面操作,我们已经找了长度为nums.length-1的数组值了。这是因为nums只有一个孤单数,其它都是三三成对,所以每次走三格,循环结束后,剩余没找的元素只有最后一个。因此我们进行判断:如果在前面的循环中,没有找到孤单数,那就说明最后一个是孤单数,返回最后一个数。如果找到了,那就返回你找到的数。至于如何判断前面找没找到,你可以直接设置一个boolean标志位,判断标志位即可。
public int singleNumber(int[] nums) {
// 一定要想到异或,因为我们已经了解到找一个孤单数就是直接异或得到。找两个孤单数就是
// 先利用异或与操作把两个孤单数分开,在分别进行异或即可得到
Arrays.sort(nums);
if(nums[0] != nums[1]){
return nums[0];
}
int result = 0;
boolean flag = false;
for(int i = 0; i < nums.length / 3; i = i + 1){
if(nums[3*i] == nums[3*i+1]){
continue;
}else{
flag = true;
result = nums[3*i];
break;
}
}
if(flag == true){
return result;
}else{
int a = nums.length / 3;
a = a * 3;
if(nums.length % 3 == 1){
result = nums[a];
}
return result;
}
上面已经有两个方法可以帮助你解决本题了,但是这好像和前面说的位运算不沾边啊??那我们就继续来看,位运算如何算本题。我们知道,数组中数都是三三出现的。那也就是说每个二进制位上数字出现都是3的整数倍,除非孤单数上也出现该位。那么我们设定一个容量为32的数组,计算一下每个位1出现的个数,如果是3的整数倍,就说明孤单数在该位上为0,否则就说明孤单数在该位上为1.
public int singleNumber(int[] nums) {
int[] helper = new int[32];
for (int n : nums) {
for (int j = 31; j >= 0; j--) {
// 求每个二进制位的加和
helper[j] += n & 1;
n = n >> 1;
}
}
int result = 0;
for (int i = 31; i >= 0; i--) {
// 如果某二进制位对3取余数不为0,则说明孤单数在该二进制位必为1. 就需要将该位算上了。
if (helper[i] % 3 != 0) {
result += Math.pow(2, 31 - i);
}
}
return result;
}
上面这个也好理解,你把数组内数都转化成32位二进制,你写下来就会发现:如果先不看孤单数,那么将这些数二进制累加起来,每一个二进制位加和的结果都是3的整数倍。所以只有在孤单数的二进制位是1的时候,其加和才不是3的整数倍。孤单数二进制位为0.不影响加和结果。理解了这些,在看代码就好懂了。