Single Number II

本文详细解析了LeetCode中的单数III问题,提供了一种直观的统计每位出现次数的方法,并介绍了使用位操作实现线性时间和常数空间复杂度的高效算法。

https://oj.leetcode.com/problems/single-number-ii/

Given an array of integers, every element appears three times except for one. Find that single one.

Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

public int singleNumber(int[] A)


这一题把重复两次,变成重复三次,难度瞬间就特别不一样了。HashMap依旧是一个可选项,但必然就不是without using extra memory了。而且,单纯的异或也不管用了。也没有什么简单的bit operation能够化解这个东西了。那么,下面,我要首先感谢code ganker。反正我是肯定想不出这种复杂的bit operation解决方案的。所以直接先给代码,然后我再努力解释一下:

    public int singleNumber(int[] A) {
        int ones = 0,twos = 0, xthrees = 0;
        for(int i : A){
            twos |= (i & ones);
            ones ^= i;
            xthrees = ~(ones & twos);
            ones &= xthrees;
            twos &= xthrees;
        }
        return ones;
    }

好,我们看到了三个变量,ones, twos, xthrees。里面保存着什么呢?正如其名。

ones保存的是只出现过一次的bit,twos保存的是只出现过两次的bit,xthrees实际上是一个filter,出现过三次的bit是0,反之则是1.

twos |= (i & ones); 这一句话的含义在于将当前数字的二进制bit和ones进行与操作,这样留下的,就是出现过两次的bit。

ones ^= i: ones里面保存的是只出现过一次的bit,所以如果在这里i也出现了,自然是要剔除的。

xthrees = ~(ones & twos); 事实上上面两步并不能保证twos和ones里不存在出现过三次的bit。举个例子,如果某一位的bit,在twos里是1,在i里也是1,但是ones里是0。那么在经历过第一次和第二次的操作的时候,ones和twos里那一位都会是1。会造成这种情况实际是因为这个bit已经出现过两次,所以在第一次出现的时候,ones里是1,twos里是0,在第二次出现的时候twos里是1,ones里是0。在第三次出现的时候就会是这样。于是乎xthrees就会承担filter的作用清除twos和ones里的这一bit。下面两句就是进行filter的。

所以最后还留在ones里的便是出现过一次的bit。根据题意,数字有出现过三次的和唯一一个出现过一次的。所以某一位bit出现的次数k可能只有两种:k % 3 == 0, 或者 k % 3 == 1。事实上ones里保留的并不是真的只出现过一次的bit,而是k % 3 == 1的bit。但就这样,就足够还原出原来的数字了。

2018-01-20

我觉得真的非常有必要重新回看一下这一题,因为刚看的时候只记得大概是 与或异或的操作却完全不记得具体如何。所以我们再一步步的寻找这一题的答案吧。接下来的所有解答都参考了leetcode的discuss板块,里面的人真的很牛逼。

我们先给出一种非常容易理解的解答。首先要明白因为不是偶数,所以异或的做法不能用了。这里需要统计的是一个整型数32位,每一位出现的次数模除3可以等于1。所以一个非常直观的算法出现了,就是循环数组所有数字32次,记录第一位到第三十二位出现的次数。这样做依旧是符合o(n)的要求的,因为o(32n)依旧是o(n)

给出代码如下

    public int singleNumber(int[] nums) {
        int result = 0;
        int count;
        for (int i = 0; i < 32; i++) {
            count = 0;
            for (int num : nums) {
                if ((num & (1 << i)) != 0) {
                    count++;
                }
            }
            if (count % 3 == 1) {
                result |= 1 << i;
            }
        }
        
        return result;
    }
这种做法的好处在于非常直观,容易理解,而且很好延伸出譬如数组中其他数字出现了任意奇数次的情况(因为偶数次都可以用异或法解决)。但是毕竟常数量是32,performance是没有那么好的。但也能够通过leetcode的检查。

当然,为了达到同样的目的,我们完全不需要32位一个个bit去进行统计。我们可以通过异或运算和与运算一次过达到目的。先给出一段代码如下:

    public int singleNumber(int[] nums) {
        int ones = 0, twos = 0;
        for (int i : nums) {
            ones ^= i & ~twos;
            twos ^= i & ~ones;
        }
        
        return ones;
    }
是不是很神奇,是不是很懵逼?这段代码比我四年前写的代码简单多了,却是同样的效果。

