数组中只出现一次的两个数字

文章讲述了如何使用二进制和异或运算的巧妙方法解决编程问题,即在一个整型数组中找出仅出现一次的两个数字,同时保持空间复杂度为O(1)和时间复杂度为O(n)。作者首先介绍了常规的哈希表方法,然后详细阐述了基于异或运算的分组策略,通过不断扩展掩码来找到唯一不同的位进行分组。
摘要由CSDN通过智能技术生成

数组中只出现一次的两个数字

背景

刷到此题的时候,只写出了最普通的解法,最后看了二进制解法,叹为观止,不禁感叹到它的巧妙,因此记录一下,共勉。

题目描述

牛客地址:

https://www.nowcoder.com/practice/389fc1c3d3be4479a154f63f495abff8

描述
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

数据范围:
数组长度 2≤n≤1000,
数组中每个数的大小 0<val≤1000000

要求:
空间复杂度 O(1),
时间复杂度 O(n)

提示:输出时按非降序排列。

示例1
输入:[1,4,1,6]

返回值:[4,6]
说明:
返回的结果中较小的数排在前面

示例2
输入:[1,2,3,3,2,9]

返回值:[1,9]

题解

方法一:
题目给的意思分析之后,很容易想到一种方法,就是用哈希表进行统计,辅助得到这两个只出现一次的数字。
此方法比较简单,直接给出代码:

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型一维数组
     */
    public int[] FindNumsAppearOnce (int[] nums) {
        Map<Integer,Integer> numMap = new HashMap();
        for(int i=0;i<nums.length;i++){
            Integer num = numMap.get(nums[i]);
            if(null == num){
                numMap.put(nums[i],1);
            }else{
                numMap.put(nums[i],2);
            }
        }
        ArrayList<Integer> res = new ArrayList<Integer>();
        Set<Integer> numKeySet = numMap.keySet();
        for(Integer numKey:numKeySet){
            if(numMap.get(numKey) == 1){
                    res.add(numKey);
            }
        }
        Collections.sort(res);
        int[] intRes = new int[res.size()];
        for(int i=0;i<res.size();i++){
            intRes[i] = res.get(i);
        }
        return intRes;
    }
}

本文主要对第二种解法的思路做一下整理和分析。

对于这道题目,我们先来想另外一个问题:

如果数组中只有一个出现了一次的数字,我们想到得到它,那么应该如何解决呢?

我们都知道异或运算:如果两个数一样则异或结果为0,不一样则异或结果为1。(二进制)

(0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0)

举个例子:

4 ⊕ 4 = 0,将4化为二进制为 0100

所以 0100

异或 0100

得到 0000

4 ⊕ 4 ⊕ 5 = 5


​ 0100

​ 0100

​ 0101

得到 0101

我们可以看到上面的运算过程,因为4=4,两者相等异或结果为0。所以0异或任意数都等于任意数。

所以,当只有一个出现了一次的数字的时候,则只需要将全部数进行异或运算,运算结果就剩下了那个只出现一次的数字了。

public int[] singleNumber(int[] nums) {
    int x = 0;
    for(int num : nums)  // 1. 遍历 nums 执行异或运算
        x ^= num;
    return x;            // 2. 返回出现一次的数字 x
}

好了,上面说了这么多,那这道题目是找两个只出现一次的数字呀~

上面的方法又是针对只出现一次的数字,假设我也一样全部执行异或运算 1⊕4⊕1⊕6,最后也还是会剩下4⊕6呀~

我们看看:

0100 ⊕ 0110 = 0010 这个结果也不能得出什么东西哇~

我们换个角度思考,能不能做个分组,将题目分为两组 ,然后每一组求出其中的出现一次的数字,最后两者一起返回,不就解决问题了吗?

那么我们要如何分组呢?位运算进行分组,我们首先想到的应该是奇偶分组,就是将所有数 &1,此时能将数字分为奇偶两组。

但是这个时候问题又来了,你又不能保证两个数字就一奇一偶,有可能都是奇数也有可能都是偶数呀~

但是,我们想一下,&1的操作,归根到底,是按照二进制最低位的不同来分组的,
例如 : 0011(3) ,0101(5),0100(4),0001(1)
对上面四个数分组,我们都&1,可以分得结果: 0011,0101,0001(奇数) 0100 (偶数)
我们很明显能够知道,当二进制&1结果为1的时候,为奇数,反之为偶数。它们是按照最低位的不同来分组的。

上面我们知道,能够将数字分为 奇偶两组,那么现在,我再给出一个难度,如何区分出 0011,0101 ?
对 0011,0101 这两个数进行分组,我们可以观察到最低位都为1,此时如果我们还是进行&1操作去分组,那肯定是分不出来的!
因为两数的最低为都是一样的,&1之后还是1,还是无法区分,那么我们看到最低的第二位0011是1,0101是0,很明显这两位就不一样,那么我们就可以将这两数&0010呀,不就能够区分出来了吗?
0011 &0010 = 0010 0101&0010 = 0000,此时还是根据结果是否为0得到分组!
那要是是 0100 和 1100呢?如何分组呢? 不就是&1000 就能够分组了吗?

所以,说了那么多,其实就是为了推出一个分组的方式,两个不同的数如何分组!
我们都知道两个不同的数,那么它的二进制表示肯定是不一样的!这是毋庸置疑的!

所以,我们要想对两者进行分组操作,就是需要找到两者中的那一位不同的二进制,然后得到分组的与值(去&的那个值),问题不就解决了吗?

那要怎么找到那一位不同的二进制呢?
我们看一个例子: 1,1,4,6
全部做异或运算结果为 4⊕6 = 0100⊕0110 = 0010
异或的运算规则是什么? 相同的为0,不同的为1。所以我们根据两者异或出来的结果 0010,不就可以知道那一位不同了嘛?(为1的那一位就是不同的)
好了,说了这么多,下面安排代码把~

    public int[] FindNumsAppearOnce (int[] nums) {
        //定义一个异或的值
        int xorRes = 0;
        //求出所有数字的异或值
        for(int num:nums){
         xorRes^=num;   
        }
        //定义分组掩码,0为1组,1为1组,正好2组
        int mask = 1;
        //寻找分组的掩码位,用来分组用,掩码只能有一位是1,否则后面
        //进行与运算可能无法分出来,因为多位的话与运算可能结果都不为0
        //只有一位为1掩码,才能确保其中一个数字结果一定为0
        while((mask&xorRes) == 0){
            mask = mask<<1;
        }
        int a=0,b=0;
        for(int num:nums){
            //如果与掩码做与运算等于0的分到第一组
            if((mask&num) == 0){
                a^=num;
            }
            //否则是第二组
            else{
                b^=num;
            }
        }
        if(a>b){
            return new int[]{b,a};
        }else{
            return new int[]{a,b};
        }
    }

复杂度分析:
时间复杂度:O(N)。数组的长度n,循环。
空间复杂度:O(1)。几个变量的空间。

备注:大部分文字转载自牛客精华题解,有兴趣的可以去看一下。

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值