算法位运算——只出现一次的数字系列

1.只出现一次的数字I

先看题目

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

方法一:map集合
看完题干,脑子第一反应就是用map来解决,数组中的元素作为key,出现的次数作为value。于是两三下就敲好代码,果然困难题我唯唯诺诺,简单题我重拳出击。

 public int singleNumber(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        int ans = 0;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if (entry.getValue() == 1) {
                ans = entry.getKey();
            }
        }
        return ans;
    }

方法二:位运算
法一的代码提交,发现耗时15ms,只击败了12%的人。我一看这个结果,心想难道还有更加巧妙的方法?于是看了下提示,说是采用位运算来解决。仔细分析题干,只有一个元素出现一次,其余出现两次。那么有没有一种位运算,可以使得相同的两个数进行运算以后的值是固定的。答案当然是^(异或)运算。

进行异或运算时,会先转成二进制,然后每一位上在进行异或,相同为0不同为1
例子:
2^3
010
011
——
001

aba^b
000
011
101
110
public int singleNumber(int[] nums) {
        int ans = 0;
        for (int num : nums) {
            ans ^= num;
        }
        return ans;
    }

2.只出现一次的数字II

先看题目

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

大家都是体面人,map的方法就不在这里展示了,直接思考怎么用位运算搞定。仔细思考发现,我们可以用逻辑电路的方法来解决。在这里插入图片描述
刚开始没有出现过是0,出现了第一次,状态变为1,再来一个状态又变为2,最后一次出现,状态置为0。这个状态我们分别用二进制00,01,10来表示,因为计算机一位只能表示0或1,所以我们需要两位来表示。画出它的真值表:(one* 和two* 表示下一个状态)

nonetwoone*two*
00000
00101
01010
10001
10110
11000

根据真值表写出逻辑关系式:
two* = ~n & ~one & two + n & ~one & ~two
化解一下
two* = two^num&~one
同理
one* = ~n & one & ~two + n & one & two

真值表化逻辑表达式:

方法一:

  1. 根据真值表当中结果是true的项,找出它的输入项,然后进行与运算;如果遇到输入项的取值是false,就需要把它写成非的形式,反之,如果输入项是true,那么就不用管,写成原来的样子就行了。
  2. 然后,遍历整一个真值表,把所有输出值为true的项进行或运算(相加),就可以得到最原始的逻辑关系式。

方法二:

  1. 根据真值表当中结果为false的项,找出输入项,然后进行或运算;如果遇到输入项的取值是true,就需要把它写成非的形式,反之,如果输入项是false,那么就不用管,写成原来的样子就行了。
  2. 然后,遍历整一个真值表,把所有输出值为false的项进行与运算(相乘),就可以得到最原始的逻辑关系式。
public int singleNumber(int[] nums) {
        int one = 0, two = 0;
        for (int num : nums) {
            int newone = (~num & one & ~two) | (num & ~one & two);
            int newtwo = two ^ num & ~one;
            one = newone;
            two = newtwo;
        }
        return two;//因为01代表出现一次,所以返回two
    }

上面代码还能简化一下,首先two的表达式不变,将two* 的值赋给two,重新画一份真值表。

nonetwoone*
0000
0010
0101
1010
1001
1100

得出one* = ~n & one & ~two + n & ~one & ~two
化解one* = one ^ num & ~two

public int singleNumber(int[] nums) {
        int one = 0, two = 0;
        for (int num : nums) {
            one = one ^ num & ~two;//第一次进来one和two是0,经过位运算one变成num 第二次one变成0,第三次one又变成num,注意这里的num指的是同一个数例如3;
            two = two ^ num & ~one;//1.two还是0 ,2.two是num,3.two是0

        }
        return one;//one保存了1次和3次,所以返回one
    }

用到的主要位运算是: a^a = 0, ~0 = -1, a&-1=a

果然大学里面学的数字电路还是有用,后悔当初没有认真学😭!

2.只出现一次的数字III

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

看到这题,首先想到的就是用异或(异或大法好啊),但是本题最关键的是怎么将两个只出现一次的数分开。核心就是这行代码 diff = ans & (-ans),是什么意思呢?这行代码是计算出转化为二进制后最右边出现的第一个1。是不是很神奇,举个例子:ans为6,那么化成二进制就是110,最右边的1在第二位,所以diff的值就为2。
假设a和b分别表示两个只出现过一次的数。那么这个最右边的1不是来自a就是来自b,那么我们可以将原数组分为两个部分,一个是这个位置上是1的数,化为一组,另一组是这个位置上不是1而是0的数。这样a与b必定不会在同一组,然后在分别进行异或。

public int[] singleNumber(int[] nums) {
        int ans = 0;
        for (int num : nums) {
            ans ^= num;//最后的结果就是a^b(a和b分别代表只出现一次的数)
        }
        int diff = ans & (-ans);//算出右边出现的第一个1,这个1不是来自a就是来自b
        int x = 0;
        for (int num : nums) {
            if ((num & diff) != 0) {
                x ^= num;//分组异或
            }
        }
        return new int[]{x, ans ^ x};
    }

珍惜所有的不期而遇,看淡所有的不辞而别!拜拜!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值