算法图解——找出整形数组里出现一次的两个数

最近参加了huawei的一个比赛,初赛刚结束,结果未知。虽然过程艰辛,经常搞到夜里1点,但是学到的知识还是挺多的。在学校没有参加很多的比赛也是一种遗憾,不得不说在学校自己的时间是真的多啊。感慨一番,继续造题。加油!

 

题目:

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6] 输出:[1,6] 或 [6,1] 示例 2:

输入:nums = [1,2,10,4,1,4,3,3] 输出:[2,10] 或 [10,2]

限制:2 <= nums.length <= 10000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解析

思路1:

利用HashMap,优缺点:查找效率高,但是有额外空间。

步骤:

1:遍历数组,存放到map中,map中的key是数组中的值,value是该值出现的次数,在这里学到了一个新方法就是hashmap的getOrDefault();

2:遍历数组,从map中查找对应数值的次数,如果是1则取出,放置返回数组res中。

不知听懂否?没听懂不要紧,“乔哥”(微信公众号:程序员乔戈里)给出了图示:

 

 

 

 

 

 

 

 

 

 

 

 以上就是遍历数组,将值和值对应出现的次数放在了map中。接下来,就是遍历数组,取出次数为1的数值了。

由于节省篇幅,想看详解的可点击文末链接(每一个步骤都有详细图解噢),再次致谢乔哥,小夕和皮皮。

在这里我只放两张典型的图(出现一次的,出现两次的)。

 

 

OK,话不多说。看具体实现:

代码实现

这个就不放注释了哈:

class Solution {
    public int[] singleNumbers(int[] nums) {
        Map<Integer, Integer> map = new HashMap();
        for(int num: nums){
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        int k= 0;
        int[] res= new int[2];
        for(int i = 0; i < nums.length; i++){
            if(map.get(nums[i]) == 1){
                res[k++] = nums[i];
            }
        }
        return res;
    }
}

上面我们也说了,这样虽然能解题,但是我们显然是不满足(条件的),哈哈哈。那么还有其他思路嘛?

拔高优化

 刷过剑指offer的童鞋看到这道题应该会稍微熟悉,因为在那本书中,有一个类似的题,说它类似是因为,它的题中是要求出数组中出现一次的单个数值,和这道题不一样的就是这里人家有两个次数为1的值。

但那道题可以利用异或的思路求解,何为异或?就是"^"它了,没错。

相同数字异或为0,不同数字异或为1。那道题的解法是:遍历数组,异或全部元素,最后剩下的就是出现一次的数值,因为出现两次的都异或为0了。

咦?那么我们是不是可以这样想?既然这道题的数组中有两个出现次数为1的数值,那么,如果我们能够把这俩数值分到不同的数组中(也就是一分为二),然后我们在利用该思路,分别对这两个数组进行遍历异或操作,不就可以得到这两个数值了吗?

是滴,没错。所以关键就是如何分组。

思路是这样的:假设遍历完数组后,得到的是a^b,那么我们可以知道a^b这个值中,二进制中,位数为1的位置代表了a和b这两个值的二进制值在该位置的不同(0和1),因为只有该位不同a^b在该位才会为1(根据异或规则)。

那好,我们就根据这个,将a和b分到不同的两个数组中,即:在该位为0的分到一个数组,在该位为1的分到另一个数组。

然后,遍历每个数组,异或数组中所有元素即可。

好了,文字描述完了,还不懂的话请看图解(感谢小夕提供的图解):

 

 

 

 

 

 

 

 

好的,中间省略....

 

结果分别异或两组数组,可得到 5 和 10 。

详细思路

再次总结步骤:

  • 对 nums 进行异或,由于相同数字异或为 0,所以上述结果最终的异或结果是 5 异或 10
  • 5 异或 10   5 的二进制 0101   10 二进制  1010   异或结果   1111
  • 接下来我们需要找到1111的第一个二进制1出现的位置,原因:异或结果为1说明a和b在这一位上不同,那用只有这一位为1的数字m去分别相与a和b,得到的结果一定不同,也就把a和b分到了不同的子数组。结合上一点得出结果。
  • 异或结果:1111 我们只需要找到第一个不为1的地方。例如:0001 0010 0100 1000 都可以
  • 使用 0001 来与数组中的每个数字相同的这一位进行相与操作
  • 遍历数组依次与0001进行异或
  • 结果不为0分组异或组res1:[1,5,1,3,3]
  • 结果为0分组异或组res2:[10,4,4]
  • 分别对res1 和 res2 进行异或 res1结果为5res2结果为10返回即可

 代码实现

class Solution {
    public int[] singleNumbers(int[] nums) {
        //用于将所有的数异或起来
        int k = 0;
        
        for(int num: nums) {
            k ^= num;
        }
        
        //获得k中最低位的1
        int mask = 1;
        while((k & mask) == 0) {//进行与操作
            mask <<= 1;
        }
        
        int a = 0;//省去了两个数组的定义,直接在后序遍历中就进行异或操作
        int b = 0;
        
        for(int num: nums) {
            if((num & mask) == 0) {
                a ^= num;
            } else {
                b ^= num;
            }
        }
        
        return new int[]{a, b};
    }
}

 

 

【参考及致谢】

1、小夕深夜联系两名知名博主开始搞事情,一起怒刷一道阿里高频面试题,击败100%的用户!

 

 

Over.......

 

posted @ 2021-04-07 00:27  额是无名小卒儿  阅读(129)  评论(0编辑  收藏  举报