先让我们复习一下异或的效果:1^0 = 1, 0 ^ 1 = 1, 0 ^ 0 = 0, 1 ^ 1 = 0。也就是如果当前位两者一样为0,不一样则为1。所以当我们做ones ^= i & ~twos的时候,表示的是,如果i这个数字用二进制表示时,出现的1而ones没有的(同时twos,也就是出现过两次的也不能有。所以我们做了~twos),就赋予ones,但是如果ones在那些位置上出现了,则表示这些bits出现了不止一次了,需要清零。twos的原理也是一样的,它的二进制表达是出现过两次(模除3)的bits。i ^ ~ones的作用是取得出现在i但没有出现在ones的数字,然后twos和其进行异或,也就是在i为1且没有出现在ones的bit中,出现过在twos的bit清零,没有的话就变成1。这里隐含了一件事情。在ones中,bit为0的情况有两种:一是从来没有出现在之前的数字里,二是出现过又被清理掉。然而如果当我们做twos ^= i & ~ones时,如果ones没有但是i有,其实就是这些在ones中为0的数字就是被当前这个i被清理掉了。所以我们可以确信这个数字出现了两次(模除3)。

简单点说:

一个特定位置上的bit,如果在遍历中出现过1次,那么它会出现在ones里但不会出现在twos里,因为在ones ^= i & ~twos中,ones那个bit是0,i中那个bit是1,~twos中那个bit是1。所以就是 0 ^ (1 & 1) = 1。而在接下来的twos ^= i & ~ones中,twos那个bit是0,i那个bit是1,~ones中那个bit是0(因为one中那个bit刚变成1)。所以就是0 ^ (1 & 0) = 0。

如果出现过两次,那么它会出现在twos里但不会出现在ones里,因为此时的ones ^= i & ~twos是 1 ^ (1 & 1) = 0,而twos ^= i & ~ones就变成了 0 ^ (1 & 1) = 1(推理过程就不重复了)。

当第三次出现的时候,那么ones和twos里面那个bit都会是0。 因为此时的ones ^= i & ~twos是 0 ^ (1 & 0) = 0,而twos ^= i & ~ones就是 1 ^ (1 & 1) = 0。

此时,一个循环就完成了,那么出现第四次,第五次,第六次的时候,就会重复上述第一次,第二次,第三次的流程,长此往复。因为抽掉那个特殊数字,所有bit出现的次数必然为3的倍数,再加入那个特殊的数字,那个特殊数字的二进制的bits就会出现3n + 1次。也就是还剩下在ones里的bits就是那个特殊数字了。其实这里还隐含着一件事情,一个bit,不会同时在ones和twos里存在,因为不会有一个数字模除3同时为1和2,也符合算法特征

我一直在思考怎么简单的去描述这个算法

ones ^= i & ~twos (ones对i说,你有,我没有而且twos没有的给我,但如果我有了,你就吃掉)

twos ^= i & ~ones   (twos对i说,你有,我没有而且ones刚才有的被你吃掉的就给我,但如果我有了,你就吃掉)

这个做法的优点在于真的简洁,思维很聪慧很屌炸天。缺点在于难以理解和扩展(主要是难以理解)。但是也不是不能扩展的,https://discuss.leetcode.com/topic/2031/challenge-me-thx/17 给出了一个可以通过类似思路扩展到如果所有别的数字都出现了5次的情况。然而你们玩得开心,我下次回来再好好斟酌。。。囧rz...

下载前必看:https://pan.quark.cn/s/a4b39357ea24 在本资料中,将阐述如何运用JavaScript达成单击下拉列表框选定选项后即时转向对应页面的功能。 此种技术适用于网页布局中用户需迅速选取并转向不同页面的情形,诸如网站导航栏或内容目录等场景。 达成此功能,能够显著改善用户交互体验,精简用户的操作流程。 我们须熟悉HTML里的`<select>`组件,该组件用于构建一个选择列表。 用户可从中选定一项,并可引发一个事件来响应用户的这一选择动作。 在本次实例中,我们借助`onchange`事件监听器来实现当用户在下拉列表框中选定某个选项时,页面能自动转向该选项关联的链接地址。 JavaScript里的`window.location`属性旨在获取或设定浏览器当前载入页面的网址,通过变更该属性的值,能够实现页面的转向。 在本次实例的实现方案里,运用了`eval()`函数来动态执行字符串表达式,这在现代的JavaScript开发实践中通常不被推荐使用,因为它可能诱发安全问题及难以排错的错误。 然而,为了本例的简化展示,我们暂时搁置这一问题,因为在更复杂的实际应用中,可选用其他方法,例如ES6中的模板字符串或其他函数来安全地构建和执行字符串。 具体到本例的代码实现,`MM_jumpMenu`函数负责处理转向逻辑。 它接收三个参数:`targ`、`selObj`和`restore`。 其中`targ`代表要转向的页面,`selObj`是触发事件的下拉列表框对象,`restore`是标志位,用以指示是否需在转向后将下拉列表框的选项恢复至默认的提示项。 函数的实现通过获取`selObj`中当前选定的`selectedIndex`对应的`value`属性值,并将其赋予`...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值