找出那个出现一次的数

本人算法没什么天赋,但时常想拿个小题想想,倒没什么功利(例如找工作)目的,充实一下时间。

曾经出现频次很高的一个问题,说一个整数序列中有除了一个数字出现一次,其余都出现了两次。现在让你扫一遍数组O(n)找出那个数。方法是将所有的数作异或操作,结果就是你要找的。
leetcode 137 对这个问题升级了一下,现在问你,其余的数都出现三次呢,该如何找出那个出现一次的数。我感觉对我来说还是挺难的,解决方法很巧,在下佩服。理一下思路。
先回忆一下为什么异或可以解决第一个问题,其实本质上是考虑了每个整数序列中的每个二进制位1出现的次数,要么出现偶数次,要么出现奇数次。并且如果那个要寻找的数在该位置上有数字的话,该位1一定出现奇数次,即最终异或结果该位值为1.
对于出现三次就这个方法不灵了啊,别着急,再想想,可不可以实现这样一个规律,该二进制位上的数如果出现三的倍数,该位置零。那么是不是每一位上都得有个计数器啊,不不不,这个空间复杂度有点高了。容我们再想想。
哦,对了! 三个的话只需要一个2bits的比特位就够了啊。那么其实两个整数就够了。即我们只需要有一个2bits 的循环计数器,按照00->01->10->00 的方式计数,就知道该位1的格式是不是3的倍数了!! 声明两个整数分别代表计数器的两个bit位: int ones = 0 , twos =0, 并设数组序列是a 。要分开看这两个比特位,找下规律。
0->0->1->0 twos
0->1->0->0 ones
观察规律发现
twos前一位是1的时候 将ones的当前位清0.
ones当前位是1的时候 将twos的当前位清0.
其余情况按照位加法递推。
由此可以得到以下递推式,意义是当扫描到a[i]这个数的时候,计数器的变化公式。
ones = (ones ^a[i])&~twos
twos = (twos^a[i])&~ones
完整代码示例:
public int singleNumber(int[] A) {
int ones = 0, twos = 0;
for(int i = 0; i < a.length; i++){
ones = (ones ^ a[i]) & ~twos;
twos = (twos ^ a[i]) & ~ones;
}
return ones;
}
给我感觉很神奇的,但是---------还可以再升级一下,除了一个数出现1次,其余的都出现五次,怎么弄。
按照相同的逻辑,我们要构造一个循环计数器按照 000->001->010->011->100->000 的方式循环。我们再来拆开看看。

0->1->0->1->0->0 ones
0->0->1->1->0->0 twos
0->0->0->0->1->0 threes

观察发现
当ones前一位为0的时候,twos值不改变
当threes前一位为0的时候,ones的当前位置0
当且仅当ones和twos当前位都为0的时候,按照正常位加法置位。
其余情况按照正常加法置位。
由此可以写出以下递推式

twos = twos^(a[i]&ones)
ones= ones^(a[i]&~threes)
threes=threes^(a[i]&ones&twos)

完整代码示例:
public int singleNumber(int[] A) {
int ones = 0, twos = 0,threes =0;
for(int i = 0; i < a.length; i++){
twos = twos^(a[i]&ones);
ones = ones^(a[i]&~threes);
threes= threes(a[i]ones^twos);
}
return ones;
}
我能吐槽以下这个递推式并不好归纳吗。真的,如果有较完整的方法的话就好了,少费点我这老年人的脑细胞了。
其实如果允许我们存储临时变量的话,本质上递推式就是一个逻辑函数。怎么讲呢。 也就是说,前面的几个比特位和a[i]在该位上的值共同决定了下一位的取值。我们完全可以列出一个真值表。对于三个比特位的例子列出如下,其中ones twos threes 表示前一位,ones' twos' threes' 表示当前位。

有了真值表如何得到逻辑函数呢,我觉得大家可能都还给老师了,我想到了,是数字逻辑课上学的卡诺图,卡诺图可以对逻辑函数化简的。理论上可以解决此类通用问题。
其实本科学的有些东西还是很有用的。

posted @ 2018-04-12 16:44  bzt007  阅读(229)  评论(0编辑  收藏  举报