力扣剑指offer之数组中的孤单数II

在一个数组 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.不影响加和结果。理解了这些,在看代码就好懂了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值